FPGA入门笔记009——UART串口发送模块设计
1、UART通信原理
如图1为UART通信连接图,其中tx为输入,rx为输出。通过tx连接rx进行数据间的发送和接收。
UART 通信在使用前需要做多项设置,最常见的设置包括:数据位数、波特率大小、奇偶校验类型和停止位数:
(1)数据位(Data bits):该参数定义单个 UART 数据传输在开始到停止期间发送的数据位数。可选择为:5、6、7 或者 8(默认)。
(2)波特率(Baud)大小:是指从一设备发到另一设备的波特率,即:每秒钟可以通信的数据比特个数。典型的波特率有 300bps/Hz, 1200bps/Hz, 2400bps/Hz, 9600bps/Hz, 19200bps/Hz, 115200bps/Hz 等。一般通信两端设备都要设为相同的波特率,但有些设备也可设置为自动检测波特率。
(3)奇偶校验类型(Parity Type):是用来验证数据的正确性。奇偶校验一般不使用,如果使用,则既可以做奇校验(Odd)也可以做偶校验(Even)。
(4)停止位(Stop bits):在每个字节的数据位发送完成之后,发送停止位,来标志着一次数据传输完成,同时用来帮助接受信号方硬件重同步。可选择为:1(默认)、1.5 或者 2 位。
在 RS-232 标准中,最常用的配置是 8N1(即八个数据位、一个停止位),其发送一个字节时序图如图 2 所示。
按照一个完整的字节包括:一位起始位、8 位数据位、一位停止位,总共十位数据来算, 要想完整的实现这十位数据的发送,就需要 11 个波特率时钟脉冲,第 1 个脉冲标记一次传输的起始,第 11 个脉冲标记一次传输的结束,如图 2 所示,其中BPS_CLK为波特率时钟信号:
2、串口发送模块整体设计
基于上述原理,本章要实现的串口发送模块整体框图,如图 3 所示,其接口列表如表 1 所示。
根据功能需求,串口发送模块可进一步细化为如图 4 所示详细结构图,其中每一子模块的作用如表 2 所示。其中绿色的框代表单一结构的寄存器,来实现数据的稳定输入以及输出。
3、接口设置
module uart_byte_tx(
Clk,
Rst_n,
Send_En,
Data_Byte,
Baud_Set,
Rs232_Tx,
Tx_Done,
UART_state
);
input Clk;
input Rst_n;
input Send_En;
input [2:0]Baud_Set;
input [7:0]Data_Byte;
output reg Rs232_Tx;
output reg Tx_Done;
output reg UART_state;
endmodule
4、波特率时钟生成模块设计
从原理部分可知,波特率是 UART 通信中需要设置的参数之一。在波特率时钟生成模 块中,计数器需要的计数值与波特率之间的关系如表 3 所示,其中系统时钟周期为 clk_period,这里为 20ns。如果接入到该模块的时钟频率为其他值,需要根据具体的 频率值修改该参数。
本模块的设计是为了保证模块的复用性。当需要不同的波特率时,只需设置不同的波特 率时钟计数器的计数值。使用查找表即可实现,下面的设计代码中(DR_LUT)只包含了针对 5 个波特率 的设置,如需要其他波特率可根据实际使用情况具体修改。
//波特率时钟生成模块
reg [15:0]bps_DR; //分频计数最大值
reg [15:0]div_cnt; //分频计数器
reg bps_clk; //波特率时钟
reg [3:0]bps_cnt; //波特率时钟计数器
//查找表(DR_LUT)
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)
bps_DR <= 16'd5207;
else begin
case(Baud_Set)
0:bps_DR <= 16'd5207;
1:bps_DR <= 16'd2603;
2:bps_DR <= 16'd1302;
3:bps_DR <= 16'd867;
4:bps_DR <= 16'd433;
default:bps_DR <= 16'd5207;
endcase
end
//分频计数器(Div_Cnt)
always@(posedge Clk or negedge Rst_n)
if(!Rst_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脉冲信号(Div_Cnt)
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)
bps_clk <= 1'b0;
else if(div_cnt == 16'd1) //当计数器开始工作时,bps_clk就产生一个高脉冲,若计数器计数到bps_DR,则脉冲信号会延后一位。
bps_clk <= 1'b1;
else
bps_clk <= 1'b0;
//波特率时钟计数器(bps_cnt)
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)
bps_cnt <= 4'd0;
else if(Tx_Done)
bps_cnt <= 4'd0;
else if(bps_clk)
bps_cnt <= bps_cnt + 1'b1;
else
bps_cnt <= bps_cnt;
//发送结束信号生成(Tx_Done)
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)
Tx_Done <= 1'b0;
else if(bps_cnt == 4'd11)
Tx_Done <= 1'b1;
else
Tx_Done <= 1'b0;
5、数据输出模块设计
//数据输出模块
//为了保证输出稳定,设置一个内部寄存器r_data_byte = Data_Byte。这样当Send_En = 1的时候,无论data_byte怎么变化,r_data_byte的值都不变。
reg [7:0]r_data_byte;
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)
r_data_byte <= 8'd0;
else if(Send_En)
r_data_byte <= Data_Byte;
//10选1多路器
localparam START_BIT = 1'b0;
localparam STOP_BIT = 1'b1;
always@(posedge Clk or negedge Rst_n) //用'*'号可能会出现毛刺,所有用时序逻辑来编写组合逻辑电路,较稳定
if(!Rst_n)
Rs232_Tx <= 1'b1; //Rs232_Tx默认为1
else begin
case(bps_cnt)
0:Rs232_Tx <= 1'b1;
1:Rs232_Tx <= 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 <= STOP_BIT;
default:Rs232_Tx <= 1'b1;
endcase
end
6、发送状态模块
//发送状态模块(UART_state)
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)
UART_state <= 1'b0;
else if(Send_En)
UART_state <= 1'b1;
else if(Tx_Done)
UART_state <= 1'b0;
else
UART_state <= uart_state;
7、仿真
`timescale 1ns/1ns
`define clk_period 20
module uart_byte_tx_tb;
reg Clk;
reg Rst_n;
reg Send_En;
reg [2:0]Baud_Set;
reg [7:0]Data_Byte;
wire Rs232_Tx;
wire Tx_Done;
wire UART_state;
uart_byte_tx uart_byte_tx(
.Clk(Clk),
.Rst_n(Rst_n),
.Send_En(Send_En),
.Data_Byte(Data_Byte),
.Baud_Set(Baud_Set),
.Rs232_Tx(Rs232_Tx),
.Tx_Done(Tx_Done),
.UART_state(UART_state)
);
initial Clk = 1'b1;
always#(`clk_period/2) Clk = ~Clk;
initial begin
Rst_n = 1'b0;
Data_Byte = 8'b0;
Send_En = 1'b0;
Baud_Set = 3'd4;
#(`clk_period*20 + 1) //让复位信号不与时钟边沿对其,更好观察时序关系
Rst_n = 1'b1;
#(`clk_period*50)
Data_Byte = 8'haa;
Send_En = 1'b1;
#`clk_period
Send_En = 1'd0;
@(posedge Tx_Done) //当Tx_Done信号出现上升沿时,进行接下来的语句
#(`clk_period*5000)
Data_Byte = 8'h55;
Send_En = 1'b1;
#`clk_period
Send_En = 1'd0;
@(posedge Tx_Done)
#(`clk_period*5000)
$stop;
end
endmodule
8、板级调试
为了实现导读中所设定的目标,将以前编写好的按键消抖模块添加到工程当中,并再次 使用 ISSP,其参数配置如图 5 所示,并加入到工程中。
然后,新建一个顶层文件,并将按键消抖、串口发送以及 ISSP 例化,并将按键状态与串口发送使能端连接即可。设计如下所示,并将串口发送状态(UART_state)连接到 LED 上,以更好的观察数据发送状态。
module uart_byte_tx_top(
Clk,
Rst_n,
Rs232_Tx,
key_in0,
led
);
input Clk;
input Rst_n;
input key_in0;
output Rs232_Tx;
output led;
wire Send_En;
wire [7:0]Dta_Byte;
wire key_flag0;
wire key_state0;
assign Send_En = key_flag0 & !key_state0; //对Send_En进行按键消抖
//uart串口发送模块
uart_byte_tx uart_byte_tx(
.Clk(Clk),
.Rst_n(Rst_n),
.Send_En(Send_En),
.Data_Byte(Data_Byte),
.Baud_Set(3'd4), //波特率为115200
.Rs232_Tx(Rs232_Tx),
.Tx_Done(),
.UART_state(led)
);
//按键消抖模块
key_filter key_filter(
.Clk(Clk),
.Rst_n(Rst_n),
.key_in(key_in0),
.key_flag(key_flag0),
.key_state(key_state0)
);
//ISSP激励源
issp issp(
.probe(),
.source(Data_Byte)
);
endmodule
编译无误后,点击 RTL_viewer 可以看到如图 6 的各模块连接图。
到此,程序和模块设计部分的内容完成。
9、串口发送多个字节的数据方案
目标:通过串口发送“HELLO”字符串。
在uart_tx_top文件中,去掉ISSP模块和该行代码:assign Send_En = key_flag0 & !key_state0; ,并编写如下代码:
//字符串"HELLO\0"发送模块
reg [2:0] cnt;
localparam
byte1 = "H",
byte2 = "E",
byte3 = "L",
byte4 = "L",
byte5 = "O",
byte6 = "\0";
//计数器计数
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)
cnt <= 3'd0;
else if(Tx_Done) //D触发器的特征:当检测到Tx_Done时,cnt不会立即更新,cnt会在下一个时钟周期上升沿到来时才跟新(延迟一拍)。
cnt <= cnt + 1'b1;
else if(key_flag0 & !key_state0) //key_flag0 & !key_state0表示按键按下并状态稳定,没有滞后
cnt <= 3'd0;
//通过发送结果控制发送
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)
Send_En <= 1'b0;
else if(key_flag0 & !key_state0) //第一次按下按键,启动一次发送,没有滞后
Send_En <= 1'b1;
else if(Tx_Done &(cnt < 3'd5)) //后面每次发送完成,Tx_Done产生一次高脉冲,启动下一次发送。最后一个字节发送完成的Tx_Done不能触发新一次的发送
Send_En <= 1'b1; //Send_En滞后一拍变成0,当第6个字节发送完成,产生Tx_Done脉冲时,此时cnt还没自加1,还是5
else
Send_En <= 1'b0;
//要发送的数据
always@(*) //组合逻辑
case(cnt)
3'd0:Data_Byte <= byte1;
3'd1:Data_Byte <= byte2;
3'd2:Data_Byte <= byte3;
3'd3:Data_Byte <= byte4;
3'd4:Data_Byte <= byte5;
3'd5:Data_Byte <= byte6;
default:Data_Byte = 0;
endcase
仿真代码如下所示:
`timescale 1ns/1ns
`define clk_period 20
module uart_byte_tx_top_tb;
reg Clk;
reg Rst_n;
wire key;
wire led;
wire Rs232_Tx;
reg press;
uart_byte_tx_top uart_byte_tx_top(
.Clk(Clk),
.Rst_n(Rst_n),
.Rs232_Tx(Rs232_Tx),
.key_in0(key),
.led(led)
);
key_model key_model(
.press(press),
.key(key)
);
initial Clk = 1'b1;
always#(`clk_period/2) Clk = ~Clk;
initial begin
Rst_n = 1'b0;
press = 1'b0;
#(`clk_period*20 + 1);
Rst_n = 1'b1;
#(`clk_period*20 + 1);
press = 1'b1;
#(`clk_period*20 + 1);
press = 1'b0;
wait(uart_byte_tx_top.Tx_Done &(uart_byte_tx_top.cnt == 3'd5)); //wait函数调用、层次的引用
#(`clk_period*200 + 1);
$stop;
end
endmodule
在如上所示的代码中,所用到的知识点为:
1、加入一个仿真模块:key_model;
2、wait函数调用
3、层次的引用