2025寒假练 - 基于小脚丫FPGA STEP BaseBoard V4.0实现交通灯控制系统
该项目使用了WebIDE的图形化编程,Diamond软件verilog语言,嘉立创IDE绘制PCB板,实现了一个基于小脚丫FPGA STEP BaseBoard V4.0的交通灯控制系统的设计,它的主要功能为:在数码管上显示十字路口四个不同方向的计时信息,交通灯PCB直观展示路口状态,接近传感器检测行人通过路口意图(人员走近),在行人闯红灯时触发蜂鸣器报警,感知环境光并自动点亮路灯。
标签
FPGA
PCB设计
2025寒假在家一起练
交通灯系统
猫与黄油面包
更新2025-03-17
山东大学
69

一.项目介绍与项目视频

本项目基于STEP Baseboard4.0底板和STEP MXO2 LPC核心板,使用webide图形化编程和diamond软件verilog编程语言进行软件开发,嘉立创IDE进行原理图和PCB设计,完成了一个FPGA交通灯控制系统。


本次使用板卡如下图所示:


根据任务要求,本次设计的交通灯系统由两个工程构成:工程一利用独立绘制的PCB板直观的展示了交通灯控制的逻辑和路口状态,路口的四个方向分别具有独立倒计时功能和蜂鸣器发声功能;工程二在简化版的交通灯情境中,考虑直行和左转两种状态,完成了检测行人闯红灯意图并使用蜂鸣器报警,同时具有检测光强,感应路灯功能。该交通灯控制系统具有形式美观,演示直观,逻辑清晰,功能实用等特点,能够实现的具体功能有:


  • 交通灯PCB板真实模拟十字路口四个方向的通行状态
  • 数码管同步显示所有方向倒计时时间
  • 蜂鸣器发声
  • 接近传感器检测,模拟行人通行(人员走近)
  • 行人闯红灯时触发报警
  • 感知环境光并自动点亮路灯

    最终实物演示效果图如下所示:

二.硬件介绍与FPGA资源占用报告

1.本项目使用的硬件资源主要来自三部分:


  • STEP Baseboard4.0底板:
    环境光和接近式传感器,8位7段数码管模块,蜂鸣器模块
  • STEP MXO2 LPC核心板:
    RGB三色LED模块,用户LED模块,按键模块,扩展IO引脚,USB Type C接口
  • 自制交通灯印刷电路板:
    该电路板使用嘉立创IDE绘制并打样,元件由本人测试后手工焊接,经验证无明显错误,原理图与PCB如图所示:



    2.两个工程的资源占用报告分别如下所示:
  • 工程一:
  • 工程二:

三.方案框图和项目设计思路介绍

1.交通灯控制系统的框图如下:

2.项目设计思路介绍

工程一根据十字路口的交通灯呈周期性计时的特点,设计traffic_counter模块,实现周期性的循环向下计数;又因为南北直行、南北左转、东西直行、东西左转四个方向的总时长固定,将该模块输出的计数值送入主逻辑operation_traffic模块,即可通过线性运算得到绿灯方向和其他红灯的倒计时数,并同步驱动数码管与PCB上的灯光亮灭。主逻辑模块输出四个方向的红绿灯状况给PCB板,同时输出四个方向倒计时时间,送入bcd_decoder模块进行bcd码的转换,最后输出给segment模块驱动底板上的八位数码管,配合dot_en的小数点驱动,完成倒计时的显示;同时设计beeper模块,该模块由tonepwm模块组成,并由beeper_data驱动,使得在交通灯工作时蜂鸣器发声。

工程二的简化交通灯部分与工程一基本类似,设计了traffic_counter模块、operation_traffic模块、bcd模块、segment_scan模块,实现了双状态左转和直行两个方向的灯光控制与倒计时控制;同时设计了rpr0521rs_driverrpr0521rs_decode模块,驱动环境光和接近式传感器,使用I2C通信得到的数据进行光亮和靠近的检测:人员靠近且直行为红灯时判断为闯红灯行为,蜂鸣器发出报警;而直行为绿灯时蜂鸣器不报警,完成闯红灯检测。在环境较暗时,会自动将streetligt[7:0]引脚的led点亮,实现自动点亮路灯。


四.工程一的软件流程图和关键代码


1.工程一的软件流程图如下:


2.关键代码介绍


(1)traffic_counter模块:


该模块的实现基础是一个用于计数的分频器,counter计到CLK12M_1S时自增1并重置,如此将时钟信号变为以1s为周期的信号,再以该信号为基准进行向下计数并输出out_data,若计数值到0则会返回最大值,实现循环减法计数器,供下一模块使用。


图形化自定义模块内部代码如下:

reg [23:0] counter;
reg [7:0] out_data;

localparam CLK12M_1S = 12_000_000;

initial begin
counter = 24'd0;
out_data = 8'd60;
end

always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
counter <= 24'd0;
out_data <= 8'd60;
end
else if(counter < CLK12M_1S) begin
counter <= counter+1;
out_data <= out_data;
end
else begin
counter <= 24'd0;
if(out_data > 0) begin
out_data <= out_data - 1;
end
else begin
out_data <= 8'd59;
end
end
end

assign out = out_data;

(2)operation_traffic模块:


该模块是该系统的主逻辑模块,采用了经典的三段式状态机编程逻辑,设置了X_STRAIGHTX_LEFTY_STRAIGHTY_LEFT 四个状态作为交通灯的。在第一个always语句中完成了状态由state向new_state的转变;第二个always语句基于输入的倒计时值count_in进行判断,满足转移条件时跳转到下一个红绿灯状态。第三个always语句按照当前状态决定输出值,输出值可驱动交通灯PCB上的LED灯、并包含x_left_dispx_str_dispy_left_dispy_str_disp四个数码管对应显示倒计时的二进制信息。


图形化自定义模块内部代码如下:

reg [7:0] led_out;    //x左红,x左绿  x直红,x直绿 y左红,y左绿  y直红,y直绿
reg [7:0] dat_en_out;
reg [7:0] x_left_disp;
reg [7:0] x_str_disp;
reg [7:0] y_left_disp;
reg [7:0] y_str_disp;

assign x_left = x_left_disp;
assign x_str = x_str_disp;
assign y_left = y_left_disp;
assign y_str = y_str_disp;
assign led = led_out; //低电平点亮
assign dat_en = dat_en_out;

localparam PERIOD = 8'd60;
localparam X_STRAIGHT = 4'b0001;
localparam X_LEFT = 4'b0010;
localparam Y_STRAIGHT = 4'b0100;
localparam Y_LEFT = 4'b1000;

reg [3:0] state;
reg [3:0] next_state;
always@(posedge clk or negedge rst_n) begin //转移执行
if(!rst_n) begin
state <= X_STRAIGHT;
end
else begin
state <= next_state;
end
end

always@(posedge clk or negedge rst_n) begin //转移条件
if(!rst_n) begin
next_state <= X_STRAIGHT;
end
else begin
case(state)
X_STRAIGHT:
begin
if(count_in < 8'd45)
next_state <= X_LEFT;
else
next_state <= X_STRAIGHT;
end
X_LEFT:
begin
if(count_in < 8'd30)
next_state <= Y_STRAIGHT;
else
next_state <= X_LEFT;
end
Y_STRAIGHT:
begin
if(count_in < 8'd15)
next_state <= Y_LEFT;
else
next_state <= Y_STRAIGHT;
end
Y_LEFT:
begin
if(count_in > 8'd45)
next_state <= X_STRAIGHT;
else
next_state <= Y_LEFT;
end
default:
begin
next_state <= next_state;
end
endcase
end

end

always@(posedge clk or negedge rst_n) begin //每个状态的输出
if(!rst_n) begin
dat_en_out <= 8'b1100_1111;
x_left_disp <= 8'd15;
x_str_disp <= 8'd0;
y_left_disp <= 8'd45;
y_str_disp <= 8'd30;
led_out <= 8'b01_10_01_01;
end
else begin
case(state)
X_STRAIGHT:
begin
dat_en_out <= 8'b1100_1111;
x_left_disp <= count_in - 8'd45;
x_str_disp <= 8'd0;
y_left_disp <= count_in - 8'd15;
y_str_disp <= count_in - 8'd30;
led_out <= 8'b01_10_01_01;
end
X_LEFT:
begin
dat_en_out <= 8'b0011_1111;
x_left_disp <= 8'd0;
x_str_disp <= count_in;
y_left_disp <= count_in - 8'd15;
y_str_disp <= count_in - 8'd30;
led_out <= 8'b10_01_01_01;
end
Y_STRAIGHT:
begin
dat_en_out <= 8'b1111_1100;
x_left_disp <= count_in + 8'd15;
x_str_disp <= count_in;
y_left_disp <= count_in - 8'd15;
y_str_disp <= 8'd0;
led_out <= 8'b01_01_01_10;
end
Y_LEFT:
begin
dat_en_out <= 8'b1111_0011;
x_left_disp <= count_in + 8'd15;
x_str_disp <= count_in;
y_left_disp <= 8'd0;
y_str_disp <= count_in + 8'd30;
led_out <= 8'b01_01_10_01;
end
default:
begin
dat_en_out <= dat_en_out;
x_left_disp <= x_left_disp;
x_str_disp <= x_str_disp;
y_left_disp <= y_left_disp;
y_str_disp <= y_str_disp;
led_out <= led_out;
end
endcase
end
end

(3)bcd_decoder模块:


本模块的作用时给数码管提供显示数据。该模块接收上模块中的输出的数码管计时信息,利用提供的8位二进制转BCD码模块,将数据由二进制转为BCD码,原理再此不多赘述。


图形化自定义模块内部代码如下:

(4)segment模块:


该数码管模块以官方例程作为基础,改为图形化编程的结构与语法,经验证功能能够正常使用。


图形化自定义模块内部代码如下:

localparam	CNT_40KHz = 300;
localparam IDLE = 3'd0;
localparam MAIN = 3'd1;
localparam WRITE = 3'd2;
localparam LOW = 1'b0;
localparam HIGH = 1'b1;

reg seg_rck;
reg seg_sck;
reg seg_din;

assign seg_rck_out = seg_rck;
assign seg_sck_out = seg_sck;
assign seg_din_out = seg_din;

reg[6:0] seg [15:0];
always @(negedge rst_n) begin
seg[0] = 7'h3f; // 0
seg[1] = 7'h06; // 1
seg[2] = 7'h5b; // 2
seg[3] = 7'h4f; // 3
seg[4] = 7'h66; // 4
seg[5] = 7'h6d; // 5
seg[6] = 7'h7d; // 6
seg[7] = 7'h07; // 7
seg[8] = 7'h7f; // 8
seg[9] = 7'h6f; // 9
seg[10] = 7'h77; // A
seg[11] = 7'h7c; // b
seg[12] = 7'h39; // C
seg[13] = 7'h5e; // d
seg[14] = 7'h79; // E
seg[15] = 7'h71; // F
end

//计数器对系统时钟信号进行计数
reg [9:0] cnt = 1'b0;
always@(posedge clk or negedge rst_n) begin
if(!rst_n) cnt <= 1'b0;
else if(cnt>=(CNT_40KHz-1)) cnt <= 1'b0;
else cnt <= cnt + 1'b1;
end

//根据计数器计数的周期产生分频的脉冲信号
reg clk_40khz = 1'b0;
always@(posedge clk or negedge rst_n) begin
if(!rst_n) clk_40khz <= 1'b0;
else if(cnt<(CNT_40KHz>>1)) clk_40khz <= 1'b0;
else clk_40khz <= 1'b1;
end

//使用状态机完成数码管的扫描和74HC595时序的实现
reg [15:0] data;
reg [2:0] cnt_main;
reg [5:0] cnt_write;
reg [2:0] state = IDLE;
always@(posedge clk_40khz or negedge rst_n) begin
if(!rst_n) begin //复位状态下,各寄存器置初值
state <= IDLE;
cnt_main <= 3'd0; cnt_write <= 6'd0;
seg_din <= 1'b0; seg_sck <= LOW; seg_rck <= LOW;
end else begin
case(state)
IDLE:begin //IDLE作为第一个状态,相当于软复位
state <= MAIN;
cnt_main <= 3'd0; cnt_write <= 6'd0;
seg_din <= 1'b0; seg_sck <= LOW; seg_rck <= LOW;
end
MAIN:begin
cnt_main <= cnt_main + 1'b1;
state <= WRITE; //在配置完发给74HC595的数据同时跳转至WRITE状态,完成串行时序
case(cnt_main)
//对8位数码管逐位扫描
//data [15:8]为段选, [7:0]为位选
3'd0: data <= {{dot_en[7],seg[dat_1]},dat_en[7]?8'hfe:8'hff};
3'd1: data <= {{dot_en[6],seg[dat_2]},dat_en[6]?8'hfd:8'hff};
3'd2: data <= {{dot_en[5],seg[dat_3]},dat_en[5]?8'hfb:8'hff};
3'd3: data <= {{dot_en[4],seg[dat_4]},dat_en[4]?8'hf7:8'hff};
3'd4: data <= {{dot_en[3],seg[dat_5]},dat_en[3]?8'hef:8'hff};
3'd5: data <= {{dot_en[2],seg[dat_6]},dat_en[2]?8'hdf:8'hff};
3'd6: data <= {{dot_en[1],seg[dat_7]},dat_en[1]?8'hbf:8'hff};
3'd7: data <= {{dot_en[0],seg[dat_8]},dat_en[0]?8'h7f:8'hff};
default: data <= {8'h00,8'hff};
endcase
end
WRITE:begin
if(cnt_write >= 6'd33) cnt_write <= 1'b0;
else cnt_write <= cnt_write + 1'b1;
case(cnt_write)
//74HC595是串行转并行的芯片,3路输入可产生8路输出,而且可以级联使用
//74HC595的时序实现,参考74HC595的芯片手册
6'd0: begin seg_sck <= LOW; seg_din <= data[15]; end //SCK下降沿时SER更新数据
6'd1: begin seg_sck <= HIGH; end //SCK上升沿时SER数据稳定
6'd2: begin seg_sck <= LOW; seg_din <= data[14]; end
6'd3: begin seg_sck <= HIGH; end
6'd4: begin seg_sck <= LOW; seg_din <= data[13]; end
6'd5: begin seg_sck <= HIGH; end
6'd6: begin seg_sck <= LOW; seg_din <= data[12]; end
6'd7: begin seg_sck <= HIGH; end
6'd8: begin seg_sck <= LOW; seg_din <= data[11]; end
6'd9: begin seg_sck <= HIGH; end
6'd10: begin seg_sck <= LOW; seg_din <= data[10]; end
6'd11: begin seg_sck <= HIGH; end
6'd12: begin seg_sck <= LOW; seg_din <= data[9]; end
6'd13: begin seg_sck <= HIGH; end
6'd14: begin seg_sck <= LOW; seg_din <= data[8]; end
6'd15: begin seg_sck <= HIGH; end
6'd16: begin seg_sck <= LOW; seg_din <= data[7]; end
6'd17: begin seg_sck <= HIGH; end
6'd18: begin seg_sck <= LOW; seg_din <= data[6]; end
6'd19: begin seg_sck <= HIGH; end
6'd20: begin seg_sck <= LOW; seg_din <= data[5]; end
6'd21: begin seg_sck <= HIGH; end
6'd22: begin seg_sck <= LOW; seg_din <= data[4]; end
6'd23: begin seg_sck <= HIGH; end
6'd24: begin seg_sck <= LOW; seg_din <= data[3]; end
6'd25: begin seg_sck <= HIGH; end
6'd26: begin seg_sck <= LOW; seg_din <= data[2]; end
6'd27: begin seg_sck <= HIGH; end
6'd28: begin seg_sck <= LOW; seg_din <= data[1]; end
6'd29: begin seg_sck <= HIGH; end
6'd30: begin seg_sck <= LOW; seg_din <= data[0]; end
6'd31: begin seg_sck <= HIGH; end
6'd32: begin seg_rck <= HIGH; end //当16位数据传送完成后RCK拉高,输出生效
6'd33: begin seg_rck <= LOW; state <= MAIN; end
default: ;
endcase
end
default: state <= IDLE;
endcase
end
end


五.工程二的软件流程图和关键代码


1.工程二的软件流程图如下:



2.关键代码介绍


(1)rpr0521rs_driver模块:


该模块为环境光和接近式传感器的驱动模块,使用I2C通信协议,同时输出光照数据和接近数据,供之后的模块进行判断使用,为官方例程模块。


代码如下:

module rpr0521rs_driver(
input clk, //系统时钟
input rst_n, //系统复位,低有效

output i2c_scl, //I2C总线SCL
inout i2c_sda, //I2C总线SDA

output reg dat_valid, //数据有效脉冲
output reg [15:0] ch0_dat, //ALS数据
output reg [15:0] ch1_dat, //IR数据
output reg [15:0] prox_dat //Prox数据
);

parameter CNT_NUM = 15;

localparam IDLE = 4'd0;
localparam MAIN = 4'd1;
localparam MODE1 = 4'd2;
localparam MODE2 = 4'd3;
localparam START = 4'd4;
localparam WRITE = 4'd5;
localparam READ = 4'd6;
localparam STOP = 4'd7;
localparam DELAY = 4'd8;

localparam ACK = 1'b0;
localparam NACK = 1'b1;

//使用计数器分频产生400KHz时钟信号clk_400khz
reg clk_400khz;
reg [9:0] cnt_400khz;
always@(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt_400khz <= 10'd0;
else if(cnt_400khz == CNT_NUM-1)
cnt_400khz <= 10'd0;
else
cnt_400khz <= cnt_400khz + 1'b1;
end
always@(posedge clk or negedge rst_n) begin
if(!rst_n)
clk_400khz <= 1'b0;
else if(cnt_400khz == CNT_NUM-1)
clk_400khz <= ~clk_400khz;
end

reg scl,sda,ack,ack_flag;
reg [3:0] cnt, cnt_main, cnt_mode1, cnt_mode2, cnt_start, cnt_write, cnt_read, cnt_stop;
reg [7:0] data_wr, dev_addr, reg_addr, reg_data, data_r, dat_l, dat_h;
reg [23:0] cnt_delay, num_delay;
reg [3:0] state, state_back;

always@(posedge clk_400khz or negedge rst_n) begin
if(!rst_n) 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;
cnt_delay <= 1'b0; num_delay <= 24'd4800;
state <= IDLE; state_back <= IDLE;
end else begin
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;
cnt_delay <= 1'b0; num_delay <= 24'd4800;
state <= MAIN; state_back <= IDLE;
end
MAIN:begin
if(cnt_main >= 4'd11) cnt_main <= 4'd4; //写完控制指令后循环读数据
else cnt_main <= cnt_main + 1'b1;
case(cnt_main)
4'd0: begin dev_addr <= 7'h38; reg_addr <= 8'h40; reg_data <= 8'h0a; state <= MODE1; end //写入配置
4'd1: begin dev_addr <= 7'h38; reg_addr <= 8'h41; reg_data <= 8'hc6; state <= MODE1; end //写入配置
4'd2: begin dev_addr <= 7'h38; reg_addr <= 8'h42; reg_data <= 8'h02; state <= MODE1; end //写入配置
4'd3: begin dev_addr <= 7'h38; reg_addr <= 8'h43; reg_data <= 8'h01; state <= MODE1; end //写入配置
4'd4: begin state <= DELAY; dat_valid <= 1'b0; end //12ms延时
4'd5: begin dev_addr <= 7'h38; reg_addr <= 8'h44; state <= MODE2; end //读取配置
4'd6: begin prox_dat<= {dat_h,dat_l}; end //读取数据
4'd7: begin dev_addr <= 7'h38; reg_addr <= 8'h46; state <= MODE2; end //读取配置
4'd8: begin ch0_dat <= {dat_h,dat_l}; end //读取数据
4'd9: begin dev_addr <= 7'h38; reg_addr <= 8'h48; state <= MODE2; end //读取配置
4'd10: begin ch1_dat <= {dat_h,dat_l}; end //读取数据
4'd11: begin dat_valid <= 1'b1; end //读取数据
default: state <= IDLE; //如果程序失控,进入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 //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 data_wr <= reg_data; state <= WRITE; end //写入数据
4'd4: begin state <= STOP; end //I2C通信时序中的STOP
4'd5: begin state <= MAIN; end //返回MAIN
default: state <= IDLE; //如果程序失控,进入IDLE自复位状态
endcase
end
MODE2:begin //两次读操作
if(cnt_mode2 >= 4'd10) 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 //I2C通信时序中的START
4'd4: begin data_wr <= (dev_addr<<1)|8'h01; state <= WRITE; end //设备地址
4'd5: begin ack <= ACK; state <= READ; end //读寄存器数据
4'd6: begin dat_l <= data_r; end
4'd7: begin ack <= NACK; state <= READ; end //读寄存器数据
4'd8: begin dat_h <= data_r; end
4'd9: begin state <= STOP; end //I2C通信时序中的STOP
4'd10: begin state <= MAIN; end //返回MAIN
default: state <= IDLE; //如果程序失控,进入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 //SCL拉低,并控制SDA输出对应的位
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; //如果程序失控,进入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 //I2C通信时序中的结束STOP
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 //SDA拉低,准备STOP
3'd1: begin sda <= 1'b0; end //SDA拉低,准备STOP
3'd2: begin scl <= 1'b1; end //SCL提前SDA拉高4.0us
3'd3: begin scl <= 1'b1; end //SCL提前SDA拉高4.0us
3'd4: begin sda <= 1'b1; end //SDA拉高
3'd5: begin sda <= 1'b1; state <= state_back; end //完成STOP操作,返回MAIN状态
default: state <= IDLE; //如果程序失控,进入IDLE自复位状态
endcase
end
DELAY:begin //12ms延时
if(cnt_delay >= num_delay) begin
cnt_delay <= 1'b0;
state <= MAIN;
end else cnt_delay <= cnt_delay + 1'b1;
end
default:;
endcase
end
end

assign i2c_scl = scl; //对SCL端口赋值
assign i2c_sda = sda; //对SDA端口赋值

endmodule

(2)rpr0521rs_decode模块:


该模块将直行标志straight_flag和传感器数据作为输入,当为非直行即straight_flag为零且检测到靠近时,判定为闯红灯,输出beeper_en蜂鸣器使能信号进行报警;同时在lux < 32'd30000条件下,判定光照暗,打开路灯。


代码如下:

module rpr0521rs_decode(
input rst_n,
input dat_valid,
input straight_flag,
input [15:0] prox_dat, //距离
input [15:0] ch0_dat, //光强
input [15:0] ch1_dat, //红外
output reg [7:0] streetlight,
output beeper_en
);

wire [31:0] lux_data;
reg [7:0] Y_out;
assign beeper_en = (!straight_flag) & (!Y_out[2]);
//assign streetlight = !(lux_data > 32'd4000);=

reg [15:0] prox_dat0,prox_dat1,prox_dat2;
always @(posedge dat_valid) begin
prox_dat0 <= prox_dat;
prox_dat1 <= prox_dat0;
if(((prox_dat1-prox_dat0) >= 16'h800)||((prox_dat1-prox_dat0) >= 16'h800))
prox_dat2 <= prox_dat2;
else
prox_dat2 <= prox_dat0;
end

always@(prox_dat2[11:9]) begin
case (prox_dat2[11:9])
3'b000: Y_out = 8'b11111110;
3'b001: Y_out = 8'b11111100;
3'b010: Y_out = 8'b11111000;
3'b011: Y_out = 8'b11110000;
3'b100: Y_out = 8'b11100000;
3'b101: Y_out = 8'b11000000;
3'b110: Y_out = 8'b10000000;
3'b111: Y_out = 8'b00000000;
default:Y_out = 8'b11111111;
endcase
end

wire [31:0] data0,data1;
reg [31:0] lux;

assign data0 = ch0_dat ;
assign data1 = ch1_dat ;

always@(data0 or data1)
if(data1 < data0*595)
lux = data0*1682 - data1*1877;
else if(data1 < data0*1015)
lux = data0*644 - data1* 132;
else if(data1 < data0*1352)
lux = data0*756 - data1*243;
else if((data1 < data0*3053))
lux = data0*766 - data1*25;
else
lux = 0;

always@(*) begin
if(lux > 32'd30000) begin //调低这个值不容易亮
streetlight <= 8'b11111111;
end
else begin
streetlight <= 8'b00000000;
end

end

//进行BCD转码处理
wire [31:0] T_data_bcd;
rpr0521rs_to_bcd u1
(
.rst_n (rst_n ), //系统复位,低有效
.bin_code (lux ), //需要进行BCD转码的二进制数据
.bcd_code (T_data_bcd ) //转码后的BCD码型数据输出
);
assign lux_data = T_data_bcd;
endmodule

(3)beeper模块:


该模块由beeper_en控制蜂鸣器发声与否,同时实例化了pwm模块,输出beeper信号驱动蜂鸣器。


代码如下:

module beeper(
input clk,
input rst_n,
input beeper_en,
output beeper
);

//根据不同音节的周期cycle值产生对应的PWM信号
pwm #(
.WIDTH(16) //ensure that 2**WIDTH > cycle
) u2(
.clk(clk),
.rst_n(rst_n),
.cycle(16'd22931), //cycle > duty
.duty((16'd22931)>>1), //duty=cycle/2,产生50%占空比
.pwm_en(beeper_en),
.pwm_out(beeper)
);

endmodule

(4)pwm模块:


该模块为官方模块改编而来,加入了pwm_en信号控制使能,实现了任意指定pwm信号的开始与关闭的功能。


代码如下:

module pwm #(
parameter WIDTH = 32 //ensure that 2**WIDTH > cycle
)
(
input clk,
input rst_n,
input [WIDTH-1:0] cycle, //cycle > duty
input [WIDTH-1:0] duty, //duty < cycle
input pwm_en,
output pwm_out
);

reg pwm;
reg [WIDTH-1:0] cnt;
//counter for cycle
always @(posedge clk or negedge rst_n)
if(!rst_n)
cnt <= 1'b1;
else if(cnt >= cycle)
cnt <= 1'b1;
else
cnt <= cnt + 1'b1;

//pulse with duty
always @(posedge clk or negedge rst_n)
if(!rst_n)
pwm <= 1'b1;
else if(cnt < duty)
pwm <= 1'b1;
else
pwm <= 1'b0;

assign pwm_out = pwm & pwm_en;

endmodule

(5)顶层模块实例化


顶层模块实例化的精髓之处在于利用了输出led[0]在不同状态下的输出值不同,可将其用作直行信号straight_flag的实例化,而无需使用其他变量。


代码如下:

module top(
input clk,
input rst_n,
//input key,
inout i2c_sda, //I2C总线SDA
output i2c_scl, //I2C总线SCL

output beeper,
output [3:0] led,
output [7:0] streetlight,

output seg_rck,
output seg_sck,
output seg_din

);

wire [7:0] count_data;
wire beeper_en;
wire [15:0] ch0_dat;
wire [15:0] ch1_dat;
wire [15:0] prox_dat;

beeper u0
(
.clk (clk),
.rst_n (rst_n),
.beeper_en (beeper_en),
.beeper (beeper)
);

traffic_counter u1
(
.clk (clk),
.rst_n (rst_n),
.out (count_data)
);

wire [7:0] x_time;
wire [7:0] y_time;
wire [7:0] seg_en;

operation_traffic u2
(
.clk (clk),
.rst_n (rst_n),
.count_in (count_data),
.led (led),
.seg_en (seg_en),
.x_time (x_time),
.y_time (y_time)

);

wire [31:0] seg_data;

segment_scan u3
(
.clk (clk),
.rst_n (rst_n),
.dat_1 (seg_data[31:28]),
.dat_2 (seg_data[27:24]),
.dat_3 (seg_data[23:20]),
.dat_4 (seg_data[19:16]),
.dat_5 (seg_data[15:12]),
.dat_6 (seg_data[11:8]),
.dat_7 (seg_data[7:4]),
.dat_8 (seg_data[3:0]),
.dat_en (seg_en),
.dot_en (8'b0110_0110),
.seg_rck (seg_rck),
.seg_sck (seg_sck),
.seg_din (seg_din)
);

bcd u4
(
.x_time (x_time),
.y_time (y_time),
.seg_data (seg_data)
);

wire dat_valid;

rpr0521rs_driver u5
(
.clk (clk),
.rst_n (rst_n),
.i2c_scl (i2c_scl),
.i2c_sda (i2c_sda),
.dat_valid (dat_valid),
.ch0_dat (ch0_dat),
.ch1_dat (ch1_dat),
.prox_dat (prox_dat)
);

rpr0521rs_decode u6
(
.rst_n (rst_n),
.dat_valid (dat_valid),
.straight_flag (led[0]),
.prox_dat (prox_dat),
.ch0_dat (ch0_dat),
.ch1_dat (ch1_dat),
.streetlight (streetlight),
.beeper_en (beeper_en)

);

endmodule


六.功能展示图及说明


1.工程一演示


将板卡上对应的扩展IO引脚与交通灯PCB相连,检查无错误后,给系统上电。


该系统具有四个方向的运行状态,将某方向的直行为绿灯作为初始状态,以交通灯PCB板上的灯光情况作为体现,并启动蜂鸣器发声作为运行标志(不便于进行图片演示,请移步视频观看)。底板的数码管会将绿灯方向对应位置上的数码管失能,同时显示其他三个红灯方向的倒计时信息,如图所示:

当存在红灯倒计时结束后,会切换该方向为绿灯,之前的绿灯将变为红灯,同时数码管的计时信息重置,如图:

当一个交通灯周期结束后,该系统将会继续运行,即重复上述过程。


2.工程二演示


该简易交通灯系统有直行和左转两种状态,以直行为初始状态,此时对应板上三色LED对应位置为绿色,另一为红色;同时在底板的数码管固定位置显示倒计时信息,如图所示:

倒计时结束会切换状态,并循环运行该逻辑,如图为直行状态变为左转状态:

该系统能够自动检测环境亮度:当环境过暗时,核心板上LED将亮起,此行为与直行和左转状态无关,如图:

该系统具有检测行人通过和闯红灯报警功能:当交通灯当前为非直行状态(即左转状态)时,此时若有行人试图通过路口,接近传感器将将检测到闯红灯意图,触发蜂鸣器报警;但若为直行状态,行人便没有闯红灯嫌疑,蜂鸣器将不会报警。由于该功能不便于进行图片演示,在此略去图片,可移步视频观看演示。


七.难题与解决方法

  • 难题一: 一开始的循环倒计时模块总是无法实现想要的效果,具体来说是在循环一轮之后的第一个直行处,时间值会出错。

    解决方法: 一方面编写仿真文件,在Modelsim软件中进行仿真排错;另一方面观察底板数码管的时间值,验证修改结果,循环倒计时模块仿真结果如下:


  • 难题二: 在webide中进行图形化编程开发时,综合后难以凭借报错定位代码位置,从而对修改工作产生一定的障碍。

    解决方法: 将自己的自定义的模块逐个综合,排除单个模块问题;再将模块整合后进行综合,排除模块之间连接问题,直至综合成功。


八.心得体会与意见建议

1.心得体会

作为一名大二学生,我在单片机和PCB板绘制方面有一定基础。但寒假在第一次接触FPGA时仍遇到了一系列问题,在解决问题的过程中锻炼了我的编程思维和问题解决能力。通过这次寒假一起练活动,我对verilog语言和图形化编程的方法有了更深层次的理解,我相信这次活动会对未来的学习产生重大的影响。以后我将使用这次的STEP Baseboard4.0底板和STEP MXO2 LPC核心板完成更多的任务,希望活动越办越好!期待下次的活动!

2.意见建议

个人的意见与建议主要围绕着webide来展开。在我看来,webide的操作便捷程度是远高于diamond软件的,非下载形式的web开发更是给开发带来了巨大的便利。但在实际使用中,发现其中仍有许多小瑕疵有待优化,包括模块有时无法保存,图形化的综合报错不直观等等。我个人对webide使用图形化编程FPGA十分看好,相信webide会在不断的优化中变得越来越好!




附件下载
2025寒假在家一起练附件.zip
包含两个工程的代码文件、嘉立创工程文件、webide链接
团队介绍
山东大学 控制科学与工程学院 李相扬
团队成员
猫与黄油面包
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号