2021寒假在家一起练4(小脚丫FPGA)——朱金波
基于小脚丫FPGA的综合技能训练平台,实现了定时,测温,报警,UART通信灯功能
标签
FPGA
小明z
更新2021-02-28
986

内容介绍

一、项目要求

  1. 实现一个可定时时钟的功能,用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟即可,到整点的时候比如8:00,蜂鸣器报警,播放音频信号,最长可持续30秒;
  2. 实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度,并同时间一起显示在OLED的屏幕上;

  3. 定时时钟整点报警的同时,将温度信息通过UART传递到电脑上,电脑上能够显示当前板子上的温度信息(任何显示形式都可以),要与OLED显示的温度值一致;

  4. PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件,OLED屏幕上显示的时间信息和温度信息都停住不再更新;

  5. 音频文件播放完毕,OLED开始更新时间信息和当前的温度信息

二、RTL,模块列表如图:Fh8pl3hejeH8ri6S0ZLFcFJM1iJ8

Fuu8BIJX7ARe6yX1ns2KWFqGDWxjFgg-HIz9N758SvpBhuJhUNeruB6a

本项目绝大部分代码都可以在电子森林开源平台里面找到,非常感谢能有这个平台学习,在调试过程中也参考了之前同学的代码,在此表示感谢

三、主要模块介绍

1.顶层文件:

实例化了各功能模块,让他们组合起来一起实现功能,之前只有C语言的基础,用了verilog后觉得这种模块化的设计可以直接调用,真的很方便,不过要注意好各种wire定义。

module Top(
input sys_clk, //外部12M时钟
input sys_rst_n, //外部复位信号,低有效
inout one_wire, //DS18B20Z传感器单总线,双向管脚

input [3:0] key, //消抖


output oled_csn, //OLCD液晶屏使能
output oled_rst, //OLCD液晶屏复位
output oled_dcn, //OLCD数据指令控制
output oled_clk, //OLCD时钟信号
output oled_dat, //OLCD数据信号

output audio,//蜂鸣器使能信号
input uart_rxd,//串口输入
output UartTx

);

//wire define
wire [15:0] temp_raw_data;
wire temp_data_out_en;

wire [3:0] temp_unit;
wire [3:0] temp_ten;
wire [3:0] temp_hun;

wire [3:0] hour_high;
wire [3:0] hour_low;
wire [3:0] min_high;
wire [3:0] min_low;
wire [3:0] sec_high;
wire [3:0] sec_low;


wire [3:0]key_pulse;//消抖

wire button; //蜂鸣器开始工作标志,同时发送温度到串口

wire fresh_flag;
//串口播放音乐需要的线
wire[7:0] uart_data;
wire clk1h;
wire uart_done;//接收过程标志信号

//*****************************************************
//** main code
//*****************************************************

//消抖
debounce debounce_inst
(
.clk (sys_clk),
.rst (sys_rst_n),
.key (key),
.key_pulse (key_pulse)
);

//时钟设置模块
timeset timeset_inst
(
.clk (sys_clk), //12MHz系统时钟
.rst_n (sys_rst_n),

.set(key_pulse), //设置时间按键

.hour_high (hour_high),
.hour_low (hour_low),
.min_high (min_high),
.min_low (min_low),
.sec_high(sec_high),
.sec_low(sec_low)
);

//DS18B20 read module
ds18b20_ctrl ds18b20_ctrl_inst(
.clk_in (sys_clk),
.rst_n_in (sys_rst_n),
.one_wire (one_wire),

.data_out (temp_raw_data),
.data_out_en (temp_data_out_en)
);

//bcd-8421 converter module
bcd bcd_inst(
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.data (temp_raw_data),

.unit (temp_unit),
.ten (temp_ten),
.hun (temp_hun)
);

//oled display module
OLED12832 OLED12832_inst(
.clk (sys_clk),
.rst_n (sys_rst_n),
.temp_unit (temp_unit),
.temp_ten (temp_ten),
.temp_hun (temp_hun),


.hour_high(hour_high),
.hour_low(hour_low),
.min_high(min_high),
.min_low(min_low),
.sec_high(sec_high),
.sec_low(sec_low),

.fresh_flag(fresh_flag),
.oled_csn (oled_csn),
.oled_rst (oled_rst),
.oled_dcn (oled_dcn),
.oled_clk (oled_clk),
.oled_dat (oled_dat)
);


//整点时通过串口发送温度信息到PC
Uart_Tx Uart_Tx_inst(
.clk_in (sys_clk),
.temp_hun (temp_hun),
.temp_ten (temp_ten),
.temp_unit (temp_unit),
.min_high(min_high),
.min_low(min_low),
.sec_high(sec_high),
.uart_out (UartTx)
);

//分频
FreqDiv FreqDiv_v1(
.clk(sys_clk),
.rst( sys_rst_n),

.clkout(clk1h)
);

//串口音乐
Beeper Beeper_inst(
.clk(sys_clk),
.clk1h(clk1h),
.rst( sys_rst_n),
.beep( audio),
.min_high(min_high),
.min_low(min_low),
.sec_high(sec_high),
.uart_data(uart_data),

.fresh_flag(fresh_flag),
.uart_done(uart_done)
);

//串口接收
Uart_Rx Uart_Rx_inst(

.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.uart_rxd(uart_rxd),

.uart_done(uart_done),
.uart_data(uart_data)
);

endmodule

2.time模块:

输入四个按键完成了时钟的计时以及调整功能,并输出时间信息到OLED上显示。四个按键分别对应分钟个位加,分钟个位减,小时个位加,小时个位减,同时引用了消抖模块对4个按键进行消抖,这样才能在按键的时候不会因为抖动出现问题。用计数器进行计数,实现秒的增加,然后编写一些逻辑,让个位到9归零,十位到6归零。

module timeset
(
input clk, //12MHz系统时钟
input rst_n,

input [3:0] set, //四个按键

output reg [3:0] hour_high,
output reg [3:0] hour_low,
output reg [3:0] min_high,
output reg [3:0] min_low,
output reg [3:0] sec_high,
output reg [3:0] sec_low
);

reg [23:0] cnt; //计数


wire add_cnt; //计时一秒
wire end_cnt;

wire add_sec_low; //秒个开始计数标志
wire end_sec_low;//秒个结束计数标志

wire add_sec_high; //秒十开始计数标志
wire end_sec_high;//秒十结束计数标志

wire add_min_low; //分个开始计数标志
wire end_min_low; //分个结束计数标志

wire add_min_high; //分十开始计数标志
wire end_min_high; //分十结束计数标志

wire add_hour_low; //时个开始计数标志
wire end_hour_low; //时个结束计数标志

wire add_hour_high; //时十开始计数标志
wire end_hour_high; //时十结束计数标志

always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
cnt <= 0;
end
else if (add_cnt)begin
if(end_cnt)
cnt <=0 ;
else
cnt <= cnt +1;
end
end
assign add_cnt = 1;
assign end_cnt = add_cnt && cnt == 12000000;
//sec_low计数
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
sec_low <= 0;
end
else if (add_sec_low)begin
if(end_sec_low)
sec_low <=0 ;
else
sec_low <= sec_low +1;
end
end
assign add_sec_low = end_cnt;
assign end_sec_low = add_sec_low && sec_low == 9;
//sec_high计数
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
sec_high <= 0;
end
else if (add_sec_high)begin
if(end_sec_high)
sec_high <=0 ;
else
sec_high <= sec_high +1;
end
end
assign add_sec_high = end_sec_low;
assign end_sec_high = add_sec_high && sec_high == 5;

//min_low计数
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
min_low <= 2;
end
else if (add_min_low)begin
if(end_min_low)
min_low <=0 ;
else
min_low <= min_low +1;
end
else if (set[0] ) begin
if ( min_low == 9)
min_low <= 0;
else
min_low <= min_low + 1;
end
end
assign add_min_low = end_sec_high;
assign end_min_low = add_min_low && min_low == 9;
//min_high计数
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
min_high <= 0;
end
else if (add_min_high)begin
if(end_min_high)
min_high <=0 ;
else
min_high <= min_high +1;
end
else if (set[1] ) begin

if ( min_high == 5)
min_high <= 0;
else
min_high <= min_high + 1;
end
end
assign add_min_high = end_min_low;
assign end_min_high = add_min_high && min_high == 5;
//hour_low计数
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
hour_low <= 0;
end
else if (add_hour_low)begin
if(end_hour_low)
hour_low <=0 ;
else
hour_low <= hour_low +1;
end
else if (set[2] ) begin

if ( hour_low == 9)
hour_low <= 0;
if ( hour_high == 2 && hour_low == 3)
hour_low <= 0;
else
hour_low <= hour_low + 1;
end
end
assign add_hour_low = end_min_high;
assign end_hour_low = add_hour_low && (hour_low == 9||(hour_high == 2 && hour_low == 3));
//hour_high计数
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
hour_high <= 0;
end
else if (add_hour_high)begin
if(end_hour_high)
hour_high <=0 ;
else
hour_high <= hour_high +1;
end
else if (set[3] ) begin

if ( hour_high == 2)
hour_high <= 0;
else
hour_high <= hour_high + 1;
end
end
assign add_hour_high = end_hour_low;
assign end_hour_high = add_hour_high && hour_high == 2;
endmodule

3.ds18b20_ctrl模块:

DS18B20单线数字温度传感器,支持“一线总线”接口,测量温度范围为-55摄氏度到+125摄氏度,精度为±0.5摄氏度。通过驱动DS18B20Z芯片获取温度数据,然后通过bcd模块转换成bcd码,方便后续进行温度的显示与报警。这个模块比较难,弄了好久,数据手册看起来比较麻烦,不过电子森林也有相关的开源代码,给我的学习带来了很大的帮助。

module ds18b20_ctrl
(
input clk_in, //系统时钟
input rst_n_in, //系统复位,低有效
inout one_wire, //DS18B20Z传感器单总线,双向管脚

output reg [15:0] data_out, //DS18B20Z有效温度数据输出
output reg data_out_en
);

/*
本设计通过驱动DS18B20Z芯片获取温度数据,
需要了解inout类型的接口如何实现双向通信,
中间涉及各种不同的延时和寄存器指令操作,注释部分以作简要说明,更多详情需参考数据手册
*/

localparam IDLE = 3'd0;
localparam MAIN = 3'd1;
localparam INIT = 3'd2;
localparam WRITE = 3'd3;
localparam READ = 3'd4;
localparam DELAY = 3'd5;

// assign temp_raw_data = data_out;

//计数器分频产生1MHz的时钟信号
reg clk_1mhz;
reg [2:0] cnt_1mhz;
// reg data_out;

always@(posedge clk_in or negedge rst_n_in) begin
if(!rst_n_in) begin
cnt_1mhz <= 3'd0;
clk_1mhz <= 1'b0;
end else if(cnt_1mhz >= 3'd5) begin
cnt_1mhz <= 3'd0;
clk_1mhz <= ~clk_1mhz; //产生1MHz分频
end else begin
cnt_1mhz <= cnt_1mhz + 1'b1;
end
end

reg [2:0] cnt;
reg one_wire_buffer;
reg [3:0] cnt_main;
reg [7:0] data_wr;
reg [7:0] data_wr_buffer;
reg [2:0] cnt_init;
reg [19:0] cnt_delay;
reg [19:0] num_delay;
reg [3:0] cnt_write;
reg [2:0] cnt_read;
reg [15:0] temperature;
reg [7:0] temperature_buffer;
reg [2:0] state = IDLE;
reg [2:0] state_back = IDLE;
//使用1MHz时钟信号做触发完成下面状态机的功能
always@(posedge clk_1mhz or negedge rst_n_in) begin
if(!rst_n_in) begin
state <= IDLE;
state_back <= IDLE;
cnt <= 1'b0;
cnt_main <= 1'b0;
cnt_init <= 1'b0;
cnt_write <= 1'b0;
cnt_read <= 1'b0;
cnt_delay <= 1'b0;
one_wire_buffer <= 1'bz;
temperature <= 16'h0;
data_out_en <= 0;
end else begin
case(state)
IDLE:begin //IDLE状态,程序设计的软复位功能,各状态异常都会跳转到此状态
state <= MAIN; //软复位完成,跳转之MAIN状态重新工作
state_back <= MAIN;
cnt <= 1'b0;
cnt_main <= 1'b0;
cnt_init <= 1'b0;
cnt_write <= 1'b0;
cnt_read <= 1'b0;
cnt_delay <= 1'b0;
one_wire_buffer <= 1'bz;
end
MAIN:begin //MAIN状态控制状态机在不同状态间跳转,实现完整的温度数据采集
if(cnt_main >= 4'd11) cnt_main <= 1'b0;
else cnt_main <= cnt_main + 1'b1;
case(cnt_main)
4'd0: begin state <= INIT; data_out_en <= 0; end //跳转至INIT状态进行芯片的复位及验证
4'd1: begin data_wr <= 8'hcc;state <= WRITE; end //主设备发出跳转ROM指令
4'd2: begin data_wr <= 8'h44;state <= WRITE; end //主设备发出温度转换指令
4'd3: begin num_delay <= 20'd750000;state <= DELAY;state_back <= MAIN; end //延时750ms等待转换完成

4'd4: begin state <= INIT; end //跳转至INIT状态进行芯片的复位及验证
4'd5: begin data_wr <= 8'hcc;state <= WRITE; end //主设备发出跳转ROM指令
4'd6: begin data_wr <= 8'hbe;state <= WRITE; end //主设备发出读取温度指令

4'd7: begin state <= READ; end //跳转至READ状态进行单总线数据读取
4'd8: begin temperature[7:0] <= temperature_buffer; end //先读取的为低8位数据

4'd9: begin state <= READ; end //跳转至READ状态进行单总线数据读取
4'd10: begin temperature[15:8] <= temperature_buffer; end //后读取的为高8为数据

4'd11: begin state <= IDLE;data_out <= temperature; data_out_en <= 1; end //将完整的温度数据输出并重复以上所有操作
default: state <= IDLE;
endcase
end
INIT:begin //INIT状态完成DS18B20Z芯片的复位及验证功能
if(cnt_init >= 3'd6) cnt_init <= 1'b0;
else cnt_init <= cnt_init + 1'b1;
case(cnt_init)
3'd0: begin one_wire_buffer <= 1'b0; end //单总线复位脉冲拉低
3'd1: begin num_delay <= 20'd500;state <= DELAY;state_back <= INIT; end //复位脉冲保持拉低500us时间
3'd2: begin one_wire_buffer <= 1'bz; end //单总线复位脉冲释放,自动上拉
3'd3: begin num_delay <= 20'd100;state <= DELAY;state_back <= INIT; end //复位脉冲保持释放100us时间
3'd4: begin if(one_wire) state <= IDLE; else state <= INIT; end //根据单总线的存在检测结果判定是否继续
3'd5: begin num_delay <= 20'd400;state <= DELAY;state_back <= INIT; end //如果检测正常继续保持释放400us时间
3'd6: begin state <= MAIN; end //INIT状态操作完成,返回MAIN状态
default: state <= IDLE;
endcase
end
WRITE:begin //按照DS18B20Z芯片单总线时序进行写操作
if(cnt <= 3'd6) begin //共需要发送8bit的数据,这里控制循环的次数
if(cnt_write >= 4'd6) begin cnt_write <= 1'b1; cnt <= cnt + 1'b1; end
else begin cnt_write <= cnt_write + 1'b1; cnt <= cnt; end
end else begin
if(cnt_write >= 4'd8) begin cnt_write <= 1'b0; cnt <= 1'b0; end //两个变量都恢复初值
else begin cnt_write <= cnt_write + 1'b1; cnt <= cnt; end
end
//对于WRITE状态中cnt_write来讲,执行过程为:0;[1~6]*8;7;8;
case(cnt_write)
//lock data_wr
4'd0: begin data_wr_buffer <= data_wr; end //将需要写出的数据缓存
//发送 1bit 数据的用时在60~120us之间,参考数据手册
4'd1: begin one_wire_buffer <= 1'b0; end //总线拉低
4'd2: begin num_delay <= 20'd2;state <= DELAY;state_back <= WRITE; end //延时2us时间,保证15us以内
4'd3: begin one_wire_buffer <= data_wr_buffer[cnt]; end //先发送数据最低位
4'd4: begin num_delay <= 20'd80;state <= DELAY;state_back <= WRITE; end //延时80us时间
4'd5: begin one_wire_buffer <= 1'bz; end //总线释放
4'd6: begin num_delay <= 20'd2;state <= DELAY;state_back <= WRITE; end //延时2us时间
//back to main
4'd7: begin num_delay <= 20'd80;state <= DELAY;state_back <= WRITE; end //延时80us时间
4'd8: begin state <= MAIN; end //返回MAIN状态
default: state <= IDLE;
endcase
end
READ:begin //按照DS18B20Z芯片单总线时序进行读操作
if(cnt <= 3'd6) begin //共需要接收8bit的数据,这里控制循环的次数
if(cnt_read >= 3'd5) begin cnt_read <= 1'b0; cnt <= cnt + 1'b1; end
else begin cnt_read <= cnt_read + 1'b1; cnt <= cnt; end
end else begin
if(cnt_read >= 3'd6) begin cnt_read <= 1'b0; cnt <= 1'b0; end //两个变量都恢复初值
else begin cnt_read <= cnt_read + 1'b1; cnt <= cnt; end
end
case(cnt_read)
//读取 1bit 数据的用时在60~120us之间,总线拉低后15us时间内读取数据,参考数据手册
3'd0: begin one_wire_buffer <= 1'b0; end //总线拉低
3'd1: begin num_delay <= 20'd2;state <= DELAY;state_back <= READ; end //延时2us时间
3'd2: begin one_wire_buffer <= 1'bz; end //总线释放
3'd3: begin num_delay <= 20'd5;state <= DELAY;state_back <= READ; end //延时5us时间
3'd4: begin temperature_buffer[cnt] <= one_wire; end //读取DS18B20Z返回的总线数据,先收最低位
3'd5: begin num_delay <= 20'd60;state <= DELAY;state_back <= READ; end //延时60us时间
//back to main
3'd6: begin state <= MAIN; end //返回MAIN状态
default: state <= IDLE;
endcase
end
DELAY:begin //延时控制
if(cnt_delay >= num_delay) begin //延时控制,延时时间由num_delay指定
cnt_delay <= 1'b0;
state <= state_back; //很多状态都需要延时,延时后返回哪个状态由state_back指定
end else cnt_delay <= cnt_delay + 1'b1;
end
endcase
end
end
assign one_wire = one_wire_buffer;
endmodule

4.OLED模块:

这里的代码直接用了电子森林的代码,并进行了修改,达到了想要的效果,因为报警的时候OLED暂停刷屏,所以引入了一个信号fresh_flag,当检测到串口接收数据时,fresh_flag=0,OLED停止刷屏

MAIN部分修改的部分如下

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 <= " TEMP ";state <= SCAN; end
5'd2: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end
5'd3: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " TIME ";state <= SCAN; end
5'd4: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end

5'd5: begin y_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd 4; char <= {4'd0,temp_hun,4'd0,temp_ten,".",4'd0,temp_unit}; state <= SCAN; end
5'd6: begin y_p <= 8'hb1; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end
5'd7: begin y_p <= 8'hb2; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd 5; char <= {4'd0,hour_high,4'd0,hour_low,":",4'd0,min_high,4'd0,min_low};state <= SCAN; end
5'd8: begin y_p <= 8'hb3; x_ph <= 8'h16; x_pl <= 8'h00; num <= 5'd2; char <= {4'd0,sec_high,4'd0,sec_low};state <= SCAN; end

default: state <= IDLE;
endcase

5.Uart_Tx串口发送模块:

此模块参考了前面同学的代码并进行了改良,在判断到整点或者温度到达预警值时分别对PC输出不同的信息。

module Uart_Tx(
input clk_in,
input [3:0] temp_hun,
input [3:0] temp_ten,
input [3:0] temp_unit,
input[3:0] min_high, //时间
input[3:0] min_low,
input[3:0] sec_high,

output uart_out
);

localparam IDLE = 2'b0;
localparam SEND = 2'b1;

reg uart_out,flag_1,flag_2,state;
reg [120:0] uart_data;
reg [7:0] tab[9:0];
reg [6:0] i;

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

reg temp_TX;
always @(posedge clk_en) begin
if (((temp_hun * 10 + temp_ten ) >= 25 )|| (min_high==0&&min_low==0))
temp_TX <= 1;
else temp_TX <= 0;
end

always @(posedge clk_en) begin
if(temp_TX==1) begin

if ((temp_hun * 10 + temp_ten ) >= 25) 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,4'd3,temp_unit,1'd0, //one tenth
1'd1,8'd46,1'd0, //point
1'd1,4'd3,temp_ten,1'd0, //unit
1'd1,4'd3,temp_hun,1'd0, //ten
1'd1,8'd33,1'd0, //!
1'd1,8'd33,1'd0, //!
1'd1,1'd1
};
end else
uart_data = {
1'd1,8'd10,1'd0, //return
1'd1,8'd32,1'd0, //space
1'd1,4'd3,temp_unit,1'd0, //one tenth
1'd1,8'd46,1'd0, //point
1'd1,4'd3,temp_ten,1'd0, //unit
1'd1,4'd3,temp_hun,1'd0, //ten
1'd1,1'd1
};
flag_1 = ~flag_1;
end
end

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

6.Uart_Rx串口接收数据模块:

通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),通常称作UART,是一种通用串行数据总线,用于异步通信。该总线双向通信,可以实现全双工传输和接收。串口接收模块输出了接收完成的标志位,可以用来判断是否接收完成以此控制OLED的刷新。

module Uart_Rx(

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

7.Beeper蜂鸣器模块

这次用的是无源蜂鸣器,通过输出不同的PWM信号来控制音调,本项目中整点报时和温度报警两次用到了蜂鸣器,输出不同的音乐。整点报时实现的方法是在代码里写好对用case输出的音调,然后通过计数器累加case选择。而温度报警实现的方法是通过串口通信接收数据,根据数据选择相应的音调。两者都是以250ms为一个基本拍。同时在接收串口数据的时候输出fresh标志位,可以用于暂停OLED的刷屏功能。

module Beeper(
input clk, //系统时钟12MHz
input clk1h,
input rst,
input uart_done, //蜂鸣器使能
input[7:0] uart_data,
input[3:0] min_high, //时间
input[3:0] min_low,
input[3:0] sec_high,
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 L_1 = 16'd22935, //低音1
L_2 = 16'd20428, //低音2
L_3 = 16'd18203, //低音3
L_4 = 16'd17181, //低音4
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, //中音4
M_5 = 16'd7652, //中音5
M_6 = 16'd6817, //中音6
M_7 = 16'd6073, //中音7
H_1 = 16'd5740,//高音1
H_2 = 16'd5107, //高音2
H_3 = 16'd4549, //高音3
H_4 = 16'd4294, //高音4
H_5 = 16'd3825, //高音5
H_6 = 16'd3408, //高音6
H_7 = 16'd3036, //高音7
full = 16'd65535; //空

parameter TIME = 3_000_000; //控制每一个音的长短(250ms)
assign beep = beep_r; //输出音乐
assign fresh_flag = fresh;

always@(posedge clk)
begin
if(min_high==0&&min_low==0&&(sec_high==0||sec_high==1||sec_high==2))//整点才响铃,并且响铃时间为30s
begin
count <= count + 1'b1; //计数器加1
if(count == count_end)
begin
count <= 17'h0; //计数器清零
beep_r <= !beep_r; //输出取反
end
end
else if(!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'd51)
state = 8'd0;
else
state = state + 1'b1;
case(state)
8'd0:count_end =M_1;
8'd1:count_end=M_1;
8'd2:count_end=M_2;
8'D3:count_end=M_2;
8'D4,8'D5:count_end=M_3;
8'D6:count_end=M_1;
8'D7:count_end=M_1;

8'D8,8'D9:count_end=M_1;
8'D10:count_end=M_2;
8'D11:count_end=M_2;
8'D12:count_end=M_3;
8'D13:count_end=M_3;
8'D14:count_end=M_1;
8'D15:count_end=M_1;

8'D16:count_end=M_3;
8'D17:count_end=M_3;
8'D18:count_end=M_4;
8'D19:count_end=M_4;
8'D20,8'D21:count_end=M_5;
8'D22:count_end=M_3;
8'D23:count_end=M_3;
8'D24,8'D25:count_end=M_4;
8'D26:count_end=M_5;
8'D27:count_end=M_5;
8'D28:count_end=M_5;
8'D29:count_end=M_6;
8'D30:count_end=M_5;
8'D31:count_end=M_4;

8'd32:count_end= M_3;
8'd33:count_end=M_1;
8'd34:count_end=M_5;
8'D35:count_end=M_6;
8'D36:count_end=M_5;
8'D37:count_end=M_4;
8'D38:count_end=M_3;
8'D39:count_end=M_1;

8'D40,8'D41:count_end=M_1;
8'D42:count_end=M_5;
8'D43:count_end=M_5;
8'D44,8'D45:count_end=M_1;
8'D46:count_end=M_1;
8'D47:count_end=M_1;

8'D48:count_end=M_5;
8'D49:count_end=M_5;
8'D50:count_end=M_1;
8'D51:count_end=M_1;
/*8'D52,8'D53:count_end=L_6;
8'D54,8'D55:count_end=L_5;

8'D56,8'D57,8'D58,8'D59,8'D60,8'D61:count_end=M_1;
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 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

8.debounce消抖模块用了电子森林的代码,消抖了四个按键,FreDiv分频模块分出了1hz的时钟,都比较简单,在此就不贴代码了。

整体FPGA资源使用报告如下:

FoMBztYWgwiLWUfFf4Tzu6RhAb5Q

四、总结

  因为本人专业能力比较低,所以这次项目做起来还是很艰难的,而且是在寒假,很难静下来学习。不过电子森林很多的例程和开源项目给了我很大的帮助,还有之前完成项目同学的代码对我也有很大的参考价值。最终还是完成了项目,并满足了所有要求。

 

附件下载

StepTop.zip
工程文件都在压缩包里

团队介绍

南京工程学院 信息与通信工程学院
团队成员
朱金波
一个好吃懒做的大三菜鸟

评论

0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号