一、串口通信基础概念

1.1 串行与并行通信

串口通信的核心是串行传输:数据按位依次在一根线上传输,而并行通信是多根线同时传输多位数据。

  • 串行通信:线路少、抗干扰强、传输距离远,适合嵌入式设备的外设通信(如 UART、USB、SPI)
  • 并行通信:速度快但抗干扰差、距离短,现在嵌入式中已很少使用

1.2 同步与异步通信

  • 同步通信:收发双方共用同一个时钟信号,严格同步(如 SPI、I2C),需要额外时钟线
  • 异步通信:收发双方各自使用本地时钟,通过约定波特率同步,UART 就是典型的异步串行通信,仅需 TX/RX 两根线即可通信

1.3 通信方式分类

UART 属于全双工异步串行通信

  • 全双工:TX 和 RX 独立工作,可同时收发数据
  • 异步:无共享时钟,靠波特率、起始位 / 停止位同步
  • 串行:单 bit 逐位传输

1.4 串口通信标准

常用的串口标准有 RS-232、RS-485 等,嵌入式开发中最常用的是TTL 电平串口(3.3V/5V 逻辑电平),通过 USB 转 TTL 模块即可和电脑通信,无需额外电平转换芯片。

1.5 波特率与数据格式

波特率

单位是 bps(bit per second),表示每秒传输的二进制位数,常用值:9600、115200、1152000 等。收发双方必须设置完全一致的波特率,否则会出现乱码。

数据格式

标准 UART 数据帧格式:起始位(1bit) + 数据位(5-8bit,常用8bit) + 校验位(可选,奇/偶校验) + 停止位(1/1.5/2bit,常用1bit)本次代码使用的是:8 位数据位、1 位停止位、无校验位,是嵌入式开发最通用的配置。

二、硬件与开发环境

2.1 核心硬件连接

本次基于NXP i.MX6ULL开发板实现 UART1 串口通信,硬件连接如下:

  • 开发板 UART1_TX → USB 转 TTL 模块的 RX
  • 开发板 UART1_RX → USB 转 TTL 模块的 TX
  • 开发板 GND → USB 转 TTL 模块的 GND(必须共地,否则电平不匹配)
  • 电脑端通过串口调试助手(如 Xshell、MobaXterm)收发数据,波特率设置 115200,8N1 格式

2.2 核心芯片作用

i.MX6ULL 内部集成了多个 UART 控制器(UART1~UART8),本次使用 UART1 控制器,其核心作用:

  • 完成并行数据→串行数据的转换(发送时)、串行数据→并行数据的转换(接收时)
  • 自动生成起始位、停止位,校验位(可选)
  • 波特率发生器,根据配置生成对应波特率的发送 / 接收时钟
  • 提供 FIFO 缓存,提升数据收发的稳定性

2.3 核心寄存器

i.MX6ULL 的 UART 控制器通过寄存器配置,核心寄存器如下表:

寄存器 作用 关键位定义
UART1_UCR1 控制寄存器 1 [0]: UARTEN (使能)
UART1_UCR2 控制寄存器 2 [0]: 软件复位[1]: RXEN (接收使能)[2]: TXEN (发送使能)[5]: WS (字长)[6]: STPB (停止位)[8]: PREN (奇偶校验使能)[14]: IRTS (忽略 RTS 流控)
UART1_UCR3 控制寄存器 3 [2]: RXD MUXSEL (接收多路复用选择)
UART1_UFCR FIFO 控制寄存器 [7-9]: RFDIV (参考时钟分频)
UART1_UBIR 波特率增量寄存器 [0-15]: UBIR
UART1_UBMR 波特率模数寄存器 [0-15]: UBMR
UART1_USR2 状态寄存器 2 [0]: RDR (接收数据就绪)[3]: TXDC (发送完成)

三、代码实现

3.1 初始化过程

UART 初始化是串口通信的核心,分为 5 个关键步骤:

步骤 1:引脚复用与电气配置

i.MX6ULL 的引脚是复用的,必须先将 UART1_TX/RX 对应的引脚配置为串口功能,再配置电气特性(上拉、驱动强度等),保证信号稳定。

// 引脚复用
IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX, 0);
IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0);

// 电气配置
IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX, 0x10b0);
IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX, 0x10b0);
步骤 2:软件复位 UART

配置前先关闭 UART,执行软件复位,避免原有配置干扰新配置:

UART1->UCR2 &= ~(1 << 0); // 软件复位
delay_us(1); // 等待复位完成
步骤 3:数据格式与功能配置

配置 UART2 控制寄存器,设置8 位数据位、1 位停止位、无校验位,忽略流控,使能收发:

unsigned int tmp = UART1->UCR2;
tmp |= (1 << 14);  // 忽略RTS流控
tmp &= ~(1 << 8);  // 关闭奇偶校验
tmp &= ~(1 << 6);  // 1位停止位
tmp |= (1 << 5);   // 8位数据位
tmp |= (1 << 2);   // 使能发送
tmp |= (1 << 1);   // 使能接收
UART1->UCR2 = tmp;
步骤 4:波特率配置

i.MX6ULL 的 UART 波特率计算公式:Baud = (REF_CLK) / (16 * (UBMR + 1)/(UBIR + 1))本次使用 80MHz 参考时钟,目标波特率 115200,计算得UBIR=999UBMR=43404,同时配置 FIFO 时钟分频为 1:

// 配置FIFO分频
tmp = UART1->UFCR;
tmp &= ~(0x7 << 7);
tmp |= (0x5 << 7);
UART1->UFCR = tmp;

// 配置波特率寄存器
UART1->UBIR = 999;
UART1->UBMR = 43404;
步骤 5:使能 UART

最后通过 UCR1 寄存器的 UARTEN 位使能 UART 控制器,完成初始化:

UART1->UCR1 |= (1 << 0);

3.2 数据发送函数

发送单字节的核心逻辑:等待发送完成→写入发送寄存器

通过USR2寄存器的TXDC位(第 3 位)判断发送是否完成,为 1 时表示发送寄存器为空,可以写入新数据:

void uart_send_byte(const unsigned char data)
{
    while(!(UART1->USR2 & (1 << 3))); // 等待发送完成
    UART1->UTXD = data; // 写入发送寄存器
}

同时实现了putc函数,用于后续标准 I/O 库的重定向,逻辑完全一致。

3.3 数据接收函数

接收单字节的核心逻辑:等待接收就绪→读取接收寄存器

通过USR2寄存器的RDR位(第 0 位)判断是否有数据就绪,为 1 时表示接收寄存器有有效数据,读取URXD寄存器即可:

unsigned char uart_recv_byte(void)
{
    unsigned char data = 0;
    while(!(UART1->USR2 & (1 << 0))); // 等待接收就绪
    data = UART1->URXD & 0xff; // 读取数据,仅取低8位
    return data;
}

同时实现了getc函数,用于标准 I/O 库的重定向。

3.4 标准 I/O 库移植

3.4.1 移植步骤

嵌入式开发中,我们通常需要用printf打印调试信息,scanf读取用户输入,因此需要将标准 I/O 库重定向到 UART:

  1. 重定向putc/getc函数:将putc绑定到 UART 发送,getc绑定到 UART 接收
  2. 实现空的raise函数:标准库链接时需要raise函数,空实现即可避免报错
  3. 修改链接脚本 / 编译选项:确保编译器使用微库(如--specs=nosys.specs),避免系统调用报错

本次代码已经完成了putc/getc/raise的实现,直接编译即可支持printf/scanf

3.5 完整 uart.c代码如下:

#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "uart.h"
#include "gpt.h"

/**
 * @brief  UART1初始化函数
 * @note   配置波特率115200,8N1格式,使能收发
 * @param  无
 * @retval 无
 */
void uart_init(void)
{
    // 1. 引脚复用配置:将引脚映射为UART1_TX/RX功能
    IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX, 0);
    IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0);

    // 2. 电气特性配置:上拉、驱动速度、压摆率等,确保信号稳定
    IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX, 0x10b0);
    IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX, 0x10b0);

    // 3. 软件复位UART,先关闭UART再配置
    UART1->UCR2 &= ~(1 << 0);
    delay_us(1); // 等待复位完成

    // 4. 配置UART2控制寄存器:8N1格式,忽略流控,使能收发
    unsigned int tmp = UART1->UCR2;
    tmp |= (1 << 14);  // 忽略RTS流控
    tmp &= ~(1 << 8);  // 关闭奇偶校验
    tmp &= ~(1 << 6);  // 1位停止位
    tmp |= (1 << 5);   // 8位数据位(WS=1)
    tmp |= (1 << 2);   // 使能发送(TXEN)
    tmp |= (1 << 1);   // 使能接收(RXEN)
    UART1->UCR2 = tmp;

    // 5. 配置UART3控制寄存器:使能接收多路复用
    UART1->UCR3 |= (1 << 2);

    // 6. 配置FIFO控制寄存器:参考时钟分频为1
    tmp = UART1->UFCR;
    tmp &= ~(0x7 << 7); // 清除原有分频
    tmp |= (0x5 << 7);  // 设置分频为1(RFDIV=5)
    UART1->UFCR = tmp;

    // 7. 配置波特率:115200,公式:Baud = (REF_CLK) / (16 * (UBMR + 1)/(UBIR + 1))
    // REF_CLK=80MHz,计算得UBIR=999,UBMR=43404
    UART1->UBIR = 999;
    UART1->UBMR = 43404;

    // 8. 使能UART控制器
    UART1->UCR1 |= (1 << 0);
}

/**
 * @brief  UART1发送单字节数据
 * @param  data: 待发送的字节
 * @retval 无
 */
void uart_send_byte(const unsigned char data)
{
    // 等待发送完成(TXDC位为1)
    while(!(UART1->USR2 & (1 << 3)));
    // 写入发送寄存器
    UART1->UTXD = data;
}

/**
 * @brief  UART1接收单字节数据
 * @param  无
 * @retval 接收到的字节
 */
unsigned char uart_recv_byte(void)
{
    unsigned char data = 0;
    // 等待接收数据就绪(RDR位为1)
    while(!(UART1->USR2 & (1 << 0)));
    // 读取接收寄存器,仅取低8位
    data = UART1->URXD & 0xff;
    return data;
}

/**
 * @brief  标准库putc重定向,用于printf
 * @param  data: 待发送的字符
 * @retval 发送的字符
 */
void putc(unsigned char data)
{
    while(!(UART1->USR2 & (1 << 3)));
    UART1->UTXD = data;
}

/**
 * @brief  标准库getc重定向,用于scanf
 * @param  无
 * @retval 接收到的字符
 */
unsigned char getc(void)
{
    unsigned char data = 0;
    while(!(UART1->USR2 & (1 << 0)));
    data = UART1->URXD & 0xff;
    return data;
}

四、总结

1. UART 本质

  • 通用异步收发器,全双工、异步串行通信接口
  • 不需要时钟线,靠约定波特率同步
  • 只需要 TX、RX、GND 即可通信

2. 通信帧格式

标准格式:起始位 (1bit) + 数据位 (8bit) + 校验位 (可选) + 停止位 (1bit)常用配置:8N1(8 位数据、无校验、1 位停止)

3. 波特率是什么?为什么必须一致?

  • 波特率:每秒传输的比特数(115200 最常用)
  • 收发双方波特率必须严格一致,否则乱码
  • 本质:用相同采样时钟对齐数据位

4. i.MX6ULL UART 初始化核心步骤

  1. 引脚复用:配置 TX/RX 为 UART 功能
  2. 电气属性:设置上下拉、速度、驱动能力
  3. 软件复位:关闭 UART 再配置
  4. 配置帧格式:8N1、使能收发、关闭流控
  5. 配置波特率:设置 UFCR、UBIR、UBMR
  6. 使能 UART:UARTEN 位置 1

5. 发送 / 接收原理

  • 发送:查询 TX 空标志 → 写数据到 UTXD
  • 接收:查询 RX 就绪标志 → 从 URXD 读数据
  • 标志位在 USR2 寄存器:
    • RDR (0):接收就绪
    • TXDC (3):发送完成

6. 轮询 vs 中断

  • 轮询:简单稳定、占用 CPU、低效率
  • 中断:CPU 利用率高、实时性强、代码复杂
  • 调试用轮询,工程多用中断 + DMA

7. 常见问题与排错

  • 乱码:波特率不对 / 时钟配置错误
  • 发不出数据:TX 引脚未复用、UART 未使能
  • 收不到数据:RX 引脚配置错误、共地不良
  • 丢数据:FIFO 未开、波特率过高、CPU 太忙
  1. UART = 异步全双工串口,8N1 格式
  2. 波特率必须一致,靠时钟采样对齐
  3. 初始化:引脚→复位→帧格式→波特率→使能
  4. 收发查标志位:TX 空、RX 就绪
  5. 重定向 putc 实现 printf
  6. 轮询简单,中断高效
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐