寒假一起练4 FPGA
由于之前我完全没有接触过FPGA,在学习的过程中,我总结了一些经验,可能对刚学习的小白有一定的帮助。
由于刚开始学,所以遇到的问题实在是太多,以至于我一直忙着解决琐碎的问题,而没有多少时间来记录我遇到的问题。我上面一篇文章里记录的问题都是一些非常典型的小白会遇到的问题,以及解决方法。
目标功能实现
- 实现一个可定时时钟的功能,用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟即可,到整点的时候比如8:00,蜂鸣器报警,播放音频信号,最长可持续30秒;
- 实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度,并同时间一起显示在OLED的屏幕上;
- 定时时钟整点报警的同时,将温度信息通过UART传递到电脑上,电脑上能够显示当前板子上的温度信息(任何显示形式都可以),要与OLED显示的温度值一致;
- PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件,OLED屏幕上显示的时间信息和温度信息都停住不再更新;
- 音频文件播放完毕,OLED开始更新时间信息和当前的温度信息
方案设计
显示列一张大体的思维导图,用于指导后续的设计。具体的修改细节后面再完善
上图是我前期列的一张思维导图。可以看到受单片机的影响很深啊,里面必有一个控制单元,用于从ROM读取下一条的控制指令来控制外设。结果后来实践发现没有书上说的那么简单。也就放弃了用控制单元的想法,转而直接使用相应的电路完成项目。
具体功能实现
时钟部分
我先是写了一个简单的分频器输出1Hz的时钟信号:
// ********************************************************************
// >>>>>>>>>>>>>>>>>>>>>>>>> COPYRIGHT NOTICE <<<<<<<<<<<<<<<<<<<<<<<<<
// ********************************************************************
// File name : divide.v
// Module name : divide
// Author : STEP
// Description : clock divider
// Web : www.stepfpga.com
//
// --------------------------------------------------------------------
// Code Revision History :
// --------------------------------------------------------------------
// Version: |Mod. Date: |Changes Made:
// V1.0 |2017/03/02 |Initial ver
// --------------------------------------------------------------------
// Module Function:任意整数时钟分频
module divide ( clk,rst_n,clkout);
input clk,rst_n; //输入信号,其中clk连接到FPGA的C1脚,频率为12MHz
output clkout; //输出信号,可以连接到LED观察分频的时钟
//parameter是verilog里常数语句
parameter WIDTH = 24; //计数器的位数,计数的最大值为 2**WIDTH-1
parameter N = 12_000_000-1; //分频系数,请确保 N < 2**WIDTH-1,否则计数会溢出
reg [WIDTH-1:0] cnt_p,cnt_n; //cnt_p为上升沿触发时的计数器,cnt_n为下降沿触发时的计数器
reg clk_p,clk_n; //clk_p为上升沿触发时分频时钟,clk_n为下降沿触发时分频时钟
//上升沿触发时计数器的控制
always @ (posedge clk or negedge rst_n ) //posedge和negedge是verilog表示信号上升沿和下降沿
//当clk上升沿来临或者rst_n变低的时候执行一次always里的语句
begin
if(!rst_n)
cnt_p<=0;
else if (cnt_p==(N-1))
cnt_p<=0;
else cnt_p<=cnt_p+1; //计数器一直计数,当计数到N-1的时候清零,这是一个模N的计数器
end
//上升沿触发的分频时钟输出,如果N为奇数得到的时钟占空比不是50%;如果N为偶数得到的时钟占空比为50%
always @ (posedge clk or negedge rst_n)
begin
if(!rst_n)
clk_p<=0;
else if (cnt_p<(N>>1)) //N>>1表示右移一位,相当于除以2去掉余数
clk_p<=0;
else
clk_p<=1; //得到的分频时钟正周期比负周期多一个clk时钟
end
//下降沿触发时计数器的控制
always @ (negedge clk or negedge rst_n)
begin
if(!rst_n)
cnt_n<=0;
else if (cnt_n==(N-1))
cnt_n<=0;
else cnt_n<=cnt_n+1;
end
//下降沿触发的分频时钟输出,和clk_p相差半个时钟
always @ (negedge clk)
begin
if(!rst_n)
clk_n<=0;
else if (cnt_n<(N>>1))
clk_n<=0;
else
clk_n<=1; //得到的分频时钟正周期比负周期多一个clk时钟
end
assign clkout = (N==1)?clk:(N[0])?(clk_p&clk_n):clk_p; //条件判断表达式
//当N=1时,直接输出clk
//当N为偶数也就是N的最低位为0,N(0)=0,输出clk_p
//当N为奇数也就是N最低位为1,N(0)=1,输出clk_p&clk_n。正周期多所以是相与
endmodule
然后就是计时部分:
reg clock_flag;//整点时钟标志位
reg [5:0] sec;
reg[3:0] hour_h,hour_l,min_h,min_l;
always @(posedge clock or negedge sys_rst_n)begin
clock_flag<= 1'd0;
time_set_ready_flag <= 1'd0;
if(!sys_rst_n)begin
hour_h <= 6'd0;
hour_l <= 6'd0;
min_h <= 6'd0;
min_l <= 6'd0;
sec <= 6'd0;
clock_flag<=1'd0;
end
else if(key_bus[1]==0)begin
min_l<= min_l + 1'b1;
end
else if(key_bus[0]==0)begin
hour_l<= hour_l + 1'b1;
end
else if(time_set_flag) begin
hour_h <= hour_h_u;
hour_l <= hour_l_u;
min_h <= min_h_u;
min_l <= min_l_u;
sec <= sec_u;
time_set_ready_flag <= 1'd1;
end
else if(usart_recieve_state != sound_set)begin
sec <= sec+1'b1;
if(sec >= 6'd60)
begin
sec <= 6'd0;
min_l <= min_l +1'b1;
end
if(min_l >= 4'd10)
begin
min_l <= 6'd0;
min_h<= min_h + 1'b1;
end
if(min_h >= 4'd6)
begin
clock_flag<= 1'd1;
min_h <= 4'd0;
hour_l<= hour_l + 1'b1;
end
if(hour_l >= 4'd10)
begin
hour_l <= 4'd0;
hour_h<= hour_h + 1'b1;
end
if((hour_h >= 4'd1) & (hour_l >= 4'd2))
begin
hour_h <= 4'd0;
hour_l <= 4'd0;
end
end
end
其中的时钟标志位是用来上位机修改时间的时候用到的。在这个模块中我嵌入了按键信号,如果按键被按下,相对应的时钟或分钟会以一秒的时间自加。这里不用消抖,因为代码的逻辑上已经有了一秒的消抖。
蜂鸣器
蜂鸣器模块我是直接抄的硬禾学堂的代码,非常好用。具体使用是在上位机中使用到了,音乐得播放速度是由上位机来决定的,理论上可以最快可以达到115200个音每秒:
sound_set:begin
if(uart_data_R!=8'h71)
begin
if(uart_data_R == "p")
begin
beep_en_uart = 1'd0;
end
else begin
Beep_status_uart = uart_data_R[4:0];beep_en_uart = 1'd1;usart_recieve_cnt = usart_recieve_cnt+1'd1;
end
end//时
else
begin usart_recieve_cnt= 8'd0;usart_recieve_state = wait_R; beep_en_uart = 1'd0;end
end
上位机、串口模块
串口的初始化我是参照了原子哥的代码,没什么多说的,原子哥的视频已经讲的很清楚了。
上位机我用的是python写的,关键代码如下:
hile True:
#Hex_str = bytes.fromhex('10 11 12 34 3f') #文本转换Hex
##=bytes.fromhex('10 11 12 34 3f')
#s.write(Hex_str) #串口发送 Hex_str()
#接收
n=s.inWaiting() #串口接收
if n:
##print("serial in")
TEMP = s.read() #bytes类型数据
if(TEMP == 't'.encode('utf-8')):#报时优先级最高
print(temperature)
s.write("f".encode())#字符用这个形式
usart_state == 't'
while(sound_cnt <len(sound)):
TEMP = s.read() #bytes类型数据
beep_play()
time.sleep(0.1)
t = threading.Timer(2,uart_send_IT)
t.start()
sound_cnt = 0
s.write(bytes.fromhex('71'))
usart_state = 0
#print("整点报时")
#if(usart_state == 't'):
#print("整点报时")
#usart_state = 0;
elif(usart_state == 'T'):
if(data_cnt > 0): #大端模式
data_cnt-=1
data = TEMP[0]+data<<8*data_cnt
##print("data_remain",data_cnt,"data",data)
else:
data_cnt=3
usart_state = 0
temperature=data*625/10000
temp_p = int(temperature*10)%10
temp_l = int(temperature)%10
temp_h = int(temperature/10)%10
#print(temperature) #计算出来的数值为十进制
data = 0
#elif TEMP == 't'.encode('utf-8'):#时间信号
##print("t")
#usart_state = 't'
elif TEMP == 'T'.encode('utf-8'):#温度信号
#print("T")
usart_state = 'T'
#else:
#print("error data:",TEMP.hex())
#time.sleep(0.0001)
具体的ds18B20的数据我其实是放在了上位机处理的,然后再发回了下位机。
DS18B20
温度数据的获取,我也是参照了原子哥的视频。因为有现成的录制视频,学起来会比较快一点。我稍微修改了一些部分。
assign data2 = data1;//(data1 * 10'd625)/ 32'd10000;//数据扩大10倍保留一位有效数字
这里我没有对数据进行处理,直接输出,然后发到了上位机进行处理。上位机会每隔两秒发送一次数据。
def uart_send_IT():
global usart_state,sound_cnt
if(usart_state == 't'):
print(temperature)
else:
s.write("T".encode())#字符用这个形式
s.write(temp_h.to_bytes(1,'little'))
s.write(temp_l.to_bytes(1,'little'))
s.write(temp_p.to_bytes(1,'little'))
s.write(bytes.fromhex('17'))#数字用这个形式
t = threading.Timer(2,uart_send_IT)
t.start()
所以,上位机拥有的数据其实是最原始的。因为可以直接在电脑上调试,而且python也不用编译,所以我就选择了这样的方法。
OLED
我用了9999分钟获得了SPI通讯模块,用了999分钟点亮了0.96寸OLED,用了99分钟显示出了数字,最后还是否定了我之前用的方案。
oled是我调试时间最长的一个模块,因为网上相对应的FPGA驱动0.91寸的OLED的资料相对较少。
一开始我是打算做一个显存ram的,就像我之前在单片机上做的一样。测试模块的时候发现可以正常显示数字之后,我把测试的模块接入到整个控制单元中,结果发现超出了我的预期的使用范畴,使用RAM的话会超用某个资源。
后来,在无意中我发现,硬禾学堂更新了OELD显示模块,赶紧白嫖一手。上手非常方便,仅用了半天就已经能对屏幕进行相应的操作了,也有可能是之前学习OLED模块积累下的经验帮助我加速学习了这个模块。
关键代码:
MAIN:begin
if(cnt_main >= 5'd16) cnt_main <= 5'd8;
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 <= " " ;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 <= " " ;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'h13; x_pl <= 8'h00; num <= 5'd16; char <= ": " ;state <= SCAN; end//:
5'd6: begin y_p <= 8'hb2; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd16; char <= ". " ; state <= SCAN; end//小数点
5'd7: begin y_p <= 8'hb2; x_ph <= 8'h14; x_pl <= 8'h08; num <= 5'd1; char <= "C" ;state <= SCAN; end//摄氏度
5'd8: if(sign)begin y_p <= 8'hb2; x_ph <= 8'h11; x_pl <= 8'h08; num <= 5'd1; char <= "-" ;state <= SCAN; end
+ //变化部分
5'd9: begin y_p <= 8'hb2; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd1; char <= temp_h ; state <= SCAN; end//温度
5'd10: begin y_p <= 8'hb2; x_ph <= 8'h12; x_pl <= 8'h08; num <= 5'd1; char <= temp_l ; state <= SCAN; end
5'd11: begin y_p <= 8'hb2; x_ph <= 8'h13; x_pl <= 8'h08; num <= 5'd1; char <= temp_p ; state <= SCAN; end//小数
5'd12: begin y_p <= 8'hb0; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd1; char <= hour_h ;state <= SCAN; end//时钟
5'd13: begin y_p <= 8'hb0; x_ph <= 8'h12; x_pl <= 8'h08; num <= 5'd1; char <= hour_l ;state <= SCAN; end
5'd14: begin y_p <= 8'hb0; x_ph <= 8'h13; x_pl <= 8'h08; num <= 5'd1; char <= min_h ;state <= SCAN; end//分钟
5'd15: begin y_p <= 8'hb0; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd1; char <= min_l ;state <= SCAN; end
5'd16: begin y_p <= 8'hb3; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd6; char <= " " ;state <= SCAN; end
default: state <= IDLE;
至此项目开发完毕。
讨论/总结
在本次实验中,完全自主编程的部分为OLED的显示模块,虽然成功显示了,但是资源占用超过了100%。这说明编程单片机得思维和硬件设计得思维在一定程度上不能兼容。但是由于由通讯协议的存在,单片机和FPGA可以进行沟通,两者可以互相弥补不足之处,比如单片机同一时间处理相对较多外设时的无力,以及FPGA对数据处理时的资源占用庞大等问题。
由于FPGA各个模块得功能都是并行进行得,各个模块之间得任务互不影响,所以运行多线程任务相对于单片机有一定的优势,比如当前爆火的AI也许可以用到这上面来。在学习的过程中,我发现FPGA不会因为引脚而局限设计,换句话说,就是FPGA的引脚设置非常灵活,在进行PCB设计的时候也许就不会因为引脚而影响走线,也就不会出现非常多的过孔以及走线层数。
本项目具体实现的是对时间的定时,和对环境温度的检测。当前智能家居非常火热,一款具有定时报警功能和温度检测功能的仪器便是智能家居的标配。
硬件设计上存在不合理。DS18B20模块如果要检测环境温度不应该放在容易发热的器件附近,相反应该尽量远离发热器件。FPGA工作时本身就会非常发热,其次时蜂鸣器。在实验的过程中,我发现蜂鸣器也有发热的情况,而蜂鸣器也在温度传感器的旁边,所以导致温度一直测不准确。
得功能都是并行进行得,各个模块之间得任务互不影响,所以运行多线程任务相对于单片机有一定的优势,比如当前爆火的AI也许可以用到这上面来。在学习的过程中,我发现FPGA不会因为引脚而局限设计,换句话说,就是FPGA的引脚设置非常灵活,在进行PCB设计的时候也许就不会因为引脚而影响走线,也就不会出现非常多的过孔以及走线层数。
本项目具体实现的是对时间的定时,和对环境温度的检测。当前智能家居非常火热,一款具有定时报警功能和温度检测功能的仪器便是智能家居的标配。
硬件设计上存在不合理。DS18B20模块如果要检测环境温度不应该放在容易发热的器件附近,相反应该尽量远离发热器件。FPGA工作时本身就会非常发热,其次时蜂鸣器。在实验的过程中,我发现蜂鸣器也有发热的情况,而蜂鸣器也在温度传感器的旁边,所以导致温度一直测不准确。
活动链接
实际验证只需要上面标注的两个文件.