3. MCU硬件SPI外设:从框图到寄存器,手把手带你初始化
说实话,很多初学者一上来就对着寄存器手册硬啃,结果越看越懵。我当年也是这样,看了三天框图,代码一行没写。后来发现,搞懂SPI外设其实有套路——先看框图,再抓关键寄存器,最后走一遍初始化流程。今天我就按这个顺序带你过一遍。
3.1 SPI外设框图:一眼看穿数据流向
先别急着翻数据手册。我画了一张精简版的SPI框图,你跟着我走一遍数据流,马上就能明白SPI是怎么工作的。
看懂这张图了吗?核心就三个模块:DR数据寄存器、移位寄存器、控制逻辑。你写数据到DR,硬件自动帮你移位发送,同时接收数据。这就是SPI的全双工本质——发的同时就在收。
3.2 关键寄存器详解:CR1、CR2、SR、DR
寄存器很多,但常用的就这几个。我按使用频率给你排个序,咱们一个一个说。
3.2.1 CR1控制寄存器1——SPI的「总开关」
CR1是SPI外设最重要的寄存器,没有之一。你配置SPI模式、波特率、数据格式全得靠它。
| 位域 | 名称 | 功能描述 | 我的建议 |
|---|---|---|---|
| Bit 15 | BIDIMODE | 双向数据模式使能 | 一般用默认0,除非你只用单线 |
| Bit 14 | BIDIOE | 双向模式输出使能 | 配合BIDIMODE使用 |
| Bit 10:9 | SSM/SSI | 软件NSS管理 | 多从机时建议用硬件NSS |
| Bit 8 | DFF | 数据帧格式:0=8位,1=16位 | 传感器通常用8位 |
| Bit 6 | SPE | SPI使能位:1=使能 | 最后一步再置1 |
| Bit 5:3 | BR[2:0] | 波特率分频系数 | 从低速开始调,别一上来就最高速 |
| Bit 2 | MSTR | 主从选择:1=主机,0=从机 | 大部分场景我们都是主机 |
| Bit 1 | CPOL | 时钟极性 | 看从机手册,配错就收不到数据 |
| Bit 0 | CPHA | 时钟相位 | 和CPOL一起决定采样边沿 |
3.2.2 CR2控制寄存器2——中断和DMA的开关
CR2相对简单,主要控制中断和DMA。我一般只用它来使能「发送完成中断」和「接收缓冲区非空中断」。
| 位域 | 名称 | 功能 |
|---|---|---|
| Bit 7 | TXEIE | 发送缓冲区空中断使能 |
| Bit 6 | RXNEIE | 接收缓冲区非空中断使能 |
| Bit 5 | ERRIE | 错误中断使能 |
| Bit 1 | FRF | 帧格式:0=Motorola,1=TI |
嗯,这里要注意:FRF位默认是0(Motorola模式),也就是我们常用的标准SPI。TI模式是另一种协议,一般用不到。
3.2.3 SR状态寄存器——通信的「信号灯」
SR寄存器是只读的,你用它来判断SPI当前的状态。我调试时最常看的就是它。
| 位域 | 名称 | 含义 | 什么时候用 |
|---|---|---|---|
| Bit 7 | BSY | 忙标志:1=正在传输 | 发送前检查是否空闲 |
| Bit 6 | OVR | 溢出错误 | 接收太快没来得及读DR |
| Bit 1 | TXE | 发送缓冲区空:1=可以写数据 | 轮询发送时用 |
| Bit 0 | RXNE | 接收缓冲区非空:1=有数据可读 | 轮询接收时用 |
3.2.4 DR数据寄存器——数据的「收发室」
DR寄存器就一个功能:你往它写数据就是发送,你从它读数据就是接收。但有个关键点——读和写是同一个地址。硬件会自动区分你是读还是写操作。
// 发送一个字节
SPI1->DR = 0xAA; // 写DR,硬件自动启动发送
// 接收一个字节
uint8_t data = SPI1->DR; // 读DR,获取接收到的数据
你想想看,为什么读和写是同一个地址?因为SPI是全双工的,你发一个字节的同时,硬件已经帮你收了一个字节。所以读写操作必须成对出现。
3.3 SPI初始化流程:七步走,步步有坑
好了,寄存器都认识了,咱们来走一遍初始化流程。我把它总结成七步,每一步都有我的血泪教训。
- 使能SPI时钟和GPIO时钟——别漏了GPIO时钟,我漏过三次
- 配置GPIO为复用功能——SCK、MOSI、MISO、NSS都要配
- 复位SPI外设——确保寄存器是默认值
- 配置CR1——设置波特率、CPOL/CPHA、主从模式、数据格式
- 配置CR2——如果需要中断或DMA,在这里使能
- 使能SPI——CR1的SPE位置1
- 发送一个哑数据——清除BSY标志,让外设进入就绪状态
为什么要有第七步?我记得第一次写SPI驱动时,初始化完直接发数据,结果第一个字节总是丢的。后来查手册才发现,SPI使能后需要一次「空操作」来清除内部状态。说白了就是让硬件「醒一醒」。
void SPI_Init(void)
{
// 第1步:使能时钟
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; // SPI1时钟
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // GPIOA时钟(别漏!)
// 第2步:配置GPIO复用功能
// PA5=SCK, PA7=MOSI, PA6=MISO, PA4=NSS
GPIOA->MODER |= GPIO_MODER_MODER5_1; // 复用功能
GPIOA->MODER |= GPIO_MODER_MODER7_1;
GPIOA->MODER |= GPIO_MODER_MODER6_1;
GPIOA->MODER |= GPIO_MODER_MODER4_1;
GPIOA->AFR[0] |= 0x00555000; // AF5 for SPI1
// 第3步:复位SPI
SPI1->CR1 = 0;
SPI1->CR2 = 0;
// 第4步:配置CR1
SPI1->CR1 |= SPI_CR1_MSTR; // 主机模式
SPI1->CR1 |= SPI_CR1_BR_2; // 分频系数,先慢后快
SPI1->CR1 &= ~SPI_CR1_CPOL; // CPOL=0
SPI1->CR1 &= ~SPI_CR1_CPHA; // CPHA=0,模式0
SPI1->CR1 &= ~SPI_CR1_DFF; // 8位数据格式
// 第5步:配置CR2(本例不使用中断)
SPI1->CR2 = 0;
// 第6步:使能SPI
SPI1->CR1 |= SPI_CR1_SPE;
// 第7步:发送哑数据,清除BSY
SPI1->DR = 0xFF;
while(!(SPI1->SR & SPI_SR_RXNE));
(void)SPI1->DR; // 读走哑数据
}
3.4 避坑指南:我踩过的那些坑
做嵌入式这么多年,SPI相关的坑我踩了不少。挑几个典型的跟你说说。
- 坑一:GPIO速度没配——SPI时钟跑快了,波形就变形了。记得把GPIO速度配到50MHz以上。
- 坑二:NSS用软件模式忘了拉高——多从机时,NSS拉低就选中了从机,忘了拉高会导致总线冲突。
- 坑三:读DR之前没等RXNE置位——读早了,拿到的是上次的数据。我因为这个查了两天bug。
- 坑四:中断服务函数里没清标志位——中断一直触发,CPU卡死在中断里。别问我怎么知道的。
嗯,这些坑你记一下,遇到了能省不少时间。SPI其实不难,就是细节多。你把框图看懂了,寄存器搞清楚了,初始化流程走顺了,后面写驱动就是水到渠成的事。