官方例程:链接:https://pan.baidu.com/s/1QAhxnMSjTp9dQ9VdAz_UZQ
提取码:step
复制这段内容后打开百度网盘手机App,操作更方便哦
一.项目背景
为了让高校电子/电气/电力等专业的同学们对FPGA的了解和熟练使用,将学习到的理论知识在实际的动手中深刻体会,也配合明年的全国大学生电子设计大赛,硬禾课堂和相关电子研究会联合开展了“2021寒假在家练活动”,并且设定了一些简单的规则,完成项目就能免费获得板卡,这让更多的同学们参与进来,让同学们学有成效。
二.项目要求
1.实现一个可定时时钟的功能,用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟即可,到整点的时候比如8:00,蜂鸣器报警,播放音频信号,最长可持续30秒;
2.实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度,并同时间一起显示在OLED的屏幕上;
3.定时时钟整点报警的同时,将温度信息通过UART传递到电脑上,电脑上能够显示当前板子上的温度信息(任何显示形式都可以),要与OLED显示的温度值一致;
4.PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件,OLED屏幕上显示的时间信息和温度信息都停住不再更新;
5.音频文件播放完毕,OLED开始更新时间信息和当前的温度信息
三.项目实现
(一)项目简述
为了实现上述项目要求,本项目主要分了四个模块:温度计—Thermometer u1;OLED显示屏—OLED u2;蜂鸣器—Beeper u3;串口—uart_seg u4。这四个模块内为了实现一些各自需要的小功能,也包含了一些其他的小模块,详细内容请看下面的结构图:
整个项目的资源报告如下图:
总体来说,因为本人初学FPGA,所以对官方给出的小脚丫的综合例程代码依赖度很高,个人对资源的占用还算满意。
(二)模块介绍
1.温度计—Thermometer u1:
下为此模块例化代码:
wire [24:0] bcd_code;
Thermometer u1
(
.clk (clk ),//系统时钟
.rst_n (rst_n ),//系统复位,低有效
.one_wire (one_wire ),//ds18b20z one-wire-bus
.bcd_code (bcd_code ) //温度bcd码 十位[23:20],个位[19:16],小数位[14:12]
);
该部分由于有以往单片机使用DS18B20的经验和电子森林提供的详细参考例程(温度计参考例程,也可查看文章开头链接的官方例程),所以很容易就能完成温度数据的读取。但需要特别注意的是,此模块输出的bcd码中温度的十位为[23:20],个位为[19:16],小数位为[14:12]。
2.OLED显示屏—OLED u2:
下为此模块例化代码:
wire o_clock;
wire [24:0] temperture_out;
OLED u2
(
.clk (clk ),//系统时钟
.rst_n (rst_n ),//系统复位,低有效
.sw_set (sw_set ),//时间设置模式拨钮
.key (key ),//按键
.temperture (bcd_code ),//温度bcd_code
.music_end (music_end ),//音乐停止信号
.o_clock (o_clock ),//整点信号
.temperture_out (temperture_out ),//整点时温度
.oled_csn (oled_csn ),//OLCD液晶屏使能
.oled_rst (oled_rst ),//OLCD液晶屏复位
.oled_dcn (oled_dcn ),//OLCD数据指令控制
.oled_clk (oled_clk ),//OLCD时钟信号
.oled_dat (oled_dat ) //OLCD数据信号
);
该部分引用电子森林里OLED详细参考例程(OLED参考例程,也可查看文章开头链接的官方例程)并加以修改完成。
在原来的基础上我添加了按键消抖模块、1s时钟分频模块和实现时钟、显示等功能的部分代码。
(1)按键消抖模块
此模块直接引用的小脚丫资料包里STEP-MAX10快速入门手册按键消抖中的代码(请查看附件中的STEP-MAX10快速入门手册),但因为需要用四个按键来调整时间,所以需要把代码中的N改为4。
(2)1s时钟分频模块
此模块是我阅读STEP-MAX10快速入门手册中的讲解后用自己的理解写的代码。
module clk_sec(clk_in,rst,clk_sec_out);
input clk_in,rst;
output reg clk_sec_out;
reg [23:0] cnt_sec = 0;
reg [23:0] Div_sec = 12000000;
always@(posedge clk_in or negedge rst)
begin
if(!rst)
begin
cnt_sec<=0;
clk_sec_out<=0;
end
else if(cnt_sec>=((Div_sec>>1)-1))
begin
cnt_sec<=0;
clk_sec_out<=~clk_sec_out;
end
else begin
cnt_sec<=cnt_sec+1;
end
end
endmodule
(3)实现计时、显示等功能
相较于电子森林中的OLED模块代码,我增加了两个输入——temperture(温度值)和music_end(音乐停止信号,返回让OLED能更新),两个输出——temperture_out(整点时温度,给串口来显示)和o_clock(整点信号,控制串口发送数据和音乐播放)。
下为实现时钟功能的代码:
//时间设置
always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
min_set <= 6'd0;hour_set <= 6'd0;
end
else if(sw_set)begin
case(key_pulse)
4'b0001: begin min_set <= min_set + 1'd1;end
4'b0010: begin min_set <= min_set - 1'd1;end
4'b0100: begin hour_set <= hour_set + 1'd1;end
4'b1000: begin hour_set <= hour_set - 1'd1;end
default: begin min_set <= min_set;hour_set <= hour_set;end
endcase
end
else begin min_set <= min;hour_set <= hour;end
end
//时间变化
always@(posedge clk_1s or negedge rst_n) begin
if(!rst_n) begin
sec <= 6'd0; min <= 6'd0;hour <= 6'd0;
sec_buff <= 6'd0; min_buff <= 6'd0; hour_buff <= 6'd0;
end
else if(sw_set)begin
sec_buff <= sec;
min_buff <= min_set;
min <= min_set;
hour_buff <= hour_set;
hour <= hour_set;
end
else if(o_clock)begin
sec_buff <= sec_buff;
min_buff <= min_buff;
hour_buff <= hour_buff;
if(sec == 6'd59) begin
sec <= 1'd0;
min <= min + 1'd1;
end
else begin
if(hour == 6'd24) hour <= 6'd0;
sec <= sec + 1'd1;
end
if((min == 6'd59) && (sec == 6'd59))begin
min <= 1'd0;
hour <= hour + 1'd1;
end
end
else begin
sec_buff <= sec;
min_buff <= min;
hour_buff <= hour;
if(sec == 6'd59) begin
sec <= 1'd0;
min <= min + 1'd1;
end
else begin
if(hour == 6'd24) hour <= 6'd0;
sec <= sec + 1'd1;
end
if((min == 6'd59) && (sec == 6'd59))begin
min <= 1'd0;
hour <= hour + 1'd1;
end
end
sec_l <= sec_buff % 10;
sec_h <= sec_buff / 10;
min_l <= min_buff % 10;
min_h <= min_buff / 10;
hour_l <= hour_buff % 10;
hour_h <= hour_buff / 10;
end
为了能够达到能够停住不更新的目的,这里用了buff来缓存数据送给OLED,但不更新的同时还是需要计时,所以整点信号有效时,sec、min、hour还是要按时赋值。
下为温度计功能的代码:
reg [24:0] temperture_buff;
always@(posedge clk or negedge rst_n) begin
if(!rst_n) temperture_buff <= 1'b0;
else if(o_clock) begin
temperture_buff <= temperture_buff;
temperture_out <= temperture_buff;
end
else begin temperture_buff <= temperture;end
end
实现的思路与上述时钟功能相似,不再赘述。
3.蜂鸣器—Beeper u3:
下为此模块例化代码:
wire music_end;
Beeper u3
(
.clk (clk ),//系统时钟
.rst_n (rst_n ),//系统复位,低有效
.o_clock (o_clock ),//整点信号
.music_end (music_end ),//音乐停止信号
.beeper (beeper ),//蜂鸣器输出
.rx_data_valid (rx_data_valid ),//接收数据有效脉冲
.rx_data_out (rx_data_out ) //接收到的数据
);
相较于官方例程中的蜂鸣器模块代码(请查看文章开头链接的官方例程),我增加了三个输入——o_clock(整点信号,控制音乐播放)、rx_data_valid(接收数据有效脉冲)和rx_data_out(接收到的音调数据),一个输出——music_end(音乐停止信号,返回让OLED能更新)。
(1)rx_data_valid和rx_data_out的运用
rx_data_valid和rx_data_out用于蜂鸣器子模块tone里,因为rx_data_valid信号很快,直接用于让音调变化达不到播放一段音乐的效果,所以此信号用来改变存储音调的“乐谱”music中的值。实现代码如下:
localparam TONE_NUM = 5'd28;//音乐音调个数
reg [7:0] music [TONE_NUM-1:0];//乐谱
reg [5:0] rx_cnt;
reg send_end;//音乐接收完毕信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin rx_cnt = 1'b0; send_end = 1'b0;end
else if(rx_data_valid)begin
music[rx_cnt] = rx_data_out - "0";//音调是用字符数字的方式发送的
rx_cnt = rx_cnt + 1'b1;
if(rx_cnt == 5'd28) begin send_end = 1'b1;end
end
else if(music_end) begin send_end = 1'b0; rx_cnt = 1'b0;end
else rx_cnt = rx_cnt;
end
其中TONE_NUM为localparam,是“乐谱”的音调个数,我用的小星星乐谱共有28个音调,全部接收完毕后产生send_end(音乐接收完毕信号);在用串口查看接收的数据时,语句“music[rx_cnt] = rx_data_out - "0";”中如果用非阻塞赋值,“乐谱”第一个存的音调会是0,这是因为rx_data_out和rx_data_valid在uart_seg中也是用的非阻塞赋值且在同一时刻,导致“乐谱”第一个存的音调是rx_data_out未变化的初始值0,而此处用阻塞赋值就能解决这个问题。
(2)音乐播放
当“乐谱”存储完后,会发送send_end有效信号,蜂鸣器才能播放音乐。我设置的是一个音调大概响0.4秒。实现代码如下:
reg [4:0] music_cnt;//乐谱音调个数
reg [7:0] music_tone;//当前音乐音调
always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
music_tone = 1'b0;music_cnt <= 1'b0;
end
else if(cnt >= 24'd4999999 && o_clock && send_end)begin
if(music_cnt == TONE_NUM) music_cnt <= music_cnt;
else music_cnt <= music_cnt + 1'b1;
case(music_cnt)
TONE_NUM: begin music_tone = 1'b0; music_end = 1'b1;end
default: music_tone = music[music_cnt];
endcase
end
else if(!o_clock) begin music_tone = 1'b0; music_cnt <= 1'b0; music_end = 1'b0;end
else begin
music_tone = music_tone;
music_cnt <= music_cnt;
end
case(music_tone)
4'd1: cycle <= 16'd45872; //L1,
4'd2: cycle <= 16'd40858; //L2,
4'd3: cycle <= 16'd36408; //L3,
4'd4: cycle <= 16'd34364; //L4,
4'd5: cycle <= 16'd30612; //L5,
4'd6: cycle <= 16'd27273; //L6,
4'd7: cycle <= 16'd24296; //L7,
default: cycle <= 16'd0; //cycle为0,PWM占空比为0,低电平
endcase
end
4.串口—uart_seg u4:
下为此模块例化代码:
wire rx_data_valid;
wire [7:0] rx_data_out;
uart_seg u4
(
.clk (clk ),//系统时钟
.rst_n (rst_n ),//系统复位,低有效
.o_clock (o_clock ),//整点信号
.temperture (temperture_out ),//整点时温度
.fpga_rx (fpga_rx ),//UART接收输入
.fpga_tx (fpga_tx ),//UART发送输出
.rx_data_valid (rx_data_valid ),//接收数据有效脉冲
.rx_data_out (rx_data_out ) //接收到的数据
);
相较于官方例程中的蜂鸣器模块代码(请查看文章开头链接的官方例程),我增加了两个输入——o_clock(整点信号,控制串口发送数据)和temperture(整点时温度值),两个输出——rx_data_valid(接收数据有效脉冲)和rx_data_out(接收到的音调数据)。
(1)温度数据发送给上位机
实现代码如下:
reg tx_data_valid;//发送数据有效脉冲,1有效
reg [7:0] tx_data_in; //要发送的数据
reg [2:0] tx_cnt;//发送操作计数
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin tx_data_valid <= 1'b0; tx_data_in <= 1'b0; tx_cnt <= 1'b0; end
else if((cnt >= 18'd119999) && o_clock)begin
case(tx_cnt)
3'd0:begin tx_data_valid <= 1'b1;tx_data_in <= "0" + temperture[23:20];end
3'd1:begin tx_data_valid <= 1'b1;tx_data_in <= "0" + temperture[19:16];end
3'd2:begin tx_data_valid <= 1'b1;tx_data_in <= ".";end
3'd3:begin tx_data_valid <= 1'b1;tx_data_in <= "0" + temperture[14:12];end
3'd4:begin tx_data_valid <= 1'b0;tx_data_in <= 1'b0;end
endcase
if(tx_cnt == 3'd4) tx_cnt <= tx_cnt;
else tx_cnt <= tx_cnt + 1'b1;
end
else if(!o_clock) tx_cnt <= 1'b0;
else begin tx_data_valid <= 1'b0; tx_data_in <= tx_data_in; end
end
因为ASCII码表中数字字符的值不为数字的值,为了方便查看数据,所以tx_data_in赋值时加上了字符“0”的值。
(2)上位机发送乐谱给FPGA
本项目用的上位机,是将乐谱的数据内嵌在了上位机中,当收到温度数据后会自动发送乐谱的数据给FPGA。因为本人对上位机了解不深,此次的上位机还不是很完善,故不分享更多内容。如果有疑问,可以用比较官方、通用的上位机,比如正点原子的XCOM,发送一下数据,也可以达到一样的效果。
四.心得体会
刚开始时,对Verilog语言是零基础,并且对FPGA并行执行的思想理解不深,在编写代码的过程中出了很多错,并且小脚丫这次运用的OLED屏幕是用SPI时序控制且大小为128*32的,相比常见的用IIC时序控制且大小为128*64的要不同寻常许多,刚开始思考项目时,因为本身对SPI了解就不多,网上能够参考的资料也不多,怎么让屏幕亮起来都让我头大,好在后来找到了本次活动官方提供的参考代码,在一次次尝试运行的过程中对FPGA的操作运用有了了解,学有所获。但是就完成时间和自己完成度与其他同学相比来看,自己也认识到了自身的不足,还需要好好努力学习呀!很感谢拥有这次机会,也很感谢在这个过程对我有所帮助的同学和朋友。加油,未来可期!