第4章 UART驱动与串口调试(上):USART外设框图与寄存器详解、轮询模式发送与接收
UART,说白了就是串口通信。做嵌入式开发的人,没有不跟它打交道的。我刚开始学STM32那会儿,第一个调通的外设就是UART——能打印出"Hello World"的那一刻,感觉整个世界都亮了。
这一章我们不讲理论绕弯子,直接看USART的硬件框图、寄存器,然后手写轮询模式的发送和接收。你跟着我走一遍,后面调试任何外设都会轻松很多。
USART外设框图:先看明白硬件怎么跑
STM32的USART模块,内部结构其实不复杂。我习惯把它分成三块来看:
- 发送通路:数据从CPU写入发送数据寄存器(TDR),经过移位寄存器,从TX引脚逐位发出
- 接收通路:RX引脚接收到的数据,经过移位寄存器,存入接收数据寄存器(RDR),CPU去读
- 控制与状态:波特率发生器、中断控制、各种状态标志位
你想想看,数据流其实就是一个"写寄存器→硬件自动移位→读寄存器"的过程。嗯,就这么简单。
核心要点:USART的发送和接收是独立的硬件通路,可以同时工作(全双工)。发送靠写TDR触发,接收靠硬件自动完成,你只需要去RDR里取数据。
USART核心寄存器:你只需要记住这几个
USART的寄存器很多,但实际开发中常用的就那么几个。我列个表,你对照着看:
| 寄存器 | 全称 | 作用 | 轮询模式必用 |
|---|---|---|---|
| SR | 状态寄存器 | 查看发送完成、接收就绪等状态 | ✅ 轮询TXE/TC/RXNE位 |
| DR | 数据寄存器 | 写=发送数据,读=接收数据 | ✅ 直接读写 |
| BRR | 波特率寄存器 | 设置通信速率 | ✅ 初始化时配置 |
| CR1 | 控制寄存器1 | 使能USART、使能发送/接收、配置数据位/奇偶校验 | ✅ 初始化时配置 |
| CR2 | 控制寄存器2 | 配置停止位位数 | ✅ 初始化时配置 |
| CR3 | 控制寄存器3 | 流控制、单线模式等 | 一般用默认值 |
我的习惯:SR寄存器里的TXE(发送数据寄存器空)和TC(发送完成)是两个容易混淆的标志位。TXE表示你可以往DR里写下一个字节了,TC表示最后一个字节已经从引脚上发完了。轮询发送时,我一般用TXE,效率更高。
轮询模式发送:BSP_UART_PutChar 实现
轮询模式,说白了就是CPU死等。发一个字节,等硬件准备好,再发下一个。虽然效率不高,但胜在简单可靠。
实现步骤就三步:
- 等待TXE标志位置1(表示TDR已空,可以写数据)
- 把要发送的字节写入DR寄存器
- (可选)等待TC标志位,确保数据完全发出
代码长这样:
/* 文件: bsp_uart.c */
#include "bsp_uart.h"
void BSP_UART_PutChar(USART_TypeDef* USARTx, uint8_t ch)
{
/* 1. 等待发送数据寄存器为空 (TXE=1) */
while (!(USARTx->SR & USART_SR_TXE));
/* 2. 写入数据寄存器,硬件自动开始发送 */
USARTx->DR = ch;
/* 3. 可选:等待发送完成 (TC=1),确保数据从引脚完全发出 */
/* 如果连续发送多个字节,可以省略这一步,用TXE就够了 */
while (!(USARTx->SR & USART_SR_TC));
}
我曾经踩过的坑:在连续发送大量数据时,如果每发一个字节都等TC,性能会明显下降。TC是在最后一个停止位结束后才置位的,而TXE在数据转移到移位寄存器后就置位了。所以连续发送时,只等TXE就够了,最后一个字节再等TC。
轮询模式接收:BSP_UART_GetChar 实现
接收比发送更简单。你只需要等RXNE(接收数据寄存器非空)标志位,然后去读DR就行了。
步骤:
- 等待RXNE标志位置1(表示收到一个字节)
- 从DR寄存器读取数据
uint8_t BSP_UART_GetChar(USART_TypeDef* USARTx)
{
/* 1. 等待接收数据寄存器非空 (RXNE=1) */
while (!(USARTx->SR & USART_SR_RXNE));
/* 2. 读取数据寄存器,硬件自动清除RXNE标志 */
return (uint8_t)(USARTx->DR);
}
嗯,就这么简单。你可能会问:读DR的时候为什么要强制转成uint8_t?因为DR寄存器是16位的,但实际数据只占低8位(8位数据模式时),高8位是保留位或用于9位数据模式。
完整的初始化流程
有了发送和接收函数,还得先初始化USART才能用。初始化步骤我总结成一张表:
| 步骤 | 操作 | 寄存器/外设 | 说明 |
|---|---|---|---|
| 1 | 使能USART时钟 | RCC->APB2ENR / APB1ENR | USART1挂在APB2上,USART2/3挂在APB1上 |
| 2 | 配置GPIO为复用功能 | GPIOx->CRH/CRL | TX推挽复用输出,RX浮空输入 |
| 3 | 设置波特率 | USARTx->BRR | 根据时钟频率和波特率计算分频值 |
| 4 | 配置数据位、停止位、校验 | USARTx->CR1, CR2 | 常用8N1:8数据位、无校验、1停止位 |
| 5 | 使能发送器和接收器 | USARTx->CR1 | 置位TE和RE位 |
| 6 | 使能USART | USARTx->CR1 | 置位UE位,USART开始工作 |
对应的初始化代码:
void BSP_UART_Init(USART_TypeDef* USARTx, uint32_t baudrate)
{
uint32_t apb_clock;
uint16_t usart_div;
/* 步骤1:使能时钟(以USART1为例) */
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
/* 步骤2:配置GPIO PA9=TX(推挽复用), PA10=RX(浮空输入) */
GPIOA->CRH &= ~(0xFF << 4); /* 清除CNF9,MODE9,CNF10,MODE10 */
GPIOA->CRH |= 0x0B << 4; /* PA9: 推挽复用输出50MHz */
GPIOA->CRH |= 0x04 << 8; /* PA10: 浮空输入 */
/* 步骤3:设置波特率 */
apb_clock = 72000000; /* 假设系统时钟72MHz,USART1挂在APB2上 */
usart_div = apb_clock / baudrate;
USARTx->BRR = usart_div;
/* 步骤4:配置8N1模式 */
USARTx->CR1 &= ~USART_CR1_M; /* 8位数据模式 */
USARTx->CR1 &= ~USART_CR1_PCE; /* 无校验 */
USARTx->CR2 &= ~USART_CR2_STOP; /* 1位停止位 */
/* 步骤5:使能发送器和接收器 */
USARTx->CR1 |= USART_CR1_TE;
USARTx->CR1 |= USART_CR1_RE;
/* 步骤6:使能USART */
USARTx->CR1 |= USART_CR1_UE;
}
关于波特率计算:STM32的BRR寄存器存放的是USARTDIV的16倍值。当OVER8=0(默认)时,公式是:USARTDIV = 时钟频率 / (16 × 波特率)。我一般直接用整数除法,误差在3%以内都能正常工作。如果你要求高精度,可以用浮点计算再四舍五入。
实战:串口打印"Hello World"
把上面的函数组合起来,写个main函数测试一下:
/* 文件: main.c */
#include "bsp_uart.h"
/* 简单的字符串发送函数 */
void BSP_UART_SendString(USART_TypeDef* USARTx, const char* str)
{
while (*str)
{
BSP_UART_PutChar(USARTx, *str++);
}
}
int main(void)
{
uint8_t rx_data;
/* 初始化USART1,波特率115200 */
BSP_UART_Init(USART1, 115200);
/* 发送欢迎信息 */
BSP_UART_SendString(USART1, "Hello World!\r\n");
BSP_UART_SendString(USART1, "UART轮询模式测试开始...\r\n");
while (1)
{
/* 接收一个字节,然后原样回显 */
rx_data = BSP_UART_GetChar(USART1);
BSP_UART_PutChar(USART1, rx_data);
}
}
把这段代码烧进板子,打开串口助手,设置波特率115200、8N1。你应该能看到"Hello World!",然后你发什么,它就回什么——这就是最简单的串口回环测试。
验证方法:如果串口助手收不到数据,先检查三件事:1) TX/RX引脚有没有接反?2) 波特率是不是一致?3) GPIO的复用功能配置对了没有?我遇到过最蠢的一次,是把PA9和PA10的配置写反了,TX配成了输入,RX配成了输出……折腾了半小时才发现。
轮询模式的优缺点
轮询模式虽然简单,但不是万能的。我列个对比,你心里有个数:
| 对比项 | 轮询模式 | 中断模式 | DMA模式 |
|---|---|---|---|
| CPU占用 | 高(死等) | 低(触发时才响应) | 极低(硬件搬运) |
| 实现复杂度 | 低 | 中 | 高 |
| 实时性 | 差(阻塞) | 好 | 好 |
| 适用场景 | 调试打印、简单通信 | 交互式通信 | 大数据量传输 |
我个人习惯是:调试阶段先用轮询模式把功能调通,然后再根据需求升级成中断或DMA。这样出问题了容易定位——如果轮询模式都不通,那肯定是硬件或初始化的问题,跟中断/DMA无关。
注意:轮询模式在接收时有个致命问题——如果对方一直不发数据,你的程序就会卡死在BSP_UART_GetChar里。所以在实际产品中,轮询接收很少用,一般都会加超时机制或者直接用中断。这个我们下一章会讲。
好了,这一章的内容就到这里。你跟着代码敲一遍,用串口助手验证一下,应该就能把USART的轮询模式吃透了。记住:外设驱动没有玄学,只有寄存器。框图看懂了,寄存器配对了,代码写对了,它就能跑起来。