资讯中心

Arduino软件SPI实现:从协议原理到驱动开发实战

📅 2026/8/2 11:14:54
Arduino软件SPI实现:从协议原理到驱动开发实战
1. 项目概述为什么需要软件SPI如果你玩过Arduino大概率用过SPI。无论是驱动一块小小的OLED屏幕还是连接一个SD卡模块SPISerial Peripheral Interface都是绕不开的高速串行通信协议。硬件SPI用起来很爽Arduino的官方库SPI.h封装好了你只需要SPI.begin()然后调用SPI.transfer()就能收发数据速度快且稳定。但硬件SPI有个硬性限制它被“焊死”在特定的引脚上。以经典的Arduino Uno为例硬件SPI的引脚是固定的MOSI (D11), MISO (D12), SCK (D13)。片选引脚SS虽然理论上可以任选但通信的核心三线是锁死的。这就引出了一个非常实际的开发困境当你的项目需要连接多个SPI设备或者这些预设的SPI引脚已经被其他功能比如PWM、中断占用时该怎么办你可能会想到I2C但它速度慢且寻址复杂。此时“软件SPI”Software SPI或称Bit-Banging SPI就成为了一个极具价值的解决方案。它不依赖微控制器内置的硬件SPI外设而是通过程序控制任意一组普通的GPIO引脚模拟出SPI通信所需的时钟SCK、数据输入MISO、数据输出MOSI时序。简而言之就是用代码“造”出一个SPI。这篇指南的目的就是带你彻底搞懂Arduino平台上的软件SPI实现。我不会只给你一个库文件了事而是会拆解其底层原理手把手教你从零构建一个稳定可靠的软件SPI驱动并分享我在实际项目中趟过的坑和积累的经验。无论你是想驱动一个引脚冲突的传感器还是学习通信协议的本质这篇文章都能给你提供可直接“抄作业”的实战方案。2. 硬件SPI与软件SPI的核心差异与选型考量在动手写代码之前我们必须先厘清一个关键问题什么情况下该用软件SPI它和硬件SPI的根本区别是什么这决定了你的项目能否稳定运行。2.1 速度与CPU占用率最根本的权衡硬件SPI由微控制器内部的专用电路实现。当你调用SPI.transfer()时CPU只是配置一下寄存器然后数据就会由这个硬件外设自动完成移位、发送和接收整个过程几乎不占用CPU时间。CPU可以同时去处理其他任务如读取传感器、运行算法。因此硬件SPI可以达到很高的时钟频率在Uno上通常可达系统时钟的一半即8MHz且通信过程稳定时序精准。软件SPI则完全相反。每一个时钟脉冲的上升沿或下降沿每一条数据位的输出与采样都需要CPU执行若干条指令来拉高或拉低引脚电平。这意味着CPU必须全程“盯”着这次通信。在传输一个字节的8位数据期间CPU无法有效处理其他中断或任务。其直接后果是速度上限低受限于CPU执行digitalWrite、digitalRead以及循环判断的速度软件SPI的时钟频率SCK很难超过1MHz通常在几百KHz的水平。对于需要高速传输大量数据的设备如TFT屏幕、高速ADC软件SPI可能成为瓶颈。CPU占用率高传输期间CPU被阻塞。如果系统中有需要及时响应的中断如串口接收、按钮按下可能会因软件SPI的长时间阻塞而丢失事件。2.2 灵活性与资源消耗鱼与熊掌硬件SPI的优势是速度和解放CPU代价是引脚固定。软件SPI的优势是极致的灵活性你可以将SPI信号线分配到任何可用的数字引脚上甚至可以动态更换引脚。这在以下场景中是不可替代的引脚冲突项目中原有的D11、D12、D13已被用作其他用途。多个同类型SPI设备例如需要驱动多片相同的SPI Flash芯片你可以为每一片分配一组完全独立的引脚简化硬件片选逻辑。特殊电平转换如果需要通过电平转换芯片如5V转3.3V连接SPI设备你可以将转换芯片接入任意GPIO而不受硬件SPI引脚位置的限制。软件SPI的“资源消耗”主要是CPU时间和代码空间Flash。一段精简的软件SPI实现可能只增加几百字节的程序体积这对于动辄有32KB Flash的Uno来说微不足道。主要的成本还是CPU时间。2.3 何时选择软件SPI一个决策流程图根据我的经验你可以遵循以下思路做决策设备是否必须用SPI先查手册看设备是否支持I2C或UART。如果支持且速度满足要求优先考虑它们可能更简单。硬件SPI引脚是否可用如果可用且设备速度要求高1MHz或系统有实时性要求无条件选择硬件SPI。如果硬件SPI引脚被占用但设备对速度不敏感如低速温湿度传感器、小容量SPI EEPROM或者你迫切需要引脚分配的灵活性那么软件SPI就是你的最佳选择。注意有一种特殊情况是“硬件SPI软件片选”。即通信本身使用硬件SPID11 D12 D13但片选引脚SS使用其他GPIO手动控制。这是最常见且推荐的做法它兼顾了速度和设备选择的灵活性。本文讨论的“软件SPI”是指连SCK、MOSI、MISO都用软件模拟的完整方案。3. 深入SPI协议理解我们即将模拟的对象要实现软件SPI不能只知其然必须知其所以然。SPI协议有四个关键要素我们的代码将围绕它们展开。3.1 SPI的四种模式时钟极性与相位这是SPI最让人困惑也最容易出错的地方。SPI通信双方必须约定好时钟极性CPOL和时钟相位CPHA。CPOL (Clock Polarity)时钟空闲时的电平。CPOL0SCK空闲时为低电平。CPOL1SCK空闲时为高电平。CPHA (Clock Phase)数据在时钟的哪个边沿被采样。CPHA0数据在SCK的第一个边沿即从空闲状态跳变到第一个有效状态的边沿被采样。CPHA1数据在SCK的第二个边沿即第一个有效状态跳变回空闲状态的边沿被采样。这组合成了四种模式Mode 0-3Mode 0 (CPOL0 CPHA0)最常用的模式。SCK空闲低数据在SCK上升沿被采样下降沿切换。Mode 1 (CPOL0 CPHA1)SCK空闲低数据在SCK下降沿被采样上升沿切换。Mode 2 (CPOL1 CPHA0)SCK空闲高数据在SCK下降沿被采样上升沿切换。Mode 3 (CPOL1 CPHA1)SCK空闲高数据在SCK上升沿被采样下降沿切换。“采样”指从设备Slave读取主机Master发出的数据MOSI或主机读取从设备发出的数据MISO的时刻。“切换”指发送方准备好下一位数据的时刻。你的软件SPI代码和连接的设备必须处于同一模式通常从设备的芯片手册会明确写明支持的SPI模式。3.2 数据顺序MSB先行还是LSB先行SPI协议本身没有规定先传输字节的最高位MSB还是最低位LSB。这同样需要主从双方约定。大部分设备默认是MSB先行。我们的代码需要能够配置这一点。3.3 片选信号SS/CS的角色片选信号是低电平有效。当主机拉低某个从机的片选引脚时表示“我接下来要和你通话了”。在通信间隙片选应拉高。软件SPI中片选完全由你手动控制这比硬件SPI更直观。4. 从零构建一个可配置的Arduino软件SPI库理解了原理我们开始动手。我们将创建一个名为SoftSPI的类它应该易于使用且支持配置模式和数据顺序。4.1 类设计与初始化首先我们定义这个类并在构造函数中传入引脚编号和配置。// SoftSPI.h #ifndef SoftSPI_h #define SoftSPI_h #include Arduino.h class SoftSPI { public: // SPI模式枚举 enum SPIMode { MODE0 0x00, // CPOL0 CPHA0 MODE1 0x01, // CPOL0 CPHA1 MODE2 0x02, // CPOL1 CPHA0 MODE3 0x03 // CPOL1 CPHA1 }; // 数据顺序枚举 enum DataOrder { MSBFIRST 0, LSBFIRST 1 }; // 构造函数传入SCK MOSI MISO引脚 模式 数据顺序 SoftSPI(uint8_t sckPin, uint8_t mosiPin, uint8_t misoPin, SPIMode mode MODE0, DataOrder dataOrder MSBFIRST); // 初始化引脚模式 void begin(); // 核心数据传输函数 uint8_t transfer(uint8_t data); // 关闭目前为空为扩展预留 void end(); private: uint8_t _sckPin _mosiPin _misoPin; SPIMode _mode; DataOrder _dataOrder; bool _cpol; // 根据_mode推导出的时钟极性 bool _cpha; // 根据_mode推导出的时钟相位 // 内联函数用于快速引脚操作比digitalWrite快 inline void sckHigh() { digitalWrite(_sckPin HIGH); } inline void sckLow() { digitalWrite(_sckPin LOW); } inline void mosiHigh(){ digitalWrite(_mosiPin HIGH); } inline void mosiLow() { digitalWrite(_mosiPin LOW); } inline bool misoRead(){ return digitalRead(_misoPin); } }; #endif对应的实现文件// SoftSPI.cpp #include SoftSPI.h SoftSPI::SoftSPI(uint8_t sckPin uint8_t mosiPin uint8_t misoPin SPIMode mode DataOrder dataOrder) { _sckPin sckPin; _mosiPin mosiPin; _misoPin misoPin; _mode mode; _dataOrder dataOrder; // 从模式中解析出CPOL和CPHA _cpol (_mode MODE2 || _mode MODE3); // MODE2/3空闲高电平 _cpha (_mode MODE1 || _mode MODE3); // MODE1/3在第二个边沿采样 } void SoftSPI::begin() { pinMode(_sckPin OUTPUT); pinMode(_mosiPin OUTPUT); pinMode(_misoPin INPUT); // 设置时钟线初始状态空闲状态 digitalWrite(_sckPin _cpol ? HIGH : LOW); // 数据线初始状态可以任意通常设为低 digitalWrite(_mosiPin LOW); } void SoftSPI::end() { // 可以在这里将引脚模式重置但不是必须 } uint8_t SoftSPI::transfer(uint8_t data) { uint8_t received 0; // 根据数据顺序决定循环方向 if (_dataOrder MSBFIRST) { // 先发送最高位(bit7) for (int8_t i 7; i 0; --i) { // 1. 准备数据位在时钟有效边沿之前将数据放到MOSI上 if (data (1 i)) { mosiHigh(); } else { mosiLow(); } // 一个小小的延时确保数据稳定。这是软件SPI速度的关键限制点。 // 实际项目中这里可以用delayMicroseconds或更精确的_delay_usAVR专用。 // 为了代码清晰这里用空循环示意。优化部分见下文。 // asm volatile(nop); // 插入一个空指令约62.5ns 16MHz // 2. 处理第一个时钟边沿 if (_cpha 0) { // CPHA0在第一个边沿采样数据 // 对于CPOL0第一个边沿是上升沿对于CPOL1第一个边沿是下降沿 // 所以我们需要翻转时钟线 if (_cpol) { sckLow(); // 从高变低下降沿 } else { sckHigh(); // 从低变高上升沿 } // 采样MISO从设备发送的数据 received (received 1) | (misoRead() ? 1 : 0); // 将时钟线恢复到空闲状态产生第二个边沿 if (_cpol) { sckHigh(); } else { sckLow(); } } else { // CPHA1在第二个边沿采样数据 // 先产生第一个边沿不采样 if (_cpol) { sckLow(); } else { sckHigh(); } // 将时钟线恢复到空闲状态产生第二个边沿 if (_cpol) { sckHigh(); } else { sckLow(); } // 在第二个边沿采样 received (received 1) | (misoRead() ? 1 : 0); } } } else { // LSBFIRST // 先发送最低位(bit0) for (uint8_t i 0; i 8; i) { if (data (1 i)) { mosiHigh(); } else { mosiLow(); } // ... 时钟控制逻辑与MSBFIRST完全相同只是received的组装顺序不同 // 为了节省篇幅此处省略重复代码。实际实现中需要完整写出。 // 核心是received (received 1) | (misoRead() ? 0x80 : 0); } } return received; }以上代码框架清晰地展示了软件SPI的核心一个根据CPOL、CPHA和DataOrder精确控制时钟与数据引脚时序的循环。transfer函数同时完成了发送和接收。4.2 关键时序与延迟的微调代码中注释提到的“延时”是软件SPI的命门。digitalWrite和digitalRead函数本身就有几微秒的延迟这在16MHz的Arduino上已经足够产生可观的时钟周期。如果你直接使用上面的代码SCK频率可能只有几十KHz。提速实战技巧使用端口寄存器直接操作这是最有效的提速方法。Arduino的digitalWrite为了通用性牺牲了速度。我们可以直接操作AVR芯片的PORT、DDR、PIN寄存器。例如如果_sckPin是引脚5对应Arduino Uno的PD5我们可以这样定义#define SCK_PORT PORTD #define SCK_DDR DDRD #define SCK_BIT 5 inline void sckHigh() { SCK_PORT | (1 SCK_BIT); } inline void sckLow() { SCK_PORT ~(1 SCK_BIT); }这样一个引脚高低电平切换从数微秒缩短到一两个时钟周期约62.5ns。但代价是代码失去了引脚可动态配置的灵活性通常用于对速度有极致要求且引脚固定的场景。精细控制NOP指令asm volatile(nop);是一条不执行任何操作的单周期指令。在关键位置插入若干条nop可以微调高低电平的保持时间以满足某些SPI设备对t_SU数据建立时间和t_HD数据保持时间的苛刻要求。你需要用逻辑分析仪或示波器观察波形来调整。牺牲灵活性换取速度如果你的项目只使用一种SPI模式比如最常见的Mode 0你可以为这种模式写一个特化的、高度优化的transfer函数移除所有if判断展开循环从而获得最高速度。5. 实战应用驱动一个SPI Flash芯片W25Q64理论说得再多不如来一次实战。我们以常见的Winbond W25Q64 SPI Flash芯片为例用我们刚写的SoftSPI库来读写数据。5.1 硬件连接与初始化假设我们因为引脚冲突将软件SPI连接到Arduino Uno的引脚2 3 4。SCK - D2MOSI - D3MISO - D4CS片选 - D5 任意GPIOW25Q64支持SPI Mode 0和Mode 3我们选用Mode 0。#include SoftSPI.h // 定义软SPI引脚和模式 #define FLASH_SCK 2 #define FLASH_MOSI 3 #define FLASH_MISO 4 #define FLASH_CS 5 // 创建SoftSPI对象模式0 MSB先行 SoftSPI mySPI(FLASH_SCK FLASH_MOSI FLASH_MISO SoftSPI::MODE0); void setup() { Serial.begin(9600); pinMode(FLASH_CS OUTPUT); digitalWrite(FLASH_CS HIGH); // 初始时取消片选 mySPI.begin(); // 初始化软SPI引脚 // 唤醒Flash芯片有些深度睡眠后需要 digitalWrite(FLASH_CS LOW); mySPI.transfer(0xAB); // 发送Release Power-down指令 digitalWrite(FLASH_CS HIGH); delay(10); // 等待唤醒时间 // 读取芯片的JEDEC ID验证通信是否成功 digitalWrite(FLASH_CS LOW); mySPI.transfer(0x9F); // JEDEC ID指令 uint8_t manufID mySPI.transfer(0x00); uint8_t memType mySPI.transfer(0x00); uint8_t capacity mySPI.transfer(0x00); digitalWrite(FLASH_CS HIGH); Serial.print(Manufacturer ID: 0x); Serial.println(manufID HEX); // Winbond通常是0xEF Serial.print(Memory Type: 0x); Serial.println(memType HEX); Serial.print(Capacity: 0x); Serial.println(capacity HEX); // 0x40 表示64Mbit } void loop() { // 后续读写操作放在这里 }5.2 实现页编程与数据读取SPI Flash的写入需要先擦除扇区擦除、块擦除或整片擦除然后才能编程。我们实现一个向指定地址写入一页256字节数据并读回验证的函数。// 等待Flash芯片内部操作完成检查BUSY位 void waitForBusy() { digitalWrite(FLASH_CS LOW); mySPI.transfer(0x05); // 读状态寄存器1指令 uint8_t status; do { status mySPI.transfer(0x00); } while (status 0x01); // 检查BUSY位bit0 digitalWrite(FLASH_CS HIGH); } // 写使能 void writeEnable() { digitalWrite(FLASH_CS LOW); mySPI.transfer(0x06); digitalWrite(FLASH_CS HIGH); } // 擦除一个4KB扇区地址必须是4K对齐 void eraseSector(uint32_t addr) { writeEnable(); digitalWrite(FLASH_CS LOW); mySPI.transfer(0x20); // 扇区擦除指令 // 发送24位地址 mySPI.transfer((addr 16) 0xFF); mySPI.transfer((addr 8) 0xFF); mySPI.transfer(addr 0xFF); digitalWrite(FLASH_CS HIGH); waitForBusy(); // 等待擦除完成 } // 写入一页数据最多256字节 void writePage(uint32_t addr const uint8_t* data uint16_t len) { if (len 256) len 256; // 页边界限制 writeEnable(); digitalWrite(FLASH_CS LOW); mySPI.transfer(0x02); // 页编程指令 mySPI.transfer((addr 16) 0xFF); mySPI.transfer((addr 8) 0xFF); mySPI.transfer(addr 0xFF); for (uint16_t i 0; i len; i) { mySPI.transfer(data[i]); } digitalWrite(FLASH_CS HIGH); waitForBusy(); } // 从任意地址读取数据 void readData(uint32_t addr uint8_t* buffer uint16_t len) { digitalWrite(FLASH_CS LOW); mySPI.transfer(0x03); // 读数据指令 mySPI.transfer((addr 16) 0xFF); mySPI.transfer((addr 8) 0xFF); mySPI.transfer(addr 0xFF); for (uint16_t i 0; i len; i) { buffer[i] mySPI.transfer(0x00); } digitalWrite(FLASH_CS HIGH); }在loop函数中调用测试void loop() { uint32_t testAddr 0x000000; // 测试地址确保该扇区已擦除或为空白 uint8_t writeBuffer[256]; uint8_t readBuffer[256]; // 准备测试数据 for (int i 0; i 256; i) { writeBuffer[i] i; // 写入0-255 } Serial.println(Erasing sector...); eraseSector(testAddr); Serial.println(Writing page...); writePage(testAddr writeBuffer 256); Serial.println(Reading back...); readData(testAddr readBuffer 256); // 验证数据 bool success true; for (int i 0; i 256; i) { if (readBuffer[i] ! writeBuffer[i]) { Serial.print(Mismatch at ); Serial.print(i); Serial.print(: Write ); Serial.print(writeBuffer[i]); Serial.print( Read ); Serial.println(readBuffer[i]); success false; break; } } if (success) { Serial.println(SPI Flash R/W Test PASSED!); } else { Serial.println(SPI Flash R/W Test FAILED!); } delay(5000); // 每5秒测试一次 }5.3 实测中的波形分析与问题定位当你把代码烧录进去打开串口监视器可能一切顺利也可能收到一堆乱码或者全0。此时光看代码逻辑是不够的。必备的调试工具逻辑分析仪。一个几十块钱的8通道逻辑分析仪配合Sigrok/PulseView软件是调试任何数字通信协议的利器。用它抓取SCK、MOSI、MISO、CS四根线上的波形。你需要重点检查时序模式对照芯片手册的时序图看你的SCK空闲电平CPOL、数据采样边沿CPHA是否正确。这是最常见的错误来源。片选时序CS是否在通信前拉低通信后拉高CS拉低后是否给了芯片足够的准备时间t_CS才发送第一个时钟指令与数据发送的指令码如0x03、0x02是否正确地址字节的顺序通常是MSB先行是否正确时钟频率软件SPI的时钟周期是否稳定是否过快超过了芯片在特定电压下的最高频率f_{CLK}可以尝试在transfer循环中增加delayMicroseconds(1)来降低频率测试。我在第一次用软件SPI驱动一个SPI温度传感器时就曾因为忽略了芯片要求CPHA1Mode 1而卡了半天。逻辑分析仪一眼就看出数据采样的边沿不对修改SoftSPI的模式参数后立刻成功。6. 进阶话题性能优化与多设备管理当你的项目复杂度上升可能会遇到两个新需求如何让软件SPI更快如何优雅地管理多个软件SPI设备6.1 极致性能优化直接端口操作与循环展开对于速度敏感的应用我们必须抛弃通用的digitalWrite。下面展示一个为Arduino UnoAVR ATmega328P引脚D2 D3 D4优化的Mode 0专用transfer函数。我们假设SCK (D2) 对应 PD2 (Port D Bit 2)MOSI (D3) 对应 PD3 (Port D Bit 3)MISO (D4) 对应 PD4 (Port D Bit 4)uint8_t transfer_SPI_Mode0_Fast(uint8_t data) { uint8_t received 0; // 预计算端口掩码避免在循环中计算 const uint8_t SCK_HIGH (1 PD2); const uint8_t SCK_LOW 0; const uint8_t MOSI_HIGH (1 PD3); const uint8_t MOSI_LOW 0; const uint8_t MISO_MASK (1 PD4); // 循环展开手动处理8个位移除所有条件判断 // 位7 if (data 0x80) PORTD | MOSI_HIGH; else PORTD ~MOSI_HIGH; asm volatile(nop\nnop); // 微小延时保证数据建立时间 PORTD | SCK_HIGH; // 产生上升沿 (CPOL0 CPHA0 第一个边沿采样) // 在时钟高电平期间采样MISO received (PINB MISO_MASK) ? 1 : 0; PORTD ~SCK_HIGH; // 恢复低电平产生下降沿 // 位6 received 1; if (data 0x40) PORTD | MOSI_HIGH; else PORTD ~MOSI_HIGH; asm volatile(nop\nnop); PORTD | SCK_HIGH; received | (PINB MISO_MASK) ? 1 : 0; PORTD ~SCK_HIGH; // ... 重复处理位5到位0 ... return received; }这种写法极其高效但完全丧失了灵活性和可读性且与具体硬件绑定。它适用于最终产品中对性能有严苛要求的固定功能模块。6.2 管理多个软件SPI设备面向对象与资源池一个项目里可能有多个SPI设备有的用硬件SPI有的用软件SPI且模式、速度可能不同。一个好的设计模式是创建SPIDevice基类或接口然后派生出HardwareSPIDevice和SoftSPIDevice。class SPIDevice { public: virtual void begin() 0; virtual uint8_t transfer(uint8_t data) 0; virtual void select() 0; // 拉低CS virtual void deselect() 0; // 拉高CS // ... 其他通用SPI操作如transfer16 transferBuffer等 }; class SoftSPIDevice : public SPIDevice { private: SoftSPI* _spi; uint8_t _csPin; public: SoftSPIDevice(SoftSPI* spi uint8_t csPin) : _spi(spi) _csPin(csPin) {} void begin() override { pinMode(_csPin OUTPUT); digitalWrite(_csPin HIGH); _spi-begin(); } uint8_t transfer(uint8_t data) override { return _spi-transfer(data); } void select() override { digitalWrite(_csPin LOW); } void deselect() override { digitalWrite(_csPin HIGH); } }; // 使用示例 SoftSPI spiBus1(2 3 4 SoftSPI::MODE0); // 第一个软SPI总线 SoftSPI spiBus2(6 7 8 SoftSPI::MODE1); // 第二个软SPI总线模式不同 SoftSPIDevice flashChip(spiBus1 5); // 使用总线1 CS引脚D5 SoftSPIDevice sensor(spiBus2 9); // 使用总线2 CS引脚D9 void setup() { flashChip.begin(); sensor.begin(); // 操作设备 flashChip.select(); flashChip.transfer(0x9F); // 读ID // ... 接收数据 flashChip.deselect(); }这样每个设备对象封装了自己的SPI总线和片选引脚管理起来清晰明了也方便未来扩展。7. 常见陷阱与避坑指南在软件SPI的实践中有些坑一踩一个准。这里我总结几个最典型的忽略上拉电阻对于MISO线如果从设备是开漏输出或者在某些高阻态下务必在Arduino的MISO引脚上启用内部上拉电阻pinMode(misoPin INPUT_PULLUP)或连接一个外部上拉电阻如4.7kΩ到VCC否则读取到的可能是浮空电平导致数据错误。电平不匹配确保主从设备的工作电压兼容。5V的Arduino连接3.3V的SPI设备时必须进行电平转换例如使用TXB0104等双向电平转换芯片否则可能损坏3.3V设备。SCK和MOSI需要从5V降到3.3VMISO从3.3V到5V一般可以直接连接因为3.3V的高电平2.5V能被5V系统识别为高。中断干扰软件SPI的transfer函数执行期间是禁止中断的noInterrupts()这取决于你的实现。如果你的transfer循环很长且系统中有定时器中断、串口接收中断等它们可能会打断transfer导致时钟波形畸形。一个简单的解决方案是在关键的transfer函数前后加上noInterrupts()和interrupts()。但要注意这会影响系统的实时性。更好的办法是优化transfer函数使其执行时间尽可能短。共享MISO线的冲突当多个SPI设备共享同一个MISO线时不推荐但有时为了节省引脚会这么做必须确保在任何时刻只有一个设备的片选是有效的低电平。其他设备的片选必须为高使其输出处于高阻态。软件SPI需要你严格管理片选时序。逻辑分析仪是你的朋友再次强调当通信不正常时不要盲目猜测。用逻辑分析仪抓取波形与芯片数据手册的时序图逐项对比是最高效的调试方法。