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 传输。配置起来也不难,分三步走:
- 配置 DMA 通道,设置源地址、目的地址、传输长度
- 使能 SPI 的 DMA 请求(SPI_CTRL2 的 TXDMAEN 和 RXDMAEN)
- 启动 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 通道配置,确认源地址和目的地址是否正确
我的调试流程:
- 先用 GPIO 模拟 SPI,确认从机能正常工作
- 再用硬件 SPI 的轮询模式,验证寄存器配置
- 最后才上 DMA,做性能优化
这样一步步来,出了问题也容易定位。
4.5 知识结构总览
下面这张图,把 SPI 驱动涉及的核心知识点串起来了。你可以对照着看,哪个环节不清楚,就回头翻翻对应的内容。
嗯,SPI 这块内容就这些。配置不难,但细节多。你只要把主从模式、时钟相位、DMA 这三个点吃透了,大部分 SPI 设备都能搞定。下次遇到 SPI 通信问题,别慌,拿逻辑分析仪抓一下波形,对照着时序图看,问题基本就浮出水面了。