4、SPI接口协议详解:SPI四种模式、MTK SPI控制器配置、DMA传输与中断处理
SPI,串行外设接口,嵌入式开发里最常用的通信接口之一。说实话,我做了这么多年联发科平台,SPI调试踩过的坑,比I2C和UART加起来都多。为什么?因为SPI的四种模式、时钟极性、相位组合,稍不留神就配错了。
今天咱们就把SPI彻底讲透。从协议原理到MTK控制器配置,再到DMA传输和中断处理,一条龙讲清楚。
4.1 SPI四种模式,你真的搞懂了吗?
SPI有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)组合决定。很多工程师背得滚瓜烂熟,但一到实际调试就懵了。
我个人习惯这样记:CPOL决定空闲时时钟电平,CPHA决定数据采样时刻。
| 模式 | CPOL | CPHA | 空闲时钟 | 数据采样边沿 |
|---|---|---|---|---|
| 模式0 | 0 | 0 | 低电平 | 第一个跳变沿(上升沿) |
| 模式1 | 0 | 1 | 低电平 | 第二个跳变沿(下降沿) |
| 模式2 | 1 | 0 | 高电平 | 第一个跳变沿(下降沿) |
| 模式3 | 1 | 1 | 高电平 | 第二个跳变沿(上升沿) |
重点来了:大多数SPI从设备默认使用模式0或模式3。为什么?因为这两种模式下,数据在时钟上升沿被采样,下降沿变化,时序最稳定。
我曾经在一个项目里,用MTK平台驱动一个加速度传感器。手册上写着「SPI mode 0」,我配了CPOL=0、CPHA=0,结果死活读不到数据。折腾了两天,最后用示波器一看——从设备实际工作在模式3。你想想看,手册也会出错,所以示波器才是最终裁判。
4.2 MTK SPI控制器配置实战
联发科平台的SPI控制器,寄存器配置比很多MCU要复杂。但核心就几个关键点:时钟分频、传输格式、片选控制。
以MT6765为例,SPI控制器基地址通常在0x1100A000附近。配置流程我总结为四步:
- 时钟使能:打开SPI模块的CG(Clock Gate)
- 引脚复用:将GPIO切换到SPI功能
- 寄存器配置:设置模式、波特率、数据位宽
- 启动传输:写触发寄存器
下面是一段我实际项目中用过的初始化代码(简化版):
// MTK SPI 初始化示例
void mtk_spi_init(void)
{
// 1. 使能SPI时钟
*(volatile uint32_t *)(0x1100A000 + 0x00) |= (1 << 0);
// 2. 配置引脚复用为SPI功能
// 具体GPIO号需查datasheet,这里省略
// 3. 配置SPI控制寄存器
// 模式0: CPOL=0, CPHA=0
// 波特率: 26MHz / (2 * (div + 1))
uint32_t ctrl = 0;
ctrl |= (0 << 0); // CPOL
ctrl |= (0 << 1); // CPHA
ctrl |= (7 << 2); // 分频系数,这里设为7
ctrl |= (8 << 8); // 数据位宽8位
*(volatile uint32_t *)(0x1100A000 + 0x04) = ctrl;
// 4. 使能SPI
*(volatile uint32_t *)(0x1100A000 + 0x00) |= (1 << 1);
}
小技巧:MTK的SPI时钟分频公式是 SPI_CLK = SRC_CLK / (2 * (div + 1))。我习惯先查从设备支持的最高频率,然后反推div值。比如从设备最高20MHz,SRC_CLK=26MHz,那div至少设为1,实际频率6.5MHz,留点余量。
4.3 DMA传输:别让CPU干苦力
SPI传输大量数据时,用CPU轮询或中断逐字节搬运,效率极低。我见过有人用CPU搬1MB数据到LCD,CPU占用率飙到90%,系统直接卡死。
MTK平台支持SPI+DMA传输,流程是这样的:
- CPU配置DMA通道,指定源地址、目的地址、传输长度
- DMA控制器自动从内存搬运数据到SPI发送FIFO
- 或者从SPI接收FIFO搬运到内存
- 传输完成触发DMA中断
配置DMA时,有几个坑要注意:
警告:我曾经在MTK平台上配置SPI DMA,发现数据总是错位。查了两天,最后发现是DMA的burst size和SPI FIFO深度不匹配。SPI FIFO深度是16字节,我配了DMA burst size为32,结果数据溢出。记住:DMA burst size ≤ SPI FIFO深度。
DMA传输的代码片段:
// SPI DMA传输配置
void spi_dma_transfer(uint8_t *tx_buf, uint8_t *rx_buf, uint32_t len)
{
// 配置DMA源地址(内存)
// 配置DMA目的地址(SPI TX FIFO)
// 设置传输长度
// 启动DMA
// 等待DMA完成中断
while(!dma_done);
// 如果是接收,从SPI RX FIFO读数据
// 注意:收发通常是同时进行的
}
4.4 中断处理:别让中断淹死CPU
SPI中断处理,核心是中断频率。如果每次传输一个字节就触发一次中断,那CPU啥也别干了。
MTK的SPI控制器支持FIFO阈值中断。什么意思?就是FIFO里数据达到一定数量才触发中断。比如设置阈值为8,那FIFO里有8个字节时才中断一次。
我建议这样配置:
- 发送:FIFO空余空间达到阈值时触发
- 接收:FIFO中数据达到阈值时触发
- 阈值一般设为FIFO深度的一半
避坑指南:我曾经在SPI中断服务函数里做了太多事情——读状态、搬数据、清标志、还要处理错误。结果中断执行时间太长,导致下一个中断丢失。后来我把数据搬运放到tasklet里,中断里只做最必要的事:读状态、清标志、唤醒tasklet。
中断服务函数模板:
// SPI中断处理
void spi_irq_handler(void)
{
uint32_t status = read_spi_status();
if(status & SPI_TX_DONE) {
// 发送完成,唤醒发送tasklet
tasklet_schedule(&spi_tx_tasklet);
}
if(status & SPI_RX_READY) {
// 接收数据就绪,唤醒接收tasklet
tasklet_schedule(&spi_rx_tasklet);
}
if(status & SPI_ERROR) {
// 错误处理,记录日志
spi_error_count++;
}
// 清中断标志
clear_spi_interrupt();
}
嗯,这里要注意:中断里千万别做耗时操作。我见过有人直接在中断里调用printf打印调试信息,结果系统直接死机。中断服务函数,越快越好。
4.5 总结一下
SPI调试,说白了就是三件事:模式配对了没?时钟频率够不够?DMA和中断有没有冲突?
我个人的调试流程:
- 先拿示波器看SCLK和CS波形,确认模式正确
- 再看MOSI/MISO数据,确认数据对齐
- 最后看DMA传输,确认没有数据丢失
记住一句话:示波器是SPI调试最好的朋友。别问我怎么知道的,问就是吃过亏。