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