
前一段时间在和导师进行沟通交流之后,我开始了学习FPGA的路程,往后的推送也大多数会和FPGA挂钩,给大家推送一些我在学习FPGA过程中的一些心得体会。 首先我开始的便是最基础的FPGA流水灯程序,我学习的FPGA编程语言是verilog语言,verilog被称为硬件电路描述语言,和c语言不同的是verilog就是在编写一个实际的硬件电路。 本期介绍的是使用AC620开发板上的4个LED状态每500ms翻转一次。AC620开发板上的晶振时钟频率为50MHz,换算为时钟周期就是20ns。 下图便是我使用的AC620 FPGA。
![]() 01 编写电路介绍
![]() 上图即为本期需要用verilog语言编写出来的计时电路,该电路由加法器、二选一选择器、D触发器和比较器组成。首先加法器的其中一个输入端始终是1,也就是每一个时钟周期都加1,而另一端则由D触发器的输出端提供。电路最初运行的时候,D触发器的输出端为0,即在加法器处为0+1,加法器的输出端为1,然后输入到二选一选择器输入端,因为是第一轮运行选择器的SEL端口也为0(SEL为0时输出A端口信号,为1时输出B端口信号),则选择器的输出端输出A端口的信号1,此信号传至D触发器的输入端,一个时钟周期后D触发器的输出端也变为1,D触发器的输出端信号1又传至加法器的输入端,如此循环自加直到D触发器的输出端达到比较器上的值之后,比较器输出端输出1,这个1又传至选择器的SEL端口,致使选择器输出B端口信号,实现D触发器清零。D触发器的时钟源连接一个50M的时钟脉冲,复位端单独连接一个引脚低电平触发复位清零。 (备注:比较器上的25'd24_999_999,此处定义的是一个25位的寄存器,在verilog中d表示十进制,b表示二进制,h表示十六进制,这个24_999_999表示的是十进制中的数字25000000,在此处是为了判断脉冲个数的计数是否达到我们要的值,因为50M的晶振为时钟周期为20ns,我们要实现500ms翻转一次,500ms=500_000_000ns,每一个脉冲20ns,则需计数500000000/20=25000000次,而比较器上写24_999_999是因为25000000是[0~24999999]) ![]() 此为AC620 FPGA上的四路LED电路图,四路LED的一端均有上拉电阻拉至高电平,即一般情况下全灭,控制时只需将拉高的一端给一个低电平,led即亮。 02 verilog编写该硬件电路 FPGA的编程软件为Quartus II,首先创建.v文件,然后我们要对需要的端口进行定义,即时钟信号、复位信号和led。 ![]() 然后需要对这些端口的类型进行定义,比如哪些是输入端口哪些是输出端口。
![]() 这个时钟信号需要连接到FPGA上的晶振时钟所以为外部输入信号,复位信号也由FPGA上的按键控制也为输入信号,我们最终是要控制LED的状态,所以LED的信号为输出信号。 接下来是对时序电路进行编写,如下图所示。
![]() 在时序电路中,我们需要一个寄存器来对脉冲计数,所以需要定义一个能在时序逻辑中自加的寄存器reg型的变量cnt,cnt前面的[24:0]表示这个寄存器是25位的(如前面解释的24_999_999,二进制寄存器的每一位只能是0和1),然后always@编写捕获时钟的上升沿和复位信号的下降沿,这是时序逻辑的写法,只有时钟的上升沿和复位信号的下降沿能触发,接下来便判断是哪个信号触发,if(!Rst_n)如果是复位信号触发则将计数脉冲寄存器cnt清零。else if(cnt == 25'd24_999_999)如果不是复位信号触发但是脉冲计数达到了我们要的标准,则也将脉冲计数寄存器清零。如果既不是复位信号触发脉冲计数寄存器也没有忙,则计数脉冲寄存器则实行加法器进行自加。 在这里面需要注意的是赋值方式,比如“cnt <= 25'd0”这是在always@里面需要用到非阻塞赋值方式,所以定义的端口类型必须为reg,像output reg [3:0]led这里就必须声明reg型,如若不声明reg直接编写output [3:0]led,则系统会自动将led识别为wire型,导致编译出错。 下面则介绍一下led的变换逻辑。
![]() 相同的这一部分也是写在always@时序中,if(!Rst_n)当复位信号为零,则四个led灯全给高电平,led灯全灭。else if(cnt == 25'd24_999_999)当计数满后则led的电平翻转一次(由4'b1111翻转为4'b0000,因为b表示二进制,二进制里面不是1就是0),即由1高电平翻转为0低电平,led灯亮。cnt每计数一次脉冲则是20ns,计数25000000(0~24_999_999)次,也就是25000000*20ns=500'000'000ns=500ms。即实现每500ms翻转一次LED。 下面介绍两种流水灯写法。
![]() 这是使用位操作进行的流水灯,需要使用一个4位寄存器led_r进行中间代换,因为是在always@时序逻辑中编写,所以led_r定义为reg型,首先给led_r为4'b0001,然后对这个1进行向左依次移位,就好比4个灯,假设1为亮,则向左依次逐个点亮(移位0001—>移位一次0010—>移位两次0100—>移位三次1000),此时就需要注意了当移位到1000时便不能再进行左移,此时就需要判断一下begin if(led_r == 4'b1000)则将led_r <= 4'b0001,如果不是led_r==1000,则else继续左移 led_r <= led_r << 1,这里的“<<”便是左移符号。 最后将左移的结果赋值给led,也就是led的管脚,此处也需要注意的便是led必须定义为wire型,因为此处在assign中进行阻塞赋值而不是在always@中。所以在定义处改为output [3:0]led,这里等效为output wire[3:0]led。 接下来还有一种骚操作。
![]() 注意此处就需要将led的类型变回来了,因为是在always@中进行赋值,所以需要定义为reg型。 if(cnt == 25'd24_999_999)计数满led <={led[2:0],led[3]}则将led[2:0]三位向前移,将led[3]移到最后面去。基本流程我做了一个表,如下所示。
![]() 03 modelsim仿真 进行modelsim仿真需要重新建立一个.v文件。编写仿真的时钟程序和复位程序。
![]() 首先开头的是设置仿真波形的周期单位为ns,第二行的相当于c语言中的宏定义,将clock_period宏定义为20。然后定义端口和端口类型,下面led_flash led_flash0是引用的上一个.v文件里的程序,flash0只是在这的一个标签。 再下面便是编写时钟,首先给一个高电平clk50M=0;然后always每10ns翻转一次,便形成一个周期为20ns的方波。接下来的复位程序便是,待500ms后也就是cnt计数25000000后复位信号置零。 接下来看一下仿真波形的现象。
![]() 最上面为clk50M的方波,当cnt从0计数到24999999时,四路led的电平全部被拉低,即led全亮。 |