1. I.MX6U UART串口开发实战指南
作为一名嵌入式开发工程师,UART串口是我们最常打交道的通信接口之一。在I.MX6U平台上,UART的使用涉及时钟配置、引脚复用、寄存器设置等多个环节。本文将带你从零开始,手把手实现115200波特率的串口通信,并最终完成一个通过串口命令控制LED和蜂鸣器的完整项目。
1.1 UART基础认知
UART(Universal Asynchronous Receiver/Transmitter)是一种异步串行通信协议,它不需要时钟信号线,仅通过TX(发送)和RX(接收)两根数据线即可实现全双工通信。在嵌入式系统中,UART主要承担三大功能:
- 调试接口:通过USB转TTL模块连接电脑,使用串口调试助手查看打印信息
- 设备通信:与蓝牙、GPS、GSM等模块进行数据交互
- 系统控制台:在嵌入式Linux中作为系统输入输出终端
I.MX6U处理器提供了8个UART接口,每个UART都具有以下核心特性:
- 兼容TIA/EIA-232F标准
- 最高支持5Mbps通信速率
- 可配置的数据位(7/8位)、停止位(1/2位)和奇偶校验
- 支持自动波特率检测(最高115.2Kbps)
- 支持IrDA红外通信协议
实际项目中,我们最常用的配置是115200波特率、8位数据位、无校验位、1位停止位(简称115200-8-N-1)。这种配置在稳定性和传输效率之间取得了良好平衡。
2. UART硬件架构解析
2.1 时钟树配置
I.MX6U的UART时钟源来自两个可选路径:
- PLL3_80M:480MHz的PLL3经过6分频得到80MHz
- OSC_CLK:直接使用24MHz的外部晶振
通过CCM_CSCDR1寄存器的UART_CLK_SEL位可以选择时钟源。建议选择PLL3_80M作为时钟源,因为:
- 更高的基础频率意味着更精确的波特率生成
- PLL3的时钟稳定性优于外部晶振
- 80MHz整数分频后更容易得到标准波特率
时钟配置代码如下:
c复制// 设置UART时钟源为PLL3_80M(默认值)
CCM->CSCDR1 &= ~(1 << 6);
// 设置分频值为1(不分频)
CCM->CSCDR1 &= ~(0x3F << 0);
最终进入UART模块的时钟频率计算公式为:
code复制UART_CLK = (PLL3_80M or OSC_CLK) / (UART_CLK_PODF + 1)
当选择PLL3_80M且不分频时,UART_CLK = 80MHz。
2.2 引脚复用配置
I.MX6U的UART1默认使用以下引脚:
- TXD:UART1_TX_DATA
- RXD:UART1_RX_DATA
引脚复用配置需要注意三点:
- 必须设置正确的ALT功能模式(ALT0~ALT7)
- 需要配置引脚的电气特性(上拉/下拉、驱动强度等)
- 硬件流控引脚(CTS/RTS)在普通场景下可以不配置
典型配置代码:
c复制// 设置UART1_TX引脚复用为ALT0模式
IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0);
// 设置UART1_RX引脚复用为ALT0模式
IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX, 0);
// 配置引脚电气属性(驱动能力、上下拉等)
IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX, 0x10B0);
IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX, 0x10B0);
引脚配置值0x10B0的含义:
- 0x10:驱动强度为DSE_6_R0_6(中等驱动能力)
- 0xB0:100K上拉、低速摆率、关闭滞回
3. 寄存器级UART驱动实现
3.1 关键寄存器解析
I.MX6U的UART控制器包含多个功能寄存器,主要分为三类:
- 控制寄存器:UCR1/2/3 - 配置工作模式、使能功能等
- 状态寄存器:USR1/2 - 反映传输状态和错误标志
- 数据寄存器:UTXD/URXD - 存放收发数据
3.1.1 UCR2控制寄存器
UCR2是最核心的控制寄存器,主要配置位包括:
| 位 | 名称 | 功能描述 |
|---|---|---|
| 0 | SRST | 软件复位(0=复位,1=正常工作) |
| 1 | RXEN | 接收使能 |
| 2 | TXEN | 发送使能 |
| 5 | WS | 数据位长度(0=7bit,1=8bit) |
| 6 | STOP | 停止位数量(0=1bit,1=2bit) |
| 8 | PREN | 奇偶校验使能 |
| 14 | IRTS | 忽略RTS硬件流控 |
典型配置流程:
c复制// 1. 软件复位UART
UART1->UCR2 &= ~(1 << 0);
delay_us(1); // 等待复位完成
// 2. 配置工作参数
uint32_t 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;
3.1.2 波特率配置
I.MX6U的波特率生成公式较为特殊:
code复制(UBMR + 1) / (UBIR + 1) = RefFreq / (16 × BaudRate)
其中:
- RefFreq:UART工作频率(80MHz)
- BaudRate:目标波特率(如115200)
- UBMR:UARTx_UBMR寄存器值
- UBIR:UARTx_UBIR寄存器值
以115200波特率为例计算:
- 计算右边:80000000/(16×115200) ≈ 43.402
- 设UBIR=999,则UBMR=43401
- 实际代码中UBMR使用43404(手册推荐值)
配置代码:
c复制// 设置分频系数为1分频
UART1->UFCR &= ~(0x7 << 7);
UART1->UFCR |= (0x5 << 7); // b101表示1分频
// 设置波特率参数
UART1->UBIR = 999;
UART1->UBMR = 43404;
波特率误差计算:
实际波特率 = 80000000/(16×(43404+1)/(999+1)) ≈ 115207
误差 = (115207-115200)/115200 ≈ 0.006% (完全可接受)
3.2 数据收发实现
3.2.1 发送单个字节
发送流程:
- 轮询USR2.TXDC位,等待发送缓冲区空闲
- 将数据写入UTXD寄存器
代码实现:
c复制void uart_send_byte(UART_Type *base, uint8_t data)
{
while(!(base->USR2 & (1 << 3))); // 等待TXDC置位
base->UTXD = data;
}
3.2.2 接收单个字节
接收流程:
- 轮询USR2.RDR位,等待接收到数据
- 从URXD寄存器读取数据(只取低8位)
代码实现:
c复制uint8_t uart_recv_byte(UART_Type *base)
{
while(!(base->USR2 & (1 << 0))); // 等待RDR置位
return base->URXD & 0xFF; // 只取有效数据位
}
实际项目中建议添加超时机制,避免死等:
c复制#define UART_TIMEOUT 100000 // 100ms超时 int uart_recv_byte_timeout(UART_Type *base, uint8_t *data) { uint32_t timeout = UART_TIMEOUT; while(!(base->USR2 & (1 << 0)) && timeout--); if(timeout == 0) return -1; // 超时 *data = base->URXD & 0xFF; return 0; }
4. 完整UART驱动封装
4.1 初始化函数整合
将时钟、引脚、寄存器配置整合为初始化函数:
c复制void uart_init(UART_Type *base)
{
// 1. 引脚复用配置
if(base == UART1) {
IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0);
IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX, 0);
IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX, 0x10B0);
IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX, 0x10B0);
}
// 其他UART接口配置...
// 2. 软件复位
base->UCR2 &= ~(1 << 0);
delay_us(1);
// 3. 基本参数配置
uint32_t tmp = base->UCR2;
tmp |= (1 << 14); // 忽略RTS
tmp &= ~(1 << 8); // 无校验
tmp &= ~(1 << 6); // 1位停止位
tmp |= (1 << 5); // 8位数据位
tmp |= (1 << 2); // 发送使能
tmp |= (1 << 1); // 接收使能
base->UCR2 = tmp;
// 4. 必须配置位
base->UCR3 |= (1 << 2); // RXDMUXSEL置1
// 5. 波特率配置
tmp = base->UFCR;
tmp &= ~(0x7 << 7);
tmp |= (0x5 << 7); // 1分频
base->UFCR = tmp;
base->UBIR = 999;
base->UBMR = 43404;
// 6. 最后使能UART
base->UCR1 |= (1 << 0);
}
4.2 字符串收发功能扩展
基于单字节收发实现字符串操作:
c复制// 发送字符串
void uart_send_string(UART_Type *base, const char *str)
{
while(*str) {
uart_send_byte(base, *str++);
}
}
// 接收字符串(遇到回车或满缓冲区停止)
int uart_recv_line(UART_Type *base, char *buf, int max_len)
{
int len = 0;
char c;
while(len < max_len - 1) {
c = uart_recv_byte(base);
if(c == '\r') break; // 回车结束
// 回显字符
uart_send_byte(base, c);
// 保存有效字符
buf[len++] = c;
}
buf[len] = '\0'; // 字符串结束符
return len;
}
5. 实战:串口控制LED和蜂鸣器
5.1 硬件连接准备
项目需要以下硬件资源:
- UART1:连接USB转TTL模块与PC通信
- GPIO1_IO03:连接LED灯(低电平点亮)
- GPIO1_IO19:连接有源蜂鸣器(高电平触发)
硬件连接示意图:
code复制I.MX6U <---> 外设
UART1_TX -- TXD(USB转TTL)
UART1_RX -- RXD(USB转TTL)
GPIO1_IO03 -- LED阳极(阴极接地)
GPIO1_IO19 -- 蜂鸣器信号线
5.2 命令解析实现
通过串口接收命令字符串,解析后控制硬件:
c复制int main(void)
{
char cmd[32];
// 硬件初始化
clk_init();
led_init(); // GPIO1_IO03
beep_init(); // GPIO1_IO19
uart_init(UART1);
uart_send_string(UART1, "System Ready\r\n");
while(1) {
uart_send_string(UART1, "CMD> ");
uart_recv_line(UART1, cmd, sizeof(cmd));
uart_send_string(UART1, "\r\n");
// 命令解析
if(strcmp(cmd, "ledon") == 0) {
led_on();
uart_send_string(UART1, "LED ON\r\n");
}
else if(strcmp(cmd, "ledoff") == 0) {
led_off();
uart_send_string(UART1, "LED OFF\r\n");
}
else if(strcmp(cmd, "beepon") == 0) {
beep_on();
uart_send_string(UART1, "BEEP ON\r\n");
}
else if(strcmp(cmd, "beepoff") == 0) {
beep_off();
uart_send_string(UART1, "BEEP OFF\r\n");
}
else {
uart_send_string(UART1, "Unknown command\r\n");
}
}
}
5.3 效果验证
- 使用串口调试助手(如SecureCRT、Putty)连接开发板
- 发送以下命令观察硬件响应:
ledon/ledoff:控制LED亮灭beepon/beepoff:控制蜂鸣器开关
- 无效命令会返回提示信息
6. 进阶技巧与问题排查
6.1 printf重定向实现
将标准输出重定向到UART,便于调试:
c复制// 实现putchar函数
int __io_putchar(int ch)
{
uart_send_byte(UART1, (uint8_t)ch);
return ch;
}
// 在main函数前调用初始化
void _sys_init(void)
{
uart_init(UART1);
}
之后即可直接使用printf:
c复制printf("System Clock: %dHz\r\n", get_system_clock());
6.2 常见问题排查
-
无输出或乱码
- 检查波特率是否匹配(115200-8-N-1)
- 确认TX/RX线序是否正确交叉连接
- 测量UART引脚是否有信号输出(示波器看波形)
-
只能发送不能接收
- 检查UCR2.RXEN是否使能
- 确认URXD引脚复用配置正确
- 检查是否有硬件上拉/下拉冲突
-
通信不稳定/丢数据
- 降低波特率测试(如改为9600)
- 检查时钟源配置(PLL3_80M更稳定)
- 添加适当的延时(特别是连续发送时)
-
无法进入接收中断
- 确认UCR1.RRDYEN中断使能位已设置
- 检查中断控制器(GIC)配置是否正确
- 验证中断服务函数是否注册成功
6.3 性能优化建议
-
使用DMA传输:对于高速或大数据量传输,配置DMA可大幅降低CPU负载
c复制// 启用UART DMA UART1->UCR4 |= (1 << 3); // 使能发送DMA UART1->UCR4 |= (1 << 2); // 使能接收DMA -
环形缓冲区:实现双缓冲机制避免数据丢失
c复制#define BUF_SIZE 256 typedef struct { uint8_t data[BUF_SIZE]; uint16_t head; uint16_t tail; } ring_buf_t; -
硬件流控:在高波特率(>1Mbps)时启用CTS/RTS流控
c复制// 配置硬件流控引脚 IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_UART1_CTS_B, 0); IOMUXC_SetPinMux(IOMUXC_UART1_RTS_B_UART1_RTS_B, 0); // 使能硬件流控 UART1->UCR2 &= ~(1 << 14); // 启用RTS
7. 关键知识点总结
- 时钟配置:选择PLL3_80M作为时钟源,1分频得到80MHz工作频率
- 波特率计算:UBMR和UBIR需根据公式(UBMR+1)/(UBIR+1)=43.402配置
- 寄存器要点:
- UCR2需配置数据位、停止位、使能收发
- UCR3的RXDMUXSEL必须置1
- 最后才使能UART(UCR1.UARTEN)
- 状态检查:发送前检查USR2.TXDC,接收时检查USR2.RDR
- 错误处理:建议添加超时机制和环形缓冲区提升可靠性
通过本项目的实践,你不仅掌握了I.MX6U UART的寄存器级编程方法,还实现了完整的串口控制应用。这种从底层驱动到上层应用的开发模式,正是嵌入式工程师的日常工作写照。建议在此基础之上,尝试添加更多命令功能,或者移植更复杂的协议栈(如Modbus),进一步提升实战能力。
