2025寒假练 - 基于小脚FPGA STEP BaseBoard V4.0实现交通灯控制系统
该项目使用了verilog语言、webIDE图形化编程,实现了交通灯控制系统的设计,它的主要功能为:通过数码管显示的倒计时控制红(40s)黄(5s)绿(35s)三色的灯跳变,在黄灯的时候蜂鸣器会响。。 该项目使用了verilog语言、Diamond 3.13,实现了环境光、距离检测的设计,它的主要功能为:通过检测环境光,控制led的亮灭;通过点亮的led灯数量来显示物体的距离。。
标签
FPGA
verilog语言
webIDE图形化编程
Diamond 3.13
666xuehao
更新2025-03-17
40

智能交通信号与环境感知系统项目总结报告

一、项目介绍

本项目实现了交通信号控制、环境光感知和距离可视化功能。项目的三大核心功能:

  1. 多模式交通灯控制:采用红(40s)、黄(5s)、绿(35s)三色循环系统,配合数码管倒计时显示,并且在黄灯的时候使用蜂鸣器报警提醒驾驶员;
  2. 环境响应式照明:基于环境光传感器的环境光检测功能实现对LED自动控制,当光照强度小于设定的阈值时,点亮LED灯;
  3. 距离可视化:通过距离传感器模块测量物体距离,以LED灯带形式直观显示。

项目采用模块化设计,集成数字显示、声光报警、环境感知等多项技术,适用于智能交通模拟、工业测距预警等场景。

二、硬件介绍

硬件模块型号参数

功能描述

无源蜂鸣器

黄灯报警响声

74HC595

级联后控制数码管

数码管

控制倒计时的显示

环境光和接近传感器

检测环境光、检测距离

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

任务一、二(交通灯)

任务一和任务二的方案框图如下所示。

绘图1.png

这工程实现了一个交通灯控制系统,包含了三个主要状态:红灯(40秒)、黄灯(5秒)、绿灯(30秒)。系统通过时钟分频和状态机控制交通灯的切换。

首先,代码通过一个12MHz的输入时钟生成一个1Hz的时钟信号(one_sec_en),作为状态机的驱动信号。分频器的计数器div_cnt每达到600万时触发一个时钟周期,转换为1秒,使能信号。

然后,使用状态机(current_state)来控制交通灯的倒计时。初始状态为红灯(S40),倒计时40秒后切换到黄灯(S05),再倒计时5秒后切换到绿灯(S35),最后绿灯倒计时30秒后再次切换到红灯。这些状态的切换由计数器counter控制。

最后,traffic_lights信号根据当前状态控制交通灯的颜色,并在黄灯时启用蜂鸣器(beep_flag)。该系统逻辑简洁,通过状态机确保交通灯按顺序正常切换,适用于基础的交通灯控制应用。

蜂鸣器驱动代码(buzzer_pwm_control)的功能是为蜂鸣器生成一个固定频率的方波信号,从而控制蜂鸣器发出声音。

环境光和接近传感器

传感器分模块 - 副本.jpg

对于本活动任务三和任务四来说最重要的就是环境光和接近传感器的控制模块,这个模块实现了一个I2C通信的驱动,用于与RPR0521RS传感器进行数据交换。该传感器可用于测量环境光(ALS)、红外(IR)光和接近传感器数据。模块通过I2C协议控制传感器,读取并处理数据。

代码首先通过分频器生成一个400kHz的时钟信号,这个信号用于控制I2C通信的时序。接着,使用状态机管理I2C通信的各个步骤,包括起始信号(START)、写操作(WRITE)、读操作(READ)和停止信号(STOP)。每个状态下,模块会根据I2C协议与传感器进行数据交换。

在数据读取部分,模块依次读取传感器的不同寄存器,并将结果存储在相应的输出信号中。通过状态机的多次迭代,模块能够完成对传感器的配置、数据采集和返回数据。延时功能用于确保I2C时序的正确性。

最终,通过输出信号,模块将有效的数据(如光照、红外、接近数据)传递给外部系统,通过处理后显示在led上面。

任务三(感知距离)

PS距离.jpg

感知距离的工程里最重要的就是解码器模块,通过输入的16位数据和数据有效信号dat_valid来控制8个灯光的开关状态。代码里定义了三个寄存器dat0dat1dat2来存储数据,并通过状态机控制数据的更新。

每次dat_valid为高电平时,模块会更新dat0dat1dat2的值。如果当前数据与之前的数据(dat1dat0)相差过大(即大于2048),则保持dat2不变,避免由于噪声或数据异常引起的错误更新。否则,dat2将更新为dat0的值。

灯光输出控制基于dat2的高3位(dat2[9:7])。根据这3位的值,模块会设置light_out的值,从而控制8个灯的开关。

通过这种方式,模块根据输入数据的变化控制灯光的状态,当物体靠近传感器的时候,被点亮的led灯数量会变多。

任务四(感知光强)

ALS环境光.jpg

该工程实现了一个基于传感器输入数据的灯光控制模块,模块通过处理两个输入通道的数据(ch0_datch1_dat)来计算一个结果,并根据该结果控制灯光的开关。

首先,输入的ch0_datch1_dat分别赋值给dataAdataB。然后,通过一个always块,根据dataAdataB的大小关系计算出一个新的数据data。具体计算规则是基于多个条件,如果dataB小于dataA的某些倍数,data将根据不同的公式进行计算。如果不满足任何条件,data将被赋值为0。

接下来,模块将计算得到的data传递给bin_to_bcd模块,该模块将二进制数据转换为BCD格式。转换后的数据data_out用于判断灯光的输出。最终,如果data_out小于设定的阈值(15'd0100_0000_0000_0000),则输出信号light8'b00000000,表示所有灯光开启(即题目中的自动点亮路灯);否则,输出light8'b11111111,表示所有灯光关闭。

四、关键代码介绍

任务一、二

任务一和任务二的整体电路图如下图所示。

微信图片_20250222085803.png

对于该任务中显示数码管的部分由数码管驱动来完成(seg_deiver),采用两个74HC595级联来控制八个八位数码管,本次使用左边两个数码管来显示倒计时。

对于无源蜂鸣器来说,需要方波去驱动它,代码如下所示。

module buzzer_pwm_control (
input sys_clk, // 时钟(如50MHz)
input sys_rst_n, // 复位信号
input [1:0] beep_flag, // 蜂鸣器信号

output reg beep // 蜂鸣器控制信号
);

// 参数配置
parameter CLK_FREQ = 50_000_000; // 输入时钟频率(单位:Hz)
parameter BEEP_FREQ = 2_000; // 蜂鸣器频率(如2kHz)
localparam HALF_PERIOD = CLK_FREQ / (2 * BEEP_FREQ); // 半周期计数值

reg [31:0] counter; // 计数器

// 生成固定频率方波
always @(posedge sys_clk or negedge sys_rst_n)
begin
if (!sys_rst_n)
begin
counter <= 0;
beep <= 0;
end
else if (beep_flag) // 只有在beep_flag为1时才执行后面的代码
begin
if (counter >= HALF_PERIOD - 1)
begin
counter <= 0;
beep <= ~beep; // 翻转信号,生成方波
end
else
begin
counter <= counter + 1;
end
end
end

endmodule

counter是一个32位的计数器,用于记录经过的时钟周期。当计数器的值达到HALF_PERIOD - 1时,表示已过了一个半周期。

生成PWM方波每当sys_clk的上升沿到来时,counter计数器会递增。counter的值达到HALF_PERIOD - 1时,counter会重置为0,并且beep信号会翻转(即从高变低,或从低变高)。这就产生了一个频率为2kHz的PWM方波。

蜂鸣器启停控制beep_flag为非零值时,蜂鸣器控制逻辑生效。beep_flag的值用于决定是否生成PWM信号。如果beep_flag不为0,PWM信号会继续生成;如果beep_flag为0,counter计数器不再更新,beep信号会保持不变。


module countdown(
input sys_clk, // 时钟信号
input sys_rst_n, // 复位信号(低有效)

output reg beep_flag,

output reg [5:0] counter,
output reg [2:0] traffic_lights // 交通灯控制信号
);

// 状态定义
parameter S40 = 2'b00; // 40秒状态
parameter S05 = 2'b01; // 5秒状态
parameter S35 = 2'b10; // 30秒状态

reg [1:0] current_state; // 当前状态

reg [25:0] div_cnt; // 分频计数器(用于生成1秒使能)
reg one_sec_en; // 1秒使能信号

// 1秒时钟分频(12MHz时钟)
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
div_cnt <= 0;
one_sec_en <= 0;
end else begin
if (div_cnt == 26'd5_999_999) begin // 12MHz→1Hz
div_cnt <= 0;
one_sec_en <= ~one_sec_en;
end else begin
div_cnt <= div_cnt + 1;
end
end
end

// 状态机计数器控制
always @(posedge one_sec_en or negedge sys_rst_n) begin
if (!sys_rst_n) begin
current_state <= S40;
counter <= 35; // 初始状态40秒
end else begin
case (current_state)
S40: begin
if (counter > 0) begin
counter <= counter - 1;
end else begin // 倒计时结束转S05
current_state <= S05;
counter <= 5;
end
end
S05: begin
if (counter > 0) begin
counter <= counter - 1;
end else begin // 倒计时结束转S35
current_state <= S35;
counter <= 40;
end
end
S35: begin
if (counter > 0) begin
counter <= counter - 1;
end else begin // 倒计时结束转S40
current_state <= S40;
counter <= 35;
end
end
endcase
end
end

// 交通灯控制逻辑
always @(*) begin
case (current_state)
S40: traffic_lights = 3'b101;// 红灯
S05:begin
traffic_lights = 3'b100;
beep_flag = 1'b1;
end // 黄灯
S35: traffic_lights = 3'b110; // 绿灯
default: traffic_lights = 3'b111;
endcase
end

endmodule

该代码主要包含三个功能模块:1秒分频器、状态机和交通灯控制逻辑。系统通过12MHz时钟分频生成1Hz使能信号驱动状态机,定义了S40(40秒)、S05(5秒)、S35(35秒)三个状态循环切换。状态转移逻辑中,S40红灯状态结束后进入S05黄灯状态并触发蜂鸣器,随后转入S35绿灯状态,最终回到S40形成循环。倒计时计数器在每个状态结束时重置对应值,交通灯通过3位二进制编码控制(如101表示红灯)。

环境光和接近传感器

module rpr0521rs_driver(
input sys_clk, //系统时钟
input sys_rst_n, //系统复位,低有效

output i2c_scl_out, //I2C总线SCL
inout i2c_sda_in, //I2C总线SDA

output reg data_out, //数据有效脉冲
output reg [15:0] als_data, //ALS数据
output reg [15:0] ir_data, //IR数据
output reg [15:0] prox_data //Prox数据
);
// 定义计数器的常量 CNT_15
parameter CNT_15 = 15;

// 状态机的各个状态定义
localparam IDLE = 4'd0; // 空闲状态
localparam MAIN = 4'd1; // 主状态
localparam MODE1 = 4'd2; // 模式1
localparam MODE2 = 4'd3; // 模式2
localparam START = 4'd4; // 启动状态
localparam WRITE = 4'd5; // 写入状态
localparam READ = 4'd6; // 读取状态
localparam STOP = 4'd7; // 停止状态
localparam DELAY = 4'd8; // 延迟状态

// ACK 和 NACK 的定义,通常用于响应信号
localparam ACK = 1'b0; // ACK 响应信号
localparam NACK = 1'b1; // NACK 响应信号

// 声明一个 400KHz 时钟信号的寄存器和计数器
reg clk400KHz; // 用于存储 400KHz 时钟信号
reg [9:0] cnt400KHz; // 用于计数的 10 位计数器

// 使用一个 10 位计数器来生成 400KHz 时钟信号
always@(posedge sys_clk or negedge sys_rst_n) begin
// 异步复位
if(!sys_rst_n)
cnt400KHz <= 10'd0; // 复位时计数器清零
// 当计数器达到设定的 CNT_15(即15)时,重新计数
else if(cnt400KHz == CNT_15-1)
cnt400KHz <= 10'd0; // 计数器达到最大值时清零
else
cnt400KHz <= cnt400KHz + 1'b1; // 否则继续计数,增加 1
end

// 生成 400KHz 的时钟信号(clk_400KHz),通过翻转信号产生
always@(posedge sys_clk or negedge sys_rst_n) begin
// 异步复位
if(!sys_rst_n)
clk400KHz <= 1'b0; // 复位时 400KHz 时钟为 0
// 当计数器达到 CNT_15-1 时翻转 clk_400KHz 信号
else if(cnt400KHz == CNT_15-1)
clk400KHz <= ~clk400KHz; // 翻转 400KHz 时钟信号
end


reg i2c_scl,i2c_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 clk400KHz or negedge sys_rst_n) begin
if(!sys_rst_n) begin // 异步复位
// 初始化I2C信号和相关控制寄存器
i2c_scl <= 1'd1;
i2c_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 // 空闲状态,主要用于程序跑飞后的处理
i2c_scl <= 1'd1;
i2c_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; // 进入 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;
data_out <= 1'b0; // 延时
end
4'd5: begin
dev_addr <= 7'h38;
reg_addr <= 8'h44;
state <= MODE2; // 读取配置
end
4'd6: begin
prox_data <= {dat_h, dat_l}; // 读取数据
end
4'd7: begin
dev_addr <= 7'h38;
reg_addr <= 8'h46;
state <= MODE2; // 读取配置
end
4'd8: begin
als_data <= {dat_h, dat_l}; // 读取数据
end
4'd9: begin
dev_addr <= 7'h38;
reg_addr <= 8'h48;
state <= MODE2; // 读取配置
end
4'd10: begin
ir_data <= {dat_h, dat_l}; // 读取数据
end
4'd11: begin
data_out <= 1'b1; // 完成读取
end
default:
state <= IDLE; // 进入IDLE状态进行复位
endcase
end

// MODE1状态,单次写操作
MODE1: begin
if(cnt_mode1 >= 4'd5)
cnt_mode1 <= 1'b0; // 控制写操作
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; // 返回到 MAIN 状态
end
default:
state <= IDLE; // 进入IDLE状态进行复位
endcase
end
// MODE2状态,进行两次读操作
MODE2: begin
if(cnt_mode2 >= 4'd10)
cnt_mode2 <= 1'b0; // 控制读操作
else
cnt_mode2 <= cnt_mode2 + 1'b1;
state_back <= MODE2;
case(cnt_mode2)
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
state <= START;
end
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
4'd10: begin
state <= MAIN; // 返回到 MAIN 状态
end
default:
state <= IDLE; // 进入IDLE状态进行复位
endcase
end
// START状态,I2C通信起始信号
START: begin
if(cnt_start >= 3'd5)
cnt_start <= 1'b0; // 控制START信号时序
else
cnt_start <= cnt_start + 1'b1;
case(cnt_start)
3'd0: begin
i2c_sda <= 1'b1;
i2c_scl <= 1'b1;
end
3'd1: begin
i2c_sda <= 1'b1;
i2c_scl <= 1'b1;
end
3'd2: begin
i2c_sda <= 1'b0;
end
3'd3: begin
i2c_sda <= 1'b0;
end
3'd4: begin
i2c_scl <= 1'b0;
end
3'd5: begin
i2c_scl <= 1'b0;
state <= state_back; // 返回上一个状态
end
default:
state <= IDLE; // 进入IDLE状态进行复位
endcase
end
WRITE:begin //I2C通信时序中的写操作WRITE和相应判断操作ACK
if(cnt <= 3'd6) begin // 共需要发送8位的数据,这里控制循环次数
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

// 根据写操作计数器cnt_write的值,确定当前要执行的动作
case(cnt_write)
// 按照I2C的时序传输数据
3'd0: begin
i2c_scl <= 1'b0; // SCL拉低
i2c_sda <= data_wr[7-cnt]; // 控制SDA线输出对应的位数据
end
3'd1: begin
i2c_scl <= 1'b1; // SCL拉高,保持4.0us以上
end
3'd2: begin
i2c_scl <= 1'b1; // 保持SCL拉高
end
3'd3: begin
i2c_scl <= 1'b0; // SCL拉低,准备发送下1bit数据
end
// 获取从设备的响应信号并判断
3'd4: begin
i2c_sda <= 1'bz; // 释放SDA线,准备接收从设备的响应信号
end
3'd5: begin
i2c_scl <= 1'b1; // SCL拉高,保持4.0us以上
end
3'd6: begin
ack_flag <= i2c_sda_in; // 获取从设备的响应信号并判断
end
3'd7: begin
i2c_scl <= 1'b0; // SCL拉低
// 如果没有接收到ACK(ack_flag为0),则回到写操作状态
if(ack_flag)
state <= state;
else
state <= state_back; // 如果接收到ACK,保持状态;否则转到恢复状态
end
default:
state <= IDLE; // 如果程序失控,进入IDLE自复位状态
endcase
end
READ: begin // I2C通信时序中的读操作READ和返回ACK的操作
if(cnt <= 3'd6) begin // 共需要接收8位数据,控制循环次数
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

// 根据读操作计数器cnt_read的值,确定当前要执行的动作
case(cnt_read)
// 按照I2C的时序接收数据
3'd0: begin
i2c_scl <= 1'b0; // SCL拉低,释放SDA线,准备接收从设备数据
i2c_sda <= 1'bz; // 释放SDA线
end
3'd1: begin
i2c_scl <= 1'b1; // SCL拉高,保持4.0us以上
end
3'd2: begin
data_r[7-cnt] <= i2c_sda_in; // 读取从设备返回的数据并保存
end
3'd3: begin
i2c_scl <= 1'b0; // SCL拉低,准备接收下1bit数据
end
// 向从设备发送响应信号
3'd4: begin
i2c_sda <= ack; // 发送ACK或NACK信号
end
3'd5: begin
i2c_scl <= 1'b1; // SCL拉高,保持4.0us以上
end
3'd6: begin
i2c_scl <= 1'b1; // 保持SCL拉高
end
3'd7: begin
i2c_scl <= 1'b0; // SCL拉低
state <= state_back; // 返回主状态
end
default:
state <= IDLE; // 如果程序失控,进入IDLE自复位状态
endcase
end
STOP: begin // I2C通信时序中的结束STOP操作
if(cnt_stop >= 3'd5)
cnt_stop <= 1'b0; // 计数器归零
else
cnt_stop <= cnt_stop + 1'b1; // 计数器递增

// 根据STOP操作计数器cnt_stop的值,确定当前要执行的动作
case(cnt_stop)
3'd0: begin
i2c_sda <= 1'b0; // SDA拉低,准备发送STOP信号
end
3'd1: begin
i2c_sda <= 1'b0; // SDA拉低,准备发送STOP信号
end
3'd2: begin
i2c_scl <= 1'b1; // SCL提前SDA拉高,准备结束传输
end
3'd3: begin
i2c_scl <= 1'b1; // SCL保持拉高
end
3'd4: begin
i2c_sda <= 1'b1; // SDA拉高,完成STOP信号的发送
end
3'd5: begin
i2c_sda <= 1'b1; // SDA保持高电平
state <= state_back; // 完成STOP操作,返回主状态
end
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_out = i2c_scl; //对SCL端口赋值
assign i2c_sda_in = i2c_sda; //对SDA端口赋值

endmodule

该驱动模块主要包括以下几个内容:

时钟生成:通过系统时钟分频产生400kHz的I2C时钟信号,使用10位计数器和时钟翻转逻辑实现精确分频。

状态机控制:采用多状态机架构(包含IDLE/MAIN/MODE等9个状态),实现以下核心操作:

初始化配置:通过MODE1状态写入4个寄存器(0x40-0x43),设置传感器工作模式

周期数据采集:在MAIN状态循环读取3个数据寄存器(0x44 Prox/0x46 ALS/0x48 IR)

I2C协议时序:精细控制START/STOP信号、8位数据读写、ACK/NACK响应等底层时序

数据接口

输出16位光学数据(ALS/IR/Prox)

提供数据有效脉冲data_out

符合I2C规范的SCL/SDA双向接口

时序控制

写入配置后插入12ms延时(DELAY状态)

每个I2C信号相位保持2.5μs(400kHz时钟)

采用计数器精确控制信号持续时间

任务三

对于任务三来说除了驱动代码最关键的就是对数据的处理代码。

module decoder (
input dat_valid, // 数据有效信号
input [15:0] data, // 输入的16位数据
output reg [7:0] light_out // 输出的8位灯光控制信号
);

// 定义寄存器
reg [15:0] dat0, dat1, dat2;

// 数据有效时,更新寄存器
always @(posedge dat_valid) begin
dat0 <= data;
dat1 <= dat0;
if (((dat1 - dat0) >= 16'h800) || ((dat0 - dat1) >= 16'h800))
dat2 <= dat2; // 如果数据差异过大,保持dat2不变
else
dat2 <= dat0; // 否则更新dat2为dat0
end

// 根据dat2的高3位进行灯光输出控制
always @(dat2[9:7]) begin
case (dat2[9:7])
3'b000: light_out = 8'b11111110;
3'b001: light_out = 8'b11111100;
3'b010: light_out = 8'b11111000;
3'b011: light_out = 8'b11110000;
3'b100: light_out = 8'b11100000;
3'b101: light_out = 8'b11000000;
3'b110: light_out = 8'b10000000;
3'b111: light_out = 8'b00000000;
default: light_out = 8'b11111111; // 默认输出
endcase
end

endmodule

这段代码实现了一个带数据稳定滤波的光强和灯光映射解码器,主要功能分为两个阶段:

数据稳定处理
dat_valid上升沿触发时,通过三级寄存器(dat0/dat1/dat2)进行数据缓存,并计算相邻两次数据的差值:

若相邻数据跳变超过±2048(0x800阈值),判定为异常波动,锁定输出值dat2保持不变

若变化在合理范围内,则更新dat2为最新采样值dat0
该设计可有效抑制传感器数据突变导致的灯光闪烁问题

光强分级灯光控制
将稳定后的dat2数据高3位([9:7])作为光强等级索引:

3位二进制值对应8个光强等级(000-111)

每个等级激活更多LED(从右向左点亮),如:

默认状态11111111全灭,提供异常保护机制

任务四

module decoder(
input sys_rst_n, // 系统复位,低有效
input [15:0] ch0_dat, // 通道0输入数据
input [15:0] ch1_dat, // 通道1输入数据
output [7:0] light // 灯光输出
);

// 定义中间数据线
wire [31:0] dataA, dataB; // 用于存储通道0和通道1的数据
reg [31:0] data; // 用于存储计算后的数据结果

// 将输入数据赋值给dataA和dataB
assign dataA = ch0_dat; // 通道0数据
assign dataB = ch1_dat; // 通道1数据

// 根据dataA和dataB的关系进行条件判断,计算出data
always @(dataA or dataB) begin
if (dataB < dataA * 595)
data = dataA * 1682 - dataB * 1877; // 第一个条件:当dataB小于dataA的595倍时
else if (dataB < dataA * 1015)
data = dataA * 644 - dataB * 132; // 第二个条件:当dataB小于dataA的1015倍时
else if (dataB < dataA * 1352)
data = dataA * 756 - dataB * 243; // 第三个条件:当dataB小于dataA的1352倍时
else if (dataB < dataA * 3053)
data = dataA * 766 - dataB * 25; // 第四个条件:当dataB小于dataA的3053倍时
else
data = 0; // 如果以上条件都不满足,则data赋值为0
end

// BCD转换模块实例化,将二进制数据转换为BCD格式
wire [31:0] data_out; // 存储BCD转换后的数据

bin_to_bcd bin_to_bcd_inst (
.sys_rst_n(sys_rst_n), // 系统复位信号,低有效
.data_in(data), // 输入需要转换的二进制数据
.data_out(data_out) // 输出转换后的BCD数据
);

// 根据BCD转换后的数据判断light的输出
assign light = (data_out < 15'd0100_0000_0000_0000) ? 8'b00000000 : 8'b11111111;
// 如果data_out小于BCD阈值,则打开所有灯光(输出0)
// 否则关闭所有灯光(输出11111111)

endmodule

该模块实现了一个基于双通道数据的光强阈值灯光控制器,其核心功能分为三部分:

双通道数据混合计算
输入通道数据ch0_datch1_dat被扩展为32位dataA/dataB,通过四级分段线性变换生成中间值data

根据dataBdataA的倍数关系(595/1015/1352/3053),选择不同系数进行dataA*K1 - dataB*K2运算

分段设计可能用于补偿传感器特性或实现非线性映射

二进制到BCD转换
通过bin_to_bcd模块将32位计算结果转换为BCD编码data_out,便于十进制阈值比较(注:阈值写法15'd0100_0000_0000_0000存在位宽错误,15位BCD无法表示400000,实际应为32位BCD码比较)

灯光二值化输出
当BCD结果低于阈值时,light输出全灭(00000000),否则全亮(11111111),实现光强开关控制

在比较两个有效且位数相同的BCD码时,若一个BCD码的二进制值大于另一个,其对应的十进制数值也必然更大。因此这里的对比是转换后的数字和设定好的阈值的BCD码进行比较。

本次项目所用到的全部工程均可在附件中找到。

五、功能展示图及说明

任务一、二

现有35s的绿灯倒计时,之后是5s的黄灯倒计时同时蜂鸣器报警提醒驾驶员,最后40秒的红灯倒计时,以此往复。


12.jpg

任务三

无物体靠近时只有最上面的led灯被点亮,随着物体距离传感器的位置越来越近,被点亮的led灯会越来越多。


距离.jpg

任务四

在正常室内光线的时候,无led被点亮,当遮挡住室内光源的时候,所有的led灯均被点亮。


环境光.jpg

六、项目中遇到的难题和解决方法

1. 图形化编程的局限性,本次WebIDE推出了的拖拽式编程,第一次使用这样的编程方式,刚开始遇到了许许多多的困惑,虽然降低了代码编写的难度,但构建包含数码管动态扫描、蜂鸣器多频报警的复杂时序系统时,还是遇到了很多的困难,在电子森林的老师们悉心教导之后,最终成功的完成了本次任务一和任务二的电路构建。

2.WebIDE在查找代码bug的时候没有逻辑分析仪或者SignalTap等专业的检测工具,希望以后WebIDE可以推出类似SignalTap的工具。对此本次活动的后两个任务在Diamond 3.13上进行建立工程编写代码查找bug等工作,最后成功实现感光和测距的功能。

3.在任务四中,对于环境光的检测,需要在光线变暗的时候点亮led灯,对此要设置一个合适的阈值,室内和室外的光线强度还是有所不同,对此我在实验室开着所有灯的情况下多次实验,最后设置了一个在室内的合适的阈值,当我在遮住光源的时候,开发板上的led灯会被全部点亮。

七、心得体会

​在参与了本次电子森林融合图形化编程与硬件设计的交通灯和传感器项目,我收获了很多宝贵的经历。
通过WebIDE图形化编程实现数码管动态显示与蜂鸣器报警控制,我掌握时序逻辑的具象化设计方法。建立硬件时序与软件中断的协同思维,而对74HC595芯片搭建数码管主控电路这一过程更加的了解。项目中Verilog模块(接近传感器与环境光检测)使我突破单一技术栈的局限。例如,传感器信号预处理需编写Verilog状态机。这种软硬协同调试过程,能培养系统级问题定位能力——例如当环境光检测异常导致路灯误触发时,需同步检查信号采样代码(Verilog)与LED驱动电路的匹配性。

八、FPGA资源占用

任务一、二

image.png

任务三

image.png

任务四

image.png

附件下载
任务一二.zip
任务一二的工程文件
任务三.zip
任务三的工程文件
任务四.zip
任务四的工程文件
团队介绍
学生,参加并完成了电子森林2024寒假在家一起练的小脚丫FPGA开发板活动
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号