目录
目录
1. FPGA的简介
FPGA(Field-Programmable Gate Array),即现场可编程门阵列,它是在PAL、GAL、CPLD等可编程器件的基础上进一步发展的产物。它是作为专门集成电路(ASIC)领域中的一种半定制电路而出现的,既解决了定制电路的不足,又克服了原有可编程器件门电路数有限的缺点。FPAG的主要特点如下:
1)采用FPGA设计ASIC电路(专用集成电路),用户不需要投片生产,就能得到合用的芯片。
2)FPGA可做其它全定制或半定制ASIC电路的中试样片。
3)FPGA内部有丰富的触发器和I/O引脚。
4)FPGA是ASIC电路中设计周期最短、开发费用最低、风险最小的器件之一。
5) FPGA采用高速CMOS工艺,功耗低,可以与CMOS、TTL电平兼容。
可以说,FPGA芯片是小批量系统提高系统集成度、可靠性的最佳选择之一。此外,由于FPGA是由存放在片内RAM中的程序来设置其工作状态的,因此,工作时需要对片内的RAM进行编程。
2. Verilog语言
2.1 数据类型
2.1.1 常量
整数:整数可以用二进制b或者8,八进制用o或者O,十进制用d或者D,十六进制用h或者H表示,例如:8’b00001111表示8位位宽的二进制整数,4’ha表示位宽的十六进制整数。
X和Z:x表示不定值,z表示高阻值,例如,5’b00x11,第三位不定值,3’b00z表示最低位为最高值。
下划线:在位数过长时可以用于分割,例如:8’b0000_1111。
参数(parameter):可以用标识符定义常量。
参数的传递:在一个模块有定义参数,在其他模块调用此模块并传递/修改参数时,可以module后用#()表示,例如:
module rom #( parameter depth = 15)。
注:localparam仅可以用于本模块内使用,不可以用于参数传递。
2.1.2 变量
Verilog 中变量的物理数据分为线型(wire型)和寄存器型(reg型)。这两种类型的变量在定义时要设置位宽,缺省为1位。变量的每一位可以是0,1,X,Z。
wire 型
wire类型变量,也叫网络类型变量,用于结构实体之间的物理链接,例如门与门之间,就像是一根连接线,不可以传储存值。利用连续赋值语句assign,可以表示两个结点的链接。例如:
wire a; //定义
assign a = b;//将b结点连接到连线a上
reg型
reg类型变量,也称为寄存器变量,可以用来储存值,但必须在always语句里使用。其定义为reg[n-1:0] a; 表示n位位宽的寄存器。也可以生产组合逻辑,例如数据选择器,敏感信号没有时钟,定义了reg Mux,最终生成为组合逻辑。
module top(a ,b ,c ,d ,sel ,Mux);
input a;
input b;
input c;
input d;
input [1:0] sel;
output reg Mux ;
always @(sel or a or b or c or d)
begin
case(sel)
2'b00 : Mux = a;
2'b01 : Mux = b;
2'b10 : Mux = c;
2'b11 : Mux = d;
endcase
end
endmodule
Memory型
可以利用memory类型来定义RAM,ROM的等存储器,其结构为reg [n-1:0] 存储器名称 [m-1:0], 其意义是m个n位宽度的寄存器。例如, reg [7:0] ram [255:0]表示了256个8位寄存器,256是存储器的深度而8是数据宽度。
2.2 Verilog的综合语言
由于大部分的关键字已经在平常的C语言编程中较为熟悉了,这里只介绍几个笔者不太熟悉的。
2.3.1 always @ ()
always语句本身并不是单一的、有意义的一条语句,而是和下面的赋值语句一起构成一个语句块,在块中进行赋值。该语句块并不是总处于激活状态,需要满足激活条件时才被执行,否则将会被挂起,当处于挂起状态时,即使操作数有变化也不会进行赋值,以确保目标值不变。此外,赋值的目标也必须是reg型 。
1. always @ (posedge CLK or negedge RSTn) // 当 CLK 和 RSTn 变化的时候
2. always @ ( * ) // 什么时候都变化, 即默认为组合逻辑
3. always @ ( A ) // 当 A 变化的时候
always @ () 的用法很多,但是用得最多的就是第 1 个和第 2 个。always模块内部的逻辑是按顺序执行,而多个always块之间是并行执行的。
2.3.2 assign
assign相当于连线,一般是将一个变量的值不间断地赋值给另一个变量,就像把这两个变量连在一起,所以习惯性的当做连线用,比如把一个模块的输出给另一个模块当输入。例如:
wire A,B,SEL,L; //声明4个线型变量
assign L=(A&~SEL)|(B&SEL); //连续赋值
在assign语句中,左边变量的数据类型必须是wire型。input和output如果不特别声明类型,默认是wire类型。
2.3.3 = 和 <=
基本上要搞懂这两个赋值操作符号的作用,就必须把“时序”的概念搞懂先。一般上,在时序中"="是引发"即时事件”,“<=”则是引发“时间点事件”。阻塞赋值(=)为执行完一条赋值语句,再执行下一条,可以理解为按顺序执行,但赋值是即刻执行的;非阻塞赋值(<=)可以理解为是并行执行的不考虑顺序,但需要在always 语句执行完后才能进行赋值。简单来说就是,一律有关 RTL 级活动的使用 “<=”,一律有关组合逻辑级的活动都使用“=”。
2.3.4 拼接运算符
“{}”拼接运算符,就是将多个信号按位拼接,例如{a[3:0],b[3:0]}将a的低四位和b的低四位并称为八3位数据。另外,{n{a[3:0]}}表示将n个[3:0] 拼接 ,{n{1'b0}}表示将n个位的0拼接。
2.3.5 条件运算符
条件运算符''? :"是一个三元运算符,即由三个参与运算的量。条件表达式的一般形式为表达式1?表达式2:表达式3,其执行过程是当表达式1为真时,则表达式2作为条件表达式的值,否则将以表达式3作为条件表达式的值。例如:a = 6, b = 7, c = ( a>b)?a:b的结果为7。
2.4 Verilog的系统函数
Verilog语言中预先定义了一些任务和函数,用于完成一些对应的功能,它们被称为系统任务和系统函数,这些函数大多只能在Testbench仿真中使用。常用的函数如下所示:
2.4.1 $display、$strobe和$write
$display("p1",p2,....pn);//例如:$display("%b+%b = %d",a,b,c);
$write("p1",p2,....pn);//例如:$write("%b+%b = %d",a,b,c);
$strobe("p1",p2,....pn);//例如:$strobe("%b+%b = %d",a,b,c);
这三个函数和系统任务的都可以用来输出和打印信息,即将参数p2到pn按参数p1给定的格式输出。参数p1通常称为“格式控制”,参数p2至pn通常称为“输出表列”。三者的不同点是:$display自动地在输出后进行换行,$write则不是这样,而$strobe只在程序最后执行。
2.4.2 $monitor
$monitor("p1",p2,....pn);//$monitor("p1",p2,....pn);//例如:$display("%b+%b = %d",a,b,c);
$monito常用于持续性检测,它提供了监控和输出参数列表中的表达式或变量值的功能。其参数列表中输出控制格式字符串和输出表列的规则和$display中的一样,但具有被测变量变化触发打印的机制。
2.4.3 $stop和$finish
$stop/$finish;
$stop/$finish(n);
$stop一般用于仿真暂停,而$finish用作仿真停止、结束。值得注意的是,第二种表达方式中的n指的是参数值,根据不同的参数值会输出相对应的信息。
参数值 | 输出信息 |
---|---|
0 | 不输出任何信息 |
1 | 输出当前仿真时刻和位置 |
2 | 输出当前仿真时刻,位置和在仿真过程中所用memory及CPU时间的统计 |
注:若使用stop后想使仿真继续进行,则需要在 控制面板端输入return -continue。
2.4.4 $time和$realtime
这两个时间系统函数可以得到当前的仿真时刻,$time可以返回一个64位的整数来表示的当前仿真时刻值,而$realtime返回的时间数字是一个实型数,该时刻都是以模块的仿真时间尺度为基准的。但是由于realtime会得到一个real型变量,所以可以表示小数时间,并且其值会自动缩放到`timescale任务所定义的时间单位。
注:`timescale timeunit / timeprecision 是用来定义仿真的单位时间和精度,以下是一些注意事项:
- 时间单位和时间精度只能是1、10和100这3个整数中的某个
- 单位可以是s、ms、us、ns、ps和fs
- 时间精度必须小于或等于时间单位
2.4.5 $readmemb和$readmemh
$readmemb("<数据文件名>",<存储器名>);
$readmemh("<数据文件名>",<存储器名>);
这两个函数都是读取文件函数 但不同之处在于前者是用来读取二进制文件函数而后者是用来读取16进制的。
2.5 Verilog的语言结构
Verilog HDL程序是由模块构成的,每个模块的内容都是镶嵌在module跟endmodule两个语句之间,并且有其特定的功能,模块之间是可以进行层次嵌套的。因此,为了更好理解程序的底层逻辑,我们将从module展开学习。
2.5.1 module
verilog中最基本的模块是module,就可以看做是一个封装好的模块,我们用verilog来写很多个基本模块,然后再用verilog描述多个模块之间的接线方式等,将多个模块组合得到一个系统。一般来说,module分为以下五个部分:模块名、端口定义列表、I/O信号说明、内部信号的声明、模块功能的实现 。例如:
//定义一个二选一多路选择器
module mux21 (A,B, outdata, sel);//模块名(端口1,端口2,...)
input [2:0] A; //I/O口的说明
input [2:0] B;
input sel;
outdata [2:0] outdata;
always@ (sel,A,B) //模块功能的实现
begin
if(sel)
outdata = A;
else
outdata = B;
end
endmodule
- 模块名是指电路的名字,由用户指定最好与文件名一致;
- 端口列表是指电路输入输出信号名称列表,信号名由用户指定个名称间用“,”隔开;
- 端口信号声明是要说明端口信号的输入输出属性、信号的数据类型及位宽;
- 参数说明要指出参数的名称和初值。
2.5.2 module的调用
假如我们已经定义好了一种类型的module,那么我们可以 需要的地方直接调用,其具体实施方法则是:module类型 module名称(.PortA(WireA), .PortB(WireB), ...);
这里借用上文提到的二选一线路器来构成三选一多路选择器进行举例:
//两次调用二选一多路选择器,构成三选一的效果
module mux31 (dataa, datab, datac, sel1, sel2, outdata);
input[2:0] dataa;
input[2:0] datab;
input [2:0] datac;
input sel1;
input sel2;
output [2:0] outdata;
wire [2:0] data;
mux21 mux21_dut1( //调用mux21
.A(dataa),
.B(datab),
.outdata(data),
.sel(sel1)
);
mux21 mux21_dut2(
.A(data),
.B(datac),
.outdata(outdata),
.sel(sel2)
);
endmodule
2.5.3 常用行为描述
以下的三种语句均必须用于过程模块中(initial和always),其中if-else与case语句用法和C类似,故不再赘述。
- if-else语句
从下至上依次进行条件判断
- case语句
仅进行一次条件判断,例如:
case(sel)
2'b00: q=a;
2'b01: q=b;
2'b10: q=c;
default: q=d;
endcase
注:casez表示不考虑“z”的值,而casex则同时不考虑“z”和“x”。
- loop语句
用于循环重复操作
1.forever loop: 连续循环执行,不可综合
例:forever #25 clk=~clk;
2.repeat loop: 执行n次循环,可综合
例:repeat(n) clk=clk+1;
3.while loop: 满足条件执行循环,不可综合
例:while(count<100) begin
end
4.for loop: 满足条件执行循环,不可综合
例:for(count=0;count<100;count=count+1) begin
end
2.6 奇数、偶数分频
频率是在电子中例如方波信号中指每秒钟周期的次数,所谓分频就是吧周期通过一定的办法给分解了,以达到降频的效果。n分频就是指,原来的信号经过n的周期,新的信号跳变一次。这样20Mhz,2分频就是10mhz,5分频就是4mhz,10分频就是1mhz。分频之后可以使输入的速度降低,单片机可以更好的响应,否则振荡频率巨高,信号持续的周期过短,主机将很难进行实际操作。计数程序可以设定每采集一个信号对应着多少个频率振荡,这对现实频率的采集结果完全没有影响,而且使得单片机工作起来占用资源更少,不必过于频繁的读取外部信号。
通常分频时,我们需要考虑分配系数n是奇数还是偶数,如果是前者则只需要上升沿采集,若后者则需要两端采集,例如:
//4分频电路设计
module divider_4 //模块名
(
input sys_clk, //时钟(设定为 50MHz)
input sys_rst_n, //复位信号(n 表示低电平有效)
output reg clk_4 //输出4分频信号
);
reg cnt; //reg 定义
//计数模块
//从0计数到1共计2个时钟周期
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
cnt <= 1'd0; //复位清零
else if(cnt == 1'd1) //从0开始计数,所以需要 -1
cnt <= 1'd0; //计满则清零
else
cnt <= cnt + 1'd1; //没记满就一直计数
end
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
clk_4 <= 1'b0; //复位清零
else if(cnt == 1'd1) //记满2个时钟周期
clk_4 <= ~clk_4; //计满则输出反转
else
clk_4 <= clk_4; //没记满就保持原来状态
end
endmodule
//3分频电路设计
module divider_3 //模块名
(
input sys_clk, //时钟(设定为 50MHz)
input sys_rst_n, //复位信号(n 表示低电平有效)
output reg clk_3 //输出3分频信号
);
reg [1:0] cnt3; //reg 定义
//计数模块
//从0计数到2共计3个时钟周期
always@(sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
cnt3 <= 1'd0; //复位清零
else if(cnt == 2'd2) //从0开始计数,所以需要 -1
cnt3 <= 2'd0; //计满则清零
else
cnt3 <= cnt3 + 1'd1; //没记满就一直计数
end
always@(sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
clk_3 <= 1'b0; //复位清零
else if(cnt == 2'd2) //记1个半时钟周期
clk_3 <= ~clk_3; //计满则输出反转
else
clk_3 <= clk_3; //没记满就保持原来状态
end
endmodule
免责声明:本文所引用的各种资料均用于自己学习使用,这里感谢黑金和野火官方的资料以及各位优秀的创作者。