小梅哥FPGA学习笔记
一、38译码器
功能: 译码器其任一时刻的稳态输出,仅仅与该时刻的输入变量的取值有关,它是一种多输入多输出的组合逻辑电路,负责将二进制代码翻译为特定的对象(如逻辑电平等)。38译码器,即将3种输入翻译成8种输出状态。
module led_test(a,b,key_in,led_out);
input a; //输入端口a
input b; //输入端口b
input key_in; //按键输入
output led_out; //led控制端口
assign led_out = (key_in == 0)? a : b;
endmodule
testbench文件
`timescale 1ns/1ps
module led_test_tb;
//激励信号定义,对应连接到待测试模块的输入端口
reg signal_a;
reg signal_b;
reg signal_c;
//待测信号定义,对应连接到待测试模块的输出端口
wire led;
//例化待测试模块
led_test led_test0(
.a(signal_a),
.b(signal_b),
.key_in(signal_c),
.led_out(led)
);
//产生激励
initial begin
signal_a=0;signal_b=0;signal_c=0;
#100;//延时100ns
signal_a=0;signal_b=0;signal_c=1;
#100;//延时100ns
signal_a=0;signal_b=1;signal_c=0;
#100;//延时100ns
signal_a=0;signal_b=1;signal_c=1;
#100;//延时100ns
signal_a=1;signal_b=0;signal_c=0;
#100;//延时100ns
signal_a=1;signal_b=0;signal_c=1;
#100;//延时100ns
signal_a=1;signal_b=1;signal_c=0;
#100;//延时100ns
signal_a=1;signal_b=1;signal_c=1;
#200;
$stop;//停止仿真
end
endmodule
Rtl Simulation
二、计数器
功能: 设计一个计数器,使其使能载板LED每500ms,状态翻转一次。核心板晶振为50MHz,也就是说时钟周期为20ns,这样可以计算出:次数 = 500_000_000ns / 20ns =25_000_000次,也就是需要一个至少25位的计数器——且每当计数次数达到目标值时清零重新计算。
module counter(Clk50M,Res_n,led);
input Clk50M; //系统时钟,50M
input Res_n; //全局复位,低电平复位
output reg led; //led输出
reg [24:0]cnt; //定义十进制计数器寄存器
//计数器计数进程
always @(posedge Clk50M or negedge Res_n) //时钟上升沿和复位信号下降沿
if(Res_n == 1'b0)
cnt <= 25'd0;
else if(cnt == 25'd24_999_999)
cnt <= 25'd0;
else
cnt <= cnt + 1'b1;
//led输出控制
always @(posedge Clk50M or negedge Res_n)
if(Res_n ==1'b0)
led <=1'b1; //高电平led灭
else if(cnt == 25'd24_999_999)
led <= ~led;
else
led <=led;
endmodule
或者合并起来写也是可以的:
module counter(Clk50M,Res_n,led);
input Clk50M; //系统时钟,50M
input Res_n; //全局复位,低电平复位
output reg led; //led输出
reg [24:0]cnt; //定义十进制计数器寄存器
always @(posedge Clk50M or negedge Res_n) //时钟上升沿和复位信号下降沿
if(Res_n == 1'b0)
begin
cnt <= 25'd0;
led <=1'b1; //高电平led灭
end
else if(cnt == 25'd24_999_999)
begin
cnt <= 25'd0;
led <= ~led;
end
else
begin
cnt <= cnt + 1'b1;
led <=led;
end
endmodule
testbench文件
`timescale 1ns/1ns
`define clock_period 20
module counter_tb;
reg clk;
reg res_n;
wire led;
counter counter0(
.Clk50M(clk),
.Res_n(res_n),
.led(led)
);
initial clk =1;
always #(`clock_period/2) clk = ~clk; //延时20ns
initial begin
res_n = 1'b0;
#(`clock_period *200);
res_n = 1'b1;
#2000000000;
$stop;
end
endmodule
Rtl Simulation
(略)
三、IP核计数器
1.创建工程后点击:Tools——Mega Wizard Plug-In Manager,以此启动 Mega Wizard 插件管理器。
2.选择创建一个新的定制IP。3.如图选择counter里的LPM_COUNTER,并且将生成文件保留在ip文件夹中,名字定义为counter。
4.计数器位数可以自选,这里选择4位,计数方式为递增计数。
5.
Plain binary:满数输出
Module,with a count modules of:设置计数最大值
Clock Enable:时钟使能信号
Count Enable:计数使能信号
Carry-in:进位链输入
Carry-out:进位链输出
6.
可选同步(异步)复位、加载和设置输入
7.确认把生成的IP核添加到工程中。
testbench文件
四、BCD计数器
功能: BCD码用4位二进制来表示十进制数中的0~9。主要应用之一是数管码。
五、阻塞赋值与非阻塞赋值
阻塞赋值:后面的语句必须等到当前的赋值语句执行完毕才能执行。操作符:“=”
非阻塞赋值:当前的赋值语句不会阻断其后的语句。操作符:“<=”
1.时序电路建模时,用非阻塞赋值。
2.锁存器电路建模时,用非阻塞赋值。
3.用always块建立组合逻辑模型时,用阻塞赋值。
4.在同一个always块中建立时序和组合逻辑电路时,用非阻塞赋值。
5.在同一个always块中不要既用非阻塞赋值又用阻塞赋值。
6.不要在一个以上的always块中为同一个变量赋值。
7.用$strobe系统任务来显示用非阻塞赋值的变量值。
8.在赋值时不要使用 #0 延迟。
六、状态机
功能: 状态机全称有限状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
状态机的种类: 摩尔型状态机(不直接依赖当前状态)和米利型状态机(输出还与当前状态有关)。
状态机的描述方式: 一段式,两段式和三段式。
一段式: 整个状态机写在一个always模块里面,在该模块中既描述状态转移,又描述状态的输入和输出。
两段式: 用两个always模块来描述状态机,其中一个always模块采用同步时序描述状态转移;另一个模块采用组合逻辑判断转移状态条件,描述状态转移规律以及输出。
三段式: 在两个always模块描述方法的基础上,使用第三个always模块,一个always模块采用同步时序描述状态移动,一个always采用组合逻辑判断状态转移条件,描述状态转移规律,另外一个always模块描述状态输出(可以用组合电路输出,也可以用时序电路输出)。
module Hello(Clk,Res_n,data,led);
input Clk; //50M
input Res_n; //复位信号
input [7:0]data; //8位位宽的输入数据
output led;
//用独热码的编码方式定义状态
//文件内部使用,无法在外部被更改
localparam
CHECK_H = 5'b0_0001,
CHECK_e = 5'b0_0010,
CHECK_la = 5'b0_0100,
CHECK_lb = 5'b0_1000,
CHECK_o = 5'b1_0000;
reg [4:0] state; //状态所需寄存器,独热码为5位位宽
always @(posedge Clk or negedge Res_n)
if(!Res_n)begin
led <= 1'b1;
state <= CHECK_H;
end
else begin
case(state)
CHECK_H:
if(data == "H")
state <= CHECK_e;
else
state <= CHECK_H;
CHECK_e:
if(data == "e")
state <= CHECK_la;
else
state <= CHECK_H;
CHECK_La:
if(data == "l")
state <= CHECK_lb;
else
state <= CHECK_H;
CHECK_lb:
if(data == "l")
state <= CHECK_o;
else
state <= CHECK_H;
CHECK_o:
begin
state <= CHECK_H;
if(data == "o")
led <= ~led;
else
led <= led;
end
default:
state <=CHECK_H;
endcase
end
endmodule
七、独立按键消抖实验
实现功能: 每次按下按键0,4个LED显示状态以二进制加法格式加1;每次按下按键1,4个LED显示状态以二进制加法格式减1。但是由于实际波形中会出现抖动状况,所以要通过硬件电路或者逻辑设计的方法来消除。
消除抖动的方法:
1.利用RS触发器进行硬件消抖。
2.通过利用电容器和电阻对噪声波形积分,吸收因触点跳动产生的噪声。
3.通过555定时器组成的单稳态触发器可以消除开关的抖动。
异步信号与同步信号:
同步信号:变化永远发生在系统时钟的上升沿时刻。
异步信号:与系统时钟无关。系统时钟采集信号时可能会采集到亚稳态的信号亚稳态——D触发器——震荡——稳定到0/1(不确定)——再加一级寄存器后,在下一个时钟信号输出稳定(但仍不确定时0/1)
使用两级寄存器对异步信号进行处理,虽然无法确定0/1状态,但是大概率能达到稳定态。
按键消抖代码(元件)
module key_filter(Clk,Res_n,key_in,key_flag,key_state);
input Clk;
input Res_n;
input key_in; //按键信号
output reg key_flag; //使能信号表示输出结果(按键一次稳定后产生一个脉冲信号)
output reg key_state; //表示按键的稳定状态,没有按下时为稳定值(抖动也认为是高电平)
//这里的key_in相对于FPGA内部信号来说是一个异步信号
//所以这里需要对其使用两级同步的触发器进行异步信号的同步化(两级寄存器)
//边缘检测+异步信号的处理
//对外部输入的异步信号进行同步处理(消除亚稳态问题)
reg key_in_a,key_in_b;
always@(posedge Clk or negedge Res_n)
if(!Res_n)begin //复位初始化
key_in_a = 1'b0;
key_in_b = 1'b0;
end
else begin
key_in_a <= key_in;
key_in_b <= key_in_a;
end
reg key_tmpa,key_tmpb; //寄存两次状态
wire pedge,nedge; //输出高电平和低电平
//使用D触发器存储两个相邻时钟上升沿时外部输入信号的电平状态(已经同步到系统时钟域中)
always@(posedge Clk or negedge Res_n)
if(!Res_n)begin //复位初始化
key_tmpa <= 1'b0;
key_tmpb <= 1'b0;
end
else begin
key_tmpa <= key_in_b;
key_tmpb <= key_tmpa;
end
//判断产生了高电平还是产生了低电平的模块
assign nedge = (!key_tmpa) & key_tmpb; //下降沿
assign pedge = key_tmpa & (!key_tmpb); //上升沿
//20ms的计数器(抖动一般持续20ms以下)
reg[19:0]cnt;
reg cnt_full;
reg en_cnt; //使能计数寄存器
//计数使能模块
always @(posedge Clk or negedge Res_n)
if(!Res_n)
cnt <= 20'd0;
else if(en_cnt)
cnt <= cnt + 1'b1;
else
cnt <= 20'd0;
//计数计满模块
always @(posedge Clk or negedge Res_n)
if(!Res_n)
cnt_full <= 1'b0;
else if(cnt == 999_999) //系统时钟20ns,故20ms为1000_000次数
cnt_full <= 1'b1;
else
cnt_full <= 1'b0;
//独热码编码状态机
localparam
IDEL = 4'b0001,
FILTER0 = 4'b0010,
DOWN = 4'b0100,
FILTER1 = 4'b1000;
//一段式状态机
reg [3:0] state; //状态机状态寄存器
always@(posedge Clk or negedge Res_n)
if(!Res_n)begin
state <= IDEL;
en_cnt <= 1'b0;
key_flag <= 1'b0;
key_state <= 1'b1;
end
else begin
case(state)
IDEL:
begin
key_flag <= 1'b0; //FILTER1返回,复位key_flag
if(nedge)begin
state <= FILTER0;
en_cnt <= 1'b1;
end
else
state <= IDEL;
end
FILTER0:
begin
if(cnt_full)begin //计数满20ms
key_flag <= 1'b1;
key_state <=1'b0;
en_cnt <= 1'b0;
state <= DOWN;
end
else if(pedge)begin //在判断下降沿的时候遇到上升沿,说明这是一个抖动
en_cnt <= 1'b0;
state <=IDEL; //返回IDEL状态等待下一个下降沿
end
else
state <= FILTER0;
end
DOWN:
begin
key_flag <= 1'b0; //脉冲信号复位
if(pedge)begin
state <= FILTER1;
en_cnt <= 1'b1;
end
else
state <= DOWN;
end
FILTER1:
begin
if(cnt_full)begin //计数满20ms
key_flag <= 1'b1;
key_state <=1'b1;
en_cnt <= 1'b0;
state <= IDEL;
end
else if(nedge)begin //在判断上升沿的时候遇到下降沿,说明这是一个抖动
en_cnt <= 1'b0;
state <=DOWN; //返回IDEL状态等待下一个下降沿
end
else
state <= FILTER1;
end
default:
begin
state <= IDEL;
en_cnt <= 1'b0;
key_flag <= 1'b0;
key_state <= 1'b1;
end
endcase
end
task语法
task<任务名>;
<端口及数据类型声明语句>
<语句1>
<语句2>
……
<语句n>
endtask
任务调用的语法
<任务名>(端口1,端口2,…端口n);
testbench文件
`timescale 1ns/1ns
`define clock_period 20
module key_filter_tb;
reg key_in;
reg Clk;
reg Res_n;
wire key_flag;
wire key_state;
reg [15:0]myrand; //随机数寄存器
task press_key;
begin
repeat(50)begin //模拟实现50次的0~65535ns的按下抖动
myrand = {$random}%65536;
#myrand key_in = ~key_in;
end
key_in = 0; //按下
#50_000_000; //延时50ms
repeat(50)begin
myrand = {$random}%65536;
#myrand key_in = ~key_in;
end
key_in = 1; //松开
#50_000_000; //延时50ms
end
endtask
key_filter key_filter0(
.Clk(Clk),
.Res_n(Res_n),
.key_in(key_in),
.key_flag(key_flag),
.key_state(key_state)
);
initial Clk=1;
always #(`clock_period/2) Clk = ~Clk;
initial begin
Res_n = 1'b0;
key_in = 1'b0;
#(`clock_period*10) Res_n = 1'b1;
#30000;
press_key;
#10000;
press_key;
#10000;
press_key;
#10000;
$stop;
end
endmodule
通过顶层文件连接,实现功能
control模块
module led_ctrl(Clk,Res_n,key_flag0,key_state0,key_flag1,key_state1,led);
input Clk;
input Res_n;
input key_flag0,key_state0;
input key_flag1,key_state1;
output [3:0]led;
reg [3:0]led_r; //由于当led为高电平时实际是灯灭,所以通过引入led_r来进行逻辑调整
always@(posedge Clk or negedge Res_n)
if(!Res_n)
led_r <= 4'b0000;
else if(key_flag0 && !key_state0)
led_r <= led_r + 1'b1;
else if(key_flag1 && !key_state1)
led_r <= led_r - 1'b1;
else
led_r <= led_r;
assign led = ~led_r;
endmodule
顶层文件
module key_led_top(Clk,Res_n,key_in0,key_in1,led);
input Clk;
input Res_n;
input key_in0;
input key_in1;
output [3:0]led;
wire key_flag0;
wire key_state0;
wire key_flag1;
wire key_state1;
key_filter key_filter0(
.Clk(Clk),
.Res_n(Res_n),
.key_in(key_in0),
.key_flag(key_flag0),
.key_state(key_state0)
);
*
key_filter key_filter1(
.Clk(Clk),
.Res_n(Res_n),
.key_in(key_in1),
.key_flag(key_flag1),
.key_state(key_state1)
);
led_ctrl led_ctrl0(
.Clk(Clk),
.Res_n(Res_n),
.key_flag0(key_flag0),
.key_state0(key_state0),
.key_flag1(key_flag1),
.key_state1(key_state1),
.led(led)
);
endmodule
testbench文件
八、8位7段数码管驱动
实现功能: 在Quartus II 中,使用 In system sources and probes editor 工具,输入需要显示在数码管上的数据,则数码管显示对应数值。(数码管采用动态显示扫描)
模块:
1.4输入查找表,8位输出。
2.分频模块,从系统时钟分频得到1KHz的扫描时钟。
3.8选1多路器,选择端为当前扫描的数码管位置。
4.8位循环移位寄存器。
数码管驱动模块逻辑电路图
driver:分频产生1KHz的扫描时钟
Shift8:8位循环位移寄存器
MUX8:数据输入选择
MUX2:使能选择
LUT:数据译码器
module HEX8(Clk,Res_n,En,disp_data,seg,sel);
input Clk; //50M
input Res_n;
input En;
input [31:0]disp_data; //待显示内容
reg [3:0]data_tmp; //待显示数据
output reg[6:0]seg; //控制显示图案(段选)
output [7:0]sel; //控制显示数码管(位选)
reg [7:0]sel_r;
//(二)分频计数器,计数1ms——500000ns/20ns(时钟周期) = 25000次
reg [14:0] divider_cnt; //计数器寄存器
reg clk_1K;
always @(posedge Clk or negedge Res_n)
if(!Res_n)
divider_cnt <= 15'd0;
else if(!En)
divider_cnt <= 15'd0;
else if(divider_cnt == 24999)
divider_cnt <= 15'd0;
else
divider_cnt <= divider_cnt + 1'b1;
//clk_1K扫描时钟生成模块
always @(posedge Clk or negedge Res_n)
if(!Res_n)
clk_1K <= 1'b0;
else if(divider_cnt == 24_999)
clk_1K <= ~clk_1K;
else
clk_1K <= clk_1K;
//8位循环移位寄存器
always @(posedge clk_1K or negedge Res_n)
if(!Res_n)
sel_r <= 8'b0000_0001;
else if(sel_r == 8'b1000_0000)
sel_r <= 8'b0000_0001;
else
sel_r <= sel_r << 1; //左移一位
//八选一多路器
always@(*) //敏感变量由综合器根据always里面的输入变量自动添加
case(sel_r)
8'b0000_0001:data_tmp = disp_data[3:0];
8'b0000_0010:data_tmp = disp_data[7:4];
8'b0000_0100:data_tmp = disp_data[11:8];
8'b0000_1000:data_tmp = disp_data[15:12];
8'b0001_0000:data_tmp = disp_data[19:16];
8'b0010_0000:data_tmp = disp_data[23:20];
8'b0100_0000:data_tmp = disp_data[27:24];
8'b1000_0000:data_tmp = disp_data[31:28];
default:data_tmp = 4'b0000;
endcase
//LUT
always@(*)
case(data_tmp) //按照段码表编码
4'h0:seg = 7'b1000_000;
4'h1:seg = 7'b1111_001;
4'h2:seg = 7'b0100_100;
4'h3:seg = 7'b0110_000;
4'h4:seg = 7'b0011_001;
4'h5:seg = 7'b0010_010;
4'h6:seg = 7'b0000_010;
4'h7:seg = 7'b1111_000;
4'h8:seg = 7'b0000_000;
4'h9:seg = 7'b0010_000;
4'ha:seg = 7'b0001_000;
4'hb:seg = 7'b0000_011;
4'hc:seg = 7'b1000_110;
4'hd:seg = 7'b0100_001;
4'he:seg = 7'b0000_110;
4'hf:seg = 7'b0001_110;
endcase
//二选一多路器
assign sel = (En) ? sel_r:8'b0000_0000;
endmodule
testbench文件
`timescale 1ns/1ns
`define clock_period 20
module HEX_tb;
reg Clk; //50M
reg Res_n;
reg En; //数码管显示使能,1使能,0关闭
reg [31:0]disp_data;
wire [7:0]sel; //数码管位选
wire [6:0]seg; //数码管段选
HEX8 HEX8(
.Clk(Clk),
.Res_n(Res_n),
.En(En),
.disp_data(disp_data),
.seg(seg),
.sel(sel)
);
initial Clk = 1;
always #(`clock_period/2) Clk = ~Clk;
initial begin
Res_n = 1'b0;
En = 1;
disp_data = 32'h12345678;
#(`clock_period*20);
Res_n = 1'b1;
#(`clock_period*20);
#(`clock_period*20);
#20_000_000; //20ms
disp_data = 32'h87654321;
#20_000_000;
disp_data = 32'h89abcdef;
#20_000_000;
$stop;
end
endmodule
板级调试
顶层代码
module HEX_top(Clk,Res_n,seg,sel); //顶层模块 = 调试模块 + 驱动模块
input Clk; //50M
input Res_n;
output [6:0]seg; //控制显示图案(段选)
output [7:0]sel; //控制显示数码管(位选)
wire [31:0]disp_data; //待显示内容
hex_data hex_data( //相当于使用现成模块
.probe(), //选空表示不连接
.source(disp_data)
);
HEX8 HEX8(
.Clk(Clk),
.Res_n(Res_n),
.En(1'b1),
.disp_data(disp_data),
.seg(seg),
.sel(sel)
);
endmodule
In system sources and probes editor(ISSP)调试工具
Tools——Mega Wizard Plug-In Manager
分配引脚后全编译无误后下载工程到开发板中。
在Quartus II 中点击Tools——In-system Source and Probes Editor 启动ISSP,可以手动输入data。
九、UART串口发送模块设计
实现功能: 实现FPGA通过UART协议发送数据。
实验现象: 在 Quartus II 中,使用 In system sources and probes editor 工具,输入需要通过串口发送出去的数据,然后按下学习板上的按键0,则FPGA自动将所需要发送的数据发送出去。
串口发送模块包括两个主要组件
1.发送波特率生成模块。
2.数据发送模块。
串口发送模块整体结构体
串口发送模块结构图
模块代码
module uart_byte_tx(
Clk,
Res_n,
data_byte,
send_en,
baud_set,
Rs232_Tx,
Tx_Done,
uart_state
);
input Clk;
input Res_n;
input [7:0]data_byte;
input send_en;
input [2:0]baud_set;
output reg Rs232_Tx;
output reg Tx_Done;
output reg uart_state;
//查找表——得到不同计数周期的计数器
reg [15:0]bps_DR; //分频计数最大值
always @(posedge Clk or negedge Res_n)
if(!Res_n)
bps_DR <= 16'd5207; //默认9600波特率
else begin
case(baud_set)
0:bps_DR <= 16'd5207;
1:bps_DR <= 16'd2603;
2:bps_DR <= 16'd1301;
3:bps_DR <= 16'd867;
4:bps_DR <= 16'd433;
default:bps_DR <= 16'd5207;
endcase
end
//实现优先级的信号控制 使能模块
always @(posedge Clk or negedge Res_n)
if(!Res_n)
uart_state <= 1'b0;
else if(send_en)
uart_state <= 1'b1;
else if(bps_cnt == 4'd11)
uart_state <= 1'b0;
else
uart_state <= uart_state;
//外部输入数据的内部寄存(保证数据稳定)
reg [7:0]r_data_byte;
always @(posedge Clk or negedge Res_n)
if(!Res_n)
r_data_byte <= 8'd0;
else if(send_en) //检测到输入信号
r_data_byte <= data_byte; //储存输入值
else
r_data_byte <= r_data_byte;
//波特率时钟
//分频时钟计数
reg [15:0]div_cnt; //分频计数器
always @(posedge Clk or negedge Res_n)
if(!Res_n)
div_cnt <= 16'd0;
else if(uart_state)begin
if(div_cnt == bps_DR)
div_cnt <= 16'd0;
else
div_cnt <= div_cnt + 1'b1;
end
else
div_cnt <=16'd0;
//bps_clk产生模块
reg bps_clk; //波特率时钟
always @(posedge Clk or negedge Res_n)
if(!Res_n)
bps_clk <= 1'b0;
else if(div_cnt == 16'd1) //当分频计数开始了,则clky而开始脉冲输出
bps_clk <= 1'b1;
else
bps_clk <= 1'b0;
//bps counter
reg [3:0]bps_cnt; //波特率计数时钟
always @(posedge Clk or negedge Res_n)
if(!Res_n)
bps_cnt <= 4'd0;
else if(bps_cnt == 4'd11) //计数满
bps_cnt <=4'd0;
else if(bps_clk)
bps_cnt <= bps_cnt +1'b1;
else
bps_cnt <= bps_cnt;
//Tx_Donesh输出,是一个闭环 发送完成信号(个人感觉写这个没必要)
always @(posedge Clk or negedge Res_n)
if(!Res_n)
Tx_Done <= 1'b0;
else if(bps_cnt == 4'd11)
Tx_Done <= 1'b1;
else
Tx_Done <= 1'b0;
//十选一多路器
always @(posedge Clk or negedge Res_n) //尽量避免组合逻辑(存在毛刺)
if(!Res_n)
Rs232_Tx <= 1'b1; //高电平为不输出状态
else begin
case(bps_cnt)
0:Rs232_Tx <= 1'b1;
1:Rs232_Tx <= 1'b0; //START_BIT
2:Rs232_Tx <= r_data_byte[0];
3:Rs232_Tx <= r_data_byte[1];
4:Rs232_Tx <= r_data_byte[2];
5:Rs232_Tx <= r_data_byte[3];
6:Rs232_Tx <= r_data_byte[4];
7:Rs232_Tx <= r_data_byte[5];
8:Rs232_Tx <= r_data_byte[6];
9:Rs232_Tx <= r_data_byte[7];
10:Rs232_Tx <= 1'b1; //STOP_BIT
default:Rs232_Tx <= 1'b1;
endcase
end
endmodule
testbench文件
`timescale 1ns/1ns
`define clock_period 20
reg Clk;
reg Res_n;
reg [7:0]data_byte;
reg send_en;
reg [2:0]baud_set;
wire Rs232_Tx;
wire Tx_Done;
wire uart_state;
uart_byte_tx uart_byte_tx(
.Clk(Clk),
.Res_n(Res_n),
.data_byte(data_byte),
.send_en(send_en),
.baud_set(baud_set),
.Rs232_Tx(Rs232_Tx),
.Tx_Done(Tx_Done),
.uart_state(uart_state)
);
initial Clk =1;
always #(`clock_period/2)Clk = ~Clk;
initial begin
Res_n = 1'b0;
data_byte = 8'd0;
send_en = 1'd0;
baud_set = 3'd4; //默认为115200(比较快)
#(`clock_period*20 + 1) //加一是为了与系统时钟错开,更好观察时序情况
Res_n = 1'b1;
#(`clock_period*50);
data_byte = 8'haa;
send_en = 1'd1;
#`clock_period;
send_en = 1'd0;
@(posedge Tx_Done) //等待完成信号的上升沿
#(`clock_period*5000)
//重新发送
data_byte = 8'h55;
send_en = 1'd1;
#`clock_period;
send_en = 1'd0;
@(posedge Tx_Done)
#(`clock_period*5000)
$stop;
end
endmodule
板级代码
1.issp模块
2.key_filter模块
module uart_tx_top(Clk,Res_n,Rs232_Tx,key_in,led);
input Clk;
input Res_n;
input key_in;
output Rs232_Tx;
output led;
wire send_en;
wire [7:0]data_byte;
wire key_flag;
wire key_state;
assign send_en = key_flag & (!key_state);
uart_byte_tx uart_byte_tx(
.Clk(Clk),
.Res_n(Res_n),
.data_byte(data_byte),
.send_en(send_en),
.baud_set(3'd4),
.Rs232_Tx(Rs232_Tx),
.Tx_Done(),
.uart_state(led) //通过led知道目前串口状态
);
key_filter key_filter0(
.Clk(Clk),
.Res_n(Res_n),
.key_in(key_in),
.key_flag(key_flag),
.key_state(key_state)
);
issp issp (
.probe(),
.source(data_byte)
);
endmodule
十、UART串口接收模块设计
实现功能: 实现FPGA接受其他设备通过UART协议发送过来的数据。
实验现象: 在Quartus II 中,使用 In system sources and probes editor 工具,查看UART接受模块接收到的数据,串口接收到的数据由PC机发出。
对于其中的每一位进行采样,一般情况下每一位数据的中间点是最稳定的,因此一般应用中,采集中间时刻时的数据即可。但是在工业应用中,往往有非常强的电磁干扰,只采样一次就作为该数据的电平判定,是不保险的,有可能恰好采集到被干扰的信号而导致结果出错,因此需要使用多次采样求概率的方式进行。
串口接收模块
1.起始位检测进程。
2.波特率产生模块。
3.数据接受进程。
波特率分频计数值 = 波特率周期 / 系统时钟周期 /16
module uart_byte_rx(
Clk,
Res_n,
baud_set,
Rs232_Rx,
data_byte,
Rx_Done
);
input Clk;
input Res_n;
input [2:0]baud_set;
input Rs232_Rx;
output reg[7:0] data_byte;
output reg Rx_Done;
reg uart_state;
reg [2:0]START_BIT,STOP_BIT;
wire nedege;
//起始位检测
reg s0_Rs232_Rx,s1_Rs232_Rx; //同步寄存器(消除亚稳态)
reg tmp0_Rs232_Rx,tmp1_Rs232_Rx; //暂存寄存器(数据寄存器)
//同步寄存器(消除亚稳态——同按键消抖)
always@(posedge Clk or negedge Res_n)
if(!Res_n)begin
s0_Rs232_Rx = 1'b0;
s1_Rs232_Rx = 1'b0;
end
else begin
s0_Rs232_Rx <= Rs232_Rx;
s1_Rs232_Rx <= s0_Rs232_Rx;
end
//数据寄存器
always@(posedge Clk or negedge Res_n)
if(!Res_n)begin
tmp0_Rs232_Rx = 1'b0;
tmp1_Rs232_Rx = 1'b0;
end
else begin
tmp0_Rs232_Rx <= s1_Rs232_Rx;
tmp1_Rs232_Rx <= tmp0_Rs232_Rx;
end
//下降沿定义,起始位检测
assign nedege = !tmp0_Rs232_Rx & tmp1_Rs232_Rx;
//查找表——得到不同计数周期的计数器
//这里是16倍的波特率时钟(因为采集信息16次)
reg [15:0]bps_DR; //分频计数最大值
always @(posedge Clk or negedge Res_n)
if(!Res_n)
bps_DR <= 16'd324; //默认9600波特率
else begin
case(baud_set)
0:bps_DR <= 16'd324;
1:bps_DR <= 16'd162;
2:bps_DR <= 16'd80;
3:bps_DR <= 16'd53;
4:bps_DR <= 16'd26;
default:bps_DR <= 16'd324;
endcase
end
//波特率时钟
//分频时钟计数
reg [15:0]div_cnt; //分频计数器
always @(posedge Clk or negedge Res_n)
if(!Res_n)
div_cnt <= 16'd0;
else if(uart_state)begin
if(div_cnt == bps_DR)
div_cnt <= 16'd0;
else
div_cnt <= div_cnt + 1'b1;
end
else
div_cnt <=16'd0;
//bps_clk产生模块
reg bps_clk; //波特率时钟
always @(posedge Clk or negedge Res_n)
if(!Res_n)
bps_clk <= 1'b0;
else if(div_cnt == 16'd1) //当分频计数开始了,则clk而开始脉冲输出
bps_clk <= 1'b1;
else
bps_clk <= 1'b0;
//bps counter
reg [7:0]bps_cnt; //波特率计数时钟
always @(posedge Clk or negedge Res_n)
if(!Res_n)
bps_cnt <= 8'd0;
else if(Rx_Done | (bps_cnt == 8'd12 && (START_BIT > 2))) //计数满或出现开始接受错误
bps_cnt <=8'd0;
else if(bps_clk)
bps_cnt <= bps_cnt +1'b1;
else
bps_cnt <= bps_cnt;
//Rx_Done输出,是一个闭环 发送完成信号
always @(posedge Clk or negedge Res_n)
if(!Res_n)
Rx_Done <= 1'b0;
else if(bps_cnt == 8'd159) //总共采集信息160次(10×16)
Rx_Done <= 1'b1;
else
Rx_Done <= 1'b0;
//数据处理
reg [2:0]r_data_byte [7:0]; //前位宽,后个数
always @(posedge Clk or negedge Res_n)
if(!Res_n)begin
START_BIT <= 3'd0;
r_data_byte[0] <= 3'd0;
r_data_byte[1] <= 3'd0;
r_data_byte[2] <= 3'd0;
r_data_byte[3] <= 3'd0;
r_data_byte[4] <= 3'd0;
r_data_byte[5] <= 3'd0;
r_data_byte[6] <= 3'd0;
r_data_byte[7] <= 3'd0;
STOP_BIT <= 3'd0;
end
else if(bps_clk)begin
case(bps_cnt)
//清零操作
0:begin
START_BIT <= 3'd0;
r_data_byte[0] <= 3'd0;
r_data_byte[1] <= 3'd0;
r_data_byte[2] <= 3'd0;
r_data_byte[3] <= 3'd0;
r_data_byte[4] <= 3'd0;
r_data_byte[5] <= 3'd0;
r_data_byte[6] <= 3'd0;
r_data_byte[7] <= 3'd0;
STOP_BIT <= 3'd0;
end
5,6,7,8,9,10:START_BIT <= START_BIT +s1_Rs232_Rx;
21,22,23,24,25,26:r_data_byte[0] <= r_data_byte[0] + s1_Rs232_Rx;
37,38,39,40,41,42:r_data_byte[1] <= r_data_byte[1] + s1_Rs232_Rx;
53,54,55,56,57,58:r_data_byte[2] <= r_data_byte[2] + s1_Rs232_Rx;
69,70,71,72,73,74:r_data_byte[3] <= r_data_byte[3] + s1_Rs232_Rx;
85,86,87,88,89,90:r_data_byte[4] <= r_data_byte[4] + s1_Rs232_Rx;
101,102,103,104,105,106:r_data_byte[5] <= r_data_byte[5] + s1_Rs232_Rx;
117,118,119,120,121,122:r_data_byte[6] <= r_data_byte[6] + s1_Rs232_Rx;
133,134,135,136,137,138:r_data_byte[7] <= r_data_byte[7] + s1_Rs232_Rx;
149,150,151,152,153,154:STOP_BIT <= STOP_BIT + s1_Rs232_Rx;
default; //由于进行了清零操作,所以STOP_BIT 和 default 不用管了
endcase
end
//控制逻辑(产生uart_state)
always @(posedge Clk or negedge Res_n)
if(!Res_n)
uart_state = 1'b0;
else if(nedege) //检测到下降沿就开始接受
uart_state = 1'b1;
else if(Rx_Done || (bps_cnt == 8'd12 && (START_BIT > 2))) //1.接收完成 2.接收错误,起始位不对(可能是干扰信号)
uart_state = 1'b0;
else
uart_state <= uart_state;
//输出数据
always @(posedge Clk or negedge Res_n)
if(!Res_n)
data_byte <= 8'd0;
else if(bps_cnt == 8'd159) begin
//data_byte[0] = (r_data_byte[2])?(1'b1):1;b0; //二进制下100=4,故最高位等于1时,寄存1,否则为0
//简化逻辑后:
data_byte[0] <= r_data_byte[0][2];
data_byte[1] <= r_data_byte[1][2];
data_byte[2] <= r_data_byte[2][2];
data_byte[3] <= r_data_byte[3][2];
data_byte[4] <= r_data_byte[4][2];
data_byte[5] <= r_data_byte[5][2];
data_byte[6] <= r_data_byte[6][2];
data_byte[7] <= r_data_byte[7][2];
end
endmodule
testbench文件
注意事项:在testbench中例化了tx模块,所以需要在在simulation将tx.v文件手动添加进来。
`timescale 1ns/1ns
`define clock_period 20
module uart_byte_rx_tb;
reg Clk;
reg Res_n;
reg [2:0]baud_set;
reg Rs232_Rx;
reg send_en;
reg [7:0]data_byte_t;
wire [7:0]data_byte_r;
wire Rx_Done;
wire Tx_Done;
wire Rs232_Tx;
wire uart_state;
uart_byte_rx uart_byte_rx(
.Clk(Clk),
.Res_n(Res_n),
.baud_set(baud_set),
.Rs232_Rx(Rs232_Tx), //将发送的Tx信号连接到Rx信号上去
.data_byte(data_byte_r),
.Rx_Done(Rx_Done)
);
uart_byte_tx uart_byte_tx(
.Clk(Clk),
.Res_n(Res_n),
.data_byte(data_byte_t),
.send_en(send_en),
.baud_set(baud_set),
.Rs232_Tx(Rs232_Tx),
.Tx_Done(Tx_Done),
.uart_state(uart_state)
);
initial Clk = 1;
always #(`clock_period/2) Clk = ~Clk;
initial begin //通过发送数据检验接受模块是否正确
Res_n = 1'b0;
data_byte_t = 8'd0;
send_en = 1'd0;
baud_set = 3'd4; //默认为115200(比较快)
#(`clock_period*20 + 1) //加一是为了与系统时钟错开,更好观察时序情况
Res_n = 1'b1;
#(`clock_period*50);
data_byte_t = 8'haa;
send_en = 1'd1;
#`clock_period;
send_en = 1'd0;
@(posedge Tx_Done) //等待完成信号的上升沿
#(`clock_period*5000)
//重新发送
data_byte_t = 8'h55;
send_en = 1'd1;
#`clock_period;
send_en = 1'd0;
@(posedge Tx_Done)
#(`clock_period*5000)
$stop;
end
endmodule
RTL simulation
板级调试
module uart_rx_top(Clk,Res_n,Rs232_Rx);
input Clk;
input Res_n;
input Rs232_Rx;
reg [7:0]data_rx_r;
wire [7:0]data_rx;
wire Rx_Done;
uart_byte_rx uart_byte_rx(
.Clk(Clk),
.Res_n(Res_n),
.baud_set(3'd0),
.Rs232_Rx(Rs232_Rx),
.data_byte(data_rx),
.Rx_Done(Rx_Done)
);
issp issp(
.probe(data_rx_r), //保证只有正确输入后
.source()
);
always @(posedge Clk or negedge Res_n)
if(!Res_n)
data_rx_r <= 8'd0;
else if(Rx_Done)
data_rx_r <= data_rx;
else
data_rx_r <=data_rx_r;
endmodule
十一、嵌入式RAM使用之双端口RAM
实验目的: 学会调用Quartus II 软件中提供爽口RAM核并进行仿真。
实验步骤:
1.新建一个新的IP核。选择 Memory Compiler 下的 RAM:2-PORT。
With one read port and one write port,As a number of words。对于单端口RAM,读写操作共用端口A的地址,数据通过端口A写入和读出;但本次使用的双端口RAM,则是一个读端口一个写端口。
时钟选择单时钟,用一个时钟和时钟使能信号控制存储块所有的寄存器。(在别的应用场景可以设置双时钟使用独立的输入时钟和输出时钟/双时钟使用单独的读时钟和写时钟)。不创建读使能信号。
对输出端口进行寄存。不创建时钟使能信号,不创建异步复位信号(这里复位并不复位RAM中的数据,而只是复位寄存器上的值)
这里不对RAM进行初始化。
2.testbench文件
`timescale 1ns/1ns
`define clock_period 20
module dpram_tb;
reg clock;
reg [7:0]data;
reg [7:0]rdaddress; //读地址端口
reg [7:0]wraddress; //写地址端口
reg wren; //使能信号
wire [7:0]q;
integer i; //定义整数i
dpram dpram(
.clock(clock),
.data(data),
.rdaddress(rdaddress),
.wraddress(wraddress),
.wren(wren),
.q(q)
);
initial clock =1;
always #(`clock_period/2) clock = ~clock;
initial begin
data = 0;
rdaddress = 0;
wraddress = 0;
wren = 0;
#(`clock_period*20 + 1);
for (i=0;i<=15;i=i+1)begin
wren = 1;
data = 255-i;
wraddress = i;
#`clock_period;
end
wren = 0;
#(`clock_period*20);
for(i=0;i<=15;i=i+1)begin
rdaddress = i;
#`clock_period;
end
#(`clock_period*20);
$stop;
end
endmodule
读数据时,可以看出第一个时钟上升沿采到地址0;第二个时钟上升沿开始
赋值,但是由于逻辑延迟,因此在第三个时钟上升沿的数据才是稳定的。
十二、搭建串口收发与存储双口RAM简易应用系统
实验目的: 以模块化设计为基础利用已编写的串口收发模块、按键模块以及RAM的IP模块来设计一个简易应用系统。通过串口发送数据到FPGA中,FPGA接收到数据后将数据存储在双口RAM的一段连续空间中,当需要时,按下按键0,则FPGA将RAM中存储的数据通过串口发送出去。
功能模块:
1.串口接收模块
2.按键消抖模块
3.RAM模块
4.串口发送模块
5.控制模块
控制模块代码
module Ctrl(Clk,Res_n,Key_flag,Key_state,Rx_Done,Tx_Done,wren,Send_en,rdaddress,wraddress);
input Clk;
input Res_n;
input Key_flag;
input Key_state;
input Rx_Done;
input Tx_Done;
output reg [7:0]rdaddress;
output reg [7:0]wraddress;
output wren;
output reg Send_en;
//实现FPGA接收到数据后将数据存储在双口RAM的一段连续空间中
//需要设计一个写地址自加的控制部分,且其控制信号为串口接收模块输出的Rx_Done信号
assign wren = Rx_Done;
always@(posedge Clk or negedge Res_n)
if(!Res_n)
wraddress <= 8'd0;
else if(Rx_Done)
wraddress <= wraddress + 1'b1; //每来一个Rx_Done也就是每成功接收一字节数,地址数进行加一
else
wraddress <= wraddress;
//实现当按下按键0,FPGA将RAM中存储的数据通过串口发送出去
//这样也就是实现一旦按键按下即启动连续读操作,再次按下即可暂停发送
reg do_send;
always@(posedge Clk or negedge Res_n)
if(!Res_n)
do_send <= 1'b0;
else if(Key_flag && !Key_state)
do_send <= ~do_send;
always@(posedge Clk or negedge Res_n)
if(!Res_n)
rdaddress <= 8'd0;
else if(do_send && Tx_Done)
rdaddress <= rdaddress + 8'd1;
else
rdaddress <= rdaddress;
//双端口RAM发现其输出延迟两个系统时钟周期。
//为了保证数据变化稳定之后才进行输出,将驱动Send_en的信号接两级寄存器进行延迟两拍。
//当按键按下后启动一次发送,然后判断上一字节是否发送结束,是则进行下一字节发送,否则不进行下一次发送
reg r0_send_done,r1_send_done;
always@(posedge Clk or negedge Res_n)
if(!Res_n)begin
r0_send_done <= 1'b0;
r1_send_done <= 1'b0;
end
else begin
r0_send_done <= (do_send && Tx_Done);
r1_send_done <= r0_send_done;
end
always@(posedge Clk or negedge Res_n)
if(!Res_n)
Send_en <= 1'b0;
else if(Key_flag && !Key_state)
Send_en <= 1'b1;
else if(r1_send_done)
Send_en <= 1'b1; //延迟了两拍
else
Send_en <= 1'b0;
endmodule
顶层模块代码
将前面模块例化进来
//该程序中波特率设置为9600bps
module UART_DPRAM(Clk,Key_in,Rs232_Rx,Res_n,Rs232_Tx);
input Clk;
input Key_in;
input Res_n;
input Rs232_Rx;
output Rs232_Tx;
wire Key_flag;
wire Key_state;
wire Rx_Done;
wire Tx_Done;
wire [7:0]rdaddress;
wire [7:0]wraddress;
wire wren;
wire Send_en;
wire [7:0]rx_data;
wire [7:0]tx_data;
wire uart_state;
key_filter key_filter(
.Clk(Clk),
.Res_n(Res_n),
.key_in(Key_in),
.key_flag(Key_flag),
.key_state(Key_state)
);
uart_byte_rx uart_byte_rx(
.Clk(Clk),
.Res_n(Res_n),
.baud_set(3'd0),
.Rs232_Rx(Rs232_Rx),
.data_byte(rx_data),
.Rx_Done(Rx_Done)
);
Ctrl Ctrl(
.Clk(Clk),
.Res_n(Res_n),
.Key_flag(Key_flag),
.Key_state(Key_state),
.Rx_Done(Rx_Done),
.Tx_Done(Tx_Done),
.wren(wren),
.Send_en(Send_en),
.rdaddress(rdaddress),
.wraddress(wraddress)
);
uart_byte_tx uart_byte_tx(
.Clk(Clk),
.Res_n(Res_n),
.data_byte(tx_data),
.send_en(Send_en),
.baud_set(3'd0),
.Rs232_Tx(Rs232_Tx),
.Tx_Done(Tx_Done),
.uart_state(uart_state)
);
dpram dpram(
.clock(Clk),
.data(rx_data),
.rdaddress(rdaddress),
.wraddress(wraddress),
.wren(wren),
.q(tx_data)
);
endmodule
系统顶层 RTL viewer
testbench文件
simulation中要加入key_model的仿真文件
`timescale 1ns/1ns
`define clock_period 20
module UART_DPRAM_tb;
reg Clk;
reg Res_n;
reg press;
reg [7:0]data_byte_t;
reg send_en;
reg [2:0]baud_set;
wire Rs232_Rx;
wire Rs232_Tx;
wire Tx_Done;
wire uart_state;
wire Key_in; //并非通过tb产生,是通过仿真模型出来的,定义为wire型
UART_DPRAM UART_DPRAM(
.Clk(Clk),
.Key_in(Key_in),
.Rs232_Rx(Rs232_Rx),
.Res_n(Res_n),
.Rs232_Tx(Rs232_Tx)
);
//这里是例化一个发送模块来对总模块功能进行测试
uart_byte_tx uart_byte_tx(
.Clk(Clk),
.Res_n(Res_n),
.data_byte(data_byte_t),
.send_en(send_en),
.baud_set(baud_set),
.Rs232_Tx(Rs232_Rx), //使用的发送模块连接到总模块的接收端口上
.Tx_Done(Tx_Done),
.uart_state(uart_state)
);
key_model key_model(
.press(press),
.key(Key_in)
);
initial Clk =1;
always #(`clock_period/2)Clk = ~Clk;
initial begin
Res_n = 1'b0;
press = 0;
data_byte_t = 8'd0;
send_en = 1'd0;
#(`clock_period*20 + 1) //加一是为了与系统时钟错开,更好观察时序情况
Res_n = 1'b1;
#(`clock_period*50);
data_byte_t = 8'haa;
send_en = 1'd1;
#`clock_period;
send_en = 1'd0;
@(posedge Tx_Done) //等待完成信号的上升沿
#(`clock_period*5000)
//再次发送
data_byte_t = 8'h55;
send_en = 1'd1;
#`clock_period;
send_en = 1'd0;
@(posedge Tx_Done)
press = 1; //然后开始连续不断地发送
#(`clock_period*3);
press = 0;
#50000000;
$stop;
end
endmodule
十三、嵌入式RAM使用之ROM
实验目标:
1.学会调用 Quartus II 软件中提供的ROM核并进行仿真。
2.学会使用Signal Tap II 软件以及In System Memory Content Editor。
实验步骤:
1.File—New—Memory Files—Memory Initialization File(数据个数:256,数据位度:8)。
2.在rom.mif文件中输入三角波形。
3.Tools—Mega Wizard Plug-In Manager—Memory Complier—ROM:1-PORT。
4.通过Browse选择三角波形.mif作为初始化文件。
5.将该IP的配置加入工程中并设置为顶层文件。