IMX6ULL UART串口通信实战
一、UART核心概念梳理
在动手写代码前,先理清嵌入式通信的核心概念,这是理解UART工作原理的基础。
1. 通信
嵌入式系统中的通信本质是两个及以上主机间的数据交互,UART是其中一种异步串行通信方式,无需时钟线同步,仅通过TX(发送)、RX(接收)两根线即可完成双向数据传输。
2. 异步 vs 同步
- 异步通信:无需专用时钟线,收发双方约定波特率、数据位、停止位等参数,通过“起始位+数据位+校验位+停止位”的帧格式实现同步(UART属于此类);
- 同步通信:需要时钟线(如SPI的SCK、I2C的SCL),收发双方严格同步于时钟信号,传输效率更高(如SPI、I2C)。
3. 串行 vs 并行
- 串行通信:数据逐位沿单根线路传输(如UART、RS485),优点是布线简单、传输距离远,缺点是速度相对慢;
- 并行通信:多位数据同时沿多根线路传输(如MCU的并行总线),优点是速度快,缺点是布线复杂、抗干扰差、传输距离短。
4. 单工/半双工/全双工
- 单工:仅支持单向传输(如遥控器→电视);
- 半双工:支持双向传输,但同一时间只能一方发送(如对讲机);
- 全双工:支持双向同时传输(UART默认全双工,TX/RX独立)。
5. TTL/RS232/RS485
三者是不同的电平标准,核心用于适配不同传输场景:
- TTL:嵌入式芯片原生电平(高电平≈3.3V/5V,低电平≈0V),传输距离短(<1米);
- RS232:负电平标准(高电平-3-15V,低电平+3+15V),传输距离≈15米;
- RS485:差分电平传输,抗干扰能力极强,传输距离可达千米级,支持多机通信。
6. 差分传输
差分传输通过两根信号线的电压差表示逻辑电平(如A-B>0.2V为逻辑1,A-B<-0.2V为逻辑0),能有效抵消共模干扰(如电源线、电磁干扰),是RS485、CAN等远距离通信的核心技术。
二、硬件原理图分析(IMX6ULL_MINI_V2.2)
本次实战基于《IMX6ULL_MINI_V2.2(Mini底板原理图).pdf》的USB USART&USB POWER模块,核心硬件组成如下:
1. 核心模块拆解
| USB_TTL接口 | 作为UART的物理接口,一端连接电脑USB,另一端连接CH340芯片 |
| CH340(U8) | USB转TTL串口芯片,实现“电脑USB总线”到“开发板TTL电平UART”的协议转换 |
| DCDC(U12/U13) | 电源稳压模块,提供防抖、抗干扰的稳定电源(⚠️ 不建议USB供电,易导致板子发热) |
2. 硬件逻辑
电脑通过USB线连接开发板的USB_TTL接口 → CH340将USB信号转为TTL电平的UART信号 → 连接到IMX6ULL的UART1_TX/RX引脚 → 实现“电脑-开发板”的UART通信。
三、UART代码实现(基于IMX6ULL)
代码开发参考《IMX6ULL参考手册.pdf》,核心分为时钟初始化、引脚初始化、寄存器配置、收发函数实现、stdio移植五个步骤。
1. 前期准备
先定义IMX6ULL的UART1寄存器基地址和引脚复用相关宏(示例):
// UART1寄存器基地址
#define UART1_BASE 0x02020000
#define UART1 ((volatile unsigned int *)UART1_BASE)
// 寄存器偏移定义(仅列出核心寄存器)
#define UART_URXD 0x00 // 接收寄存器
#define UART_UTXD 0x40 // 发送寄存器
#define UART_UCR1 0x80 // 控制寄存器1
#define UART_UCR2 0x84 // 控制寄存器2
#define UART_UCR3 0x88 // 控制寄存器3
#define UART_UFCR 0x90 // FIFO控制寄存器
#define UART_USR2 0xB8 // 状态寄存器2
#define UART_UBIR 0xA4 // 波特率增量寄存器
#define UART_UBMR 0xA8 // 波特率调制寄存器
// 引脚复用函数声明(IMX6ULL标准库)
void IOMUXC_SetPinMux(unsigned int muxRegister, unsigned int muxMode);
void IOMUXC_SetPinConfig(unsigned int configRegister, unsigned int configValue);
2. 步骤1:时钟初始化
IMX6ULL的UART参考时钟默认由外设时钟提供,本次配置基准时钟为80MHz,预分频1分频(即参考时钟=80MHz):
/**
* @brief UART时钟初始化:80MHz基准时钟,1分频
*/
void uart_clk_init(void) {
// 配置UART1所属的外设时钟为80MHz(具体寄存器需参考IMX6ULL时钟树)
CCM->CSCDR1 &= ~(0x1F << 6); // 清除原有分频值
CCM->CSCDR1 |= (0x00 << 6); // 1分频,UART参考时钟=80MHz
}
3. 步骤2:引脚初始化
IMX6ULL的引脚是“多功能复用”的,需将指定引脚配置为UART1_TX/RX功能,并设置电气属性:
/**
* @brief UART1引脚初始化:TX->GPIO1_IO04,RX->GPIO1_IO05
*/
void uart_pin_init(void) {
// 1. 配置TX引脚(GPIO1_IO04 → UART1_TX)
IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0); // 复用为UART1_TX
// 配置电气属性:上拉、100MHz速率、驱动能力等(0x10B0为标准配置)
IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX, 0x10B0);
// 2. 配置RX引脚(GPIO1_IO05 → UART1_RX)
IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX, 0); // 复用为UART1_RX
IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX, 0x10B0);
}
参数说明:
- IOMUXC_SetPinMux第二个参数0:使用默认复用模式,无额外配置;
- 0x10B0:二进制0001 0000 1011 0000,核心配置:
- 位[15:12]:0001 → 引脚速率100MHz;
- 位[11:8]:1011 → 上拉22KΩ;
- 位[7:0]:0000 → 无开漏、无中断等。
4. 步骤3:UART寄存器配置
核心是配置UART的工作模式(8位数据、1位停止位、无校验、115200波特率):
/**
* @brief UART1寄存器配置:8N1(8位数据+1位停止+无校验),115200波特率
*/
void uart_reg_init(void) {
// 1. 软件复位UART(UCR2[0]置1,保持4个时钟周期后清0)
UART1[UART_UCR2/4] |= (1 << 0);
while((UART1[UART_UCR2/4] & (1 << 0))); // 等待复位完成
// 2. 配置UCR1:开启UART总开关(UCR1[0] = 1)
UART1[UART_UCR1/4] |= (1 << 0);
// 3. 配置UCR2:8位数据、1位停止、无校验、使能收发、忽略RTS流控
UART1[UART_UCR2/4] |= (1 << 1); // RXEN=1,使能接收
UART1[UART_UCR2/4] |= (1 << 2); // TXEN=1,使能发送
UART1[UART_UCR2/4] &= ~(1 << 5); // WS=0,8位数据位
UART1[UART_UCR2/4] &= ~(1 << 6); // STPB=0,1位停止位
UART1[UART_UCR2/4] &= ~(1 << 8); // PREN=0,关闭奇偶校验
UART1[UART_UCR2/4] |= (1 << 14); // IRTS=1,忽略RTS流控
// 4. 配置UCR3:RXDMUXSEL=1(IMX6ULL必须置1,多路复用模式)
UART1[UART_UCR3/4] |= (1 << 2);
// 5. 配置UFCR:RFDIV=0(参考时钟1分频)
UART1[UART_UFCR/4] &= ~(0x7 << 7); // 清除原有值
UART1[UART_UFCR/4] |= (0x0 << 7); // 1分频
// 6. 配置波特率:115200(参考时钟80MHz)
// 公式:BaudRate = Ref Freq/(16 * ((UBMR + 1)/(UBIR + 1)))
// 推导:(UBMR+1)/(UBIR+1) = 80000000/(16*115200) ≈ 43.4028
// 选取UBIR=15,UBMR=659((659+1)/(15+1)=660/16=41.25,接近目标值)
UART1[UART_UBIR/4] = 15;
UART1[UART_UBMR/4] = 659;
}
5. 步骤4:收发函数实现
基于寄存器封装putc(发单个字符)、puts(发字符串)、getc(收单个字符):
/**
* @brief 发送单个字符
* @param d 要发送的字符
*/
void putc(unsigned char d) {
// 轮询TXDC位(USR2[3]),直到发送完成
while ((UART1[UART_USR2/4] & (1 << 3)) == 0);
UART1[UART_UTXD/4] = d; // 写入发送寄存器,硬件自动发送
}
/**
* @brief 发送字符串(自动加换行)
* @param pStr 字符串首地址
*/
void puts(const char *pStr) {
while (*pStr) { // 遍历字符串,直到'\\0'
putc(*pStr++);
}
putc('\\n'); // 末尾加换行
}
/**
* @brief 接收单个字符(阻塞式)
* @return 接收到的字符
*/
unsigned char getc(void) {
// 轮询RDR位(USR2[0]),直到有数据就绪
while ((UART1[UART_USR2/4] & (1 << 0)) == 0);
return (unsigned char)(UART1[UART_URXD/4] & 0xFF); // 读取8位数据
}
核心逻辑:
- 发送:通过TXDC位判断上一个字符是否发送完成,避免数据覆盖;
- 接收:通过RDR位判断是否有新数据,阻塞等待直到接收到数据。
6. 步骤5:移植stdio库(支持printf/scanf)
默认情况下,IMX6ULL的裸机程序无法使用printf/scanf,需移植stdio库,步骤如下:
(1)添加空的raise函数
在uart.c中添加以下函数(stdio库依赖,缺失会编译报错):
// 用于兼容stdio库,函数体为空即可
void raise(int n) {
(void)n; // 避免未使用参数警告
}
(2)修改汇编文件扩展名
将启动文件project/start.s改为project/start.S:
- .s:直接编译,不做预处理;
- .S:先预处理(支持#include、#define等),再编译,适配裸机工程的宏定义。
(3)修改Makefile
添加stdio库的头文件/源码路径,链接gcc库:
# 目标文件
target = imx6ull_uart
# 编译器路径(根据自己的交叉编译器调整)
cc = arm-linux-gnueabihf-gcc
ld = arm-linux-gnueabihf-ld
objcopy = arm-linux-gnueabihf-objcopy
objdump = arm-linux-gnueabihf-objdump
# 1. 添加stdio相关路径
incdirs = bsp imx6ull stdio/include # 头文件目录
srcdirs = bsp project stdio/lib # 源码目录
# 2. 指定gcc库路径(根据自己的编译器路径调整)
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
# 编译选项:Wall(开启警告)、thumb模式、不使用标准库、禁用内置函数
CFLAGS = -Wall -Wa,-mimplicit-it=thumb -nostdlib -fno-builtin
# 收集所有源码文件
srcfiles = $(foreach dir, $(srcdirs), $(wildcard $(dir)/*.c $(dir)/*.S))
objfiles = $(patsubst %.c, %.o, $(patsubst %.S, %.o, $(srcfiles)))
# 链接生成elf文件
$(target).elf: $(objfiles)
$(ld) -Timx6ull.lds -o $@ $^ $(libpath)
# 编译c文件
%.o: %.c
$(cc) $(CFLAGS) -I$(incdirs) -c -o $@ $<
# 编译汇编文件
%.o: %.S
$(cc) $(CFLAGS) -I$(incdirs) -c -o $@ $<
# 生成bin文件
bin: $(target).elf
$(objcopy) -O binary $< $(target).bin
# 清理
clean:
rm -rf $(objfiles) $(target).elf $(target).bin
7. 测试代码
在main.c中调用封装的函数,测试UART通信:
#include "uart.h"
int main(void) {
unsigned char recv_data;
// 初始化UART
uart_clk_init();
uart_pin_init();
uart_reg_init();
// 发送测试字符串
puts("IMX6ULL UART Test Start!");
puts("Please input a char:");
while (1) {
recv_data = getc(); // 接收字符
putc(recv_data); // 回显字符
puts(" Recv Success!"); // 提示接收成功
}
return 0;
}
四、关键注意事项
五、总结
本文从“概念→硬件→代码”完整讲解了IMX6ULL UART串口通信的实现过程,核心关键点:
- 理解UART的异步串行通信本质,掌握帧格式、波特率等核心参数;
- 硬件上重点关注CH340的USB转TTL功能,避免电源选型错误;
- 代码上核心是寄存器配置(尤其是波特率、收发使能)和stdio移植;
- 轮询方式是裸机UART最基础的实现方式,后续可扩展中断/ DMA方式。



