第四十三章:交通灯实验

本文对应原书第1115页起,第五篇「实战篇」。

本章在讲什么?(先看这个)

本章是一个小型综合项目:在扩展板上复现十字路口的交通灯——红灯 30 秒、绿灯 27 秒、黄灯 3 秒,东西方向与南北方向交替放行,一个完整循环 60 秒,同时用 4 位数码管实时倒计时显示两个方向各自的剩余秒数,黄灯期间以 0.2 秒间隔闪烁。

这个实验把前面学的东西全部串起来了:状态机(按键实验)、分频计数(呼吸灯)、动态扫描数码管(IO 扩展)、组合逻辑译码(数码管段码)。还引入了两个新技能:二进制转 BCD(移位加 3 算法)和多位数码管动态扫描

核心概念拆解

交通灯状态机:4 状态循环 60 秒

状态 东西方向 南北方向 时长
状态1 绿 27s
状态2 3s
状态3 绿 27s
状态4 3s

LED 分配:led[5:3] 是东西向红绿黄、led[2:0] 是南北向红绿黄。

图43.4.4 状态跳转图

1Hz 分频:traffic_light_ctrl 的节拍器

系统时钟 50MHz,先分频出 1Hz 时钟作为状态计时节拍:

parameter WIDTH_CNT = 25_000_000;       // 50MHz/25M = 2Hz 翻转 → 1Hz
always @(posedge sys_clk or negedge sys_rst_n) begin
    if (!sys_rst_n) begin
        clk_cnt <= 25'd0;
        clk_1hz <= 1'b0;
    end
    else if (clk_cnt == WIDTH_CNT - 1'b1) begin
        clk_cnt <= 25'd0;
        clk_1hz <= ~clk_1hz;            // 计满翻转
    end
    else
        clk_cnt <= clk_cnt + 1'b1;
end

注意计满翻转而不是归零脉冲,所以分频系数是 25_000_000(对应周期 1 秒)。

倒计时与防下溢

用 time_cnt 在 1Hz 节拍下递减计数,time_cnt==1 时切换状态。为什么是 1 而不是 0?因为代码里每秒做 time_cnt - 1,如果等计到 0 再判断,0-1 会下溢成 63(6 位寄存器回卷),状态时长就乱了。这个"提前 1 拍切换"的细节是初学者最容易出错的地方。

数码管动态扫描:seg_led 模块

4 位数码管如果每位都独立接段选线,引脚会爆炸。动态扫描的做法:所有位的段选线并联,轮流使能位选——只要轮换速度够快(>50Hz),人眼就看不出闪烁。

// 1ms 计数器(50MHz 下计 50000)
parameter CNT_1MS = 50_000;
always @(posedge sys_clk or negedge sys_rst_n) begin
    if (!sys_rst_n)
        cnt_1ms <= 15'd0;
    else if (cnt_1ms == CNT_1MS - 1'b1)
        cnt_1ms <= 15'd0;
    else
        cnt_1ms <= cnt_1ms + 1'b1;
end

// 每 1ms 切换一位:cnt_state 0~3 循环
// 位选轮换(PNP三极管驱动,低电平有效)
sel <= 4'b1110 >> cnt_state;   // 1110 → 1101 → 1011 → 0111

扩展板数码管的位选由 PNP 三极管驱动,低电平有效;段码同样是低电平有效(0=8'b1100_0000 对应数字 0)——注意这里与 ch42 静态显示的极性不同,那是驱动电路决定的。

二进制转 BCD:binary2bcd 模块

倒计时秒数是二进制(0~59),数码管显示需要十进制各位(BCD 码)。经典算法是移位加 3(Shift-Add-3 / Double Dabble)

  • 参数 CNT_SHIFT_NUM = 6(移位次数),data_shift 寄存器 14 位(高段 BCD 区 + 低二进制区);
  • 每轮移位前检查各 BCD 位是否 ≥5,是则 +3(为下一步进位做准备),然后整体左移一位;
  • 移位 6 次后,高 8 位就是两个 BCD 位(十位、个位)。
// 移位加3核心:判断后加3再整体左移
if (data_shift[13:10] >= 5)  data_shift[13:10] <= data_shift[13:10] + 2'd3;
if (data_shift[9:6]  >= 5)   data_shift[9:6]  <= data_shift[9:6]  + 2'd3;
// ……左移
data_shift <= {data_shift[12:0], data_in[5]};

这个算法不用除法器就能完成进制转换,是 FPGA/嵌入式里非常经典的技巧。

黄灯闪烁:led_ctrl 模块

黄灯要求 0.2 秒亮 0.2 秒灭。用参数 TWINKLE_CNT = 10_000_000(50MHz 下 0.2 秒)做计数,计满翻转 led[0](南北黄)和 led[3](东西黄):

else if (cnt == TWINKLE_CNT - 1'b1) begin
    cnt <= 20'd0;
    led[0] <= ~led[0];
    led[3] <= ~led[3];
end

只有对应状态(状态2/状态4)才使能黄灯输出,红绿灯状态下黄灯强制熄灭。

顶层 top_traffic

例化 traffic_light_ctrl(状态机+倒计时)、binary2bcd(两组:东西向/南北向时间转BCD)、seg_led(动态扫描显示两位倒计时)、led_ctrl(黄灯闪烁),顶层做信号连接。

图43.4.2 总体模块框图

图片对照

  • 原理:图 43.1.2(十字路口信号灯示意图)、图 43.1.3(状态跳转表)——先看懂真实路口的时序再写代码。
  • 硬件:图 43.2.1(交通信号灯原理图)、图 43.2.2(实物图)、图 43.2.3(多位数码管内部连接图,理解动态扫描的硬件基础)。
  • 状态机与分频:图 43.4.3(交通灯控制模块框图)、图 43.4.4(状态跳转图)、图 43.4.5(1Hz 分频波形)、图 43.4.6/43.4.7(控制模块波形)。
  • BCD 转换:图 43.4.8(移位寄存器仿真图)——对照算法理解移位加 3 的每一步。
  • 数码管:图 43.4.9(显示模块框图)、图 43.4.10(1ms 计数器)、图 43.4.11(位选与段选的产生)、图 43.4.12(显示模块波形)。
  • LED 控制:图 43.4.13(led_ctrl 框图)、图 43.4.14(波形)、图 43.4.16(黄灯闪烁效果)。
  • 上板:图 43.5.2(硬件连接图)、图 43.5.3(交通灯实验现象)。

初学者容易踩的坑

  1. 状态切换条件写成 time_cnt==0:配合减一计数会导致 0-1 下溢回卷成 63,红灯秒数全乱。正确做法是 time_cnt==1 时切状态并重装计数值。
  2. 1Hz 分频系数写错:50MHz 计 25_000_000 次翻转一次才是 1Hz(翻转即半秒×2),写成 50_000_000 会变成 0.5Hz,倒计时慢一倍。
  3. BCD 转换忘了"先加 3 再移位"的顺序:必须在移位之前判断加 3,顺序反了结果就错。对照图 43.4.8 的仿真波形逐步验证。
  4. 位选/段选极性搞混:本实验的数码管经 PNP 三极管驱动,位选和段选都是低电平有效;而 ch42 的静态显示是高电平有效。两个实验的段码表不能互相抄。
  5. 动态扫描刷新率过低:每位停留时间若远大于 1ms(比如改成 10ms),会看到明显闪烁。1ms/位对应 250Hz 轮换率,安全。
  6. 黄灯闪烁计数器没有复位:从状态2跳回状态1时黄灯计数器若不清零,黄灯可能在下次进入状态2时从半亮状态开始,闪烁周期错位。

小结 & 下一步

本章是"实战篇"前半程的收官之作:4 状态交通灯状态机 + 1Hz 分频节拍 + 移位加 3 的 BCD 转换 + 动态扫描数码管 + 黄灯闪烁控制。尤其是二进制转 BCD 的移位加 3 算法和"防下溢提前切换"这两个细节,是可以在任何工程里直接复用的经验。

下一章我们回到通信领域,在串口通信实验的基础上更进一步:定义一套带包头、命令、校验的数据包协议,让上位机可以遥控开发板上的 LED、蜂鸣器和呼吸灯。