用Vivado在ego1开发板上“点亮”交通灯:从状态机建模到硬件验证的完整实战
你有没有试过,只靠几行Verilog代码,让FPGA板子上的LED像真实路口一样自动切换红绿黄?这听起来像是嵌入式高手才玩得转的事——但其实,只要你掌握了有限状态机(FSM)+ 计数定时 + 引脚映射这三个核心逻辑,就能亲手实现一个全自动交通灯系统。
本文基于Xilinx Vivado平台和Digilent ego1开发板,带你一步步完成这个经典数字系统设计项目。不讲空话,不堆术语,重点解决你在实际操作中会遇到的真问题:状态跳不准?计时对不上?LED反着亮?别急,我们一个一个来破。
为什么选交通灯作为FPGA入门项目?
在高校电子类课程中,“ego1开发板大作业vivado”几乎是每位初学者绕不开的一关。而交通灯控制系统之所以成为高频选题,是因为它完美融合了数字逻辑设计的四大关键能力:
- 状态控制:用有限状态机描述行为流程;
- 时间管理:通过计数器实现秒级延时;
- 并行输出:多路LED同步驱动;
- 硬件绑定:引脚约束与物理接口对接。
更重要的是,它的结果看得见、摸得着——绿灯变黄灯那一刻,你会真切感受到“我写的代码真的变成了硬件逻辑”。
核心架构一瞥:整个系统是怎么跑起来的?
先来看一张简化的系统框图,搞清楚信号流向:
[50MHz时钟] → [FPGA逻辑单元] ↓ [状态机控制器] ↙ ↘ [计数器] [LED输出逻辑] ↑ ↓ [时间使能] → [ego1板载LED阵列]所有逻辑运行在同一个50MHz主频下,没有额外的分频时钟。状态切换由内部计数器触发,LED输出直接由当前状态决定。整个过程纯硬件、全同步、零软件干预。
下面我们就拆解三大模块,逐个击破。
模块一:Moore型状态机设计——让系统“知道自己在哪”
交通灯的本质是一个周期性轮转的状态系统。我们以标准十字路口为例,设定四个基本状态:
| 状态 | 主干道 | 支路 |
|---|---|---|
| S_MAIN_GREEN | 绿灯 | 红灯 |
| S_MAIN_YELLOW | 黄灯 | 红灯 |
| S_SIDE_GREEN | 红灯 | 绿灯 |
| S_SIDE_YELLOW | 红灯 | 黄灯 |
注意:这里我们省略了全红过渡阶段,因为ego1大作业通常只要求基础循环;若需更高安全性,可自行加入短暂全红相位。
为什么选Moore型而不是Mealy?
简单说:输出更稳定。
- Moore型:输出仅取决于当前状态,不受输入瞬态干扰。
- Mealy型:输出依赖当前状态+输入,容易因毛刺导致误动作。
对于交通灯这种安全敏感场景,我们宁可多花一点资源,也要保证输出干净可靠。
状态编码方式怎么选?
常见有三种:二进制、格雷码、独热码(One-Hot)。在Artix-7这类查找表丰富的FPGA上,我们推荐使用独热码。
比如这样定义:
localparam S_MAIN_GREEN = 4'b1000, S_MAIN_YELLOW = 4'b0100, S_SIDE_GREEN = 4'b0010, S_SIDE_YELLOW = 4'b0001;虽然占用了4个寄存器表示4个状态(而二进制只需2位),但优势明显:
- 状态译码极简:每个状态对应一位,无需复杂组合逻辑;
- 切换速度快:路径短,利于时序收敛;
- 易于调试:仿真时一眼看出当前状态是哪一位被拉高。
💡 小贴士:Artix-7芯片寄存器资源充足,独热码带来的面积开销完全可以接受,换来的是更高的可读性和稳定性。
状态转移逻辑怎么写?
核心思想是:次态由当前状态和条件共同决定。
我们采用“两段式FSM”写法——一段负责状态更新(时序逻辑),一段负责次态判断(组合逻辑):
// 状态寄存器更新 always @(posedge clk or posedge rst) begin if (rst) current_state <= S_MAIN_GREEN; else current_state <= next_state; end // 次态生成逻辑 always @(*) begin case(current_state) S_MAIN_GREEN: next_state = (time_tick) ? S_MAIN_YELLOW : S_MAIN_GREEN; S_MAIN_YELLOW: next_state = (time_tick) ? S_SIDE_GREEN : S_MAIN_YELLOW; S_SIDE_GREEN: next_state = (time_tick) ? S_SIDE_YELLOW : S_SIDE_GREEN; S_SIDE_YELLOW: next_state = (time_tick) ? S_MAIN_GREEN : S_SIDE_YELLOW; default: next_state = S_MAIN_GREEN; endcase end其中time_tick是一个脉冲信号,表示“当前状态已持续足够长时间”,由计数器产生。
⚠️ 关键细节:一定要加
default分支!防止因未知状态卡死系统,这是工业级设计的基本素养。
模块二:不用分频,也能精准计时?揭秘“高频时钟+计数比较”技巧
很多新手第一反应是:“我要把50MHz分频成1Hz!”于是开始翻手册找PLL IP核……慢着!对于秒级定时任务,根本不需要这么复杂。
ego1开发板提供的是50MHz 差分时钟(经IBUFG接入),周期为20ns。如果我们用一个25位计数器,最大能计到 $2^{25} - 1 = 33,554,431$,对应时间就是:
$$
33,554,431 \times 20\text{ns} ≈ 0.671\text{s}
$$
等等,不到一秒?错了!
正确计算应为:
$$
1\text{秒} = 50,000,000 \text{ 个时钟周期}
\Rightarrow 需要至少 }26}\text{ 位计数器
$$
所以我们将计数器设为[25:0],共26位,足以覆盖60秒以内任意设定。
不生成新时钟,而是生成“时间使能信号”
这才是关键思路转变:
✅ 正确做法:保持全局单一时钟域,用计数达到阈值来产生一个单周期脉冲(time_tick),作为状态迁移的使能条件。
❌ 错误做法:生成低频时钟去驱动状态机——会导致多时钟域同步问题,增加STA难度。
具体实现如下:
reg [25:0] counter; wire time_tick; reg [25:0] compare_value; // 动态设置比较值 always @(*) begin case(current_state) S_MAIN_GREEN, S_SIDE_GREEN: compare_value = 26'd25_000_000; // 0.5s × 50MHz S_MAIN_YELLOW, S_SIDE_YELLOW: compare_value = 26'd5_000_000; // 0.1s × 50MHz default: compare_value = 26'd25_000_000; endcase end // 计数器逻辑 always @(posedge clk) begin if (rst) begin counter <= 0; end else if (current_state != next_state) begin counter <= 0; // 状态切换时清零 end else begin counter <= counter + 1; end end // 生成time_tick脉冲 assign time_tick = (counter == compare_value - 1);🔍 注意:我们在
counter == compare_value - 1时拉高time_tick,确保下一个周期刚好完成跳转。也可以在等于时拉高,但在组合逻辑中判断更安全。
这种方法的优势非常明显:
- 所有逻辑工作在同一时钟域,避免跨时钟域同步风险;
- 修改时间只需改参数,无需重新综合时钟网络;
- 资源消耗极低,连PLL都不用调用。
模块三:LED驱动与引脚绑定——让代码真正“亮起来”
再完美的逻辑,如果灯不亮,也算失败。而LED控制中最容易踩的坑,就是电平极性搞反了。
先确认硬件连接方式
ego1开发板上的LED是共阳极接法,即:
- 阳极接VCC(3.3V)
- 阴极通过限流电阻接到FPGA引脚
- FPGA输出低电平(0)时,LED两端形成压差 → 点亮
- 输出高电平(1)→ 截止 → 灭
也就是说:逻辑0亮,逻辑1灭。
如果你发现“应该绿灯亮却没反应”,很可能就是因为忘了取反。
不过我们在设计时可以先按“高电平有效”来写逻辑,最后统一加一层反相输出即可。
输出逻辑怎么写最清晰?
建议使用连续赋值语句(assign),简洁直观:
// 高电平有效逻辑(便于理解) assign main_green = (current_state == S_MAIN_GREEN); assign main_yellow = (current_state == S_MAIN_YELLOW); assign main_red = (current_state == S_SIDE_GREEN || current_state == S_SIDE_YELLOW); assign side_green = (current_state == S_SIDE_GREEN); assign side_yellow = (current_state == S_SIDE_YELLOW); assign side_red = (current_state == S_MAIN_GREEN || current_state == S_MAIN_YELLOW); // 最终输出到管脚时取反(适配共阳极) assign LD0 = ~main_red; // 假设LD0接主路红灯 assign LD1 = ~main_yellow; assign LD2 = ~main_green; assign LD3 = ~side_red; assign LD4 = ~side_yellow; assign LD5 = ~side_green;这样做的好处是:逻辑层与物理层分离,便于后期更换引脚或修改极性。
引脚约束文件(XDC)怎么写?
这是从仿真走向硬件的关键一步。必须在.xdc文件中明确指定每个信号对应的FPGA引脚编号。
根据Digilent官方文档,ego1的用户LED连接如下:
| LED | FPGA Pin | Signal |
|---|---|---|
| LD0 | U16 | main_red_led |
| LD1 | V16 | main_yellow_led |
| LD2 | W16 | main_green_led |
| LD3 | W17 | side_red_led |
| LD4 | V17 | side_yellow_led |
| LD5 | U17 | side_green_led |
对应的XDC约束:
set_property PACKAGE_PIN U16 [get_ports main_red_led] set_property IOSTANDARD LVCMOS33 [get_ports main_red_led] set_property PACKAGE_PIN V16 [get_ports main_yellow_led] set_property IOSTANDARD LVCMOS33 [get_ports main_yellow_led] set_property PACKAGE_PIN W16 [get_ports main_green_led] set_property IOSTANDARD LVCMOS33 [get_ports main_green_led] set_property PACKAGE_PIN W17 [get_ports side_red_led] set_property IOSTANDARD LVCMOS33 [get_ports side_red_led] set_property PACKAGE_PIN V17 [get_ports side_yellow_led] set_property IOSTANDARD LVCMOS33 [get_ports side_yellow_led] set_property PACKAGE_PIN U17 [get_ports side_green_led] set_property IOSTANDARD LVCMOS33 [get_ports side_green_led]✅ 提醒:不要忘记设置IO标准为LVCMOS33(3.3V CMOS),否则可能烧毁电路!
实战避坑指南:那些仿真没问题、下载后出错的“神坑”
❌ 坑点1:计数器不清零,导致第一次绿灯特别短
现象:上电后主绿灯只亮了一瞬间就跳黄灯。
原因:状态刚切换时,计数器没有及时清零,继续从上次残留值开始累加。
✅ 解决方案:在计数器逻辑中加入状态变化检测:
if (rst) begin counter <= 0; end else if (current_state != next_state) begin counter <= 0; end else begin counter <= counter + 1; end❌ 坑点2:复位信号太短,状态机没初始化到位
ego1开发板的复位按钮是机械按键,弹跳严重。如果只用边沿检测,可能导致复位无效。
✅ 推荐做法:添加简单的同步去抖逻辑,或者延长复位时间(如用计数器延时1ms再释放)。
❌ 坑点3:仿真波形正常,但板子上灯乱闪
检查是否漏了XDC约束!如果没有锁定引脚,Vivado会随机分配,可能导致多个信号挤在一个引脚上,造成冲突。
✅ 对策:每次实现前检查Report DRC,确保无未约束端口。
总结与延伸:这不仅仅是个大作业
当你看到LD2(主绿)亮起30秒后平稳过渡到LD1(黄),再切换到支路通行时,你会意识到:这不是简单的LED闪烁实验,而是一个真正的自主运行的数字系统。
这套设计方法论完全可以扩展到更复杂的场景:
- 加入左转专用车道 → 增加两个状态
- 接入按键模拟紧急车辆请求 → 添加中断优先级处理
- 连接七段数码管显示倒计时 → 引入BCD转换和动态扫描
- 使用传感器检测车流量 → 实现自适应调度算法
更重要的是,你已经走完了完整的FPGA开发流程:
编写代码 → 行为仿真 → 综合实现 → 引脚约束 → 下载验证
每一步都贴近真实工程项目的要求。下次面对“智能停车场”“电梯控制”之类的题目时,你会发现,底层逻辑其实都是一样的:状态 + 时间 + 输出。
如果你正在做“ego1开发板大作业vivado”,希望这篇文章能帮你少走弯路;如果你已经做完,不妨试试加入倒计时显示或夜间黄灯闪烁模式,把它变成真正属于你的作品。
有什么问题或优化想法?欢迎留言交流!