1. 项目概述
SPI(Serial Peripheral Interface)作为嵌入式开发中最常用的通信协议之一,其硬件实现虽然高效但引脚资源有限。当项目需要多个SPI设备或引脚冲突时,软件模拟SPI就成为了必备技能。STM32CubeMX作为ST官方推出的可视化配置工具,配合HAL库可以快速构建软件SPI的四种工作模式(0-3),这种方案特别适合需要灵活调整通信时序或引脚映射的场景。
我在多个工业传感器项目中都采用过CubeMX配置软件SPI,实测在72MHz的STM32F103上,模拟SPI的时钟频率最高可达1.5MHz(需配合寄存器级优化)。相比硬件SPI,软件方案虽然速度略低,但具有三大优势:引脚可任意分配(甚至可用GPIO扩展器)、模式切换无需改硬件、时序调试更直观。下面将详解CubeMX中的配置要点和四种模式的核心差异。
2. 环境准备与CubeMX基础配置
2.1 工程创建与时钟设置
在CubeMX中新建工程时,选择对应型号后需特别注意时钟树配置。软件SPI对时钟精度要求较高,建议:
- 使用外部晶振作为时钟源(8MHz或25MHz)
- 在Clock Configuration标签页确保HCLK达到芯片最高频率(如STM32F407的168MHz)
- 对使用的GPIO端口启用时钟(在Pinout视图右键GPIO选择Enable)
注意:如果使用内部RC振荡器,需在代码中额外校准时序,否则可能出现±10%的时钟偏差。
2.2 GPIO引脚配置
软件SPI需要手动配置四个信号线:
- MOSI:主设备输出从设备输入(推挽输出)
- MISO:主设备输入从设备输出(上拉输入)
- SCK:时钟线(推挽输出)
- NSS:片选线(推挽输出,可选)
在CubeMX的Pinout视图:
- 右键目标引脚选择GPIO_Output(MOSI/SCK/NSS)
- 对MISO引脚选择GPIO_Input
- 在Configuration标签页的GPIO设置中:
- 输出引脚设为Push-Pull模式
- MISO引脚启用Pull-up
- 所有引脚速度选择High
c复制// 生成的GPIO初始化代码示例(HAL库)
GPIO_InitStruct.Pin = GPIO_PIN_7|GPIO_PIN_5|GPIO_PIN_4; // MOSI/SCK/NSS
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_6; // MISO
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
3. SPI四种模式详解与实现
3.1 模式0(CPOL=0, CPHA=0)
这是最常用的模式,特点包括:
- 时钟空闲时为低电平(CPOL=0)
- 数据在时钟第一个边沿(上升沿)采样(CPHA=0)
实现代码关键点:
c复制void SPI_Mode0_WriteByte(uint8_t data) {
for(uint8_t i=0; i<8; i++) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // SCK低
// 数据在时钟上升沿前建立
if(data & 0x80)
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET); // MOSI高
else
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET);
HAL_Delay(1); // 保持时间(根据器件需求调整)
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // SCK上升沿
data <<= 1;
HAL_Delay(1); // 时钟高电平时间
}
}
3.2 模式1(CPOL=0, CPHA=1)
与模式0的主要区别在于采样时刻:
- 时钟空闲仍为低电平(CPOL=0)
- 数据在时钟第二个边沿(下降沿)采样(CPHA=1)
代码差异体现在时钟操作顺序:
c复制void SPI_Mode1_WriteByte(uint8_t data) {
for(uint8_t i=0; i<8; i++) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // SCK先置高
// 数据在时钟下降沿前建立
if(data & 0x80)
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET);
else
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET);
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // SCK下降沿
data <<= 1;
HAL_Delay(1);
}
}
3.3 模式2(CPOL=1, CPHA=0)
此时钟极性反转:
- 时钟空闲时为高电平(CPOL=1)
- 数据在时钟第一个边沿(下降沿)采样(CPHA=0)
典型应用在OLED显示屏驱动中:
c复制void SPI_Mode2_WriteByte(uint8_t data) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // SCK初始高
for(uint8_t i=0; i<8; i++) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // SCK下降沿
if(data & 0x80)
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET);
else
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET);
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // SCK上升沿
data <<= 1;
HAL_Delay(1);
}
}
3.4 模式3(CPOL=1, CPHA=1)
这是四种模式中最少见的组合:
- 时钟空闲高电平(CPOL=1)
- 数据在时钟第二个边沿(上升沿)采样(CPHA=1)
某些RFID读卡器会采用此模式:
c复制void SPI_Mode3_WriteByte(uint8_t data) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // SCK初始高
for(uint8_t i=0; i<8; i++) {
if(data & 0x80)
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET);
else
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET);
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // SCK下降沿
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // SCK上升沿采样
data <<= 1;
}
}
4. 性能优化与高级技巧
4.1 时序精确控制
HAL_Delay()基于毫秒延时,实际使用时需要更精确的控制:
- 使用定时器产生微秒级延时
- 直接操作寄存器实现纳秒级翻转(以STM32F4为例):
c复制#define SPI_SCK_HIGH() (GPIOA->BSRR = GPIO_PIN_5)
#define SPI_SCK_LOW() (GPIOA->BSRR = (uint32_t)GPIO_PIN_5 << 16)
#define SPI_MOSI_HIGH() (GPIOA->BSRR = GPIO_PIN_7)
#define SPI_MOSI_LOW() (GPIOA->BSRR = (uint32_t)GPIO_PIN_7 << 16)
void SPI_FastWrite(uint8_t data) {
for(uint8_t i=0; i<8; i++) {
SPI_SCK_LOW();
if(data & 0x80) SPI_MOSI_HIGH();
else SPI_MOSI_LOW();
__NOP(); __NOP(); __NOP(); // 约15ns@168MHz
SPI_SCK_HIGH();
data <<= 1;
__NOP(); __NOP();
}
}
4.2 多设备管理技巧
当需要驱动多个SPI设备时:
- 为每个设备创建独立的片选引脚
- 在CubeMX中配置多个GPIO输出
- 使用结构体管理设备参数:
c复制typedef struct {
GPIO_TypeDef *cs_port;
uint16_t cs_pin;
uint8_t spi_mode;
} SPI_Device;
SPI_Device dev1 = {GPIOA, GPIO_PIN_4, 0}; // 设备1使用PA4片选,模式0
SPI_Device dev2 = {GPIOB, GPIO_PIN_0, 3}; // 设备2使用PB0片选,模式3
void SPI_SelectDevice(SPI_Device *dev) {
// 先取消所有片选
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
// 选中指定设备
HAL_GPIO_WritePin(dev->cs_port, dev->cs_pin, GPIO_PIN_RESET);
}
5. 常见问题与调试方法
5.1 信号完整性问题
症状:通信不稳定,偶尔出现数据错误
解决方案:
- 在MOSI/SCK线上串联33Ω电阻
- 在MISO引脚加10pF对地电容
- 缩短走线长度(最好<10cm)
5.2 时序不匹配
症状:从设备无响应或返回乱码
排查步骤:
- 用逻辑分析仪捕获SPI波形
- 检查CPOL/CPHA设置是否与从设备一致
- 测量时钟边沿到数据稳定的时间(tSU/tH)
5.3 典型错误代码
c复制// 错误示例:缺少片选控制
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // 忘记置高其他片选
SPI_Write(data);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
// 正确写法:
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
SPI_Write(data);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
// 保持片选高电平至少1us再操作其他设备
6. 实际项目应用案例
6.1 温度传感器MAX6675驱动
该传感器要求SPI模式1,典型驱动流程:
- 拉低片选至少100ns
- 发送16个时钟周期读取数据
- 数据在时钟下降沿有效
c复制float MAX6675_ReadTemp(void) {
uint16_t temp = 0;
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
HAL_Delay(1);
for(uint8_t i=0; i<16; i++) {
HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_SET);
HAL_Delay(1);
if(HAL_GPIO_ReadPin(MISO_GPIO_Port, MISO_Pin))
temp |= 1 << (15-i);
HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_RESET);
HAL_Delay(1);
}
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
if(temp & 0x4) return NAN; // 检测热电偶断开
return (temp >> 3) * 0.25;
}
6.2 无线模块NRF24L01通信
该模块支持所有四种SPI模式,推荐使用模式0:
- 时钟速率建议≤8MHz
- 每次传输前需要拉低CSN至少100ns
- 命令字在MOSI上升沿锁存
c复制void NRF24_WriteReg(uint8_t reg, uint8_t data) {
HAL_GPIO_WritePin(CSN_GPIO_Port, CSN_Pin, GPIO_PIN_RESET);
SPI_WriteByte(reg | 0x20); // 写入命令
SPI_WriteByte(data);
HAL_GPIO_WritePin(CSN_GPIO_Port, CSN_Pin, GPIO_PIN_SET);
HAL_Delay(1);
}
通过CubeMX配置软件SPI时,建议将SCK引脚初始状态设为模式0对应的电平(低电平),这样在切换模式时只需修改数据传输阶段的时序逻辑,而不需要改变引脚初始化配置。实测在1MHz时钟频率下,四种模式的通信稳定性相当,主要差异体现在从设备的兼容性上。