FPGA学习-testbench的设计 文件读取和写入操作
1. 激励的产生
对于 testbench 而言,端口应当和被测试的 module 一一对应。
端口分为 input,output 和 inout 类型产生激励信号的时候,
input 对应的端口应当申明为 reg,
output 对应的端口申明为 wire,
inout 端口比较特殊,下面专门讲解。
在FPGA整体设计中,测试环节占据着很重要的一个配置,即是逻辑设计在正确,没有测试的验证部分,就不能算是一个整体的FPGA设计流程,通过测试编写信号激励的测试文件,testbench文件,来验证逻辑正确性以及核心信号的行为;今天这篇文章告知大家,测试以及如何设计测试代码的书写以及编码规范。
1)直接赋值
一般用 initial 块给信号赋初值,initial 快执行一次,always 或者 forever 表示由事件激发反复执行。
举例,一个 module
`timescale 1ns/1ps
module exam();
reg rst_n;
reg clk;
reg data;
initial
begin
clk = 1'b0;
rst = 1'b1;
#10
rst = 1'b0;
#500
rst = 1'b1;
end
always
begin
#10 clk = ~clk;
end
endmodule
大家应该注意到有个#符号,该符号的意思是指延迟相应的时间单位。该时间单位由 timscale 决定.
一般在 testbench 的开头定义时间单位和仿真精度,比如`timescale 1ns/1ps
前面一个是代表时间单位,后面一个代表仿真时间精度。
以上面的例子而言,一个时钟周期是 20 各单位,也就是 20ns。
而仿真时间精度的概念就是,你能看到 1.001ns 时对应的信号值,
而假如 timescale 1ns/1ns,1.001ns 时候的值就无法看到。
对于一个设计而言,时间刻度应该统一,如果设计文件和 testbench 里面的时间刻度不一致,
仿真器默认以 testbench 为准。
一个较好的办法是写一个 global.v 文件,然后用 include 的办法,可以防止这个问题。
对于反复执行的操作,可写成 task,然后调用,比如
task load_count;
input [3:0] load_value;
begin
@(negedge clk_50);
$display($time, " << Loading the counter with %h >>", load_value);
load_l = 1’b0;
count_in = load_value;
@(negedge clk_50);
load_l = 1’b1;
end
endtask //of load_count
initial
begin
load_count(4’hA); // 调用 task
end
其他像 forever,for,function 等等语句用法类似,虽然不一定都能综合,但是用在 testbench 里面很方便,大家可以自行查阅参考文档
2) 文件输入
有时候,需要大量的数据输入,直接赋值的话比较繁琐,可以先生成数据,再将数据读入到寄存器中,
需要时取出即可。
用 $readmemb 系统任务从文本文件中读取二进制向量(可以包含输入激励和输出期望值)。
$readmemh 用于读取十六进制文件。例如:
reg [7:0] mem[256:1] // a 8-bit, 256-word 定义存储器 mem
initial $readmemh ( "E:/readhex/mem.dat", mem ) // 将.dat 文件读入寄存器 mem 中
initial $readmemh ( "E:/readhex/mem.dat", mem, 128, 1 ) // 参数为寄存器加载数据的地址始终
文件调入和打印中,我们 $fread $fwrite 用的更多一些, 大家可以自行查阅参考文档
2. 查看仿真结果
对于简单的 module 来说,要在 modelsim 的仿真窗口里面看波形,就用 add wave ..命令
比如, testbench 的顶层 module 名叫 tb,要看时钟信号,就用 add wave tb.clk
要查看所有信号的时候,就用 add wave /*
当然,也可以在 workspace 下的 sim 窗口里面右键单击 instance 来添加波形
对于复杂的仿真,免不了要记录波形和数据到文件里面去。
1)波形文件记录(这部分暂时我没用到呢!)
常见的波形文件一般有两种, vcd 和 fsdb, debussy 是个很好的工具,支持 fsdb,所以最好是 modelsim+debussy 的组
合默认情况下, modelsim 不认识 fsdb,所以需要先装 debussy,再生成 fsdb 文件。
$dumpfile 和$dumpvar 是 verilog 语言中的两个系统任务, 可以调用这两个系统任务来创建和将指定信息导入 VCD 文件.
对于 fsdb 文件来说,对应的命令是 fsdbDumpfile,dumpfsdbvars
(什么是 VCD 文件? 答:VCD 文件是在对设计进行的仿真过程中,记录各种信号取值变化情况的信息记录文件。EDA
工具通过读取 VCD 格式的文件,显示图形化的仿真波形,所以,可以把 VCD 文件简单地视为波形记录文件.)下面分别
描述它们的用法并举例说明之。
$dumpfile 系统任务:为所要创建的 VCD 文件指定文件名。
举例( "//"符号后的内容为注释文字):
initial
$dumpfile ("myfile.dump"); //指定 VCD 文件的名字为 myfile.dump,仿真信息将记录到此文件
$dumpvar 系统任务:指定需要记录到 VCD 文件中的信号,可以指定某一模块层次上的所有信号,也可以单独指定某一个信号。
典型语法为$dumpvar(level, module_name); 参数 level 为一个整数, 用于指定层次数, 参数 module 则指定要记录的模块。整句的意思就是,对于指定的模块,包括其下各个层次(层次数由 level 指定)的信号,都需要记录到 VCD 文件中去。
举例:
initial
$dumpvar (0, top); //指定层次数为 0,则 top 模块及其下面各层次的所有信号将被记录
initial
$dumpvar (1, top); //记录模块实例 top 以下一层的信号
//层次数为 1,即记录 top 模块这一层次的信号
//对于 top 模块中调用的更深层次的模块实例,则不记录其信号变化
initial
$dumpvar (2, top); //记录模块实例 top 以下两层的信号
//即 top 模块及其下一层的信号将被记录
假设模块 top 中包含有子模块 module1,而我们希望记录 top.module1 模块以下两层的信号,则语法举例如下:
initial
$dumpvar (2, top.module1); //模块实例 top.module1 及其下一层的信号将被记录
假设模块 top 包含信号 signal1 和 signal2(注意是变量而不是子模块), 如我们希望只记录这两个信号,则语法举例如下:
initial
$dumpvar (0, top.signal1, top.signal2); //虽然指定了层次数,但层次数是不影响单独指定的信号的
//即指定层次数和单独指定的信号无关
我们甚至可以在同一个$dumpvar 的调用中,同时指定某些层次上的所有信号和某个单独的信号,假设模块 top 包含信
号 signal1,同时包含有子模 块 module1,如果我们不但希望记录 signal1 这个独立的信号,而且还希望记录子模块 module1
以下三层的所有信号,则语法举例如下:
initial
$dumpvar (3, top.signal1, top.module1); //指定层次数和单独指定的信号无关
//所以层次数 3 只作用于模块 top.module1, 而与信号
top.signal1 无关
上面这个例子和下面的语句是等效的:
initial
begin
$dumpvar (0, top.signal1);
$dumpvar (3, top.module1);
end
$dumpvar 的特别用法(不带任何参数):
initial
$dumpvar; //无参数,表示设计中的所有信号都将被记录最后,我们将$dumpfile 和$dumpvar 这两个系统任务的使用方法在下面的例子中综合说明,假设我们有一个设计实例,名为 i_design,此设计中包含模块 module1,模块 module1 下面还有很多层次,我们希望对这个设计进行仿真,并将仿真过程中模块 module1 及其以下所有层次中所有信号的变化情况,记录存储到名为 mydesign.dump 的 VCD 文件中去,则例示如下:
initial
begin
$dumpfile ("mydesign.dump"); //指定 VCD 文件名为 mydesign.dump
$dumpvar (0, i_design.module1); //记录 i_design.module1 模块及其下面层次中所有模块的所有信号
end
对于生成 fsdb 文件而言,也是类似的
initial
begin
$fsdbDumpfile("tb_xxx.fsdb");
$fsdbDumpvars(0,tb_xxx);
end
2)文件输出结果
integer out_file; // out_file 是一个文件描述,需要定义为 integer 类型
out_file = $fopen ( " cpu.data " ); // cpu.data 是需要打开的文件,也就是最终的输出文本
设计中的信号值可以通过$fmonitor, $fdisplay,$fwrite
其中$fmonitor 只要有变化就一直记录, $fdisplay 和$fwrite 需要触发条件才记录
例子:
initial begin
$fmonitor(file_id, "%m: %t in1=%d o1=%h", $time, in1, o1);
end
always@(a or b)
begin
$fwrite(file_id,"At time%t a=%b b=%b",$realtime,a,b);
end
3 参考“A Verilog HDL Test Bench Primier.pdf”
1) DUT(Design Under Test)部分
//-------------------------------------------------
// File: count16.v
// Purpose: Verilog Simulation Example
//-------------------------------------------------
`timescale 1 ns / 100 ps
module count16 (
clk,
rst_n,
load_l,
enable_l,
cnt_in,
oe_l,
count,
count_tri
);
input clk;
input rst_n;
input load_l;
input enable_l;
input [3:0] cnt_in;
input oe_l;
output [3:0] count;
output [3:0] count_tri;
reg [3:0] count;
// tri-state buffers
assign count_tri = (!oe_l) ? count : 4'bZZZZ;
// synchronous 4 bit counter
always @ (posedge clk or negedge rst_n)
if (!rst_n) begin
count <= 4'd0;
end
else begin
if (!load_l) begin
count <= cnt_in;
end
else if (!enable_l) begin
count <= count + 1;
end
end
endmodule //of count16
2) Test Bench
//-------------------------------------------------
// File: tb.v
// Purpose: Verilog Simulation Example
// Test Bench
//-----------------------------------------------------------
`timescale 1ns / 100ps
module tb ();
//---------------------------------------------------------
// inputs to the DUT are reg type
reg clk_50;
reg rst_n;
reg load_l;
reg enable_l;
reg [3:0] count_in;
reg oe_l;
//--------------------------------------------------------
// outputs from the DUT are wire type
wire [3:0] cnt_out;
wire [3:0] count_tri;
//----------------------------------------------------------
// create a 50Mhz clock
always #10 clk_50 = ~clk_50; // every ten nanoseconds invert
//-----------------------------------------------------------
// initial blocks are sequential and start at time 0
initial
begin
$display($time, " << Starting the Simulation >>");
clk_50 = 1'd0;
// at time 0
rst_n = 0;
// reset is active
enable_l = 1'd1;
// disabled
load_l = 1'd1;
// disabled
count_in = 4'h0;
oe_l = 4'b0;
// enabled
#20 rst_n = 1'd1;
// at time 20 release reset
$display($time, " << Coming out of reset >>");
@(negedge clk_50); // wait till the negedge of
// clk_50 then continue
load_count(4'hA);
// call the load_count task
@(negedge clk_50);
$display($time, " << Turning ON the count enable >>");
enable_l = 1'b0;
// turn ON enable
// let the simulation run,
// the counter should roll
wait (cnt_out == 4'b0001); // wait until the count
// equals 1 then continue
$display($time, " << count = %d - Turning OFF the count enable >>",cnt_out);
enable_l = 1'b1;
#40;
// let the simulation run for 40ns
// the counter shouldn't count
$display($time, " << Turning OFF the OE >>");
oe_l = 1'b1;
// disable OE, the outputs of
// count_tri should go high Z.
#20;
$display($time, " << Simulation Complete >>");
$stop;
// stop the simulation
end
//--------------------------------------------------------------
// This initial block runs concurrently with the other
// blocks in the design and starts at time 0
initial begin
// $monitor will print whenever a signal changes
// in the design
$monitor(
$time,
" clk_50=%b, rst_n=%b, enable_l=%b, load_l=%b, count_in=%h, cnt_out=%h, oe_l=%b, count_tri=%h",
clk_50, rst_n, enable_l, load_l, count_in, cnt_out, oe_l, count_tri
);
end
//--------------------------------------------------------------
// The load_count task loads the counter with the value passed
task load_count;
input [3:0] load_value;
begin
@(negedge clk_50);
$display($time, " << Loading the counter with %h >>", load_value);
load_l = 1'b0;
count_in = load_value;
@(negedge clk_50);
load_l = 1'b1;
end
endtask //of load_count
//---------------------------------------------------------
// instantiate the Device Under Test (DUT)
// using named instantiation
count16 count16_m0 (
.clk (clk_50),
.rst_n (rst_n),
.load_l (load_l),
.cnt_in (count_in),
.enable_l (enable_l),
.oe_l (oe_l),
.count (cnt_out),
.count_tri (count_tri)
);
//---------------------------------------------------------
// read and write data
reg [7:0] mem[10:1];//read data from file
initial $readmemh ("F:/IC/prj/testbench/prj0/data/mem.dat", mem ); // 将.dat 文件读入寄存器 mem 中
//设计中的信号值可以通过$fmonitor, $fdisplay,$fwrite
//其中$fmonitor 只要有变化就一直记录, $fdisplay 和$fwrite 需要触发条件才记录
integer file_out; // out_file 是一个文件描述,需要定义为 integer 类型
initial file_out = $fopen("F:/IC/prj/testbench/prj0/data/wr_mem.dat", "w");
// wr_mem.dat 是需要打开的文件,也就是最终的输出文本
always @(posedge clk_50)
if (/*tb.count16_m0.*/enable_l == 1'd0) begin
$fwrite (file_out, "%h\n", cnt_out[3:0]);
// $fdisplay(file_out, "%h", cnt_out[3:0]);
end
endmodule //of cnt16_tb
3) sim.do文件
#Time: 2016-07-26
#By : times_poem
quit -sim
cd F:/IC/prj/testbench/prj0
if [file exists work] {
vdel -all
}
vlib work
vlog ./*.v
vlog ./src/*.v
vsim -t ps -novopt work.tb
log -r /*
do wave.do
run -all
如何用FPGA加速卷积神经网络(CNN)?
雷锋网(公众号:雷锋网) AI科技评论按,本文来源于王天祺在知乎问题【如何用FPGA加速卷积神经网络(CNN)?】下的回答,雷锋网 AI科技评论获其授权转发。
以下主要引用自西安邮电大学李涛老师关于连接智能和符号智能的报告,以及fpl2016上ASU的 Yufei Ma的文章和slide
Scalable and Modularized RTL Compilation of Convolutional Neural Network onto FPGA
地址:http://fpl2016.org/slides/S5b_1.pdf
推荐去读下原文
我做过一些计算加速的工作,个人感觉要入手先要想好几个问题: 要加速的是什么应用,应用的瓶颈是什么,再针对这个瓶颈,参考前人工作选择合适的方案。
过早地执着于fpga的技术细节(用hdl还是hls,用啥芯片,用啥接口)容易只见树木不见森林。现在software define network/flash/xxx,已然大势所趋。之前开组会时跟同志们聊过,算法是纲,纲举目张;软件是妈,软件是爹,软件比基金委都亲。所以推荐先把cnn的算法看一下,拿一些开源代码跑一下经典的例子(lenet, alexnet, etc)看好输入输出,摸清算法。
比如以下是一个lenet的cPP和opencl的实现
nachiket/papaa-opencl
地址:https://github.com/nachiket/papaa-opencl
以下图片源自Yufei Ma的Slide
可以看到cnn算法主要由conv ,pooling,norm等几个部分组成。工作时将image跟weight灌进去,最终得到预测结果。
接下来拿profiler(比如perf)去分析下软件算法,找找热点和性能瓶颈。在cnn里面主要耗时的就是conv二维卷积了。性能瓶颈也主要在于卷积时需要大量乘加运算,参与计算的大量weight参数会带来的很多访存请求。
接下来考察下前人的工作和当前的灌水热点。按理说这种大量的乘加运算用dsp应该不错,但是在cnn中大家并不需要这么大的位宽,有时候8位就够了。dsp动辄32/64位的乘加器实在是浪费。于是乎大家就开始减位宽,多堆几个运算单元。面对大量的访存请求,大家就开始设计各种tricky的缓存了。
以下是大家的一些灌水方向
于是就有了以下各路硬件设计
有人照着dsp风格去设计加速器
ceva也出了一系列面向CNN的IP
有人用了脉动阵列或者Dataflow的风格
有人设计了专用的芯片比如计算所的Cambricon
还有的就是你提到的fpga
所有的事情到了硬件层面实际上能用的手段也就有限了。不外乎堆资源和切流水两招。再不然就是做一些bit level的小技巧,比如乘法器变查表之类的,这些技巧在很多二十年前的dsp教材里面都描述得很细致了,拿来用就好。比如这本书亲测有效。
VLSI Digital Signal Processing System--Design and Implementation by Keshab
典型的fpga实现可以参考Yufei Ma的文章,不论是conv,还是pooling,依葫芦画瓢设计data path,切好流水,再想好状态机加上控制信号。这些就看大家撸rtl的基本功了。
比如Conv模块如下图,主要拿一堆乘法器以及加法器树搭好data path,切好流水,接着加上控制信号。
Pooling也是大同小异
还有Norm
最后把这些模块通过router连接,外面再套一层控制模块,封成ip就好了。
剩下的就是集成进你的系统(microblaze, nios还是arm,配好dma,写好灌数据的驱动,这些就是各有各的道儿了)。推荐动手码rtl前先写好文档,约定好端口,寄存器和软件api,否则边写边改容易乱。
整体来说,cnn这种应用流水线控制相对cpu简单,没有写cpu的那一堆hazard让人烦心,也不用写汇编器啥的。太大的cnn放在fpga里挺费劲,做出创新很难,但是fpga上写个能用的lenet这种级别的cnn还是挺容易的。最后还可以依照惯例跟cpu比性能,跟gpu比功耗。
雷锋网版权文章,未经授权禁止转载。详情见转载须知。
相关问答
麻烦好基友们有谁懂:濮阳PR900窄带 数字 中转台怎么选,PR900...[回答]转台A轴编程时,A后所跟数字值代表转台旋转角度。如G91G00A30;表示:转台逆时针快速旋转30°G90G01A-30F100;表示:转台以100mm/min速度旋转到-30...