常数
整数
当把带有size的负常数赋给一个reg类型的变量时,不管这个变量是否为signed,对这个负常数进行符号扩展
未指明signed的reg是无符号的
赋值号右边的类型是有符号类型时,赋值给左边变量时都进行符号扩展,无论左边的变量是否为有符号类型
赋值号右边的类型是无符号类型时,赋值给左边变量时都进行0扩展,无论左边的变量是否为无符号类型
sign必须和base一起使用
1 | 1's1; // 错误 |
注意:
- -4’d12是无符号类型,但是赋值给reg时会进行符号扩展
有base没sign的类型就是无符号类型,不管前面有没有负号,负号只是取其补码
字符串
字符串中一个字符8位
空串””也是8位,有一个”\0”
字符串填充问题会影响比较,如
1 | reg [8*10, 1] s1, s2; |
数据类型
变量
reg、time和integer的初始化值为x
real和realtime的初始化值为0.0
线网类型的初始化值为z
integer等价于reg signed[31:0],time等价于reg unsigned [63:0]
不能对real和realtime使用位索引和部分索引
多维数组
引用数组中的多个元素是非法的,如reg [23:0] image [0:599][0:799],image[3][0:50]是非法的
表达式
操作符
对reg变量进行赋值,不管reg是否为signed,若右边的数是符号数,则进行符号扩展后赋给reg
比较操作符中,只有两个操作数都为符号数时,比较才按符号数比较,对较小的数进行符号扩展;如果有一个为无符号数,则按无符号数进行比较,对较小的数进行0扩展
如果两个操作数有一个是实数(real,realtime),则先将另一个转换为实数再进行比较
移位操作符的右操作数始终被当作无符号数
在做复制操作时,函数调用只执行一次,如
1 | result = {4{func(w)}}; |
part-select
语法:vec[ base +: width], base可以是变量,width必须是常量
1 | // 最高位在左边的例子 |
1 | // 最高位在右边的例子 |
自决定表达式
连接操作的结果是无符号数
bit-select和part-select的结果为无符号数
比较操作的结果为无符号数
表达式i op j的位长位i、j中的最大值
条件表达式 cond ?i : j;的位长为i, j 中的最大值
移位和幂(**),a op b的位长为a的长度,如 a >> 5, 表达式位长为a的位长
在计算表达式时,中间结果就取操作数的最大位长
1 | reg [15:0] a, b, answer; |
计算表达式的步骤
- 确定表达式的位长(最长的操作数确定)
- 确定表达式的符号,如果有一个为无符号,则为无符号
- 把表达式的位长和符号传播到上下文
signed应用的错误
1 | reg signed [8:0] out, in; |
指数运算
verilog支持指数运算**
,当左右两边都是常数时才可以综合
赋值操作
如果对一个变量既有变量声明赋值,又在initial块中赋了其他值,那么他们的执行顺序是不确定的
行为模型
initial只执行一次,而always整个仿真期间并发执行
赋值语句
1 | // 当遇到clk上升沿时才赋值给a,这是事件控制的赋值 |
阻塞赋值的也有同样的语法,略
仿真器必须保证在一个过程块内对同一变量的不同非阻塞赋值的执行顺序,如
1 | initial begin |
如果是不同块的同一时间赋值,则结果是不定的
1 | initial begin |
1 | initial #8 a <= #8 1; |
repeat的括号中的表达式值为x或z,则不执行repeat
forever和always的区别:
- forever不能独立写在程序中,只能写在initial中
- always能够独立写在程序中
1 | initial begin |
for循环个数是常量才可以综合
disable语句作用类似于c语言中的break,用于中止块,disable 块名
posedge中,0 -> 1会触发,0->x和0->z也会触发,x,z->1也会触发!
negedge中,1->x,z,0都会触发,x,z->0也会触发!
使用fork join交换两个变量的值
1 | // a和b的值在延迟前被计算,延迟后才赋值 |
当always敏感列表中出现边沿敏感的,就不要出现非边沿敏感的
always敏感列表中出现没用的变量不会出错,但是会使仿真运行变慢
always @()中的\代表always块中出现的所有右侧变量
case
verilog的case实现是顺序比较的,排在前面的case项有更高的优先级
case提供的是精确匹配,包括x,z。但是case项中出现了x或z,这样的case是不可综合的
反向case适用于任何时刻只出现一个匹配的case,例如FSM中的独热编码
1 | // 反向case的例子 |
casez
casez会忽略z和?对应的bit,x不会被忽略
综合工具指令//synposys full_case 只用于综合工具,而没有作用于仿真工具,该语句告诉综合工具case语句是全部定义的
parallel_case
parallel_case是case表达式只能匹配到一个case项,不会匹配到多个case项
如果一个case表达式能够匹配到多个case项,则生成优先级编码器,否则生成多路选择器
但是即使每个case项都是独立的,一个case表达式只会匹配到一个case项,仍然可能综合出优先级编码器,如下面的代码
1 | // irq只会匹配到一个case项,但是仍可能生成优先级编码器 |
可以使用注释//synopsys parallel_case告诉综合工具生成非优先级编码器,但是仿真工具仍然可能按优先级编码器仿真,导致前后仿真不一致
case语句的编码原则
- 小心使用casez,不要使用casex
- 小心使用反向case,最好只针对parallel_case使用
- 使用casez时用?表示不关心的位,而不用z
- 最好不使用//synopsys parallel_case和//synopsys parallel full_case
task和function
函数和任务都是可综合的
两者的不同点:
- function不能包含时序控制语句,如#, @和wait
- function不能调用task,但是task能调用function
- function至少要有一个input类型的参数,不能有output和inout类型的参数,而task的参数类型和个数可以有多个
- function返回一个值,而task不返回值
对task的调用叫使能(enable),对函数才叫调用(call)
task
task能使能其他task,只有完成所有task后才返回到使能task的过程
使用automatic让给每个task动态分配内存,而不是一个实例的所有task共享内存,如
1 | task [automatic] task_name (port_type portname); |
没有使用automatic的task中所有变量是静态的,包括input,output,inout参数,都会保持最后一次使用时候的值,在仿真0时刻,他们被默认初始化位0或x
task中所有参数是按值传递,而不是按引用传递!
1 | // 下面的task不会退出 |
function
function的作用是返回一个值,然后把它用于表达式中
声明function时可以指明返回类型,如果没有指明,则默认是1bit的标量reg。function内部隐含地声明了一个于function名字一样的变量
1 | function [automatic] [7:0] getbyte ( |
function不能使用非阻塞赋值!
function不能触发任何事件
function总是综合出来组合逻辑
调度和赋值
仿真模型
verilog的仿真模型将事件分为了五个队列:
- 活动事件队列:阻塞赋值、非阻塞赋值的右侧表达式计算、连续赋值、$display、计算原语和实例的输入信号,还包括@触发事件
- 非活动事件队列:用于0延迟的调度,即
a = #0 b;
,避免使用0延迟! - 非阻塞赋值更改队列:非阻塞的赋值更新
- 监控事件队列:
$monitor
和$strobe
- 未来事件队列:保存未来执行的事件(非本次仿真时间(time-step)调度的事件,当前time-step的延迟事件也会被放到未来事件队列中)
所有活动事件的处理称为一个仿真周期,verilog调度算法可以选取任意的活动事件进行处理,因此执行不确定
仿真参考模型如下:
1 | while (队列中有事件要处理) { |
可以看到调度一个活动事件时会因为表达式结果的算出或者对象的赋值而激活其他事件,如下面的例子,假如1先被调度执行,然后调度2,2的赋值成功会重新激活1,因此1最后会再被调度一次。注意所有事件的发送都在同一个time-step
1 | a = b; // 1 |
当非阻塞赋值的更新事件被调度时,在当前time-step可能又会激活其他的事件 。如下面的例子,2和1的右侧计算属于活动事件,会先调度执行,然后活动事件调度完成后会激活非阻塞赋值的更新事件到活动事件中,调度a的更新操作,然后因为a的值变化又会激活2再执行一次(都在同一time-step)
1 | a <= 5; // 1 |
可以看到非阻塞赋值的右侧是先被计算(属于活动事件),当所有活动事件处理完后才会进行非阻塞的赋值!因此下面的语句不会产生竞争,能够成功交换a、b的值
1 | a <= b; |
如果在当前的仿真时间里还有活动事件和非阻塞赋值更改事件,那么仿真时间不会向前
确定性和不确定性
确定性
begin/end块中的语句必须按照它们在块中呈现的顺序执行
非阻塞赋值的赋值顺序应该和语句执行的顺序保持一致
不确定性
下面的例子中,display显示1或者0都是对的,因为#1后,q=0
和$display
都属于活动事件,因此仿真器可以任意调度其中一个先执行
1 | assign p = q; |
阻塞赋值和非阻塞赋值
要在always块中使用阻塞赋值生成组合逻辑,使用非阻塞赋值生成时序逻辑,这样是放置前仿真和后仿真一致
阻塞赋值
阻塞赋值是在过程快中的过程赋值,只能针对于reg类型,使用=
操作符,对wire类型的赋值虽然也使用了=
操作符,但是把他们称为连续赋值
带有延迟的阻塞赋值,使用当前值计算RHS,然后让执行进程挂起,并作为未来事件调度,如下面的例子,当前time-step先计算b&c
,然后在未来再执行a的赋值操作
1 | a = #5 b & c; |
非阻塞赋值
非阻塞赋值是在过程快中的过程赋值,操作符是<=
。非阻塞赋值只能针对reg类型,只能在initial和always块中用
非阻塞赋值由两步操作构成:在time-step的开始计算RHS,在time-step的结束更改LHS
端口连接
端口连接当作隐含的连续赋值或隐含双向连接处理
- 如果是input端口,就当作一个从外面表达式到内部线网的连续赋值
- 如果是output端口,就当作一个从内部线网到外部表达式的连续赋值,因此输出端口只能用wire变量驱动
- 如果是inout端口,就当作一个内部线网与外部线网连接的没有强度减少的晶体管
赋值使用原则
- 当模型触发器和锁存器时,使用非阻塞赋值
- 当模型组合逻辑时使用阻塞赋值
- 当在同一个always块中,如果既模型时序逻辑,又模型组合逻辑,那么使用非阻塞赋值
- 不要在一个always块中同时使用阻塞赋值和非阻塞赋值
- 不要在多个always块中对同一个变量赋值
- 使用
$strobe
显示那些用非阻塞赋值的变量 - 不要对赋值使用
#0
延迟
各个#delay的位置
下面两个没区别
1 | #5 |
下面两个等价
1 | always @(*) begin |
延迟线模型
正确的延迟线模型,**注意在模块中使用#delay时需要加上``timescale`**,因为不知道编译的顺序,会使用它之前的最后声明的timescale
1 |
|
下面的是错误的,没有延迟之前的信号值,y1,y2会被赋值为TAP1,TAP2后的值
1 |
|
验证平台技巧
0时刻复位
如果要让第一个复位信号在0时刻生效,使用非阻塞赋值。下面的例子使用了阻塞赋值,在0时刻可能先调度always块,导致initial块中的rst_n信号没有被捕捉到
1 | initial begin |
或者在仿真器开始1~2个时钟周期后,再让复位在时钟无效沿起效
时钟生成
时钟生成推荐使用如下的方式,在0时刻使用非阻塞赋值是为了触发那些对negedge clk敏感的过程块
1 | initial begin |
参数调整
模块参数parameter在声明时可以有类型和范围,也可以没有。在模块实例化时使用下面的原则调整参数的类型和范围:
- 对于没有范围和没有类型的参数,那么就默认使用模块实例时传进来数值的范围和类型
- 对于有范围但没有类型的参数,就认为参数的类型时unsigned,传进来的数值的范围和类型会转换为参数的范围和类型
- 对于没有范围但有类型的参数,模块实例化时传进来的数值类型要转换为参数的类型,参数范围使用传进来数值的范围
- 对于有范围又有类型的参数,传进来的数值要转换为参数的范围和类型
下面的例子中,A有范围无类型,默认为unsigned类型,B没有范围也没有类型。因此3.1415传给A时会转换为unsigned,因此A=3;B直接被赋值为3.1415
1 | module foo; |
实数传递
实数类型不能直接连接到端口上,需要使用$realtobits
和$bitstoreal
来传递位模式
1 | module driver (output net_r); |
generate
generate区域不能嵌套,只能在模块内直接使用
generate能用来实例化多个模块或者生成多条assign语句
begin块必须带名字
loop generate
使用for循环多次实例化生成块,循环变量要为genvar类型,genvar类型有如下要求
- genvar既可以在generate里面声明,也可以在generate语句外面声明
- genvar时正整数,只能用于generate循环,在仿真时不存在
- 两个使用同一个genvar的generate循环不能嵌套
- 可以把genvar当作常量,用于修改模块的parameter
生成一组assign
1 | genvar i; |
实例化多个模块
1 | genvar i; |
使用generate会产生层次化,并且选择的模块或者产生的模块都会具有一个名称。如果未命名,则编译器将自动分配一个通用名称
要访问generate块中的模块项,必须使用<generate_blk_name>.<module_item_name>
进行分层访问,如bit[1].g1
使用不同genvar的for循环可以嵌套
1 | genvar i, j, k; |
condition generate
在generate块中使用if或case条件生成模块,因为最多选择一个,所以生成块的名称可以相同,下面的例子中所有生成块的名称都相同,模块例化的名称也相同
1 | generate |
层次名字
在任意模块内,子模块实例、生成块实例、任务、函数、命名begin/end和fork/join块定义了层次的一个新分支(平行的),通过层次名字可以引用内部的变量
1 | module wave; |
系统任务和函数
打印函数
$display和$write的区别:
- $display会在输出后自动加上换行符
- $display没有参数时会显示一个换行符,而$write什么都不显示
$strobe会在当前time-step的最后打印信息,适合用来输出非阻塞赋值的变量,而$display属于活动事件,会先调度执行
$monitor会当除了$time、$realtime、$stime的变量变化时在time-step的最后显示出来,如果同一时间有多个参数发生变化,只会显示一次
##$readmemb和$readmemh
$readmemb和$readmemh函数用于从文件中加载数据到memory中
1 | $readmemb ("file_name", memory_name, start_addr, finish_addr); // 数据文件必须是2进制 |
其中start_addr和finish_addr为memory的地址
文件中可以有空格字符或注释(//, /**/),数据没有长度限制,可以有x、z和_
如果没有指定finish_addr,该函数会读到文件读完或者memory填满
$finish
$finish函数让仿真器退出执行,可以带参数,默认为1:
- 参数为0则什么都不显示
- 参数为1则显示仿真时间和位置
- 参数为2则显示仿真时间、位置、内存使用统计、用于仿真的cpu时间
$random
$random返回一个32-bit的随机数,而且是有符号数,可以使用拼接符号来将$random返回的32bit有符号数转换为无符号数,然后可用来产生无符号随机数
1 | $random % b // 返回的数在[-b+1, b-1]之间 |
其他
$time返回64bit的整数仿真时间;$stime返回32bit的整数仿真时间;$realtime返回一个按时间单位表示的实数仿真时间
$fgetc从指定的fd读入一个字符(8-bit),如果读发生错误就会返回-1,因此需要将接收字符的变量长度设置为大雨8-bit,这样才能将0xff和-1区分开
编译指令
将所有宏定义放到一个文件中,并在编译时先读这个文件
`timescale的例子,#d的延迟时间是16n
1 |
|
状态机设计
独热编码的FSM更加高效,因为组合逻辑比使用二进制编码的FSM要少,而FPGA的性能一般和设计中的组合逻辑大小有关
独热码编码FSM使用反向case的编码方法,localparam不是表示状态的编码,而是表示状态的索引
1 | localparam idle=0, bbusy=1, bwait=2; |
再FSM中使用寄存器输出可以保证输出没有毛刺,而使用组合逻辑输出可能会有毛刺,下面的例子将上面的gnt信号改为寄存器输出
1 | //assign gnt = state[bbusy] | state[bwait]; |