前言:
很早就听说过FPGA,那时对FPGA的唯一印象就是用代码写硬件,然而这很让人费解。在上个学期的课程中一门数字逻辑设计的课程实验中便使用到了FPGA,通过几次数电实验,我渐渐地对FPGA有了初步的认识并产生了兴趣。在本次“寒假一起练活动中”,我对FPGA有了更近一步的理解,其不同于51,STM32等单片机开发,FPGA并行执行的逻辑与单片机的串行执行逻辑有很大区别,需要用新的思维和思路去完成本次的任务,然而本次项目对于几乎没有任何FPGA基础的我而言很困难,陌生的编程语言、开发环境不熟悉、各种项目要求都增加了这个项目的挑战性。
项目要求:
1.实现一个可定时时钟的功能,用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟即可,到整点的时候比如8:00,蜂鸣器报警,播放音频信号,最长可持续30秒;
2.实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度,并同时间一起显示在OLED的屏幕上;
3.定时时钟整点报警的同时,将温度信息通过UART传递到电脑上,电脑上能够显示当前板子上的温度信息(任何显示形式都可以),要与OLED显示的温度值一致;
4.PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件,OLED屏幕上显示的时间信息和温度信息都停住不再更新;
5.音频文件播放完毕,OLED开始更新时间信息和当前的温度信息
设计思路:
我把整个项目要求分为了以下几个模块进行:OLED显示、时钟信号、DS18B20温度采集、串口通信、蜂鸣器使能、按键输入。
当然仅仅是做一个小小的模块规划是完全不够的,由于所有的代码都需要使用到Verilog这门全新的语言,所以我不得不花一些时间在Bilibili和CSDN上学习这门语言的基础语法,并且看各个博主的代码以理解其编程思路,从0开始完成项目。在学习过程中遇到了很多问题,比如阻塞赋值和非阻塞赋值,这二者在我初学的时候被弄混了很多次。此外,从串行执行的逻辑思维转换为并行执行的逻辑思维,我花了不少时间过渡去理解。
对于这个项目,我的思路是以OLED显示为中心,其他功能围绕OLED展开设计,因为OLED是整个项目中最直观的能看出效果的模块。
其次就是实时时钟,时钟这个素材也经常在蓝桥杯比赛中作为重点考察项目,然而区别就是,并没有DS1302或者STM32内部RTC这种现成的时钟,需要自己产生时钟信号去计数,这也是难点之一,尽管没有要求,我还是精确到秒进行显示,这样比较直观。
接下来的温度显示,按键设置,都是基于OLED显示去进行操作的,所以我打算把串口通信和蜂鸣器报响的功能放在了最后去完成。
串口通信的上位机我打算采用STC的串口助手,也就是51单片机编程烧录的那个软件。
以上是我大体的项目完成思路。
资源占用:
资源报告图如下:
显而易见的,作为一名初学者我还并不能够很好地利用资源,也能够看出资源占用很多,不过能完成项目,这也是达到了我的要求。
代码实现:
以下只列出部分代码:
(1)1KHz时钟信号以及秒钟时序
在获得1KHz的信号后,也就能够确定秒了,接下来分钟和小时的设定只需要按照时钟的进制去完成即可。
always @(posedge Clk_1s, negedge Rst_n)//Second
if (!Rst_n)
sec_l <= 4'd0;
else if (sec_l == 4'd9)
sec_l <= 4'd0;
else if (key1)
sec_l <= timer_sec_l;
else
sec_l <= sec_l + 1'b1;
always @(posedge Clk, negedge Rst_n)//1Hz信号
if (!Rst_n)begin
Clk_1s <= 1'd0;
cnt_1s <= 24'd0;
end
else if (cnt_1s == 24'd12_000_000 - 1) begin
Clk_1s <= 1'd1;
cnt_1s <= 24'd0;
end
else begin
Clk_1s <= 1'd0;
cnt_1s <= cnt_1s + 1'b1;
end
OLED显示部分:
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
SCAN:begin //刷屏状态,从RAM中读取数据刷屏
if(cnt_scan == 5'd11) begin
if(num) cnt_scan <= 5'd3;
else cnt_scan <= cnt_scan + 1'b1;
end else if(cnt_scan == 5'd12) cnt_scan <= 1'b0;
else cnt_scan <= cnt_scan + 1'b1;
case(cnt_scan)
5'd0: begin oled_dcn <= CMD; char_reg <= y_p; state <= WRITE; state_back <= SCAN; end //定位列页地址
5'd1: begin oled_dcn <= CMD; char_reg <= x_pl; state <= WRITE; state_back <= SCAN; end //定位行地址低位
5'd2: begin oled_dcn <= CMD; char_reg <= x_ph; state <= WRITE; state_back <= SCAN; end //定位行地址高位
5'd3: begin num <= num - 1'b1;end
5'd4: begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end //将5*8点阵编程8*8
5'd5: begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end //将5*8点阵编程8*8
5'd6: begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end //将5*8点阵编程8*8
5'd7: begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][39:32]; state <= WRITE; state_back <= SCAN; end
5'd8: begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][31:24]; state <= WRITE; state_back <= SCAN; end
5'd9: begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][23:16]; state <= WRITE; state_back <= SCAN; end
5'd10: begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][15: 8]; state <= WRITE; state_back <= SCAN; end
5'd11: begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][ 7: 0]; state <= WRITE; state_back <= SCAN; end
5'd12: begin state <= MAIN; end
default: state <= IDLE;
endcase
end
//我的OLED显示//
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 <= " MAIN ";state <= SCAN; end
5'd2: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16;
char <= " : : ";state <= SCAN; end
5'd3: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16;
char <= " TEMP : . C ";state <= SCAN; end
5'd4: begin y_p <= 8'hb2; x_ph <= 8'h16; x_pl <= 8'h00; num <= 5'd 1;
char <= sec_l; state <= SCAN; end
5'd5: begin y_p <= 8'hb2; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd 1;
char <= sec_h; state <= SCAN; end
5'd6: begin y_p <= 8'hb2; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd 1;
char <= min_l; state <= SCAN; end
5'd7: begin y_p <= 8'hb2; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd 1;
char <= min_h; state <= SCAN; end
5'd8: begin y_p <= 8'hb2; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd 1;
char <= hour_l; state <= SCAN; end
5'd9: begin y_p <= 8'hb2; x_ph <= 8'h11; x_pl <= 8'h00; num <= 5'd 1;
char <= hour_h; state <= SCAN; end
5'd10:begin y_p <= 8'hb3; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd 1;
char <= temp_h; state <= SCAN; end
5'd11:begin y_p <= 8'hb3; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd 1;
char <= temp_l; state <= SCAN; end
5'd12:begin y_p <= 8'hb3; x_ph <= 8'h16; x_pl <= 8'h00; num <= 5'd 1;
char <= temp_s; state <= SCAN; end
default: state <= IDLE;
endcase
DS18B20温度采集:
采用One Wire协议(底层过于冗余不放出):
module temperature(Clk,Rst_n,one_wire,temp_h,temp_l,temp_s);
input Clk;
input Rst_n;
inout one_wire;
output [3:0]temp_h;
output [3:0]temp_l;
output [3:0]temp_s;
wire [15:0]data_out;
DS18B20 DS18B20(
.clk(Clk), // system clock
.rst_n(Rst_n), // system reset, active low
.one_wire(one_wire), // ds18b20z one-wire-bus
.data_out(data_out) // ds18b20z data_out
);
wire temperature_flag = data_out[15:11]? 1'b0:1'b1;
wire [10:0] temperature_code = temperature_flag? data_out[10:0]:(~data_out[10:0])+1'b1;
wire [20:0] bin_code = temperature_code * 16'd625;
wire [24:0] bcd_code; //十位[23:20],个位[19:16],小数位[14:12]
reg [45:0]shift_reg;
bin_to_bcd bin_to_bcd_uut(
.rst_n(Rst_n), // system reset, active low
.bin_code(bin_code), // binary code
.bcd_code(bcd_code) // bcd code
);
assign temp_h[3:0] = bcd_code[23:20];
assign temp_l[3:0] = bcd_code[19:16];
assign temp_s[3:0] = bcd_code[15:12];
endmodule
此外还涉及到二进制转BCD码:
module bin_to_bcd #
(
parameter B_SIZE = 21
)
(
input rst_n, // system reset, active low
input [B_SIZE-1:0] bin_code, // binary code
output reg [B_SIZE+3:0] bcd_code // bcd code
);
reg [2*B_SIZE+3:0] shift_reg;
always@(bin_code or rst_n)begin
shift_reg= {25'h0,bin_code};
if(!rst_n) bcd_code <= 0;
else begin
repeat(B_SIZE)//repeat B_SIZE times
begin
if (shift_reg[24:21] >= 5) shift_reg[24:21] = shift_reg[24:21] + 2'b11;
if (shift_reg[28:25] >= 5) shift_reg[28:25] = shift_reg[28:25] + 2'b11;
if (shift_reg[32:29] >= 5) shift_reg[32:29] = shift_reg[32:29] + 2'b11;
if (shift_reg[36:33] >= 5) shift_reg[36:33] = shift_reg[36:33] + 2'b11;
if (shift_reg[40:37] >= 5) shift_reg[40:37] = shift_reg[40:37] + 2'b11;
if (shift_reg[44:41] >= 5) shift_reg[44:41] = shift_reg[44:41] + 2'b11;
shift_reg = shift_reg << 1;
end
bcd_code<=shift_reg[45:21];
end
end
endmodule
在单片机编程中由于底层都是事先给好的,所以几乎没有研究过底层,报错进制转换等,深入了解后受益匪浅。
串口通信:
串口通信发送的音频文件为字符串 !!%%&&%$$##""!
直接在STC串口助手中发送到FPGA上即可。
波特率为9600,多次测试发现9600波特率的误码率很低。
上报温度信息:
always @(posedge Clk, negedge Rst_n)
if (!Rst_n)
timeout_flag <= 1'b0;
else if (timeout)
timeout_flag <= 1'b1;
else if (send_out)
timeout_flag <= 1'b0;
else
timeout_flag <= timeout_flag;
always @(posedge Clk, negedge Rst_n)
if (!Rst_n)
cnt <= 6'd0;
else if (cnt == 6'd45)
cnt <= 6'd0;
else if (Tx_Done)
cnt <= cnt + 1'b1;
else
cnt <= cnt;
always @(posedge Clk, negedge Rst_n)
if (!Rst_n)
Send_En <= 1'b0;
else if (timeout_flag && !send_out)
Send_En <= 1'b1;
else
Send_En <= 1'b0;
always @(posedge Clk, negedge Rst_n)
if (!Rst_n) begin
tx_data <= 8'h00;
send_out <= 1'b0;
end
else if (timeout_flag)
case (cnt)
0: tx_data <= "T";
1: tx_data <= "E";
2: tx_data <= "M";
3: tx_data <= "P";
4: tx_data <= 8'hA3;//:
5: tx_data <= 8'hBA;
6: tx_data <= 8'h0D;//
7: tx_data <= 8'h30 + temp_h;
8: tx_data <= 8'h30 + temp_l;
9: tx_data <= 8'h2E;//.
10: tx_data <= 8'h30 + temp_s;
11: tx_data <= 8'h0D;//
12: tx_data <= " ";
13: tx_data <= "C";
14: tx_data <= 8'h0D;//\r
15: tx_data <= 8'h0A;//\n
16: begin tx_data <= 8'h00; send_out <= 1'b1;end
default: tx_data <= 8'h00;
endcase
else
tx_data <= 8'h00;
数据接收:
//数据处理,将异步输入信号转化为同步输入信号//
reg s0_Rs232_Rx,s1_Rs232_Rx; //同步寄存器
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)begin
s0_Rs232_Rx <= 1'b0;
s1_Rs232_Rx <= 1'b0;
end
else begin
s0_Rs232_Rx <= Rs232_Rx;
s1_Rs232_Rx <= s0_Rs232_Rx;
end
reg tmp0_Rs232_Rx,tmp1_Rs232_Rx; //数据寄存器
always@(posedge Clk or negedge Rst_n)//数据寄存器
if(!Rst_n)begin
tmp0_Rs232_Rx <= 1'b0;
tmp1_Rs232_Rx <= 1'b0;
end
else begin
tmp0_Rs232_Rx <= s1_Rs232_Rx;
tmp1_Rs232_Rx <= tmp0_Rs232_Rx;
end
//数据接收状态的判断
assign nedege = !tmp0_Rs232_Rx && tmp1_Rs232_Rx;
always@(posedge Clk or negedge Rst_n)//数据接收状态
if(!Rst_n) begin
uart_state <= 1'b0;
display_Flag <= 1'b0;
end
else if(nedege) begin
uart_state <= 1'b1;
display_Flag <= 1'b1;
end
else if(Rx_Done || (bps_cnt == 8'd12 && (START_BIT > 2))) begin
uart_state <= 1'b0;
display_Flag <= 1'b0;
end
else
uart_state <= uart_state;
蜂鸣器音调:
调整PWM高电平的占空比,就能够达到改变音调的效果。
module Buzzer(Clk,Rst_n,data_in,buzz_en,pwm_out,
sec_l,
sec_h,
min_l,
min_h,
hour_l,
hour_h
);
input Clk;
input Rst_n;
input [7:0]data_in;
input buzz_en;
output reg pwm_out;
input [3:0]sec_l;
input [3:0]sec_h;
input [3:0]min_l;
input [3:0]min_h;
input [3:0]hour_l;
input [3:0]hour_h;
parameter L1 = 8'b0001_0001;
parameter L2 = 8'b0001_0010;
parameter L3 = 8'b0001_0011;
parameter L4 = 8'b0001_0100;
parameter L5 = 8'b0001_0101;
parameter L6 = 8'b0001_0110;
parameter L7 = 8'b0001_0111;
parameter M1 = 8'b0010_0001;
parameter M2 = 8'b0010_0010;
parameter M3 = 8'b0010_0011;
parameter M4 = 8'b0010_0100;
parameter M5 = 8'b0010_0101;
parameter M6 = 8'b0010_0110;
parameter M7 = 8'b0010_0111;
parameter H1 = 8'b0100_0001;
parameter H2 = 8'b0100_0010;
parameter H3 = 8'b0100_0011;
parameter H4 = 8'b0100_0100;
parameter H5 = 8'b0100_0101;
parameter H6 = 8'b0100_0110;
parameter H7 = 8'b0100_0111;
reg [15:0]tone;
reg [15:0]cnt;
always@(data_in) begin
case (data_in)
L1: tone = 16'd45872;
L2: tone = 16'd40858;
L3: tone = 16'd36408;
L4: tone = 16'd34364;
L5: tone = 16'd30612;
L6: tone = 16'd27273;
L7: tone = 16'd24296;
M1: tone = 16'd22931;
M2: tone = 16'd20432;
M3: tone = 16'd18201;
M4: tone = 16'd17180;
M5: tone = 16'd15306;
M6: tone = 16'd13636;
M7: tone = 16'd12148;
H1: tone = 16'd11478;
H2: tone = 16'd10215;
H3: tone = 16'd9101;
H4: tone = 16'd8590;
H5: tone = 16'd7653;
H6: tone = 16'd6818;
H7: tone = 16'd6074;
default: tone = 16'd0;
endcase
end
always @(posedge Clk, negedge Rst_n)
if (!Rst_n)
cnt <= 16'd0;
else if (cnt == tone - 1)
cnt <= 16'd0;
else
cnt <= cnt + 1'b1;
always @(posedge Clk, negedge Rst_n)
if (!Rst_n)
pwm_out <= 1'b0;
else if(sec_h==4'b0000 &&
min_h==4'b0000 && min_l==4'b0000 &&
hour_h==4'b0000 && hour_l==4'b0000)begin
if (cnt <= (tone/2) && buzz_en)
pwm_out <= 1'b1;
else
pwm_out <= 1'b0;
end
endmodule
按键部分:
参考了很多别人的代码和官方例程:
always @(posedge Clk,negedge Rst_n)
if (!Rst_n)begin
state = IDEL;
key_flag <= 1'b0;
key_state <= 1'b1;
en_cnt = 1'b1;
end
else begin
case(state)
IDEL: begin
key_flag <= 1'b0;
if (nedge)begin
state <= FILTER0;
en_cnt <= 1'b1;
end
else
state <= IDEL;
end
FILTER0: begin
if (cnt_full)begin
key_flag <= 1'b1;
key_state <= 1'b0;
en_cnt <=1'b0;
state <= DOWN;
end
else if (pedge)begin
state <= IDEL;
en_cnt <= 1'b0;
end
else
state <= FILTER0;
end
DOWN: begin
key_flag = 1'b0;
if (pedge)begin
state <= FILTER1;
en_cnt <= 1'b1;
end
else
state = DOWN;
end
FILTER1: begin
if (cnt_full) begin
key_flag <= 1'b1;
key_state <= 1'b1;
state <= IDEL;
en_cnt <= 1'b0;
end
else if(nedge)begin
en_cnt <= 1'b0;
state = DOWN;
end
else
state <= FILTER1;
end
default: begin
state <= IDEL;
en_cnt <= 1'b0;
key_flag = 1'b0;
key_state = 1'b1;
end
endcase
end
以下为按键对边界值的设置:
此处列出“加”按键,“减”按键同理
initial begin
timer[0] = 4'd1;
timer[1] = 4'd3;
timer[2] = 4'd5;
timer[3] = 4'd9;
timer[4] = 4'd5;
timer[5] = 4'd0;
end
always @(posedge Clk or negedge Rst_n)
if (!Rst_n)
i <= 0;
else if (key4_in)
if (i == 5)
i <= 0;
else
i <= i + 1;
else
i <= i;
always @(posedge Clk)
if (key3_in)
case (i)
0:
if (timer[i] == 4'd2)
timer[i] <= 4'd0;
else
timer[i] <= timer[i] + 1'b1;
1:
if (timer[0] == 4'd2)begin
if (timer[i] == 4'd3)
timer[i] <= 4'd0;
else
timer[i] <= timer[i] + 1'b1;
end
else begin
if (timer[i] == 4'd9)
timer[i] <= 4'd0;
else
timer[i] <= timer[i] + 1'b1;
end
2:
if (timer[i] == 4'd5)
timer[i] <= 4'd0;
else
timer[i] <= timer[i] + 1'b1;
4:
if (timer[i] == 4'd5)
timer[i] <= 4'd0;
else
timer[i] <= timer[i] + 1'b1;
default:
if (timer[i] == 4'd9)
timer[i] <= 4'd0;
else
timer[i] <= timer[i] + 1'b1;
endcase
难点和问题解决办法:
(1)从单片机的编程思路转变到FPGA的编程思路。
-> 这需要进行很多练习,同时借鉴别人的代码,看看别人对同一个功能是什么样的思路。
(2)时序问题
-> 时序就是FPGA的心脏,一个稳定可靠的时序将会大大提高代码的稳定性,我通过查阅资料解决。
(3)各种意想不到的报错
-> 在编程过程中经常出现各种报错,我通过复制错误在网络搜索,后发现多为语法问题以及代码不规范。
项目心得:
第一次接触FPGA,感触颇多,在整个项目中我不断在想这些功能如果是在51单片机或者STM32上实现,那么项目会轻松很多,不过这也是该项目的挑战性所在,在这个全新的平台,让曾经广泛使用的功能也变得不会用,变得陌生,这不得不每个功能,求我需要深入去了解每个功能的原理,这对我们作为学生来说是很重要也很有意义的一步。
此外硬件的代码逻辑完全不同于C语言逻辑,尽管Verilog是一门类C语言。还有就是时序,极其重要,也是FPGA的核心部分。
总而言之,通过本次项目,我对FPGA有了进一步的认识,学会了很多知识。