软件模拟SPI:原理、代码实现与调试优化全解析
1. 项目概述:为什么需要软件模拟SPI?
在嵌入式开发或者单片机项目中,SPI(Serial Peripheral Interface)总线是连接传感器、存储器、显示屏等外设的“高速公路”。硬件SPI控制器速度快、效率高,是开发者的首选。但现实情况往往很骨感:你的主控芯片可能只有一个硬件SPI接口,却需要同时驱动一个TFT屏幕和一个SD卡;或者你手头正好有一款性价比极高但偏偏不带硬件SPI的MCU;又或者,为了调试和验证时序逻辑,你需要一个完全可控、可以随时“暂停”和“观察”的SPI信号源。在这些场景下,硬件SPI就显得捉襟见肘了。
这时,“软件模拟SPI”(Software SPI或Bit-Banging SPI)就登场了。它不依赖芯片内置的专用硬件控制器,而是通过程序控制普通的GPIO(通用输入输出)引脚,按照SPI的通信协议,一位一位地“捏”出时钟(SCLK)、数据输出(MOSI)等信号。听起来是不是有点“手工打造”的味道?没错,它的本质就是用软件代码去模拟一个硬件模块的行为。我最初接触软件SPI是为了驱动一块老式的诺基亚5110 LCD屏,用的是一块只有单硬件SPI的STM32,不得已而为之。但深入使用后我发现,这不仅仅是一个“备胎”方案,它更是一个理解SPI通信底层时序、调试复杂外设、以及应对资源受限场景的利器。
2. 软件模拟SPI的核心原理与模式解析
要动手模拟,必须先吃透协议。SPI通信的核心是主从设备之间通过四根线进行同步、全双工的数据交换。这四根线是:
- SCLK (Serial Clock):时钟信号,由主设备产生,用于同步数据位。
- MOSI (Master Out Slave In):主设备数据输出,从设备数据输入。
- MISO (Master In Slave Out):从设备数据输出,主设备数据输入。
- SS/CS (Slave Select / Chip Select):片选信号,由主设备控制,低电平有效时选中对应的从设备。
软件模拟SPI,就是我们要用代码在GPIO上精确地控制这些信号的时序。这里的关键在于两个概念:时钟极性(CPOL)和时钟相位(CPHA)。它们共同定义了四种SPI模式,决定了时钟空闲状态和数据的采样边沿。
- CPOL (Clock Polarity):
- CPOL=0:时钟空闲时为低电平。
- CPOL=1:时钟空闲时为高电平。
- CPHA (Clock Phase):
- CPHA=0:数据在时钟的第一个边沿(对于CPOL=0是上升沿,对于CPOL=1是下降沿)被采样,在第二个边沿变化。
- CPHA=1:数据在时钟的第二个边沿被采样,在第一个边沿变化。
这组合起来就是常见的Mode 0, 1, 2, 3。绝大多数SPI设备(如Flash、ADC、OLED屏)都工作在Mode 0 (CPOL=0, CPHA=0)或Mode 3 (CPOL=1, CPHA=1)。这两种模式的数据采样边沿都是时钟的上升沿。我个人的经验是,先确认你的外设数据手册要求哪种模式,这是成功通信的第一步。
软件模拟的核心流程,以8位数据、Mode 0为例,发送一个字节(0xAA,二进制10101010):
- 拉低片选CS,选中设备。
- 确保初始时钟SCLK为低(CPOL=0)。
- 循环8次: a. 根据要发送的字节的最高位(MSB First)或最低位(LSB First),设置MOSI引脚为高或低电平(对于0xAA,MSB是1,所以置高)。 b. 拉高SCLK(产生上升沿)。此时,从设备会在SCLK的这个上升沿采样MOSI上的数据位。 c. (可选)如果同时需要读取,在此刻或稍后读取MISO引脚上的电平,作为接收到的数据位。 d. 拉低SCLK(产生下降沿)。此时,主从设备可以准备下一位数据(对于CPHA=0,数据在下降沿变化)。 e. 将要发送的字节左移或右移一位,准备下一位。
- 循环结束,拉高CS,结束本次传输。
注意:数据移位的顺序(MSB还是LSB first)同样由外设决定,必须严格遵循数据手册。很多通信失败就源于模式或位序设错。
2.1 模拟SPI的优缺点与适用场景
理解了原理,我们再来客观看待它的价值。
优点:
- 极致灵活:不占用硬件外设,理论上可以用任意GPIO模拟,数量不限。你可以用PA5, PA6, PA7模拟一组SPI驱动屏幕,同时用PB10, PB11, PB12模拟另一组SPI读写SD卡,完美解决硬件资源冲突。
- 完全可控:你可以精确控制每个时钟沿的时机,插入微秒级的延时进行调试,或者实现非常规的、低速的特殊通信时序。这对于研究协议、逆向工程或者驱动一些非标设备非常有用。
- 跨平台通用:代码逻辑几乎可以不加修改地在不同架构的MCU(如STM32、ESP32、Arduino、51单片机)上运行,只要提供基本的GPIO操作和延时函数即可,移植性极强。
- 成本与入门:在一些超低成本的8位单片机(可能无硬件SPI)上实现SPI功能,降低了硬件成本和学习门槛。
缺点:
- 速度慢:这是最显著的缺点。因为每个位的操作都需要多条CPU指令(置高低电平、移位、循环判断),其速度远低于硬件SPI的DMA传输。通常软件SPI的极限频率在几百KHz到一两MHz(取决于CPU主频和代码优化),而硬件SPI轻松可达数十MHz。
- CPU占用率高:通信期间CPU被完全占用,无法执行其他任务,不适合在需要高实时性或复杂多任务的系统中高速、频繁地通信。
- 时序精度依赖CPU:时序由软件延时循环保证,容易受到中断干扰。如果通信过程中发生中断,可能导致时钟脉宽变形,通信失败。
适用场景总结:
- 驱动低速SPI设备,如OLED屏(通常几MHz以内)、温湿度传感器、IO扩展芯片等。
- 硬件SPI引脚被占用或数量不足时的补充方案。
- 教学、调试、理解SPI协议底层。
- 在无硬件SPI的廉价MCU上实现SPI功能。
- 需要特殊、非标准时序的场合。
3. 软件模拟SPI的代码实现与深度优化
理论说再多,不如一行代码。下面我将以ARM Cortex-M内核的STM32为例,使用标准外设库(Standard Peripheral Library)风格进行实现和解析。之所以选它,是因为其寄存器操作清晰,便于理解本质,且易于移植到HAL库或其他平台。
3.1 基础架构与GPIO初始化
首先,我们需要定义一个结构体来管理一组模拟SPI所需的引脚和配置。这比使用分散的全局变量要清晰得多。
typedef struct { GPIO_TypeDef* GPIOx_SCLK; // SCLK引脚所在的GPIO组 uint16_t Pin_SCLK; // SCLK引脚号 GPIO_TypeDef* GPIOx_MOSI; // MOSI引脚所在的GPIO组 uint16_t Pin_MOSI; // MOSI引脚号 GPIO_TypeDef* GPIOx_MISO; // MISO引脚所在的GPIO组(如果只发送则可省略) uint16_t Pin_MISO; // MISO引脚号 GPIO_TypeDef* GPIOx_CS; // CS引脚所在的GPIO组 uint16_t Pin_CS; // CS引脚号 uint8_t CPOL; // 时钟极性,0或1 uint8_t CPHA; // 时钟相位,0或1 uint8_t BitOrder; // 位顺序,0为MSB First,1为LSB First uint32_t Delay; // 时钟半周期延时(粗略控制频率) } SoftSPI_HandleTypeDef;初始化函数负责将这些GPIO配置为推挽输出(SCLK, MOSI, CS)和上拉输入(MISO)。
void SoftSPI_Init(SoftSPI_HandleTypeDef *hspi) { GPIO_InitTypeDef GPIO_InitStruct = {0}; // 初始化时钟(假设已开启) // RCC_APB2PeriphClockCmd(对应RCC, ENABLE); // 配置SCLK, MOSI, CS为推挽输出 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; // 高速输出以获得更陡峭的边沿 GPIO_InitStruct.GPIO_Pin = hspi->Pin_SCLK; GPIO_Init(hspi->GPIOx_SCLK, &GPIO_InitStruct); GPIO_InitStruct.GPIO_Pin = hspi->Pin_MOSI; GPIO_Init(hspi->GPIOx_MOSI, &GPIO_InitStruct); GPIO_InitStruct.GPIO_Pin = hspi->Pin_CS; GPIO_Init(hspi->GPIOx_CS, &GPIO_InitStruct); // 配置MISO为上拉输入(如果使用) if (hspi->GPIOx_MISO != NULL) { GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStruct.GPIO_Pin = hspi->Pin_MISO; GPIO_Init(hspi->GPIOx_MISO, &GPIO_InitStruct); } // 设置初始状态:CS高(不选中),SCLK根据CPOL设置空闲电平 GPIO_SetBits(hspi->GPIOx_CS, hspi->Pin_CS); if (hspi->CPOL == 0) { GPIO_ResetBits(hspi->GPIOx_SCLK, hspi->Pin_SCLK); // CPOL=0, 空闲低 } else { GPIO_SetBits(hspi->GPIOx_SCLK, hspi->Pin_SCLK); // CPOL=1, 空闲高 } // MOSI初始状态可高可低,通常置低或置高取决于设备要求 GPIO_ResetBits(hspi->GPIOx_MOSI, hspi->Pin_MOSI); }3.2 核心收发函数与四种模式实现
这是软件SPI的“心脏”。我们需要实现一个函数,能处理四种模式。下面是一个支持全双工(同时收发)的SoftSPI_TransmitReceiveByte函数。
uint8_t SoftSPI_TransmitReceiveByte(SoftSPI_HandleTypeDef *hspi, uint8_t txData) { uint8_t rxData = 0; uint8_t i; uint8_t bitMask; // 根据位序决定掩码和移位方向 if (hspi->BitOrder == 0) { // MSB First bitMask = 0x80; // 1000 0000 } else { // LSB First bitMask = 0x01; // 0000 0001 } // 循环8次,处理每个数据位 for (i = 0; i < 8; i++) { // ---------- 第一个时钟边沿的处理 (与CPHA相关) ---------- if (hspi->CPHA == 0) { // CPHA=0: 数据在第一个边沿被采样,因此需要在第一个边沿之前设置好数据 if (txData & bitMask) { GPIO_SetBits(hspi->GPIOx_MOSI, hspi->Pin_MOSI); } else { GPIO_ResetBits(hspi->GPIOx_MOSI, hspi->Pin_MOSI); } // 产生第一个边沿(对于CPOL=0是上升沿,CPOL=1是下降沿) // 即:将SCLK从空闲状态翻转到非空闲状态 if (hspi->CPOL == 0) { GPIO_SetBits(hspi->GPIOx_SCLK, hspi->Pin_SCLK); // 低->高,上升沿 } else { GPIO_ResetBits(hspi->GPIOx_SCLK, hspi->Pin_SCLK); // 高->低,下降沿 } // 采样点:在第一个边沿后短暂延时,然后读取MISO SoftSPI_Delay(hspi->Delay); if (hspi->GPIOx_MISO != NULL) { if (GPIO_ReadInputDataBit(hspi->GPIOx_MISO, hspi->Pin_MISO)) { if (hspi->BitOrder == 0) { rxData |= bitMask; } else { rxData |= bitMask; } } } // 保持一段时间 SoftSPI_Delay(hspi->Delay); // 产生第二个边沿,回到空闲状态 if (hspi->CPOL == 0) { GPIO_ResetBits(hspi->GPIOx_SCLK, hspi->Pin_SCLK); // 高->低 } else { GPIO_SetBits(hspi->GPIOx_SCLK, hspi->Pin_SCLK); // 低->高 } } else { // CPHA == 1 // CPHA=1: 数据在第二个边沿被采样,第一个边沿用于数据建立 // 产生第一个边沿(数据变化边沿) if (hspi->CPOL == 0) { GPIO_SetBits(hspi->GPIOx_SCLK, hspi->Pin_SCLK); // 低->高 } else { GPIO_ResetBits(hspi->GPIOx_SCLK, hspi->Pin_SCLK); // 高->低 } // 在第一个边沿后设置数据 SoftSPI_Delay(hspi->Delay); if (txData & bitMask) { GPIO_SetBits(hspi->GPIOx_MOSI, hspi->Pin_MOSI); } else { GPIO_ResetBits(hspi->GPIOx_MOSI, hspi->Pin_MOSI); } SoftSPI_Delay(hspi->Delay); // 产生第二个边沿(采样边沿) if (hspi->CPOL == 0) { GPIO_ResetBits(hspi->GPIOx_SCLK, hspi->Pin_SCLK); // 高->低,下降沿 } else { GPIO_SetBits(hspi->GPIOx_SCLK, hspi->Pin_SCLK); // 低->高,上升沿 } // 采样点:在第二个边沿后读取MISO if (hspi->GPIOx_MISO != NULL) { if (GPIO_ReadInputDataBit(hspi->GPIOx_MISO, hspi->Pin_MISO)) { if (hspi->BitOrder == 0) { rxData |= bitMask; } else { rxData |= bitMask; } } } } // 移位,准备下一位 if (hspi->BitOrder == 0) { bitMask >>= 1; // MSB First,右移 } else { bitMask <<= 1; // LSB First,左移 } // 可选:在回到空闲状态后加一个微小延时,确保时序稳定 // SoftSPI_Delay(hspi->Delay/2); } return rxData; }为了方便使用,我们通常封装只发送和只接收的函数:
void SoftSPI_Transmit(SoftSPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size) { GPIO_ResetBits(hspi->GPIOx_CS, hspi->Pin_CS); // 拉低CS for (uint16_t i = 0; i < Size; i++) { (void)SoftSPI_TransmitReceiveByte(hspi, pData[i]); // 忽略接收值 } GPIO_SetBits(hspi->GPIOx_CS, hspi->Pin_CS); // 拉高CS } void SoftSPI_Receive(SoftSPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size) { GPIO_ResetBits(hspi->GPIOx_CS, hspi->Pin_CS); for (uint16_t i = 0; i < Size; i++) { pData[i] = SoftSPI_TransmitReceiveByte(hspi, 0xFF); // 通常发送0xFF以读取数据 } GPIO_SetBits(hspi->GPIOx_CS, hspi->Pin_CS); }SoftSPI_Delay函数是一个简单的忙等待延时,用于控制时钟频率。它的精度直接决定了SPI的速率和稳定性。
static void SoftSPI_Delay(uint32_t count) { while (count--) { __NOP(); // 空操作指令,产生一个CPU周期的延时 } }Delay参数需要根据你的CPU主频进行校准。例如,主频72MHz时,__NOP()大约耗时1/72us ≈ 13.9ns。一个count循环除了__NOP()还有自减和跳转指令,总周期数更多。你需要通过示波器测量或计算来调整count值,以达到目标SCLK频率(例如,1MHz的周期是1us,半周期就是500ns)。
3.3 性能优化实战技巧
基础版本能工作,但可能很慢。在资源紧张的系统中,每一微秒都值得争取。以下是我在实践中总结的优化手段:
寄存器直接操作替代库函数:这是最有效的优化。像
GPIO_SetBits、GPIO_ResetBits这类库函数内部有判断和跳转,开销大。直接操作GPIOx->BSRR(置位/复位寄存器)或GPIOx->ODR(输出数据寄存器)速度极快。// 优化前: GPIO_SetBits(GPIOA, GPIO_Pin_5); // 优化后: GPIOA->BSRR = GPIO_Pin_5; // 置位 GPIOA->BSRR = (uint32_t)GPIO_Pin_5 << 16; // 复位 // 或者直接操作ODR(需先读取-修改-写入): GPIOA->ODR |= GPIO_Pin_5; GPIOA->ODR &= ~GPIO_Pin_5;循环展开:将8次循环展开成顺序执行的8组语句,消除循环判断和自增的开销。代码会变长,但速度显著提升。
// 优化前:for(i=0; i<8; i++) { ... } // 优化后(MSB First示例): // 处理第7位 if(txData & 0x80) MOSI_HIGH(); else MOSI_LOW(); SCLK_TOGGLE(); DELAY(); READ_MISO_BIT(7); DELAY(); SCLK_TOGGLE(); // 处理第6位 if(txData & 0x40) MOSI_HIGH(); else MOSI_LOW(); SCLK_TOGGLE(); DELAY(); READ_MISO_BIT(6); DELAY(); SCLK_TOGGLE(); // ... 处理第0位使用内联汇编或编译器内置函数:对于延时,可以用精确的汇编指令(如
__asm__ volatile(“nop”))或编译器提供的__NOP()。在IAR或Keil中,可以插入精确的NOP指令序列来控制延时。根据CPOL/CPHA预编译分支:如果你的SPI模式是固定的,不要在运行时用
if判断CPHA。可以写成两个独立的函数(如SoftSPI_TransmitReceiveByte_Mode0和SoftSPI_TransmitReceiveByte_Mode3),或者使用宏定义在编译时选择代码路径,消除条件判断的开销。降低延时精度要求:对于很多低速设备(如传感器),对时钟的占空比和精确频率并不敏感。你可以适当减少延时循环的次数,只要通信能稳定即可。用示波器观察波形,找到稳定工作的最小延时。
实操心得:优化是一把双刃剑。在追求极致速度前,先用一个稳定、清晰的基础版本实现功能并测试通过。优化后的代码可读性和可移植性会下降。我的建议是,为同一个软件SPI模块维护两个版本:一个“清晰版”用于调试和移植,一个“优化版”用于最终产品。优化时,务必用逻辑分析仪或示波器验证波形是否合规。
4. 软件模拟SPI的调试与实战问题排查
代码写好了,但设备没反应?这是最考验耐心和经验的环节。下面是我用逻辑分析仪和示波器调试软件SPI时,总结的一套排查流程和常见问题。
4.1 调试工具与连接
工欲善其事,必先利其器。
- 逻辑分析仪:必备神器。推荐Saleae Logic系列或其国产兼容版。它能同时捕获多路数字信号(SCLK, MOSI, MISO, CS),并以时序图方式显示,直观看到每一位数据、每个边沿的关系。配合解码功能(如SPI解码器),能直接将波形翻译成十六进制数据,极大提升调试效率。
- 示波器:用于观察信号质量,如上升/下降时间、过冲、振铃等模拟特性。当通信距离较长或速度较高时特别有用。
- 连接:确保逻辑分析仪的探针地线(GND)与你的MCU板共地。探针连接到对应的GPIO引脚。如果引脚是3.3V电平,确保逻辑分析仪阈值电压设置为2.0V左右(CMOS电平)。
4.2 常见问题排查清单
当你发送了数据但设备无响应时,请按以下顺序排查:
| 问题现象 | 可能原因 | 排查方法与解决方案 |
|---|---|---|
| 完全没有波形 | 1. GPIO初始化错误,未配置为输出。 2. 代码未执行到SPI发送函数。 3. 硬件连接断开或短路。 | 1. 检查初始化代码,用万用表测量引脚在代码运行后是否为高阻态(错误)或固定电平。 2. 在SPI函数入口设置断点或翻转一个LED,确认函数被调用。 3. 检查杜邦线、焊点。 |
| 有SCLK和MOSI波形,但设备不响应 | 1.SPI模式(CPOL/CPHA)设置错误。这是最常见的原因! 2. 片选CS信号异常(常高、常低或时序不对)。 3. 数据位序(MSB/LSB)错误。 4. 时钟频率太快,设备跟不上。 | 1.用逻辑分析仪捕获波形。对照设备数据手册的时序图,检查SCLK空闲电平(CPOL)和数据采样边沿(CPHA)是否匹配。Mode 0和Mode 3最易混淆。 2. 确认CS在传输前拉低,传输后拉高。检查CS是否有毛刺或意外抖动。 3. 检查数据解码是否正确。尝试切换位序。 4. 增大 Delay参数,降低SCLK频率再试。 |
| 波形畸形(上升沿缓慢、过冲) | 1. GPIO驱动能力不足或负载过重。 2. 导线过长,引入寄生电容。 3. 未使能GPIO引脚的内置上拉/下拉。 | 1. 将GPIO速度配置为最高(如50MHz)。 2. 缩短连接线,或在靠近MCU引脚处加一个几十欧姆的串联电阻阻尼振荡。 3. 对于MISO输入引脚,使能上拉电阻,避免悬空。 |
| 通信偶尔出错,数据位跳变 | 1. 软件延时被中断打断,导致时钟脉宽异常。 2. 电源噪声干扰。 3. 多个设备总线冲突(MISO线竞争)。 | 1. 在SPI收发函数的关键时序部分(整个for循环)关闭全局中断(__disable_irq()),操作完毕后再开启(__enable_irq())。这是软件SPI稳定的关键!2. 检查电源稳定性,在MCU和设备的电源引脚就近加退耦电容(如100nF)。 3. 确保同一时刻只有一个设备的CS被选中,未选中的设备MISO应呈高阻态。 |
| 能写不能读 | 1. MISO引脚未正确配置为输入。 2. 读取MISO的时机不对(CPHA影响)。 3. 从设备需要特定命令才输出数据。 | 1. 确认MISO GPIO模式为浮空输入或上拉输入。 2. 用逻辑分析仪对比MISO信号和你的代码读取点,调整读取语句在时钟沿后的位置。 3. 查阅设备手册,确认读操作的正确命令序列。 |
4.3 一个具体的调试案例:驱动W25Qxx Flash
以驱动常见的SPI Flash芯片W25Q64为例。其典型模式是Mode 0,MSB First。数据手册要求,在发送命令后,需要等待一个“忙状态”解除才能进行下一步。
问题:发送“写使能”命令(0x06)后,紧接着发送“页编程”命令(0x02)和数据,但数据写入失败。
排查过程:
- 逻辑分析仪抓取波形:发现命令字节(0x06, 0x02)的波形完全正确,模式、时序都对。
- 对比手册:发现“页编程”命令前,必须确保“写使能锁存器”已被置位。而“写使能”命令执行后,芯片需要一小段时间(t_WEL,典型值几微秒)来置位这个锁存器。
- 根源:我的代码在发送0x06后,没有等待t_WEL,就立刻拉高CS结束传输,然后马上开始下一次传输发送0x02。这可能导致芯片内部还未准备好。
- 解决:在“写使能”命令后,拉高CS,插入一个
Delay_us(10)的延时,再拉低CS开始发送“页编程”命令。或者,更规范的做法是,在“页编程”前发送“读状态寄存器1”命令(0x05),轮询BUSY位,等待其变为0。
这个案例告诉我们,软件模拟SPI不仅要关注波形本身,还要严格遵守外设芯片的命令序列和时序要求。硬件SPI的库函数有时会封装这些等待,而软件模拟需要我们自己实现。
5. 进阶应用:多设备管理与特殊时序模拟
掌握了单设备通信,我们可以玩点更复杂的。
5.1 模拟多路SPI总线
假设我们需要用软件模拟驱动三个设备:一个OLED(只写),一个温度传感器(读写),一个Flash(读写)。它们的CS引脚分别接在PA4、PA3、PA2上,但共享SCLK(PA5)、MOSI(PA6)、MISO(PA7)。
实现思路:
- 初始化时,将所有共享引脚(SCLK, MOSI, MISO)和所有CS引脚都初始化为输出(MISO为输入)。
- 为每个设备定义一个独立的
SoftSPI_HandleTypeDef结构体实例,其中CS引脚各自独立,其他引脚指针指向相同的GPIO和Pin。 - 在每次传输函数(
SoftSPI_Transmit)内部,操作CS引脚时,操作的是当前设备实例中指定的CS引脚。这样,当向OLED发送数据时,只有OLED的CS被拉低,其他设备的CS保持高电平,避免了总线冲突。
代码示例:
SoftSPI_HandleTypeDef hspi_oled, hspi_sensor, hspi_flash; // 初始化共享引脚 void SoftSPI_Multi_Init(void) { // 初始化SCLK, MOSI, MISO (PA5, PA6, PA7) // ... // 初始化三个CS引脚 (PA4, PA3, PA2) 为输出高 // ... // 配置OLED(只写,无需MISO) hspi_oled.GPIOx_SCLK = GPIOA; hspi_oled.Pin_SCLK = GPIO_Pin_5; hspi_oled.GPIOx_MOSI = GPIOA; hspi_oled.Pin_MOSI = GPIO_Pin_6; hspi_oled.GPIOx_MISO = NULL; hspi_oled.Pin_MISO = 0; hspi_oled.GPIOx_CS = GPIOA; hspi_oled.Pin_CS = GPIO_Pin_4; hspi_oled.CPOL = 0; hspi_oled.CPHA = 0; hspi_oled.BitOrder = 0; // 配置传感器和Flash类似,CS引脚不同,MISO指向PA7 } // 使用时 SoftSPI_Transmit(&hspi_oled, oled_data, sizeof(oled_data)); // 只有PA4拉低 uint8_t temp = SoftSPI_TransmitReceiveByte(&hspi_sensor, 0x00); // 只有PA3拉低5.2 模拟特殊时序:应对非标设备
我曾遇到过一个老式的数码管驱动芯片,它使用类似SPI的协议,但数据是在SCLK的上升沿和下降沿都有效(即每个时钟周期传输2位数据)。硬件SPI无法直接生成这种波形,但软件模拟可以轻松实现。
思路:在for循环内,每位数据操作两次SCLK翻转,并在不同的边沿放置数据。
// 伪代码,每个时钟周期传输2位 (bit1在上升沿,bit2在下降沿) for(int i=0; i<16; i+=2) { // 假设16位数据 // 准备bit1 set_mosi(get_bit(data, i)); // 产生上升沿 sclk_high(); delay(); // 准备bit2 set_mosi(get_bit(data, i+1)); // 产生下降沿 sclk_low(); delay(); }这种灵活性是硬件SPI无法比拟的。软件模拟让你成为时序的“导演”,可以创造出符合任何非标准设备要求的通信波形。
6. 软件模拟SPI与硬件SPI的协同与选型建议
最后,我们来谈谈在项目中如何做选择。
何时选择软件模拟SPI?
- 低速设备:通信频率要求低于1MHz,且对CPU占用不敏感。
- 引脚资源冲突:硬件SPI引脚被其他功能占用(如重映射失败)。
- 需要多个SPI接口:硬件SPI数量不足。
- 协议调试与学习:需要观察和操控每一个时钟沿。
- 非标准时序:设备时序不符合标准SPI模式。
- 低成本MCU:使用的MCU没有硬件SPI外设。
何时必须使用硬件SPI?
- 高速通信:频率超过1MHz,特别是需要传输大量数据(如图像刷新、音频流)。
- 低功耗要求:硬件SPI由外设自动完成,CPU可以进入睡眠模式。
- 高实时性系统:CPU需要处理其他紧急任务,不能被SPI通信长时间阻塞。
- 需要DMA:进行大数据量、零CPU占用的传输。
混合使用方案:一个务实的方案是**“硬件SPI为主,软件模拟为辅”**。例如,用硬件SPI+DMA驱动高速TFT屏,同时用软件SPI与一个低速的传感器通信。两者可以共存,只要使用不同的GPIO引脚即可。
软件模拟SPI是一项看似简单却内涵丰富的技能。它强迫你去理解通信协议最底层的时序逻辑,这种理解对于调试复杂的硬件问题、阅读外设数据手册、乃至设计自己的数字接口都大有裨益。从“它能用”到“它稳定高效”,中间隔着一堆需要踩的坑和需要优化的细节。希望这篇长文里分享的原理、代码和踩坑经验,能让你在下次遇到SPI资源紧张或者时序难题时,多一份从容和底气。
