声明:作为初学FPGA,这篇文章仅属于学习中的个人理解笔记,应该有一些错误,仅做参考,大神勿喷。
注:首先区别一下C与Verilog HDL两种语言的区别。C语言是最后变成指令集给CPU进行处理,而Verilog是综合成电路,设计时要求和C不一样,若是设计不合理电路之间传输太长,那么久无法跑较高的时钟频率,时序不满足。
1、一一法则:初学者最好一个always中产生一个信号,一个信号只在一个always中存在。(只允许一个输出,但是可以有多个输入)
作用:这样书写可以预防同时思考多种信号而出错;后期方便调试修改。
//错误示例
always@*
...
dout <= 1'b1;
...
always@*
...
dout <= 1'b0;
...
2、两种条件:条件语句一般只用 if…else 和 case。
3、三种电路:组合逻辑电路(用“=”);同步复位时序电路(复位信号取决与时钟的上升沿到来时才触发,不独立。占用FPGA的内部资源较多,但是“冒险”较少。);异步复位时序电路(复位信号和时钟信号可触发于任意时刻,两者独立。但是容易产生“冒险”。)
tip:有posedge与negedge的为沿触发则一定为D触发器一定为时序逻辑,在时序电路中必须有复位信号。
//异步复位
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
b <= 1'b0;
else
b <= a;
end
//组合逻辑
always@(*)begin
语句
end
//同步复位
always@(posedge clk)begin
if(!rst_n)
b <= 1'b0;
else
b <= a;
end
同步 RTL Vier:
tip:由于同步异步复位均有缺点,这里可以采用异步复位,同步释放。详情见特权同学的《深入浅出玩转FPGA》P60。
另外在设计代码的时候,如果复位时信号的值为0,那么电路是D触发器的清零逻辑;如果复位信号的值不为0,则电路是一个以复位信号为条件的多路器。
4、每个always,assgin,module模块之间都是并行的,但是在always中的begin…end是顺序执行的(if语句也是)。
always@(posedge clk or negedge rst_n)begin
if(condition1)
语句1;
else if(condition2)
语句2;
else if(condition3)
语句3;
end
如上述代码的例子,
当条件1成立时直接执行语句;若条件2成立(包含条件3的情况)则不执行条件3了;若条件1,2,3均不成立,那么保持数据。这里省略了else,因为在时序语句中的if是可以没有else的,在没有else的情况下判定为保持数据,若是组合逻辑中,没有else则会产生锁存器!
5、reg型与wire型:
设计代码:由模块产生,且是用always产生的信号,用reg型(“<=”)
测试代码:由initial产生的信号,用reg型
其他都用wire型,wire型为线性信号;reg一般为寄存器信号,也可以不产生寄存器。
assgin一般用于赋值,assgin的左侧必须为wire型。
不管在组合逻辑还是时序逻辑中,只要在等号的左端均应用reg型。
(一般来说输入是wire,输出是reg,因为输出需要给一个值。但assgin的左侧必须为wire型)
tip:一个reg型变量对应一个触发器。如:always@(posedge clk)begin a <= cnt; b <= a; c <= b; end
则代表a b c 各有一个触发器。
tip:一个wire型变量对应一个线型值
6、逻辑运算符:+、-、×、/、%中尽量使用 +、-、× 少用/、%,后者占用资源较多。
条件运算符:&&与、||或、!非 一般用于条件判断,且一般两边都是1bit信号(多bit也可以,但不UI建)
位运算符:~按位非、|按位或、&按位与、^按位异或,一般用于赋值。
移位运算符:<<左移、>>右移。一般用于乘除运算,右移1位==除以2,右移3位=除以8,左移2位=乘以4(在FPGA中,若以2的指数形式来进行乘除,可以使用移位运算符节约资源)
7、模块例化并调节参数
mul_module #(.A(3),.B(4))mul_module1(
例化的语句
);
//把mul_module模块中parameterA改为3 B改为4,例化在mul_module1中
8、时序逻辑是设计的核心,寄存器是时序逻辑的基础。
9、在设计中,面积与速度不可兼得,如何取舍看设计的要求。
如a-y为关键路径如下:y=((a&b&c)|d)&~e 经过等式变换 y=(a|d)&((b&c)|d)&~e
左边的式子a传了3级到达了y资源占用率相比右式低很多,也就是面积小,速度慢。
右边的式子a传了2级到达了y资源占用率相比左式高很多,也就是面积大,速度快。
10、一个信号加_n代表低电平有效。rst一般表示复位信号,则rst_n则是低电平有效的复位信号
低电平复位有效 if(!rst_n) 等价于 if(rst ==0) ; 高电平复位有效 if(rst)等价于if(rst ==1)
tip:常用于第一种写法,简洁
11、always@()begin…end 在always里的begin和end可以不写,但其他地方语句超过两句就必须写。
12、在testbench里什么时候reg什么时候wire——例化语句中,输入在testbench中为reg型(取初值时),输出为wire型
13、亚稳态——在寄存器中,输入传到输出的时间。所谓竞争冒险,即多个触发器中,在传入到传出过程中可能产生的值造成的影响,而这个值并不是触发器最终的状态,有时会造成特别严重的后果。
预防亚稳态的方法:
<1>对于控制信号,可以同步信号,即打2拍或者3拍(D触发器)。
<2>对于数据流,用FIFO。
<3>对于少量,发送可控的数据流,增加指示信号。
14、项目的一些理解:一个九位位宽的单个ROM存12位数据,则有2的9次方个数据(512),每个数据的位数为2的12次方(2048);一个f为5k的信号,采样512个点,问采样频率。则信号1S跳5000个周期,每个周期采样512个点,那么采样频率为512*5000=25.6MHz。
15、modelsim中的超过2个仿真文件的仿真设置问题:
当仿真的top文件要调用另外一个仿真文件时,在modelsim中需要把两个仿真文件同时放如testbench设置里。需要注意只要是仿真文件必须要有时间刻度:`timescale。
16、在分析信号的时候,需要考虑这个信号是在哪个时钟域,在不同的时钟域产生的误差,同步等均不相同。
17、8421BCD码
[1]8421BCD码(不指明BCD码就默认8421):是一个四位的二进制数,从0000(0’d)到1001(9’d)。eg:1001_0110_0100(964’d)
[2]BCD码的意义:BCD应用在数码管上,若把158拆分为1、5、8 需要做除法运算 158/100=1 158%100=58 58/10=5 58%10=8。但是除法运算需要占用计算时间,转换为BCD码可以大幅度减少运算时间。
[3]计算规则:4位BCD码大于1001b(4’d9)则加0110b(4’d6)进行修正。如0101b+1000b=1101b (大于1001b)则加0110b =1101b+0110b=10011b 补码后:0001_0011为13。
18、Verilog代码书写风格
注:在《Verilog编程艺术》书上根据我的需要,写下的一些对我个人有用的笔记
<1>取名字一般用下划线或者大写。eg:post_payment PostPayment
<2>名字的约定习俗:_clk代表时钟,_n代表低电平有效,_r寄存器输出,_a异步信号,clk—clock,rst—reset,addr—address,dat—data,rdy—ready,cnt—count,req—request,ack—acknowledge,dout—dataout
<3>子模块的名字应该使用顶层模块的名字作为前缀。eg:hex,hex_reg,hex_divider
<4>在书写代码时,按功能分组,主控在前。
eg: A、异步复位
B、时钟信号
C、使能信号
D、控制信号
E、地址信号
F、响应信号
G、数据信号
<5>module名字后面的“( ”不加空格要另起一行,这样后续的input,output以此对齐。
<6>每一个语句独立成行
<7>always,for,while语句,begin在它们的下一行;initial,if,else if,else,begin和它们同行且begin后换行;end单独成行
<8>对于运算符号如=,+,-,*,/,%,<<、&,&&,or等,在符号两边各加一个空格
<9>对于逗号,分号,在它们的后面加一个空格
19、组合逻辑和时序逻辑都不能作为时钟信号和复位信号,因为组合逻辑和时序逻辑都属于中间信号,没有经过特别优化,不稳定,容易产生亚稳态和毛刺从而影响结果。
20、组合逻辑常见错误,case 中缺少default ,if中缺少else。这样会产生一个锁存器,对FPGA及其不利。
锁存器的组成特性:是有门逻辑资源搭建起来的电平敏感的存储器,电平触发,对时钟边沿不敏感(时序逻辑对边沿敏感);相比寄存器,所需要的门逻辑资源要少,常用于ASIC设计中。
设计缺点:<1>属于非同步设计,不易于信号控制;
<2>没有边沿触发,输出的信号会有毛刺;(组合逻辑会产生毛刺)
<3>使时序分析很困难。(对时序不敏感,电平触发)
21、用于不定时计数的(即每轮计数多少个不定),用自减计数器(相比自增代码简洁)。如pwm波形占空比的调节。
22、计数器的复用:在一个程序中有多个计数器且过程相同时,可以以最小的计数器为单位合并计算器,以这个计数器再计数,可以节约FPGA内部资源。
//eg:计数1s,2s,3s,作为其他程序启动的标志。
//建立计数1s的计数器
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
count <= 26'b0;
end
else if(count==TIME_1S-1)begin
count <= 32'b0;
end
else begin
count <= count + 1'b1;
end
end
//对上述1s的计数器进行再计数
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
count_pulse <= 0;
end
else if(count==TIME_1S-1)begin
if(count_pulse==2)begin
count_pulse <= 2'b0;
end
else begin
count_pulse <= count_pulse + 1'b1;
end
end
else begin
count_pulse <= count_pulse;
end
//2s条件count_pulse==1&&count==TIME_1S-1;3s条件count_pulse==2&&count==TIME_1S-1
//本来计数1s、2s、3s如用三个计数器来计数,需要约90bit数据。而此时巧妙的运用了把1s的计数器进行再计数,这样只用到了26bit+3bit=29bit数据,大大节省了内部资源!
23、在进行移位操作时的两种写法:
reg [7:0] led;
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
led <= 8'b0000_0001;
else
//法一:直接移位
led <= led <<1;
end
//法二:拼接符移位
//led <= {led[6:0],led[7]};
//三态门
inout data;
data = wr_en?wr_data:1'bz;
rd_data = data;
FPGA会自动的将此代码综合成三态门;三态门只用于管脚中,FPGA内部是不允许有X态和Z态的。
25、不用除法求余。
总所周知的,除法在FPGA中资源占用率太高,所以尽量避免除法的使用以节省资源。若对一个数123,求他除N的余数,可用下列思想。123-N是否大于N,若大于N,则判断再减N即123-N-N是否大于N,若小于N则为余数,大于N继续减N直到小于。
26、FIFO一般用于:1.异步时钟域处理 2.缓存数据。打一拍来处理一般只用于1bit的数据,延迟的时间要长不能一周期就变一次,这样会拍出错误的数据。
27、补码操作:在FPGA中有些需要用到正负,即符号位。当为负的时候,即最高位为1时,需要进行补码操作。如4bit数据 +2表示为4’b0010 +1表示为4’b0001 -2表示为4’b1010
(+2) + (-2) = 0
检测到负数时,对负数进行补码操作(最高位符号位不变,其余全取反,并+1‘b1’),1010变为
1101+1=1110。这个时候进行加法操作0010+1110=0000 = 0,检测到最高位为正,则不进行补码操作。
(+1) + (-2) = -1
检测到负数时,对负数进行补码操作,1010变为1101+1=1110。这时进行加法操作0001+1110=1111,
检测到最高位为负,则进行补码操作,1111变为1001=-1
28、同一个工程(程序一样)下,编译出来的.sof文件可能不同。
原因有如下:
软件问题——由于没有固定硬件器件的约束,所以在编译综合的时候,会随机选择最优的路径(每次可能不一样)。对于Intel quartus来说,一般17.0以上的版本重新编译不会变,13.1版本一直变。
系统问题——不同的操作系统,如windows家庭版,旗舰版等编译出来的.sof文件可能不同。
代码问题——可能出现代码改变,如不小心按下了空格,或改了某个信号忘再编译。
不同的.sof文件的影响:
对于大型工程来说,FPGA对时序的要求特别高,若.sof不一样,很可能导致某次或多次(也可能永远)硬件的时序不满足,那么板子将无法工作。
29、在给代码赋值时,只有十进制的可以为所欲为
给代码赋值的时候,除了十进制('d)都要把所有位数写全,不然可能出现未知的错误。
例如赋值为1,常见的几种进制表示
assign dout = 8'b1;//改为8'b0000_0001;
assign dout = 8'h1;//改为8'h01;
assign dout = 8'd1;//为所欲为
30、数据处理——有小数运算中的精度提升
//令x=12.3456789,若要进行下列的计算
assgin y = (2^32*x)/50_000_000;
//直接运算,那么y=1060.4857424483,在fpga中小数后的数直接丢弃。
//若我们需要用这个数操作,那么小数点后的数很可能会影响较大的精度。怎么保留精度呢???
//采取乘和除的形式
assgin y = (2^32*x*1000)/50_000_000/1000;
//这样精度可以扩大三位数,由于除法占用逻辑资源高,考虑使用向右移位的方式做除法,但是需要2次方为基本。则与1000最靠近的是2^10=1024
assgin y = (((2^32*x)<<10)/50_000_000)>>10;
31、数据处理——按键数字增加思路
若我们需要给一个按键,分别按下1,2 实现数字12的显示。
out <= 0; //初始化
out <= (out<<1) + (out<<3) + key_value;//key_value为按键值
当按下第一个键1,显示数字1。按下第二个键2时,显示12。则相当于 1*10+2。在移位操作中,移1位代表2^1,移3位代表2 ^3,2 ^1+2 ^3 = 10,就相当于把第一个数字提高了十倍!
32、计数器中嵌套计数器
实现计数计到某个数的时,暂停计数开始计另一个计数器,计完再继续前面的计数器。
always @(posedge clk)
if(cnt1==10)
cnt1 <= 4'd0;
else if(!en_cnt2)
cnt1 <= cnt1 + 1'b1;
assign en_cnt2 = (cnt1==5)&&(data_num>1);
alwasy @(posedge clk)
if(en_tx)
data_num <= length;
else if(en_cnt2)
data_num <= data_num - 1'b1;
//代码实现 自增10计数器1,在计到第5个数时,暂停计数器,开始自减length计数器2,完毕后继续执行计数器1。
代码使用场景,在MAC层发包中自增计数器1是MAC层的协议,前导码,dmac,smac,type_length,(自减计数器2)数据段,CRC校验。数据段来自上层协议(如ARP,UDP等)。
33、FPGA中的“数组”
其实在FPGA中没有真正意义上的数组的,只是用数组的形式定义了多个寄存器罢了。
下面进入正题给代码
reg[11:0] memory[255:0];//[11:0]12位的数据,[4095:0]代表有4096个12位数据
memeoyr[30] //指的是第31个memory
memory[30][11:8]//指的是第31个memory的高4位
tip:一些特例写法:
1、[39-:8]等价于[39:32]
2、assign z = {y,5{x}};等价于assign z = {y,x,x,x,x,x};
附几个在网上搜的对初学者有用的学习链接:
如何学习FPGA
学习FPGA需要留意,实用性比较好
简谈异步电路中的时钟同步处理方法
如何利用FPGA进行时序分析设计