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附近。配置流程我总结为四步:

  1. 时钟使能:打开SPI模块的CG(Clock Gate)
  2. 引脚复用:将GPIO切换到SPI功能
  3. 寄存器配置:设置模式、波特率、数据位宽
  4. 启动传输:写触发寄存器

下面是一段我实际项目中用过的初始化代码(简化版):

// 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和中断有没有冲突?

我个人的调试流程:

  1. 先拿示波器看SCLK和CS波形,确认模式正确
  2. 再看MOSI/MISO数据,确认数据对齐
  3. 最后看DMA传输,确认没有数据丢失

记住一句话:示波器是SPI调试最好的朋友。别问我怎么知道的,问就是吃过亏。