3. MCU硬件SPI外设:从框图到寄存器,手把手带你初始化

说实话,很多初学者一上来就对着寄存器手册硬啃,结果越看越懵。我当年也是这样,看了三天框图,代码一行没写。后来发现,搞懂SPI外设其实有套路——先看框图,再抓关键寄存器,最后走一遍初始化流程。今天我就按这个顺序带你过一遍。

3.1 SPI外设框图:一眼看穿数据流向

先别急着翻数据手册。我画了一张精简版的SPI框图,你跟着我走一遍数据流,马上就能明白SPI是怎么工作的。

STM32/GD32 SPI外设核心框图 AHB/APB总线 DR数据寄存器 移位寄存器 控制逻辑(CR1/CR2) 波特率发生器 SR状态寄存器 IO引脚 SCK MOSI MISO NSS 数据流向说明: 1. CPU通过AHB/APB总线将待发送数据写入DR寄存器 2. DR数据自动加载到移位寄存器,同时移位寄存器开始移位输出 3. 每移出一位,从MISO引脚读入一位,实现全双工通信 4. 传输完成后,移位寄存器数据回写到DR,SR状态位更新

看懂这张图了吗?核心就三个模块:DR数据寄存器、移位寄存器、控制逻辑。你写数据到DR,硬件自动帮你移位发送,同时接收数据。这就是SPI的全双工本质——发的同时就在收。

我的经验:调试SPI通信时,90%的问题都出在「数据没写进DR」或者「没等发送完成就读DR」。记住这个数据流,你排查问题就有方向了。

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一起决定采样边沿
我曾经踩过的坑:有一次调试一个温湿度传感器,死活读不到数据。折腾了两小时,最后发现是CPOL和CPHA配反了。传感器手册上写的是「Mode 0」,我配成了「Mode 3」。记住:CPOL=0, CPHA=0 是模式0;CPOL=1, CPHA=1 是模式3。这两个最常用。

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=有数据可读 轮询接收时用
调试口诀:「写前等TXE,读前等RXNE,发完等BSY清零」。我每次写SPI驱动都默念一遍,基本不会出问题。

3.2.4 DR数据寄存器——数据的「收发室」

DR寄存器就一个功能:你往它写数据就是发送,你从它读数据就是接收。但有个关键点——读和写是同一个地址。硬件会自动区分你是读还是写操作。

// 发送一个字节
SPI1->DR = 0xAA;  // 写DR,硬件自动启动发送

// 接收一个字节
uint8_t data = SPI1->DR;  // 读DR,获取接收到的数据

你想想看,为什么读和写是同一个地址?因为SPI是全双工的,你发一个字节的同时,硬件已经帮你收了一个字节。所以读写操作必须成对出现。

3.3 SPI初始化流程:七步走,步步有坑

好了,寄存器都认识了,咱们来走一遍初始化流程。我把它总结成七步,每一步都有我的血泪教训。

  1. 使能SPI时钟和GPIO时钟——别漏了GPIO时钟,我漏过三次
  2. 配置GPIO为复用功能——SCK、MOSI、MISO、NSS都要配
  3. 复位SPI外设——确保寄存器是默认值
  4. 配置CR1——设置波特率、CPOL/CPHA、主从模式、数据格式
  5. 配置CR2——如果需要中断或DMA,在这里使能
  6. 使能SPI——CR1的SPE位置1
  7. 发送一个哑数据——清除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;  // 读走哑数据
}
个人习惯:我一般把波特率先设到最低(比如256分频),等通信调通了再逐步提高。这样即使从机响应慢,也不会丢数据。调通后再优化速度,稳得很。

3.4 避坑指南:我踩过的那些坑

做嵌入式这么多年,SPI相关的坑我踩了不少。挑几个典型的跟你说说。

  • 坑一:GPIO速度没配——SPI时钟跑快了,波形就变形了。记得把GPIO速度配到50MHz以上。
  • 坑二:NSS用软件模式忘了拉高——多从机时,NSS拉低就选中了从机,忘了拉高会导致总线冲突。
  • 坑三:读DR之前没等RXNE置位——读早了,拿到的是上次的数据。我因为这个查了两天bug。
  • 坑四:中断服务函数里没清标志位——中断一直触发,CPU卡死在中断里。别问我怎么知道的。

嗯,这些坑你记一下,遇到了能省不少时间。SPI其实不难,就是细节多。你把框图看懂了,寄存器搞清楚了,初始化流程走顺了,后面写驱动就是水到渠成的事。