STM32硬件SPI资源不足?混合驱动方案实现精准时序扩展
1. 项目概述当硬件SPI口不够用时我们怎么办在嵌入式开发尤其是基于STM32这类MCU的项目里SPI串行外设接口是连接各类传感器、存储芯片、显示屏的绝对主力。但STM32的型号繁多引脚资源也各不相同。我遇到过不止一次这样的情况项目板上已经挂了一个SPI Flash存数据一个SPI接口的OLED屏做显示这时候客户临时要求再加一个高精度的SPI接口ADC芯片。一翻数据手册傻眼了——这颗STM32F103就只有一个硬件SPI1。硬件资源就这么点需求却摆在那里项目又不能换芯片这时候怎么办“STM32通过硬件SPI模块软件模拟驱动来进行拓展”这个标题指向的就是这个非常实际且经典的工程问题。它的核心思路不是去更换硬件而是在软件层面做文章利用一个现有的硬件SPI模块作为“种子”通过GPIO通用输入输出口和精准的时序控制在软件层面“克隆”或“模拟”出额外的SPI通信能力。这听起来有点像“用一个引擎驱动多辆车”其价值在于它能以极低的硬件成本几乎为零和可接受的软件开销突破MCU原生外设数量的限制为项目赢得宝贵的灵活性和扩展空间。这篇文章我就结合自己多次“踩坑”和“填坑”的经历来彻底拆解这个方案。我会从为什么需要这么做开始讲到具体的设计思路、代码实现中的魔鬼细节再到如何调试这种“软硬结合”的通信最后分享几个我总结出来的、能显著提升模拟SPI稳定性和效率的实战技巧。无论你是正在为SPI口不够用而发愁还是想深入理解SPI协议和MCU底层操作相信都能从中找到直接的参考。2. 核心思路与架构设计并非简单的GPIO翻转很多人一听到“软件模拟SPI”第一反应可能就是那不就是用几个GPIO按照SPI的时序图用HAL_GPIO_WritePin和HAL_GPIO_ReadPin函数去模拟时钟SCK、数据线MOSI/MISO的跳变吗这种做法通常被称为“Bit-Banging”位撞击它完全由CPU通过指令控制GPIO实现简单但效率低下且会严重占用CPU时间。而我们标题里提到的“通过硬件SPI模块软件模拟驱动”是一种更高级、更巧妙的混合架构。它的核心思想是**“硬件为主软件为辅协同扩展”**。2.1 混合驱动架构解析这种架构的精髓在于分层和复用硬件SPI层作为核心与基准。我们首先初始化并配置好一个可用的硬件SPI外设例如SPI1。这一步的意义在于我们得到了一个经过芯片厂商严格测试和优化的、时序绝对精确的“时钟源”和“数据发送引擎”。硬件SPI的SCK时钟频率稳定、占空比准确这是软件模拟很难媲美的。软件模拟扩展层这是实现拓展的关键。我们不直接使用硬件SPI的MOSI和MISO数据线或者仅使用其中之一而是将其“让”给最高优先级或最要求性能的从设备。然后我们额外定义几组普通的GPIO引脚分别作为“扩展SPI”的SCK、MOSI、MISO。协同工作机制时钟同步扩展SPI的SCK信号不再由软件延时循环产生而是严格同步于硬件SPI的SCK时钟。我们可以通过配置硬件SPI工作在“仅输出时钟Master Output Disabled”模式或者简单地将其MOSI/MISO引脚配置为普通推挽输出并固定电平只“借用”其产生的精准SCK时钟信号。更常见的做法是在硬件SPI的传输完成中断TXE/RXNE或DMA传输回调函数中去触发和操作我们扩展的GPIO进行数据读写。这样扩展SPI的每一位数据切换都与硬件SPI的时钟边沿严格对齐。数据独立扩展SPI的MOSI主机输出和MISO主机输入数据线则由我们通过软件直接控制GPIO电平来实现。我们在硬件SPI时钟的节拍下进行数据的移出和移入操作。这样做的好处是显而易见的我们获得了硬件SPI的时序精度和稳定性同时实现了多个独立SPI通道的扩展。CPU的负担远低于纯Bit-Banging因为精准的时钟节拍由硬件负责CPU只需要在正确的时刻“喂数据”和“读数据”即可。2.2 方案选型与权衡为什么选择这种混合方案而不是其他我们来对比一下方案优点缺点适用场景纯软件模拟 (Bit-Banging)实现最简单不依赖特定硬件引脚任意指定。CPU占用率高时序精度差受中断、任务调度影响通信速率很低通常1Mbps。极低速设备如RC522读卡器或硬件SPI完全不可用时的应急方案。硬件SPI多路复用器通信性能与硬件SPI一致稳定可靠。需要外部芯片如74HC4052等增加BOM成本和PCB面积需要额外的GPIO控制片选。对性能要求高且硬件成本不敏感的项目。本文的混合扩展方案时序精准依托硬件时钟软件开销适中无需外部硬件引脚配置灵活。实现复杂度较高需要深入理解SPI协议和MCU中断/DMA机制。软件逻辑若编写不当可能引入时序错误。最适用于需要扩展1-2个中低速SPI从设备且对时序有一定要求的场景。例如扩展一个SPI接口的传感器如BME280或ADC如ADS1256。注意这种方案扩展出的SPI其最大通信速率受限于两个因素一是你所“依附”的那个硬件SPI本身的时钟频率二是你的软件代码在中断服务程序或DMA回调中执行数据搬移操作的速度。通常它能达到依附的硬件SPI速率的一半或三分之一就已经是非常理想的结果了。3. 关键实现细节与驱动设计理论清晰了我们进入实战环节。我将以STM32CubeMX和HAL库为例展示如何一步步构建这个混合扩展SPI驱动。假设我们的主硬件SPI是SPI1需要扩展出一个“SPI_EX”扩展SPI来驱动一个传感器。3.1 硬件SPI的初始化与“时钟源”配置首先我们需要正确初始化作为“心脏”的硬件SPI1。这里有一个关键技巧我们可能不需要它完整地工作。// spi.c SPI_HandleTypeDef hspi1; void MX_SPI1_Init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; // 主机模式 hspi1.Init.Direction SPI_DIRECTION_2LINES; // 全双工但我们可能只关心时钟 hspi1.Init.DataSize SPI_DATASIZE_8BIT; // 数据大小根据实际情况定 hspi1.Init.CLKPolarity SPI_POLARITY_LOW; // 时钟极性CPOL0 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // 时钟相位CPHA0模式0 hspi1.Init.NSS SPI_NSS_SOFT; // 软件管理片选 hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_8; // 波特率预分频决定SCK频率 hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; // 高位先行 hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial 10; if (HAL_SPI_Init(hspi1) ! HAL_OK) { Error_Handler(); } }初始化后SPI1的SCK引脚如PA5就会输出对应频率的时钟信号。但此时如果我们不启动传输时钟是静止的。为了让时钟持续运行我们可以启动一个空的DMA传输或中断传输。例如我们可以设置一个循环发送缓冲区里面全是0xFF无效数据让SPI1不断地发送从而持续产生SCK时钟。这是我们方案中“偷”时钟的一种方法。更优雅的一种方法是利用SPI的传输事件来驱动我们的扩展逻辑。我们启动一次SPI1的传输比如发送一个字节然后在SPI1的“传输完成中断”或“DMA传输完成回调函数”中进行我们扩展SPI的数据操作并再次启动SPI1的传输形成链式反应。3.2 扩展SPI的GPIO与数据结构定义接下来定义我们扩展SPI所用的引脚。假设我们用PB0、PB1、PB2分别作为SPI_EX的SCK、MOSI、MISO。// spi_ex.h typedef struct { GPIO_TypeDef* sck_port; uint16_t sck_pin; GPIO_TypeDef* mosi_port; uint16_t mosi_pin; GPIO_TypeDef* miso_port; uint16_t miso_pin; GPIO_TypeDef* cs_port; // 片选引脚每个从设备独立 uint16_t cs_pin; SPI_HandleTypeDef* hw_spi; // 关联的硬件SPI句柄用于同步时钟 uint8_t mode; // SPI模式 (0,1,2,3) uint8_t data_size; // 数据位宽如8或16 } SPI_EX_HandleTypeDef; // 初始化一个扩展SPI实例 void SPI_EX_Init(SPI_EX_HandleTypeDef *hspi_ex); // 扩展SPI阻塞式传输函数 HAL_StatusTypeDef SPI_EX_TransmitReceive(SPI_EX_HandleTypeDef *hspi_ex, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size);在初始化函数SPI_EX_Init中我们需要将定义的GPIO配置为推挽输出SCK MOSI和输入上拉/下拉MISO。这里有一个至关重要的细节SCK引脚的初始化。在混合方案中扩展SPI的SCK引脚不应该由软件直接翻转而是作为“时钟输入监测”或干脆不用。真正的时钟同步依赖于软件逻辑与硬件SPI事件的同步。因此这个SCK GPIO可以初始化为输入模式用于在调试时测量波形或者就初始化为输出但保持固定电平不起实际作用。时钟同步的本质是代码执行与硬件SPI时钟节拍的同步而不是物理引脚信号的同步。3.3 核心传输函数的实现与硬件时钟同步这是整个驱动最核心、最精妙的部分。我们以实现阻塞式单字节收发为例讲解如何与硬件SPI1的时钟同步。假设我们采用“利用SPI1中断事件同步”的方式。我们首先写一个辅助函数它会在SPI1的传输事件中被调用。// spi_ex.c static uint8_t SPI_EX_CurrentBit 0; static uint8_t SPI_EX_TxByte 0; static uint8_t SPI_EX_RxByte 0; static SPI_EX_HandleTypeDef *Current_SPI_EX NULL; // 这个函数被SPI1的传输完成中断调用 void SPI_EX_ClockSync_Callback(void) { if(Current_SPI_EX NULL) return; // 判断当前是发送时钟的上升沿还是下降沿这取决于SPI1的模式(CPHA) // 假设我们使用模式0 (CPOL0, CPHA0): 数据在SCK上升沿采样下降沿变化。 // 那么在SPI1的“数据寄存器空”或“传输完成”中断时对应一个时钟边沿。 // 我们需要根据模式决定在哪个时刻设置MOSI和读取MISO。 // 简化模型我们约定在SPI1开始传输后在其每个“位时间”的中间点进行数据操作。 // 更可靠的做法是使用SPI的TXE和RXNE中断。 } // 更实际的做法基于SPI1的轮询或中断手动构建位循环 HAL_StatusTypeDef SPI_EX_TransmitReceiveByte(SPI_EX_HandleTypeDef *hspi_ex, uint8_t txByte, uint8_t *rxByte) { uint8_t rx 0; uint8_t tx txByte; Current_SPI_EX hspi_ex; // 1. 拉低片选如果需要 HAL_GPIO_WritePin(hspi_ex-cs_port, hspi_ex-cs_pin, GPIO_PIN_RESET); // 2. 根据SPI模式CPHA进行首次数据设置 if((hspi_ex-mode 0x01) 0) { // CPHA 0: 数据在第一个时钟边沿SCK从空闲状态第一次跳变被采样。 // 因此在时钟跳变前主机必须先准备好数据设置MOSI。 SPI_EX_SetMOSI((tx 0x80) ? 1 : 0); // 假设MSB先发 tx 1; } // 3. 启动一次硬件SPI1的传输发送一个无关字节以产生8个时钟脉冲 uint8_t dummy 0xFF; // 这里我们使用轮询方式等待SPI1发送完成。在等待期间我们需要“跟随时钟”处理数据。 // 但轮询无法知道精确的时钟边沿。因此更好的方法是利用SPI1的TXE/RXNE中断。 // 我们以中断方案为例重构思路 }由于在HAL库的阻塞式传输中我们无法插入精确的位操作因此中断驱动是混合方案更可行的选择。我们需要配置SPI1使其在每个字节传输期间产生中断通过使能SPI_IT_TXE或SPI_IT_RXNE然后在中断服务程序中进行扩展SPI的位操作。重构后的核心逻辑如下使能SPI1的TXE发送缓冲区空中断。当TXE中断触发意味着SPI1可以加载下一个要发送的数据了。此时硬件SPI的移位寄存器正在移出上一个数据位并产生一个SCK时钟边沿。在TXE中断服务程序里 a.读取扩展SPI的MISO引脚电平拼接到接收字节中。 b.根据要发送的数据设置扩展SPI的MOSI引脚电平。 c. 为了维持SPI1的时钟持续产生向SPI1的数据寄存器(DR)写入一个无关的字节如0xFF。重复步骤2-3共8次完成一个字节的收发。在SPI1的传输完成中断里进行收尾工作如拉高片选。实操心得这种中断同步方式对代码的执行时间有严格要求。中断服务程序ISR必须非常短小精悍执行时间要远小于一个SPI位的时间。如果ISR执行太慢可能会错过下一个时钟边沿导致数据错位。因此在ISR中只做最必要的位操作和寄存器读写避免复杂的计算或函数调用。对于高速SPI甚至需要考虑用汇编来优化关键部分。4. 实战代码剖析与移植要点为了让思路更清晰我提供一个简化但更直观的“延时同步”版本代码。这个版本不依赖SPI1的中断而是利用其精准的时钟频率通过计算延时来模拟位时序。它适用于中低速场景且对时序要求不是极端苛刻的情况。我们假设SPI1被配置为产生1MHz的SCK时钟周期为1us。// spi_ex.c (延时同步版) void SPI_EX_Delay(uint32_t us) { // 实现一个微秒级延时函数可以使用DWT周期计数器或SysTick。 // 这里假设有一个精准的Delay_us函数可用。 Delay_us(us); } HAL_StatusTypeDef SPI_EX_TransmitReceiveByte_Delay(SPI_EX_HandleTypeDef *hspi_ex, uint8_t txByte, uint8_t *rxByte) { uint8_t rx 0; uint8_t tx txByte; uint32_t half_bit_delay 0; // 半个位时间的延时 // 计算半个SCK时钟周期的延时单位us // 例如SPI1时钟为1MHz周期1us半周期0.5us。但软件延时精度有限可能需要调整。 half_bit_delay 0.5; // 理论上0.5us实际可能需要根据测试调整 // 拉低片选 HAL_GPIO_WritePin(hspi_ex-cs_port, hspi_ex-cs_pin, GPIO_PIN_RESET); for(int i 0; i 8; i) { // 设置MOSI (根据是否MSB先行) if(hspi_ex-first_bit SPI_FIRSTBIT_MSB) { HAL_GPIO_WritePin(hspi_ex-mosi_port, hspi_ex-mosi_pin, (tx 0x80) ? GPIO_PIN_SET : GPIO_PIN_RESET); tx 1; } else { HAL_GPIO_WritePin(hspi_ex-mosi_port, hspi_ex-mosi_pin, (tx 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); tx 1; } // 等待半个周期建立时间 SPI_EX_Delay(half_bit_delay); // 产生时钟上升沿/下降沿 (通过控制关联的硬件SPI不这里我们模拟) // 注意在这个版本里我们只是用延时来对齐时间并没有物理的SCK信号从扩展引脚输出。 // 真正的时钟同步是“时间概念”上的同步。我们需要在此时启动一次硬件SPI的传输吗 // 更好的方法是在这个时刻我们读取硬件SPI的状态或者利用一个由硬件SPI时钟触发的定时器。 // 这变得复杂了。因此“延时同步”版更接近于纯软件模拟只是延时参数参考了硬件SPI的时钟。 // 读取MISO if(HAL_GPIO_ReadPin(hspi_ex-miso_port, hspi_ex-miso_pin) GPIO_PIN_SET) { if(hspi_ex-first_bit SPI_FIRSTBIT_MSB) { rx | (0x80 i); } else { rx | (0x01 i); } } // 再等待半个周期保持时间 SPI_EX_Delay(half_bit_delay); // 模拟时钟的另一个边沿... } // 拉高片选 HAL_GPIO_WritePin(hspi_ex-cs_port, hspi_ex-cs_pin, GPIO_PIN_SET); if(rxByte ! NULL) { *rxByte rx; } return HAL_OK; }这段代码揭示了一个关键点纯粹的“延时同步”版本实际上已经退化为一种高精度定时模拟SPI它依赖于一个非常精准的微秒延时函数。它的稳定性受系统中断、其他任务的影响很大。因此它并不是标题所述方案的最佳实现。真正的“通过硬件SPI模块驱动”的精髓在于事件同步而非延时同步。我们需要一个硬件机制来告知软件“现在正好是时钟边沿的时刻”。这可以通过以下方式实现SPI中断法如前所述利用SPI的TXE/RXNE中断。定时器捕获法将硬件SPI的SCK引脚连接到一个定时器的输入捕获通道。在SCK的每个上升沿或下降沿产生捕获中断在中断里进行数据位操作。这种方法硬件连接稍复杂但软件同步非常直接。DMAPWM法配置一个定时器产生PWM波作为扩展SPI的SCK同时配置DMA将发送数据缓冲区自动搬运到GPIO的位设置寄存器如BSRR。接收则可以通过另一个定时器输入捕获或外部中断读取。这种方法性能最高几乎不占用CPU但实现也最复杂。5. 调试技巧与常见问题排查调试这种软硬结合的通信协议逻辑分析仪或者示波器几乎是必备的。光靠点灯打印很难定位时序上的细微错误。5.1 调试步骤与工具使用先验证硬件SPI本身单独测试硬件SPI1驱动一个简单的设备如SPI Flash的ID读取确保其配置模式、速率、相位绝对正确波形干净。可视化时序波形将硬件SPI1的SCK、MOSI、MISO以及你定义的扩展SPI的MOSI_EX、MISO_EX、CS_EX引脚都连接到逻辑分析仪。同时抓取这些信号。观察点1扩展SPI的MOSI_EX数据变化是否严格对齐硬件SPI1的SCK时钟边沿偏移有多大观察点2在CS_EX拉低期间硬件SPI1的SCK是否持续、等间隔地输出脉冲这验证了你的“时钟引擎”是否在正常工作。观察点3从设备返回的MISO_EX数据是否在正确的SCK边沿被采样你的软件读取MISO_EX的时机是否恰当使用调试器与变量观察在中断服务程序中设置断点观察发送和接收缓冲区的数据变化。但注意断点会严重破坏实时性可能导致通信失败所以只能用于检查初始状态和最终结果。5.2 常见问题速查表现象可能原因排查思路与解决方案扩展SPI完全无通信1. 片选CS引脚未正确控制。2. 扩展SPI的GPIO模式配置错误输出/输入。3. 硬件SPI未成功启动或时钟未产生。1. 用逻辑分析仪确认CS信号。2. 检查GPIO初始化代码确认MOSI为输出MISO为上拉/浮空输入。3. 测量硬件SPI的SCK引脚是否有波形。检查SPI初始化代码和启动代码。数据错位如0x55收成0xAA1. SPI模式CPOL/CPHA不匹配。2. 数据位顺序MSB/LSB不匹配。3. 软件读写数据的时机与时钟边沿未对齐。1. 核对从设备数据手册和代码中的SPI模式设置。2. 核对数据位顺序设置。3.这是最可能的原因。用逻辑分析仪放大看单个位的时序检查MOSI变化和MISO采样点相对于SCK边沿的位置。调整中断触发点或延时。通信不稳定时好时坏1. 中断服务程序执行时间过长错过时钟事件。2. 系统中有更高优先级中断打断了SPI或扩展逻辑。3. 电源噪声或信号完整性问题。1. 优化ISR代码移除任何非必要操作如打印。考虑使用DMA。2. 调整中断优先级确保SPI相关中断有足够高的优先级但不要是最高避免阻塞系统。3. 检查PCB布线SCK和MOSI/MISO走线是否过長是否有并联端接。在信号线上增加串联电阻如22Ω-100Ω。通信速度远低于预期1. 软件模拟部分开销太大。2. 使用的硬件SPI本身速率配置不高。3. 中断或任务调度引入额外延迟。1. 评估代码性能。对于“中断同步法”ISR本身的耗时必须小于一个SPI位时间。2. 尝试提高硬件SPI的波特率设置。3. 如果使用RTOS确保通信任务优先级足够高且关中断的临界区尽量短。只能发送不能接收或接收全为0/11. MISO引脚配置错误应为输入。2. 从设备未正确输出数据检查从设备电源、配置。3. 软件读取MISO的时机不对在读的时候引脚电平已变化。1. 确认MISO GPIO初始化为上拉/浮空输入模式。2. 用逻辑分析仪看MISO_EX引脚上是否有从设备发送的数据波形。3. 仔细分析SPI模式确认采样边沿。在读取MISO前确保时钟边沿已稳定建立。避坑技巧在项目初期可以故意降低硬件SPI的通信速率比如降到100kHz或更低。在这个低速下软件模拟部分的时序容错空间大更容易调试成功。等逻辑完全正确后再逐步提高速率观察通信的稳定性边界在哪里从而为项目留下足够的余量。6. 性能优化与高级应用探讨当基本功能实现后我们自然会考虑如何让它更快、更稳定、更省资源。6.1 提升通信速率的关键精简ISR接近极限对于“中断同步法”ISR里只保留最核心的位操作和寄存器访问。使用位带操作如果MCU支持或直接操作寄存器来替代HAL_GPIO_ReadPin/WritePin函数后者有函数调用开销。例如// 假设PB0是MOSI 使用位带操作快速置位/清零 #define MOSI_PIN_BITBAND (*(__IO uint32_t *)(0x42000000 (GPIOB_BASE 0x14 - 0x40000000)*32 0*4)) // 在ISR中 if(tx_data mask) { MOSI_PIN_BITBAND 1; // 置高比HAL_GPIO_WritePin快得多 } else { MOSI_PIN_BITBAND 0; // 置低 }转向DMA驱动的事件同步这是终极优化方案。思路是配置硬件SPI使用DMA进行数据传输发送和接收。配置一个与SPI时钟同步的定时器可以从SPI的SCK得到触发。使用这个定时器触发另一个DMA将你的发送数据缓冲区搬运到扩展MOSI的GPIO寄存器。同时配置另一个DMA在定时器的另一个相位将扩展MISO的GPIO寄存器状态搬运到接收缓冲区。这样整个扩展SPI的通信完全由DMA硬件完成CPU零干预可以达到接近硬件SPI的性能。但实现复杂度极高需要对STM32的DMA和定时器联动有很深的理解。6.2 扩展多个SPI从设备我们的架构天生支持扩展多个设备。只需要为每个扩展的SPI从设备定义独立的CS片选、MOSI、MISO引脚组即可。它们可以共享同一个硬件SPI作为时钟基准。在驱动层你需要管理多个SPI_EX_HandleTypeDef实例。当需要与某个设备通信时选中对应的实例操作其对应的GPIO引脚。关键在于同一时间只能有一个扩展设备被选中CS为低否则数据线会产生冲突。6.3 应对不同的SPI模式我们的示例代码主要围绕SPI模式0CPOL0 CPHA0。如果从设备需要模式1、2或3我们需要调整软件中数据设置和采样的时机。这需要在中断服务程序或位操作循环中根据hspi_ex-mode进行条件判断。例如对于CPHA1的模式数据是在第二个时钟边沿采样因此需要在第一个边沿之后才设置MOSI在第二个边沿之前读取MISO。这要求你的同步机制中断或捕获能区分第一个和第二个边沿。7. 总结与项目启示回过头看“STM32通过硬件SPI模块软件模拟驱动来进行拓展”这个项目远不止是几行GPIO控制代码。它是一个典型的资源受限环境下通过软硬件协同设计解决实际问题的案例。它考验的是开发者对底层协议SPI、硬件外设SPI、GPIO、中断、DMA以及系统实时性的综合把握能力。在实现过程中我最大的体会是理解时序比编写代码更重要。在动手写第一行驱动之前必须反复研读STM32的SPI外设手册和从设备的数据手册弄清楚每一个时钟边沿和数据变化的关系。逻辑分析仪是你的眼睛没有它调试这种时序相关的代码就像在黑暗中摸索。这个方案的成功实施能为老旧型号或引脚紧张的STM32项目带来新的生机。但它也不是银弹它的性能、稳定性和实现复杂度需要根据项目需求仔细权衡。对于超高速10MHz或对时序抖动极其敏感的应用还是应该优先选择硬件多路复用器或更换更多SPI外设的MCU型号。最后分享一个我常用的测试技巧在编写扩展SPI驱动时可以先不连接真实的从设备而是用杜邦线将扩展的MOSI和MISO短接起来实现“自发自收”的环回测试。这样能最快速地验证你的驱动基本逻辑和时序是否正确排除了从设备本身故障的干扰可以让你更专注于驱动代码本身的调试。