资讯动态

基于VHDL的交通灯设计:状态机、时钟分频与FPGA避坑指南

发布时间:2026/10/14 1:29:07 来源:尧图企业网站定制
如果你的数字电路课或者EDA课程有一个必做实验八成离不开这个题目基于VHDL的交通灯设计。它看起来实在不算难——无非是东西方向绿灯亮几十秒南北方向红灯跟着亮几十秒再换过来周而复始。但真上手你就会发现一个完整数字系统该有的元素它全都有时钟分频、有限状态机、计数器、数码管动态扫描、同步时序、仿真验证再加上下载到FPGA开发板之后各种奇奇怪怪的硬件问题。这篇文章就来聊聊我做这个实验反复踩坑后总结的一些经验希望给刚入门的同学一点参考也给要带实验的朋友提供一份避坑清单。1. 问题拆解与选型别急着写代码先把需求定清楚1.1 一个最小可用的交通灯控制器要实现什么很多人拿到题目就开始写VHDL代码写一半发现状态定义不清楚、倒计时显示不知道接哪、复位逻辑和灯信号对不上。问题基本出在需求没有先量化。做工程的第一步永远是把行为描述清楚再想怎么用代码实现。以最常见的十字路口模型为例一套最小可复用的功能指标可以这样定义四组灯东西方向一组红、黄、绿南北方向一组红、黄、绿。正常时序东西绿灯亮20秒然后黄灯亮5秒南北方向在这25秒内保持红灯。接着南北绿灯亮20秒、黄灯亮5秒东西方向保持红灯。循环往复。倒计时显示两组数码管分别显示东西、南北方向的剩余秒数方便观察。异步复位按下复位键后系统回到初始状态即“东西绿灯、南北红灯”。这里的时间参数不是死的课程要求可能是主路30秒、支路15秒也可能要求带左转箭头灯。但最小模型先跑通后面加复杂功能都是在这个框架上做增量。为什么把东西方向设为25秒周期、南北方向也是25秒周期因为两个方向周期一致、相位差半周期实现最简洁状态机的状态数量最少波形验证也直观。如果你一上来就设计四个方向各自不同的绿灯时间不是不行但状态切换和倒计时显示的计算会麻烦很多教学演示完全没必要。1.2 为什么这个实验默认用VHDL而不是Verilog现在Verilog在产业界使用更广泛但高校课程里“基于VHDL的交通灯设计”依然是经典题目原因其实很实际。VHDL是强类型语言信号赋值、端口类型、库引用都很严格写错的地方编译器能提前拦住一大半它的实体entity和结构体architecture分离非常贴合硬件设计的“引脚封装 内部逻辑”思想对初学者理解模块化设计帮助很大。另外大量已有的实验箱、参考代码、课程资料都是VHDL写的生态延续性很强。我的建议是不要纠结语言之争。VHDL语法约束多写起来确实比Verilog“啰嗦”但正是这种啰嗦让你在写每一行之前多想想它对应什么硬件结构。交通灯这个体量用VHDL写核心逻辑大概一百来行刚好是熟悉语法和培养硬件直觉的好素材。等你把状态机、分频器、显示扫描这些基本招数学会了以后切到Verilog或者SystemVerilog也就是一个语法迁移的问题。1.3 方案选型状态机优于“超级计数器”有的同学会这样想时间都是顺序走的我直接用一个能计到60秒的计数器在计数值落在不同区间时让对应的灯亮不也行吗确实能跑但这是典型的“能用”和“好用”之间的差距。用大计数器控制所有现象时间参数会以魔法数字的形式散落在代码各处比如“cnt 500_0000”是绿灯时段“cnt 500_0000 and cnt 625_0000”是黄灯时段一旦要调整时长你得重新算一整组边界非常容易错。后面如果再加夜间模式、紧急全红模式这种写法基本要推翻重来。更合理的方式是有限状态机。交通灯本质上是“在一组时间序列控制下反复切换输出状态”的系统状态机模型就是为这件事而生的。每个状态表示一组灯的组合状态内只关心“当前已经维持了多久”和“什么时候切走”逻辑层次非常清楚。代码量和大计数器方案差不多但可读性、可维护性、扩展性完全不在一个量级。这也是为什么工业界的控制逻辑比如电梯、自动售货机、家电主控几乎清一色用状态机实现而不是靠一堆计数器到处设阈值。2. 整体架构拆解交通灯系统的三个核心模块2.1 顶层模块怎么划分一个干净的设计顶层尽量保持简单内部按功能切成几个部分。我习惯这样划分时钟分频模块clk_div把开发板上的高频时钟常见的是50MHz变成系统需要的“秒使能脉冲”和“数码管扫描使能”。主控制状态机traffic_fsm根据当前状态和秒使能脉冲进行状态切换同时输出红黄绿灯信号。倒计时数据生成count_down根据当前状态输出各组剩余时间用于数码管显示。数码管显示模块seg_display接收倒计时的BCD码或二进制数做动态扫描输出段码和位选信号。顶层实体端口一般包括系统时钟clk、复位rst_n、东西方向三色灯输出、南北方向三色灯输出以及数码管的段选和位选输出。这样划分的好处是每个模块都能独立仿真、独立替换。比如你觉得数码管扫描写得有问题直接单独测试seg_display模块不用把整个交通灯系统都拉出来遛一遍。模块之间用清晰命名的信号连接顶层不需要知道内部细节这本身就是硬件工程里很重要的解耦思想。2.2 状态机设计四个状态和两种编码主状态机我的建议是定义四个状态名字取得能看懂不要用裸数字状态东西方向南北方向持续时间S_EW_GREEN绿灯亮红灯亮20秒S_EW_YELLOW黄灯亮红灯亮5秒S_NS_GREEN红灯亮绿灯亮20秒S_NS_YELLOW红灯亮黄灯亮5秒状态从S_EW_GREEN开始按顺序走到S_NS_YELLOW然后回到S_EW_GREEN首尾相接循环。这是最标准的“摩尔型状态机”输出只取决于当前状态不取决于输入用VHDL实现时用case语句对每个状态赋值灯信号即可。状态编码有两种常见选择二进制编码00、01、10、11和独热码One-Hot1000、0100、0010、0001。四种状态用二进制编码最省触发器独热码的状态寄存器位数更多但组合逻辑简单、时序更好适合状态数量较多的场景。对交通灯这个实验两种编码差异不大我更推荐二进制编码代码直观综合报告也更干净。关键是不要用一个std_logic_vector信号硬编码来表示状态一定要用自定义枚举类型这样case语句里写的是状态名不是看不懂的“10”或“01”。2.3 秒脉冲生成为什么“使能脉冲”比“1Hz时钟”靠谱这是整个实验里我认为最值得讲的一个经验。很多人写分频器第一反应是板载时钟50MHz我要1秒的时间基准那就分频出一个1Hz的时钟信号把它接到状态机的时钟端口。这个方案在仿真里能跑通但在真实FPGA上埋了不少雷。直接分频出的1Hz信号在FPGA内部走的是普通布线资源不是专用时钟网络综合工具不会把它当成系统时钟来建树时序约束也不好做。真实场景里时钟偏斜、毛刺、跨时钟域问题都可能在现场偶发出现。更关键的是“用分频后的低频时钟作为状态机时钟”这种做法会让你的全部状态转移都依赖一个非理想时钟这是个坏味道。正确做法是什么呢保持系统时钟始终是板载高频时钟在这个统一时钟域里产生一个“秒使能脉冲en_1s”。这个脉冲平时为0每到整数秒拉高一个时钟周期然后立刻回到0。时钟还是原来的50MHz状态机在en_1s为1的那个上升沿更新计数或切换状态。这样所有逻辑都在同一个时钟域里综合工具处理起来非常干净时序收敛也稳定。以50MHz时钟为例1秒是50,000,000个时钟周期。计数器从0数到49,999,999正好一个循环在第50,000,000个周期回到0并输出一个高位脉冲。代码框架如下library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity clk_div is port ( clk : in std_logic; rst_n : in std_logic; en_1s : out std_logic ); end clk_div; architecture rtl of clk_div is signal cnt : integer range 0 to 49999999 : 0; begin process(clk, rst_n) begin if rst_n 0 then cnt 0; en_1s 0; elsif rising_edge(clk) then if cnt 49999999 then cnt 0; en_1s 1; else cnt cnt 1; en_1s 0; end if; end if; end process; end rtl;这个代码里的计数器上限是49,999,999不是50,000,000因为计数从0开始。如果板子时钟是100MHz就把边界改成99,999,999如果是12MHz等非标频率先算周期再填数字不要想当然。我见过不少同学这里少写一个9或者把边界直接填成50_000_000结果仿真里时间轴怎么看怎么不对。2.4 主控制状态机的核心代码有了秒使能脉冲状态机写起来就非常顺畅了。内部需要一个计数器记录当前状态已经维持的秒数每次en_1s拉高就加1加到该状态的持续时间上限后切换状态并清零计数。核心骨架类似这样type state_type is (S_EW_GREEN, S_EW_YELLOW, S_NS_GREEN, S_NS_YELLOW); signal state : state_type : S_EW_GREEN; signal sec_cnt : integer range 0 to 30 : 0; signal en_1s : std_logic; process(clk, rst_n) begin if rst_n 0 then state S_EW_GREEN; sec_cnt 0; elsif rising_edge(clk) then if en_1s 1 then case state is when S_EW_GREEN if sec_cnt 19 then state S_EW_YELLOW; sec_cnt 0; else sec_cnt sec_cnt 1; end if; when S_EW_YELLOW if sec_cnt 4 then state S_NS_GREEN; sec_cnt 0; else sec_cnt sec_cnt 1; end if; -- 其余状态类似 when others state S_EW_GREEN; sec_cnt 0; end case; end if; end if; end process;注意“sec_cnt 19”代表第20个秒脉冲来时切换不是“sec_cnt 20”。因为sec_cnt从0开始0到19一共20个值。很多人这里会出“多亮1秒”或者“少亮1秒”的偏差检查时优先看边界条件。灯信号输出可以用另一个组合逻辑进程实现用case语句对每个状态分别赋值。这个进程只做输出映射不处理时序是整个设计里最容易理解也最容易出问题的地方。最容易踩的坑是case分支覆盖不全或者没有对所有输出信号做默认赋值导致综合器“推断”出锁存器latch。锁存器在时序分析里是个麻烦东西明明仿真看起来没问题下载到板子上表现却不稳定。后面第4章我会专门讲这个问题怎么排查。注意组合逻辑进程里建议在case之前先给所有输出信号赋一个默认值比如先让所有灯为灭或按安全原则先让红灯亮再在case里覆盖。这样既可以避免锁存器又能保证状态切换瞬间不会出现“所有灯全灭”或者“绿灯和黄灯同时亮”的危险状态。3. 实操全过程从仿真、约束到开发板跑通3.1 Testbench怎么写才能快速验证代码写完先别急着上板仿真验证是第一步。Testbench其实不算太难但有几个细节直接影响验证效率。时钟产生一个标准的做法是用generate循环比如50MHz时钟的周期是20nsclk_process : process begin clk 0; wait for 10 ns; clk 1; wait for 10 ns; end process;复位信号要保证至少覆盖一个完整时钟周期让异步复位逻辑能够被正确触发。如果你用低电平复位rst_n就先把rst_n拉低几个周期再拉高释放。这里特别提醒复位极性和代码里的if条件必须一一对应代码写“if rst_n 0”测试平台却把复位默认拉高仿真波形就会一片红X这是新手最常见的第一道坎。还有一个非常实用的技巧验证状态机完整轮换时不需要真的仿真20秒。仿真时间是虚拟的20秒看着不长但在波形窗口里翻看会累。更高效的办法是把持续时间做成generic参数或者临时把秒使能逻辑改成毫秒级验证完再改回来。我常用做法是在顶层模块里加一个speed参数正常模式用1秒使能仿真模式用1毫秒使能一套Testbench就能重复用。3.2 引脚约束和下载别让代码死在最后一步仿真全绿之后上板才是真正考验。很多同学下载到板子上发现灯不亮或者乱亮第一反应是代码有问题重新仿真半天发现逻辑完全正确最后查来查去是引脚约束没写对。FPGA开发板的每个LED、按键、晶振都连接到芯片特定的物理引脚你必须根据开发板原理图把这些引脚名字或编号填进约束文件。比如系统时钟通常接在某个专门的时钟引脚LED可能通过74系列芯片或直接接FPGA的IO按键按下可能是低电平也可能是高电平。这些信息每个板子都不一样一定要以板子自带的原理图和例程为准。约束文件的写法取决于厂商工具链。Xilinx新工具链用XDC旧工具链用UCFIntel的Quartus则是在QSF或SDC里做引脚分配。无论哪种核心就是把端口名和物理引脚绑定。我去过很多次实验室发现最容易出问题的点往往不是某个工具的语法而是“想当然”。看到LED低电平点亮是共阳接法却用默认的高电平点亮逻辑看到复位按键按下是低电平却用if rst_n 0逻辑。这些只要照着原理图核对一遍就能避免。下载到板子之后正常现象应该是初始状态东西绿灯亮、南北红灯亮然后周期轮换。如果你发现灯亮但顺序不对或者数码管乱跳先不要改逻辑用万用表或者逻辑分析仪量一下FPGA引脚的实际电平和期望值对比很快就能定位是输出逻辑问题还是外部电路问题。3.3 数码管倒计时显示动态扫描的正确打开方式如果实验要求有倒计时显示数码管这块是绕不开的。四位数码管显示两组秒数本质是一个动态扫描的过程任何时候只点亮其中一位以足够快的速度循环点亮四位人眼视觉暂留效应会感觉四个数字同时亮着。动态扫描频率选择很关键。太高位选切换太快可能出现重影和拖尾太低肉眼能明显看到闪烁。我建议扫描周期在1KHz到2KHz之间也就是每一位停留500微秒到1毫秒。实现思路可以再用一个计数器产生扫描使能每来一个扫描周期就让位选信号切换到下一位同时把这一位对应的数字转换成段码送出去。这里有一个非常影响显示效果的细节位选切换时先灭掉段码再切换位选再点亮下一位。否则上一位的段码会短暂泄漏到下一位产生“鬼影”现象。用代码来说就是先让段码全灭再改变位选信号最后输出新的段码。这个顺序很重要很多人数码管显示乱、拖影问题就出在这里。段码表也要看板子的共阴共阳接法。共阴数码管“0”的段码通常是0x3F共阳则是0xC0高四位和低四位的接线顺序也可能不一样。不要在网上随便抄一个段码表就往上套最稳的办法是拿一根杜邦线手动让某一段点亮确认段位和引脚对应关系后再填表。4. 常见问题与排查技巧实录4.1 仿真波形全是X状态机完全不滚这是我在实验课上遇到频率最高的问题没有之一。原因基本是复位没生效或者复位极性搞反。Testbench里没有给复位信号赋初值或者赋了初值但在整个仿真过程中没有翻转状态机的初始状态就是未定义所有信号自然显示为X。解决方法是检查Testbench中复位信号的时序确保先进入复位态再释放复位。另外检查代码里异步复位的敏感列表是否完整如果你用了异步复位process语句的敏感列表里必须同时包含clk和rst_n漏掉任何一个复位逻辑都可能是错的。4.2 综合报告出现latch状态机像丢了魂锁存器是组合逻辑进程里的“幽灵”。仿真时ModelSim/Vivado Simulator能给出符合预期的波形但综合后生成的硬件结构和仿真模型并不一致导致上板行为异常。出现latch的典型场景是case语句没有覆盖所有分支if语句没有写else或者组合进程的敏感列表漏了输入信号。排查方法很简单综合完成后查看报告里的warning搜索“latch”一旦发现“inferred latch for signal xxx”就要回头修代码。修法就是在组合进程开头给所有输出信号赋默认值或者把case补上others分支让每个状态下所有输出都有确定值。4.3 下载到板子完全不工作或者灯乱亮综合下载后不工作优先怀疑三件事引脚约束有没有对应、复位极性对不对、LED点亮极性是共阳还是共阴。我见过最经典的案例是代码本身完全正确但复位引脚约束错了一个位置按下复位键实际控制的是另一颗LED引脚系统自然不响应。另一个高频问题是FPGA芯片时钟引脚在原理图上用特殊符号标注有些人把它当成普通IO约束结果时钟根本进不来。这时候不要一上来就怀疑逻辑先用开发板自带LED闪烁例程验证板子和下载环境是否正常再逐步替换成自己的代码分阶段定位问题。4.4 数码管闪烁、乱码、拖影如果倒计时数字自己会跳变、闪烁先检查动态扫描频率是否太低如果是数字错乱重点检查段码表是否匹配共阴共阳以及位选信号是否写反如果是拖影基本是没做“先灭段、再换位、再送段”的处理。下面这个表基本能覆盖常见现象现象常见原因解决办法仿真全是X复位无效或极性错误、敏感列表不完整检查Testbench复位时序、补全敏感列表上板不工作引脚约束错误、时钟引脚不对、下载文件不完整核对原理图约束、用LED例程验证环境灯乱亮LED极性接反、输出逻辑出现latch检查共阳/共阴、修组合进程默认赋值数码管闪烁扫描频率太低提高扫描使能频率到1KHz以上数码管乱码段码表错误、位选极性反验证段位、按板子丝印调整数码管拖影未消隐直接切位先灭段码再切换位选再送新码把这个表贴在实验记录本里遇到问题先对号入座基本能省掉一半排查时间。5. 把实验做成作品扩展方向与个人体会5.1 三个低成本扩展方向基础交通灯跑通之后其实有很多值得尝试的扩展玩法既能加深理解又能让实验报告更好看。第一个是加入左转箭头灯把状态从四个增加到七个或八个每次直行、左转分开发放。第二个是加夜间模式用一个拨码开关或者按键切换夜间模式下所有方向黄灯同时闪烁这个模式在状态机里实现只多一个状态但能演示模式切换的逻辑。第三个是加车流量感应用两个按键模拟传感器当某个方向检测到“有车等待”时按比例延长该方向绿灯时间。这几个方向都不需要改动底层框架只是增加状态和条件判断非常适合做课程设计的提升部分。如果还想玩得更深一点可以把倒计时数据从原来的二进制改成BCD码让数码管显示更自然也可以把显示模块改成LCD1602液晶屏滚动显示各方向剩余秒数和当前状态文字。LCD驱动本身就是一个很有价值的练习写一次到处能用。5.2 做了很多次之后的一点经验这个实验我从学生时代做到后来带人做前前后后经历了不少版本。最大的体会是先画状态转换图再写代码这句话听起来像废话但真正做到的人不多。交通灯的状态机逻辑不复杂可是不画图直接开写很容易在“黄灯和红灯是否同时亮”“绿灯切换瞬间要不要全红过渡”这些小细节上纠缠不清。把状态图、时间表、输出表先画好代码基本就是翻译的过程写起来快错了也容易对照修改。另外一个特别想说的是仿真通过只是第一关上板跑起来才是真的会了。数字逻辑里太多问题是仿真模型和真实硬件行为之间的差异比如时钟分配、信号延迟、引脚极性、板子噪声。遇到问题别慌按照“先环境、后约束、再逻辑”的顺序排查大多数问题都能在一个小时内定位。最后工程习惯这件事越早养成越好。文件名、实体名、信号名要起得表意清晰关键逻辑要加注释状态条件要用常量而不是魔法数字。这些习惯短期内看起来浪费时间等到代码量上来、或者隔一段时间再回来看自己的设计时才知道当年那些“顺手写的注释”和“勤快的模块拆分”有多值钱。交通灯实验是一个很小的系统但如果你能认认真真走完“定需求—画状态—写代码—仿真—上板—调bug”这整个流程它对硬件设计的理解帮助比单纯刷一百道题都大。

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

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

免费获取报价 →
↑