1. 红绿灯控制器的现实意义与技术挑战
十字路口的红绿灯系统是城市交通的神经末梢,每天默默指挥着成千上万辆车的通行。表面上看只是"红灯停、绿灯行"的简单循环,但当你真正动手用Verilog实现一个可调时的控制器时,才会发现那些藏在LED背后的精妙设计。
我最近用FPGA开发板复现了这个系统,最大的感触是:交通信号控制是数字电路设计的绝佳练手项目。它既包含时序电路的核心思想(状态机、时钟分频),又涉及实际工程中的关键问题(防抖处理、参数可配置)。通过这个项目,你能真正理解为什么Verilog被称为"硬件描述语言"而非编程语言——我们不是在写软件,而是在用代码"焊接"数字电路。
这个项目的独特价值在于:
- 状态机设计:交通灯本质上就是多状态切换系统
- 参数化设计:需要支持不同方向的通行时间可配置
- 硬件思维:理解信号同步、时钟域等硬件特有概念
- 调试技巧:掌握FPGA开发中的逻辑分析仪使用
2. 系统架构与核心模块设计
2.1 整体设计方案
典型的十字路口有四个方向(南北直行、南北左转、东西直行、东西左转),我们的控制器需要:
- 分时控制12组信号灯(每个方向红黄绿三色)
- 允许通过外部按键调整各方向绿灯时长
- 提供倒计时显示输出
- 支持紧急模式(全红或特定方向常绿)
verilog复制module traffic_light(
input clk, // 50MHz主时钟
input rst_n, // 复位信号(低有效)
input [1:0] mode_btn, // 模式切换按钮
input time_adj_btn, // 时间调整按钮
output [2:0] NS_light,// 南北方向灯控(R,Y,G)
output [2:0] EW_light,// 东西方向灯控
output [7:0] seg // 七段数码管输出
);
2.2 时钟分频模块
FPGA的晶振通常是50MHz,而交通灯需要秒级计时。我们需要分频器将高频时钟转换为1Hz信号:
verilog复制reg [25:0] cnt;
reg clk_1hz;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt <= 0;
clk_1hz <= 0;
end
else if(cnt == 26'd24_999_999) begin
cnt <= 0;
clk_1hz <= ~clk_1hz;
end
else begin
cnt <= cnt + 1;
end
end
注意:实际工程中会使用PLL而非计数器分频,这里为演示基本原理
2.3 有限状态机设计
交通灯最核心的就是状态机。以基础的两相位控制为例:
verilog复制parameter S_NS_GREEN = 2'b00; // 南北绿灯
parameter S_NS_YELLOW = 2'b01;
parameter S_EW_GREEN = 2'b10; // 东西绿灯
parameter S_EW_YELLOW = 2'b11;
reg [1:0] current_state;
reg [7:0] timer;
always @(posedge clk_1hz or negedge rst_n) begin
if(!rst_n) begin
current_state <= S_NS_GREEN;
timer <= NS_GREEN_TIME;
end
else begin
case(current_state)
S_NS_GREEN: begin
if(timer == 0) begin
current_state <= S_NS_YELLOW;
timer <= YELLOW_TIME;
end
else timer <= timer - 1;
end
// 其他状态类似...
endcase
end
end
3. 关键实现细节与调试技巧
3.1 按钮防抖处理
机械按钮会产生10-20ms的抖动,必须进行消抖处理。这里采用经典的计数器法:
verilog复制reg [19:0] btn_cnt;
reg btn_stable;
always @(posedge clk) begin
if(time_adj_btn) begin
if(btn_cnt < 20'hFFFFF) btn_cnt <= btn_cnt + 1;
end
else btn_cnt <= 0;
btn_stable <= (btn_cnt == 20'hFFFFF);
end
3.2 时间参数存储
绿灯时长应该存储在非易失性存储器中。FPGA中可以用寄存器初始值实现:
verilog复制reg [7:0] NS_GREEN_TIME = 8'd30; // 默认30秒
reg [7:0] EW_GREEN_TIME = 8'd20;
always @(posedge btn_stable) begin
if(mode_btn == 2'b01)
NS_GREEN_TIME <= NS_GREEN_TIME + 5; // 步进5秒
else if(mode_btn == 2'b10)
EW_GREEN_TIME <= EW_GREEN_TIME + 5;
end
3.3 数码管显示驱动
倒计时显示需要七段译码器。推荐使用查找表法:
verilog复制reg [3:0] digit;
always @(*) begin
case(digit)
4'd0: seg = 8'b11000000;
4'd1: seg = 8'b11111001;
// ...其他数字编码
endcase
end
4. 常见问题与解决方案
4.1 状态机跑飞问题
症状:灯控顺序突然混乱
排查步骤:
- 检查状态机是否所有可能状态都有明确定义
- 确认复位信号是否可靠(建议上电复位+按键复位)
- 用SignalTap抓取state寄存器观察跳转过程
4.2 时间不同步问题
症状:倒计时与实际灯控时间不一致
解决方案:
- 确保所有计时器共用同一个1Hz时钟
- 检查状态切换条件是否与计时器归零同步
- 避免使用多个always块控制同一计时器
4.3 参数保存失效
症状:重新上电后自定义时长丢失
改进方案:
- 使用FPGA配置存储器(如EPCS)
- 添加EEPROM芯片存储参数
- 至少实现拨码开关设置初始值
5. 进阶优化方向
5.1 多相位控制
实际路口可能需要4个甚至更多相位(如单独左转相位)。这时状态机可以这样扩展:
verilog复制parameter PHASE1 = 3'b000;
parameter PHASE2 = 3'b001;
// ...更多相位
parameter ALL_RED = 3'b111;
reg [2:0] phase;
reg [3:0] sub_state; // 每个相位内的子状态
5.2 自适应调时
通过车流量检测实现智能控制:
verilog复制input NS_sensor; // 南北方向车辆检测
input EW_sensor;
always @(posedge clk) begin
if(NS_sensor && !EW_sensor)
NS_GREEN_TIME <= MIN_TIME + 10;
// 其他条件...
end
5.3 总线通信接口
添加UART或SPI接口接收远程控制:
verilog复制reg [7:0] uart_rx_data;
always @(posedge uart_rx_ready) begin
case(uart_rx_data[7:6])
2'b00: NS_GREEN_TIME <= uart_rx_data[5:0];
2'b01: EW_GREEN_TIME <= uart_rx_data[5:0];
endcase
end
6. 硬件实现注意事项
- 信号灯驱动:LED需要串联限流电阻(通常220Ω)
- 输出隔离:建议使用光耦隔离FPGA与外部电源
- 电源设计:数码管需要额外驱动电流(考虑使用74HC595)
- 散热考虑:大功率LED需要散热片
我在实际调试中发现一个有趣现象:当同时驱动多组LED时,如果所有灯同时切换,会在电源线上产生较大毛刺。解决方法是在Verilog代码中错开切换时间:
verilog复制always @(posedge clk_1hz) begin
if(timer == 1) begin // 提前1秒准备切换
NS_red <= 1'b0; // 先关闭当前红灯
#10 NS_green <= 1'b1; // 稍后开启绿灯
end
end
这个项目最让我惊喜的是,当把所有模块正确连接后,看着自己设计的信号灯按照预想节奏交替闪烁,那种成就感远超软件编程。它生动展示了硬件描述语言的魅力——我们不是在创造指令,而是在塑造电子流动的路径。