4. 串口驱动接入(UART)
UART,说白了就是嵌入式世界里最基础的“对话线”。Bootloader 阶段,屏幕没亮,网络没通,你靠什么跟芯片交流?绝大多数时候,靠的就是这根串口线。我最早做 Bootloader 时,第一件事就是把 UART 调通,因为后面所有的调试信息、升级指令、状态打印,全得从这条通道走。
这一章,我们就来聊聊 UART 协议的基础,以及在 Bootloader 这种裸机环境下,怎么用轮询模式把串口驱动跑起来。
4.1 UART 协议基础
UART 全称是 Universal Asynchronous Receiver/Transmitter,通用异步收发器。注意“异步”这两个字——它没有单独的时钟线,收发双方靠约定的波特率来同步。
一个标准的 UART 数据帧长这样:
| 起始位 | 数据位(5~8位) | 校验位(可选) | 停止位(1/1.5/2位) |
|---|---|---|---|
| 1位低电平 | LSB 先发 | 奇/偶/无 | 高电平 |
我个人习惯用 8N1 配置——8 位数据、无校验、1 位停止位。这是最通用的组合,兼容性最好。你在 Bootloader 里用这个配置,基本不会踩坑。
4.2 Bootloader 中 UART 的轮询模式驱动实现
Bootloader 阶段,中断系统可能还没完全初始化,或者你根本不想引入中断的复杂性。这时候,轮询模式就是最靠谱的选择。
轮询模式的核心思路很简单:CPU 不断去检查 UART 的状态寄存器,看看有没有数据可读,或者能不能发数据。说白了,就是“主动问,不等待通知”。
4.2.1 初始化流程
初始化 UART,通常需要做这几步:
- 使能时钟——给 UART 外设和对应的 GPIO 口提供时钟。
- 配置 GPIO 复用——把 TX、RX 引脚切换到 UART 功能。
- 设置波特率——通过分频寄存器算出匹配的数值。
- 配置数据格式——数据位、停止位、校验位。
- 使能 UART——打开收发器。
我在项目中遇到过一个问题:初始化顺序搞反了。先使能 UART,再配 GPIO,结果 TX 引脚在使能瞬间输出了一段乱码。从那以后,我都是先配好 GPIO 和时钟,最后才打开 UART 使能位。
4.2.2 发送一个字节
发送的逻辑很简单:等发送保持寄存器空,然后把数据写进去。
void uart_putc(char c)
{
// 等待发送缓冲区空
while (!(UART->SR & UART_SR_TXE));
// 写入数据
UART->DR = c;
}
这里有个细节:有些芯片的 TXE 标志位表示“数据已转移到移位寄存器”,但可能还没发完。如果你紧接着要关串口或进低功耗,最好再等 TC(发送完成)标志位。嗯,这里要注意。
4.2.3 接收一个字节
接收也是类似的轮询:
char uart_getc(void)
{
// 等待接收数据就绪
while (!(UART->SR & UART_SR_RXNE));
// 读取数据
return (char)(UART->DR);
}
你可能会问:如果一直没数据,程序不就卡死了?没错,所以实际 Bootloader 里,我通常会加一个超时机制。比如用 SysTick 或者一个简单的循环计数,超过一定时间就返回错误。
4.3 波特率配置与数据收发
波特率是 UART 通信的“节奏”。收发双方必须用同一个节奏,否则数据就是一团乱码。
4.3.1 波特率计算
大多数 MCU 的 UART 波特率生成器,都是基于系统时钟分频的。公式大致是:
波特率 = 时钟频率 / (16 * USARTDIV)
USARTDIV 是一个分频系数,通常写在波特率寄存器里。比如系统时钟 72MHz,想要 115200 波特率:
USARTDIV = 72000000 / (16 * 115200) = 39.0625
取整后写入寄存器,实际波特率会有微小偏差。偏差在 ±2% 以内通常没问题。我见过一个项目,晶振精度不够,偏差到了 3.5%,结果 115200 下频繁丢字节,降到 9600 就正常了。
| 目标波特率 | 时钟 72MHz | 时钟 48MHz | 时钟 16MHz |
|---|---|---|---|
| 115200 | 39.0625 | 26.0417 | 8.6806 |
| 57600 | 78.1250 | 52.0833 | 17.3611 |
| 9600 | 468.7500 | 312.5000 | 104.1667 |
4.3.2 数据收发流程
在 Bootloader 里,数据收发通常不是单个字节,而是一包一包地处理。我习惯的做法是:
- 发送:把要打印的字符串或数据包,逐个字节调用
uart_putc。 - 接收:用超时轮询收一个完整的数据帧,比如以 0x0A 换行符结尾,或者固定长度。
举个例子,一个简单的串口打印函数:
void uart_puts(const char *str)
{
while (*str)
{
uart_putc(*str++);
}
}
接收一个带超时的字节:
int uart_getc_timeout(char *c, uint32_t timeout)
{
while (timeout--)
{
if (UART->SR & UART_SR_RXNE)
{
*c = (char)(UART->DR);
return 0; // 成功
}
// 简单的延时,具体延时量取决于你的系统时钟
for (volatile uint32_t i = 0; i < 100; i++);
}
return -1; // 超时
}
4.4 本章知识体系
下面这张图,把 UART 在 Bootloader 阶段的接入逻辑串起来了:
这张图把整个流程串起来了。从协议基础开始,到初始化配置,再到收发实现,最后落到 Bootloader 的具体应用里。你写代码的时候,按这个顺序走,基本不会乱。
我记得有一次,一个同事在 Bootloader 里加了中断接收,结果中断向量表还没重定位,一收数据就跑飞。我帮他改成轮询模式,问题立刻解决。所以说,Bootloader 阶段,简单就是美。