这一篇在干嘛?

上一篇的静态显示有个致命局限:所有位只能显示同一个字符。这一篇用动态扫描破解它——8 位数码管其实每一瞬间只有一位在亮,只是切换速度快到人眼无法分辨,看起来就是 8 位同时显示各自的数字。你会学到扫描计数器、位选轮流通电、段码跟随切换这套经典的「轮播」结构。

实验目标与扫描原理

实验任务:

  1. 学习硬件扫描显示电路的设计;
  2. 掌握利用 FPGA 设计 8 位七段数码管扫描显示驱动电路的方法。

段码与位码

LED 数码管有段码位码之分:

  • 段码:让 LED 显示出某个字符(如「8.」)的那 8 位数据。一般要通过译码电路,将输入的 4 位二进制数转换为与 LED 显示对应的段码(上一篇的七段译码器干的就是这个活);
  • 位码:LED 的显示使能端。对于共阳极的 LED 而言,低电平使能——位码中某一位为 0,对应的那位数码管才接通显示。

为什么轮流点亮却能「同时」显示

要让 8 个数码管各自显示不同数据,做法是:不停地循环扫描每一个数码管,在使能每一个数码管的同时,输入它所需显示数据对应的 8 位段码

虽然 8 个数码管是依次显示的,但只要扫描频率足够高——大于 50Hz(即每位点亮的时间短于人眼视觉暂留的分辨极限)——受视觉分辨率的影响,看到的现象就是 8 个 LED 同时工作。这是利用人眼视觉暂留的经典设计,和电影每秒 24 帧看起来是连续画面是同一个道理。

板上电路回顾

HX4S20 板采用 8 位共阳数码管(数码管原理及驱动方式可参照实验 4,或查阅「使用手册」原理图)。8 位数码扫描显示电路中,每个数码管的 8 个段 h、g、f、e、d、c、b、a(h 是小数点)都分别连在一起,8 个数码管分别由 8 个选通信号 k1~k8 来选择。被选通的数码管显示数据,其余关闭。例如某一时刻 k3 为低电平、其余选通信号为高电平,这时仅 k3 对应的数码管显示来自段信号端的数据,其他 7 个数码管呈现关闭状态。

所以,要在 8 个数码管上显示想要的数据,就必须让 8 个选通信号 k1~k8 分别被单独选通,同时在段信号输入口加上该对应数码管要显示的数据——随着选通信号的扫变,就能实现扫描显示的目的。

8 位数码管实物如下图:


1.1 8 位数码实物图

扫描频率的两端约束

扫描太慢(低于 50Hz)会看到明显的闪烁;扫描太快则每位点亮时间过短、亮度下降。本实验用 16 位扫描计数器的最高 3 位 cnt_scan[15:13] 分频选位:50MHz 时钟下计数器每 8×65536 ≈ 0.5ms 换一位,8 位轮完一轮约 1ms 多,整轮刷新率近千 Hz,远高于 50Hz,显示稳定无闪烁。

Verilog 实现

实现的动态数码管显示模块如下,可在第 3 个 always 里修改需要显示的数字(原文 OCR 排版有破碎,已按原文信号定义、段码表与逻辑补全还原):

module sm_scan(
    clk,
    rst,
    dataout,
    en
);
input clk, rst;
output[6:0] dataout;
output[7:0] en; // COM 使能输出
reg[6:0] dataout; // 各段数据输出
reg[7:0] en;
reg[15:0] cnt_scan; // 扫描频率计数器
reg[3:0] dataout_buf;
always @(posedge clk or negedge rst)
begin
    if (!rst)
    begin // 低电平复位
    cnt_scan <= 0;
    end
    else
    begin
    cnt_scan <= cnt_scan + 1'b1;
    end
end
always @(cnt_scan) // 段码扫描频率
begin
    case(cnt_scan[15:13])
    3'b000 :    en = 8'b1111_1110;
    3'b001 :    en = 8'b1111_1101;
    3'b010 :    en = 8'b1111_1011;
    3'b011 :    en = 8'b1111_0111;
    3'b100 :    en = 8'b1110_1111;
    3'b101 :    en = 8'b1101_1111;
    3'b110 :    en = 8'b1011_1111;
    3'b111 :    en = 8'b0111_1111;
    default :    en = 8'b1111_1110;
    endcase
end
always@(en) //对应 COM 信号给出各段数据,段码
begin
    case(en)
    8'b1111_1110:    dataout_buf=1;//输入将要显示的数字
    8'b1111_1101:    dataout_buf=2;
    8'b1111_1011:    dataout_buf=3;
    8'b1111_0111:    dataout_buf=4;
    8'b1110_1111:    dataout_buf=5;
    8'b1101_1111:    dataout_buf=6;
    8'b1011_1111:    dataout_buf=7;
    8'b0111_1111:    dataout_buf=8;
    default:    dataout_buf=0;
    endcase
end
always@(dataout_buf)
begin
case(dataout_buf) //将要显示的数字译成段码
4'b0000 : dataout <= 7'b1000_000;//7'b0111_111;
4'b0001 : dataout <= 7'b1111_001;//7'b0000_110;
4'b0010 : dataout <= 7'b0100_100;//7'b1011_011;
4'b0011 : dataout <= 7'b0110_000;//7'b1001_111;
4'b0100 : dataout <= 7'b0011_001;//7'b1100_110;
4'b0101 : dataout <= 7'b0010_010;//7'b1101_101;
4'b0110 : dataout <= 7'b0000_010;//7'b1111_101;
4'b0111 : dataout <= 7'b1111_000;//7'b0000_111;
4'b1000 : dataout <= 7'b0000_000;//7'b1111_111;
4'b1001 : dataout <= 7'b0010_000;//7'b1101_111;
4'b1010 : dataout <= 7'b0001_000;//7'b1110_111;
4'b1011 : dataout <= 7'b0000_011;//7'b1111_100;
4'b1100 : dataout <= 7'b1000_110;//7'b0111_001;
4'b1101 : dataout <= 7'b0100_001;//7'b1011_110;
4'b1110 : dataout <= 7'b0000_110;//7'b1111_001;
4'b1111 : dataout <= 7'b0001_110;//7'b1110_001;
default : dataout <= 7'b1000_000;//7'b0111_111;
endcase
end
endmodule

这个模块是「三段接力」结构,数据流为:cnt_scanen(位选)→ dataout_buf(当前该显示什么数字)→ dataout(段码):

① 扫描计数器(时序逻辑)

cnt_scan 是 16 位自由计数器,每个时钟加 1。它的低 13 位实际上在做高速分频,而最高 3 位 cnt_scan[15:13] 每 2^13 = 8192 个时钟才进位一次——用它的值来轮流选位,相当于自带了合适的人工扫描节拍。

② 位选轮播(组合逻辑)

case(cnt_scan[15:13]) 把 3 位值翻译成 8 位位选 en1111_1110 选中第 0 位(最低位数码管),1111_1101 选中第 1 位……0111_1111 选中第 7 位。任一时刻只有一位为 0(低电平使能),8 个状态循环轮转——这就是「轮流点亮」的物理实现。

③ 位选→显示值映射(组合逻辑)

case(en) 决定当前被选通的那一位显示什么数字:选中第 0 位显示 1、第 1 位显示 2……第 7 位显示 8。改这个 always 里的数字,就能让每位显示你想要的内容——原文注释特意指出「可在第 3 个 always 里修改需要显示的数字」,实际指的就是这里的映射表。

④ 数字→段码译码(组合逻辑)

case(dataout_buf) 把 0~F 译成 7 位段码。段码表与七段译码器实验完全相同(共阳、亮 = 低电平、g 在最高位 a 在最低位)。每行注释里 // 后面还保留了一份极性相反的段码(如 7'b0111_111),那是共阴极数码管的版本——如果你的板子是共阴管,直接换成注释里的值即可,这是原文贴心埋好的对照表。

这里没有用时钟驱动的状态机

注意后三个 always 的敏感列表分别是 @(cnt_scan)@(en)@(dataout_buf)——全是组合逻辑。扫描的「时序感」完全来自 cnt_scan 这个唯一 的时序件。初学者常犯的错误是把位选也写成 always @(posedge clk) 的状态机,反而增加复杂度。记住这个分工:计数靠时序,翻译靠组合。

修改显示内容的三种玩法

① 把位选→数字映射改成每位显示同一个数字,可验证扫描通路是否正常;② 把映射改成 dataout_buf = ~en[0] 之类的算式要慎重——先在纸上演算每个 en 值对应的结果;③ 想显示多位不同数字(如倒计时 87654321),就在第 3 个 case 里逐位填好你想要的值,段码译码层完全不用动。

引脚约束

set_pin_assignment { clk } { LOCATION = R7; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { dataout[0] } { LOCATION = C15; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[1] } { LOCATION = C16; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[2] } { LOCATION = B6; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[3] } { LOCATION = A5; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[4] } { LOCATION = B5; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[5] } { LOCATION = A8; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[6] } { LOCATION = A7; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[0] } { LOCATION = D11; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[1] } { LOCATION = E11; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[2] } { LOCATION = F10; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[3] } { LOCATION = C13; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[4] } { LOCATION = E10; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[5] } { LOCATION = E12; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[6] } { LOCATION = E16; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[7] } { LOCATION = F16; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { rst } { LOCATION = A2; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }

整理成表格:

信号FPGA 引脚说明
clkR750MHz 系统时钟
rstA2复位按键,低电平复位
dataout[0]~[6]C15, C16, B6, A5, B5, A8, A7七段段选(a~g)
en[0]~[7]D11, E11, F10, C13, E10, E12, E16, F168 位位选(COM 使能)

段选、位选引脚与静态数码管实验完全一致——它们物理上接的就是同一组数码管。

下载测试

按标准流程编译布线并下载(注意 DIG/LED 拨动开关拨到数码管一侧):

正常现象:8 位数码管「同时」显示 1、2、3、4、5、6、7、8 各位不同——实际上是每约 0.5ms 只有一位在亮,扫描速度快于人眼分辨极限。用手机相机对准数码管拍摄,画面里可能会看到某一位比其他位亮,或者出现滚动条纹——相机快门「冻结」了扫描过程,恰好反向证明了动态扫描的存在。

现象与总结

本实验的关键收获:

  1. 动态扫描三板斧:扫描计数器(定节拍)→ 位选轮播(选通一位)→ 段码跟随(给对数据)。三者时序上严丝合缝,位选切换的同一时刻段码必须已经就位;
  2. 视觉暂留阈值:整轮扫描频率 > 50Hz 就看不到闪烁。这是所有「分时复用」硬件设计(数码管、点阵屏、VGA)的通用法则;
  3. 组合/时序的分工:本模块只有 cnt_scan 一个时序件,其余都是组合翻译层。结构越简单,时序越可控;
  4. 共阳/共阴对照:段码表每行注释里藏着共阴版本,换板子时按极性选用。

至此,数码管三部曲(七段译码 → 静态显示 → 动态扫描)完成。你已经具备了做时钟、计分牌、倒计时器等数码管应用的全套基础。

自测