关于小白如何学FPGA这件事

关于小白如何学FPGA这件事

注意点:如果输入信号在最终没有输出,verilog是不会各这个信号分配资源的。

学习编程就是一个从学会初步,到自己时间发现预期效果和真实情况相背,再到重新学习,再做出来的过程,

前言

由于某些原因,我要开始学习FPGA了。之前一直是接触的单片机,积累了一定的编程知识,以及模电数电知识。现在想要挑战编程FPGA.首先第一个映入眼帘的是verilog这门语言,不能说这门语言和C语言毫不相关,但也可以说是大相径庭。虽然看上去,语法都是差不多的。但是C语言编程的思想完全不能用在verilog这门语言上。C语言面向的是过程,而verilog面向的是硬件。里面有好多东西,以C语言的逻辑是不能理解的。虽然都有模块化的思想,但是相差还是有点多的。

怎么学?

我觉得是这样,想要学习一门技术、语言。那必然是有利可图,也就是有一定的原因、动力去学习。关于这个动力不归我管,自己发现去。比如我的动力就是要完成一个项目,得到的奖励。有了动力,也就有了目的。明确了目的也就明白了,要学什么。

关于小白如何学FPGA这件事_第1张图片

知道了目的,就把目标列出来。列一张思维到图就行。

关于小白如何学FPGA这件事_第2张图片

顺便再把大体的框架列出来

关于小白如何学FPGA这件事_第3张图片

好了,现在需要学习的东西已经列好了。现在要干什么?学习,学什么?怎么用模块吗?不是。

首先,需要了解verilog的语法。俗话说的好,磨刀不误砍柴功。要是练verilog语法都不会,看verilog的例程就像英语考试不会英语单词一样,无法想象直接。

我花了几天的时间来学习verilog语法。也总算是有自信能看懂带有注释的例程。接下来,学习各个相应的模块,先从按键学起。先把简单的学会。

串口

我把串口放在第一个,是因为串口时必不可少的交互工具。开发了串口功能,也就开发了一种人机交互的方法,为以后开发各种模块提供了便利。

接下来加入按键。按键是否成功,用串口发送。

按键

关于小白如何学FPGA这件事_第4张图片

可以通过按下按键,来改变电平的高低。硬件中常常通过电平变化来执行语句。在看了一圈按键的例程后,我的感觉是,既然可以用电平的变化来执行语句,那为什么不用按键线上的变化来执行语句呢?网上有的说法是要消抖,所以依靠时钟来变化的。嗯,好像有点道理。但是,这不是硬件编程语言嘛,所以要贴合硬件。想想硬件上,按键时怎么消抖的。并联一个电容。额,好像在逻辑电路上有点问题。那想想消抖是为了什么?为了防止按键被多次按下,那就是用时钟延时了嘛,延时的话,果然还是要用到时序逻辑电路。下面是小脚丫官网的一个例程,里面的消抖方法感觉还挺常见的。

// ********************************************************************
// >>>>>>>>>>>>>>>>>>>>>>>>> COPYRIGHT NOTICE <<<<<<<<<<<<<<<<<<<<<<<<<
// ********************************************************************
// File name    : debounce.v
// Module name  : debounce
// Author       : STEP
// Description  : 
// Web          : www.stepfpga.com
// 
// --------------------------------------------------------------------
// Code Revision History : 
// --------------------------------------------------------------------
// Version: |Mod. Date:   |Changes Made:
// V1.0     |2017/03/02   |Initial ver
// --------------------------------------------------------------------
// Module Function:按键消抖
 
module debounce (clk,rst,key,key_pulse);
 
        parameter       N  =  1;                      //要消除的按键的数量
 
	input             clk;
        input             rst;
        input 	[N-1:0]   key;                        //输入的按键					
	output  [N-1:0]   key_pulse;                  //按键动作产生的脉冲	
 
        reg     [N-1:0]   key_rst_pre;                //定义一个寄存器型变量存储上一个触发时的按键值
        reg     [N-1:0]   key_rst;                    //定义一个寄存器变量储存储当前时刻触发的按键值
 
        wire    [N-1:0]   key_edge;                   //检测到按键由高到低变化是产生一个高脉冲
 
        //利用非阻塞赋值特点,将两个时钟触发时按键状态存储在两个寄存器变量中
        always @(posedge clk  or  negedge rst)
          begin
             if (!rst) begin
                 key_rst <= {N{1'b1}};                //初始化时给key_rst赋值全为1,{}中表示N个1
                 key_rst_pre <= {N{1'b1}};
             end
             else begin
                 key_rst <= key;                     //第一个时钟上升沿触发之后key的值赋给key_rst,同时key_rst的值赋给key_rst_pre
                 key_rst_pre <= key_rst;             //非阻塞赋值。相当于经过两个时钟触发,key_rst存储的是当前时刻key的值,key_rst_pre存储的是前一个时钟的key的值
             end    
           end
 
        assign  key_edge = key_rst_pre & (~key_rst);//脉冲边沿检测。当key检测到下降沿时,key_edge产生一个时钟周期的高电平
 
        reg	[17:0]	  cnt;                       //产生延时所用的计数器,系统时钟12MHz,要延时20ms左右时间,至少需要18位计数器     
 
        //产生20ms延时,当检测到key_edge有效是计数器清零开始计数
        always @(posedge clk or negedge rst)
           begin
             if(!rst)
                cnt <= 18'h0;
             else if(key_edge)
                cnt <= 18'h0;
             else
                cnt <= cnt + 1'h1;
             end  
 
        reg     [N-1:0]   key_sec_pre;                //延时后检测电平寄存器变量
        reg     [N-1:0]   key_sec;                    
 
 
        //延时后检测key,如果按键状态变低产生一个时钟的高脉冲。如果按键状态是高的话说明按键无效
        always @(posedge clk  or  negedge rst)
          begin
             if (!rst) 
                 key_sec <= {N{1'b1}};                
             else if (cnt==18'h3ffff)
                 key_sec <= key;  
          end
       always @(posedge clk  or  negedge rst)
          begin
             if (!rst)
                 key_sec_pre <= {N{1'b1}};
             else                   
                 key_sec_pre <= key_sec;             
         end      
       assign  key_pulse = key_sec_pre & (~key_sec);     
 
endmodule

蜂鸣器

之前在stm32哪个项目上用过了,是无源蜂鸣器。只需要产生一定频率的PWM波,就可以使他响。最简单的方法就是用计数器将时钟分频,然后映射到管脚就行了。这个和单片机上的差不多。

电子森林给了现成的代码。每个接口的作用也都标清楚了。

// --------------------------------------------------------------------
// >>>>>>>>>>>>>>>>>>>>>>>>> COPYRIGHT NOTICE <<<<<<<<<<<<<<<<<<<<<<<<<
// --------------------------------------------------------------------
// Module: Beeper
// 
// Author: Step
// 
// Description: Beeper
// 
// Web: www.stepfapga.com
// 
// --------------------------------------------------------------------
// Code Revision History :
// --------------------------------------------------------------------
// Version: |Mod. Date:   |Changes Made:
// V1.0     |2016/04/20   |Initial ver
// --------------------------------------------------------------------
module Beeper
(
input					clk_in,		//系统时钟
input					rst_n_in,	//系统复位,低有效
input					tone_en,	//蜂鸣器使能信号
input			[4:0]	tone,		//蜂鸣器音节控制
output	reg				piano_out	//蜂鸣器控制输出
);
/*
无源蜂鸣器可以发出不同的音节,与蜂鸣器震动的频率(等于蜂鸣器控制信号的频率)相关,
为了让蜂鸣器控制信号产生不同的频率,我们使用计数器计数(分频)实现,不同的音节控制对应不同的计数终值(分频系数)
计数器根据计数终值计数并分频,产生蜂鸣器控制信号
*/
reg [15:0] time_end;
//根据不同的音节控制,选择对应的计数终值(分频系数)
//低音1的频率为261.6Hz,蜂鸣器控制信号周期应为12MHz/261.6Hz = 45871.5,
//因为本设计中蜂鸣器控制信号是按计数器周期翻转的,所以几种终值 = 45871.5/2 = 22936
//需要计数22936个,计数范围为0 ~ (22936-1),所以time_end = 22935
always@(tone) begin
	case(tone)
		5'd1:	time_end =	16'd22935;	//L1,
		5'd2:	time_end =	16'd20428;	//L2,
		5'd3:	time_end =	16'd18203;	//L3,
		5'd4:	time_end =	16'd17181;	//L4,
		5'd5:	time_end =	16'd15305;	//L5,
		5'd6:	time_end =	16'd13635;	//L6,
		5'd7:	time_end =	16'd12147;	//L7,
		5'd8:	time_end =	16'd11464;	//M1,
		5'd9:	time_end =	16'd10215;	//M2,
		5'd10:	time_end =	16'd9100;	//M3,
		5'd11:	time_end =	16'd8589;	//M4,
		5'd12:	time_end =	16'd7652;	//M5,
		5'd13:	time_end =	16'd6817;	//M6,
		5'd14:	time_end =	16'd6073;	//M7,
		5'd15:	time_end =	16'd5740;	//H1,
		5'd16:	time_end =	16'd5107;	//H2,
		5'd17:	time_end =	16'd4549;	//H3,
		5'd18:	time_end =	16'd4294;	//H4,
		5'd19:	time_end =	16'd3825;	//H5,
		5'd20:	time_end =	16'd3408;	//H6,
		5'd21:	time_end =	16'd3036;	//H7,
		default:time_end =	16'd65535;	
	endcase
end
 
reg [17:0] time_cnt;
//当蜂鸣器使能时,计数器按照计数终值(分频系数)计数
always@(posedge clk_in or negedge rst_n_in) begin
	if(!rst_n_in) begin
		time_cnt <= 1'b0;
	end else if(!tone_en) begin
		time_cnt <= 1'b0;
	end else if(time_cnt>=time_end) begin
		time_cnt <= 1'b0;
	end else begin
		time_cnt <= time_cnt + 1'b1;
	end
end
 
//根据计数器的周期,翻转蜂鸣器控制信号
always@(posedge clk_in or negedge rst_n_in) begin
	if(!rst_n_in) begin
		piano_out <= 1'b0;
	end else if(time_cnt==time_end) begin
		piano_out <= ~piano_out;	//蜂鸣器控制输出翻转,两次翻转为1Hz
	end else begin
		piano_out <= piano_out;
	end
end
 
endmodule

时钟计数器

现在用的时钟没有弄错的话就是12MHz的时钟频率。那就简单了,想要产生1Hz的波就需要对他经行12*106分频。

关于小白如何学FPGA这件事_第5张图片

由图可得需要24位的计数器,计数到一半,也就是6 * 106-1的时候转为低电平。计数到12 * 106 - 1时转为高电平。记住初始化的时候默认为高电平。

现在需要考虑,具体时间问题。考虑到时间之间的自然进位不相互影响。我打算使用三个寄存器,分别记录小时,分钟和秒钟。方便起见,我使用三个6位的寄存器。在时间转换中,我使用组合逻辑。

DS18B20

关于小白如何学FPGA这件事_第6张图片

DS18B20之前就有听说过,是一款很流行的温度传感器,但是说到具体使用,倒是没怎么使用过。

现在直接找视频学习一下.

关于小白如何学FPGA这件事_第7张图片

需要读取的温度在RAM中

关于小白如何学FPGA这件事_第8张图片

数据格式为补码,低11位的二进制数转化为十进制数后再乘以0.0625得到所测的实际温度值。

关于小白如何学FPGA这件事_第9张图片

关于小白如何学FPGA这件事_第10张图片

写数据的方法。

关于小白如何学FPGA这件事_第11张图片

读时序

关于小白如何学FPGA这件事_第12张图片

读取操作

关于小白如何学FPGA这件事_第13张图片

需要注意的是,18B20是以微妙为单位操作的。所以,我直接给他一微秒的时间。

由于需要按照时序读写DS18B20,所以直接使用状态机读写。状态直接按上图写就行。

读出来的格式为16位的格式,只有后十二位有效,高五位均为同一个值。低十二位的数据格式为补码格式。所以用代码转化。

always @(posedge clk_1us or negedge rst_n) begin
    if(!rst_n) begin
        sign  <=  1'b0;
        data1 <= 11'b0;
    end
    else if(org_data[15] == 1'b0) begin
        sign  <= 1'b0;
        data1 <= org_data[10:0];
    end
    else if(org_data[15] == 1'b1) begin
        sign  <= 1'b1;
        data1 <= ~org_data[10:0] + 1'b1;
    end
end

取到后十一位的真实数据,并且输出符号位。

OLED

SPI

SPI采样有四种模式。CPOL控制时钟空闲状态电平,CPHA控制时钟在奇偶沿采样。

在前面各个模块都验证完毕后,总算要上手OLED了,之前在网上搜OLED的教程是在是少的可怜,最多也就是LCD的教程。

所以这个OLED的程序我打算自己写。在之前的学习中我累计的经验告诉我。单片机里面的时序,可以由状态机解决。所以我打算使用状态机对OLED进行读写。

像这样的OLED的初始化操作:

在这里插入代码片`
/******************************************************************************
  * @file    OLED init 
  * @author  zero Team
  * @version V1.0
  * @date    13-March-2019
  * @brief   初始化OLED显示屏幕  
  ******************************************************************************/
void OLED_Init(void)
{
	delay_ms(500);//初始化之前的延时 important
	//初始化指令 由厂商给定
	OLED_Write_cmd(0xAE);//display off
	OLED_Write_cmd(0xD5);//set memory addressing Mode
	OLED_Write_cmd(0X80);//分频因子
	OLED_Write_cmd(0xA8);//设置驱动路数
	OLED_Write_cmd(0x1F);//默认0X3f(1/64) 0x1f(1/32)
	OLED_Write_cmd(0xD3); //设置显示偏移
	OLED_Write_cmd(0x00);//默认值00
		
	OLED_Write_cmd(0x40);//设置开始行 【5:0】,行数
	
	OLED_Write_cmd(0x8D);//电荷泵设置
	OLED_Write_cmd(0x14);//bit2,开启/关闭
	
	OLED_Write_cmd(0x20);//设置内存地址模式
	OLED_Write_cmd(0x02);//[[1:0],00,列地址模式;01,行地址模式;10,页地址模式;默认10;
	OLED_Write_cmd(0xA1);//段重定义设置,bit0:0,0->0;1,0->127;
	OLED_Write_cmd(0xC8);//设置COM扫描方向
	
	OLED_Write_cmd(0xDA);//设置COM硬件引脚配置
	OLED_Write_cmd(0x02);//0.91英寸128*32分辨率
	
	OLED_Write_cmd(0x81);//对比度设置
	OLED_Write_cmd(0x8f);//1~255(亮度设置,越大越亮)
	
	OLED_Write_cmd(0xD9);//设置预充电周期
	OLED_Write_cmd(0xf1);//[3:0],PHASE 1;[7:4],PHASE 2;
	OLED_Write_cmd(0xDB);//设置VCOMH 电压倍率
	OLED_Write_cmd(0x40);//[6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc;

	OLED_Write_cmd(0xA4);//全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏)
	OLED_Write_cmd(0xA6);//设置显示方式;bit0:1,反相显示;0,正常显示

	OLED_Write_cmd(0x2E);//停用滚动条

	OLED_Write_cmd(0xAF);//开启显示
	delay_ms(100);//延时一段时间
	OLED_Clear();//清除显示屏幕,防止屏幕中存在噪点
}`

只需要在状态机的对应位置写入

5'd1 : OLED_data = 8'hae; //关闭显示
5'd2 : OLED_data = 8'hd5; //设置时钟分频因子,震荡频率
5'd3 : OLED_data = 8'h80; //[3:0],分频因子;[7:4],震荡频率
5'd4 : OLED_data = 8'ha8; //设置驱动路数
5'd5 : OLED_data = 8'h3f; //默认0X3f(1/64) 0x1f(1/32) 
5'd6 : OLED_data = 8'hd3; //设置显示偏移
5'd7 : OLED_data = 8'h00; //默认为0
5'd8 : OLED_data = 8'h40; //设置显示开始行 [5:0],行数.
5'd9 : OLED_data = 8'h8d; //电荷泵设置
5'd10: OLED_data = 8'h14; //bit2,开启/关闭
5'd11: OLED_data = 8'h20; //设置内存地址模式
5'd12: OLED_data = 8'h02; //[1:0],00,列地址模式;01,行地址模式;10,页地址模式;默认10;
5'd13: OLED_data = 8'ha1; //段重定义设置,bit0:0,0->0;1,0->127;
5'd14: OLED_data = 8'hc0; //设置COM扫描方向
5'd15: OLED_data = 8'hda; //设置COM硬件引脚配置
5'd16: OLED_data = 8'h12; //0.91英寸128*32分辨率
5'd17: OLED_data = 8'h81; //对比度设置
5'd18: OLED_data = 8'hef; //1~255(亮度设置,越大越亮)
5'd19: OLED_data = 8'hd9; //设置预充电周期
5'd20: OLED_data = 8'hf1; //[3:0],PHASE 1;[7:4],PHASE 2;
5'd21: OLED_data = 8'hdb; //设置VCOMH 电压倍率
5'd22: OLED_data = 8'h30; //[6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc;
5'd23: OLED_data = 8'ha4; //全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏)
5'd24: OLED_data = 8'ha6; //设置显示方式;bit0:1,反相显示;0,正常显示
5'd25: OLED_data = 8'haf; //开启显示

在经历了四天的奋斗后,总算是把OLED点亮了。(久违的兴奋)就像是经历了一场持久战。从什么都不会,到把底层SPI输出代码代码搭建完成,单方面测试成功,到解决综合输出没有波形,到波形输出成功,输出内容不正确,到波形无输出问题,OLED无法点亮,到最后OLED终于点亮。这一路的艰辛通过言语是无法分享的,只有通过亲身实践,去踩到误区。才能体会。由于这四天(2.1-2.4)我基本上是全天调试内容,没有怎么记录过程。所以在这里分享一些我认为重要的东西,对小白来说一定有用。

  1. 复位一定要做!!!

    在设计每一个模块的时候复位一定要加上,具体复位要做的内容,根据模块实际的效果来决定。比如状态机的状态转换模块。那就是复位到一个固定的地址。在always调试模块中也要加入复位的下降沿条件。例程如下

    always@(posedge w_Master_TX_Ready or negedge rst_n)begin
        if(~rst_n)
          begin
            OLED_SPI_STATE = 1;
          end
        else
            。。。
    
  2. 如果是学习单片机过来的朋友,可以重视一下状态机和verilog中的TASK和function的功能.

    其中,function和task的功能和单片机中C语言的功能差不多。可能比C语言差一点,但是总比没有强啊。而状态机则是像汇编里面的规则一样,重视地址的变化,在做完一个任务后,就能获取到下一个的地址。如果是这么理解的话,编程起来思路可能会更清晰一点。

  3. verilog是并行语句

    由于我接触单片机比较多,所以编程思维一半都是串行,也就是一句一句写。FPGA中的状态机也是这个调性。但是FPGA的特点就是各个模块并行运行,所以,要用并行的思维去对其编程,才能达到最好的效果,反之,就有可能发生很多意想不到的bug。

  4. OLED的引脚注意点

    正常的通讯线路需要接上:MOSI、CLK、D/C这三条先就不多说了。CS需要下拉表示芯片选中。RST引脚需要有一个上升沿,才能点亮。也就是说,需要先下拉,然后再上拉才行。

  5. 如果使用task、function之类的内容,需要注意输入信号的位数。

在显示温度数值时使用了BCD码.由于十进制转BCD码需要用到除法,所以占用的资源较多。到后期如果资源不够的话可以改方法,当前使用除余法是最方便的。

当完成了各个模块的初始化以及验证接下来就是各个模块的连接

现在已经能够成功在oled上动态显示数字,就是温度显示并不是非常准确。现在打算直接把温度数据发送出去,给上位机,燃梦后上位机处理完成,然后发送回来。

上位机需要做的事情

  1. 处理温度数据,转换成BCD码传回FPGA
  2. 接收到FPGA中时钟的数据,并且可以对其进行修改
  3. 奖实现准备好的音频文件以某种方式发给FPGA然后,驱动FPGA的蜂鸣器模块使其发声。

下位机FPGA需要做的事情

  1. 自动更新时钟信号,并且可以接收上位机发送过来的修改时钟信号的指令。还可以将整点时钟信号发送给上位机。
  2. 接收DS18B20的数据,并且发送给上位机。然后接收上位机发送回来的BCD码的格式。
  3. 接收到音频信号,驱动蜂鸣器模块播放。

首先使将温度信号传送到上位机进行解码。看看温度信息是否正确,然后再发送回FPGA。

传统艺能,上位机修改时间,格式如下:

74 11 30 30
头、时、分、秒

现在把下位机整点报时整好

音乐模块

用python写的上位机。格式如下调整数字就能调整音调。空格表示暂停。

sound = "6 6 7 7 8 8 7 7 6 6 3 3 1 1 . . 5 5 4 4 3 3 4 5 4 0 4 4 5 5 6 6 7 7 5 5 2 2 4 4 3 3 2 3 4 3" #声音音调

块使其发声。

下位机FPGA需要做的事情

  1. 自动更新时钟信号,并且可以接收上位机发送过来的修改时钟信号的指令。还可以将整点时钟信号发送给上位机。
  2. 接收DS18B20的数据,并且发送给上位机。然后接收上位机发送回来的BCD码的格式。
  3. 接收到音频信号,驱动蜂鸣器模块播放。

首先使将温度信号传送到上位机进行解码。看看温度信息是否正确,然后再发送回FPGA。

传统艺能,上位机修改时间,格式如下:

74 11 30 30
头、时、分、秒

现在把下位机整点报时整好

音乐模块

用python写的上位机。格式如下调整数字就能调整音调。空格表示暂停。

sound = "6 6 7 7 8 8 7 7 6 6 3 3 1 1 . . 5 5 4 4 3 3 4 5 4 0 4 4 5 5 6 6 7 7 5 5 2 2 4 4 3 3 2 3 4 3" #声音音调

你可能感兴趣的:(FPGA)