4、SPI时序与模式:CPOL和CPHA详解、四种SPI模式(Mode 0-3)的时序图、数据采样与输出边沿

SPI(Serial Peripheral Interface)作为一种高速、全双工、同步的串行通信协议,其物理层只有四根线:SCLK(串行时钟)、MOSI(主出从入)、MISO(主入从出)和CS(片选)。然而,正是这看似简单的四线接口,在实际工程中却常常因为时序配置不当而导致通信失败。究其根本,绝大多数问题都出在时钟极性(CPOL)和时钟相位(CPHA)的匹配上。本章将深入剖析CPOL与CPHA的本质,详细拆解SPI的四种工作模式(Mode 0~3),并结合实际示波器波形与代码配置,帮助您彻底掌握SPI时序的核心要点。

SPI 四种模式时序对比与采样边沿判定 Mode 0 (CPOL=0, CPHA=0) SCLK 空闲低 ▲采样 MOSI D0 D1 D2 D3 D4 D5 D6 D7 ↑上升沿 采样 ↑上升沿 采样 Mode 1 (CPOL=0, CPHA=1) SCLK 空闲低 ▲采样 MOSI D0 D1 D2 D3 D4 D5 D6 D7 ↓下降沿 采样 Mode 2 (CPOL=1, CPHA=0) SCLK 空闲高 ▲采样 MOSI D0 D1 D2 D3 D4 D5 D6 D7 ↓下降沿 采样 Mode 3 (CPOL=1, CPHA=1) SCLK 空闲高 ▲采样 MOSI D0 D1 D2 D3 D4 D5 D6 D7 ↑上升沿 采样 CPOL决定空闲电平,CPHA决定采样边沿(第一个边沿还是第二个边沿)

上图直观展示了四种SPI模式下SCLK的波形形态以及数据采样点的位置。在深入细节之前,我们需要建立一个核心认知:SPI协议本身并没有规定必须使用哪种模式,通信双方必须通过软件配置达成一致。如果主机配置为Mode 0,而从机硬件固定为Mode 3,那么通信将完全错乱。接下来,我们将从CPOL和CPHA的定义出发,逐步构建完整的时序知识体系。

4.1 核心原理:CPOL与CPHA的本质

CPOL(Clock Polarity,时钟极性)决定了SPI总线在空闲状态(即CS拉高、无数据传输)时,SCLK时钟线的电平状态。当CPOL=0时,SCLK空闲时为低电平;当CPOL=1时,SCLK空闲时为高电平。这一位设置直接影响时钟信号的起始沿方向:CPOL=0时,第一个跳变沿是上升沿;CPOL=1时,第一个跳变沿是下降沿。

CPHA(Clock Phase,时钟相位)决定了数据位相对于时钟边沿的采样时机。CPHA=0时,数据在时钟的第一个边沿(即从空闲状态切换到活跃状态的第一个跳变沿)被采样;CPHA=1时,数据在时钟的第二个边沿(即时钟回到空闲状态前的最后一个跳变沿)被采样。这里需要特别注意的是,CPHA=0时,数据必须在第一个边沿之前就准备好,即数据输出发生在第一个边沿之前的半周期;而CPHA=1时,数据输出发生在第一个边沿,采样发生在第二个边沿。

为了更清晰地理解,我们可以将SCLK的一个完整周期分为两个半周期:前半周期和后半周期。CPHA=0意味着数据在前半周期的起始边沿被锁存,而CPHA=1意味着数据在后半周期的起始边沿(即第二个边沿)被锁存。这种差异导致了对数据建立时间(Setup Time)和保持时间(Hold Time)的不同要求。

关键记忆点:CPOL决定“空闲电平”,CPHA决定“采样边沿是第几个”。两者组合形成四种模式,其中Mode 0(CPOL=0, CPHA=0)和Mode 3(CPOL=1, CPHA=1)是最常用的两种模式,因为它们都在上升沿采样数据,符合大多数数字电路的习惯。

4.2 四种SPI模式的详细时序拆解

下面我们逐一分析每种模式的具体时序行为,并给出对应的数据输出与采样边沿。

模式 CPOL CPHA 空闲SCLK电平 数据输出边沿 数据采样边沿 典型应用
Mode 0 0 0 低电平 下降沿(第一个边沿的前半周期) 上升沿(第一个边沿) 大多数Flash、SD卡、传感器
Mode 1 0 1 低电平 上升沿(第一个边沿) 下降沿(第二个边沿) 部分音频编解码器
Mode 2 1 0 高电平 上升沿(第一个边沿的前半周期) 下降沿(第一个边沿) 某些特定外设
Mode 3 1 1 高电平 下降沿(第一个边沿) 上升沿(第二个边沿) 多数EEPROM、LCD驱动

从上表可以看出,Mode 0和Mode 3的采样边沿都是上升沿,区别仅在于空闲电平。Mode 1和Mode 2的采样边沿都是下降沿。在实际工程中,Mode 0是最为通用的默认模式,绝大多数SPI从设备都支持Mode 0。但为了确保兼容性,务必查阅目标芯片的数据手册(Datasheet)中的时序图。

4.2.1 Mode 0(CPOL=0, CPHA=0)详解

在Mode 0下,SCLK空闲为低。当CS拉低开始传输时,主机在SCLK的第一个上升沿之前,先将第一位数据(D0)输出到MOSI线上。随后,在SCLK的上升沿,从机采样MOSI线上的数据。同时,从机在上升沿之前将MISO线上的数据准备好,主机在同一个上升沿采样MISO。因此,数据输出发生在下降沿(即第一个边沿的前半周期),数据采样发生在上升沿。这种模式的特点是数据变化与时钟下降沿同步,采样点位于时钟周期的中间位置,具有较好的建立时间裕量。

4.2.2 Mode 3(CPOL=1, CPHA=1)详解

Mode 3与Mode 0在采样边沿上相同(都是上升沿),但空闲电平为高。当CS拉低后,SCLK从高电平跳变到低电平(第一个下降沿),此时主机输出第一位数据。随后SCLK从低跳变到高(第二个上升沿),从机采样数据。因此,数据输出发生在下降沿,数据采样发生在上升沿。Mode 3在高速通信中有时比Mode 0更受青睐,因为其时钟空闲为高,抗干扰能力稍强,且与某些器件的内部逻辑更匹配。

4.3 判断方法:如何快速确定从机所需的SPI模式

在实际项目开发中,我们往往面对的是一个具体的从设备芯片。确定其SPI模式的方法主要有以下三种:

  1. 查阅数据手册(Datasheet):这是最权威的方法。在数据手册的“SPI Interface”或“Timing Characteristics”章节中,通常会有一个时序图,明确标注了SCLK的空闲电平、数据建立时间(tSU)、数据保持时间(tH)以及采样边沿。根据时序图即可反推CPOL和CPHA。
  2. 查看寄存器配置说明:很多芯片的寄存器中有一个SPI Mode配置位(如MODE[1:0]),数据手册会说明该寄存器位与CPOL/CPHA的对应关系。
  3. 实验试探法:如果数据手册缺失或描述不清,可以通过编写一个简单的回环测试程序,依次尝试四种模式,观察从机是否返回正确的响应数据。例如,向从机发送一个已知的命令(如读取ID),如果返回的数据符合预期,则说明模式匹配。

经验之谈:在调试初期,建议优先尝试Mode 0。如果通信失败,不要急于修改硬件,先用示波器抓取SCLK和MOSI的波形,观察空闲电平以及数据变化与时钟边沿的关系,再对照数据手册的时序图进行匹配。这比盲目切换模式要高效得多。

4.4 实战案例:基于STM32的W25Q64 Flash驱动配置

下面我们通过一个完整的案例来演示如何正确配置SPI模式。假设我们使用STM32F407微控制器驱动华邦W25Q64 SPI Flash芯片。

背景与约束:W25Q64支持Mode 0和Mode 3。系统要求SPI时钟频率为10MHz,MCU主频168MHz。我们需要在初始化代码中正确设置SPI外设的CPOL和CPHA位。

初始现象:工程师小王在初始化时,将SPI配置为Mode 0,但读取芯片ID(命令0x9F)时,返回的数据始终是0xFF或0x00,与预期的0xEF14不符。

分析判断过程:小王首先使用示波器测量了SCLK和MOSI的波形。他发现SCLK空闲为低,数据在上升沿被采样,这符合Mode 0的特征。但为什么读不到正确的ID呢?进一步检查发现,他在初始化SPI时,虽然设置了CPOL=0、CPHA=0,但没有正确配置SPI的数据帧格式(8位)和MSB先行。W25Q64要求数据以MSB(最高有效位)先行传输,而STM32的SPI默认可能配置为LSB先行,导致命令字节0x9F被发送成了0xF9,芯片自然无法识别。

实际操作步骤:

// STM32 SPI1 初始化代码片段
void SPI1_Init(void) {
    SPI_InitTypeDef SPI_InitStructure;
    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;      // 8位数据帧
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;             // CPOL=0,空闲低电平
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;           // CPHA=0,第一个边沿采样
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;              // 软件管理片选
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; // 168MHz/16=10.5MHz
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;     // MSB先行,关键!
    SPI_InitStructure.SPI_CRCPolynomial = 7;
    SPI_Init(SPI1, &SPI_InitStructure);
    SPI_Cmd(SPI1, ENABLE);
}
// 读取芯片ID函数
uint32_t W25Q64_ReadID(void) {
    uint8_t cmd = 0x9F;
    uint8_t id[3] = {0};
    // CS拉低
    W25Q64_CS_LOW();
    // 发送命令
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
    SPI_I2S_SendData(SPI1, cmd);
    // 接收数据
    for (int i = 0; i < 3; i++) {
        while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET