3. 数码管动态扫描显示

数码管动态扫描显示 学习卡片

任务目标

设计一个基于FPGA的4位数码管动态扫描显示电路,实现数字0~9在4个数码管上的循环显示,并通过仿真验证扫描时序的正确性。动态扫描利用人眼视觉暂留效应,以较高频率轮流点亮各数码管,从而在4位数码管上同时呈现不同数字。

设计原理与实现方案

动态扫描的核心思想是:在任意时刻仅点亮一个数码管(位选有效),同时在该位上发送对应的段码(段选),并以足够高的频率(通常>1kHz)循环扫描所有位。本设计采用状态机+计数器结构:一个时钟分频计数器产生扫描时钟(约1kHz),驱动位选信号循环移位;同时每个位对应一个显示数字(0~9循环),通过组合逻辑将数字转换为7段码输出。

Verilog代码实现

module seg_dynamic_scan (
    input       clk,        // 系统时钟 50MHz
    input       rst_n,      // 异步复位,低有效
    output reg [3:0] seg_sel,   // 位选信号,低电平有效
    output reg [7:0] seg_data   // 段选信号,{dp,g,f,e,d,c,b,a},低电平点亮
);

// 分频计数器:50MHz -> 1kHz 扫描频率
reg [15:0] cnt;
reg scan_clk;
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        cnt <= 16'd0;
        scan_clk <= 1'b0;
    end else if (cnt == 16'd24999) begin
        cnt <= 16'd0;
        scan_clk <= ~scan_clk;
    end else begin
        cnt <= cnt + 1'b1;
    end
end

// 扫描计数器:0~3 循环,对应4个数码管
reg [1:0] scan_cnt;
always @(posedge scan_clk or negedge rst_n) begin
    if (!rst_n)
        scan_cnt <= 2'd0;
    else
        scan_cnt <= scan_cnt + 1'b1;
end

// 位选译码:低电平选中对应位
always @(*) begin
    case (scan_cnt)
        2'd0:    seg_sel = 4'b1110;  // 第1位数码管
        2'd1:    seg_sel = 4'b1101;  // 第2位数码管
        2'd2:    seg_sel = 4'b1011;  // 第3位数码管
        2'd3:    seg_sel = 4'b0111;  // 第4位数码管
        default: seg_sel = 4'b1111;
    endcase
end

// 数字生成:每个位显示不同的循环数字(0~9)
reg [3:0] num_display;
always @(*) begin
    case (scan_cnt)
        2'd0:    num_display = (scan_cnt + 1) % 10;  // 位0显示1
        2'd1:    num_display = (scan_cnt + 2) % 10;  // 位1显示2
        2'd2:    num_display = (scan_cnt + 3) % 10;  // 位2显示3
        2'd3:    num_display = (scan_cnt + 4) % 10;  // 位3显示4
        default: num_display = 4'd0;
    endcase
end

// 7段码译码(共阴数码管,段码低电平点亮)
always @(*) begin
    case (num_display)
        4'd0: seg_data = 8'b11000000; // 0
        4'd1: seg_data = 8'b11111001; // 1
        4'd2: seg_data = 8'b10100100; // 2
        4'd3: seg_data = 8'b10110000; // 3
        4'd4: seg_data = 8'b10011001; // 4
        4'd5: seg_data = 8'b10010010; // 5
        4'd6: seg_data = 8'b10000010; // 6
        4'd7: seg_data = 8'b11111000; // 7
        4'd8: seg_data = 8'b10000000; // 8
        4'd9: seg_data = 8'b10010000; // 9
        default: seg_data = 8'b11111111;
    endcase
end

endmodule

仿真验证

编写testbench验证扫描时序:检查位选信号seg_sel是否按1110→1101→1011→0111循环变化,且每个位选有效期间seg_data输出对应的段码。仿真时钟设为50MHz,观察至少4个扫描周期。

module tb_seg_dynamic_scan;
    reg clk = 0;
    reg rst_n = 0;
    wire [3:0] seg_sel;
    wire [7:0] seg_data;

    seg_dynamic_scan uut (
        .clk(clk),
        .rst_n(rst_n),
        .seg_sel(seg_sel),
        .seg_data(seg_data)
    );

    always #10 clk = ~clk;  // 50MHz

    initial begin
        #20 rst_n = 1;
        #500000;  // 仿真足够长时间观察多个扫描周期
        $finish;
    end

    // 监控位选变化
    always @(posedge clk) begin
        if (rst_n && seg_sel !== 4'b1111)
            $display("Time=%0t seg_sel=%b seg_data=%h", $time, seg_sel, seg_data);
    end
endmodule

运行与验证方法

  1. 仿真工具:Vivado/ModelSim/Quartus均可,将上述两个文件加入工程。
  2. 编译仿真:运行testbench,观察波形。
  3. 通过条件:
    • seg_sel按1110→1101→1011→0111循环变化,每个状态持续约1ms(扫描时钟周期);
    • 当seg_sel=1110时,seg_data对应数字1的段码0xF9;当seg_sel=1101时,对应数字2的段码0xA4;依此类推,位0~3分别显示1、2、3、4;
    • 扫描周期稳定,无毛刺或位选重叠。

常见问题

  • 显示亮度不均:扫描频率过低(<500Hz)会明显闪烁,建议保持1kHz以上;频率过高(>10kHz)则可能因驱动能力不足导致亮度下降。
  • 段码错误:确认数码管是共阴还是共阳,本设计按共阴低电平点亮编写,若为共阳需取反段码。
  • 位选信号极性:本设计位选低电平有效,若硬件为高电平有效,需将seg_sel赋值取反。