用Verilog三段式状态机搞定一个智能交通灯:从状态图到FPGA上板全流程

用Verilog三段式状态机搞定一个智能交通灯:从状态图到FPGA上板全流程 用Verilog三段式状态机实现智能交通灯从设计到FPGA部署的工程实践十字路口的红绿灯控制系统是数字逻辑设计的经典案例也是初学者掌握状态机应用的绝佳切入点。本文将带你完整实现一个具备车辆检测功能的智能交通灯控制器从状态图绘制到FPGA上板调试全程采用业界推崇的三段式状态机写法。不同于教科书上的简化示例这里会直面工程实践中的真实问题如何设计可综合的延时逻辑怎样避免阻塞赋值导致的仿真与硬件行为不一致Testbench如何覆盖边界条件1. 需求分析与状态图设计任何可靠的状态机设计都始于清晰的需求文档。我们的智能交通灯需要实现以下功能主路优先默认状态下主路保持绿灯支路红灯车辆感应通过传感器信号x判断支路是否有车辆等待x1表示有车状态转换当x1时主路绿灯→黄灯5秒→红灯1秒缓冲→支路绿灯当x0时支路绿灯→黄灯5秒→红灯1秒缓冲→主路绿灯1.1 状态转移图绘制用Graphviz绘制的状态转移图更直观实际工程文档推荐使用专业工具如Visiodigraph traffic_light { rankdirLR; node [shapecircle]; S0 [labelS0\n主路绿\n支路红]; S1 [labelS1\n主路黄\n支路红]; S2 [labelS2\n主路红\n支路红]; S3 [labelS3\n主路红\n支路绿]; S4 [labelS4\n主路红\n支路黄]; S0 - S1 [labelx1]; S1 - S2 [label计时5s]; S2 - S3 [label计时1s]; S3 - S4 [labelx0]; S4 - S0 [label计时5s]; S0 - S0 [labelx0]; S3 - S3 [labelx1]; }1.2 状态编码策略在FPGA实现中状态编码方式直接影响资源利用率编码方式优点缺点顺序二进制节省触发器容易产生毛刺One-Hot简化译码逻辑占用更多触发器Gray码减少状态切换时的毛刺编码复杂度较高对于这个5状态的设计推荐使用One-Hot编码parameter S0 5b00001, S1 5b00010, S2 5b00100, S3 5b01000, S4 5b10000;2. 三段式状态机实现业界公认的三段式写法将状态机清晰地划分为三个部分有效避免组合逻辑与时序逻辑的混合。2.1 状态寄存器时序逻辑// 第一段状态寄存器 always (posedge clk or negedge rst_n) begin if (!rst_n) current_state S0; else current_state next_state; end注意必须使用非阻塞赋值()这是硬件并行的关键特征2.2 次态逻辑组合逻辑// 第二段次态逻辑 always (*) begin case (current_state) S0: next_state (x) ? S1 : S0; S1: next_state (timer_done) ? S2 : S1; S2: next_state (timer_done) ? S3 : S2; S3: next_state (!x) ? S4 : S3; S4: next_state (timer_done) ? S0 : S4; default: next_state S0; endcase end2.3 输出逻辑时序逻辑// 第三段输出逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin main_light GREEN; branch_light RED; end else begin case (current_state) S0: begin main_light GREEN; branch_light RED; end S1: begin main_light YELLOW; branch_light RED; end S2: begin main_light RED; branch_light RED; end S3: begin main_light RED; branch_light GREEN; end S4: begin main_light RED; branch_light YELLOW; end endcase end end3. 关键子模块设计3.1 可综合的定时器模块硬件中的延时不能直接用#delay需要设计计数器module timer ( input clk, input reset, input enable, output reg done ); parameter COUNT_MAX 500_000_000; // 5秒100MHz reg [31:0] counter; always (posedge clk or posedge reset) begin if (reset) begin counter 0; done 0; end else if (enable) begin if (counter COUNT_MAX-1) begin counter 0; done 1; end else begin counter counter 1; done 0; end end else begin done 0; end end endmodule3.2 防抖滤波电路车辆传感器信号容易产生毛刺需要添加防抖逻辑module debounce ( input clk, input noisy, output reg clean ); reg [19:0] count; reg new; always (posedge clk) begin if (noisy ! new) begin new noisy; count 0; end else if (count 20d999_999) begin clean new; end else begin count count 1; end end endmodule4. 验证与调试4.1 自动化Testbench设计timescale 1ns/1ps module tb_traffic(); reg clk 0; reg rst_n 0; reg x 0; wire [1:0] main, branch; always #5 clk ~clk; traffic_light dut (.*); initial begin // 复位序列 #100 rst_n 1; // 测试场景1支路有车 #200 x 1; #1000 x 0; // 测试场景2随机车辆触发 repeat (10) begin #($urandom_range(500,2000)) x 1; #($urandom_range(100,500)) x 0; end #2000 $finish; end // 自动检查状态转换 always (posedge clk) begin if (rst_n) begin // 检查黄灯持续时间 if (dut.current_state dut.S1) begin #600 assert (dut.next_state dut.S2) else $error(S1-S2 transition failed); end end end endmodule4.2 常见调试问题排查状态锁死检查所有状态转移条件是否完备添加default分支捕获非法状态输出抖动确认所有输出寄存器都有复位值检查组合逻辑是否产生毛刺时序违例# Vivado中查看时序报告 report_timing_summary -delay_type min_max -max_paths 105. FPGA实现步骤5.1 Vivado工程创建流程创建RTL工程vivado -mode gui 添加设计文件add_files -norecurse { traffic_light.v timer.v debounce.v }设置引脚约束以Basys3为例set_property PACKAGE_PIN W5 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports clk] set_property PACKAGE_PIN V17 [get_ports x]5.2 上板调试技巧LED呼吸灯调试法// 在状态机中添加调试输出 assign debug_led (current_state S0) ? slow_pwm : (current_state S1) ? fast_pwm : 0;虚拟逻辑分析仪# 在Vivado中设置ILA create_debug_core u_ila ila set_property ALL_PROBE_SAME_MU true [get_debug_cores u_ila]在Basys3开发板上实际部署时发现当主路绿灯切换到黄灯时如果突然有车辆通过状态机需要完成当前黄灯周期后再响应新的车辆信号。这需要通过添加pending_request寄存器来实现请求缓存确保交通灯变化的确定性。