串行通信学习:UART通信电路&串行通信原理

Posted on 2023-01-20 22:59  _Chapman  阅读(1301)  评论(0编辑  收藏  举报

串行通信&并行通信区别

串行通信(典型:UART通信)指的是,数据通过一条信号线,把数据一bit一bit地传送出去。并行通信(典型:后背是牛屎芯片的LCD1602屏幕)指的是,数据通过一排线,每个时刻传输一个数据传出去。
image
上图为并行通信
image
上图为串行通信
显然,若发送一位8bit数据,串行通信用时是并行的八倍。明显地,并行通信速率比串行通信更快,但同时占用资源更多。

串行通信原理

串行同步通信

串行通信分为两种:异步串行通信和同步串行通信。同步通信指的是通信设备之间有共同的时钟线,异步通信指的是通信设备之间没有共同时钟线。对于传输精度要求高的场景下,同步通信要比异步通信好(异步通信双方波特率都是尽可能详尽,因此有一定几率出现传输数据错误情况)。

串行异步通信

异步通信在没有共同时钟线的条件下,通过约定发送数据的速率实现通信,这个约定速率就是————波特率。

例如波特率115200————指的是数据传输速率为1s传输115200个bit数据。

则发送1bit数据用时:
1/115200秒
若机器的时钟频率为:50MHz,20ns。
则发送1bit数据用去:1s/(115200*20ns)≈434个时钟周期

只要通信双方约定了波特率,即使两边时钟频率不一致,都可以进行通信,只是两边传输1bit数据用去的时钟周期个数不同而已。

串行异步通信协议

下图是串行异步通信的协议简图。可以看出:
1、空闲时,数据线处于高电平。
2、其中一方开始发送时候,会线拉低电平一个bit时间(起始位)
3、然后从LSB发送到MSB,一般地这里会发送8个bit(这个很常用)。
4、然后再发送一个校验位,这里是奇偶校验。

关于奇校验:
假如数据从起始位到校验位为:
0 1001 1000 0
因为LSB-》MSB有3个“1”,数量为奇数,校验位给0。如果校验位给了1,这一包数据可以都丢了,因为出错了。
假如数据从起始位到校验位为:
0 1001 1010 1
因为LSB-》MSB有4个“1”,数量为偶数,校验位给1。如果校验位给了0,这一包数据可以都丢了,因为出错了。

关于偶校验:
假如数据从起始位到校验位为:
0 1001 1000 0
因为LSB-》MSB有3个“1”,数量为奇数,校验位给1。如果校验位给了0,这一包数据可以都丢了,因为出错了。
假如数据从起始位到校验位为:
0 1001 1010 1
因为LSB-》MSB有4个“1”,数量为偶数,校验位给0。如果校验位给了1,这一包数据可以都丢了,因为出错了。

5、最后是停止位,拉高1个bit时间。
6、回到空闲状态。

image

关于LPUART

这一种通信方式,一般应用在低功耗设备。这一种通信方式和UART其实没什么区别。唯一区别就是空闲时间为低电平(因为要减少电量消耗),然后就是起始位拉高1个bit时间。

关于LIN通信

我是从一个做过汽车电子的老前辈(年轻的前辈以前在日本工作过,在丰田里面搞电车的充电桩)那里学来的,我觉得这个是串行通信的黑科技版本。这种通信方式是,半双工通信方式,而且把电源线和数据线合成一条线使用。和UART一样,空闲时候,数据线为高电平,同时主机给从机供电(其实更像是CAN)。 一般我们作电路板,肯定给电源加个滤波电容,这个电容能存储的电量绝对给板子掉电之后撑个1s时间没什么问题的。(对于接收低电平信号的时候不会出现没电死机的情况) 那么,这个通信方式是这样的:
1、有数据来的时候,会有一个报头【包括间隔场、同步场(可以间隔性给从机充电)、标识符场(里面一般包括地址内容,也可以间隔性给从机充电)】
2、后面就是发送数据位
3、最后是检验场

发送完毕之后,拉高信号线给从机供电。这种通信方式可以省掉一条线的成本,但是通信速率有点低。
image

在FPGA里面实现UART

接收时候

如下图所示,UART接收时候,从机端通信简图如下:
image

在这里,start_flat是用来标记出出现下降沿的状态,用来标记接收状态rx_flat的信号。clk_cnt是用来数一个bit里面跑到了第几个时钟周期的寄存器。rx_cnt是用来数接收到了第几个bit。uart_done是用来标记接收是否完成状态,用于告诉内部电路,uart这部分电路的接收工作完成了,防止在工作的时候被打断,直到空闲的时候拉低。

代码如下:

module uart_recv(
input sys_clk, //系统时钟
input sys_rst_n, //系统复位,低电平有效
input uart_rxd, //UART 接收端口

output reg uart_done, //接收一帧数据完成标志,同于内部线路使用。
output reg rx_flag, //接收过程标志信号,同于内部线路使用。
output reg [3:0] rx_cnt,//接收数据计数器,同于内部线路使用。
output reg [7:0] rxdata,//存放到寄存器的接口,同于内部线路使用。
output reg [7:0] uart_data //接收的数据
);

//parameter define
parameter CLK_FREQ = 50000000; //系统时钟频率
parameter UART_BPS = 9600; //串口波特率
localparam BPS_CNT = CLK_FREQ/UART_BPS;
//为得到指定波特率,需要对系统时钟计数 BPS_CNT 次
//reg define
reg uart_rxd_d0;//用于双寄存器,相当于缓冲
reg uart_rxd_d1;//用于双寄存器,相当于缓冲
//这个寄存器的缓冲数据主要是用来捕抓起始位的下降沿
reg [15:0] clk_cnt; //系统时钟计数

wire start_flag;

//捕获接收端口下降沿(起始位),得到一个时钟周期的脉冲信号
assign start_flag = uart_rxd_d1 & (~uart_rxd_d0);

//对 UART 接收端口的数据延迟两个时钟周期(不停采集,顺便把下降沿也抓了)
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
uart_rxd_d0 <= 1'b0;
uart_rxd_d1 <= 1'b0;
end
else begin
uart_rxd_d0 <= uart_rxd;
uart_rxd_d1 <= uart_rxd_d0;
end
end

//当脉冲信号 start_flag 到达时,进入接收过程
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
rx_flag <= 1'b0;
else begin
if(start_flag) //检测到起始位
rx_flag <= 1'b1; //进入接收过程,标志位 rx_flag 拉高
//计数到停止位中间时,停止接收过程
else if((rx_cnt == 4'd9) && (clk_cnt == BPS_CNT/2))
rx_flag <= 1'b0; //接收过程结束,标志位 rx_flag 拉低
else
rx_flag <= rx_flag;
end
end
/*
备注:刚开始的时候,起始位下降沿抓了之后start_flag确实被置1。但是后面数据传输一定会出现上降沿,同时也会被抓。使得start_flag变成0。但是在这一个always里面,当start_fla≠1时,rx_flag不会改变。要在接收bit位到了9的时候才会变成1。
*/

/*
进入接收过程后,启动系统时钟计数器,这个开始数一个bit时间中的时钟周期个数
*/
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
clk_cnt <= 16'd0;
else if ( rx_flag ) begin //处于接收过程
if (clk_cnt < BPS_CNT - 1)
clk_cnt <= clk_cnt + 1'b1;
else
clk_cnt <= 16'd0; //对系统时钟计数达一个波特率周期后清零
end

/*
根据接收数据计数器来寄存 uart 接收端口数据
这里都是数到每个bit一半时间的时候才采集数据
因为这个时候数据肯定稳定,如果不稳定,就是你的电路板硬件设计绝对有问题,有严重的电磁干扰,没办法了,重新画板子。多看看高速电路PCB设计的书,这里不展开讨论。
*/
always @(posedge sys_clk or negedge sys_rst_n) begin
if ( !sys_rst_n)
rxdata <= 8'd0;
else if(rx_flag) //系统处于接收过程
if (clk_cnt == BPS_CNT/2) begin
//判断系统时钟计数器计数到数据位中间,就可以采集数据
case ( rx_cnt )
4'd1 : rxdata[0] <= uart_rxd_d1; //寄存数据位最低位
4'd2 : rxdata[1] <= uart_rxd_d1;
4'd3 : rxdata[2] <= uart_rxd_d1;
4'd4 : rxdata[3] <= uart_rxd_d1;
4'd5 : rxdata[4] <= uart_rxd_d1;
4'd6 : rxdata[5] <= uart_rxd_d1;
4'd7 : rxdata[6] <= uart_rxd_d1;
4'd8 : rxdata[7] <= uart_rxd_d1; //寄存数据位最高位
default:;
endcase
end
else
rxdata <= rxdata;
else
rxdata <= 8'd0;
end

//数据接收完毕后给出标志信号并寄存输出接收到的数据
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
uart_data <= 8'd0;
uart_done <= 1'b0;
end
else if(rx_cnt == 4'd9) begin //接收数据计数器计数到停止位时
uart_data <= rxdata; //寄存输出接收到的数据
uart_done <= 1'b1; //并将接收完成标志位拉高
end
else begin
uart_data <= 8'd0;
uart_done <= 1'b0;
end
end

endmodule

发送时候

过程和接收是倒着来的,这里就不罗嗦了,因为懂了接收,发送也就懂了。顶层模块是吧发送和接收例化变成一个模块的,形成一个回环。

代码如下:
module uart_send(
input sys_clk, //系统时钟
input sys_rst_n, //系统复位,低电平有效
input uart_en, //发送使能信号
input [ 7:0] uart_din, //待发送数据
output uart_tx_busy, //发送忙状态标志
output en_flag,
output reg tx_flag,//发送过程标志信号
output reg [ 7:0] tx_data,//寄存发送数据
output reg [ 3:0] tx_cnt, //发送数据计数器
output reg uart_txd //UART发送端口
);

//parameter define
parameter CLK_FREQ = 50000000; //系统时钟频率
parameter UART_BPS = 9600; //串口波特率
localparam BPS_CNT = CLK_FREQ/UART_BPS;
//为得到指定波特率,对系统时钟计数BPS_CNT次

//reg define
reg uart_en_d0;
reg uart_en_d1;
reg [15:0] clk_cnt;
//系统时钟计数器

//在串口发送过程中给出忙状态标志
assign uart_tx_busy = tx_flag;

//捕获uart_en(发送数据从这里来的)上升沿,
//得到一个时钟周期的脉冲信号
//这个和接收一样,要弄一个flat出来告诉其他电路我在发送数据
assign en_flag = (~uart_en_d1) & uart_en_d0;

//对发送使能信号uart_en延迟两个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
uart_en_d0 <= 1'b0;
uart_en_d1 <= 1'b0;
end
else begin
uart_en_d0 <= uart_en;
uart_en_d1 <= uart_en_d0;
end
end

//当脉冲信号en_flag到达时,寄存待发送的数据,并进入发送过程
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
tx_flag <= 1'b0;
tx_data <= 8'd0;
end
else if (en_flag) begin//检测到发送使能上升沿
tx_flag <= 1'b1;
//进入发送过程,标志位tx_flag拉高
tx_data <= uart_din;//寄存待发送的数据
end
//计数到停止位结束时,停止发送过程
else if ((tx_cnt == 4'd9) && (clk_cnt == BPS_CNT - (BPS_CNT/16))) begin
tx_flag <= 1'b0;
//发送过程结束,标志位tx_flag拉低
tx_data <= 8'd0;
end
else begin
tx_flag <= tx_flag;
tx_data <= tx_data;
end
end

//进入发送过程后,启动系统时钟计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
clk_cnt <= 16'd0;
else if (tx_flag) begin //处于发送过程
if (clk_cnt < BPS_CNT - 1)
clk_cnt <= clk_cnt + 1'b1;
else
clk_cnt <= 16'd0;
//对系统时钟计数达一个波特率周期后清零
end
else
clk_cnt <= 16'd0;
//发送过程结束
end

//进入发送过程后,启动发送数据计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
tx_cnt <= 4'd0;
else if (tx_flag) begin //处于发送过程
if (clk_cnt == BPS_CNT - 1)
//对系统时钟计数达一个波特率周期,bit的个数加一
tx_cnt <= tx_cnt + 1'b1;
else
tx_cnt <= tx_cnt;
end
else
tx_cnt <= 4'd0;//发送过程结束
end

//根据发送数据计数器来给uart发送端口赋值
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
uart_txd <= 1'b1;
else if (tx_flag)
case(tx_cnt)
4'd0: uart_txd <= 1'b0; //起始位
4'd1: uart_txd <= tx_data[0]; //数据位最低位
4'd2: uart_txd <= tx_data[1];
4'd3: uart_txd <= tx_data[2];
4'd4: uart_txd <= tx_data[3];
4'd5: uart_txd <= tx_data[4];
4'd6: uart_txd <= tx_data[5];
4'd7: uart_txd <= tx_data[6];
4'd8: uart_txd <= tx_data[7]; //数据位最高位
4'd9: uart_txd <= 1'b1; //停止位
default: ;
endcase
else
uart_txd <= 1'b1;//空闲时发送端口为高电平
end

endmodule

回环电路图如下:
image