在瑞芯微RK3288驱动开发过程中,串口是嵌入式工程师打交道最频繁的外设——无论是板卡 bring-up 阶段用调试串口看内核日志,还是产品里挂接 4G 模组、GPS、传感器,UART 串口驱动几乎贯穿整个开发周期。本文以一个真实的板卡调试案例为线索,拆解 RK3288 UART 串口驱动与调试串口的配置全流程,覆盖设备树、内核驱动框架、应用层收发代码和常见翻车点。

在这里插入图片描述

一、问题场景

新拿到一块 RK3288 核心板,Uboot 阶段串口打印正常,但内核起来之后:

  1. 调试串口只输出了几行就戛然而止,后面一片空白;
  2. 换一个 UART 口挂外部模组,cat /dev/ttyS1 能收到数据,但 echo 发出去的数据对方收不到,或者乱码;
  3. stty -F /dev/ttyS1 115200 配置波特率后,实测波形波特率偏离标称值,长报文偶发丢字节。

这三个问题正好对应了串口驱动里的三个层次:设备树配置、驱动框架挂载、波特率时钟精度。下面从原理出发逐个击破。

二、原理分析

1. RK3288 的 UART 硬件

RK3288 内部集成了 5 路 UART 控制器(UART0~UART4),IP 内核来自 Synopsys 的 DesignWare APB UART(DW_apb_uart)。这几路串口有一个很有意思的细节:UART2 是官方默认的调试串口,基地址被单独放到了 0xFF690000,其他四路(UART0/1/3/4)则依次排在 0xFF180000~0xFF1C0000 区间。

控制器基地址默认用途
UART00xFF180000一般用途
UART10xFF190000一般用途
UART20xFF690000调试串口(默认 console)
UART30xFF1B0000一般用途
UART40xFF1C0000一般用途

2. Linux 内核驱动框架

Linux 对这类 8250 兼容串口统一走 serial/8250 子系统。DW_apb_uart 的具体驱动是 drivers/tty/serial/8250/8250_dw.c,它通过设备树 compatible 绑定:

// drivers/tty/serial/8250/8250_dw.c 中的匹配表(关键片段)
static const struct of_device_id dw8250_of_match[] = {
    { .compatible = "snps,dw-apb-uart" },
    { /* rockchip wrapper 继承 snps 兼容,走同一套驱动 */ },
    { /* end of list */ },
};

RK3288 的设备树节点里,compatible 会写成 "rockchip,rk3288-uart",前面挂一个 "snps,dw-apb-uart" 作为兼容回退。驱动加载后,每个 UART 注册一个 ttyS* 设备节点。

3. 波特率是怎么算出来的

这是串口驱动最容易出错的点。DW_apb_uart 的波特率由时钟分频得到:

baud = baudclk / (16 × divisor)

其中 divisor 由 DLL/DLM 两个寄存器拼成。RK3288 的串口 baudclk 默认取 24MHz(来自 CRU 的 SCLK_UART*)。以 115200 为例:

divisor = 24000000 / (16 × 115200) = 13.02

硬件只能取整数 13,于是实际波特率变成 24000000/(16×13) = 115384,偏差约 0.16%。这个误差在短报文里无所谓,但长报文或高波特率(如 921600)下误差被放大,就会丢字节。DW_apb_uart 专门提供了一个 DLF 分数分频寄存器来修正这个小数部分,内核 8250_dw 驱动会自动处理,前提是 clock-frequency 与 CRU 实际输出频率一致——不一致正是乱码的头号元凶。

三、代码实战

1. 设备树配置

在 RK3288 的设备树里打开一个 UART 节点,核心是三件事:status、pinctrl、时钟

&uart2 {
    status = "okay";              /* 使能 UART2 调试串口 */
    pinctrl-names = "default";
    pinctrl-0 = <&uart2_xfer>;    /* 绑定引脚复用 */
};

&pinctrl {
    uart2 {
        uart2_xfer: uart2-xfer {
            rockchip,pins =
                /* TX : GPIO4_C0,复用为 UART2_TX */
                <4 16 RK_FUNC_1 &pcfg_pull_none>,
                /* RX : GPIO4_C1,复用为 UART2_RX */
                <4 17 RK_FUNC_1 &pcfg_pull_none>;
        };
    };
};

如果要把 UART2 作为内核 console,还需要在命令行里指定。Uboot 的 bootargs 里加:

console=ttyS2,115200n8        # ttyS2 对应 UART2,n8 = 无校验 8 数据位

2. 应用层串口收发(termios 配置)

硬件和内核就绪后,应用层通过 termios 配置串口参数。下面是一段标准的串口打开+配置+收发代码:

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>

int main(int argc, char *argv[])
{
    int fd;
    struct termios opt;

    if (argc != 2) {
        printf("usage: %s /dev/ttyS1\n", argv[0]);
        return -1;
    }

    /* 打开串口:O_NOCTTY 防止变成控制终端,O_NDELAY 非阻塞打开 */
    fd = open(argv[1], O_RDWR | O_NOCTTY | O_NDELAY);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    tcgetattr(fd, &opt);

    /* 输入输出波特率统一 115200 */
    cfsetispeed(&opt, B115200);
    cfsetospeed(&opt, B115200);

    /* 8 数据位、无校验、1 停止位 */
    opt.c_cflag &= ~PARENB;    /* 关闭校验位 */
    opt.c_cflag &= ~CSTOPB;    /* 1 个停止位 */
    opt.c_cflag &= ~CSIZE;     /* 清除数据位掩码 */
    opt.c_cflag |= CS8;        /* 8 数据位 */

    opt.c_cflag &= ~CRTSCTS;   /* 关闭硬件流控(硬件没接 RTS/CTS 必须关) */
    cfmakeraw(&opt);           /* 原始模式,关闭行缓冲与回显 */

    tcsetattr(fd, TCSANOW, &opt);

    /* 发送测试字符串 */
    char *msg = "RK3288 UART test\n";
    write(fd, msg, strlen(msg));

    /* 回读串口数据 */
    char buf[64] = {0};
    int n = read(fd, buf, sizeof(buf) - 1);
    printf("recv %d bytes: %s\n", n, buf);

    close(fd);
    return 0;
}

3. 命令行快速自测(回环测试)

先把 TX/RX 短接,用 shell 一行命令验证收发:

# 配置波特率与原始模式
stty -F /dev/ttyS1 115200 raw -echo

# 后台持续读,前台写
cat /dev/ttyS1 &
echo "hello rk3288" > /dev/ttyS1

四、运行结果

回环测试下,终端会立刻打印出写入的字符串:

hello rk3288

用交叉编译的 C 程序在板端运行:

# ./uart_test /dev/ttyS1
recv 16 bytes: RK3288 UART test

串口波特率实测(逻辑分析仪抓 TX 波形):

标称 115200  →  实测 115384(偏差 0.16%)
标称 921600  →  实测 923076(偏差 0.16%)

这个偏差在 921600 波特率下仍小于容限(一般要求 <2%),收发正常,说明 24MHz 时钟源与 clock-frequency 一致,分数分频生效。

五、避坑指南

1. 调试串口起来后就没下文了。 九成是 bootargs 里的 console= 写错,或 UART2 的 pinctrl 没配上、TX/RX 引脚被别的外设复用。确认 console=ttyS2,115200n8 里的设备号与设备树 aliases 的映射一致。

2. 能发不能收,或收到乱码。 优先怀疑波特率时钟源。检查 DTS 里 clock-frequency 是否等于 CRU 实际输出的 SCLK_UART*,二者不一致时内核按错误的时钟算分频,必然乱码。

3. 挂了模组却死活收不到数据。 看是否误开了硬件流控。代码里 CRTSCTS 若开启,而硬件没接 RTS/CTS 线,发送会被流控信号卡住。模组场景通常直接关流控。

4. 长报文偶发丢字节。 两个方向排查:一是 FIFO 触发阈值与中断是否正常;二是高波特率下分数分频是否失效(时钟配置错误导致误差放大)。确认 8250_dw 驱动识别到 DLF 寄存器。

5. 应用层 open() 返回 Permission denied。 这是 ttyS 设备节点权限问题。临时可 chmod 666 /dev/ttyS1,正式量产建议写 udev 规则给串口节点授权。

6. 早期内核日志看不到。 bring-up 阶段 console 未初始化前的打印,靠 earlycon 打通。RK3288 UART2 用 8250 的 mmio32 早期控制台:

earlycon=uart8250,mmio32,0xff690000

六、总结

RK3288 的 UART 串口驱动本质上是「设备树正确配置 + 8250_dw 框架 + 时钟精度」三件事的叠加。设备树管使能和引脚复用,8250 子系统负责注册 ttyS* 节点与中断收发,而波特率精度则由 24MHz 时钟源和 DLF 分数分频共同保证。把这三层捋顺,串口这个最基础也最容易踩坑的外设就能一次点亮。调试串口和业务串口遵循同一套逻辑,区别只在 console/earlycon 的启动参数配置上。

你在 RK3288 串口调试中遇到过哪些诡异的乱码或丢字节问题?欢迎在评论区交流你的排查思路。

更多推荐