06-UART串口通信.md 52 KB


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 = 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

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)定义一致:

/* 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

#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

/*
 * @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 引脚初始化

/*
 * @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 使能与复位

/* 关闭指定的 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 三个寄存器值:

/*
 * @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

/*
 * @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

/*
 * @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

/*
 * @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、72uart_softresetuart_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();
        }
    }
}

4.5 main.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 编译要点

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

五、printf 重定向 / 格式化函数移植

5.1 为什么要移植

裸机只有 putc / puts,要输出数字必须手工转字符串,非常不便。把 printfscanf 映射到串口后,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 中:

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 目标文件名
INCDIRSstdio/include 找到 stdio.h
SRCDIRSstdio/lib 编译格式化函数源码
-Wa,-mimplicit-it=thumb 否则报 thumb conditional instruction should be in IT block -- addcs r5,r5,#65536

5.3 重定向原理:fputc / fgetc

标准 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 在等待输入时会一直卡住,因此不适合放在中断或需要并发处理的位置。

5.4 测试程序

#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 代码差异示例

同样是「发送一个字节」,三家写法完全不同:

/* 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 = 71UBMR+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 三种串口收发方式的取舍?

参考答案:

方式 原理 优点 缺点 适用
轮询 whileUSR2.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. RFDIVUFCR[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_uartbsp/uart/bsp_uart.cbsp_uart.h)、14_printfstdio/includestdio/lib寄存器依据: 《I.MX6ULL 参考手册》第 55 章 Universal Asynchronous Receiver/Transmitter(UART);NXP SDK MCIMX6Y2.h / fsl_uart.h 最后更新: 2026-09-17