基于Verilog与Quartus的智能交通灯模块化设计与仿真实现
1. 项目概述与设计思路
大家好,今天我想和大家分享一个非常实用的FPGA项目——基于Verilog和Quartus的智能交通灯系统设计。这个项目特别适合刚接触数字逻辑设计的初学者,也适合有一定经验的开发者想要深化模块化设计理念的同行。我自己在实际教学中多次使用这个案例,学生反馈都说非常直观,能够快速理解状态机和模块化设计的精髓。
智能交通灯系统看似简单,但包含了数字系统设计的多个核心概念:状态机控制、时钟分频、数码管显示驱动,以及模块间的协同工作。我们设计的系统包含两个路口(主干道和支干道),每个路口有红黄绿三种信号灯,按照特定的时序循环切换。主干道绿灯50秒、黄灯10秒,支干道绿灯50秒、黄灯10秒,同时确保任何时候只有一个方向是通行状态(绿灯或黄灯),另一个方向必须是红灯。
为什么要采用模块化设计呢?在实际工程中,复杂的系统往往由多个功能模块组合而成。模块化设计不仅让代码更易维护和调试,还能提高代码的复用性。比如这个项目中,我们可以将系统划分为分频模块、状态控制模块、显示驱动模块等,每个模块独立设计和仿真,最后再整合到一起。这样即使某个模块需要修改,也不会影响其他模块的功能。
2. 开发环境与工具准备
做FPGA开发,首先得准备好工具链。我们用的是Intel的Quartus Prime开发环境,这是目前最主流的FPGA开发软件之一,支持从设计输入、综合、布局布线到烧录的全流程。Verilog作为硬件描述语言,是我们实现逻辑功能的主要工具。如果你之前接触过C语言,会发现Verilog的语法有很多相似之处,但要注意的是Verilog描述的是硬件电路,而不是软件程序。
Quartus的安装比较简单,直接从Intel官网下载Lite版本就够用了,毕竟我们的设计规模不大。安装完成后,建议先熟悉一下Quartus的界面:Project Navigator用于管理工程文件,Text Editor用于编写Verilog代码,Tool菜单下的Netlist Viewer和RTL Viewer可以查看综合后的电路结构,Simulation Waveform Editor则用于仿真验证。
除了Quartus,仿真工具也非常重要。虽然Quartus自带了仿真功能,但我个人更喜欢使用ModelSim,因为它更专业,波形查看和调试功能更强大。不过对于初学者,Quartus自带的仿真工具也完全够用。关键是要学会如何编写测试脚本(Testbench),以及如何分析仿真波形来验证设计是否正确。
3. 系统架构与模块划分
我们的智能交通灯系统采用自上而下的设计方法。顶层模块负责将所有子模块连接起来,而每个子模块实现特定的功能。系统主要分为三个核心模块:分频模块(将50MHz系统时钟分频得到1Hz时钟)、状态控制模块(负责交通灯的状态转换和时序控制)、显示驱动模块(驱动数码管显示倒计时)。
这种模块划分不仅符合硬件设计的思想,也便于团队协作。比如一个人负责分频模块,另一个人负责状态机,最后再整合。每个模块都可以独立仿真,确保功能正确后再进行联调。在实际项目中,这种方法是提高开发效率的关键。
顶层模块的接口定义也很重要。我们需要明确每个输入输出信号:系统时钟(clk_50MHz)、复位信号(rst),以及输出到LED灯和数码管的信号。输出信号包括主干道和支干道的红黄绿LED控制信号,以及两个数码管的段选和位选信号。这些信号必须与开发板的物理引脚对应,否则无法正确烧录和运行。
4. 分频模块设计与仿真
分频模块是数字系统中最基础的模块之一。我们的开发板提供的系统时钟通常是50MHz,但交通灯的状态切换需要以秒为单位,所以我们必须将50MHz时钟分频成1Hz时钟。1Hz意味着每秒一个脉冲,这样每个脉冲代表一秒,用于倒计时和状态转换。
Verilog实现分频器有多种方法,最常用的是计数器-based分频。50MHz时钟的周期是20ns,要得到1Hz时钟,需要计数50,000,000次。但直接计数这么大数值会占用大量资源,所以通常采用分级分频:先分频到较低频率(比如1kHz),再分频到1Hz。不过为了简化,我们这里直接使用一个计数器实现50,000,000次分频。
分频模块的代码如下:
module clk_div(
input clk_50MHz,
input rst,
output reg clk_1Hz
);
reg [25:0] count; // 26位计数器,最大计数值50,000,000
always @(posedge clk_50MHz or posedge rst) begin
if (rst) begin
count <= 0;
clk_1Hz <= 0;
end else if (count == 26'd50_000_000) begin
count <= 0;
clk_1Hz <= ~clk_1Hz;
end else begin
count <= count + 1;
end
end
endmodule
这段代码通过一个26位计数器实现分频。每当计数器达到50,000,000时,clk_1Hz信号取反,从而产生1Hz时钟。复位信号rst用于初始化和强制清零。
仿真分频模块时,我们需要编写Testbench生成50MHz时钟和复位信号,然后观察clk_1Hz的输出波形。由于计数值很大,仿真时可以通过减少分频系数来加速仿真。比如将50,000_000改为50,这样仿真很快就能看到效果。仿真波形应该显示clk_1Hz的周期是1秒(或按比例缩小后的值),且与系统时钟同步。
5. 状态控制模块实现
状态控制模块是整个系统的核心,它决定了交通灯的工作逻辑。我们采用有限状态机(FSM)来实现,这是数字系统设计中最常用的控制单元设计方法。状态机有四个状态:S0(主干道绿灯、支干道红灯)、S1(主干道黄灯、支干道红灯)、S2(主干道红灯、支干道绿灯)、S3(主干道红灯、支干道黄灯)。
每个状态的持续时间是固定的:S0状态50秒(主干道绿灯),S1状态10秒(主干道黄灯),S2状态50秒(支干道绿灯),S3状态10秒(支干道黄灯)。状态转换由1Hz时钟驱动,每秒检查一次当前状态的计时,时间到则切换到下一个状态。
状态机的Verilog实现如下:
module state_control(
input clk_1Hz,
input rst,
output reg [2:0] main_LEDs, // 主干道LEDs: {red, green, yellow}
output reg [2:0] branch_LEDs, // 支干道LEDs: {red, green, yellow}
output reg [7:0] main_time, // 主干道倒计时
output reg [7:0] branch_time // 支干道倒计时
);
parameter S0 = 2'd0, S1 = 2'd1, S2 = 2'd2, S3 = 2'd3;
reg [1:0] state;
reg [7:0] counter;
always @(posedge clk_1Hz or posedge rst) begin
if (rst) begin
state <= S0;
counter <= 0;
end else begin
case (state)
S0: if (counter < 50) counter <= counter + 1;
else begin state <= S1; counter <= 0; end
S1: if (counter < 10) counter <= counter + 1;
else begin state <= S2; counter <= 0; end
S2: if (counter < 50) counter <= counter + 1;
else begin state <= S3; counter <= 0; end
S3: if (counter < 10) counter <= counter + 1;
else begin state <= S0; counter <= 0; end
endcase
end
end
// 根据状态输出LED控制信号
always @(*) begin
main_LEDs = 3'b000;
branch_LEDs = 3'b000;
case (state)
S0: begin main_LEDs = 3'b010; branch_LEDs = 3'b100; end
S1: begin main_LEDs = 3'b001; branch_LEDs = 3'b100; end
S2: begin main_LEDs = 3'b100; branch_LEDs = 3'b010; end
S3: begin main_LEDs = 3'b100; branch_LEDs = 3'b001; end
endcase
end
// 倒计时输出
always @(*) begin
case (state)
S0: begin main_time = 50 - counter; branch_time = 60; end
S1: begin main_time = 10 - counter; branch_time = 60 - counter; end
S2: begin main_time = 60; branch_time = 50 - counter; end
S3: begin main_time = 60 - counter; branch_time = 10 - counter; end
endcase
end
endmodule
这段代码定义了四个状态,并通过一个计数器控制状态持续时间。每个状态结束时,计数器清零并切换到下一个状态。组合逻辑部分根据当前状态输出LED控制信号和倒计时数值。
6. 数码管显示驱动设计
数码管显示是系统的重要组成部分,它让用户能够直观地看到倒计时时间。我们用的数码管通常是七段数码管,通过段选信号(控制哪些段亮)和位选信号(控制哪个数码管亮)来显示数字。
数码管显示模块需要完成两个任务:一是将倒计时数值转换为七段数码管的段选信号,二是通过动态扫描方式交替显示两个数码管(主干道和支干道)。动态扫描利用人眼的视觉暂留效应,快速切换位选信号,看起来像是同时显示。
段选译码器可以用查找表实现,将4位二进制数转换为7位段选信号(假设是共阳极数码管)。例如,数字0的段选信号是7'b1000000(对应a段亮,其他段灭),数字1是7'b1111001,等等。
显示驱动模块的代码如下:
module seg_driver(
input clk_50MHz,
input [7:0] main_time,
input [7:0] branch_time,
output reg [7:0] seg_data,
output reg [1:0] seg_sel
);
reg [15:0] counter;
reg [3:0] digit;
always @(posedge clk_50MHz) begin
counter <= counter + 1;
if (counter == 16'd50000) begin
counter <= 0;
seg_sel <= seg_sel + 1;
end
end
always @(*) begin
case (seg_sel)
0: digit = main_time / 10; // 主干道十位数
1: digit = main_time % 10; // 主干道个位数
2: digit = branch_time / 10; // 支干道十位数
3: digit = branch_time % 10; // 支干道个位数
endcase
end
always @(*) begin
case (digit)
0: seg_data = 8'b11000000;
1: seg_data = 8'b11111001;
2: seg_data = 8'b10100100;
3: seg_data = 8'b10110000;
4: seg_data = 8'b10011001;
5: seg_data = 8'b10010010;
6: seg_data = 8'b10000010;
7: seg_data = 8'b11111000;
8: seg_data = 8'b10000000;
9: seg_data = 8'b10010000;
default: seg_data = 8'b11111111;
endcase
end
endmodule
这段代码通过一个计数器控制位选信号的切换频率,约每1ms切换一次数码管。段选信号根据当前显示的数码管和数值生成。注意这里的段选信号是针对共阳极数码管,如果你的开发板是共阴极,需要取反段选信号。
7. 顶层模块集成与引脚分配
顶层模块的作用是将所有子模块实例化并连接起来。它相当于系统的总接线图,定义了模块之间的互连关系以及对外的输入输出接口。顶层模块通常不包含复杂的逻辑,主要是端口映射和线网声明。
我们的顶层模块需要实例化分频模块、状态控制模块和显示驱动模块,并将它们正确连接。分频模块输出的clk_1Hz连接到状态控制模块的时钟输入,状态控制模块输出的倒计时数值连接到显示驱动模块,显示驱动模块的输出直接连接到FPGA的引脚,驱动LED和数码管。
顶层模块的代码如下:
module traffic_light_top(
input clk_50MHz,
input rst,
output [2:0] main_LEDs,
output [2:0] branch_LEDs,
output [7:0] seg_data,
output [1:0] seg_sel
);
wire clk_1Hz;
wire [7:0] main_time, branch_time;
clk_div u_clk_div(
.clk_50MHz(clk_50MHz),
.rst(rst),
.clk_1Hz(clk_1Hz)
);
state_control u_state_control(
.clk_1Hz(clk_1Hz),
.rst(rst),
.main_LEDs(main_LEDs),
.branch_LEDs(branch_LEDs),
.main_time(main_time),
.branch_time(branch_time)
);
seg_driver u_seg_driver(
.clk_50MHz(clk_50MHz),
.main_time(main_time),
.branch_time(branch_time),
.seg_data(seg_data),
.seg_sel(seg_sel)
);
endmodule
引脚分配是FPGA设计的关键一步,必须根据开发板的原理图将Verilog中的信号映射到正确的物理引脚。例如,系统时钟clk_50MHz可能连接到特定的时钟引脚,LED信号连接到GPIO引脚,数码管段选和位选信号也连接到对应的引脚。在Quartus中,可以通过Assignment Editor或者QSF文件进行引脚分配。
8. 系统仿真与调试技巧
仿真验证是确保设计正确的关键步骤。我们需要编写Testbench来模拟实际输入,并观察输出波形是否符合预期。Testbench通常包括时钟生成、复位控制、输入激励生成,以及输出监测。
对于交通灯系统,我们需要验证几个关键点:状态转换是否正确、倒计时数值是否正确、LED输出是否符合预期、数码管显示是否正确。由于系统包含多个模块,建议先对每个模块单独仿真,再对整个系统进行集成仿真。
分频模块的Testbench示例:
module tb_clk_div;
reg clk_50MHz, rst;
wire clk_1Hz;
clk_div uut(.clk_50MHz(clk_50MHz), .rst(rst), .clk_1Hz(clk_1Hz));
initial begin
clk_50MHz = 0;
forever #10 clk_50MHz = ~clk_50MHz;
end
initial begin
rst = 1;
#100 rst = 0;
#200000000 $finish;
end
endmodule
这个Testbench生成50MHz时钟和复位信号,并观察分频后的1Hz时钟。仿真时需要运行足够长的时间才能看到1Hz时钟的变化,因为50MHz到1Hz的分频比很大。为了加速仿真,可以暂时减小分频系数。
状态控制模块的仿真更复杂,需要验证状态转换和时序。我们可以通过波形查看器观察state信号的变化,以及main_LEDs和branch_LEDs的输出。特别要注意状态切换的边界条件,比如计数器达到阈值时是否立即切换状态。
调试技巧方面,我强烈建议使用Quartus的Signal Tap逻辑分析仪。它可以在FPGA运行时实时捕捉内部信号的变化,非常有助于排查硬件问题。如果仿真正确但实际运行不正常,很可能是引脚分配错误、时钟约束问题或者物理连接问题。
9. 实际部署与优化建议
设计通过仿真验证后,就可以编译并烧录到FPGA开发板上了。在Quartus中,编译过程包括综合、布局布线、生成编程文件等步骤。编译成功后,通过Programmer工具将sof文件烧录到FPGA中。
实际部署时可能会遇到一些仿真时没有发现的问题。比如数码管显示闪烁、LED亮度不均、按钮抖动等。对于数码管显示,如果切换频率太低,会出现闪烁;太高则可能导致亮度不足。一般1kHz左右的刷新频率比较合适。
按钮抖动是常见问题,机械按钮在按下和释放时会产生多个脉冲。可以通过添加消抖模块来解决,通常采用采样滤波或者计数器方式。例如,连续检测到多次相同状态才认为按钮状态稳定。
系统还可以进一步优化。比如添加紧急模式(所有方向红灯)、夜间模式(黄灯闪烁)、倒计时调整功能等。这些功能可以通过添加输入按钮和扩展状态机来实现。另外,可以考虑使用参数化设计,让红绿灯时间更容易调整。
代码优化方面,可以尝试使用二进制编码代替一位热码编码状态机,以节省资源。对于分频模块,可以考虑使用PLL代替计数器分频,提高时钟质量。显示驱动模块可以添加亮度调节功能,通过PWM控制数码管亮度。
最后,记得编写详细的文档和注释。好的注释不仅方便自己后期维护,也方便他人理解你的设计。特别是状态机的状态定义和模块接口说明,一定要清晰明了。
更多推荐
所有评论(0)