前言
在嵌入式开发中,当硬件SPI资源紧张或需要特殊时序调整时,软件模拟SPI(Software SPI)成为重要的解决方案。本文将基于STM32F103平台,详细讲解如何通过GPIO模拟实现SPI通信,并分享时序优化经验。
一、硬件SPI vs 软件SPI
特性 | 硬件SPI | 软件SPI |
---|---|---|
实现方式 | 专用外设电路 | GPIO模拟时序 |
时钟频率 | 可达数十MHz | 通常低于1MHz |
CPU占用 | 低(支持DMA) | 高 |
灵活性 | 固定特性 | 可自定义任意时序 |
引脚限制 | 固定映射引脚 | 任意GPIO均可使用 |
二、软件SPI实现原理
2.1 核心时序控制
通过代码控制GPIO电平变化模拟四个信号:
-
SCK:手动拉高/拉低产生时钟脉冲
-
MOSI:按位输出数据
-
MISO:按位读取数据
-
NSS:手动控制片选信号
2.2 时序参数控制
// 通过延时函数控制时序间隔
#define SPI_Delay() \
for(uint8_t i=0; i<5; i++) // 根据主频调整循环次数
三、标准代码实现(模式0)
3.1 GPIO初始化
// 定义模拟SPI使用的GPIO(示例使用PA5~PA7)
#define SOFT_SPI_SCK_PIN GPIO_Pin_5
#define SOFT_SPI_MOSI_PIN GPIO_Pin_6
#define SOFT_SPI_MISO_PIN GPIO_Pin_7
#define SOFT_SPI_PORT GPIOA
void SoftSPI_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// SCK和MOSI配置为推挽输出
GPIO_InitStruct.GPIO_Pin = SOFT_SPI_SCK_PIN | SOFT_SPI_MOSI_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SOFT_SPI_PORT, &GPIO_InitStruct);
// MISO配置为上拉输入
GPIO_InitStruct.GPIO_Pin = SOFT_SPI_MISO_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(SOFT_SPI_PORT, &GPIO_InitStruct);
// 初始状态
GPIO_SetBits(SOFT_SPI_PORT, SOFT_SPI_SCK_PIN); // 模式0初始高电平
}
3.2 基本收发函数
// 发送接收一个字节(模式0:CPOL=0, CPHA=0)
uint8_t SoftSPI_TransferByte(uint8_t txData)
{
uint8_t rxData = 0;
for(uint8_t i=0; i<8; i++) {
// 下降沿输出数据
GPIO_ResetBits(SOFT_SPI_PORT, SOFT_SPI_SCK_PIN);
// 设置MOSI
if(txData & 0x80) {
GPIO_SetBits(SOFT_SPI_PORT, SOFT_SPI_MOSI_PIN);
} else {
GPIO_ResetBits(SOFT_SPI_PORT, SOFT_SPI_MOSI_PIN);
}
txData <<= 1;
SPI_Delay();
// 上升沿采样数据
GPIO_SetBits(SOFT_SPI_PORT, SOFT_SPI_SCK_PIN);
rxData <<= 1;
if(GPIO_ReadInputDataBit(SOFT_SPI_PORT, SOFT_SPI_MISO_PIN)) {
rxData |= 0x01;
}
SPI_Delay();
}
return rxData;
}
四、多模式兼容实现
4.1 模式选择宏定义
typedef enum {
SPI_MODE0 = 0, // CPOL=0, CPHA=0
SPI_MODE1, // CPOL=0, CPHA=1
SPI_MODE2, // CPOL=1, CPHA=0
SPI_MODE3 // CPOL=1, CPHA=1
} SPIMode_TypeDef;
// 当前模式配置(示例使用模式3)
SPIMode_TypeDef spi_mode = SPI_MODE3;
4.2 改进版收发函数
uint8_t SoftSPI_TransferByte_Advanced(uint8_t txData)
{
uint8_t rxData = 0;
bool cpol = (spi_mode == SPI_MODE2 || spi_mode == SPI_MODE3);
bool cpha = (spi_mode == SPI_MODE1 || spi_mode == SPI_MODE3);
// 初始时钟状态
GPIO_WriteBit(SOFT_SPI_PORT, SOFT_SPI_SCK_PIN, cpol ? Bit_SET : Bit_RESET);
for(uint8_t i=0; i<8; i++) {
if(!cpha) { // 在时钟前沿采样
// 先改变数据再跳变时钟
GPIO_WriteBit(SOFT_SPI_PORT, SOFT_SPI_MOSI_PIN, (txData & 0x80) ? Bit_SET : Bit_RESET);
txData <<= 1;
// 时钟跳变
GPIO_WriteBit(SOFT_SPI_PORT, SOFT_SPI_SCK_PIN, cpol ? Bit_RESET : Bit_SET);
SPI_Delay();
GPIO_WriteBit(SOFT_SPI_PORT, SOFT_SPI_SCK_PIN, cpol ? Bit_SET : Bit_RESET);
// 读取数据
rxData <<= 1;
rxData |= GPIO_ReadInputDataBit(SOFT_SPI_PORT, SOFT_SPI_MISO_PIN);
} else {
// 模式1/3的实现逻辑(代码类似,调整时序顺序)
// 此处省略具体实现...
}
SPI_Delay();
}
return rxData;
}
五、性能优化技巧
5.1 加速策略
-
寄存器级操作:直接操作ODR/IDR寄存器提升速度
// 替换GPIO_SetBits/ResetBits
#define SCK_HIGH() (GPIOA->ODR |= GPIO_Pin_5)
#define SCK_LOW() (GPIOA->ODR &= ~GPIO_Pin_5)
2.循环展开:手动展开for循环减少判断开销
// 将8次循环展开为顺序执行
SCK_LOW(); MOSI = bit7; SCK_HIGH(); read bit7;
SCK_LOW(); MOSI = bit6; SCK_HIGH(); read bit6;
// ...依此类推...
5.2 实测数据对比
优化方法 | 传输速率(1MHz主频) |
---|---|
标准库函数 | 约120Kbps |
寄存器操作 | 约480Kbps |
循环展开+寄存器操作 | 约860Kbps |
六、应用实例——驱动OLED屏幕
// 发送命令函数
void OLED_WriteCommand(uint8_t cmd)
{
OLED_CS_LOW(); // 片选使能
OLED_DC_LOW(); // 命令模式
SoftSPI_TransferByte(cmd);
OLED_CS_HIGH();
}
// 发送数据函数
void OLED_WriteData(uint8_t dat)
{
OLED_CS_LOW();
OLED_DC_HIGH(); // 数据模式
SoftSPI_TransferByte(dat);
OLED_CS_HIGH();
}
七、常见问题排查
-
时序偏移
-
使用示波器测量SCK周期与占空比
-
调整SPI_Delay()的循环次数
-
-
数据错位
-
检查MSB/LSB传输顺序是否与外设一致
-
确认时钟极性/相位匹配
-
-
信号干扰
-
增加上拉电阻(特别是MISO线路)
-
缩短连接线长度
-
结语
软件SPI虽然牺牲了部分性能,但其灵活的引脚选择和时序调整能力使其在特定场景下不可替代。建议将关键参数(如时钟极性、相位等)设计为可配置宏定义,提升代码复用性。对于低速设备(如温湿度传感器、OLED屏),软件SPI是完全可行的解决方案。