1. SPI基础概念与四种模式详解
SPI(Serial Peripheral Interface)是一种高速、全双工、同步的串行通信总线,广泛应用于嵌入式系统中连接微控制器与各种外设。与I2C总线不同,SPI采用主从架构,通常由一个主设备和一个或多个从设备组成,通过四根线进行通信:
- SCK(Serial Clock):时钟信号,由主设备产生
- MOSI(Master Out Slave In):主设备输出,从设备输入
- MISO(Master In Slave Out):主设备输入,从设备输出
- NSS(Slave Select):从设备选择(低电平有效)
SPI总线的工作模式由两个关键参数决定:时钟极性(CPOL)和时钟相位(CPHA)。这两个参数的组合形成了SPI的四种工作模式:
1.1 时钟极性(CPOL)解析
CPOL定义了时钟信号在空闲状态时的电平:
- CPOL=0:SCK空闲时为低电平
- CPOL=1:SCK空闲时为高电平
在实际应用中,CPOL的选择需要考虑外设的时序要求。例如,某些传感器可能要求空闲时SCK保持高电平以避免误触发。
1.2 时钟相位(CPHA)解析
CPHA定义了数据采样的时机:
- CPHA=0:数据在SCK的第一个边沿(CPOL变化的边沿)被采样
- CPHA=1:数据在SCK的第二个边沿(CPOL反向变化的边沿)被采样
理解CPHA的关键在于识别"有效边沿"。例如,当CPOL=0且CPHA=0时,上升沿是采样边沿;而当CPOL=0且CPHA=1时,下降沿成为采样边沿。
1.3 四种SPI模式详解
基于CPOL和CPHA的组合,SPI定义了四种工作模式:
1.3.1 模式0(CPOL=0, CPHA=0)
- 空闲状态:SCK保持低电平
- 数据采样:在SCK上升沿采样数据
- 数据变化:在SCK下降沿数据变化
- 典型应用:这是最常用的模式,适用于大多数SPI外设如Flash存储器、ADC等
1.3.2 模式1(CPOL=0, CPHA=1)
- 空闲状态:SCK保持低电平
- 数据采样:在SCK下降沿采样数据
- 数据变化:在SCK上升沿数据变化
- 典型应用:某些特定传感器和显示模块使用此模式
1.3.3 模式2(CPOL=1, CPHA=0)
- 空闲状态:SCK保持高电平
- 数据采样:在SCK下降沿采样数据
- 数据变化:在SCK上升沿数据变化
- 典型应用:较少使用,某些特殊通信协议可能要求
1.3.4 模式3(CPOL=1, CPHA=1)
- 空闲状态:SCK保持高电平
- 数据采样:在SCK上升沿采样数据
- 数据变化:在SCK下降沿数据变化
- 典型应用:某些RFID读卡器和无线模块使用此模式
重要提示:主设备和从设备必须配置为相同的SPI模式才能正常通信。模式不匹配是SPI通信失败的常见原因之一。
2. STM32CubeMX定时器延时实现
在软件模拟SPI时,精确的延时控制至关重要。STM32CubeMX提供了便捷的定时器配置方式,我们可以利用基本定时器(TIM6/TIM7)实现微秒级精确延时。
2.1 定时器选型与配置
STM32的基本定时器TIM6和TIM7具有以下特点:
- 16位自动重装载计数器
- 仅支持向上计数模式
- 挂载在APB1总线上
- 无外部IO,纯内部使用
在CubeMX中的配置步骤:
- 打开TIM6或TIM7定时器
- 时钟源选择"Internal Clock"
- 配置Prescaler(分频系数)和Counter Period(自动重装载值)
- 使能定时器中断(如需要)
2.2 精确延时函数实现
基于HAL库的延时函数实现需要考虑以下关键点:
2.2.1 微秒级延时(delay_us)
c复制void delay_us(uint16_t tim_us)
{
__HAL_TIM_SET_COUNTER(&htim6, 0); // 重置计数器
HAL_TIM_Base_Start(&htim6); // 启动定时器
while(__HAL_TIM_GetCounter(&htim6) < tim_us); // 等待计时完成
HAL_TIM_Base_Stop(&htim6); // 停止定时器
}
2.2.2 毫秒级延时优化
原始代码中的毫秒延时实现有误,应采用以下方式:
c复制void delay_ms(uint16_t tim_ms)
{
for(uint16_t i=0; i<tim_ms; i++)
{
delay_us(1000); // 调用1000次1us延时
}
}
注意事项:定时器的时钟频率配置直接影响延时精度。例如,如果APB1时钟为84MHz,定时器不分频,则每个计数周期为1/84MHz≈11.9ns。
2.3 定时器时钟计算
假设系统时钟配置为:
- APB1时钟:84MHz
- 定时器预分频:83(即84分频)
- 定时器时钟 = 84MHz / (83+1) = 1MHz(1us计数周期)
这种配置下,定时器每个计数对应1us,非常适合实现精确延时。
3. 软件模拟SPI实现详解
当硬件SPI资源不足或需要特殊时序控制时,软件模拟SPI成为理想选择。下面详细解析基于GPIO的SPI模拟实现。
3.1 GPIO引脚配置
首先需要定义SPI相关引脚:
c复制// SPI引脚定义
#define SPI_SCK_PIN GPIO_PIN_4
#define SPI_SCK_PORT GPIOB
#define SPI_MOSI_PIN GPIO_PIN_5
#define SPI_MOSI_PORT GPIOB
#define SPI_MISO_PIN GPIO_PIN_2
#define SPI_MISO_PORT GPIOB
#define SPI_NSS_PIN GPIO_PIN_3
#define SPI_NSS_PORT GPIOB
// 宏定义简化IO操作
#define MOSI_H HAL_GPIO_WritePin(SPI_MOSI_PORT, SPI_MOSI_PIN, GPIO_PIN_SET)
#define MOSI_L HAL_GPIO_WritePin(SPI_MOSI_PORT, SPI_MOSI_PIN, GPIO_PIN_RESET)
#define SCK_H HAL_GPIO_WritePin(SPI_SCK_PORT, SPI_SCK_PIN, GPIO_PIN_SET)
#define SCK_L HAL_GPIO_WritePin(SPI_SCK_PORT, SPI_SCK_PIN, GPIO_PIN_RESET)
#define MISO HAL_GPIO_ReadPin(SPI_MISO_PORT, SPI_MISO_PIN)
#define NSS_H HAL_GPIO_WritePin(SPI_NSS_PORT, SPI_NSS_PIN, GPIO_PIN_SET)
#define NSS_L HAL_GPIO_WritePin(SPI_NSS_PORT, SPI_NSS_PIN, GPIO_PIN_RESET)
3.2 四种模式的实现差异
四种SPI模式的核心区别在于SCK初始电平和数据采样边沿,下面分别说明:
3.2.1 模式0实现(CPOL=0, CPHA=0)
c复制uint8_t SOFT_SPI_RW_MODE0(uint8_t write_dat)
{
uint8_t i, read_dat = 0;
for(i=0; i<8; i++)
{
// 准备数据(下降沿变化)
if(write_dat & 0x80) MOSI_H;
else MOSI_L;
write_dat <<= 1;
delay_us(1); // 保持数据稳定
// 上升沿采样
SCK_H;
read_dat <<= 1;
if(MISO) read_dat++;
delay_us(1);
SCK_L; // 恢复空闲状态
__nop();
}
return read_dat;
}
3.2.2 模式1实现(CPOL=0, CPHA=1)
c复制uint8_t SOFT_SPI_RW_MODE1(uint8_t byte)
{
uint8_t i, Temp=0;
for(i=0;i<8;i++)
{
SCK_H; // 首先产生上升沿
// 准备数据
if(byte&0x80) MOSI_H;
else MOSI_L;
byte <<= 1;
delay_us(1);
// 下降沿采样
SCK_L;
Temp <<= 1;
if(MISO) Temp++;
delay_us(1);
}
return Temp;
}
3.2.3 模式2实现(CPOL=1, CPHA=0)
c复制uint8_t SOFT_SPI_RW_MODE2(uint8_t byte)
{
uint8_t i, Temp=0;
SCK_H; // 初始高电平
for(i=0;i<8;i++)
{
// 准备数据
if(byte&0x80) MOSI_H;
else MOSI_L;
byte <<= 1;
delay_us(1);
// 下降沿采样
SCK_L;
Temp <<= 1;
if(MISO) Temp++;
delay_us(1);
SCK_H; // 恢复高电平
}
return Temp;
}
3.2.4 模式3实现(CPOL=1, CPHA=1)
c复制uint8_t SOFT_SPI_RW_MODE3(uint8_t write_dat)
{
uint8_t i, read_dat = 0;
SCK_H; // 初始高电平
for(i=0; i<8; i++)
{
// 下降沿变化数据
SCK_L;
if(write_dat & 0x80) MOSI_H;
else MOSI_L;
write_dat <<= 1;
delay_us(1);
// 上升沿采样
SCK_H;
read_dat <<= 1;
if(MISO) read_dat++;
delay_us(1);
}
return read_dat;
}
3.3 软件SPI的优化技巧
- 延时调整:根据实际SCK频率需求调整延时时间,平衡速度和稳定性
- IO速度配置:将GPIO设置为最高速度模式(如Very High)
- 循环展开:对时间敏感的场合可以展开循环减少指令开销
- 内联函数:使用__inline关键字减少函数调用开销
4. 逻辑分析仪验证与调试
验证SPI通信的正确性离不开逻辑分析仪。下面介绍如何使用逻辑分析仪验证四种SPI模式。
4.1 测试环境搭建
- 连接逻辑分析仪通道到SCK、MOSI、MISO、NSS
- 设置采样率至少为SPI时钟频率的4倍以上
- 配置解码器为SPI协议分析
4.2 四种模式的波形特征
4.2.1 模式0波形特征
- 空闲时SCK为低电平
- 数据在上升沿稳定,下降沿变化
- 第一个数据位在第一个上升沿被采样
4.2.2 模式1波形特征
- 空闲时SCK为低电平
- 数据在下降沿稳定,上升沿变化
- 第一个数据位在第一个下降沿被采样
4.2.3 模式2波形特征
- 空闲时SCK为高电平
- 数据在上升沿稳定,下降沿变化
- 第一个数据位在第一个下降沿被采样
4.2.4 模式3波形特征
- 空闲时SCK为高电平
- 数据在下降沿稳定,上升沿变化
- 第一个数据位在第一个上升沿被采样
4.3 常见问题排查
-
无波形输出
- 检查GPIO初始化是否正确
- 验证NSS信号是否有效
- 确认程序执行到SPI函数
-
波形畸形
- 检查延时时间是否足够
- 确认GPIO配置为推挽输出
- 测量电源稳定性
-
数据错误
- 确认主从设备模式匹配
- 检查采样边沿设置
- 验证MSB/LSB顺序
-
通信不稳定
- 缩短信号线长度
- 添加适当的上拉电阻
- 降低通信速率测试
在实际项目中,我曾遇到模式3通信异常的情况,最终发现是从设备要求在NSS下降沿后需要至少100ns的建立时间。通过逻辑分析仪捕获波形后,增加了NSS拉低后的延时,问题得以解决。这个经验告诉我,仔细阅读外设的时序要求并实际验证波形至关重要。