项目背景:
微控制器作为目前嵌入式系统设计的主力军在各行各业得到了广泛的应用,但随着物联网、智能硬件、VR等一系列新兴概念产品的问世,市场产品有明显的多样化趋势,功能更为丰富,因而这也给工程师在嵌入式产品设计的时候提出了新的挑战。举个例子,虽然如今的微控制器产品系列细分化更为彻底,对每个层次的领域都有相关的MCU产品支持,但这也意味着器件的选型和资源评估需要更加谨慎,传统MCU开发平台的选型就是一个难题,对于讲究适用就够的原则,选性能功能强大丰富的微控制器浪费资源、浪费成本,选低端的入门级微控制器可能又会出现功能不支持,IO口不足等问题,另外不同平台的有不同的开发环境流程等要熟悉,这也大大延长了工程师在项目开发中的时间;而在另一个对于以前来说相对小众的FPGA领域中,随着工艺的进步和EDA设计工具的不断发展,FPGA的集成度越来越高,而对应的功耗和成本却在不断降低,FPGA的门槛(学习成本和价格成本)也相应地越来越低,因而也使得其被广泛应用到各种领域中去,越来越多的嵌入式系统设计直接采FPGA设计,或者使用FPGA产品作为系统功能的拓展,总之,目前的嵌入式系统设计中越来越多的出现FPGA的身影。
项目描述及需要实现的功能:
-
实现一个可定时时钟的功能,用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟即可,到整点的时候比如8:00,蜂鸣器报警,播放音频信号,最长可持续30秒;
-
实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度,并同时间一起显示在OLED的屏幕上;
-
定时时钟整点报警的同时,将温度信息通过UART传递到电脑上,电脑上能够显示当前板子上的温度信息(任何显示形式都可以),要与OLED显示的温度值一致;
-
PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件,OLED屏幕上显示的时间信息和温度信息都停住不再更新;
- 音频文件播放完毕,OLED开始更新时间信息和当前的温度信息
项目实现的思路:
引脚定义:
资源使用率:
综合好之后的电路图:
操作与按键功能介绍:
- 拨码开关1:调节时间的界面切换
- 拨码开关2:控制蜂鸣器,使其使能与不使能
- 拨码开关4:复位
- 按键1:将设定好的时间赋值给时钟
- 按键2:时/分/秒减少
- 按键3:时/分/秒增加
- 按键4:切换对时/分/秒的调节
- LED1:亮即为正在调节时间
- LED2:亮即为蜂鸣器处于使能状态
- LED7:亮即为FPGA在向上位机发送信息
- LED8:亮即为FPGA处于复位状态
核心代码介绍:
温度模块:
温度模块我采用的是硬核学堂提供的温度采集模块,直接将其例化后加入到top文件中使用,具体代码就不在这展示了,稍后我会打包放在附件里面,说一下实现的思路。DS18B20采集到数据后,返回到tempture中进行处理,得到一个十六进制的数,然后将其转换位8421BCD码,放到一个数组中,再将其按照十、个、小数的顺序一位一位输出到oled屏上。这个实现的难度相对较为简单,只要直接抄代码就行。
时钟模块:
本来我参考的是杨彧老哥的代码,想用时、分、秒一个一个的写,然后再通过除十求余法把两位数一位一位的分开,再一位一位的显示在oled上来实现。这样写的好处是代码很简单,占用的资源也少,但是我遇到了一个致命的问题,就是我不能通过除十求余法把两位数给分开为两个数字,我尝试了三天,用尽了各种办法来分开显示,还是没有效果,遂放弃该方案。不过我仍然觉得如果这种方案能实现的话,一定是最好的方案。所以,经过折衷考虑,加上向各位同学咨询,确定了一种避免分位数的方法,也就是一位一位的来加。秒的各位满10进1,秒的十位满6进1,分的个位满10进一,分的十位满6进1……以此类推,最终完成了我的时钟模块,而且可以实现精准计时以及显示了。
module clock(
input Clk,
input Rst_n,
input key1_in,
input [3:0]timer_sec_l,
input [3:0]timer_sec_h,
input [3:0]timer_min_l,
input [3:0]timer_min_h,
input [3:0]timer_hour_l,
input [3:0]timer_hour_h,
output reg[3:0]sec_l,//输出量
output reg[3:0]sec_h,
output reg[3:0]min_l,
output reg[3:0]min_h,
output reg[3:0]hour_l,
output reg[3:0]hour_h,
output timeout
);
reg Clk_1s;
reg [23:0]cnt_1s;
reg key1;
assign timeout = (min_h == 4'd0 && min_l == 4'd0 && sec_h == 4'd0 && sec_l == 4'd0);
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
always @(posedge key1_in or posedge Clk_1s or negedge Rst_n)//更新设定时间
if (!Rst_n)
key1 <= 1'b0;
else if (key1_in)
key1 <= 1'b1;
else
key1 <= 1'b0;
always @(posedge Clk_1s, negedge Rst_n)//秒
if (!Rst_n)
sec_l <= 4'd0;
else if (key1)
sec_l <= timer_sec_l;
else if (sec_l == 4'd9)
sec_l <= 4'd0;
else
sec_l <= sec_l + 1'b1;
always @(posedge Clk_1s, negedge Rst_n)
if (!Rst_n)
sec_h <= 4'd5;
else if (key1)
sec_h <= timer_sec_h;
else if (sec_h == 4'd5 && sec_l == 4'd9)
sec_h <= 4'd0;
else if (sec_l == 4'd9)
sec_h <= sec_h + 1'b1;
else
sec_h <= sec_h;
always @(posedge Clk_1s, negedge Rst_n)//分
if (!Rst_n)
min_l <= 4'd9;
else if (key1)
min_l <= timer_min_l;
else if (sec_h == 4'd5 && sec_l == 4'd9)
if (min_l == 4'd9)
min_l <= 4'd0;
else
min_l <= min_l + 1'b1;
else
min_l <= min_l;
always @(posedge Clk_1s, negedge Rst_n)
if (!Rst_n)
min_h <= 4'd5;
else if (key1)
min_h <= timer_min_h;
else if (min_h == 4'd5 && min_l == 4'd9 && sec_h == 4'd5 && sec_l == 4'd9)
min_h <= 4'd0;
else if (min_l == 4'd9 && sec_h == 4'd5 && sec_l == 4'd9)
min_h <= min_h + 1'b1;
else
min_h <= min_h;
always @(posedge Clk_1s, negedge Rst_n)//时
if (!Rst_n)
hour_l <= 4'd3;
else if (key1)
hour_l <= timer_hour_l;
else if (min_h == 4'd5 && min_l == 4'd9 && sec_h == 4'd5 && sec_l == 4'd9)
if (hour_l == 4'd9 || (hour_h == 4'd2 && hour_l == 4'd3 && min_h == 4'd5 && min_l == 4'd9 && sec_h == 4'd5 && sec_l == 4'd9))
hour_l <= 4'd0;
else
hour_l <= hour_l + 1'b1;
else
hour_l <= hour_l;
always @(posedge Clk_1s, negedge Rst_n)
if (!Rst_n)
hour_h <= 4'd2;
else if (key1)
hour_h <= timer_hour_h;
else if (hour_h == 4'd2 && hour_l == 4'd3 && min_h == 4'd5 && min_l == 4'd9 && sec_h == 4'd5 && sec_l == 4'd9)
hour_h <= 4'd0;
else if (hour_l == 4'd9 && min_h == 4'd5 && min_l == 4'd9 && sec_h == 4'd5 && sec_l == 4'd9)
hour_h <= hour_h + 1'b1;
else
hour_h <= hour_h;
endmodule
下面是时钟调节的控制模块,因为之前用51以及DS1302做过一个时钟,精准的控制了时钟每一位的加减,所以将这个思想移植到这里来,也能很好的实现我们所需要的功能。
module timer(
input Clk,
input Rst_n,
input key2_in,//minus
input key3_in,//add
input key4_in,//shift
output [3:0]timer_sec_l,
output [3:0]timer_sec_h,
output [3:0]timer_min_l,
output [3:0]timer_min_h,
output [3:0]timer_hour_l,
output [3:0]timer_hour_h
);
reg [3:0]timer[5:0];
integer i;
assign timer_hour_h = timer[0];
assign timer_hour_l = timer[1];
assign timer_min_h = timer[2];
assign timer_min_l = timer[3];
assign timer_sec_h = timer[4];
assign timer_sec_l = timer[5];
initial begin
timer[0] = 4'd2;
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'd4)
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
else if (key2_in)
case (i)
0:
if (timer[i] == 4'd0)
timer[i] <= 4'd2;
else
timer[i] <= timer[i] - 1'b1;
1:
if (timer[0] == 4'd2)begin
if (timer[i] == 4'd0)
timer[i] <= 4'd4;
else
timer[i] <= timer[i] - 1'b1;
end
else begin
if (timer[i] == 4'd0)
timer[i] <= 4'd9;
else
timer[i] <= timer[i] - 1'b1;
end
2:
if (timer[i] == 4'd0)
timer[i] <= 4'd5;
else
timer[i] <= timer[i] - 1'b1;
4:
if (timer[i] == 4'd0)
timer[i] <= 4'd5;
else
timer[i] <= timer[i] - 1'b1;
default:
if (timer[i] == 4'd0)
timer[i] <= 4'd9;
else
timer[i] <= timer[i] - 1'b1;
endcase
else
timer[i] <= timer[i];
endmodule
按键处理:
不得不说,当我看见杨彧老哥通过FPGA特有的时钟边沿信号来对按键采样、消抖的时候,我不禁大为惊叹,这个想法避免了大段的按键消抖的处理过程,但是后来也出现了一个问题,就是按键检测不灵敏,有的时候按下去没反应,于是我又去到处找按键处理的程序,终于在曹坤的代码中找到了答案,学谁的代码不是学的,在此表示感谢,他的代码是通过状态机的方式来实现消抖和按键采集的,我试了一下,很不错,很灵敏,再次表示感谢,我贴在下面。
module key(
Clk,
Rst_n,
key_in,
key_flag,
key_state
);
input Clk;
input Rst_n;
input key_in;
output reg key_flag;//按键是否处于触发沿
output reg key_state;//按键当前电平
reg key_in_s1,key_in_s2;
always@(posedge Clk, negedge Rst_n)//对外部输入信号进行处理
if(!Rst_n)begin
key_in_s1 <= 1'b0;
key_in_s2 <= 1'b0;
end
else begin
key_in_s1 <= key_in;
key_in_s2 <= key_in_s1;
end
reg key_tmp1,key_tmp2;
wire pedge,nedge;
always@(posedge Clk,negedge Rst_n)//D触发器存储两个相邻时钟上升沿时的外部输入信号
if(!Rst_n)begin
key_tmp1 <= 1'b0;
key_tmp2 <= 1'b0;
end
else begin
key_tmp1 <= key_in_s2;
key_tmp2 <= key_tmp1;
end
assign pedge = (!key_tmp1) & key_tmp2;//跳变沿
assign nedge = key_tmp1 & (!key_tmp2);
reg[19:0]cnt;
reg en_cnt;
reg cnt_full;
always@(posedge Clk,negedge Rst_n)//计数器
if (!Rst_n)
cnt <= 20'b0;
else if (en_cnt)
cnt <= cnt + 1'b1;
else
cnt <= 20'b0;
always@(posedge Clk,negedge Rst_n)
if (!Rst_n)
cnt_full <= 1'b0;
else if (cnt == 20'd999_999)
cnt_full <= 1'b1;
else
cnt_full <= 1'b0;
localparam
IDEL = 4'b0001,
FILTER0 = 4'b0010,
DOWN = 4'b0100,
FILTER1 = 4'b1000;
reg [3:0]state;
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
endmodule
OLED显示模块:
有一点不得不说,硬禾的有些例程做的确实很好,全部都放在网站上,有需要的自己都可以找到,锻炼大家的动手搜资料的能力。我这个显示模块就是基于硬禾的OLED显示例程,这个网页里不仅有OLED的例程,还有SPI通信总线控制OLED的一些基本的原理方法,讲的很清楚了,这里就不过多的赘述了。当然啦,我也要说一下我改动的地方,其实也很简单。一个是修改时间的页面重新写了一个放在了下面,很简单,只要重新编辑一个页面,然后把那些可以自己修改的变量放上就行了。还有一个很重要的功能实现我也放在了这个OLED模块里面,就是UART传数据给小脚丫的时候,小脚丫OLED上的信息不更新,当小脚丫接收到上位机传来的信息时,会将此刻的时间和温度信息保存在past这个变量中,然后在fresh_flag这个标志位的作用下跳转到只显示这个最后保存到past中的信息,以实现OLED信息的停止更新,代码我放在下面了。
module OLED_SPI
(
input clk, //12MHz系统时钟
input rst_n, //系统复位,低有效
input sw_1_oledmode,
input [7:0]sec_l,
input [7:0]sec_h,
input [7:0]min_l,
input [7:0]min_h,
input [7:0]hour_l,
input [7:0]hour_h,
input [7:0]temp_h,
input [7:0]temp_l,
input [7:0]temp_s,
input [7:0]timer_sec_l,
input [7:0]timer_sec_h,
input [7:0]timer_min_l,
input [7:0]timer_min_h,
input [7:0]timer_hour_l,
input [7:0]timer_hour_h,
input fresh_flag,//暂停标志位
output reg oled_csn, //OLCD液晶屏使能
output reg oled_rst, //OLCD液晶屏复位
output reg oled_dcn, //OLCD数据指令控制
output reg oled_clk, //OLCD时钟信号
output reg oled_dat //OLCD数据信号
);
localparam INIT_DEPTH = 16'd25; //LCD初始化的命令的数量
localparam IDLE = 6'h1, MAIN = 6'h2, INIT = 6'h4, SCAN = 6'h8, WRITE = 6'h10, DELAY = 6'h20;
localparam HIGH = 1'b1, LOW = 1'b0;
localparam DATA = 1'b1, CMD = 1'b0;
reg [7:0] cmd [24:0];
reg [39:0] mem [122:0];
reg [7:0] y_p, x_ph, x_pl;
reg [(8*21-1):0] char;
reg [7:0] num, char_reg; //
reg [4:0] cnt_main, cnt_init, cnt_scan, cnt_write;
reg [15:0] num_delay, cnt_delay, cnt;
reg [5:0] state, state_back;
reg [7:0] sec_l_past;
reg [7:0] sec_h_past;
reg [7:0] min_l_past;
reg [7:0] min_h_past;
reg [7:0] hour_l_past;
reg [7:0] hour_h_past;
reg [7:0] temp_h_past;
reg [7:0] temp_l_past;
reg [7:0] temp_s_past;
always @(negedge fresh_flag) begin
sec_l_past <= sec_l;
sec_h_past <= sec_h;
min_l_past <= min_l;
min_h_past <= min_h;
hour_l_past <= hour_l;
hour_h_past <= hour_h;
temp_h_past <= temp_h;
temp_l_past <= temp_l;
temp_s_past <= temp_s;
end
always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt_main <= 1'b0; cnt_init <= 1'b0; cnt_scan <= 1'b0; cnt_write <= 1'b0;
y_p <= 1'b0; x_ph <= 1'b0; x_pl <= 1'b0;
num <= 1'b0; char <= 1'b0; char_reg <= 1'b0;
num_delay <= 16'd5; cnt_delay <= 1'b0; cnt <= 1'b0;
oled_csn <= HIGH; oled_rst <= HIGH; oled_dcn <= CMD; oled_clk <= HIGH; oled_dat <= LOW;
state <= IDLE; state_back <= IDLE;
end else begin
case(state)
IDLE:begin
cnt_main <= 1'b0; cnt_init <= 1'b0; cnt_scan <= 1'b0; cnt_write <= 1'b0;
y_p <= 1'b0; x_ph <= 1'b0; x_pl <= 1'b0;
num <= 1'b0; char <= 1'b0; char_reg <= 1'b0;
num_delay <= 16'd5; cnt_delay <= 1'b0; cnt <= 1'b0;
oled_csn <= HIGH; oled_rst <= HIGH; oled_dcn <= CMD; oled_clk <= HIGH; oled_dat <= LOW;
state <= MAIN; state_back <= MAIN;
end
MAIN:begin
if(fresh_flag)begin
if (sw_1_oledmode) begin//时间温度显示模式
if(cnt_main >= 5'd4) cnt_main <= 5'd1;
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 <= " NICE CLOCK ";state <= SCAN; end
5'd2: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " TIME ";state <= SCAN; end
5'd3: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= {" ",hour_h,hour_l,":",min_h,min_l,":",sec_h,sec_l," "};state <= SCAN; end
5'd4: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= {" ","TEMP:",temp_h,temp_l,".",temp_s," C"," "};state <= SCAN; end
default: state <= IDLE;
endcase
end
else begin//时间设置模式
if(cnt_main >= 5'd4) cnt_main <= 5'd1;
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 SETTING ";state <= SCAN; end
5'd2: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " HOU MIN SEC ";state <= SCAN; end
5'd3: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= {" ",timer_hour_h,timer_hour_l,":",timer_min_h,timer_min_l,":",timer_sec_h,timer_sec_l," "};state <= SCAN; end
5'd4: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "PRESS K1 TO SET ";state <= SCAN; end
default: state <= IDLE;
endcase
end
end
else begin
if(cnt_main >= 5'd4) cnt_main <= 5'd1;
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 <= " NICE CLOCK ";state <= SCAN; end
5'd2: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " TIME ";state <= SCAN; end
5'd3: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= {" ",hour_h_past,hour_l_past,":",min_h_past,min_l_past,":",sec_h_past,sec_l_past," "};state <= SCAN; end
5'd4: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= {" ","TEMP:",temp_h_past,temp_l_past,".",temp_s_past," C"," "};state <= SCAN; end
default: state <= IDLE;
endcase
end
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 //当25条指令及数据发出后,配置完成
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'd 0: begin oled_dcn <= CMD; char_reg <= y_p; state <= WRITE; state_back <= SCAN; end //定位列页地址
5'd 1: begin oled_dcn <= CMD; char_reg <= x_pl; state <= WRITE; state_back <= SCAN; end //定位行地址低位
5'd 2: begin oled_dcn <= CMD; char_reg <= x_ph; state <= WRITE; state_back <= SCAN; end //定位行地址高位
5'd 3: begin num <= num - 1'b1;end
5'd 4: begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end //将5*8点阵编程8*8
5'd 5: begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end //将5*8点阵编程8*8
5'd 6: begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end //将5*8点阵编程8*8
5'd 7: begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][39:32]; state <= WRITE; state_back <= SCAN; end
5'd 8: begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][31:24]; state <= WRITE; state_back <= SCAN; end
5'd 9: 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
WRITE:begin //WRITE状态,将数据按照SPI时序发送给屏幕
if(cnt_write >= 5'd17) cnt_write <= 1'b0;
else cnt_write <= cnt_write + 1'b1;
case(cnt_write)
5'd 0: begin oled_csn <= LOW; end //9位数据最高位为命令数据控制位
5'd 1: begin oled_clk <= LOW; oled_dat <= char_reg[7]; end //先发高位数据
5'd 2: begin oled_clk <= HIGH; end
5'd 3: begin oled_clk <= LOW; oled_dat <= char_reg[6]; end
5'd 4: begin oled_clk <= HIGH; end
5'd 5: begin oled_clk <= LOW; oled_dat <= char_reg[5]; end
5'd 6: begin oled_clk <= HIGH; end
5'd 7: begin oled_clk <= LOW; oled_dat <= char_reg[4]; end
5'd 8: begin oled_clk <= HIGH; end
5'd 9: begin oled_clk <= LOW; oled_dat <= char_reg[3]; end
5'd10: begin oled_clk <= HIGH; end
5'd11: begin oled_clk <= LOW; oled_dat <= char_reg[2]; end
5'd12: begin oled_clk <= HIGH; end
5'd13: begin oled_clk <= LOW; oled_dat <= char_reg[1]; end
5'd14: begin oled_clk <= HIGH; end
5'd15: begin oled_clk <= LOW; oled_dat <= char_reg[0]; end //后发低位数据
5'd16: begin oled_clk <= HIGH; end
5'd17: begin oled_csn <= HIGH; state <= DELAY; end //
default: state <= IDLE;
endcase
end
DELAY:begin //延时状态
if(cnt_delay >= num_delay) begin
cnt_delay <= 16'd0; state <= state_back;
end else cnt_delay <= cnt_delay + 1'b1;
end
default:state <= IDLE;
endcase
end
end
//OLED配置指令数据
always@(posedge rst_n)
begin
cmd[ 0] = {8'hae};
cmd[ 1] = {8'h00};
cmd[ 2] = {8'h10};
cmd[ 3] = {8'h00};
cmd[ 4] = {8'hb0};
cmd[ 5] = {8'h81};
cmd[ 6] = {8'hff};
cmd[ 7] = {8'ha1};
cmd[ 8] = {8'ha6};
cmd[ 9] = {8'ha8};
cmd[10] = {8'h1f};
cmd[11] = {8'hc8};
cmd[12] = {8'hd3};
cmd[13] = {8'h00};
cmd[14] = {8'hd5};
cmd[15] = {8'h80};
cmd[16] = {8'hd9};
cmd[17] = {8'h1f};
cmd[18] = {8'hda};
cmd[19] = {8'h00};
cmd[20] = {8'hdb};
cmd[21] = {8'h40};
cmd[22] = {8'h8d};
cmd[23] = {8'h14};
cmd[24] = {8'haf};
end
//5*8点阵字库数据
always@(posedge rst_n)
begin
mem[ 0] = {8'h3E, 8'h51, 8'h49, 8'h45, 8'h3E}; // 48 0
mem[ 1] = {8'h00, 8'h42, 8'h7F, 8'h40, 8'h00}; // 49 1
mem[ 2] = {8'h42, 8'h61, 8'h51, 8'h49, 8'h46}; // 50 2
mem[ 3] = {8'h21, 8'h41, 8'h45, 8'h4B, 8'h31}; // 51 3
mem[ 4] = {8'h18, 8'h14, 8'h12, 8'h7F, 8'h10}; // 52 4
mem[ 5] = {8'h27, 8'h45, 8'h45, 8'h45, 8'h39}; // 53 5
mem[ 6] = {8'h3C, 8'h4A, 8'h49, 8'h49, 8'h30}; // 54 6
mem[ 7] = {8'h01, 8'h71, 8'h09, 8'h05, 8'h03}; // 55 7
mem[ 8] = {8'h36, 8'h49, 8'h49, 8'h49, 8'h36}; // 56 8
mem[ 9] = {8'h06, 8'h49, 8'h49, 8'h29, 8'h1E}; // 57 9
mem[ 10] = {8'h7C, 8'h12, 8'h11, 8'h12, 8'h7C}; // 65 A
mem[ 11] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h36}; // 66 B
mem[ 12] = {8'h3E, 8'h41, 8'h41, 8'h41, 8'h22}; // 67 C
mem[ 13] = {8'h7F, 8'h41, 8'h41, 8'h22, 8'h1C}; // 68 D
mem[ 14] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h41}; // 69 E
mem[ 15] = {8'h7F, 8'h09, 8'h09, 8'h09, 8'h01}; // 70 F
mem[ 32] = {8'h00, 8'h00, 8'h00, 8'h00, 8'h00}; // 32 sp
mem[ 33] = {8'h00, 8'h00, 8'h2f, 8'h00, 8'h00}; // 33 !
mem[ 34] = {8'h00, 8'h07, 8'h00, 8'h07, 8'h00}; // 34
mem[ 35] = {8'h14, 8'h7f, 8'h14, 8'h7f, 8'h14}; // 35 #
mem[ 36] = {8'h24, 8'h2a, 8'h7f, 8'h2a, 8'h12}; // 36 $
mem[ 37] = {8'h62, 8'h64, 8'h08, 8'h13, 8'h23}; // 37 %
mem[ 38] = {8'h36, 8'h49, 8'h55, 8'h22, 8'h50}; // 38 &
mem[ 39] = {8'h00, 8'h05, 8'h03, 8'h00, 8'h00}; // 39 '
mem[ 40] = {8'h00, 8'h1c, 8'h22, 8'h41, 8'h00}; // 40 (
mem[ 41] = {8'h00, 8'h41, 8'h22, 8'h1c, 8'h00}; // 41 )
mem[ 42] = {8'h14, 8'h08, 8'h3E, 8'h08, 8'h14}; // 42 *
mem[ 43] = {8'h08, 8'h08, 8'h3E, 8'h08, 8'h08}; // 43 +
mem[ 44] = {8'h00, 8'h00, 8'hA0, 8'h60, 8'h00}; // 44 ,
mem[ 45] = {8'h08, 8'h08, 8'h08, 8'h08, 8'h08}; // 45 -
mem[ 46] = {8'h00, 8'h60, 8'h60, 8'h00, 8'h00}; // 46 .
mem[ 47] = {8'h20, 8'h10, 8'h08, 8'h04, 8'h02}; // 47 /
mem[ 48] = {8'h3E, 8'h51, 8'h49, 8'h45, 8'h3E}; // 48 0
mem[ 49] = {8'h00, 8'h42, 8'h7F, 8'h40, 8'h00}; // 49 1
mem[ 50] = {8'h42, 8'h61, 8'h51, 8'h49, 8'h46}; // 50 2
mem[ 51] = {8'h21, 8'h41, 8'h45, 8'h4B, 8'h31}; // 51 3
mem[ 52] = {8'h18, 8'h14, 8'h12, 8'h7F, 8'h10}; // 52 4
mem[ 53] = {8'h27, 8'h45, 8'h45, 8'h45, 8'h39}; // 53 5
mem[ 54] = {8'h3C, 8'h4A, 8'h49, 8'h49, 8'h30}; // 54 6
mem[ 55] = {8'h01, 8'h71, 8'h09, 8'h05, 8'h03}; // 55 7
mem[ 56] = {8'h36, 8'h49, 8'h49, 8'h49, 8'h36}; // 56 8
mem[ 57] = {8'h06, 8'h49, 8'h49, 8'h29, 8'h1E}; // 57 9
mem[ 58] = {8'h00, 8'h36, 8'h36, 8'h00, 8'h00}; // 58 :
mem[ 59] = {8'h00, 8'h56, 8'h36, 8'h00, 8'h00}; // 59 ;
mem[ 60] = {8'h08, 8'h14, 8'h22, 8'h41, 8'h00}; // 60 <
mem[ 61] = {8'h14, 8'h14, 8'h14, 8'h14, 8'h14}; // 61 =
mem[ 62] = {8'h00, 8'h41, 8'h22, 8'h14, 8'h08}; // 62 >
mem[ 63] = {8'h02, 8'h01, 8'h51, 8'h09, 8'h06}; // 63 ?
mem[ 64] = {8'h32, 8'h49, 8'h59, 8'h51, 8'h3E}; // 64 @
mem[ 65] = {8'h7C, 8'h12, 8'h11, 8'h12, 8'h7C}; // 65 A
mem[ 66] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h36}; // 66 B
mem[ 67] = {8'h3E, 8'h41, 8'h41, 8'h41, 8'h22}; // 67 C
mem[ 68] = {8'h7F, 8'h41, 8'h41, 8'h22, 8'h1C}; // 68 D
mem[ 69] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h41}; // 69 E
mem[ 70] = {8'h7F, 8'h09, 8'h09, 8'h09, 8'h01}; // 70 F
mem[ 71] = {8'h3E, 8'h41, 8'h49, 8'h49, 8'h7A}; // 71 G
mem[ 72] = {8'h7F, 8'h08, 8'h08, 8'h08, 8'h7F}; // 72 H
mem[ 73] = {8'h00, 8'h41, 8'h7F, 8'h41, 8'h00}; // 73 I
mem[ 74] = {8'h20, 8'h40, 8'h41, 8'h3F, 8'h01}; // 74 J
mem[ 75] = {8'h7F, 8'h08, 8'h14, 8'h22, 8'h41}; // 75 K
mem[ 76] = {8'h7F, 8'h40, 8'h40, 8'h40, 8'h40}; // 76 L
mem[ 77] = {8'h7F, 8'h02, 8'h0C, 8'h02, 8'h7F}; // 77 M
mem[ 78] = {8'h7F, 8'h04, 8'h08, 8'h10, 8'h7F}; // 78 N
mem[ 79] = {8'h3E, 8'h41, 8'h41, 8'h41, 8'h3E}; // 79 O
mem[ 80] = {8'h7F, 8'h09, 8'h09, 8'h09, 8'h06}; // 80 P
mem[ 81] = {8'h3E, 8'h41, 8'h51, 8'h21, 8'h5E}; // 81 Q
mem[ 82] = {8'h7F, 8'h09, 8'h19, 8'h29, 8'h46}; // 82 R
mem[ 83] = {8'h46, 8'h49, 8'h49, 8'h49, 8'h31}; // 83 S
mem[ 84] = {8'h01, 8'h01, 8'h7F, 8'h01, 8'h01}; // 84 T
mem[ 85] = {8'h3F, 8'h40, 8'h40, 8'h40, 8'h3F}; // 85 U
mem[ 86] = {8'h1F, 8'h20, 8'h40, 8'h20, 8'h1F}; // 86 V
mem[ 87] = {8'h3F, 8'h40, 8'h38, 8'h40, 8'h3F}; // 87 W
mem[ 88] = {8'h63, 8'h14, 8'h08, 8'h14, 8'h63}; // 88 X
mem[ 89] = {8'h07, 8'h08, 8'h70, 8'h08, 8'h07}; // 89 Y
mem[ 90] = {8'h61, 8'h51, 8'h49, 8'h45, 8'h43}; // 90 Z
mem[ 91] = {8'h00, 8'h7F, 8'h41, 8'h41, 8'h00}; // 91 [
mem[ 92] = {8'h55, 8'h2A, 8'h55, 8'h2A, 8'h55}; // 92 .
mem[ 93] = {8'h00, 8'h41, 8'h41, 8'h7F, 8'h00}; // 93 ]
mem[ 94] = {8'h04, 8'h02, 8'h01, 8'h02, 8'h04}; // 94 ^
mem[ 95] = {8'h40, 8'h40, 8'h40, 8'h40, 8'h40}; // 95 _
mem[ 96] = {8'h00, 8'h01, 8'h02, 8'h04, 8'h00}; // 96 '
mem[ 97] = {8'h20, 8'h54, 8'h54, 8'h54, 8'h78}; // 97 a
mem[ 98] = {8'h7F, 8'h48, 8'h44, 8'h44, 8'h38}; // 98 b
mem[ 99] = {8'h38, 8'h44, 8'h44, 8'h44, 8'h20}; // 99 c
mem[100] = {8'h38, 8'h44, 8'h44, 8'h48, 8'h7F}; // 100 d
mem[101] = {8'h38, 8'h54, 8'h54, 8'h54, 8'h18}; // 101 e
mem[102] = {8'h08, 8'h7E, 8'h09, 8'h01, 8'h02}; // 102 f
mem[103] = {8'h18, 8'hA4, 8'hA4, 8'hA4, 8'h7C}; // 103 g
mem[104] = {8'h7F, 8'h08, 8'h04, 8'h04, 8'h78}; // 104 h
mem[105] = {8'h00, 8'h44, 8'h7D, 8'h40, 8'h00}; // 105 i
mem[106] = {8'h40, 8'h80, 8'h84, 8'h7D, 8'h00}; // 106 j
mem[107] = {8'h7F, 8'h10, 8'h28, 8'h44, 8'h00}; // 107 k
mem[108] = {8'h00, 8'h41, 8'h7F, 8'h40, 8'h00}; // 108 l
mem[109] = {8'h7C, 8'h04, 8'h18, 8'h04, 8'h78}; // 109 m
mem[110] = {8'h7C, 8'h08, 8'h04, 8'h04, 8'h78}; // 110 n
mem[111] = {8'h38, 8'h44, 8'h44, 8'h44, 8'h38}; // 111 o
mem[112] = {8'hFC, 8'h24, 8'h24, 8'h24, 8'h18}; // 112 p
mem[113] = {8'h18, 8'h24, 8'h24, 8'h18, 8'hFC}; // 113 q
mem[114] = {8'h7C, 8'h08, 8'h04, 8'h04, 8'h08}; // 114 r
mem[115] = {8'h48, 8'h54, 8'h54, 8'h54, 8'h20}; // 115 s
mem[116] = {8'h04, 8'h3F, 8'h44, 8'h40, 8'h20}; // 116 t
mem[117] = {8'h3C, 8'h40, 8'h40, 8'h20, 8'h7C}; // 117 u
mem[118] = {8'h1C, 8'h20, 8'h40, 8'h20, 8'h1C}; // 118 v
mem[119] = {8'h3C, 8'h40, 8'h30, 8'h40, 8'h3C}; // 119 w
mem[120] = {8'h44, 8'h28, 8'h10, 8'h28, 8'h44}; // 120 x
mem[121] = {8'h1C, 8'hA0, 8'hA0, 8'hA0, 8'h7C}; // 121 y
mem[122] = {8'h44, 8'h64, 8'h54, 8'h4C, 8'h44}; // 122 z
mem[123] = {8'h00, 8'h00, 8'h00, 8'h03, 8'h03}; // 123
end
endmodule
串口通信模块:
首先我想说的就是uart_rx,也就是串口接收模块,这个发送模块我参考了CSDN上的一段代码,直接搬过来用,很好用,收到上位机的信息后,直接输出一个接受完成标志和一组输出数据,同时与蜂鸣器联动,控制蜂鸣器发出音乐,蜂鸣器是很重要的一个部分,也是很有意思的一个部分,这个我放到下面去说。
module uart_rxd(
input sys_clk, //系统时钟
input sys_rst_n, //系统复位,低电平有效
input uart_rxd, //UART接收端口
output reg uart_done, //接收一帧数据完成标志信号
output reg [7:0] uart_data //接收的数据
);
//parameter define
parameter CLK_FREQ = 12_000_000; //系统时钟频率
parameter UART_BPS = 9600; //串口波特率
localparam BPS_CNT = CLK_FREQ/UART_BPS; //为得到指定波特率,
//需要对系统时钟计数BPS_CNT次
//reg define
reg uart_rxd_d0;
reg uart_rxd_d1;
reg [15:0] clk_cnt; //系统时钟计数器
reg [ 3:0] rx_cnt; //接收数据计数器
reg rx_flag; //接收过程标志信号
reg [ 7:0] rxdata; //接收数据寄存器
//wire define
wire start_flag;
//*****************************************************
//** main code
//*****************************************************
//捕获接收端口下降沿(起始位),得到一个时钟周期的脉冲信号
assign start_flag = uart_rxd_d1 & (~uart_rxd_d0);
//对UART接收端口的数据延迟两个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
uart_rxd_d0 <= 1'b0;
uart_rxd_d1 <= 1'b0;
end
else begin
uart_rxd_d0 <= uart_rxd;
uart_rxd_d1 <= uart_rxd_d0;
end
end
//当脉冲信号start_flag到达时,进入接收过程
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
rx_flag <= 1'b0;
else begin
if(start_flag) //检测到起始位
rx_flag <= 1'b1; //进入接收过程,标志位rx_flag拉高
else if((rx_cnt == 4'd9)&&(clk_cnt == BPS_CNT/2))
rx_flag <= 1'b0; //计数到停止位中间时,停止接收过程
else
rx_flag <= rx_flag;
end
end
//进入接收过程后,启动系统时钟计数器与接收数据计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_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; //此时接收数据计数器加1
end
end
else begin //接收过程结束,计数器清零
clk_cnt <= 16'd0;
rx_cnt <= 4'd0;
end
end
//根据接收数据计数器来寄存uart接收端口数据
always @(posedge sys_clk or negedge sys_rst_n) begin
if ( !sys_rst_n)
rxdata <= 8'd0;
else if(rx_flag) //系统处于接收过程
if (clk_cnt == BPS_CNT/2) begin //判断系统时钟计数器计数到数据位中间
case ( rx_cnt )
4'd1 : rxdata[0] <= uart_rxd_d1; //寄存数据位最低位
4'd2 : rxdata[1] <= uart_rxd_d1;
4'd3 : rxdata[2] <= uart_rxd_d1;
4'd4 : rxdata[3] <= uart_rxd_d1;
4'd5 : rxdata[4] <= uart_rxd_d1;
4'd6 : rxdata[5] <= uart_rxd_d1;
4'd7 : rxdata[6] <= uart_rxd_d1;
4'd8 : rxdata[7] <= uart_rxd_d1; //寄存数据位最高位
default:;
endcase
end
else
rxdata <= rxdata;
else
rxdata <= 8'd0;
end
//数据接收完毕后给出标志信号并寄存输出接收到的数据
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
uart_data <= 8'd0;
uart_done <= 1'b0;
end
else if(rx_cnt == 4'd9) begin //接收数据计数器计数到停止位时
uart_data <= rxdata; //寄存输出接收到的数据
uart_done <= 1'b1; //并将接收完成标志位拉高
end
else begin
uart_data <= 8'd0;
uart_done <= 1'b0;
end
end
endmodule
然后我要说的是uart_tx,这个是下位机发送的模块,但是这个不是我操作的重点,这部分代码我就放到下面打包的程序当中去了,我直接讲重点。重点是我控制温度信息传送的代码,这个模块名叫timeout_tx_ctrl,当然最重要的先讲,这段代码如下:
always @(posedge Clk or 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;
这个就是用来实现温度的传送,我发现传送温度的时候,如果是英文字母的话直接传就完事了,但是如果是中文汉字的话就稍微有点麻烦,就需要它的GB2312码,对应的一个汉字两个码,一起传过去才可以显示一个完整的汉字。
而控制这个传送过程我用了两个变量,一个是时间到整点时有效的timeout,一个是发送结束标志位send_out,两个变量同时满足timeout_flag && !send_out时才能启动发送,不然即为无效状态,不能发送,这段完整的代码我就放下面了。
module timeout_tx_ctrl(
input Clk,
input Rst_n,
input timeout,
input Tx_Done,
input [3:0]temp_h,
input [3:0]temp_l,
input [3:0]temp_s,
output reg Send_En,
output reg[7:0]tx_data
);
reg timeout_flag;
reg send_out;
reg [5:0]cnt;//42
initial send_out = 1'b0;
always @(posedge Clk or negedge Rst_n)begin
if (!Rst_n)
timeout_flag <= 1'b0;
else if (send_out)
timeout_flag <= 1'b0;
else if (timeout)begin
timeout_flag <= 1'b1;
//send_out <= 1'b0;
end
else
timeout_flag <= timeout_flag;
end
always @(posedge Clk or 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 or 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 or 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;
endmodule
传送成功的上位机模块截图:
ps:上位机发送数据靠的是串口助手的连续发送功能,点击自动循环发送即可实现。
最最最最重要的蜂鸣器模块:
我觉得这个是最值得我说的一个模块,因为在做这个项目之前我只知道蜂鸣器有响与不响的区分,没想到蜂鸣器还可以用来演奏音乐,很是令我惊讶,不过知道了它的工作原理之后,我发现其实很简单,就是不同频率的信号产生不同的音调来发出一首完美的曲子,然后每个音之间隔开250ms,就可以奏乐了!
这是不同音调对应的频率:
然后产生这些不同的频率就只要使用PWM波就可以了,确实是十分的巧妙,令我惊叹不已,代码我就直接放下面了,大家可以直接欣赏:
module song(
input clk, //系统时钟12MHz
input clk1h,
input rst,
input tone_en, //蜂鸣器使能
input uart_done, //蜂鸣器使能
input[7:0] uart_data,
input[3:0] sec_h, //时间
input[3:0] min_l,
input[3:0] min_h,
output fresh_flag,
output beep //蜂鸣器输出端
);
reg uart_done_cnt; //uart_done计数
reg beep_r; //寄存器
reg[16:0]count,count_end,count_end1;
reg[23:0]count1;
reg[7:0]state;
reg fresh;
//乐谱参数:D=F/2K (D:参数,F:时钟频率,K:音高频率)
parameter M_1 = 16'd11464, //中音1
M_2 = 16'd10215, //中音2
M_3 = 16'd9100, //中音3
M_4 = 16'd8589,//中音4
M_5 = 16'd7652, //中音5
M_6 = 16'd6817, //中音6
H_1 = 16'd5740; //高音1
parameter TIME = 3_000_000; //控制每一个音的长短(250ms)
assign beep = beep_r; //输出音乐
assign fresh_flag = fresh;
always@(posedge clk)
begin
if(tone_en && min_h == 0 && min_l == 0 && sec_h < 3)//整点才响铃,并且响铃时间为10s
begin
count <= count + 1'b1; //计数器加1
if(count == count_end)
begin
count <= 17'h0; //计数器清零
beep_r <= !beep_r; //输出取反
end
end
else if(tone_en && !fresh)
begin
count <= count + 1'b1; //计数器加1
if(count == count_end1)
begin
count <= 17'h0; //计数器清零
beep_r <= !beep_r; //输出取反
end
end
// else
// count <= 0;
end
//自动温度控制 曲谱 产生分频的系数并描述出曲谱
always @(posedge clk)
begin
if(count1 < TIME) //一个节拍250mS
count1 = count1 + 1'b1;
else
begin
count1 = 24'd0;
if(state == 8'd36)
state = 8'd0;
else
state = state + 1'b1;
case(state)
8'd0,8'd1:count_end=M_1;
8'd2,8'D3:count_end=M_5;
8'd4,8'D5:count_end=M_6;
8'D6:count_end=M_5;
8'D7,8'D8:count_end=M_4;
8'D9,8'D10:count_end=M_3;
8'D11,8'D12:count_end=M_2;
8'D13:count_end=M_1;
8'D14,8'D15:count_end=M_5;
8'D15,8'D16:count_end=M_4;
8'D17,8'D18:count_end=M_3;
8'D19:count_end=M_2;
8'D16,8'D17:count_end=M_5;
8'D18,8'D19:count_end=M_4;
8'D20,8'D21:count_end=M_3;
8'D22:count_end=M_2;
8'd23,8'd24:count_end=M_1;
8'd25,8'D26:count_end=M_5;
8'd27,8'D28:count_end=M_6;
8'D29:count_end=M_5;
8'D30,8'D31:count_end=M_4;
8'D32,8'D33:count_end=M_3;
8'D34,8'D35:count_end=M_2;
8'D36:count_end=M_1;
default: count_end = 16'h0;
endcase
end
end
//串口控制 曲谱 产生分频的系数并描述出曲谱
always @(posedge clk)
begin
if(uart_done)
begin
case(uart_data)
8'h1:count_end1 <=16'd22935; //L1,
8'h2:count_end1 <=16'd20428; //L2,
8'h3:count_end1 <=16'd18203; //L3,
8'h4:count_end1 <=16'd17181; //L4,
8'h5:count_end1 <=16'd15305; //L5,
8'h6:count_end1 <=16'd13635; //L6,
8'h7:count_end1 <=16'd12147; //L7,
8'h8:count_end1 <=16'd11464; //M1,
8'h9:count_end1 <=16'd10215; //M2,
8'ha:count_end1 <=16'd9100; //M3,
8'hb:count_end1 <=16'd8589; //M4,
8'hc:count_end1 <=16'd7652; //M5,
8'hd:count_end1 <=16'd6817; //M6,
8'he:count_end1 <=16'd6073; //M7,
8'hf:count_end1 <=16'd5740; //H1,
8'h10:count_end1 <=16'd5107; //H2,
8'h11:count_end1 <=16'd4549; //H3,
8'h12:count_end1 <=16'd4294; //H4,
8'h13:count_end1 <=16'd3825; //H5,
8'h14:count_end1 <=16'd3408; //H6,
8'h15:count_end1 <=16'd3036; //H7,
default:count_end1 <=16'd65535;// 无声
endcase
end
end
always@(posedge uart_done or posedge clk1h)
begin
if(uart_done)
begin
uart_done_cnt <= 1'b1;
end
else
if(uart_done_cnt)
begin
fresh <= 1'b0;
uart_done_cnt <= 1'b0;
end
else
fresh <= 1'b1;
end
endmodule
很棒,很好控制的一个代码,然后串口接收模块,包括上位机发送的内容也是根据这给代码来给出的。
至此,所有的模块都介绍完了,功能也都实现了。
遇到的难题:
- 在完成项目的过程中我遇到的一个问题就是如何使OLED上的信息不更新,我刚开始想的是能不能让OLED停在那个界面上不刷新,就是在显示是做手脚,但是后来苦于没有找到相应的实现的代码,遂放弃,于是在其他同学的项目里,我好到了一个很好的方法就是,找一个黑盒子给它把那个时间点的信息都装起来,然后在发送音频信息时直接显示这个黑盒子里面的东西,这个代码很好实现,也很简单。
- 第二个遇到的问题就是如何使蜂鸣器“唱歌”,这个我其实有那么一点知道是靠不同频率来发出不同音调的,因为毕竟声音的本质就是这样的,但是我苦于找不到如何产生这些个不同的频率。后来是在电子森林的网站上找到了这个方法,就是用PWM波来产生,确实,做工程需要多方面联动才能把这个项目做好。
- 还有一个就是并行运行方式,FPGA并行的运行方式导致很多单片机里的代码语言都不饿能直接用,同时也给我的编程造成了一定的困扰,有些报错实在是看不懂,请问了群友之后发现竟是并行运算与串行没转换过来,实属低级错误。
未来的计划:
我打算再学学FPGA里面的状态机还有就是IP核的例化以及使用,这些东西真的不是很熟练,也不太懂里面的原理。另外的就是,包括以前单片机的学习,都是靠丰富的底层代码来求生的,现在我觉得该进一步学学那些底层的最基本的原理了,不然就像苏老师说的那样,以后出去工作很多都是浮于表面,没有实质性的东西是不行的,所以得抓紧学学底层的代码,学的好了真的是受益终生。我觉得我最终的目的是要学会设计,当然,这还有很长的路要走,我还要多加学习。