项目需求:
- 实现一个可定时时钟的功能,用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟即可,到整点的时候比如8:00,蜂鸣器报警,播放音频信号,最长可持续30秒;
- 实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度,并同时间一起显示在OLED的屏幕上;
- 定时时钟整点报警的同时,将温度信息通过UART传递到电脑上,电脑上能够显示当前板子上的温度信息(任何显示形式都可以),要与OLED显示的温度值一致;
- PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件,OLED屏幕上显示的时间信息和温度信息都停住不再更新;
- 音频文件播放完毕,OLED开始更新时间信息和当前的温度信息
整体功能框图:
操作方法:
实现的思路:
首先逐条分析这些需求;
第一项:
第一点:设计一个可定时时钟,用小脚丫FPGA核心模块的四个按键设置当前时间。
回想以前买的电子手表也是包括以上功能,很容易想到这里的设计需要用到状态机,设想的思路是按下第一个按键key_set,状态机跳转到设置时钟模式,此时时钟数闪烁,然后此时的秒钟应该不再增加,按下key_up或key_down按键分别会将当前数字加1或者减1(避免有时候不小心多按一次加1又得调一圈),然后再按下key_set按键跳转到设置分钟模式,设置同上。接着是秒钟数、年份、月份、日期、星期。第八次按下key_set回到正常显示模式,秒钟继续增加。
举例说明:state共有8个状态,正常显示以及7个设置时间模式,关键在于每个变量都有两种增加数字的途径,一个是由低一级时间单位的进位,另一个是由按键设置的增加,需要区分开。需要注意的是,每个时间单位都有其上限,如秒钟上限59,时钟上限23,月份上限12等。
always @ (posedge clk or negedge rst_n)
begin
if (!rst_n) min <= 1'b0;
else
begin
if (state == display)
begin
if (min_add)
begin
if (min == 6'd59) min <= 1'b0;
else min <= min + 1'b1;
end
else min <= min;
end
else if (state == set_minute)
begin
if (up_pluse)
begin
if (min == 6'd59) min <= 1'b0;
else min <= min + 1'b1;
end
else if (down_pluse)
begin
if (min == 6'd00) min <= 6'd59;
else min <= min - 1'b1;
end
else min <= min;
end
else min <= min;
end
end
第二点:OLED显示数字钟的当前时间,精确到分钟即可。
这里涉及到我的知识盲区,以前没有写过OLED的驱动,于是查阅了网上的资料,学习到此款12832OLED使用的SSD1306驱动芯片,SSD1206驱动芯片有个128*64bit的RAM存储像素信息,可以理解为一个128列*64行的点阵,点阵存储的数据若为高位1,则OLED屏的对应像素点点亮(可以改设置低位点亮),因此问题转换为把时钟数字和分钟数字的信息要传递给SSD1306,此款芯片有多种通信方式,而安装在训练板上之后设置成了spi通信模式,所以需要先写一个spi发送数据的模块。
其次,每次发送给SSD1306的数据是8bit(1word)的数据,学习了SSD1306芯片的数据手册和网上资料,需要先将芯片进行复位(复位位拉低100ms左右)
always @ (posedge clk or negedge rst_n)
begin
if (!rst_n)
begin
cnt <= 1'b0;
res_oled <= 1'b0;
res_done <= 1'b0;
end
else if (cnt == RES100MS)
begin
res_oled <= 1'b1;
res_done <= 1'b1;
cnt <= RES100MS;
end
else
begin
cnt <= cnt + 1'b1;
res_oled <= 1'b0;
res_done <= 1'b0;
end
end
然后写入初始化信息(如显示模式,按行写或者按写等等),最后循环写入要显示的数据信息(循环是因为数据是会变的,要实时更新)。
除了项目4要求的显示时钟和分钟、温度,我额外增加了两个页面,是用拨码开关换页的。
8'd76:
//choose which page of three
begin
if (sw_change) begin i <= 8'd0; y <= 1'b0; end
else if (sw_yinghe) begin i <= 8'd61; end
else begin i <= 8'd0; y <= 1'b0; end
end
其中显示第一页的时候,i取值为34~52,第二页为61~76,第三页为81~86,i为0~32是清屏操作(如果不清屏的话之前显示的内容没有更改的话将不会消失,会有残留)在最后一个i的时候,都会判断此时拨码开关的状态,从而选择是继续显示当前页还是跳转到清屏状态,在i为33的时候会判断拨码开关的状态,跳转到上述三个显示范围的初始值。
接着,需要写入的数据是某一个数字如“1”的点阵信息,利用取模软件可以得到16*16(汉字)以及16*8(英文及阿拉伯数字、符号)等的16进制点阵信息,这里需要用到0~9这十个阿拉伯数字、冒号“:”、时间、温度、日期、星期、硬禾学堂还有用位图产生的硬禾学堂logo
得到点阵信息之后初始化到例化的IP——ROM里面备用。例化现有的IP会比使用寄存器更节省资源。
然后,由于之前设计的时钟数和分钟数分别是用5位和6位二进制数存储的,此时还应该先用“左移+3法“将其转换成BCD码存储,将时钟数和分钟数分别用8位二进制数表示,高4位表示十位,低4位表示个位.
always @ (posedge clk or negedge rst_n)
begin
if (!rst_n)
begin
bcd_data_reg <= 1'b0;
//i <= 1'b0;
hex2bcd_done <= 1'b1;
end
else
begin
if (hex2bcd_en)
begin
if (({bcd_data_reg[2:0],hex_data[7-i]} > 3'd4) & (i < 3'd7))
bcd_data_reg <= {bcd_data_reg[6:0],hex_data[7-i]} + 2'd3;
else bcd_data_reg <= {bcd_data_reg[6:0],hex_data[7-i]};
if (i == 7)
begin
//i <= 1'b0;
hex2bcd_done <= 1'b1;
end
else
begin
//i <= i + 1'b1;
hex2bcd_done <= 1'b0;
end
end
else bcd_data_reg <= 1'b0;
end
end
通过这个8位二进制数的高4位和低4位的值,得到所对应的阿拉伯数字的点阵数据对应的rom地址,读取其中的内容由spi发送给SSD1306即可。
需要注意的是,rom只有一个地址输入,需要根据此时所需要显示的数据,得到对应的地址。其中year_1、year_2等是使能信号,通过使能信号选择地址。同时,由于一个英文字符存储在8个地址,一个汉字字符存储在16个地址,也要根据所写的不同页,选择不同地址。
assign rom_addr_2 = year_1 ? ((y==4'd4 | y==4'd5) ? (x + year_qian_1) : (x + year_qian_2)) :
( year_2 ? ((y==4'd4 | y==4'd5) ? (x + year_bai_1 ) : (x + year_bai_2 )) :
( year_3 ? ((y==4'd4 | y==4'd5) ? (x + year_shi_1 ) : (x + year_shi_2 )) :
( year_4 ? ((y==4'd4 | y==4'd5) ? (x + year_ge_1 ) : (x + year_ge_2 )) :
( xie_1 ? ((y==4'd4 | y==4'd5) ? (x + addr_xie_1 ) : (x + addr_xie_2 )) :
( mon_1 ? ((y==4'd4 | y==4'd5) ? (x + mon_shi_1 ) : (x + mon_shi_2 )) :
( mon_2 ? ((y==4'd4 | y==4'd5) ? (x + mon_ge_1 ) : (x + mon_ge_2 )) :
( xie_2 ? ((y==4'd4 | y==4'd5) ? (x + addr_xie_1 ) : (x + addr_xie_2 )) :
( day_1 ? ((y==4'd4 | y==4'd5) ? (x + day_shi_1 ) : (x + day_shi_2 )) :
( day_2 ? ((y==4'd4 | y==4'd5) ? (x + day_ge_1 ) : (x + day_ge_2 )) :
( ri ? ((y==4'd0 | y==4'd1) ? (x + ri_1 ) : (x + ri_2 )) :
( qi_1 ? ((y==4'd0 | y==4'd1) ? (x + addr_qi_1 ) : (x + addr_qi_2 )) :
( xing ? ((y==4'd0 | y==4'd1) ? (x + xing_1 ) : (x + xing_2 )) :
( qi_2 ? ((y==4'd0 | y==4'd1) ? (x + addr_qi_1 ) : (x + addr_qi_2 )) :
( zhou ? ((y==4'd4 | y==4'd5) ? (x + addr_week_1) : (x + addr_week_2)) :
(8'h0)))))))))))))));
第三点:到整点的时候比如8:00,蜂鸣器报警,播放音频信号,最长可持续30秒。
到整点的含义即为分钟数和秒钟数同时为“0”的时候,设置一个警报使能检测分钟数和秒钟数是否同时为零从而触发蜂鸣器响应。蜂鸣器的驱动也很简单,通过不同频率的震动发出不同的音调,21个音阶总共对应21个频率,低音1的频率为261.6Hz,蜂鸣器控制信号周期应为12MHz/261.6Hz = 45871.5,因为本设计中蜂鸣器控制信号是按计数器周期翻转的,翻转的时钟周期为45871.5/2 = 22936,设计一个计数器,计数到22936时候就将输出信号piano_out翻转,此信号连接至蜂鸣器,即可让蜂鸣器发出低音1的音调,其他音调类似。然后查阅了周董的《简单爱》的乐谱,将乐谱的音阶用1~21来表示,将转换后的乐谱信息存储到例化的IP——RAM里面备用,当触发整点警报之后,每隔0.25s读RAM地址加1,读出存储在内的乐谱信息给蜂鸣器,即可弹奏出乐曲。
always@(tone) begin
case(tone)
5'd1: time_end = 16'd22935; //L1,
5'd2: time_end = 16'd20428; //L2,
5'd3: time_end = 16'd18203; //L3,
5'd4: time_end = 16'd17181; //L4,
5'd5: time_end = 16'd15305; //L5,
5'd6: time_end = 16'd13635; //L6,
5'd7: time_end = 16'd12147; //L7,
5'd8: time_end = 16'd11464; //M1,
5'd9: time_end = 16'd10215; //M2,
5'd10: time_end = 16'd9100; //M3,
5'd11: time_end = 16'd8589; //M4,
5'd12: time_end = 16'd7652; //M5,
5'd13: time_end = 16'd6817; //M6,
5'd14: time_end = 16'd6073; //M7,
5'd15: time_end = 16'd5740; //H1,
5'd16: time_end = 16'd5107; //H2,
5'd17: time_end = 16'd4549; //H3,
5'd18: time_end = 16'd4294; //H4,
5'd19: time_end = 16'd3825; //H5,
5'd20: time_end = 16'd3408; //H6,
5'd21: time_end = 16'd3036; //H7,
default:time_end = 16'd0;
endcase
end
reg [17:0] time_cnt;
//当蜂鸣器使能时,计数器按照计数终值(分频系数)计数
always@(posedge clk_in or negedge rst_n_in) begin
if(!rst_n_in) begin
time_cnt <= 1'b0;
end else if(!tone_en) begin
time_cnt <= 1'b0;
end else if(time_cnt >= time_end) begin
time_cnt <= 1'b0;
end else begin
time_cnt <= time_cnt + 1'b1;
end
end
//根据计数器的周期,翻转蜂鸣器控制信号
always@(posedge clk_in or negedge rst_n_in) begin
if(!rst_n_in) begin
piano_out <= 1'b0;
end else if((time_cnt==time_end) & (time_end != 1'b0)) begin
piano_out <= ~piano_out; //蜂鸣器控制输出翻转,两次翻转为1Hz
end else begin
piano_out <= piano_out;
end
end
第二项:实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度,并同时间一起显示在OLED的屏幕上。
第一点:实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度。
训练板上有一枚DS18D20Z温度传感器,可以将检测到的环境信息存储在内部RAM里面,只需要写一个该芯片的驱动读出RAM中的数据即可。然而比较麻烦的问题是该芯片有严格的初始化要求和读写时许要求,需要好好琢磨数据手册的介绍。读取到RAM中的温度信息之后进行信息的处理,因为读取到的信息包括温度的正负、整数位和小数位,需要进行分割存储到不同的寄存器,同样的也要尽心BCD转码,这里对于小数的BCD转码用了一个偷懒的办法,因为只需要显示一位小数,而小数部分只有4位,意味着小数部分只有16个数值,步进是0.0625,便直接使用case语句得到16种情况对应的十分位的数据即可(偷懒做法,因为小数位的严格BCD转码有点麻烦)
//convert fractional part of temperature
reg [3:0] bcd_data_temp_fen;
always @(temp_fen)
case(temp_fen)
4'd0 : bcd_data_temp_fen = 4'd0;
4'd1,4'd2 : bcd_data_temp_fen = 4'd1;
4'd3 : bcd_data_temp_fen = 4'd2;
4'd4,4'd5 : bcd_data_temp_fen = 4'd3;
4'd6,4'd7 : bcd_data_temp_fen = 4'd4;
4'd8 : bcd_data_temp_fen = 4'd5;
4'd9,4'd10 : bcd_data_temp_fen = 4'd6;
4'd11 : bcd_data_temp_fen = 4'd7;
4'd12,4'd13: bcd_data_temp_fen = 4'd8;
4'd14,4'd15: bcd_data_temp_fen = 4'd9;
endcase
第二点:并同时间一起显示在OLED的屏幕上
这一步同上述显示时间一样,只需将要显示的数据加入所要写的数据之后即可。
第三项:定时时钟整点报警的同时,将温度信息通过UART传递到电脑上,电脑上能够显示当前板子上的温度信息(任何显示形式都可以),要与OLED显示的温度值一致。
至此,需要写一个UART串口发送的模块,将读取得到的温度信息以UART通信传输给PC端,同时用MATLAB写了一个上位机,用来接收和显示温度信息,这里需要注意的点是,得到的温度信息是BCD码,而传输的数据是ASCII码,需要做一个转换。
第四项:PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件,OLED屏幕上显示的时间信息和温度信息都停住不再更新。
第一点:PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件。
在上位机接收到串口发来的信息之后,立马将上述所说的转换后的乐谱信息用串口发送个FPGA,就需要写一个UART接收模块,接收完数据后存储在例化的IP——RAM里面,然后读取出来驱动蜂鸣器发声。
第二点:OLED屏幕上显示的时间信息和温度信息都停住不再更新;结合第五项:音频文件播放完毕,OLED开始更新时间信息和当前的温度信息
利用上述的警报使能位,设计一个锁存器,当警报使能的时候,所有需要显示的信息都进行锁存,当警报使能为低的时候,锁存器为透明状态,即可实现数据更新。
always @ (posedge clk or negedge rst_n)
begin
if (!rst_n)
begin
temp_zheng_reg <= 1'b0;
temp_fen_reg <= 1'b0;
end
else if (en_waring & temp_zheng_reg != 1'b0)
begin
temp_zheng_reg <= temp_zheng_reg;
temp_fen_reg <= temp_fen_reg;
end
else
begin
temp_zheng_reg <= temp_zheng;
temp_fen_reg <= temp_fen;
end
end
完成的功能:
显示功能:
拨码开关1打到on的时候OLED显示时钟(时分秒)、实时温度
拨码开关1打到off,拨码开关4打到on的时候OLED显示日期、星期
拨码开关1打到off,拨码开关4打到off的时候显示硬禾学堂字符和logo
警报功能:
整点时候,通过uart串口发送温度信息到matlab上位机,上位机显示当前温度,并且发送回音频信息,由蜂鸣器播放出来,同时OLED上面的信息不再更新,11秒音频信息播放完之后,信息重新更新。
达到的性能:
资源使用情况截图
遇到的主要难题:
1.12832的OLED和12864一样是8页,按页写的模式一页仅4com,所以用spi写的时候是按数据抽取来点亮OLED。
2.hex 转 bcd的时候有个开始转换和结束转换的标志,如果没有符合一定的时序会在不正确的时间点输出错误的数据。
3.uart串口通信传输字符的时候还需要再先转换成ASCII码。
未来的计划等:
可以考虑增加一个WiFi模块,上电自动连接上位机更新时间。
重新写出参考别人的代码。
尽可能的优化资源。