Verilog HDL
要制作CPU,我没有选择使用分立元件构造,我选择使用了使用FPGA+代码的方式来实现。所以要学习Verilog HDL语言。这是一种HDL(硬件描述语言),可以进行抽象度很高的RTL电路设计。
设计电路的步骤
电路描述
Verilog HDL使用模块来设计一个功能单位的逻辑,模块是Verilog HDL中最基本的构成单位。
模块的声明
模块声明语法:
module<module name>{
<输入信号定义>
<输入信号定义>
...
};
<电路描述>
endmodule
比如使用模块来描述一个32位的加法器
module adder{
input wire [31:0] in_0, //输入0
input wire [31:0] in_1, //输入1
output wire [31:0] out //输出
};
assign out = in_0 + in_1; //将in_0和in_1相加结果代入out中
endmodule
模块的实例化
模块的实例化语法:
<模块名> <实例名>(
.<相连的端口名>(相连的信号名),
.<相连的端口名>(相连的信号名),
...
);
adder adder01(
.in_0 (adder01_in_0), //adder01_in_0信号连接到in_0端口
.in_1 (adder01_in_1), //adder01_in_1信号连接到in_1端口
.out (adder01_out)
);
逻辑值和常数表达
逻辑值表达:
常数表达:
变量的声明与数据类型
变量声明
数据类型和变量名是必要项目,其他项可以省略。符号和位宽如果省略则根据数据类型设置为默认值,元素数省略默认声明元素数为1的变量。
数据类型
数据类型有寄存器型和网络型两种。寄存器类型是可以保存上次写入数据的数据类型,根据程序的不同可以生成锁存器、触发器等存储元件,也可能生成组合电路。
寄存器型变量
寄存器型变量会在always和initial语句中实现过程赋值。过程赋值分为阻塞式和非阻塞式赋值两种。
阻塞式赋值:按照代码顺序进行赋值的方式。在先赋值的代码赋值完成之前阻塞后续代码的赋值,因此得名阻塞式赋值。使用=
非阻塞赋值:所有代码不会相互阻塞,同时进行赋值。使用<=
在一个过程块中,两种方式只能使用其中一种
网络型变量
网络型是用来描述模块和寄存器间连接的数据类型。网络型只描述信号的传递不持有数据。
网络型变量可以在assign语句(分配语句)或声明语句中实现连续赋值,连续赋值就是进行连续的赋值。
运算符
运算符和运算符优先级
运算符特殊使用方法
条件分支语句if和case
略
循环语句for和while
略
always过程块
always过程块是为了描述过程块而存在的语句。语法如下:
如果指定always语句中的事件表达式时,所指定的事件触发时执行其中的语句序列。事件可以是特定信号的变化、信号的上升沿、下降沿等。
如果always语句使用常数,则会在每次经过该常数时,执行一次always中的语句序列。
这个功能主要是在仿真时使用。always过程中可以使用寄存器变量赋值、if、case、for、while等语句。
always语句描述组合电路
使用always语句描述组合电路时,事件表达式描述方式如下:
组合电路描述中锁存器的推定和Don’t care
使用always语句描述组合电路时,如果信号未被赋值,有可能会引入锁存器。寄存器推定发生的原因是不完整的case语句或者没有else的if语句。为了规避这个问题,一定要将case语句或者if…else…写全。
也存在这种情况:确定不存在case和if语句设定之外的条件,或者设定条件之外随便输出什么都可以。这种情况称为don’t care.方法就是在default中为输出赋予不定值。
always语句描述时序电路
always描述时序电路时,事件表达式描述方法如图:
时序电路含有触发器等存储元件,基本上都是按照时钟同步执行。因此事件表达式中要指定时钟的信号边沿和时钟信号名。
时钟信号边沿:
- 确定在时钟信号上升或下降时触发电路动作。上升记为posedge,下降记为negedge,然后描述信号名。
- 事件表达式还可以使用or列举多个条件。
- 为存储元件设置异步复位(reset)信号时,除了时钟信号还要写上复位信号的边沿和信号名
预处理
预处理是在代码编译前对其进行预先处理的程序。Verilog HDL中的预处理可以实现宏定义和条件编译。预处理使用编译指示符可对编译器进行各种控制。
编译指示符以后引号(`)开头。
- `include语句:插入引用文件
- `define语句:进行宏定义(Verilog HDL中为了区分宏与变量名,宏的名字前面也加后引号(`),代码中使用宏,要像`BYTE_DATA_W一样描述。)
- `ifdef & `ifndef:实现条件编译。
- `ifdef:在指定的宏存在的条件下,执行`ifdef到`endif之间的代码
- `ifndef:在指定的宏不存在的条件下,执行`ifndef & `endif之间的代码
- 两者都可以使用`else指定不满足条件时执行的动作
Verilog HDL程序实例
设计目标
MISP CPU(32个32位的寄存器堆)
Q:什么是寄存器堆?
A:寄存器堆顾名思义,就是多个寄存器组合成的一个集合
为了方便访问寄存器堆中的寄存器,对他们进行统一编码,称为寄存器号或者寄存器地址。
该寄存器堆有:
- 2个读端口
- 1个写端口
- 异步读取,同步写入
程序代码
/*regfile.h*/
`ifndef __REGFILE_HEADER__
`define __REGFILE_HEADER__
/*信号电平*/
`define HIGH 1'b1 //高电平
`define LOW 1'b0 //低电平
/*逻辑值*/
`define ENABLE_ 1'b0 //有效
`define DISABLE_ 1'b1 //无效
/*数据*/
`define DATA_W 32 //数据宽度
`define DataBus 31:0 //数据总线
`define DATA_D 32 //数据深度
/*地址*/
`define ADDR_W 5 //地址宽度
`define AddrBus 4:0 //地址总线
`endif
/*regfile.v*/
/*引用头文件*/
`include "regfile.h"
/*模块声明*/
module regfile{
/*时钟和复位*/
input wire clk, //时钟
input wire reset_, //异步复位(低电平有效)
/*访问接口*/
input wire [`AddrBus] addr, //地址总线输入 5b
input wire [`DataBus] d_in, //数据总线输入 32b
input wire we_, //写入有效(低电平有效)
output wire [`DataBus] d_out //数据总线输出 5b
};
/*内部信号声明*/
reg [`DataBus] ff [`DATA_D-1:0]; //寄存器序列 32
integer i; //迭代器
/*读取访问*/
assign d_out = ff[addr];
/*写入访问*/
always @(posedge clk or negedge reset_) begin
if (reset_ == `ENABLE_) begin //异步复位
for(i=0;i<`DATA_W;i=i+1) begin
ff[i] <= #1 {`DATA_W{1'b0}}; //#:延迟(ns),延迟1ns将ff清零
end
end else begin
if(we_ == `ENABLE_) begin //同步写入
ff[addr] <= #1 d_in
end
end
end
endmodule
电路仿真
使用Verilog HDL不仅可以设计电路,还可以对所设计的电路进行仿真。仿真可以实现逻辑验证,从而测试设计好的电路能否正常工作。记述仿程序的文件称为Testbench。
Testbench的构造
Testbench是对制作的电路进行仿真、测试的模块。通常,输入端口为了将值代入,使用寄存器型变量,而输出信号接网络型变量对输出值进行观测。Testbench中,使用initial语句生成测试用例,然后观测模块的输出。
`timescale 1ns/1ps //设定timescale
/*定义Testbench模块。无输入输出端口。
因为是一个抽象的测试模型,所以没有实际的输入输出。*/
module test_bench;
reg adder01_in_0; //(simple)输入0
reg adder01_in_1; //(simple)输入1
wire adder01_out; //(simple)输出
adder adder01( //实例化模块
.in_0 (adder01_in_0),
.in_1 (adder01_in_1),
.out (adder01_out)
);
initial begin
/*记述测试用例*/
end
endmodule
timescale
`timescale用来设定仿真执行时间单位,使用数字和单位指定单位时间和时间精度。
单位时间:指定仿真的一个单位时间相当于多少秒
时间精度:表示仿真处理的时间精度,根据时间精度取数值的近似值(没有必要取过小的时间精度,这会延长仿真时间)
单位时间和时间精度的关系必须满足:单位时间>=时间精度
`timescale <单位时间>/<时间精度>
initial语句生成测试用例
initial语句是在仿真开始时只执行一次的语句,或是理解为仿真初始化设置。initial语句和延迟描述组合,可以用来生成测试用例。
【格式】
initial begin
... //过程的描述
end
【例】
initial begin
#0 begin //时刻0 执行
...
end
#10 begin //时刻10 执行
...
end
#10 begin //时刻20 执行
...
end
end
延迟语句
# 字符用来记述延迟语句。延迟语句中的指定的数值意味着`timescale中设定单位时间的个数。
【格式】
# <常数表达式>
【例】
always @(*) begin
a = #10 b; //b在10个单位时间延迟后赋值给a
end
initial begin //initial 语句只在时刻0时执行一次
#0 c = 1'b1 //c在时刻0时为1
#10 c = 1'b0 //c在时刻10时为0
#10 c = 1'b1 //c在时刻20时为1
#10 c = 1'b0 //c在时刻20时为0
end
延迟语句只用在仿真程序中,用来在特定时间延迟后施加信号并生成测试用例。不会对逻辑综合的结果产生影响。
时钟的生成
如果被测模块要用到时钟,需要在Testbench中生成时钟信号。
always #10 begin
clk <= ~clk //每经过10个单位时间clk的值翻转一次
end
initial begin
#0 begin
clk <= 1'b0 //clk在时刻0时被初始化
end
...
end
系统任务
通过使用Verilog HDL预置的系统任务,可以达到控制仿真、输出字符串的目的。
- $display(“含有格式的字符串”,…)
- $write(“含有格式的字符串”,…)
- $time
- $finish
载入存储镜像
仿真时,有时候需要向存储器等读入预先准备好的数据。可以使用$readmemh系统任务从文件中读入数据并设置存储器。
波形的输出
仿真时的信号变化可以输出到波形文件中。波形文件有很多种,我们使用VCD格式波形文件的输出方法。
VCD文件的输出使用$dumpfile和$dumpvars两个系统任务来实现。
【格式】
$dumpfile(<文件名>)
$dumpvars(<开始时刻>,<输出波形的模块名或信号名>)
【例】
initial begin
$dumpfile("test.vcd"); //将波形输出到test.vcd文件
$dumpvars(0,test); //从时刻0开始输出模块test的波形
end
在initial中调用$dumpfile和$dumpvars可以实现波形文件的输出
Testbench实例
`timescale 1ns/1ps
`include "regfile.h"
module regfile_test;
/*定义内部信号*/
reg clk;
reg reset_;
reg [`AddrBus] addr;
reg [`DataBus] d_in;
reg we_
wire [`DataBus] d_out;
integer i;
parameter STEP = 100.0000; //100ns
/*生成频率为10MHz的时钟*/
always #(STEP/2) begin
clk <= ~clk;
end
/*实例化测试模块*/
regfile regfile(
.clk (clk),
.reset_ (reset_),
.addr (addr),
.d_in (d_in),
.we_ (we_),
.d_out (d_out)
);
/*测试用例*/
initial begin
/*信号线初始化*/
#0 begin
clk <= `HIGH;
reset_ <= `ENABLE_;
addr <= {`ADDR_W{1'b0}};
d_in <= {`DATA_W{1'b0}};
we_ <= `DISABLE_;
end
#(STEP*3/4)
/*解除复位信号*/
#STEP begin
reset_ <= `DISABLE_;
end
/*对寄存器堆的读写进行验证*/
#STEP begin
for(i=0;i<`DATA_D;i=i+1) begin
//地址为i的寄存器中写入i
#STEP begin
addr <= i;
d_in <= i;
we_ <= `ENABLE_;
end
//地址为i的寄存器中读出i
#STEP begin
addr <= {`ADDR_W{1'b0}};
d_in <= {`DATA_W{1'b0}};
we_ <= `DISABLE_;
//使用if语句检测结果
if(d_out == i) begin
$display($time," ff[%d] Read/Write Check OK!",i);
end else begin
$display($time," ff[%d] Read/Write Check NG!",i);
end
end
end
end
#STEP begin
$finish;
end
end
/*输出波形*/
initial begin
$dumpfile("regfile.vcd");
$dumpvars(0,regfile);
end
endmodule
使用Icarus Verilog进行仿真
使用Icarus Verilog进行仿真,首先需要用iverilog命令对原编码进行编译。为iverilog命令的参数指定正确的选项和源代码文件,执行后就会输出编译后的文件(.out文件)。iverilog命令的选项如表所示:
编译之后,使用vvp命令来执行仿真。vvp的参数中需要指定iverilog命令所输出的文件。vvp命令执行后,就会按照Testbench中记述的测试序列进行仿真。如果Testbench有波形输出,就会输出波形文件。
iverilog -s regfile_test -o regfile_test.out regfile_test.v regfile.v
生成regfile_test.out文件
vvp regfile_test.out
vvp进行仿真,参数设置为iverilog的输出文件。画面中出现Testbench中的输出信息。Testbench执行后的波形文件输出到了regfile.vcd中。