从零实现FPGA交通灯控制系统:状态机设计与紧急通行模块实战
1. 项目背景与核心需求
十字路口交通灯控制是FPGA初学者绝佳的练手项目。这个看似简单的系统,实际上融合了状态机设计、时序控制、外设驱动等多个数字电路核心概念。对于刚接触Verilog的同学来说,通过实现一个支持紧急通行功能的交通灯控制器,可以快速掌握FPGA开发的完整流程。
典型的交通灯系统需要满足以下核心需求:
- 多状态循环:主干道与支路的红绿灯按预设时间自动切换
- 倒计时显示:通过数码管实时显示当前状态剩余时间
- 紧急中断:特殊车辆通过时可强制切换为全红灯状态
- 黄灯过渡:任何绿灯到红灯的转换都需要3秒黄灯缓冲
// 状态定义示例 parameter MAIN_GREEN = 3'b001; // 主干道绿灯 parameter MAIN_YELLOW = 3'b010; // 主干道黄灯 parameter SIDE_GREEN = 3'b100; // 支路绿灯2. 系统架构设计
2.1 整体模块划分
我们的交通灯控制系统采用模块化设计,主要包含以下功能单元:
| 模块名称 | 功能描述 | 关键信号 |
|---|---|---|
| 时钟分频模块 | 将系统时钟分频为1Hz计时脉冲 | clk_1Hz |
| 状态控制模块 | 实现交通灯状态机及紧急通行逻辑 | current_state, next_state |
| 倒计时模块 | 生成各状态的倒计时数值 | count_val[7:0] |
| 数码管驱动模块 | 将倒计时数值转换为7段数码管信号 | seg_data[6:0] |
| LED驱动模块 | 控制实际红绿灯LED的亮灭 | red, yellow, green |
2.2 状态机设计
交通灯控制的核心是一个有限状态机(FSM),我们定义7个主要状态:
- S0:主干道绿灯(29秒),支路红灯
- S1:主干道黄灯(3秒),支路红灯
- S2:主干道左转绿灯(14秒),支路红灯
- S3:主干道黄灯(3秒),支路红灯
- S4:支路绿灯(20秒),主干道红灯
- S5:支路黄灯(3秒),主干道红灯
- S6:紧急通行模式(全红灯10秒)
// 状态转移逻辑片段 always @(posedge clk_1Hz or negedge reset_n) begin if (!reset_n) begin current_state <= S0; end else begin case(current_state) S0: if (timer_expired) current_state <= S1; S1: if (timer_expired) current_state <= S2; // ...其他状态转移 S6: if (timer_expired) current_state <= S0; endcase end end3. 关键模块实现细节
3.1 时钟分频模块
FPGA开发板通常提供50MHz系统时钟,我们需要将其分频为1Hz信号用于计时:
module clock_divider ( input clk_50MHz, input reset_n, output reg clk_1Hz ); reg [25:0] counter; always @(posedge clk_50MHz or negedge reset_n) begin if (!reset_n) begin counter <= 0; clk_1Hz <= 0; end else if (counter == 26'd24_999_999) begin counter <= 0; clk_1Hz <= ~clk_1Hz; end else begin counter <= counter + 1; end end endmodule3.2 倒计时与显示模块
每个状态都有特定的持续时间,倒计时模块需要根据当前状态加载不同的初始值:
| 状态 | 初始值 | 数码管显示 |
|---|---|---|
| S0 | 29 | 29→00 |
| S1 | 3 | 03→00 |
| S2 | 14 | 14→00 |
| S3 | 3 | 03→00 |
| S4 | 20 | 20→00 |
| S5 | 3 | 03→00 |
| S6 | 10 | 10→00 |
// 倒计时模块核心逻辑 always @(posedge clk_1Hz or negedge reset_n) begin if (!reset_n) begin count_val <= STATE_DURATION[current_state]; end else if (emergency) begin count_val <= 10; // 紧急模式固定10秒 end else if (count_val > 0) begin count_val <= count_val - 1; end else begin count_val <= STATE_DURATION[next_state]; end end3.3 紧急通行模块实现
紧急通行功能通过外部按钮触发,当检测到按钮按下时:
- 立即中断当前状态
- 强制切换到全红灯状态(S6)
- 保持10秒后恢复原状态循环
// 紧急模式处理逻辑 always @(posedge emergency_btn or posedge clk_1Hz) begin if (emergency_btn) begin saved_state <= current_state; current_state <= S6; end else if (current_state == S6 && timer_expired) begin current_state <= saved_state; end end4. 调试与优化技巧
4.1 Modelsim仿真要点
编写测试平台时,需要重点验证以下场景:
- 正常状态循环是否按时序切换
- 紧急按钮能否正确中断当前状态
- 倒计时显示是否准确
- 状态恢复后时序是否正确
// 测试平台示例片段 initial begin // 初始化信号 reset_n = 0; emergency_btn = 0; #100 reset_n = 1; // 正常运行观察 #2900000000; // 观察S0状态 // 触发紧急模式 emergency_btn = 1; #100 emergency_btn = 0; #10000000000; // 观察紧急模式 $finish; end4.2 常见问题与解决
- 状态机跑飞:确保所有状态转移条件完备,添加default分支
- 倒计时不同步:检查时钟分频是否准确,1Hz信号是否稳定
- 按钮抖动:添加防抖逻辑,通常20ms延时足够
- 显示异常:验证数码管译码表是否正确
调试提示:可先单独测试每个模块的功能,再逐步集成。使用SignalTap等工具实时观察FPGA内部信号。
5. 完整系统集成
将各模块例化连接后,顶层模块主要完成:
- 时钟分配与全局复位
- 按钮信号去抖处理
- 状态机与各功能模块互联
- 输出信号分配到实际IO引脚
module traffic_light_top ( input clk_50MHz, input reset_n, input emergency_btn, output [2:0] main_light, // [red, yellow, green] output [2:0] side_light, output [6:0] seg_main, output [6:0] seg_side ); wire clk_1Hz; wire debounced_btn; clock_divider u_clk_div( .clk_50MHz(clk_50MHz), .reset_n(reset_n), .clk_1Hz(clk_1Hz) ); debounce u_debounce( .clk(clk_50MHz), .btn_in(emergency_btn), .btn_out(debounced_btn) ); traffic_fsm u_fsm( .clk_1Hz(clk_1Hz), .reset_n(reset_n), .emergency(debounced_btn), .main_light(main_light), .side_light(side_light), .count_val(count_val) ); seg_driver u_seg( .value(count_val), .seg_out({seg_main, seg_side}) ); endmodule6. 功能扩展思路
基础功能实现后,可以考虑以下增强功能:
- 夜间模式:通过光敏传感器或定时器切换为黄灯闪烁
- 车流量检测:使用传感器动态调整绿灯时长
- 行人按钮:添加行人过街请求功能
- 无线遥控:通过蓝牙/WiFi实现远程控制
// 夜间模式实现示例 always @(posedge clk_1Hz) begin if (night_mode) begin main_light <= {1'b0, ~main_light[1], 1'b0}; side_light <= {1'b0, ~side_light[1], 1'b0}; end end在实现基础版本后,我建议先进行充分仿真验证,再下载到开发板测试。实际调试时,可以先用较短的定时值(如29秒改为2.9秒)来加速测试循环。遇到问题时,采用"分而治之"的策略,逐步隔离定位问题源。