title: UART串口通信 tags: [UART, 串口, IMX6ULL, 裸机开发, 波特率, printf移植, RS232, RS485] created: 2026-09-16 updated: 2026-09-17 pdf_ref:
💡 关联知识: [[05-定时器与看门狗]] | [[04-GPIO与中断系统]] | [[07-BSP工程管理与封装]] | [[STM32学习笔记/06-串口USART通信]] | [[面试-ARM架构与裸机]]
UART = Universal Asynchronous Receiver/Transmitter,通用异步串行收发器。它是串行接口的一种:数据一位一位地顺序传输,收发各用一条线,因此与外界相连最少只需要三根线: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 |
一次 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 停止),这也是后面计算波特率误差容限的关键前提。
| 参数 | 说明 | 常用值 |
|---|---|---|
| 波特率 | 每秒传输的位数(bit/s),非字节数 | 9600、19200、38400、57600、115200 |
| 数据位 | 每帧有效数据位数 | 8 |
| 停止位 | 每帧停止位数 | 1 |
| 校验位 | 奇偶校验模式 | 无(None) |
| 流控 | RTS/CTS 硬件流控或 XON/XOFF 软件流控 | 一般关闭 |
⚠️ 注意区分波特率与比特率:在 UART 这种二电平基带传输中两者数值相等,但概念不同——波特率衡量符号变化速率,比特率衡量信息速率。
同一种 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 芯片非常常见。
UART 没有时钟线,接收方靠起始位的下降沿重新对齐,然后在每个位时间的中点采样。如果收发双方波特率有偏差,采样点会逐位漂移:
这解释了为什么正点原子移植自 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%,完全匹配 |
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)。
I.MX6U 的 UART 寄存器布局是非连续的:接收/发送寄存器各占 0x40 字节空间。以下结构体与 NXP SDK(MCIMX6Y2.h)定义一致:
/* 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)
| 寄存器 | 位 | 名称 | 说明 |
|---|---|---|---|
| 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 自动清零 |
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 触发水平 |
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 触发水平 |
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 位为待发送数据。
| 寄存器 | 位 | 名称 | 说明 |
|---|---|---|---|
| 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)
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,比值不变。
① 使能时钟 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
#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 | 空函数,防止链接报错 |
/*
* @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); /* 使能串口 */
}
逐行要点:
uart_disable() 必须在配置前调用,否则写控制寄存器可能不生效;uart_softreset() 写 0 后必须轮询等待 SRST 自动置 1,否则后续配置丢失;UCR2 |= (1<<14) 置位 IRTS,关闭硬件流控——不接 RTS/CTS 时这一步很关键,否则可能一直无法发送;UCR3 |= 1<<2 是硬性要求,RXDMUXSEL 必须为 1;uart_enable(),避免配置过程中串口已开始工作。/*
* @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);
}
/* 关闭指定的 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 |
该函数移植自 NXP 官方 SDK,使用逐次逼近方式由目标波特率反算 UFCR.RFDIV、UBIR、UBMR 三个寄存器值:
/*
* @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 |
/*
* @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 中该选项的由来。
/*
* @description : 接收一个字符(阻塞)
* @param : 无
* @return : 接收到的字符
*/
unsigned char getc(void)
{
while ((UART1->USR2 & 0x1) == 0); /* 等待 RDR=1,即至少收到一个字节 */
return UART1->URXD; /* 读 URXD,RDR 自动清零 */
}
| 判据 | 含义 | 副作用 |
|---|---|---|
USR2[0] RDR |
接收 FIFO 中至少有一个字节 | 读 URXD 后硬件自动清零 |
/*
* @description : 防止编译器报错
*/
void raise(int sig_nr)
{
}
raise 是 C 标准库的信号函数原型,链接器可能在 libgcc 中引用到它,这里给一个空实现即可。
本章例程采用查询(polling)方式:getc() 用 while 死等,putc() 用 while 等空。这种方式实现简单,但有两个明显缺陷:
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。uart_softreset、uart_enable 等初始化代码完全复用,只需额外打开中断使能位,并在 bsp_int.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();
}
}
}
#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 指示程序确实在响应。
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 内建函数重名 |
| 链接脚本 | 与前一章保持不变 |
烧写与启动:
chmod 777 imxdownload # 给予可执行权限,一次即可
./imxdownload uart.bin /dev/sdd # 烧写到 SD 卡,切勿选到 /dev/sda
裸机只有 putc / puts,要输出数字必须手工转字符串,非常不便。把 printf、scanf 映射到串口后,SecureCRT 就能作为开发板终端使用。正点原子提供了一份从 U-Boot 中「抠」出来的 stdio 源码(光盘路径:1、例程源码 -> 5、模块驱动源码 -> 2、格式化函数源码 -> stdio)。
⚠️ 这份
stdio并非完整版:printf不支持浮点数(%f),但整数、字符串、十六进制等基本够用。需要浮点时建议改为整数定点输出,或移植完整版vsnprintf。
stdio/
├── include/ # 头文件(stdio.h 等)
└── lib/ # 源码(printf.c、sprintf.c、scanf.c、vsprintf.c ...)
把 stdio 复制到工程根目录,然后在 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 |
标准 printf 最终会把每个字符交给 fputc() 输出,scanf 则通过 fgetc() 取字符。因此只要重写这两个函数,把底层换成串口收发即可:
#include <stdio.h>
/* 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在等待输入时会一直卡住,因此不适合放在中断或需要并发处理的位置。
#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,即移植成功。
| 工具 | 平台 | 特点 |
|---|---|---|
| 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 退出 |
File -> Quick Connect...,协议选 Serial;Port 选开发板对应的 COM 口(如 COM8,可在设备管理器查看 CH340);Connect。若显示乱码,打开 Options -> Session Options...,在 Terminal -> Appearance 中把字符编码设为 UTF-8,并把字体改为支持中文的等宽字体;| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 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 |
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 完全无输出 | 波特率寄存器未写入 / 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 使能过早、上电瞬态 | 先配置再使能,必要时丢弃首字节 |
| 特性 | 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 | 无 | 无 |
同样是「发送一个字节」,三家写法完全不同:
/* 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 方向控制。
参考答案:
UART 是异步通信,双方事先约定波特率,接收方靠起始位的下降沿重新建立位同步:
之所以在 16 倍过采样下取中点三取二:一是保证采样点尽可能落在位中心,抗抖动;二是三取二投票提升抗噪能力。根本前提是双方波特率偏差足够小——10 位帧下总偏差一般要求 < ±2%~3%,否则到第 10 位时采样点会移出该位窗口,导致帧错误(USR1.FRAMERR / USR2 错误标志)。
参考答案:
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+1 有范围限制,所以 NXP SDK 的 UART_SetBaudRate() 要用逐次逼近求最大公约数并做缩放;RFDIV 改变了 Ref Freq,UBIR/UBMR 必须重算。参考答案:
先算目标比值,再找一对满足 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%,超过则不写入并保持旧配置(这本身是一个「静默失败」陷阱,工程中应加返回值检查);UART_CLK_PODF 保持 1 分频),否则分辨率不够。参考答案:
| 方式 | 原理 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 轮询 | while 查 USR2.TXDC/RDR |
实现最简单、无并发问题 | CPU 空转、易丢数据、实时性差 | 调试打印、低速小数据量 |
| 中断 | 使能 UCR1.RRDYEN / UCR4.DREN,在 ISR 中读写 FIFO |
CPU 利用率高、响应及时 | 中断频繁时开销大,ISR 中不能做耗时操作 | 中低速收发、命令交互 |
| DMA | 使能 UCR1.RXDMAEN/TXDMAEN,由 DMA 搬运 |
几乎零 CPU 占用、适合大块数据 | 配置复杂,需配合环形缓冲与半传输中断 | 高波特率、大吞吐 |
关键工程细节:
while (USR2 & RDR) data = URXD;),否则每字节一次中断,开销反而更大;UCR2.ATEN 使能,UTIM 设置超时值,超时触发 USR1.AGTIM,适合 Modbus 这类变长帧;TRDYEN 中断里取数据填 FIFO,避免忙等。参考答案:
按「信号链」从芯片内部到外部逐段排查,不要一上来就怀疑代码:
CCM_CSCDR1.UART_CLK_SEL(0 = 80 MHz pll3,1 = 24 MHz osc)与 UART_CLK_PODF 是否与代码里假设的 Ref Freq 一致。如果实际是 24 MHz 而代码按 80 MHz 算,波特率会差 3.33 倍,必然乱码。UFCR[9:7] 的分频值是否与计算假设一致(编码是「倒序」的:101 才是 1 分频,容易记错)。UBRC 或反算)。IOMUXC_SetPinMux 是否选对复用功能,IOMUXC_SetPinConfig 是否配置成了输入/开漏导致驱动能力不足。UCR2.IRTS = 1 忽略 RTS,否则发送会被硬件流控挂起。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.MX6ULL 参考手册》第 55 章 Universal Asynchronous Receiver/Transmitter(UART);NXP SDK MCIMX6Y2.h / fsl_uart.h
最后更新: 2026-09-17