2. STM32 SPI外设架构:从框图到实战

SPI外设的架构,说白了就是一张硬件地图。你只有把这张地图看懂了,写驱动的时候才知道数据往哪走、时钟从哪来、中断怎么触发。我刚开始学的时候,直接对着寄存器手册硬啃,结果越看越懵。后来老老实实把框图抄了一遍,才豁然开朗。

2.1 SPI框图解析:数据流动的“高速公路”

先看这张核心框图,它展示了SPI外设内部的数据流向和控制逻辑。

STM32 SPI外设核心架构框图 STM32 SPI外设核心架构框图 SPI控制寄存器组 波特率分频器 CPOL / CPHA 控制 主/从模式选择 数据寄存器 (DR) 8/16位移位寄存器 中断 / DMA 请求 说明: 数据从DR写入移位寄存器,在SCK时钟驱动下逐位移出/移入。主从模式决定SCK由谁产生。

这张图里,我重点标出了几个关键模块。你写驱动的时候,本质上就是在操作这些模块的寄存器。

  • 控制寄存器组:配置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几乎零负载
实时性要求高 中断 + 高优先级 保证响应时间