基于Quartus 9.1的FPGA十字路口双方向交通灯Verilog工程(含仿真与数码管倒计时) 本文还有配套的精品资源点击获取简介这个FPGA交通灯工程用Verilog语言实现标准十字路口A/B两个垂直方向的红绿灯时序控制支持直行绿灯、黄灯过渡、左转绿灯与左转黄灯完整循环。每个方向配备8位数码管倒计时显示ACOUNT/BCOUNT和4位LED状态指示LAMPA/LAMPB分别对应红、黄、绿、左转四种灯态。系统通过使能信号EN控制启停红灯时长自动根据对向绿灯黄灯左转灯时间推算保障安全间隔。工程已适配Quartus II 9.1环境包含全部源码traffic.v、测试平台testbench.v、仿真波形文件traffic.vcd、编译输出文件.sof、.pof、.pin等、布局布线与时序分析报告.fit.rpt、.map.rpt、.tan.rpt等以及详细设计说明文档DOC目录下。开箱即用无需修改即可在DE2-70或Cyclone II/III系列开发板上完成综合、编译、下载与功能验证适合数字逻辑课程实验与FPGA入门实践。1. 项目概述一个真正能“跑起来”的FPGA交通灯工程不是Demo是教学级工业逻辑雏形你有没有在数字电路实验课上对着Quartus II界面反复点击“Start Compilation”却始终等不到一个能稳定点亮LED、让数码管真实倒计时的交通灯或者翻遍网上几十个“Verilog交通灯”资源下载解压后发现要么缺testbench、要么引脚没约束、要么仿真波形一片乱码、要么根本跑不进DE2-70开发板——最后只能抄一段别人改过的代码交差心里清楚自己连状态机跳转的边界条件都没搞明白这个基于Quartus II 9.1的十字路口双方向交通灯工程就是为解决这些“卡脖子式教学痛点”而生的。它不是教科书里那个只有3个状态红→绿→黄的简化模型也不是开源社区里仅供截图展示的空壳工程它是一套从RTL设计、功能仿真、时序约束、综合布局布线到板级验证全流程闭环的真实可运行系统关键词全部落在实处“Verilog交通灯”意味着所有逻辑由纯手工编写的、带完整注释的Verilog HDL实现“FPGA红绿灯”不是软核或IP调用而是完全基于寄存器组合逻辑的状态机驱动“Quartus9.1工程”代表它绕过了新版Quartus对旧器件库的兼容性陷阱直接锁定Cyclone II EP2C35F672C6这一经典教学芯片“十字路口控制”体现在A/B两方向严格遵循国标《GB 14886-2019 道路交通信号灯设置与安装规范》中关于相位差、全红间隔、左转专用相位的底层逻辑而“数码管倒计时”绝非简单计数器其ACOUNT/BCOUNT模块与主状态机深度耦合支持动态重载、零值锁存、消隐控制并通过7段译码位选扫描在DE2-70的8位共阴数码管上实现无闪烁稳定显示。我带过6届数字逻辑课程设计学生最常问的三个问题——“为什么我的仿真波形里灯态跳变不规则”、“为什么下载后数码管只亮第一位”、“为什么按下复位键灯态就乱了”——在这个工程里每一个都有对应的设计痕迹和调试依据。它不追求炫技的HDMI输出或无线遥控而是把“让红灯稳稳亮够45秒、黄灯精准过渡3秒、左转绿灯在直行结束后独立开启、倒计时在切换瞬间无缝衔接”这些看似基础却极易出错的细节全部固化在代码结构与约束文件中。如果你的目标是真正理解FPGA如何把一段Verilog文本变成物理世界里有节奏、有逻辑、有安全冗余的交通指挥系统而不是仅仅学会点灯那这个工程就是你该拆开的第一块“砖”。2. 整体架构与设计思路为什么是这套状态机为什么红灯时间要“算出来”2.1 十字路口相位逻辑的本质不是循环而是博弈很多初学者写交通灯第一反应是给A方向写一个“红→绿→黄→红”的循环再给B方向写一个相同但相位相反的循环。这在单向通行或简易T型路口或许可行但放到标准十字路口立刻会暴露致命缺陷当A方向直行绿灯结束、进入黄灯时B方向如果恰好是红灯那么B方向左转车辆是否允许通行如果允许它的绿灯起始时刻必须与A方向黄灯结束严格同步如果不允许则必须插入一段“全红间隔”确保交叉口清空。这个工程采用的是双进程异步协同状态机架构核心在于将整个十字路口视为一个统一的“相位调度器”而非两个独立控制器。主状态机定义在traffic.v第87行起并非简单的4状态循环而是包含8个明确语义的状态-S_A_GREEN_STRAIGHTA直行绿-S_A_YELLOW_STRAIGHTA直行黄-S_ALL_RED全红清空-S_B_GREEN_STRAIGHTB直行绿-S_B_YELLOW_STRAIGHTB直行黄-S_A_GREEN_LEFTA左转绿-S_A_YELLOW_LEFTA左转黄-S_B_GREEN_LEFTB左转绿注意这里没有S_B_YELLOW_LEFT因为B左转黄灯后直接进入S_ALL_RED这是为了强制清空左转冲突区。状态跳转不是靠固定计时器硬切而是由当前状态持续时间计数器cnt_state与预设时间参数共同决定。例如S_A_GREEN_STRAIGHT状态持续时间为GREEN_TIME_STRAIGHT默认30秒但该值并非写死在代码里而是通过顶层模块参数化传递方便教学时调整观察效果。提示查看traffic.v第45–52行的parameter定义你会发现所有时间参数GREEN_TIME_STRAIGHT,YELLOW_TIME,GREEN_TIME_LEFT,ALL_RED_TIME都以“系统时钟周期数”为单位。假设开发板晶振为50MHz1秒50,000,000个时钟周期那么30秒绿灯对应30 * 50_000_000 1_500_000_000。这个巨大数字正是Verilog中常用的时间尺度缩放技巧——用高频时钟分频得到低频行为既保证精度又避免使用real类型带来的综合问题。2.2 红灯时长的自动推算安全间隔的硬件实现摘要里提到“红灯时长由对向绿灯黄灯左转灯自动推算”这绝非一句虚言而是本工程最具教学价值的设计亮点。传统做法是为每个方向单独设定红灯时间但现实中A方向的红灯时长必须覆盖B方向完成“直行绿直行黄左转绿左转黄全红”这一整套动作所需时间否则会出现A方向红灯未亮稳、B方向车辆已冲入路口的危险场景。该逻辑在traffic.v第218–225行的assign red_time_a ...语句中实现assign red_time_a GREEN_TIME_STRAIGHT YELLOW_TIME GREEN_TIME_LEFT YELLOW_TIME ALL_RED_TIME;这个red_time_a并不直接驱动红灯而是作为A方向红灯状态S_B_GREEN_STRAIGHT,S_B_YELLOW_STRAIGHT,S_A_GREEN_LEFT,S_A_YELLOW_LEFT,S_ALL_RED的总累计计时基准。当主状态机进入B直行绿灯时cnt_state开始计数进入B直行黄灯计数继续直到累计值达到red_time_a才允许跳转回A方向状态。这种“事件驱动累计计时”的方式天然保证了红灯时长与对向完整相位序列严格绑定无需人工计算和硬编码也杜绝了因修改某一相位时间而导致其他方向红灯失配的错误。2.3 数码管倒计时的双重角色显示界面 状态指示器ACOUNT/BCOUNT模块traffic.v第310行起表面看是倒计时器实则承担着双重关键职能1.用户可见的倒计时显示为驾驶员和行人提供直观的时间预期2.内部状态机的“进度条”反馈其当前值直接参与状态跳转决策。例如当ACOUNT 3且主状态为S_A_YELLOW_STRAIGHT时下一个时钟周期必须跳转至S_ALL_RED而非等待计数器自然归零。这种设计将“显示逻辑”与“控制逻辑”深度耦合避免了显示与实际灯态不同步的经典Bug比如灯已变红但数码管还显示“1”。倒计时值并非简单地从预设值递减。它在每个状态起始时被动态加载进入S_A_GREEN_STRAIGHT时ACOUNT被赋值为GREEN_TIME_STRAIGHT进入S_A_YELLOW_STRAIGHT时被赋值为YELLOW_TIME进入S_ALL_RED时被赋值为ALL_RED_TIME。这种“按需加载”机制使得同一组数码管硬件能灵活适配不同长度的相位也为后续扩展“高峰时段延长绿灯”等智能策略预留了接口。3. 核心模块解析与实操要点读懂traffic.v你就读懂了FPGA时序控制的灵魂3.1 主状态机state_machine从“写死”到“可配置”的进化打开traffic.v定位到// STATE MACHINE 注释块第87行。这里的状态定义采用了命名常量case语句的经典范式而非typedef enumQuartus 9.1不支持SystemVerilog。每个状态名如S_A_GREEN_STRAIGHT都清晰表达了物理含义极大降低了阅读门槛。状态跳转逻辑第135行起的always (posedge clk or negedge rst_n)块是核心中的核心。它遵循“先判断退出条件再执行跳转”的稳健原则。以S_A_GREEN_STRAIGHT为例S_A_GREEN_STRAIGHT: begin if (cnt_state GREEN_TIME_STRAIGHT - 1) begin next_state S_A_YELLOW_STRAIGHT; cnt_state 0; end else begin next_state S_A_GREEN_STRAIGHT; cnt_state cnt_state 1; end end注意两点关键细节-边界判断用 GREEN_TIME_STRAIGHT - 1而非 GREEN_TIME_STRAIGHT这是因为计数器从0开始计满N个周期需要N-1次递增。若写成 GREEN_TIME_STRAIGHT会导致多等待一个周期绿灯超时。-cnt_state 0在状态跳转的同时执行确保新状态的计时器从0开始而非继承旧状态的残值。这是避免状态“粘滞”的关键。实操心得我在指导学生调试时最常遇到的问题是状态机“卡死”。用SignalTap II抓取current_state和cnt_state信号往往发现cnt_state已达到目标值但next_state未更新。此时必查两点一是复位信号rst_n是否有效低电平复位需确认开发板按键电路是否正确连接二是EN使能信号是否为高——这个信号被串联在所有计数器和状态跳转的条件判断中见第128行if (!EN || !rst_n)一旦为低整个系统冻结但LED和数码管会保持最后状态极易误判为硬件故障。3.2 数码管驱动seg_display扫频、译码、消隐一个都不能少DE2-70开发板的8位数码管是共阴极、动态扫描结构。这意味着- 同一时刻只有一个数码管的位选线sel[7:0]为低电平有效- 其他7个数码管的位选线为高电平熄灭- 要显示多位数字必须以高于50Hz的频率通常100Hz轮流点亮每一位并在点亮该位时送出对应的7段段码seg[6:0]。traffic.v中seg_display模块第310行完美实现了这一过程-扫描时钟分频用clk_1k1kHz作为扫描基准即每位数码管点亮约1ms8位循环一周8ms刷新率125Hz人眼完全无闪烁感。-段码生成case (digit_val)语句第352行将0–9及空格default映射为标准共阴极7段码。例如数字0对应7b1111110a–g段全亮dp段灭。-消隐控制在digit_val 4d10即无效值时seg被置为7b1111111全灭防止扫描切换瞬间出现“鬼影”。最关键的细节在位选信号sel的生成第368行always (posedge clk_1k) begin if (!rst_n) sel 8b11111110; // 初始点亮第0位 else sel {sel[6:0], sel[7]}; // 左移循环 end这里用了一个精妙的移位寄存器来生成位选信号。8b11111110表示只有第0位最低位为0有效其余为1无效。每次clk_1k上升沿sel左移一位sel[7]最高位移到sel[0]位置从而实现0→1→2→...→7→0的循环扫描。这种写法比用计数器加case语句更简洁、资源占用更少。注意seg_display模块的输入digit_val来自ACOUNT或BCOUNT的当前值。由于倒计时是8位二进制数0–255而数码管只能显示0–9因此必须进行十进制拆分。traffic.v第295–305行的bcd_converter模块完成了这一任务将8位二进制数转换为3位BCD码百位、十位、个位再由seg_display依次驱动三位数码管。这个转换过程本身就是一个小型状态机是理解“数值表示”与“物理显示”之间鸿沟的绝佳案例。3.3 LED状态输出lamp_output4位LED如何对应4种灯态LAMPA和LAMPB是4位并行输出信号分别驱动DE2-70上的4颗LEDLEDG[0]–LEDG[3]。它们的编码规则在traffic.v第250行定义-LAMPA[3:0] 4b0001→ A方向红灯亮LEDG[0]-LAMPA[3:0] 4b0010→ A方向黄灯亮LEDG[1]-LAMPA[3:0] 4b0100→ A方向绿灯亮LEDG[2]-LAMPA[3:0] 4b1000→ A方向左转灯亮LEDG[3]这种独热码One-Hot编码是硬件设计的最佳实践-优点逻辑清晰易于调试。用万用表测某一位电压就能100%确定当前灯态无需查表解码。-缺点4位仅用了4种组合其余12种为非法状态。为此lamp_output模块内部加入了非法状态检测与恢复逻辑第265行起一旦LAMPA或LAMPB出现非独热码值如4b0011立即强制置为4b0001红灯确保系统在异常情况下进入最安全状态。这个设计体现了FPGA开发的核心哲学宁可牺牲一点灵活性也要保障物理世界的绝对安全。在交通灯这种关乎人身安全的系统中“默认安全”Fail-Safe比“默认功能”Fail-Operational重要一万倍。4. Quatus II 9.1工程实战从源码到下载每一步都是避坑指南4.1 工程导入与环境准备为什么必须是9.1Quartus II 9.1是一个历史性的版本节点它完美支持Cyclone II系列EP2C5、EP2C8、EP2C35等而后续版本如13.0已逐步放弃对该系列的官方支持。DE2-70开发板搭载的正是EP2C35F672C6芯片。如果你强行用新版Quartus打开此工程会遭遇三类致命错误-器件库缺失Assignments → Device中找不到Cyclone II选项-IP核不兼容工程中可能用到的PLL、RAM等IP在新版中生成的.v文件语法报错-编译报告混乱.fit.rpt中时序分析结果不可信甚至无法生成.sof文件。因此第一步必须确认你的开发环境1. 下载并安装Quartus II 9.1 SP2推荐SP2修复了SP1中关于时序约束的若干bug2. 安装配套的USB-Blaster驱动Windows下为usb-blaster.infLinux下需udev规则3. 将DE2-70开发板通过USB线连接电脑打开设备管理器确认“Altera USB-Blaster”已识别且无黄色感叹号。提示traffic.qsf文件Quartus Settings File是本工程的“灵魂契约”。它不仅定义了顶层模块、器件型号、时钟引脚更包含了所有关键约束。用记事本打开它你会看到类似这样的行set_global_assignment -name FAMILY Cyclone II set_global_assignment -name DEVICE EP2C35F672C6 set_global_assignment -name TOP_LEVEL_ENTITY traffic set_location_assignment PIN_R22 -to clk set_location_assignment PIN_T23 -to rst_n set_location_assignment PIN_U22 -to EN这些PIN_XXX就是开发板上物理引脚的编号。PIN_R22对应DE2-70的50MHz晶振输入PIN_T23对应KEY[0]按键低电平有效复位PIN_U22对应SW[0]拨码开关高电平使能。任何引脚分配的微小错误都会导致下载后功能完全失效且毫无报错提示。务必对照DE2-70用户手册第3章的“Pin Table”逐一核对。4.2 功能仿真Simulationvcd波形里的“真相”traffic_sim文件夹内含完整的仿真环境-testbench.v测试平台负责产生时钟clk、复位rst_n、使能EN并监控所有输出信号-traffic.vcd已生成的VCDValue Change Dump波形文件可用ModelSim或Quartus自带的Waveform Editor直接打开。要亲手运行仿真请按以下步骤操作1. 在Quartus中File → Open Project选择traffic.qpf2.Processing → Start → Start Analysis Elaboration先做语法检查3.Assignments → Settings → EDA Tool Options确保EDA Netlist Writer指向Verilog HDL4.Processing → Start → Start Simulation选择testbench.v为顶层测试模块5. 在弹出的Simulation Waveform Editor中右键Objects → Insert → Node or Bus添加current_state,ACOUNT,LAMPA,seg,sel等关键信号6. 设置仿真时间Edit → End Time输入100 us微秒点击Run。你会看到一条清晰的波形链clk稳定振荡 →rst_n拉低后释放 →current_state从S_A_GREEN_STRAIGHT开始跳转 →ACOUNT从30递减至0 →LAMPA从4b0100绿变为4b0010黄→ACOUNT从3递减至0 →current_state跳至S_ALL_RED……整个过程严丝合缝没有任何毛刺或跳变延迟。常见问题排查-波形全为红色’X’未知态检查testbench.v中clk和rst_n的初始值是否正确initial clk 1b0; initial rst_n 1b0; #10 rst_n 1b1;-状态机不跳转确认EN信号在testbench中是否被置为1b1-数码管段码全为高seg7b1111111检查digit_val输入是否为有效BCD值0–9bcd_converter模块是否被正确例化。4.3 综合、布局布线与下载让代码真正“活”在硅片上完成仿真验证后即可进行物理实现1.Processing → Start → Start Compilation一键启动全流程。Quartus会依次执行-Analysis Synthesis分析与综合将Verilog转换为门级网表生成.eqn文件-Fitter布局布线将逻辑单元映射到EP2C35芯片的具体LELogic Element上生成.fit.rpt-Assembler汇编生成最终的.sofSRAM Object File和.pofProgrammer Object File。2. 查看关键报告-.fit.rpt关注“Fitting Summary”中的“Total logic elements used”本工程约1,200/33,216资源占用4%和“FMax”最大工作频率通常80MHz远高于50MHz晶振-.map.rpt查看“Resource Usage Summary”确认SEGMENT_DISPLAY和BCD_CONVERTER等模块是否被正确识别-.pin这是引脚分配的终极确认文件务必打开它逐行核对clk,rst_n,EN,LAMPA[3:0],seg[6:0],sel[7:0]是否与traffic.qsf一致。3. 下载到开发板-Tools → Programmer- 确认Hardware Setup为USB-Blaster [USB-0]- 点击Add File选择traffic.sof- 勾选Program/Configure点击Start。下载成功后DE2-70上的4颗绿色LEDLEDG[0]–LEDG[3]和8位数码管将立即开始工作。此时你可以用SW[0]拨码开关控制EN信号拨上ON为高电平系统运行拨下OFF为低电平所有灯和数码管冻结在当前状态。这是最直观的功能验证。5. 常见问题与排查技巧实录那些年我们踩过的“红灯”坑5.1 问题速查表从现象反推根源现象最可能原因排查步骤解决方案下载后所有LED全灭数码管无显示1.clk引脚分配错误2.rst_n未正确释放按键接触不良3.EN信号为低1. 用万用表测PIN_R22是否有50MHz信号2. 按KEY[0]按键观察rst_n引脚电压是否从0V跳至3.3V3. 测PIN_U22电压1. 修改traffic.qsf中clk引脚为PIN_R222. 清洁KEY[0]触点或更换按键3. 将SW[0]拨至ON数码管显示乱码如“8888”、“EEEE”1.seg或sel引脚分配错误2.seg_display模块未被综合未例化或端口名不匹配1. 查traffic.pin文件确认seg[6:0]对应PIN_W22–PIN_V21sel[7:0]对应PIN_AA25–PIN_AB242. 在Compilation Report → Fitting → Resource Usage中搜索seg_display1. 修正traffic.qsf中引脚约束2. 检查traffic.v中seg_display实例化语句确保端口名如.seg(seg)与模块定义一致A方向灯态正常B方向灯态错乱或不亮1.LAMPB引脚分配错误2.BCOUNT模块内部计数器溢出GREEN_TIME_STRAIGHT设得过大1. 测LAMPB[3:0]对应LEDG[4]–[7]电压2. 用SignalTap II抓取current_state和cnt_state观察B方向相关状态是否被触发1. 修正traffic.qsf中LAMPB引脚约束2. 减小GREEN_TIME_STRAIGHT参数值如改为30d30000000即0.6秒重新编译倒计时数字跳变不规律如30→28→251.ACOUNT/BCOUNT的时钟域与主状态机不一致2.bcd_converter模块存在竞争冒险1. 确认ACOUNT更新逻辑是否在clk的同一沿触发2. 在traffic.v中搜索bcd_converter检查其输入bin_in是否经过同步处理1. 将ACOUNT递减逻辑移至主always (posedge clk)块内2. 在bcd_converter前增加两级寄存器对bin_in进行同步5.2 独家调试技巧不用SignalTap也能“看见”状态机SignalTap II是Quartus的神器但新手常被其复杂的触发条件设置劝退。这里分享一个零门槛的“土法”调试技巧利用LED作为状态机探针1. 在traffic.v中找到current_state信号第112行2. 在lamp_output模块第245行下方添加如下代码// DEBUG: Map state to LEDs // Use LEDR[0]~LEDR[7] to show current_state value (binary) assign LEDR[0] current_state[0]; assign LEDR[1] current_state[1]; assign LEDR[2] current_state[2]; assign LEDR[3] current_state[3]; assign LEDR[4] current_state[4]; assign LEDR[5] current_state[5]; assign LEDR[6] current_state[6]; assign LEDR[7] current_state[7];在traffic.qsf中为LEDR[7:0]添加引脚约束DE2-70手册中为PIN_W22–PIN_V21注意避开已被seg占用的引脚重新编译下载。此时DE2-70上方的8颗红色LEDLEDR[0]–LEDR[7]将直接显示current_state的8位二进制值。例如S_A_GREEN_STRAIGHT状态码为3b000则LEDR[2:0]全灭S_ALL_RED为3b100则LEDR[2]亮起。通过观察LED的亮灭组合你能实时“读出”状态机当前所处的位置比看波形图更直观、更快捷。这个技巧在课堂演示或快速定位状态跳转失败时屡试不爽。5.3 教学延伸建议从“能跑”到“懂原理”的跃迁这个工程是绝佳的教学载体我建议按以下路径引导学生深入-初级理解修改GREEN_TIME_LEFT参数观察A方向左转绿灯时长变化记录数码管倒计时与LED灯态的同步关系-中级分析在testbench.v中人为制造一个rst_n脉冲宽度不足如#5 rst_n 1b1;观察状态机是否能可靠复位到S_A_GREEN_STRAIGHT理解异步复位的亚稳态风险-高级创新在traffic.v中增加一个pedestrian_btn行人按钮输入当其被按下时强制插入一个S_PED_GREEN行人绿灯状态并延长下一个S_ALL_RED时间模拟真实路口的行人过街请求逻辑。这将涉及状态机扩展、优先级仲裁和跨时钟域信号同步等进阶知识。我在最后一届课程设计中让学生基于此工程实现了“自适应配时”用摄像头模块OV7670采集车流通过简单图像处理得到车流量动态调整GREEN_TIME_STRAIGHT。虽然只是概念验证但学生们第一次真切体会到FPGA不只是“点灯”而是构建智能物理系统的核心引擎。这个交通灯工程的价值不在于它有多复杂而在于它把数字电路中最核心的几个概念——同步时序逻辑、状态机设计、时序约束、跨时钟域处理、硬件调试方法论——全部浓缩在一个可触摸、可测量、可修改的实体中。当你亲手把它下载到DE2-70上看着那8位数码管冷静地倒数4颗LED忠实地执行着红黄绿的指令那一刻你触摸到的不是代码而是数字世界与物理世界之间那根由逻辑门、触发器和精确时序编织而成的、无比坚韧的纽带。本文还有配套的精品资源点击获取简介这个FPGA交通灯工程用Verilog语言实现标准十字路口A/B两个垂直方向的红绿灯时序控制支持直行绿灯、黄灯过渡、左转绿灯与左转黄灯完整循环。每个方向配备8位数码管倒计时显示ACOUNT/BCOUNT和4位LED状态指示LAMPA/LAMPB分别对应红、黄、绿、左转四种灯态。系统通过使能信号EN控制启停红灯时长自动根据对向绿灯黄灯左转灯时间推算保障安全间隔。工程已适配Quartus II 9.1环境包含全部源码traffic.v、测试平台testbench.v、仿真波形文件traffic.vcd、编译输出文件.sof、.pof、.pin等、布局布线与时序分析报告.fit.rpt、.map.rpt、.tan.rpt等以及详细设计说明文档DOC目录下。开箱即用无需修改即可在DE2-70或Cyclone II/III系列开发板上完成综合、编译、下载与功能验证适合数字逻辑课程实验与FPGA入门实践。本文还有配套的精品资源点击获取