项目描述及要求:
基于包含Lattice版本的小脚丫FPGA综合训练板(STEP-MXO2)完成以下基本功能:
1.实现一个可定时时钟的功能,用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟即可,到整点的时候比如8:00,蜂鸣器报警,播放音频信号,持续30秒;
2.实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度,并同时间一起显示在OLED的屏幕上;
3.定时时钟整点报警的同时,将温度信息通过UART传递到电脑上,电脑上能够显示当前板子上的温度信息,要与OLED显示的温度值一致;
4.PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件,OLED屏幕上显示的时间信息和温度信息都停住不再更新;
5.音频文件播放完毕,OLED开始更新时间信息和当前的温度信息
实现思路:
基本功能模块的关系:
Netlist分析电路:
串口调试工具设定页面:
完成功能及部分功能模块展示:
经过对相关代码的编写及调试,完成了项目要求里的内容,实现了定时、报警、显示、音乐播放等功能,下面对功能模块做一定的阐释。
TOP模块:
top模块是顶层模块,是实现各个功能的控制中心,通过在top模块里调用各个模块功能并进行组合调用,从而实现完整的功能。编程语言为verilog,此语言对于模块的组合调用可以说是十分方便,甚至是精妙,它能够使各种复杂的数字逻辑变得十分清晰甚至有序,不失为数字电路中一种好的编程语言,下面为部分代码(仅展示部分,显示其调用关系,完整的代码见附件):
module top(
input clk,
input key_hour,//小时增加键
input key_min_a,//分钟增加键
input key_min_m,//分钟减少键
input rst_n,//复位键
input uart_rxd, //串口输入
input uart_on,
inout tone_en,
inout one_wire,
output oled_csn, //OLCD液晶屏使能
output oled_rst, //OLCD液晶屏复位
output oled_dcn, //OLCD数据指令控制
output oled_clk, //OLCD时钟信号
output oled_dat, //OLCD数据信号
output uart_out, //串口输出
output beeper //蜂鸣器
);
wire[15:0] d_out;
wire[3:0] temp1,temp2,temp3,temp4;
wire[3:0] hour1,hour2,min1,min2,sec1,sec2;
wire flag;
//温度监测
DS18B20Z DS18B20Z_v1(
.clk_in(clk),
.rst_n_in(rst_n),
.one_wire(one_wire),
.data_out(d_out)
);
//二进制转十进制
bin_to_code bin_to_code_v1(
.clk(clk),
.rst(rst_n),
.flag(flag),
.data_in(d_out),
.d_out1(temp1),
.d_out2(temp2),
.d_out3(temp3),
.d_out4(temp4)
);
Ds18b20模块:
Ds18b20模块温度传感模块,DS18B20是我们日常设计中常用的一款温度传感器芯片,只需要一根总线就可以实现通信,非常的方便,利用FPGA驱动,需将总线做上拉处理,此总线为双向总线,编程时需了解如何设置双向通信及一系列指令操作。此部分参考了电子森林的代码:
module DS18B20Z
(
input clk_in, //系统时钟
input rst_n_in, //系统复位,低有效
inout one_wire, //DS18B20Z传感器单总线,双向管脚
output reg [15:0] data_out //DS18B20Z有效温度数据输出
);
/*
本设计通过驱动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;
//计数器分频产生1MHz的时钟信号
reg clk_1mhz;
reg [2:0] cnt_1mhz;
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;
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; 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; 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
转码模块:
bin_to_code为转码模块,将温度信息转化为BCD码输出,此部分利用现成的代码,此处不再累赘。
OLED模块:
oled模块是显示模块,通过驱动芯片SSD1306实现点阵屏幕的显示,编程时应用了状态机的知识,数据通过4线串行总线输入刷新RAM,代码参考了电子森林的部分代码,同时由于显示温度和时间两种信息,输入输出寄存器端口等较复杂,与温度模块和时钟模块的数据输出端联系。
module OLED12832
(
input clk, //12MHz系统时钟
input rst_n, //系统复位,低有效
input [3:0] temp1,temp2,temp3,temp4,
input [3:0] hour1,hour2,min1,min2,sec1,sec2,
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 [123: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;
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(cnt_main >= 5'd6) cnt_main <= 5'd5;
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: "; state <= SCAN; end
5'd2: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "TEMP: "; state <= SCAN; end
5'd3: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " "; state <= SCAN; end
5'd4: begin y_p <= 8'hb2; 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'h13; x_pl <= 8'h00; num <= 5'd 5; char <= {4'd0, hour1, 4'd0,hour2, ":", 4'd0,min1, 4'd0, min2}; state <= SCAN;
end
5'd6:if(temp1 == 4'd2)
begin
y_p <= 8'hb3; x_ph <= 8'h12; x_pl <= 8'h08; num <= 5'd 6; char <= {"-", 4'd0, temp3, 4'd0, temp4, ".", 4'd0, temp1,8'h7b,"C"}; state <= SCAN;
end
else if(temp2 == 4'd1)
begin
y_p <= 8'hb3; x_ph <= 8'h12; x_pl <= 8'h08; num <= 5'd 6; char <= {4'd0, temp2, 4'd0, temp3, 4'd0, temp4, ".", 4'd0, temp1,8'h7b,"C"}; state <= SCAN;
end
else
begin
y_p <= 8'hb3; x_ph <= 8'h12; x_pl <= 8'h08; num <= 5'd 6; char <= {4'd0, temp3, 4'd0, temp4, ".", 4'd0, temp1,8'h7b,"C"}; state <= SCAN;
end
default: state <= IDLE;
endcase
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 //5条指令及数据发出后,配置完成
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 //?*8点阵编程8*8
5'd 5: begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end //?*8点阵编程8*8
5'd 6: begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end //?*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串口输入输出模块:
此模块需要实现将整点及温度信息传至PC端,以及进行温度报警时将一段音频发送给小脚丫通过蜂鸣器进行播放,此部分参考了CSDN部分代码及李卓然同学串口代码,对于接受和发送上位机,一开始我准备自己用python编写一个出来,但写了一些就遇到了很多问题,最终决定参考使用串口工具XCOM,十分方便,代码也不放了,自行下载查看吧。
beeper模块:
beeper模块为蜂鸣器,负责音乐的播放,实现此功能要求将乐谱转化为代码形式。恰好最近一直在研究乐谱的相关知识,因此对于乐谱知识十分熟悉,但对代码的对应关系却一窍不通,参考其他小伙伴的转化代码,明白了乐谱参数与时钟音高的关系,实现了蜂鸣器音乐的播放。整点播放音乐选用了《谁不说咱家乡好》这首歌,并对播放时长和循环做了定义,下面为代码展示:
module beeper(
input clk, //系统时钟12MHz
input clk_1h,
input rst_n,
input tone_en, //蜂鸣器使能
input uart_en, //串口
input[7:0] uart_data,
input[3:0] min1,min2,sec1,
output trflag,
output beeper //蜂鸣器输出
);
reg uart_cnt; //计数
reg beep;
reg tr;
//乐谱音高、分频、时长参数设定
parameter TIME = 3_000_000;
parameter L1 = 16'd22935, //L1,5997
L2 = 16'd20428, //L2,4fcc
L3 = 16'd18203, //L3,471b
L4 = 16'd17181, //L4,431d
L5 = 16'd15305, //L5,3bc9
L6 = 16'd13635, //L6,3541
L7 = 16'd12147, //L7,2f73
M1 = 16'd11464, //M1,2cc8
M2 = 16'd10215, //M2,27e7
M3 = 16'd9100, //M3,238c
M4 = 16'd8589, //M4,218d
M5 = 16'd7652, //M5,1de4
M6 = 16'd6817, //M6,1aa1
M7 = 16'd6073, //M7,17b9
H1 = 16'd5740, //H1,166c
H2 = 16'd5107, //H2,13f3
H3 = 16'd4549, //H3,11c5
H4 = 16'd4294, //H4,10c6
H5 = 16'd3825, //H5,0ef1
H6 = 16'd3408, //H6,0d50
H7 = 16'd3036; //H7,0bdc
assign beeper = beep; //输出
assign trflag = tr;
reg[16:0]count,count_end,count_end1;
reg[23:0]count1;
reg[7:0]state;
always@(posedge clk)
begin//整点播放
if(tone_en && min1 == 0 && min2 == 0 && sec1 < 3)
begin
count <= count + 1'b1;
if(count == count_end)
begin
count <= 17'h0;
beep <= !beep;
end
end
else if(tone_en && !tr)
begin
count <= count + 1'b1;
if(count == count_end1)
begin
count <= 17'h0;
beep <= !beep;
end
end
end
//设定音乐乐谱
always @(posedge clk)
begin
if(count1 < TIME)
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:count_end =M1;
8'd1:count_end=M1;
8'd2:count_end=M2;
8'D3:count_end=M1;
8'D4:count_end=M5;
8'D5:count_end=M3;
8'D6:count_end=M5;
8'D7:count_end=M1;
8'D8:count_end=M1;
8'D9:count_end=M2;
8'D10:count_end=M3;
8'D11:count_end=M5;
8'D12,8'D13:count_end=M2;
8'D14:count_end=L5;
8'D15:count_end=L6;
8'D16:count_end=L6;
8'D17:count_end=L3;
8'D18:count_end=M2;
8'D19:count_end=M1;
8'D20:count_end=M1;
8'D21:count_end=L7;
8'D22:count_end=M3;
8'D23:count_end=M2;
8'D24:count_end=M2;
8'D25:count_end=L5;
8'D26:count_end=L6;
8'D27:count_end=L6;
8'D28:count_end=M1;
8'D29:count_end=M2;
8'D30:count_end=M3;
8'D31:count_end=M5;
8'd32:count_end = M3;
8'd33:count_end=M2;
8'd34:count_end=L6;
8'D35:count_end=L6;
8'D36:count_end=L7;
8'D37:count_end=M2;
8'D38:count_end=L6;
8'D39:count_end=L5;
8'D40,8'D41:count_end=M5;
8'D42:count_end=M5;
8'D43:count_end=M5;
8'D44:count_end=M6;
8'D45:count_end=M3;
8'D46:count_end=M5;
8'D47:count_end=M3;
8'D48:count_end=M2;
8'D49:count_end=L7;
8'D50:count_end=M5;
8'D51:count_end=M5;
8'D52:count_end=M3;
8'D53:count_end=M2;
8'D54:count_end=L5;
8'D55:count_end=M1;
8'D56:count_end=M2;
8'D57:count_end=M1;
8'D58:count_end=L6;
8'D59:count_end=M1;
8'D60:count_end=M2;
8'D61:count_end=M3;
8'D62:count_end=M6;
8'D63:count_end=M5;
default: count_end = 16'h0;
endcase
end
end
//串口输出乐谱设定
always @(posedge clk)
begin
if(uart_en)
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;//stopp
endcase
end
end
always@(posedge uart_en or posedge clk_1h)
begin
if(uart_en)
begin
uart_cnt <= 1'b1;
end
else
if(uart_cnt)
begin
tr <= 1'b0;
uart_cnt <= 1'b0;
end
else
tr <= 1'b1;
end
endmodule
clock模块及clk_1h模块:
这两个模块实现了时钟的分频和控制,参考了刘建伟同学及森林的代码,通过对四个按键进行编程,实现对小时和分钟的加减以及复位,并在OLED显示。或许是编C等其他语言编多了,对于时钟定时和计时的编程使用了大量的嵌套循环判断(类似于if、while语句的特点),尝试了用标志位判断或其他方法,但不太顺利,最终只能折中使用类似的方法进行定时和恢复功能,此部分代码有点繁琐,不在此贴出。
遇到的主要难题:
对于一个对FPGA“一清二白”的我来说,完成此项目还是有一定难度的,之前接触学习过Arduino单片机等,接触FPGA并进行编程总是感觉有阻碍,最终抛弃了之前的编程思路,重新进行了参考学习,而学习基本知识就用去了很长时间,多亏有很多开源的代码和案例用于参考。
在对温度报警停止刷新时遇到了一定的阻碍,无法实现音乐播放停止刷新,后采用了标志位flag判断时钟和数据的接收,解决了此问题。在OLED的显示方面也遇到了问题,由于对点阵屏幕的驱动显示知识的错误理解,导致输出总是重叠或超出显示范围,最终参考了诸多显示代码后发现了问题所在,实现了显示功能。
未来的计划:
1.自行编写上位机串口调试
2.音乐播放实现不同音长,转音和半音的流畅播放
3.实现OLED屏幕信息的字体字号的调整显示