楼主在初学verilog的时候就一直对testbench该怎么写感到困惑,之后的学习过程中也陆陆续续地看过一些testbench文件,其中有一些其实相当于就在testbench里重写了一下要验证地模块,个人感觉这有点”鸡生蛋“和”蛋生鸡“那味儿了。虽说在testbench里写不用考虑能否综合的问题,可以用一些更为方便的写法,但是终归还是用的Verilog体系内的语法,描述待测试模块预期的功能时很有可能会犯类似的错误,因此楼主觉得这种testbench的写法应付比较简单的模块可能还行,比较简单直接,但是应对复杂点的模块则出错的可能性偏大,不太可取。
除了前述testbench的写法外,遍历穷举应该是常见的testbench写法之一,即借由高级语言穷举生成所有的输入和对应的输出,来比对模块的输出以达到验证的目的,因此本文就分享一个适用于这种穷举遍历思路的testbench模板,下面就借由一个原码输入原码输出的带饱和处理的减法器模块进行这一模板的写法说明。
原码减法器
选取被减数
A
A
A和减数
B
B
B的数据位宽为
4
4
4,输出结果
r
e
s
res
res的数据位宽也为4,设计框图如下
设计说明如下:
- 输入为原码(Sign-magnitude,SM)表示,而加减法一般习惯用二的补码(2’s compliment,2’s),因此先将 A A A和 B B B转换为补码形式,其中 B B B为被减数,可以考虑直接对(-B)进行补码转换,即上图中的红色减号,此时后续”减法“则转为加法进行;
- 二的补码加法比较简单,直接相加即可,本设计并不需要用到进位,因此可以只在模块内声明一下作为连线即可;
- 最后再进行溢出时的饱和处理,即溢出时符号位不变,幅值取最大值。具体到溢出的判断,本设计就不考虑最高位进位和次高位进位的溢出判断了,而是直接用输入和输出的符号位作为判断依据,比较直观。
Verilog模块
代码如下所示
/**
* @function:
* compute res = A - B and get sign-magnitude representation result
* @input:
* A, B -> sign-magnitude representation
* @output:
* res -> sign-magnitude representation
**/
module SmSub #(
parameter WIDTH = 4
) (
input [(WIDTH - 1) : 0] A,
input [(WIDTH - 1) : 0] B,
output [(WIDTH - 1) : 0] res
);
wire [(WIDTH - 1) : 0] tmpA; // magnitude part of A
wire [(WIDTH - 1) : 0] complA; // 2's complement representation of A
wire [(WIDTH - 1) : 0] complB; // 2's complement representation of B
wire [(WIDTH - 1) : 0] complRes; // 2's complement representation of res
reg [(WIDTH - 1) : 0] tmpRes; // for negative result
wire carry;
reg [(WIDTH - 1) : 0] satcomplRes; // saturated complRes
wire [(WIDTH - 1) : 0] negMax; // maximum negative value using 2's complement representation
// sign-magnitude -> 2's complement for A
assign tmpA = {1'b0, A[(WIDTH - 2):0]};
assign complA = (1'b1 == A[(WIDTH - 1)]) ? ((~tmpA[(WIDTH - 1):0]) + 1) : A[(WIDTH - 1):0];
// sign-magnitude -> 2's complement for (-B)
assign complB = (1'b1 == B[(WIDTH - 1)]) ? {1'b0, B[(WIDTH - 2):0]} : ((~B[(WIDTH - 1):0]) + 1);
// get result
assign {carry, complRes} = complA + complB;
// overflow process
always @(*) begin
case ({complA[(WIDTH - 1)], complB[(WIDTH - 1)], complRes[(WIDTH - 1)]})
// postive spillover
3'b001: begin
satcomplRes = {1'b0, {(WIDTH - 1){1'b1}}};
end
// negtive spillover
3'b110: begin
satcomplRes = {1'b1, {(WIDTH - 1){1'b1}}};
end
// no spillover
default: begin
// 2's complement -> sign-magnitude for complRes
tmpRes = (~complRes[(WIDTH - 1):0]) + 1;
satcomplRes = (1'b1 == complRes[(WIDTH - 1)]) ? {1'b1, tmpRes[(WIDTH - 2):0]} : complRes[(WIDTH - 1):0];
end
endcase
end
// handle special case N'b1000…000
assign negMax = {1'b1, {(WIDTH - 1){1'b0}}};
assign res = (negMax[(WIDTH - 1):0] == satcomplRes[(WIDTH - 1):0]) ? {1'b1, {(WIDTH - 1){1'b1}}} : satcomplRes[(WIDTH - 1):0];
endmodule
matlab生成数据
matlab代码如下所示,即穷举所有输入的情况,一共
2
4
×
2
4
=
256
2^4\times 2^4=256
24×24=256种,算出对应的输出,借由dec2bin()
函数转为二进制形式写入到result.txt
文件中,以供testbench读入。
注:下列代码整体风格其实与C很像,之所以不用C而是用matlab只是图matlab里有现成的十进制转二进制函数罢了……
dataWidth = 4;
fileName = "../result.txt";
fileID = fopen(fileName, "w");
dataNum = power(2, dataWidth);
magnitudeMax = power(2, dataWidth - 1) - 1;
result = zeros(dataNum, dataNum);
for i = 1 : dataNum
for j = 1 : dataNum
% convert to sign-magnitude representation
if (i <= power(2, dataWidth - 1))
tmpI = i - 1;
else
tmpI = power(2, dataWidth - 1) + 1 - i;
end
if (j <= power(2, dataWidth - 1))
tmpJ = j - 1;
else
tmpJ = power(2, dataWidth - 1) + 1 - j;
end
tmpRes = tmpI - tmpJ;
% saturated the results and convert to binary representation
if tmpRes < 0
if abs(tmpRes) >= magnitudeMax % saturated
fprintf(fileID, "%s\n", dec2bin(magnitudeMax + power(2, dataWidth - 1), dataWidth)); % sign-magnitude representation
else
fprintf(fileID, "%s\n", dec2bin(abs(tmpRes) + power(2, dataWidth - 1), dataWidth));
end
else
if tmpRes >= magnitudeMax % saturated
fprintf(fileID, "%s\n", dec2bin(magnitudeMax, dataWidth));
else
fprintf(fileID, "%s\n", dec2bin(tmpRes, dataWidth));
end
end
end
end
fclose(fileID);
testbench编写
本文的重头戏来了,先放代码
`timescale 1 ns/ 10 ps
`define period 10
module SmSub_tb();
/********** 1. ports and signal declaration **********/
reg [3:0] A;
reg [3:0] B;
wire [3:0] res;
reg [3:0] refResMem[0:(16 * 16 - 1)];
integer i;
integer j;
integer tmpIdx;
integer errCnt;
/********** 2. module instantiation and assignment **********/
SmSub #(
.WIDTH(4)
) uut0 (
.A(A),
.B(B),
.res(res)
);
/********** 3. Initialization **********/
initial begin
errCnt = 0;
$readmemb("./sim/result.txt", refResMem);
for (i = 0; i < 16; i = i + 1) begin
for (j = 0; j < 16; j = j + 1) begin
A = i; B = j;
#(`period / 2) checkOutputs;
#(`period / 2);
end
end
if (errCnt) begin
$display("***************** Total Errors: %d *****************\n", errCnt);
end
else begin
$display("***************** No Errors! *****************\n");
end
#(`period*10) $stop;
end
/********** 4. check part **********/
task checkOutputs;
begin
tmpIdx = (i * 16) + j;
if (res != refResMem[tmpIdx]) begin
error;
end
end
endtask
task error;
begin
errCnt = errCnt + 1;
$display("ERROR AT %d: expected -> %h, get -> %h", tmpIdx, refResMem[tmpIdx], res);
end
endtask
endmodule
说明如下:
- 第一部分就是testbench都有的端口和接下来要用的信号声明;
- 第二部分是测试模块的实例化,较为复杂点的模块可能还需要在testbench里额外添加一些连线;
- 第三部分为初始化,先将错误计数变量初始化为0,再用系统函数
$readmemb
/$readmemh
将前一步用matlab生成的参考输出读入,而考虑到本模块中的输入比较有规律,所以输入用整型变量i
和j
产生,若数据量较大或不规律,可以参照前一步,用高级语言生成对应的输入文件读入。再之后,每产生一个输入,就调用对应的checkOutputs
任务进行输出对比,如果存在错误则调用error
任务进行错误处理即打印出错信息到终端,并对错误进行计数。最后则是检查错误个数,打印对应的输出信息,并调用$stop
中止仿真。 - 第四部分即输出对比函数和错误处理函数,本设计并不复杂,所以只是简单的比对仅有的一个输出,错误处理也只是单纯地打印,并不算复杂;
总结
这一模板是楼主学Verilog以来感觉比较好用的一个,当然了,这种遍历穷举的验证方式在实际工程中还是有不小局限性的,输入比特数较大时往往就只能随机生成众多输入情况的一部分进行验证了,最终还是要借助于系统的验证方法学。尽管如此,个人感觉这一模板还是对初学Verilog的人来说还是比较友好的!