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帧结构,你可以直观地看到每个位的位置:

UART 数据帧结构 (8N1) 起始位 (低电平) Bit 0 (LSB) Bit 1 Bit 2 Bit 3 Bit 4 Bit 5 Bit 6 Bit 7 (MSB) 停止位 (高电平) 空闲状态:TX线保持高电平 数据发送方向(LSB → MSB)

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串口驱动分为三层:

  1. 应用层:用户空间的程序,通过tty设备节点与硬件交互。
  2. tty核心层:Linux内核的tty子系统,提供统一的接口规范。
  3. UART驱动层:具体硬件相关的驱动,操作UART控制器的寄存器。

下面这张图,展示了Linux串口驱动的整体架构:

Linux 串口驱动框架 应用层 (User Space) 应用程序:open/read/write/ioctl tty 核心层 (tty Core) tty驱动注册、线路规程、数据调度 UART 驱动层 (uart_ops) startup/shutdown/set_termios/start_tx 硬件层 (Hardware) UART控制器寄存器、中断控制器、DMA 分层说明 用户态 ↓ 系统调用 ↓ 内核态 ↓ 硬件抽象 ↓ 寄存器

写一个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这个老朋友彻底吃透。