用Verilog三段式状态机实现智能交通灯:从设计到FPGA部署的工程实践
十字路口的红绿灯控制系统是数字逻辑设计的经典案例,也是初学者掌握状态机应用的绝佳切入点。本文将带你完整实现一个具备车辆检测功能的智能交通灯控制器,从状态图绘制到FPGA上板调试,全程采用业界推崇的三段式状态机写法。不同于教科书上的简化示例,这里会直面工程实践中的真实问题:如何设计可综合的延时逻辑?怎样避免阻塞赋值导致的仿真与硬件行为不一致?Testbench如何覆盖边界条件?
1. 需求分析与状态图设计
任何可靠的状态机设计都始于清晰的需求文档。我们的智能交通灯需要实现以下功能:
- 主路优先:默认状态下主路保持绿灯,支路红灯
- 车辆感应:通过传感器信号
x判断支路是否有车辆等待(x=1表示有车) - 状态转换:
- 当
x=1时,主路绿灯→黄灯(5秒)→红灯(1秒缓冲)→支路绿灯 - 当
x=0时,支路绿灯→黄灯(5秒)→红灯(1秒缓冲)→主路绿灯
- 当
1.1 状态转移图绘制
用Graphviz绘制的状态转移图更直观(实际工程文档推荐使用专业工具如Visio):
digraph traffic_light { rankdir=LR; node [shape=circle]; S0 [label="S0\n主路绿\n支路红"]; S1 [label="S1\n主路黄\n支路红"]; S2 [label="S2\n主路红\n支路红"]; S3 [label="S3\n主路红\n支路绿"]; S4 [label="S4\n主路红\n支路黄"]; S0 -> S1 [label="x==1"]; S1 -> S2 [label="计时5s"]; S2 -> S3 [label="计时1s"]; S3 -> S4 [label="x==0"]; S4 -> S0 [label="计时5s"]; S0 -> S0 [label="x==0"]; S3 -> S3 [label="x==1"]; }1.2 状态编码策略
在FPGA实现中,状态编码方式直接影响资源利用率:
| 编码方式 | 优点 | 缺点 |
|---|---|---|
| 顺序二进制 | 节省触发器 | 容易产生毛刺 |
| One-Hot | 简化译码逻辑 | 占用更多触发器 |
| Gray码 | 减少状态切换时的毛刺 | 编码复杂度较高 |
对于这个5状态的设计,推荐使用One-Hot编码:
parameter S0 = 5'b00001, S1 = 5'b00010, S2 = 5'b00100, S3 = 5'b01000, S4 = 5'b10000;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 == 20'd999_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 10
5. 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寄存器来实现请求缓存,确保交通灯变化的确定性。