UART协议详解
UART(Universal Asynchronous Receiver/Transmitter)即通用异步收发器,是一种串行、异步、全双工的通信协议。
一帧由起始位(1位)、数据位(5-9位)、校验位(0或1位)、停止位(1-2位)等四部分构成。
数据传输线空闲的时候保持高电平,当开始传输时,拉低一个时钟周期,这就是起始位。之后传输数据位,一般从小端开始传输。然后紧随一个可选的奇偶校验位。然后电平拉高,指示一帧发送的终止,即停止位。
采样时钟一般为波特率的数倍(常见的如8倍或16倍),并在中间时刻进行采样,以避免滑码或误码。
空闲位:
UART协议规定,当总线处于空闲状态时信号线的状态为‘1’即高电平
起始位:
开始进行数据传输时发送方要先发出一个低电平’0’来表示传输字符的开始。因为空闲位一直是高电平所以开始第一次通讯时先发送一个明显区别于空闲状态的信号即为低电平。
数据位:
起始位之后就是要传输的数据,数据可以是5,6,7,8,9位,构成一个字符,一般都是8位。先发送最低位最后发送最高位。
奇偶校验位:
数据位传送完成后,要进行奇偶校验,串口校验分几种方式:
1.无校验(no parity)
2.奇校验(odd parity):如果数据位中’1’的数目是偶数,则校验位为’1’,如果’1’的数目是奇数,校验位为’0’。
3.偶校验(even parity):如果数据为中’1’的数目是偶数,则校验位为’0’,如果为奇数,校验位为’1’。
4.mark parity:校验位始终为1
5.space parity:校验位始终为0
停止位:
数据结束标志,可以是1位,1.5位,2位的高电平。
波特率:
数据传输速率使用波特率来表示,单位bps(bits per second),常见的波特率9600bps,115200bps等等,其他标准的波特率是1200,2400,4800,19200,38400,57600。举个例子,如果串口波特率设置为9600bps,那么传输一个比特需要的时间是1/9600≈104.2us。
流程图
UART实际上可以分为UART_tx、UART_rx两个独立的部分,其流程图分别如下:
- UART_TX
- UART_RX
FPGA代码实现
/******************************FILE HEAD**********************************
* file_name : uart.v
* function : uart顶层模块
* author : 今朝无言
* version & date : 2021/9/16 & v1.0
*************************************************************************/
module uart(
input clk_50M,
input rst_n,
output TXD,
input RXD,
input [7:0] wrdat,
input tx_en,
output tx_done,
output reg [7:0] rddat,
output rx_done,
output dataerror, //数据出错标志
output frameerror, //帧出错标志
input [19:0] Baudrate, //波特率
input [3:0] DataLen, //数据帧长(小于等于8) 小端
input isParity, //是否有校验位
input ParityMode //奇偶校验标志 0为偶校验 1为奇校验
); //起始位和停止位都为1bit
reg [19:0] Baudrate_reg = 9600;
wire [23:0] cnt_ceil = 3_125_000/Baudrate_reg;
reg [23:0] cnt = 0;
reg clk = 0;
always @(negedge rst_n) begin
Baudrate_reg <= Baudrate;
end
always @(posedge clk_50M) begin //驱动时钟,为波特率的16倍
if(cnt>=cnt_ceil)
cnt <= 1;
else
cnt <= cnt+1;
if(cnt>cnt_ceil>>1)
clk <= 0;
else
clk <= 1;
end
//发送模块
uart_tx uart_tx_inst(
.clk (clk),
.rst_n (rst_n),
.tx (TXD),
.data (wrdat),
.tx_en (tx_en),
.tx_done (tx_done),
.DataLen_wire (DataLen),
.isParity_wire (isParity),
.ParityMode_wire(ParityMode)
);
//接收模块
wire [7:0] rddat_wire;
uart_rx uart_rx_inst(
.clk (clk),
.rst_n (rst_n),
.rx (RXD),
.dataout (rddat_wire),
.rx_done (rx_done),
.dataerror (dataerror),
.frameerror (frameerror),
.DataLen_wire (DataLen),
.isParity_wire (isParity),
.ParityMode_wire(ParityMode)
);
always @(posedge rx_done) begin
rddat <= rddat_wire;
end
endmodule
//END OF uart.v FILE***************************************************
/******************************FILE HEAD**********************************
* file_name : uart_tx.v
* function : uart发送子模块
* author : 今朝无言
* version & date : 2021/9/16 & v1.0
*************************************************************************/
module uart_tx(
input clk,
input rst_n,
output reg tx,
input [7:0] data,
input tx_en,
output tx_done,
input [3:0] DataLen_wire,
input isParity_wire,
input ParityMode_wire
);
reg busy; //线路状态指示,高为线路忙,低为线路空闲
reg send;
reg wrsigbuf;
reg wrsigrise;
reg presult;
reg [7:0] cnt;
reg [3:0] DataLen = 4'd8;
reg isParity = 1'b0;
reg paritymode = 1'b0;
reg [3:0] dataN_send = 4'd0; //记录当前将要发送的数据(亦即已发送的数据位个数)
always @(negedge rst_n) begin //在rst拉低时配置数据位长度、是否使用校验位、奇偶校验
DataLen <= DataLen_wire;
isParity <= isParity_wire;
paritymode <= ParityMode_wire;
end
//检测上升沿
always @(posedge clk) begin
wrsigbuf <= tx_en;
wrsigrise <= (~wrsigbuf) & tx_en;
end
//发送结束信号
assign tx_done = ~busy;
//启动串口发送程序
always @(posedge clk) begin
if(wrsigrise && (~busy)) begin //当发送命令有效且线路为空闲时,启动新的数据发送
send <= 1'b1;
end
else if(cnt==((DataLen+2+isParity)<<4)-4) begin
send <= 1'b0;
end
end
//串口发送程序,16个时钟发送一个bit
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
tx <= 1'b1;
busy <= 1'b0;
cnt <= 8'd0;
presult <= 1'b0;
dataN_send <= 4'd0;
end
else if(send==1'b1) begin
if(cnt==8'd0) begin
tx <= 1'b0; //产生起始位
dataN_send <= 4'd0;
presult <= paritymode;
busy <= 1'b1;
cnt <= cnt + 8'd1;
end
else if(cnt==(dataN_send+1)<<4 && dataN_send<DataLen) begin
tx <= data[dataN_send]; //发送数据位 小端
presult <= data[dataN_send]^presult;
busy <= 1'b1;
cnt <= cnt + 8'd1;
dataN_send <= dataN_send+1;
end
else if(cnt==(DataLen+1)<<4) begin
if(isParity)begin
tx <= presult; //发送奇偶校验位
busy <= 1'b1;
cnt <= cnt + 8'd1;
end
else begin
tx <= 1'b1; //发送停止位
busy <= 1'b1;
cnt <= cnt + 8'd1;
end
end
else if(cnt==((DataLen+2)<<4)-4) begin
if(isParity) begin
tx <= 1'b1; //发送停止位
busy <= 1'b1;
cnt <= cnt + 8'd1;
end
else begin
tx <= 1'b1;
busy <= 1'b0;
cnt <= cnt + 8'd1;
end
end
else if(cnt==((DataLen+3)<<4)-4) begin
tx <= 1'b1;
busy <= 1'b0;
cnt <= cnt + 8'd1;
end
else begin
cnt <= cnt + 8'd1;
end
end
else begin
tx <= 1'b1;
cnt <= 8'd0;
busy <= 1'b0;
end
end
endmodule
//END OF uart_tx.v FILE***************************************************
/******************************FILE HEAD**********************************
* file_name : uart_rx.v
* function : uart接收子模块
* author : 今朝无言
* version & date : 2021/9/16 & v1.0
*************************************************************************/
module uart_rx(
input clk, //采样时钟
input rst_n,
input rx,
output reg [7:0] dataout,
output rx_done, //数据接收完成
output reg dataerror, //数据出错
output reg frameerror, //帧出错
input [3:0] DataLen_wire,
input isParity_wire,
input ParityMode_wire
);
reg rdsig;
reg [7:0] cnt;
reg rxbuf, rxfall, receive;
reg presult, busy;
assign rx_done = ~busy; //上升沿对应一帧接收完成的时刻
reg [3:0] DataLen = 4'd8;
reg isParity = 1'b0;
reg paritymode = 1'b0;
reg [3:0] dataN_rec = 4'd0; //记录当前将要接收的数据位(亦即已接收的数据位个数)
always @(negedge rst_n) begin //在rst拉低时配置数据位长度、是否使用校验位、奇偶校验
DataLen <= DataLen_wire;
isParity <= isParity_wire;
paritymode <= ParityMode_wire;
end
always @(posedge clk) begin //检测线路的下降沿
rxbuf <= rx;
rxfall <= rxbuf & (~rx);
end
/**********************启动串口接收程序********************************/
always @(posedge clk) begin
if (rxfall && (~busy)) begin //检测到线路的下降沿并且原先线路为空闲,启动接收数据进程
receive <= 1'b1;
end
else if(cnt == ((DataLen+2+isParity)<<4)-4) begin //接收数据完成
receive <= 1'b0;
end
end
/********串口接收程序, 16个时钟接收一个bit*********/
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
busy <= 1'b0;
cnt <= 8'd0;
rdsig <= 1'b0;
frameerror <= 1'b0;
dataerror <= 1'b0;
presult <= 1'b0;
dataout <= 8'd0;
end
else if(receive == 1'b1) begin
if(cnt == 8'd0) begin //接收起始位
busy <= 1'b1;
presult <= paritymode;
dataN_rec <= 4'd0;
cnt <= cnt + 8'd1;
rdsig <= 1'b0;
dataout <= 8'd0;
end
else if((cnt == ((dataN_rec+1)<<4) + 8) && (dataN_rec < DataLen)) begin //接收数据位 在每个bit的中间采样,避免滑码或误码
busy <= 1'b1;
dataout[dataN_rec] <= rx;
presult <= presult^rx;
cnt <= cnt + 8'd1;
rdsig <= 1'b0;
dataN_rec <= dataN_rec + 1;
end
else if(cnt == ((DataLen+1)<<4) + 8) begin
if(isParity) begin //如有校验位,接收奇偶校验位
busy <= 1'b1;
if(presult == rx)
dataerror <= 1'b0;
else
dataerror <= 1'b1; //如果奇偶校验位不对,表示数据出错
cnt <= cnt + 8'd1;
rdsig <= 1'b1;
end
else begin //否则接收停止位
busy <= 1'b1;
if(1'b1 == rx) begin
frameerror <= 1'b0;
end
else begin
frameerror <= 1'b1; //如果没有接收到停止位,表示帧出错
end
cnt <= cnt + 8'd1;
rdsig <= 1'b1;
end
end
else if(cnt == ((DataLen+2)<<4) + 8) begin //有奇偶校验位,则在该时刻接收停止位
busy <= 1'b1;
if(rx == 1'b1) begin
frameerror <= 1'b0;
end
else begin
frameerror <= 1'b1; //如果没有接收到停止位,表示帧出错
end
cnt <= cnt + 8'd1;
rdsig <= 1'b1;
end
else begin
cnt <= cnt + 8'd1;
end
end
else begin
cnt <= 8'd0;
busy <= 1'b0;
rdsig <= 1'b0;
end
end
endmodule
//END OF uart_rx.v FILE***************************************************
仿真
/******************************FILE HEAD**********************************
* file_name : uart_tb.v
* function : uart模块的TestBench
* author : 今朝无言
* version & date : 2021/9/17 & v1.0
*************************************************************************/
`default_nettype none
`timescale 1ns/1ps
module uart_tb();
parameter Baudrate = 115200;
parameter dataLen = 8;
parameter isParity = 1;
parameter ParityMode = 0;
parameter timeDelay = 1e9/Baudrate;
parameter cntBaud = 5e7/Baudrate;
reg clk_50M;
reg rst;
reg RXD;
wire TXD;
reg [7:0] wrdat;
wire [7:0] rddat;
reg tx_en;
wire tx_done;
wire rx_done;
wire dataerror;
wire frameerror;
//initial vars
reg start_tb;
initial begin
clk_50M <= 0;
RXD <= 1;
tx_en <= 0;
start_tb <= 0;
rst <= 1; //初始化UART模块
#20 rst <= 0;
#20 rst <= 1;
start_tb <= 1;
end
//main test_bench
initial begin
wait(start_tb);
#timeDelay;
//TXD_test
write_txd(8'h12);
write_txd(8'h34);
write_txd(8'h56);
//RXD_test
read_rxd(8'h78);
read_rxd(8'h9A);
#(timeDelay*10);
$stop;
end
//----------------------------------------------------------------------------
always begin //50MHz时钟
#10 clk_50M <= 1;
#10 clk_50M <= 0;
end
reg clk_Baudrate = 0;
reg [12:0] cnt_Baudrate = 0;
always @(posedge clk_50M) begin
if(cnt_Baudrate < cntBaud)
cnt_Baudrate <= cnt_Baudrate + 1;
else
cnt_Baudrate <= 0;
if(cnt_Baudrate < cntBaud/2)
clk_Baudrate <= 1;
else
clk_Baudrate <= 0;
end
//测试TX模块
task write_txd;
input [dataLen-1:0] data;
begin
wrdat <= data;
tx_en <= 1;
#timeDelay tx_en <= 0;
#(timeDelay*(dataLen+(isParity?1:0)+1));
end
endtask
//测试RX模块
task read_rxd;
input [dataLen-1:0] data;
begin
RXD <= 0;
#timeDelay;
begin : SendData
integer i;
for(i=0; i<dataLen; i=i+1) begin
RXD <= data[i];
#timeDelay;
end
end
if(isParity) begin
RXD <= ^data;
#timeDelay;
end
RXD <= 1;
#timeDelay;
end
endtask
uart uart_inst(
.clk_50M (clk_50M),
.rst_n (rst),
.TXD (TXD),
.RXD (RXD),
.wrdat (wrdat),
.tx_en (tx_en),
.tx_done (tx_done),
.rddat (rddat),
.rx_done (rx_done),
.dataerror (dataerror),
.frameerror (frameerror),
.Baudrate (Baudrate),
.DataLen (dataLen),
.isParity (isParity),
.ParityMode (ParityMode)
);
endmodule
//END OF uart_tb.v FILE***************************************************
仿真结果:
上板测试
测试思路:PC端通过串口调试助手向FPGA发送数据,FPGA端接收到数据后,转发这个数据回PC端,作为应答信号。即进行回环测试。
测试结果如下: