4、SPI控制器驱动:寄存器详解与实战配置

大家好,我是你们的老朋友。今天我们来聊聊SPI控制器驱动里最核心的部分——寄存器配置、DMA传输、中断处理,还有那个让很多人头疼的时钟极性与相位。

说实话,我早年刚接触SPI驱动时,也被时钟极性搞晕过。有一次调试Flash读写,数据死活不对,折腾了两天才发现是CPOL和CPHA配反了。嗯,从那以后,我养成了一个习惯:先看从设备的数据手册,再配主控寄存器。

4.1 SPI控制器寄存器详解

先说说SPI控制器的寄存器。不同厂家的MCU,寄存器布局略有差异,但核心寄存器就那么几个。我以最常见的STM32为例,带你过一遍。

寄存器名称 功能描述 关键位域
SPI_CR1 控制寄存器1 CPOL, CPHA, MSTR, SPE, BR[2:0]
SPI_CR2 控制寄存器2 TXEIE, RXNEIE, ERRIE, FRF, SSOE
SPI_SR 状态寄存器 BSY, TXE, RXNE, MODF, OVR
SPI_DR 数据寄存器 16位数据,读写共用
SPI_CRCPR CRC多项式寄存器 用于CRC校验

我个人习惯,初始化SPI时,先配CR1,再配CR2。CR1决定了通信的底层参数,比如波特率、时钟极性。CR2则控制中断和DMA使能。

核心要点:SPI_DR寄存器是读写共用的。你往里面写数据,就是发送;你读它,就是接收。这个设计很巧妙,但新手容易搞混——发送和接收是同时进行的。

4.2 时钟极性与相位配置实战

时钟极性和相位,说白了就是决定数据在时钟的哪个边沿被采样。CPOL决定时钟空闲时的电平,CPHA决定数据采样发生在第一个边沿还是第二个边沿。

我画了一张图,帮你理清这四种模式:

SPI时钟极性与相位四种模式 模式0 (CPOL=0, CPHA=0) SCK 空闲低,第一个边沿采样 模式1 (CPOL=0, CPHA=1) SCK 空闲低,第二个边沿采样 模式2 (CPOL=1, CPHA=0) SCK 空闲高,第一个边沿采样 模式3 (CPOL=1, CPHA=1) SCK 空闲高,第二个边沿采样 数据线MOSI/MISO 数据在时钟边沿被采样 采样边沿:上升沿或下降沿,取决于CPOL和CPHA的组合 NOR Flash通常使用模式0或模式3

你想想看,NOR Flash最常用的是哪种模式?我翻过Winbond、Macronix、GD等多家厂商的数据手册,绝大多数Flash默认支持模式0(CPOL=0, CPHA=0)和模式3(CPOL=1, CPHA=1)。

实战技巧:配置时钟极性时,先看Flash数据手册里的时序图。如果图上显示SCK空闲为低,数据在第一个边沿采样,那就是模式0。如果SCK空闲为高,数据在第一个边沿采样,那就是模式2。别猜,看手册最靠谱。

4.3 DMA传输配置

SPI用轮询方式读写,CPU会被占得死死的。尤其是读写Flash时,一页256字节,如果每字节都等中断,效率太低了。这时候DMA就派上用场了。

DMA传输的核心配置,我总结为三步:

  1. 配置DMA通道:选择DMA通道,设置源地址、目的地址、传输方向
  2. 配置传输参数:数据宽度(8位或16位)、传输模式(普通或循环)、优先级
  3. 使能DMA请求:在SPI_CR2中使能TX DMA或RX DMA

我曾经在一个项目里,用DMA传输SPI数据,结果发现数据错位。排查了半天,原来是DMA的地址递增没配对。发送时源地址递增,目的地址固定;接收时源地址固定,目的地址递增。这个细节很容易忽略。

// DMA发送配置示例(伪代码)
void SPI_DMA_Transmit(uint8_t *data, uint16_t len) {
    // 1. 配置DMA通道
    DMA_InitTypeDef dma_init;
    dma_init.PeriphAddr = (uint32_t)&SPI1->DR;   // 外设地址固定
    dma_init.MemAddr    = (uint32_t)data;        // 内存地址递增
    dma_init.Direction  = DMA_MEM_TO_PERIPH;     // 内存到外设
    dma_init.BufferSize = len;
    dma_init.PeriphInc  = DMA_PINC_DISABLE;      // 外设地址不递增
    dma_init.MemInc     = DMA_MINC_ENABLE;       // 内存地址递增
    dma_init.PeriphDataWidth = DMA_PDATAWIDTH_8BIT;
    dma_init.MemDataWidth    = DMA_MDATAWIDTH_8BIT;
    dma_init.Mode       = DMA_NORMAL;            // 普通模式
    dma_init.Priority   = DMA_PRIORITY_HIGH;
    
    DMA_Init(DMA1_Channel3, &dma_init);
    
    // 2. 使能DMA通道
    DMA_Cmd(DMA1_Channel3, ENABLE);
    
    // 3. 使能SPI的DMA发送请求
    SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);
}

注意:DMA传输完成后,一定要检查SPI的BSY位是否清零。DMA只负责把数据搬到DR寄存器,但SPI硬件还在发送最后一个字节。如果此时关闭SPI或切换CS片选,最后一个字节可能发不出去。我吃过这个亏,后来在DMA传输完成中断里加了个等待BSY清零的循环。

4.4 中断处理机制

SPI中断主要有三个:发送缓冲区空(TXE)、接收缓冲区非空(RXNE)、错误中断(ERR)。

实际开发中,我通常这样分配:

  • 轮询方式:适合单字节读写,比如读Flash ID
  • 中断方式:适合少量数据,比如读写状态寄存器
  • DMA方式:适合大量数据,比如读写Flash页

中断处理函数里,核心逻辑就是判断中断标志位,然后处理数据。我写过一个简洁的中断处理框架:

void SPI1_IRQHandler(void) {
    // 检查接收中断
    if(SPI_I2S_GetITStatus(SPI1, SPI_I2S_IT_RXNE) == SET) {
        uint8_t rx_data = SPI_I2S_ReceiveData(SPI1);
        // 将数据存入接收缓冲区
        if(rx_buf_index < RX_BUF_SIZE) {
            rx_buf[rx_buf_index++] = rx_data;
        }
        SPI_I2S_ClearITPendingBit(SPI1, SPI_I2S_IT_RXNE);
    }
    
    // 检查发送中断
    if(SPI_I2S_GetITStatus(SPI1, SPI_I2S_IT_TXE) == SET) {
        if(tx_buf_index < tx_len) {
            SPI_I2S_SendData(SPI1, tx_buf[tx_buf_index++]);
        } else {
            // 发送完成,关闭发送中断
            SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_TXE, DISABLE);
        }
    }
    
    // 检查错误中断
    if(SPI_I2S_GetITStatus(SPI1, SPI_I2S_IT_ERR) == SET) {
        // 处理溢出或模式错误
        uint8_t tmp = SPI_I2S_ReceiveData(SPI1); // 读DR清错误标志
        SPI_I2S_ClearITPendingBit(SPI1, SPI_I2S_IT_ERR);
    }
}

为什么会这样设计?因为SPI的发送和接收是同步的。你发一个字节,同时也会收到一个字节。所以中断处理时,发送和接收要配对处理。我见过有人只开了发送中断,结果接收缓冲区溢出,数据全丢了。

避坑指南:我曾经在中断里调用了printf打印调试信息,结果导致中断嵌套,系统直接死机。记住,中断服务函数里不要做耗时操作,尤其是打印和延时。如果非要调试,用GPIO翻转波形看时序。

4.5 实战配置流程

好了,理论讲完了,我们来个完整的配置流程。这是我个人总结的SPI初始化步骤:

  1. 使能时钟:GPIO时钟、SPI时钟、DMA时钟(如果用DMA)
  2. 配置GPIO:SCK、MOSI、MISO、CS,设置为复用功能推挽输出/输入
  3. 配置SPI参数:波特率、CPOL、CPHA、数据格式(MSB/LSB)、数据宽度
  4. 配置中断:设置优先级,使能NVIC
  5. 配置DMA:设置通道、地址、传输长度(如果用DMA)
  6. 使能SPI:设置SPE位,开始通信

嗯,这里要注意,使能SPI之前,一定要确保所有配置都已完成。我曾经在使能SPI后又修改了CR1寄存器,结果SPI直接罢工了。后来查手册才发现,SPE使能后,CR1的某些位是不能修改的。

最后说一句,SPI驱动看似简单,但细节很多。尤其是时钟极性和相位,配错了数据全乱。我的建议是:先用逻辑分析仪抓波形,确认时序正确,再写上层驱动。这样能省去很多调试时间。

个人习惯:我每次移植SPI驱动到新平台,都会先写一个回环测试——把MOSI和MISO短接,发一串数据,看收回来的是不是一样的。如果回环测试通过,说明SPI基本配置没问题。然后再接Flash,进行读写验证。


公众号:蓝海资料掘金营,微信deep3321