基于VHDL的交通灯控制器:Quartus II 9.0全流程实战与避坑指南
简介这份资源是基于VHDL实现的十字路口交通灯控制器完整工程包面向电子信息、自动化等专业学习数字逻辑设计与FPGA开发的学生及工程师可用于课程设计、实验报告或自学练手。工程在Quartus II 9.0环境下编译仿真实现东西、南北两方向红黄绿指示倒计时采用两组数码管显示红灯35秒、绿灯50秒、黄灯5秒按S1键可进入特殊状态数码管闪烁、计数器保持并全路口红灯解除后继续计数另支持总体清零。压缩包共603个文件、约2.27MB以cdb、hdb、qmsg、rpt等Quartus中间与报告文件为主另有vhd源码、bdf/bsf原理图符号、vwf仿真波形、sof/pof下载文件及readme说明完整保留了从源码到综合下载的全流程痕迹。目前已有876人学习下载适合对照波形与报告理解状态机、分频和数码管扫描的实现思路也可直接复现或二次修改。1. 从一份 VHDL 交通灯控制器压缩包说起它到底能跑通什么如果你手头正好有一块老式 FPGA 开发板或者正在被 Quartus II 9.0 的课程设计卡住这份「基于 VHDL 的交通灯控制器.zip」大概率能让你少熬两个晚上。它不是一个只放了几张截图的演示包而是一套完整的、能在 Quartus II 9.0 下走完「新建工程 → 写代码 → 分配引脚 → 功能仿真 → 时序仿真 → 下载验证」全流程的 VHDL 工程。交通灯控制器本身不复杂但它是数字逻辑设计里少有的「麻雀虽小五脏俱全」的案例时钟分频、状态机、倒计时计数、数码管动态扫描、按键消抖这些模块你都能在一个工程里看到它们怎么互相咬合。适合谁正在学 EDA 课程、需要交一份能演示的 VHDL 大作业、或者想拿一个真实可综合的工程来练手 Quartus 操作流程的人。它不教你 VHDL 语法入门但能让你看到语法怎么变成电路。2. 拆开压缩包工程结构、模块划分与状态机设计2.1 文件目录与顶层实体拿到压缩包后先别急着双击 .qpf 文件解压到一个纯英文路径下路径里不要有空格和中文这是 Quartus II 9.0 的老毛病路径带中文会导致编译时找不到文件。解压后典型的目录结构是这样的traffic_light/ ├── traffic_light.qpf # Quartus II 工程文件 ├── traffic_light.qsf # 引脚与编译设置 ├── traffic_light.vhd # 顶层实体 ├── clk_div.vhd # 时钟分频模块 ├── traffic_fsm.vhd # 主状态机 ├── counter.vhd # 倒计时计数器 ├── seg_driver.vhd # 数码管动态扫描 ├── debounce.vhd # 按键消抖 └── tb_traffic_light.vhd # 仿真测试平台顶层实体traffic_light.vhd只做端口声明和模块例化不写具体逻辑。这种写法在课程设计里经常被忽略很多人把所有逻辑塞进一个文件结果改一处就要通读三百行。把分频、状态机、计数、显示拆成独立模块每个模块单独仿真出了问题能快速定位是状态跳转错了还是数码管扫描频率不对。2.2 主状态机的状态编码与转移条件交通灯的核心是一个有限状态机。常见做法是用 Moore 型状态机输出只取决于当前状态这样毛刺少适合直接驱动 LED。状态定义一般分四个南北绿灯、南北黄灯、东西绿灯、东西黄灯。每个状态里再嵌一个倒计时计数器计到零就跳转。-- traffic_fsm.vhd 状态机核心片段 type state_type is (NS_GREEN, NS_YELLOW, EW_GREEN, EW_YELLOW); signal current_state, next_state : state_type; signal cnt_en : std_logic; -- 计数器使能 signal cnt_clr : std_logic; -- 计数器清零 process(clk, rst_n) begin if rst_n 0 then current_state NS_GREEN; elsif rising_edge(clk) then current_state next_state; end if; end process; process(current_state, cnt_done) begin next_state current_state; case current_state is when NS_GREEN if cnt_done 1 then next_state NS_YELLOW; end if; when NS_YELLOW if cnt_done 1 then next_state EW_GREEN; end if; when EW_GREEN if cnt_done 1 then next_state EW_YELLOW; end if; when EW_YELLOW if cnt_done 1 then next_state NS_GREEN; end if; end case; end process;这段代码里cnt_done是计数器模块输出的完成信号状态机不直接管计数只负责在收到完成信号后跳转。这样做的好处是状态机和计数器可以独立仿真改绿灯时长只需要改计数器模块的预置数不用动状态机。参数方面绿灯时长一般设 20 到 30 秒黄灯 3 到 5 秒具体值在计数器模块里用常量定义方便统一修改。2.3 时钟分频与数码管扫描的配合开发板上的晶振通常是 50MHz直接拿来计数倒计时计数器位宽会大得离谱。所以需要一个分频模块先产生 1Hz 的秒脉冲再送给倒计时计数器。同时数码管动态扫描需要另一个频率一般在 1kHz 左右太快了亮度不够太慢了会闪烁。-- clk_div.vhd 产生 1Hz 秒脉冲和 1kHz 扫描时钟 process(clk_50m, rst_n) begin if rst_n 0 then cnt_1hz 0; cnt_1khz 0; elsif rising_edge(clk_50m) then if cnt_1hz 24999999 then cnt_1hz 0; clk_1hz not clk_1hz; else cnt_1hz cnt_1hz 1; end if; if cnt_1khz 24999 then cnt_1khz 0; clk_1khz not clk_1khz; else cnt_1khz cnt_1khz 1; end if; end if; end process;50MHz 分频到 1Hz计数上限是 25M分频到 1kHz 计数上限是 25k。这两个值在仿真时如果嫌跑得太慢可以临时改小比如把 1Hz 的计数上限改成 249仿真波形里就能快速看到状态跳转。但下载到板子上之前一定要改回来否则绿灯一秒就变看起来像坏了。3. 在 Quartus II 9.0 里走通编译与仿真从新建工程到波形验证3.1 新建工程与文件添加的先后顺序Quartus II 9.0 的界面和现在新版差别很大很多人第一次打开会找不到北。正确顺序是File → New Project Wizard → 指定工程路径和名称 → Add Files 时把 .vhd 文件全部加进去 → 选择目标器件。器件选择这一步很关键如果你用的是某高校实验室常见的 Altera Cyclone II EP2C5T144C8就选这个型号如果选错了后面引脚分配对不上编译能过但下载报错。工程建好后在 Project Navigator 里应该能看到所有 VHDL 文件按层次排列。顶层实体是traffic_light下面挂着分频、状态机、计数、扫描、消抖五个子模块。如果某个文件前面有问号图标说明它没有被正确解析检查文件里 entity 名和文件名是否一致Quartus II 9.0 对大小写不敏感但要求文件名和实体名匹配。3.2 功能仿真用 Testbench 跑出状态跳转波形功能仿真不涉及器件延迟只看逻辑对不对。压缩包里已经带了tb_traffic_light.vhd里面例化了顶层实体并给定了时钟和复位激励。在 Quartus 里点 Assignments → Settings → Simulator Settings选择 ModelSim-Altera 作为仿真工具然后 Tools → Run Simulation Tool → RTL Simulation。-- tb_traffic_light.vhd 激励片段 process begin rst_n 0; wait for 100 ns; rst_n 1; wait; end process; process begin clk_50m 0; wait for 10 ns; clk_50m 1; wait for 10 ns; end process;时钟周期设成 20ns对应 50MHz。复位信号先拉低 100ns 再拉高确保状态机回到初始状态。仿真跑起来后在 Wave 窗口里把current_state、cnt_done、clk_1hz拖进去你应该能看到状态按 NS_GREEN → NS_YELLOW → EW_GREEN → EW_YELLOW 循环跳转每次跳转都发生在cnt_done拉高的那个时钟沿。如果状态卡在某个值不动先查cnt_done有没有产生再查状态机里跳转条件是不是写成了cnt_done 0。3.3 引脚分配与下载验证仿真过了不代表板子能跑。引脚分配是 Quartus II 9.0 里最容易翻车的一步。点 Assignments → Pins把顶层实体的每个端口映射到实际引脚。时钟输入接开发板的 50MHz 晶振引脚复位接按键LED 输出接红黄绿三组灯数码管段选和位选接对应的 IO。分配完引脚后重新编译一次然后打开 Programmer选择 USB-Blaster 下载线加载 .sof 文件点 Start。如果下载进度条走到 100% 但板子没反应先检查复位按键是不是一直按着再检查数码管是共阴还是共阳共阴和共阳的段码表是反的写错了数码管会显示乱码或者全灭。提示Quartus II 9.0 在 Win10/Win11 上安装后USB-Blaster 驱动可能需要手动指定安装目录下的驱动文件自动搜索经常装不上。4. 避坑与排查仿真过了板子不亮、数码管乱码、状态卡死4.1 现象功能仿真波形正常下载后 LED 全灭原因通常有两个。一是引脚分配时把 LED 输出分配到了没有接 LED 的 IO 上或者分配到了输入专用引脚。二是复位按键的极性搞反了代码里写的是低电平复位但板子上的按键按下是低电平、松开是高电平如果松开时复位一直有效状态机就永远停在初始状态LED 自然不亮。解决方法是先用万用表量一下按键松开和按下时对应引脚的电平再确认代码里的复位极性。4.2 现象数码管显示的数字缺笔画或者全亮这是段码表写错了。共阴数码管的段码是「1 亮 0 灭」共阳是「0 亮 1 灭」。很多人从网上抄了一段共阴的段码表但板子上焊的是共阳数码管结果该亮的段灭了该灭的段亮了。解决方法是查开发板原理图确认数码管公共端接的是 GND 还是 VCC然后重新生成段码表。另外动态扫描的频率也要注意1kHz 左右比较合适低于 500Hz 会看到明显闪烁高于 5kHz 亮度会下降。4.3 现象状态机在仿真里跳转正常下载后卡在黄灯不跳黄灯状态卡死多半是倒计时计数器在黄灯状态下的预置数设成了 0。状态机跳到黄灯后计数器加载 0cnt_done立刻拉高状态机应该马上跳走。但如果计数器模块里黄灯的预置数写成了 0而状态机又在等cnt_done的上升沿就可能出现竞争导致状态机误判。解决方法是在计数器模块里给黄灯设一个非零的短时长比如 3 秒同时确保cnt_done是一个时钟周期的脉冲而不是电平信号。4.4 现象编译报错「Cant resolve multiple constant drivers」这个错误通常是因为在多个 process 里对同一个信号赋值了。VHDL 里一个信号只能在一个 process 里被驱动如果你在状态机 process 里给cnt_en赋值又在计数器 process 里给它赋值综合器就不知道该听谁的。解决方法是把cnt_en和cnt_clr统一放到状态机 process 里产生计数器 process 只读不写。4.5 现象时序仿真波形和功能仿真不一致时序仿真加入了器件延迟和布线延迟波形上会出现毛刺和延迟。如果时序仿真里状态跳转晚了一两个时钟周期这是正常的。但如果状态完全乱了说明组合逻辑路径太长建立时间不够。解决方法是在状态机输出上加一级寄存器把组合逻辑输出打一拍牺牲一个时钟周期的延迟换取时序稳定。5. 进阶玩法把倒计时改成可调参数与仿真脚本自动化5.1 用 generic 把绿灯时长做成可配置参数原始工程里绿灯时长是写死的常量改一次要重新编译。更优雅的做法是在实体端口里用 generic 声明参数顶层例化时传入不同值这样同一套代码可以适配不同路口的时长要求。entity traffic_fsm is generic ( GREEN_TIME : integer : 25; -- 绿灯时长单位秒 YELLOW_TIME : integer : 3 -- 黄灯时长单位秒 ); port ( clk_1hz : in std_logic; rst_n : in std_logic; cnt_done : in std_logic; state_out : out std_logic_vector(3 downto 0) ); end entity;例化时这样写u_fsm : entity work.traffic_fsm generic map ( GREEN_TIME 30, YELLOW_TIME 5 ) port map ( clk_1hz clk_1hz, rst_n rst_n, cnt_done cnt_done, state_out state_out );这样改时长不用动状态机内部逻辑只改例化处的数字就行。注意 generic 的值在综合时是常量不能运行时动态改如果需要运行时通过按键调时长那得用寄存器存预置数复杂度会高不少。5.2 用 ModelSim 命令行脚本批量跑仿真每次改完代码手动点仿真很费时间可以写一个 .do 脚本让 ModelSim 自动跑完并保存波形。# run_sim.do vlib work vcom -93 ../src/clk_div.vhd vcom -93 ../src/counter.vhd vcom -93 ../src/traffic_fsm.vhd vcom -93 ../src/seg_driver.vhd vcom -93 ../src/debounce.vhd vcom -93 ../src/traffic_light.vhd vcom -93 ../sim/tb_traffic_light.vhd vsim -t 1ns work.tb_traffic_light add wave -r /* run 10 ms在 ModelSim 命令行里执行do run_sim.do它会自动编译所有文件、加载测试平台、添加所有信号到波形窗口、跑 10ms 仿真时间。10ms 足够看到好几个完整的红绿灯循环。这个脚本的好处是每次改完代码不用重新点菜单直接重新跑一遍就行适合反复调参的场景。5.3 一个容易被忽略的细节信号命名与综合属性VHDL 里信号名不要用clk、reset这种太通用的词综合器可能会把它们当成全局时钟资源导致布局布线时被强制分配到专用时钟引脚。如果那个引脚恰好没接晶振就会报错。我一般习惯用clk_50m、rst_n这种带后缀的名字既清楚又不会和综合器的保留字冲突。另外状态机的状态编码建议用type state_type is (...)让综合器自动选择编码方式不要手动指定 one-hot 或 gray 码除非你明确知道目标器件的资源情况。从那以后我每次拿到一个 VHDL 工程都先确认三件事路径有没有中文、复位极性对不对、数码管是共阴还是共阳。这三件事查完基本能避开八成以上的翻车。希望帮到你。本文还有配套的精品资源点击获取