1、项目介绍
本项目设计并实现了一套智能交通灯控制系统,结合WebIDE图形化编程与Verilog硬件描述语言,在STEP Baseboard4.0开发板与MXO2 LPC核心板上完成部署。在FPGA内部实现包含了74系列常规数字集成电路和其他个人设计电路,集交通灯控制、环境感知与人机交互功能于一体,适用于城市道路交叉口的智能化管理场景。
系统核心功能包含四部分:首先,通过图形化编程构建交通灯状态机,驱动红黄绿灯按预设时序(红灯30秒,黄灯3秒,绿灯20秒)切换,并利用分频器对12MHz的时钟实现精准分频;其次,利用计数器和74HC48显示译码器控制最小系统板上的2位数码管实时显示倒计时信息。此外,通过Verilog编写的i2c通信状态机,实时与STEP-Baseboard-V4.0开发板上的进行通信,可在行人靠近时激活开发板上的蜂鸣器,提升通行安全性;最后,系统集成环境光传感器,当环境较暗时自动点亮板载LED路灯,实现节能控制。
项目创新性地融合了图形化快速开发与底层硬件编程的优势,编程主要通过WebIDE拖拽式图形化编程高效实现,传感器驱动采用Verilog优化时序逻辑。经实际测试,各模块协同工作稳定,基本功能实现正常。本设计为智能交通系统提供了低成本、高可靠性的解决方案,后续可通过扩展通信模块实现远程联网控制,具备良好的工程应用价值。
2、使用到的硬件介绍
本次使用到的硬件主要使用了STEP-MXO2小脚丫最小系统板,和STEP-Baseboard-V4.0开发板。最小系统板上使用了12MHz晶振,两个RGB三色LED灯,8个普通红光LED灯,一个按键,两个普通数码管。而STEP-Baseboard-V4.0开发板上主要使用了RPR-0521RS芯片以及蜂鸣器。对于FPGA综合出来的电路,除了74HC48显示译码器外,其余基本都是个人设计的数字电路。
3、方案框图和项目设计思路介绍
本项目主要把任务分为两个部分来进行实现,且这两个部分之间完全独立(不互相占用资源),同时在FPGA上运行。这充分利用了FPGA的高并行性。红绿灯部分主要用了红黄绿三种状态的状态机切换来实现。而目标接近报警和光亮检测主要是利用一个实时与RSR-5021RS通信的I2C状态机来实现。
4、软件流程图和关键代码介绍
关键代码介绍:
除法器模块:
reg [15:0] dividend_r;
reg [15:0] divisor_r;
reg [3:0]temp;
assign shi = temp[3:0];
integer i;
always @(*) begin
dividend_r = {0,use1[7:0]};
divisor_r [15:8] = 10;
divisor_r [7:0] = 0;
temp = 0;
for (i = 0; i < 8; i = i + 1) begin
dividend_r = dividend_r<<1;
temp = temp<<1;
if (dividend_r>=divisor_r) begin
dividend_r = dividend_r-divisor_r;
temp[0] = 1;
end
end
end
模拟二进制除法笔算的步骤得到结果。需要用到大量的硬件资源。
i2c状态机:
case(state)
IDLE:begin //软件自复位,主要用于程序跑飞后的处理
scl <= 1'd1; sda <= 1'd1; ack <= ACK; ack_flag <= 1'b0; cnt <= 1'b0;
cnt_main <= 1'b0; cnt_mode1 <= 1'b0; cnt_mode2 <= 1'b0;
cnt_start <= 1'b0; cnt_write <= 1'b0; cnt_read <= 1'b0; cnt_stop <= 1'b0;
state <= MAIN; state_back <= IDLE;
end
MAIN:begin
if(cnt_main >= 4'd6) cnt_main <= 4'd1;
else cnt_main <= cnt_main + 1'b1;
case(cnt_main)
4'd0: begin dev_addr <= 7'h38; reg_addr <= 8'h41; reg_data <= 8'he9; state <= MODE1; end //写入配置
4'd1: begin dev_addr <= 7'h38; reg_addr <= 8'h44; state <= MODE2; end
4'd2: begin dev_addr <= 7'h38; reg_addr <= 8'h45; state <= MODE3; end
4'd3: begin dev_addr <= 7'h38; reg_addr <= 8'h46; state <= MODE4; end
4'd4: begin dev_addr <= 7'h38; reg_addr <= 8'h47; state <= MODE5; end
4'd5: begin dev_addr <= 7'h38; reg_addr <= 8'h48; state <= MODE6; end
4'd6: begin dev_addr <= 7'h38; reg_addr <= 8'h49; state <= MODE7; end
default: state <= IDLE;
endcase
end
MODE1:begin //单次写操作
if(cnt_mode1 >= 4'd5) cnt_mode1 <= 1'b0; //对START中的子状态执行控制cnt_start
else cnt_mode1 <= cnt_mode1 + 1'b1;
state_back <= MODE1;
case(cnt_mode1)
4'd0: begin state <= START; end
4'd1: begin data_wr <= dev_addr<<1; state <= WRITE; end
4'd2: begin data_wr <= reg_addr; state <= WRITE; end
4'd3: begin data_wr <= reg_data; state <= WRITE; end
4'd4: begin state <= STOP; end
4'd5: begin state <= MAIN; end
default: state <= IDLE;
endcase
end
MODE2:begin
if(cnt_mode2 >= 4'd8) cnt_mode2 <= 1'b0; //对START中的子状态执行控制cnt_start
else cnt_mode2 <= cnt_mode2 + 1'b1;
state_back <= MODE2;
case(cnt_mode2)
4'd0: begin state <= START; end //I2C通信时序中的START
4'd1: begin data_wr <= dev_addr<<1; state <= WRITE; end //设备地址
4'd2: begin data_wr <= reg_addr; state <= WRITE; end
4'd3: begin state <= START; end
4'd4: begin data_wr <= (dev_addr<<1)|8'h01; state <= WRITE; end
4'd5: begin ack <= NACK; state <= READ; end
4'd6: begin data_out[7:0] <= data_r; end
4'd7: begin state <= STOP; end
4'd8: begin state <= MAIN; end
default: state <= IDLE;
endcase
end
MODE3:begin
if(cnt_mode2 >= 4'd8) cnt_mode2 <= 1'b0; //对START中的子状态执行控制cnt_start
else cnt_mode2 <= cnt_mode2 + 1'b1;
state_back <= MODE3;
case(cnt_mode2)
4'd0: begin state <= START; end //I2C通信时序中的START
4'd1: begin data_wr <= dev_addr<<1; state <= WRITE; end //设备地址
4'd2: begin data_wr <= reg_addr; state <= WRITE; end
4'd3: begin state <= START; end
4'd4: begin data_wr <= (dev_addr<<1)|8'h01; state <= WRITE; end
4'd5: begin ack <= NACK; state <= READ; end
4'd6: begin data_out[15:8] <= data_r; end
4'd7: begin state <= STOP; end
4'd8: begin state <= MAIN; end
default: state <= IDLE;
endcase
end
MODE4:begin
if(cnt_mode2 >= 4'd8) cnt_mode2 <= 1'b0; //对START中的子状态执行控制cnt_start
else cnt_mode2 <= cnt_mode2 + 1'b1;
state_back <= MODE4;
case(cnt_mode2)
4'd0: begin state <= START; end //I2C通信时序中的START
4'd1: begin data_wr <= dev_addr<<1; state <= WRITE; end //设备地址
4'd2: begin data_wr <= reg_addr; state <= WRITE; end
4'd3: begin state <= START; end
4'd4: begin data_wr <= (dev_addr<<1)|8'h01; state <= WRITE; end
4'd5: begin ack <= NACK; state <= READ; end
4'd6: begin data_out[23:16] <= data_r; end
4'd7: begin state <= STOP; end
4'd8: begin state <= MAIN; end
default: state <= IDLE;
endcase
end
MODE5:begin
if(cnt_mode2 >= 4'd8) cnt_mode2 <= 1'b0; //对START中的子状态执行控制cnt_start
else cnt_mode2 <= cnt_mode2 + 1'b1;
state_back <= MODE5;
case(cnt_mode2)
4'd0: begin state <= START; end //I2C通信时序中的START
4'd1: begin data_wr <= dev_addr<<1; state <= WRITE; end //设备地址
4'd2: begin data_wr <= reg_addr; state <= WRITE; end
4'd3: begin state <= START; end
4'd4: begin data_wr <= (dev_addr<<1)|8'h01; state <= WRITE; end
4'd5: begin ack <= NACK; state <= READ; end
4'd6: begin data_out[31:24] <= data_r; end
4'd7: begin state <= STOP; end
4'd8: begin state <= MAIN; end
default: state <= IDLE;
endcase
end
MODE6:begin
if(cnt_mode2 >= 4'd8) cnt_mode2 <= 1'b0; //对START中的子状态执行控制cnt_start
else cnt_mode2 <= cnt_mode2 + 1'b1;
state_back <= MODE6;
case(cnt_mode2)
4'd0: begin state <= START; end //I2C通信时序中的START
4'd1: begin data_wr <= dev_addr<<1; state <= WRITE; end //设备地址
4'd2: begin data_wr <= reg_addr; state <= WRITE; end
4'd3: begin state <= START; end
4'd4: begin data_wr <= (dev_addr<<1)|8'h01; state <= WRITE; end
4'd5: begin ack <= NACK; state <= READ; end
4'd6: begin data_out[39:32] <= data_r; end
4'd7: begin state <= STOP; end
4'd8: begin state <= MAIN; end
default: state <= IDLE;
endcase
end
MODE7:begin
if(cnt_mode2 >= 4'd8) cnt_mode2 <= 1'b0; //对START中的子状态执行控制cnt_start
else cnt_mode2 <= cnt_mode2 + 1'b1;
state_back <= MODE7;
case(cnt_mode2)
4'd0: begin state <= START; end //I2C通信时序中的START
4'd1: begin data_wr <= dev_addr<<1; state <= WRITE; end //设备地址
4'd2: begin data_wr <= reg_addr; state <= WRITE; end
4'd3: begin state <= START; end
4'd4: begin data_wr <= (dev_addr<<1)|8'h01; state <= WRITE; end
4'd5: begin ack <= NACK; state <= READ; end
4'd6: begin data_out[47:40] <= data_r; end
4'd7: begin state <= STOP; end
4'd8: begin state <= MAIN; end
default: state <= IDLE;
endcase
end
START:begin //I2C通信时序中的起始START
if(cnt_start >= 3'd5) cnt_start <= 1'b0; //对START中的子状态执行控制cnt_start
else cnt_start <= cnt_start + 1'b1;
case(cnt_start)
3'd0: begin sda <= 1'b1; scl <= 1'b1; end //将SCL和SDA拉高,保持4.7us以上
3'd1: begin sda <= 1'b1; scl <= 1'b1; end //clk_400khz每个周期2.5us,需要两个周期
3'd2: begin sda <= 1'b0; end //SDA拉低到SCL拉低,保持4.0us以上
3'd3: begin sda <= 1'b0; end //clk_400khz每个周期2.5us,需要两个周期
3'd4: begin scl <= 1'b0; end //SCL拉低,保持4.7us以上
3'd5: begin scl <= 1'b0; state <= state_back; end //clk_400khz每个周期2.5us,需要两个周期,返回MAIN
default: state <= IDLE; //如果程序失控,进入IDLE自复位状态
endcase
end
WRITE:begin //I2C通信时序中的写操作WRITE和相应判断操作ACK
if(cnt <= 3'd6) begin //共需要发送8bit的数据,这里控制循环的次数
if(cnt_write >= 3'd3) begin cnt_write <= 1'b0; cnt <= cnt + 1'b1; end
else begin cnt_write <= cnt_write + 1'b1; cnt <= cnt; end
end else begin
if(cnt_write >= 3'd7) begin cnt_write <= 1'b0; cnt <= 1'b0; end //两个变量都恢复初值
else begin cnt_write <= cnt_write + 1'b1; cnt <= cnt; end
end
case(cnt_write)
//按照I2C的时序传输数据
3'd0: begin scl <= 1'b0; sda <= data_wr[7-cnt]; end
3'd1: begin scl <= 1'b1; end //SCL拉高,保持4.0us以上
3'd2: begin scl <= 1'b1; end //clk_400khz每个周期2.5us,需要两个周期
3'd3: begin scl <= 1'b0; end //SCL拉低,准备发送下1bit的数据
//获取从设备的响应信号并判断
3'd4: begin sda <= 1'bz; end //释放SDA线,准备接收从设备的响应信号
3'd5: begin scl <= 1'b1; end //SCL拉高,保持4.0us以上
3'd6: begin ack_flag <= i2c_sda; end //获取从设备的响应信号并判断
3'd7: begin scl <= 1'b0; if(ack_flag)state <= state; else state <= state_back; end //SCL拉低,如果不应答循环写
default: state <= IDLE;
endcase
end
READ:begin //I2C通信时序中的读操作READ和返回ACK的操作
if(cnt <= 3'd6) begin //共需要接收8bit的数据,这里控制循环的次数
if(cnt_read >= 3'd3) begin cnt_read <= 1'b0; cnt <= cnt + 1'b1; end
else begin cnt_read <= cnt_read + 1'b1; cnt <= cnt; end
end else begin
if(cnt_read >= 3'd7) begin cnt_read <= 1'b0; cnt <= 1'b0; end //两个变量都恢复初值
else begin cnt_read <= cnt_read + 1'b1; cnt <= cnt; end
end
case(cnt_read)
//按照I2C的时序接收数据
3'd0: begin scl <= 1'b0; sda <= 1'bz; end //SCL拉低,释放SDA线,准备接收从设备数据
3'd1: begin scl <= 1'b1; end //SCL拉高,保持4.0us以上
3'd2: begin data_r[7-cnt] <= i2c_sda; end //读取从设备返回的数据
3'd3: begin scl <= 1'b0; end //SCL拉低,准备接收下1bit的数据
//向从设备发送响应信号
3'd4: begin sda <= ack; end //发送响应信号,将前面接收的数据锁存
3'd5: begin scl <= 1'b1; end //SCL拉高,保持4.0us以上
3'd6: begin scl <= 1'b1; end //SCL拉高,保持4.0us以上
3'd7: begin scl <= 1'b0; state <= state_back; end //SCL拉低,返回MAIN状态
default: state <= IDLE; //如果程序失控,进入IDLE自复位状态
endcase
end
STOP:begin
if(cnt_stop >= 3'd5) cnt_stop <= 1'b0; //对STOP中的子状态执行控制cnt_stop
else cnt_stop <= cnt_stop + 1'b1;
case(cnt_stop)
3'd0: begin sda <= 1'b0; end
3'd1: begin sda <= 1'b0; end
3'd2: begin scl <= 1'b1; end
3'd3: begin scl <= 1'b1; end
3'd4: begin sda <= 1'b1; end
3'd5: begin sda <= 1'b1; state <= state_back; end
default: state <= IDLE ;
endcase
end
default:;
endcase
开始时先给地址为8'h41的寄存器写入8'he9,之后不断循环六个指令读取芯片六个和距离、光亮有关的寄存器。通过读取到的距离和光亮寄存器来使能LED和蜂鸣器。
5、功能展示图及说明
1、红灯状态展示:
红灯时,计数器将会被置为30s。两个同时三色led中的红灯会被点亮,其它处于熄灭状态。当计数器的值逐渐减到0时,下一个时钟来临时,状态将切换为黄色。
2、黄灯状态展示:
黄灯时,计数器将会被置为3s。两个同时三色led中的红灯和绿灯会被点亮,其它处于熄灭状态。当计数器的值逐渐减到0时,下一个时钟来临时,状态将切换为绿色。
3、绿灯状态展示:
绿灯时,计数器将会被置为20s。两个同时三色led中的绿灯会被点亮,其它处于熄灭状态。当计数器的值逐渐减到0时,下一个时钟来临时,状态将切换为黄色。
天暗自动点灯功能展示:
当环境光传感器的值足够低时(图中用手遮住光传感器模拟光暗时的情况),最小系统板上的8个led灯将会被点亮。本次检测,将会同时用到RSR-5021RS上分别检测两个不同波长范围的光的光传感器。只有两个传感器上的值同时低于阈值,led灯才会被点亮。
接近报警功能描述:
由于接近报警涉及到蜂鸣器,无法用图片来展示其功能。如果想要看到其具体的效果,可以参考视频讲解的部分。具体的效果为,当手距离传感器的距离约3cm以内时,蜂鸣器将会被使能。配合之前专门分配给蜂鸣器的分频器,蜂鸣器将会固定发出2.28khz的音频。
6、项目中遇到的难题和解决方法
主要遇到的问题:
1、除法器在FPGA逻辑综合时,发现综合出来的电流是错误的。因此在红绿灯倒计时显示时由于需要用到除法操作得到计数器的十位部分,需要自己利用FPGA的资源,自己制作除法器。
2、在自制FPGA的数据双向选择器时,发现即使把某一路置为高阻态,高阻态的那一路依然会影响正在使用的电路。
3、在利用官方提供的历程时,发现lab8的示例在webide上无法运行。因此,无法直接移植官方给我们事先写好的案例。需要自己对代码进行理解,并猜测可能出现的问题。最后自己完成与RSR-5021RS的代码。
4、webide的图形化编程有时会出现无法综合的情况。
5、webide的图形化编程在保存模块时,时常会覆盖掉原有的已经写好的代码。
6、一开始测试最小系统板上的数码管时,发现系统板上的数码管无法点亮。
解决方法:
1、在网上寻找资料,并且自己加以理解,完成一个试用于本次项目的除法器代码。本次个人自制的除法器模块,可以通过输入调节除数、被除数和商的位数的参数,自动调整最终得到的除法器的复杂程度。一方面,这样做可以使本来就十分消耗FPGA内部资源的除法器节省大量的内部资源。同时,还能使得该除法器能够兼容更加多的芯片和使用场景。在其他任务需要用到除法器时,也能够很方便地移植。
2、解决方法为,让原本的双向数据选择器的两条数据线合并成一条数据线。同时利用状态机来避免有多台设备同时竞争使用数据总线。采用该方法后,在FPGA上实验的结果表明该解决方法十分有效。
3、参考了官方eeprom示例的代码,主要参考了其中的状态机和I2C通信部分,打造了适用于该方案的,与RPR-0521RS芯片兼容的I2C通信和状态机代码。
4、这个暂时没有特别有效的解决方法,只能在发现综合失败后,重新新建一个项目。
5、专门写了一个模块,来保存目前已写的图形化程序的进度。类似于玩游戏时的保存目前的存档。如果由于保存模块,导致目前已写代码的丢失,只需要在之前专门保存进度的模块中,把之前保存的内容复制粘贴出来即可。
6、后来发现,原来数码管的段选部分的有效电平和数码管显示的部分是反过来的,数码管显示是高电平有效,而段选是低电平有效。因此只需要把段选的电平改过来即可。
7、FPGA资源占用报告
1、综合(Synthesis)阶段的资源报告
2、映射(Map)阶段的资源报告:
3、布局布线(PAR,Place And Route)阶段的资源报告:
8、对本次活动的心得体会
本次项目是我使用FPGA完整完成的第一个项目。也是我第一次接触FPGA,并细心钻研的一次机会。这次活动使用到了图形化编程(WebIDE)与Verilog硬件描述语言结合开发,使我深刻体会到:
1、图形化编程的优势在于快速搭建基础逻辑,显著降低状态机等模块的开发门槛。也可以让各个模块之间的连接显得更加直观。同时我也看到了本次使用的WebIDE在功能上的不完善,也希望官方能够进一步改善开发环境。
2、Verilog 在传感器信号处理中展现精准控制能力,能够对硬件之间的各种动作做出精准的描述,与图形化结合可以大幅提升开发效率。
同时在完成任务的过程中我也遇到了很多问题,并且在官方的活动群中积极反应了这些问题,和大家一起讨论。大家遇到的问题,我也会积极给出自己的回答。在解决问题的过程中,我不仅加强了对FPGA的理解,而且对于解决问题的耐心,我也有了不小的进步。
最后,我还知晓了在FPGA开发的过程中,仿真的重要性。在软件上,难以检测各个寄存器上变量的值,而利用软件仿真,我们可以轻松地根据各个寄存器上的值,通过锁定那些违背预期的寄存器的值,来判断问题出现的原因。这样的做法,只在硬件上是难以做到的。
对于本次活动的意见和建议:
1、希望官方能够进一步完善webide,无论是代码编程上还是图形化编程上。寻找综合卡死,综合异常,以及图形化编程在保存模块时会覆盖掉原有的进度的问题。就像本次寒假完成任务时,一了解到webide在分配管脚时,无法显示inout类型的变量时,就立即着手解决一样。webide在图形化的管脚分配方面实际上十分地便利了我们开发的效率。
2、希望官方能够提供更加丰富的功能,如跨平台开发(WebIDE+Diamond工作流整合)、补充更多关于webide的教学内容(如去耦电容布局、信号阻抗匹配)、增加自动化测试案例。
图形化编程webide链接:https://www.stepfpga.com/project/16592/gui?type=top