项目任务需求
-
实现一个可定时时钟的功能,用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟即可,到整点的时候比如8:00,蜂鸣器报警,播放音频信号,最长可持续30秒;
-
实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度,并同时间一起显示在OLED的屏幕上;
-
定时时钟整点报警的同时,将温度信息通过UART传递到电脑上,电脑上能够显示当前板子上的温度信息(任何显示形式都可以),要与OLED显示的温度值一致;
-
PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件,OLED屏幕上显示的时间信息和温度信息都停住不再更新;
-
音频文件播放完毕,OLED开始更新时间信息和当前的温度信息
项目实现过程
模块功能设计
main主程序
在main界面,主要任务为调整好各信号间的输入输出关系以及pin脚的使用,下面仅显示部分代码,后续内容仅为导入其他模块的内容。
module xjy_main(
input clk,
input key_hour,//小时+
input key_min_a,//分钟+
input key_min_m,//分钟-
input rst_n,//复位00
input uart_rxd,
input uart_on,
inout tone_en,
inout one_wire,
output oled_csn, //使能信号
output oled_rst, //复位信号
output oled_dcn, //指令控制信号
output oled_clk, //时钟信号
output oled_dat, //数据信号
output uart_out,
output beeper
);
wire[15:0] d_out;
wire[3:0] temp1,temp2,temp3,temp4;
wire[3:0] hour1,hour2,min1,min2,sec1,sec2;
wire flag;
OLED模块
在OLED模块处的任务为显示数据信息,根据显示情况表在128*32的点阵上显示字符,由时钟控制OLED屏幕进行定时的刷新,此外OLED要保持好与时钟模块和温度传感模块的数据信息传递,代码基本信息参考电子森林。
https://www.eetree.cn/wiki/oled_spi_verilog
MAIN:begin
if(cnt_main >= 5'd6) cnt_main <= 5'd5;
else cnt_main <= cnt_main + 1'b1;
case(cnt_main) //MAIN状态
5'd0: begin state <= INIT; end
5'd1: begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "TIME: "; state <= SCAN; end
5'd2: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "TEMP: "; state <= SCAN; end
5'd3: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " "; state <= SCAN; end
5'd4: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " "; state <= SCAN; end
5'd5: begin
y_p <= 8'hb0; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd 5; char <= {4'd0, hour1, 4'd0,hour2, ":", 4'd0,min1, 4'd0, min2}; state <= SCAN;
end
5'd6:if(temp1 == 4'd2)
begin
y_p <= 8'hb3; x_ph <= 8'h12; x_pl <= 8'h08; num <= 5'd 6; char <= {"-", 4'd0, temp3, 4'd0, temp4, ".", 4'd0, temp1,8'h7b,"C"}; state <= SCAN;
end
else if(temp2 == 4'd1)
begin
y_p <= 8'hb3; x_ph <= 8'h12; x_pl <= 8'h08; num <= 5'd 6; char <= {4'd0, temp2, 4'd0, temp3, 4'd0, temp4, ".", 4'd0, temp1,8'h7b,"C"}; state <= SCAN;
end
else
begin
y_p <= 8'hb3; x_ph <= 8'h12; x_pl <= 8'h08; num <= 5'd 6; char <= {4'd0, temp3, 4'd0, temp4, ".", 4'd0, temp1,8'h7b,"C"}; state <= SCAN;
end
default: state <= IDLE;
endcase
end
INIT:begin //初始化状态
case(cnt_init)
5'd0: begin oled_rst <= LOW; cnt_init <= cnt_init + 1'b1; end
5'd1: begin num_delay <= 16'd25000; state <= DELAY; state_back <= INIT; cnt_init <= cnt_init + 1'b1; end //延时大于3us
5'd2: begin oled_rst <= HIGH; cnt_init <= cnt_init + 1'b1; end
5'd3: begin num_delay <= 16'd25000; state <= DELAY; state_back <= INIT; cnt_init <= cnt_init + 1'b1; end //延时大于220us
5'd4: begin
if(cnt>=INIT_DEPTH) begin
cnt <= 1'b0;
cnt_init <= cnt_init + 1'b1;
end else begin
cnt <= cnt + 1'b1; num_delay <= 16'd5;
oled_dcn <= CMD; char_reg <= cmd[cnt]; state <= WRITE; state_back <= INIT;
end
end
5'd5: begin cnt_init <= 1'b0; state <= MAIN; end //初始化完成,返回MAIN状态
default: state <= IDLE;
endcase
end
温度传感模块
温度传感模块主要利用FPGA驱动底板上的DS18B20Z单总线温度传感器进行温度数据的采集。在此环节中,我们完成对采集到的温度信息传送到OLED上显示。电子森林代码参考:
https://www.eetree.cn/wiki/temp_sensor_verilog
下面仅展示部分代码
READ:begin //按照DS18B20Z芯片单总线时序进行读操作
if(cnt <= 3'd6) begin //共需要接收8bit的数据,这里控制循环的次数
if(cnt_read >= 3'd5) 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'd6) begin cnt_read <= 1'b0; cnt <= 1'b0; end //两个变量都恢复初值
else begin cnt_read <= cnt_read + 1'b1; cnt <= cnt; end
end
case(cnt_read)
//读取 1bit 数据的用时在60~120us之间,总线拉低后15us时间内读取数据,参考数据手册
3'd0: begin one_wire_buffer <= 1'b0; end //总线拉低
3'd1: begin num_delay <= 20'd2;state <= DELAY;state_back <= READ; end //延时2us时间
3'd2: begin one_wire_buffer <= 1'bz; end //总线释放
3'd3: begin num_delay <= 20'd5;state <= DELAY;state_back <= READ; end //延时5us时间
3'd4: begin temperature_buffer[cnt] <= one_wire; end //读取DS18B20Z返回的总线数据,先收最低位
3'd5: begin num_delay <= 20'd60;state <= DELAY;state_back <= READ; end //延时60us时间
//back to main
3'd6: begin state <= MAIN; end //返回MAIN状态
default: state <= IDLE;
endcase
end
DELAY:begin //延时控制
if(cnt_delay >= num_delay) begin //延时控制,延时时间由num_delay指定
cnt_delay <= 1'b0;
state <= state_back; //很多状态都需要延时,延时后返回哪个状态由state_back指定
end else cnt_delay <= cnt_delay + 1'b1;
end
时钟控制与分频模块
在该部分一方面对于时间24小时进制,分钟60进制循环进行控制,另一方面基于12MHz的晶振进行所需周期频率clock信号的代码设计,内容基本与直播课上展示的一致,此基本主要参考了刘建伟同学的代码,部分代码如下:
always@(posedge clk1, negedge rst_n)
begin
if(!rst_n)
begin
hour1_q <= 4'd0;
hour2_q <= 4'd0;
min1_q <= 4'd0;
min2_q <= 4'd0;
sec1_q <= 4'd0;
sec2_q <= 4'd0;
end
else
begin
case({key_hour, key_min_a, key_min_m})
3'b111:
begin
if(sec2_q < 9)
sec2_q<= sec2_q + 1;
else
begin
sec2_q <= 4'd0;
if(sec1_q < 5)
sec1_q <= sec1_q + 1;
else
begin
sec1_q <= 4'd0;
if(min2_q < 9)
min2_q <= min2_q + 1;
else
begin
min2_q <= 4'd0;
if(min1_q < 5)
min1_q <= min1_q + 1;
else
begin
min1_q <= 4'd0;
if(hour2_q == 3 && hour1_q == 2)
begin
hour2_q <= 0;
hour1_q <= 0;
end
else if(hour2_q < 9)
hour2_q <= hour2_q + 1;
else
begin
hour2_q<= 4'd0;
if(hour1_q < 2)
hour1_q <= hour1_q + 1;
else
hour1_q <= 0;
end
end
end
end
end
end
3'b110://小时减少
begin
if(hour2_q > 0)
hour2_q <= hour2_q - 1;
else//five_now = 0
begin
if(hour1_q == 2)
begin
hour2_q <= 9;
hour1_q <= 1;
end
else if(hour1_q == 1)
begin
hour2_q <= 9;
hour1_q <= 0;
end
else//if(six_now == 0)
begin
hour2_q <= 3;
hour1_q <= 2;
end
end
end
3'b101://分钟增加
begin
if(min2_q > 0)
min2_q <= min2_q - 1;
else
begin
min2_q <= 9;
if(min1_q> 0)
min1_q <= min1_q - 1;
else
min1_q<= 5;
end
end
default://分钟减少
begin
if(min2_q < 9)
min2_q <= min2_q + 1;
else
begin
min2_q <= 0;
if(min1_q < 5)
min1_q <= min1_q + 1;
else
min1_q <= 0;
end
end
endcase
end
end
蜂鸣器模块
该模块主要是根据自己所需要的音乐设置相应频率所对应的代码,通过不同频率的音符播放实现响铃功能。电子森林代码参考:
https://www.eetree.cn/wiki/%E8%9C%82%E9%B8%A3%E5%99%A8%E6%A8%A1%E5%9D%97
//设定音乐乐谱
always @(posedge clk)
begin
if(count1 < TIME)
count1 = count1 + 1'b1;
else
begin
count1 = 24'd0;
if(state == 8'd63)
state = 8'd0;
else
state = state + 1'b1;
case(state)
8'd0:count_end =M1;
8'd1:count_end=M1;
8'd2:count_end=M2;
8'D3:count_end=M1;
8'D4:count_end=M5;
8'D5:count_end=M3;
8'D6:count_end=M5;
8'D7:count_end=M1;
8'D8:count_end=M1;
8'D9:count_end=M2;
8'D10:count_end=M3;
8'D11:count_end=M5;
8'D12,8'D13:count_end=M2;
8'D14:count_end=L5;
8'D15:count_end=L6;
8'D16:count_end=L6;
8'D17:count_end=L3;
8'D18:count_end=M2;
8'D19:count_end=M1;
8'D20:count_end=M1;
8'D21:count_end=L7;
8'D22:count_end=M3;
8'D23:count_end=M2;
8'D24:count_end=M2;
8'D25:count_end=L5;
8'D26:count_end=L6;
8'D27:count_end=L6;
8'D28:count_end=M1;
8'D29:count_end=M2;
8'D30:count_end=M3;
8'D31:count_end=M5;
8'd32:count_end = M3;
8'd33:count_end=M2;
8'd34:count_end=L6;
8'D35:count_end=L6;
8'D36:count_end=L7;
8'D37:count_end=M2;
8'D38:count_end=L6;
8'D39:count_end=L5;
8'D40,8'D41:count_end=M5;
8'D42:count_end=M5;
8'D43:count_end=M5;
8'D44:count_end=M6;
8'D45:count_end=M3;
8'D46:count_end=M5;
8'D47:count_end=M3;
8'D48:count_end=M2;
8'D49:count_end=L7;
8'D50:count_end=M5;
8'D51:count_end=M5;
8'D52:count_end=M3;
8'D53:count_end=M2;
8'D54:count_end=L5;
8'D55:count_end=M1;
8'D56:count_end=M2;
8'D57:count_end=M1;
8'D58:count_end=L6;
8'D59:count_end=M1;
8'D60:count_end=M2;
8'D61:count_end=M3;
8'D62:count_end=M6;
8'D63:count_end=M5;
default: count_end = 16'h0;
endcase
end
end
BCD码模块
将温度信息以BCD码的形式进行输入,代码参考杨晓楠同学:
always@*
begin
scanch = data_in;
if(scanch[15:12] == 4'b1111)
begin
scanch = ~scanch + 1'b1;
d_outq2 = 4'd2;
end
else
d_outq2 = 4'b0;
if(scanch >> 8 >= 4'b0110)
begin
d_outq2 = 4'b1;
scanch = scanch- 12'b0110_0100_0000;
end
if(scanch>>4 >= 8'b0101_1010)
begin
d_outq3 = 4'd9;
scanch = scanch - 12'b0101_1010_0000;
end
else if(scanch>>4 >= 8'b0101_0000)
begin
d_outq3 = 4'd8;
scanch = scanch - 12'b0101_0000_0000;
end
else if(scanch>>4 >= 8'b0100_0110)
begin
d_outq3 = 4'd7;
scanch = scanch - 12'b0100_0110_0000;
end
else if(scanch>>4 >= 8'b0011_1100)
begin
d_outq3 = 4'd6;
scanch = scanch - 12'b0011_1100_0000;
end
else if(scanch>>4 >= 8'b0011_0010)
begin
d_outq3 = 4'd5;
scanch = scanch - 12'b0011_0010_0000;
end
else if(scanch>>4 >= 8'b0010_1000)
begin
d_outq3 = 4'd4;
scanch = scanch - 12'b0010_1000_0000;
end
else if(scanch>>4 >= 8'b0001_1110)
begin
d_outq3 = 4'd3;
scanch = scanch - 12'b0001_1110_0000;
end
else if(scanch>>4 >= 8'b0001_0100)
begin
d_outq3 = 4'd2;
scanch = scanch- 12'b0001_0100_0000;
end
else if(scanch>>4 >= 8'b0000_1010)
begin
d_outq3 = 4'd1;
scanch = scanch - 12'b0000_1010_0000;
end
else
d_outq3 = 4'd0;
if(scanch >>4 >= 4'd9)
begin
d_outq4 = 4'd9;
scanch = scanch - 4'd9;
end
else if(scanch >>4 >= 4'd8)
begin
d_outq4 = 4'd8;
scanch = scanch - 4'd8;
end
else if(scanch >>4 >= 4'd7)
begin
d_outq4 = 4'd7;
scanch = scanch - 4'd7;
end
else if(scanch >>4 >= 4'd6)
begin
d_outq4 = 4'd6;
scanch = scanch - 4'd6;
end
else if(scanch >>4 >= 4'd5)
begin
d_outq4 = 4'd5;
scanch = scanch - 4'd5;
end
else if(scanch >>4 >= 4'd4)
begin
d_outq4 = 4'd4;
scanch = scanch - 4'd4;
end
else if(scanch >>4 >= 4'd3)
begin
d_outq4 = 4'd3;
scanch = scanch - 4'd3;
end
else if(scanch >>4 >= 4'd2)
begin
d_outq4 = 4'd2;
scanch = scanch - 4'd2;
end
else if(scanch >>4 >= 4'd1)
begin
d_outq4 = 4'd1;
scanch = scanch - 4'd1;
end
else
d_outq4 = 4'd0;
scanch = (scanch <<12) >> 12;
if(scanch >= 4'd14)
d_outq1 = 4'd9;
else if(scanch >= 13)
d_outq1 = 4'd8;
else if(scanch >= 11)
d_outq1 = 4'd7;
else if(scanch >= 9)
d_outq1 = 4'd6;
else if(scanch >= 8)
d_outq1 = 4'd5;
else if(scanch >= 6)
d_outq1 = 4'd4;
else if(scanch >= 4)
d_outq1 = 4'd3;
else if(scanch >= 3)
d_outq1= 4'd2;
else if(scanch >= 1)
d_outq1 = 4'd1;
else
d_outq1 = 4'd0;
end
串口发送接收模块
主要利用第三方串口调试工具进行,在这里使用了XCOM工具2.6版本,故在代码中主要完成接口的匹配和数据的线路控制以及存储即可。该模块主要参考了李卓然同学的代码。
module uart_send
(
input clk_in,
input [3:0] temp1,temp3,temp4,
input [3:0] hour1,hour2,min1,min2,sec1,sec2,
input uart_sw,//发送串口开关
output reg uart_out//输出
);
localparam IDLE = 2'b0;
localparam SEND = 2'b1;
reg clk_uart;
reg[9:0] times;
always @(posedge clk_in) begin
if(times < 625)
times = times+1;
else begin
clk_uart = ~clk_uart;
times = 0;
end
end
reg clk_en;
reg [23:0] clk_en_cnt;
always @(posedge clk_in) begin
if (clk_en_cnt <= 12_000_000) begin
clk_en_cnt = clk_en_cnt + 1;
end else begin
clk_en_cnt = 0;
clk_en = ~clk_en;
end
end
reg flag_1,flag_2,state;
reg [120:0] uart_data;
reg [7:0] i;
always @(posedge clk_en)
begin
if((sec1 == 2 && sec2 == 0 &&min1 == 0 && min2 == 0 && uart_sw) || (temp3 >= 3 && temp4 >= 4))
begin
uart_data = { //输送串口数据
1'd1,8'd13,1'd0,
//1'd1,8'd10,1'd0,
1'd1,4'd3,min2,1'd0,
1'd1,4'd3,min1,1'd0,
1'd1,8'd58,1'd0,
1'd1,4'd3,hour2,1'd0,
1'd1,4'd3,hour1,1'd0,
1'd1,8'd32,1'd0,
1'd1,8'd67,1'd0,
1'd1,4'd3,temp1,1'd0,
1'd1,8'd46,1'd0,
1'd1,4'd3,temp4,1'd0,
1'd1,4'd3,temp3,1'd0,
1'd1,1'd1
};
flag_1 = ~flag_1;
end
end
always @(posedge clk_uart) begin
case(state)
IDLE: begin
if(flag_2 != flag_1) begin
flag_2 = flag_1;
state = SEND;
end
end
SEND: begin
if(i < 122) begin
uart_out = uart_data[i];
i = i+1;
end else begin
i = 0;
state = IDLE;
end
end
endcase
end
endmodule
module uart_receive(
input clk, //系统时钟
input rst_n, //系统复位
input uart_rxd, //UART接收端口
output reg uart_en,
output reg [7:0] uart_data
);
//时钟分频
parameter CLK_FREQ = 12_000_000;
parameter UART_BPS = 9600;
localparam BPS_CNT = CLK_FREQ/UART_BPS;
//时钟计数
reg uart_rxd0;
reg uart_rxd1;
reg [15:0] clk_cnt;
reg [ 3:0] rx_cnt;
reg rx_flag;
reg [ 7:0] rxdata;
wire wflag;
assign wflag = uart_rxd1 & (~uart_rxd0);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
uart_rxd0 <= 1'b0;
uart_rxd1 <= 1'b0;
end
else begin
uart_rxd0 <= uart_rxd;
uart_rxd1 <= uart_rxd0;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
rx_flag <= 1'b0;
else begin
if(wflag)
rx_flag <= 1'b1;
else if((rx_cnt == 4'd9)&&(clk_cnt == BPS_CNT/2))
rx_flag <= 1'b0;
else
rx_flag <= rx_flag;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk_cnt <= 16'd0;
rx_cnt <= 4'd0;
end
else if ( rx_flag ) begin
if (clk_cnt < BPS_CNT - 1) begin
clk_cnt <= clk_cnt + 1'b1;
rx_cnt <= rx_cnt;
end
else begin
clk_cnt <= 16'd0;
rx_cnt <= rx_cnt + 1'b1;
end
end
else begin
clk_cnt <= 16'd0;
rx_cnt <= 4'd0;
end
end
always @(posedge clk or negedge rst_n) begin
if ( !rst_n)
rxdata <= 8'd0;
else if(rx_flag)
if (clk_cnt == BPS_CNT/2) begin
case ( rx_cnt )
4'd1 : rxdata[0] <= uart_rxd1;
4'd2 : rxdata[1] <= uart_rxd1;
4'd3 : rxdata[2] <= uart_rxd1;
4'd4 : rxdata[3] <= uart_rxd1;
4'd5 : rxdata[4] <= uart_rxd1;
4'd6 : rxdata[5] <= uart_rxd1;
4'd7 : rxdata[6] <= uart_rxd1;
4'd8 : rxdata[7] <= uart_rxd1;
default:;
endcase
end
else
rxdata <= rxdata;
else
rxdata <= 8'd0;
end
//数据接收并寄存输出接收到的数据
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
uart_data <= 8'd0;
uart_en <= 1'b0;
end
else if(rx_cnt == 4'd9) begin
uart_data <= rxdata;
uart_en <= 1'b1;
end
else begin
uart_data <= 8'd0;
uart_en<= 1'b0;
end
end
endmodule
项目难题与进展
我本人对于FPGA完全不了解,从0开始相对有些不习惯,对于verilog的语法编写,对Diamond软件的使用都显得极为不熟练。
对于项目目标而言,所有数据的采集、显示、收发功能都基本实现,由于在操作中不够熟练,经常会长期使用综合训练板,导致温度长期偏高且跨度很大,故任务目标4中对于达到报警温度的数据控制并未实现,但后续功能以及完成。
在实验过程中,由于一开始对于XCOM工具的版本使用不当,经常在数据收发环节出现问题,耽误了很多的时间。
未来计划与期望
完全实现所有预期功能。
解决目前明显的延迟问题和加入按键防抖功能。