verilog+十大基本功

  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

收集整理自开源 IP 通吃岛 /bbs
verilog 十大基本功
SOCvista 的版主 dancia 最近对国内的主要 EDA 论坛做了一项调查,耗时一个多月,经过仔细的总结,发现一个有趣的
现象:国内关于 HDL 提出问题的帖子,有 80%都集中在一小部分基本问题上。同样的一个问题,从 5 年前开始就有
人问起,一直问到今天,很可能还会持续到 N 年后。貌似新人们总也避不开这些说 难其实也不难的问题,而且实际的
调查还发现,就连好多工作多年的工程师们对这些问题也是稀里糊涂,似懂非懂。
为了帮助大家一劳永逸解决这些基本难题,从而迈向自由编程的康庄大道,我们特地开辟了这个版块,要把这些基本
功一一传授给大家。如果觉得我们的帖子还不过瘾,你可以回帖继续提问,务必把所有概念搞清搞懂。
另外所谓十项只是一个概述,请勿过分计较。
谢谢大家的关注和支持。
原帖见:dancia《高手也搞不清楚的十项基本功》
/bbs/search.php?searchid=11&orderby=lastpost&ascdesc=desc&searchsubmit=yes
以下是大家讨论的一个总结:
1、阻塞赋值与非阻塞赋值
前言:阻塞与非阻塞赋值是 Verilog 语言中最基本的部分,也是让大部分 Verilog 新手最困惑的地方。
关于阻塞与非阻塞的著作文章可谓汗牛充栋,这些文章对阻塞与非阻塞赋值的原理进行了非常详细的讲
解,但新手读了之后依然有种似懂非懂的感觉,编码过程中一如既往的犯错。所以,本文的目的立足于
提供一种实用化的解决方案,用最简单的语言和形象的类比让新手能够一目了然的明白正确的编码方式
以及相应的电路行为逻辑,关于仿真细节的讲解不是本文重点,需要了解更多细节的朋友可以参考文后
列举的参考文献。
本文共分为三部分,第一部分是正确使用阻塞与非阻塞赋值的基本原则。第二部分是阻塞与非阻塞赋值
对应电路的行为逻辑。第三部分是阻塞与非阻塞赋值的原理简介。
Golden Rule
编码原则很多,就阻塞非阻塞赋值而言,新手最需要牢记的是其中三条:
1、时序逻辑一定用非阻塞赋值”<=”,一旦看到敏感列表有 posedge 就用”<=”。
2、组合逻辑一定用”=”,一旦敏感列表没有 posedge 就用”=”,一旦看到 assign 就用”=”。
3、时序逻辑和组合逻辑分成不同的模块,即一个 always 模块里面只能出现非阻塞赋值”<=”或者”=”。如
果发现两种赋值并存,一个字”改”,心存侥幸可能会给后续工作带来更多麻烦。
以上三条,对新手而言不必追求为什么,需要的就是条件反射的照章办事。最后说一句,新手可能记不
住哪个符号是阻塞赋值

,哪个是非阻塞赋值,大家可以数数,”非阻塞赋值”一共 5 个字,“阻塞赋值“4
个字,所以非阻塞用的符号”<=”比阻塞赋值用的符号”=”长。
电路行为逻辑。
第一节给出了三条最基本的编码原则,有个朋友可能会想,按照这三条编码原则写出来的代码会按怎样
的逻辑工作呢?这一节就是回答这个问题。
首先解释一下阻塞赋值与非阻塞赋值的含义。所谓的阻塞赋值”=”就是说,在这个语句没有执行完之前,
后面的语句是不执行的。这里执行的含义是指完成变量值的更新。非阻塞赋值”<=”是指,所有的语句可
以并发执行,而前面的值是否执行完毕不会影响后面的语句,换句话说,语句的顺序是无关紧要的。
收集整理自开源 IP 通吃岛 /bbs
举个例子,假设一个模块,有 2 个寄存器,b 和 c,初值都是 1。a 为输入信号线。在某个时刻,因为某
种原因,模块被触发执行。对于组合逻辑而言,一般是输入信号值变了,对于时序逻辑而言,一般是时
钟沿到了。
首先看组合逻辑:假设输入 a = 2;
always@(a ,b)
begin
b = a;
c = b;
end
由于是阻塞赋值,所以首先执行完第一句 b=a,执行完成之后 b=2。接着执行 c=b,执行完成后 c=2,一
次仿真结束后 b=c=2;
对于时序逻辑而言,依然假设 a =2;
always@(posedge clk)
begin
b <= a;
c <= b;
end
由于是非阻塞赋值,首先执行第一句 b<=a,这时候 a = 2,但是还没有执行完第一句的时候,第二句 c<=b
也执行了,由于第一句没有执行完,b 的值还是 1,这时候赋值给 c 的值也是 1。执行完毕的结果就是
c=1,b=2.等到模块再次被触发的时候 c 的值更新为 2。有个朋友可能就会问了,凭啥第一句执行到一半就
该第二句执行呢?到底是第一句先完成赋值呢还是第二句先完成赋值?答案是,谁先完成赋值都没关系,
结果是一样的。
为什么说结果一样呢?因为两种赋值方式分别是按照下面的顺序执行的。阻塞赋值,就跟 C 语言一样,
严格按照代码书写的先后顺序执行,所有值都是立即更新,并且在下面的语句中按照新值执行。而时序
电路就不一样了,大家可以这么理解时序逻辑的代码行为,一次执行分为两轮:第一轮是所有的左值都
先赋给临时变量,第二轮用输入值以及和右值同名的临时变量值去更新左值。比如上面的例子,第一轮,
赋给临时变量:tempc=c;tempb=b。第二轮,临时变量更新左值,b = a;c = tempb;output = tempc。从上面
的分析也可以看出,组合逻辑的结果与代码顺序直接相关,而时序逻辑与代码顺序没有关系。这就是所
谓的顺序执行(组合逻辑)与并发执行(时

序逻辑)。
为了进一步理解这两种赋值方式的行为,下面用对应的电路进行说明,以前面的代码为例。大家首先闭
上眼睛想想,对应的电路是什么样子的呢?
其实答案很简单,对于阻塞赋值来说,如图一所示,综合的结果就是一根导线,当然,可能有反相器,
buf 什么的,反正还是可以看作一根线。
图一
到这里,应该就很容易理解顺序执行的行为方式了。
有细心的朋友可能会问,如果换种写法呢?
收集整理自开源 IP 通吃岛 /bbs
always@(a,b)
begin
c = b;
b = a;
end
很显然,这种电路的行为跟之前是不一样的,从逻辑来看会产生类似于非阻塞赋值的结果,但很显然不
满足非阻塞赋值并发执行的特点。如果把输入电平 a 触发改成时钟边沿触发 posedge clk,出来的就是寄
存器,但这违反了时序逻辑不用阻塞赋值的原则,所以严重不推荐。至于这种组合逻辑描述方式出来的
电路是啥我也不知道,大家可以自己综合看看,或者哪位高人补上~~应该注意的是,如果想象不出这种
怪异的 coding 方式会产生何种电路,就不要这么写,因为实现这种逻辑最好的办法是采用非阻塞方式描
述。
而对于非阻塞赋值而言,如图二所示,综合出来的结果就是 2 个寄存器。对 b,c 赋值的过程就是寄存器
输入采样的过程,很显然两个采样是同时进行的,而且一次时钟沿只会采样一次,所以输入值 a 会首先
被采样到 b,再在下一个时钟被采样到 c.
图二
总结一下,关于两种赋值方式,首先讲述了代码执行的过程,然后用直连线和寄存器分别对应了两种描
述方式。应该指出的是,非阻塞赋值用寄存器的类比是完全准确的,而阻塞赋值用直连线的类比却未必
准确,只不过因为一般认为直连线是从输入到输出依次更新的,而且没有传输以外的延迟,所以这种类
比有助于新人理解,虽然不够严密。大家熟悉了之后就应该按照更严谨的方式去理解。
补充一下,如果写出下面这样的代码,在时序电路中使用阻塞赋值的话,综合出来就只有一个寄存器,不是两个,寄
存器的输入 D 端是 a,输出 Q 端是 b 和 c,两个信号在同一根线上
always@(posedge clk)
begin
b = a;
c = b;
end
所以这样的写法很容易造成设计和综合结果不匹配的情况,即使不是要设计两级寄存器,也最好不要用阻塞赋值。
另外一个就是,脑袋里面需要有时序逻辑和组合逻辑的框图。将组合逻辑和时序逻辑分开写,组合逻辑用=,时序逻
辑用<=,时序逻辑内的判断越简单越好,尽量都放在组合逻辑里面做,时序逻辑只负责 flop 一下。
所以,最重要的是,新手

应该严格按照规则来,见到 posedge 就写 <= ,而根本不考虑 = ,免得综合出怪东西来。
这种代码应该严格杜绝,呵呵~~

收集整理自开源 IP 通吃岛 /bbs
xueping406:对于 VHDL, 描述寄存器的格式相对固定,也没有两种赋值方法的区别,按照这个写法一般都不会错
异步复位:
process (clk, rst_b)
begin
if rst_b = '0' then
a <= '0';
elsif (clk'event and clk = '1')
a <= b;
end if;
end process;
同步复位:
process (clk, rst_b)
begin
if (clk'event and clk = '1')
if rst_b = '0' then
a <= '0';
else
a <= b;
end if;
end if;
end process;
VHDL 里如果想在 process 里做到类似阻塞赋值的效果
可以定义信号为 variable 类型,对 vaviable 类型的赋值,需要用 := 符号
这种赋值一般在 testbench 中使用,可综合的 RTL 一般不用这个
VHDL
:= 表示对信号直接赋值,可以用来表示信号初始值,不产生延时;
<=表示代入赋值,是变量之间信号的传递,代入赋值允许产生延时。
另外,直接从英文 nonblocking 和 blocking 的意思理解比较直观,而且在学习的时候,要自己动手实践才有所收获。如果
把 always 的里加上 nonblockig、blocking 的组合这些都列出来,是一个很大的工作量,而且也要看 simulator 处理的方
式,有时候,同样的东西,不同的仿真器结果会相去甚远。所以建议写正规的写法,按照前人总结的规则写,就不会
有太大问题,而且好分析,EDA TOOL 也好做出你要的东东,若是为突出自己能力有多牛,写出只有自己能分析的怪
东西,那恭喜你,EDA TOOL 会让你死的很难看,呵呵!
关于 nonblocking 和 blocking,本 PDF 文档后面还有一篇详细的文章,大家可以参考目录“Verilog 非阻塞赋值的仿真/
综合问题”

收集整理自开源 IP 通吃岛 /bbs
2、流水线设计
dancia:以下参考了 edacn 上 ytsun,ericflying,hover_edacn 等人的精彩发言,向这些热心的朋友致敬~~
第一, 什么是流水线
流水线设计就是将组合逻辑系统地分割,并在各个部分(分级)之间插入寄存器,并暂存中间数据的方法。目
的是将一个大 操作分解成若干的小操作,每一步小操作的时间较小,所以能提高频率,各小操作能并行执行,所以能
提高数据吞吐率(提高处理速度)。
第二,什么时候用流水线设计
使用流水线一般是时序比较紧张,对电路工作频率较高的时候。典型情况如下:
1)功能模块之间的流水线,用乒乓 buffer 来交互数据。代价是增加了 memory 的数量,但是和获得的巨大性能提升相
比,可以忽略不计。
2)I/O 瓶颈,比如某个运算需要输入 8 个数据,而 memroy 只能同时提供 2 个数

据,如果通过适当划分运算步骤,使用
流水线反而会减少面积。
3)片内 sram 的读操作,因为 sram 的读操作本身就是两极流水线,除非下一步操作依赖读结果,否则使用流水线是自
然而然的事情。
4)组合逻辑太长,比如(a+b)*c,那么在加法和乘法之间插入寄存器是比较稳妥的做法。
第三, 使用流水线的优缺点:
1)优点:流水线缩短了在一个时钟周期内给的那个信号必须通过的通路长度,增加了数据吞吐量,从而可以提高时钟
频率,但也导致了数据的延时。举例如下:
例如:一个 2 级组合逻辑,假定每级延迟相同为 Tpd,
1.无流水线的总延迟就是 2Tpd,可以在一个时钟周期完成,但是时钟周期受限制在 2Tpd;
2.流水线:
每一级加入寄存器(延迟为 Tco)后,单级的延迟为 Tpd+Tco,每级消耗一个时钟周期,流水线需要 2 个时钟周期
来获得第一个计算结果,称 为首次延迟,它要 2*(Tpd+Tco),但是执行重复操作时,只要一个时钟周期来获得最后的
计算结果,称为吞吐延迟(Tpd+Tco)。可见只要 Tco 小于 Tpd,流水线就可以提高速度。特别需要说明的是,流水线
并不减小单次操作的时间,减小的是整个数据的操作时间,请大家认真体会。
2) 缺点: 功耗增加,面积增加,硬件复杂度增加,特别对于复杂逻辑如 cpu 的流水线而言而言,流水越深,发生
需要 hold 流水线或 reset 流水线的情况时,时间损失越大。所以使用流水线并非有利无害,大家需权衡考虑。
第四,一个 8bit 流水线加法器的小例子。
非流水线:
module add8(a, b, c);
input [7:0] a;
input [7:0] b;
output [8:0] c;
assign c = {1'b0, a} + {1'b0, b};
endmodule
采用两级流水线:第一级低 4bit,第二级高 4bit,所以第一个输出需要 2 个时钟周期有效,后面的数据都是 1 个周期
之后有效。
module adder8_2(cout,sum ,clk ,cina ,cinb ,cin);
output [7:0] sum;
output cout ;
收集整理自开源 IP 通吃岛 /bbs
input [7:0] cina ,cinb ;
input clk ,cin ;
reg cout ;
reg cout1 ; ////插入的寄存器
reg[3 :0 ]sum1 ; //插入的寄存器
reg[7 :0 ]sum;
reg[3:0] cina_reg,cinb_reg;//插入的寄存器
always @(posedge clk)begin
{cina_reg,cinb_reg} <= {cina[7:4],cinb[7:4]};
end
always @(posedge clk) //第一级流水
begin
{cout1 , sum1} <= cina[3:0] + cinb [3:0] +cin ;
end
always @(posedge clk) //第二级流水
begin
{cout ,sum} <= {{1'b0,cina_reg} + {1'b0,cinb_reg} + cout1 ,sum1} ;
end
endmodule
这里讲到的流水线,主要是一种硬件设计的算法,如第一条中表述的流 水线设计就是将组合逻辑系统地分割,并在各
个部分(分级)之间插入寄存器,并暂存中间数据的方法。
本 PDF

后面提供资料针对处理器中的流水线结构。比如,比如 5—6 个不同功能的电路单元组成一条指令处理流水线,
然后将一条指令分成 5—6 步后再由这些电 路单元分别执行,这样就能实现在一个 CPU 时钟周期完成一条指令,因此
提高 CPU 的运算速度。一般的 CPU 中,每条整数流水线都分为四级流水,即指令预 取、译码、执行、写回结果,openrisc
采用的是 5 级整数流水线。
当然它们的核心思想都是利用并行执行提高效率。
总结一下,流水线就是插入寄存器,以面积换取速度。

收集整理自开源 IP 通吃岛 /bbs
十大基本功之 testbench
1. 激励的产生
对于 testbench 而言,端口应当和被测试的 module 一一对应。端口分为 input,output 和 inout 类型产
生激励信号的时 候,input 对应的端口应当申明为 reg, output 对应的端口申明为 wire,inout 端口
比较特殊,下面专门讲解。
1)直接赋值。
一般用 initial 块给信号赋初值,initial 块执行一次,always 或者 forever 表示由事件激发反复执行。
举例,一个 module
module exam();
reg rst_n;
reg clk;
reg data;
initial
begin
clk=1'b0;
rst=1'b1;
#10
rst=1'b0;
#500
rst=1'b1;
end
always
begin
#10
clk=~clk;
end
大家应该注意到有个#符号,该符号的意思是指延迟相应的时间单位。该时间单位由 timscale 决定.一般
在 testbench 的开头定义时间单位和仿真 精度,比如`timescale 1ns/1ps,前面一个是代表时间单位,
后面一个代表仿真时间精度。以上面的例子而言,一个时钟周期是 20 个单位,也就是 20ns。而仿真时
间精度的概 念就是,你能看到 1.001ns 时对应的信号值,而假如 timescale 1ns/1ns,1.001ns 时候的
值就无法看到。对于一个设计而言,时间刻度应该统一,如果设计文件和 testbench 里面的时间刻度不
一致,仿真 器默认以 testbench 为准。一个较好的办法是写一个 global.v 文件,然后用 include 的办
法,可以防止这个问题。
对于反复执行的操作,可写成 task,然后调用,比如
task load_count;
input [3:0] load_value;

收集整理自开源 IP 通吃岛 /bbs
begin
@(negedge clk_50);
$display($time, " << Loading the counter with %h >>", load_value);
load_l = 1’b0;
count_in = load_value;
@(negedge clk_50);
load_l = 1’b1;
end
endtask //of load_count
initial
begin
load_count(4’hA); // 调用 task
end
其他像 forever,for,function 等等语句用法类似,虽然不一定都能综合,但是用在 testbench 里面很方
便,大家可以自行查阅参考文档
2) 文件输入
有时候,需要大量的数据输入,直接赋值的话比较繁琐,可以先生成数据,再将数据读入到寄存器中


需要时取出即可。用 $readmemb 系统任务从文本文件中读取二进制向量(可以包含输入激励和输出期望
值)。$readmemh 用于读取十六进制文件。例如:
reg [7:0] mem[1:256] // a 8-bit, 256-word 定义存储器 mem
initial $readmemh ( "E:/readhex/mem.dat", mem ) // 将.dat 文件读入寄存器 mem 中
initial $readmemh ( "E:/readhex/mem.dat", mem, 128, 1 ) // 参数为寄存器加载数据的地址始终
文件调入和打印中,我们 $fread $fwrite 用的更多一些, 大家可以自行查阅参考文档
2. 查看仿真结果
对于简单的 module 来说,要在 modelsim 的仿真窗口里面看波形,就用 add wave ..命令
比如,testbench 的顶层 module 名叫 tb,要看时钟信号,就用 add wave tb.clk
要查看所有信号的时候,就用 add wave /*
当然,也可以在 workspace 下的 sim 窗口里面右键单击 instance 来添加波形
对于复杂的仿真,免不了要记录波形和数据到文件里面去。
1)波形文件记录
常见的波形文件一般有两种,vcd 和 fsdb,debussy 是个很好的工具,支持 fsdb,所以最好是 modelsim+debussy 的组
合默认情况下,modelsim 不认识 fsdb,所以需要先装 debussy,再生成 fsdb 文件。
$dumpfile 和$dumpvar 是 verilog 语言中的两个系统任务,可以调用这两个系统任务来创建和将指定信息导入 VCD 文件.
对于 fsdb 文件来说,对应的命令是 fsdbDumpfile,dumpfsdbvars
(什么是 VCD 文件? 答:VCD 文件是在对设计进行的仿真过程中,记录各种信号取值变化情况的信息记录文件。EDA
工具通过读取 VCD 格式的文件,显示图形化的仿真波形,所以,可以把 VCD 文件简单地视为波形记录文件.)下面分别
描述它们的用法并举例说明之。
收集整理自开源 IP 通吃岛 /bbs
$dumpfile 系统任务:为所要创建的 VCD 文件指定文件名。
举例("//"符号后的内容为注释文字):
initial
$dumpfile ("myfile.dump"); //指定 VCD 文件的名字为 myfile.dump,仿真信息将记录到此文件
$dumpvar 系统任务:指定需要记录到 VCD 文件中的信号,可以指定某一模块层次上的所有信号,也可以单独指定某一
个信号。
典型语法为$dumpvar(level, module_name); 参数 level 为一个整数,用于指定层次数,参数 module 则指定要记录的模块。
整句的意思就是,对于指定的模块,包括其下各个层次(层次数由 level 指定)的信号,都需要记录到 VCD 文件中去。
举例:
initial
$dumpvar (0, top); //指定层次数为 0,则 top 模块及其下面各层次的所有信号将被记录
initial
$dumpvar (1, top); //记录模块实例 top 以下一层的信号
//层次数为 1,即记录 top 模块这一层次的信号
//对于 top 模块中调用的更深层次的模块实例,则不记录其信号

变化
initial
$dumpvar (2, top); //记录模块实例 top 以下两层的信号
//即 top 模块及其下一层的信号将被记录
假设模块 top 中包含有子模块 module1,而我们希望记录 top.module1 模块以下两层的信号,则语法举例如下:
initial
$dumpvar (2, top.module1); //模块实例 top.module1 及其下一层的信号将被记录
假设模块 top 包含信号 signal1 和 signal2(注意是变量而不是子模块), 如我们希望只记录这两个信号,则语法举例如下:
initial
$dumpvar (0, top.signal1, top.signal2); //虽然指定了层次数,但层次数是不影响单独指定的信号的
//即指定层次数和单独指定的信号无关
我们甚至可以在同一个$dumpvar 的调用中,同时指定某些层次上的所有信号和某个单独的信号,假设模块 top 包含信
号 signal1,同时包含有子模 块module1,如果我们不但希望记录 signal1 这个独立的信号,而且还希望记录子模块module1
以下三层的所有信号,则语法举例如下:
initial
$dumpvar (3, top.signal1, top.module1); //指定层次数和单独指定的信号无关
//所以层次数 3 只作用于模块 top.module1, 而与信号
top.signal1 无关
上面这个例子和下面的语句是等效的:
initial
begin
$dumpvar (0, top.signal1);
$dumpvar (3, top.module1);
end
$dumpvar 的特别用法(不带任何参数):
initial
$dumpvar; //无参数,表示设计中的所有信号都将被记录

收集整理自开源 IP 通吃岛 /bbs
最后,我们将$dumpfile 和$dumpvar 这两个系统任务的使用方法在下面的例子中综合说明,假设我们有一个设计实例,
名为 i_design,此设计中包含模块 module1,模块 module1 下面还有很多层次,我们希望对这个设计进行仿真,并将仿
真过程中模块 module1 及其以下所有层次中所有信号的变化情况,记录存储到名为 mydesign.dump 的 VCD 文件中去,
则例示如下:
initial
begin
$dumpfile ("mydesign.dump"); //指定 VCD 文件名为 mydesign.dump
$dumpvar (0, i_design.module1); //记录 i_design.module1 模块及其下面层次中所有模块的所有信号
end
对于生成 fsdb 文件而言,也是类似的
initial
begin
$fsdbDumpfile("tb_xxx.fsdb");
$fsdbDumpvars(0,tb_xxx);
end
2)文件输出结果
integer out_file; // out_file 是一个文件描述,需要定义为 integer 类型
out_file = $fopen ( " cpu.data " ); // cpu.data 是需要打开的文件,也就是最终的输出文本
设计中的信号值可以通过$fmonitor, $fdisplay,$fwrite
其中$fmonitor 只要有变化就一直记录,$fdisplay 和$fwrite 需要触发条件才记录
例子:
initial begin
$fmonitor(file_id, "%m: %t in1=%d o1=%h", $time, in1, o1);
end
always@(a or b)
begin
$fwrite(file_id,"At time%t a=%b b=%b",$realtime,a,b);
end
关于产生 FS

DB 波形文件技巧,有更精彩的讨论:
/bbs/viewthread.php?tid=2539&extra=page%3D1
3 testbench 的技巧
1).如果激励中有一些重复的项目,可以考虑将这些语句编写成一个 task,这样会给书写和仿真带来很大方便。例如,
一个存储器的 testbench 的激励可以包含 write,read 等 task。
2).如果 DUT 中包含双向信号(inout),在编写 testbench 时要注意。需要一个 reg 变量来表示其输入,还需要一个 wire
变量表示其输出。
3).如果 initial 块语句过于复杂,可以考虑将其分为互补相干的几个部分,用数个 initial 块来描述。在仿真时,这些
initial 块会并发运行。这样方便阅读和修改。
4).每个 testbench 都最好包含$stop 语句,用以指明仿真何时结束。
5).加载测试向量时,避免在时钟的上下沿变化,比如数据最好在时钟上升沿之前变化,这也符合建立时间的要求。
4.一个简单的例子
收集整理自开源 IP 通吃岛 /bbs
module counter (clk, reset, enable, count);
input clk, reset, enable;
output [3:0] count;
reg [3:0] count;
always @ (posedge clk)
if (reset == 1'b1) begin
count <= 0;
end else if ( enable == 1'b1) begin
count <= count + 1;
end
endmodule
testbench:
module counter_tb;
reg clk, reset, enable;
wire [3:0] count;
counter U0 (
.clk (clk),
.reset (reset),
.enable (enable),
.count (count)
);
initial begin
clk = 0;
reset = 0;
enable = 0;
end
always
#5 clk = ! clk;
initial begin
$dumpfile ("counter.vcd");
$dumpvars;
end
initial begin
$display("\t\ttime,\tclk,\treset,\tenable,\tcount");
收集整理自开源 IP 通吃岛 /bbs
$monitor("‰d,\t‰b,\t‰b,\t‰b,\t‰d",$time, clk,reset,enable,count);
end
initial
#100 $finish;
//Rest of testbench code after this line
endmodule
5 双向端口
这个没用过,从网上找的,如果有问题,大家再讨论吧
芯片外部引脚很多都使用 inout 类型的,为的是节省管腿。一般信号线用做总线等双向数据传输的时候就要用到 INOUT
类型了。就是一个端口同时做输入和 输出。 inout 在具体实现上一般用三态门来实现。三态门的第三个状态就是高阻
'Z'。当 inout 端口不输出时,将三态门置高阻。这样信号就不会因为两端同时 输出而出错了,更详细的内容可以搜索一
下三态门 tri-state 的资料.
1 使用 inout 类型数据,可以用如下写法:
inout data_inout;
input data_in;
reg data_reg;//data_inout 的映象寄存器
reg link_data;
assign data_inout=link_data?data_reg:1’bz;//link_data 控制三态门
//对于 data_reg,可以通过组合逻辑或者时序逻辑根据 data_in 对其赋值.通过控制 link_data 的高低电平,从而设置 data_inout
是输出数据还是处于高阻态

,如果处于高阻态,则此时当作输入端口使用.link_data 可以通过相关电路来控制.
2 编写测试模块时,对于 inout 类型的端口,需要定义成 wire 类型变量,而其它输入端口都定义成 reg 类型,这两者是有区别
的.
当上面例子中的 data_inout 用作输入时,需要赋值给 data_inout,其余情况可以断开.此时可以用 assign 语句实现:assign
data_inout=link?data_in_t:1’bz;其中的 link ,data_in_t 是 reg 类型变量,在测试模块中赋值.
另外,可以设置一个输出端口观察 data_inout 用作输出的情况:
Wire data_out;
Assign data_out_t=(!link)?data_inout:1’bz;
else,in RTL
inout use in top module(PAD)
dont use inout(tri) in sub module
也就是说,在内部模块最好不要出现 inout,如果确实需要,那么用两个 port 实现,到顶层的时候再用三态实现。理由
是:在非顶层模块用双向口的话,该 双向口必然有它的上层跟它相连。既然是双向口,则上层至少有一个输入口和一
个输出口联到该双向口上,则发生两个内部输出单元连接到一起的情况出现,这样在 综合时往往会出错。
对双向口,我们可以将其理解为 2 个分量:一个输入分量,一个输出分量。另外还需要一个控制信号控制输出分量何
时输出。此时,我们就可以很容易地对双向端口建模。
例子:
收集整理自开源 IP 通吃岛 /bbs
CODE:
module dual_port (
....
inout_pin,
....
);
inout inout_pin;
wire inout_pin;
wire input_of_inout;
wire output_of_inout;
wire out_en;
assign input_of_inout = inout_pin;
assign inout_pin = out_en ? output_of_inout : 高阻;
endmodule
可见,此时 input_of_inout 和 output_of_inout 就可以当作普通信号使用了。
在仿真的时候,需要注意双向口的处理。如果是直接与另外一个模块的双向口连接,那么只要保证一个模块在输出的
时候,另外一个模块没有输出(处于高阻态)就可以了。
如果是在 ModelSim 中作为单独的模块仿真,那么在模块输出的时候,不能使用 force 命令将其设为高阻态,而是使用
release 命令将总线释放掉
很多初学者在写 testbench 进行仿真和验证的时候,被 inout 双向口难住了。仿真器老是提示错误不能进行。下面是我个
人对 inout 端口写 testbench 仿真的一些总结,并举例进行说明。在这里先要说明一下 inout 口在 testbench 中要定义为
wire 型变量。
先假设有一源代码为:
module xx(data_inout , ........);
inout data_inout;
........................
assign data_inout=(! link)?datareg:1'bz;
endmodule
方法一:使用相反控制信号 inout 口,等于两个模块之间用 inout 双向口互连。这种方法要注意 assign 语句只能放在 initial
和 always 块内。

收集整理自开源 IP

通吃岛 /bbs
module test();
wire data_inout;
reg data_reg;
reg link;
initial begin
..........
end
assign data_inout=link?data_reg:1'bz;
endmodule
方法二:使用 force 和 release 语句,但这种方法不能准确反映双向端口的信号变化,但这种方法可以反在块内。
module test();
wire data_inout;
reg data_reg;
reg link;
#xx; //延时
force data_inout=1'bx; //强制作为输入端口
...............
#xx;
release data_inout; //释放输入端口
endmodule
很多读者反映仿真双向端口的时候遇到困难,这里介绍一下双向端口的仿真方法。一个典型的双向端口如图所示。
典型的双向端口可以用 Verilog 语言描述如下:
module bidirection_io(inner_port,out_en,outer_port);
input out_en;
inout[7:0] inner_port;
inout[7:0] outer_port;
assign outer_port=(out_en==1)?inner_port:8'hzz;
assign inner_port=(out_en==0)?outer_port:8'hzz;
endmodule
收集整理自开源 IP 通吃岛 /bbs
用 VHDL 语言描述双向端口如下:
library ieee;
use IEEE.STD_LOGIC_1164.ALL;
entity bidirection_io is
port ( inner_port : inout std_logic_vector(7 downto 0);
out_en : in std_logic;
outer_port : inout std_logic_vector(7 downto 0) );
end bidirection_io;
architecture behavioral of bidirection_io is
begin
outer_port<=inner_port when out_en='1' else (OTHERS=>'Z');
inner_port<=outer_port when out_en='0' else (OTHERS=>'Z');
end behavioral;
仿真时需要验证双向端口能正确输出数据,以及正确读入数据,因此需要驱动 out_en 端口,当 out_en 端口为 1 时,
testbench 驱动 inner_port 端口,然后检查 outer_port 端口输出的数据是否正确;当 out_en 端口为 0 时,testbench 驱动
outer_port 端口,然后检查 inner_port 端口读入的数据是否正确。由于 inner_port 和 outer_port 端口都是双向端口(在 VHDL
和 Verilog 语言中都用 inout 定义),因此驱动方法与单向端口有所不同。
用 Verilog 代码编写的 testbench 如下,其中使用了自动结果比较,随机化激励产生等技术。
`timescale 1ns/10ps
module tb();
reg[7:0] inner_port_tb_reg;
wire[7:0] inner_port_tb_wire;
reg[7:0] outer_port_tb_reg;
wire[7:0] outer_port_tb_wire;
reg out_en_tb;
integer i;
initial
begin
out_en_tb=0;
inner_port_tb_reg=0;
outer_port_tb_reg=0;
i=0;
repeat(20)
begin
#50
i=$random;
out_en_tb=i[0]; //randomize out_en_tb
inner_port_tb_reg=$random; //randomize data
outer_port_tb_reg=$random;
收集整理自开源 IP 通吃岛 /bbs
end
end
//**** drive the ports connecting to bidirction_io
assign inner_port_tb_wire=(out_en_tb==1)?inner_port_tb_reg:8'hzz;
assign outer_port_tb_wire=(out_en_tb==0)?outer_port_tb_reg:8'hzz;
//instatiate the bidirction_io module
bidirection_io bidirection_io_inst(.inner_port(inner_port_tb_w

ire),
.out_en(out_en_tb),
.outer_port(outer_port_tb_wire));
//***** monitor ******
always@(out_en_tb,inner_port_tb_wire,outer_port_tb_wire)
begin
#1;
if(outer_port_tb_wire===inner_port_tb_wire)
begin
$display("\n **** time=%t ****",$time);
$display("OK! out_en=%d",out_en_tb);
$display("OK! outer_port_tb_wire=%d,inner_port_tb_wire=%d",
outer_port_tb_wire,inner_port_tb_wire);
end
else
begin
$display("\n **** time=%t ****",$time);
$display("ERROR! out_en=%d",out_en_tb);
$display("ERROR! outer_port_tb_wire != inner_port_tb_wire" );
$display("ERROR! outer_port_tb_wire=%d, inner_port_tb_wire=%d",
outer_port_tb_wire,inner_port_tb_wire);
end
end
endmodule
6. 高级用法
比如 PLI 之类。有需要的,大家再讨论
总体感觉,testbench 是个很难的事情,这里讨论的只是一些最基本的东西。真正有技术含量的是 testcase 的设计,设 计
阶段合理层次设计以及模块划分等等,我没有做过很大的项目,所以这方面也没有办法提供更多的帮助。经验丰富的
大牛不妨出来讲讲经验,^_^

收集整理自开源 IP 通吃岛 /bbs
testbench 用的较多的东西是:
1、输入数据文件的产生,一般由 matlab 产生,这方面经常涉及浮点到定点的转换、进制的转换的问题;
2、输入数据文件的输出文件的设置;
3、VCD、fsdb 等和其他 eda 软件接口的文件的输出;
4、一定范围内的随机数的产生
5、双向端口的仿真
6、与上层 dsp 等 cpu 接口的时序仿真。
....
panwest:testbench 设计的掌握的技术可多可少,模块级的 HDL 就可以搞定了:
激励产生:高级语言实现(C, C++, Java 等)
DUT:HDL 实现
参考单元:高级语言实现(C, C++, Java 等)
初始化:脚本语言(perl,Tcl/TK)
波形输出:一般脚本自动对比了
数据通过 PLI 进行联系
日后 systemVerilog,e 等语言完善后会减轻一些建立平台的工作量。
synopsys vera support verification.
bench : vera produce
DUT:
ref module: vera
monitor: vera
coverage analysis : VCS or vera
vera can load c, C++ file .
systemverilog 应该是以后的趋势
一些 random scenario 与 tlm base model 都可以用
systemverilog 完成
而一各好的 framework 很重要
现在有两各 framework 可以选择
vmm, ovm
不过要用上述的 framework 需要很大的 oop 基础
要花很长时间学习
关于 systemC 或者 system verilog,大家可以查找相关文档。

Verilog 非阻塞赋值的仿真/综合问题

源文件作者:Clifford E. Cummings (Sunburst Design, Inc.)

原标题:Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!

ATTN:所有括号内“外注”为理解方便或有疑问的地方,原文里并没有。所有翻译都是为
遵循“共同进步”的理想但并没有去努力得到

原作者的任何书面和其它方式许可,
所以仅供大家参考。本人英文和设计水平都极有限,所以不能保证与原文的精确一
致和正确(只能以“驽马十步”稍作安慰吧),惭愧之后还希望大家多指教!



在 Verilog 语言最难弄明白的结构中“非阻塞赋值”要算一个。甚至是一些很有经验的工
程师也不完全明白“非阻塞赋值”在仿真器(符合 IEEE 标准的)里是怎样被设定执行的,以及
什么时候该用“非阻塞赋值”。这篇文章将介绍怎样设定“非阻塞赋值”和“阻塞赋值”,给出
了重要的使得编码可以被正确地综合的编码指导方针,和避免仿真竞争的编码风格细节。



1.0 介绍

众所周知的逻辑建模方针是:

* 在 always 块里用“阻塞赋值=”产生组合逻辑。

* 在 always 块里用“非阻塞赋值<=”产生时序逻辑。

但是为什么?(外注:在实现组合逻辑的 assign 结构中,当然采用阻塞赋值语句否则的话
编译工具会提醒你进行修改的。)

普通的回答是:那只是关于仿真的,即使不遵照上面的规则也照样可以产生正确的综合结
果。但问题是综合前的仿真结果也许会跟综合后的电路行为仿真不匹配。



要明白上述建模方针背后的原因,就必须明白“非阻塞赋值”和“阻塞赋值”它们的功能
和时序安排(the functionality and scheduling of blocking and nonblocking assignments.)。这篇
文章将详细描述有关问题。文章里将用到两个缩写形式:RHS(right-hand-side)和LHS
(left-hand-side)。前者指等式右边的表达式或者变量(RHS expression or RHS variable),
后者指指等式左边的表达式或者变量(RHS expression or RHS variable)。



2.0 Verilog 仿真竞争条件

IEEE Verilog Standard [2] 定义: “保证性的赋值描述”和“非保证性的赋值”描述
分别用“非阻塞赋值”和“阻塞赋值”。("Determinism", section 5.4.1;"Nondeterminism", section
5.4.2 & "Race conditions", section 5.5)

IEEE Verilog 标准允许在同一仿真时间里赋值竞争的产生。当赋值陈述有所不同时,会产
生不同的结果。(译注:即可以认为:“非阻塞赋值”有更高的优先权对变量进行赋值或者是指
陈述的次序不同会产生不同结果?)

为了避免含竞争的描述(race condition),明白Verilog“非阻塞赋值”和“阻塞赋值”
的时序安排是非常重要的。



3.0 阻塞赋值(blocking assignments)

阻塞赋值由等号“=”表示。“阻塞赋值”由它的赋值操作行为而得名:当没有其它的Verilog
描述可以打断“阻塞赋值”时,操作将会估计RHS的值并完成赋值。“阻塞”即是说在当前的赋
值完成前阻塞其它类型的赋值任务

。一个例外是:对阻塞操作的RHS进行延时(delays)的阻塞
赋值(在延时未完成前不会阻塞其它赋值任务),但是这被我们认为是不好的编码方式。




“阻塞赋值“可以看作一步进程(one-step process):

当没有其它可以打断赋值的描述时,估计等式右边(RHS)的指并赋予左边(LHS)。

在同一个always块里面,阻塞赋值结果将一直持续下去直到赋值结束。



阻塞赋值的一个问题是:当一个程序块(比如always块) 阻塞赋值描述里面的 “RHS变量” 同
时是另外一个程序块(比如always块) 阻塞赋值描述里面的 “LHS变量”,并且两个等式的执
行被安排在同一个仿真时间步里面执行(比如同一个时钟上升沿),那么竞争条件就产生了,这
样的情况下其执行次序将是未知的。

为了举例说明这种情况,请看Verilog代码描述的例一:



module fbosc1 (y1, y2, clk, rst);

output y1, y2;

input clk, rst;

reg y1, y2;

always @(posedge clk or posedge rst)

if (rst) y1 = 0; // reset

else y1 = y2;

always @(posedge clk or posedge rst)

if (rst) y2 = 1; // preset

else y2 = y1;

endmodule

Example 1 - Feedback oscillator with blocking assignments



依据IEEE Verilog标准,这两个always块可以以任意的次序执行。如果在reset
后第一个块先被执行,结果将是y1和y2都获得赋值1;如果在reset后第二个块先被执行,结果将
是y1和y2都被赋值0。这个例子清楚地展示了一个Verilog竞争条件地产生。



4.0 非阻塞赋值(nonblocking assignments)

非阻塞赋值使用一个小于等于号“<=”。“非阻塞赋值”由它的赋值操作行为而
得名:在一个时间步(time step)的开始估计RHS expression的值并在这个时间步(time step)
结束时用等式右边的值更新取代LHS。在估算RHS expression和更新LHS expression的中间时间
段,其它的对LHS expression的非阻塞赋值可以被执行。即是说“非阻塞赋值”从估计RHS开始
并不阻碍执行其它的Verilog描述。



“非阻塞赋值”可以看作二步进程(one-step process):

1. 在时间步开始估计RHS;

2. 在时间步结束时更新LHS;



“非阻塞赋值”为寄存器数据类型而设,所以只能被允许在程序块里面出现,比如initial块和
always块。不允许持续性赋值(continuous assignments)。



为了举例说明,请看Verilog 编码例二:




module fbosc2 (y1, y2, clk, rst);

output y1, y2;

input clk, rst;

reg y1, y2;

always @(posedge clk or posedge rst)

if (rst) y1 <= 0; // reset

else y1 <= y2;

always @(posedge clk or posedge rst)

if (rst) y2 <= 1; // preset

else y2 <= y1;

endmodule

Example 2 - Feedback oscillator with nonblocking ass

ignments



依据IEEE Verilog标准,这两个块可以以任意的次序执行。在reset后,不管哪
一个块先被执行,在时间步的开始两个RHS expression同时被估值,在时间步结束LHS variables

同时更新赋值。在使用者看来,这两个非阻塞描述是并行发生的。



5.0 Verilog 编码指导仿真



在对“非阻塞赋值”和“阻塞赋值”作更深一步的举例和说明之前,现列举八
条指导方针是有帮助的。这些仿真可以帮助正确地用Verilog对硬件建模和仿真。谨遵这些方针
可以帮助Verilog设计者减少所遇到的90-100%的Verilog竞争。



#1: 当为时序逻辑建模,使用“非阻塞赋值”。

#2: 当为锁存器(latch)建模,使用“非阻塞赋值”。

#3: 当用always块为组合逻辑建模,使用“阻塞赋值”

#4: 当在同一个always块里面既为组合逻辑又为时序逻辑建模,使用“非阻
塞赋值”。

#5: 不要在同一个always块里面混合使用“阻塞赋值”和“非阻塞赋值”。

#6: 不要在两个或两个以上always块里面对同一个变量进行赋值。

#7: 使用$strobe以显示已被“非阻塞赋值”的值。

#8: 不要使用#0延迟的赋值。



关于这些指导方针的来源,这篇文章的余下部分将会给出。Verilog的新手们一定要记住并
使用这些方针直到完全弄明白了它们根本的功能。遵循这些方针将会帮助你避免“Verilog痛苦”
(“death by Verilog!”)。



6.0 层积事件列 ("stratified event queue")

仔细地考察一下Verilog的层积事件列(stratified event queue,见表一)可以帮助解释
Verilog的层积事件列是如何发挥作用的。对于用于安排仿真事件顺序的不同Verilog事件列,
“层积事件列”是一个迷人的和有想象力的名字。在IEEE Verilog标准里被描述成一种概念上
的模范------用于鉴定各个供应商的仿真器能力,尽管它们各自对事件列的执行细节是它们各
自所独有的。这些细节问题不是本章所要讨论的。

(外注:首先一个“事件轴”可以用来理解仿真事件:


当前时刻 t1 t2 t3












)。



在IEEE 1364-1995 Verilog标准的5.3节,“层积事件列”被划分为四个迥然不
同的列,它们分别归为当前仿真时间列和将来仿真时间列。




“激活事件列”(Active Events)是最多的被预备执行的Verilog事件,包括非阻塞
赋值、连续赋值、$display命令、利用对实例(instance)和初原元件(primitive,可能
这样翻不合适,但是知道Verilog的家伙都知道这个,就先将就着了!)更新的输出值估出
实例(instance)和初原元件(primitive)的输入值、估出“非阻塞赋值”的RHS expres

sions。
注意“非阻塞赋值”的LHS不在“激活事件列”里更新值。



事件可以被加到任意的事件列里(由IEEE标准强制约束的)但是只可能从“激活事件
列”里被移出。其它事件列里的事件最终总是要成为“激活事件”的(或者提升为“激活事
件)。IEEE1364-1995 Verilog标准第5.4节列出了当其它事件列(event queues)被激活后
的算法。



在当前仿真时间里,两个常见的事件列(event queues)是“非阻塞赋值更新”事件
列和“monitor”事件列。具体描述如下:



“非阻塞赋值更新”事件列(The nonblocking assign updates event queue)即是“非阻
塞赋值”的LHS expression被安排更新赋值 的那些事件。在一个仿真时间步(simulation time
step)的开始,“RHS expression 的估值”与其它被激活事件 是以任意的次序进行的。

文本框: 事件2
文本框: 事件5
文本框: 事件1
文本框: 事件0
文本框: 事件4
文本框: 事件3



“monitor”事件列 是由那些被安排的“$strobe”和“$monitor”显示命令带来的。$strobe
和 $monitor 用于显示一个仿真时间步结束时变量更新后的值(这时该仿真时间步里所有的赋值
分配都已经完成)。



IEEE1364-1995 Verilog标准第5.3节描述了“怠惰事件列”,即被赋为零延迟(#0)的事
件。实际上“零延迟”是有缺陷的。一般设计者使用零延迟是想为在不同程序块(procedural
blocks)被赋值的变量提供一个避免仿真竞争的环境。设计者希望一个赋值语句在另一个之后“一
点儿”替代前面赋值。这是没必要的,只会增加(仿真器)分析事件列的难度。作者不知道有哪
一种情况下必须要使用零延迟以至于不用零延时就不能用别的不同的、更有效的编码风格来达到
所想的目的。所以不推荐使用零延迟。



建模方针8:不要使用零延迟。



上面图一的“层积事件列”将经常参考以用来解释下面的Verilog行为描述例子。
“事件列”也将作为证明5.0节列举的八条建模方针的参考。



7.0 自触发always块

一般来讲,一个always块不能够自触发。考虑下面例三的振荡器:

module osc1 (clk);

output clk;

reg clk;

initial #10 clk = 0;

always @(clk) #10 clk = ~clk;

endmodule

Example 3 - Non-self-triggering oscillator using blocking assignments



这个振荡器使用“阻塞赋值”,这样的话RHS估值和LHS赋值是不被打断地执行。在clk边沿触发
能被安排执行之前,非阻塞赋值就已经必须安排执行。即在边沿事件之前,对clk的赋值已经完
成。所以, 没有“触发事件”(@(clk))来触发always块里面的触发事件(to trigger the @(clk)
trigger)。



与之形成

对比的是,例4的振荡器使用“非阻塞赋值”:



module osc2 (clk);

output clk;

reg clk;

initial #10 clk = 0;

always @(clk) #10 clk <= ~clk;

endmodule

Example 4 - Self-triggering oscillator using nonblocking assignments



在第一个@(clk)触发之后,非阻塞赋值的RHS expression被估值,并且LHS值
被送入“非阻塞赋值更新”事件列。在“非阻塞赋值更新事件列”被激活以前,仿真过程遇到@clk


触发描述,所以always块又一次对clk信号变化敏感------然后在同一时间步的结束当LHS被更新
时,@clk被又一次触发。所以osc2是可以自触发的(尽管不是我们有必要推荐的风格)。

x


clk==0


RHS估值


time step

RHS估值 10,LHS更新,clk==1

@(clk) 发生clk事件!等待、开始RHS估值


#10 估值完毕,clk_reg==1

@(clk),等待、开始RHS估值

(外注:这个另外添加的仿真波形可以帮助理解,原文里并没有。其中clk_reg表示寄存
在内存的clk值。)



8.0 流水线建模

图二示意了一个简单的时序(sequential)流水线寄存器。



从例5到例8列举了一个工程师可能选用的4种使用阻塞赋值为它建模的方案。



module pipeb1 (q3, d, clk);

output [7:0] q3;

input [7:0] d;

input clk;

reg [7:0] q3, q2, q1;

always @(posedge clk) begin

q1 = d;

q2 = q1;

q3 = q2;

end

endmodule

Example 5 - Bad blocking-assignment sequential coding style #1


(外注:综合报告:WARNING: Signal is assigned but never used.


WARNING:Signal is assigned but never used.)

在例5里面,接连的“阻塞赋值”命令将使得输入D连续地覆盖所有寄存器输出(在下一个
posedge clk到来时)。即在每一个clk边沿,输入值被无延迟地传到q3的输出。这很明显并没有
建立一个流水线而只是为一个寄存器建模------实际综合结果将是上面的图3。



module pipeb2 (q3, d, clk);

output [7:0] q3;

input [7:0] d;

input clk;

reg [7:0] q3, q2, q1;

always @(posedge clk) begin

q3 = q2;

q2 = q1;

q1 = d;

end

endmodule

Example 6 - Bad blocking-assignment sequential coding style #2 - but it works!

上面的pipeb2里面,阻塞赋值被仔细地安排了次序以使得行为仿真正确。这种
建模同样也可以得到正确的综合结果。(外注:Found 3-bit shift register for signal .

Summary: inferred 8 Shift register(s). )



在下面的例3里,“阻塞赋值”被安排在不同的always块里面。这样Verilog标
准允许以任意的次序 来仿真执行3个always块-------这也许会使得该流水线仿真结果产生错
误,因为这产生了Verilog竞争条件。由不同的always块执行顺序会产生不同的结果。尽管这样,
它的综合结果将是

正确的! 这就意味着综合前仿真和综合后仿真不匹配。Pipeb4或者其它的
类似always块同样也许会产生仿真与综合不匹配的结果------综合结果是对的,但是仿真结果也
许不正确。(外注:pipeb4只是又颠倒了一下次序,对实际仿真次序却不产生决定作用.)



module pipeb3 (q3, d, clk);

output [7:0] q3;

input [7:0] d;

input clk;

reg [7:0] q3, q2, q1;

always @(posedge clk) q1=d;

always @(posedge clk) q2=q1;

always @(posedge clk) q3=q2;

endmodule

Example 7 - Bad blocking-assignment sequential coding style #3



module pipeb4 (q3, d, clk);

output [7:0] q3;

input [7:0] d;

input clk;

reg [7:0] q3, q2, q1;

云形标注: 注意次序的更改!

always @(posedge clk) q2=q1;

always @(posedge clk) q3=q2;

always @(posedge clk) q1=d;

endmodule

Example 8 - Bad blocking-assignment sequential coding style #4



假如每一个上面的例子都改用“非阻塞赋值”那么将会都能得到正确的仿真结果,并综合
出想要的流水线逻辑。



module pipen1 (q3, d, clk);

output [7:0] q3;

input [7:0] d;

input clk;

reg [7:0] q3, q2, q1;

always @(posedge clk) begin

q1 <= d;

q2 <= q1;

q3 <= q2;

end

endmodule

Example 9 - Good nonblocking-assignment sequential coding style #1



module pipen2 (q3, d, clk);

output [7:0] q3;

input [7:0] d;

input clk;

reg [7:0] q3, q2, q1;

always @(posedge clk) begin

q3 <= q2;

q2 <= q1;

q1 <= d;

end

endmodule

Example 10 - Good nonblocking-assignment sequential coding style #2



module pipen3 (q3, d, clk);

output [7:0] q3;

input [7:0] d;

input clk;

reg [7:0] q3, q2, q1;

always @(posedge clk) q1<=d;

always @(posedge clk) q2<=q1;

always @(posedge clk) q3<=q2;

endmodule


Example 11 - Good nonblocking-assignment sequential coding style #3



module pipen4 (q3, d, clk);

output [7:0] q3;

input [7:0] d;

input clk;

reg [7:0] q3, q2, q1;

always @(posedge clk) q2<=q1;

always @(posedge clk) q3<=q2;

always @(posedge clk) q1<=d;

endmodule

Example 12 - Good nonblocking-assignment sequential coding style #4



从上面的流水线编码风格例子可以看出:

仅一个“阻塞赋值”的描述可以保证仿真正确。

三个“阻塞赋值”的描述可以得到正确综合结果。

四个“非阻塞赋值”描述都可以保证仿真结果正确。

四个“非阻塞赋值”描述都可以得到正确综合结果。(原文这一条是“阻
塞赋值”大概是有误?)

虽然,如果限制在一个always块里面,并小心地组织好一个always块里面阻塞
赋值的次序(外注:一个always块里面的几个“阻塞赋值”是按照陈述的次序串行仿真执行的,
综合执行次序也是?)同样可能

会正确地为流水线建模;但是另一方面,我们可以很容易地使用
“非阻塞赋值”来为上面的流水线建模------它们既可以正确仿真也可以正确综合。



9.0 阻塞赋值 & 简单例子



有许多将Verilog和Verilog综合的书,它们举了很多成功地利用“阻塞赋值”
为一些简单的时序电路建模的小例子。例13是一个在大多数Verilog书本里用来为一个触发器
(flip-flop)建模的例子(这是简单而有缺陷的阻塞赋值建模,但是它确实可以工作):



module dffb (q, d, clk, rst);

output q;

input d, clk, rst;

reg q;

always @(posedge clk)

if (rst) q = 1'b0;

else q = d;

endmodule

Example 13 - Simple flawed blocking-assignment D-flipflop model - but it works!



如果工程师们想把所有的模块(module)都集中到一个always里面描述,“阻
塞赋值”可以用来正确地为所需要的逻辑建模、仿真和综合。但是不幸的是这个原因导致了喜
欢在其它情况下也使用“阻塞赋值”的习惯,并且更复杂的时序always块将会产生竞争条件
------在前面已经详细阐述过。




module dffx (q, d, clk, rst);

output q;

input d, clk, rst;

reg q;

always @(posedge clk)

if (rst) q <= 1'b0;

else q <= d;

endmodule

Example 14 - Preferred D-flipflop coding style with nonblocking assignments



应该努力养成使用“非阻塞赋值”为 所有的 时序逻辑建模的习惯------象
上面的例14一样------即使是为了对付任何一个简单的模块。



下面考虑一下一个稍微复杂的时序逻辑,一个线性反馈移位寄存器(Linear
Feedback shift-Register)或称之为LFSR。



10.0 为时序反馈建模 (Sequential feedback modeling)

一个LFSR是一种带反馈环路(feedback loop)的时序逻辑。反馈环路(feedback
loop)为工程师们带来了一个难题使得他们试图使用细心组织次序的“阻塞赋值”来为它正确
建模,如下面的例子:





module lfsrb1 (q3, clk, pre_n);

output q3;

input clk, pre_n;

reg q3, q2, q1;

wire n1;

assign n1 = q1 ^ q3;

always @(posedge clk or negedge pre_n)

if (!pre_n) begin

q3 = 1'b1;

q2 = 1'b1;

q1 = 1'b1;

end

else begin

q3 = q2;

q2 = n1;

q1 = q3;

end

endmodule

Example 15 - Non-functional LFSR with blocking assignments(外注:综合报告―――>

Register equivalent to has been removed

Found 1-bit register for signal .


Found 1-bit xor2 for signal .

Found 1-bit register for signal .)

Summary:

inferred 2 D-type flip-flop(s).

没有办法通过调整描述次序的方法来正确建模除非引入一个临时的变量(

外注:例如引入“wire n2”――>

module xxxxx (q3, clk, pre_n);

output q3;



input clk, pre_n;

reg q3, q2, q1;

wire n1,n2;

assign n1 = q1 ^ q3;

assign n2 = q3;

always @(posedge clk or negedge pre_n)

if (!pre_n) begin

q3 = 1'b1;

q2 = 1'b1;

q1 = 1'b1;

end

else begin

q3 = q2;

q2 = n1;

q1 = n2;

end

endmodule



这样可以得到正确的综合结果:

Found 1-bit register for signal .

Found 1-bit xor2 for signal .

Found 1-bit register for signal .

Found 1-bit register for signal .

Summary:

inferred 3 D-type flip-flop(s).)。

可以通过把所有赋值弄到一个等式的方式(one-line equations)来避免使用临时变量,例如下面
的例16所示。但是现在编码显得更难于理解尤其当涉及的表达式更大更长时,编写代码和调试都
变得比较困难,因此不鼓励使用这种风格。



module lfsrb2 (q3, clk, pre_n);

output q3;

input clk, pre_n;

reg q3, q2, q1;

always @(posedge clk or negedge pre_n)

if (!pre_n) {q3,q2,q1} = 3'b111;

else {q3,q2,q1} = {q2,(q1^q3),q3};


endmodule

Example 16 - Functional but cryptic LFSR with blocking assignments



如果把例15和例16的阻塞赋值(blocking assignment)都替换为非阻塞赋值
(nonblocking assignment),如下面例17和18所示,那么所有的仿真都将如我们对一个LFSR所
期望的那样。

module lfsrn1 (q3, clk, pre_n);

output q3;

input clk, pre_n;

reg q3, q2, q1;

wire n1;

assign n1 = q1 ^ q3;

always @(posedge clk or negedge pre_n)

if (!pre_n) begin

q3 <= 1'b1;

q2 <= 1'b1;

q1 <= 1'b1;

end

else begin

q3 <= q2;

q2 <= n1;

q1 <= q3;

end

endmodule

Example 17 - Functional LFSR with nonblocking assignments



module lfsrn2 (q3, clk, pre_n);

output q3;

input clk, pre_n;

reg q3, q2, q1;

always @(posedge clk or negedge pre_n)

if (!pre_n) {q3,q2,q1} <= 3'b111;

else {q3,q2,q1} <= {q2,(q1^q3),q3};

endmodule

Example 18 - Functional but cryptic LFSR with nonblocking assignments



根据8.0段例子pipeline和10.0段例子LFSR,我们推荐对所有时序逻辑建模时
使用非阻塞赋值(nonblocking assignment)。相似的分析也将显示出对latch建模时使用非阻塞赋
值(nonblocking assignment)是最安全的。



#1: 当为时序逻辑建模,使用“非阻塞赋值”。

#2: 当为锁存器(latch)建模,使用“非阻塞赋值”。



11.0 组合逻辑―使用阻塞赋值(blocking assignment)




用Verilog可以有很多种方法为组合逻辑建模,但是当使用always块来为组合逻辑
建模时,应该使用阻塞赋值(blocking assignment)。



如果在某个always块里面只有一个赋值(表达),那么使用阻塞或者非阻塞赋值都
可以正确工作。但是如果您对养成好的编码习惯有兴趣的

相关文档
最新文档