--- 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 串口通信概述 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第六十三章内容,为扩展知识(原书本章重点为 UART 驱动框架、设备树配置与 minicom 测试)。 ### 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 串口驱动框架图 ```mermaid 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_driver`、`uart_port`、`uart_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 — 串口驱动描述 ```c // 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 — 串口端口描述 ```c // 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 — 串口操作函数集 ```c // 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`中的定义: ```dts uart3: serial@021ec000 { compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart", "fsl,imx21-uart"; reg = <0x021ec000 0x4000>; interrupts = ; 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_DATA`和`UART3_RX_DATA`两个引脚: ```dts /* 在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`)中: ```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 ```c int uart_register_driver(struct uart_driver *drv); void uart_unregister_driver(struct uart_driver *drv); ``` 驱动入口函数中注册`uart_driver`,出口函数中注销: ```c 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 ```c 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()`将端口绑定到驱动: ```c 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驱动注册框架 ```c /* 设备树匹配表 */ 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驱动: ```mermaid 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初始化 ```c 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 — 端口初始化 ```c 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 — 底层操作函数集 ```c 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): ```dts &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连接到同一控制线,省掉了额外的GPIO。原书 63.3 节说明:SP3485 的 RO 为数据输出端、DI 为数据输入端,RE 是接收使能(低有效)、DE 是发送使能(高有效);原理图中 **RE 和 DE 经过一系列电路最终由 RS485_RX 信号控制**,从而把 RS485 完全当作串口使用: ``` MCU UART3_TX ──▶ SP3485 DI(数据输入) MCU UART3_RX ◀── SP3485 RO(数据输出) UART3_RX 经外围电路 ─┬─▶ SP3485 DE(发送使能,高有效) └─▶ SP3485 /RE(接收使能,低有效) ▲ │ 硬件电路自动切换方向 │ RS485总线 A/B ``` 关键:RE和DE通过电路设计连接到同一信号,发送时DE高电平使能发送,接收时RE低电平使能接收。这种设计将RS485完全当作串口使用,无需软件控制方向。 RS485在驱动中的配置: ```c // 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` | 发送完成后发送总线终止信号 | **`struct serial_rs485` 定义**(`include/uapi/linux/serial.h`,Linux 4.1.15): ```c struct serial_rs485 { __u32 flags; /* RS485 功能标志 */ #define SER_RS485_ENABLED (1 << 0) #define SER_RS485_RTS_ON_SEND (1 << 1) #define SER_RS485_RTS_AFTER_SEND (1 << 2) #define SER_RS485_RX_DURING_TX (1 << 4) __u32 delay_rts_before_send; /* 发送前 RTS 延迟(ms) */ __u32 delay_rts_after_send; /* 发送后 RTS 延迟(ms) */ __u32 padding[5]; /* 预留,动态分配内存用 */ }; ``` > 原书示例代码 63.2.5 中,驱动侧设置 `sport->port.rs485.flags = SER_RS485_RTS_ON_SEND | SER_RS485_RX_DURING_TX;`。`SER_RS485_TERMINATE_BUS` 等标志在较新内核中才定义。 用户空间可通过`ioctl()`配置RS485参数: ```c #include 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.0 移植 ncurses 与 minicom(原书 63.5 节) 原书使用 **minicom 2.7.1** 测试串口,它依赖 **ncurses 6.0**。 **1) 移植 ncurses**(`ncurses-6.0.tar.gz`): ```bash tar -vxzf ncurses-6.0.tar.gz ./configure --prefix=/home/zuozhongkai/linux/IMX6ULL/tool/ncurses \ --host=arm-linux-gnueabihf --target=arm-linux-gnueabihf \ --with-shared --without-profile --disable-stripping \ --without-progs --with-manpages --without-tests make make install # 将 include/lib/share 分别拷到根文件系统 /usr/include、/usr/lib、/usr/share sudo cp lib/* /home/zuozhongkai/linux/nfs/rootfs/usr/lib/ -rfa sudo cp share/* /home/zuozhongkai/linux/nfs/rootfs/usr/share/ -rfa sudo cp include/* /home/zuozhongkai/linux/nfs/rootfs/usr/include/ -rfa ``` 在根文件系统 `/etc/profile`(没有则新建)中加入: ```sh #!/bin/sh LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH export TERM=vt100 export TERMINFO=/usr/share/terminfo ``` **2) 移植 minicom**(`minicom-2.7.1.tar.gz`): ```bash tar -vxzf minicom-2.7.1.tar.gz cd minicom-2.7.1/ ./configure CC=arm-linux-gnueabihf-gcc \ --prefix=/home/zuozhongkai/linux/IMX6ULL/tool/minicom \ --host=arm-linux-gnueabihf \ CPPFLAGS=-I/home/zuozhongkai/linux/IMX6ULL/tool/ncurses/include \ LDFLAGS=-L/home/zuozhongkai/linux/IMX6ULL/tool/ncurses/lib \ --enable-cfg-dir=/etc/minicom make make install sudo cp bin/* /home/zuozhongkai/linux/nfs/rootfs/usr/bin/ ``` **3) 新建 `/etc/passwd`**:如果执行 `minicom -s` 提示 “Go away”,在根文件系统新建 `/etc/passwd` 并写入下面内容后重启: ``` root:x:0:0:root:/root:/bin/sh ``` 完成后 `minicom -v` 应显示版本 2.7.1,`minicom -s` 可正常打开配置界面。 ### 6.1 stty命令 — 串口参数配置 > ⚠️ **来源说明**:6.1~6.2 节的 stty/picocom/echo 用法不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(原书使用 minicom 测试)。 ```bash # 查看当前串口参数 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 — 串口调试工具 ```bash # 配置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 — 轻量级串口工具 ```bash # 打开串口 picocom -b 115200 /dev/ttymxc2 # 退出(Ctrl+A → Ctrl+X) ``` #### 直接读写测试 ```bash # 发送数据 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数据: ```bash # 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 跨平台对比 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。 ### 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 面试精选 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。 ### 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()`中设置: ```c 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_driver`的`probe()`函数被调用 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参数: ```c #include 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切换为接收模式。 --- **内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第六十三章 Linux RS232/485/GPS驱动实验 **最后更新**: 2026-09-17