Verilog实战二
1、模块化设计
1)module 和 port
#1、module
模块module ,是Verilog 的基本描述单位,module后的内容描述某个设计的功能和结构,及其与其他模块通信的外部端口。
module 后接用户定义的模块名,括号内为模块中使用的端口列表,模块以module 开始以endmodule 结束。
一个工程中通常包含多个设计模块,模块间通过“例化(Instantition)” 实现接口的数据交互。多模块使得设计具备一定的层级结构。处于最上层的模块称为“顶层模块(top-level module)”,顶层模块引出的端口将会直接分为定义到FPGA器件的物理引脚上,和FPGA外部的芯片进行通信,所有其他的设计模块都会直接或者间接与顶层模块实现数据交互。
下层模块称之为其上层模块的“子模块”
语法:
module [模块名] ([端口列表]);
[逻辑功能]
endmodule
#2、port
端口port,用于设计模块对外的接口信号的描述,即不同模块之间通过port的连接定义实现数据交互,整个设计工程的顶层模块通过port定义FPGA器件与外部芯片的接口信号。
FPGA设计中的每个模块通常都必须有port定义,只有对设计进行仿真验证的测试脚本的顶层模块可以无需port定义。
port 主要有:
- input
- output
- inout(输入输出双向端口)
三种类型的端口。
通常在一个module 开始时对port 进行申明,port 申明时可以同时指向数据类型,如:wire,和reg 类型。input 和 inout 端口不能定义数据类型为 reg
测试脚本中的端口定义:
在测试脚本中,通常被例化模块的input 或 inout 端口声明为 wire 数据类型,将被例化模块的output 端口声明为reg数据类型。
2)设计
#1、功能要求
- 设计一个脉冲计数器,其功能如下:
- 输入脉冲:4路脉冲信号,分别对每路进行脉冲检测并计数
- 使能信号:高电平进行计数,低电平清零计数器
- 计数器:在使能信号高电平期间,对脉冲信号的上升沿进行检测并递增计数值
- 编写测试脚本,进行仿真验证
#2、代码
vivado 使用技巧:
把设计文件添加到vivado 中的时候会自动的识别代码之间的层级关系
被高亮的模块通常就被认为是顶层模块
也可以右键指定module ,set as top 设为顶层模块,显示如果是灰的就意味着已经是顶层模块了
如果没有显示出指定的子模块,就会显示为:
就会这样显示,这样显示的原因肯定是代码出错了
verilog 技巧:
'b0 这样是将信号全部赋为低电平
'b1 这样是将信号全部赋为高电平
4‘b1110 这样就能保证四路的脉冲有一路是非有效的
设计文件:
`timescale 1ns/1ps
module vlg_design(
input i_clk,
input i_rst_n,
input[3:0] i_pulse,
input i_en,
output[63:0] o_pulse_cnt
);
wire[3:0] w_cnt0;
wire[3:0] w_cnt1;
wire[3:0] w_cnt2;
wire[3:0] w_cnt3;
cnt_design cnt0(
.i_clk(i_clk),
.i_rst_n(i_rst_n),
.i_pulse(i_pulse[0]),
.i_en(i_en),
.o_cnt(w_cnt0)
);
cnt_design cnt1(
.i_clk(i_clk),
.i_rst_n(i_rst_n),
.i_pulse(i_pulse[1]),
.i_en(i_en),
.o_cnt(w_cnt1)
);
cnt_design cnt2(
.i_clk(i_clk),
.i_rst_n(i_rst_n),
.i_pulse(i_pulse[2]),
.i_en(i_en),
.o_cnt(w_cnt2)
);
cnt_design cnt3(
.i_clk(i_clk),
.i_rst_n(i_rst_n),
.i_pulse(i_pulse[3]),
.i_en(i_en),
.o_cnt(w_cnt3)
);
assign o_pulse_cnt = w_cnt0 + w_cnt1 + w_cnt2 +w_cnt3;
endmodule
module cnt_design(
input i_clk,
input i_rst_n,
input i_pulse,
input i_en,
output reg[3:0] o_cnt
);
reg[1:0] r_pulse;
wire pulse_rise_edge;
always @(posedge i_clk)
if(!i_rst_n) r_pulse <= 'b0;
else r_pulse <= {r_pulse[0],i_pulse};
assign pulse_rise_edge = r_pulse[0] & ~r_pulse[1];
always @(posedge i_clk)
if(!i_rst_n) o_cnt <= 'b0;
else if(i_en)
if(pulse_rise_edge) begin
o_cnt <= o_cnt + 1'b1;
end
else o_cnt <= o_cnt;
else o_cnt <= 'b0;
endmodule
testbench:
`timescale 1ns/1ps
module testbench_top();
`define CLK_PERIORD 10
reg clk;
reg rst_n;
reg [3:0] i_pulse;
reg i_en;
wire [63:0] o_pulse_cnt;
vlg_design uut_vlg_design(
.i_clk(clk),
.i_rst_n(rst_n),
.i_pulse(i_pulse),
.i_en(i_en),
.o_pulse_cnt(o_pulse_cnt)
);
initial begin
clk <= 0;
rst_n <= 0;
#1000;
rst_n <= 1;
end
always #(`CLK_PERIORD/2) clk = ~clk;
integer i;
initial begin
i_pulse <= 4'b0;
i_en <= 'b0;
@(posedge rst_n); //等待复位完成
@(posedge clk); //等一个时钟周期
repeat(10) begin
@(posedge clk);
end
#4;
i_pulse <= 4'b1;
i_en <= 1'b1;
// 脉冲生成
for(i = 0 ; i < 15 ; i = i + 1) begin
//每个begin end 内部是顺序执行的
#500;
i_pulse <= 4'd15;
#300;
i_pulse <= 4'b0;
end
i_en <= 1'b0; //这里是顺序执行的,执行完for 才执行的这一
#10_000;
i_en <= 1'b1;
for(i = 0 ; i < 10 ; i = i + 1) begin
//每个begin end 内部是顺序执行的
#500;
i_pulse <= 4'd15;
#300;
i_pulse <= 4'b0;
end
i_en <= 1'b0;
#10_000;
i_en <= 1'b0;
for(i = 0 ; i < 15 ; i = i + 1) begin
//每个begin end 内部是顺序执行的
#500;
i_pulse <= 4'd15;
#300;
i_pulse <= 4'b0;
end
i_en <= 1'b0;
#10_000;
$stop;
end
endmodule
2、generate 语法
1)优化模块化设计
之前我们进行的模块化设计是将例化重复了4次,这对线路数的代码编写上是没有什么问题的,但是对于高线路数的代码在编写的过程中就有问题了,会增加代码编写的复杂性,增加代码量
2)generate 语法
用于语句重复:
generate 生成,可以通过gennerate 实现某些语句的重复。genvar 与 generate 是Verilog 2001才有的,功能非常强大,可以配合条件语句、分支语句等做一些有规律的例化或者赋值的操作。
对于简介代码很有帮助,同时也减少了人为的影响。
generate 有 gennerate for 、generate if 和 generate case 三种,可以在generate 中使用的语句包括 module ,UDP(用户自定义原语),门级原语,连续赋值语句,always 语句和initial 语句等。
通过定义genvar 作为generate 的循环变量
语法结构:
genvar 循环变量名;
generate
// generate 循环语句,或generate 条件语句,或generate 分支语句
//或嵌套的 generate 语句
endgenerate
不能在运行中改变generate 内部重复的次数比如说:
genavr i;
generate
for (i = 0 ; i < DATA_SIZE; i ++ ) begin
.....
end
endgenerate
就是说DATA_SIZE 这个值必须是确定好的,是个常量,而不能是变量,不能再运行的过程中再进行修改,相应的就是说需要设计多少个元件块,需要占用多少资源必须是一定的。
必须在编译的过程中完成。
3)设计
#1、功能要求
- 设计一个脉冲计数器,其功能如下:
- 输入脉冲:16路脉冲信号,分别对每路进行脉冲检测并计数
- 使能信号:高电平进行计数,低电平清零计数器
- 计数器:在使能信号高电平期间,对脉冲信号的上升沿进行检测并递增计数值
#2、代码
verilog 使用技巧:
定制二维矩阵
示例:wire[15:0] r_cnt [15:0];
二维矩阵,前面定义的是每一个寄存器的位宽,后面的是定义位宽的寄存器的个数
使用随机数
示例:i_pulse <= {$random} % 65536; //表示 0 ~ 65535 的随机数
vscode 使用技巧:
按住alt 选中多处可以对代码同时进行修改
设计文件:
`timescale 1ns/1ps
module vlg_design(
input i_clk,
input i_rst_n,
input[15:0] i_pulse,
input i_en,
output[15:0] o_pulse_cnt0,
output[15:0] o_pulse_cnt1,
output[15:0] o_pulse_cnt2,
output[15:0] o_pulse_cnt3,
output[15:0] o_pulse_cnt4,
output[15:0] o_pulse_cnt5,
output[15:0] o_pulse_cnt6,
output[15:0] o_pulse_cnt7,
output[15:0] o_pulse_cnt8,
output[15:0] o_pulse_cnt9,
output[15:0] o_pulse_cnta,
output[15:0] o_pulse_cntb,
output[15:0] o_pulse_cntc,
output[15:0] o_pulse_cntd,
output[15:0] o_pulse_cnte,
output[15:0] o_pulse_cntf
);
wire[15:0] r_pulse_cnt[15:0]; //二维矩阵,前面的是每一个寄存器的位宽,后面的是对应位宽寄存器的个数
genvar i;
generate
for(i =0 ; i < 16 ; i = i+1) begin
cnt_design cnt(
.i_clk(i_clk),
.i_rst_n(i_rst_n),
.i_pulse(i_pulse[i]),
.i_en(i_en),
.o_cnt(r_pulse_cnt[i]) //这里的i是代表的第几个寄存器,而不是代表位宽
);
end
endgenerate
assign o_pulse_cnt0 = r_pulse_cnt[0];
assign o_pulse_cnt1 = r_pulse_cnt[1];
assign o_pulse_cnt2 = r_pulse_cnt[2];
assign o_pulse_cnt3 = r_pulse_cnt[3];
assign o_pulse_cnt4 = r_pulse_cnt[4];
assign o_pulse_cnt5 = r_pulse_cnt[5];
assign o_pulse_cnt6 = r_pulse_cnt[6];
assign o_pulse_cnt7 = r_pulse_cnt[7];
assign o_pulse_cnt8 = r_pulse_cnt[8];
assign o_pulse_cnt9 = r_pulse_cnt[9];
assign o_pulse_cnta = r_pulse_cnt[10];
assign o_pulse_cntb = r_pulse_cnt[11];
assign o_pulse_cntc = r_pulse_cnt[12];
assign o_pulse_cntd = r_pulse_cnt[13];
assign o_pulse_cnte = r_pulse_cnt[14];
assign o_pulse_cntf = r_pulse_cnt[15];
endmodule
module cnt_design(
input i_clk,
input i_rst_n,
input i_pulse,
input i_en,
output reg[15:0] o_cnt
);
reg[1:0] r_pulse;
wire pulse_rise_edge;
always @(posedge i_clk)
if(!i_rst_n) r_pulse <= 'b0;
else r_pulse <= {r_pulse[0],i_pulse};
assign pulse_rise_edge = r_pulse[0] & ~r_pulse[1];
always @(posedge i_clk)
if(!i_rst_n) o_cnt <= 'b0;
else if(i_en)
if(pulse_rise_edge) begin
o_cnt <= o_cnt + 1'b1;
end
else o_cnt <= o_cnt;
else o_cnt <= 'b0;
endmodule
testbench:
`timescale 1ns/1ps
module testbench_top();
`define CLK_PERIORD 10
reg clk;
reg rst_n;
reg [15:0] i_pulse;
reg i_en;
wire[15:0] o_pulse_cnt0;
wire[15:0] o_pulse_cnt1;
wire[15:0] o_pulse_cnt2;
wire[15:0] o_pulse_cnt3;
wire[15:0] o_pulse_cnt4;
wire[15:0] o_pulse_cnt5;
wire[15:0] o_pulse_cnt6;
wire[15:0] o_pulse_cnt7;
wire[15:0] o_pulse_cnt8;
wire[15:0] o_pulse_cnt9;
wire[15:0] o_pulse_cnta;
wire[15:0] o_pulse_cntb;
wire[15:0] o_pulse_cntc;
wire[15:0] o_pulse_cntd;
wire[15:0] o_pulse_cnte;
wire[15:0] o_pulse_cntf;
vlg_design uut_vlg_design(
.i_clk(clk),
.i_rst_n(rst_n),
.i_pulse(i_pulse),
.i_en(i_en),
.o_pulse_cnt0(o_pulse_cnt0),
.o_pulse_cnt1(o_pulse_cnt1),
.o_pulse_cnt2(o_pulse_cnt2),
.o_pulse_cnt3(o_pulse_cnt3),
.o_pulse_cnt4(o_pulse_cnt4),
.o_pulse_cnt5(o_pulse_cnt5),
.o_pulse_cnt6(o_pulse_cnt6),
.o_pulse_cnt7(o_pulse_cnt7),
.o_pulse_cnt8(o_pulse_cnt8),
.o_pulse_cnt9(o_pulse_cnt9),
.o_pulse_cnta(o_pulse_cnta),
.o_pulse_cntb(o_pulse_cntb),
.o_pulse_cntc(o_pulse_cntc),
.o_pulse_cntd(o_pulse_cntd),
.o_pulse_cnte(o_pulse_cnte),
.o_pulse_cntf(o_pulse_cntf)
);
initial begin
clk <= 0;
rst_n <= 0;
#1000;
rst_n <= 1;
end
always #(`CLK_PERIORD/2) clk = ~clk;
integer i;
initial begin
i_pulse <= 4'b0;
i_en <= 'b0;
@(posedge rst_n); //等待复位完成
@(posedge clk); //等一个时钟周期
repeat(10) begin
@(posedge clk);
end
#4;
i_pulse <= 4'b1;
i_en <= 1'b1;
// 脉冲生成
for(i = 0 ; i < 15 ; i = i + 1) begin
//每个begin end 内部是顺序执行的
#500;
i_pulse <= {$random } % 65536; //表示 0~ 65535 的随机数
#300;
i_pulse <= 16'b0;
end
i_en <= 1'b0; //这里是顺序执行的,执行完for 才执行的这一
#10_000;
i_en <= 1'b1;
for(i = 0 ; i < 10 ; i = i + 1) begin
//每个begin end 内部是顺序执行的
#500;
i_pulse <= {$random } % 65536;
#300;
i_pulse <= 16'b0;
end
i_en <= 1'b0;
#10_000;
i_en <= 1'b0;
for(i = 0 ; i < 15 ; i = i + 1) begin
//每个begin end 内部是顺序执行的
#500;
i_pulse <= {$random } % 65536;
#300;
i_pulse <= 16'b0;
end
i_en <= 1'b0;
#10_000;
$stop;
end
endmodule
原理图:
重复实现了16个脉冲检测加脉冲计数元件
波形图:
3、频率计数器设计
1)功能定义
- 在使能信号控制下,计算输入脉冲的每两个上升沿之间的时钟周期数并输出,即输出脉冲频率的计数值
- 输入信号:
- 周期性脉冲信号:需要做检测的脉冲频率信号
- 使能信号:高电平进行频率计数,低电平清零计数器
- 输出信号:
- 计数值:输出脉冲频率的计数值
- 有效信号:该信号拉高时,输出计数值有效
2)设计
设计文件:
`timescale 1ns/1ps
module vlg_design(
input i_clk,
input i_rst_n,
input i_pulse,
input i_en,
output o_vld, //表示当前的o_pulse_cnt 是有效的
output [15:0] o_pulse_cnt
);
wire w_rise_edge;
reg[1:0] r_pulse;
/
//脉冲边沿检测逻辑
always @(posedge i_clk)
if(!i_rst_n) begin
r_pulse <= 2'b00;
end
else r_pulse <= {r_pulse[0],i_pulse};
assign w_rise_edge = r_pulse & ~r_pulse[1];
/
//输出有效信号
reg r_flag;
always @(posedge i_clk)
if(!i_rst_n) r_flag <= 'b0;
else if(!i_en) r_flag <= 'b0;
else if(w_rise_edge) r_flag <=1'b1; //检测到w_rise_reg 是高电平的时候,就会在下一个周期讲r_flag 拉高正好差一个周期
else ; //确定这里就是要产生一个锁存器,就这样写
assign o_vld = w_rise_edge & r_flag;
/
//脉冲计数逻辑
reg r_pulse_cnt;
always @(posedge i_clk)
if(!i_en) r_pulse_cnt <= 'b0;
else if(w_rise_edge) r_pulse_cnt <= 'b0;
else r_pulse_cnt <= r_pulse_cnt + 1'b1;
assign o_pulse_cnt = r_pulse_cnt + 1;
endmodule
目标波形:
testbench:
`timescale 1ns/1ps
module testbench_top();
`define CLK_PERIORD 10
reg clk;
reg rst_n;
reg i_pulse;
reg i_en;
wire o_vld;
wire [7:0] o_pulse_cnt;
vlg_design uut_vlg_design(
.i_clk(clk),
.i_rst_n(rst_n),
.i_pulse(i_pulse),
.i_en(i_en),
.o_vld(o_vld),
.o_pulse_cnt(o_pulse_cnt)
);
initial begin
clk <= 0;
rst_n <= 0;
#1000;
rst_n <= 1;
end
always #(`CLK_PERIORD/2) clk = ~clk;
integer i;
initial begin
i_pulse <= 1'b0;
i_en <= 'b0;
@(posedge rst_n); //等待复位完成
@(posedge clk); //等一个时钟周期
repeat(10) begin
@(posedge clk);
end
#4;
i_pulse <= 1'b1;
i_en <= 1'b1;
// 脉冲生成
for(i = 0 ; i < 50 ; i = i + 1) begin
//每个begin end 内部是顺序执行的
#1000;
i_pulse <= 1'b1;
#1000;
i_pulse <= 1'b0;
end
i_en <= 1'b0; //这里是顺序执行的,执行完for 才执行的这一
#10_000;
i_en <= 1'b1;
for(i = 0 ; i < 69 ; i = i + 1) begin
//每个begin end 内部是顺序执行的
#5000;
i_pulse <= 1'b1;
#5000;
i_pulse <= 1'b0;
end
i_en <= 1'b0;
#10_000;
i_en <= 1'b0;
for(i = 0 ; i < 50 ; i = i + 1) begin
//每个begin end 内部是顺序执行的
#500;
i_pulse <= 1'b1;
#300;
i_pulse <= 1'b0;
end
i_en <= 1'b0;
#10_000;
$stop;
end
endmodule
波形图:
小问题:
时序逻辑和组合逻辑放到一块的时候的时序应该如何去完成
比如说:
这一块在实现的时候,会在最后添加一个加法器,但是从波形上看为什么在一开始的时候就会对r_pulse_cnt + 1 是如何检测是什么时候再对波形 + 1的
原理图:
问题定位:
不明白时序逻辑下的组合逻辑的表达在功能中是如何定义的。
这块原理图看不是很明白,我觉得还是数电上的问题,对于数电这部分还需要再加深
写代码的时候心中有电路是不是意味着写代码的时候每一个语句需要知道他模拟出来的电路是什么含义
4、if 和 case 语句
条件判断:if 语句
分支判断:case 语句
1)格雷码
在一组数的编码中,若任意两个相邻的代码只有一位二进制数不同则称这种编码为格雷码,且最大数和最小数之间也仅一位不同,及首位相连,因此又称为循环换码或者反射码。
在数组增加递增的过程中,也只有一位会发生反转
使用场景:
在特定的情况下如果电路多路发生变化,可能导致电路状态错误或者输入错误,使用格雷码可以避免这种错误,格雷码有多种编码格式。
2)if 语法
包含多个if 嵌套或者if 里面有多条语句执行的时候一定要写上begin end 加以区分
条件判断if 语句实现3-8 译码器:
module vlg_design2 (
input[2:0] i_sel, //选择信号
output reg [7:0] o_data //译码结果
);
always@(*) begin
if(i_sel == 3'd0) o_data <= 8'b0000_0001;
else if (i_sel == 3'd1) o_data <= 8'b0000_0010;
else if (i_sel == 3'd2) o_data <= 8'b0000_0100;
else if (i_sel == 3'd3) o_data <= 8'b0000_1000;
else if (i_sel == 3'd4) o_data <= 8'b0001_0000;
else if (i_sel == 3'd5) o_data <= 8'b0010_0000;
else if (i_sel == 3'd6) o_data <= 8'b0100_0000;
else o_data <= 8'b1000_0000;
end
endmodule //vlg_design2
3)case 语句
分支判断语句case 实现3-8译码器:
module vlg_design2 (
input[2:0] i_sel, //选择信号
output reg [7:0] o_data //译码结果
);
always@(*) begin
case(i_sel )
3'd0 : o_data <= 8'b0000_0001;
3'd1 : o_data <= 8'b0000_0010;
3'd2 : o_data <= 8'b0000_0100;
3'd3 : o_data <= 8'b0000_1000;
3'd4 : o_data <= 8'b0001_0000;
3'd5 : o_data <= 8'b0010_0000;
3'd6 : o_data <= 8'b0100_0000;
3'd7 : o_data <= 8'b1000_0000;
default: ;
endcase
end
endmodule //vlg_design2
在实际的书写当中,为了保证语法的完整性,通常建议写上default,即便没有逻辑处理,也可以书写如下的空语句。
default :;
4)设计
#1、功能要求
实现格雷码的转换
#2、代码
使用ifelse 实现的设计文件:
`timescale 1ns/1ps
module vlg_design(
input i_clk,
input i_rst_n,
input[3:0] i_data,
output[3:0] o_gray
);
reg[3:0] r_gray;
always@(posedge i_clk)
if(!i_rst_n) r_gray <= 'b0;
else if(i_data == 4'b0000) r_gray <= 4'b0000;
else if(i_data == 4'b0001) r_gray <= 4'b0001;
else if(i_data == 4'b0010) r_gray <= 4'b0011;
else if(i_data == 4'b0011) r_gray <= 4'b0010;
else if(i_data == 4'b0100) r_gray <= 4'b0110;
else if(i_data == 4'b0101) r_gray <= 4'b0111;
else if(i_data == 4'b0110) r_gray <= 4'b0101;
else if(i_data == 4'b0111) r_gray <= 4'b0100;
else if(i_data == 4'b1000) r_gray <= 4'b1100;
else if(i_data == 4'b1001) r_gray <= 4'b1101;
else if(i_data == 4'b1010) r_gray <= 4'b1111;
else if(i_data == 4'b1011) r_gray <= 4'b1110;
else if(i_data == 4'b1100) r_gray <= 4'b1010;
else if(i_data == 4'b1101) r_gray <= 4'b1011;
else if(i_data == 4'b1110) r_gray <= 4'b1001;
else r_gray <= 4'b1000;
assign o_gray = r_gray;
endmodule
使用 case 语句实现的设计文件:
`timescale 1ns/1ps
module vlg_design(
input i_clk,
input i_rst_n,
input[3:0] i_data,
output[3:0] o_gray
);
reg[3:0] r_gray;
always@(posedge i_clk)
if(!i_rst_n) r_gray <= 'b0;
else begin
case(i_data)
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;
default r_gray <= 4'b1000;
endcase
end
assign o_gray = r_gray;
endmodule
testbench 激励文件:
`timescale 1ns/1ps
module testbench_top();
`define CLK_PERIORD 10
reg clk;
reg rst_n;
reg[3:0] i_data;
wire[3:0] o_gray;
vlg_design uut_vlg_design(
.i_clk(clk),
.i_rst_n(rst_n),
.i_data(i_data),
.o_gray(o_gray)
);
vlg_design1 uut_vlg_design1(
.i_clk(clk),
.i_rst_n(rst_n),
.i_data(i_data),
.o_gray(o_gray)
);
initial begin
clk <= 0;
rst_n <= 0;
#1000;
rst_n <= 1;
end
always #(`CLK_PERIORD/2) clk = ~clk;
integer i = 0 ;
initial begin
i_data <= 4'b0000;
@(posedge rst_n);
@(posedge clk);
for(i = 0 ; i <16 ; i = i +1) begin
@(posedge clk)
i_data <= i_data +1;
end
$stop;
end
endmodule
波形图:
case 语句原理图:
if语句原理图:
5、四位格雷码计数器
1)显示任务
系统显示任务$display 和 w r i t e 在 仿 真 测 试 中 是 最 为 常 用 的 信 息 显 示 方 式 ( 不 可 综 合 ) , 主 要 区 别 在 于 : write 在仿真测试中是最为常用的信息显示方式(不可综合),主要区别在于: write在仿真测试中是最为常用的信息显示方式(不可综合),主要区别在于:diplay 在一次输出后自动换行,$write 不会,他们的其他用法格式基本类似:
语法结构:
[任务名] (“[可选字符串] + [格式]”,[信号1],[信号2]...)
【任务名】 可以是 d i p l a y , diplay, diplay,displayb, d i s p l a y o , displayo, displayo,displayh, w r i t e , write, write,writeb, w r i t e o 或 writeo或 writeo或writeh。
【格式】由% 和格式字符组成,信号为进行显示的信号名,【信号】数量和【格式】数量必须对应,若不指定显示【信号】和【格式】,则【信号】显示的格式将会是$display 和 w r i t e 默 认 为 十 进 制 , write 默认为十进制, write默认为十进制,displayb和 w r i t e b 默 认 为 二 进 制 , writeb 默认为二进制, writeb默认为二进制,displayo和 w r i t e o 默 认 为 八 进 制 , writeo 默认为八进制, writeo默认为八进制,displayh和$writeh 默认为十六进制
一些特殊字符的显示如下表:
示例:
rval = 3'b101;
$display("rval = %h hex %d decimal",rval,rval)
默认情况下十六进制,八进制,二进制位数不满时会用0来占位,但是十进制显示不会用0来占位。
如果要顶格可以是 %0d
2)监控任务$monitor
系统监视任务 m o n i t o r , 在 仿 真 测 试 是 脚 本 中 可 以 实 现 对 ∗ ∗ 任 何 变 量 或 者 表 达 式 取 值 的 监 视 和 显 示 ∗ ∗ 。 当 monitor ,在仿真测试是脚本中可以实现对**任何变量或者表达式取值的监视和显示**。当 monitor,在仿真测试是脚本中可以实现对∗∗任何变量或者表达式取值的监视和显示∗∗。当monitor 语法结构以及用法都与$display 相似
当%monitor 任务中包含一个或多个监控信号并运行时,若参数列表中有任何的变量或表达式的值发生改变时,所有参数列表中的信号都将输出并显示。同一时刻,若两个或多个参数的值发生变化,则此时将会合并一次输出并显示。
m o n i t o r 任 务 在 申 明 后 默 认 开 启 , 在 其 运 行 期 间 , 若 调 用 系 统 任 务 monitor 任务在申明后默认开启,在其运行期间,若调用系统任务 monitor任务在申明后默认开启,在其运行期间,若调用系统任务monitoroff 则将会关闭 m o n i t o r , 直 到 调 用 系 统 任 务 monitor ,直到调用系统任务 monitor,直到调用系统任务monitoron 后将重新开启$monitor
$monitor(“[可选字符串] + [格式]”,[信号1],[信号2]...)
示例:
initial begin
@(posedge rst_n)
$monitor ("o_cnt is %d at %0dns",o_cnt,$time);
//$time 表示时间戳,是一直会变的,但是并不会触发monitor,只有o_cnt才会触发
end
always @(posedge clk) begin
if(o_cnt == 4'd5) $monitoroff; //关闭
else if(o_cnt == 4'd12) $monitoron ; //开启
end
3)设计
设计文件:
`timescale 1ns/1ps
module vlg_design(
input i_clk, //50Mhz
input i_rst_n,
//input[3:0] i_data,
output[3:0] o_gray
);
//
//1S定时计数
localparam TIMER_1S_MAX_CNT = 32'd1_000_000_000/40;
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 +1;
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 + 1;
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;
default r_gray <= 4'b1000;
endcase
end
assign o_gray = r_gray;
endmodule
testbench:
`timescale 1ns/1ps
module testbench_top();
`define CLK_PERIORD 20
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 = 0 ;
initial begin
@(posedge rst_n); //等待复位完成
$monitor("o_gray is %d at %tns",o_gray,$time); //当任务发生后,不管o_gray发生任何的变化都会把他打印出来�?�同时显示当前的系统时间
/*
时间戳在显示的时候如果是d% 显示的话就是ns 去显示,如果�? t%去显示的话就是ps 也就是按照精度去显示
*/
$stop;
end
endmodule
波形图:
电路图:
打印输出:
从图上可以看出来的是对于r_cnt 的计数是从1开始的,这个就比较有意思了,为什么不是从0开始的呢?
最后发现了问题:
正好差两个时钟周期一个是r_second 的检验一个是o_gray 的赋值都是时序逻辑电路,相当于踩了两个时钟周期,但是看一开始的话会发现其实并不是从1开始的,只是他们之间差了一个时钟周期。
但是我还是有个疑惑,返回头去看代码:
这块代码TIMER_1S_MAX_CNT = 25_000_000 ,但是这里是小于 TIMER_1S_MAX_CNT -1 也就是 < 24_999_999 ,按道理来说是到不了24_999_999的,但是为什么能到了呢?
很奇怪。
6、基于查找表的八位格雷码转换
1)查找表(look-up-table)
简称为LUT
- 简单来说就是一个预先存储好结构的数据表
- 可以访问这张预先存储好结果的数据表,可以快速的获取不同输入的输出结果,这张表的访问其实就是一个存储器的访问,输入是存储器的地址,输出是存储器上不同地址指向的数据结果
- 地址–> 结果
- 查找表可以免去运算的过程,尤其对于复杂的运算可以大大减少运算的开销和运行时间
在某种情况下是可以这样的,就像内存一样,如果 这个表做的非常大了就不是很合适了,因为FPGA内部的存储资源也是非常有限的。
2)ROM初始化COE文件制作
-
xilinx 的COE文件用于对rom做初始化赋值
-
memory_initialization_radix = 16 ; //进制的设置 memory_initialization_vector = 000, .... ffff;
-
-
memory_initialization_radix 后面是数据格式,COE文件中的数据格式可以是2(Binary)10(Decimal)16(Hex)
-
memory_initialization_vector 后是初始化数据
3)配置IP
1)选中IP Catalog -> ROM -> Block Memory Generator
2)选中Single Port ROM
3)设置端口等相关数据
这里设置的Port A Optional Register -> Primitives Output Register
含义是:原语输出寄存器,就是在输出的时候再寄存一下,这个时候,当地址输入到IP中的时候是应该是下一个时钟上升沿就能读取到数据,但是这样就需要再过一个时钟上升沿才能读取到数据
也就是在Summary中总结显示的:
‘
Total Port A Read Latency (端口A 的读延迟):2 Clock Cycles
图示:
4)初始化Rom
因为rom是一个只读不写的存储单元,所以在一开始的时候就需要对rom进行初始化,而初始化rom就需要上面的.COE
文件进行设置
#1、创建gray_lut.coe文件
memory_initialization_radix = 2 ; 这是进行数值编码的配置,这里配置的是二进制。
memory_initialization_vector = 这是对于lut内部存储单元进行配置,将存储单元的值根据顺序赋值为.coe文件中的值
最后一个数据后面是有分号的
#2、初始化rom
选中对应的IP右键re_customize IP 然后设置通过文件进行初始化。
这样LUT的存储单元初始化就完成了。
然后就可以进行编码运行
5)设计
设计文件:
`timescale 1ns/1ps
module vlg_design(
input i_clk, //50Mhz
input i_rst_n,
input i_en,
input[7:0] i_data,
output[7:0] o_gray,
output o_vld
);
reg[1:0] r_vld;
always @(posedge i_clk)
if(!i_rst_n) r_vld <= 'b0;
else r_vld <= {r_vld[0],i_en};
assign o_vld = r_vld[1];
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
testbench:
`timescale 1ns/1ps
module testbench_top();
`define CLK_PERIORD 20
reg clk;
reg rst_n;
reg en;
reg[7:0] data;
wire [7:0] gray;
wire vld;
vlg_design uut_vlg_design(
.i_clk(clk),
.i_rst_n(rst_n),
.i_en(en),
.i_data(data),
.o_vld(vld),
.o_gray(gray)
);
initial begin
clk <= 0;
rst_n <= 0;
#1000;
rst_n <= 1;
end
always #(`CLK_PERIORD/2) clk = ~clk;
integer i = 0 ;
initial begin
en <= 'b0;
data <= 'b0;
@(posedge rst_n); //等待复位完成
en <= 'b1;
data <= 'b0;
repeat(255) begin
@(posedge clk)
data <= data + 1;
end
en <= 'b0;
data <= 'b0;
#1000;
$stop;
end
always @(posedge clk) begin
//用二进制来输出
if(vld) $display("%b",gray);
else;
end
endmodule
波形:
打印输出:
需要注意的是可以发现:
-
首先data 对应的地址输入之后(对应LUT存储单元当中的地址),第二个时钟周期才开始生成gray 也就是对应输出的格雷码转换信号。
-
vld 作为有效信号,是针对输出gray数据的有效信号,那么他的有效值也需要和数据保持同步,也就是延迟两个周期,实现的方法就是通过编辑一个锁存器
这里其实是对vld 进行了多一次的锁存
从这里可以看到的是使能信号其实是和数据信号同时赋值的,如果正常的时序电路来说下一个时钟周期就应该正常的开始信号的,但是这里进行了一次锁存,第一个时钟周期对
r_vld = { 0 , 1}
此时 o_vld = 0
第二个时钟周期对r_vld[1] 又进行了一次锁存,因而r_vld 此时的值才会赋值给o_vld
-
还需注意display 的用法
-
以及一开始的数据深度就是在LUT内的存储深度