4. UART驱动与串口通信:UART硬件原理、Linux串口驱动框架、tty驱动注册、波特率/数据位/校验位配置

大家好,我是你们的讲师。今天我们来聊聊UART驱动与串口通信。说实话,串口在POS机外设里太常见了——打印机、密码键盘、扫码枪,十有八九都走串口。我刚开始做驱动时,觉得串口嘛,不就是收收发发,能有多复杂?结果第一次调一个高速打印机,丢数据丢到怀疑人生。嗯,从那以后,我再也不敢小看UART了。

4.1 UART硬件原理:从物理层说起

UART,全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。说白了,它就是个串并转换器。CPU发过来的是并行数据,UART把它转成串行,一根线发出去;接收端再转回并行,交给CPU。

为什么叫异步?因为没有独立的时钟线。收发双方靠约定的波特率来同步。你想想看,两个人约好每秒说多少个字,一个说一个听,节奏对了才能沟通。

UART的物理层就两根数据线:TX(发送)和RX(接收)。再加上地线GND,三根线就能干活。我见过不少新手把TX和RX接反了,结果死活不通。嗯,这里要注意:TX接对方的RX,RX接对方的TX,交叉连接。

数据帧的结构是这样的:

  • 起始位:1位,逻辑0,表示开始传输
  • 数据位:5~8位,通常是8位,一个字节
  • 校验位:可选,奇校验或偶校验
  • 停止位:1位或2位,逻辑1,表示传输结束

举个例子,8N1表示8位数据、无校验、1位停止位。这是串口通信最常用的配置。我在项目中遇到过一台老式打印机,非要7E1(7位数据、偶校验、1位停止位),折腾了半天才发现是协议不匹配。

核心要点:UART通信的关键参数就是波特率、数据位、校验位、停止位。收发双方必须完全一致,否则就是乱码或不通。

4.2 Linux串口驱动框架:三层结构

Linux的串口驱动,我习惯把它理解成三层结构:

  • tty核心层:负责tty设备的注册、文件操作接口、线路规程
  • tty驱动层:提供统一的ops接口,比如open、close、write、read
  • UART驱动层:直接操作硬件寄存器,处理中断、DMA等

为什么会这样分层?说白了,就是为了复用。不同的串口硬件(16550、PL011、IMX UART等),只要实现了底层的UART ops,上层tty驱动几乎不用改。我做过一个项目,从IMX6换到RK3288,底层UART驱动重写,上层应用代码一行没动。

Linux内核里,UART驱动层通过struct uart_portstruct uart_ops来抽象硬件。uart_ops里最重要的回调函数有:

  • startup:启动串口,申请中断、初始化硬件
  • shutdown:关闭串口,释放资源
  • set_termios:配置波特率、数据位、校验位等
  • start_tx:启动发送
  • stop_tx:停止发送

我记得第一次写UART驱动时,set_termios这个函数让我头疼了好几天。因为波特率计算涉及到时钟分频,不同芯片的算法还不一样。

4.3 tty驱动注册:让设备节点出现

tty驱动注册,说白了就是让系统里出现/dev/ttyS0/dev/ttyS1这样的设备节点。用户程序才能通过open、read、write来操作串口。

注册流程大致如下:

  1. 分配并初始化struct uart_driver
  2. 调用uart_register_driver注册驱动
  3. 分配并初始化struct uart_port
  4. 调用uart_add_one_port添加端口

代码示例:

static struct uart_driver my_uart_driver = {
    .owner          = THIS_MODULE,
    .driver_name    = "my_uart",
    .dev_name       = "ttyMY",
    .major          = TTY_MAJOR,
    .minor          = 64,
    .nr             = 2,  // 支持2个端口
};

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;
}

注册成功后,系统里就会出现/dev/ttyMY0/dev/ttyMY1。嗯,这里要注意:dev_name字段决定了设备节点的名字前缀。

个人经验:调试时可以用ls -l /dev/tty*查看设备节点是否生成。如果节点没出现,多半是注册流程出了问题,或者设备树里没有使能对应的UART。

4.4 波特率/数据位/校验位配置:set_termios详解

配置串口参数,核心就是set_termios回调函数。用户程序通过tcsetattr系统调用,最终会触发这个函数。

我习惯把配置流程拆成三步:

  1. 解析termios结构体:从struct ktermios中提取c_cflag标志位
  2. 计算硬件参数:根据波特率计算分频系数,根据数据位/校验位设置寄存器
  3. 更新硬件寄存器:把计算好的值写入UART控制寄存器

波特率计算是重点。以16550 UART为例,公式是:

divisor = UART_CLOCK / (16 * baud_rate)

其中UART_CLOCK是UART模块的输入时钟频率。我遇到过一个问题:板子上UART时钟是24MHz,但驱动里写死了16MHz,结果波特率怎么调都不对。查了两天才发现是时钟配置错了。

数据位和校验位的配置,主要通过c_cflag的这几个位来控制:

参数 c_cflag设置 说明
5位数据 CS5 很少用
6位数据 CS6 偶尔见
7位数据 CS7 老设备用
8位数据 CS8 最常用
无校验 PARENB不置位
奇校验 PARENB | PARODD 校验位为奇数个1
偶校验 PARENB 校验位为偶数个1

set_termios的实现示例:

static void my_uart_set_termios(struct uart_port *port,
                                struct ktermios *termios,
                                struct ktermios *old)
{
    unsigned int baud, cflag = termios->c_cflag;
    unsigned int divisor;

    // 计算波特率
    baud = uart_get_baud_rate(port, termios, old, 0, 115200);
    divisor = port->uartclk / (16 * baud);
    uart_update_timeout(port, cflag, baud);

    // 设置数据位
    switch (cflag & CSIZE) {
    case CS5: /* 5位 */ break;
    case CS6: /* 6位 */ break;
    case CS7: /* 7位 */ break;
    case CS8: /* 8位 */ break;
    }

    // 设置校验位
    if (cflag & PARENB) {
        if (cflag & PARODD)
            /* 奇校验 */;
        else
            /* 偶校验 */;
    } else {
        /* 无校验 */;
    }

    // 写入硬件寄存器
    writel(divisor, port->membase + UART_DLL);
    writel(divisor >> 8, port->membase + UART_DLM);
}

避坑指南:我曾经在配置校验位时,只设置了PARENB,忘了清除PARODD位,结果奇校验和偶校验搞反了。调试了两天才发现。建议每次配置前,先把相关位清零,再按需置位。

4.5 实战经验:串口调试三板斧

最后,分享几个我常用的调试方法:

  • 示波器看波形:这是最直接的方法。看TX/RX引脚有没有电平变化,波特率对不对。我习惯先发一个0x55(二进制01010101),波形一目了然。
  • 逻辑分析仪抓数据:比示波器便宜,能直接解析UART帧。我常用它来验证数据位和校验位配置是否正确。
  • loopback测试:把TX和RX短接,发什么收什么。如果收发一致,说明硬件通路没问题。如果不对,八成是驱动配置有误。

嗯,UART驱动这部分内容就到这里。下一章我们会讲SPI驱动,那又是另一番天地了。记住一句话:串口通信,参数一致是王道,硬件通路是基础,驱动配置是关键。