资讯动态

Verilog有限状态机实现交通灯控制器:从原理到Vivado仿真

发布时间:2026/8/19 4:26:45 来源:尧图企业网站定制
1. 项目缘起从十字路口的混乱到数字世界的秩序每次开车经过十字路口看着红绿灯有条不紊地切换你有没有想过这背后是怎么实现的是哪个程序员写的代码在指挥交通其实很多交通灯的核心控制逻辑其本质就是一个状态机。今天我们不谈复杂的交通系统就从一个最经典的入门级数字电路设计项目说起用Verilog硬件描述语言实现一个基于有限状态机FSM的交通灯控制器并在Xilinx的Vivado开发套件里完成仿真验证。这个项目听起来简单但它几乎是所有数字逻辑设计、FPGA开发工程师的“必修课”。它麻雀虽小五脏俱全时钟、复位、状态机、计数器、输出逻辑一个典型的数字系统该有的模块它都有了。更重要的是它能让你亲手把抽象的“状态转移图”变成实实在在的、能在硬件上运行的逻辑。很多人学Verilog语法背得滚瓜烂熟但一上手就懵问题往往就出在缺乏这样一个从理论到实践的完整闭环。通过这个项目你不仅能理解FSM的设计精髓更能掌握使用Vivado进行设计、仿真、调试的全流程这才是真正“会了”的标志。2. 核心需求与系统规格定义交通灯到底要“守”什么规矩在动手写代码之前我们必须把需求搞清楚。一个交通灯控制器不是随心所欲地闪灯它必须遵循严格的时序和逻辑规则否则就不是疏导交通而是制造混乱了。我们以一个典型的十字路口单向比如南北方向两组灯红、黄、绿为例来定义我们的系统规格。首先是基本的时序规则。这通常由交通管理部门根据车流量和安全要求设定。我们假设一个经典的循环周期绿灯亮允许通行持续30个时钟周期。黄灯亮警告即将禁止通行持续5个时钟周期。红灯亮禁止通行持续40个时钟周期。 然后循环回到绿灯。其次是安全规则。这是硬性要求绝对不能出错互斥规则同一方向的信号灯红、黄、绿在任何时刻有且只能有一个灯亮起。红灯、黄灯、绿灯同时亮起是绝对不允许的这会给司机造成混乱和危险。状态切换规则状态切换必须严格按照“绿 - 黄 - 红 - 绿”的顺序进行不能跳变比如直接从绿跳到红或者从红跳到黄。黄灯作为缓冲至关重要。最后我们需要定义系统的接口。作为一个独立的数字模块它需要与外部世界交互输入clk系统时钟输入所有状态切换和计时都基于此时钟的上升沿。rst_n低电平有效的全局复位信号。当复位有效时系统应进入一个确定的、安全的初始状态通常是红灯状态因为红灯代表“停止”是最安全的状态。输出RYG分别代表红灯、黄灯、绿灯的输出信号。通常用1‘b1表示灯亮1‘b0表示灯灭。有了这些明确的规格我们的设计目标就清晰了设计一个电路在时钟驱动下按照预设的时长循环切换三个输出信号并严格遵守互斥和顺序规则。3. 有限状态机FSM设计交通灯的大脑有限状态机是数字系统控制逻辑的灵魂尤其适合描述这种按步骤、有顺序的工作流程。对于交通灯控制器FSM的设计可以非常直观。3.1 状态定义与状态转移图根据我们的时序规则系统显然有三个主要状态GREEN绿灯状态。YELLOW黄灯状态。RED红灯状态。状态之间的转移条件就是时间。我们需要在每一个状态内部进行计时当计时达到预设值时就触发状态转移到下一个状态。因此我们的FSM是一个典型的“摩尔型”状态机——输出只依赖于当前状态例如在GREEN状态G输出为1R和Y输出为0。我们可以画出状态转移图--------------- | RED |---复位或红灯时间到 | (计时40个周期)| -------------- | (红灯计时满) v -------------- | GREEN | | (计时30个周期)| -------------- | (绿灯计时满) v -------------- | YELLOW | | (计时5个周期)| -------------- | (黄灯计时满) -------回到RED状态这个图清晰地描述了系统的行为。在Verilog中实现它通常有两种编码风格一段式、两段式、三段式。对于初学者我强烈推荐三段式FSM写法因为它结构清晰将状态转移逻辑、状态寄存器更新和输出逻辑分离易于编写、调试和综合。3.2 三段式FSM的Verilog实现骨架三段式具体指第一段时序逻辑用同步时序逻辑描述状态寄存器即当前状态state在时钟沿下的更新。第二段组合逻辑用组合逻辑描述次态逻辑next_state即根据当前状态和输入条件决定下一个时钟周期系统应该进入哪个状态。第三段输出逻辑描述输出。可以是组合逻辑输出与当前状态和输入有关米勒型也可以是时序逻辑输出与当前状态有关摩尔型。我们的交通灯是摩尔型。下面是用参数化方式定义状态和实现骨架的示例module traffic_light_fsm ( input wire clk, // 时钟 input wire rst_n, // 低电平复位 output reg R, // 红灯 output reg Y, // 黄灯 output reg G // 绿灯 ); // 状态定义使用独热码One-Hot或二进制码。独热码在FPGA中有时更高效。 parameter S_RED 2b00; parameter S_GREEN 2b01; parameter S_YELLOW 2b10; // 注意2位宽最多表示4个状态我们用了3个。也可以定义为3‘b001, 3’b010, 3‘b100独热码。 reg [1:0] state, next_state; // 状态寄存器 reg [5:0] counter; // 计时器位宽根据最大计数值40决定6位可计0-63。 // 计时常数方便修改 parameter CNT_GREEN 6d29; // 计30个周期从0到29 parameter CNT_YELLOW 6d4; // 计5个周期从0到4 parameter CNT_RED 6d39; // 计40个周期从0到39 // 第一段状态寄存器时序逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state S_RED; // 复位进入红灯状态 counter 6d0; end else begin state next_state; // 更新状态 // 计数器逻辑状态不变则累加状态改变则清零 if (state next_state) begin if (counter ... ) // 这里需要根据当前状态判断是否达到最大值 counter 6d0; // 实际设计中计数器满会触发状态转移这里清零由转移逻辑处理更简洁 else counter counter 1; end else begin counter 6d0; // 状态切换计数器清零 end end end // 第二段次态组合逻辑 always (*) begin next_state state; // 默认保持当前状态 case (state) S_RED: begin if (counter CNT_RED) // 红灯计时满 next_state S_GREEN; end S_GREEN: begin if (counter CNT_GREEN) // 绿灯计时满 next_state S_YELLOW; end S_YELLOW: begin if (counter CNT_YELLOW) // 黄灯计时满 next_state S_RED; end default: next_state S_RED; // 异常情况回到安全状态 endcase end // 第三段输出组合逻辑摩尔型 always (*) begin // 默认输出全灭避免锁存器 R 1b0; Y 1b0; G 1b0; case (state) S_RED: R 1b1; S_GREEN: G 1b1; S_YELLOW: Y 1b1; default: R 1b1; // 异常情况亮红灯安全第一 endcase end endmodule注意上面的代码是一个清晰的骨架但计数器的处理逻辑在状态寄存器和次态逻辑中有所交织。一个更常见的做法是将计数器作为一个独立的时序逻辑模块或者将计数器的“满”信号作为判断条件。上面的写法为了展示完整性将计数器更新放在了第一段。在实际优化时我们可能会把计数器逻辑单独拆分。4. 计数器模块的深度集成为状态机提供“心跳”状态机只知道要切换状态但“什么时候切换”需要计数器来告知。计数器是FSM的计时核心。我们需要一个能按不同状态要求进行不同时长计数的模块。一个高效的设计是让计数器成为一个独立的、受状态机控制的模块。状态机告诉计数器“开始计数”和“计数目标值”计数器数完后反馈一个“时间到”信号给状态机。但为了初学者的理解我们也可以采用一种更一体化的设计即在FSM内部维护一个计数器根据当前状态判断其是否计满。然而这要求计数器在每个状态都能与一个特定的阈值比较。让我们优化一下之前的代码将计数器逻辑更清晰地整合进去// ... 参数和状态定义同上 ... reg [5:0] counter; // 第一段状态与计数器更新 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state S_RED; counter 6d0; end else begin case (state) S_RED: begin if (counter CNT_RED) begin state S_GREEN; counter 6d0; end else begin counter counter 1; end end S_GREEN: begin if (counter CNT_GREEN) begin state S_YELLOW; counter 6d0; end else begin counter counter 1; end end S_YELLOW: begin if (counter CNT_YELLOW) begin state S_RED; counter 6d0; end else begin counter counter 1; end end default: begin // 安全处理 state S_RED; counter 6d0; end endcase end end // 第二段次态逻辑在这种写法下次态逻辑已经隐含在第一段的判断中可以省略显式的next_state // 但为了保持三段式结构清晰我们可以保留一个只做状态转移判断的组合逻辑块而将状态更新和计数器更新放在一个时序块。 // 更常见的“三段式”变体是将状态转移判断和输出作为组合逻辑状态和计数器更新作为时序逻辑。 // 下面我们展示一种更规范的三段式写法其中计数器作为状态转移的条件信号。 reg counter_full; // 计数器满标志位 // 组合逻辑块A计算counter_full always (*) begin counter_full 1b0; case (state) S_RED: counter_full (counter CNT_RED); S_GREEN: counter_full (counter CNT_GREEN); S_YELLOW: counter_full (counter CNT_YELLOW); default: counter_full 1b0; endcase end // 组合逻辑块B次态逻辑 always (*) begin next_state state; case (state) S_RED: if (counter_full) next_state S_GREEN; S_GREEN: if (counter_full) next_state S_YELLOW; S_YELLOW: if (counter_full) next_state S_RED; default: next_state S_RED; endcase end // 时序逻辑块更新状态寄存器和计数器 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state S_RED; counter 6d0; end else begin state next_state; // 更新计数器如果状态即将改变则清零否则累加 if (state ! next_state) begin counter 6d0; end else if (!counter_full) begin // 状态不变且未计满则计数 counter counter 1; end // 注意当counter_full为1且状态不变时理论上不会发生因为counter_full会立刻引起状态转移。 end end // 第三段输出逻辑不变 always (*) begin R 1b0; Y 1b0; G 1b0; case (state) S_RED: R 1b1; S_GREEN: G 1b1; S_YELLOW: Y 1b1; default: R 1b1; endcase end这种写法将“判断是否计满”和“根据计满信号决定次态”用组合逻辑实现而将状态和计数器的实际更新用时序逻辑实现结构更清晰也更容易进行时序分析。counter_full信号就像一个“时间到”闹钟通知状态机该换班了。5. Vivado工程创建与仿真测试让代码“跑”起来看波形设计写完了但它对吗硬件设计不像软件可以printf调试。我们必须通过仿真观察信号波形是否符合预期。Xilinx Vivado集成了强大的仿真工具我们用它来验证我们的设计。5.1 创建工程与添加设计文件打开Vivado点击“Create Project”。输入项目名称和位置选择“RTL Project”并勾选“Do not specify sources at this time”我们手动添加。选择默认的板卡或器件对于纯仿真选一个通用的如xc7a100tcsg324-1即可。在“Project Manager”的“Sources”窗口右键“Design Sources” - “Add Sources” - “Add or create design sources”创建新的Verilog文件如traffic_light_fsm.v将我们的代码粘贴进去。同样地我们需要创建一个测试平台文件。右键“Simulation Sources” - “Add Sources” - “Add or create simulation sources”创建新的Verilog文件如tb_traffic_light_fsm.v。5.2 编写测试平台Testbench测试平台的作用是模拟真实世界给我们的设计模块施加激励时钟、复位并观察其输出。一个基本的测试平台如下timescale 1ns / 1ps // 时间单位/精度 module tb_traffic_light_fsm(); // 输入信号连接到被测模块的输入定义为reg类型以便在initial块中赋值 reg clk; reg rst_n; // 输出信号连接到被测模块的输出定义为wire类型用于观察 wire R, Y, G; // 实例化被测模块 traffic_light_fsm uut ( .clk(clk), .rst_n(rst_n), .R(R), .Y(Y), .G(G) ); // 生成时钟信号周期20ns频率50MHz这是一个常用的测试时钟 initial begin clk 0; forever #10 clk ~clk; // 每10ns翻转一次周期20ns end // 生成复位信号及其他激励 initial begin // 初始化 rst_n 0; // 开始时复位有效 #100; // 保持复位100ns rst_n 1; // 释放复位 // 让仿真运行足够长的时间观察多个完整周期 #5000; // 仿真运行5000ns $finish; // 结束仿真 end // 可选将信号变化记录到VCD文件便于其他工具分析 initial begin $dumpfile(traffic_light_wave.vcd); $dumpvars(0, tb_traffic_light_fsm); // 0表示记录所有层次信号 end endmodule5.3 运行仿真与波形分析在Vivado左侧流程导航栏中找到并点击“Run Simulation” - “Run Behavioral Simulation”。Vivado会自动编译设计文件和测试平台然后打开仿真波形窗口。默认可能只显示顶层信号。你需要将实例uut下的信号statecounterRYGcounter_full等拖拽到波形窗口中。点击重新运行Restart和运行Run按钮。如何分析波形复位阶段最开始的100nsrst_n为0。观察state是否被正确置为S_RED或2‘b00counter是否为0输出R是否为1Y和G是否为0。释放复位后观察state是否保持在S_RED。观察counter是否在每个时钟上升沿加1因为我们的时钟周期是20ns所以每20ns加1。当counter数到39对应CNT_RED6‘d39时下一个时钟沿state是否跳变到S_GREEN2’b01同时counter是否清零。紧接着输出R应变0G应变1。同理验证counter从0数到29后状态是否切换到S_YELLOW输出变为Y1数到4后是否切回S_RED。检查互斥性在任何时刻RYG三个信号中最多只能有一个为高电平。你可以通过添加一个虚拟信号来检查assign error (R Y) | (Y G) | (G R);如果error信号出现高脉冲说明设计有严重错误。5.4 仿真中的调试技巧与常见问题信号为红色或蓝色虚线高阻Z或不定态X这通常是因为信号没有被正确驱动。检查测试平台中连线是否正确被测模块的输出是否在always块中被赋值组合逻辑要确保所有分支都有赋值否则会生成锁存器Latch导致仿真出现X。计数器不计数或状态不切换首先检查时钟clk和复位rst_n信号是否正常。然后检查counter的位宽是否足够比如CNT_RED39用5位宽只能表示0-31会溢出导致无法判断counter CNT_RED永远为假。最后检查比较逻辑if (counter CNT_RED)和if (counter CNT_RED)是有区别的前者计满40个周期0-39后者是40个周期0-39后的下一个周期才触发。我们的设计应该用。状态编码冲突如果你用二进制码2‘b11这个状态是未定义的。在case语句中必须用default分支处理将其导向一个已知安全状态如S_RED。使用$display辅助调试在Testbench或设计文件中加入$display(“Time%t, state%b, counter%d”, $time, state, counter);可以在Tcl控制台打印信息帮助理解仿真过程。6. 设计优化与功能扩展从基础到实用一个基础的交通灯控制器跑通了但真实的交通灯要复杂得多。我们可以基于这个核心FSM进行优化和扩展这更能体现一个工程师的设计能力。6.1 使用独热码One-Hot Encoding优化对于FPGA状态寄存器使用独热码有时比二进制码更有优势。独热码每个状态只有一个比特位为1。对于3个状态我们需要3位宽parameter S_RED 3b001; parameter S_GREEN 3b010; parameter S_YELLOW 3b100; reg [2:0] state, next_state;优势译码简单输出逻辑和状态判断更简单直接。例如G state[1];如果S_GREEN是第二位。时序可能更好在有些FPGA架构中相邻状态切换如001变010只有一位变化可能减少毛刺和功耗。易于调试在波形图上独热码的状态一目了然。注意独热码需要更多的触发器D触发器但FPGA中触发器资源通常比较丰富。需要确保状态机不会进入非法状态如3‘b011同样需要default分支进行恢复。6.2 添加使能信号与手动控制现实中交通灯可能需要夜间模式、紧急车辆优先或手动调试。我们可以增加输入信号enable使能信号。为0时交通灯保持当前状态并停止计时或全部变为黄灯闪烁为1时正常FSM工作。manual_redmanual_green手动控制信号。当某个手动信号有效时强制进入对应状态用于维护或紧急情况。这需要在次态逻辑和输出逻辑中加入对这些信号的处理优先级通常是手动控制 使能 正常FSM逻辑。6.3 实现双向交通灯与倒计时显示一个真正的十字路口有南北、东西两组灯。这两组灯的工作是互锁的一组绿灯时另一组必须是红灯。这需要设计一个更复杂的状态机可能包含4个或更多状态如南北绿-东西红 南北黄-东西红 南北红-东西绿 南北红-东西黄。状态转移图会变成一个更复杂的循环。同时我们可能需要驱动数码管来显示当前状态的剩余时间倒计时。这需要将counter的计数值转换为十进制并通过一个“数码管驱动模块”输出。例如在绿灯状态显示CNT_GREEN - counter的值。6.4 综合与上板验证进阶仿真通过后你可以进行综合Synthesis将RTL代码转换为门级网表。Vivado会报告资源使用情况LUT、FF等和时序分析结果是否满足时钟要求。如果有时序违例Setup/Hold Time Violation你可能需要流水线将复杂的组合逻辑路径拆开。重新设计优化状态编码或输出逻辑。添加约束在Xilinx Design Constraints (XDC) 文件中正确设置时钟周期。最后你可以生成比特流文件将其下载到真实的FPGA开发板上连接实际的LED灯观察交通灯是否按预期工作。这才是硬件设计的完整闭环。7. 从仿真到实战我踩过的那些坑最后分享几点从仿真到可能上板过程中容易忽略的细节这些在教科书里往往不会提复位策略的选择我们用了低电平复位rst_n。在FPGA上电时寄存器的初始值是不确定的。一个全局复位信号至关重要。在实际系统中这个复位信号可能来自外部的复位按钮或者由电源管理芯片产生。在Testbench中一定要充分测试复位和释放复位后的行为。组合逻辑环路与毛刺在编写输出逻辑或次态逻辑时如果always (*)块中的赋值存在潜在的反馈例如a b; b a c;可能会形成组合逻辑环路导致仿真振荡或综合失败。我们的交通灯输出逻辑是简单的case语句一般没问题。但要警惕。仿真与现实的差距仿真时时钟是理想的没有抖动和偏移。实际上板后如果时钟频率很高比如超过100MHz布线延迟可能成为问题需要通过时序约束来保证。我们的交通灯时钟频率很低比如1Hz通常不会有问题。参数化的好处我们把CNT_GREEN等时长定义成了parameter。这极大地提高了代码的可维护性。如果你想改变绿灯时长只需要修改一个地方不用在代码里到处找数字29。养成使用参数和宏定义的好习惯。代码风格与可综合性并非所有可仿真的Verilog代码都是可综合的即能转换成实际电路。initial块、#延迟、fork/join、部分系统任务如$display通常不可综合只能用于测试平台。我们的设计代码traffic_light_fsm.v中只使用了always、if/else、case、assign等可综合语句这是正确的。这个基于FSM的交通灯控制器项目就像数字电路世界的“Hello World”但它蕴含了硬件设计的核心思想用并行的、受时钟节拍控制的状态转移来描述逻辑。吃透它你就拿到了进入数字系统设计大门的第一把钥匙。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价