跳到主要内容

实验 08:数码管动态扫描

🛒 开发板: EK301 GW5A FPGA 开发板 芯片: GW5A-LV25UG324C1/I0 (Arora V 系列)


一、实验目的

  1. 理解动态扫描原理——利用人眼视觉暂留,让 6 位数码管"看起来"同时显示不同数字。
  2. 掌握扫描时钟生成:从 50MHz 分频得到约 1kHz 的位选切换频率。
  3. 学习消影技术——在位选切换的间隙关闭段码,防止"鬼影"现象。
  4. 学会多路选择器实现——根据当前扫描位号,从 6 组待显示数据中选择对应位。

二、实验过程

2.1 硬件连接

信号方向FPGA 引脚Bank说明
CLK输入T9Bank 450MHz 有源晶振
seg[0]~seg[7]输出P12/T12/P11/R11/N10/R10/G8/T11Bank 0/4段码,经 74LVC2G17+510R
sel[0]~sel[5]输出U16/V16/V15/U15/N9/V9Bank 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

端口列表

端口方向位宽说明
clkinput150MHz 系统时钟
segoutput[7:0]段码 (0=亮, 1=灭)
seloutput[5:0]位选 (独热码, 1=选中)

关键内部信号

信号位宽说明
scan_cnt[12:0]扫描分频计数器 (0~4165)
scan_tick1位选切换脉冲 (~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_tickseg_clean=0xFF
独热码每时刻仅 1 位为 1sel 的 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