ARM嵌入式 --- UART串口通信
一、串口通信基础概念
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=999,UBMR=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:
- 重定向
putc/getc函数:将putc绑定到 UART 发送,getc绑定到 UART 接收 - 实现空的
raise函数:标准库链接时需要raise函数,空实现即可避免报错 - 修改链接脚本 / 编译选项:确保编译器使用微库(如
--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 初始化核心步骤
- 引脚复用:配置 TX/RX 为 UART 功能
- 电气属性:设置上下拉、速度、驱动能力
- 软件复位:关闭 UART 再配置
- 配置帧格式:8N1、使能收发、关闭流控
- 配置波特率:设置 UFCR、UBIR、UBMR
- 使能 UART:UARTEN 位置 1
5. 发送 / 接收原理
- 发送:查询 TX 空标志 → 写数据到 UTXD
- 接收:查询 RX 就绪标志 → 从 URXD 读数据
- 标志位在 USR2 寄存器:
- RDR (0):接收就绪
- TXDC (3):发送完成
6. 轮询 vs 中断
- 轮询:简单稳定、占用 CPU、低效率
- 中断:CPU 利用率高、实时性强、代码复杂
- 调试用轮询,工程多用中断 + DMA
7. 常见问题与排错
- 乱码:波特率不对 / 时钟配置错误
- 发不出数据:TX 引脚未复用、UART 未使能
- 收不到数据:RX 引脚配置错误、共地不良
- 丢数据:FIFO 未开、波特率过高、CPU 太忙
- UART = 异步全双工串口,8N1 格式
- 波特率必须一致,靠时钟采样对齐
- 初始化:引脚→复位→帧格式→波特率→使能
- 收发查标志位:TX 空、RX 就绪
- 重定向 putc 实现 printf
- 轮询简单,中断高效
更多推荐


所有评论(0)