3. STM32串口硬件基础:USART/UART外设框图、时钟树与波特率生成、TX/RX引脚与GPIO复用、中断与DMA机制
好,咱们进入正题。这一章我打算把STM32的串口硬件底子给你讲透。说白了,你写驱动之前,得先知道硬件长什么样、怎么干活。不然代码写得再漂亮,跑不起来也是白搭。
我个人习惯,拿到一个新MCU,第一件事就是翻它的外设框图。这玩意儿比看几百页手册管用多了。来,咱们先看一张我画的串口核心结构图。
3.1 外设框图:一眼看穿串口内部结构
你看上面这张图,我把USART的核心模块拆成了几块:时钟树、波特率生成器、发送器、接收器,再加上外围的GPIO复用、中断控制器和DMA。
嗯,这里要注意,STM32的USART和UART其实有区别。USART支持同步模式(带时钟线SCLK),UART只支持异步。不过咱们做嵌入式串口通信,99%的情况都用异步模式,所以后面我统一叫串口。
我在项目中遇到过一个问题:有个同事把USART1当UART用,结果发现TX引脚死活没波形。查了半天,原来是忘了配置SCLK引脚的复用功能,导致时钟输出干扰了数据线。所以啊,用USART的异步模式时,记得把SCLK引脚配成普通GPIO或者干脆不用。
3.2 时钟树:串口的心跳从哪来
串口要工作,首先得有时钟。STM32的时钟树比较复杂,但咱们只需要关心串口挂在哪条总线上。
| 串口外设 | 挂载总线 | 典型时钟源 | 最大频率 |
|---|---|---|---|
| USART1、USART6 | APB2 | PCLK2(通常=SYSCLK) | 最高90MHz(F4系列) |
| USART2、USART3、UART4~8 | APB1 | PCLK1(通常=SYSCLK/2) | 最高45MHz(F4系列) |
说白了,APB1上的串口速度上限只有APB2上的一半。你想想看,如果你要用高速串口(比如3Mbps以上),最好选USART1或USART6。我刚开始做项目时没注意这个,把USART2配成了4.5Mbps,结果数据全是乱码。查了三天才发现是APB1时钟频率不够。
CONFIG_USART1_CLOCK_SOURCE和CONFIG_USART2_CLOCK_SOURCE这两个宏定义。我见过有人把USART1的时钟源配成了PCLK1,结果波特率死活算不对。
3.3 波特率生成:算不对就等着收乱码
波特率生成是串口驱动里最容易出错的地方。STM32用的是分数波特率发生器,公式长这样:
// 波特率计算公式(过采样16模式)
Tx/Rx Baud = PCLK / (16 * USARTDIV)
// USARTDIV 是一个20位小数,高16位是整数部分,低4位是小数部分
// 例如:PCLK=72MHz,目标波特率=115200
// USARTDIV = 72000000 / (16 * 115200) = 39.0625
// 整数部分 = 39 = 0x27
// 小数部分 = 0.0625 * 16 = 1 = 0x1
// USART_BRR = (39 << 4) | 1 = 0x271
为什么会这么设计?说白了,就是为了用整数和小数组合来逼近精确的波特率。我建议你在写驱动时,先拿计算器算一遍,再对照寄存器值验证。我曾经在F103上配了一个2Mbps的波特率,算出来USARTDIV是2.8125,小数部分0.8125*16=13,结果实际误差达到了0.8%,通信偶尔丢包。
3.4 TX/RX引脚与GPIO复用:别把线接错了
串口的TX和RX引脚不是随便选的。每个串口都有固定的引脚映射,而且通常有多个可选映射(比如USART1可以是PA9/PA10,也可以是PB6/PB7)。
在NuttX下配置引脚复用,一般是在board.h里这样写:
/* 以STM32F4Discovery板为例,USART1使用PA9(TX)和PA10(RX) */
#define GPIO_USART1_TX GPIO_USART1_TX_1 /* PA9, AF7 */
#define GPIO_USART1_RX GPIO_USART1_RX_1 /* PA10, AF7 */
嗯,这里要注意,不同STM32系列的AF编号不一样。F1系列用AF7,F4系列也是AF7,但G0系列可能用AF1或AF2。我建议你每次换芯片型号时,都去翻一下数据手册的引脚定义表,别凭经验硬套。
3.5 中断机制:别让CPU傻等
串口中断是驱动开发的核心。STM32的USART中断事件很多,但咱们常用的就这几个:
- TXE(发送数据寄存器空):可以往DR寄存器写下一个字节了
- TC(发送完成):最后一个字节已经从移位寄存器送出去了
- RXNE(接收数据寄存器非空):收到一个字节,赶紧来读
- IDLE(总线空闲):接收线上超过一个帧时间没数据了,常用于不定长接收
我个人习惯,发送用TXE中断配合环形缓冲区,接收用RXNE中断配合DMA。为什么?因为如果每个字节都进中断,高频下CPU根本忙不过来。你想想看,115200波特率下,每秒钟要进11520次中断,每次中断保存恢复上下文就要几十个周期,CPU开销太大了。
在NuttX中,中断服务函数的注册方式是这样的:
/* 在串口驱动初始化时注册中断 */
irq_attach(STM32_IRQ_USART1, up_uart_interrupt, priv);
up_enable_irq(STM32_IRQ_USART1);
/* 中断处理函数中判断中断源 */
uint32_t sr = priv->usartbase->sr;
if (sr & USART_SR_RXNE) {
/* 处理接收 */
uint16_t data = priv->usartbase->dr;
/* 存入环形缓冲区 */
}
if (sr & USART_SR_TXE) {
/* 处理发送 */
/* 从环形缓冲区取数据写入DR */
}
3.6 DMA机制:让数据自己跑起来
DMA是串口高性能通信的关键。说白了,就是让DMA控制器代替CPU搬运数据,CPU可以腾出手来做别的事。
STM32的串口DMA请求通常是这样分配的:
| 串口 | TX DMA通道 | RX DMA通道 | DMA控制器 |
|---|---|---|---|
| USART1 | DMA2 Stream7 | DMA2 Stream2 | DMA2 |
| USART2 | DMA1 Stream6 | DMA1 Stream5 | DMA1 |
| USART3 | DMA1 Stream3 | DMA1 Stream1 | DMA1 |
我在项目中遇到过一个问题:用DMA接收不定长数据时,怎么知道一帧数据结束了?我的做法是:DMA接收 + IDLE中断。DMA负责把数据搬到内存,IDLE中断负责告诉CPU「数据收完了,快来处理」。这样既保证了高速接收,又实现了帧同步。
- DMA传输方向:外设到内存(接收)或内存到外设(发送)
- 外设地址固定(USART_DR寄存器),内存地址递增
- 数据宽度:8位(串口数据是8位)
- 循环模式:接收用循环模式,发送用普通模式
- 优先级:如果多个DMA通道同时工作,记得配好优先级
好了,这一章的内容就到这儿。串口的硬件基础是驱动开发的根基,时钟树、波特率、引脚复用、中断和DMA,每一个环节都马虎不得。下一章咱们就要动手写代码了,到时候我会带着你一步步把NuttX的串口驱动框架搭起来。