--- title: UART串口通信 tags: [UART, 串口, IMX6ULL, 裸机开发, 波特率, printf移植, RS232, RS485] created: 2026-09-16 updated: 2026-09-17 pdf_ref: - "正点原子 I.MX6U嵌入式Linux驱动开发指南V2.0.1 第二十一章 UART串口通信实验" - "正点原子 I.MX6U嵌入式Linux驱动开发指南V2.0.1 第二十二章 串口格式化函数移植实验" --- # UART串口通信 > 💡 **关联知识**: [[05-定时器与看门狗]] | [[04-GPIO与中断系统]] | [[07-BSP工程管理与封装]] | [[STM32学习笔记/06-串口USART通信]] | [[面试-ARM架构与裸机]] --- ## 一、UART 协议概述 ### 1.1 什么是 UART UART = **U**niversal **A**synchronous **R**eceiver/**T**ransmitter,通用异步串行收发器。它是串行接口的一种:数据一位一位地顺序传输,收发各用一条线,因此与外界相连**最少只需要三根线**:TXD(发送)、RXD(接收)、GND(地线)。 与之相对的是 USART(Universal **Synchronous**/Asynchronous Receiver/Transmitter),比 UART 多了一个同步模式,硬件上体现为多一条时钟线。USART 可以当作 UART 使用,也就是放弃同步功能。 I.MX6U 一共有 **8 个 UART**,本章使用 UART1。主要特性: | 特性 | 参数 | | ---------- | ---------------------------- | | 电平标准 | 兼容 TIA/EIA-232F | | 最高速率 | 5 Mbit/s | | 串行 IR | 兼容 IrDA,最高 115.2 Kbit/s | | RS-485 | 支持 9 位 / 多节点模式 | | 停止位 | 1 或 2 位 | | 校验 | 可编程奇校验 / 偶校验 | | 自动波特率 | 最高支持 115.2 Kbit/s | ### 1.2 数据帧格式 一次 UART 传输以「帧」为单位。空闲时数据线为逻辑 1(高电平),发送方先拉低一个位作为起始位,接收方检测到下降沿即开始同步。 ``` 空闲 起始 D0 D1 D2 D3 D4 D5 D6 D7 校验 停止 空闲 1 0 LSB ...................... MSB ? 1 1 └────────── 8 位有效数据 ──────────┘ ``` | 字段 | 位数 | 说明 | | ---------- | ----------- | ------------------------------------------------- | | 空闲位 | — | 逻辑 1(高电平),表示总线空闲无数据传输 | | 起始位 | 1 | 逻辑 0(拉低),标志一帧开始 | | 数据位 | 5~8 | 实际数据,**低位在前**先发,高位后发;常规取 8 位 | | 奇偶校验位 | 0 或 1 | 对数据中「1」的个数做校验,可关闭 | | 停止位 | 1 / 1.5 / 2 | 高电平,标志一帧结束,常规取 1 位 | 以最常见的 8N1 配置(8 数据位、无校验、1 停止位)为例,传输 1 字节实际需要 **10 个位时间**(1 起始 + 8 数据 + 1 停止),这也是后面计算波特率误差容限的关键前提。 ### 1.3 关键参数 | 参数 | 说明 | 常用值 | | ------ | ------------------------------------- | --------------------------------- | | 波特率 | 每秒传输的**位数**(bit/s),非字节数 | 9600、19200、38400、57600、115200 | | 数据位 | 每帧有效数据位数 | 8 | | 停止位 | 每帧停止位数 | 1 | | 校验位 | 奇偶校验模式 | 无(None) | | 流控 | RTS/CTS 硬件流控或 XON/XOFF 软件流控 | 一般关闭 | > ⚠️ 注意区分**波特率**与**比特率**:在 UART 这种二电平基带传输中两者数值相等,但概念不同——波特率衡量符号变化速率,比特率衡量信息速率。 ### 1.4 电平标准 同一种 UART 协议帧,底层电平可以不同: | 标准 | 逻辑 1 | 逻辑 0 | 特点 | | ------ | ----------------- | ------------ | --------------------------- | | TTL | 高电平(3.3V/5V) | 低电平(0V) | 板级直连,仅限短距离 | | RS-232 | −3 ~ −15 V | +3 ~ +15 V | 抗干扰、距离远,需 DB9 接口 | | RS-485 | 差分 A/B 线 | 差分反向 | 总线型、多节点、长距离 | I.MX6U-ALPHA 开发板的 COM3(UART3)是 RS-232 电平;开发板板载 **CH340** 芯片完成 UART1 到 USB 的转换,所以只需一条 USB 线即可连接电脑。由于现在电脑已无 DB9 口,CH340、PL2303 这类 USB 转 TTL 芯片非常常见。 ### 1.5 波特率误差分析 UART 没有时钟线,接收方靠**起始位的下降沿**重新对齐,然后在每个位时间的**中点**采样。如果收发双方波特率有偏差,采样点会逐位漂移: - 一帧 10 位(8N1),最坏情况是**第 10 位(停止位)**的采样偏差最大; - 采样点位于位中点,因此单边允许的漂移约为半个位时间; - 折算到整帧,收发双方波特率**合计**偏差一般不能超过 **±2% ~ ±3%**。 这解释了为什么正点原子移植自 NXP SDK 的 `uart_setbaudrate()` 用 `< 3%` 作为接受阈值(源码中即 `baudDiff < (baudrate / 100) * 3`)。实际工程中建议单侧误差控制在 ±2% 以内。 **误差计算示例(Ref Freq = 80 MHz,目标 115200):** | 计算项 | 数值 | | ---------------------- | ---------------------------------------------- | | 目标值 | 115200 | | UBIR = 71、UBMR = 3124 | 80 000 000 × (71+1) / (16 × (3124+1)) = 115200 | | 结论 | 误差 0%,完全匹配 | --- ## 二、I.MX6ULL UART 寄存器详解 ### 2.1 时钟源与时钟分频 UART 的时钟由 `CCM_CSCDR1` 决定,进入 UART 模块前可再经 `UFCR.RFDIV` 分频: | 寄存器 | 位域 | 名称 | 说明 | | ---------- | ------ | ------------- | ----------------------------------------------- | | CCM_CSCDR1 | bit6 | UART_CLK_SEL | 0 = pll3_80m(80 MHz);1 = osc_clk(24 MHz) | | CCM_CSCDR1 | bit5:0 | UART_CLK_PODF | 分频值 0~63,对应 1~64 分频,常规取 0(1 分频) | | UFCR | bit9:7 | RFDIV | 参考时钟二级分频,见下表 | **UFCR.RFDIV 分频表:** | RFDIV(bit9:7) | 分频值 | | ------------- | ------------------------------------- | | 000 | 6 分频 | | 001 | 5 分频 | | 010 | 4 分频 | | 011 | 3 分频 | | 100 | 2 分频 | | **101** | **1 分频**(常规,Ref Freq = 80 MHz) | | 110 | 7 分频 | | 111 | 保留 | 因此最终进入 UART 的参考时钟 `Ref Freq = pll3_80m / PODF / RFDIV = 80 MHz`。 此外,裸机例程 `clk_enable()` 直接把 `CCM_CCGR1` ~ `CCM_CCGR6` 全部置 `0xFFFFFFFF`,简单粗暴地打开所有时钟门控。正式驱动中应只打开 UART1 对应的 `CGx` 位(UART1 在 `CCM_CCGR1`)。 ### 2.2 寄存器映射(UART_Type) I.MX6U 的 UART 寄存器布局是**非连续**的:接收/发送寄存器各占 0x40 字节空间。以下结构体与 NXP SDK(`MCIMX6Y2.h`)定义一致: ```c /* UART 寄存器结构体(offset 与 I.MX6ULL 参考手册 55.15 节一致) */ typedef struct { volatile unsigned int URXD; /* 0x00 接收数据寄存器 */ unsigned char RESERVED_0[60]; volatile unsigned int UTXD; /* 0x40 发送数据寄存器 */ unsigned char RESERVED_1[60]; volatile unsigned int UCR1; /* 0x80 控制寄存器 1 */ volatile unsigned int UCR2; /* 0x84 控制寄存器 2 */ volatile unsigned int UCR3; /* 0x88 控制寄存器 3 */ volatile unsigned int UCR4; /* 0x8C 控制寄存器 4 */ volatile unsigned int UFCR; /* 0x90 FIFO 控制寄存器 */ volatile unsigned int USR1; /* 0x94 状态寄存器 1 */ volatile unsigned int USR2; /* 0x98 状态寄存器 2 */ volatile unsigned int UESC; /* 0x9C 逃逸字符寄存器 */ volatile unsigned int UTIM; /* 0xA0 逃逸定时器寄存器 */ volatile unsigned int UBIR; /* 0xA4 波特率增量寄存器 */ volatile unsigned int UBMR; /* 0xA8 波特率模数寄存器 */ volatile unsigned int UBRC; /* 0xAC 波特率计数寄存器 */ volatile unsigned int ONEMS; /* 0xB0 1ms 计数寄存器 */ volatile unsigned int UTS; /* 0xB4 UART 测试寄存器 */ volatile unsigned int UMCR; /* 0xB8 RS-485 模式控制寄存器 */ } UART_Type; #define UART1_BASE 0x02020000 #define UART2_BASE 0x021E8000 #define UART3_BASE 0x021EC000 #define UART4_BASE 0x021F0000 #define UART5_BASE 0x021F4000 #define UART6_BASE 0x021FC000 #define UART7_BASE 0x02018000 #define UART8_BASE 0x02288000 #define UART1 ((UART_Type *)UART1_BASE) #define UART2 ((UART_Type *)UART2_BASE) #define UART3 ((UART_Type *)UART3_BASE) #define UART4 ((UART_Type *)UART4_BASE) #define UART5 ((UART_Type *)UART5_BASE) #define UART6 ((UART_Type *)UART6_BASE) #define UART7 ((UART_Type *)UART7_BASE) #define UART8 ((UART_Type *)UART8_BASE) ``` ### 2.3 UCR1/USR2 中本章用到的位 | 寄存器 | 位 | 名称 | 说明 | | ------ | --- | --------- | ------------------------------------------------------------- | | UCR1 | 0 | UARTEN | 1 = 使能 UART,0 = 关闭 UART | | UCR1 | 14 | ADBR | 1 = 使能自动波特率检测,0 = 关闭 | | UCR2 | 0 | SRST | 写 0 触发软件复位;复位完成后硬件自动置 1,写 1 被忽略 | | UCR2 | 1 | RXEN | 1 = 打开接收 | | UCR2 | 2 | TXEN | 1 = 打开发送 | | UCR2 | 5 | WS | 1 = 8 位数据位,0 = 7 位 | | UCR2 | 6 | STPB | 1 = 2 位停止位,0 = 1 位 | | UCR2 | 7 | PROE | 校验模式:1 = 奇校验,0 = 偶校验 | | UCR2 | 8 | PREN | 1 = 使能奇偶校验 | | UCR2 | 14 | IRTS | 1 = 忽略 RTS 引脚(关闭硬件流控) | | UCR3 | 2 | RXDMUXSEL | **必须始终为 1**(参考手册 55.15 节明确要求) | | USR2 | 0 | RDR | 1 = 至少收到一个数据;读 URXD 后自动清零 | | USR2 | 3 | TXDC | 1 = TxFIFO 与移位寄存器均为空(发送完成);写 TxFIFO 自动清零 | ### 2.4 控制寄存器位域全表 **UCR1(0x80)** | 位 | 名称 | 说明 | | ----- | -------- | -------------------- | | 0 | UARTEN | UART 使能 | | 1 | DOZE | DOZE 低功耗模式 | | 2 | ATDMAEN | 老化定时器 DMA 使能 | | 3 | TXDMAEN | 发送 DMA 使能 | | 4 | SNDBRK | 发送 BREAK 字符 | | 5 | RTSDEN | RTS 差值中断使能 | | 6 | TXMPTYEN | 发送缓冲空中断使能 | | 7 | IREN | 红外接口使能 | | 8 | RXDMAEN | 接收 DMA 使能 | | 9 | RRDYEN | **接收就绪中断使能** | | 11:10 | ICD | 空闲条件检测 | | 12 | IDEN | 空闲中断使能 | | 13 | TRDYEN | **发送就绪中断使能** | | 14 | ADBR | 自动波特率检测使能 | | 15 | ADEN | 老化定时器使能 | **UCR2(0x84)** | 位 | 名称 | 说明 | | ------- | ----------- | ----------------------- | | 0 | SRST | 软件复位 | | 1 / 2 | RXEN / TXEN | 接收 / 发送使能 | | 3 | ATEN | 老化定时器使能 | | 4 | RTSEN | RTS 硬件流控使能 | | 5 | WS | 数据位长度 | | 6 | STPB | 停止位数量 | | 7 | PROE | 校验模式选择 | | 8 | PREN | 奇偶校验使能 | | 10:9 | RTEC | 接收触发结束条件 | | 11 | ESCEN | 逃逸序列使能 | | 12 / 13 | CTS / CTSC | CTS 状态 / CTS 流控控制 | | 14 | IRTS | 忽略 RTS 引脚 | | 15 | ESCI | 逃逸序列中断使能 | **UCR3(0x88)** | 位 | 名称 | 说明 | | ---------- | ------------------- | ------------------------- | | 0 | ACIEN | 自动波特率检测中断使能 | | 1 | INVT | 接收数据反相 | | 2 | RXDMUXSEL | 固定为 1 | | 3 | DTRDEN | DTR/DSR 中断使能 | | 4 / 5 | AWAKEN / AIRINTEN | AWAKE / 异步 IR 中断使能 | | 6 | RXDSEN | 接收 IDLE 中断使能 | | 7 | ADNIMP | 自动波特率检测不匹配 | | 8 / 9 / 10 | RI / DCD / DSR | 调制解调器输入状态 | | 11 / 12 | FRAERREN / PARERREN | 帧错误 / 奇偶错误中断使能 | | 13 | DTREN | DTR 中断使能 | | 15:14 | DPEC | DTR 边沿极性控制 | **UCR4(0x8C)** | 位 | 名称 | 说明 | | ----- | ------------ | ---------------------------- | | 0 | DREN | **数据就绪(接收)中断使能** | | 1 | OREN | 溢出中断使能 | | 2 | BKEN | BREAK 检测中断使能 | | 3 | TCEN | 发送完成中断使能 | | 4 | LPBYP | 低功耗旁路 | | 5 | IRSC | IR 采样时钟 | | 6 | IDDMAEN | 空闲 DMA 使能 | | 7 / 8 | WKEN / ENIRI | WAKE / IR 中断使能 | | 9 | INVR | 接收数据反相 | | 15:10 | CTSTL | CTS 触发水平 | ### 2.5 FIFO 控制寄存器 UFCR(0x90) UART 收发各带一个 32 级 FIFO,UFCR 负责设置触发水平与参考时钟分频: | 位 | 名称 | 说明 | | ----- | ------ | -------------------------------------------------- | | 5:0 | RXTL | RX FIFO 触发水平(0~32,达到即产生 RRDY/DMA 请求) | | 6 | DCEDTE | DCE/DTE 模式选择 | | 9:7 | RFDIV | 参考时钟分频(见 2.1 表) | | 15:10 | TXTL | TX FIFO 触发水平 | ### 2.6 状态寄存器位域 **USR1(0x94)** | 位 | 名称 | 说明 | | --- | --------- | ----------------------------- | | 3 | SAD | RS-485 从机地址检测 | | 8 | AGTIM | 老化定时器中断标志 | | 9 | RRDY | 接收就绪(FIFO 中已有数据) | | 10 | FRAMERR | 帧错误 | | 13 | TRDY | 发送就绪(FIFO 低于触发水平) | | 15 | PARITYERR | 奇偶校验错误 | **USR2(0x98)** | 位 | 名称 | 说明 | | --- | ---- | ---------------------------- | | 0 | RDR | 接收数据就绪(读 URXD 清零) | | 1 | ORE | 接收溢出错误 | | 2 | BRCD | BREAK 条件检测 | | 3 | TXDC | 发送完成 | | 4 | RTSF | RTS 边沿触发 | | 11 | ACST | 自动波特率计数停止 | | 12 | IDLE | 空闲状态 | | 14 | TXFE | 发送 FIFO 空 | | 15 | ADET | 自动波特率检测完成 | **URXD(0x00)**:低 8 位为接收数据,高 8 位携带错误标志(PRERR bit10、BRK bit11、FRMERR bit12、OVRRUN bit13、ERR bit14、CHARRDY bit15)。**UTXD(0x40)**:低 8 位为待发送数据。 ### 2.7 波特率设置寄存器 | 寄存器 | 位 | 名称 | 说明 | | ------ | ---- | ----------------------------- | --------------------------------------------------- | | UBIR | 15:0 | INC | 波特率增量(分子相关),**必须先写 UBIR 再写 UBMR** | | UBMR | 15:0 | MOD | 波特率模数(分母相关) | | UBRC | 15:0 | BCNT | 自动波特率计数器当前值(只读) | | ONEMS | 15:0 | — | 1ms 计数,用于老化定时器/UTIM | | UTS | 0 | SOFTRST | 测试寄存器软复位 | | UTS | 3~6 | RXFULL/TXFULL/RXEMPTY/TXEMPTY | 收发 FIFO 满/空(用于调试) | | UTS | 12 | LOOP | 内部回环测试模式 | **波特率计算公式(来自参考手册 55.15 节):** ``` Ref Freq Baud Rate = ───────────────────────────── 16 × (UBMR + 1) / (UBIR + 1) Ref Freq × (UBIR + 1) 等价形式: Baud Rate = ────────────────────────── 16 × (UBMR + 1) ``` - **Ref Freq**:经过 UFCR.RFDIV 分频后进入 UART 的最终时钟频率; - **UBIR / UBMR**:寄存器 UARTx_UBIR、UARTx_UBMR 中的值。 **115200 的推导过程:** | 步骤 | 取值 | 结果 | | ----- | ----------------------------- | --------------------------------------- | | RFDIV | 0b101(1 分频) | Ref Freq = 80 MHz | | UBIR | 71 | UBIR + 1 = 72 | | UBMR | 3124 | UBMR + 1 = 3125 | | 计算 | 80 000 000 / (16 × 3125 / 72) | = 80 000 000 × 72 / 50 000 = **115200** | **常用波特率参数(Ref Freq = 80 MHz,RFDIV = 101,按上式推导):** | 波特率 | UBIR | UBMR | 校验 | | ---------- | ------ | -------- | ----------------------- | | 4800 | 5 | 6249 | 80e6×6/(16×6250) = 4800 | | 9600 | 11 | 6249 | 80e6×12/100000 = 9600 | | 19200 | 23 | 6249 | 80e6×24/100000 = 19200 | | 38400 | 47 | 6249 | 80e6×48/100000 = 38400 | | 57600 | 71 | 6249 | 80e6×72/100000 = 57600 | | **115200** | **71** | **3124** | 80e6×72/50000 = 115200 | > 115200 有两组等价参数:(71, 6249) 与 (71, 3124)。正点原子例程使用后者,本质是把分子分母同时除以 2,比值不变。 --- ## 三、UART 驱动初始化 ### 3.1 初始化流程 ``` ① 使能时钟 CCM_CCGR1~6 = 0xFFFFFFFF(裸机例程在 clk_enable() 中完成) ② 配置 IO 复用 IOMUXC_SetPinMux(UART1_TX / UART1_RX) ③ 配置 IO 电气属性 IOMUXC_SetPinConfig(..., 0x10B0) ④ 关闭 UART UCR1 &= ~UARTEN ⑤ 软件复位 UART UCR2 &= ~SRST,轮询等待 SRST 自动置 1 ⑥ 清 UCR1,关自动波特率检测 ⑦ 配置 UCR2/UCR3 字长、停止位、校验、收发使能 ⑧ 配置波特率 UFCR.RFDIV + UBIR + UBMR ⑨ 使能 UART UCR1 |= UARTEN ``` ### 3.2 头文件 bsp_uart.h ```c #ifndef _BSP_UART_H #define _BSP_UART_H #include "imx6ul.h" /* 函数声明 */ void uart_init(void); void uart_io_init(void); void uart_disable(UART_Type *base); void uart_enable(UART_Type *base); void uart_softreset(UART_Type *base); void uart_setbaudrate(UART_Type *base, unsigned int baudrate, unsigned int srcclock_hz); void putc(unsigned char c); void puts(char *str); unsigned char getc(void); void raise(int sig_nr); #endif ``` | 函数 | 参数 | 作用 | | ------------------ | ----------------------------- | --------------------------------- | | `uart_init` | 无 | 初始化串口 1,波特率 115200 | | `uart_io_init` | 无 | 配置 UART1 所用 IO 引脚 | | `uart_disable` | base:UART 实例 | 关闭指定 UART | | `uart_enable` | base:UART 实例 | 使能指定 UART | | `uart_softreset` | base:UART 实例 | 软件复位指定 UART | | `uart_setbaudrate` | base / baudrate / srcclock_hz | 由目标波特率反算 UFCR、UBIR、UBMR | | `putc` | c:待发送字符 | 发送一个字节 | | `puts` | str:字符串 | 发送一串字符 | | `getc` | 无 | 阻塞接收一个字节 | | `raise` | sig_nr | 空函数,防止链接报错 | ### 3.3 初始化函数 uart_init ```c /* * @description : 初始化串口 1,波特率为 115200 * @param : 无 * @return : 无 */ void uart_init(void) { /* 1、初始化串口 IO */ uart_io_init(); /* 2、初始化 UART1 */ uart_disable(UART1); /* 先关闭 UART1 */ uart_softreset(UART1); /* 软件复位 UART1 */ UART1->UCR1 = 0; /* 先清除 UCR1 寄存器 */ UART1->UCR1 &= ~(1 << 14); /* 关闭自动波特率检测 */ /* * 设置 UCR2 寄存器:字长、停止位、校验模式,关闭硬件流控 * bit14: 1 忽略 RTS 引脚 * bit8 : 0 关闭奇偶校验 * bit6 : 0 1 位停止位 * bit5 : 1 8 位数据位 * bit2 : 1 打开发送 * bit1 : 1 打开接收 */ UART1->UCR2 |= (1 << 14) | (1 << 5) | (1 << 2) | (1 << 1); UART1->UCR3 |= 1 << 2; /* UCR3 的 bit2 必须为 1 */ /* * 设置波特率 * 波特率计算公式:Baud Rate = Ref Freq / (16 * (UBMR + 1) / (UBIR + 1)) * 要设置波特率为 115200,可使用如下参数: * Ref Freq = 80M,即 UFCR 的 bit9:7 = 101(1 分频) * UBMR = 3124 * UBIR = 71 * 因此波特率 = 80000000 / (16 * (3124+1) / (71+1)) * = (80000000 * 72) / (16 * 3125) * = 115200 */ UART1->UFCR = 5 << 7; /* ref freq = ipg_clk / 1 = 80MHz */ UART1->UBIR = 71; UART1->UBMR = 3124; #if 0 uart_setbaudrate(UART1, 115200, 80000000); /* 也可用函数自动计算 */ #endif uart_enable(UART1); /* 使能串口 */ } ``` **逐行要点:** 1. `uart_disable()` 必须在配置前调用,否则写控制寄存器可能不生效; 2. `uart_softreset()` 写 0 后必须**轮询等待 SRST 自动置 1**,否则后续配置丢失; 3. `UCR2 |= (1<<14)` 置位 IRTS,关闭硬件流控——不接 RTS/CTS 时这一步很关键,否则可能一直无法发送; 4. `UCR3 |= 1<<2` 是硬性要求,`RXDMUXSEL` 必须为 1; 5. **写波特率寄存器的顺序必须是先 UBIR 后 UBMR**; 6. 最后才 `uart_enable()`,避免配置过程中串口已开始工作。 ### 3.4 IO 引脚初始化 ```c /* * @description : 初始化串口 1 所使用的 IO 引脚 * @param : 无 * @return : 无 */ void uart_io_init(void) { /* UART1_RXD -> UART1_TX_DATA * UART1_TXD -> UART1_RX_DATA */ IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0); IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX, 0); /* * 配置 IO 属性,0x10B0 位域含义: * bit 16 : 0 HYS 关闭 * bit 15:14: 00 默认 100K 下拉 * bit 13 : 0 keeper 功能 * bit 12 : 1 pull/keeper 使能 * bit 11 : 0 关闭开路输出 * bit 7:6 : 10 速度 100MHz * bit 5:3 : 110 驱动能力 R0/6 * bit 0 : 0 低转换率(SRE) */ IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX, 0x10B0); IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX, 0x10B0); } ``` ### 3.5 UART 使能与复位 ```c /* 关闭指定的 UART */ void uart_disable(UART_Type *base) { base->UCR1 &= ~(1 << 0); /* 清 UARTEN */ } /* 打开指定的 UART */ void uart_enable(UART_Type *base) { base->UCR1 |= (1 << 0); /* 置 UARTEN */ } /* 复位指定的 UART */ void uart_softreset(UART_Type *base) { base->UCR2 &= ~(1 << 0); /* 写 0 触发复位 */ while ((base->UCR2 & 0x1) == 0); /* 等待 SRST 自动置 1 */ } ``` | 函数 | base 取值 | 关键操作 | | ---------------- | ------------- | ------------------------------ | | `uart_enable` | UART1 ~ UART8 | UCR1[0] = 1 | | `uart_disable` | UART1 ~ UART8 | UCR1[0] = 0 | | `uart_softreset` | UART1 ~ UART8 | UCR2[0] 写 0,等待硬件自动置 1 | ### 3.6 通用波特率计算函数 uart_setbaudrate 该函数移植自 NXP 官方 SDK,使用**逐次逼近**方式由目标波特率反算 UFCR.RFDIV、UBIR、UBMR 三个寄存器值: ```c /* * @description : 波特率计算,由目标波特率反算 UFCR、UBIR、UBMR * @param - base : 要计算的串口 * @param - baudrate : 目标波特率 * @param - srcclock_hz : 串口时钟源频率,单位 Hz * @return : 无 */ void uart_setbaudrate(UART_Type *base, unsigned int baudrate, unsigned int srcclock_hz) { uint32_t numerator = 0u; uint32_t denominator = 0U; uint32_t divisor = 0U; uint32_t refFreqDiv = 0U; uint32_t divider = 1U; uint64_t baudDiff = 0U; uint64_t tempNumerator = 0U; uint32_t tempDenominator = 0u; /* 求 srcclock_hz 与 (baudrate<<4) 的最大公约数并约分 */ numerator = srcclock_hz; denominator = baudrate << 4; divisor = 1; while (denominator != 0) { divisor = denominator; denominator = numerator % denominator; numerator = divisor; } numerator = srcclock_hz / divisor; denominator = (baudrate << 4) / divisor; /* numerator 取值范围 1 ~ 7*64k,denominator 取值范围 1 ~ 64k */ if ((numerator > (UART_UBIR_INC_MASK * 7)) || (denominator > UART_UBIR_INC_MASK)) { uint32_t m = (numerator - 1) / (UART_UBIR_INC_MASK * 7) + 1; uint32_t n = (denominator - 1) / UART_UBIR_INC_MASK + 1; uint32_t max = m > n ? m : n; numerator /= max; denominator /= max; if (0 == numerator) { numerator = 1; } if (0 == denominator) { denominator = 1; } } divider = (numerator - 1) / UART_UBIR_INC_MASK + 1; switch (divider) { /* 由 divider 反查 RFDIV 编码 */ case 1: refFreqDiv = 0x05; break; case 2: refFreqDiv = 0x04; break; case 3: refFreqDiv = 0x03; break; case 4: refFreqDiv = 0x02; break; case 5: refFreqDiv = 0x01; break; case 6: refFreqDiv = 0x00; break; case 7: refFreqDiv = 0x06; break; default: refFreqDiv = 0x05; break; } /* 计算实际波特率与目标值的偏差,Deviation < 3% 才写入寄存器 */ tempNumerator = srcclock_hz; tempDenominator = (numerator << 4); divisor = 1; while (tempDenominator != 0) { divisor = tempDenominator; tempDenominator = tempNumerator % tempDenominator; tempNumerator = divisor; } tempNumerator = srcclock_hz / divisor; tempDenominator = (numerator << 4) / divisor; baudDiff = (tempNumerator * denominator) / tempDenominator; baudDiff = (baudDiff >= baudrate) ? (baudDiff - baudrate) : (baudrate - baudDiff); if (baudDiff < (baudrate / 100) * 3) { base->UFCR &= ~UART_UFCR_RFDIV_MASK; base->UFCR |= UART_UFCR_RFDIV(refFreqDiv); base->UBIR = UART_UBIR_INC(denominator - 1); /* 先 UBIR */ base->UBMR = UART_UBMR_MOD(numerator / divider - 1); /* 后 UBMR */ } } ``` | 关键宏 | 定义 | 含义 | | ---------------------- | ------ | ---------------------- | | `UART_UFCR_RFDIV_MASK` | 0x380 | UFCR 的 RFDIV 位域 | | `UART_UFCR_RFDIV(x)` | x << 7 | 写入 RFDIV | | `UART_UBIR_INC_MASK` | 0xFFFF | UBIR 位域宽度(16 位) | | `UART_UBIR_INC(x)` | x | 写入 UBIR.INC | | `UART_UBMR_MOD(x)` | x | 写入 UBMR.MOD | --- ## 四、完整源码分析 ### 4.1 发送:putc 与 puts ```c /* * @description : 发送一个字符 * @param - c : 要发送的字符 * @return : 无 */ void putc(unsigned char c) { while (((UART1->USR2 >> 3) & 0x01) == 0); /* 等待上一次发送完成(TXDC=1) */ UART1->UTXD = c & 0xFF; /* 写入发送寄存器 */ } /* * @description : 发送一个字符串 * @param - str : 要发送的字符串 * @return : 无 */ void puts(char *str) { char *p = str; while (*p) { putc(*p++); } } ``` | API | 判据 | 说明 | | ------ | -------------- | --------------------------------------------------- | | `putc` | `USR2[3]` TXDC | TXDC=1 表示 TxFIFO 与移位寄存器都空,可以写入新数据 | | `puts` | `*p != '\0'` | 循环调用 `putc`,直到字符串结束符 | > 注意:`putc` / `puts` 与 C 标准库内建函数**同名**,编译裸机工程时必须加 `-fno-builtin`,否则会报 `conflicting types for built-in function 'putc'`。这也是正点原子 Makefile 中该选项的由来。 ### 4.2 接收:getc ```c /* * @description : 接收一个字符(阻塞) * @param : 无 * @return : 接收到的字符 */ unsigned char getc(void) { while ((UART1->USR2 & 0x1) == 0); /* 等待 RDR=1,即至少收到一个字节 */ return UART1->URXD; /* 读 URXD,RDR 自动清零 */ } ``` | 判据 | 含义 | 副作用 | | ------------- | -------------------------- | ------------------------ | | `USR2[0]` RDR | 接收 FIFO 中至少有一个字节 | 读 `URXD` 后硬件自动清零 | ### 4.3 补充说明函数 raise ```c /* * @description : 防止编译器报错 */ void raise(int sig_nr) { } ``` `raise` 是 C 标准库的信号函数原型,链接器可能在 libgcc 中引用到它,这里给一个空实现即可。 ### 4.4 轮询模式的局限与中断/ DMA 方案 本章例程采用**查询(polling)方式**:`getc()` 用 `while` 死等,`putc()` 用 `while` 等空。这种方式实现简单,但有两个明显缺陷: 1. **CPU 空转**:等待期间无法处理其他任务,实时性差; 2. **接收可能丢数据**:两次 `getc()` 之间收到的数据只能靠 32 级 FIFO 缓冲,一旦溢出(USR2.ORE 置位)数据就丢了。 工程上更完善的做法: | 方案 | 使能位 | 适用场景 | | ------------ | ------------------------------------- | ------------------------------ | | 接收就绪中断 | `UCR1[9]` RRDYEN 或 `UCR4[0]` DREN | 数据量小、实时性要求高 | | 发送就绪中断 | `UCR1[13]` TRDYEN | 配合环形缓冲区按需发送 | | 接收超时中断 | `UCR2[3]` ATEN + UTIM | 判断一帧结束(配合老化定时器) | | DMA | `UCR1[8]` RXDMAEN / `UCR1[3]` TXDMAEN | 大数据量、高波特率 | 中断方式下,接收中断号由 GIC 提供,I.MX6ULL 的 UART1~UART8 依次为 **IRQ 58~62、49、71、72**(即 `UART1_IRQn=58`、`UART2_IRQn=59` … `UART5_IRQn=62`、`UART6_IRQn=49`、`UART7_IRQn=71`、`UART8_IRQn=72`,与官方头文件 `MCIMX6Y2.h` 一致)。`uart_softreset`、`uart_enable` 等初始化代码完全复用,只需额外打开中断使能位,并在 `bsp_int.c` 中注册中断服务函数: ```c /* 中断方式接收的最小实现框架(对照本章轮询版本) */ void uart1_irqhandler(void) { unsigned char data; /* 接收就绪:RRDY(USR1[9]) 或 RDR(USR2[0]) 置位 */ if (UART1->USR1 & (1 << 9)) { while (UART1->USR2 & 0x1) { /* 一次把 FIFO 读空,降低中断频率 */ data = UART1->URXD; ringbuf_put(data); /* 存入环形缓冲区供上层消费 */ } } /* 发送就绪:TRDY(USR1[13]) —— 从环形缓冲区取数据填充 TxFIFO */ if (UART1->USR1 & (1 << 13)) { while (!(UART1->USR2 & 0x8) && ringbuf_has_data()) { UART1->UTXD = ringbuf_get(); } } } ``` ### 4.5 main.c 回显测试 ```c #include "bsp_clk.h" #include "bsp_delay.h" #include "bsp_led.h" #include "bsp_beep.h" #include "bsp_key.h" #include "bsp_int.h" #include "bsp_uart.h" /* * @description : main 函数 */ int main(void) { unsigned char a = 0; unsigned char state = OFF; int_init(); /* 初始化中断(一定要最先调用!) */ imx6u_clkinit(); /* 初始化系统时钟 */ delay_init(); /* 初始化延时 */ clk_enable(); /* 使能所有的时钟 */ led_init(); /* 初始化 LED */ beep_init(); /* 初始化 beep */ uart_init(); /* 初始化串口,波特率 115200 */ while (1) { puts("请输入 1 个字符:"); a = getc(); putc(a); /* 回显 */ puts("\r\n"); puts("您输入的字符为:"); putc(a); puts("\r\n\r\n"); state = !state; led_switch(LED0, state); } return 0; } ``` **执行流程**:`uart_init()` 配置 UART1 → 循环中打印提示 → `getc()` 阻塞等待 SecureCRT 发来的字节 → 原样回显并翻转 LED0,用 LED 指示程序确实在响应。 ### 4.6 编译要点 ```makefile CROSS_COMPILE ?= arm-linux-gnueabihf- TARGET ?= uart CC := $(CROSS_COMPILE)gcc LD := $(CROSS_COMPILE)ld OBJCOPY := $(CROSS_COMPILE)objcopy OBJDUMP := $(CROSS_COMPILE)objdump # uart_setbaudrate 用到了除法,必须链接编译器数学库 LIBPATH := -lgcc -L /usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/4.9.4 INCDIRS := imx6ul bsp/clk bsp/led bsp/delay bsp/beep bsp/gpio \ bsp/key bsp/exit bsp/int bsp/epittimer bsp/keyfilter bsp/uart SRCDIRS := project bsp/clk bsp/led bsp/delay bsp/beep bsp/gpio \ bsp/key bsp/exit bsp/int bsp/epittimer bsp/keyfilter bsp/uart $(TARGET).bin : $(OBJS) $(LD) -Timx6ul.lds -o $(TARGET).elf $^ $(LIBPATH) $(OBJCOPY) -O binary -S $(TARGET).elf $@ $(OBJDUMP) -D -m arm $(TARGET).elf > $(TARGET).dis # -fno-builtin:自定义 putc/puts,避免与内建函数冲突 $(COBJS) : obj/%.o : %.c $(CC) -Wall -nostdlib -fno-builtin -c -O2 $(INCLUDE) -o $@ $< ``` | 关键点 | 原因 | | ------------------------ | ------------------------------------------------- | | `LIBPATH = -lgcc -L ...` | `uart_setbaudrate` 中使用了除法,需要编译器数学库 | | `-fno-builtin` | 自定义 `putc` / `puts` 与 GCC 内建函数重名 | | 链接脚本 | 与前一章保持不变 | 烧写与启动: ```bash chmod 777 imxdownload # 给予可执行权限,一次即可 ./imxdownload uart.bin /dev/sdd # 烧写到 SD 卡,切勿选到 /dev/sda ``` --- ## 五、printf 重定向 / 格式化函数移植 ### 5.1 为什么要移植 裸机只有 `putc` / `puts`,要输出数字必须手工转字符串,非常不便。把 `printf`、`scanf` 映射到串口后,SecureCRT 就能作为开发板终端使用。正点原子提供了一份从 **U-Boot** 中「抠」出来的 `stdio` 源码(光盘路径:`1、例程源码 -> 5、模块驱动源码 -> 2、格式化函数源码 -> stdio`)。 > ⚠️ 这份 `stdio` 并非完整版:**`printf` 不支持浮点数(%f)**,但整数、字符串、十六进制等基本够用。需要浮点时建议改为整数定点输出,或移植完整版 `vsnprintf`。 ### 5.2 目录结构与集成 ``` stdio/ ├── include/ # 头文件(stdio.h 等) └── lib/ # 源码(printf.c、sprintf.c、scanf.c、vsprintf.c ...) ``` 把 `stdio` 复制到工程根目录,然后在 Makefile 中: ```makefile TARGET ?= printf INCDIRS := imx6ul stdio/include bsp/clk ... bsp/uart # 加入 stdio/include SRCDIRS := project stdio/lib bsp/clk ... bsp/uart # 加入 stdio/lib # 增加 -Wa,-mimplicit-it=thumb,否则汇编阶段报 IT block 错误 $(COBJS) : obj/%.o : %.c $(CC) -Wall -Wa,-mimplicit-it=thumb -nostdlib -fno-builtin -c -O2 $(INCLUDE) -o $@ $< ``` | 修改点 | 作用 | | ---------------------------- | ---------------------------------------------------------------------------------- | | `TARGET = printf` | 目标文件名 | | `INCDIRS` 加 `stdio/include` | 找到 `stdio.h` | | `SRCDIRS` 加 `stdio/lib` | 编译格式化函数源码 | | `-Wa,-mimplicit-it=thumb` | 否则报 `thumb conditional instruction should be in IT block -- addcs r5,r5,#65536` | ### 5.3 重定向原理:fputc / fgetc > ⚠️ **来源说明**:教材第22章仅说明移植的 `stdio` 源码「抠自 U-Boot」,并未展开 `fputc`/`fgetc` 的实现细节;本节的重定向代码为扩展知识。 标准 `printf` 最终会把每个字符交给 `fputc()` 输出,`scanf` 则通过 `fgetc()` 取字符。因此**只要重写这两个函数,把底层换成串口收发即可**: ```c #include /* printf 的底层出口:每个字符经此送到串口 1 */ int fputc(int ch, FILE *f) { putc((unsigned char)ch); /* 调用第四章的串口发送函数 */ return ch; } /* scanf 的底层入口:从串口 1 取字符 */ int fgetc(FILE *f) { return getc(); } ``` | 函数 | 原型 | 说明 | | ------- | ---------------------------- | ------------------------------------------------------- | | `fputc` | `int fputc(int ch, FILE *f)` | 必须返回写入的字符;`f` 未使用时可用 `(void)f` 消除警告 | | `fgetc` | `int fgetc(FILE *f)` | 必须返回 `int`,以便用 `EOF(-1)` 表示结束 | > 裸机环境需要注意:`getc()` 是**阻塞**的,`scanf` 在等待输入时会一直卡住,因此不适合放在中断或需要并发处理的位置。 ### 5.4 测试程序 ```c #include "bsp_clk.h" #include "bsp_delay.h" #include "bsp_led.h" #include "bsp_beep.h" #include "bsp_key.h" #include "bsp_int.h" #include "bsp_uart.h" #include "stdio.h" int main(void) { unsigned char state = OFF; int a, b; int_init(); imx6u_clkinit(); delay_init(); clk_enable(); led_init(); beep_init(); uart_init(); while (1) { printf("输入两个整数,使用空格隔开:"); scanf("%d %d", &a, &b); /* 等待键盘输入 */ printf("\r\n数据%d + %d = %d\r\n\r\n", a, b, a + b); state = !state; led_switch(LED0, state); } return 0; } ``` 输入 `32 5` 回车后,串口输出 `数据32 + 5 = 37`,即移植成功。 --- ## 六、实验验证 ### 6.1 终端工具选择 | 工具 | 平台 | 特点 | | ----------------- | ------- | --------------------------------------------------------- | | SecureCRT | Windows | 正点原子教程主推,图形化,需配置字符编码 | | MobaXterm / PuTTY | Windows | 免费,PuTTY 轻量 | | minicom | Linux | `minicom -D /dev/ttyUSB0 -b 115200` | | picocom | Linux | `picocom -b 115200 /dev/ttyUSB0`,按 `Ctrl-A Ctrl-X` 退出 | ### 6.2 SecureCRT 配置步骤 1. `File -> Quick Connect...`,协议选 **Serial**; 2. `Port` 选开发板对应的 COM 口(如 COM8,可在设备管理器查看 CH340); 3. 参数:**Baud rate 115200、Data bits 8、Parity None、Stop bits 1、Flow control None**; 4. 点击 `Connect`。若显示乱码,打开 `Options -> Session Options...`,在 `Terminal -> Appearance` 中把**字符编码设为 UTF-8**,并把字体改为支持中文的等宽字体; 5. 设置完成后 OK,清屏并复位开发板。 ### 6.3 测试步骤与预期现象 | 步骤 | 操作 | 预期结果 | | ---- | ----------------------------------------------------------------------- | -------------------------------------------------- | | 1 | 用 USB 线连接开发板 UART1 到电脑,设置好 JP5 跳线(RXD→P116、TXD→P117) | 设备管理器出现 CH340 串口 | | 2 | 烧写 `uart.bin` 到 SD 卡,插卡复位 | SecureCRT 输出 `请输入 1 个字符:` | | 3 | 键盘输入任意字符(如 `A`) | 回显 `A`,并打印 `您输入的字符为:A`,LED0 状态翻转 | | 4 | 重复输入 | 每输入一次 LED0 翻转一次 | | 5 | 烧写 `printf.bin` 复位 | 输出 `输入两个整数,使用空格隔开:` | | 6 | 输入 `32 5` 并回车 | 输出 `数据32 + 5 = 37` | ### 6.4 常见故障排查 | 现象 | 可能原因 | 排查方向 | | ------------ | --------------------------------------------- | --------------------------------------- | | 完全无输出 | 波特率寄存器未写入 / UARTEN 未使能 | 检查 UBIR 是否先于 UBMR 写入、`UCR1[0]` | | 乱码 | 终端编码非 UTF-8、波特率不匹配、Ref Freq 算错 | 检查 RFDIV、CCM_CSCDR1 时钟源选择 | | 接收不到 | TX/RX 引脚接反、IO 复用错误 | 核对 IOMUXC 复用与跳线帽 | | 只能发不能收 | `RXEN` 未置位、硬件流控未关闭 | 检查 `UCR2[1]`、`UCR2[14]` IRTS | | 高速下丢数据 | 轮询间隙 FIFO 溢出 | 读 `USR2[1]`(ORE),改用中断或 DMA | | 首字符丢失 | UARTEN 使能过早、上电瞬态 | 先配置再使能,必要时丢弃首字节 | --- ## 七、跨平台对比 ### 7.1 寄存器与配置差异 | 特性 | I.MX6ULL UART | STM32 USART | RK3568 UART | | ------------ | ------------------------------ | --------------------------- | --------------------------------------------- | | 控制寄存器 | UCR1~UCR4 | CR1/CR2/CR3 | 各 SoC 私有(UART_RBR/THR/DLL 等 16550 风格) | | 波特率寄存器 | UBIR + UBMR(分数分频) | BRR(整数+小数分频) | DLL/DLH + 私有分数分频 | | 波特率公式 | RefFreq×(UBIR+1)/(16×(UBMR+1)) | fCK/(16×USARTDIV) | 16550 兼容,除数锁存 | | 状态寄存器 | USR1 / USR2 | SR(TXE/TC/RXNE/ORE/FE/PE) | LSR | | FIFO | 收发各 32 级 | 部分型号 8~16 级 | 常见 64/128 字节 | | DMA | UCR1 的 RXDMAEN/TXDMAEN | DMA 请求映射 | DMA 控制器外设请求线 | | 中断号 | IRQ 58~62、49、71、72 | NVIC 固定向量号 | GIC SPI | | 关键约束 | UCR3[2] RXDMUXSEL 必须为 1 | 无 | 无 | ### 7.2 代码差异示例 同样是「发送一个字节」,三家写法完全不同: ```c /* I.MX6ULL:先等 TXDC,再写 UTXD */ void putc(unsigned char c) { while (((UART1->USR2 >> 3) & 0x01) == 0); UART1->UTXD = c & 0xFF; } /* STM32 HAL:等 TXE 标志,再写 DR */ void putc(uint8_t c) { while (!(USART1->SR & USART_SR_TXE)); USART1->DR = c; } /* Linux 用户态(RS232/485 章节常用):一切皆文件 */ int fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY); write(fd, &c, 1); ``` | 层次 | 差异本质 | | ------------------------ | ------------------------------------------------------------------------------------ | | 裸机(I.MX6ULL / STM32) | 直接操作寄存器,需自己管理时钟、复用、标志位 | | Linux 字符设备 | 通过 `termios` 配置波特率/数据位,`read`/`write` 收发,中断与 DMA 由内核串口框架负责 | > 相关:[[04-Linux总线与接口驱动/05-RS232与485通信]] 覆盖了 Linux 下 `termios` 配置与 RS-485 方向控制。 --- ## 八、面试精选 ### Q1:UART 为什么不需要时钟线也能正确接收?采样点是怎么确定的? **参考答案:** UART 是异步通信,双方**事先约定波特率**,接收方靠**起始位的下降沿**重新建立位同步: 1. 接收方在空闲态(高电平)检测到下降沿,判定为起始位; 2. 从起始位开始,内部以 **16 倍波特率的采样时钟**计数,在第 8、9、10 个采样点(即位中点附近)进行**三取二表决**,抑制毛刺并确定该位电平; 3. 之后每 16 个采样时钟采一位,直到停止位。 之所以在 16 倍过采样下取中点三取二:一是保证采样点尽可能落在位中心,抗抖动;二是三取二投票提升抗噪能力。**根本前提是双方波特率偏差足够小**——10 位帧下总偏差一般要求 < ±2%~3%,否则到第 10 位时采样点会移出该位窗口,导致帧错误(USR1.FRAMERR / USR2 错误标志)。 ### Q2:I.MX6ULL 为什么用 UBIR/UBMR 分数分频而不是简单的整数分频? **参考答案:** I.MX6ULL 的波特率公式是: ``` Baud = Ref Freq × (UBIR + 1) / (16 × (UBMR + 1)) ``` 它本质是一个**分数分频器**(BRM,Baud Rate Modulator):`UBIR+1` 做分子、`UBMR+1` 做分母,相当于在整数分频基础上插入了 **1/(UBMR+1)** 的细分步进。 优势在于**精度**:整数分频(如 STM32 的 BRR 只有 4 位小数)在「时钟源 / 16 / 目标波特率」不是整数时会产生较大误差;而分数分频可以从一个较粗的整数除数出发,用分子微调,把误差压到极小。 代价是**配置更麻烦且有时序约束**: - 必须**先写 UBIR 再写 UBMR**,否则 BRM 会按旧参数工作; - `UBIR+1` 有范围限制,所以 NXP SDK 的 `UART_SetBaudRate()` 要用逐次逼近求最大公约数并做缩放; - 一旦 `RFDIV` 改变了 Ref Freq,UBIR/UBMR 必须重算。 ### Q3:给定 Ref Freq = 80 MHz 和目标 115200,如何验证配置无误?如果换到 921600 怎么办? **参考答案:** 先算目标比值,再找一对满足 16 位范围的整数: ``` 目标比值 = 波特率 × 16 / RefFreq 115200 × 16 / 80e6 = 0.02304 = 72 / 3125 ``` 于是 `UBIR+1 = 72 → UBIR = 71`,`UBMR+1 = 3125 → UBMR = 3124`,代入得 115200,误差 0。 换成 **921600**: ``` 921600 × 16 / 80e6 = 0.18432 = 576 / 3125 UBIR = 575, UBMR = 3124 → 80e6 × 576 / (16 × 3125) = 921600,误差 0 ``` 注意几个边界: - `UBIR/UBMR` 都是 **16 位**,分子分母不能超范围;`uart_setbaudrate()` 里 `numerator > UART_UBIR_INC_MASK * 7` 的判断就是为此; - 若某波特率无法整除,`uart_setbaudrate()` 会自动寻找最接近的参数,并用 `baudDiff < (baudrate/100)*3` 判断误差是否 **< 3%**,超过则不写入并保持旧配置(这本身是一个「静默失败」陷阱,工程中应加返回值检查); - 高波特率下 Ref Freq 也需要同步提高(`UART_CLK_PODF` 保持 1 分频),否则分辨率不够。 ### Q4:轮询、中断、DMA 三种串口收发方式的取舍? **参考答案:** | 方式 | 原理 | 优点 | 缺点 | 适用 | | ---- | ---------------------------------------------------- | ----------------------------- | -------------------------------------- | ---------------------- | | 轮询 | `while` 查 `USR2.TXDC/RDR` | 实现最简单、无并发问题 | CPU 空转、易丢数据、实时性差 | 调试打印、低速小数据量 | | 中断 | 使能 `UCR1.RRDYEN` / `UCR4.DREN`,在 ISR 中读写 FIFO | CPU 利用率高、响应及时 | 中断频繁时开销大,ISR 中不能做耗时操作 | 中低速收发、命令交互 | | DMA | 使能 `UCR1.RXDMAEN/TXDMAEN`,由 DMA 搬运 | 几乎零 CPU 占用、适合大块数据 | 配置复杂,需配合环形缓冲与半传输中断 | 高波特率、大吞吐 | 关键工程细节: 1. **中断方式的 ISR 必须一次把 FIFO 读空**(`while (USR2 & RDR) data = URXD;`),否则每字节一次中断,开销反而更大; 2. **超时中断(aging timer)解决「不知道一帧何时结束」的问题**:`UCR2.ATEN` 使能,`UTIM` 设置超时值,超时触发 `USR1.AGTIM`,适合 Modbus 这类变长帧; 3. **FIFO 触发水平(UFCR.RXTL/TXTL)**决定中断频率与延迟的折中:水平高→中断少但延迟大,水平低→响应快但中断多; 4. 发送侧通常配合**环形缓冲区**:应用只往 buffer 写,ISR 在 `TRDYEN` 中断里取数据填 FIFO,避免忙等。 ### Q5:串口输出乱码,给出完整的排查思路。 **参考答案:** 按「信号链」从芯片内部到外部逐段排查,不要一上来就怀疑代码: 1. **终端侧**:先确认 SecureCRT/PuTTY 的波特率、数据位、停止位、校验、流控与代码一致;再确认字符编码(UTF-8/GBK)与字体——正点原子教程里第一次显示乱码,最常见原因就是编码没设对。 2. **时钟源**:`CCM_CSCDR1.UART_CLK_SEL`(0 = 80 MHz pll3,1 = 24 MHz osc)与 `UART_CLK_PODF` 是否与代码里假设的 Ref Freq 一致。如果实际是 24 MHz 而代码按 80 MHz 算,波特率会差 3.33 倍,必然乱码。 3. **RFDIV**:`UFCR[9:7]` 的分频值是否与计算假设一致(编码是「倒序」的:101 才是 1 分频,容易记错)。 4. **UBIR/UBMR**:是否**先写 UBIR 后写 UBMR**;数值是否超出 16 位;是否真的写进去了(可在调试器里回读 `UBRC` 或反算)。 5. **电平与接线**:TTL/RS-232 电平是否匹配(RS-232 是负逻辑,直接接 TTL 必然不通);TX/RX 是否交叉连接;共地是否可靠。 6. **IO 复用与电气属性**:`IOMUXC_SetPinMux` 是否选对复用功能,`IOMUXC_SetPinConfig` 是否配置成了输入/开漏导致驱动能力不足。 7. **流控**:未接 RTS/CTS 时必须使 `UCR2.IRTS = 1` 忽略 RTS,否则发送会被硬件流控挂起。 8. **软件约束**:是否漏了 `UCR3.RXDMUXSEL = 1`;是否漏了 `uart_softreset()` 后的等待;是否在配置完成前就使能了 UART。 **只用一条判据快速定位**:把开发板 TX 直接短接到自己的 RX(UART 内部 `UTS.LOOP` 环回也可),自发自收。若环回正常,问题在外部线路或终端;若环回也乱码,则问题一定在时钟/波特率/帧格式配置。 --- **代码来源**: 裸机例程 `13_uart`(`bsp/uart/bsp_uart.c`、`bsp_uart.h`)、`14_printf`(`stdio/include`、`stdio/lib`) **内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第21章 UART串口通信实验、第22章 串口格式化函数移植实验 **寄存器依据**: 《I.MX6ULL 参考手册》第 55 章 Universal Asynchronous Receiver/Transmitter(UART);NXP SDK `MCIMX6Y2.h` / `fsl_uart.h` **最后更新**: 2026-09-17