ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA交通灯课设高分指南:状态机设计与Verilog实现

FPGA交通灯课设高分指南:状态机设计与Verilog实现 简介FPGA课程设计——交通灯设计项目源码与配套报告打包评审分99分适合计算机、电子类专业学生完成课程设计或期末大作业也可用于数字电路与FPGA综合实验。工程基于Vivado开发含Verilog源码、引脚约束、时序约束、位流文件、仿真脚本与测试波形并附完整课设报告读者可对照源码复现红绿灯状态转换、倒计时显示等核心逻辑。项目围绕交通灯控制展开覆盖状态机设计、时钟分频、计数器、数码管扫描等FPGA基础知识点报告包含系统框图、仿真结果与设计总结便于理解整体架构并快速上手。压缩包共209个文件主要涵盖.v设计源码、.xdc约束、.dcp综合网表、.bit配置文件以及.rpt报告与.log日志等包体仅1.37MB目录划分清晰便于按模块查阅。已有202人学习下载适合需要高分课设模板或想通过项目实战练习FPGA开发的初学者。1. 交通灯课设的高分密码不在代码量而在状态机交通灯控制系统是一个看起来人畜无害、实际最能拉开课设差距的FPGA题目。很多人以为把红黄绿三盏灯的时序写对就能拿高分结果抱着几百行Verilog上板灯光确实按8秒、3秒、12秒在跑答辩时老师一句“你的状态编码为什么这么写”“复位信号有没有做异步复位同步释放”就卡壳。这个题目的技术含量其实集中在三件事状态机建模是否规范、计数器与状态切换是否解耦、仿真与上板调试是否有可验证的证据链。而一份高分课设报告的意义就是把这三件事讲成可以被追问的故事。本篇文章不展开某个答辩现场的复盘而是按一个课设项目从零到收尾的顺序把手边能做的那套交通灯方案写成可复现的路径覆盖需求分析、Verilog实现、Testbench仿真、上板验证和报告书写适合正在做FPGA课程设计或准备用这个题目参加创新竞赛的本科生和研究生参考。2. 交通灯控制器的需求拆解与状态机选型2.1 十字路口的时序逻辑到底要拆出几个状态一份能拿高分的课设报告需求分析部分通常不是罗列“红灯亮、绿灯亮、黄灯亮”这种表层现象而是把十字路口当作一个有限状态集合来建模。主干道和次干道的红黄绿三色灯按照常规配时比如主干道绿灯40秒、黄灯5秒、次干道绿灯30秒、黄灯5秒其中次干道红灯期间主干道必须保持绿灯这种互斥关系天然适合用Moore状态机来描述因为Moore机的输出只取决于当前状态不会受输入信号的毛刺干扰这在交通灯这种要求输出绝对稳定的场景里是重要优势。拆状态时有一个常见的误区新手容易把“主干道绿灯40秒”拆成40个状态这会让状态图膨胀且难以维护。正确的做法是只拆出四个相位状态主干道绿灯、主干道黄灯、次干道绿灯、次干道黄灯时间流逝交给独立的计数模块处理。每个相位持续的时间放到参数定义里修改配时不需要动状态机本身只改参数值即可。这样拆下来状态机的状态转移条件就只剩下“计时结束”这一个事件输入信号少综合后的LUT用量低时序收敛也容易。状态编码的选择也是报告里能写出一段话的得分点。口诀是状态少用独热码状态多用二进制码。四个状态用独热码需要四个触发器用二进制只需两个但独热码的次态逻辑更简单、时序更好、布线更稳定。在低端芯片如Artix-7或Cyclone IV上做这种小型控制器触发器资源非常充裕独热码是更稳妥的选择。// 状态编码定义独热码 localparam S_GREEN_MAIN 4b0001, // 主干道绿灯 S_YELLOW_MAIN 4b0010, // 主干道黄灯 S_GREEN_SUB 4b0100, // 次干道绿灯 S_YELLOW_SUB 4b1000; // 次干道黄灯这段编码定义了四个互斥状态每个状态只占一个bit位判断当前状态时用case (current_state)匹配即可。参数名采用“S_颜色_道路”的命名规则可读性好在报告的状态转移图里也能与代码一一对应。状态转移条件统一由timer_done信号触发这是一种让计数逻辑和状态跳转逻辑完全解耦的常见写法。2.2 秒计时模块与状态机解耦的两种实现方式计时模块和状态机的关系是整个交通灯设计的架构核心。一种实现方式是把倒计时逻辑放进状态机内部在进入每个状态时加载对应的计数值每个时钟周期减一减到零时拉高timer_done信号并跳转到下一状态。这种方式代码量少但计数器和状态机耦合在一起状态多、计数值多时会让always块变得冗长综合后也不容易看出问题所在。另一种方式是让计时模块独立为一个功能模块端口只暴露时钟、复位、加载值和计时完成标志。在课设层面我更建议采用第一种带预装载倒计时的方式理由很实在代码写起来直观报告里的状态转移图也画得顺老师追问“如果绿灯时间要改成动态配时怎么办”时可以答“把预装载值从常量改成寄存器增加一个外部接口即可”这个扩展思路本身就是加分项。倒计时模块内部用BCD码还是二进制也是报告里能分析出一段的内容。BCD码在数码管显示时无需做二进制转十进制直接用四位bit表示一位数字但十位和个位需要分开计数逻辑稍重二进制计数则省触发器显示前需要做二进制到BCD的转换。下面是经过实际验证的倒计时模块写法// 每秒产生一个脉冲信号的计数器用于状态机计时 module timer_1s( input wire clk, input wire rst_n, output reg pulse_1s ); parameter FREQ 50_000_000; // 50MHz 系统时钟 reg [25:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 26d0; pulse_1s 1b0; end else if (cnt (FREQ - 1)) begin cnt 26d0; pulse_1s 1b1; end else begin cnt cnt 1b1; pulse_1s 1b0; end end endmodule这个模块的用途不是直接驱动灯而是产生一个秒脉冲pulse_1s状态机里的倒计时寄存器在pulse_1s为高时才减一。使用这种两层结构的好处是状态机的时序逻辑只以秒为粒度不受系统时钟频率影响换一块50MHz之外频率的开发板也只需要修改FREQ常量。FREQ的位宽计算方法是50MHz 的二进制表示需要26位2^26 67,108,864所以cnt定义为[25:0]这是一个容易被忽略的细节定义位宽不足会直接导致综合报错。2.3 状态机三段式写法在交通灯中的落地状态机的书写风格在课程设计中是一个显性评分点。一段式把次态逻辑、状态寄存器和输出逻辑全部塞进同一个always块里写起来最快但输出是寄存器输出会滞后一个时钟周期而且组合逻辑与时序逻辑混在一起阅读和维护都很痛苦。二段式把时序逻辑和组合逻辑分开分别是“当前状态寄存”和“次态确定”两个always块输出逻辑可以放在组合逻辑里也可以单独用assign赋值。三段式则在二段式基础上把输出逻辑也分离成独立的时序always块或组合always块。交通灯控制器的输出是灯的状态属于典型的状态解码输出适合把输出逻辑单独用一个always (*)组合块来处理。这样状态跳转、状态寄存、灯输出三者互不干扰仿真波形里也能清晰地看到状态编码与灯色输出之间的关系。三段式配合独热码是FPGA课程设计的黄金组合既避免了锁存器推导问题又让综合工具做状态编码优化时有更大的发挥空间。// 交通灯主控制器三段式状态机 module traffic_light_controller( input wire clk, input wire rst_n, input wire pulse_1s, output reg [2:0] main_light, // 主干道灯光 {红, 黄, 绿} output reg [2:0] sub_light, // 次干道灯光 {红, 黄, 绿} output reg [5:0] count_display // 倒计时BCD码输出 ); localparam S_GREEN_MAIN 4b0001, S_YELLOW_MAIN 4b0010, S_GREEN_SUB 4b0100, S_YELLOW_SUB 4b1000; parameter GREEN_MAIN_T 8d12; // 主干道绿灯时长秒 parameter YELLOW_MAIN_T 8d3; // 主干道黄灯时长秒 parameter GREEN_SUB_T 8d8; // 次干道绿灯时长秒 parameter YELLOW_SUB_T 8d3; // 次干道黄灯时长秒 reg [3:0] current_state, next_state; reg [7:0] counter; // 第一段状态寄存 always (posedge clk or negedge rst_n) begin if (!rst_n) current_state S_GREEN_MAIN; else current_state next_state; end // 第二段次态逻辑 always (*) begin next_state current_state; case (current_state) S_GREEN_MAIN: if (counter 0) next_state S_YELLOW_MAIN; S_YELLOW_MAIN: if (counter 0) next_state S_GREEN_SUB; S_GREEN_SUB: if (counter 0) next_state S_YELLOW_SUB; S_YELLOW_SUB: if (counter 0) next_state S_GREEN_MAIN; default: next_state S_GREEN_MAIN; endcase end // 倒计时逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin counter GREEN_MAIN_T; end else if (pulse_1s) begin if (counter 0) begin case (next_state) S_GREEN_MAIN: counter GREEN_MAIN_T; S_YELLOW_MAIN: counter YELLOW_MAIN_T; S_GREEN_SUB: counter GREEN_SUB_T; S_YELLOW_SUB: counter YELLOW_SUB_T; endcase end else begin counter counter - 1b1; end end end // 第三段输出逻辑 always (*) begin case (current_state) S_GREEN_MAIN: begin main_light 3b001; sub_light 3b100; end S_YELLOW_MAIN: begin main_light 3b010; sub_light 3b100; end S_GREEN_SUB: begin main_light 3b100; sub_light 3b001; end S_YELLOW_SUB: begin main_light 3b100; sub_light 3b010; end default: begin main_light 3b100; sub_light 3b100; end endcase end endmodule这里有三处细节值得在报告中用黑体标出。counter的预装载操作刻意使用了next_state而不是current_state。这是因为计数器减到0是在当前状态的最后一个秒脉冲里发生的此时状态还没有跳转如果使用current_state判断装载的是当前状态自身的时长参数而不是下一状态的时间参数整个循环的时序就会整体错位。第二个细节是输出逻辑采用阻塞赋值因为这里只做纯组合逻辑的状态解码不存在多路信号竞争。第三个细节是default分支无论如何都要写一旦状态寄存器的值因为上电不确定性而落到非独热码区域也能自动恢复到第一安全状态。3. 仿真验证交通灯逻辑的Testbench编写与关键信号观测3.1 Testbench里如何构造一个可复现的秒时序交通灯的仿真有一个特殊之处它的核心时间单位是秒而仿真器的推进单位是纳秒。如果Testbench里真的去数50MHz时钟的5000万个周期仿真会卡在漫长的事件调度里。实际工程里不会用真实秒来仿真整个绿灯时长的逻辑而是采用参数化时间缩放的技巧把时钟周期改成便于观察的单位或者直接构造pulse_1s信号来模拟秒脉冲。Testbench里给timer_1s模块喂一个虚拟的pulse_1s是更聪明的做法。主控制器只认pulse_1s的上升沿不会关心这个脉冲是怎么产生的因此Testbench完全可以用#10 pulse_1s ~pulse_1s之类的周期信号来代替真实的秒脉冲发生器复杂度从50MHz分频降到零。这种仿真时间缩放的做法在工程上叫抽象化能用高层次的激励代替低层次细节的就不要在激励里引入真实底层时钟。module traffic_light_tb; reg clk, rst_n, pulse_1s; wire [2:0] main_light, sub_light; wire [5:0] count_display; traffic_light_controller uut( .clk(clk), .rst_n(rst_n), .pulse_1s(pulse_1s), .main_light(main_light), .sub_light(sub_light), .count_display(count_display) ); initial begin clk 0; forever #10 clk ~clk; // 20ns周期50MHz end initial begin rst_n 1b0; pulse_1s 1b0; #30 rst_n 1b1; // 复位释放 repeat(30) begin #20 pulse_1s 1b1; #20 pulse_1s 1b0; end $finish; end initial begin $monitor(time%0t state%b main%b sub%b cnt%d, $time, uut.current_state, main_light, sub_light, uut.count_display); end endmodule这段Testbench里脉冲宽度刻意做成了20ns正好匹配一个时钟周期保证pulse_1s为高时能被时钟采样到。如果脉冲宽度是10ns而时钟周期是20ns就存在采样漏掉的风险。$monitor里直接引用了uut.current_state这个内部寄存器这是层次化引用的写法可以让监测信号不经过模块端口直接访问内部逻辑。在报告里写测试结果时把$monitor打印的时间戳和状态编码贴出来就构成了完整的可复现验证证据链。仿真通过后再用真实时间重复跑一轮完整仿真确认分频计数器的边界条件这两步合起来是课设验证部分最扎实的做法。3.2 交通灯功能仿真里必查的五个边界时刻仿真分析不能只看灯有没有按红黄绿顺序亮还要对状态切换的边界时刻逐一核对。最容易暴露问题的五个时刻分别是复位释放后的第一个时钟沿看当前状态是否是主干道绿灯主干道绿灯计时结束时counter是否从1减到0后再跳变到黄灯而不是直接从1跳到黄灯的加载值次干道绿灯与主干道黄灯的切换是否互斥有没有出现主干道黄灯和次干道绿灯同时点亮的重叠窗口counter值为0的周期里pulse_1s是否恰好为高导致计数器减出下溢以及在仿真时间推进到最后一轮循环时状态能否回到初始状态。这五个时刻在仿真波形里都对应具体的time点。用Vivado的仿真窗口直接打开traffic_light_controller模块把current_state、next_state、counter、pulse_1s这四个信号加到波形窗口把游标拖到每个边界点上核对信号值。尤其要注意第三点因为输出的两路灯光是组合逻辑理论上不会出现重叠但若输出逻辑中误用了if和else if的顺序在状态跳变瞬间的毛刺阶段就可能出现一个短暂的双灯亮。// 输出逻辑中禁止出现这种写法 if (current_state S_GREEN_MAIN) main_light 3b001; if (current_state S_YELLOW_MAIN) main_light 3b010;上面的连续if写法在状态跳变瞬间存在两个分支同时生效的可能尽管实际综合后大概率不会出错但在仿真和报告层面都难以解释。统一的case语句天然互斥是唯一推荐的状态解码输出形式。此外所有状态相关信号在Modelsim或Vivado Simulator中显示为不定态x时优先检查复位信号是否在初始化阶段正确拉低过一段时间而不是怀疑逻辑本身。4. 上板综合的引脚约束与排错手段4.1 在Vivado中给交通灯模块添加引脚约束文件仿真通过只说明逻辑正确上板验证才是FPGA课设不能缺席的一环。以常用的Xilinx Artix-7开发板为例在Vivado中新建工程后把timer_1s和traffic_light_controller两个模块添加进工程再新建一个顶层模块把两者例化顶层里引出板载100MHz系统时钟、复位按键和六路LED灯。引脚约束文件XDC的写法如下需要根据手上开发板的丝印调整引脚编号下面只是格式示范set_property PACKAGE_PIN E3 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports clk] set_property PACKAGE_PIN C12 [get_ports rst_n] set_property IOSTANDARD LVCMOS33 [get_ports rst_n] set_property PACKAGE_PIN H17 [get_ports {main_light[2]}] set_property PACKAGE_PIN K17 [get_ports {main_light[1]}] set_property PACKAGE_PIN L14 [get_ports {main_light[0]}] # 次干道LED引脚按此格式继续添加此处略每个端口的物理引脚和电平标准必须成对出现。若使用的开发板上有多个Bank记得检查对应Bank的供电电压是否与IOSTANDARD一致3.3V逻辑接到1.8V的Bank上虽能点亮灯但长期使用有损伤IO的风险。全部约束完成后运行Synthesis和Implementation然后Open Hardware Manager生成比特流烧录即可。从综合到上板最恶劣的现象是下载成功但灯全灭或者全亮。排查顺序是先确认rst_n。板载复位按键通常是按下为低、释放为高如果按键接的是低电平复位而代码里写的是高电平有效复位上电后芯片一直处于复位态灯当然不动。其次确认时钟频率分频模块里FREQ参数与实际板载时钟不同会导致灯切换速度不对比如原本应亮12秒的绿灯1秒就跳走了。最后用Vivado里的ILA核抓内部信号把current_state和counter拉进调试窗口触发条件设为上升沿就能看到内部计数是否工作。4.2 数码管显示倒计时的动态扫描与时序避让带数码管的交通灯课设比纯LED版本高一个档次但代价是要处理动态扫描的刷新时序。动态扫描的原理很简单多个数码管共用段选线用位选信号轮流点亮每一个数码管只要刷新频率高于人眼的闪烁融合频率约60Hz以上看上去就是四个数码管同时在亮。// 动态扫描位选逻辑 reg [1:0] scan_counter; reg [3:0] bcd_data; reg [3:0] seg_sel; always (posedge clk or negedge rst_n) begin if (!rst_n) scan_counter 2b00; else scan_counter scan_counter 1b1; // 1kHz扫描频率 end always (*) begin case (scan_counter) 2b00: begin seg_sel 4b1110; bcd_data sec_ones; end 2b01: begin seg_sel 4b1101; bcd_data sec_tens; end 2b10: begin seg_sel 4b1011; bcd_data sec_ones; end 2b11: begin seg_sel 4b0111; bcd_data sec_tens; end endcase end扫描频率的选取规则是扫描频率过低会出现闪烁过高则会因为刷新周期小于数码管的响应时间而亮度下降。1kHz到2kHz之间是工程上常用的区间也就是scan_counter每翻转一次约0.5到1ms。这里需要注意动态扫描的位选逻辑放在组合逻辑里而scan_counter是时序逻辑二者之间有一个时钟周期的延迟不会造成显示错位。扫描切换与倒计时更新之间潜在的数据竞争问题通常用双缓冲解决不过在课设这种低频场景下直接读写同一个寄存器通常也不会出现肉眼可见的异常。5. 报告撰写与答辩讲解的一体化打磨5.1 课设报告里放流程图比放代码段更安全课程设计报告最忌讳大段贴代码老师看报告的时间有限状态转移图、仿真波形、RTL视图三件套远比代码更直观。报告结构可以固定为五章第一章绪论写设计目标与技术选型第二章系统总体设计画模块框图标注timer_1s、traffic_light_controller、display三个模块之间的信号关系第三章详细设计里用状态转移表列出四个状态、转移条件和输出第四章给出仿真波形和截图第五章写调试过程与问题记录。附录里再放完整源码。状态转移表是报告里性价比最高的一张表。列名建议设为“当前状态 | 转移条件 | 下一状态 | 输出主干道/次干道”下面填上表格内容。一张表容纳了状态机的全部信息比用Visio画状态转移图更紧致清晰程度也更高。仿真波形截图时务必把时间轴单位缩放到能看清秒脉冲的位置截全信号名、时间戳、数值变化三个要素否则无法证明下板前做过验证。5.2 答辩前必练的三个高频追问答辩环节的质量往往决定了课设成绩能否从良好跨到优秀。这三个问题几乎必出现为什么用独热码而不用格雷码如果主干道和次干道要分别支持早晚高峰不同配时需要改哪里仿真通过但上板不亮你按什么顺序排查。第一个问题的标准回答是独热码的状态寄存器每个状态只有一个bit翻转次态逻辑最简功耗和时序性能都能接受且FPGA触发器资源充足四状态独热码比二进制码多消耗两个寄存器并不可惜。第二个问题要能答到点子上把GREEN_MAIN_T等参数从parameter改成可直接赋值的寄存器或引入拨码开关输入状态机本体一行不改。第三个问题的排查顺序是复位极性、时钟频率、引脚约束、上板实测信号按这四个层次回答基本不会漏掉得分点。最后一句话概括这个项目最该被记录的心得交通灯的价值不在灯而在状态机与计数器解耦后如何优雅地表达时间驱动的系统行为。本文还有配套的精品资源点击获取
返回列表