【STM32学习笔记】软件模拟SPI通信的实现

前言

在嵌入式开发中,当硬件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 加速策略

  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();
}

七、常见问题排查

  1. 时序偏移

    • 使用示波器测量SCK周期与占空比

    • 调整SPI_Delay()的循环次数

  2. 数据错位

    • 检查MSB/LSB传输顺序是否与外设一致

    • 确认时钟极性/相位匹配

  3. 信号干扰

    • 增加上拉电阻(特别是MISO线路)

    • 缩短连接线长度

结语

软件SPI虽然牺牲了部分性能,但其灵活的引脚选择和时序调整能力使其在特定场景下不可替代。建议将关键参数(如时钟极性、相位等)设计为可配置宏定义,提升代码复用性。对于低速设备(如温湿度传感器、OLED屏),软件SPI是完全可行的解决方案。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值