4. SPI 总线驱动:主从模式、时钟相位与 DMA 传输

SPI 这玩意儿,说简单也简单,说坑也真不少。我最早接触 SPI 是在一块老式的 ARM 板子上,当时觉得不就是四根线嘛,SCK、MOSI、MISO、CS,能有多复杂?结果一上逻辑分析仪,波形乱七八糟,从机根本不搭理我。后来才发现,时钟极性和相位配错了。

今天咱们就好好捋一捋 CH32V203 的 SPI 模块。我会把主从模式、时钟配置、DMA 传输这些点,结合我踩过的坑,一个一个说清楚。

4.1 SPI 主从模式配置

CH32V203 的 SPI 外设,支持主模式和从模式。说白了,主模式负责产生时钟,从模式被动接收。配置起来其实不复杂,但有几个关键寄存器你得记住。

寄存器 功能 关键位
SPI_CTRL1 控制寄存器 1 MSTR(主从选择)、BR[2:0](波特率)、SPE(使能)
SPI_CTRL2 控制寄存器 2 SSOE(片选输出使能)、DMAEN(DMA使能)
SPI_STAT 状态寄存器 BSY(忙标志)、RXNE(接收非空)、TXE(发送空)
SPI_DAT 数据寄存器 8位或16位数据

我个人习惯,初始化 SPI 时先关掉外设(SPE=0),配好所有参数,最后再使能。这样能避免配置过程中产生意外时钟。

主模式初始化要点:

  • 设置 MSTR=1,选择主模式
  • 配置 BR[2:0] 分频系数,决定 SCK 频率
  • 设置 CPOL 和 CPHA,匹配从机要求
  • 配置数据帧格式(8位或16位)
  • 使能 SPI(SPE=1)
// 主模式初始化示例
void SPI1_Master_Init(void) {
    // 使能 SPI1 时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
    
    // 配置 GPIO:SCK、MOSI 为推挽输出,MISO 为浮空输入
    // ... GPIO 配置代码省略 ...
    
    // 关闭 SPI
    SPI_Cmd(SPI1, DISABLE);
    
    // 配置为主模式,8位数据,CPOL=0, CPHA=0
    SPI_InitTypeDef spi_init = {0};
    spi_init.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    spi_init.SPI_Mode = SPI_Mode_Master;
    spi_init.SPI_DataSize = SPI_DataSize_8b;
    spi_init.SPI_CPOL = SPI_CPOL_Low;
    spi_init.SPI_CPHA = SPI_CPHA_1Edge;
    spi_init.SPI_NSS = SPI_NSS_Soft;
    spi_init.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;
    spi_init.SPI_FirstBit = SPI_FirstBit_MSB;
    SPI_Init(SPI1, &spi_init);
    
    // 使能 SPI
    SPI_Cmd(SPI1, ENABLE);
}

4.2 时钟极性与相位设置

时钟极性和相位,这是 SPI 最容易出问题的地方。我见过太多人在这上面栽跟头了。

CPOL(时钟极性):决定空闲时 SCK 的电平。

  • CPOL=0:空闲时 SCK 为低电平
  • CPOL=1:空闲时 SCK 为高电平

CPHA(时钟相位):决定数据在哪个边沿被采样。

  • CPHA=0:第一个边沿采样(上升沿或下降沿,取决于 CPOL)
  • CPHA=1:第二个边沿采样

你想想看,这两个参数组合起来就是四种模式。很多从机芯片的数据手册里会明确写“Mode 0”或“Mode 3”,其实就是指 CPOL 和 CPHA 的组合。

SPI 模式 CPOL CPHA 采样边沿
Mode 0 0 0 上升沿采样
Mode 1 0 1 下降沿采样
Mode 2 1 0 下降沿采样
Mode 3 1 1 上升沿采样

我曾经踩过的坑: 有一次调试一个温湿度传感器,数据读出来全是 0xFF。折腾了半天,最后用逻辑分析仪一看,发现从机是在 SCK 下降沿输出数据,而我配置的是上升沿采样。说白了,就是 CPHA 配反了。改过来之后,数据就正常了。

所以我的建议是:拿到一个从机芯片,第一件事就是看数据手册里的时序图。找到 SCK 和 MOSI/MISO 的对应关系,确认是 Mode 几,然后再配置。

4.3 DMA 传输配置

SPI 配合 DMA,才能发挥出真正的性能。尤其是你要传输大量数据的时候,比如驱动 LCD 屏幕或者读写 SD 卡,用 CPU 一个一个字节去搬,效率太低了。

CH32V203 的 SPI1 和 SPI2 都支持 DMA 传输。配置起来也不难,分三步走:

  1. 配置 DMA 通道,设置源地址、目的地址、传输长度
  2. 使能 SPI 的 DMA 请求(SPI_CTRL2 的 TXDMAEN 和 RXDMAEN)
  3. 启动 DMA 传输
// SPI1 DMA 发送示例
void SPI1_DMA_Send(uint8_t *data, uint16_t len) {
    // 配置 DMA1 通道3 用于 SPI1_TX
    DMA_InitTypeDef dma_init = {0};
    
    dma_init.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DAT;
    dma_init.DMA_MemoryBaseAddr = (uint32_t)data;
    dma_init.DMA_DIR = DMA_DIR_PeripheralDST;
    dma_init.DMA_BufferSize = len;
    dma_init.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    dma_init.DMA_MemoryInc = DMA_MemoryInc_Enable;
    dma_init.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    dma_init.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    dma_init.DMA_Mode = DMA_Mode_Normal;
    dma_init.DMA_Priority = DMA_Priority_High;
    dma_init.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel3, &dma_init);
    
    // 使能 SPI1 的 TX DMA
    SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);
    
    // 启动 DMA
    DMA_Cmd(DMA1_Channel3, ENABLE);
}

小技巧: 做 DMA 传输时,记得检查 DMA 传输完成标志。我一般会在 DMA 中断里做后续处理,比如关闭 SPI 片选或者通知上层应用。别在 DMA 还没传完就去操作 SPI 寄存器,容易出问题。

4.4 调试与故障定位

SPI 调试,工具很重要。我强烈建议你备一个逻辑分析仪,哪怕是几十块钱的 USB 逻辑分析仪,也比用示波器方便得多。为什么?因为逻辑分析仪可以同时抓多路信号,还能解析协议,一眼就能看出问题。

常见的 SPI 故障,我总结了一下:

  • 没时钟输出:检查 SPI 是否使能,GPIO 配置是否正确,时钟分频是否合理
  • 数据全是 0xFF:大概率是时钟相位不匹配,或者从机没被选中(CS 没拉低)
  • 数据错位:检查数据帧格式(8位/16位),以及 MSB/LSB 顺序
  • DMA 传输卡死:检查 DMA 通道配置,确认源地址和目的地址是否正确

我的调试流程:

  1. 先用 GPIO 模拟 SPI,确认从机能正常工作
  2. 再用硬件 SPI 的轮询模式,验证寄存器配置
  3. 最后才上 DMA,做性能优化

这样一步步来,出了问题也容易定位。

4.5 知识结构总览

下面这张图,把 SPI 驱动涉及的核心知识点串起来了。你可以对照着看,哪个环节不清楚,就回头翻翻对应的内容。

SPI 驱动知识结构图 CH32V203 SPI 外设 主从模式配置 时钟极性与相位 DMA 传输 MSTR 位设置 BR 分频系数 CPOL 配置 CPHA 配置 DMA 通道 中断 调试验证:逻辑分析仪 → 波形检查 → 数据比对 无时钟输出 数据全 0xFF 数据错位 DMA 卡死

嗯,SPI 这块内容就这些。配置不难,但细节多。你只要把主从模式、时钟相位、DMA 这三个点吃透了,大部分 SPI 设备都能搞定。下次遇到 SPI 通信问题,别慌,拿逻辑分析仪抓一下波形,对照着时序图看,问题基本就浮出水面了。