此笔记仅作为Verilog临时速成的主观简单记录,对于Verilog的语法和性质等并没有进行完备的记录、对于Verilog更深入的内容也并不记载,同时并不保证笔记的绝对正确
Verilog与C相似,很多语法结构和设计思想都可以参考C
Verilog是一门硬件描述语言,虽然与C有很多相似,但在基本设计思想上与C有本质不同
设计代码时应当时刻谨记我们在描述硬件而非设计程序
项目结构
设计方法
Verilog项目一般采用自顶向下的设计方法,即先定义顶层模块功能,进而分析要构成顶层模块的必要子模块;然后进一步对各个模块进行分解、设计,直到到达无法进一步分解的底层功能块。Verilog与C有许多相似之处,同样地,顶层模块一般可以视作C代码的main函数,编译器将由顶层模块开始组织项目
模块结构
简单而言,Verilog模块内部由模块定义声明、内部信号声明和各种逻辑语句、赋值语句以及其他结构构成
基本语法
基本
Verilog语句以分号结尾,多行语句一般用begin
、end
包住
代码使用//进行单行注释、用/*
、*/
进行多行注释
单个模块使用module
、endmodule
包住
编译指令
`define、 `undef
`define
用于编译阶段文本替换,与C的#define
相似`undef
用来取消之前的宏定义
`include
用于编译阶段的文件包含,与C的#include
相似
`timescale
用于定义时延、仿真的单位和精度
`timescale time_unit / time_precision
time_unit
表示时间单位,time_precision
表示时间精度,它们均是由数字以及单位s(秒),ms(毫秒),us(微妙),ns(纳秒),ps(皮秒)和 fs(飞秒)组成
时间精度大小不能超过时间单位大小
可以使用#n
来实现n个时间单位的时延
数值表示
Verilog有0、1、x或X(未知)、z或Z(高阻态)四种电平逻辑
Verilog允许使用下划线_
来分隔多位数字增加可读性
Verilog在整数声明时有四种基数格式:十进制('d
或'D
)、十六进制('h
或'H
)、二进制('b
或'B
)、八进制('o
或'O
)
数值可不指明位宽
负数通常在位宽前表示
示例
4'b1011 // 4bit 数值
32'h3022_c0de // 32bit 的数值
'd100 //一般会根据编译器自动分频位宽,常见的为32bit
-6'd15 //负数
Verilog在实数声明时使用十进制或科学计数法表示
Verilog里字符串是由双引号包起来的字符队列
字符串不能多行书写,即字符串中不能包含回车符
字符串本质是一系列的单字节ASCII字符队列
数据类型
interger(整数)
integer var_name ;
若将实数值赋值给整数变量,将只保留其整数部分
real(实数)
real var_name ;
可用十进制或科学计数法表示
time(时间)
Verilog使用特殊的时间寄存器time
型变量,对仿真时间进行保存。其宽度一般为64 bit,通过调用系统函数 $time
获取当前仿真时间
示例
time current_time ;
initial begin
#100 ;
current_time = $time ; //current_time 的大小为 100
end
parameter/localparam(参数)
parameter name = data;
localparam name = data;
参数用于表示常量,其只能赋值一次
局部参数用localparam
声明,其与parameter
不同之处在于其只能在本模块中被调用
数组
type var_name [n0:m0][n1:m1]… ; //声明
var_name [a][b]… ; //访问
Verilog允许声明多维数组
wire(线网)
wire var_name ;
wire
类型表示硬件单元之间的物理连线,由其连接的器件输出端连续驱动
如果没有驱动元件连接到wire
型变量,缺省值一般为高阻态 "Z"
reg(寄存器)
reg var_name ;
寄存器用来表示存储单元,其会在被改写前一直保持旧值integer
、real
、time
本质为寄存器类型
向量
[n:m] //位宽=n-m+1
当位宽大于1时wire
与reg
可以声明为向量形式
n和m可以为表达式
Verilog允许我们指定使用某一位或向量若干相邻位
Verilog允许我们指定当前位前/后若干位的选择
Verilog允许使用大括号将向量合成新的向量
示例
reg [3:0] counter ; //声明4bit位宽的寄存器counter
wire [32-1:0] gpio_data; //声明32bit位宽的线型变量gpio_data
wire [8:2] addr ; //声明7bit位宽的线型变量addr,位宽范围为8:2
reg [0:31] data ; //声明32bit位宽的寄存器变量data, 最高有效位为0
//表达式可变向量域
reg [31:0] data1 ;
reg [7:0] byte1 [3:0];
integer j ;
always@* begin
for (j=0; j<=3;j=j+1) begin
byte1[j] = data1[(j+1)*8-1 : j*8];
//把data1[7:0]…data1[31:24]依次赋值给byte1[0][7:0]…byte[3][7:0]
end
end
//下面 2 种赋值是等效的
A = data1[31-: 8] ;
A = data1[31:24] ;
//下面 2 种赋值是等效的
B = data1[0+ : 8] ;
B = data1[0:7] ;
//向量合成
wire [31:0] temp1, temp2 ;
assign temp1 = {byte1[0][7:0], data1[31:8]}; //数据拼接
assign temp2 = {32{1'b0}}; //赋值32位的数值0
存储器
reg var_name [n:m] ;
存储器本质为寄存器数组,可用于描述RAM、ROM的行为
字符串
reg [0:strlen*8-1] str ;
initial begin
str = "your_string" ;
end
字符串保存在寄存器变量中,每个字符占8 bit
若长度溢出则直接截断,若长度不足则补零
特殊字符需要使用前缀来转义
逻辑操作
Verilog 中提供了大约 9 种操作符,分别是算术、关系、等价、逻辑、按位、归约、移位、拼接、条件操作符,大部分与C相似
- 算术:乘
*
、除/
、加+
、减-
、求幂**
、取模%
- 关系:大于
>
,小于<
,大于等于>=
,小于等于<=
- 关系操作符的正常结果有2种,真(1)或假(0),如果操作数中有一位为x或z,则关系表达式的结果为x
- 等价:逻辑相等
==
,逻辑不等!=
,全等===
,非全等!==
- 等价操作符的正常结果有2种:为真(1)或假(0)
- 逻辑相等/不等操作符不能比较x或z,当操作数包含一个x或z,则结果为不确定值
- 全等比较时,如果按位比较有相同的x或z,返回结果也可以为1,即全等比较可比较x或z
- 逻辑:逻辑与
&&
,逻辑或||
,逻辑非!
- 如果一个操作数不为0,它等价于逻辑1
- 如果它任意一位为x或z,它等价于x
- 如果任意一个操作数包含x,逻辑操作符运算结果不一定为x
- 按位:取反
~
,与&
,或|
,异或^
,同或~^
- 归约:归约与
&
,归约与非~&
,归约或|
,归约或非~|
,归约异或^
,归约同或~^
- 归约其实意为从最左位开始与每位右一位进行逻辑运算,可用于进行是否全1/全0等多位判断、归约操作
- 移位:左移
<<
,右移>>
,算术左移<<<
,算术右移>>>
- 算术右移会根据最左位决定补1/补0来保证补码的正确性,其余操作符一律补0
- 拼接:大括号
{
、}
- 条件:
expression ? true_expression : false_expression
- 等价于C的三目运算符
过程结构、时序控制
assign语句
assign target = expression ;
assign
语句称作连续赋值语句
之所以称为连续赋值语句是指其总是处于激活状态,只要表达式中的操作数有变化,立即进行计算和赋值
赋值目标必须是wire
型assign
语句中没有begin
、end
always语句
always @(signal_condition) //星号(*)表示全体信号
expression ;
always
语句块又称过程块always
语句本身不是单一的有意义的一条语句,而是和下面的语句一起构成一个语句块,称之为过程块,过程块中的赋值语句称过程赋值语句
该语句块不是总处于激活状态,当满足激活条件时才能被执行,否则被挂起,挂起时即使操作数有变化,也不执行赋值,赋值目标值保持不变,激活条件分为边沿敏感(上升沿或下降沿)和电平敏感(目标电平发生变化时激活)
常用边沿敏感条件有上升沿posedge
、下降沿negedge
,电平敏感条件可用逗号或or
并列
赋值目标必须是reg
型always
语句中还可以使用循环、分支等语句使其功能更加强大
initial语句
initial begin
statement ;
…
end
initial
语句从0时刻开始执行,只执行一次
多个initial
块之间是相互独立的initial
语句理论上来讲是不可综合的,多用于初始化、信号检测等
赋值
由于硬件设计的对时序要求的特殊性,Verilog的赋值分为阻塞赋值和非阻塞赋值
阻塞赋值=
按语句顺序执行
非阻塞赋值<=
则所有语句并行执行
示例
begin
m = a*b;
y = m; //y=a*b
end
begin
m <= a*b;
y <= m; //y=old_m
end
设计组合电路时常用阻塞赋值,设计时序电路时常用非阻塞赋值
不建议在一个always
块中混合使用阻塞赋值和非阻塞赋值
分支语句
if语句
if (condition1) true_statement1 ;
else of (condition2) true_statement1 ;
else default_statement ;
case语句
case(case_expr)
condition1 : true_statement1 ;
condition2 : true_statement2 ;
…
default : default_statement ;
endcase
循环语句
Verilog 循环语句有4种类型,分别是while
,for
,repeat
和forever
循环
循环语句只能在always
或initial
块中使用,但可以包含延迟表达式
while循环
while (condition) begin
…
end
for循环
for(initial_assignment; condition ; step_assignment) begin
…
end
repeat循环
repeat (loop_times) begin
…
end
repeat
的功能是执行固定次数的循环,循环的次数必须是一个常量、变量或信号
如果循环次数是变量信号,则循环次数是开始执行循环时变量信号的值
即便执行期间,循环次数代表的变量信号值发生了变化,执行次数也不会改变
forever循环
forever begin
…
end
forever
表示永久循环,不包含任何条件表达式,一旦执行便无限的执行下去
系统函数$finish
可退出循环
函数和任务
函数
function [range-1:0] function_id ;
input_declaration ;
other_declaration ;
procedural_statement ;
endfunction
function_id(input1, input2, …);
函数在声明时,会隐式的声明一个宽度为range
、 名字为function_id
的寄存器变量,函数的返回值通过这个变量进行传递
当该寄存器变量没有指定位宽时,默认位宽1
函数通过指明函数名与输入变量进行调用,函数结束时,返回值被传递到调用处
Verilog中,一般函数的局部变量是静态的,若函数发生并发调用则会产生难以预测的结果
可以在function
后加上automatic
关键字来说明此类函数在调用时自动分配新的内存空间,也可以理解为此类函数是可并发调用、可递归的
任务
task task_id ;
port_declaration ;
procedural_statement ;
endtask
task_id(input1, input2, …,outpu1, output2, …);
任务中使用关键字input
、output
和inout
对端口进行声明input
、inout
型端口将变量从任务外部传递到内部,output
、inout
型端口将任务执行完毕时的结果传回到外部
进行任务的逻辑设计时,可以把input
声明的端口变量看做wire
型,把output
声明的端口变量看做reg
型
但是不需要用reg
对output
端口再次说明。
对output
信号赋值时也不要用关键字assign
为避免时序错乱,建议output
信号采用阻塞赋值
函数和任务的异同
和函数一样,任务可以用来描述共同的代码段,并在模块内任意位置被调用,让代码更加的直观易读。函数一般用于组合逻辑的各种转换和计算,而任务更像一个过程,不仅能完成函数的功能,还可以包含时序控制逻辑
下面对任务与函数的区别进行概括:
比较点 | 函数 | 任务 |
---|---|---|
输入 | 函数至少有一个输入,端口声明不能包含 inout 型 | 任务可以没有或者有多个输入,且端口声明可以为 inout 型 |
输出 | 函数没有输出 | 任务可以没有或者有多个输出 |
返回值 | 函数至少有一个返回值 | 任务没有返回值 |
仿真时刻 | 函数总在零时刻就开始执行 | 任务可以在非零时刻执行 |
时序逻辑 | 函数不能包含任何时序控制逻辑 | 任务不能出现 always 语句,但可以包含其他时序控制,如延时语句 |
调用 | 函数只能调用函数,不能调用任务 | 任务可以调用函数和任务 |
书写规范 | 函数不能单独作为一条语句出现,只能放在赋值语言的右端 | 任务可以作为一条单独的语句出现语句块中 |
模块内子程序出现下面任意一个条件时,则必须使用任务而不能使用函数
- 子程序中包含时序控制逻辑,例如延迟、事件控制等
- 没有输入变量
- 没有输出或输出端的数量大于1
门原语
Verilog语言提供已经设计好的门,称为门原语(共12个)
门原语包括逻辑门(and
、or
、not
、xor
、nand
、nor
、xnor
)、缓冲器(buf
)、三态门(bufif1
、notif1
、bufif0
、notif0
)
门原语的调用类似于模块调用,此处不赘述
模块
模块定义声明
module 模块名 ([端口列表);
[端口信号声明]; //[输入/输出属性] [数据类型] [位宽] [名称]
[参数声明]; //[]
[语句];
endmodule
- 模块名应符合命名规则(与C类似),根据代码规范最好与文件名一致(一文件一模块)
- 端口列表指电路的输入/输出信号名称列表,信号间用逗号隔开
- 端口信号声明包括端口信号的属性、数据类型、位宽和信号名
- 属性有
input
、output
、inout
- 类型常用的有
wire
和reg
- 位宽用
[n:m]
表示,位宽=n-m+1
且默认为1,根据代码规范通常规定n>m - 数据类型默认
wire
型
- 属性有
- 参数声明需要说明参数名及其初值
- 端口信号声明可以代替端口列表直接写在括号内
示例
module full_adder (A,B,CIN,S,COUT);
input [3:0] A,B;
input CIN;
output reg [3:0] S;
output COUT;
endmodule
模块例化
在一个模块中引用另一个模块,对其端口进行相关连接,叫做模块例化
可以类比理解为C++类的实例化
module_name name (ports_list);
例化模块需要进行端口映射,端口映射有命名法和顺序法两种方法
命名法
module_name name (.port_name(signal_name),…,.port_name(signal_name));
顺序法
module_name name (signal_name,…,signal_name); //按照原模块端口定义顺序
值得注意的是,Verilog规定,在上层设计中若信号是从子模块输出,则不能使用reg
型而应使用wire
型
示例
示例1:4bit ALU
其实是没写过其他项目,显然没什么可参考性()
alu_4.v
`timescale 1ns / 1ps
module alu_1(
input A,
input B,
input cin,
input[2:0] M,
output S,
output C
);
reg result=1'b0;
reg result1=1'b0;
always @*
begin
case(M)
3'b000:
begin
result=A&B;
result1=~result;
end
3'b001:
begin
result=A|B;
result1=~result;
end
3'b010:
begin
result=~A;
result1=~result;
end
3'b011:
begin
result=~B;
result1=~result;
end
3'b100:
begin
result=A^B;
result1=~result;
end
3'b101:
begin
// result=(A^B)^cin;
// result1=((A&B)|(A^B))&cin;
{result1,result}=A+B+cin;
end
3'b110:
begin
// result=(A^B)^cin;
// result1=(~A&(B^cin))|(B&cin);
{result1,result}=A-cin-B;
end
default:
begin
result=0;
result1=~result;
end
endcase
end
assign S=result;
assign C=result1;
endmodule
module alu_4(
input[3:0] A,
input[3:0] B,
input[2:0] M,
output[3:0] S,
output C
);
wire [2:0] Cn;
alu_1 a1(A[0],B[0],0,M,S[0],Cn[0]);
alu_1 a2(A[1],B[1],Cn[0],M,S[1],Cn[1]);
alu_1 a3(A[2],B[2],Cn[1],M,S[2],Cn[2]);
alu_1 a4(A[3],B[3],Cn[2],M,S[3],C);
endmodule
alu_4_sim.v
`timescale 1ns / 1ps
module alu_4_sim();
reg [3:0] A;
reg [3:0] B;
reg [2:0] M;
wire [3:0] S;
wire C;
integer i;
integer j;
integer k;
alu_4 alu_4_sim (
.A(A),
.B(B),
.M(M),
.S(S),
.C(C)
);
initial
begin
for(i=0,A=4'b0000;i<16;i=i+1)
begin
for(j=0,B=4'b0000;j<16;j=j+1)
begin
for(k=0,M=3'b000;k<8;k=k+1)
begin
#1;
M=M+3'b001;
end
B=B+4'b0001;
end
A=A+4'b0001;
end
end
endmodule
vivado仿真结果
示例2:汽车尾灯
要求使用JK触发器和数据选择器实现
通过触发器实现移位寄存器,从而实现汽车尾灯左转、右转和闪烁功能
设计得闭眼可见的烂,但蒟蒻如我实在没活了()
main.v
`timescale 1ns / 1ps
module jk(clk, cr, j, k, q);
input clk, cr, j, k;
output q;
reg q=1'b0;
always@(posedge clk) begin
if(cr) begin
q<=1'b0;
end
else begin
case({j,k})
2'b00: q<=q;
2'b01: q<=1'b0;
2'b10: q<=1'b1;
2'b11: q<=~q;
endcase
end
end
endmodule
module right(clk, cr, s0, s1, out);
input clk, cr, s0, s1;
output [3:0]out;
jk j1(clk, cr, s0, s1, out[0]);
jk j2(clk, cr, out[0], ~out[0], out[1]);
jk j3(clk, cr, out[1], ~out[1], out[2]);
jk j4(clk, cr, out[2], ~out[2], out[3]);
endmodule
module left(clk, cr, s0, s1, out);
input clk, cr, s0, s1;
output [3:0]out;
jk j1(clk, cr, s0, s1, out[3]);
jk j2(clk, cr, out[3], ~out[3], out[2]);
jk j3(clk, cr, out[2], ~out[2], out[1]);
jk j4(clk, cr, out[1], ~out[1], out[0]);
endmodule
module stop(clk, out);
input clk;
output [3:0]out;
jk j1(clk, 0, 1, 1, out[3]);
jk j2(clk, 0, 1, 1, out[2]);
jk j3(clk, 0, 1, 1, out[1]);
jk j4(clk, 0, 1, 1, out[0]);
endmodule
module choose(s0, s1 , s2, i0, i1, i2, i3, i4, i5, i6, i7, out);
input s0, s1, s2, i0, i1, i2, i3 , i4, i5, i6, i7;
output out;
reg t=1'b0;
always@* begin
case({s0,s1,s2})
3'b000: t=i0;
3'b010: t=i1;
3'b100: t=i2;
3'b110: t=i3;
3'b111: t=i4;
3'b101: t=i5;
3'b001: t=i6;
3'b011: t=i7;
endcase
end
assign out=t;
endmodule
module light(
input clk,
input s0,
input s1,
input s2,
output [3:0] lo,
output [3:0] ro,
output [3:0] so,
output [7:0] t
);
left l0(clk, &lo, 1&s0, 0, lo);
right r0(clk, &ro, 1&s0, 0, ro);
stop st0(clk, so);
choose m0(s0, s1, s2, 0, 0, ro[0], lo[0], 0, 0, so[0], so[0], t[0]);
choose m1(s0, s1, s2, 0, 0, ro[1], lo[1], 0, 0, so[1], so[1], t[1]);
choose m2(s0, s1, s2, 0, 0, ro[2], lo[2], 0, 0, so[2], so[2], t[2]);
choose m3(s0, s1, s2, 0, 0, ro[3], lo[3], 0, 0, so[3], so[3], t[3]);
choose m4(s0, s1, s2, 0, 0, ro[0], lo[0], 0, 0, so[0], so[0], t[4]);
choose m5(s0, s1, s2, 0, 0, ro[1], lo[1], 0, 0, so[1], so[1], t[5]);
choose m6(s0, s1, s2, 0, 0, ro[2], lo[2], 0, 0, so[2], so[2], t[6]);
choose m7(s0, s1, s2, 0, 0, ro[3], lo[3], 0, 0, so[3], so[3], t[7]);
endmodule
sim.v
`timescale 1ns / 1ps
module light_sim();
reg clk;
reg s0;
reg s1;
reg s2;
// wire [7:0] out;
wire [3:0] ro;
wire [3:0] lo;
wire [3:0] so;
wire [7:0] t;
// integer i;
light light_sim(
.clk(clk),
.s0(s0),
.s1(s1),
.s2(s2),
// .out(out)
.lo(lo),
.ro(ro),
.so(so),
.t(t)
);
initial begin
for(clk=1'b0;1;) begin
#10;
clk=~clk;
end
end
initial begin
for({s0,s1,s2}=3'b000;1;) begin
{s0,s1,s2}=3'b000;
#200;
{s0,s1,s2}=3'b100;
#200;
{s0,s1,s2}=3'b000;
#200;
{s0,s1,s2}=3'b001;
#200;
{s0,s1,s2}=3'b000;
#200;
{s0,s1,s2}=3'b110;
#200;
{s0,s1,s2}=3'b000;
#200;
{s0,s1,s2}=3'b011;
#200;
end
end
endmodule
vivado仿真结果:
踩过的坑:
模块只能在单独的语句块中调用,而不能在其他语句中调用
上层模块从子模块接收的输入变量只接受wire
型,不接受reg
型
Verilog的for
循环的初始化和判断不允许留空
寄存器最好声明一个初值,否则仿真伊始会出现x值影响触发器
Comments NOTHING