本文是我本人在寒假自学Verilog的过程总结的笔记,参考书:《Verilog HDL数字设计与综合(第二版)》
刷题网站是 老石推荐的hdlbits
笔记是个人总结,有可能会有纰漏,欢迎指正。(是直接从Notion复制过来的,Notion的会实时更新,这里可能不会)
新人入站,如果这篇文章对您有帮助,请不要吝啬手中的点赞,你们的点赞和关注是我持续更新的动力
先附Notion笔记链接和HDLbits链接 以及HDLBits先附Notion笔记链接和HDLbits链接 以及答案链接
Notion – The all-in-one workspace for your notes, tasks, wikis, and databases.https://purrfect-barnacle-2d8.notion.site/FPGA-329e6bb7c1074347bcfd133994cd0260HDLBitshttps://hdlbits.01xz.net/wiki/Main_PageHDLBits答案汇总_谦豫-CSDN博客_hdlbits答案前言该博客为本人做HDLBits习题时的心得记录总结,欢迎大家一起交流进步。HDLBits网站链接Verilog LanguageBasicsVectorsModules:HierarchyProceduresMore Verilog FeaturesCircuitsCombinational LogicBasic gatesMultiplexersArithmetic CircuitsKarnaugh Map to CircuitSequential LogicLatchehttps://blog.csdn.net/qq_42334072/article/details/113854656
文章较长,先附目录
目录
Verilog HDL基本概念
在程序运行过程中,其值不能被改变的量称为常量。下面先对Verilog HDL语言中使用的数字和表示方式进行总结。
数字声明
采用了一种很全面的表达数字的方式,即: < 位 宽 > < 进 制 > < 数 字 >的表达方式,
其中位宽和进制是可选项, 当省略位宽项时,表示采用默认位宽(由具体的机器系统决定,至少32位);当省略进制项时表达采用默认的十进制表示方式。
在Verilog HDL中,整型常量即整常数值有以下四种进制的表示形式: a. 二进制整数(b或 B) b. 十进制整数(d或D) c. 八进制整数(o或O) d. 十六进制整数(h或H)
<aside> ⚠️ 位宽是值存储带宽,不符合会截断,如1’d2 = 0 ;
</aside>
-
举例如下:
8'b10101100 //8位二进制数 12’habc //12位十六进制数
-
此外,在Verilog中我们可以采用x和z来表示不定值和高阻值,这经常用于判断语句和case语句中,以提高程序的可读性,如下例:
4'b10x0 //位宽为4的二进制数从低位数起第2位为不定值 12'dz //位宽为12的十进制数,其值为高阻值 8'h4x //位宽为8的十六进制数,其低4位值为不定值 0100_XXXX
值得注意的是,当采用不同进制表示时,x和z表示的位数也不同。
当我们需要表示负数时,只需要在表达式的最前面加上一个减号即可,如:
<aside> 💡 -8'd5//十进制 注意:写成8'-d5和8'd-5的形式都是错误的。
</aside>
最后,可以用下划线来分隔开数的表达以提高程序的可读性,如:
16'1010_1011_1111_1010
-
如果定义的长度比为常量指定的长度长,通常在左边填0 补位。但是如果数最左边一位为x 或z ,就相应地用x 或z 在左边补位。例如:
10'b10 左边添0 占位, 0000000010 10'bx0x1 左边添x 占位, x x x x x x x 0 x 1
-
如果长度定义得更小,那么最左边的位相应地被截断。例如:
3 ' b1001 _ 0011 与3'b011 相等 5'H0FFF 与5'H1F 相等
字符串
双引号括起来 Verilog将字符串当做一个字节的ACSII字符队列。 字符串保存在reg的类型变量中,每个字符占8位。
如果寄存器变量宽度大于字符串的大小,则Verilog使用0在填补左边空余位; 如果寄存器变量宽度小于字符串的大小,则截去最左边的位。
reg [8*18:1] string_value;
initial
string_value = “Hello Verilog World”;
关键字和标识符
Verilog中关键字全部采用小写
标识符是程序代码中对象的名字,由字母数字字符、下划线(_)和美元($)组成。区分大小写,且开始必须是字母数字字符或下划线
- reg value //reg是关键字;value是标识符
- input CLK //input是关键字;CLK是标识符
值的种类
线网(net)
net表示硬件元件之间的连接,线网由其连接的元器件输出端驱动。 一般采用wire进行声明。如果没有显式说明为向量,则默认线网宽度为1。默认值为z(trireg类型除外,其为x)例如:
- wire a = 1’b0 ;//声明a是wire类型,并赋逻辑值0
- wire[width - 1 : 0 ] b;//定义一个width位的wire类型
<aside> 👉 注意:net是一组数据类型包括wire,wand,wor,tri,triand,trior和trireg等 RTL基本知识:线网类型知多少 - 魏老师说IC - 博客园
</aside>
寄存器(reg)
并非硬件寄存器,仅仅意味是一个保存数值的变量。定义语法:
reg [msb : lsb] name;
范围定义可选,若没有定义,则默认为1位寄存器。
- reg signed [63 : 0] m;//64位带符号的值
整数(integer)
整数是一种通用的寄存器数据类型,默认位宽为宿主机的字的位数,但最小应为12位。整形类型为有符号数。
integer counter;
initial
counter = -1;
实数和时间寄存器
向量
向量的声明(declaring vector)
net和register类型的数据均可以声明为向量(位宽>1)
向量通过[high# :low#]或[low# :high#]进行说明8
wire [7:0] w; // 8-bit wire
reg [4:1] x; // 4-bit reg
output reg [0:0] y; // 1-bit reg that is also an output port (this is still a vector)
input wire [3:-2] z; // 6-bit wire input (negative ranges are allowed)
output [3:0] a; // 4-bit output wire. Type is 'wire' unless specified otherwise.
wire [0:7] b; // 8-bit wire where b[0] is the most-significant bit.
<aside> ⚠️ 方括号左边的数总是代表最高有效位。
</aside>
向量域的选择
可以指定某一位和若干位,用法同C;
- bus [ 2 : 0 ] //向量bus的最低3位
可变域选择
[<starting_bit>+:width]:从起始开始递增,位宽为width [<starting_bit> - : width]:从起始开始递减,位宽为width
reg [255 : 0] data1;
for ( j = 0;j <= 31; j = j + 1)
byte = data1 [(j*8)+:8];//次序是[7:0],[15:8]……[255:148]
参数(parameter)
在Verilog HDL中用parameter来定义常量,即用parameter来定义一个标识符代表一个常量,称为符号常量,即标识符形式的常量,采用标识符代表的一个常量可以提高程序的可读性和可维护性。格式如下:
parameter 参数名1 = 表达式, 参数名2 = 表达式, 参数名3 = 表达式, ..., 参数名n = 表达式;其中,parameter是参数性数据的确认符。
值得注意的是,表达式中只能包含之前定义的参数或者常量数字,如下例:
parameter msb = 7; //定义参数msb为常量7
parameter e = 25, f = 29; //定义两个常量参数
parameter r = 5.7; //声明r为一个实型参数
parameter hyte_size = 8, byte_msb = byte_size - 1; //用常数表达式赋值
parameter average_delay = (r + f) / 2; //用常数表达式赋值
在模块或实例引用时,可通过参数传递改变在别引用模块或实例中已定义的参数,或者defparam(参数重载)语句改变参数值。
局部参数(localparam)
局部参数可以使用localparam,但其值不能改变,不能通过参数重载语句(defparam)或通过有序参数列表或命名参数赋值来直接修改。状态机编码是不能被修改的,故应定义为localparam
系统任务和编译指令
系统任务
$monitor
$display
编译指令
define 类似于C的#define结构,在编译阶段,当编译器遇到
<宏定义名>时,使用预定义的文本宏进行互换。
`define WORD_SIZE 32; //在代码中用`WORD_SIZE表示规定字长
`define S $stop; //定义别名,可以用`s来代替$stop
操作符
基本运算符
+、-、*、/、%、**(求幂)类似C
按位运算符
取反(~)、与(&)、或( | )、异或(^)、同或(^~,~^)
若两个操作数位宽不等,则使用0向左扩展较短的操作数。
<aside> 💡 注意和 x 有关的运算,0 * x = 0 ; 1 + x = 1 ; 异或同或均为x
</aside>
等价操作符
逻辑等(=)逻辑不等(!=)case等(===)case不等(!==)
逻辑等价操作符,若两个操作数有一个含有 x 或 z,则结果为 x ; case等价操作符,是对两个操作数严格进行比较,包括 x 和 z ,完全相等的情况下才为1。
关系操作符
拼接操作符
通过此操作符{}可以实现两个或以上信号的某些位拼接起来;
{信号 1 的某几位,信号 2 的某几位,.....}
位拼接还可以采用重复来简化表达式
//A = 1'b1 ;B = 2'b00 ;C = 2'b10 ;D = 3'b110
Y = { A , B , C , D , 3'b001};//11'b10010110001
Y = {4{A}} ;//4'b1111
缩减运算符
缩减与(&)、缩减与非( ~& )、缩减或( | )、缩减异或(~^,^~)、缩减或非(~ | )。缩减操作符只有一个操作符。它对向量操作数逐位(从左至右)运算。
// X = 4'b1010
&X //1 & 0 & 1 &0 结果为1'b0
连续赋值语句(数据流建模)
- 总是处于激活状态:只要任意一个操作数发生变化,表达式就会被立即重新计算
- 连续赋值语句的左值一定是一个标量或者向量线网,或者是标量或线网的拼接。
- 操作数类似C语言,可以是向量或者标量或者是函数调用
When you have multiple assign statements, the order in which they appear in the code does not matter. Unlike a programming language, assign statements ("continuous assignments") describe connections between things, not the action of copying a value from one thing to another.
assign {addr[15:0] ,sum [3:0]} = addr1_bits[15:0]^addr2_bits[15:0];
assign {c_out ,sum [3:0]} = a[3:0] + b[3:0] + c_in;
隐式线网声明
- 如果一个信号名作为连续赋值函数的左值,Verilog认为其是一个隐式声明的线网
wire i1 , i2;
assign out = i1 & i2;//out是一个隐式声明的线网
隐式连续赋值
wire out ;
assign out = i1 & i2;
//使用隐式赋值实现上述两行代码
wire out = i1 & i2;
过程赋值语句(行为级建模)
- 只有在执行的时候才起作用。
阻塞赋值(=)
Blocking Assignment:下一条语句的执行会被本条阻塞型赋值语句阻塞,只有在当前的阻塞型赋值语句执行完后,下条语句才会被执行。
在begin-end串行语句块中,将以他们的顺序块后排列次序一次得到执行; 先计算右边赋值表达式的值,然后立即将计算结果赋值给“=”左边被赋值量。
非阻塞赋值(<=)
Nonblocking Assignment
在begin-end的串行语句块中,一条非阻塞赋值语句的执行不会阻塞下一条语句的执行 先计算右边赋值表达式的值,等到仿真时间结束时在将该计算结果赋值变量。也就是说,这种情况下是在同一仿真时刻的其他操作完成时执行
a = 1;
b = 2;
c = 3;
initial
begin
a = b;
c = a;
end
//result:a=b=c=2
a = 1;
b = 2;
c = 3;
initial
begin
a <= b;
c <= a;
end
//result:a=b=2,c=1
⚠️数值交换
//阻塞赋值
always @(posedge)
begin
temp_a = a;
temp_b = b;
a = temp_a;
b = temp_b;
end
//先读后写
//非阻塞赋值
always @(posedge)
a <= b;
always @(posedge)
b <= a;
//若此处将 <= 改成 = ,会造成竞争
🤔三种赋值语句对比
There are three types of assignments in Verilog:
- Continuous assignments (assign x = y;). Can only be used when not inside a procedure ("always block").
- Procedural blocking assignment: (x = y;). Can only be used inside a procedure.
- Procedural non-blocking assignment: (x <= y;). Can only be used inside a procedure.
In a combinational always block, use blocking assignments. In a clocked always block, use non-blocking assignments. A full understanding of why is not particularly useful for hardware design and requires a good understanding of how Verilog simulators keep track of events. Not following this rule results in extremely hard to find errors that are both non-deterministic and differ between simulation and synthesized hardware.
⚠️ A note on wire vs. reg: The left-hand-side of an assign statement must be a net type (e.g., wire), while the left-hand-side of a procedural assignment (in an always block) must be a variable type (e.g., reg). These types (wire vs. reg) have nothing to do with what hardware is synthesized, and is just syntax left over from Verilog's use as a hardware simulation language.
模块和端口
端口连接规则
端口与外部信号的连接
顺序端口连接
module fulladd4( sum , c_out , a , b , c_in);
endmodule
fulladd4 fa_ordered(SUM , C_OUT , A , B , C_IN );//调用(实例引用)fulladd4命名为fa_ordered
命名端口连接
module fulladd4( sum , c_out , a , b , c_in);
endmodule
fulladd4 fa_byname(.sum(SUM) , .c_out(C_OUT) , .a(A) , .b(B), .c_in(C_IN))
//调用(实例引用)fulladd4,命名为fa_byname
门级建模
- 在使用门级原语时,我们可以不指定具体实例的名字
- 输入端口(与或门)、输出端口(缓冲器、非门)可以超过两个,这时Verilog会自动根据端口数目选择合适的门
基本门的实例引用
and a1(OUT, IN1, IN2);
nand na1(OUT, IN1, IN2);
or or1(OUT, IN1, IN2);
nor nor1(OUT, IN1, IN2);
xor x1(OUT, IN1, IN2); //异或
xnor nx1(OUT, IN1, IN2);
buf b1(OUT1, OUT2, IN);
not n1(OUT1, IN);
实例数组
简化对某种类型的门多次调用
wire [7:0] OUT, IN1, IN2;
nand n_gate[7:0](OUT, IN1, IN2);
<aside> ⚠️ 调用用户定义模块的实例时必须指定名字!
</aside>
结构化过程语句
是行为建模的基本语句,其他行为语句只能出现在这两种结构化语句中
initial语句
- 从仿真0时刻开始运行
- 若一个模块中含有若干个initial块,则这些快块时刻开始并发执行,且每个块的执行是各自独立的
- 如果在块内包含了多条行为语句,那么需要将这些语句组成一组,一般式使用关键字begin和end将他们组合在一个块语句
module stimulus;
reg x,y, a,b, m
initial
m = 1'b0;
initial
begin
#5 a = 1'b1;
#25 b = 1'b0;
end
initial
begin
#10 x = 1'b0;
#25 y = 1'b1;
end
initial
#50 $finish;
endmodule
例子中,三条initial语句在仿真0时刻开始并行执行
时间 所执行的语句 0 m = 1'b0; 5 a = 1'b1; 10 x = 1'b0; 30 b = 1'b0; 35 y = 1'b1; 50 $finish;
always语句
- 从仿真的0时刻开始顺序执行其中的行为语句,类似C中的while(1)
- 真实地反映了硬件电路在通电以后连续反复执行
module clock_gen (output reg clock);
//建立时钟发生器的一种方法
initial
clock = 1'b0;
always
#10 clock = ~clock;
initial
#1000 $finish;
endmodule
延时
门延时
连续赋值语句的延时
普通赋值延时59
隐式连续赋值
线网声明延时
wire #10 out = in1 & in2;
//等效于
wire out;
assign #10 out = in1 & in2;
wire #10 out = in1 & in2;
assign out = in1 & in2;
//等效于
wire out;
assign #10 out = in1 & in2;
时序控制
基于延时
💡 延时值可以是数字、标识符、表达式(unsigned_number | parameter_identifier | specparam_identifier | mintypmax_expression)
常规延时控制
遇到常规时延时,语句需要等待一定时间,然后将计算结果赋值给目标信号。
#10 procedural_statement //10个单位时间后,再执行后面的语句
#10 ; //单独写一句,表示延时10个时间单位;
内嵌延时
遇到内嵌延时时,该语句先将计算结果保存,然后等待一定的时间后赋值给目标信号。
value_embed = #10 value_test ;//内嵌时延控制加在赋值号之后;