给开发板装上嘴巴与耳朵:i.MX6ULL 裸机串口 (UART) 驱动终极指南

文章目录

  • [给开发板装上嘴巴与耳朵:i.MX6ULL 裸机串口 (UART) 驱动终极指南](#给开发板装上嘴巴与耳朵:i.MX6ULL 裸机串口 (UART) 驱动终极指南)
    • [🧭 核心概念:串口到底在干嘛?](#🧭 核心概念:串口到底在干嘛?)
    • [🛠️ 第一步:铺设物理道路(引脚复用与时钟)](#🛠️ 第一步:铺设物理道路(引脚复用与时钟))
    • [📜 第二步:制定通信法典(UCR 寄存器群)](#📜 第二步:制定通信法典(UCR 寄存器群))
    • [🧮 第三步:极限"对表"------波特率推导](#🧮 第三步:极限“对表”——波特率推导)
    • [🚦 第四步:红绿灯与数据的交响曲(USR2 & 数据寄存器)](#🚦 第四步:红绿灯与数据的交响曲(USR2 & 数据寄存器))
    • [🚀 终极源码:直接复制,一次点亮!](#🚀 终极源码:直接复制,一次点亮!)
    • [🎉 结语](#🎉 结语)

给开发板装上嘴巴与耳朵:i.MX6ULL 裸机串口 (UART) 驱动终极指南

在嵌入式裸机开发的"新手村"里,点亮 LED 只是热身。当你开始编写复杂的逻辑,却发现板子死机时,如果你没有显示屏,你将陷入无尽的黑夜------你根本不知道代码卡在了哪一行。

这时候,你需要嵌入式世界里最伟大、最古老、也最可靠的调试神器:串口(UART,通用异步收发传输器)

今天,我们将以 i.MX6ULL 为例,手撕上千页的芯片手册,不调用任何第三方库,纯手工配置寄存器,打通开发板与电脑之间的任督二脉!


🧭 核心概念:串口到底在干嘛?

串口全称叫"异步"收发器。所谓"异步",就是发送方(开发板)和接收方(电脑)之间没有共用的时钟线

这就好比两个人隔着悬崖在黑夜里用手电筒发摩斯密码,为了保证对方能准确断句,双方必须提前死死约定好两个规矩:

  1. 通信格式 :最经典的是 8-N-1(1个起始位,8个数据位,无奇偶校验,1个停止位)。
  2. 通信语速(波特率 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)。
  • 我们的目标是解出 UBIRUBMR

为了方便你以后修改波特率,我为你做了一个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! 时,你会真切地感受到:这块冰冷的硅片,终于拥有了可以和你交流的灵魂。

相关推荐
三佛科技-134163842122 小时前
融蜡机方案,脱毛热蜡机MCU控制方案开发
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
三佛科技-134163842122 小时前
智能小夜灯方案,智能遥控台灯方案开发MCU控制方案设计
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
wdfk_prog2 小时前
MCU内核电压不稳导致程序跑飞的现象、原因与影响
数据库·单片机·嵌入式硬件
biter down2 小时前
深入浅出 C++ string 类:从原理到实战
开发语言·c++
Lhan.zzZ2 小时前
Qt多线程数据库操作:安全分离连接,彻底解决段错误
数据库·c++·qt·安全
changzehai3 小时前
RustRover + J-Link 一键调试 STM32 教程
stm32·单片机·嵌入式硬件·rust·rustrover
小樱花的樱花3 小时前
C++引用:高效编程的技巧
开发语言·数据结构·c++·算法
Yupureki3 小时前
《算法竞赛从入门到国奖》算法基础:动态规划-最长子序列
c语言·c++·算法·动态规划
南境十里·墨染春水3 小时前
C++笔记 继承中重载规则 公有私有继承的区别(面向对象)
开发语言·c++·笔记