2. STM32 SPI外设架构:从框图到实战
SPI外设的架构,说白了就是一张硬件地图。你只有把这张地图看懂了,写驱动的时候才知道数据往哪走、时钟从哪来、中断怎么触发。我刚开始学的时候,直接对着寄存器手册硬啃,结果越看越懵。后来老老实实把框图抄了一遍,才豁然开朗。
2.1 SPI框图解析:数据流动的“高速公路”
先看这张核心框图,它展示了SPI外设内部的数据流向和控制逻辑。
这张图里,我重点标出了几个关键模块。你写驱动的时候,本质上就是在操作这些模块的寄存器。
- 控制寄存器组:配置SPI的模式、使能、数据格式等。说白了就是SPI的“大脑”。
- 波特率分频器:决定SCK时钟频率。我习惯叫它“油门”,踩多大看外设能跑多快。
- CPOL/CPHA控制:时钟极性和相位。这个配错了,数据就全乱套。
- 数据寄存器(DR):读写数据的入口。你往DR写一个字节,硬件自动帮你发出去。
- 移位寄存器:真正的数据搬运工。数据在这里串行移出/移入。
我的习惯: 每次拿到一个新板子,第一件事就是对着参考手册的框图,把每个模块的寄存器地址标出来。这样写代码的时候,心里有张“地图”,不会迷路。
2.2 时钟树与波特率配置:别让外设“跑太快”
SPI的时钟源来自APB总线。你想想看,APB时钟经过分频后,才能得到SCK。这个分频系数由SPI_CR1寄存器的BR[2:0]位控制。
| BR[2:0] | 分频系数 | SCK频率 (APB=72MHz) | 典型应用场景 |
|---|---|---|---|
| 000 | 2 | 36 MHz | 高速ADC、存储器 |
| 001 | 4 | 18 MHz | LCD显示屏 |
| 010 | 8 | 9 MHz | SD卡、传感器 |
| 011 | 16 | 4.5 MHz | 温湿度传感器 |
| 100 | 32 | 2.25 MHz | 慢速外设 |
| 101 | 64 | 1.125 MHz | 隔离通信 |
| 110 | 128 | 562.5 kHz | 长距离传输 |
| 111 | 256 | 281.25 kHz | 极低速外设 |
我曾经踩过的坑: 有一次驱动一个温湿度传感器,手册上写最大支持10MHz。我直接配了18MHz,结果数据全是0x00。查了半天才发现,传感器虽然标称10MHz,但实际布线太长,信号衰减严重。后来降到2.25MHz,一切正常。所以,波特率不是越高越好,够用就行。
配置波特率的代码很简单,但要注意顺序:
// 假设APB时钟为72MHz,目标SCK为9MHz(分频8)
SPI1->CR1 |= (0x02 << 3); // BR[2:0] = 010, 分频8
// 我习惯先配置分频,再使能SPI
SPI1->CR1 |= SPI_CR1_SPE; // 使能SPI
2.3 数据寄存器与FIFO:读写数据的“门”
数据寄存器(DR)是SPI外设和CPU之间的数据通道。你往DR写数据,硬件自动发送;你读DR,得到的是接收到的数据。
但这里有个细节:DR是16位的。如果你配置了8位数据格式,写入DR时只使用低8位;16位格式则使用全部16位。
关键点: 读DR和写DR其实是两个不同的物理寄存器,但映射到同一个地址。硬件会自动区分你是读还是写。所以不用担心“写DR会不会覆盖接收数据”这种问题。
对于有FIFO的STM32型号(比如F4、F7系列),事情就更有意思了。FIFO可以缓存多个数据,减少CPU的干预次数。
| 特性 | 无FIFO | 有FIFO |
|---|---|---|
| 数据缓冲深度 | 1个字节/半字 | 4个字节/半字 |
| 中断频率 | 每字节触发一次 | 可配置阈值触发 |
| CPU负载 | 高 | 低 |
| 适用场景 | 低速、简单通信 | 高速、大数据量 |
我建议:如果芯片支持FIFO,尽量用起来。特别是做显示驱动或者SD卡读写的时候,FIFO能省不少CPU时间。
2.4 中断与DMA请求线:让CPU“喘口气”
SPI外设可以产生多种中断事件。你想想看,如果每次收发一个字节都要CPU轮询,那CPU基本就废了。中断和DMA就是来解放CPU的。
SPI的中断事件主要有这几个:
- TXE (发送缓冲区空):可以往DR写下一个数据了。
- RXNE (接收缓冲区非空):DR里有数据可以读了。
- ERR (错误事件):溢出、模式错误、CRC错误等。
配置中断的代码示例:
// 使能SPI1的RXNE中断
SPI1->CR2 |= SPI_CR2_RXNEIE;
// 配置NVIC
NVIC_EnableIRQ(SPI1_IRQn);
NVIC_SetPriority(SPI1_IRQn, 1);
我的经验: 中断服务函数里尽量只做“搬数据”这件事。别在中断里做复杂的计算或者调用延时函数。我曾经见过有人在SPI中断里调printf,结果系统直接卡死。中断里要快进快出。
如果数据量比较大,比如一次传几百个字节,用中断还是有点累。这时候就该DMA上场了。
SPI的DMA请求线有两条:
- SPI_TX_DMA:发送请求。当发送缓冲区空时触发。
- SPI_RX_DMA:接收请求。当接收缓冲区非空时触发。
配置DMA传输的代码:
// 配置DMA发送通道
DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)tx_buffer;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStruct.DMA_BufferSize = 256;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
DMA_Init(DMA2_Stream3, &DMA_InitStruct);
// 使能SPI的DMA发送请求
SPI1->CR2 |= SPI_CR2_TXDMAEN;
注意: 使用DMA时,一定要确保SPI的DMA请求使能位和DMA通道的使能位都配置正确。我遇到过好几次,DMA配置了半天没反应,最后发现是SPI_CR2里的TXDMAEN没置位。这种低级错误,查起来最费时间。
总结一下中断和DMA的选择:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 单次收发1-2字节 | 轮询 | 简单直接,代码量小 |
| 间歇性收发,数据量小 | 中断 | 不占用CPU,响应及时 |
| 连续大数据量传输 | DMA | CPU几乎零负载 |
| 实时性要求高 | 中断 + 高优先级 | 保证响应时间 |