对于学“电”的同学来说,一提到串口肯定不陌生,工业自动化领域普遍使用的RS485/RS232/RS422总线通信,或者电子DIY时使用的各种USB-TTL模块,串口通信简直就是电子世界的"普通话"——简单、通用、无处不在。

图
1 USB转TTL USB串口转换器但是你真的了解它的工作原理吗?你真的了解串口通信的底层协议吗?今天,我们将从零开始实现一个真正的UART通信系统,并且在FPGA上实现它!

UART:电子世界的通用语言

前面我们提到的串口通信,他们的底层接口指的都是UART接口,或者叫通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),通常称作UART,是一种通用串行数据总线,用于异步通信。该总线双向通信,可以实现全双工传输和接收。
在工业应用中存在多种串行通信方式,其中RS-232是一种最常用的串行数据通信接口标准之一,它在UART协议的基础上使用特殊的电平标准来表示逻辑“0”和逻辑“1” 。当电压差值在+3V ~ +15V之间表示逻辑“0”,当电压差值在-3V ~ -15V之间表示逻辑“1”。除此之外,还有其他电平标准的串行通信接口标准,比如使用差分电平来表示逻辑信号的RS485、RS422等。他们本质上使用的还是UART协议,只是传输时所使用的电气标准不一样而已。现在的商用计算机或笔记本电脑上已经不再集成RS232接口了,但是大量的工业自动化设备、智能仪器仪表或嵌入式设备还在使用RS232接口作为设备与设备之间的远距离通信。
串行通信的两端设备一般使用一个叫做DB9的硬件接口,如图4所示。这个串行通信端口有9个引脚,其中引脚 2 (RX)和引脚 3 (TX)可以直接连接到另一个设备的RS232端口进行串行通信,因此DB9连接器分为公头(引脚是针)和母座(引脚是孔),如图2所示,有的串行传输线的两端往往是一公一母,分别连接两端设备的母座和公头。

图2 DB9连接器示意图

图3 DB9接口的串行连接线
我们今天使用的大多数计算机不再提供这种RS232连接器,我们电脑现在常用的串行通信接口是USB,因此如果我们想在硬件设备和计算机之间建立RS232串行通信,那么必须使用一个叫做USB转TTL的USB串口转换器,如图4所示。该硬件的核心是一颗USB-UART协议转换芯片,比如CP2102、CH340、FT232等。

图4 USB转TTL USB串口转换器
USB串口转换器是一个转换桥,将电脑的USB接口和设备的UART接口连接在一起,并且在电脑端模拟一个串行通信设备。当我们使用USB数据线连接USB转TTL 串口转换器和电脑时,在电脑的设备管理器,端口一栏将显示一个USB串行设备并分配COM端口编号,如图5所示。

图5在设备管理器端口一栏下显示的串行通信设备

什么是UART通信?

UART(通用异步收发传输器)是一种广泛使用的串行通信协议,用于在设备之间传输数据。它在工业自动化、嵌入式系统和电子DIY项目中都有广泛应用。

UART的基本原理

UART通信是异步通信,这意味着发送端和接收端不需要共享时钟信号。相反,它们通过约定的波特率(每秒传输的位数)来同步数据传输。

UART的数据帧格式

一个标准的UART数据帧包含以下部分:
- 起始位:一个逻辑低电平(0),表示数据帧的开始。
- 数据位:通常为8位,表示实际传输的数据。
- 可选的奇偶校验位:用于错误检测。
- 停止位:一个或多个逻辑高电平(1),表示数据帧的结束。

UART通信协议

在硬件层面,两个设备A和B想要在 UART协议中“交谈”,每个设备需要两个GPIO,TX用于发送和RX用于接收,并按照图6所示的方式连接。

图6 为两个硬件建立物理UART连接
UART是异步传输,尽管没有同步的时钟线,但是为了“在一个频道上说话”,两边总要约定好“沟通的语言”,也就是通信协议。在UART通信中,数据以字节为单位发送,这意味着每次有8位数据依次从设备A的TX引脚传输到设备B的RX引脚,为了确保接收端能正确地接收数据,在每个字节的两端添加了逻辑0(低电平)的起始位和逻辑1(高电平)的停止位。我们也可以在MSB(最高有效位)后添加奇偶校验,这是可选的。发送1字节数据的协议如图7所示。

图7 从TX线发送1字节的数据格式
两端“说话的语速”也就是数据传输的速度是以单位时间内的波特数为单位来衡量的,有时称为波特率,定义为1秒内数据通过传输线时符号数变化的速率。在UART通信中,我们可以将波特率视为在1秒内传输的位数。只要通信的两端约定好了波特率,不管波特率多少,都是可以沟通的,但是为了减少沟通的成本,我们还是要约定几种标准的波特率。如表1所示,列出了一些标准的波特率及位传输时间。第三列是Verilog 设计的分频参数,我们稍后会提到。
表1 在不同的波特下发送每个比特的时间
标准波特率 | 位持续时间(ms) | 分频系数BPS_PARA(基准12MHz) |
1200 | 8.33 | 10000 |
2400 | 4.16 | 5000 |
4800 | 2.08 | 2500 |
9600 | 1.04 | 1250 |
19200 | 0.52 | 625 |
38400 | 0.26 | 313 |
57600 | 0.176 | 208 |
115200 | 0.0868 | 104 |
要注意的是,波特率和比特率不是同一个概念,两者经常混淆,后者的单位是位每秒(bps)。在UART中,每个符号由1位数据表示,因此UART中的波特率与比特率相同。然而,电信中的一些其他编码机制使用多个比特来表示一个符号。例如,在曼彻斯特编码中,每个符号携带2位信息,因此10波特率在曼彻斯特编码中意味着20bps。举个例子,我们将展示一个完整的数据传输过程,其中一个字节(10111100)2从硬件A发送到硬件B,整个过程分三步进行。
第一步:发射端在输入8位数据tx_data,这是8位并行输入的,如图8所示。

图8 UART通信演示-数据发送
第二步:添加起始位和结束位(奇偶校验位可选),并通过TX线发送位流,数据以顺序的方式从硬件A发送到硬件B,如下图9所示。

图9 UART通信演示-数据传输
第三步:接收端将传输线上的一帧数据去掉起始位和结束位,将比特流转换为rx_data上的8位并行数据,如图10所示。

图10 UART通信演示-数据接收

FPGA实现UART

如果你之前有使用过STM32一类的单片机或Arduino、Raspberry Pi等开源硬件的经验,那么使用串口调试助手的小软件,将硬件微控制器的字符发送到计算机上打印出来,这种操作对你来说肯定很简单。但是我们现在要说的是如何设计一个UART通信模块并让它跑起来呢?生产一块芯片肯定是不现实的,最容易的方式就是在FPGA上来实现你的想法。
相比使用现成的串口芯片,用FPGA实现UART其实是非常有意思的事情:
- 精确控制时序:可以精确控制每个比特的传输时序。
- 灵活定制协议:可以根据需要修改数据位、停止位和校验方式。
- 深度理解原理:通过从底层实现,可以更深入地理解串口通信机制。
- 系统集成能力:可以轻松与其他数字逻辑模块集成。
这种掌控全局,一切自己来实现的快感不正是我们工程师想要的吗。
下面准备工作:
一块FPGA开发板(FPGA厂商无所谓,如果是新手建议选好上手的,比如小脚丫FPGA开发板

图11小脚丫FPGA开发板
一台电脑,一根数据线。
一个USB-TTL串口转换模块(在电脑上显示字符时需要,如果只是串口通信可以不需要)。
一本《入门FPGA数字电路设计的奇妙之旅》,接地气的新手友好型FPGA入门书籍。一本介绍Verilog HDL的语法书。
接下来我们分步骤介绍实现过程。

手搓UART核心:从理论到实践

1. 波特率生成器 - 通信的"心跳"
精确的波特率是UART通信的基础,通过FPGA内部计数器实现:
首先,我们需要一个生成不同标准波特率的波特率分频模块。波特率分频模块的基本定义如图12所示。要设置不同的波特率,请根据表1更改其他标准波特的参数BPS_PARA。

图12 波特率分频模块定义
module Baud
input clk, rst_n,
input bps_en, // Connects to bps_en on UART_Tx
output reg bps_clk // Connects to bps_clk on UART_Tx
);
reg [12:0] cnt;
always @ (posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 1'b0;
else if((cnt >= BPS_PARA-1)||(!bps_en)) // if bps_en is low, stop Bauds
cnt <= 1'b0;
else
cnt <= cnt + 1'b1;
end
// Setup for different Bauds according to the parameter given
always @ (posedge clk or negedge rst_n)
begin
if(!rst_n)
bps_clk <= 1'b0;
else if(cnt == (BPS_PARA>>1)) // Use the middle point for sampling, see Figure 5.6.x
bps_clk <= 1'b1;
else
bps_clk <= 1'b0;
end
endmodule
我们还需要一个模块,它以字节为单位接收数据,然后按顺序一位一位的输出数据。UART_Tx模块的结构如图13所示,连续地输入8位的dataByte,并通过输出线tx依次发送每一位。

图13 UART_Tx的模块定义
module Uart_Tx (
input clk,rst_n,
input bps_clk, // 连接到 BaudGen 模块的 bps_clk
output reg bps_en, // 连接到 BaudGen 模块的 bps_en
input tx_en, // 发生使能位
input [7:0] tx_data,// 待发送数据
output reg tx // 串行输出信号
);
图14显示了UART_Send模块的完整结构。该模块接受8位输入数据 dataByte(在这里我们可以设置一个模拟的输入数据,比如一个固定的8位数据0x55),并通过tx线以特定的波特率(默认为9600)顺序发送出去。接下来,我们将把这个模块rs32_tx连接到USB-TTL串口转换模块上,并通过COM端口观察串口转换模块输出数据。

图14通过UART发送数据的数字模块结构
2. UART接收器 - 数据的"解译者"
先对RX信号多级缓存消除亚稳态,同时检测下降沿,程序实现如下:
input uart_rx, //UART接收输入
reg uart_rx0,uart_rx1,uart_rx2;
//多级延时锁存去除亚稳态
always @ (posedge clk) begin
uart_rx0 <= uart_rx;
uart_rx1 <= uart_rx0;
uart_rx2 <= uart_rx1;
end
//检测UART接收输入信号的下降沿
wire neg_uart_rx = uart_rx2 & ~uart_rx1;
当检测RX有下降沿后,使能节拍使能信号,同时自锁直到完成接收操作后再复位节拍使能信号。程序实现如下:
//接收时钟使能信号的控制
always @ (posedge clk or negedge rst_n) begin
if(!rst_n)
bps_en <= 1'b0;
else if(neg_uart_rx && (!bps_en)) //当检测到传输,使能节拍使能信号
bps_en <= 1'b1;
else if(num==4'd9) //完成UART接收操作,复位节拍使能信号
bps_en <= 1'b0;
end
根据节拍信号完成UART总线的数据采样,得到8位有效数据,程序实现如下:
reg [7:0] rx_data;
//当处于工作状态中时,按照接收时钟的节拍获取数据
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
num <= 4'd0;
rx_data <= 8'd0;
end else if(bps_en) begin
if(bps_clk) begin
num <= num + 1'b1;
if(num<=4'd8) rx_data[num-1] <= uart_rx1; //先低位后高位
end else if(num == 4'd9) begin
num <= 4'd0;
end
end else begin
num <= 4'd0;
end
end
当UART接收操作完成后,将得到的8位有效数据输出给后级电路,程序实现如下:
//将接收的数据输出,同时控制输出有效信号产生脉冲
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
rx_data_out <= 8'd0;
rx_data_valid <= 1'b0;
end else if(num == 4'd9) begin
rx_data_out <= rx_data;
rx_data_valid <= 1'b1;
end else begin
rx_data_out <= rx_data_out;
rx_data_valid <= 1'b0;
end
end
最后将节拍模块Baud和接收模块Uart_rx实例化并连接,完成发送功能的设计,如图15:

图15 通过UART接收数据的数字模块结构
整个UART驱动设计是由两个独立的功能组合而成:发送功能部分和接收功能部分。UART功能总体设计框图如下图16所示:

图16 UART功能总体设计框图
当我们需要UART发送数据的时候只需要实例化发送功能部分设计,需要UART接收数据的时候只需要实例化接收功能部分设计,例如本设计中FPGA驱动UART模块接收电脑串口调试助手发出的数据,所以我们就只需要实例化接收功能部分设计即可。

电子工程师的终极浪漫

当我们用Verilog从零构建UART收发器时,才真正理解了:
为什么起始位是低电平?
设置不同的波特率时,在芯片内部发生了什么?
没有时钟同步,信号是如何接收的?
“造轮子”不是重复劳动,而是对本质的追问。
