一、功能描述
(1)、实现了一个可定时时钟的功能。用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟。
(2)、实现了温度计的功能。通过功能底板上的温度传感器实时测量环境温度,并显示在OLED的屏幕上;
(3)、实现了与PC通信的功能。定时时钟到达整点时,将温度信息通过UART传递到电脑上,电脑上通过串口助手显示与OLED一致的温度信息;同时,PC端在接收到温度信息后,将一段音频信息通过UART发送给小脚丫,驱动小脚丫底板上的蜂鸣器播放这段音频。注:在播放音频文件时,OLED屏幕上的信息停止刷新,音频文件播放完毕后,OLED继续更新相关信息。
二、模块划分
三、设计思路简述
总体思路是根据功能先编写、测试好各个子模块,之后编写顶层模块,串联起各模块接口信号。在实现项目时,借鉴(白嫖)了不少“小脚丫开源社区”(https://www.stepfpga.com/doc/stepfpgaboard)项目中的代码,同时也从网上大佬那里取了不少经,在此向各位大佬表示感谢。
(1)、无源蜂鸣器模块
想让蜂鸣器奏乐,控制好音调(频率)和节拍(时长)即可。具体实现时,可将来自上位机的字节数据作为计数的终点,翻转蜂鸣器输出信号,再控制好同一音符的重复频次。
module beeper
(
input clk_in,
input rst_n_in,
input tone_en, //蜂鸣器使能信号
input [15:0] tone, //接收来自上位机的乐谱
output reg piano_out
);
reg [6:0] music;
reg [15:0] time_end;
always@(*) begin
time_end = tone;//根据接收的乐谱,确定计时终点
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) begin
piano_out <= ~piano_out;
end else begin
piano_out <= piano_out;
end
end
endmodule
(2)、RAM读写控制模块
为了能让蜂鸣器奏乐,实现时将上位机发送的乐谱数据暂存在了RAM中,然后从RAM中读出乐谱信息,并驱动蜂鸣器奏乐。项目中调用了diamond提供的伪双端口RAM IP,只需按照手册上的时序信息编写读写控制模块即可:
module ram_ctrl#
( //parameter是verilog里参数定义
parameter MC = 80 //表示乐谱包含的字节个数
)
(
input clk ,
input rst_n ,
input bps_en_rx ,//表示一个字节数据接收完毕:0表示完成接收 1表示接收中
output reg [8:0]wraddress,
output reg [8:0]rdaddress,
output reg we,
output reg rdclocken,
output reg wrclocken
);
reg [6:0] count ;
//向ram写入数据
/* assign we = !bps_en_rx ;//每次接收完成都把ram写使能打开 */
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
wraddress <= 9'b0;
count <= 7'd0;
end
else if (wrclocken)begin//写时钟使能有效,开始写入
wraddress <= wraddress + 1 ; //每一次写完数据,地址加一
count <= count + 7'd1; //每次接收完数据后,将计数器加一
end
else begin
wraddress <= wraddress ;
count <= count;
end
end
//捕捉we的上升沿,产生wrclocken信号
reg temp_we;
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
temp_we <= 1'b0;
wrclocken <= 1'b0;
end
else begin
we <= !bps_en_rx;
temp_we <= we;
wrclocken <= we & (!temp_we);
end
end
//产生读时钟有效信号
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
rdclocken <= 1'd0;
else if(count == MC)//当乐谱的所有字节都写入到ram时,可以启动rdclocken来进行读操作
rdclocken <= 1'b1;
else
rdclocken <= rdclocken;
end
//从到dpram读出数据:地址自加一
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
rdaddress <= 9'd0;
else if(rdclocken)//读时钟使能有效时,进行读操作
rdaddress <= rdaddress + 1;
else
rdaddress <= rdaddress;
end
endmodule
(3)、温度信息获取模块
首先根据单总线获取到16位的温度输出,之后将其按照各位的含义,处理得到温度的整数部分和小数部分。最后,利用“加三移位法”将十六进制表示的数转为BCD(8421)形式,以方便后面在OLED上显示:
//将采集的温度信息转为十进制,分为整数部分和小数部分
module ds18b20z(
input [15:0] in,
output [7:0] out_int,//温度整数部分
output [7:0] out_f//温度小数部分
);
wire [7:0] high,low;
wire [7:0] temp1,temp2;
assign high = in[15:8];
assign low = in[7:0];
assign temp1 = (high << 4) + (low >> 4);
assign temp2 = low & 8'h0F;
assign out_int = temp1;
assign out_f = (temp2 * 5)>>3;//相当于乘以0.625,获得小数部分
endmodule
//二进制转BCD(8421)码模块,采用加三移位法
module bin_to_bcd(
input [7:0] binary,
output reg [3:0] Hundreds,//百位表示
output reg [3:0] Tens,//十位表示
output reg [3:0] Ones//个位表示
);
integer i;
always @(binary)begin
Hundreds = 4'd0;
Tens = 4'd0;
Ones = 4'd0;
for(i = 7; i >= 0; i = i - 1)begin
if(Hundreds >= 5)
Hundreds = Hundreds + 3;
if(Tens >= 5)
Tens = Tens + 3;
if(Ones >= 5)
Ones = Ones + 3;
Hundreds = Hundreds << 1;
Hundreds[0] = Tens[3];
Tens = Tens << 1;
Tens[0] = Ones[3];
Ones = Ones << 1;
Ones[0] = binary[i];
end
end
endmodule
(4)、时间设置
由于要进行时间的设置,故引入了一个模式的切换,当键按下时,系统跳到另一个状态。同时,在进行时间设置前,不要忘记按键消抖:
//模式切换
always @(posedge clk,negedge rst_n)begin
if(!rst_n)begin
mode <= 1'b1;
end
else if(key_pulse[0])begin//表示模式键已经按下
mode <= ~mode;
end
else begin
mode <= mode;
end
end
//设置时间
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
hh1 <= h1;
hh2 <= h2;
mm1 <= m1;
mm2 <= m2;
end
else if(!mode)begin
if(key_pulse[1])begin//时钟位增加一
hh2 <= hh2 + 4'd1;
if(hh2 == 4'd9)begin
hh2 <= 4'd0;
hh1 <= hh1 + 4'd1;
end
else if(hh2 == 4'd3 && hh1 == 4'd2)begin
hh1 <= 4'd0;
hh2 <= 4'd0;
end
end
else if(key_pulse[2])begin//分钟位增加一
mm2 <= mm2 + 4'd1;
if(mm2 == 4'd9)begin
mm2 <= 4'd0;
mm1 <= mm1 + 4'd1;
if(mm1 == 4'd5)begin
mm1 <= 4'd0;
mm2 <= 4'd0;
end
end
end
end
else begin
hh1 <= h1;
hh2 <= h2;
mm1 <= m1;
mm2 <= m2;
end
end
(5)、OLED显示
当整点播放音乐时,OLED停止刷新。实现时,将字符处理的MAIN状态加上蜂鸣器使能判断:
MAIN:begin
//蜂鸣器响则屏幕停止刷新
if(cnt_main >= 5'd30 && !t_piano_en2) cnt_main <= 5'd1;
else if(cnt_main >= 5'd27 && t_piano_en2) cnt_main <= 5'd25;
else
cnt_main <= cnt_main + 1'b1;
case(cnt_main) //MAIN状态
中文字符的显示:
利用点阵字库生成器生成16*16的点阵字库数据,并用G和H的ascii码作为其存储器的下标:
mem[ 71] = {8'h10,8'h60,8'h02,8'h8C,8'h00,8'h00,8'hFE,8'h92,
8'h92,8'h92,8'h92,8'h92,8'hFE,8'h00,8'h00,8'h00,
8'h04,8'h04,8'h7E,8'h01,8'h40,8'h7E,8'h42,8'h42,
8'h7E,8'h42,8'h7E,8'h42,8'h42,8'h7E,8'h40,8'h00}; // 65 G 温
mem[ 72] = {8'h00,8'h00,8'hFC,8'h24,8'h24,8'h24,8'hFC,8'h25,
8'h26,8'h24,8'hFC,8'h24,8'h24,8'h24,8'h04,8'h00,
8'h40,8'h30,8'h8F,8'h80,8'h84,8'h4C,8'h55,8'h25,
8'h25,8'h25,8'h55,8'h4C,8'h80,8'h80,8'h80,8'h00}; // 66 H 度
由于利用了16*16的表示形式,故在OLED显示时,通过改变列页地址,在两页上显示一个字符,按序写页1页2:
case(cnt_main) //MAIN状态
5'd0: begin state <= INIT; end
5'd1: begin num_delay <= 24'd10; y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd2; char <= " ";state <= SCAN; end
5'd2: begin num_delay <= 24'd10; y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd2; char <= " ";state <= SCAN; end
//h1
5'd3: begin num_delay <= 24'd10; y_p <= 8'hb0; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd1; char <= t_h12;state <= SCAN; end
5'd4: begin num_delay <= 24'd10; y_p <= 8'hb1; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd1; char <= t_h12;state <= SCAN; end
//h2
5'd5: begin num_delay <= 24'd10; y_p <= 8'hb0; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd1; char <= t_h22;state <= SCAN; end
5'd6: begin num_delay <= 24'd10; y_p <= 8'hb1; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd1; char <= t_h22;state <= SCAN; end
//m1
5'd7: begin num_delay <= 24'd10; y_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd1; char <= t_m12;state <= SCAN; end
5'd8: begin num_delay <= 24'd10; y_p <= 8'hb1; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd1; char <= t_m12;state <= SCAN; end
//m2
5'd9: begin num_delay <= 24'd10; y_p <= 8'hb0; x_ph <= 8'h16; x_pl <= 8'h00; num <= 5'd1; char <= t_m22;state <= SCAN; end
5'd10: begin num_delay <= 24'd10; y_p <= 8'hb1; x_ph <= 8'h16; x_pl <= 8'h00; num <= 5'd1; char <= t_m22;state <= SCAN; end
//空格
5'd11: begin num_delay <= 24'd10; y_p <= 8'hb0; x_ph <= 8'h17; x_pl <= 8'h00; num <= 5'd1; char <= " ";state <= SCAN; end
5'd12: begin num_delay <= 24'd10; y_p <= 8'hb1; x_ph <= 8'h17; x_pl <= 8'h00; num <= 5'd1; char <= " ";state <= SCAN; end
//“温度:”
5'd13: begin num_delay <= 24'd10; y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd3; char <= "GH:";state <= SCAN; end
5'd14: begin num_delay <= 24'd10; y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd3; char <= "GH:";state <= SCAN; end
//十位
5'd15: begin num_delay <= 24'd10; y_p <= 8'hb2; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd1; char <= data_decade;state <= SCAN; end
5'd16: begin num_delay <= 24'd10; y_p <= 8'hb3; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd1; char <= data_decade;state <= SCAN; end
//个位
5'd17: begin num_delay <= 24'd10; y_p <= 8'hb2; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd1; char <= data_units;state <= SCAN; end
5'd18: begin num_delay <= 24'd10; y_p <= 8'hb3; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd1; char <= data_units;state <= SCAN; end
//小数点
5'd19: begin num_delay <= 24'd10; y_p <= 8'hb2; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd1; char <= ".";state <= SCAN; end
5'd20: begin num_delay <= 24'd10; y_p <= 8'hb3; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd1; char <= ".";state <= SCAN; end
//小数部分
5'd21: begin num_delay <= 24'd10; y_p <= 8'hb2; x_ph <= 8'h16; x_pl <= 8'h00; num <= 5'd1; char <= out_f[3:0];state <= SCAN; end
5'd22: begin num_delay <= 24'd10; y_p <= 8'hb3; x_ph <= 8'h16; x_pl <= 8'h00; num <= 5'd1; char <= out_f[3:0];state <= SCAN; end
为了产生冒号闪动的效果,在冒号显示后增加了空格的显示,并各自增加了1s的显示延时,注意将冒号显示放在最后,避免造成数字等显示延时:
//冒号闪动
5'd25: begin num_delay <= 24'd10; y_p <= 8'hb0; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd1; char <= ":";state <= SCAN; end
5'd26: begin num_delay <= 24'd10; y_p <= 8'hb1; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd1; char <= ":";state <= SCAN; end
5'd27: begin num_delay <= 24'd12000000; state <= DELAY; state_back <= MAIN; end
5'd28: begin num_delay <= 24'd10; y_p <= 8'hb0; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd1; char <= " "; state <= SCAN; end
5'd29: begin num_delay <= 24'd10; y_p <= 8'hb1; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd1; char <= " "; state <= SCAN; end
5'd30: begin num_delay <= 24'd12000000; state <= DELAY; state_back <= MAIN; end
四、资源占用情况
五、总结感悟
1、当代码通过了功能仿真,上板测试却总是出错时,或许可以考虑考虑,是不是硬件出了问题,比如这次因为自己懒得焊接排针,OLED总是无法显示,最后平生第一次拿起了焊接枪,OLED终于乖乖显示了(在此感谢硬禾学堂的客服老师);
2、不要忘了异步时钟的同步;
3、一定要认真阅读文档,细节决定成败。
六、未来的计划
好好学习,天天向上,多找类似的项目练练手,多多锻炼自己的文档阅读及coding的能力。