UART的全称是通用异步收发器(Universal Asynchronous Receiver/Transmitter)。
Universal 通用性: 体现在UART使用范围广上,作为一个通用的接口协议,UART广泛的应用在各类MCU和SOC产品上。
Asynchronous 异步性: 体现在”不需要额外的时钟线进行数据的同步传输”即只要信号拉低,即可开始传送数据,而另一些通讯协议,需要引入时钟信号来进行操作,如AMBA,需要在时钟的上升沿发送数据。
Receiver/Transmitter收发器:则更好理解,即一个数据的发送方和一个数据的接收方,也意味着在数字IC设计中需要分别设计Receiver和Transmitter。
发送端的UART
将来自控制设备(如CPU)的并行数据转换为串行数据,以串行方式将其发送到接收端的UART
,然后由接收端的UART将串行数据转换为并行数据以用于接收设备的正常处理。
这里只需要两条线RX/TX即可在两个UART之间传输数据。具体如下图所示;
UART的一帧由1位起始位 、5~9个 数据位 、一个可选的奇偶校验位 和 停止位 组成。数据逐位传输,如下图所示。
为什么UART的传输需要起始位?
因为UART没有控制线,要让接收方知道什么时候开始接收数据,需要一些手段。
在发送数据之前,先发一位逻辑“0”作为数据发送的起始标志,接收方在空闲时,当检测到有一个低电平,则开始以波特率的频率接逐位接收数据。
在具体设计UART的过程中,如何检测低电平,使用到了电平检测电路。很显然这里使用的是下降沿检测电路。
UART基本的数据形式
默认无传输数据时,为高电平。
当信号拉低,传输线上的电平拉低,意味着开始进行数据传输
紧接着起始位的是数据位,它可以是5、6、7或8位。
UART的“校验位”紧挨着“数据位”,采用奇偶校验 方式,根据设置,校验位可以存在也可以不存在。
UART将停止位作为停止标志,是在数据位(没有校验位)和校验位(有校验位)之后发送1~2位的逻辑“1”高电平。当发送完停止位之后,UART总线进入空闲。
为什么UART的数据位可变?
数据位包含正在传输的实际数据。如果使用奇偶校验位,则可以是5位,最多8位。如果不使用奇偶校验位,则数据帧的长度可以为9位。
因为UART是一种低速总线,每多发一位都占用不少的时间(由传输波特率决定),所以可以根据传输数据的特点,采用不同位宽以节约数据传输的时间。
如果从更高的level审视UART传输协议,如嵌入式开发者的角度,我们会发现,在使用具体的UART协议前,我们需要对发送端和接收端进行波特率的同步,以此来确保发射端的数据可以在接收端得到正确的采集。常用的波特率可以是300,1200,2400,9600,19200,38400,115200,这些数意味着什么呢?别着急,我们接下来要讨论这个内容。
什么是波特率
波特率等于每秒钟传输的数据位数,即Bit/s。 假如波特率设置为9600,那么意味着每秒该UART传输协议可以传输9600bits的数据,换句话说传输1比特需时间约为:10^9(ns)/9600=104166(ns)。
如何换算波特率
时钟频率假如为100MHz,这意味着我们的时钟周期为10ns,因此10416个时钟周期我们就可以传输1bit数据,换言之我们需要一个大小为10416的分频电路来对100MHz时钟进行处理,因此在设计UART的过程中,我们需要使用分频电路依据波特率处理全局时钟,依据分频后的时钟节奏来发送数据和接收数据。
同样的,参考作者之前的文章,我们可以获知分频电路的设计方法
波特率和采样频率是一样的吗?
按照前文所说,好像波特率和采样频率是一个意思,即9600波特率对应接收端1s进行9600次采样,也对应发射端1s进行9600bit的发射,那么请读者思考,真的是这样吗?
答案其实是否定的
这是因为:在数据的传输中,信号可能受到一些干扰而产生一些抖动(比如说电磁兼容性设计中的近端串扰),如果接收端只对这些信号进行一次采样,那么它有可能采样到的是不准确的数据,所以接收端在采样时,通常都要采样多次,然后通过处理获得准确的数据,比如说,我们可以用多数表决的方法来在接收端进行多次采样,得到准确值。
UART、RS232、RS485在串口通信中,主要区别是电平的不同,其中UART通常使用TTL电平,下面介绍这几个存在的差异;
TTL
TTL全名是晶体管-晶体管逻辑集成电路(Transistor-Transistor Logic)
输入高电平最小2V,输出高电平最小2.4V,典型值3.4V;
输入低电平最大0.8V,输出低电平最大0.4V,典型值0.2V。
RS232
RS232 逻辑1电平(MARK)=-3V~-15V,逻辑0电平(SPACE)=+3~+15V;
同样的,对于传输数据0x55,即二进制的01010101,RS232和TTL的区别如下;(LSB:最低位,MSB:最高位)
RS485
RS485是差分信号进行串行传输;
逻辑1以两线间的电压差为+(2 ~ 6)V表示 ; 逻辑 "0"以两线间的电压差为-(2 ~ 6)V表示 ;
在工业通信中,使用RS485比较多,因为RS485是差分信号,可以抑制共模干扰,因此在恶劣的环境中拥有很好的抗干扰性,比较稳定;
FPGA采用TTL电平:3.3V(1)、0V(0)。而PC采用的是负逻辑电平:-15~-5为逻辑1,+5 ~ +15为逻辑0。
因此,PC与FPGA之间通信时,需要进行电平转换。
老早以前的台式机和笔记本可都是有串口的,虽然现在很多台式机串口没有了,笔记本也没有串口了,但是串口从来没有被丢弃,只是以另外一种形式存在,这就是 USB 转串口芯片。串口只需要 2 根线就可以实现一
收一发,使用简单,可靠方便,在低速场合大量使用。
本实例设计的UART特性:
原理图如下,分两个大模块,一个==数据接收控制模块(Receive_Control)==,一个数据发送控制模块(Send_Control)。
模块启动后,
端口说明:
接收控制模块与发送控制模块内部都有一个波特率时钟产生模块(BuadRate_set),用于将电路输入时钟(clk)进行分频产生波特率时钟,用于接收和发送数据控制。
可以看到,BuadRate_set模块有一个enable控制信号,只有当enable信号为高时,BuadRate_set模块才工作。 在发送控制模块里面,只有发送数据的时候才拉高其相应的enable;在接收控制模块里面,只有检测到有数据发送进来的时候才拉高其相应的enable。这是为了降低功耗。
`timescale 1ns / 1ps
module BuadRate_set #(
parameter CLK_Period=50000000,//the unit is Hz
parameter Buad_Rate=9600 //the unit is bits/s
)(
input clk, //原始系统时钟
input rst_n, //复位
input enable, //模块使能信号
output Buad_clk //输出的分频
);
localparam DIV_PEREM=CLK_Period/Buad_Rate/2; //计数器最大值
reg[15:0] cnt;
always @( posedge clk )
if( !rst_n )
cnt <= 16'b0000;
else if( enable ) begin
if( cnt != DIV_PEREM )
cnt <= cnt+1'b1;
else
cnt <= 16'h0000;
end
else
cnt <= 16'h0000;
reg DIV_clk;
always @( posedge clk )
if( !rst_n || !enable ) //当复位或者模块不使能的时候,分频器停止。
DIV_clk <= 1'b1;
else if(cnt==DIV_PEREM) //计数器满,输出时钟翻转
DIV_clk <= ~DIV_clk;
assign Buad_clk = DIV_clk;
endmodule
`timescale 1ns / 1ps
module Send_Control #(
parameter CLK_Period=50000000,//the unit is Hz
parameter Buad_Rate=9600 //the unit is bits/s
)(
input clk,
input rst_n,
input tx_en,
input [7:0] tx_data,
output tx_finish,
output uart_tx
);
localparam IDLE=2'b00; //0
localparam EN_TX=2'b10; //2
localparam END_BIT=2'b11; //3
wire Buad_clk;
wire Buad_en;
BuadRate_set #(
.CLK_Period(CLK_Period),//the unit is Hz
.Buad_Rate (Buad_Rate) //the unit is bits/s
) BuadRate_set_inst(
.clk (clk ),
.rst_n (rst_n ),
.enable (Buad_en ),
.Buad_clk(Buad_clk)
);
reg [9:0] data_to_send; //发送10个数
reg [1:0] state;
reg [3:0] cnt;
//发送模块状态机
always @( posedge clk )
if( !rst_n )
state <= IDLE;
else
case(state)
IDLE:if( tx_en ) //tx_en发送有效的下一个时钟上升沿,开始发送数据
state <= EN_TX;
EN_TX: if( cnt=='d10 )
state <= IDLE;
END_BIT: if( cnt=='d0 )
state <= IDLE;
default: state <= IDLE;
endcase
//波特率时钟发生启动信号
assign Buad_en = (state == EN_TX || state == END_BIT);
//发送计数0~9
always @( posedge Buad_clk or negedge rst_n)
if( !rst_n )
cnt <= 4'b000;
else begin
if( cnt<4'd10)
cnt <= cnt + 1'b1;
else
cnt <= 4'b0000;
end
//发送数据
always @( posedge Buad_clk or negedge rst_n )
if( !rst_n )
data_to_send <= 10'h3ff;
else if( cnt == 'd0 ) //发送第一个数
data_to_send <= {1'b1,tx_data,1'b0};
else //之后,数据右移位
data_to_send <= {1'b1,data_to_send[8:1]};
assign uart_tx = (Buad_en)?data_to_send[0]:1'b1;
assign tx_finish = (state == END_BIT)? 1'b1:1'b0;
endmodule
`timescale 1ns / 1ps
module tb_send();
reg clk;
reg rst_n;
reg tx_en;
reg [7:0] tx_data;
wire tx_finish;
wire uart_tx;
// 50MHz,9600bps
Send_Control my_Send_Control(clk, rst_n, tx_en, tx_data, tx_finish, uart_tx);
initial begin
clk=0;rst_n=0; tx_en=0; //复位信号不能太短。
#100 rst_n=1; tx_en=1; tx_data=8'b11001100;
# 100000000 $stop;
end
//50Mhz的时钟
always #10 clk=~clk;
endmodule
数据接收模块的时序如下所示:
// 公众号: 小鱼FPGA
// Engineer: littlefish
module BuadRate_set_multi #(
parameter CLK_Period=50000000,//the unit is Hz
parameter Buad_Rate=9600 //the unit is bits/s
)(
input clk, //原始系统时钟
input rst_n, //复位
input enable, //模块使能信号
output reg Buad_en, //输出的分频
output reg Cap_en
);
localparam DIV_PEREM=CLK_Period/Buad_Rate; //计数器最大值
localparam Cap_DIV_PEREM=DIV_PEREM/8; //计数器最大值
reg[15:0] cnt;
//波特率分频
always @( posedge clk )
if( !rst_n )
cnt <= 16'b0000;
else if( enable ) begin
if( cnt != DIV_PEREM )
cnt <= cnt+1'b1;
else
cnt <= 16'h0000;
end
else
cnt <= 16'h0000;
always @( posedge clk )
if( !rst_n || !enable ) //当复位或者模块不使能的时候,分频器停止。
Buad_en <= 1'b0;
else if(cnt==DIV_PEREM) //计数器满,输出时钟翻转
Buad_en <= 1;
else
Buad_en<=0;
//采集分频
reg[15:0] cap_cnt;
always @( posedge clk )
if( !rst_n )
cap_cnt <= 16'b0000;
else if( enable ) begin
if( cap_cnt < Cap_DIV_PEREM )
cap_cnt <= cap_cnt+1'b1;
else
cap_cnt <= 16'h0000;
end
else
cap_cnt <= 16'h0000;
always @( posedge clk )
if( !rst_n || !enable ) //当复位或者模块不使能的时候,分频器停止。
Cap_en <= 1'b0;
else if(cap_cnt==Cap_DIV_PEREM) //计数器满,输出时钟翻转
Cap_en <= 1;
else
Cap_en<=0;
endmodule
`timescale 1ns / 1ps
module receive_control_opt #(
parameter CLK_Period=50000000,//the unit is Hz
parameter Buad_Rate=9600 //the unit is bits/s
)(
input clk,
input rst_n,
input uart_rx,
output wire [7:0] rx_data,
output rx_finish,
output o_valid
);
localparam IDLE=2'b00;
localparam GET_DATA=2'b01;
localparam END=2'b11;
reg[5:0] get_start_bit;
reg start;
wire rx_start;
wire Buad_en;
wire Cap_en;
reg [8:0] rx_data_tmp; //8位数据位+1位停止位
//波特率采集时钟生成
BuadRate_set_multi #(
.CLK_Period(CLK_Period),//the unit is Hz
.Buad_Rate (Buad_Rate) //the unit is bits/s
) BuadRate_set_multi_inst(
.clk (clk ),
.rst_n (rst_n ),
.enable (rx_start ),
.Buad_en(Buad_en),
.Cap_en(Cap_en)
);
//启动低电平信号检测!
always @( posedge clk )
if( !rst_n )
get_start_bit <= 6'b111111;
else
get_start_bit <= {get_start_bit[4:0],uart_rx};
always @( posedge clk )
if( !rst_n )
start <= 1'b0;
else if(state !=IDLE) //至到状态state转为非IDLE状态时,start才转为0。
start <= 1'b0;
else if( get_start_bit==6'b111000 ) //有三个联系低电平0出现是,start==1。
start <= 1'b1;
//波特率产生模块启动信号:当未检测到起始位并且状态是IDLE空闲时,置无效。其他时刻为有效。
assign rx_start =( ~(start==1'b0 && state==IDLE) )? 1'b1:1'b0;
reg [1:0] state;
reg [3:0] cnt;
//接收模块状态机
always @( posedge clk or negedge rst_n ) //特定波特率采集时钟下
if( !rst_n )
state <= IDLE;
else
case( state )
IDLE: begin
if( start ) //检测到起始低电平信号,状态转为开始接收,并开始生成检测波特率时钟
state <= GET_DATA;
end
GET_DATA: begin //开始接收9个数据:1个起始位+8个数据位
if( cnt=='d9 )
state <= END;
end
END: begin //接收停止位
state <= IDLE;
end
default: begin
state <= IDLE;
end
endcase
reg [4:0] cap_tmp;
reg [3:0] cap_cnt;
//每bit数据采集7次,取7次的平均值作为该bit的数值
always @( posedge clk or negedge rst_n ) begin
if( !rst_n || Buad_en==1)
cap_tmp <= 5'd15;
else if( state == GET_DATA && Cap_en==1 ) begin
if(uart_rx==1)
cap_tmp<=cap_tmp+1;
else if(uart_rx==0)
cap_tmp<=cap_tmp-1;
end
end
always @( posedge clk or negedge rst_n ) begin
if( !rst_n )
cap_cnt <=0;
else if( state == GET_DATA && Cap_en==1 ) begin
if(cap_cnt<7)
cap_cnt<=cap_cnt+1;
else
cap_cnt<=0;
end
end
//接收9位有效数据:1个起始位+8个数据位 存放于rx_data_tmp
always @( posedge clk or negedge rst_n ) begin
if( !rst_n )
rx_data_tmp <= 9'b0;
else if( state == GET_DATA &&Buad_en==1) begin
if(cap_tmp>'d15)
rx_data_tmp <= {'b1,rx_data_tmp[8:1]};
else
rx_data_tmp <= {'b0,rx_data_tmp[8:1]};
end
end
//接收计数,接收9位有效数据:1个起始位+8个数据位,取其中的8位数据位即可。
always @( posedge clk or negedge rst_n) begin
if( !rst_n || state==IDLE ||state==END )
cnt <= 'd0;
else if(state==GET_DATA&&Buad_en==1 )
cnt <= cnt+1'b1;
end
assign rx_finish=(state==END);
reg finish_d;
always @( negedge clk or negedge rst_n )
if( !rst_n )
finish_d<=1'b0;
else
finish_d<=rx_finish;
assign o_valid = rx_finish&(~finish_d);
assign rx_data=(state==GET_DATA)?8'b0:rx_data_tmp[8:1];
endmodule
module tb_receive_opt();
reg clk;
reg rst_n;
reg uart_rx;
wire [7:0] rx_data;
wire rx_finish;
wire o_valid;
// 50MHz,9600bps
receive_control_opt my_Receive_Control(clk, rst_n, uart_rx, rx_data, rx_finish, o_valid);
initial begin
clk=0;rst_n=0; //复位信号不能太短。
#15 rst_n=1;uart_rx=1;
//1位起始位
#104166 uart_rx=0;
//8位数据:先发低位
#104166 uart_rx=1;
#104166 uart_rx=0;
#104166 uart_rx=1;
#104166 uart_rx=0;
#104166 uart_rx=1;
#104166 uart_rx=0;
#104166 uart_rx=1;
#104166 uart_rx=0;
//1位停止位
#104166 uart_rx=1;
#104166 uart_rx=1;
#104166 uart_rx=1;
#104166 uart_rx=1;
#104166 uart_rx=1;
end
//50Mhz的时钟
always #10 clk=~clk;
endmodule