工程师深度:FPGA 高手养成记
一
Verliog语法基础
基本的语法略过,主要想写一些关于框架,规范,技术难点的博文,这样对于我们养成好的编码习惯是有好处的,就定这样一个flag吧.希望大家可以一起好好学习,共同进步.
接口时序设计规范
模块和模块之间的通过模块的接口实现关联, 因此规范的时序设计, 对于程序设计的过程, 以及程序的维护, 团队之间的沟通都是非常必要的。
命名规则
1、 顶层文件
对象+功能+top
比如:video_oneline_top
2、 逻辑控制文件
介于顶层和驱动层文件之间
对象+ctr
比如:ddr_ctr.v
3、 驱动程序命名
对象+功能+dri
比如:lcd_dri.v、 uart_rxd_dri.v
4、 参数文件命名
对象+para
比如:lcd_para.v
5、 模块接口命名:文件名+u
比如 lcd_dir lcd_dir_u(........)
6、 模块接口命名:特征名+文件名+u
比如 mcb_read c3_mcb_read_u
7、 程序注释说明
/*****************************************************************/
// Company:
// Engineer:
// WEB:
// BBS:
// Create Date: 07:28:50 07/31/2019
// Design Name: FPGA STREAM
// Module Name: FPGA_USB
// Project Name: FPGA STREAM
// Target Devices: XC6SLX16-FTG256/XC6SLX25-FTG256 Mis603
// Tool versions: ISE14.7
// Description: CY7C68013A SLAVE FIFO comunication with fpga
// Revision: V1.0
// Additional Comments:
//1) _i input
//2) _o output
//3) _n activ low
//4) _dg debug signal
//5) _r delay or register
//6) _s state mechine
/*****************************************************************/
8、 端口注释
input Video_vs_i,//输入场同步入
9、 信号命名
命名总体规则:对象+功能(+极性) +特性
10、 时钟信号
对象+功能+特性
比如:phy_txclk_i、 sys_50mhz_i
11、 复位信号
对象+功能+极性+特性
比如:phy_rst_n_i、 sys_rst_n_i
12、 延迟信号
对象+功能+特性 1+特征 2
比如:fram_sync_i_r0、 fram_sync_i_r1
13、 特定功能计数器
对象+cnt
比如:line_cnt、 div_cnt0、 div_cnt1
功能+cnt
比如:wr_cnt、 rd_cnt
对象+功能+cnt
比如:fifo_wr_cnt、 mcb_wr_cnt、 mem_wr_cnt
对象+对象+cnt
比如:video_line_cnt、 video_fram_cnt
14、 一般计数器
cnt+序号
用于不容易混淆的计数
比如:cnt0、 cnt1、 cnt2
15、 时序同步信号
对象+功能+特性
比如:line_sync_i、 fram_sysc_i
16、 使能信号
功能+en
比如:wr_en、 rd_en
对象+功能+en
比如:fifo_wr_en、 mcb_wr_en
Verilog 最最基础语法
C 语言和 Verilog 的关键词和结构对比:
C 语言和 Verilog 运算符对比:
关键字
信号部分:
input 关键词, 模块的输入信号, 比如 input Clk, Clk 是外面关键输入的时钟信号;
output 关键词, 模块的输出信号, 比如 output[3:0]Led; 这个地方正好是一组输出信号。其中[3:0]表示 0~3 共 4 路信号。
inout 模块输入输出双向信号。这种类型, 我们的例子 24LC02 中有使用。数总线的通信中, 这种信号被广泛应用;
wire 关键词, 线信号。例如:wire C1_Clk; 其中 C1_Clk 就是 wire 类型的信号;
线信号,三态类型, 我们一般常用的线信号类型有input,output,inout,wire;
reg 关键词, 寄存器。和线信号不同, 它可以在 always 中被赋值, 经常用于时序逻辑中。比如 reg[3:0]Led;表示了一组寄存器。
结构部分:
module()
… en
dmodule
代表一个模块, 我们的代码写在这个两个关键字中间
always@()括号里面是敏感信号。这里的 always@(posedge Clk)敏感信号是 posedge Clk
含义是在上升沿的时候有效, 敏感信号还可以 negedge Clk 含义是下降沿的时候有效,
这种形式一般时序逻辑都会用到。还可以是*这个一符号, 如果是一个*则表示一直是敏感的, 一般用于组合逻辑。
assign 用来给
output,inout 以及 wire 这些类型进行连线。assign 相当于一条连线, 将表达式右边的电路直接通过
wire(线)连接到左边, 左边信号必须是 wire 型(output 和 inout 属于 wire 型) 。当右边变化了左边立马变化,
方便用来描述简单的组合逻辑。
符号部分:
这里重点讲解一下“<=” 赋值符号,非阻塞赋值 ,“=” 阻塞赋值,“{}”
“<=”
赋值符号, 非阻塞赋值, 在一个 always 模块中, 所有语句一起更新。它也可以表示小于等于,
具体是什么含义编译环境根据当前编程环境判断, 如果“<=” 是用在一个 if 判断里如:if(a <=
10);当然就表示小于等于了。
“=” 阻塞赋值, 或者给信号赋值, 如果在 always 模块中, 这条语句被立刻执行。阻塞赋值和非阻塞赋值将再后面详细举例说明。
“{} ” 在 Verilog 中表示拼接符, {a,b}这个的含义是将括号内的数按位并在一起, 比如:{1001,1110}表示的是 10011110。拼接是 Verilog 相对于其他语言的一大优势, 在以后的编程中请慢慢体会。
参数部分:
parameter
parameter a = 180;//十进制, 默认分配长度 32bit(编译器默认)
parameter a = 8’d180;//十进制
parameter a = 8’haa; //十六进制
parameter a = 8’b1010_1010; //二进制
预处理命令
`include file1.v
`define X = 1;
`deine Y;
`ifdef Y
Z=1;
`else
Z=0;
`endif
Verilog 中数值表示的方式
如果我们要表示一个十进制是 180 的数值, 在 Verilog 中的表示方法如下:
二进制:8’ b1010_1010; //其中“_” 是为了容易观察位数, 可有可无。
十进制:8’ d180;
16 进制:8’ hAA;
讲到这里, 具备这些基础知识, 需要通过代码来学习Veriog 语言。最后, 笔者提一点建议, 学习 Verilog 多看别人写的优秀的代码, 多看官方提供的代码和文档。其中官方提供的代码, 很多时候代表了最新的用法, 或者推荐的用法。读者学习, 首先把最最基础的掌握好, 这样, 在项目中遇到了问题, 也能快速学习, 快速解决。
对于理论知识的学习, 没必要一开始就研究得那么深刻, 只是搞理论学习, 对于学习Verilog 语言, 或者 FPGA 开发是不实际的, 要联系理论和实践结合。多仿真, 多验证, 多问题, 多学习, 多改进.
三
浅谈状态机
01. 前言
状态机是FPGA设计中一种非常重要、非常根基的设计思想,堪称FPGA的灵魂,贯穿FPGA设计的始终。
02. 状态机简介
什么是状态机:状态机通过不同的状态迁移来完成特定的逻辑操作(时序操作)状态机是许多数字系统的核心部件, 是一类重要的时序逻辑电路。通常包括三个部分:
下一个状态的逻辑电路
存储状态机当前状态的时序逻辑电路
输出组合逻辑电路
03. 状态机分类
通常, 状态机的状态数量有限, 称为有限状态机(FSM) 。由于状态机所有触发器的时钟由同一脉冲边沿触发, 故也称之为同步状态机。
根据状态机的输出信号是否与电路的输入有关分为 Mealy 型状态机和 Moore 型状态机
3.1,Mealy 型状态机
电路的输出信号不仅与电路当前状态有关, 还与电路的输入有关
3.2,Moore 型状态机
电路的输出仅仅与各触发器的状态, 不受电路输入信号影响或无输入
状态机的状态转移图, 通常也可根据输入和内部条件画出。一般来说, 状态机的设计包含下列设计步骤:
根据需求和设计原则, 确定是 Moore 型还是 Mealy 型状态机;
分析状态机的所有状态, 对每一状态选择合适的编码方式, 进行编码;
根据状态转移关系和输出绘出状态转移图;
构建合适的状态机结构, 对状态机进行硬件描述。
04. 状态机描述
状态机的描述通常有三种方法, 称为一段式状态机, 二段式状态机和三段式状态机。
状态机的描述通常包含以下四部分:
利用参数定义语句 parameter 描述状态机各个状态名称, 即状态编码。状态编码通常有很多方法包含自然二进制编码, One-hot 编码,格雷编码码等;
用时序的 always 块描述状态触发器实现状态存储;
使用敏感表和 case 语句(也采用 if-else 等价语句) 描述状态转换逻辑;
描述状态机的输出逻辑。
下面根据状态机的三种方法来具体说明
4.1,一段式状态机
module detect_1(
input clk_i,
input rst_n_i,
output out_o
);
reg out_r;
//状态声明和状态编码
reg [1:0] state;
parameter [1:0] S0=2'b00;
parameter [1:0] S1=2'b01;
parameter [1:0] S2=2'b10;
parameter [1:0] S3=2'b11;
always@(posedge clk_i)
begin
if(!rst_n_i)begin
state<=0;
out_r<=1'b0;
end
else
case(state)
S0 :
begin
out_r<=1'b0;
state<= S1;
end
S1 :
begin
out_r<=1'b1;
state<= S2;
end
S2 :
begin
out_r<=1'b0;
state<= S3;
end
S3 :
begin
out_r<=1'b1;
end
endcase
end
assign out_o=out_r;
endmodul
一段式状态机是应该避免使用的, 该写法仅仅适用于非常简单的状态机设计。
4.2,两段式状态机
module detect_2(
input clk_i,
input rst_n_i,
output out_o
);
reg out_r;
//状态声明和状态编码
reg [1:0] Current_state;
reg [1:0] Next_state;
parameter [1:0] S0=2'b00;
parameter [1:0] S1=2'b01;
parameter [1:0] S2=2'b10;
parameter [1:0] S3=2'b11;
//时序逻辑: 描述状态转换
always@(posedge clk_i)
begin
if(!rst_n_i)
Current_state<=0;
else
Current_state<=Next_state;
end
//组合逻辑:描述下一状态和输出
always@(*)
begin
out_r=1'b0;
case(Current_state)
S0 :
begin
out_r=1'b0;
Next_state= S1;
end
S1 :
begin
out_r=1'b1;
Next_state= S2;
end
S2 :
begin
out_r=1'b0;
Next_state= S3;
end
S3 :
begin
out_r=1'b1;
Next_state=Next_state;
end
endcase
end
assign out_o = out_r;
endmodule
两段式状态机采用两个 always 模块实现状态机的功能, 其中一个 always 采用同步时序逻辑描述状态转移, 另一个 always 采用组合逻辑来判断状态条件转移。
4.3,三段式状态机
module detect_3(
input clk_i,
input rst_n_i,
output out_o
);
reg out_r;
//状态声明和状态编码
reg [1:0] Current_state;
reg [1:0] Next_state;
parameter [1:0] S0=2'b00;
parameter [1:0] S1=2'b01;
parameter [1:0] S2=2'b10;
parameter [1:0] S3=2'b11;
//时序逻辑: 描述状态转换
always@(posedge clk_i)
begin
if(!rst_n_i)
Current_state<=0;
else
Current_state<=Next_state;
end
//组合逻辑: 描述下一状态
always@(*)
begin
case(Current_state)
S0:
Next_state = S1;
S1:
Next_state = S2;
S2:
Next_state = S3;
S3:
begin
Next_state = Next_state;
end
default :
Next_state = S0;
endcase
end
//输出逻辑: 让输出 out, 经过寄存器 out_r 锁存后输出, 消除毛刺
always@(posedge clk_i)
begin
if(!rst_n_i)
out_r<=1'b0;
else
begin
case(Current_state)
S0,S2:
out_r<=1'b0;
S1,S3:
out_r<=1'b1;
default :
out_r<=out_r;
endcase
end
end
assign out_o=out_r;
endmodule
三段式状态机在第一个 always 模块采用同步时序逻辑方式描述状态转移, 第二个always 模块采用组合逻辑方式描述状态转移规律, 第三个 always 描述电路的输出。通常让输出信号经过寄存器缓存之后再输出, 消除电路毛刺。
05. 状态机优缺点
1、一段式状态机:只涉及时序电路,没有竞争与冒险,同时消耗逻辑比较少。
但是如果状态非常多,一段式状态机显得比较臃肿,不利于维护。
2、两段式状态机:当一个模块采用时序(状态转移),一个模块采用组合时候(状态机输出),组合逻辑电路容易造成竞争与冒险;当两个模块都采用时序,可以避免竞争与冒险的存在,但是整个状态机的时序上会延时一个周期。
两段式状态机是推荐的状态机设计方法。
3、三段式状态机:三段式状态机在状态转移时采用组合逻辑电路+格雷码,避免了组合逻辑的竞争与冒险;状态机输出采用了同步寄存器输出,也可以避免组合逻辑电路的竞争与冒险;采用这两种方法极大的降低了竞争冒险。并且在状态机的采用这种组合逻辑电路+次态寄存器输出,避免了两段式状态机的延时一个周期(三段式状态机在上一状态中根据输入条件判断当前状态的输出,从而在不插入额外时钟节拍的前提下,实现寄存器的输出)。
三段式状态机也是比较推崇的,主要是由于维护方便, 组合逻辑与时序逻辑完全独立。
06. 总结
灵活选择状态机,不一定要拘泥理论,怎样方便怎样来
07.扩展
四段式不是指三个always代码,而是四段程序。使用四段式的写法,可参照明德扬GVIM特色指令Ztj产生的状态机模板。
明·德·扬四段式状态机符合一次只考虑一个因素的设计理念。
第一段代码,照抄格式,完全不用想其他的。
第二段代码,只考虑状态之间的跳转,也就是说各个状态机之间跳转关系。
第三段代码,只考虑跳转条件。
第四段,每个信号逐个设计。
有兴趣的话可以自己去学习一下,或者http://www.mdyedu.com/product/299.html自行看视频学习一下,努力,奋斗......
四
Test bench文件结构一览无余
01,前言
Verilog测试平台是一个例化的待测(MUT)模块,重要的是给它施加激励并观测其输出。逻辑模块与其对应的测试平台共同组成仿真模型,应用这个模型可以测试该模块能否符合自己的设计要求。
编写TESTBENCH的目的是为了对使用硬件描述语言设计的电路进行仿真验证,测试设计电路的功能、性能与设计的预期是否相符。通常,编写测试文件的过程如下:
产生模拟激励(波形);
将产生的激励加入到被测试模块中并观察其响应;
将输出响应与期望值相比较。
02,完成的Test bench文件结构
通常,一个完整的测试文件其结构为
module Test_bench();//通常无输入无输出
信号或变量声明定义
逻辑设计中输入对应reg型
逻辑设计中输出对应wire型
使用initial或always语句产生激励
例化待测试模块
监控和比较输出响应
endmodule
03,时钟激励设计
下面列举出一些常用的封装子程序, 这些是常用的写法, 在很多应用中都能用到。
3.1,时钟激励产生方法一
50%占空比时钟
parameter ClockPeriod=10;
initial
begin
clk_i=0;
forever
#(ClockPeriod/2) clk_i=~clk_i;
end
3.2,时钟激励产生方法二
50%占空比时钟
initial
begin
clk_i=0;
always #(ClockPeriod/2) clk_i=~clk_i;
end
3.3,时钟激励产生方法三:
产生固定数量的时钟脉冲
initial
begin
clk_i=0;
repeat(6)
#(ClockPeriod/2) clk_i=~clk_i;
end
3.4,时钟激励产生方法四
产生非占空比为50%的时钟
initial
begin
clk_i=0;
forever
begin
#((ClockPeriod/2)-2) clk_i=0;
#((ClockPeriod/2)+2) clk_i=1;
end
04,复位信号设计
4.1,复位信号产生方法一
异步复位
initial
begin
rst_n_i=1;
#100;
rst_n_i=0;
#100;
rst_n_i=1;
end
4.2,复位信号产生方法二
同步复位
initial
begin
rst_n_i=1;
@(negedge clk_i)
rst_n_i=0;
#100; //固定时间复位
repeat(10) @(negedge clk_i); //固定周期数复位
@(negedge clk_i)
rst_n_i=1;
end
4.3复位信号产生方法三
复位任务封装
task reset;
input [31:0] reset_time; //复位时间可调,输入复位时间
RST_ING=0; //复位方式可调,低电平或高电平
begin
rst_n=RST_ING; //复位中
#reset_time; //复位时间
rst_n_i=~RST_ING; //撤销复位,复位结束
end
endtask
05,双向信号设计
5.1,双向信号描述一
inout在testbench中定义为wire型变量
//为双向端口设置中间变量inout_reg作为inout的输出寄存,其中inout变
//量定义为wire型,使用输出使能控制传输方向
//inout bir_port;
wire bir_port;
reg bir_port_reg;
reg bi_port_oe;
assign bi_port=bi_port_oe ? bir_port_reg : 1'bz;
5.2双向信号描述二
强制force
//当双向端口作为输出口时,不需要对其进行初始化,而只需开通三态门
//当双向端口作为输入时,只需要对其初始化并关闭三态门,初始化赋值需
//使用wire型数据,通过force命令来对双向端口进行输入赋值
//assign dinout=(!en) din :16'hz; 完成双向赋值
initial
begin
force dinout=20;
#200
force dinout=dinout-1;
end
06,特殊信号设计
6.1特殊激励信号产生描述一
输入信号任务封装
task i_data;
input [7:0] dut_data;
begin
@(posedge data_en); send_data=0;
@(posedge data_en); send_data=dut_data[0];
@(posedge data_en); send_data=dut_data[1];
@(posedge data_en); send_data=dut_data[2];
@(posedge data_en); send_data=dut_data[3];
@(posedge data_en); send_data=dut_data[4];
@(posedge data_en); send_data=dut_data[5];
@(posedge data_en); send_data=dut_data[6];
@(posedge data_en); send_data=dut_data[7];
@(posedge data_en); send_data=1;
#100;
end
endtask
//调用方法:i_data(8'hXX);
6.2特殊激励信号产生描述二
多输入信号任务封装
task more_input;
input [7:0] a;
input [7:0] b;
input [31:0] times;
output [8:0] c;
begin
repeat(times) //等待times个时钟上升沿
@(posedge clk_i)
c=a+b; //时钟上升沿a,b相加
end
endtask
//调用方法:more_input(x,y,t,z); //按声明顺序
6.3,特殊激励信号产生描述三
输入信号产生,一次SRAM写信号产生
initial
begin
cs_n=1; //片选无效
wr_n=1; //写使能无效
rd_n=1; //读使能无效
addr=8'hxx; //地址无效
data=8'hzz; //数据无效
#100;
cs_n=0; //片选有效
wr_n=0; //写使能有效
addr=8'hF1; //写入地址
data=8'h2C; //写入数据
#100;
cs_n=1;
wr_n=1;
#10;
addr=8'hxx;
data=8'hzz;
end
Testbench中@与wait
//@使用沿触发
//wait语句都是使用电平触发
initial
begin
start=1'b1;
wait(en=1'b1);
#10;
start=1'b0;
end
07,仿真控制语句及系统任务描述
7.1,仿真控制语句及系统任务描述
$stop //停止运行仿真,modelsim中可继续仿真
$stop(n) //带参数系统任务,根据参数0,1或2不同,输出仿真信息
$finish //结束运行仿真,不可继续仿真
$finish(n) //带参数系统任务,根据参数0,1或2不同,输出仿真信息
//0:不输出任何信息
//1:输出当前仿真时刻和位置
//2:输出当前仿真时刻、位置和仿真过程中用到的memory以及CPU时间的统计
$random //产生随机数
$random % n //产生范围-n到n之间的随机数
{$random} % n //产生范围0到n之间的随机数
7.2,仿真终端显示描述
$monitor //仿真打印输出,大印出仿真过程中的变量,使其终端显示
/*
$monitor($time,,,"clk=%d reset=%d out=%d",clk,reset,out);
*/
$display //终端打印字符串,显示仿真结果等
/*
$display(” Simulation start ! ");
$display(” At time %t,input is %b%b%b,output is %b",$time,a,b,en,z);
*/
$time //返回64位整型时间
$stime //返回32位整型时间
$realtime //实行实型模拟时间
7.3文本输入方式
$readmemb/$readmemh
//激励具有复杂的数据结构
//verilog提供了读入文本的系统函数
$readmemb/$readmemh("<数据文件名>",<存储器名>);
$readmemb/$readmemh("<数据文件名>",<存储器名>,<起始地址>);
$readmemb/$readmemh("<数据文件名>",<存储器名>,<起始地址>,<结束地址>);
$readmemb:/*读取二进制数据,读取文件内容只能包含:空白位置,注释行,二进制数
数据中不能包含位宽说明和格式说明,每个数字必须是二进制数字。*/
$readmemh:/*读取十六进制数据,读取文件内容只能包含:空白位置,注释行,十六进制数
数据中不能包含位宽说明和格式说明,每个数字必须是十六进制数字。*/
/*当地址出现在数据文件中,格式为@hh...h,地址与数字之间不允许空白位置,
可出现多个地址*/
module
reg [7:0] memory[0:3];//声明8个8位存储单元
integer i;
initial
begin
$readmemh("mem.dat",memory);//读取系统文件到存储器中的给定地址
//显示此时存储器内容
for(i=0;i<4;i=i+1)
$display("Memory[%d]=%h",i,memory[i]);
end
endmodule
/*mem.dat文件内容
@001
AB CD
@003
A1
*/
//仿真输出为
Memory[0] = xx;
Memory[1] = AB;
Memory[2] = CD;
Memory[3] = A1;
08,总结
一个完整的设计,除了好的功能描述代码,对于程序的仿真验证是必不可少的。学会如何去验证自己所写的程序,即如何调试自己的程序是一件非常重要的事情。而RTL逻辑设计中,学会根据硬件逻辑来写测试程序,即Testbench是尤其重要的。
五
【很重要】Testbenth前仿真全过程
01. 前言
在FPGA 高手养成记-Test bench文件结构一览无余 只是简单的例举了常用的 testbench 写法,在工程应用中基本能够满足我们需求, 至于其他更为复杂的 testbench 写法, 大家可参考其他书籍或资料。
testbench没有像RTL代码设计那样严谨,我们可以在符合语法规则的前提下,随意编写我们的测试文件,有些在RTL代码中不可综合的语句,我们可以在testbench中实现。大体流程如下:
02. 测试模块设计
要测试我们的cpu需要ROM和RAM模块,这就需要我们先做好这两个模块
这里定义了一个 1024 x 8 的RAM
再定义一个8192 x 8 的ROM
ROM和RAM都还没有装入数据,等会我们会调用函数给他们装数据,接下来是地址译码器,来控制ROM和RAM的打开与关闭。
各模块建立好之后我们就开始仿真了。
03.仿真
这次教学我们用的是modelsim SE 10.0 版本进行教学,直接先在quartus II中建一个.v文件将其保存在原来的工程文件目录中,并命名为cpu_top.v,直接在这里写测试代码
下面大家可以来完成cpu 的仿真过程了
3.1,模块包含
首先,我们需要将我们刚写好的那几个模块包含进去,即CPU模块,ROM模块,RAM模块,地址译码器模块,并写好时间测量度,见下图
3.2,定义顶层模块
由于我们的设计只有两个输入,即时钟模块和复位模块,凡是输入信号在testbench中统一定义成reg型变量,凡是输出或者双向输入输出信号统一定义成wire型变量,我们的设计只有输入没有输出,故只定义输入和连线即可
下图便是我们要组成的测试顶层模块图,我们定义的wire型变量,实际就是我们顶层模块中,模块模块与模块间的连线。而这些连线就是我们cpu的输出,这样我们就可以用我们的测试模块来测试我们的cpu是否能正确工作
3.3. 元件例化
就是将各个模块连接起来即可,这里就不做太多的说明了,因为以前都写过很多次了
3.4.测试激励的书写
先写好时钟产生模块和复位模块.并将复位模块用task任务封装,这样我们在测试过程中就可以随时调用复位任务进行复位
时钟为50Mhz,复位时间为20ns
然后,我们再用task封装我们需要的模块,我们来想一下,上电后,CPU会从ROM中读两个时钟周期的数据是吧,但是我们的ROM现在还是空的,所以我们需要一个任务是往ROM中装入程序,给ROM中装数据我们可以用系统函数$readmemb,即打开一个文件,并将其中的数据送到我们之前定义的ROM中去
而test1.pro文件是需要我们自己定义的,我们可以在quartusII中再新建一个.v文件,在里面写上我们自己定义的程序,并将其保存为.pro文件即可,至于写什么程序,是我们随便定义的.
装完ROM和RAM的数据之后,按说就可以了进行波形仿真了,因为cpu是自动读取数据的,下面我们先来做第一步仿真,我先把之后的代码注释掉,大家先看没有被注释掉的代码
里面都是我们之前封装好的函数,刚开始进行复位,然后进行第一步测试,之后停止,将其保存之后,并默认为用其打开,打开后见下图
然后,file——new——library——ok即建好一个库
点击左上角的编译按钮,将我们之前写好的所有.v文件全部都编译进去
看到transcript一栏显示编译成功后即可,若没有transcript一栏,可以选择菜单中的view——transcript即可,若显示有红色错误,那就请读者按照它的要求进行修改代码,这说明你的代码有问题,一般是连接问题
3.5,波形仿真
编译成功后,双击cpu_top就可以开始波形仿真了
进入仿真页面后,我们右击cpu模块将其加入至波形
大家先看两个图,等会结合这两个图给大家细细讲解仿真过程
我们先来看第一个过程
上电后,cpu先从ROM中读回两个周期的数据,是从ROM的0地址开始的,再对比我们之前定义好的ROM,数据读取正确,读回的数据的前三位是111,即指令码JMP,后13位003c为地址码,JMP指令是将读回的数据作为新的地址码来读取相应地址的数据。那么,下一步,cpu应该是从ROM的003c地址处读数据才对,再看一下波形
对比波形后可知,cpu正好是从003c处读取数据,读到的数据指令码位111即JMP,地址码位0006,再到ROM的0006地址处看
这次读回的指令码位101,即LDA,也就是说将后13位地址码对应的RAM中的数据读回,送到累加器中,想一下,这时的RAM应该是打开的,而且双向输入输出口的数据总线上应该是来自RAM的8位数据,由于ROM0006地址处的地址码为1800是13位的,而RAM的地址是9位的,因此实际上我们从RAM中读回的数据是从RAM的0地址读回的,即我们之前给RAM写好的0000_0000,再看一下波形
正如我们所想的一样,数据总线上是0000_0000,RAM是打开的,地址为1800
就这样,读者可以自己再试一下,看看我们的cpu是不是按照我们之前给他的程序运行的,在这里我就不再给大家一一介绍了
虽然波形仿真很直观,但是看久了就会令人眼花缭乱,尤其是数据很多的时候,我们只能看其中一部分,不能讲所有数据看完整,这时候我们单单是用波形来仿真就远远不够了,下面介绍用系统任务仿真的过程
3.6,系统任务仿真
再回到我们的代码,注释掉了一些代码吧,我们把那些代码给加上,以其中一个过程为例
假设读回的指令码位101,即LDA,如果我在fentch_8的高电平期间且在cpu输出地址为奇数的时候记录一下此时的时间、指令、地址、目的地址、数据的话就可以不用看波形,让电脑来帮助我们来分析了,因此作如下处理
这里我延时60ns,是因为第一次记录的时候数据总线上还没有数据,只有延时一会才会有数据,即上面那张波形图右边那根黄色的线处记录一下数据,并将其显示。我们也可以加上一下标注,来帮助我们观察
这样我们再来仿真的时候就不用看波形了,直接打开transcript一栏观察记录即可
这样便可以为我们省下大量的仿真时间
04. 总结
这里提出以下几点建议供大家参考:
• 封装有用且常用的 testbench, testbench 中可以使用 task 或 function 对代码进行封装, 下次利用时灵活调用即可;
• 如果待测试文件中存在双向信号(inout)需要注意, 需要一个 reg 变量来表示输入, 一个 wire 变量表示输出;
• 单个 initial 语句不要太复杂,可分开写成多个 initial 语句, 便于阅读和修改;
• Testbench 说到底是依赖 PC 软件平台, 必须与自身设计的硬件功能相搭配。
六
串行口通信电路设计
1、顶层模块
写程序都一样,不能多有的程序都写在一个模块里,那样看起来很麻烦,出了错误也不好维护,对于一些小的程序我们可以写在一个模块里,但程序一旦复杂起来还是要懂得模块化编程的,对于顶层模块,最好是只写接口就好了,例如:
这段代码中,rx_232是我们的底层模块名,后面跟着的那个rx呢是我们自己取的名字,是任意的。后面的一大串呢就是接口,为了直观呢,建议大家采用我的这种写法,看上去比较清楚明白,括号里面的接口是我们顶层文件的接口,括号外面的是我们调用底层模块的接口,这些接口要一一对应正确才能保证数据之间的传输。
在顶层模块中,我们只定义了数据输入接口,用来接收数据,数据输出接口,用于发送数据,时钟接口,和复位接口。这四个接口是有输入输出关系的,对于其他的接口,是属于我们整个模块内部的接口,是模块与模块之间的接口,既非输入,也非输出,相当于一根导线一样,所以我们把他们定义成wire型变量
2、波特率选择模块
单片机或者计算机在串口通信时的传输速率用波特率表示,9600bps表示的就是每秒钟传送9600位的数据,这里之所以计数到5027,在这里算一下。
1秒传送9600位,那么传送一位的时间就可以算出,即1s=1000_000_000ns,所以传送一位数据需要1000_000_000/9600=
104166ns,而我们的时钟周期为20ns,因此需要计数到104166/20=5028个时钟周期
下面是串口通信时序图
我再来解释一下这个图吧,我当时学单片机的时候还真是没怎么重视这张图,只知道只要一个指令就可以发送,没有真正搞清楚是怎么发送和接受的,那就在这里复习一下吧,计算机和单片机之间进行通信,这里用的是rs232通信方式,即通信之前,计算机和单片机之前要设定好相同的波特率,只有波特率相同了才能进行通信。
其次,计算机发送数据时要先发送一个起始位,一般是低电平,后面跟着的是8位数据位,奇偶校验位,停止位等,当起始位低电平信号传送到我们的接收端口时,在接收模块中会发送一个命令给波特率时钟计数器,开始计时,计时到一半的时候会产生一个采样高脉冲信号,当接收模块检测到这个高脉冲之后就会将数据存到寄存器中,当检测到第11个脉冲信号时,也就是代表一帧的数据接收完毕,发送模块就给波特率选择模块发送一个停止信号告诉它停止计时。
同时,当数据接收完毕之后也会产生一个信号告诉发送模块,信号已经接收完毕,准备发送,这个时候发送模块再给波特率计时模块发送一个信号开始计时,计数到某一位的中间时产生一个采样信号,当发送模块检测到采样信号之后就将寄存器里的数据送到发送端,每次只送一位,这样就实现了数据的接收与发送。
下面是波特率计时模块的主要程序部分
3、数据接收模块
在接收模块中,为了准确的检测计算机发送来的数据起始位的那个低电平信号,用到了边沿脉冲检测法,可以有效的避免毛刺现象带来的问题
下面是发送部分的主要程序段
4、数据发送模块
发送模块原理上和接受模块是一样的,不同点就是接收模块通过边沿检测法检测起始位低电平信号来启动接收数据,而发送模块是通过检测数据发送完毕后,我们认为得置一个低电平信号,发送模块通过检测这个低电平信号来启动发送。见下图
下面是生成的RTL视图
下面是测试结果
七
手把手解析时序逻辑乘法器代码
下面是一段16位乘法器的代码,大家可以先浏览一下,之后我再做详细解释
module mux16(
clk,rst_n,
start,ain,bin,yout,done
);
input clk; //芯片的时钟信号。
input rst_n; //低电平复位、清零信号。定义为0表示芯片复位;定义为1表示复位信号无效。
input start; //芯片使能信号。定义为0表示信号无效;定义为1表示芯片读入输入管脚得乘数和被乘数,并将乘积复位清零。
input[15:0] ain; //输入a(被乘数),其数据位宽为16bit.
input[15:0] bin; //输入b(乘数),其数据位宽为16bit.
output[31:0] yout; //乘积输出,其数据位宽为32bit.
output done; //芯片输出标志信号。定义为1表示乘法运算完成.
reg[15:0] areg; //乘数a寄存器
reg[15:0] breg; //乘数b寄存器
reg[31:0] yout_r; //乘积寄存器
reg done_r;
reg[4:0] i; //移位次数寄存器
//------------------------------------------------
//数据位控制
always @(posedge clk or negedge rst_n)
if(!rst_n) i <= 5'd0;
else if(start && i < 5'd17) i <= i+1'b1;
else if(!start) i <= 5'd0;
//------------------------------------------------
//乘法运算完成标志信号产生
always @(posedge clk or negedge rst_n)
if(!rst_n) done_r <= 1'b0;
else if(i == 5'd16) done_r <= 1'b1; //乘法运算完成标志
else if(i == 5'd17) done_r <= 1'b0; //标志位撤销
assign done = done_r;
//------------------------------------------------
//专用寄存器进行移位累加运算
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
areg <= 16'h0000;
breg <= 16'h0000;
yout_r <= 32'h00000000;
end
else if(start) begin //启动运算
if(i == 5'd0) begin //锁存乘数、被乘数
areg <= ain;
breg <= bin;
end
else if(i > 5'd0 && i < 5'd16) begin
if(areg[i-1]) yout_r = {1'b0,yout[30:15]+breg,yout_r[14:1]}; //累加并移位
else yout_r <= yout_r>>1; //移位不累加
end
else if(i == 5'd16 && areg[15]) yout_r[31:16] <= yout_r[31:16]+breg; //累加不移位
end
end
assign yout = yout_r;
endmodule
要理解这段代码,首先要弄明白几个点。
1、我们通常写的十进制的乘法竖式,同样适用于二进制。下面我们就以这个算式为例:1011 x 0111 =0100_1101。
2、两个16位的数相乘,结果是32位的,没有32位要在高位补零。
3、计算两个16位的数相乘需要移位15次。
例如:
1 0 1 1
x 0 1 1 1
------------------------------------
1 0 1 1
1 0 1 1
1 0 1 1
0 0 0 0
------------------------------------
1 0 0 1 1 0 1
前三次计算是移位的,最后一次没有移位
4、两个16位的数相加,结果是17位的,不够17位最高位补零。
例如语句yout[30:15]+breg,结果是17位的。
知道了这些,我们就开始看代码了
1)、接口部分注释写的很清楚,这里就不提了
2)、数据位控制部分
always @(posedge clk or negedge rst_n)
if(!rst_n) i <= 5'd0;
else if(start && i < 5'd17) i <= i+1'b1;
else if(!start) i <= 5'd0;
当start为1时,芯片读入两个数,此时开始计数,计数16次,乘法运算开始
3)、乘法运算完成标志信号产生
always @(posedge clk or negedge rst_n)
if(!rst_n) done_r <= 1'b0;
else if(i == 5'd16) done_r <= 1'b1; //乘法运算完成标志
else if(i == 5'd17) done_r <= 1'b0; //标志位撤销
assign done = done_r;
这部分也很好理解
4)、专用寄存器进行移位累加运算
这里为了简单,就用15到18位代替15到30位
以上部分是最主要的计算部分,其他地方相对来说还比较简单,例如当乘数某一位为0时,不用累加,直接右移,当i计数到16时,此时就不用再移位了,可以直接用位数表示,直接累加即可。
下面是仿真图
八
基于FIFO的串口发送机设计全流程
首先来解释一下FIFO的含义,FIFO就是First Input First Output的缩写,就是先入先出的意思,按照我的理解就是,先进去的数据先出,例如一个数组的高位先进,那么读出来的时候也就高位先出。下面是百度百科的解释。
FIFO一般用于不同时钟域之间的数据传输,比如FIFO的一端是AD数据采集,另一端是计算机的PCI总线,假设其AD采集的速率为16位 100K SPS,那么每秒的数据量为100K×16bit=1.6Mbps,而PCI总线的速度为33MHz,总线宽度32bit,其最大传输速率为1056Mbps,在两个不同的时钟域间就可以采用FIFO来作为数据缓冲。另外对于不同宽度的数据接口也可以用FIFO,例如单片机为8位数据输出,而DSP可能是16位数据输入,在单片机与DSP连接时就可以使用FIFO来达到数据匹配的目的。
我们将这三个模块分别定义为dataoutput块,fifo_ctrl块和uart_ctrl块。现在考虑连线,具体到每一根连线,这样根据图来写代码要比直接用脑子构图要方便的多。三个模块,先考虑时钟和复位信号线,三个模块都有,然后,数据产生模块要将产生的数据发给FIFO模块,所以要有数据写入线,我们定义它为wr-datain,数据写入FIFO块后总要输出,这些数据就是我们要发送的数据,所以定义输出数据线tx_data,先不管FIFO,我们再来定义数据发送模块的连线,数据发送总要有个启动信号,所以我们定义变量tx_start,之后,还要有一个输出端给PC机,我们定义这个输出端位rs232。
对于FIFO模块的例化过程很简单就不做过多的说明,只把接口说一下,FIFO模块除了时钟,复位信号外,还有数据输入端口,这个端口要和之前的数据产生模块的数据输出端口相连,还有写请求端口,高电平有效,数据发送模块每隔1秒钟产生一个16位的数据,并发送写请求命令给FIFO,还有读请求命令,高电平有效数据发送模块在发送数据时要发送一个读请求给FIFO,从中读取数据后再发送给PC机,还有空信号empty,只要检测到FIFO中有数据,empty就为低电平,我们可用这个信号来启动数据发送模块。这样一来,我们的整体框架就出来了有了这个整体框架,再写代码就容易多了。
下面是RTL视图
按照这个框架,先把接口定义出来,中间的连线用wire型
设计完端口之后我们就来设计底层模块,先设计数据产生模块dataoutput,这个部分主要是产生数据,可用一个分频电路实现每1s发送一次的数据,产生这16位数据的时候,需要16个时钟,每个时钟数据自加1,总体来说比较简单
写完一个模块之后养成好习惯,马上把端口例化
数据产生以后就要进入缓冲器FIFO,由于这段代码我们是调用的,所以只要例化接口就好了,只需要将产生的fifo_ctrl_inst文件中例化好的代码拷贝粘贴就好
最后我们要写数据发送部分,之前已经讲过,数据发送部分还要包括两个子模块,一个是波特率匹配模块,一个是发送模块,既然又包括两个子模块,那么我们还要构建一个框图
按照之前的例子,当FIFO当中有数据时empty就会拉低,我们把它取反后送给发送模块,告诉发送模块准备发送,这样,发送模块就会产生一个波特率计数器启动信号bps_start给波特率匹配模块,波特率匹配模块收到信号后立马开始匹配计数,并产生采集信号,将采集信号传给发送模块,发送模块根据采集信号,将数据一位一位发送出去。知道了这个原理之后,我们构建起这样一个框架
根据这个框图,我们定义端口和线
定义完端口之后,开始写发送模块,用边沿脉冲检测法检测启动信号tx_start信号的上升沿来启动发送部分,波特率配置模块具体代码在前面也文章中有给出,就不在说明,写完之后例化端口,这两个模块作为数据发送模块的子模块,要在数据发送模块下例化
这样一来,我们整个设计就完成了,看上去很简单,但是从我自己实践的角度来说还是有点挑战的,包括中间出现的各种问题,下面就来分享一下我在做这个设计时遇到的问题
1.例化问题
在例化端口时,要注意括号里面的才是本层模块的端口,也就是说在本层模块上面已经定义过的变量,括号外面的才是被调用模块的端口,也在下层模块的顶部被声明,我在写这段程序的时候将二者颠倒了,导致连线不成功,最终是通过查看RTL视图知道了哪根线有问题才修改成功的
2.同一个变量不能在多个always语句中被赋值
我们可能习惯这么写
always @ (posedge clk ornegedge rst_n)
if(!rst_n) num <= 1'b0;
else num <= num+1'b1;
那么,num的值在其他always语句中就不允许再被赋值或者清零,我在写的时候在其他always语句中将num 清零了,导致编译不成功
3. 定义变量之前不要出现该变量,即使后面又定义了
例如,我先进行num的运算,之后再定义num,reg [3:0] num,这样写的话虽然编译没有错误,但是在调用modelsim仿真的时候它会出现编译错误,所以为了规范,不要这样写
4. 在边沿脉冲检测的时候,习惯于检测下降沿,而这里是检测tx_en 的上升沿,所以我在复位清零的时候错误的将两级寄存器赋值为0,实际上在检测上升沿时要对两级寄存器复位时置一,再把最后一级寄存器取反后与上一级相与。
5. 在发送数据部分,由于受到上次写接收部分程序的影响,没有将起始位发送出去,因为在接收部分,是不需要接收起始位的,是从第一位开始,而在发送部分只有先发送起始位才能和上位机握手通信,还有在发送完数据后要发送停止位,其他情况下都发送高电平来阻止通信的进行
6.最后一个问题是最棘手的问题,我找了好大一半天也没发现,最后还是根据源代码找出来的,不过我还是不知道将这两条语句颠倒了对程序有什么影响,只知道颠倒后数据会一直在发送,不会像预设一样,每隔一秒发送一次,至今还是搞不清楚,希望大神指点迷津
总结
语法上的错误到不至于太难,写的多了就不会出错了,关键是逻辑上的错误很隐蔽,也很难发现,可以通过RTL视图来检测连线上是否正确,还可以借助仿真工具。
来源:技术让梦想更伟大