软件模拟SPI:从GPIO时序控制到嵌入式通信的灵活解决方案

📅 2026/7/29 9:12:55
软件模拟SPI:从GPIO时序控制到嵌入式通信的灵活解决方案
1. 项目概述为什么需要软件模拟SPI在嵌入式开发里SPISerial Peripheral Interface总线几乎是工程师的老朋友了从驱动一块小小的Flash芯片到点亮一块高分辨率的LCD屏再到与各种传感器通信它的身影无处不在。硬件SPI控制器集成在MCU内部用起来确实方便配置好寄存器数据收发就交给DMACPU可以腾出手来做其他事效率高时序也精准。但现实项目里你总会遇到一些“尴尬”的情况手头的MCU硬件SPI接口用完了但项目还得加个SPI设备或者为了极致成本选了一款没有硬件SPI的便宜MCU又或者硬件SPI的引脚被PCB布局固定死了无法连接到你需要的新设备上。这时候“软件模拟SPI”Software SPI或Bit-Banging SPI就成了救命稻草。简单说软件模拟SPI就是不用MCU自带的专用硬件模块而是直接用普通的GPIO通用输入输出口通过程序代码的精确延时和控制来模拟出SPI通信所需要的时钟SCLK、数据输入MISO、数据输出MOSI和片选CS这四根线的时序波形。这听起来像是开手动挡汽车一切换挡、离合、油门都得自己来不如自动挡硬件SPI省心但它给了你无与伦比的灵活性任何GPIO都可以成为SPI引脚时序可以微调以适配非标设备甚至可以实现一些硬件SPI不直接支持的特殊模式。最近在社区里围绕软件模拟SPI的讨论热度一直不减。从新手在STM32上驱动ILI9341 SPI屏时遇到的时序问题到老手用ESP32的RMT远程控制模块去精准控制SPI波形以驱动WS2812灯带再到在FPGA里用Verilog写一个完全可综合的SPI从机模块这些话题的核心都离不开对SPI协议本质的理解和精准的时序控制能力。而软件模拟正是深入理解SPI协议原理的最佳实践途径。搞懂了它你不仅能解决眼前的引脚冲突问题更能透彻理解SPI的四种模式、时钟极性与相位、数据移位的边沿这些知识会让你在调试任何SPI相关问题时都游刃有余。2. SPI协议核心原理快速回顾在动手写代码之前我们必须把SPI的“交通规则”彻底搞清楚。很多人在调试SPI时出的问题根源都在于对这几个基本概念理解模糊。2.1 SPI的四种工作模式CPOL与CPHA这是SPI最核心也最容易混淆的地方。SPI模式由时钟极性CPOL和时钟相位CPHA两个参数组合而成共四种模式Mode 0, 1, 2, 3。时钟极性CPOL决定了SCLK时钟线在空闲状态即片选CS无效无数据传输时的电平。CPOL 0SCLK空闲时为低电平。CPOL 1SCLK空闲时为高电平。 你可以把它想象成信号的“默认休息状态”。时钟相位CPHA决定了数据在SCLK的哪个边沿被采样捕获以及在哪个边沿被改变输出。CPHA 0数据在SCLK的第一个边沿即从空闲状态跳变到第一个有效时钟的那个边沿被采样在下一个边沿改变。CPHA 1数据在SCLK的第二个边沿被采样在第一个边沿改变。这里的关键是理解“第一个边沿”和“第二个边沿”是相对于时钟周期而言的而时钟周期的起点是SCLK从空闲状态发生跳变的那一刻。我习惯用“抓拍”和“换姿势”来类比SCLK的边沿就像快门数据采样就是抓拍瞬间数据改变则是模特换下一个姿势。CPHA决定了是“快门按下时抓拍然后模特换姿势”CPHA0还是“模特先换好姿势等快门按下时抓拍”CPHA1。模式对照表模式CPOLCPHA空闲时钟数据采样边沿数据改变边沿常见设备Mode 000低电平上升沿第一个下降沿第二个很多传感器如BMP280 Flash芯片Mode 101低电平下降沿第二个上升沿第一个一些ADC 老式SD卡SPI模式Mode 210高电平下降沿第一个上升沿第二个较少见Mode 311高电平上升沿第二个下降沿第一个一些RFID读卡器如RC522注意绝大多数SPI设备的模式是固定的必须在主设备你的MCU端配置正确。通常设备数据手册的时序图会明确标注。调试时如果数据全错或完全无响应首先检查模式是否匹配。2.2 数据移位顺序MSB/LSBSPI协议本身没有规定先传高位MSB First还是先传低位LSB First这完全由设备决定。大部分设备尤其是Flash、显示屏控制器默认都是MSB First。但有些特定芯片比如某些音频编解码器可能会是LSB First。这个配置错误会导致你读到的数据高低位完全颠倒。2.3 时钟频率与建立/保持时间软件模拟SPI的速率受限于你代码循环和GPIO操作的速度。对于8位或16位的MCU模拟几MHz的SPI时钟可能就很吃力了对于ARM Cortex-M系列达到10MHz左右是可能的但需要精心优化代码甚至使用汇编或特殊指令如STM32的__WFI配合GPIO位带操作。更重要的是建立时间Setup Time和保持时间Hold Time。以Mode 0为例主机在SCLK上升沿采样从机发来的MISO数据。这意味着在上升沿到来之前MISO线上的数据必须已经稳定了一段时间建立时间在上升沿之后数据还需要再保持稳定一段时间保持时间。软件模拟时我们通过延时来保证这些时间要求。如果延时太短数据可能采样错误如果延时过长虽然通信稳定但极限速率会降低。3. 软件模拟SPI的驱动设计与实现理解了原理我们就可以动手设计一个健壮、可配置的软件SPI驱动了。一个好的驱动应该做到接口清晰、模式可配、时序可控。3.1 GPIO抽象与初始化首先我们需要抽象出SPI所需的四个引脚。不建议在驱动函数里直接写死硬件寄存器而是通过一个结构体来配置。typedef struct { GPIO_TypeDef* cs_port; // 片选引脚端口 uint16_t cs_pin; // 片选引脚编号 GPIO_TypeDef* sclk_port; // 时钟引脚端口 uint16_t sclk_pin; GPIO_TypeDef* mosi_port; // 主机输出引脚端口 uint16_t mosi_pin; GPIO_TypeDef* miso_port; // 主机输入引脚端口 uint16_t miso_pin; } SoftSPI_PinConfig_t; typedef struct { SoftSPI_PinConfig_t pins; uint8_t mode; // SPI模式0, 1, 2, 3 uint8_t data_order; // 数据顺序SOFT_SPI_MSB_FIRST 或 SOFT_SPI_LSB_FIRST uint32_t delay_us; // 半时钟周期延时粗略控制速率 } SoftSPI_HandleTypeDef;初始化函数负责配置这些GPIO为推挽输出SCLK MOSI CS和上拉输入MISO。这里有个关键点根据CPOL初始化SCLK的电平。如果CPOL0Mode 0或1初始化后应将SCLK引脚置为低电平如果CPOL1Mode 2或3则初始化为高电平。这确保了SCLK线处于正确的空闲状态。3.2 核心收发函数实现收发一个字节的函数是软件SPI的灵魂。我们以最常用的Mode 0 (CPOL0 CPHA0)为例拆解其实现。uint8_t SoftSPI_TransmitReceiveByte(SoftSPI_HandleTypeDef *hspi, uint8_t tx_data) { uint8_t rx_data 0; uint8_t bit; if(hspi-data_order SOFT_SPI_MSB_FIRST) { // MSB First从最高位bit7开始发送 for(bit 0x80; bit 0; bit 1) { // 1. 设置MOSI输出当前bit if(tx_data bit) { HAL_GPIO_WritePin(hspi-pins.mosi_port, hspi-pins.mosi_pin, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(hspi-pins.mosi_port, hspi-pins.mosi_pin, GPIO_PIN_RESET); } // 2. 短暂延时保证MOSI数据稳定建立时间 SoftSPI_Delay(hspi-delay_us); // 3. 产生上升沿SCLK从0-1Mode 0下从机在此刻采样MOSI主机准备采样MISO HAL_GPIO_WritePin(hspi-pins.sclk_port, hspi-pins.sclk_pin, GPIO_PIN_SET); // 4. 读取MISO引脚状态并入接收数据 if(HAL_GPIO_ReadPin(hspi-pins.miso_port, hspi-pins.miso_pin)) { rx_data | bit; } // 5. 延时保证主机采样稳定 SoftSPI_Delay(hspi-delay_us); // 6. 产生下降沿SCLK从1-0Mode 0下从机在此刻改变其MISO输出 HAL_GPIO_WritePin(hspi-pins.sclk_port, hspi-pins.sclk_pin, GPIO_PIN_RESET); // 7. 可以不延时或短暂延时进入下一个bit循环 } } else { // LSB First从最低位bit0开始发送逻辑类似循环方向相反 for(bit 0x01; bit ! 0; bit 1) { // ... 类似代码注意bit操作和移位方向 } } return rx_data; }代码逻辑解读与注意事项建立时间步骤2在拉高SCLK产生采样边沿之前先设置好MOSI并延时。这个延时确保了数据在从机采样边沿到来前已经稳定。对于高速通信这个延时至关重要。采样点步骤4在SCLK上升沿之后立即读取MISO。在Mode 0下从机会在SCLK上升沿改变其MISO数据供主机在下一个下降沿采样等等这里容易出错重新核对时序对于Mode 0 (CPHA0)从机是在SCLK的第一个边沿上升沿采样主机数据同时从机输出的数据也是在同一个上升沿就已经有效了或者说是在上一个下降沿准备好的。因此主机应该在上升沿之后步骤4立即采样MISO这是正确的。从机数据的改变发生在下降沿那是为下一个上升沿采样做准备的。所以我们的代码逻辑是对的。保持时间步骤5的延时可以看作是主机采样数据的保持时间同时也构成了SCLK高电平的脉宽。SCLK下降沿步骤6这个下降沿告诉从机“我主机这个bit发完了你可以准备下一个bit的数据了”。同时主机在下降沿后改变MOSI数据在下一轮循环的第一步为下一个上升沿的传输做准备。实操心得SoftSPI_Delay函数的实现直接影响通信速率和稳定性。简单的for循环空转受编译器优化和中断影响大。对于时间要求不严的应用比如100KHz以下可以凑合用。对于更高要求可以考虑使用MCU的硬件定时器产生精确延时或者像在ESP32上使用RMT、在STM32上使用定时器PWM结合DMA来产生精准的SCLK波形这才是软件模拟SPI的高阶玩法。3.3 支持四种模式的通用函数要让驱动支持四种模式关键在于抽象出“产生边沿”和“采样数据”的动作。我们可以根据CPHA来调整采样和改变数据的顺序。uint8_t SoftSPI_TransmitReceiveByte_Generic(SoftSPI_HandleTypeDef *hspi, uint8_t tx_data) { uint8_t rx_data 0; uint8_t i; uint8_t cpol (hspi-mode 1) 0x01; // 提取CPOL uint8_t cpha hspi-mode 0x01; // 提取CPHA // 根据CPOL设置初始时钟电平 GPIO_PinState sclk_idle cpol ? GPIO_PIN_SET : GPIO_PIN_RESET; // 第一个边沿是时钟从空闲状态翻转的边沿 GPIO_PinState first_edge cpol ? GPIO_PIN_RESET : GPIO_PIN_SET; GPIO_PinState second_edge cpol ? GPIO_PIN_SET : GPIO_PIN_RESET; for(i 0; i 8; i) { uint8_t bit_to_send; // 根据数据顺序决定发送哪一位 if(hspi-data_order SOFT_SPI_MSB_FIRST) { bit_to_send (tx_data (0x80 i)) ? 1 : 0; } else { bit_to_send (tx_data (0x01 i)) ? 1 : 0; } // --- CPHA 0 的处理逻辑 --- if(cpha 0) { // 在第一个边沿之前设置MOSI数据 HAL_GPIO_WritePin(hspi-pins.mosi_port, hspi-pins.mosi_pin, bit_to_send ? GPIO_PIN_SET : GPIO_PIN_RESET); SoftSPI_Delay(hspi-delay_us); // 产生第一个边沿采样边沿 HAL_GPIO_WritePin(hspi-pins.sclk_port, hspi-pins.sclk_pin, first_edge); // 在第一个边沿之后立即采样MISO uint8_t bit_received HAL_GPIO_ReadPin(hspi-pins.miso_port, hspi-pins.miso_pin); if(hspi-data_order SOFT_SPI_MSB_FIRST) { rx_data | (bit_received (7 - i)); } else { rx_data | (bit_received i); } SoftSPI_Delay(hspi-delay_us); // 产生第二个边沿 HAL_GPIO_WritePin(hspi-pins.sclk_port, hspi-pins.sclk_pin, second_edge); } // --- CPHA 1 的处理逻辑 --- else { // 产生第一个边沿数据改变边沿 HAL_GPIO_WritePin(hspi-pins.sclk_port, hspi-pins.sclk_pin, first_edge); // 在第一个边沿之后设置MOSI数据 HAL_GPIO_WritePin(hspi-pins.mosi_port, hspi-pins.mosi_pin, bit_to_send ? GPIO_PIN_SET : GPIO_PIN_RESET); SoftSPI_Delay(hspi-delay_us); // 产生第二个边沿采样边沿 HAL_GPIO_WritePin(hspi-pins.sclk_port, hspi-pins.sclk_pin, second_edge); // 在第二个边沿之后采样MISO uint8_t bit_received HAL_GPIO_ReadPin(hspi-pins.miso_port, hspi-pins.miso_pin); if(hspi-data_order SOFT_SPI_MSB_FIRST) { rx_data | (bit_received (7 - i)); } else { rx_data | (bit_received i); } SoftSPI_Delay(hspi-delay_us); } } // 循环结束后确保SCLK回到空闲状态 HAL_GPIO_WritePin(hspi-pins.sclk_port, hspi-pins.sclk_pin, sclk_idle); return rx_data; }这个通用函数通过cpha变量分支处理清晰地体现了两种相位下的不同数据-时钟关系。特别注意在CPHA1的循环结束后SCLK可能不处于空闲状态需要额外操作将其拉回取决于循环内最后一个边沿是什么。更稳健的做法是在每次传输开始前都先将SCLK设置为空闲电平。4. 软件模拟SPI的进阶优化与实战技巧基础功能实现后我们要考虑性能、稳定性和特殊场景。软件模拟SPI的瓶颈和灵活性都在于GPIO操作和延时。4.1 提升通信速率的方法减少函数调用开销将HAL_GPIO_WritePin/ReadPin这类库函数替换为直接操作寄存器。例如在STM32中可以使用位带操作Bit-Banding或直接写GPIOx-BSRR/ODR寄存器。这能大幅减少单个GPIO操作所需的CPU周期。// 假设已定义好引脚位带别名 #define SOFT_SPI_SCLK_HIGH() (pSCLK_BSRR PIN_MASK) #define SOFT_SPI_SCLK_LOW() (pSCLK_BRR PIN_MASK) #define SOFT_SPI_MOSI_HIGH() (pMOSI_BSRR PIN_MASK) #define SOFT_SPI_MOSI_LOW() (pMOSI_BRR PIN_MASK) #define SOFT_SPI_READ_MISO() ((pMISO_IDR PIN_MASK) ? 1 : 0)使用汇编或内联汇编对于最核心的bit-banging循环用汇编语言编写可以精确控制每条指令的周期数消除C编译器优化带来的不确定性。对于ARM Cortex-M可以使用__asm volatile内联汇编。利用硬件定时器与其用不精确的软件空循环延时不如配置一个基本定时器在需要延时的地方等待定时器标志位。这样可以获得微秒级甚至纳秒级精度的延时通信速率更稳定且不受中断和编译器优化影响。DMAPWM模拟SCLK高阶这是软件模拟SPI的“终极形态”。以STM32为例你可以将一个定时器通道配置为PWM输出模式连接到作为SCLK的GPIO上。然后将需要发送的数据缓冲区通过DMA搬运到另一个作为MOSI的GPIO的ODR输出数据寄存器或BSRR寄存器。同时再开启一个定时器输入捕获或者另一个DMA通道来读取MISO引脚的状态并存入接收缓冲区。这样你只需要启动一次DMA和PWM就能完成整帧数据的收发CPU完全解放速率可以接近GPIO的理论翻转极限。社区里“STM32H750 DMA驱动SPI LCD问题”的讨论很多最终方案就采用了类似思路来解决硬件SPI DMA的兼容性问题。4.2 处理特殊SPI设备与模式三线SPI半双工有些设备为了节省引脚将MOSI和MISO合并为一根数据线SIO。通信是半双工的需要主设备在发送和接收模式间切换。软件模拟实现时需要动态改变该数据线GPIO的方向推挽输出/上拉输入。关键点切换方向后需要加入少量延时等待内部电路稳定。SPI菊花链Daisy Chain多个SPI从设备通过MISO-MOSI串联共享一个片选。主设备发送的数据依次通过所有从设备每个从设备同时接收上位设备传来的数据并将自己的数据移位出去。软件模拟时你需要连续发送足够长度的数据设备数×每设备数据长度并在最后一次性读取。这要求你的驱动能处理任意长度的数据收发。与FPGA通信当你用MCU的软件SPI与FPGA通信时FPGA端通常实现的是SPI从机。这时对时序的要求可能比与专用芯片通信更严格因为FPGA的逻辑是你自己写的两边对时序的理解必须完全一致。建议在FPGA端设计一个稳健的从机接口包含对SCLK的边沿检测、数据采样和同步逻辑并留出足够的建立保持时间余量。调试时双通道示波器同时抓取SCLK和MOSI/MISO波形对照数据手册和代码是定位问题最快的方法。4.3 软件片选 vs 硬件片选软件片选就像我们上面代码里做的在传输开始前手动拉低某个GPIO作为CS传输结束后再拉高。这是最灵活的方式你可以在一次传输中任意控制CS的脉冲甚至用同一个CS控制多个设备通过分时复用。硬件片选NSS有些MCU的硬件SPI模块提供了专用的NSS从机选择引脚硬件可以在数据传输开始时自动拉低结束时自动拉高。在软件模拟中我们很少去模拟这种自动行为因为手动控制更简单直观。但在某些从机模式下MCU作为SPI从设备可能需要处理硬件NSS信号。我的经验是除非有特殊要求如严格的帧同步否则一律使用软件片选。它简单可靠调试时也方便观察CS信号。5. 典型问题排查与调试实录软件模拟SPI出问题时现象可能千奇百怪数据全为0xFF或0x00偶尔能读对一次或者完全没反应。别慌按照以下步骤系统性排查。5.1 基础检查清单电气连接这是第一步也是最容易忽略的一步。用万用表蜂鸣档检查SCLK MOSI MISO CS GND是否都正确连通有没有虚焊、短路。确保电源电压符合要求。引脚配置确认你的GPIO配置正确。MOSI、SCLK、CS是推挽输出MISO是上拉输入或浮空输入具体看从机驱动能力。特别注意如果MISO配置成了输出可能会和从机的输出冲突导致电流过大甚至损坏芯片。模式与顺序99%的通信失败源于此。反复核对从机设备数据手册的时序图确认CPOL、CPHA、MSB/LSB。一个快速验证的方法是用逻辑分析仪或示波器抓取波形对照时序图一个一个边沿看。片选信号确认CS信号在传输期间保持有效低有效或高有效看设备。传输前后是否有正确的无效电平有些设备要求CS在两次传输之间有最小间隔时间。时钟频率你的软件延时产生的SCLK频率是否在从机设备支持的范围内太快了从机跟不上太慢了可能影响整体性能。先用一个很低的频率比如100KHz测试确保通信基本功能正常再逐步提高。5.2 逻辑分析仪/示波器波形分析这是调试数字通信的“眼睛”。将四根线SCLK MOSI MISO CS接到逻辑分析仪上设置好触发通常用CS下降沿触发然后运行你的程序。看整体CS有效期间SCLK是否有规律的脉冲脉冲数量是否等于8的倍数对应字节数看时序参数测量SCLK的频率、占空比。测量MOSI数据在SCLK采样边沿根据CPHA确定是第一个还是第二个边沿之前的建立时间t_SU和之后的保持时间t_HD。确保它们大于设备手册要求的最小值。看数据对齐在正确的采样边沿MOSI和MISO的数据是否稳定用分析仪的SPI协议解码功能直接查看解码出的字节数据与你代码中发送/预期接收的是否一致。常见异常波形SCLK无脉冲检查代码中控制SCLK的GPIO操作是否执行延时函数是否陷入死循环。数据全高或全低检查MOSI/MISO引脚配置输入/输出检查从机是否上电、使能。MISO全高可能是上拉电阻导致从机未驱动全低可能是从机损坏或模式不对。数据错位检查MSB/LSB顺序。如果解码出的数据像是原数据的位反转那就是顺序错了。偶尔出错可能是建立/保持时间不足在高速时因干扰或电源波动导致采样错误。增加延时或检查PCB布线是否有过长、过近的平行线引入串扰。5.3 软件层面的调试技巧简化测试先写一个最简单的测试函数只发一个固定的命令比如很多SPI Flash的读ID命令0x9F然后读取返回。屏蔽其他复杂逻辑。打印调试在GPIO操作前后加入日志如果系统支持或者通过一个额外的GPIO翻转来在示波器上标记代码执行到哪个阶段。检查延时函数用示波器测量你的SoftSPI_Delay函数实际产生的延时。不同优化等级、不同时钟频率下这个延时可能变化很大。中断干扰确保你的SPI通信函数在执行过程中不会被高优先级中断打断。如果必须打断考虑在通信关键段临时关闭中断。5.4 针对特定热词问题的思路stm32h750 dma 驱动 spi lcd 问题这类问题常源于硬件SPI的DMA配置与LCD控制器时序的细微不匹配。如果用软件模拟SPI你可以完全掌控时序。重点模拟LCD控制器要求的写数据(0x2C)和写命令(0x2A等)的时序特别是命令和数据之间的DC数据/命令线切换延时。软件模拟可以轻松插入nop或短延时来满足这些要求。软件模拟iic rc522I2C和SPI都是软件模拟的常见对象。RC522这类MFRC522芯片常用SPI模式。注意它的SPI模式可能是Mode 0且数据在SCLK的上升沿采样。还要注意它的地址读写位组合。软件模拟时仔细对照RC522数据手册的SPI时序图。spi硬件片选与软件片选如前所述除非硬件有特殊自动帧处理需求否则软件片选更推荐。硬件片选可能涉及复杂的NSS管理模式如硬件NSS输出、输入在软件模拟中实现价值不大且复杂。esp32 spi rmt这是一个高级应用。ESP32的RMT模块原本用于红外遥控但其精准控制脉冲序列的特性可以用来生成非常规的、高速的SPI波形例如驱动非标准SPI协议的LED灯带如WS2812它更像是单线归零码。这已经超出了标准SPI模拟的范畴属于“协议模拟”的领域。6. 从软件模拟到深入理解SPI的变体与扩展思考通过亲手实现软件模拟SPI你获得的不仅仅是一个备用方案更是对同步串行通信本质的深刻理解。这种理解能帮助你应对更多变体QSPI/OSPI四线或八线SPI用于高速Flash原理依然是双沿采样、数据线倍增。理解了基本的单线SPI时序这些只是扩展。Dual SPI/Quad SPI将MISO和MOSI作为双向数据线使用提高吞吐量。软件模拟时需要更频繁地切换GPIO方向。SIOP一些厂商自定义的协议可能是在SPI基础上增加了握手线或改变了帧格式。有了软件模拟的功底你可以灵活地修改代码来适配这些非标协议。最后关于软件模拟SPI的性能它永远无法在极限速度上超越精心设计的硬件SPIDMA。它的价值在于灵活性、可调试性和教育意义。在资源紧张、引脚冲突或需要适配特殊时序的场景下它是一个极其强大的工具。当你下次再遇到SPI通信问题时不妨想想如果我用GPIO来模拟这时序应该怎么走很多时候答案就在这个思考过程中变得清晰。