3. SPI控制器驱动核心:寄存器、时钟与传输模式

SPI控制器驱动,说白了就是跟寄存器打交道。你想想看,CPU要控制外设,最终都得落到寄存器上。我刚开始做SPI驱动时,总觉得datasheet里那几十页寄存器描述看着头疼,后来踩过几次坑才明白——搞懂核心的几个寄存器,比背下所有细节重要得多。

3.1 SPI控制器寄存器详解

不同厂商的SPI控制器寄存器布局差异很大,但核心功能寄存器基本逃不出下面这几类。我以ARM架构下最常见的SPI控制器为例,给你拆开讲讲。

寄存器类别 典型名称 核心作用
控制寄存器 SPI_CR1 / SPI_CR2 使能SPI、设置主从模式、配置数据帧格式
时钟分频寄存器 SPI_CKDIV / SPI_BAUD 设定SCK时钟频率
状态寄存器 SPI_SR 指示发送/接收缓冲区状态、忙标志
数据寄存器 SPI_DR 发送和接收数据的通道
配置寄存器 SPI_CFG / SPI_MODE 设置极性(CPOL)、相位(CPHA)、LSB/MSB优先

核心要点:控制寄存器决定SPI能不能跑起来,时钟分频寄存器决定跑多快,配置寄存器决定怎么跟从设备对上暗号。状态寄存器则是你判断传输是否完成的依据——我见过不少人忘了查忙标志,直接往数据寄存器里写,结果数据全丢了。

3.2 SPI时钟与极性配置

时钟配置这块,我建议你从两个维度去理解:频率和极性。

3.2.1 时钟频率

SPI时钟频率由系统总线时钟经过分频得到。举个例子,如果系统时钟是100MHz,分频系数设成100,那SCK就是1MHz。分频系数通常写在SPI_CKDIV或SPI_BAUD寄存器里。

我的经验:别一上来就把时钟拉到最高。我在一个项目里遇到过,SPI从设备标称支持20MHz,结果实际跑15MHz就丢数据了。后来查出来是PCB走线太长,信号质量不行。建议先跑低速验证通信,再逐步提频。

3.2.2 极性(CPOL)与相位(CPHA)

这两个位决定了SCK空闲时的电平,以及数据在哪个边沿被采样。说白了,就是主设备和从设备怎么对表。

  • CPOL = 0:SCK空闲时为低电平
  • CPOL = 1:SCK空闲时为高电平
  • CPHA = 0:数据在SCK第一个边沿被采样
  • CPHA = 1:数据在SCK第二个边沿被采样

组合起来就是四种模式。我刚开始做的时候总觉得这玩意儿抽象,后来画了个时序图才彻底搞明白。

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 采样 采样 采样 采样 红色箭头表示数据采样点,模式0和模式3在第一个边沿采样,模式1和模式2在第二个边沿采样

3.3 SPI传输模式详解

四种模式怎么选?我个人的习惯是:先看从设备datasheet。大多数SPI从设备会明确告诉你它支持哪种模式。如果没写,那就用模式0试试——这是最通用的配置。

模式 CPOL CPHA 空闲电平 采样边沿 常见应用
模式0 0 0 低 上升沿 大多数SPI Flash、SD卡
模式1 0 1 低 下降沿 部分ADC、DAC
模式2 1 0 高 下降沿 某些传感器
模式3 1 1 高 上升沿 部分射频芯片

我曾经踩过的坑:有一次调试一个温湿度传感器,死活读不到数据。用逻辑分析仪一看,SCK波形没问题,但数据就是不对。折腾了两小时才发现——从设备要求模式2,我配成了模式0。CPOL和CPHA搞反了,数据采样点全错位了。从那以后,我每次调SPI都先用逻辑分析仪抓波形确认。

3.4 代码示例:配置SPI模式0

下面这段代码展示了如何配置一个ARM SPI控制器工作在模式0,时钟频率1MHz。我建议你重点关注寄存器位的设置顺序——先关SPI再配置,配完再使能,这个顺序不能乱。

/* SPI控制器基地址 */
#define SPI_BASE        0x40013000
#define SPI_CR1         (SPI_BASE + 0x00)
#define SPI_CKDIV       (SPI_BASE + 0x04)
#define SPI_CFG         (SPI_BASE + 0x08)
#define SPI_SR          (SPI_BASE + 0x0C)
#define SPI_DR          (SPI_BASE + 0x10)

/* 配置SPI为模式0,主模式,1MHz */
void spi_mode0_init(void)
{
    uint32_t reg_val;
    
    /* 第一步:先关闭SPI */
    reg_val = readl(SPI_CR1);
    reg_val &= ~(1 << 0);  /* 清除使能位 */
    writel(reg_val, SPI_CR1);
    
    /* 第二步:配置时钟分频 */
    /* 假设系统时钟100MHz,分频100得到1MHz */
    writel(100, SPI_CKDIV);
    
    /* 第三步:配置模式0 */
    reg_val = readl(SPI_CFG);
    reg_val &= ~((1 << 1) | (1 << 0));  /* CPOL=0, CPHA=0 */
    reg_val |= (1 << 2);                   /* 设置为主模式 */
    writel(reg_val, SPI_CFG);
    
    /* 第四步:使能SPI */
    reg_val = readl(SPI_CR1);
    reg_val |= (1 << 0);   /* 设置使能位 */
    writel(reg_val, SPI_CR1);
    
    /* 等待SPI就绪 */
    while (!(readl(SPI_SR) & (1 << 0)));
}

小技巧:配置时钟分频时,我习惯先算一下理论值,再留点余量。比如从设备支持10MHz,我一般配到8MHz左右。这样即使PCB走线有点干扰,通信也不会崩。另外,别忘了检查状态寄存器的忙标志——我见过有人连续发数据,结果前一个还没发完,后一个就把缓冲区覆盖了。

3.5 传输流程:一次完整的数据交换

SPI传输是同步的——主设备发一个字节的同时,会收到一个字节。这个机制很多人一开始不理解,我画个流程你就明白了。

SPI单字节传输流程 主设备(CPU) 从设备(外设) 1. 写数据到SPI_DR 2. 从设备接收并回复 3. 读SPI_DR获取数据 注意:写SPI_DR会同时触发发送和接收,读SPI_DR获取从设备返回的数据

流程其实就三步:

  1. 写数据:把要发送的字节写入SPI_DR
  2. 等待完成:轮询状态寄存器,等发送完成标志置位
  3. 读数据:从SPI_DR读出从设备返回的字节

关键点:SPI_DR是发送和接收共用的。你写进去的是要发的数据,读出来的是收到的数据。如果你只想接收数据,也得先写一个字节——通常写0xFF或0x00,用来产生时钟。这个机制我刚开始也觉得别扭,后来习惯了就好。

3.6 避坑指南

做SPI驱动这些年,我总结了几条血泪教训:

  • 时钟极性搞反:用逻辑分析仪看SCK空闲电平,跟datasheet对一下。我曾经因为CPOL配反,调了一整天。
  • 分频系数算错:有些控制器的分频寄存器是2的幂次,有些是线性分频。看清楚再配,别想当然。
  • 忘了查忙标志:连续发送时一定要等前一个传输完成。我见过有人连续写SPI_DR,结果数据全乱了。
  • 片选信号没处理好:有些从设备要求片选在传输期间一直拉低,有些允许片选在字节间拉高。这个得看具体设备。

特别提醒:如果你在调试时发现SPI通信时好时坏,先别急着怀疑代码。检查一下GPIO的复用功能配置——我遇到过好几次,GPIO没配成SPI功能,结果SCK和MOSI输出全是乱的。

好了,SPI控制器的核心寄存器、时钟配置和四种传输模式就讲到这里。这些东西看着简单,但每个细节都可能让你在调试时抓狂。记住:先看datasheet,再用逻辑分析仪验证,最后才写代码。这个顺序能帮你省下大把时间。