FPGA安路赛项第六讲:led流水灯

第 06 讲:LED:流水灯实验 —— 参数化架构、使能节拍与环形移位寄存器


课前导读:从“单点闪烁”到“时序状态机”的跨越

在上一讲中,我们完成了所有 FPGA 工程师的开山之作——LED 闪烁实验。我们用一个 24 位计数器折叠了时间,让 4 颗灯在 0101 与 1010 之间两两交替跳变。

但如果我们要实现更炫酷的“流水灯(LED Chaser / Running LED)”——让 4 颗灯像流水或跑马灯一样,一颗接着一颗循环点亮,该怎么设计?

很多初学者会写出冗长笨拙的代码:

  • 开一个庞大的状态机,写十几个 case 分支;
  • 或者写一堆复杂的嵌套 if-else。

而在资深芯片架构师眼里,这正是传授三大现代数字系统核心设计范式的最佳时机:

  1. 参数化设计(Parameterization):如何写出能在不同开发板上“一秒换挡、随处移植”的高内聚代码;
  2. 全局同步时钟使能模式(Clock Enable / Tick 机制):为什么芯片设计中绝对严禁产生分频时钟去驱动后级?
  3. 环形移位寄存器(Ring Shift Register):如何利用位拼接算子 { },以零比较器开销实现一条吞噬自己尾巴的“数字贪吃蛇”。

第一章:工业级设计思想(一)—— 参数化配置的艺术

在工程实战中,绝对禁止在代码逻辑深处硬编码“魔法数字”(Magic Numbers)(例如在代码深处写死 10_000_000 或 50_000_000)。

1.1 为什么要使用 parameter?

假设你的模块写死了数到一千万($0.2\text{s}$)。

  • 场景 A:客户突然提出需求:“流水灯太慢了,请调快一倍(改为 0.1 秒)”;
  • 场景 B:你需要把这个模块从 50MHz 的安路开发板,移植到 100MHz 的 Xilinx 板子上;
  • 场景 C:在 ModelSim 做自动化仿真时,数一千万拍需要卡死 10 秒,你希望在仿真中改成数 10 拍瞬间跑完。

如果你在代码里到处硬编码数字,你需要全局搜索替换,极易漏改引发隐蔽 Bug。
工业级解法:在模块头部开设“全局参数旋钮”!

module led_chaser_board_top #(
    parameter integer TICK_MAX = 10_000_000, // 走一格需计数的时钟周期数
    parameter LED_ACTIVE_LOW = 1'b0          // 硬件极性:0为高有效,1为低有效
)(
    input  wire       sys_clk,
    input  wire       sys_rst_n,
    output wire [3:0] led
);
  • 语法要素拆解:
    • #( ... ):在第 03 讲我们讲过,紧跟在模块名后面的 #( ) 代表参数声明列表;
    • parameter integer TICK_MAX = 10_000_000;:声明一个 32 位整数常量,出厂默认预设为一千万;
    • 外部重载机制:后续在顶层模块调用或在 Testbench 仿真时,外界可以用一行代码强行把参数拧成任何新值(如 #( .TICK_MAX(10) )),底层的加法器位宽和计数终点会自动重新计算,核心逻辑一行都不用改动!

第二章:工业级设计思想(二)—— 严禁门控时钟,拥抱“使能脉冲(Tick)”

这是大学教科书与工业界实战之间最大的鸿沟之一!

2.1 致命的“学生写法”:分频时钟(Ripple Clock)

很多教科书教流水灯时,会让学生写出这样的代码:

// ❌ 工业界极度排斥的业余写法:
reg div_clk;
always @(posedge sys_clk) begin
    if (cnt == 9999999) div_clk <= ~div_clk; // 产生一个慢速时钟
end

// 灾难出现:拿内部寄存器产生的新信号去当时钟!
always @(posedge div_clk) begin
    led <= ...;
end

为什么这种写法在真实芯片里是“定时炸弹”?

  1. 没有专属全局时钟树保护:FPGA 硅片内部有斥巨资铺设的超低阻抗**“全局时钟树骨干网络(Global Clock Network)”**,确保时钟到达每个触发器的延时严格一致(无时钟偏斜 Clock Skew);
  2. 门电路延迟与毛刺(Glitch):用普通逻辑门生成的 div_clk 走的是普通的狭窄路由通道。随着温度变化和供电波动,这个“人造时钟”会产生几十皮秒的漂移甚至毛刺,导致时序分析全线崩溃,在上板时引发随机死机!

2.2 顶级工业标准:单主频 + 单周期使能脉冲(Tick Pattern)

铁律:整片芯片永远只共用一个纯洁、由主晶振直接驱动的全局高频时钟(sys_clk)!
我们不用慢时钟去驱动后级,而是让计数器在数满一千万时,瞬间射出一发仅持续 1 个时钟周期的“使能脉冲(Tick)”!

sys_clk (50MHz)  : ┌┐┌┐┌┐┌┐┌┐┌┐┌┐┌┐┌┐┌┐┌┐ ... ┌┐┌┐┌┐┌┐┌┐┌┐
                   └┘└┘└┘└┘└┘└┘└┘└┘└┘└┘└┘     └┘└┘└┘└┘└┘└┘
                                                │
tick 脉冲 (0.2s到) : ───────────────────────────└──┐┌─────── (仅高电平 20ns!)
                                                   │
后级触发器动作   : ────────────────────────────────┴─────── (在全局时钟沿下,根据 tick=1 移位一次)

后级所有的触发器依然运行在健壮的 50MHz 全局时钟下,只是平时被按住不准动,唯独看到 tick == 1 的那一微观瞬间,才允许往前移位一格! 这就是高可靠性数字系统的设计基石。


第三章:led_chaser_board_top.v 完整源码与手术级解剖

下面是流水灯模块的完整工业标准代码:

module led_chaser_board_top #(
    parameter integer TICK_MAX = 10_000_000,
    parameter LED_ACTIVE_LOW = 1'b0
)(
    input  wire       sys_clk,
    input  wire       sys_rst_n,
    output wire [3:0] led
);
    reg [31:0] tick_counter;
    reg [3:0]  led_logic;
    wire tick = (tick_counter == TICK_MAX - 1);

    always @(posedge sys_clk or negedge sys_rst_n) begin
        if (!sys_rst_n)
            tick_counter <= 32'd0;
        else if (tick)
            tick_counter <= 32'd0;
        else
            tick_counter <= tick_counter + 1'b1;
    end

    always @(posedge sys_clk or negedge sys_rst_n) begin
        if (!sys_rst_n)
            led_logic <= 4'b0001;
        else if (tick)
            led_logic <= {led_logic[2:0], led_logic[3]};
    end

    assign led = LED_ACTIVE_LOW ? ~led_logic : led_logic;
endmodule

我们分块对其进行微观解剖:


3.1 节拍发生器(进程一:时序测量)(第 11 ~ 21 行)

    reg [31:0] tick_counter;
    wire tick = (tick_counter == TICK_MAX - 1);

    always @(posedge sys_clk or negedge sys_rst_n) begin
        if (!sys_rst_n)
            tick_counter <= 32'd0;
        else if (tick)
            tick_counter <= 32'd0;
        else
            tick_counter <= tick_counter + 1'b1;
    end
  • reg [31:0] tick_counter;:
    • 开辟 32 位宽计数器,能轻松数到 42 亿次。在这里它负责精确倒数 1000 万次。
  • wire tick = (tick_counter == TICK_MAX - 1);:
    • 组合逻辑连续赋值。当计数器正好走到 9_999_999 时,物理比较器瞬间导通,tick 输出高电平 1。
  • 进程一的状态转移:
    • 按下复位按键(!sys_rst_n):秒表归零清空;
    • tick 为 1(一千万次计数跑满):秒表立即归零,准备重新倒数;
    • 平常时刻:每个时钟节拍默默累加 1。
    • 算算时间:$\frac{10,000,000}{50,000,000\text{Hz}} = \mathbf{0.2\text{ 秒}}$。每隔 0.2 秒,tick 就会发出一次 20ns 宽度的脉冲!

3.2 环形移位寄存器(进程二:状态迁移)(第 23 ~ 28 行)

    always @(posedge sys_clk or negedge sys_rst_n) begin
        if (!sys_rst_n)
            led_logic <= 4'b0001;
        else if (tick)
            led_logic <= {led_logic[2:0], led_logic[3]};
    end

这是全代码中最令人拍案叫绝的一笔!

① 为什么复位必须设为 4'b0001?

  • 许多同学写习惯了清零,随手写成 led_logic <= 4'b0000;。
  • 灾难结果:如果 4 位全为 0,无论你怎么移位,{000, 0} 永远等于 0000!流水灯将陷入全黑死锁状态,永远无法点亮!
  • 必须在复位时,在这 4 个触发器中亲手播种下一颗火种(置一个单点 1),后续的移位才能带着这个 1 循环跑动!

② 位拼接算子 {led_logic[2:0], led_logic[3]} 怎么实现流水?

我们在第 03 讲学过,大括号 { } 是把导线重新排布拼接。
我们把 4 个触发器排成一列,看它们内部的数据流转:

           ┌──────────────────────────────────────────────────────────┐
           │                                                          │ (最高位抽出来塞回最右边)
           ▼                                                          │
     [ 触发器 0 ] ◄────── [ 触发器 1 ] ◄────── [ 触发器 2 ] ◄────── [ 触发器 3 ]
       最低位                                                         最高位
    led_logic[0]          led_logic[1]          led_logic[2]          led_logic[3]
  • 拆解推演:
    • led_logic[2:0]:当前低 3 位的值,把它抽出来,放到新数值的高 3 位(相当于整体向左滑动一格!);
    • led_logic[3]:当前最高位的值,把它抽出来,拼在新数值的最低位(咬住尾巴,实现闭环回卷!)。

我们手把手跟踪每一次 0.2 秒的跳变:

触发时间 触发事件 移位前状态 led_logic 低三位 [2:0] 最高位 [3] 拼接运算 { [2:0], [3] } 移位后状态 led_logic 物理点亮灯珠
开机上电 异步复位 — — — — 4'b0001 LED0
$0.2\text{s}$ 第 1 次 Tick 4'b0001 001 0 {001, 0} 4'b0010 LED1
$0.4\text{s}$ 第 2 次 Tick 4'b0010 010 0 {010, 0} 4'b0100 LED2
$0.6\text{s}$ 第 3 次 Tick 4'b0010 100 0 {100, 0} 4'b1000 LED3
$0.8\text{s}$ 第 4 次 Tick
(见证回卷!)
4'b1000 000 1 {000, 1} 4'b0001 LED0(重回起点!)
  • 零比较器、零加法器! 仅仅利用 4 个 D 触发器之间的硬连线交叉,就实现了完美的无缝四拍跑马灯!

第四章:硬件极性适配与三目运算(跨板级兼容)

很多初学者的代码只在一块板子上能跑,换一块板子灯的亮灭全是反的。本代码展示了工业级 IP 核的通用封装手法:

    assign led = LED_ACTIVE_LOW ? ~led_logic : led_logic;
  • 三目运算符硬件推导:
    在第 03 讲我们讲过,? : 在硅片中会被综合为一个 2选1多路数据选择器(4位宽 MUX):
                   +-------------+
  led_logic ─────► | 0选通端     |
                   |      4位MUX | ───► output wire [3:0] led
 ~led_logic ─────► | 1选通端     |
                   +-------------+
                          ▲
                          │ 选通控制线 (由参数静态决定)
                   LED_ACTIVE_LOW
  1. 共阴极开发板(高电平点亮,LED_ACTIVE_LOW = 1'b0):
    • MUX 选通 0 端,直接把内部算好的 led_logic(0001、0010)原汁原味送到引脚上;
  2. 共阳极开发板(低电平点亮,LED_ACTIVE_LOW = 1'b1):
    • MUX 选通 1 端,把信号全盘取反(~led_logic)后输出:
      • 0001 变成 1110(第 0 位是 0,低电平导通,正好点亮 LED0!);
      • 0010 变成 1101(正好点亮 LED1!)。
  • 收益:不管把代码拿到谁的开发板上,用户只需要在顶层把参数设为 0 或 1,模块内部代码完全通用免修改!

第五章:微观时序推演与真值滚动表

我们把 ModelSim 波形图放大到纳秒级,看第 $0.2\text{s}$ 满溢前后,三个核心变量是如何咬合动作的:

连续时钟周期 内部计数器 tick_counter 使能脉冲 tick 移位寄存器 led_logic 现态 移位寄存器次态准备 宏观状态分析
第 N-1 拍 9_999_998 0 4'b0001 保持不变 距 0.2 秒还差 1 拍,灯不动
第 N 拍
(Tick 产生瞬时!)
9_999_999
(达到终点线!)
1!
(组合逻辑瞬间导通)
4'b0001 移位逻辑准备就绪:{001, 0} 闹钟敲响!
下一个时钟上升沿将触发移位
第 N+1 拍
(移位完成瞬间!)
0
(计数器清零)
0
(闹钟自动熄灭)
4'b0010!
(触发器正式更新数值)
保持不变 流水动作完成!
光标从 LED0 跃迁到 LED1
第 N+$ 拍 1 0 4'b0010 保持不变 下一个 0.2 秒周期起跑
  • 极度严密的单拍时序:
    • tick 信号从产生到消失,严格只存活了 1 个时钟周期(20ns);
    • led_logic 在这 20ns 的窗口里被全局 50MHz 时钟敲击采样了一次,跳入新状态;
    • 随后 tick 熄灭,led_logic 锁定新状态,静静等待下一个一千万拍的流逝。

第六章:常见踩坑与工程进阶拓展

6.1 实验室两大翻车惨剧

  1. 惨剧一:上电后全灭,灯死活不跑
    • 排查诊断:检查 always 复位块,是否误写成了 led_logic <= 4'b0000;!没有播下 1 的种子,0 移一万次依然是 0。
  2. 惨剧二:流水灯闪得像神经病,四颗灯疯狂狂跳
    • 排查诊断:检查 ADC 管脚约束文件中的 LOCATION。如果手抖把 LED 引脚和板载按键或调试引脚弄混,或者时钟引脚接错,时钟线串扰会导致移位错乱。

6.2 进阶课后拓展:如何做“左右往返弹跳流水灯(霹雳游侠灯)”?

现在的代码是从右到左单向流动(0001 -> 0010 -> 0100 -> 1000 -> 0001)。
如果客户要求做成影视剧里的**“往返扫描灯”**:
$$\text{灯0} \to \text{灯1} \to \text{灯2} \to \text{灯3} \to \text{灯2} \to \text{灯1} \to \text{灯0}$$

  • 架构思考:
    1. 需要增加一个 1 位的方向标寄存器 reg dir;(1 代表向左流,0 代表向右流);
    2. 当流动到 led_logic == 4'b1000 时,把方向标翻转为右移;
    3. 当流动到 led_logic == 4'b0001 时,把方向标翻转为左移;
    4. 向左移动使用左移 {led_logic[2:0], 1'b0},向右移动使用右移 {1'b0, led_logic[3:1]}。

本讲核心总结

  1. 参数化(Parameter)是通用代码的标配:告别硬编码,全局旋钮一处改、处处改。
  2. 永远不要使用分频时钟:单全局主频 + 单周期使能脉冲(Tick)是高可靠系统的铁律。
  3. 环形移位寄存器(Ring Shift Register):巧妙使用位拼接算子 {led_logic[2:0], led_logic[3]},用最精简的硬件资源实现完美自循环。
  4. 板级极性适配:三目运算符搭配参数,打造跨平台、跨板卡兼容的高内聚 IP 模块。