14、数码管显示:静态显示与动态扫描、段选与位选控制、数字时钟显示实战
数码管这东西,看着简单,但坑不少。我刚开始用FPGA驱动数码管时,以为不就是亮几个灯嘛,结果一上电,显示乱跳,数字重影,折腾了一整天才搞定。今天咱们就把这块彻底讲透。
14.1 数码管的工作原理
先说说数码管内部是啥结构。说白了,它就是8个LED灯排成"8"字形,外加一个小数点。每个LED对应一个段,从a到g,再加上dp(小数点)。
数码管分两种:共阴极和共阳极。共阴极是所有LED的负极连在一起接地,正极分别控制。共阳极则相反,正极连在一起接VCC,负极分别控制。
我个人习惯用共阴极数码管,因为控制逻辑更直观——给高电平就亮,给低电平就灭。不过在实际项目中,两种都会遇到,你得学会灵活切换。
段选与位选的概念
段选:控制数码管显示什么数字(哪个段亮)
位选:控制哪一位数码管工作(选通哪一位)
举个例子:你要显示"1234",段选决定了每个位置显示1、2、3、4这些数字的段码,位选则决定当前是第1位、第2位、第3位还是第4位在亮。
14.2 静态显示 vs 动态扫描
静态显示,就是每个数码管独立控制,一直亮着。优点是显示稳定,不闪烁。缺点嘛,占用的IO口太多——4位数码管就要32个IO口,FPGA的引脚可经不起这么折腾。
动态扫描就不一样了。它利用人眼的视觉暂留效应,快速轮流点亮每一位数码管。只要刷新频率够快(一般50Hz以上),人眼看起来就是所有位同时亮的。
我在项目中遇到过一个问题:动态扫描的刷新频率太低,导致数码管明显闪烁。后来我把刷新频率提高到100Hz,问题就解决了。你想想看,如果刷新频率低于30Hz,人眼就能感觉到闪烁,那体验就很差了。
| 对比项 | 静态显示 | 动态扫描 |
|---|---|---|
| IO口占用 | 多(每位8个) | 少(段选8个+位选N个) |
| 显示稳定性 | 高,无闪烁 | 需保证刷新频率 |
| 功耗 | 较高 | 较低 |
| 适用场景 | 位数少(1-2位) | 位数多(4位以上) |
14.3 段码表的设计
段码表是数码管显示的核心。每个数字对应一个8位二进制数,从高位到低位分别是dp、g、f、e、d、c、b、a。
以共阴极数码管为例,显示数字"0"时,a、b、c、d、e、f亮,g和dp灭,对应的段码就是8'b11000000(注意这里高位在前)。
// 共阴极数码管段码表
// 位顺序:dp g f e d c b a
parameter SEG_0 = 8'b1100_0000; // 显示0
parameter SEG_1 = 8'b1111_1001; // 显示1
parameter SEG_2 = 8'b1010_0100; // 显示2
parameter SEG_3 = 8'b1011_0000; // 显示3
parameter SEG_4 = 8'b1001_1001; // 显示4
parameter SEG_5 = 8'b1001_0010; // 显示5
parameter SEG_6 = 8'b1000_0010; // 显示6
parameter SEG_7 = 8'b1111_1000; // 显示7
parameter SEG_8 = 8'b1000_0000; // 显示8
parameter SEG_9 = 8'b1001_0000; // 显示9
小技巧:如果你用的是共阳极数码管,段码取反就行。比如共阴极的SEG_0是8'b1100_0000,共阳极就是8'b0011_1111。我一般会在代码里加一个条件编译,方便切换。
14.4 动态扫描的实现
动态扫描的核心就是一个计数器,轮流选通每一位数码管。每次只让一位亮,同时输出对应的段码。
嗯,这里要注意:切换位选时,要先关闭所有位(消隐),再更新段码,最后打开新的位选。否则会出现"鬼影"——前一位的显示残留到后一位上。
// 动态扫描模块
module seg_dynamic_scan (
input clk, // 系统时钟
input rst_n, // 复位
input [15:0] data_in, // 要显示的4位BCD码
output reg [3:0] sel, // 位选信号
output reg [7:0] seg // 段选信号
);
// 扫描计数器,分频到1kHz左右
reg [15:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 16'd0;
else
cnt <= cnt + 1'b1;
end
// 位选切换
reg [1:0] scan_pos;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
scan_pos <= 2'd0;
else if (cnt == 16'd49999) // 50MHz时钟,1ms切换一次
scan_pos <= scan_pos + 1'b1;
end
// 段码输出和位选控制
always @(*) begin
case (scan_pos)
2'd0: begin
sel = 4'b1110; // 选通第1位
seg = seg_table(data_in[3:0]); // 显示个位
end
2'd1: begin
sel = 4'b1101; // 选通第2位
seg = seg_table(data_in[7:4]); // 显示十位
end
2'd2: begin
sel = 4'b1011; // 选通第3位
seg = seg_table(data_in[11:8]); // 显示百位
end
2'd3: begin
sel = 4'b0111; // 选通第4位
seg = seg_table(data_in[15:12]); // 显示千位
end
endcase
end
endmodule
避坑指南:我曾经在动态扫描时忘记加消隐处理,结果数码管显示重影严重,数字看起来像叠在一起。后来我在切换位选前先让所有位选无效(sel=4'b1111),再更新段码,最后才选通目标位,问题就解决了。
14.5 数字时钟显示实战
好了,理论讲完了,咱们来做个实战——用4位数码管显示数字时钟,格式为"HH:MM",中间的小数点闪烁表示秒。
这个项目分三部分:时钟计数模块、数码管驱动模块、顶层连接模块。
时钟计数模块负责计时,产生时和分。数码管驱动模块就是咱们刚才写的动态扫描。顶层模块把两者连起来。
// 数字时钟顶层模块
module digital_clock (
input clk, // 50MHz系统时钟
input rst_n, // 复位
output [3:0] sel, // 位选
output [7:0] seg // 段选
);
// 时钟计数
reg [31:0] clk_cnt;
reg [5:0] sec, min, hour;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk_cnt <= 32'd0;
sec <= 6'd0;
min <= 6'd0;
hour <= 6'd0;
end
else begin
if (clk_cnt == 32'd49_999_999) begin // 1秒到达
clk_cnt <= 32'd0;
if (sec == 6'd59) begin
sec <= 6'd0;
if (min == 6'd59) begin
min <= 6'd0;
if (hour == 6'd23)
hour <= 6'd0;
else
hour <= hour + 1'b1;
end
else
min <= min + 1'b1;
end
else
sec <= sec + 1'b1;
end
else
clk_cnt <= clk_cnt + 1'b1;
end
end
// 将时分转换为BCD码
wire [15:0] display_data;
assign display_data = {hour/10, hour%10, min/10, min%10};
// 小数点闪烁(每秒闪烁一次)
wire dp_ctrl;
assign dp_ctrl = (sec[0]) ? 1'b1 : 1'b0; // 秒的奇偶控制小数点
// 实例化数码管驱动
seg_dynamic_scan u_scan (
.clk (clk),
.rst_n (rst_n),
.data_in (display_data),
.sel (sel),
.seg (seg)
);
// 注意:这里seg的最高位(dp)需要根据dp_ctrl来控制
// 实际使用时,可以在seg_table中根据dp_ctrl调整小数点位的输出
endmodule
你看,整个数字时钟的核心代码其实没多少行。但实际调试时,我遇到过不少问题。比如时钟计数不准,原因是分频系数算错了——50MHz时钟,1秒需要计数50,000,000次,但计数器是从0开始的,所以应该是49,999,999。
实战要点总结
- 动态扫描刷新频率建议在60-120Hz之间
- 切换位选时必须先消隐,再更新段码,最后选通
- 段码表要仔细核对,一个bit错整个数字就错了
- 时钟分频要注意边界条件,计数器从0开始计数
说实话,数码管驱动是FPGA入门的基础,但也是检验基本功的好题目。你如果能独立完成这个数字时钟项目,那对时序控制、状态机、分频这些概念就会有更深的理解。
下一章咱们会讲按键消抖和输入控制,到时候就能给这个时钟加上调时功能了。嗯,先把这个消化掉,有问题随时交流。