8. UART 操作指南

本文档说明在 CV184x 平台上配置与使用 UART(串口)、环回测试、常用波特率设置以及用户态程序开发的基本流程。

8.1. 操作准备

8.1.1. 内核要求

  • 使用官方发布的 SDK 编译后的内核镜像。

8.1.2. 设备树中使能 UART

在设备树中将目标 UART 节点的 status 设为 "okay" 。UART 节点定义位于 SDK 的 build/boards/default/dts/cv184x/cv184x_base.dtsi 中。以 UART1 为例:

uart1: serial@04150000 {
    compatible = "snps,dw-apb-uart";
    reg = <0x0 0x04150000 0x0 0x1000>;
    clock-frequency = <25000000>;
    reg-shift = <2>;
    reg-io-width = <4>;
    status = "okay";   /* 关闭则设为 "disabled" */
};

修改后需重新编译设备树并烧录到设备。

8.1.3. UART 使用 DMA 时的设备树配置

若需使能 UART DMA,需同时修改板级 DTS 与 cv184x_base.dtsi:

  • 板级 DTS 路径:build/boards/cv184x/板名/dts_arm/板名.dts(板名替换为实际板卡名)。

  • 在板级 DTS 的 sysdma_remap 中为 UART 分配 DMA 通道,并在对应 UART 节点中增加 dmasdma-names 等属性。

cv184x_base.dtsi 中 UART1 的 DMA 示例(与 sysdma_remap 配合):

sysdma_remap {
    ch-remap = <CVI_I2S0_RX CVI_I2S2_TX CVI_UART1_RX CVI_UART1_TX
        CVI_SPI_NOR_RX CVI_SPI_NOR_TX CVI_I2S2_RX CVI_I2S3_TX>;
};

uart1: serial@04150000 {
    compatible = "snps,dw-apb-uart";
    reg = <0x0 0x04150000 0x0 0x1000>;
    clock-frequency = <25000000>;
    reg-shift = <2>;
    reg-io-width = <4>;
    status = "okay";
    dmas = <&dmac 2 1 1>, <&dmac 3 1 1>;
    dma-names = "rx", "tx";
    capability = "txrx";
};

dmas 中的通道号需与 sysdma_remap 中的分配一致,例如 CVI_UART1_RX 对应 dmac 通道 2,CVI_UART1_TX 对应 dmac 通道 3。修改后重新编译设备树并烧录。

8.1.4. 引脚复用

通过 cvi_pimux 工具或设备树将所用 GPIO 引脚配置为 UART 功能(TX、RX、CTS、RTS 等),具体引脚以板级原理图为准。

8.2. 操作流程

8.2.1. UART 环回测试

用于验证串口收发与引脚是否正常:将 UART 的 TX 与 RX 短接,发送数据后应能从同一串口读回相同内容。

  1. 短接 UART1 的 TX 与 RX 引脚。

  2. 在终端执行环回测试命令(以 ttyS1 为例):

stty -F /dev/ttyS1 115200 cs8 -cstopb -parenb
hexdump -C < /dev/ttyS1 &
echo "LOOPBACK_TEST" > /dev/ttyS1
_images/UART3.jpg

若短接正常,hexdump 应能打印出与 echo 一致的内容。

8.2.2. UART 串口通信(终端收发)

设置波特率与帧格式(以 ttyS1、115200 8N1 为例):

stty -F /dev/ttyS1 115200 cs8 -cstopb -parenb

发送数据:

echo "Hello UART" > /dev/ttyS1

接收数据(需在另一终端或另一台机连接该串口):

cat /dev/ttyS1

查看当前串口配置:

stty -F /dev/ttyS1 -a

8.3. UART 扩展操作(常用 / 排障 / 高级)

除上述基础收发、环回测试与查看配置外,以下按 常用扩展操作排障类操作高级配置操作 的顺序补充串口调试、参数固化、流控、数据捕获与排障等说明。示例中设备节点以 /dev/ttyS1 为例,请按实际串口替换。

8.3.1. 常用扩展操作(基础功能补充)

1. 串口流控配置(缓解大流量丢包)

若串口传输大文件或高频数据时出现丢包,可尝试开启硬件或软件流控。硬件流控需板级支持 RTS/CTS 引脚并正确连接。

# 开启硬件流控(RTS/CTS)
stty -F /dev/ttyS1 crtscts 115200 cs8

# 开启软件流控(XON/XOFF)
stty -F /dev/ttyS1 ixon ixoff 115200 cs8

# 关闭所有流控(默认)
stty -F /dev/ttyS1 -crtscts -ixon -ixoff

硬件流控可靠性更高,适合工业等对稳定性要求高的场景;软件流控无需额外引脚,适合简单场景。

2. 串口数据捕获与分析(调试用)

需要抓取串口收发的原始数据用于调试协议时,可采用以下方式。

cat + tee 保存接收数据

cat /dev/ttyS1 | tee /tmp/uart_recv.log   # 实时查看并保存

3. 串口发送 / 接收二进制数据(非文本)

前文 echo 仅适合发送文本。传输二进制文件(如固件、数据包)时建议使用 cat/dd,避免换行或转义影响数据。

# 发送二进制文件到串口
cat /mnt/sd/data.bin > /dev/ttyS1
# 文件根据实际修改

# 从串口接收二进制数据并保存(按块读取)
dd if=/dev/ttyS1 of=/tmp/recv_bin.bin bs=1024 count=10   # 读取 10KB

8.3.2. 排障类操作(解决串口异常)

1. 检查串口硬件与占用状态

# 查看串口设备是否存在
ls -l /dev/ttyS*

# 查看串口驱动状态(占用、中断、错误计数等,嵌入式常用)
cat /proc/tty/driver/serial

# 检查串口是否被其他进程占用
lsof /dev/ttyS1   # 有输出则表示被占用,可结束对应进程

2. 清除串口缓存 / 重置串口

串口出现乱码、卡死时,可尝试清空缓冲或恢复默认配置。

# 清空串口输入/输出缓冲
stty -F /dev/ttyS1 flush 0    # 清空输入缓存
stty -F /dev/ttyS1 flush 1    # 清空输出缓存

# 将串口恢复为合理默认配置
stty -F /dev/ttyS1 sane

8.3.3. 高级配置操作(适配特殊场景)

1. 配置串口奇偶校验

前文示例为无校验(-parenb)。若外设要求奇偶校验,可按需设置。

# 偶校验(even parity)
stty -F /dev/ttyS1 115200 cs8 -cstopb parenb -parodd

# 奇校验(odd parity)
stty -F /dev/ttyS1 115200 cs8 -cstopb parenb parodd

# 无校验(默认,与前文一致)
stty -F /dev/ttyS1 115200 cs8 -cstopb -parenb

2. 调整串口输入 / 输出与回显(缓解粘包等)

# 设置输出延迟等(单位 0.01 秒量级),可减少小包发送时的粘包
stty -F /dev/ttyS1 ospeed 115200 ocrnl onlcr -ocrnl -onlcr delay 1

# 关闭输入回显(交互时避免本地回显重复)
stty -F /dev/ttyS1 -echo

8.3.4. UART 扩展操作小结

  • 常用配置 :永久保存串口参数(rc.local 或 systemd)、配置流控(减少丢包)、使用 cat/dd 传输二进制数据。

  • 排障操作 :检查串口设备与占用(ls、/proc/tty/driver/serial、lsof/fuser)、清空缓冲与重置(stty flush/sane)、循环收发验证稳定性。

  • 高级调试 :数据捕获(tee、screen、minicom)、奇偶校验、输出/回显与延迟、串口控制台绑定。

注意 :串口参数(波特率、流控、校验、数据位/停止位)需与对端外设完全一致,否则易出现乱码或丢包;调试时优先使用 screen 或 minicom 等工具;二进制数据传输避免使用 echo,使用 cat/dd 更可靠。

8.4. UART 特定波特率输出(以 3M 为例)

当需要 3M 等非标准波特率时,需修改内核中波特率表与 termbits,并保证设备树中 UART 时钟足够大,否则会出现乱码或无法工作。

步骤 1:修改内核波特率表与 termbits

  1. 修改 linux_5.10/drivers/tty/tty_baudrate.c

    baud_tablebaud_bits 中增加 3000000(若需其他波特率,将 3000000 替换为目标值)。示例片段:

static const speed_t baud_table[] = {
    0, 50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400,
    4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800,
#ifdef __sparc__
    76800, 153600, 307200, 614400, 921600, 500000, 576000,
    1000000, 1152000, 1500000, 2000000
#else
    500000, 576000, 921600, 1000000, 1152000, 1500000, 2000000,
    3125000, 3000000, 6125000, 12500000
#endif
};

static const tcflag_t baud_bits[] = {
    B0, B50, B75, B110, B134, B150, B200, B300, B600, B1200, B1800, B2400,
    B4800, B9600, B19200, B38400, B57600, B115200, B230400, B460800,
#ifdef __sparc__
    B76800, B153600, B307200, B614400, B921600, B500000, B576000,
    B1000000, B1152000, B1500000, B2000000
#else
    B500000, B576000, B921600, B1000000, B1152000, B1500000, B2000000,
    B3125000, B3000000, B6125000, B12500000
#endif
};
_images/UART4.jpg
_images/UART5.jpg
  1. 修改 linux_5.10/include/uapi/asm-generic/termbits.h

    • 将 3M 波特率注释打开,并注释掉 4000000 波特率(如需修改其他波特率则将 3000000 替换为其他数值)。

      #define  B3000000 0010015
      // #define  B4000000 0010015
      
    • 注释掉重复的 B3000000 定义(即数值为 0020001 的那一行),保留与 baud_table 一致的定义。

      // #define B3000000 0020001
      

修改后重新编译内核。

步骤 2:设备树中 UART 时钟

若环回测试通过但实际通信无输出或乱码,多为 UART 输入时钟(CLK)偏小。UART 时钟建议不小于 波特率 × 16 。在设备树中将该 UART 的 clock-frequency 调大(例如改为 187500000 即 187.5MHz),重新编译设备树并烧录。

_images/UART1.jpg

步骤 3:验证 3M 波特率是否正常

按本文「操作流程」中的 UART 环回测试UART 串口通信(终端收发) 进行 UART 收发测试:先将目标 UART 的 TX 与 RX 短接做环回测试,或将两台设备通过串口相连做收发测试;在 stty 中设置波特率为 3000000(例如 stty -F /dev/ttyS1 3000000 cs8 -cstopb -parenb),发送并读回数据。若环回/对端接收内容与发送一致,则 3M 波特率工作正常。

8.5. 用户态程序开发示例

以下示例演示用户空间串口收发流程:打开串口、配置波特率与 8N1、执行读写测试。

示例内容:

  • uart_config:8N1、可配置波特率、原始模式、读超时 10 秒

  • uart_write / uart_read:发送与接收

  • main:循环收发

请按实际设备节点替换 /dev/ttyS1。若内核未支持 3M 波特率,请将 B3000000 改为 B115200 等(参见本文「UART 特定波特率输出(以 3M 为例)」小节)。

基本流程:打开设备、配置 termios、收发、关闭

  1#include <fcntl.h>
  2#include <termios.h>
  3#include <unistd.h>
  4#include <stdio.h>
  5#include <string.h>
  6#include <errno.h>
  7
  8/* 配置串口参数(8N1,波特率由参数指定) */
  9int uart_config(int fd, speed_t baudrate) {
 10    struct termios tty;
 11    memset(&tty, 0, sizeof(tty));
 12
 13    if (tcgetattr(fd, &tty) != 0) {
 14        printf("获取串口配置失败: %s\n", strerror(errno));
 15        return -1;
 16    }
 17
 18    cfsetispeed(&tty, baudrate);
 19    cfsetospeed(&tty, baudrate);
 20
 21    tty.c_cflag &= ~PARENB;
 22    tty.c_cflag &= ~PARODD;
 23    tty.c_cflag &= ~CSTOPB;
 24    tty.c_cflag &= ~CSIZE;
 25    tty.c_cflag |= CS8;
 26    tty.c_cflag &= ~CRTSCTS;
 27    tty.c_cflag |= CREAD | CLOCAL;
 28
 29    tty.c_lflag &= ~ICANON;
 30    tty.c_lflag &= ~ECHO;
 31    tty.c_lflag &= ~ECHOE;
 32    tty.c_lflag &= ~ISIG;
 33
 34    tty.c_iflag &= ~(IXON | IXOFF | IXANY);
 35    tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL);
 36
 37    tty.c_oflag &= ~OPOST;
 38    tty.c_oflag &= ~ONLCR;
 39
 40    tty.c_cc[VTIME] = 100;   /* 10 秒超时 */
 41    tty.c_cc[VMIN] = 1;
 42
 43    if (tcsetattr(fd, TCSANOW, &tty) != 0) {
 44        printf("设置串口配置失败: %s\n", strerror(errno));
 45        return -1;
 46    }
 47    tcflush(fd, TCIOFLUSH);
 48    return 0;
 49}
 50
 51ssize_t uart_write(int fd, const char *data, size_t len) {
 52    if (fd < 0 || !data || len == 0) return -1;
 53    ssize_t ret = write(fd, data, len);
 54    if (ret < 0) {
 55        printf("写数据失败: %s\n", strerror(errno));
 56        return -1;
 57    }
 58    printf("发送成功:%.*s (长度:%zd字节)\n", (int)ret, data, ret);
 59    return ret;
 60}
 61
 62ssize_t uart_read(int fd, char *buf, size_t buf_len) {
 63    if (fd < 0 || !buf || buf_len == 0) return -1;
 64    memset(buf, 0, buf_len);
 65    ssize_t ret = read(fd, buf, buf_len - 1);
 66    if (ret < 0) {
 67        printf("读数据失败: %s\n", strerror(errno));
 68        return -1;
 69    } else if (ret == 0) {
 70        printf("读数据超时(10秒未收到数据)\n");
 71        return 0;
 72    }
 73    buf[ret] = '\0';
 74    printf("接收成功:%s (长度:%zd字节)\n", buf, ret);
 75    return ret;
 76}
 77
 78int main(void) {
 79    int fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY);
 80    if (fd < 0) {
 81        printf("打开串口ttyS1失败: %s\n", strerror(errno));
 82        return -1;
 83    }
 84    printf("成功打开ttyS1,文件描述符:%d\n", fd);
 85
 86    if (uart_config(fd, B3000000) != 0) {
 87        close(fd);
 88        return -1;
 89    }
 90    printf("串口配置完成(3000000 8N1)\n");
 91
 92    char send_buf[64] = "Hello ttyS1! This is UART test.\n";
 93    char recv_buf[128];
 94    while (1) {
 95        uart_write(fd, send_buf, strlen(send_buf));
 96        uart_read(fd, recv_buf, sizeof(recv_buf));
 97        sleep(1);
 98    }
 99
100    close(fd);
101    return 0;
102}

运行方式说明

  1. 保存源码:将上述代码保存为文件,例如 uart_demo.c

  2. 编译:在宿主机或目标板上使用对应工具链编译。

    # 交叉编译示例(以实际工具链为准)
    arm-none-linux-uclibcgnueabihf-gcc -o uart_demo uart_demo.c -static
    
  3. 运行:将可执行文件拷贝到开发板后,在目标板上执行。需对串口设备节点(如 /dev/ttyS1)有读写权限。

    ./uart_demo
    

    程序会循环发送 Hello ttyS1! This is UART test. 并阻塞等待接收(最多 10 秒),可使用串口工具打开ttyS1对应的串口并发送数据。

  4. 修改设备与波特率:源码中 open("/dev/ttyS1", ...) 可改为实际串口节点;uart_config(fd, B3000000) 可改为 B115200B921600 等(需内核与设备树支持该波特率,参见本文「高波特率配置」)。