3. UART协议详解:异步串行通信原理、波特率、帧格式、流控及Linux串口驱动框架
UART,全称是Universal Asynchronous Receiver/Transmitter。说白了,就是通用异步收发器。这玩意儿在嵌入式世界里,可以说是最基础、最常用的通信接口了。我刚开始做嵌入式那会儿,第一个调通的硬件外设就是UART,用它打印“Hello World”的感觉,到现在还记得。
它为什么这么普及?因为简单。两根线(TX和RX)就能实现全双工通信,不需要时钟线,成本极低。你想想看,从调试控制台到GPS模块,从蓝牙芯片到传感器,几乎每个嵌入式设备都会留一个UART口。今天我们就把它彻底讲透。
3.1 异步串行通信原理
UART是异步的,意味着收发双方没有共享的时钟信号。那它们怎么同步呢?靠的是双方事先约定好的“波特率”。
发送端把并行数据(比如一个字节)转换成串行的比特流,逐位发送。接收端则在约定的时间点上采样电平,再把串行数据拼回并行数据。这个过程,我习惯叫它“串并转换”。
核心要点:UART通信的物理层就是两根线——TX(发送)和RX(接收)。设备A的TX连设备B的RX,设备A的RX连设备B的TX。这就是交叉连接,也叫“Null Modem”接法。
这里有个坑,我刚开始做项目时踩过。有一次调试两块板子通信,死活收不到数据。查了半天,发现是TX和RX接反了。嗯,这种低级错误,相信不少人都犯过。
3.2 波特率
波特率,就是每秒传输的符号数。对于UART来说,一个符号就是一个比特,所以波特率就等于比特率。常见的波特率有:9600、19200、38400、115200、921600等。
为什么是这些数字?因为它们都是从1.8432MHz晶振分频得来的整数倍。这个历史原因,我们不用深究,记住常用值就行。
| 波特率 (bps) | 每比特时间 (μs) | 传输1字节耗时 (ms) | 典型应用场景 |
|---|---|---|---|
| 9600 | 104.17 | 约1.04 | 调试控制台、低速传感器 |
| 115200 | 8.68 | 约0.087 | GPS模块、蓝牙模块 |
| 921600 | 1.09 | 约0.011 | 高速数据传输、固件升级 |
我的经验:调试阶段建议先用9600或115200。这两个波特率最稳定,兼容性最好。等通信调通了,再根据实际需求提高波特率。我曾经在一个项目中直接上921600,结果线缆长了点,误码率飙升,排查了很久才发现是信号完整性问题。
3.3 帧格式
UART的数据帧,就像一列火车。有车头、车厢和车尾。标准的UART帧格式包含:
- 起始位(1位):逻辑低电平,告诉接收端“我要开始发数据了”。
- 数据位(5~9位):通常是8位,也就是一个字节。LSB(最低有效位)先发。
- 校验位(可选,0或1位):奇校验或偶校验,用于简单的错误检测。
- 停止位(1、1.5或2位):逻辑高电平,表示帧结束。
最常见的配置是:8个数据位、无校验、1个停止位,简写为8N1。你几乎在所有串口配置里都能看到这个组合。
注意:收发双方的帧格式必须完全一致。数据位、校验位、停止位,任何一个不匹配,通信就会失败。我曾经帮同事排查过一个Bug,他那边配置的是7E1(7数据位、偶校验、1停止位),而设备默认是8N1,结果数据全是乱码。
下面这张图,是我手绘的UART帧结构,你可以直观地看到每个位的位置:
3.4 流控
流控,就是控制数据流的“阀门”。当接收端来不及处理数据时,告诉发送端“你先停一下”。UART的流控有两种:
- 硬件流控(RTS/CTS):用额外的两根线。RTS(请求发送)和CTS(清除发送)。接收端准备好接收时,拉高CTS;缓冲区满了,拉低CTS。发送端看到CTS为低,就停止发送。
- 软件流控(XON/XOFF):用特殊字符。接收端发送0x13(XOFF)让发送端暂停,发送0x11(XON)让发送端继续。这种方式不需要额外引脚,但只适用于文本数据传输。
我建议:如果硬件支持,优先用硬件流控。它更可靠,不会把控制字符和数据混淆。我在做工业控制项目时,遇到过因为软件流控字符和数据内容冲突导致通信卡死的案例,后来换成硬件流控就再没出过问题。
3.5 Linux串口驱动框架
在Linux下,UART驱动被抽象成了tty设备。你看到的/dev/ttyS0、/dev/ttyAMA0、/dev/ttyUSB0,都是串口设备节点。应用程序通过标准的文件操作接口(open、read、write、ioctl)来操作它们。
Linux串口驱动分为三层:
- 应用层:用户空间的程序,通过tty设备节点与硬件交互。
- tty核心层:Linux内核的tty子系统,提供统一的接口规范。
- UART驱动层:具体硬件相关的驱动,操作UART控制器的寄存器。
下面这张图,展示了Linux串口驱动的整体架构:
写一个UART驱动,核心是实现struct uart_ops结构体中的回调函数。最重要的几个:
startup:初始化UART硬件,申请中断。shutdown:关闭UART,释放资源。set_termios:设置波特率、数据位、校验位、停止位等参数。start_tx:启动发送,把数据写入发送寄存器。stop_tx:停止发送。
下面是一个简化的UART驱动初始化代码片段,展示了注册流程:
static struct uart_driver my_uart_driver = {
.owner = THIS_MODULE,
.driver_name = "my_uart",
.dev_name = "ttyMY",
.major = TTY_MAJOR,
.minor = 64,
.nr = 1,
};
static struct uart_port my_uart_port = {
.lock = __SPIN_LOCK_UNLOCKED(my_uart_port.lock),
.type = PORT_16550,
.iotype = UPIO_MEM,
.mapbase = 0x10000000, // 寄存器基地址
.irq = 42, // 中断号
.ops = &my_uart_ops,
.fifosize = 16,
};
static int __init my_uart_init(void)
{
int ret;
ret = uart_register_driver(&my_uart_driver);
if (ret)
return ret;
ret = uart_add_one_port(&my_uart_driver, &my_uart_port);
if (ret)
uart_unregister_driver(&my_uart_driver);
return ret;
}
避坑指南:我曾经在写驱动时,忘记在set_termios里更新波特率分频系数。结果应用层用tcsetattr设置了115200,硬件实际跑的却是9600。数据能通,但全是乱码。排查了整整一天,才发现是驱动里没有把新配置写入寄存器。记住:set_termios里一定要把配置真正写到硬件里。
应用层配置串口,通常用termios结构体。下面是一个典型的配置流程:
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
int setup_serial(int fd, int baudrate)
{
struct termios tty;
// 读取当前配置
tcgetattr(fd, &tty);
// 设置波特率
cfsetispeed(&tty, baudrate);
cfsetospeed(&tty, baudrate);
// 8N1 模式
tty.c_cflag &= ~PARENB; // 无校验
tty.c_cflag &= ~CSTOPB; // 1位停止位
tty.c_cflag &= ~CSIZE; // 清除数据位设置
tty.c_cflag |= CS8; // 8位数据位
// 启用接收,忽略调制解调器控制线
tty.c_cflag |= CREAD | CLOCAL;
// 禁用硬件流控
tty.c_cflag &= ~CRTSCTS;
// 原始模式输入
tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
tty.c_iflag &= ~(IXON | IXOFF | IXANY);
tty.c_oflag &= ~OPOST;
// 设置读取超时
tty.c_cc[VMIN] = 0;
tty.c_cc[VTIME] = 10; // 1秒超时
// 应用配置
tcsetattr(fd, TCSANOW, &tty);
return 0;
}
我的习惯:每次配置完串口,我都会用tcgetattr再读回来检查一遍,确认配置确实生效了。有些驱动实现有Bug,配置了但没写进去。多检查一步,能省很多调试时间。
UART看似简单,但真正用好它,需要理解从物理层到应用层的每一层细节。波特率匹配、帧格式一致、流控选择、驱动实现,任何一个环节出问题,通信就会失败。希望今天的内容,能帮你把UART这个老朋友彻底吃透。