在此感谢 特权同学,接下来写到的FPGA杂记基本上源于他的B站讲解视频,同时这是他的淘宝商铺链接,感兴趣的同学也可以在B站中看到他更多的一些视频学习资料,一起学习一起成长
1. 显示和监视任务
1.1 显示任务 $display 以及 $write
两者的语法结构大概都是:任务名(“可选字符串+格式”,信号列表)
- 任务名包括$displayb(二进制)、 $displayh(十六进制)、 $displayo(八进制)以及 $write的其他任务名
- 可选字符串+格式包括%h(16进制输出)、%c(ASCII输出)、%b(二进制输出)、%s(字符串输出)、%t(以当前时间格式输出显示)等等
- display和write的区别在于display输出后会自动换行
%0d,加个0会使得输出的数据前面的零或者空格删掉(一般发生在输出要求位数大于实际输出位数的情况下)例如,32位的数据以十进制显示为65,用显示任务不加0的结果是:____(空格)65,但若是要求为%0d,那么输出结果就是65
1.2 监视任务$monitor
$monitor任务在声明后默认开启,只有在其运行期间调用系统任务 $monitoroff 则会关闭监视任务知道系统再次调用系统任务 $monitoron后将重新开启监视
以上两种主要在testbench中调用任务,方便在测试中验证自己的设计、逻辑功能的实现
2. 4位格雷码计数器设计
2.1 设计实现
设计目标:以1S为一个计数周期(注意不是系统时钟),计数值从0一直往上叠加,并将计数值不断转换成格雷码输出
`timescale 1ns/1ps
module vlg_design(
input i_clk, //时钟为50MHz,20ns
input i_rst_n,
output[3:0] o_gray
);
/
//1s定时计数
localparam TIMER_1S_MAX_CNT = 32'd1_000_000_000/20; //50MHZ的时钟在1s内的最大周期数,1S标志位
reg[31:0] r_cnt;
always @(posedge i_clk)
if(!i_rst_n) r_cnt <= 'b0;
else if(r_cnt < (TIMER_1S_MAX_CNT - 1)) r_cnt <= r_cnt + 'b1;
else r_cnt <= 'b0;
//
//1s计数器
reg[3:0] r_second;
always @(posedge i_clk)
if(!i_rst_n) r_second <= 'b0;
else if(r_cnt == (TIMER_1S_MAX_CNT-1)) r_second <= r_second + 'b1;
else ;
/
//r_second译码为格雷码输出
reg[3:0] r_gray;
always @(posedge i_clk)
if(!i_rst_n) r_gray <= 'b0;
else begin
case(r_second)
4'b0000: r_gray <= 4'b0000;
4'b0001: r_gray <= 4'b0001;
4'b0010: r_gray <= 4'b0011;
4'b0011: r_gray <= 4'b0010;
4'b0100: r_gray <= 4'b0110;
4'b0101: r_gray <= 4'b0111;
4'b0110: r_gray <= 4'b0101;
4'b0111: r_gray <= 4'b0100;
4'b1000: r_gray <= 4'b1100;
4'b1001: r_gray <= 4'b1101;
4'b1010: r_gray <= 4'b1111;
4'b1011: r_gray <= 4'b1110;
4'b1100: r_gray <= 4'b1010;
4'b1101: r_gray <= 4'b1011;
4'b1110: r_gray <= 4'b1001;
4'b1111: r_gray <= 4'b1000;
default: ;
endcase
end
assign o_gray = r_gray;
endmodule
看整个运行过程,各部分都会定义一个寄存器暂时储存输出结果,最后再进行赋值操作
2.2 测试脚本testbench
`timescale 1ns/1ps
module testbench_top();
//参数定义
`define CLK_PERIORD 20 //时钟周期设置为20ns(50MHz)
//接口申明
reg clk;
reg rst_n;
wire[3:0] o_gray;
//对被测试的设计进行例化
vlg_design uut_vlg_design(
.i_clk(clk),
.i_rst_n(rst_n),
.o_gray(o_gray)
);
//复位和时钟产生
//时钟和复位初始化、复位产生
initial begin
clk <= 0;
rst_n <= 0;
#1000;
rst_n <= 1;
end
//时钟产生
always #(`CLK_PERIORD/2) clk = ~clk;
//测试激励产生
integer i;
initial begin
@(posedge rst_n); //等待复位完成
$monitor("o_gray is %b at %0dns",o_gray,$time);
$stop;
end
endmodule
2.3 测试结果
3. 基于查找表的8位格雷码转换
3.1 查找表LUT
查找表是相当于预先存储好结果的数据表,免去了运算过程,从另外一种角度来说相当于ROM。我们接下来利用IP的ROM加入查找表,通过VIVADO创建出ROM的初始化COE文件用于存放LUT的数据
3.2 ROM初始化COE文件
COE文件用于对ROM作初始化赋值,具体格式如下
memory_initialization_radix = 位数(数据格式);
memory_initialization_vector = 初始化数据; //查找表数据
COE文件制作以及使用ROM流程:
- 打开vivado工程文件点击IP Catalog选择生成ROM(以Block Memory Generator为例)
- 将打开的ROM的Memory Type改成Single Port ROM
- 在Port A Options里面将位宽与数据个数改成相应的数据,同时使能always
- 在Other Options中导入COE文件,这里由于没有COE文件可以在线创建一个即可
- 然后点击OK会弹出一个generate的窗口直接生成即可
- 回到vivado界面的Sources里面的IP Sources中查看你所配置IP的Instantiation Template中的原语 .veo 文件,并将原语用于你所设计的verilog代码中
这里要注意一下,只要在IP中勾选了Primitives Output Register,那么就意味着你会延时两个时钟周期输出数据,在Summary中也可查看
3.3 设计代码.v
`timescale 1ns/1ps
module vlg_design(
input i_clk,
input i_rst_n,
input i_en,
input[7:0] i_data,
output o_vld, //有效信号
output[7:0] o_gray
);
//
//o_vld是i_en两个时钟周期的延时
reg[1:0] r_vld;
always@(posedge i_clk)
if(!i_rst_n) r_vld <= 'b00;
else r_vld <= {r_vld[0],i_en};
assign o_vld = r_vld[1];
//IP ROM_LUT
blk_mem_gen_0 uut_blk_mem_gen_0 (
.clka(i_clk), // input wire clka
.addra(i_data), // input wire [7 : 0] addra
.douta(o_gray) // output wire [7 : 0] douta
);
endmodule
3.4 测试脚本
`timescale 1ns/1ps
module testbench_top();
//参数定义
`define CLK_PERIORD 20 //时钟周期设置为20ns(50MHz)
//接口申明
reg clk;
reg rst_n;
reg i_en;
reg[7:0] i_data;
wire o_vld;
wire[7:0] o_gray;
//对被测试的设计进行例化
vlg_design uut_vlg_design(
.i_clk(clk),
.i_rst_n(rst_n),
.i_en(i_en),
.i_data(i_data),
.o_vld(o_vld),
.o_gray(o_gray)
);
//复位和时钟产生
//时钟和复位初始化、复位产生
initial begin
clk <= 0;
rst_n <= 0;
#1000;
rst_n <= 1;
end
//时钟产生
always #(`CLK_PERIORD/2) clk = ~clk;
//测试激励产生
initial begin
i_en <= 'b0;
i_data <= 'b0;
@(posedge rst_n); //等待复位完成
@(posedge clk);
i_en <= 'b1;
i_data <= 'b0;
@(posedge clk);
repeat(255) begin //循环叠加256次
i_data <= i_data + 1;
@(posedge clk);
end
i_en <= 'b0;
i_data <= 'b0;
#1000;
$stop;
end
//
//实时显示
always@(posedge clk) begin
if(o_vld) $display("%b",o_gray);
else ;
end
endmodule
4. 格雷码转换的快速算法
4.1 算法介绍
- 二进制码(自然数)的最高位是MSB,最低位是0
- 二进制码的MSB位直接赋值给格雷码的MSB位
- 对于次高位(MSB-1)位到最低位0,使用二进制码相邻位异或的方式获取格雷码
- 若二进制码字的第i位与第i+1位相同,则第 i 位的格雷码为0
- 若二进制码字的第i位与第i+1位不同,则第 i 位的格雷码为1
举个例子:4位的二进制码1010转换格雷码(不通过LUT,直接通过RTL实现)
位数 | 操作 | 按位输出格雷码 |
---|---|---|
Bit3 | 取MSB位 | 1 |
Bit2 | 1 xor 0 | 1 |
Bit1 | 0 xor 1 | 1 |
Bit0 | 1 xor 0 | 1 |
最终输出结果:1111
4.2 模块设计
`timescale 1ns/1ps
module vlg_design #
(parameter MSB = 7) //参数定义
(
input i_clk,
input i_rst_n,
input i_en,
input[MSB:0] i_data,
output reg o_vld,
output reg[MSB:0] o_gray
);
always @(posedge i_clk)
if(!i_rst_n) o_vld <= 'b0;
else o_vld <= i_en;
//快速转换设计
always @(posedge i_clk)
o_gray[MSB] <= i_data[MSB];
genvar i;
generate
for(i=MSB-1;i>=0;i=i-1) begin
always @(posedge i_clk) begin
o_gray[i] <= i_data[i] ^ i_data[i+1];
end
end
endgenerate
endmodule
4.3 测试脚本
`timescale 1ns/1ps
module testbench_top();
//参数定义
`define CLK_PERIORD 10 //时钟周期设置为10ns(100MHz)
parameter GRAY_MSB = 7; //定义参数
//接口申明
reg clk;
reg rst_n;
reg i_en;
reg[GRAY_MSB:0] i_data;
wire o_vld;
wire[GRAY_MSB:0] o_gray;
//对被测试的设计进行例化
vlg_design
#(.MSB(GRAY_MSB))
uut_vlg_design(
.i_clk(clk),
.i_rst_n(rst_n),
.i_en(i_en),
.i_data(i_data),
.o_vld(o_vld),
.o_gray(o_gray)
);
//复位和时钟产生
//时钟和复位初始化、复位产生
initial begin
clk <= 0;
rst_n <= 0;
#1000;
rst_n <= 1;
end
//时钟产生
always #(`CLK_PERIORD/2) clk = ~clk;
//测试激励产生
initial begin
i_en <= 'b0;
i_data <= 'b0;
$display("The value of GRAY_MSB is %0d",GRAY_MSB);
@(posedge rst_n); //等待复位完成
@(posedge clk);
i_en <= 'b1;
i_data <= 'b0;
repeat(2**(GRAY_MSB+1)-1) begin //一共有(MSB+1)位,相当于计数值最大为2^(MSB+1)-1
@(posedge clk);
i_en <= 'b1;
i_data <= i_data + 1;
end
@(posedge clk);
i_en <='b0;
$stop;
end
always @(posedge clk)
if(o_vld) $display("%b",o_gray);
else ;
endmodule
4.4 测试结果
脚本显示(modelsim):
测试波形: