核心目标:从UART 异步串行通信本质出发,拆解 Linux TTY 终端子系统与串口驱动的分层架构,掌握UART 设备树配置、用户态串口编程、内核态串口外设驱动三大核心技能,通过完整的收发实战解决 “串口无法打开、数据乱码、丢包、流控异常” 等嵌入式高频问题,完成从同步串行总线(SPI/I2C)异步串行总线的能力拓展,吃透嵌入式最通用的调试接口与板间通信方案。

一、本质追问:UART 串口的核心价值与 Linux 实现

在嵌入式系统的所有串行总线中,UART(通用异步收发传输器) 是最基础、应用最广的通信接口 —— 它既是系统调试的核心入口(调试串口),也是板间设备、外接模块的标准通信方式(GPS、蓝牙、4G、工控板卡均支持 UART),是嵌入式开发者必须精通的 “基础中的基础”。

1.1 UART 与 SPI/I2C 的核心区别

SPI/I2C 属于板内芯片间的同步串行总线,依赖时钟信号同步收发,主要用于 PCB 板上的芯片互联;而 UART 属于异步串行总线,无需时钟线,通过约定的波特率实现收发同步,主要用于板与板之间、设备与设备之间的中低速通信,二者的核心差异如下:

表格

对比维度 UART SPI I2C
通信方式 异步全双工,无时钟线,靠波特率同步 同步全双工,靠 SCLK 时钟同步 同步半双工,靠 SCL 时钟同步
信号线 最少 2 根(TX/RX)+GND,可选流控(CTS/RTS) 4 根(SCLK/MOSI/MISO/CS),多设备加 CS 线 2 根(SDA/SCL)
传输速率 中低速(常见 9600~115200bps,最高几 Mbps) 高速(几十~上百 MHz) 低速(100k~400kbps)
寻址方式 无地址,点对点通信,一条总线对应一个设备 独立片选引脚,多设备靠 CS 区分 7/10 位从机地址,总线多设备
典型场景 调试串口、GPS / 蓝牙 / 4G 模块、板间通信 Flash、显示屏、高速 ADC 传感器、RTC、EEPROM

核心本质:UART 是点对点的异步串行协议,无需主机时钟,收发双方必须提前约定完全一致的通信参数(波特率、数据位、停止位、校验位),否则会出现数据乱码、通信失败。

1.2 串口通信核心参数(必须完全匹配)

UART 异步通信的前提是收发双方参数完全一致,这 5 个参数是串口开发的核心,也是 90% 串口乱码问题的根源:

  1. 波特率(Baud Rate):每秒传输的比特数,单位 bps,嵌入式常用值:9600、19200、38400、115200、921600;
  2. 数据位(Data Bits):每个字节的数据位数,通常为 8 位,也有 5/6/7 位;
  3. 停止位(Stop Bits):字节结束的标志位,通常 1 位,也有 1.5/2 位;
  4. 校验位(Parity):数据校验方式,常用无校验(None),也有奇校验(Odd)、偶校验(Even);
  5. 流控(Flow Control):防止数据溢出的机制,分为无流控、硬件流控(RTS/CTS)、软件流控(XON/XOFF),嵌入式常用无流控。

常用组合:嵌入式最通用的配置是 115200 8N1(波特率 115200,8 位数据位,无校验,1 位停止位,无流控),调试串口、绝大多数模块默认都是该配置。

1.3 裸机 UART 开发的致命痛点

裸机开发中直接操作 UART 寄存器,虽然简单,但在复杂嵌入式系统中存在诸多问题:

  • 硬件强耦合:不同芯片的 UART 控制器寄存器完全不同,代码可移植性差,更换芯片需重写全部串口逻辑;
  • 无缓冲区管理:裸机 UART 通常只有 1~2 字节的硬件 FIFO,大数据量收发时极易丢包,需手动实现环形缓冲区;
  • 无标准化接口:每个项目的串口操作函数都不一样,上层应用无法复用;
  • 无流控与错误处理:需手动处理溢出、帧错误、奇偶校验错误,代码繁琐且易出错。

1.4 Linux TTY 子系统的核心使命

Linux 内核通过TTY(Teletypewriter,终端)子系统统一管理所有串行终端设备(包括硬件串口、虚拟串口、USB 转串口),其核心使命是屏蔽硬件差异,提供标准化的终端操作接口,实现缓冲区管理、线路规程、流控等通用能力,具体价值如下:

  1. 硬件抽象,统一接口:所有串口最终都暴露为/dev/ttySx(硬件串口)、/dev/ttyUSBx(USB 转串口)等标准设备文件,应用通过统一的termios接口配置参数、收发数据,无需关心底层硬件;
  2. 分层架构,职责分离:分为 TTY 核心层、线路规程层、UART 驱动层,开发者只需关注业务逻辑,底层驱动、缓冲区、流控均由内核实现;
  3. 完善的缓冲区机制:内核为每个串口分配独立的收发环形缓冲区(几十 KB),大幅降低丢包概率,支持中断 + DMA 收发,CPU 占用极低;
  4. 标准化线路规程:内置行规程(行编辑、回显、特殊字符处理),默认适配终端交互,也可切换为原始模式(Raw)用于数据传输。

1.5 嵌入式 Linux 串口的两种开发角色

和 SPI/I2C 类似,串口开发分为两类角色,对应不同的开发内容,嵌入式开发者 90% 的工作集中在第二类:

  1. UART 控制器驱动:操作芯片的 UART 寄存器,实现底层数据收发,由芯片厂商(瑞芯微、NXP、ST)提供,已集成到内核,开发者只需通过设备树启用即可;
  2. 串口外设应用 / 驱动:基于内核提供的标准串口设备,开发外接模块(GPS、蓝牙、4G)的通信逻辑,分为用户态应用开发(最常用,通过文件操作串口)和内核态驱动开发(特殊场景,封装为专用字符设备)。

二、内核基础:Linux TTY 子系统架构与串口开发核心

Linux TTY 子系统采用经典的三层分层架构,从上到下分别是TTY 字符设备层、线路规程层、UART 硬件驱动层,每层职责明确,解耦性极强。

2.1 TTY 子系统三层架构

plaintext

用户态:应用程序 → 操作/dev/ttySx设备文件
↓
TTY字符设备层:统一的字符设备接口,管理TTY设备号、设备节点
↓
线路规程层(Line Discipline):数据加工层,默认N_TTY(终端行规程),可切换为原始模式
↓
UART核心层(serial core):串口核心框架,管理UART控制器与端口
↓
UART驱动层:具体芯片的UART控制器驱动(厂商提供),操作硬件寄存器
↓
硬件层:UART控制器 + 外接串口设备
  1. TTY 字符设备层

    • 核心作用:向用户态暴露标准字符设备接口,所有 TTY 设备共享主设备号 4,次设备号区分不同终端(如 ttyS0 次设备号 0,ttyS1 次设备号 1);
    • 关键能力:实现open/read/write/ioctl等标准文件接口,应用无需关心底层是硬件串口还是虚拟串口。
  2. 线路规程层(Line Discipline)

    • 核心作用:TTY 子系统的 “数据加工层”,对收发的数据进行预处理,是 TTY 区别于普通字符设备的核心;
    • 默认规程N_TTY:实现终端行编辑(退格、删除)、回显、特殊字符处理(Ctrl+C 中断),适合调试串口、shell 终端;
    • 原始模式:关闭行编辑和回显,数据原样收发,适合模块通信、数据传输,是嵌入式串口开发的常用模式;
    • 可动态切换:通过ioctl命令切换线路规程,无需修改驱动。
  3. UART 核心层与驱动层

    • UART 核心层:提供标准化的 UART 驱动框架,定义通用的数据结构和操作接口,屏蔽不同控制器的差异;
    • UART 驱动层:由芯片厂商实现,操作具体的 UART 控制器寄存器,实现数据收发、中断处理、DMA 配置;
    • 核心抽象:struct uart_driver(UART 驱动)、struct uart_port(一个 UART 端口,对应一个硬件串口)、struct uart_ops(端口操作函数集)。

2.2 UART 驱动核心数据结构

2.2.1 struct uart_driver:UART 驱动整体描述

描述一个 UART 控制器驱动的全局信息,包含驱动名、设备号、端口数量等,注册后内核会为其分配 TTY 设备资源。

c

运行

struct uart_driver {
    struct module *owner;
    const char *driver_name;    // 驱动名
    const char *dev_name;       // 设备名前缀,如"ttyS"
    int major;                  // 主设备号,0表示动态分配
    int minor;                  // 起始次设备号
    int nr;                     // 支持的端口数量
    struct uart_state *state;   // 端口状态数组
    struct tty_driver *tty_driver; // 关联的TTY驱动
};
2.2.2 struct uart_port:单个串口端口描述

对应一个物理串口,包含端口的硬件信息(寄存器地址、中断号、时钟、波特率)、运行状态、操作函数集,是 UART 驱动的核心实体。

c

运行

struct uart_port {
    void __iomem *membase;      // 寄存器虚拟地址
    unsigned int irq;           // 中断号
    unsigned int uartclk;       // 串口时钟频率
    unsigned int baudrate;      // 当前波特率
    unsigned char fifosize;     // 硬件FIFO大小
    const struct uart_ops *ops; // 端口操作函数集
    struct device *dev;         // 设备指针
    // 状态、锁、缓冲区等其他字段
};
2.2.3 struct uart_ops:端口操作函数集

UART 驱动需要实现的硬件操作函数,包括发送、接收、设置波特率、启动停止等,是控制器驱动的核心。

c

运行

struct uart_ops {
    unsigned int (*tx_empty)(struct uart_port *);
    void (*set_termios)(struct uart_port *, struct ktermios *new, struct ktermios *old); // 设置串口参数
    void (*start_tx)(struct uart_port *); // 启动发送
    void (*stop_tx)(struct uart_port *);  // 停止发送
    void (*start_rx)(struct uart_port *); // 启动接收
    void (*stop_rx)(struct uart_port *);  // 停止接收
    int (*startup)(struct uart_port *);   // 初始化端口
    void (*shutdown)(struct uart_port *); // 关闭端口
    // 其他操作函数
};

嵌入式开发提示:以上结构体均由芯片厂商在控制器驱动中实现,开发者无需手动编写,只需通过设备树配置端口参数、启用端口即可。

2.3 UART 设备树配置规范

UART 控制器驱动通常已集成在内核中,开发者的核心工作是通过设备树启用指定 UART 端口、配置引脚复用、设置默认参数,这是串口可用的前提。

2.3.1 UART 控制器节点配置(启用端口)

以 RK3328 的 UART2 为例,芯片厂商已提供控制器节点,开发者只需修改statusokay,配置引脚复用即可:

dts

&uart2 {
    status = "okay";          // 启用UART2,对应设备/dev/ttyS2
    pinctrl-names = "default";
    pinctrl-0 = <&uart2m0_xfer>; // 引脚复用:TX/RX引脚配置为UART功能
    // 可选:硬件流控引脚配置
    // rts-gpios = <&gpio1 22 GPIO_ACTIVE_LOW>;
    // cts-gpios = <&gpio1 23 GPIO_ACTIVE_LOW>;
};
2.3.2 引脚复用配置(关键,常被忽略)

UART 的 TX/RX 引脚默认可能是 GPIO 功能,必须在设备树中配置为 UART 功能,否则串口无法通信,这是新手最容易踩的坑:

dts

// 引脚复用节点,配置GPIO1_24为UART2_TX,GPIO1_25为UART2_RX
uart2m0_xfer: uart2m0-xfer {
    rockchip,pins = 
        <1 24 RK_FUNC_2 &pcfg_pull_up>, // TX引脚,复用为UART2功能,上拉
        <1 25 RK_FUNC_2 &pcfg_pull_up>; // RX引脚,复用为UART2功能,上拉
};
2.3.3 串口外设节点(serdev 总线)

如果要编写内核态的串口外设驱动(如 GPS 模块),现代内核推荐使用serdev 总线(串行设备总线),和 I2C/SPI 总线类似,在设备树中添加外设节点,驱动通过 compatible 匹配:

dts

&uart2 {
    status = "okay";
    pinctrl-0 = <&uart2m0_xfer>;

    // 挂载GPS模块到UART2
    gps@0 {
        compatible = "neo-6m,gps";
        status = "okay";
    };
};

2.4 用户态串口开发核心:termios 接口

嵌入式开发中,90% 以上的串口应用都在用户态实现(如 GPS 数据解析、蓝牙 AT 指令、4G 拨号),通过标准文件接口操作/dev/ttySx,核心是通过termios结构体配置串口参数。

2.4.1 核心头文件与结构体

c

运行

#include <termios.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

// termios核心结构体(精简)
struct termios {
    tcflag_t c_iflag;  // 输入模式标志
    tcflag_t c_oflag;  // 输出模式标志
    tcflag_t c_cflag;  // 控制模式标志(波特率、数据位、停止位、校验位)
    tcflag_t c_lflag;  // 本地模式标志(行编辑、回显)
    cc_t c_cc[NCCS];   // 特殊字符数组
};
2.4.2 核心配置函数

c

运行

// 获取当前串口参数
int tcgetattr(int fd, struct termios *termios_p);
// 设置串口参数(TCSANOW:立即生效)
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
// 设置输入波特率
int cfsetispeed(struct termios *termios_p, speed_t speed);
// 设置输出波特率
int cfsetospeed(struct termios *termios_p, speed_t speed);
// 刷新缓冲区(TCIFLUSH:清输入,TCOFLUSH:清输出,TCIOFLUSH:都清)
int tcflush(int fd, int queue_selector);
2.4.3 原始模式配置(数据传输必备)

用于模块通信、数据传输的串口必须配置为原始模式(Raw),关闭行编辑、回显、特殊字符处理,确保数据原样收发:

c

运行

// 配置为原始模式
cfmakeraw(&termios);

该宏会自动关闭所有终端处理逻辑,适合纯数据传输场景。

2.5 内核态串口开发:serdev 总线接口

如果需要在内核态操作串口(如实现专用字符设备、内核态数据解析),推荐使用serdev 总线,这是 Linux 4.11 之后引入的标准串口外设总线,用法和 I2C/SPI 驱动高度一致。

核心 API:

c

运行

// 注册/注销serdev驱动
int serdev_device_driver_register(struct serdev_device_driver *drv);
void serdev_device_driver_unregister(struct serdev_device_driver *drv);
// 串口收发
int serdev_device_write(struct serdev_device *serdev, const u8 *buf, size_t count);
int serdev_device_read(struct serdev_device *serdev, u8 *buf, size_t count);
// 设置波特率
int serdev_device_set_baudrate(struct serdev_device *serdev, unsigned int speed);
// 设置接收回调
void serdev_device_set_client_ops(struct serdev_device *serdev, const struct serdev_device_ops *ops);

三、实战一:UART 设备树配置与基础调试

理论落地第一步:通过设备树启用串口,使用调试工具验证串口硬件和驱动正常,这是所有串口开发的基础。

3.1 硬件连接准备

  1. 串口接线规则
    • 交叉连接:板卡的 TX 接另一设备的 RX,板卡的 RX 接另一设备的 TX;
    • 必须共地:两个设备的 GND 必须连接在一起,否则电平基准不一致,通信失败;
    • 电平匹配:注意串口电平是 3.3V TTL 还是 RS232,嵌入式板卡通常是 3.3V TTL,接 RS232 需要电平转换芯片。
  2. 调试工具:USB 转 TTL 模块(CH340/PL2303),一端接电脑 USB,一端接板卡 UART 的 TX/RX/GND。

3.2 设备树配置(以 IMX6ULL UART3 为例)

  1. 找到内核设备树中的 UART3 控制器节点,启用并配置引脚:

dts

&uart3 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart3>;
    status = "okay"; // 启用UART3,对应/dev/ttymxc2(IMX平台命名规则)
};

// 引脚复用配置
pinctrl_uart3: uart3grp {
    fsl,pins = <
        MX6UL_PAD_UART3_TX_DATA__UART3_DCE_TX 0x1b0b1 // TX引脚
        MX6UL_PAD_UART3_RX_DATA__UART3_DCE_RX 0x1b0b1 // RX引脚
    >;
};
  1. 重新编译设备树,烧录到板卡并重启。

3.3 板卡端验证串口设备

重启后执行以下命令,确认串口设备节点已生成:

bash

运行

# 查看所有串口设备
ls /dev/tty* | grep tty
# 查看特定串口(根据平台命名不同,RK是ttySx,IMX是ttymxcx)
ls -l /dev/ttymxc2

成功标志:存在对应设备节点,如/dev/ttymxc2,主设备号 207,次设备号 16。

3.4 串口回环测试(验证硬件与驱动)

最可靠的硬件验证方式是回环测试:将板卡串口的 TX 和 RX 短接,自发自收验证收发通路。

  1. 硬件操作:用杜邦线将 UART3 的 TX 和 RX 引脚短接;
  2. 板卡端执行回环测试:

bash

运行

# 安装串口测试工具
apt-get install setserial
# 配置串口为115200 8N1原始模式
stty -F /dev/ttymxc2 115200 cs8 -cstopb -parenb raw -echo
# 后台读取串口数据,写入到文件
cat /dev/ttymxc2 > /tmp/uart_recv.txt &
# 向串口写入测试数据
echo "UART Loopback Test!" > /dev/ttymxc2
# 等待1秒,查看接收文件
sleep 1 && cat /tmp/uart_recv.txt

成功标志:接收文件中显示写入的测试字符串,说明串口的发送、接收通路都正常,硬件和驱动无问题。

3.5 常用串口调试工具使用

3.5.1 stty:命令行配置串口参数

bash

运行

# 查看串口当前参数
stty -F /dev/ttymxc2 -a
# 配置为115200 8N1,原始模式,关闭回显
stty -F /dev/ttymxc2 115200 cs8 -cstopb -parenb raw -echo
3.5.2 minicom:交互式串口终端
  1. 安装:apt-get install minicom
  2. 配置:minicom -s,进入 Serial port setup,设置串口设备、波特率、关闭硬件流控;
  3. 运行:minicom -D /dev/ttymxc2,即可和串口设备交互;
  4. 退出:按Ctrl+A,再按X
3.5.3 screen:轻量级串口终端

bash

运行

# 打开串口,115200波特率
screen /dev/ttymxc2 115200
# 退出:按Ctrl+A,再按\,确认退出

四、实战二:用户态串口应用开发(完整可运行代码)

用户态串口编程是嵌入式开发者的核心技能,本节实现一个通用串口工具,支持配置参数、发送数据、循环接收数据,可直接用于模块调试和二次开发。

4.1 开发需求

实现一个通用串口应用,满足以下需求:

  1. 支持指定串口设备、波特率、数据位、停止位、校验位;
  2. 支持原始模式,关闭回显和行编辑,适合模块通信;
  3. 支持发送指定字符串,循环接收并打印串口数据;
  4. 代码可移植,可在所有 Linux 平台运行。

4.2 完整代码:uart_tool.c

c

运行

/*************************************************************************
*  通用串口工具:嵌入式Linux用户态串口开发实战
*  用法:./uart_tool /dev/ttymxc2 115200
*  功能:配置串口为8N1原始模式,循环接收并打印数据,输入内容可发送
*************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <errno.h>
#include <sys/select.h>

#define BUF_SIZE 1024

/**
 * @brief  设置串口参数
 * @param  fd: 串口文件描述符
 * @param  baudrate: 波特率
 * @param  databits: 数据位(5/6/7/8)
 * @param  stopbits: 停止位(1/2)
 * @param  parity: 校验位('n'无,'o'奇,'e'偶)
 * @return 成功返回0,失败返回-1
 */
int uart_set_param(int fd, int baudrate, int databits, int stopbits, char parity)
{
    struct termios options;
    speed_t speed;

    // 获取当前串口配置
    if (tcgetattr(fd, &options) != 0) {
        perror("tcgetattr failed");
        return -1;
    }

    // 1. 设置波特率
    switch (baudrate) {
        case 9600:   speed = B9600; break;
        case 19200:  speed = B19200; break;
        case 38400:  speed = B38400; break;
        case 57600:  speed = B57600; break;
        case 115200: speed = B115200; break;
        case 230400: speed = B230400; break;
        case 460800: speed = B460800; break;
        case 921600: speed = B921600; break;
        default:
            printf("Unsupported baudrate: %d\n", baudrate);
            return -1;
    }
    cfsetispeed(&options, speed);
    cfsetospeed(&options, speed);

    // 2. 设置数据位
    options.c_cflag &= ~CSIZE; // 清除数据位掩码
    switch (databits) {
        case 5: options.c_cflag |= CS5; break;
        case 6: options.c_cflag |= CS6; break;
        case 7: options.c_cflag |= CS7; break;
        case 8: options.c_cflag |= CS8; break;
        default:
            printf("Unsupported databits: %d\n", databits);
            return -1;
    }

    // 3. 设置停止位
    switch (stopbits) {
        case 1: options.c_cflag &= ~CSTOPB; break; // 1位停止位
        case 2: options.c_cflag |= CSTOPB; break;  // 2位停止位
        default:
            printf("Unsupported stopbits: %d\n", stopbits);
            return -1;
    }

    // 4. 设置校验位
    switch (parity) {
        case 'n': case 'N': // 无校验
            options.c_cflag &= ~PARENB;
            options.c_iflag &= ~INPCK;
            break;
        case 'o': case 'O': // 奇校验
            options.c_cflag |= PARENB;
            options.c_cflag |= PARODD;
            options.c_iflag |= INPCK;
            break;
        case 'e': case 'E': // 偶校验
            options.c_cflag |= PARENB;
            options.c_cflag &= ~PARODD;
            options.c_iflag |= INPCK;
            break;
        default:
            printf("Unsupported parity: %c\n", parity);
            return -1;
    }

    // 5. 配置为原始模式(核心:关闭终端处理,数据原样收发)
    cfmakeraw(&options);

    // 6. 其他配置:启用接收,忽略调制解调器状态
    options.c_cflag |= CREAD | CLOCAL;
    // 关闭软件流控
    options.c_iflag &= ~(IXON | IXOFF | IXANY);
    // 关闭输出处理
    options.c_oflag &= ~OPOST;

    // 7. 设置超时:非阻塞模式,read立即返回
    options.c_cc[VMIN] = 0;  // 最少读取0字节
    options.c_cc[VTIME] = 1; // 超时100ms

    // 8. 刷新缓冲区,立即生效
    tcflush(fd, TCIOFLUSH);
    if (tcsetattr(fd, TCSANOW, &options) != 0) {
        perror("tcsetattr failed");
        return -1;
    }

    printf("UART config: baud=%d, databits=%d, stopbits=%d, parity=%c\n",
           baudrate, databits, stopbits, parity);
    return 0;
}

int main(int argc, char *argv[])
{
    int fd;
    char *dev_path;
    int baudrate;
    char buf[BUF_SIZE];
    fd_set readfds;
    int ret;

    // 参数检查
    if (argc < 3) {
        printf("Usage: %s <uart_dev> <baudrate>\n", argv[0]);
        printf("Example: %s /dev/ttymxc2 115200\n", argv[0]);
        return -1;
    }
    dev_path = argv[1];
    baudrate = atoi(argv[2]);

    // 1. 打开串口设备(读写模式,不成为控制终端)
    fd = open(dev_path, O_RDWR | O_NOCTTY | O_NDELAY);
    if (fd < 0) {
        perror("open uart failed");
        return -1;
    }
    printf("Open %s success, fd=%d\n", dev_path, fd);

    // 2. 配置串口参数:115200 8N1 原始模式
    if (uart_set_param(fd, baudrate, 8, 1, 'n') != 0) {
        close(fd);
        return -1;
    }

    printf("UART tool running, press Ctrl+C to exit\n");
    printf("----------------------------------------\n");

    // 3. 主循环:监听串口和标准输入,实现收发交互
    while (1) {
        FD_ZERO(&readfds);
        FD_SET(fd, &readfds);      // 监听串口可读
        FD_SET(STDIN_FILENO, &readfds); // 监听键盘输入

        // select多路复用,等待可读事件
        ret = select(fd + 1, &readfds, NULL, NULL, NULL);
        if (ret < 0) {
            perror("select failed");
            break;
        }

        // 串口有数据:读取并打印
        if (FD_ISSET(fd, &readfds)) {
            memset(buf, 0, BUF_SIZE);
            ret = read(fd, buf, BUF_SIZE - 1);
            if (ret > 0) {
                printf("[Recv %d bytes]: %s", ret, buf);
                fflush(stdout);
            } else if (ret < 0) {
                perror("read uart failed");
                break;
            }
        }

        // 键盘有输入:读取并发送到串口
        if (FD_ISSET(STDIN_FILENO, &readfds)) {
            memset(buf, 0, BUF_SIZE);
            fgets(buf, BUF_SIZE, stdin);
            ret = write(fd, buf, strlen(buf));
            if (ret < 0) {
                perror("write uart failed");
                break;
            }
            printf("[Send %d bytes]: %s", ret, buf);
        }
    }

    // 关闭串口
    close(fd);
    printf("\nUART tool exit\n");
    return 0;
}

4.3 代码核心亮点

  1. 通用可移植:标准 POSIX 接口,所有 Linux 平台通用,可直接用于 ARM、x86 等架构;
  2. 参数完整:支持常用波特率、数据位、停止位、校验位配置,满足绝大多数场景;
  3. 原始模式:通过cfmakeraw配置为原始模式,数据原样收发,适合模块通信;
  4. 多路复用:使用select同时监听串口和键盘输入,实现交互式收发,无需多线程;
  5. 非阻塞超时:配置读取超时,避免 read 阻塞卡死,稳定性强。

4.4 交叉编译与运行测试

  1. 交叉编译(ARM 架构):

bash

运行

arm-linux-gnueabihf-gcc uart_tool.c -o uart_tool
  1. 拷贝到板卡

bash

运行

scp uart_tool root@192.168.1.100:/opt/app/
  1. 运行测试

bash

运行

# 赋予执行权限
chmod +x uart_tool
# 运行,指定串口和波特率
./uart_tool /dev/ttymxc2 115200
  1. 测试方法
    • 接 USB 转 TTL 到电脑,电脑端打开串口助手,波特率 115200;
    • 电脑端发送数据,板卡终端会打印接收内容;
    • 板卡终端输入内容回车,电脑端串口助手会收到数据。

五、串口开发常见问题排查

串口开发的问题 90% 集中在硬件、配置、参数匹配三个方面,以下是嵌入式开发中最高频的问题及排查方案。

5.1 问题 1:串口设备节点不存在

现象ls /dev/ttySx无对应设备,打开串口提示No such file or directory排查方案

  1. 检查设备树中 UART 节点的status是否为okay,设备树是否重新烧录生效;
  2. 检查 UART 控制器驱动是否编译进内核或加载为模块(lsmod | grep uart);
  3. 确认串口命名规则:RK 平台为ttySx,IMX 平台为ttymxcx,全志为ttySx
  4. 查看内核启动日志:dmesg | grep uart,看是否有串口初始化失败的报错。

5.2 问题 2:接收数据乱码,发送数据对方识别不了

现象:能收到数据,但都是乱码、方块、无意义字符。 核心原因:收发双方波特率 / 参数不匹配,这是最常见原因。 排查方案

  1. 严格核对收发双方的 5 个参数:波特率、数据位、停止位、校验位、流控,必须完全一致;
  2. 检查串口时钟配置:设备树中 UART 的时钟源是否正确,时钟频率偏差会导致波特率不准;
  3. 检查电平匹配:确认是 TTL 电平还是 RS232 电平,错接会导致乱码甚至烧毁芯片;
  4. 降低波特率测试:先降到 9600 测试,排除高速下的硬件干扰问题。

5.3 问题 3:只能发不能收,或只能收不能发

现象:一方发送正常,另一方收不到,单向通信异常。 排查方案

  1. 检查 TX/RX 是否接反:板卡 TX 接对方 RX,板卡 RX 接对方 TX,交叉连接;
  2. 检查引脚复用配置:确认 TX/RX 引脚都配置为 UART 功能,不是 GPIO 模式;
  3. 回环测试验证:短接自身 TX/RX,自发自收,判断是发送通路还是接收通路问题;
  4. 检查硬件引脚:用万用表测量 TX 引脚电平,空闲时应为高电平,发送时有电平变化。

5.4 问题 4:大数据量传输丢包

现象:少量数据正常,连续发送大数据时出现丢字节。 排查方案

  1. 检查应用读取是否及时:用户态处理太慢导致内核缓冲区溢出,可增大读取频率或使用多线程;
  2. 开启硬件流控:如果支持 RTS/CTS,开启硬件流控防止溢出;
  3. 降低波特率:波特率过高时处理不及时容易丢包;
  4. 查看内核缓冲区:通过stty -a查看缓冲区大小,必要时调整内核参数增大缓冲区。

5.5 问题 5:打开串口权限不足

现象:打开串口提示Permission denied解决方案

  1. 临时:以 root 用户运行,或修改设备权限:chmod 666 /dev/ttymxc2
  2. 永久:添加 udev 规则,在/etc/udev/rules.d/下创建99-uart.rules

    plaintext

    KERNEL=="ttymxc[0-9]*", MODE="0666"
    
    重启 udev:udevadm control --reload-rules && udevadm trigger
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐