实验 08:数码管动态扫描
🛒 开发板: EK301 GW5A FPGA 开发板 芯片: GW5A-LV25UG324C1/I0 (Arora V 系列)
一、实验目的
- 理解动态扫描原理——利用人眼视觉暂留,让 6 位数码管"看起来"同时显示不同数字。
- 掌握扫描时钟生成:从 50MHz 分频得到约 1kHz 的位选切换频率。
- 学习消影技术——在位选切换的间隙关闭段码,防止"鬼影"现象。
- 学会多路选择器实现——根据当前扫描位号,从 6 组待显示数据中选择对应位。
二、实验过程
2.1 硬件连接
| 信号 | 方向 | FPGA 引脚 | Bank | 说明 |
|---|---|---|---|---|
| CLK | 输入 | T9 | Bank 4 | 50MHz 有源晶振 |
| seg[0]~seg[7] | 输出 | P12/T12/P11/R11/N10/R10/G8/T11 | Bank 0/4 | 段码,经 74LVC2G17+510R |
| sel[0]~sel[5] | 输出 | U16/V16/V15/U15/N9/V9 | Bank 4/5 | 位选,SS8050 驱动 |
数码管型号:6 位共阳 3601AS。段码低电平有效(亮),位选高电平有效(选中)。
2.2 动态扫描原理
EK301 的 6 位数码管段码引脚是并联的(所有位的 A 段连在一起,B 段连在一起……),通过 6 个位选信号(SEL1~SEL6)分时选通。
为什么不能静态驱动 6 位? 因为段码并联——如果在段码总线上输出"1"的段码,同时选中所有 6 位,则所有位都会显示"1",无法实现不同位显示不同数字。
动态扫描解决方案:
时间轴: t0 t1 t2 t3 t4 t5 t0 ...
↓ ↓ ↓ ↓ ↓ ↓ ↓
SEL1: ON OFF OFF OFF OFF OFF ON
SEL2: OFF ON OFF OFF OFF OFF OFF
...
段码: "1" "2" "3" "4" "5" "6" "1"
- 每个时刻仅选中 1 位,在段码总线上输出该位对应的数字。
- 6 位轮流被选中,切换频率 > 50Hz(每位刷新率 > 50/6 ≈ 8Hz)。
- 人眼视觉暂留效应让人"看到"所有 6 位同时稳定显示。
2.3 扫描频率计算
扫描周期 = 50MHz / (2 × SCAN_MAX)
= 50,000,000 / (2 × 4166)
≈ 6000 Hz
每位刷新率 = 6000 / 6 ≈ 1000 Hz >> 50Hz(无闪烁阈值)
SCAN_MAX = 4166 - 1 = 4165。- 实际每位切换频率约 1kHz,远超无闪烁要求。
scan_tick是单周期脉冲,在计数到达最大值时产生。
💡 为什么
SCAN_MAX除了 2?因为scan_tick在计到 MAX 时置 1(1 个周期),然后计数器再耗 MAX+1 个周期回到 MAX。实际上每个完整周期是(MAX+1)×2个时钟周期。
2.4 模块设计
系统框图:
top.v (seg_dynamic)
├─ 扫描时钟生成 (scan_cnt + scan_tick)
├─ 位选切换 (digit_sel 0→5 循环)
├─ 位选输出 (sel: 独热码)
├─ 当前位数据选择 (MUX)
├─ seg_decoder (复用exp_05)
└─ 消影处理 (seg_clean)
模块名:seg_dynamic
端口列表:
| 端口 | 方向 | 位宽 | 说明 |
|---|---|---|---|
clk | input | 1 | 50MHz 系统时钟 |
seg | output | [7:0] | 段码 (0=亮, 1=灭) |
sel | output | [5:0] | 位选 (独热码, 1=选中) |
关键内部信号:
| 信号 | 位宽 | 说明 |
|---|---|---|
scan_cnt | [12:0] | 扫描分频计数器 (0~4165) |
scan_tick | 1 | 位选切换脉冲 (~1kHz) |
digit_sel | [2:0] | 当前位号 (0-5) |
disp[0:5] | [3:0] | 6 组待显示 BCD 数据 |
seg_clean | [7:0] | 消影后的段码输出 |
三、代码逐段讲解
3.1 扫描时钟生成
localparam SCAN_MAX = 4166 - 1;
always @(posedge clk) begin
if (scan_cnt == SCAN_MAX) begin
scan_cnt <= 0;
scan_tick <= 1;
end else begin
scan_cnt <= scan_cnt + 1;
scan_tick <= 0;
end
end
scan_cnt从 0 计到 4165,共 4166 个计数周期。- 每到最大值时产生 1 个周期的
scan_tick脉冲,然后归零。 - 这是经典的单周期脉冲分频方式,与实验 02 的
tick_1hz逻辑完全一致。
3.2 位选切换
always @(posedge clk) begin
if (scan_tick) begin
if (digit_sel == 5)
digit_sel <= 0;
else
digit_sel <= digit_sel + 1;
end
end
- 每次
scan_tick到来,digit_sel加 1,在 0~5 之间循环。 digit_sel决定了当前点亮哪一位数码管。
3.3 位选输出(独热码)
case (digit_sel)
3'd0: sel <= 6'b000001; // 仅 SEL1=1
3'd1: sel <= 6'b000010; // 仅 SEL2=1
// ...
3'd5: sel <= 6'b100000; // 仅 SEL6=1
endcase
- 独热码(one-hot):每个时刻有且仅有一位为 1。
sel=6'b000001表示选中第 1 位数码管(sel[0]=1→ SS8050 导通 → 公共阳极接 VCC)。
3.4 消影处理
always @(posedge clk) begin
if (scan_tick)
seg_clean <= 8'hFF; // 切换瞬间关段码
else
seg_clean <= seg_raw; // 正常输出段码
end
消影原理:
理想: 位1显示"1" → 位2显示"2"
SEL1=ON SEL1→OFF, SEL2=ON
段码="1" 段码="2"
无消影: 段码从"1"直接变为"2"
→ 在SEL2刚刚导通的一瞬间,位2先短暂看到"1",再更新为"2"
→ 产生"鬼影"
有消影: 切换间隙先关段码(全灭)
→ SEL1→OFF, 段码=FF(全灭), SEL2=ON, 段码="2"
→ 消除了短暂残留
scan_tick=1时,seg_clean置为8'hFF(共阳全灭)。scan_tick=0后,seg_clean恢复为译码器的正常输出seg_raw。
3.5 段码译码器例化
seg_decoder u_seg (
.digit(cur_digit),
.seg (seg_raw)
);
- 复用实验 05 的
seg_decoder.v,将 4-bit BCD 码转为 8-bit 共阳段码。 cur_digit来自disp[digit_sel],即当前扫描位的待显示数字。
四、实验现象
| 操作 | 预期结果 |
|---|---|
| 上电 | 6 位数码管同时稳定显示 "123456" |
| 数码管 1 | 显示数字 "1",段 B/C 亮 |
| 数码管 2 | 显示数字 "2",段 A/B/D/E/G 亮 |
| ... | ... |
| 数码管 6 | 显示数字 "6",段 A/C/D/E/F/G 亮 |
验证要点:
- ✅ 6 位数字同时可见,无闪烁(刷新率 > 50Hz)。
- ✅ 数字之间无"鬼影"(相邻位不会短暂显示对方的数字)。
- ✅ 亮度均匀,各位之间无明显亮度差。
- ✅ 用相机/手机拍摄时,画面可能出现"缺段"或"滚动条纹",这是因为相机快门与扫描频率不同步——这是正常现象,人眼看不到。
- ✅ 修改
disp[0:5]的值重新下载,可显示任意 6 位数字。
五、掌握知识点
| 知识点 | 说明 | 在本实验中的体现 |
|---|---|---|
| 动态扫描 | 分时选通多位显示,利用视觉暂留 | 6 位轮流点亮,~1kHz 切换 |
| 扫描频率 | 总刷新率 > 50Hz 无闪烁 | 每位 ~1kHz,远超阈值 |
| 消影 | 切换间隙关闭段码 | scan_tick 时 seg_clean=0xFF |
| 独热码 | 每时刻仅 1 位为 1 | sel 的 6 种位选状态 |
| 多路选择器 | 根据索引选择数据 | disp[digit_sel] |
| 单周期脉冲 | 仅持续 1 个 clk 的使能信号 | scan_tick |
附录:源码
top.v
// =============================================================================
// EK301 实验06: 数码管动态扫描
// 芯片: GW5A-LV25UG324C1/I0 | 时钟: 50MHz (T9)
// 功能: 6位共阳数码管动态扫描, 扫描频率≈1kHz, 显示"123456"
// 复用实验05的seg_decoder.v模块
// =============================================================================
module seg_dynamic (
input wire clk, // 50MHz (T9)
output wire [7:0] seg, // 段码 {DP,G,F,E,D,C,B,A} 0=亮
output reg [5:0] sel // 位选 [5:0], 1=选中
);
// ---- 扫描参数 ----
// 扫描频率 = 50MHz / (2 * SCAN_MAX) ≈ 1kHz (每位833Hz, 6位≈139Hz > 50Hz无闪烁)
localparam SCAN_MAX = 4166 - 1; // 50M / (6 * 2 * 1k) ≈ 4167
reg [12:0] scan_cnt; // 扫描时分频计数器
reg [2:0] digit_sel; // 当前选中的数码管位 (0-5)
reg scan_tick;
// ---- 要显示的6位数字 (修改此处切换显示内容) ----
wire [3:0] disp [0:5];
assign disp[0] = 4'd1; // 第1位显示"1"
assign disp[1] = 4'd2; // 第2位显示"2"
assign disp[2] = 4'd3; // 第3位显示"3"
assign disp[3] = 4'd4; // 第4位显示"4"
assign disp[4] = 4'd5; // 第5位显示"5"
assign disp[5] = 4'd6; // 第6位显示"6"
// ---- 扫描时钟生成 ----
always @(posedge clk) begin
if (scan_cnt == SCAN_MAX) begin
scan_cnt <= 0;
scan_tick <= 1;
end else begin
scan_cnt <= scan_cnt + 1;
scan_tick <= 0;
end
end
// ---- 位选切换 (每scan_tick切换一位) ----
always @(posedge clk) begin
if (scan_tick) begin
if (digit_sel == 5)
digit_sel <= 0;
else
digit_sel <= digit_sel + 1;
end
end
// ---- 位选输出 (独热码) ----
always @(posedge clk) begin
case (digit_sel)
3'd0: sel <= 6'b000001; // SEL1
3'd1: sel <= 6'b000010; // SEL2
3'd2: sel <= 6'b000100; // SEL3
3'd3: sel <= 6'b001000; // SEL4
3'd4: sel <= 6'b010000; // SEL5
3'd5: sel <= 6'b100000; // SEL6
default: sel <= 6'b000000;
endcase
end
// ---- 当前位数据选择 ----
wire [3:0] cur_digit;
assign cur_digit = disp[digit_sel];
// ---- 段码输出 ----
// 消影: 位选切换前先关段码, 防止鬼影
reg [7:0] seg_clean;
always @(posedge clk) begin
if (scan_tick)
seg_clean <= 8'h00; // 切换位选间隙关段码(全1=共阳全灭)
else
seg_clean <= seg_raw; // 正常输出
end
// 例化段码译码器 (复用exp_05)
wire [7:0] seg_raw;
seg_decoder u_seg (
.digit(cur_digit),
.seg (seg_raw)
);
assign seg = seg_clean;
endmodule