项目需求
- 实现一个可定时时钟的功能,用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟即可,到整点的时候比如8:00,蜂鸣器报警,播放音频信号,最长可持续30秒;
- 实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度,并同时间一起显示在OLED的屏幕上;
- 定时时钟整点报警的同时,将温度信息通过UART传递到电脑上,电脑上能够显示当前板子上的温度信息(任何显示形式都可以),要与OLED显示的温度值一致;
- PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件,OLED屏幕上显示的时间信息和温度信息都停住不再更新;
- 音频文件播放完毕,OLED开始更新时间信息和当前的温度信息
设计思路
该系统由时钟模块,温度采集模块,温度格式转换模块,OLED显示模块,蜂鸣器(音乐播放)模块,UART通信模块组成,具体如下图:
时钟模块
输入:系统内部12MHz的时钟,系统复位信号,设置时间点的按键
输出:为时分秒的数字
该模块比较简单,首先通过时钟分频得到1Hz的时钟,然后在其触发下确定秒的变化,以此类推得到分针和时针。具体代码如下:
module clock(
input clk,
input rst_n,
input hour,//时钟+
input minutes_up,//分针+
input minutes_down,//分针-
input fresh_flag,
output reg clk_1Hz,
output [3:0] hour1,//时针十位
output [3:0] hour0,//时针个位
output [3:0] minutes1,//分针十位
output [3:0] minutes0,//分针个位
output [3:0] second1,//秒针十位
output [3:0] second0//秒针个位
);
reg [3:0] hour1_now,hour0_now,minutes1_now,minutes0_now,second1_now,second0_now;
reg [3:0] hour1_next,hour0_next,minutes1_next,minutes0_next,second1_next,second0_next;
//时钟分频,得到周期为1s的时钟clk_1Hz
reg [23:0] cnt;
parameter N = 12_000_000;
always@(posedge clk or negedge rst_n)begin
if(!rst_n) begin
clk_1Hz<=1'b0;
cnt<=0;
end
else if (cnt==N/2) begin
cnt<=24'd0;
clk_1Hz<=~clk_1Hz;
end
else
cnt<=cnt+1'b1;
end
/***************************
wire clk1Hz_down;
reg clk1Hz_d1,clk1Hz_d0;
//捕获接下降沿,得到一个时钟周期的脉冲信号
assign clk1Hz_down = clk1Hz_d1 & (~clk1Hz_d0);
//对clk_1Hz延迟两个时钟周期
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk1Hz_d0 <= 1'b0;
clk1Hz_d1 <= 1'b0;
end
else begin
clk1Hz_d0 <= clk_1Hz;
clk1Hz_d1 <= clk1Hz_d0;
end
end
此段未用,本想提高改变时间的反应速度,但发现1s也还可以
****************************************/
assign hour1 = hour1_now;
assign hour0 = hour0_now;
assign minutes1 = minutes1_now;
assign minutes0 = minutes0_now;
assign second1 = second1_now;
assign second0 = second0_now;
always @(posedge clk_1Hz or negedge rst_n) begin
if(!rst_n)begin //复位为00:00:00
hour1_now<=4'd0;
hour0_now<=4'd0;
minutes1_now<=4'd0;
minutes0_now<=4'd0;
second1_now<=4'd0;
second0_now<=4'd0;
end
else if(!fresh_flag) begin
second0_now<=second0_now;
second1_now<=second1_now;
hour1_now<=hour1_now;
hour0_now<=hour0_now;
minutes1_now<=minutes1_now;
minutes0_now<=minutes1_now;
end
else begin
hour1_now<=hour1_next;
hour0_now<=hour0_next;
minutes1_now<=minutes1_next;
minutes0_now<=minutes0_next;
second1_now<=second1_next;
second0_now<=second0_next;
end
end
always@(posedge clk_1Hz or negedge rst_n)// or hour or minutes_up or minutes_down )
begin
if(!rst_n)
begin
second0_next<=1'b0;
second1_next<=1'b0;
hour1_next<=4'd0;
hour0_next<=4'd0;
minutes1_next<=4'd0;
minutes0_next<=4'd0;
end
else begin
//else if(fresh_flag)
//begin
if(!hour)
begin
if(hour1_now==4'd2&&hour0_now==4'd3)
begin
hour1_next<=4'd0;
hour0_next<=4'd0;
end
else if(hour0_now==4'd9)
begin
hour0_next<=4'd0;
hour1_next<=hour1_now+1'b1;
end
else
hour0_next<=hour0_now+1'b1;
end
else if(!minutes_up)
begin
if(minutes1_now==4'd5&&minutes0_now==4'd9)
begin
minutes1_next<=4'd0;
minutes0_next<=4'd0;
if(hour1_now==4'd2&&hour0_now==4'd3)
begin
hour1_next<=4'd0;
hour0_next<=4'd0;
end
else if(hour0_now==4'd9)
begin
hour0_next<=4'd0;
hour1_next<=hour1_now+1'b1;
end
else
hour0_next<=hour0_now+1'b1;
end
else if(minutes0_now==4'd9&&minutes1_now<4'd5)
begin
minutes0_next<=4'd0;
minutes1_next<=minutes1_now+1'b1;
end
else
begin
minutes0_next<=minutes0_now+1'b1;
end
end
else if(!minutes_down)
begin
if(minutes1_now==4'd0&&minutes0_now==4'd0)
begin
minutes1_next<=4'd5;
minutes0_next<=4'd9;
end
else if(minutes1_now>4'd0&&minutes0_now==4'd0)
begin
minutes1_next<=minutes1_now-1'b1;
minutes0_next<=4'd9;
end
else
begin
minutes0_next<=minutes0_now-1'b1;
end
end
else
begin //每秒钟秒+1
if(second0_next==4'd9)//若秒的个位是9
begin
second0_next<=4'd0;//个位变为0,十位+1
if(second1_next==5&&second0_next==4'd9)
begin//若十位是5,即现在是59s
second1_next<=4'd0;//十位变为为0,即现在秒为00s,分钟+1
if(minutes0_next==4'd9)
begin//若分的个位是9
minutes0_next<=4'd0;//个位变为0,十位+1
if(minutes1_next==4'd5)
begin//若十位是5,即现在是59min
minutes1_next<=4'd0;//十位变为为0,即现在秒为00min,小时+1
if(hour0_next==4'd9)
begin//若小时的个位是9
hour0_next<=4'd0;//个位变为0,十位+1
hour1_next<=hour1_next+1'b1;
end
else if(hour0_next==4'd3&&hour1_next==4'd2 )
begin//若现在是23h,则下一小时为00
hour0_next<=4'd0;
hour1_next<=4'd0;
end
else
begin//否则小时个位+1
hour0_next<=hour0_next+1'b1;
end
end
else //否则分钟十位+1
minutes1_next<=minutes1_next+1'b1;
end
else //否则分钟个位+1
minutes0_next<=minutes0_next+1'b1;
end
else //否则秒十位+1
second1_next <= second1_next+1'b1;
end
else//否则秒个位+1
second0_next <= second0_next+1'b1;
end
end
end
endmodule
温度采集模块
此模块包括DS18B20Z信号采集转换为有效的温度信号,采用平台已给代码( https://www.eetree.cn/wiki/temp_sensor_verilog )得到16位二进制有效温度,再将二进制转换成bcd码的二进制传给显示屏和数码管进行显示(参考 https://blog.csdn.net/li200503028/article/details/19507061 )。
采集温度的代码在平台上,此处不再粘贴,数据格式转换主要代码如下:
// judge sign of temperature
wire temperature_flag ;
assign temperature_flag=dataout[15:11]? 1'b0:1'b1;
//temperature_flag==0:负温度; temperature_flag==1:正温度
// complement if negative
wire [10:0] temperature_code;
assign temperature_code = temperature_flag? dataout[10:0]:(~dataout[10:0])+1'b1;
// translate temperature_code to real temperature
wire [20:0] bin_code;
assign bin_code = temperature_code * 16'd625;
//reg [24:0] bcd_code; //十位[23:20],个位[19:16],小数位[14:12]parameter B_SIZE=21;
//Translate binary code to 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
OLED显示模块
此处也是采用平台给的代码,只是将显示内容改为了温度和时间,其他均未修改,在此不再贴出代码。
蜂鸣器模块
蜂鸣器通过改变振动频率发出不同的音调,我们通过改变一个周期内(这里是250ms)的震动次数来改变音调。整点报时的音乐是欢乐颂(虽然听起来并没有那么壮阔)。具体如下:
module Beeper(
input clk, //系统时钟12MHz
input clk_1Hz,
input rst_n,
input tone_en, //蜂鸣器使能
input uart_done, //串口使能
input[7:0] uart_data,
input[3:0] second1, //时间
input[3:0] minutes0,
input[3:0] minutes1,
output fresh_flag,
output beeper //蜂鸣器输出端
);
reg uart_done_cnt; //uart_done计数
reg beeper_reg; //寄存器
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 L_1 = 16'd22935,
L_2 = 16'd20428, //低音2
L_3 = 16'd18203, //低音3
L_5 = 16'd15305, //低音5
L_6 = 16'd13635, //低音6
L_7 = 16'd12147, //低音7
M_1 = 16'd11464, //中音1
M_2 = 16'd10215, //中音2
M_3 = 16'd9100, //中音3
M_4 = 16'd8589,
M_5 = 16'd7652, //中音5
M_6 = 16'd6817, //中音6
H_1 = 16'd5740; //高音1
parameter TIME = 3_000_000; //控制每一个音的长短(250ms)
assign beeper = beeper_reg; //输出音乐
assign fresh_flag = fresh;
always@(posedge clk)
begin
if(tone_en && minutes1 == 0 && minutes0 == 0 && second1 < 3)//整点才响铃,并且响铃时间为30s
begin
count <= count + 1'b1; //计数器加1
if(count == count_end)
begin
count <= 17'h0; //计数器清零
beeper_reg <= !beeper_reg; //输出取反
end
end
else if(tone_en && !fresh)
begin
count <= count + 1'b1; //计数器加1
if(count == count_end1)
begin
count <= 17'h0; //计数器清零
beeper_reg <= !beeper_reg; //输出取反
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'd63)
state = 8'd0;
else
state = state + 1'b1;
case(state)
8'd0,8'd1:count_end = M_3;
8'd2:count_end=M_4;
8'D3,8'd4:count_end=M_5;
8'D5:count_end=M_4;
8'D6:count_end=M_3;
8'D7:count_end=M_2;
8'D8,8'D9:count_end=M_1;
8'D10:count_end=M_2;
8'D11:count_end=M_3;
8'D12:count_end=L_3;
8'D13,8'D14:count_end=L_2;
8'D15:count_end=M_3;
8'D16:count_end=M_3;
8'D17:count_end=M_4;
8'D18:count_end=M_5;
8'D19:count_end=M_5;
8'D20:count_end=M_4;
8'D21:count_end=M_3;
8'D22:count_end=M_2;
8'D23,8'D24:count_end=M_1;
8'D25:count_end=M_2;
8'D26:count_end=M_3;
8'D27:count_end=L_2;
8'D28,8'D29:count_end=L_1;
8'D30,8'D31:count_end=M_2;
8'd32:count_end = M_3;
8'd33:count_end=M_1;
8'd34:count_end=M_2;
8'D35:count_end=M_3;
8'D36:count_end=M_4;
8'D37:count_end=M_3;
8'D38:count_end=M_1;
8'D39:count_end=M_2;
8'D40:count_end=M_3;
8'D41:count_end=M_4;
8'D42:count_end=M_3;
8'D43:count_end=M_2;
8'D44:count_end=M_1;
8'D45:count_end=M_2;
8'D46:count_end=L_5;
8'D47,8'D48,8'D49:count_end=M_3;
8'D50:count_end=M_4;
8'D51,8'D52:count_end=M_5;
8'D53:count_end=M_4;
8'D54:count_end=M_3;
8'D55:count_end=M_4;
8'D56:count_end=M_2;
8'D57,8'D58:count_end=M_1;
8'D59:count_end=M_2;
8'D60:count_end=M_3;
8'D61:count_end=L_2;
8'D62:count_end=M_1;
8'D63: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 clk_1Hz)
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
UART通信模块
- 串口发送
借鉴平台上叶开同学的代码:https://www.eetree.cn/project/detail/113
module uart_tx(
input clk_in,
input uart_en,//发送串口开关
input [3:0] temperature2,
input [3:0] temperature1,
input [3:0] temperature0,
input [3:0] hour1,
input [3:0] hour0,
input [3:0] minutes1,
input [3:0] minutes0,
input [3:0] second1,
input [3:0] second0,
output reg uart_out
);
localparam IDLE = 2'b0;
localparam SEND = 2'b1;
reg flag_1,flag_2,state;
reg [120:0] uart_data;
reg clk_uart;
reg[9:0] times;
//分频的到串口始终
always @(posedge clk_in) begin
if(times < 625)
times <= times+1;
else begin
clk_uart <= ~clk_uart;
times <= 0;
end
end
reg clk_en;
reg [23:0] clk_en_cnt;
always @(posedge clk_in) begin
if (clk_en_cnt <= 12_000_000) begin
clk_en_cnt <= clk_en_cnt + 1;
end else begin
clk_en_cnt <= 0;
clk_en <= ~clk_en;
end
end
//enter = \r\n
always @(posedge clk_en)
begin
if (temperature2*10+temperature1 >= 40)// 温度超过40° => 开始向pc发送时间和温度数据
begin
uart_data <= {
1'd1,8'd10,1'd0, //return
1'd1,8'd33,1'd0, //!
1'd1,8'd33,1'd0, //!
1'd1,8'd109,1'd0, //m
1'd1,8'd114,1'd0, //r
1'd1,8'd97,1'd0, //a
1'd1,8'd108,1'd0, //l
1'd1,8'd65,1'd0, //A
1'd1,8'd112,1'd0, //p
1'd1,8'd109,1'd0, //m
1'd1,8'd101,1'd0, //e
1'd1,8'd84,1'd0, //T
1'd1,1'd1
};
end
else if(second0 == 1 && second1 == 0 &&minutes0 == 0 && minutes1 == 0 && uart_en)//整点
begin
uart_data <= {
1'd1,8'd10,1'd0, //回车
1'd1,4'd3,minutes0,1'd0, //分钟低位
1'd1,4'd3,minutes1,1'd0, //分钟高位
1'd1,8'd58,1'd0, //:
1'd1,4'd3,hour0,1'd0, //小时低位
1'd1,4'd3,hour1,1'd0, //小时高位
1'd1,8'd32,1'd0, //space
1'd1,8'd67,1'd0, //C
1'd1,4'd3,temperature0,1'd0, //温度小数位
1'd1,8'd46,1'd0, //point
1'd1,4'd3,temperature1,1'd0, //温度个位
1'd1,4'd3,temperature2,1'd0, //温度十位
1'd1,1'd1
};
flag_1 <= ~flag_1;
end
end
reg [7:0] i;
always @(posedge clk_uart) begin
case(state)
IDLE: begin
if(flag_2 != flag_1) begin
flag_2 <= flag_1;
state <= SEND;
end
end
SEND: begin
if(i < 122) begin
uart_out <= uart_data[i];
i <= i+1;
end
else begin
i <= 0;
state <= IDLE;
end
end
endcase
end
endmodule
- 串口接收
找了网上一个通用的uart模板,仅修改波特率和内部时钟晶振频率https://blog.csdn.net/weifengdq/article/details/103168587
module uart_rx(
input clk, //系统时钟
input rst_n, //系统复位,低电平有效
input uart_rxd, //UART接收端口
output reg uart_done, //接收一帧数据完成标志信号
output reg [7:0] uart_data //接收的数据
);
//parameter define
parameter CLK_FREQ = 12_000_000; //系统时钟频率12MHz
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 clk or negedge rst_n) begin
if (!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 clk or negedge rst_n) begin
if (!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 clk or negedge rst_n) begin
if (!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 clk or negedge rst_n) begin
if ( !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 clk or negedge rst_n) begin
if (!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
学习心得:
硬禾这个平台的资源实在是太丰富了,这次的项目最大的体会是站在前人的肩膀上,路会好走很多,感谢做开源项目的人,为初学者提供了一个个向上的台阶。
遗憾:很多模块都是调用平台已有的代码,自己仅处于看懂阶段,有时间自己还是要自己写代码实现。感觉Diamond软件使用不太方便,也可能是自己不习惯,每个模块的仿真都是copy到vivado找问题再copy回来。