项目背景
STEP-MXO2第二代是小脚丫团队推出的最新一款FPGA开发板,选用了Lattice公司的MXO2系列的4000HC,逻辑资源较一代产品提升了近4倍。同时,在板卡的背面集成了编程器,通过USB数据线能够完成FPGA的编程和下载,使用硬禾学堂的小脚丫训练板,可以实现多种不同方式的输入输出功能。
项目要求
1. 实现一个可定时时钟的功能,用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟即可,到整点的时候比如8:00,蜂鸣器报警,播放音频信号,最长可持续30秒;
2. 实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度,并同时间一起显示在OLED的屏幕上;
3. 定时时钟整点报警的同时,将温度信息通过UART传递到电脑上,电脑上能够显示当前板子上的温度信息(任何显示形式都可以),要与OLED显示的温度值一致;
4. PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件,OLED屏幕上显示的时间信息和温度信息都停住不再更新;
5. 音频文件播放完毕,OLED开始更新时间信息和当前的温度信息
整体思路
我实现主要功能的思路如下图所示,通过不同模块实现特定的功能,并通过顶层模块中的wire变量进行数据交换。
完成功能
1. 在训练板的OLED显示屏上显示当前时间和功能,显示结果正确。 2. 通过开发板上的开关和按键进行时间设置,KEY2选择需要更改的时间位,显示屏上H、h、M、m别代表小时高低位,分钟高低位,SW1~SW4四个开关以BCD码的形式调为需要设定的数字,按下KEY2后,对应位被设定。 3. 到达整点时,播放特定的一段音乐,此时OLED屏幕的显示信息不再刷新,开发板向电脑发送当前温度,显示屏温度一致。 4. 从电脑中由串口工具向开发板发送乐谱信息,发送完成后,蜂鸣器可以正确播放这段乐谱。
5. 播放完成后OLED先视频继续刷新,显示当前的时间和温度。
6. KEY3按键为重置按键,按下后系统中的多个状态寄存器会被归零,时间会被重置为“22:22”。
代码实现
主模块包含了所有对外的输入输出声明以及子模块的例化,其中还包含了时钟分频的功能:
always @(posedge clk)//分频
begin
count=count+1;
countt=countt+1;
if(count>=720000000) count<=0;
if(countt>=1250) countt<=0;
end
wire clk1min=count[29];
wire clk2=count[18];
wire clk_t=countt[10];
时钟模块:
输入为分频后的周期为1min的时钟,时钟沿到来时进行进位操作,调节时间时首先由寄存器choice确定需要改变的时间位(小时高位、小时低位、分钟高位、分钟低位),当置位按键低有效时,将四个开关sw所对应的BCD数值赋值给对应的时间位,实现时间调节。最开始,我尝试在检测按键按下后对对应时间位进行+1的操作,但开发板上的按键抖动十分严重,无法有效地调整时间,因此我采用了开关加按键的调整时间方法。
module clock(
input clk1min,clk2,clk,
input rst_n,
input [3:0] sw,
input cc,
input cd,
output reg[7:0]num,num2,num3,num4,
output [2:0]choose
);
reg [2:0]choice=0;
wire [2:0]choose=choice;
initial
begin
num=2;
num2=2;
num3=2;
num4=2;
end
always@(posedge cc)
begin
choice=choice+1;
if(choice>=5) choice=0;
end
always@(negedge clk1min or negedge rst_n or negedge cd)
begin
if(!rst_n)
begin
num<=2;
num2<=2;
num3<=2;
num4<=2;
end
else if(!cd)
begin
case(choice)
1:num=sw;
2:num2=sw;
3:num3=sw;
4:num4=sw;
endcase
end
else
begin
if(num4>=9)
begin
num4<=0;
if(num3>=5)
begin
num3<=0;
if(num2>=9)
begin
num2<=0;
num<=num+1;
end
else if(num2>=3&&num>=2)
begin
num2<=0;
num<=0;
end
else
num2<=num2+1;
end
else
num3<=num3+1;
end
else
num4 <= num4+1;
end
end
endmodule
OLED显示模块:
主要参考了电子森林百科中的代码,输入时间、温度等信息,实现在OLED屏上实时显示。由于代码较长,不在此放出,详见附件。
温度获取部分:
包括DS18B20Z驱动模块以及BCD转换模块,前者直接使用了电子森林里的温度传感器驱动,得到16位的二进制温度信息。
在BCD转换模块中对16位数据进行转换,由前5位判断温度的正负,将后12位的二进制数乘以0.0625即可得到实际的摄氏温度。由于电路无法处理小数,因此先乘以625后取十位、个位以及小数位,通过移位加三的方法实现BCD转换,以节约电路资源,移位加三的方法参考了电子森林中的分享案例。
module temtrans(
input clk,
input [15:0]temout,
output reg [7:0]t2,t1,t0,
output reg [7:0]ts
);
reg [45:0] shift_reg;
reg [10:0] x;
reg [20:0] y;
reg [3:0]z;
always @(posedge clk) begin
x=temout[10:0];
y=x*625;
z=temout[15:12];
if(z==0) ts<=8'h2B;
else ts<=8'h2D;
shift_reg= {25'h0,y};
repeat(21)
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
t2=shift_reg[44:41];
t1=shift_reg[40:37];
t0=shift_reg[36:33];
end
endmodule
UART部分包括发送和接收:
发送部分我主要参考了电子森林中的分享案例,输入9600Hz的发送时钟,以及需要发送的温度的十位、个位、小数位以及符号,在使能有效时通过CH340发出。
接收部分我采用了电子森林百科中的UART驱动代码,在上位机发送数据后,将数据储存在八位寄存器中,并令数据有效信号有效,代码在此不再赘述。
音频播放部分:
此部分的核心是蜂鸣器驱动模块,我直接使用了电子森林百科中的驱动源码,只需把需要播放的音调信号储存在一个五位寄存器中作为输入,再输入使能,就能播放对应的单音调。
音频加载模块用于接收,用一个176位的寄存器存放乐谱,可以存放22个音符(开始的时候正好想到了一个22个音符的旋律,之后没有再修改),当检测接收数据有效时,将接收到的音频信息存放在寄存器的对应位,存满22个之后产生flag有效信号以便播放。
module load(
input valid,
input [7:0]rdata,
output reg[175:0] song,
input [5:0] state,
output reg flag,
input rst_n
);
reg [4:0] count=0;
initial
begin
flag=0;
count=0;
end
always@(posedge valid or negedge rst_n)
begin
if(!rst_n)
begin
count<=0;
flag<=0;
end
else begin
case(count)
0:begin song[7:0]=rdata[7:0];song[7:0]=rdata[7:0];end
1:begin song[15:8]=rdata[7:0];song[15:8]=rdata[7:0];end
2:begin song[23:16]=rdata[7:0];song[23:16]=rdata[7:0];end
3:begin song[31:24]=rdata[7:0];song[31:24]=rdata[7:0];end
4:begin song[39:32]=rdata[7:0];song[39:32]=rdata[7:0];end
5:begin song[47:40]=rdata[7:0];song[47:40]=rdata[7:0];end
6:begin song[55:48]=rdata[7:0];song[55:48]=rdata[7:0];end
7:begin song[63:56]=rdata[7:0];song[63:56]=rdata[7:0];end
8:begin song[71:64]=rdata[7:0];song[71:64]=rdata[7:0];end
9:begin song[79:72]=rdata[7:0];song[79:72]=rdata[7:0];end
10:begin song[87:80]=rdata[7:0];song[87:80]=rdata[7:0];end
11:begin song[95:88]=rdata[7:0];song[95:88]=rdata[7:0];end
12:begin song[103:96]=rdata[7:0];song[103:96]=rdata[7:0];end
13:begin song[111:104]=rdata[7:0];song[111:104]=rdata[7:0];end
14:begin song[119:112]=rdata[7:0];song[119:112]=rdata[7:0];end
15:begin song[127:120]=rdata[7:0];song[127:120]=rdata[7:0];end
16:begin song[135:128]=rdata[7:0];song[135:128]=rdata[7:0];end
17:begin song[143:136]=rdata[7:0];song[143:136]=rdata[7:0];end
18:begin song[151:144]=rdata[7:0];song[151:144]=rdata[7:0];end
19:begin song[159:152]=rdata[7:0];song[159:152]=rdata[7:0];end
20:begin song[167:160]=rdata[7:0];song[167:160]=rdata[7:0];end
21:begin song[175:168]=rdata[7:0];song[175:168]=rdata[7:0];end
endcase
count=count+1;
if(count>=22)
begin
count=0;
flag=1;
end
end
end
endmodule
音频播放模块:
核心在于一个54种状态的状态机,在状态0~29,播放内定的音频(这个音频有30个音符),每一种状态对应一个音符,状态为2的同时使能,将温度信息发送给上位机,并停止OLED的更新。30状态为等待输入状态,接收完乐谱信号后,才进入下一状态。状态31~52为播放外部音频的状态,将接收到的乐谱信全部播放出来。播放完后进入53状态,恢复OLED更新,停止对蜂鸣器使能。分钟数据更新后,状态变回0。连续相同音符中间需要有一定的间断,因此通过一个计数器,在每次计满前几个数时中断蜂鸣器使能,使每次播放的音符有间断。由于蜂鸣器模块中,8对应中音do,1的ascii码为49(十进制),将收到的acsii码数据减41之后就可以有1、2、3、……、7和do、re、mi、……ti的对应关系,而低音区域则通过ascii42~48表示,高音通过ascii56~63表示。
module music(
input clk,
input clk2,
input valid,
input [15:0]temout,
input [7:0]m1,m0,rdata,
output reg tone_en=0,
output reg oled_en=1,
output reg [4:0] tone,
input rst_n,
output reg tx_en,
input flag,
input [175:0]song,
output reg [5:0]state
);
initial
begin
oled_en=1;
state=0;
end
reg [3:0]count;
reg [7:0]x;
always @(posedge clk2)
begin
if(m1==0&&m0==0)
begin
if(state<30)
begin
if(count<4'b1101) tone_en=1;
else tone_en=0;
count<=count+1;
if(count==4'b1111)
begin
count<=0;
state<=state+1;
end
if(state==2)
begin
tx_en=1;//发送数据
oled_en=0;//暂停更新
end
end
else if(state==30)//载入乐曲状态
begin
tx_en=0;
tone_en=0;
if(flag==1) state=state+1;
end
else if(state<53&&state>30)
begin
if(count<4'b1101) tone_en=1;
else tone_en=0;
count<=count+1;
if(count==4'b1111)
begin
count<=0;
state<=state+1;
end
end
else if(state==53)
begin
oled_en=1;
tone_en=0;
end
end
else//不在整点,重置状态
begin
tone_en=0;
state=0;
oled_en=1;
end
end
always@(state)
begin
case(state)
0:tone=10;
1:tone=10;
2:tone=11;
3:tone=12;
4:tone=12;
5:tone=11;
6:tone=10;
7:tone=9;
8:tone=8;
9:tone=8;
10:tone=9;
11:tone=10;
12:tone=10;
13:tone=9;
14:tone=9;
15:tone=10;
16:tone=10;
17:tone=11;
18:tone=12;
19:tone=12;
20:tone=11;
21:tone=10;
22:tone=9;
23:tone=8;
24:tone=8;
25:tone=9;
26:tone=10;
27:tone=9;
28:tone=8;
29:tone=8;
31:
begin
x=song[7:0]-41;
tone=x[4:0];
end
32:
begin
x=song[15:8]-41;
tone=x[4:0];
end
33:
begin
x=song[23:16]-41;
tone=x[4:0];
end
34:
begin
x=song[31:24]-41;
tone=x[4:0];
end
35:
begin
x=song[39:32]-41;
tone=x[4:0];
end
36:
begin
x=song[47:40]-41;
tone=x[4:0];
end
37:
begin
x=song[55:48]-41;
tone=x[4:0];
end
38:
begin
x=song[63:56]-41;
tone=x[4:0];
end
39:
begin
x=song[71:64]-41;
tone=x[4:0];
end
40:
begin
x=song[79:72]-41;
tone=x[4:0];
end
41:
begin
x=song[87:80]-41;
tone=x[4:0];
end
42:
begin
x=song[95:88]-41;
tone=x[4:0];
end
43:
begin
x=song[103:96]-41;
tone=x[4:0];
end
44:
begin
x=song[111:104]-41;
tone=x[4:0];
end
45:
begin
x=song[119:112]-41;
tone=x[4:0];
end
46:
begin
x=song[127:120]-41;
tone=x[4:0];
end
47:
begin
x=song[135:128]-41;
tone=x[4:0];
end
48:
begin
x=song[143:136]-41;
tone=x[4:0];
end
49:
begin
x=song[151:144]-41;
tone=x[4:0];
end
50:
begin
x=song[159:152]-41;
tone=x[4:0];
end
51:
begin
x=song[167:160]-41;
tone=x[4:0];
end
52:
begin
x=song[175:168]-41;
tone=x[4:0];
end
endcase
end
endmodule
资源使用情况
项目总结
由于春节期间快递停运,没有及时下单,我在2月22号才收到开发板,时间比较紧迫。之前,我在学校的课程中接触到了基于Xillinx的FPGA开发板,在收到板子之前,我对各个模块进行了简单的规划,复习了Verilog语言的相关知识并且学习了Diamond软件的使用方法。
开发板到手之后,我才发现,细节上的问题比我想象的要多很多,不实际烧录到板子上很多问题是发现不了的。我之前所做的FPGA都没有这次项目这么复杂,在多个模块的整合过程中,需要不断调整,也经常出现奇怪的问题,好在电子森林百科中有大量的小脚丫开发板的相关代码,模块的驱动可以直接使用现有的代码,这为此次项目降低了不少难度。
最开始我打算通过乘除法实现温度数据的BCD转换,但这种方法需要的资源超出了开发板的资源数量,我参考了平台上的案例分享,使用移位加三的方法实现转换。进行硬件编程与对软件的编程有很大的区别,不仅思路上不一样,还需要关注到资源的使用情况,很多软件编程轻易实现的功能都需要多加考虑。
多个always语句不能对同一个reg变量赋值的问题也给我添了不少麻烦,在实际编写之前没有考虑好,导致出现问题后大规模删改。在硬件编程时,需要有硬件的意识,以电路的角度思考能否实现。
实现串口收发功能时,我尝试了多个电脑端的串口工具,在开发板作为接收端时,很多串口工具发送的乐谱接收后播放的声音不一致,多次测试之后,我才找到一个比较稳定的串口助手。但是,这也说明我相关的代码适应性不够好,以后需要再仔细研读UART的手册和时序,编写适应性更好的Verilog程序。
未来计划:此次项目需要改进的地方还有很多,因为时间仓促,很多地方没有好好斟酌,资源的使用上应该还有很大的节约空间;时间调节部分,可以增加按键消抖模块,加入只通过按键进行时间调节的功能;整点报时部分,加入一些判断,在如深夜等时间不再报时;在音乐播放方面,增加可以输入乐谱的长度,并且可以进行不同长度的识别,并且,对半拍以及多拍的音符进行不同的处理;我还计划加入电脑向开发板发送的其他信息,实现电脑对开发板的控制,并自主编写可以自动操作的上位机;训练板中还有好几个模块这次没有使用到,今后可以加以利用,实现更多功能。
最后,非常感谢硬禾学堂带给我这次机会,让我对数字逻辑电路、FPGA有了更深入的认识,对Verilog语言的使用也有了不少的提升,同时也给我了一个更加充实的假期。
参考资料
https://www.bilibili.com/read/cv3543776/
https://www.eetree.cn/wiki/stepfpga_training_board
https://www.eetree.cn/wiki/uart%E4%B8%B2%E5%8F%A3%E6%A8%A1%E5%9D%97
https://www.eetree.cn/explore/project?q=%E5%AF%92%E5%81%87%E5%9C%A8%E5%AE%B6%E7%BB%83