简介:这套基于Cyclone II EP2C35F672C6的FPGA交通灯设计资源,面向FPGA入门与数字逻辑课程设计人群,完整呈现十字路口四组信号灯的状态转换逻辑,并通过四组数码管实时显示红绿黄灯倒计时,覆盖了从状态机设计到引脚约束的典型流程。压缩包共120个文件,约1.25MB,以Quartus工程文件(qpf/qsf)、VHDL源文件(vhd)、编译与仿真报告(rpt/cdb/qmsg)为主,同时包含doc/docx说明文档和png示意图,便于对照学习。解压后目录划分清晰:traaffic为可运行工程,程序文件夹保存了编写过程中的成功与修改版本,错误文件夹则整理了常见编译报错记录,为排错提供了直接参考。已有420人学习下载,对正在完成FPGA课设或想理解交通灯状态机实现的同学,这份资料不仅提供可直接参考的工程框架,还包含完整的调试过程,能帮助规避同类问题。 做了这么多年FPGA开发,要说练手最经典的项目,交通灯控制绝对排得上号。正好最近帮一个学生调了一块Cyclone II的老开发板,型号就是EP2C35F672C6,把交通灯程序完整跑通了。这块芯片是Altera当年Cyclone II系列里比较经典的料,逻辑单元够用、价格便宜,用来做状态机控制类的实验项目非常合适。这篇就把整个项目的设计思路、代码实现、硬件接线和调试踩坑完整记录下来,给正在用这块板子或者准备拿FPGA做控制类项目的朋友做个参考。
1. 方案设计与整体思路拆解
1.1 为什么用FPGA做交通灯,而不是单片机
很多人会问,一个交通灯逻辑那么简单,用STM32或者51单片机几行代码就搞定了,为什么要用FPGA来做?这个问题如果面试的时候被问到,标准答案是“锻炼数字逻辑设计能力”,但实际做项目,FPGA的优势在于并行处理和多路时序控制。
交通灯系统看起来简单,实际上涉及到多个方向的红绿灯协同工作,每个方向的灯色切换时间都不同,再加上倒计时数码管显示、夜间模式切换、紧急车辆优先通行等扩展功能,如果用单片机做,靠定时器中断一个个去判断,代码会越写越复杂,后期调试也比较痛苦。FPGA用状态机的方式实现,逻辑清晰,所有状态的跳转都在同一个时钟节拍下完成,时序天然同步,不会出现单片机那种“这个中断没执行完、另一个中断又来了”的竞争问题。
另外,EP2C35F672C6这颗芯片的资源对一个交通灯项目来说可以说杀鸡用牛刀,但也正因为资源绰绰有余,可以在基础功能之上随意扩展——加多个路口联动、加传感器输入、加串口调试接口,都不用担心逻辑单元不够用。对于学习来说,这种“留有余量”的设计环境反而更适合折腾。
1.2 交通灯时序分析与状态机建模
交通灯的核心本质是“按时间序列循环切换状态”。以一个标准十字路口为例,两个方向(东西方向和南北方向)的灯色必须互斥,即东西方向绿灯时,南北方向必须是红灯,中间还需要黄灯过渡,避免双向同时通行造成事故。
一个完整的交通周期可以这样设计:南北方向绿灯30秒、黄灯3秒、红灯33秒;对应东西方向红灯33秒、绿灯30秒、黄灯3秒。这个时间如果拆解成状态机,一共是4个主状态:南北直行、南北左转、东西直行、东西左转(如果简化版本,可以只用南北通行和东西通行两个大状态加黄灯)。
我实际写代码时用的是五状态结构:S0南北绿灯、S1南北黄灯、S2东西绿灯、S3东西黄灯、S4紧急状态(可选)。每个状态内部用一个计数器计时,计时结束后跳转到下一个状态。这里有一个细节值得注意:计数器是每个状态单独设一个,还是全局共用一个长计数器?我建议每个状态单独设计数器,代码可读性更好,而且后续如果要改某个方向的通行时间,不需要动全局逻辑,只改一个状态里的参数就行。
// 参数定义,单位:秒 localparam S_NORTH_SOUTH_GO = 30; // 南北直行时间 localparam S_NORTH_SOUTH_LEFT = 15; // 南北左转时间 localparam S_YELLOW = 3; // 黄灯时间 localparam S_EAST_WEST_GO = 30; // 东西直行时间2. 核心模块设计与代码实现
2.1 分频模块:从50MHz到1Hz的计数逻辑
EP2C35F672C6开发板上通常配的是50MHz有源晶振,也就是每秒钟振荡5000万次。要让计数器实现“1秒”的精度,就需要把50MHz分频成1Hz。这里最容易犯的错误是计数器初值算错。
50MHz分频到1Hz,计数周期是50,000,000次。由于计数器从0开始计数,计数到49,999,999时刚好是一个完整周期,然后清零重新计数。如果你拿50,000,000直接作为比较值,会导致实际周期变成50,000,001个时钟周期,虽然误差在纳秒级别肉眼看不出来,但对于追求严谨的项目来说,边界条件必须算清楚。
module clk_div_1hz( input clk_50m, input rst_n, output reg clk_1hz ); reg [25:0] cnt; always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt <= 26'd0; clk_1hz <= 1'b0; end else if (cnt == 26'd49_999_999) begin cnt <= 26'd0; clk_1hz <= ~clk_1hz; end else begin cnt <= cnt + 1'b1; end end endmodule这里注意计数器位宽至少需要26位,因为50,000,000的二进制表示需要26位(2^25=33,554,432不够,2^26=67,108,864够)。如果用24位或者25位计数器,综合的时候Quartus不会报错,但仿真和上板时计数器会溢出,表现出来的现象就是倒计时跳变。这个问题在课堂上经常遇到,属于典型的“位宽不够”错误。
2.2 状态机模块:三段式写法与状态跳转
状态机写法我推荐三段式:第一段是状态寄存器时序逻辑,第二段是次态组合逻辑,第三段是输出逻辑。这种写法的好处是结构清晰,组合逻辑和时序逻辑分离,后期维护方便,也不容易出竞争冒险。
交通灯的状态转移非常直观,拿南北方向举例:通行时间到了就切黄灯,黄灯时间到了就切红灯。真正决定整个系统是否正确的,是状态跳转条件必须“只在一个时钟沿生效一次”。这里我踩过一个坑:如果把计数器的归零信号直接作为状态跳转条件,而不做边沿检测,可能会出现一个状态连续跳两拍的情况。解决办法是让计数器计数到目标值时拉一个单时钟周期脉冲,状态机在检测到这个脉冲的上升沿时才跳转。
// 状态跳转示例 always @(posedge clk_1hz or negedge rst_n) begin if (!rst_n) current_state <= S0; else if (cnt_enable) begin case (current_state) S0: if (cnt_done) current_state <= S1; S1: if (cnt_done) current_state <= S2; S2: if (cnt_done) current_state <= S3; S3: if (cnt_done) current_state <= S0; default: current_state <= S0; endcase end end计数器模块在每个状态的入口处加载对应的比较值。比如进入S0(南北绿灯)时,计数器目标值是29(0-29共30秒),每来一个1Hz脉冲就加一,加到29时拉高cnt_done信号,状态机跳走,计数器清零。
2.3 数码管显示:BCD码与动态扫描
EP2C35F672C6开发板通常带有4位或6位数码管。倒计时显示需要把3位的秒数拆成个位和十位,转成BCD码后送段码表译码。这里有一个取舍问题:是用查表法还是用计算法?
查表法最直接:定义一个长度为10的数组,索引0-9对应数码管a-g七个段的编码(共阴极还是共阳极查一下板子原理图)。比如共阴极数码管,数字0对应7'b1000000(a段亮),具体编码根据不同板子的丝印略有不同,最好以实际点亮效果为准。
动态扫描是数码管显示的标准做法。由于人眼有视觉暂留效应,只要扫描频率高于60Hz,看起来所有位就是同时亮的。实现方式是一个2-3ms的定时器,轮询选通每一位。实际调试中我发现扫描频率太低会闪烁,太高则有轻微鬼影,一般1ms切换一位、四位数码管4ms一轮是比较舒服的节奏。
3. 硬件连接与引脚约束
3.1 EP2C35F672C6引脚分配原则
FPGA开发不同于单片机,代码写完后不能直接下载,还必须做引脚约束,告诉Quartus每个逻辑信号对应芯片的哪个物理引脚。如果约束错了或者漏了约束,综合能过,但布局布线后下载到板子上没有任何反应。
引脚分配要参考开发板的原理图。以常见的EP2C35开发板为例,时钟引脚通常在PIN_N2或PIN_E1(不同板子不同),按键在PIN_R22或PIN_T22,LED在PIN_AA1到PIN_AE12这样的区域。分配引脚时有个原则必须记住:时钟信号必须连到芯片的专用时钟引脚,否则PLL无法工作,时序约束也做不进去。全局复位、使能这类信号虽然没有硬性要求,但最好也分配到全局网络引脚上,减少布线延迟。
Quartus里配置引脚的方法有两种:一种是通过Assignment Editor图形界面手动输入,另一种是写SDC或QSF文件。我习惯直接编辑QSF文件,批量填写引脚号,效率更高,也方便版本管理。
set_location_assignment PIN_N2 -to clk_50m set_location_assignment PIN_R22 -to rst_n set_location_assignment PIN_AA1 -to led_north_green set_location_assignment PIN_AA2 -to led_north_yellow set_location_assignment PIN_AA3 -to led_north_red set_location_assignment PIN_AB1 -to led_east_green3.2 LED灯与数码管的板级连接
交通灯状态显示用LED最直观。每个方向红、黄、绿三个LED,两个方向共6个LED。EP2C35开发板上的LED大多是低电平点亮(共阳极接法),也就是FPGA引脚输出0时LED亮,输出1时熄灭。如果代码里写“led_green = 1表示绿灯亮”,而实际电路是低电平有效,现象就是绿灯永远不亮,反而是不亮的时候逻辑判断为亮。
这种高低电平反了的问题,在仿真里完全看不出来,只有在板子上跑才会暴露。我的经验是写代码前先看原理图,确认LED是灌电流还是拉电流方式,再决定输出极性。如果原理图暂时不确定,可以在代码里做一个极性切换的parameter,比如parameter LED_ON = 1'b0;,万一板上现象反了,只改一个参数就行,不用动逻辑。
数码管的连接就复杂一些,一般分位选和段选两组信号。位选信号决定哪一位数码管点亮,段选信号决定该位显示什么数字。EP2C35的IO口资源丰富,可以直接用GPIO连接,但如果数码管较多,可以考虑加74HC595串转并芯片,节省IO资源。
3.3 完整上板调试流程记录
把代码下载到EP2C35F672C6的过程和我预想的差不多:先用Quartus编译,看有没有时序错误或警告。第一次编译时果然报了几个警告,主要是复位信号存在竞争条件和时钟分频器使用了门控时钟。门控时钟在FPGA设计中是大忌,因为它会导致时钟树不均衡,时序难以收敛。我改成了时钟使能的方式,用clk_1hz的上升沿作为所有时序逻辑的驱动,把分频器的输出只作为使能信号,而不是时钟信号。
下载用的是JTAG方式,Quartus的Programmer直接识别USB-Blaster,选对.sof文件后点Start,芯片配置完成后板子上的交通灯状态就按预期跑起来了。南北方向先亮绿灯,30秒后切黄灯,黄灯闪烁3秒后切红灯,东西方向同步切换,数码管倒计时显示正确。第一次全流程跑通的瞬间,有一种“代码里设计的东西在物理世界动起来”的感觉,这是单片机开发很难替代的体验。
4. 常见问题与排错实录
4.1 分频后时钟不同步导致状态跳转异常
这个问题出现的频率非常高。很多初次接触FPGA交通灯的同学会写成“在clk_50m的时钟域里再做一个计数器,计数满后直接把计数值当作1Hz信号去驱动状态机”。表面看没问题,但综合后这个1Hz信号是组合逻辑产生的,不是真正的时钟,Quartus会提示“PLL not used”或“clock not balanced”。
后果就是状态机有时候跳转正常,有时候跳两次或卡死,完全看运气。我在实际项目中遇到过一次,倒计时到29秒时状态机直接跳到了红灯,跳过了黄灯。排查过程花了不少时间,最后定位到是分频计数器的进位信号被同时用作了时钟源和状态跳转条件,产生了一个极窄的毛刺,触发了状态寄存器的额外翻转。
解决方案就是前面提到的:通过时钟使能方式处理。所有时序逻辑始终由主时钟驱动,分频模块只负责产生“每秒使能一次”的信号,状态机在使能信号有效时判断计数器是否到达目标值。这样重新综合后,时序完全正常,现象也稳定了。
4.2 数码管显示乱码与闪烁的两种原因
数码管显示乱码,大概率是段码表定义错了。共阴极和共阳极的段码是反的,同一个数字0,共阴极是7'b1000000,共阳极是7'b0111111,如果搞混了,数字会错乱甚至显示空白。
另一种情况是位选和段选信号同步问题。动态扫描时,如果先切换位选信号再给段选数据,会短暂出现上一个位的数据显示在当前位上的鬼影现象。解决办法是先给段选赋值,再切换位选,让数据稳定一个极短时间后再换位。这句代码顺序看起来微不足道,但在实际显示效果上影响很大,尤其是数码管位数多的时候。
闪烁问题则基本是扫描频率太低。把扫描周期从10ms改到2ms后,闪烁立刻消失。这里可以不用改代码逻辑,只要调整分频参数就行。
4.3 按键控制的亚稳态与消抖处理
如果给交通灯加夜间模式或者紧急优先功能,按键输入处理就是一个绕不开的话题。FPGA里按键信号直接接进时序逻辑会引入亚稳态问题,因为按键按下瞬间的电平变化是随机的,不满足触发器的建立保持时间要求,采到的值可能既不是0也不是1,而是不确定状态。这个状态如果刚好被当成有效触发,整个状态机就乱了。
我在这块板上用的方案是两级同步器加计数消抖:先把按键信号打两拍,消除亚稳态,再用一个10ms左右的计数器判断电平是否稳定。只有连续检测到同一电平超过10ms才认为按键有效,有效后拉一个单时钟脉冲作为状态机输入。这个方法在EP2C35上实测非常可靠,按下、松开都不会产生误触发。
// 两级同步器 always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin key_sync1 <= 1'b1; key_sync2 <= 1'b1; end else begin key_sync1 <= key_in; key_sync2 <= key_sync1; end end4.4 Quartus版本兼容性与工程配置建议
EP2C35F672C6是Cyclone II系列的老芯片,新版本的Quartus从18.0之后就不再支持Cyclone II系列器件了。如果电脑上装的是最新版Quartus,打开工程时可能直接找不到这块芯片,需要安装Quartus 13.0sp1或更早版本。安装时注意选择Cyclone II器件的支持包,默认安装不一定包含。
工程建立时还有一个细节容易被忽视:顶层实体名称不能与文件名冲突,也不能用Verilog关键字。我之前有一次把顶层模块命名成traf,结果下载后FPGA内部逻辑没有正确加载,折腾了很久才发现是工程设置里Eda Tool Settings的仿真工具没配置对,导致综合后的网表有问题。这种问题一旦遇到,检查顺序应该是:编译报告有没有严重警告,引脚约束是否生效,时序约束文件是否加载,最后再怀疑代码本身。
5. 从基础到扩展:这个项目还能怎么玩
交通灯程序的完成并不意味着这个项目的终结。我的经验是,先用基础版本跑通全流程,建立信心之后再逐步加功能,每一步都是新的挑战,但都在可控范围内。
一个比较推荐的扩展方向是加入紧急车辆优先通行功能。用两个按键模拟传感器输入,一个代表南北方向有紧急车辆,一个代表东西方向有紧急车辆。当检测到请求信号时,状态机可以立即切到黄灯,黄灯结束后进入请求方向的绿灯,让紧急车辆通过,同时拉高一个“紧急状态”的输出信号,可以在板上点亮一个独立LED用于指示。这个功能本身不难,但要把优先级、恢复机制、请求清除逻辑处理好,非常锻炼状态机的能力。
另一个有意思的扩展是夜间模式。当检测到夜间开关按下时,交通灯不执行正常的红绿灯时序,而是黄灯闪烁——两个方向的黄灯以1秒为周期交替闪烁,提醒车辆经过路口时注意观察。这个模式的实现只需要在状态机外再套一层模式选择逻辑,如果当前是夜间模式,输出逻辑直接输出闪烁波形,不走状态机的主流程。
串口调试也是值得加的功能。通过UART把当前状态、计数器值、模式编号等调试信息发送到上位机,调试效率能翻倍。EP2C35F672C6上实现一个简单的UART发送模块也就几十行代码,波特率115200,一个启动位八个数据位一个停止位,配合实现上板后不用再靠肉眼看LED猜状态了,直接看串口助手的打印信息,省下大量时间。
我在实际使用中发现,做完基础交通灯之后,如果能把上述三个扩展功能全部完成,你对FPGA状态机设计、时序控制、模块化开发这些核心能力的掌握会提升一个档次。这个项目的意义不在于交通灯本身,而在于通过一个足够简单但又足够完整的案例,把数字系统设计的思维框架彻底打通。后面再接触以太网、图像处理、高速接口这些复杂方向,会发现底层的设计理念都是相通的。
本文还有配套的精品资源,点击获取