第四十三章:交通灯实验
本文对应原书第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] 是南北向红绿黄。

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.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(交通灯实验现象)。
初学者容易踩的坑
- 状态切换条件写成 time_cnt==0:配合减一计数会导致 0-1 下溢回卷成 63,红灯秒数全乱。正确做法是 time_cnt==1 时切状态并重装计数值。
- 1Hz 分频系数写错:50MHz 计 25_000_000 次翻转一次才是 1Hz(翻转即半秒×2),写成 50_000_000 会变成 0.5Hz,倒计时慢一倍。
- BCD 转换忘了"先加 3 再移位"的顺序:必须在移位之前判断加 3,顺序反了结果就错。对照图 43.4.8 的仿真波形逐步验证。
- 位选/段选极性搞混:本实验的数码管经 PNP 三极管驱动,位选和段选都是低电平有效;而 ch42 的静态显示是高电平有效。两个实验的段码表不能互相抄。
- 动态扫描刷新率过低:每位停留时间若远大于 1ms(比如改成 10ms),会看到明显闪烁。1ms/位对应 250Hz 轮换率,安全。
- 黄灯闪烁计数器没有复位:从状态2跳回状态1时黄灯计数器若不清零,黄灯可能在下次进入状态2时从半亮状态开始,闪烁周期错位。
小结 & 下一步
本章是"实战篇"前半程的收官之作:4 状态交通灯状态机 + 1Hz 分频节拍 + 移位加 3 的 BCD 转换 + 动态扫描数码管 + 黄灯闪烁控制。尤其是二进制转 BCD 的移位加 3 算法和"防下溢提前切换"这两个细节,是可以在任何工程里直接复用的经验。
下一章我们回到通信领域,在串口通信实验的基础上更进一步:定义一套带包头、命令、校验的数据包协议,让上位机可以遥控开发板上的 LED、蜂鸣器和呼吸灯。