北航计算机学院-计算机组成原理课程设计-2020秋
PreProject-Verilog HDL 与 ISE
Verilog语法
本系列所有博客,知识讲解、习题以及答案均由北航计算机学院计算机组成原理课程组创作,解析部分由笔者创作,如有侵权联系删除。
从本节开始,课程组给出的教程中增添了很多视频讲解。为了避免侵权,本系列博客将不会搬运课程组的视频讲解,而对于文字讲解也会相应地加以调整,重点在于根据笔者自己的理解给出习题的解析。因此带来的讲解不到位敬请见谅。
模块的定义方法
模块(module)是Verilog HDL的基本功能单元,它实际上代表了具有一定功能的电路实体。下面我们通过一个简单的与门实例来说明模块定义的基本语法:(Verilog HDL的注释方式与C语言相同)
方法1:(ISE默认生成的方式)
方法2:
两种方法没有实质上的区别,只是形式上有所不同:方法1对方法2中的端口定义及IO说明进行了合并。
从上面的例子可以看出,一个模块以module
开始,以endmodule
结束,包括模块名、端口定义、I/O说明、内部信号声明和功能定义等部分。需要指出的是,模块中的语句除了顺序块之外,都是“并行的”;输入输出端口若不特别说明类型及位宽,默认为1位wire型。(更多关于数据类型的介绍请参阅下一节的内容)
模块定义练习
我们现在来定义一个3输入8位或门。给出模块框架如下:
框架中的a,b,c和result分别对应相应的IO端口。那么,我们需要在每条注释处填入什么内容呢?
Hint:本题中的端口位宽统一使用[7:0];答题时,不允许出现多余的空格。
a处:答案:input [7:0] a,
b处:答案:input [7:0] b,
c处:答案:input [7:0] c
d处:答案:output [7:0] result
本题的模块是三输入8位与门,模块内的赋值告诉我们result信号是a、b、c三个信号相与的结果,因此a、b、c位8位输入,result为8位输出。注意题目提示本题端口位宽统一使用[7:0]。
常用数据类型
在本节中,我们将对Verilog HDL中常用的几种数据类型作一简要介绍。
wire型
wire型数据属于线网nets型数据,通常用于表示组合逻辑信号,可以将它类比为电路中的导线。它本身并不能存储数据,需要有输入才有输出(这里输入的专业术语叫驱动器),且输出随着输入的改变而即时改变。一般使用assign语句对wire型数据进行驱动(assign语句将在下一节中进行讲解)。
wire型的数据分为标量(1位)和向量(多位)两种。可以在声明过程中使用范围指示器指明位数,如wire [31:0] a;
。冒号两侧分别代表最高有效位(MSB, Most Significant Bit)和最低有效位(LSB, Least Significant Bit)。在访问时,可以使用形如a[7:4]
的方式取出a的第7-4位数据。
一般在使用wire型数据前应先声明它。但如果在模块实例的端口信号列表中使用了一个未声明的变量,则会将其默认定义为1位的wire变量。
需要注意的是,信号变量与C语言中的变量有所不同,不能像C语言一样随意赋值,一般需要按照组合逻辑的规则进行操作。比如,对于wire型变量a,assign a = a + 1
是不合法的。
wire数据类型练习
下列说法正确的有:
A. wire数据类型可以用于建模组合逻辑
B. wire数据类型具有存储功能
C. 在Verilog HDL中,未声明即使用的变量默认为1位wire型
D. 可以使用形如a[7,3]的方式获取wire型数据a的7-3位
答案:A、C
A项正确,wire型变量常用于建模组合逻辑。B项,wire型不具有存储功能,它类比于电路中的导线,只能将信号直接相连,无法存储。Verilog HDL中,未声明即使用的变量默认为1位wire型,C项正确(这一点很坑,如果你写错了一个变量的名字,Verilog并不会直接向你报错,而会将其默认为1位wire型)。对于D项,获取wire型的其中若干位使用的是形如a[7:3],冒号而非逗号。
reg型
reg型是寄存器数据类型,具有存储功能。它也分为标量和向量,类似wire型,可以类比前面的教程。一般在always块内使用reg型变量(always块将在本章后面提到),通过赋值语句来改变寄存器中的值。为了确定何时进行赋值,我们经常需要用到各种控制结构,包括while、for、switch等,这与C语言中的做法十分相似。
需要注意的是,reg型变量不能使用assign赋值。而且,reg型并不一定被综合成寄存器,它也可和always关键字配合(下一节会讲到),建模组合逻辑。
利用reg数据类型建模存储器
我们可以通过对reg型变量建立数组来对存储器建模,例如reg [31:0] mem [0:1023];
,其中前面的中括号内为位宽,后面的中括号内为存储器数量。这种写法在我们开始搭建CPU后会用到。
我们可以通过引用操作访问存储器型数据元素,类似于位选择操作,例如mem[2]
就是访问mem中的第3个元素。
需要注意的是,Verilog HDL中没有多维数组。
reg数据类型练习
下列说法正确的有:
A. reg型变量一定会被综合成寄存器
B. reg数据类型只可以在时序逻辑中出现,不可以用来建模组合逻辑
C. 可以根据需要,定义任意多维的reg型数组用来建模存储器
D. reg [23:16] mem [0:15]; 声明了一个8位16单元存储器
E. 可以使用reg型变量驱动wire型变量
答案:D、E
reg类型变量虽然名为reg,但综合时不一定是寄存器,还有可能被综合成RAM一类的存储器,A错误。reg类型变量也完全可以出现在组合逻辑中,它只是一类功能相对更多样的变量而已,B错误。Verilog中不能定义多维数组,C错误。reg [23:16] mem [0:15]; 声明了一个8位16单元存储器,注意[23:16]也是8位,这样的声明是正确的。可以使用reg型变量驱动wire型变量,正确,可以将wire型想象成导线,将reg型想象成存储器,E正确。
数字字面量
Verilog中的数字字面量可以按二进制(b或B)、八进制(o或O)、十六进制(h或H)、十进制(d或D)表示。 数字的完整表达为<位宽>’<进制><值>,如10’d100。省略位宽时采用默认位宽(与机器有关,一般为32位),省略进制时默认为十进制,值部分可以用下划线分开提高可读性,如16’b1010_1011_1111_1010。 Verilog中除了普通的数字以外,还有两个特殊的值:x和z。x为不定值,当某一二进制位的值不能确定时出现,变量的默认初始值为x。z为高阻态,代表没有连接到有效输入上。对于位宽大于1的数据类型,x与z均可只在部分位上出现。
数字字面量练习
下列对数字字面量的定义,正确的有:
A. 2b10
B. 3’101
C. 'h20
D. 32’d-100
E. 8’o100
F. 8’b_0011_1010
G. 4’b10x0
H. 4’b101z
I. -8’d5
答案:C、E、G、H、I
A选项缺少了位宽与数之间的 ’ 。
进制缺省时默认为10进制,但B项位宽为3,十进制数上限为7,超过位宽上限表示是不合法的,B错误。
C项正确,位宽缺省时默认为32位。
D项中,在数字本身之中表示负号是不合法的,D错误。
E项表示8位八进制100,该数本身占7位,没有超过位宽上限,正确。
数字中间可以用短横 _ 隔开,Verilog语法规定短横只能出现在数字之间,F错误。但是!这样的书写在实际编译中是不会报错的,我认为此处考查很不严谨。
G和H均符合规范,特殊值x和z可以出现在多位数字中的某几位。
I也属于正确的,不过我认为8’d5的表述正确,但Verilog数字本身并不能添加负号,这一表述可以看作对数字的运算,它是合法的。
此题命制过于刁钻,在此必须要批评课程组。Verilog本身就不是一个完美的语言,甚至说不上是精美的,但它是目前多数人使用的硬件描述语言,并且其功能是强大的。我认为我们学习的重点是通过语言去构建CPU,而不是在语言本身不完美的地方做这样的考查。
integer型
integer数据类型一般为32位,与C语言中的int类似,默认为有符号数,在我们的实验中主要用于for循环(将在本章后面提到)。
parameter型
parameter类型用于在编译时确认值的常量,通过形如parameter 标识符 = 表达式;
的语句进行定义,如:parameter width = 8;
。在实例化模块时,可通过参数传递改变在被引用模块实例中已定义的参数(模块的实例化将在后面的章节进行介绍)。parameter虽然看起来可变,但它属于常量,在编译时会有一个确定的值。
parameter可以用于搭建数据位宽可变的模块等。
parameter数据类型练习
判断:在仿真时,parameter的值就不可以被改变了。
答案:正确。
在仿真时,模块已经被编译好,parameter型所对应的值已经在编译时确定,因此已经不可改变了。举个例子,可以理解为,在Logisim中MUX元件的位宽和输入管脚数都是可变的,但是我们具体搭建的模块所使用的实例化的MUX,其位宽和管脚数都是确定的。
几个实例
wire[7:0] w1;
// [7:0]为该变量的位宽,代表该变量为8位,可以通过索引访问任意一位。
wire[32:1] w2, w3;
// 位宽可以不从0开始,此时访问某一位时需与声明相符,如w2的最高位为w2[32]。
reg[31:0] r1, r2, mem[1023:0];
//可以同时声明存储器和单个reg,存储器的地址索引同样可以不从0开始。
2'b11; //2位,对应十进制3
32'd12; //32位,对应十进制12
32'h11; //32位,对应十进制17
32'o11; //32位,对应十进制9
4'b10x0; //从低位数第2位为不定值
另外需要注意,assign、always等关键字不可以用作变量名。
接线
假设有一个已经定义好的模块,输入输出定义如下:
如果输出的逻辑采用以下方式实现,那么在使用ISim进行仿真时,提示接线错误的代码有哪些?
A. o1[3:0] = a[3:0];
B. o1[3:0] = b[7:4];
C. o1[3:0] = b[4:7];
D. o2[4:7] = c[8:11];
E. o2[4:7] = c[11:8];
答案:B、E
信号定义好之后,其不仅决定了位宽还决定了方向,例如定义为[4:7]的b信号,四个管脚分别为4,5,6,7,在使用中只能正向接,不能反向接,因此接b[4:7]是合法的,而b[7:4]是不合法的;同理接c[8:11]是合法的,接c[11:8]是不合法的。
组合逻辑建模常用语法
assign语句
assign语句是连续赋值语句,是组合逻辑的建模利器,其作用是用一个信号来驱动另一个信号。如assign a = b;
,其中a为wire型(也可由位拼接得到,见运算符部分),b是由数据和运算符组成的表达式。
assign语句与C语言的赋值语句有所不同,这里“驱动”的含义类似于电路的连接,也就是说,a的值时刻等于b。这也解释了assign a = a + 1;
这样的语句为什么是不合法的。由于这样的特性,assign语句不能在always和initial块中使用。
assign语句经常与三目运算符配合使用建模组合逻辑。一般来说,assign语句综合出来的电路是右侧表达式化简后所对应的逻辑门组合。
assign语句练习
假设有以下变量:
wire w1, w2;
reg r1, r2;
下列对于assign语句的使用,正确的有:
A. assign r1 = w1;
B. assign r2 = r1;
C. assign w1 = r1;
D. assign w2 = w1;(假设w1未被驱动)
E. assign w2 = w1;(假设w1已被驱动)
F. assign w1 = w1 | w1;
答案:C、E
reg类型变量不可以被assign,A、B错误。
未被驱动的wire型变量可以理解为一段没有连接任何信号的导线,它和其他导线相连是没有意义的,D错误。
assign意味着左侧的信号值始终等于右侧,因此F项中令w1始终等于w1 | w1是错误的。
常用运算符
Verilog HDL中有相当多的运算符都与C语言基本相同,如"+", “-”, “*”, “/”, “%“等基本运算,”&”, “|”, “~”, “^”, “>>”, “<<“等位运算,”&&”, “||”, “!” 等逻辑运算, “>”, “<”, “>=”, “<=“等关系运算以及条件运算符”? :”, 这些运算的运算规则与C语言相同,只是在操作数中出现了不定值x和高阻值z的话最终结果可能也是带x或z的。另外Verilog中没有自增、自减运算符。下面主要介绍其他与C不同的部分。
-
逻辑右移运算符>>与算术右移运算符>>>
它们的区别主要在于前者在最高位补0,而后者在最高位补符号位。
-
相等比较运算符==与===和!=与!==
==和!=可能由于不定值x和高阻值z的出现导致结果为不定值x,而===和!==的结果一定是确定的0或1(x与z也参与比较)。
-
阻塞赋值=和非阻塞赋值<=
不同于assign语句,这两种赋值方式被称为过程赋值,通常出现在initial和always块中,为reg型变量赋值。这种赋值类似C语言中的赋值,不同于assign语句,赋值仅会在一个时刻执行。由于Verilog描述硬件的特性,Verilog程序内会有大量的并行,因而产生了这两种赋值方式。这两种赋值方式的详细区别我们会在之后的小节内介绍,这里暂时只需记住一点:为了写出正确、可综合的程序,在描述时序逻辑时要使用非阻塞式赋值<= 。
-
位拼接运算符{}
这个运算符可以将几个信号的某些位拼接起来,例如
{a, b[3:0], w, 3'b101};
;可以简化重复的表达式,{4{w}}
等价于{w,w,w,w}
;还可以嵌套,{b, {3{a, b}}}
等价于{b, {a, b, a, b, a, b}}
,也就等价于{b, a, b, a, b, a, b}
。 -
缩减运算符
运算符&(与)、|(或)、^(异或)等作为单目运算符是对操作数的每一位递推运算,如对于
reg[31:0] B;
&B代表将B的每一位与起来得到的结果。
运算符练习
16’hf000 >> 4 = ? 答案:16’h0f00
$signed(16’hf000) >>> 4 = ? 答案:16’hff00
逻辑右移:高位补0即可;算术右移:在高位补符号位。
注意上面算术右移的写法,Verilog中表示有符号数需要加 $signed()。
下列选项中,与数字{^(32’ha081), {2{1’b0}}, |(32’o11)}位数和数值完全相同的是:
A. {1,0,0,1}
B. 2’o11
C. 4’b1
D. 4’h9
E. 4’o1
F. 1’d1
答案:C、E
{^(32’ha081), {2{1’b0}}, |(32’o11)} = {0, {0, 0}, 1} = {0, 0, 0, 1},该数表示4位的1,选项中应该选择C和E。
二选一多路选择器可以用一行带有三目运算符的代码完成。例如:输入A、B,输出C,选择信号temp=0时输出A,temp=1时输出B。请用一行代码完成这个功能。
(输入一行完整的代码,不要输入行首的空格或制表符,行尾的换行符,不要有多余的空格类字符或括号,同时尽可能短。)
答案:assign C=temp?A:B;
三目运算符 ? : 和C语言中的用法相同,两个符号隔开三个表达式,第一个是判断条件,为1时结果为第2个表达式,为0时结果为第3个表达式。注意本题要求尽可能短,不要输入多余的空格。
时序逻辑建模常用语法
always块
always块有如下两种用法:
-
若always之后紧跟
@(...)
,其中括号内是敏感条件列表,表示当括号中的条件满足时,将会执行always之后紧跟的语句或顺序语句块(和C语言中的语句块类似,只是将大括号用begin和end替换了)。这种用法主要用于建模时序逻辑。举例如下:
always @(posedge clk) // 表示在clk上升沿触发后面的语句块 begin // 一些操作 end
-
若always之后紧跟
@ *
或@(*)
,则表示对其后紧跟的语句或语句块内所有信号的变化敏感。这种用法主要用于与reg型数据和阻塞赋值配合,建模组合逻辑。 -
若always紧跟语句,则表示在该语句执行完毕之后立刻再次执行。这种用法主要配合后面提到的时间控制语句使用,来产生一些周期性的信号。
always的敏感条件列表中,条件使用变量名称表示,例如always @(a)
表示当变量a发生变化时执行之后的语句;若条件前加上posedge关键字,如always @(posedge a)
,表示当a达到上升沿,即从0变为1时触发条件,下降沿不触发;加上negedge则是下降沿触发条件,上升沿不触发。每个条件使用逗号“,”或“or”隔开,只要有其中一个条件被触发,always之后的语句都会被执行。
为了良好的代码可读性与可综合性,不要在多个always块中对同一个变量进行赋值!
always块练习
下列说法正确的有:
A. always块既可以是边沿敏感的,也可以是电平敏感的
B. always块既可以用于建模时序逻辑,也可以用于建模组合逻辑
C. 即使在多个always块中对同一个变量进行了赋值,代码仍然是可综合的
答案:A、B
always块既可以是边沿敏感的,也可以是电平敏感的,A正确。上面的讲解提到已经提到了always块建模时序逻辑和组合逻辑的两种常用方法,B正确。在多个always块中对同一个变量进行赋值的代码是不可综合的,C错误。
initial块
initial块后面紧跟的语句或顺序语句块在硬件仿真开始时就会运行,且仅会运行一次,一般用于对reg型变量的取值进行初始化。initial块通常仅用于仿真,是不可综合的。下面的代码用于给寄存器a赋初始值0:
reg a;
initial begin
a = 0;
end
always与initial练习1
在初步接触了Verilog之后,小孔同学想要设计一个带“记忆”功能的与门:输出为两个输入变量进行与运算的结果,同时这个结果将维持不变,直到下一个时钟上升沿到来。
他费了九牛二虎之力,终于完成了相应的设计,如下图:
但是,测试时他却没能通过语法检测。关键的报错如下:
你能帮帮可怜弱小又无助的小孔同学吗?在保证目标功能实现的前提下,根据提示信息,小孔至少需要对哪些行进行修改?(输入图中标注的相应行号;如果有多行需要修改,从小到大输入,并通过英文半角逗号‘,’隔开,如“1,2,3”(不包括引号))
答案:28
wire型变量是无法在initial块中赋值的,并且always块中对wire型变量的赋值也无法实现记忆功能,在28行定义处将mem改为reg型即可。
always与initial练习2
按照你的建议进行修改之后,小孔同学顺利地完成了设计。但是他经过反思之后,觉得mem变量的存在并没有什么意义,于是又进行了以下删减:
不幸的是,他又没能通过语法检测。请聪明的你告诉他,只需要修改哪一行(不允许修改空白行),就能解决这个问题,同时不影响目标功能的实现呢?(输入图中标注的相应行号,如“1”(不包含引号))
答案:25
这一行的内容应该替换成什么?(请输入完整的一行代码;不要输入行首的空格或制表符、行尾的换行符;不要有多余的空格类字符;不要改变模块期望的的外部性质)
答案:output reg c
Verilog模块中的输出可以直接定义为reg类型,在端口声明中output后面加上reg即可。
always与initial进阶练习
“秋冬之交在即,此乃祭祖之期。”小孔同学刚结束快乐的暑假,开始紧张刺激的计组学习了。可是现在有一个选择摆在他的面前,大家可以告诉他应该怎么选择吗?
仔细阅读下列代码:
判断:当clk和reset信号都处于下降沿时,mem会被清零。
答案:错误
注意,always @(*) 表示的是触发条件是always引导的顺序块中所有驱动信号的变化,而不是本模块所有信号的变化。该always块中向mem赋值的是0,是常量,因此这一always块始终不会被触发。
if语句
Verilog中if语句的语法和C语言基本相同,也有else if、else这样的用法。但是,if语句只能出现在顺序块中,其后的分支也只能是语句或顺序块。举例如下(下面的例子也使用了always建模组合逻辑):
always @ * begin
if (a > b) begin
out = a;
end
else begin
out = b;
end
end
为了避免意料之外的锁存器的生成而导致错误,建议大家为所有的if语句都写出相应的else分支。
if语句练习
判断:对于没有else的if语句,若没有进入任何分支,则相应变量的值保持不变。
答案:正确
case语句
Verilog中的case语句与C语言的写法略有区别,详见下方的示例。case语句同样只能出现在顺序块中,其中的分支也只能是语句或顺序块。与C语言不同,case语句在分支执行结束后不会落入下一个分支,而会自动退出。举例如下:
always @(posedge clk) begin
case(data)
0: out <= 4;
1: out <= 5;
2: out <= 2;
3: begin
out <= 1;
end
default: ;
endcase
end
需要指出的是,case语句进行的是全等比较,也就是每一位都相等(包括x和z)才认为相等。另外,还有casex和casez两种语句,我们的课程涉及不多,感兴趣的同学可以自行查阅相关资料。
for语句
Verilog中for语句的语法和C语言基本相同。只是我们通常会定义一个integer类型的变量作为循环变量。下面给出一个例子(七人投票表决器,仅为演示使用,不一定为最佳实现):
寄存器初始化练习
我们将0-31号寄存器定义如下:
在对其进行初始化的过程中:
一共犯了几处错误?
答案:2
两处错误,首先循环变量 i 显然不能设置成wire型,应为integer型,其次Verilog中没有自增运算符++,应当书写为 i = i + 1。
while语句
Verilog中while语句的语法和C语言基本相同。下面给出一个例子(对一个8位二进制数中值为1的位进行计数,仅为演示使用,不一定为最佳实现):
循环语句练习
判断:for语句和while语句可以直接出现在语句块之外。
答案:错误
for语句和while语句既可以用于建模组合逻辑,也可以用来建模时序逻辑。
答案:正确
时间控制语句
时间控制语句通常出现在测试模块中,用来产生符合我们期望变化的测试信号,比如每隔5个时间单位变更一次信号等。这个语句通过关键字**#实现延时,格式为"#时间**",当延时语句出现在顺序块中时它后面的语句会在延时完毕后继续执行。举例如下:
#3; //延迟3个时间单位
#5 b = a; //b为reg型,延迟5个时间单位后执行赋值语句
always #5 clk = ~clk; //每过5个时间单位触发一次,时钟信号反转,时钟周期为10个时间单位
assign #5 b = a; //b为wire型,将表达式右边的值延时5个时间单位后赋给b
时间控制语句练习
对于下图所示的代码:
在50个时间单位以后,ans1和ans2的值分别为:(填写十进制数字,如“10”(不包含引号))
ans1 ?答案:1
ans2 ?答案:2
模块内部不同的顺序块之间是并行的,always块中的cnt每个10个时间单位增加1,该操作和initial块同时开始,初始时cnt、ans1、ans2均为0,10秒后cnt在always块中被修改为1,第15秒ans1赋值为当时的cnt,即1;第20秒cnt在always块中被修改为2,第25秒ans2赋值为当时的cnt,即2.
模块的典型内部结构
一个模块的典型结构可以大致划分为三个部分:组合逻辑、时序逻辑和对其他模块的引用。
在组合逻辑部分,通常使用到的语法为assign语句,用于对wire型变量进行连续赋值。根据情况,我们也可能会使用always块来建模组合逻辑。
在时序逻辑部分,always块是必不可少的。通常我们会在always块中使用各种流程控制语句建模时序逻辑。有时,我们还需要initial块对变量进行一定的初始化。
引用其他模块时,我们会用到模块实例化的语法。这个语法将在后面的小节内为大家详细介绍。大家可以通过视频教程和练习题做初步了解。
模块实例化
假设已经有一个定义好的模块Sample,输入输出定义如下:
我们在设计的过程中,需要引用上述模块,其中变量x对应管脚a,变量y对应管脚b,变量z对应管脚c,reset管脚没有使用。
在下列对该模块的引用中,正确的是:
A. Sample uut(x, y, z);
B. Sample uut(y, x, .c(z));
C. Sample uut(.x(a), .y(b), .z©);
D. Sample uut(.b(y), .a(x), .c(z));
答案:D
模块实例化时接口的映射有两种:
位置映射:参数列表直接用逗号隔开接入模块的信号,按顺序接入实例化的模块所定义的接口,例如 Sample uut(x, y, z, t);
直接映射:显式地表明实例化模块的端口和待接入端口的对应关系,例如:Sample uut(.b(y), .a(x), .c(z));
阻塞赋值与非阻塞赋值
阻塞赋值与非阻塞赋值的区别
下面我们通过一个简单的实例,介绍阻塞赋值与非阻塞赋值的区别。
非阻塞赋值语句
处在一个always块中的非阻塞赋值是在块结束时同时并发执行的。对于ISim,在每一条非阻塞赋值执行前,仿真器“按下快门”保存下了在“<=”右边参与运算的变量值。在块结束进行赋值时,对于“<=”左边被赋值的变量,都是用“快照”中的值参与运算的。
阻塞赋值语句
阻塞赋值语句的执行是具有明确顺序关系的,在begin-end的顺序块中,当前一句阻塞赋值完成后(即“=”左边的变化为右边的值后),下一条阻塞赋值语句才会被继续执行。
阻塞赋值与非阻塞赋值练习
小李同学希望用Verilog HDL设计一个缓冲器,来实现下列功能:
- 端口in接收串行输入,输入位宽为1。所谓串行,就是使用一条数据线,将数据一位一位地依次传输,每一位数据占据一个固定的时间长度。
- 端口clk接收时钟信号。
- 端口out是寄存器类型,初值为0,out的波形与in形状相同,但恰落后2个时钟周期。(外部需要保证in恰好在clk的每个上升沿变化。)
小李同学动手写好了代码。其中模拟的时间粒度是ns,前10ns用来初始化,从10ns开始观察波形,代码如下:
经过ISim模拟,不符合期望的是,out的波形不是比in晚了两个时钟周期(20ns),而是一个时钟周期(10ns)。请指出他最少改动哪些行,就可以得到正确结果。请不要增删任何行,只考虑改变语句的内容,也不要改变in/clk的波形。(直接写出需要修改的行号,每个行号用半角逗号隔开,注意不要附加别的符号。例如:1,2,3,4)
提示:
如果你在ISim中进行试验就会发现,testbench中=和<=在时钟上升沿的临界状态下,行为是不同的。假如赋值和always块在同一个时钟上升沿发生,=会在always块之前运行,从而影响alway块的赋值;<=可以视为和always块并行运行,且本次不对always块中的<=的所有右值产生影响。需要改动的内容限定在buffer_tb内。
注意答题格式。
答案:32,34,36
本题较为细致地考察了阻塞赋值与非阻塞赋值的区别。
首先观察原有的代码,buffer模块中的always块采用了非阻塞赋值,而题目中强调了“外部需要保证in恰好在clk的每个上升沿变化”,那么正确的运行方式应当是:当clk上升沿的瞬间,in的值也发生改变,但在always块中参与运算的是旧的in值,即所谓的“本次不对always块中的<=的所有右值产生影响”。那么旧的in值给到buffer,旧的buffer的值给到out,以此out应当落后in两个时钟周期。
但上述正确运行过程的前提是,“本次不对always块中的<=的所有右值产生影响”,这一前提的保证是,外部的in变化与clk上升沿同时发生,而在原有的代码中,test bench中initial块的赋值都是阻塞赋值,在10ns时刻是clk的上升沿,同时是in信号的阻塞赋值变化,而阻塞赋值是要先于always块执行的,于是in的值先更新,在buffer模块的always块中in使用了新值,out只落后in一个时钟周期。
题目要求只修改test bench中的代码,那么将initial块中的阻塞赋值改为非阻塞即可,非阻塞赋值并不改变always块中的旧值,可以使out落后in两个时钟周期,而最少的修改只需要将in后来变化时的赋值方式修改即可,即第32、34、36行的阻塞赋值改为非阻塞赋值。
有符号数的处理方法
在Verilog HDL中,wire、reg等数据类型默认都是无符号的。当你希望做符号数的操作时,你需要使用**$signed()**。
下面我们通过一个比较器的例子进行详细说明:
我们希望程序实现比较a, b大小的功能,若a>b,res输出1,否则输出0,下面进行测试。
我们初始化a=4,b=1,100ns后b变为-1。期望的结果是res始终为1。下面是波形:
可以看到100ns后,res输出变为了0,与预期不符。其原因在于比较时Verilog将a和b默认视为无符号数,-1会被认为是15(4‘b1111)。
将比较代码修改为assign res = $signed(a) > $signed(b);
,程序即可达到预期结果。
值得一提的是,在对无符号数和符号数同时操作时,Verilog会自动地做数据类型匹配,将符号数向无符号数转化。同样使用上面的例子,将代码修改为assign res = a > $signed(b);
,这样得到的结果仍然是错误的,因为在执行a > $signed(b)
时,a是无符号数,b是符号数,Verilog默认向无符号类型转化,得到的结果仍是无符号数的比较结果。
此外,对于移位运算符,其右侧的操作数总是被视为无符号数,并且不会对运算结果的符号性产生任何影响。结果的符号由运算符左侧的操作数和表达式的其余部分共同决定。
希望大家可以通过编写测试代码并进行ISE仿真来掌握符号数的使用方法,这在后期CPU的功能实现过程中将有重要的作用。
Verilog中数字的存储
判断正误:
在Verilog中,若将一个变量赋值为负数,则该变量中存储的是该数的补码形式,且默认被作为有符号数看待。
答案:错误
负数在存储时确实会以补码形式存储,但使用时仍然被当作无符号数。Verilog中想要使用有符号数必须添加$signed()
宏定义的简单使用
类似C语言,Verilog HDL也提供了编译预处理指令。下面我们对其中的宏定义部分作一简要介绍。
在Verilog HDL语言中,为了和一般的语句相区别,编译预处理命令以符号"`"开头(位于主键盘左上角,其对应的上键盘字符为"~"。注意这个符号不同于单引号)。这些预处理命令的有效作用范围为定义命令之后到本文结束或到其他命令定义替代该命令之处。
宏定义用一个指定的标识符(即名字)来代表一个字符串,它的一般形式为:``define 标识符(宏名) 字符串(宏内容)`。它的作用是指定用标识符来代替字符串,在编译预处理时,把程序中该命令以后所有的标识符都替换成字符串。举例如下:
`define WORDSIZE 8
// 省略模块定义
reg[1:`WORDSIZE] data;
// 相当于定义reg[1:8] data;
注意,**引用宏名时也必须在宏名前加上符号"`"**表明该名字是经过宏定义的名字。