05-RS232与485通信.md 29 KB


title: "Linux RS232与485通信驱动" tags:

  • embedded-linux
  • uart
  • rs232
  • rs485
  • serial-driver
  • imx6ull created: 2026-09-17 updated: 2026-09-17 pdf_ref: "69_第六十三章 Linux RS232_485_GPS驱动实验.pdf" ---

Linux RS232与485通信驱动

串口是嵌入式系统中最常用的通信外设之一。Linux下通过串口驱动框架实现对UART的统一管理,无论底层电平是TTL、RS232还是RS485,驱动程序的本质都是UART驱动。


1 串口通信概述

1.1 RS232 vs RS485 vs TTL

特性 TTL电平 RS232 RS485
电平范围 0V / 3.3V或5V -3V~-15V / +3V~+15V 差分信号 ±2V~±6V
传输距离 <1m <15m <1200m
通信方式 单端 单端 差分
最大速率 取决于MCU 115200bps 10Mbps
抗干扰能力 一般
网络拓扑 点对点 点对点 多点总线
典型芯片 MCU内置UART MAX232/SP3232 MAX485/SP3485

1.2 电平转换原理

┌──────────┐    TTL电平    ┌─────────────┐   RS232电平   ┌──────────┐
│  MCU     │ ──────────▶  │  SP3232     │ ──────────▶   │  PC/DB9  │
│  UART    │ ◀──────────  │  电平转换    │ ◀──────────   │  串口    │
└──────────┘   0/3.3V     └─────────────┘   ±12V       └──────────┘

┌──────────┐    TTL电平    ┌─────────────┐   RS485差分   ┌──────────┐
│  MCU     │ ──────────▶  │  SP3485     │ ──────────▶   │  总线    │
│  UART    │ ◀──────────  │  差分转换    │ ◀──────────   │  A/B线   │
└──────────┘              └─────────────┘              └──────────┘
  • TTL电平:MCU原生UART信号,0V表示逻辑0,3.3V/5V表示逻辑1
  • RS232电平:通过SP3232芯片将TTL转换为±12V负逻辑,抗干扰能力强于TTL
  • RS485电平:通过SP3485芯片将TTL转换为差分信号(A-B),抗共模干扰能力强,适合远距离传输

1.3 串口参数

串口通信需要双方约定相同的参数:

  • 波特率(Baud Rate):每秒传输的符号数,常见值:9600、19200、38400、57600、115200
  • 数据位(Data Bits):每帧数据的有效位数,通常为5、6、7、8位(最常用8位)
  • 停止位(Stop Bits):每帧数据结束标志,可选1、1.5、2位(最常用1位)
  • 校验位(Parity Bit):差错检测机制
    • 无校验(None):不使用校验位
    • 奇校验(Odd):数据位+校验位中1的个数为奇数
    • 偶校验(Even):数据位+校验位中1的个数为偶数

串口帧格式示例(8N1 = 8数据位 + 无校验 + 1停止位):

起始位(0) | D0 | D1 | D2 | D3 | D4 | D5 | D6 | D7 | 停止位(1)
   1bit   ─────── 8bit数据 ───────                   1bit

2 Linux串口架构

2.1 串口驱动框架图

graph TD
    A[用户空间] -->|"open()/read()/write()"| B[/dev/ttymxcX]
    B --> C[tty_core]
    C --> D[tty_operations]
    C --> E[uart_core]
    E --> F[uart_driver]
    E --> G[uart_port]
    E --> H[uart_ops]
    F -->|"注册/注销"| I[串口驱动层]
    G -->|"绑定到driver"| I
    H -->|"底层操作回调"| I
    I --> J[硬件UART寄存器]

2.2 tty_driver框架

Linux串口驱动采用分层架构:

  • tty_core:tty核心层,提供字符设备接口,用户空间通过/dev/ttymxcX访问
  • uart_core:串口核心层,封装通用UART操作,提供uart_driveruart_portuart_ops三要素
  • 硬件驱动层:具体SOC厂商实现,如NXP的imx.c,直接操作UART寄存器

用户空间调用链:

open("/dev/ttymxc2") → tty_open() → uart_open() → startup() → imx_startup()
write(fd, data)      → tty_write() → uart_write() → start_tx() → imx_start_tx()
read(fd, buf)        → tty_read()  → uart_read()  → 等待中断
close(fd)            → tty_release() → uart_close() → shutdown() → imx_shutdown()

2.3 核心数据结构

uart_driver — 串口驱动描述

// include/linux/serial_core.h
struct uart_driver {
    struct module    *owner;         // 模块所属者
    const char       *driver_name;   // 驱动名字(内核内部名)
    const char       *dev_name;      // 设备名字前缀(如"ttymxc")
    int              major;          // 主设备号
    int              minor;          // 次设备号起始值
    int              nr;             // 支持的最大端口数
    struct console   *cons;          // 控制台(可选)
    struct uart_state *state;        // 内部状态数组
    struct tty_driver *tty_driver;   // 关联的tty驱动
};

uart_port — 串口端口描述

// include/linux/serial_core.h (简化)
struct uart_port {
    spinlock_t         lock;         // 端口自旋锁
    unsigned long      iobase;       // I/O端口基地址(x86)
    unsigned char __iomem *membase;  // 内存映射基地址(ARM常用)
    unsigned int       type;         // 端口类型(PORT_IMX等)
    unsigned int       iotype;       // 访问方式(UPIO_MEM等)
    unsigned int       irq;          // 中断号
    unsigned int       fifosize;     // FIFO大小
    unsigned int       line;         // 端口索引(0,1,2...)
    const struct uart_ops *ops;      // 操作函数集
    unsigned int       uartclk;      // 时钟频率
    resource_size_t    mapbase;      // 物理基地址(ioremap用)
    struct device      *dev;         // 父设备
    struct serial_rs485 rs485;       // RS485配置参数
    int (*rs485_config)(struct uart_port *, struct serial_rs485 *);
};

uart_ops — 串口操作函数集

// include/linux/serial_core.h
struct uart_ops {
    unsigned int  (*tx_empty)(struct uart_port *);      // 发送FIFO是否为空
    void          (*set_mctrl)(struct uart_port *, unsigned int); // 设置Modem控制
    unsigned int  (*get_mctrl)(struct uart_port *);     // 获取Modem状态
    void          (*stop_tx)(struct uart_port *);       // 停止发送
    void          (*start_tx)(struct uart_port *);      // 启动发送
    void          (*stop_rx)(struct uart_port *);       // 停止接收
    void          (*enable_ms)(struct uart_port *);     // 使能Modem状态中断
    void          (*break_ctl)(struct uart_port *, int); // Break信号控制
    int           (*startup)(struct uart_port *);       // 启动串口(申请中断、使能时钟等)
    void          (*shutdown)(struct uart_port *);      // 关闭串口(释放资源)
    void          (*flush_buffer)(struct uart_port *);  // 清空发送缓冲区
    void          (*set_termios)(struct uart_port *, struct ktermios *, struct ktermios *); // 设置波特率等参数
    const char    *(*type)(struct uart_port *);         // 返回端口类型字符串
    void          (*release_port)(struct uart_port *);  // 释放I/O资源
    int           (*request_port)(struct uart_port *);  // 请求I/O资源
    void          (*config_port)(struct uart_port *, int); // 配置端口
    int           (*verify_port)(struct uart_port *, struct serial_struct *); // 验证端口参数
};

3 串口设备树配置

3.1 UART设备节点格式

I.MX6U的UART3在imx6ull.dtsi中的定义:

uart3: serial@021ec000 {
    compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart", "fsl,imx21-uart";
    reg = <0x021ec000 0x4000>;
    interrupts = <GIC_SPI 28 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6UL_CLK_UART3_IPG>,
             <&clks IMX6UL_CLK_UART3_SERIAL>;
    clock-names = "ipg", "per";
    dmas = <&sdma 29 4 0>, <&sdma 30 4 0>;
    dma-names = "rx", "tx";
    status = "disabled";
};

属性说明:

属性 说明
compatible 匹配字符串,驱动通过此属性查找匹配的platform驱动
reg 寄存器基地址和大小(0x021EC000为UART3基地址)
interrupts 中断号和触发方式
clocks 时钟源(IPG总线时钟和PER外设时钟)
dma DMA通道配置(UART3使用SDMA通道29/30)
status 状态,"disabled"表示默认关闭,需要在板级设备树中使能

3.2 pinctrl引脚配置

UART3使用UART3_TX_DATAUART3_RX_DATA两个引脚:

/* 在iomuxc节点下添加 */
pinctrl_uart3: uart3grp {
    fsl,pins = <
        MX6UL_PAD_UART3_TX_DATA__UART3_DCE_TX  0x1b0b1
        MX6UL_PAD_UART3_RX_DATA__UART3_DCE_RX  0x1b0b1
    >;
};

pinctrl引脚电气属性值说明:

位段 含义
bit[16] HYS滞回使能(1=使能)
bit[15:14] Pull/Keeper选择
bit[13] Pull保留使能(1=上拉)
bit[12:11] Pull/Keeper强度
bit[10] Open Drain使能
bit[7:6] 驱动强度
bit[3:0] 压摆率

0x1b0b1对应的配置:上拉使能、驱动强度适中、压摆率适中。

3.3 完整设备树配置示例

在板级设备树(如imx6ull-alientek-emmc.dts)中:

/* 删除uart2节点(与UART3引脚冲突) */
/delete-node/uart2;

/* 使能UART3 */
&uart3 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart3>;
    status = "okay";
};

系统启动后会生成设备节点:/dev/ttymxc2(UART3对应第3个串口,索引从0开始)。

设备号对应关系:

UART 设备文件 索引
UART1 /dev/ttymxc0 0
UART2 /dev/ttymxc1 1
UART3 /dev/ttymxc2 2
UART4 /dev/ttymxc3 3
UART5 /dev/ttymxc4 4

4 串口驱动API

4.1 uart_register_driver / uart_unregister_driver

int uart_register_driver(struct uart_driver *drv);
void uart_unregister_driver(struct uart_driver *drv);

驱动入口函数中注册uart_driver,出口函数中注销:

static struct uart_driver imx_reg = {
    .owner          = THIS_MODULE,
    .driver_name    = "IMX",           // 内核内部驱动名
    .dev_name       = "ttymxc",        // /dev/ttymxcX的前缀
    .major          = SERIAL_IMX_MAJOR, // 主设备号(通常253)
    .minor          = MINOR_START,      // 次设备号起始(通常64)
    .nr             = ARRAY_SIZE(imx_ports), // 支持的最大端口数
    .cons           = IMX_CONSOLE,      // 控制台(可选)
};

static int __init imx_serial_init(void)
{
    int ret = uart_register_driver(&imx_reg);  // 注册uart_driver
    if (ret)
        return ret;
    ret = platform_driver_register(&serial_imx_driver); // 注册platform驱动
    if (ret != 0)
        uart_unregister_driver(&imx_reg);
    return ret;
}

static void __exit imx_serial_exit(void)
{
    platform_driver_unregister(&serial_imx_driver);
    uart_unregister_driver(&imx_reg);
}

module_init(imx_serial_init);
module_exit(imx_serial_exit);

4.2 uart_add_one_port / uart_remove_one_port

int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport);
int uart_remove_one_port(struct uart_driver *drv, struct uart_port *uport);
参数 说明
drv 对应的uart_driver(如imx_reg)
uport 要添加/移除的uart_port
返回值 0成功,负值失败

在platform驱动的probe()中调用uart_add_one_port()将端口绑定到驱动:

static int serial_imx_probe(struct platform_device *pdev)
{
    struct imx_port *sport;
    // ... 初始化sport ...
    sport->port.ops = &imx_pops;      // 设置操作函数集
    return uart_add_one_port(&imx_reg, &sport->port);
}

4.3 I.MX6ULL platform驱动注册框架

/* 设备树匹配表 */
static const struct of_device_id imx_uart_dt_ids[] = {
    { .compatible = "fsl,imx6q-uart", .data = &imx_uart_devdata[IMX6Q_UART], },
    { .compatible = "fsl,imx1-uart",  .data = &imx_uart_devdata[IMX1_UART], },
    { .compatible = "fsl,imx21-uart", .data = &imx_uart_devdata[IMX21_UART], },
    { /* sentinel */ }
};

/* platform驱动结构体 */
static struct platform_driver serial_imx_driver = {
    .probe    = serial_imx_probe,
    .remove   = serial_imx_remove,
    .suspend  = serial_imx_suspend,
    .resume   = serial_imx_resume,
    .id_table = imx_uart_devtype,
    .driver   = {
        .name  = "imx-uart",
        .of_match_table = imx_uart_dt_ids,
    },
};

4.4 API参数说明表

函数 用途 调用时机
uart_register_driver() 注册uart_driver到内核 模块init
uart_unregister_driver() 从内核注销uart_driver 模块exit
uart_add_one_port() 将uart_port绑定到uart_driver platform probe
uart_remove_one_port() 将uart_port从uart_driver解绑 platform remove

5 完整源码分析

5.1 I.MX6ULL串口驱动结构

I.MX6ULL的UART驱动位于drivers/tty/serial/imx.c,本质上是一个platform驱动:

classDiagram
    class uart_driver {
        +owner: module*
        +driver_name: char*
        +dev_name: char*
        +major: int
        +minor: int
        +nr: int
    }
    class uart_port {
        +lock: spinlock_t
        +membase: void*
        +type: unsigned int
        +irq: unsigned int
        +fifosize: unsigned int
        +ops: uart_ops*
        +rs485: serial_rs485
    }
    class uart_ops {
        +startup()
        +shutdown()
        +start_tx()
        +stop_tx()
        +set_termios()
        +set_mctrl()
    }
    class imx_port {
        +port: uart_port
        +timer: timer_list
        +clk_ipg: clk*
        +clk_per: clk*
    }
    uart_driver "1" --> "*" uart_port : 绑定多个端口
    uart_port --> uart_ops : ops指针
    imx_port --|> uart_port : 继承

5.2 imx_reg — uart_driver初始化

static struct uart_driver imx_reg = {
    .owner          = THIS_MODULE,
    .driver_name    = DRIVER_NAME,      // "IMX"
    .dev_name       = DEV_NAME,         // "ttymxc"
    .major          = SERIAL_IMX_MAJOR,  // 主设备号
    .minor          = MINOR_START,       // 次设备号起始值
    .nr             = ARRAY_SIZE(imx_ports), // 最大端口数
    .cons           = IMX_CONSOLE,       // 控制台
};

5.3 serial_imx_probe — 端口初始化

static int serial_imx_probe(struct platform_device *pdev)
{
    struct imx_port *sport;
    void __iomem *base;
    int ret = 0;
    struct resource *res;
    int txirq, rxirq, rtsirq;

    // 1. 分配imx_port内存
    sport = devm_kzalloc(&pdev->dev, sizeof(*sport), GFP_KERNEL);
    if (!sport)
        return -ENOMEM;

    // 2. 从设备树获取DT配置(compatible等)
    ret = serial_imx_probe_dt(sport, pdev);
    if (ret > 0)
        serial_imx_probe_pdata(sport, pdev);  // 传统方式
    else if (ret < 0)
        return ret;

    // 3. 获取寄存器基地址并进行内存映射
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    // 4. 获取中断号(RX/TX/RTS)
    rxirq = platform_get_irq(pdev, 0);
    txirq = platform_get_irq(pdev, 1);
    rtsirq = platform_get_irq(pdev, 2);

    // 5. 初始化uart_port成员
    sport->port.dev    = &pdev->dev;
    sport->port.mapbase = res->start;         // 物理基地址
    sport->port.membase = base;               // 虚拟基地址
    sport->port.type   = PORT_IMX;            // 端口类型
    sport->port.iotype = UPIO_MEM;            // 内存映射IO
    sport->port.irq    = rxirq;               // 中断号
    sport->port.fifosize = 32;                // FIFO大小32字节
    sport->port.ops    = &imx_pops;           // 操作函数集
    sport->port.flags  = UPF_BOOT_AUTOCONF;   // 自动波特率检测

    // 6. 配置RS485参数
    sport->port.rs485_config = imx_rs485_config;
    sport->port.rs485.flags =
        SER_RS485_RTS_ON_SEND | SER_RS485_RX_DURING_TX;

    // 7. 初始化定时器(超时检测)
    init_timer(&sport->timer);
    sport->timer.function = imx_timeout;
    sport->timer.data     = (unsigned long)sport;

    // 8. 获取时钟
    sport->clk_ipg = devm_clk_get(&pdev->dev, "ipg");
    sport->clk_per = devm_clk_get(&pdev->dev, "per");
    sport->port.uartclk = clk_get_rate(sport->clk_per);

    // 9. 申请中断
    if (txirq > 0) {
        devm_request_irq(&pdev->dev, rxirq, imx_rxint, 0,
                         dev_name(&pdev->dev), sport);
        devm_request_irq(&pdev->dev, txirq, imx_txint, 0,
                         dev_name(&pdev->dev), sport);
    } else {
        devm_request_irq(&pdev->dev, rxirq, imx_int, 0,
                         dev_name(&pdev->dev), sport);
    }

    imx_ports[sport->port.line] = sport;
    platform_set_drvdata(pdev, sport);

    // 10. 将端口添加到uart_driver
    return uart_add_one_port(&imx_reg, &sport->port);
}

5.4 imx_pops — 底层操作函数集

static struct uart_ops imx_pops = {
    .tx_empty     = imx_tx_empty,      // 检查发送FIFO是否为空
    .set_mctrl    = imx_set_mctrl,     // 设置Modem控制(RTS/CTS/DTR等)
    .get_mctrl    = imx_get_mctrl,     // 获取Modem状态
    .stop_tx      = imx_stop_tx,       // 停止发送
    .start_tx     = imx_start_tx,      // 启动发送
    .stop_rx      = imx_stop_rx,       // 停止接收
    .enable_ms    = imx_enable_ms,     // 使能Modem状态中断
    .break_ctl    = imx_break_ctl,     // Break信号控制
    .startup      = imx_startup,       // 启动串口(使能时钟、申请中断)
    .shutdown     = imx_shutdown,      // 关闭串口(释放资源)
    .flush_buffer = imx_flush_buffer,  // 清空缓冲区
    .set_termios  = imx_set_termios,   // 设置波特率/数据位/停止位/校验位
    .type         = imx_type,          // 返回端口类型字符串
    .config_port  = imx_config_port,   // 配置端口类型
    .verify_port  = imx_verify_port,   // 验证端口参数合法性
};

5.5 RS232驱动配置

RS232在I.MX6U-ALPHA开发板上的硬件连接:

MCU UART3_TX ──▶ SP3232 TXDin ──▶ RS232 DB9 TXD
MCU UART3_RX ◀── SP3232 RXDout ◀── RS232 DB9 RXD
                    ▲
                    │ JP1跳线帽 1-3, 2-4 连接
                    │
MCU UART3_TX/RX ───┘

设备树配置(RS232无需额外驱动,只需使能UART3):

&uart3 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart3>;
    status = "okay";
};

JP1跳线帽设置:将1-3和2-4连接,使UART3通过SP3232芯片输出RS232电平。

5.6 RS485驱动配置(方向控制)

RS485是半双工通信,需要控制收发方向。I.MX6U-ALPHA开发板通过硬件巧妙地将RE和DE连接到同一控制线RS485_RX,省掉了额外的GPIO:

MCU UART3_TX ──▶ SP3485 DI(数据输入)
MCU UART3_RX ◀── SP3485 RO(数据输出)
MCU RTS/GPIO ──┬─▶ SP3485 DE(发送使能,高有效)
               └─▶ SP3485 /RE(接收使能,低有效)
                        ▲
                        │ 硬件电路自动切换方向
                        │
                   RS485总线 A/B

关键:RE和DE通过电路设计连接到同一信号,发送时DE高电平使能发送,接收时RE低电平使能接收。这种设计将RS485完全当作串口使用,无需软件控制方向。

RS485在驱动中的配置:

// serial_imx_probe中的RS485配置
sport->port.rs485_config = imx_rs485_config;
sport->port.rs485.flags =
    SER_RS485_RTS_ON_SEND |    // 发送时RTS置高
    SER_RS485_RX_DURING_TX;    // 发送期间仍使能接收

RS485标志位说明:

标志位 说明
SER_RS485_ENABLED 使能RS485模式
SER_RS485_RTS_ON_SEND 发送时RTS为高(控制方向)
SER_RS485_RTS_AFTER_SEND 发送完成后RTS为高
SER_RS485_RX_DURING_TX 发送期间允许接收
SER_RS485_TERMINATE_BUS 发送完成后发送总线终止信号

用户空间可通过ioctl()配置RS485参数:

#include <linux/serial.h>

struct serial_rs485 rs485conf;
int fd = open("/dev/ttymxc2", O_RDWR);

// 使能RS485,发送时RTS高电平
rs485conf.flags |= SER_RS485_ENABLED;
rs485conf.flags |= SER_RS485_RTS_ON_SEND;
ioctl(fd, TIOCSRS485, &rs485conf);

// 读取RS485配置
ioctl(fd, TIOCGRS485, &rs485conf);

6 实验验证

6.1 stty命令 — 串口参数配置

# 查看当前串口参数
stty -F /dev/ttymxc2 -a

# 设置波特率115200,8数据位,1停止位,无校验
stty -F /dev/ttymxc2 115200 cs8 -cstopb -parenb

# 设置波特率9600
stty -F /dev/ttymxc2 9600

# 关闭硬件流控
stty -F /dev/ttymxc2 crtscts -crtscts

# 关闭软件流控
stty -F /dev/ttymxc2 ixon -ixon ixoff -ixoff

常用stty选项:

选项 说明
cs8 / cs7 8/7数据位
cstopb / -cstopb 2/1停止位
parenb / -parenb 使能/禁用校验
parodd / -parodd 奇/偶校验
crtscts / -crtscts 使能/禁用硬件流控
ixon / -ixon 使能/禁用XON/XOFF软件流控

6.2 串口收发测试

minicom — 串口调试工具

# 配置minicom
minicom -s

# 直接打开串口(波特率115200)
minicom -D /dev/ttymxc2 -b 115200

# 打开回显(Ctrl+A → E)
# 退出minicom(Ctrl+A → X)

minicom配置要点:

  1. 选择串口设备:/dev/ttymxc2
  2. 设置波特率:115200
  3. 数据位:8,停止位:1,校验:无
  4. 关闭硬件流控和软件流控

picocom — 轻量级串口工具

# 打开串口
picocom -b 115200 /dev/ttymxc2

# 退出(Ctrl+A → Ctrl+X)

直接读写测试

# 发送数据
echo "Hello RS232" > /dev/ttymxc2

# 接收数据(阻塞)
cat /dev/ttymxc2

# 后台接收并保存
cat /dev/ttymxc2 > /tmp/uart_data.log &

6.3 RS485测试要点

RS485测试与RS232完全一致,区别仅在于:

  1. JP1跳线帽设置为3-5和4-6连接(RS485模式)
  2. 需要另一个RS485设备(如USB转RS485转换器)
  3. A接A,B接B,不能接反
  4. 通信参数(波特率等)必须一致

6.4 GPS模块测试

GPS模块通过UART3输出NMEA数据:

# GPS模块默认波特率38400
minicom -D /dev/ttymxc2 -b 38400

# 或使用picocom
picocom -b 38400 /dev/ttymxc2

NMEA数据格式示例:

$GPGGA,062219.00,3150.7892,N,11711.9538,E,1,08,1.03,45.6,M,-3.3,M,,*67
$GPRMC,062219.00,A,3150.7892,N,11711.9538,E,0.05,31.5,170926,,,A*64

7 跨平台对比

7.1 IMX6ULL vs STM32 vs RK3568

特性 IMX6ULL STM32F4xx RK3568
UART数量 8个 6个 6个
FIFO深度 32字节 128字节 32字节
DMA支持 SDMA DMA1/DMA2 DMA
设备树驱动 否(HAL库)
RS485硬件支持 RTS/DE自动切换 需要额外GPIO RTS/DE自动切换
时钟配置 ipg+per双时钟 APB1/APB2 bus_clk+fun_clk
驱动文件 drivers/tty/serial/imx.c stm32-usart.c drivers/tty/serial/rk_tty.c

7.2 驱动差异要点

IMX6ULL

  • 使用platform驱动框架,设备树匹配
  • uart_clock分为ipg和per两个时钟
  • 支持SDMA,适合高吞吐量场景
  • RS485通过硬件自动控制方向

STM32

  • 无Linux设备树,使用HAL库或标准外设库
  • USART/UART区分(USART支持同步通信)
  • FIFO可配置为16或128字节
  • RS485方向控制通常需要额外GPIO

RK3568

  • 设备树驱动,架构类似IMX6ULL
  • UART时钟分为bus和fun两个
  • 支持I2S模式(可复用为I2S音频接口)
  • RS485支持硬件自动方向控制

8 面试精选

Q1: Linux串口驱动中uart_driver、uart_port、uart_ops三者的关系?

:三者是"驱动-端口-操作"的分层关系:

  • uart_driver描述串口驱动整体信息(设备号、设备名、最大端口数),一个驱动管理多个端口
  • uart_port描述具体端口的硬件资源(寄存器地址、中断号、FIFO大小),一个端口绑定到一个驱动
  • uart_ops是操作函数集,定义了串口的底层操作(启动发送、设置波特率等),每个端口通过ops指针指向它

关系链:uart_driver管理N个uart_port,每个uart_port通过ops指针调用uart_ops中的函数。在platform驱动的probe()中,通过uart_add_one_port()将端口绑定到驱动。

Q2: RS485方向控制是如何实现的?软控制和硬控制有什么区别?

硬控制(本方案):将DE和RE连接到同一信号线(如RTS),驱动在发送时自动拉高该信号切换为发送模式,发送完成自动拉低切换为接收模式。优点:无需软件干预,延迟小。

软控制:通过额外GPIO控制DE/RE引脚,在start_tx()中拉高GPIO,在发送完成中断中拉低GPIO。缺点:软件延迟可能导致总线冲突或数据丢失。

Linux内核通过serial_rs485结构体配置RS485方向控制。在serial_imx_probe()中设置:

sport->port.rs485_config = imx_rs485_config;
sport->port.rs485.flags = SER_RS485_RTS_ON_SEND;

用户空间通过ioctl(TIOCSRS485)配置RS485模式。

Q3: 串口驱动中startup()函数需要做哪些事情?

startup()是串口打开时调用的初始化函数,主要工作:

  1. 使能时钟:调用clk_enable()使能UART模块时钟(ipg和per)
  2. 复位FIFO:清空发送和接收FIFO
  3. 申请中断:调用request_irq()注册RX和TX中断处理函数
  4. 配置引脚:使能UART相关引脚的复用功能
  5. 使能接收:配置UART控制寄存器,使能接收中断
  6. RS485初始化(如果使用):配置RS485方向控制参数

对应的shutdown()需要做相反操作:释放中断、关闭时钟、禁用引脚。

Q4: /dev/ttymxc2设备文件是如何生成的?

:设备文件生成流程:

  1. 设备树中UART3节点的compatible = "fsl,imx6ul-uart"与驱动匹配
  2. platform_driverprobe()函数被调用
  3. probe()中调用uart_add_one_port()将端口添加到imx_reg驱动
  4. uart_add_one_port()内部调用tty_register_device()注册tty设备
  5. 内核根据imx_reg.dev_name("ttymxc")和端口索引(line=2)生成设备名
  6. udev/mdev根据内核事件创建/dev/ttymxc2设备文件

所以/dev/ttymxc2对应UART3(索引从0开始:UART1→ttymxc0,UART2→ttymxc1,UART3→ttymxc2)。

Q5: 如何从用户空间控制RS485的发送方向?

:通过ioctl()系统调用配置RS485参数:

#include <linux/serial.h>

int fd = open("/dev/ttymxc2", O_RDWR);
struct serial_rs485 rs485conf;

// 使能RS485模式
ioctl(fd, TIOCGRS485, &rs485conf);  // 先读取当前配置
rs485conf.flags |= SER_RS485_ENABLED;
rs485conf.flags |= SER_RS485_RTS_ON_SEND;  // 发送时RTS高
rs485conf.delay_rts_before_send = 1;       // 发送前延迟1ms
rs485conf.delay_rts_after_send = 1;        // 发送后延迟1ms
ioctl(fd, TIOCSRS485, &rs485conf);  // 写入配置

// 之后正常write()即可,驱动自动控制方向
write(fd, data, len);

驱动层面,rs485_config回调函数(如imx_rs485_config())根据用户配置设置UART控制寄存器中的RTS极性、发送/接收延迟等参数。start_tx()时驱动自动拉高RTS切换为发送模式,发送完成中断中自动拉低RTS切换为接收模式。