文章目录
- [给开发板装上嘴巴与耳朵:i.MX6ULL 裸机串口 (UART) 驱动终极指南](#给开发板装上嘴巴与耳朵:i.MX6ULL 裸机串口 (UART) 驱动终极指南)
-
- [🧭 核心概念:串口到底在干嘛?](#🧭 核心概念:串口到底在干嘛?)
- [🛠️ 第一步:铺设物理道路(引脚复用与时钟)](#🛠️ 第一步:铺设物理道路(引脚复用与时钟))
- [📜 第二步:制定通信法典(UCR 寄存器群)](#📜 第二步:制定通信法典(UCR 寄存器群))
- [🧮 第三步:极限"对表"------波特率推导](#🧮 第三步:极限“对表”——波特率推导)
- [🚦 第四步:红绿灯与数据的交响曲(USR2 & 数据寄存器)](#🚦 第四步:红绿灯与数据的交响曲(USR2 & 数据寄存器))
- [🚀 终极源码:直接复制,一次点亮!](#🚀 终极源码:直接复制,一次点亮!)
- [🎉 结语](#🎉 结语)
给开发板装上嘴巴与耳朵:i.MX6ULL 裸机串口 (UART) 驱动终极指南
在嵌入式裸机开发的"新手村"里,点亮 LED 只是热身。当你开始编写复杂的逻辑,却发现板子死机时,如果你没有显示屏,你将陷入无尽的黑夜------你根本不知道代码卡在了哪一行。
这时候,你需要嵌入式世界里最伟大、最古老、也最可靠的调试神器:串口(UART,通用异步收发传输器)。
今天,我们将以 i.MX6ULL 为例,手撕上千页的芯片手册,不调用任何第三方库,纯手工配置寄存器,打通开发板与电脑之间的任督二脉!
🧭 核心概念:串口到底在干嘛?
串口全称叫"异步"收发器。所谓"异步",就是发送方(开发板)和接收方(电脑)之间没有共用的时钟线 。
这就好比两个人隔着悬崖在黑夜里用手电筒发摩斯密码,为了保证对方能准确断句,双方必须提前死死约定好两个规矩:
- 通信格式 :最经典的是 8-N-1(1个起始位,8个数据位,无奇偶校验,1个停止位)。
- 通信语速(波特率 Baud Rate) :比如约定的语速是
115200,意思是一秒钟发送 115200 个 0/1 信号。
在 i.MX6ULL 中,要想建立这个"跨国邮局",我们需要严格按照以下四个步骤来通关。
🛠️ 第一步:铺设物理道路(引脚复用与时钟)
硬件要工作,粮草(时钟)和道路(引脚)必须先行。
1. 激活时钟
i.MX6ULL 的串口时钟源可以有多种选择。我们通过配置 CSCDR1 寄存器,将 UART 的根时钟(uart_clk_root)选择为稳定的 pll3_80m(即 80MHz),并且设置分频系数为 1。
c
CSCDR1 &= ~(1 << 6); // 根时钟源选为 pll3_80m
CSCDR1 &= ~0x3F; // 1 分频,此时 UART 根时钟为 80MHz
2. 引脚复用 (IOMUXC)
我们需要两根物理引脚,一根做 TX(发送),一根做 RX(接收)。查阅手册,将对应的引脚复用模式(MUX_MODE)设置为 ALT0,正式变身 UART1_TX 和 UART1_RX。
电气属性(PAD_CTL)统一配置为 0x10b0(中等速度,充足驱动能力,关键是必须带有上拉电阻,防止线路闲置时电平乱跳导致乱码)。
🚨 血泪排坑:DAISY 寄存器
在 i.MX6ULL 中,芯片内部的一个 UART 模块可以从外壳上的多个不同引脚接收信号。你必须明确告诉芯片走哪条路!通过配置 IOMUXC_UART1_RX_DATA_SELECT_INPUT(DAISY 寄存器)为 3,才能精准锁定我们物理连接的那个 RX 引脚。无数新手卡死在这个不起眼的寄存器上!
📜 第二步:制定通信法典(UCR 寄存器群)
进入 UART 模块内部,我们要通过控制寄存器(UCR1~3)把 8-N-1 的规矩定死。
为了安全,我们在修改规则前,先关闭串口并执行一次软复位:
c
UART1_UCR1 &= ~1; // 禁用串口
UART1_UCR2 &= ~1; // 软复位
while((UART1_UCR2 & 1) == 0); // 死等复位完成
接着,在 UCR2 寄存器中,我们刻下终极法典:
- 开启接收 (
RXEN): Bit 1 置 1 - 开启发送 (
TXEN): Bit 2 置 1 - 8位数据宽度 (
WS): Bit 5 置 1(设为0则是7位) - 禁用硬件流控 (
IRTS): Bit 14 置 1(我们只有 TX/RX 两根线,不用 RTS/CTS 硬件流控)
c
UART1_UCR2 |= (1 << 1) | (1 << 2) | (1 << 5) | (1 << 14);
UART1_UCR3 |= (1 << 2); // 芯片原厂规定,必须将 RXDMUXSEL 置 1
🧮 第三步:极限"对表"------波特率推导
这是全篇最硬核的数学环节。我们已经确定 UART 的输入时钟是 80MHz,要想输出极其精准的 115200 波特率,必须配置 UBIR(分子)和 UBMR(分母)寄存器。
NXP 官方给出的玄学公式如下:
BaudRate = Ref_Freq / (16 * (UBMR + 1) / (UBIR + 1))
Ref_Freq就是我们的 80MHz (80,000,000)。- 我们的目标是解出
UBIR和UBMR。
为了方便你以后修改波特率,我为你做了一个i.MX6ULL 专属波特率计算器,你可以随意切换目标波特率,直接提取寄存器的值:
json?chameleon
{"component":"LlmGeneratedComponent","props":{"height":"600px","prompt":"Build an interactive 'i.MX6ULL UART Baud Rate Calculator'. Strategy: Form Layout. Inputs: 'Reference Clock (MHz)' (default 80), 'Desired Baud Rate' (dropdown: 9600, 19200, 38400, 115200, default 115200). Behavior: When inputs change, calculate and display the closest integer values for UBIR and UBMR using the formula: BaudRate = RefFreq / (16 * (UBMR + 1) / (UBIR + 1)). Since there are multiple solutions, explicitly set UBIR to 71 (which makes UBIR+1 = 72) as a fixed constant for this specific calculator, and then solve for UBMR based on the formula. Show the calculated UBMR, the actual achieved baud rate, and the error percentage. Include a code snippet showing how to set these registers in C.","id":"im_5f9d4f8fd4d0313c"}}
按照我们在计算器里算出的经典组合:
c
UART1_UFCR |= (5 << 7); // 设置 RFDIV=5,即 1 分频,保证时钟不缩水
UART1_UBIR = 71;
UART1_UBMR = 3124; // 完美实现 115200 波特率!
一切就绪,推上总电闸使能串口:UART1_UCR1 |= 1;
🚦 第四步:红绿灯与数据的交响曲(USR2 & 数据寄存器)
规矩定好了,怎么发数据(发微信)和收数据(看微信)?
绝对不能直接把数据强塞进数据寄存器(UTXD/URXD)! CPU 的速度是一秒钟上亿次,而串口发送一个字节需要几十微秒。如果强塞,前面的信还没发出去,就被后面的覆盖了,全是乱码。
我们需要死死盯住 USR2(状态寄存器) 这个红绿灯。
发送单个字符 (putchar)
发送前,必须死等 USR2 的 Bit 3 (TXDC 发送完成标志)。如果是 0,说明上一封信还在路上;变成 1,说明信箱空了,可以塞新数据。
c
void sendc(unsigned char c) {
while((UART1_USR2 & (1<<3)) == 0); // 阻塞死等红灯变绿
UART1_UTXD = c; // 绿灯亮,把数据丢进信箱发送
}
接收单个字符 (getchar)
接收前,必须死等 USR2 的 Bit 0 (RDR 接收就绪标志)。变成 1,才说明有别人发来的新数据躺在信箱里。
c
unsigned char readc(void) {
while((UART1_USR2 & 1) == 0); // 等待新消息提示音
return UART1_URXD; // 把数据从信箱拿出来
}
🚀 终极源码:直接复制,一次点亮!
将以上所有逻辑整合,就得到了下面这份极其干净、极其工程化的串口初始化与收发驱动代码(你可以直接拿去放到你的 main.c 里调用)。
c
#define CSCDR1 (*(volatile unsigned int*)0x020C4024U)
#define UART1_UCR1 (*(volatile unsigned int*)0x02020080U)
#define UART1_UCR2 (*(volatile unsigned int*)0x02020084U)
#define UART1_UCR3 (*(volatile unsigned int*)0x02020088U)
#define UART1_UFCR (*(volatile unsigned int*)0x02020090U)
#define UART1_UBMR (*(volatile unsigned int*)0x020200A8U)
#define UART1_UBIR (*(volatile unsigned int*)0x020200A4U)
#define UART1_USR2 (*(volatile unsigned int*)0x02020098U)
#define UART1_UTXD (*(volatile unsigned int*)0x02020040U)
#define UART1_URXD (*(volatile unsigned int*)0x02020000U)
void uart1_reset() {
UART1_UCR2 &= ~1;
while((UART1_UCR2 & 1) == 0); // 等待软复位完成
}
void uart_init() {
/* 1. 物理引脚初始化 (此处省略 IOMUXC 相关的引脚复用和 DAISY 配置,见前文原理) */
/* 2. 配置时钟为 80MHz */
CSCDR1 &= ~(1<<6);
CSCDR1 &= ~0x3F;
/* 3. 禁用串口,准备配置 */
UART1_UCR1 &= ~1;
uart1_reset();
/* 4. 配置 8-N-1 与 UCR 寄存器 */
UART1_UCR1 &= ~(1<<14); // 禁用自动波特率检测
UART1_UCR2 |= (1<<1) | (1<<2) | (1<<5) | (1<<14);
UART1_UCR3 |= (1<<2); // 必须置1
/* 5. 配置 115200 波特率 (输入 80MHz) */
UART1_UFCR &= ~(7<<7);
UART1_UFCR |= (5<<7); // 1分频
UART1_UBIR = 71;
UART1_UBMR = 3124;
/* 6. 正式使能串口 */
UART1_UCR1 |= 1;
}
// 发送字符串的便捷函数
void send(const char *str) {
for (int i = 0; str[i]; i++) {
sendc(str[i]);
}
}
int main(void) {
uart_init();
send("\r\nUART is ready! Hello World!\r\n");
while(1) {
// 互动小游戏:把收到的字符回显给电脑
char c = readc();
sendc(c);
}
return 0;
}
🎉 结语
从时钟源的分配、引脚的物理复用,再到波特率极其严谨的除法公式,最后用 while 循环死死卡住硬件的状态标志位。
当你编译烧录,用一根数据线连上电脑,在 Xshell 或串口助手里看到那句清脆的 UART is ready! 时,你会真切地感受到:这块冰冷的硅片,终于拥有了可以和你交流的灵魂。