首先附上小梅哥FPGA视频教程链接:https://www.bilibili.com/video/BV1va411c7Dz?p=2&spm_id_from=pageDriver
小梅哥yyds!!!!!!
FPGA(Field-Programmable Gate Array), 即现场可编程门阵列,它是在PAL、GAL、CPLD等可编程器件的基础上进一步发展的产物。它是作为专用集成电路(ASIC)领域中的一种半定制电路而出现的,既解决了定制电路的不足,又克服了原有可编程器件门电路数有限的缺点。
以下是本篇文章正文内容,下面案例可供参考
Verilog 的设计多采用自上而下的设计方法(top-down),即先定义顶层模块功能,进而分析要构成顶层模块的必要子模块;然后进一步对各个模块进行分解、设计,直到到达无法进一步分解的底层功能块。这样,可以把一个较大的系统,细化成多个小系统,从时间、工作量上分配给更多的人员去设计,从而提高了设计速度,缩短了开发周期。
工作人员需要对用户提出的功能要求进行分析理解,做出电路系统的整体规划,形成详细的技术指标,确定初步方案。例如,要设计一个电子屏,需要考虑供电方式、工作频率、产品体积、成本、功耗等,电路实现采用 ASIC 还是选用 FPGA/CPLD 器件等。
功能划分
正确地分析了用户的电路需求后,就可以进行逻辑功能的总体设计,设计整个电路的功能、接口和总体结构,考虑功能模块的划分和设计思路,各子模块的接口和时序(包括接口时序和内部信号的时序)等,向项目组成员合理分配子模块设计任务。
文本描述
可以用任意的文本编辑器,也可以用专用的 HDL 编辑环境,对所需求的数字电路进行设计建模,保存为 .v 文件。
功能仿真(前仿真)
对建模文件进行编译,对模型电路进行功能上的仿真验证,查找设计的错误并修正。
此时的仿真验证并没有考虑到信号的延迟等一些 timing 因素,只是验证逻辑上的正确性。
逻辑综合
综合(synthesize),就是在标准单元库和特定的设计约束的基础上,将设计的高层次描述(Verilog 建模)转换为门级网表的过程。逻辑综合的目的是产生物理电路门级结构,并在逻辑、时序上进行一定程度的优化,寻求逻辑、面积、功耗的平衡,增强电路的可测试性。
但不是所有的 Verilog 语句都是可以综合成逻辑单元的,例如时延语句。
布局布线
根据逻辑综合出的网表与约束文件,利用厂家提供的各种基本标准单元库,对门级电路进行布局布线。至此,已经将 Verilog 设计的数字电路,设计成由标准单元库组成的数字电路。
时序仿真(后仿真)
布局布线后,电路模型中已经包含了时延信息。利用在布局布线中获得的精确参数,用仿真软件验证电路的时序。单元器件的不同、布局布线方案都会给电路的时序造成影响,严重时会出现错误。出错后可能就需要重新修改 RTL(寄存器传输级描述,即 Verilog 初版描述),重复后面的步骤。这样的过程可能反复多次,直至错误完全排除。
FPGA/CPLD 下载或 ASIC 制造工艺生产
完成上面所有步骤后,就可以通过开发工具将设计的数字电路目标文件下载到 FPGA/CPLD 芯片中,然后在电路板上进行调试、验证。
如果要在 ASIC 上实现,则需要制造芯片。一般芯片制造时,也需要先在 FPGA 板卡上进行逻辑功能的验证。
实现功能为:通过sel按钮,绝对输出的led灯状态为按钮a的值还是按钮b的值。
设计文件代码如下:
//所有程序总是从modbule开始,endmodule结束
module mux2(
a,
b,
sel,
out
) ;
//配置变量的输入输出模式
//配置端口
input a;
input b;
input sel;
output out;
//assgin为关键词,赋值,不断进行sel值判断,并给out赋值
//二选一多路器,通过sel的值决定输出的为in1的值还是in2的值
assign out = (sel == 1)?a:b;
endmodule
test bench代码如下所示:
`timescale 1ns/1ns //时间刻度 前面的1ns是时间路径 后面的代表精度
module mux2_tb();
reg s_a; //a为输入端口,此时需要给a激励信号,所以使用reg
reg s_b;
reg sel;
wire out; //wire就是一个线网
mux2 mux2_inst0( //前面为原始名称,后面为逆化名称,可随意进行编写,也可与原有名称相同
.a(s_a),
.b(s_b),
.sel(sel),
.out(out)
) ;
initial begin
s_a = 0;s_b=0;sel=0;
#200; //延迟200,仅仅用于仿真中
s_a = 0;s_b=0;sel=1;
#200; //延迟200,仅仅用于仿真中
s_a = 0;s_b=1;sel=0;
#200; //延迟200,仅仅用于仿真中
s_a = 0;s_b=1;sel=1;
#200; //延迟200,仅仅用于仿真中
s_a = 1;s_b=0;sel=0;
#200; //延迟200,仅仅用于仿真中
s_a = 1;s_b=0;sel=1;
#200; //延迟200,仅仅用于仿真中
s_a = 1;s_b=1;sel=0;
#200; //延迟200,仅仅用于仿真中
s_a = 1;s_b=1;sel=1;
#200; //延迟200,仅仅用于仿真中
end
endmodule
A.组合逻辑电路
组合逻辑电路在逻辑功能上的特点是任意时刻的输出仅仅取决于该时刻的输入,与电路原来的状态无关。
组合逻辑电路在电路结构上,不涉及对信号跳变沿的处理,无存储电路,也没有反馈电路,通常可以通过真值表的形式表达出来。
B.时序逻辑电路
时序逻辑从电路特征上看来,其特点为任意时刻的输出不仅取决于该时刻的输入,而且还和电路原来的状态有关。
时序逻辑电路在电路结构上,不管输入如何变化,仅当时钟的沿(上升沿或下降沿)到达时,才有可能使输出发生变化。
译码器(Decoder)是一种多输入多输出的组合逻辑电路,负责将二进制代码翻译为特定的对象(如逻辑电平等),功能与编码器相反。
手上只有Alinx的ZYNQ开发板,因此将视频教程中的38译码器改为24。
主要功能为:通过a、b两个按钮的不同状态,控制4个LED灯的亮灭
设计文件代码如下:
module decoder_2_4(
a,
b,
out
);
input a;
input b;
output reg[3:0] out; //[3:0]表示带宽为4位
// reg [3:0]out; //定义out为reg型(定义对象的类型)
// wire [3:0]d;
// assign d = {a,1'b0,b,c};
//以Always块描述的信号赋值,被赋值对象必须定义为reg型
//一直关注括号内的内容 *代表所有输入参数
always@(*)begin
//等效于always@(a,b,c);
case({a,b}) //{a,b} 变成一个两位的信号,这种操作叫做位拼接
2'b00: out = 4'b0001;//当监测到a为0,b为0时,输出0001
2'b01: out = 4'b0010;
2'b10: out = 4'b0100;
2'b11: out = 4'b1000;
endcase
end
// b 二进制 3'b101 8'b0000 1010
// o 八进制
// d 十进制 3'd5 8'd10
// h 十六进制 8'ha
endmodule
test bench代码如下所示:
`timescale 1ns/1ns //配置时间尺度
module decoder_2_4tb(); //引用设计文件
reg s_a; //定义仿真程序变量
reg s_b;
wire [3:0]out;
decoder_2_4 decode_2_4(
.a(s_a),
.b(s_b),
.out(out)
); //第一个decoder_2_4相当于原函数名 第二个相当于新函数名(可随意设置) s_a相当于实参,a相当于形参
initial begin
s_a = 0 ; s_b = 0;
#200; //#代表延时,单位为ns
s_a = 0 ; s_b = 1;
#200;
s_a = 1 ; s_b = 0;
#200;
s_a = 1 ; s_b = 1;
#200;
end
endmodule
//最终可通过仿真观测信号变化
0:逻辑 0 或 "假"
1:逻辑 1 或 "真"
x 或 X:未知
z 或 Z:高阻
x 意味着信号数值的不确定,即在实际电路里,信号可能为 1,也可能为 0。
z 意味着信号处于高阻状态,常见于信号(input, reg)没有驱动时的逻辑结果。
例如一个 pad 的 input 呈现高阻状态时,其逻辑值和上下拉的状态有关系。上拉则逻辑值为 1,下拉则为 0
指明位宽:
4'b1011 // 4bit 数值
32'h3022_c0de // 32bit 的数值(其中,下划线 _ 是为了增强代码的可读性)
一般直接写数字时,默认为十进制表示,例如下面的 3 种写法是等效的:
counter = 'd100 ; //一般会根据编译器自动分频位宽,常见的为32bit
counter = 100 ;
counter = 32'h64 ;
十进制:
30.123
6.0
3.0
0.001
科学计数法:
1.2e4 //大小为12000
1_0001e4 //大小为100010000
1E-3 //大小为0.001
字符串表示方法:
字符串是由双引号包起来的字符队列。字符串不能多行书写,即字符串中不能包含回车符。
Verilog 将字符串当做一系列的单字节 ASCII 字符队列。
例如,为存储字符串 "www.runoob.com", 需要 14*8bit 的存储单元。例如:
reg [0: 14*8-1] str ;
initial begin
str = "www.runoob.com";
end
线网(wire)
wire 类型表示硬件单元之间的物理连线,由其连接的器件输出端连续驱动。
如果没有驱动元件连接到 wire 型变量,缺省值一般为 "Z"。举例如下:
wire interrupt ;
wire flag1, flag2 ;
wire gnd = 1'b0 ;
线网型还有其他数据类型,包括 wand,wor,wri,triand,trior,trireg 等。这些数据类型用的频率不是很高
寄存器(reg)
寄存器(reg)用来表示存储单元,它会保持数据原有的值,直到被改写。声明举例如下:
reg clk_temp;
reg flag1, flag2
实现功能为:单个Led灯每500ms亮灭状态转换。
设计文件代码如下:
//50MHz T=20ns
//500ms/20ns = 25,000,000
//Xilinx FPGA 的D触发器结构,决定其使用高电平复位能够节约一定的资源,课程里面仍旧使用低电平复位,是为了符合开发板上的常态为高电平的按键作为复位按键功能的使用
module led(
Clk,
Reset_n,//_n代表低电平有效
Led
);
input Clk;
input Reset_n;
output reg Led;
reg [24:0]counter;
always@(posedge Clk or negedge Reset_n) //posedge上升沿,即以时钟的上升沿为敏感信号,或者复位信号的下降沿
if(!Reset_n)
counter <= 0;
else if(counter == 25000000-1)//让counter复位置0也需要耗费一个周期,所以需要延时多久翻转,则需要将延时时间除以周期时长再减去1,得到此处的counter数
counter <= 0;
else
counter <= counter+1'd1;
always@(posedge Clk or negedge Reset_n) //posedge上升沿,即以时钟的上升沿为敏感信号,或者复位信号的下降沿
if(!Reset_n)
Led <= 0 ;
else if(counter == 25000000-1)
Led <= !Led; //在always里赋值,则必须为reg型
// always@(posedge Clk or negedge Reset_n) //posedge上升沿,即以时钟的上升沿为敏感信号,或者复位信号的下降沿
// if(!Reset_n)
// begin
// counter <= 0;
// Led <= 0 ;
// end
// else if(counter == 25000000)
// begin
// Led <= !Led; //在always里赋值,则必须为reg型
// counter <= 0;
// end
// else
// begin
// counter <= counter+1'd1;
// end
endmodule
test bench代码如下所示:
`timescale 1ns / 1ns
module led_tb2();
reg Clk ;
reg reset;
wire led;
led Led_tb(
.Clk(Clk),
.Reset_n(reset),//_n代表低电平有效
.Led(led)
);
initial Clk = 1;
always #10 Clk = ~Clk; //延时10ns,翻转Clk值,模拟实现时钟信号
initial begin
reset = 0;
#201; //延迟时间不应为周期的整数倍
reset = 1;
#2000000000;//等待两秒,看实际状态
end
endmodule
实现功能为:四个Led灯以500ms的时间间隔交替点亮。
设计文件代码如下:
module led_horse(
Clk,
reset,
led
);
input Clk;
input reset;
output reg [3:0]led;
reg [29:0]count;
always@(posedge Clk or negedge reset)
begin
if (!reset)
count <= 0;
else if( count == 99999999) //每计数到2s,则复位
count <= 0 ;
else
count <= count + 1'd1;
end
always@(posedge Clk or negedge reset)
begin
if (!reset)
led <= 4'b1111;
else if( count ==25000000) //第一个500ms
led <= 4'b1110;
else if( count ==50000000)//第二个500ms
led <= 4'b1101;
else if( count ==75000000)//第三个500ms
led <= 4'b1011;
else if( count == 99999999)//第四个500ms
led <= 4'b0111;
end
endmodule
test bench代码如下所示:
`timescale 1ns / 1ns
module led_horsetb();
reg Clk;
reg reset;
wire [3:0]led;
led_horse led_horsetb(
.Clk(Clk),
.reset(reset),
.led(led)
);
initial Clk = 1;
always #10 Clk = ~Clk; //建立仿真时钟信号
initial begin
reset = 0;
#201;
reset = 1;
#1000000000; //等待10s
end
endmodule
always@(posedge Clk or negedge reset)
if (!reset)
led <= 4'b0001;
else if( count == MCNT)
begin
if ( led == 4'b1000)
led <= 4'b0001; //当1处于最高位时,无法通过移位进行处理
else
led = led << 1;//每当计数器达标时,1左移一位
end
else
led <= led;
always@(posedge Clk or negedge reset)
if (!reset)
led <= 4'b1110;
else if( count == MCNT)
begin
led <= {led[2:0],led[3]};//通过位组合的形式,不断更新输出
end
else
led <= led;
如:初始值为1110
第一次赋值变为:{110,1},即1101
第二次赋值变为:{101,1},即1011
第三次赋值变为:{011,1},即0111
第四次赋值变为:{111,0},即1110
则可以实现四个led灯的亮灭循环。
实现功能为:单个Led灯每500ms亮灭状态转换。
设计文件代码如下:
module led_run(
Clk,
reset,
led
);
input Clk;
input reset;
output [3:0]led;//使用调用模块的功能,对于输出则不需要定义reg型,在子模块中已经对out进行了reg定义
reg [24:0]count;
parameter MCNT = 25'd24999999; //parameter 参数
always@(posedge Clk or negedge reset)
begin
if (!reset)
count <= 0;
else if( count == MCNT)
count <= 0 ;
else
count <= count + 1'b1;
end
// always@(posedge Clk or negedge reset)
// if (!reset)
// led <= 4'b1110;
// else if( count == MCNT)
// begin
if ( led == 4'b1000)
led <= 4'b0001;
else
led = led << 1;
// led <= {led[2:0],led[3]};
// end
// else
// led <= led;
reg [1:0]count2;//[1:0]为两位数据,最高只有4
always@(posedge Clk or negedge reset)
if (!reset)
count2 <=0;
else if( count == MCNT)
count2 <= count2+1'b1; //由于count定义为两位数据,最大值为4,因此不需要给count重新赋0值
//调用24译码器功能模块,需要将decoder_2_4文件也导入到工程中
decoder_2_4(
.a(count2[1]), //当count等于01时 输出为0001
.b(count2[0]), //a,b为形参,count2[1]、count2[0]为实参
.out(led)
);
endmodule
实现功能为:四个Led灯分别以不同的频率进行闪烁。
设计文件代码如下:
module led_flash4(
Clk,
Reset_n,//_n代表低电平有效
Led
);
input Clk;
input Reset_n;
output [3:0]Led;
led_flash led_flash4inst0(
.Clk(Clk),
.Reset_n(Reset_n),//_n代表低电平有效
.Led(Led[0])
);
defparam led_flash4inst0.MCNT = 24999999;
led_flash led_flash4inst1(
.Clk(Clk),
.Reset_n(Reset_n),//_n代表低电平有效
.Led(Led[1])
);
defparam led_flash4inst1.MCNT = 49999999;
led_flash led_flash4inst2(
.Clk(Clk),
.Reset_n(Reset_n),//_n代表低电平有效
.Led(Led[2])
);
defparam led_flash4inst2.MCNT = 74999999;
led_flash led_flash4inst3(
.Clk(Clk),
.Reset_n(Reset_n),//_n代表低电平有效
.Led(Led[3])
);
defparam led_flash4inst3.MCNT = 99999999;
endmodule
子模块代码如下所示:
//50MHz T=20ns
//500ms/20ns = 25,000,000
//Xilinx FPGA 的D触发器结构,决定其使用高电平复位能够节约一定的资源,课程里面仍旧使用低电平复位,是为了符合开发板上的常态为高电平的按键作为复位按键功能的使用
module led_flash(
Clk,
Reset_n,//_n代表低电平有效
Led
);
parameter MCNT = 24999999;
input Clk;
input Reset_n;
output reg Led;
reg [26:0]counter;
always@(posedge Clk or negedge Reset_n) //posedge上升沿,即以时钟的上升沿为敏感信号,或者复位信号的下降沿
if(!Reset_n)
counter <= 0;
else if(counter == MCNT)//让counter复位置0也需要耗费一个周期,所以需要延时多久翻转,则需要将延时时间除以周期时长再减去1,得到此处的counter数
counter <= 0;
else
counter <= counter+1'd1;
always@(posedge Clk or negedge Reset_n) //posedge上升沿,即以时钟的上升沿为敏感信号,或者复位信号的下降沿
if(!Reset_n)
Led <= 0 ;
else if(counter == MCNT)
Led <= !Led; //在always里赋值,则必须为reg型
// always@(posedge Clk or negedge Reset_n) //posedge上升沿,即以时钟的上升沿为敏感信号,或者复位信号的下降沿
// if(!Reset_n)
// begin
// counter <= 0;
// Led <= 0 ;
// end
// else if(counter == 25000000)
// begin
// Led <= !Led; //在always里赋值,则必须为reg型
// counter <= 0;
// end
// else
// begin
// counter <= counter+1'd1;
// end
endmodule