前言
本文介绍UART的基本概念,并给出参考的Verilog代码
UART概念
UART(Universal Asynchronous Receiver/Transmitter),异步通用串行总线,是常见的通信协议之一. 在一次通信中,主机的TX连接从机的RX,主机的RX连接从机的TX
UART的传输格式: 起始位+数据(通常为8位)+校验+停止位 ,其中IDLE为高电平
由于UART没有时钟,因此需要通信双方确定好传输速率,即波特率,常用的波特率由9600,115200
校验位采用奇偶校验,在奇校验中,数据位+校验位中1的个数为奇数个,则该位置1,偶校验同理,在传输中需要确定校验方式.
停止位通常是高电平,停止位可以是多位,因此在传输中需要确定停止位的个数
UART结构
UART主要由发送TX模块与接收RX模块组成,下图为UART的发送模块,接收模块类似.
波特率生成模块:根据设定的波特率,生成控制传输的时钟
打包:将7个串行数据增加起始位,校验位和停止位,打包成符和协议的一帧数据
移位控制:使用计数器,一个一个数据发送,直一帧数据发送完毕
状态机:控制波特率何时工作,以及在传输完毕后生成TX_done
UART-TX代码
本文所介绍的UART统一采用115200波特率,奇校验,1位停止位,此外系统时钟为50MHz
1.端口定义
端口名 | 方向 | 说明 |
clk | inputt | 系统时钟,这里采用50MHz |
rst_n | inputt | 复位信号,低有效 |
tx_data | inputt | 发送数据(8bit) |
tx_en | inputt | 发送使能 |
tx | output | TX信号线 |
tx_done | output | 发送完成标志 |
module uart_tx #(parameter BAUD_RATE = 115200,
parameter CLK_FREQ = 50
)
(
input clk,
input rst_n,
input [7:0] tx_data,
input tx_en,
output reg tx,
output tx_done
)
2.波特率生成
根据设定的波特率与系统时钟,计算每个bit数据发送需要的系统时钟个数,并且进行计数,生成协议需要的时钟
localparam DIV_NUM = (CLK_FREQ * 1000000 / BAUD_RATE); //计算每个bit需要的时钟周期
reg [8:0] div_cnt; //需要计数434个,需要9位数据
wire tx_edg; //发送数据的边沿
always@(posedge clk or negedge rst_n)
if(!rst_n)
div_cnt <= 9'd0;
else if(div_cnt == DIV_NUM -1)
div_cnt <= 9'd0; //计满434个,归零
else if(c_state == SEND)
div_cnt <= div_cnt + 1; //在状态机为发送时,开始计数
else
div_cnt <= div_cnt;
assign tx_edg = (div_cnt == DIV_NUM -1); //当计满一次434后,该信号拉高一个周期
3.状态机与移位控制
根据协议,状态机应当具有以下几个状态,IDLE空闲状态、LOAD数据打包状态、SEND数据发送状态,不同状态进行该状态下的工作。
注意:进行数据打包时,应当保证打包的数据不会发生变化,因此需要将tx_data寄存起来
localparam IDLE = 2'b00; //空闲状态
localparam LOAD = 2'b01; //打包状态
localparam SEND = 2'b10; //发送状态
reg [1:0] c_state; //当前状态-current
reg [1:0] n_state; //下个状态-next
reg [10:0] shift_data; //需移位的数据,一帧数据为11位
reg [7:0] tx_data_buf; //发送数据缓存,保证该数据不会变化
reg [3:0] send_num; //计数发送的数据个数,一帧数据11bit
wire parity; //计数校验位
always@(posedge clk or negedge rst_n)
if(!rst_n)
tx_data_buf <= 8'd0;
else if(tx_en)
tx_data_buf <= tx_data; //发送使能时,将需发送的数据进行缓存
else
tx_data_buf <= tx_data_buf; //其余时候保持不变
always@(posedge clk or negedge rst_n)
if(!rst_n)
send_num <= 4'd0;
else if(send_num == 4'd10)
send_num <= 4'd0; //计满11个即一帧发送完毕,归零
else if(tx_edg)
send_num <= send_num + 1'b1; //每个发送数据边沿,计数+1
esle
send_num <= send_num;
/*****************三段式状态机******************/
always@(posedge clk or negedge rst_n)
if(!rst_n)
c_state <= IDLE;
else
c_state <= n_state;
always@(*) begin
case(c_state)
IDLE: n_state = tx_en? LOAD:IDLE; //空闲状态时,如果发送使能,则进入打包
LOAD: n_state = SEND; //打包只需要一拍,进入打包阶段直接进入发送阶段
SEND: n_state = (send_num == 4'd10 && tx_edg)? IDLE:SEND;
//发送完11个数据,进入IDLE状态,注意,需要等待发送数据的边沿
default: n_state = IDLE;
endcase
end
always@(posedge clk or negedge rst_n)
if(!rst_n)
shift_data <= 11'd0;
else if(c_state == LOAD)
shift_data <= {1'b1,parity,tx_data_buf,1'b0};
//将数据打包,注意移位从最低位开始,因此数据最低位为起始位,最高位为停止位
else if(c_state == SEND && tx_edg)
shift_data <= {1'b1,shift_data[10:1]};
//在发送阶段,在每个发送边沿,将数据右移一位,高位用1补
else
shift_data <= shitf_data;
assign parity = ~(^tx_data[7:0]);
//奇校验位计算,数据位依次相与,奇数个为1,此时校验位应当为0,才能保证1的个数依然是奇数
assign tx = (c_state == SEND)? shift_data[0]:1'b0; //在发送阶段,输出移位寄存器的最低位
always@(posedge clk or negedge rst_n)
if(!rst_n)
tx_done <= 1'b0;
else if(send_num == 4'd10 && tx_edg)
tx_done <= 1'b1; //当发送完一帧数据后,tx_done置1
else
tx_done <= 1'b0;
UART-RX代码
1.端口定义
端口名 | 方向 | 说明 |
clk | input | 系统时钟,50MHz |
rst_n | input | 复位信号,低有效 |
rx | input | RX信号线 |
rx_en | output | 接收使能 |
rx_data | output | 读出的数据(8bit) |
rx_done | output | 读完毕标志(非必要) |
module uart_rx #(parameter BAUD_RATE = 115200,
parameter CLK_FREQ = 50
)
(input clk,
input rst_n,
input rx,
output reg [7:0] rx_data,
output reg rx_en,
output reg rx_done
);
2.波特率生成
由于是接收模块,接收数据需要在中间部分采样,因此,取波特率计数的中间值作为触发接收的边沿
localparam DIV_NUM = (CLK_FREQ * 1000000 ) / BAUD;
reg [8:0] div_cnt; //波特率计数值
reg rx_edg; //接收数据边沿
always@(posedge clk or negedge rst_n)
if(!rst_n)
div_cnt <= 9'd0;
else if(div_num == DIV_NUM -1)
div_cnt <= 9'd0; //计满434,归零
else if(c_state == READ)
div_cnt <= div_cnt + 1'b1; //在读数据阶段,进行计数
assign rx_edg = (div_cnt == DIV_NUM>>1); //取中间值作为采样边沿
3.读使能判定
当接收数据线RX由高电平变为低电平-即发送了起始位,表示开始传输数据,此时读使能置1;
边沿的判断通过寄存器打拍判断
reg rx_d1; //rx打一拍后的数据
reg rx_d2; //rx打两拍后的数据
always@(posedge clk or negedge rst_n)
if(!rst_n) begin
rx_d1 <= 1'b1;
rx_d2 <= 1'b1; //rx在空闲状态为高电平
end
else begin
rx_d1 <= rx;
rx_d2 <= rx_d1;
end
assign rx_en = !rx_d1 && rx_d2; //前为高,后为低,则为下降沿
4.接收数据及状态机
接收数据模块状态只有两个,IDLE空闲状态与READ接收数据状态,在接收完数据后,去除起始位、校验位与终止位,留些接收的数据
localparam IDLE = 2'b00; //空闲状态
localparam READ = 2'b01; //读数据状态
reg [1:0] c_state; //当前状态-current
reg [1:0] n_state; //下一状态-next
reg [3:0] read_num; //接收数据计数
reg parity; //校验位
reg [10:0] rx_shitf_data; //接收数据移位寄存器
alwasy@(posedge clk or negedge rst_n)
if(!rst_n)
read_num <= 4'd0;
else if(read_num == 4'd10 && rx_edg)
read_num <= 4'd0; //计数收到11个,并且在接收边沿,归零
else if(rx_edg)
read_num <= read_num + 1'b1; //每个接收边沿,计数值+1
else
read_num <= read_num;
/***************三段式状态机**********************/
always@(posedge clk or negedge rst_n)
if(!rst_n)
c_state <= IDLE;
else
c_state <= n_state;
always@(*) begin
case(c_state)
IDLE: n_state = rx_en? READ:IDLE; //在空闲状态,若读使能,则进入读状态
READ: n_state = (read_num == 4'd10 && rx_edg)? ILDE: READ;
//在读状态,若接受完11个数据,且在接收边沿,则返回IDLE状态
default: n_state = IDLE;
endcase
end
alwasy@(posedge clk or negedge rst_n)
if(!rst_n)
rx_shitf_data <= 11'd0;
else if(c_state==READ && rx_edg)
rx_shift_data <= {rx_d2,rx_shift_data[10:1]};
//在读状态,依次接收rx数据(打两拍后的数据)
else
rx_shift_data <= rx_shift_data;
assign parity = (read_num == 4'd10 && rx_edg)? ~(^rx_shift_data[10:2]):1'b0;
//在接收完数据,判断数据为+校验位是否正确
assign rx_data = rx_shift_data[8:1]; //去除起始位、校验位与终止位
/************rx_done*******************/
alwasy@(posedge clk or negedge rst_n)
if(!rst_n)
rx_done <= 1'b0;
else if(read_num == 4'd10 && rx_edg)
rx_done <= 1'b1;
else
rx_done <= 1'b0;
补充说明:
完成了UART的TX与RX,完整的UART需要例化两个模块,根据需求编写代码(主机或从机),完成使用UART总线进行读写的功能