寒假一起练4 FPGA
因为我以前有接触过,所以之前没有接触过。FPGA,在学习的过程中,我总结了一些经验,可能对刚学习的小白有所帮助。
关于小白如何学习FPGA
具体实验中遇到的问题
因为我刚开始学习,我遇到了太多的问题,所以我一直忙于解决琐碎的问题,没有太多的时间来记录我遇到的问题。我在上面的文章中记录的问题是一些非常典型的小白色问题和解决方案。
实现目标功能
- 用小脚实现定时钟的功能FPGA当前时间设置了核心模块的四个按钮,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>>一是右移一位,除以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; ed
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工作时本身就会非常发热,其次时蜂鸣器。在实验的过程中,我发现蜂鸣器也有发热的情况,而蜂鸣器也在温度传感器的旁边,所以导致温度一直测不准确。 活动链接
b站视频
验证代码
实际验证只需要上面标注的两个文件.