这一篇在干嘛?

数码管是 FPGA 开发板上最直观的「显示器」,而驱动它的核心是一段七段译码器——把 4 位二进制数翻译成 7 个段的亮灭组合。这一篇从共阳数码管的结构和真值表讲起,用纯组合逻辑(case 语句)实现 0~F 的十六进制译码,并完成仿真与上板验证。这是组合逻辑设计的入门必修课。

实验目标与电路原理

实验任务:学习共阳数码管 7 段数码显示译码器的硬件设计。

数码管的结构

一个七段数码管由 7 个条形 LED(段)组成,习惯上记为 g、f、e、d、c、b、a(g 是中间横段,按从上到下、顺时针排列),如果想显示小数点还要加第 8 段 h。

「共阳」的意思是:7 个 LED 的阳极全部连在一起接电源正极,每个段的阴极由 FPGA 引脚单独控制。因此——

共阳数码管是「低电平点亮」

段信号输出 0(低电平)→ 该段导通发亮;输出 1(高电平)→ 该段熄灭。这一点和点亮普通 LED 的直觉相反,是七段译码最容易写反的地方。写真值表时先问自己一句:我的段亮是 0 还是 1?

为什么要用 FPGA 做译码

7 段数码显示本质上是纯组合电路(输出只由当前输入决定,没有时钟参与)。传统的小规模专用 IC,如 74 系列或 4000 系列器件,只能做十进制 BCD 码译码;然而数字系统中的数据处理和运算都是二进制的,输出表达通常是十六进制(0~F)。为了满足十六进制数的译码显示,最方便的方法就是利用 Verilog/VHDL 译码程序在 FPGA 中来实现。

共阳数码管示意图如下:


图 1.1 共阳数码管

从真值表到段码

设输入的 4 位码为 A[3:0],输出控制 7 段共阳数码管的 7 位数据为 LED7S[6:0]。输出信号 LED7S 的 7 位分别接数码管的 7 个段,高位在左、低位在右,即 LED7S[6] 对应 g 段、LED7S[0] 对应 a 段。

举例验证一下极性:当 LED7S 输出为 7'b1000_000 时,g 段为 1(灭),f、e、d、c、b、a 六段全为 0(亮),正好拼出一个「0」。按这个规则(亮 = 0),0~F 的完整真值表如下:

输入 A显示字符LED7S[6:0](g f e d c b a)
4’b000007’b1000_000
4’b000117’b1111_001
4’b001027’b0100_100
4’b001137’b0110_000
4’b010047’b0011_001
4’b010157’b0010_010
4’b011067’b0000_010
4’b011177’b1111_000
4’b100087’b0000_000
4’b100197’b0010_000
4’b1010A7’b0001_000
4’b1011b7’b0000_011
4’b1100C7’b1000_110
4’b1101d7’b0100_001
4’b1110E7’b0000_110
4’b1111F7’b0001_110

对照这张表自检几行:显示「8」要 7 段全亮,所以是 7'b0000_000(全 0);显示「1」只亮 b、c 两段,所以是 7'b1111_001。看懂这两行,整张表你就掌握了。

Verilog 实现

程序按照 case 语句的表述方法,根据上面的真值表直接写出:

module DECL7S (A,LED7S,B);
input [3:0] A; //四位二进制输入
output [6:0] LED7S; //动态数码管七段
output reg[7:0] B; //动态数码8位位选信号
reg [6:0] LED7S;
always @(A)
begin
B <= 8'b11111110; //置最右侧数码管点亮,左侧七个数码管灭
case(A)
4'b0000 :    LED7S <= 7'b1000_000; //七段译码:显示 0
4'b0001 :    LED7S <= 7'b1111_001; //显示 1
4'b0010 :    LED7S <= 7'b0100_100; //显示 2
4'b0011 :    LED7S <= 7'b0110_000; //显示 3
4'b0100 :    LED7S <= 7'b0011_001; //显示 4
4'b0101 :    LED7S <= 7'b0010_010; //显示 5
4'b0110 :    LED7S <= 7'b0000_010; //显示 6
4'b0111 :    LED7S <= 7'b1111_000; //显示 7
4'b1000 :    LED7S <= 7'b0000_000; //显示 8
4'b1001 :    LED7S <= 7'b0010_000; //显示 9
4'b1010 :    LED7S <= 7'b0001_000; //显示 A
4'b1011 :    LED7S <= 7'b0000_011; //显示 b
4'b1100 :    LED7S <= 7'b1000_110; //显示 C
4'b1101 :    LED7S <= 7'b0100_001; //显示 d
4'b1110 :    LED7S <= 7'b0000_110; //显示 E
4'b1111 :    LED7S <= 7'b0001_110; //显示 F
default :    LED7S <= 7'b1000_000;
endcase
end
endmodule

(说明:原文 PDF 经 OCR 后,case 中 6 之后的分支损毁严重,以上按原文可辨识的 0~5 分支与同系列动态扫描实验的段码表补全,段码值与开发板通用译码表一致。)

几个代码要点:

  1. 敏感列表是 @(A) 而不是 @(posedge clk)——没有时钟,这就是纯组合逻辑。输入 A 一变,LED7S 立即重新译码;
  2. B <= 8'b11111110 放在 always 里:位选信号 B 是 8 位数码管的位选(低电平选通),置 11111110 表示只点亮最右侧一个数码管,左侧七个灭。本实验只演示一位显示,所以位选固定不动;
  3. 如果要考虑小数点,需要增加段 h,然后把 LED7S 改为 8 位输出即可。

组合逻辑的 always 也可以用 assign 改写

这个译码器完全可以用 assign LED7S = ... + 函数/三目运算实现,但 16 个分支用 case 写最清晰易查表。记住组合逻辑两条铁律:敏感列表写全输入;case 记得补 default,否则综合器会推断出锁存器(latch),引入意想不到的毛刺。

实验仿真

根据描述语言进行仿真测试,分析输入 A 变化时 LED7S 的译码输出波形是否符合真值表:


图 1.2 VWF 仿真结果

检查波形的要点:每当 A[3:0] 从 0000 递增到 1111,LED7S[6:0] 应当依次跳变为真值表中的对应值。例如 A=0001 时 LED7S 应为 1111001;A=1000 时应为 0000000。用波形工具放大到每个输入跳变处逐一核对。

引脚约束

本实验的输入用 4 位拨码/按键开关 A[3:0],输出包括 7 段段选 LED7S[6:0] 和 8 位位选 B[7:0]:

set_pin_assignment { A[0] } { LOCATION = C8; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { A[1] } { LOCATION = C7; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { A[2] } { LOCATION = C6; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { A[3] } { LOCATION = C5; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { B[0] } { LOCATION = D11; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { B[1] } { LOCATION = E11; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { B[2] } { LOCATION = F10; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { B[3] } { LOCATION = C13; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { B[4] } { LOCATION = E10; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { B[5] } { LOCATION = E12; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { B[6] } { LOCATION = E16; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { B[7] } { LOCATION = F16; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LED7S[0] } { LOCATION = C15; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LED7S[1] } { LOCATION = C16; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LED7S[2] } { LOCATION = B6; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LED7S[3] } { LOCATION = A5; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LED7S[4] } { LOCATION = B5; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LED7S[5] } { LOCATION = A8; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LED7S[6] } { LOCATION = A7; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }

整理成表格便于核对:

信号FPGA 引脚方向说明
A[0]C8input输入第 0 位
A[1]C7input输入第 1 位
A[2]C6input输入第 2 位
A[3]C5input输入第 3 位
LED7S[0]~[6]C15, C16, B6, A5, B5, A8, A7output七段段选(a~g)
B[0]~[7]D11, E11, F10, C13, E10, E12, E16, F16output8 位位选

引脚号别跨实验照搬

本实验中复位/按键用到的 C5 在这里被分配给了输入位 A[3],位选引脚与后面静态/动态数码管实验一致(它们驱动同一组数码管硬件)。每个实验的约束表都以其原文为准,跨实验「复制粘贴」引脚前先确认硬件连接没有变化。

下载测试

按标准流程编译布线并下载后,用开关 SW1~SW4 分别输入 0000、0001、0010、0011……1111,观察数码管 1 的显示效果:

正常现象:数码管依次显示 0、1、2、3、4、5、6、7、8、9、A、b、C、d、E、F,共 16 个字符,且只亮最右侧一位(位选 B 固定为 11111110)。若某段不亮或常亮,先对照真值表检查对应段的极性是否写反,再核对 LED7S 各位与引脚的对应顺序(高位在左 g、低位在右 a)。

现象与总结

本实验的关键收获:

  1. 组合逻辑的最小范例always @(A) + case 查表,输入变输出立刻变,无时钟、无寄存器节拍;
  2. 真值表驱动的设计方法:先在纸上把 16 种输入对应的输出列全,再机械地翻译成 case 分支——数字逻辑设计里「先表后码」的标准工作流;
  3. 共阳极性:亮 = 低电平。段码写反的故障现象是「该亮的灭、该灭的亮」,按位取反即可修正;
  4. 位选与段选分离:段选决定「显示什么」,位选决定「哪一位显示」,这个二维度控制结构是下一篇动态扫描显示的理论基础。

自测