1. 项目概述与设计思路
第一次用FPGA做交通灯控制器时,我以为就是个简单的状态机切换。直到实际动手才发现,从时钟分频到状态保持,每个环节都藏着工程师的智慧。这次我们要实现一个带独立左转信号的交通灯系统,A、B两个通道需要严格遵循时序:A通道绿灯30秒→黄灯5秒→左转灯15秒→黄灯5秒,此时B通道始终保持红灯;之后角色互换,B通道获得40秒绿灯时长(含相同黄灯和左转时序),A通道则保持红灯。
选择Verilog实现的核心优势在于其硬件描述特性能够完美匹配交通灯控制的并行需求。与单片机方案相比,FPGA的并行处理能力可以确保各个通道的信号灯状态切换绝对同步,不会出现软件方案中可能存在的时序抖动问题。整个设计最关键的三个部分:精确的时钟分频模块、严谨的状态机设计、以及可靠的输出控制逻辑。
2. 核心模块实现细节
2.1 时钟分频设计
开发板提供的50MHz时钟对于秒级交通灯控制来说太快,需要分频到1Hz。这里采用26位计数器实现50,000,000分频(50MHz→1Hz):
verilog复制reg [25:0] div_cnt; // 2^26=67,108,864 > 50,000,000
always @(posedge clk_50m) begin
if(div_cnt == 50_000_000-1) begin // 从0开始计数
clk_1hz <= ~clk_1hz; // 时钟翻转
div_cnt <= 0;
end else begin
div_cnt <= div_cnt + 1;
end
end
实际项目中更推荐使用时钟使能信号而非直接生成派生时钟,可以避免跨时钟域问题。这里为了代码简洁采用翻转方式,但需要特别注意时序约束。
2.2 状态机设计
采用独热码(One-Hot)编码方式定义状态,虽然会多用一些触发器,但能简化组合逻辑并提高稳定性:
verilog复制reg [3:0] state;
// A通道状态定义
parameter A_GREEN = 4'b0001,
A_YELLOW = 4'b0010,
A_LEFT = 4'b0100,
A_Y2 = 4'b1000;
// B通道状态定义(与A通道独立)
parameter B_GREEN = 4'b0001,
B_YELLOW = 4'b0010,
B_LEFT = 4'b0100,
B_Y2 = 4'b1000;
// 各状态持续时间
parameter A_G_TIME = 30, // 单位:秒
A_Y_TIME = 5,
A_L_TIME = 15,
B_G_TIME = 40,
B_Y_TIME = 5,
B_L_TIME = 15;
状态转移控制是核心,采用三段式状态机写法(当前状态→次态逻辑→输出逻辑):
verilog复制reg [5:0] counter; // 6位足够计数到63
always @(posedge clk_1hz) begin
case(state)
A_GREEN: begin
if(counter == A_G_TIME-1) begin // 注意-1修正
state <= A_YELLOW;
counter <= 0;
end else begin
counter <= counter + 1;
end
end
A_YELLOW: begin
if(counter == A_Y_TIME-1) begin
state <= A_LEFT;
counter <= 0;
end else begin
counter <= counter + 1;
end
end
// 其他状态类似...
default: state <= A_GREEN; // 安全恢复
endcase
end
2.3 输出控制逻辑
灯组输出采用组合逻辑实现,确保无延迟响应:
verilog复制// A通道信号灯:light[2]=绿, light[1]=黄, light[0]=左转
assign A_light = (state == A_GREEN) ? 3'b100 :
(state == A_YELLOW) ? 3'b010 :
(state == A_LEFT) ? 3'b001 :
(state == A_Y2) ? 3'b010 : 3'b000;
// B通道红灯控制:当A通道处于任何活动状态时点亮
assign B_red = (state inside {A_GREEN, A_YELLOW, A_LEFT, A_Y2}) ? 1'b1 : 0;
SystemVerilog的inside操作符比多个或条件更简洁,但需要确保综合工具支持此语法。
3. 仿真与调试要点
3.1 Modelsim仿真配置
编写测试激励时需要注意:
verilog复制initial begin
clk_50m = 0;
forever #10 clk_50m = ~clk_50m; // 20ns周期=50MHz
end
initial begin
#600000000 $stop; // 仿真运行600ms(约3个完整周期)
end
关键检查点:
- A通道绿灯是否精确持续30个时钟周期
- 状态切换时B通道红灯是否立即响应
- 左转信号是否在正确时段激活
3.2 上板实测问题排查
实际部署时遇到的典型问题及解决方案:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 灯光闪烁不稳定 | 机械继电器响应延迟 | 增加10ms消抖逻辑 |
| 状态切换不同步 | 时钟偏移过大 | 检查时钟树约束 |
| 左转灯不亮 | 引脚分配错误 | 重新检查.qsf文件 |
引脚分配示例(Cyclone IV EP4CE6):
tcl复制set_location_assignment PIN_B14 -to clk_50m
set_location_assignment PIN_A13 -to A_light[0] # 左转灯
set_location_assignment PIN_B13 -to A_light[1] # 黄灯
set_location_assignment PIN_A14 -to A_light[2] # 绿灯
4. 工程优化建议
4.1 状态机优化方向
当前设计可以扩展:
- 增加紧急车辆优先通行模式(通过外部中断触发)
- 添加夜间模式(黄灯闪烁)
- 实现倒计时显示功能
4.2 时序约束示例
创建.sdc文件确保时序收敛:
tcl复制create_clock -name clk_50m -period 20 [get_ports clk_50m]
set_clock_groups -asynchronous -group {clk_50m} -group {clk_1hz}
4.3 资源利用率统计
在Cyclone IV EP4CE6上的实现结果:
| 资源类型 | 使用量 | 总量 | 利用率 |
|---|---|---|---|
| 逻辑单元 | 112 | 6272 | 1.8% |
| 寄存器 | 28 | 6272 | 0.4% |
| 引脚 | 9 | 179 | 5.0% |
5. 进阶开发思路
这个基础框架可以扩展为智能交通系统:
- 添加雷达或红外传感器检测车辆排队长度
- 实现动态时长调整算法
- 集成VGA显示模块实时展示状态图
- 通过UART接口连接上位机配置参数
我在实际调试中发现一个有趣现象:当使用机械继电器控制大功率信号灯时,状态切换瞬间会在电源线上产生毛刺。解决方法是在FPGA输出和继电器之间加入光耦隔离,同时在电源端增加大容量滤波电容。这种实战经验在教科书里可找不到。
