一、理论分析
对于前一章已基本分析Verilog和C语言的区别,接下来介绍Verilog的一个基本面。
对于单片机而言,时钟和复位引脚固定,一般初始化IO为输出模式,设定一下速率等,通过GPIO_Setbits等就可以实现对一个引脚的控制了,本质上就是输出高或者低。
对于FPGA而言,时钟和复位引脚并不固定,所以需要手动配置,也就是常说的input、output。
FPGA不像STM32,每个引脚要么是通用IO,要么是像IIC、SPI、UART等,其底层驱动协议已经写好,只需要对引脚配置即可,FPGA所有的IO基本上都是“通用IO”,所以对于LED和KEY,相应的也要有input、output。
例如如下:
input sys_clk ,
input sys_rst_n ,
input [1:0] key ,
output reg [1:0] led
sys_clk即system clock,系统时钟。sys_rst_n即system reset,系统复位。
key即按键,[1:0]就代表了2位,对于IO而言就是两个引脚。led即灯,其为输出引脚同样有两个,reg先不讲述。
需求:根据两个按键(KEY0 和 KEY1)的状态,在不同的 LED 状态下,分别设置 LED 的显示模式 (是同时闪烁,或者交替闪烁)。
1、 对于Verilog而言,并行处理架构加时序逻辑结构,先看懂如下代码:
一个时序逻辑结构的基本如图所示:always总是、posedge上升沿、or或者、negedge下降沿、begin开始、end结束。大白话就是总是在sys_clk上升沿或者sys_rst_n下降沿执行中间的代码。<=为赋值语句,右边赋值到左边,相当于C语言的=。
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
cnt <= 25'd0;
else if(cnt < 25'd2500_0000) //计数500ms
cnt <= cnt + 1'b1;
else
cnt <= 25'd0;
end
2、刚才说当sys_rst_n为下降沿也可以执行代码,一般就是当复位按键按下,或系统自动复位后(不同的板载电路不一样,本人板子复位按键按下为0,不按为1,如果你恰巧反过来,那么将negedge改为posedge,!去掉)。初始上电,时钟开始、自动复位,若手动将复位按键按下由1变为0,此时捕获到sys_rst_n的下降沿,且if(!sys_rst_n)成立,运行cnt<=25'd0,也就是初始化。FPGA复位一般就是对值的刷新,状态机的回溯。
按键弹起或系统正常工作后,if(!sys_rst_n)不再成立,每次sys_clk上升沿到来即执行else if和else对应的语句。非常简单,当cnt<25'd2500_0000就++,当cnt>=25'd2500_0000就清零,实现cnt在0-2500_0000间变化。(注:cnt可以达到2500_0000)。
3、25'd2500_0000,25代表25位二进制,d代表十进制数,2500_0000代表实际的值。就比如:
int a=25000000,其中int代表25,a代表cnt,C语言默认10进制赋值。对于FPGA而言,不会被int、float、uint、double等束缚,只需要计算,这个值最大需要多少位二进制,例如本次最大需要2500_0000。对于如图计算器,其中HEX为16进制,DEC为10进制,OCT为8进制,BIN为二进制,当我们输入25000000后,寻找1的最高位,即4*7-3=25位。
对于cnt<25'd2500_0000,可以有如下表述:cnt<25'h17D7840、cnt<25'o137274100、cnt<25'b0001011111010111100001000000,无论用哪种表示,25位二进制永远不变。
利用这个特性,当sys_clk时钟为50MHz,即一秒运行50_000_000次,对于0~25_000_000(25_000_000计数),一秒可以运行两次。即完全计一次数需要0.5s。
理论计算:t=1/f*n=1/50_000_000*2500_0000=0.5s(t为时长,f为频率,n为计数。)
所以,我们可以在另一个always中判断当cnt == 25'd2500_0000,即需要改变LED状态,若这个状态需要被经常使用,我们可以用另一个变量表示,从而降低代码段,提示可读性。
例如对C语言而言,你可以在定时器中不断增加i的值,你可以在main的while中判断,当i>=50,就点亮LED,也可以在定时器中同时增加:if(i>=50){LED_MODE=1;i=0}。在主函数中,判断if(LED_MODE=1){LED_MODE=0;LED0_turgo;}。对于Verilog而言也可以:
//每隔500ms就更改LED的闪烁状态
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
led_ctrl <= 1'b0;
else if(cnt == 25'd2500_0000)
led_ctrl <= ~led_ctrl;
else led_ctrl <=led_ctrl;
end
在之后,还可以创建一个always块,通过判断led_ctrl来对LED的IO进行操作,如此我们可以初步估计:cnt在0-25000000逐步增加,当到25000000时,将led_ctrl反转,当led_ctrl为0时,LED为01,否则为10。从而实现两个LED来回翻转。(led <= 2'b10;即LED低位为0,高位为1)
//根据按键的状态以及LED的闪烁状态来赋值LED
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
led <= 2'b11;
else if(led_ctrl == 1'b0)
led <= 2'b01;
else
led <= 2'b10;
end
在此基础上,我们加入KEY实现更多的功能:先自行看代码,再看下面讲解:
//根据按键的状态以及LED的闪烁状态来赋值LED
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
led <= 2'b11;
else case(key)
2'b10 :
if(led_ctrl == 1'b0)
led <= 2'b01;
else
led <= 2'b10;
2'b01 :
if(led_ctrl == 1'b0)
led <= 2'b11;
else
led <= 2'b00;
2'b11 :
led <= 2'b11;
default: ;
endcase
end
通过case语句,判断KEY状态,若按键0按下,LED交替闪烁,按键1按下,LED同时闪烁,若都未按下,则全亮。
二、Verilog实现
1、通过vivado建立工程,选好芯片信号。
2、以下为代码的实现:
module key_led#
(parameter cnt_max=25'd25000000)
(
input sys_clk ,
input sys_rst_n ,
input [1:0] key ,
output reg [1:0] led
);
//reg define
reg [24:0] cnt;
reg led_ctrl;
//*****************************************************
//** main code
//*****************************************************
//计数器
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
cnt <= 25'd0;
else if(cnt < cnt_max) //计数500ms
cnt <= cnt + 1'b1;
else
cnt <= 25'd0;
end
//每隔500ms就更改LED的闪烁状态
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
led_ctrl <= 1'b0;
else if(cnt == cnt_max)
led_ctrl <= ~led_ctrl;
end
//根据按键的状态以及LED的闪烁状态来赋值LED
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
led <= 2'b11;
else case(key)
2'b10 : //如果按键0按下,则两个LED交替闪烁
if(led_ctrl == 1'b0)
led <= 2'b01;
else
led <= 2'b10;
2'b01 : //如果按键1按下,则两个LED同时亮灭交替
if(led_ctrl == 1'b0)
led <= 2'b11;
else
led <= 2'b00;
2'b11 : //如果两个按键都未按下,则两个LED都保持点亮
led <= 2'b11;
default: ;
endcase
end
endmodule
可以看到我把25'd25000000改为了cnt_max,且cnt_max是用parameter定义的。这个模块在被顶层文件定义的时候,顶层文件可以再度parameter这个cnt_max,例如改到25,这样sys_clk只需要计数25次,对应的仿真时间会大大缩短。
三、tb仿真
初学者一定要运行tb仿真和观看RTL视图,tb仿真一方面锻炼仿真文件编写能力,一方面可以快速验证你的代码是否有误,且不占用芯片的资源(后续介绍)。
tb仿真顶层文件:1ns/1ps,就是运行是1ns级别,仿真图抽样为1ps。即解释了,always #10 sys_clk=~sys_clk,这个10的单位了,即10个1ns。
`timescale 1ns / 1ps
module tb_key_led;
reg sys_clk;
reg sys_rst_n;
reg [1:0]key;
wire [1:0]led;
//parameter cnt_max=25'd5;
always #10 sys_clk=~sys_clk; //每10ns,sys_clk反转一次,故20ns为一个周期,即50MHz
key_led #(
.cnt_max(25'd5)) //实现传入parameter
key_led_u(//对例化的模块进行二次命名,一个模块可以被多个文件例化,所以可以起名字,实现区分
.sys_clk(sys_clk),//连接引脚,其中.后面的为原模块的变量即非tb的,()里的为调用人的变量即tb
.sys_rst_n(sys_rst_n),
.key(key),
.led(led)
);
initial //仿真文件的开始
begin
sys_clk=1'b1;//时钟初始先为高电平
sys_rst_n=1'b0;//复位初始先为低电平即复位
key=2'b11;
#1000//延时1000ns
sys_rst_n=1'b1;//复位结束
#200//延时100ns
key=2'b01;
#200 //延时300ns
key=2'b10;
#200 //延时300ns
key=2'b11;
end
endmodule
如下为运行图:默认会添加在module tb_key_led定义的变量: sys_clk、sys_rst_n、key、led;所以,如果你还想看到cnt计数到多少,可以在源文件中加入output [24:0]cnt,在仿真文件通过wire [24:0]cnt,并进行相连,实现仿真文件中也出现。
可以看到,仿真的效果和理论一致,在黑色区域,右键变量,可以选择数字信号还是模拟信号,信号的radix,即:二进制、八进制、十六进制、无符号十进制、有符号十进制。
四、wire、reg介绍:
wire,即线。更多的是做一个连接的过程,reg,寄存器。有人简单称为:时序逻辑用reg、组合逻辑用wire,这么乍一看好像确实没毛病。
reg只能做输出 | 承载仅为wire |
wire做输入时 | 承载可以时wire、reg |
wire做输出时 | 承载仅能为wire |
时序逻辑电路里可以被赋值的只有:reg、integer、time、genvar。
wire的变量,只能在非时序逻辑赋值,可以通过定义时就赋值,例如wire a=1;也可以先定义后赋值:wire a; assign a=1;不能写a=1;
reg的变量,只能在时序逻辑赋值,通常先定义,reg a;(无法定义时赋值)。在时序逻辑电路里,令a<=1;