
SPI协议是单片机开发中最常用的外设通讯方式之一但很多开发者只是“能用”却未必“会用”。这篇文章不讲空洞的理论直接聚焦于如何在实际项目中正确、高效、稳定地使用SPI与各类外设芯片通讯。无论是驱动一块LCD屏幕、读取一个传感器还是与高速ADC/DAC通信理解SPI的正确“姿势”都能让你事半功倍避免时序错乱、数据丢失、总线冲突这些头疼的问题。我们将从SPI最核心的硬件连接和四种工作模式讲起然后深入到实际编程中的软件模拟与硬件SPI外设使用最后通过驱动常见外设如Flash、OLED屏的完整代码示例展示如何构建健壮的通讯程序。本文适合所有使用STM32、ESP32、51单片机等平台需要与SPI外设打交道的嵌入式开发者和学生。读完你将能清晰规划SPI引脚、正确配置模式、编写可靠驱动并具备排查常见SPI通讯故障的能力。1. 核心能力速览SPISerial Peripheral Interface是一种高速、全双工、同步的串行通信总线协议。它的核心价值在于简单、高效广泛应用于单片机与存储器、传感器、显示屏等外设的短距离通讯。能力项说明与特点通讯性质全双工、同步串行通信。主机和从机可同时收发数据。硬件需求至少4线SCK, MOSI, MISO, CS支持一主多从。对单片机硬件资源要求低也可软件模拟。速度范围速度可配置通常从几百Kbps到几十Mbps远高于I2C和UART。主要优势协议简单无寻址开销传输效率高编程实现直接。典型应用场景驱动Flash存储器W25Qxx、OLED显示屏SSD1306/SSD1351、ADC/DAC芯片AD7705、以太网控制器、SD卡、触摸屏控制器等。关键配置点时钟极性(CPOL)、时钟相位(CPHA)、数据位顺序(MSB/LSB First)、时钟频率。这四点配置错误是通讯失败的主因。硬件/软件实现优先使用单片机硬件SPI外设效率高且不占用CPU。若无硬件SPI或引脚冲突可用GPIO软件模拟Bit-Banging。2. 适用场景与使用边界SPI协议并非万能理解其适用边界是正确选型的第一步。SPI最适合的场景高速数据流传输如读写SPI Flash、传输图像数据到屏幕、高速ADC采样。其全双工特性在此类场景下优势明显。点对点或一主多从一个主机控制多个从设备每个从设备需要独立的片选线CS。适合板载多个同类型外设如多个传感器。对协议开销敏感SPI没有像I2C那样的地址字节和ACK/NACK位也没有UART的起始/停止位纯数据流传输效率极高。简单控制外设许多外设如数字电位器、IO扩展芯片寄存器读写操作简单SPI的“发命令-读响应”模式非常匹配。SPI不推荐或需谨慎使用的场景远距离通信SPI通常用于PCB板内或板间短距离厘米级通信抗干扰能力弱不适合长线缆。超多节点网络SPI采用片选线寻址每增加一个从机就需要一根额外的GPIO作为CS线。当从机数量很多时会大量占用主机的GPIO资源。热插拔支持SPI总线在通讯过程中CS线需要保持稳定。不支持像USB那样在系统运行时随意插拔设备。多主机系统标准SPI是单主机协议构建多主机系统需要复杂的仲裁逻辑通常不如使用I2C或CAN总线方便。安全与可靠性边界电平匹配确保主机与从机电平一致如均为3.3V或5V否则需使用电平转换芯片防止损坏设备。时序容限在接近最高时钟频率下工作时需考虑信号完整性、走线长度和负载否则可能出现数据错误。软件模拟的实时性在高频下软件模拟SPI会消耗大量CPU资源并可能因中断干扰导致时序错乱关键高速通讯务必使用硬件SPI。3. 环境准备与前置条件在开始编写SPI驱动代码前需要做好以下软硬件准备。硬件准备主控制器单片机如STM32F1/F4系列、ESP32、GD32、ATmega328PArduino等。确认其是否具备硬件SPI外设以及对应的引脚。从设备外设如W25Q128 SPI Flash、SSD1306 OLED屏、AD7705 ADC芯片等。准备好其数据手册。连接线杜邦线或PCB走线。确保连接牢固。电源为MCU和外设提供稳定且电压匹配的电源。示波器或逻辑分析仪可选但强烈推荐用于抓取SPI波形是调试时序问题无可替代的工具。软件准备开发环境如Keil MDK、IAR Embedded Workbench、STM32CubeIDE、Arduino IDE、ESP-IDF等。单片机固件库或HAL库如STM32 Standard Peripheral Library、STM32Cube HAL、ESP-IDF SPI Master Driver、Arduino SPI库等。外设芯片的驱动程序或数据手册至少要有数据手册了解其寄存器映射、命令集和SPI时序要求。核心检查清单[ ] 确认主从设备供电电压一致。[ ] 根据数据手册确认外设支持的SPI模式CPOL, CPHA。[ ] 规划好单片机上的SPI引脚SCK, MOSI, MISO和片选引脚CS。注意硬件SPI引脚通常是固定的。[ ] 准备好一个简单的测试程序例如读取外设的ID寄存器作为通讯成功的标志。4. SPI硬件连接与四种模式详解这是SPI的基石配置错误直接导致通讯失败。标准四线连接SCK (Serial Clock)时钟信号由主机产生同步数据位。MOSI (Master Out Slave In)主机输出从机输入数据线。MISO (Master In Slave Out)主机输入从机输出数据线。CS/SS (Chip Select / Slave Select)片选信号低电平有效。主机通过拉低对应从机的CS线来选中它。一主多从连接所有从机的SCK、MOSI、MISO分别并联到主机的对应引脚。每个从机需要独占主机的一个GPIO作为其CS线。主机通过控制不同的CS线来选择与哪个从机通信。四种工作模式由CPOL和CPHA决定模式配置是SPI最难理解也最关键的部分必须与外设芯片数据手册要求完全一致。模式CPOL (时钟极性)CPHA (时钟相位)时钟空闲状态数据采样时刻数据变化时刻Mode 000低电平在SCK的第一个边沿上升沿在SCK的第二个边沿下降沿Mode 101低电平在SCK的第二个边沿下降沿在SCK的第一个边沿上升沿Mode 210高电平在SCK的第一个边沿下降沿在SCK的第二个边沿上升沿Mode 311高电平在SCK的第二个边沿上升沿在SCK的第一个边沿下降沿如何确定外设的模式查阅外设数据手册的“SPI Interface”或“Serial Communication Timing”章节通常会给出类似下图或明确写明“CPOL0, CPHA0”的文字。常见外设模式参考W25Q Flash通常用Mode 0或Mode 3很多OLED屏如SSD1306用Mode 0AD7705用Mode 3。5. 软件模拟SPIBit-Banging实现当硬件SPI引脚被占用或单片机没有硬件SPI时可以用GPIO模拟。虽然速度慢但灵活且有助于理解SPI时序本质。以下是一个针对Mode 0的软件SPI主设备发送一个字节的示例代码C语言/** * brief 软件SPI引脚定义 (需根据实际连接修改) */ #define SOFT_SPI_SCK_PIN GPIO_Pin_13 #define SOFT_SPI_MOSI_PIN GPIO_Pin_15 #define SOFT_SPI_PORT GPIOB #define SOFT_SPI_SCK_LOW() GPIO_ResetBits(SOFT_SPI_PORT, SOFT_SPI_SCK_PIN) #define SOFT_SPI_SCK_HIGH() GPIO_SetBits(SOFT_SPI_PORT, SOFT_SPI_SCK_PIN) #define SOFT_SPI_MOSI_LOW() GPIO_ResetBits(SOFT_SPI_PORT, SOFT_SPI_MOSI_PIN) #define SOFT_SPI_MOSI_HIGH() GPIO_SetBits(SOFT_SPI_PORT, SOFT_SPI_MOSI_PIN) /** * brief 软件SPI发送一个字节 (Mode 0) * param data: 要发送的数据 */ void Soft_SPI_WriteByte(uint8_t data) { uint8_t i; // Mode 0: CPOL0, CPHA0. 空闲时SCK为低电平。 SOFT_SPI_SCK_LOW(); // 确保起始状态为空闲状态 for (i 0; i 8; i) { // 在时钟第一个边沿上升沿之前准备好数据MOSI变化 if (data 0x80) { // 判断最高位(MSB First) SOFT_SPI_MOSI_HIGH(); } else { SOFT_SPI_MOSI_LOW(); } data 1; // 左移准备下一位 // 产生时钟上升沿 (从机在上升沿采样) SOFT_SPI_SCK_HIGH(); // 此处可插入短暂延时控制SPI速度 // Delay_us(1); // 产生时钟下降沿 (主机在下降沿变化数据为下一位准备) SOFT_SPI_SCK_LOW(); // Delay_us(1); } // 循环结束后SCK保持在低电平空闲状态 }软件SPI关键点严格时序SOFT_SPI_SCK_HIGH()和SOFT_SPI_SCK_LOW()之间的延时决定了SCK频率。延时越短SPI时钟越快但需在GPIO翻转速度范围内。模式适配上述代码是Mode 0。若要改为Mode 1只需将数据变化和时钟边沿的顺序对调。读取MISO如果需要同时接收数据全双工需要在SOFT_SPI_SCK_HIGH()之后、SOFT_SPI_SCK_LOW()之前读取MISO引脚的电平并组合成接收到的字节。片选控制在调用Soft_SPI_WriteByte前需拉低对应外设的CS引脚发送完成后再拉高CS。6. 硬件SPI外设驱动实战以STM32 HAL库为例使用硬件SPI效率高CPU负担小。以下是使用STM32CubeMX配置及HAL库驱动SPI的典型流程。步骤1使用STM32CubeMX配置SPI在Pinout Configuration标签页找到SPI1或其他SPI接口。将模式设置为Full-Duplex Master。配置参数Prescaler (时钟预分频)决定SPI时钟SCK的频率。公式SCK频率 APB总线频率 / Prescaler。需小于等于外设支持的最高频率。CPOL 和 CPHA根据外设手册选择模式。Data Size通常为8 bits。First Bit选择MSB First大多数外设或LSB First。NSS Signal Type选择Software软件控制。我们将用普通GPIO手动控制CS引脚这样更灵活。生成代码。步骤2编写SPI读写函数生成的代码会初始化SPI硬件。我们需要编写应用层的读写函数。// spi.c #include spi.h #include main.h // 包含生成的gpio定义 extern SPI_HandleTypeDef hspi1; // CubeMX生成的SPI句柄 /** * brief 通过SPI发送并接收一个字节全双工 * param tx_data: 要发送的数据 * retval 接收到的数据 */ uint8_t SPI1_ReadWriteByte(uint8_t tx_data) { uint8_t rx_data; HAL_SPI_TransmitReceive(hspi1, tx_data, rx_data, 1, HAL_MAX_DELAY); return rx_data; } /** * brief 向SPI设备写入一个字节半双工只发不收 * param tx_data: 要发送的数据 */ void SPI1_WriteByte(uint8_t tx_data) { HAL_SPI_Transmit(hspi1, tx_data, 1, HAL_MAX_DELAY); } /** * brief 从SPI设备读取一个字节半双工只收不发 * note 通常需要发送一个哑元数据Dummy Byte如0xFF来产生时钟 * retval 接收到的数据 */ uint8_t SPI1_ReadByte(void) { uint8_t tx_data 0xFF; // Dummy byte uint8_t rx_data; HAL_SPI_TransmitReceive(hspi1, tx_data, rx_data, 1, HAL_MAX_DELAY); return rx_data; }步骤3控制片选(CS)引脚在读写数据前后需要手动控制CS引脚。// 假设CS引脚连接在PB12上已在CubeMX中配置为GPIO_Output #define SPI_FLASH_CS_PIN GPIO_PIN_12 #define SPI_FLASH_CS_PORT GPIOB #define SPI_FLASH_CS_LOW() HAL_GPIO_WritePin(SPI_FLASH_CS_PORT, SPI_FLASH_CS_PIN, GPIO_PIN_RESET) #define SPI_FLASH_CS_HIGH() HAL_GPIO_WritePin(SPI_FLASH_CS_PORT, SPI_FLASH_CS_PIN, GPIO_PIN_SET) /** * brief 读取SPI Flash的制造商和设备ID * param manufacturer_id: 返回的制造商ID * param device_id: 返回的设备ID */ void W25Q_ReadID(uint8_t *manufacturer_id, uint8_t *device_id) { uint8_t cmd 0x90; // W25Q的读ID命令 SPI_FLASH_CS_LOW(); // 选中芯片 SPI1_WriteByte(cmd); // 发送命令 SPI1_WriteByte(0x00); // 发送3个地址字节对于读ID命令通常为0x00 SPI1_WriteByte(0x00); SPI1_WriteByte(0x00); *manufacturer_id SPI1_ReadByte(); // 读取制造商ID *device_id SPI1_ReadByte(); // 读取设备ID SPI_FLASH_CS_HIGH(); // 取消选中 }7. 实战案例驱动SPI Flash (W25Q128)以华邦W25Q128为例演示完整的SPI设备驱动编写流程。1. 初始化与ID读取// w25qxx.h #define W25Q_ID_MANUFACTURER 0xEF // 华邦 #define W25Q_ID_DEVICE_W25Q128 0x4018 // 设备ID // w25qxx.c uint8_t W25Q_Init(void) { uint8_t manufacturer, device_id_high, device_id_low; W25Q_ReadID(manufacturer, device_id_high, device_id_low); uint16_t device_id (device_id_high 8) | device_id_low; if (manufacturer W25Q_ID_MANUFACTURER device_id W25Q_ID_DEVICE_W25Q128) { printf(W25Q128 Init OK.\r\n); return 0; // 成功 } else { printf(W25Q Init Failed. ID: 0x%02X%04X\r\n, manufacturer, device_id); return 1; // 失败 } }2. 实现页编程写入SPI Flash写入前必须先擦除Erase然后以页Page通常256/512字节为单位编程。// 等待Flash空闲检查BUSY位 void W25Q_WaitForBusy(void) { uint8_t status; do { SPI_FLASH_CS_LOW(); SPI1_WriteByte(0x05); // 读状态寄存器1命令 status SPI1_ReadByte(); SPI_FLASH_CS_HIGH(); } while (status 0x01); // 检查BUSY位bit0 } // 写使能 void W25Q_WriteEnable(void) { SPI_FLASH_CS_LOW(); SPI1_WriteByte(0x06); // 写使能命令 SPI_FLASH_CS_HIGH(); } // 页编程写入一页数据不超过256字节 void W25Q_PageProgram(uint32_t addr, uint8_t *data, uint16_t len) { W25Q_WriteEnable(); // 1. 写使能 SPI_FLASH_CS_LOW(); SPI1_WriteByte(0x02); // 页编程命令 SPI1_WriteByte((addr 16) 0xFF); // 发送24位地址 SPI1_WriteByte((addr 8) 0xFF); SPI1_WriteByte(addr 0xFF); for (uint16_t i 0; i len; i) { SPI1_WriteByte(data[i]); // 发送数据 } SPI_FLASH_CS_HIGH(); W25Q_WaitForBusy(); // 等待写入完成 }3. 实现数据读取// 读取数据 void W25Q_ReadData(uint32_t addr, uint8_t *buffer, uint32_t len) { SPI_FLASH_CS_LOW(); SPI1_WriteByte(0x03); // 读数据命令 SPI1_WriteByte((addr 16) 0xFF); SPI1_WriteByte((addr 8) 0xFF); SPI1_WriteByte(addr 0xFF); for (uint32_t i 0; i len; i) { buffer[i] SPI1_ReadByte(); // 连续读取 } SPI_FLASH_CS_HIGH(); }4. 主函数测试int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); printf(System Init.\r\n); if (W25Q_Init() 0) { uint8_t write_buf[10] {0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x53, 0x50, 0x49, 0x21}; // Hello SPI! uint8_t read_buf[10] {0}; // 先擦除一个扇区4KB然后写入 // W25Q_SectorErase(0x000000); // 擦除函数需另外实现 W25Q_PageProgram(0x000000, write_buf, 10); W25Q_ReadData(0x000000, read_buf, 10); printf(Read Data: ); for (int i 0; i 10; i) { printf(%c, read_buf[i]); } printf(\r\n); } while (1) { // 主循环 } }8. 资源占用与性能观察硬件SPI vs 软件SPICPU占用硬件SPI由外设自动完成数据移位CPU仅需操作数据寄存器占用极低。软件SPI需要CPU循环控制GPIO在高速通讯时占用率可达100%。最高速率硬件SPI速率取决于APB总线时钟和分频器可达数十MHz。软件SPI速率受限于GPIO翻转速度和软件循环开销通常在1MHz以下。时序精度硬件SPI由硬件保证精度高。软件SPI易受中断干扰在高频下时序可能抖动。如何观察与优化SPI性能使用示波器/逻辑分析仪这是最直接的方法。测量SCK频率是否与配置一致观察MOSI/MISO数据在正确的时钟边沿是否稳定检查CS信号在数据帧前后是否正确。调整时钟分频如果通讯不稳定特别是长线连接首先尝试降低SPI时钟频率。优化软件SPI延时软件SPI中Delay_us(1)的精度和实际耗时是关键。可以使用定时器或NOP指令进行更精确的延时。使用DMA如果支持对于大批量数据传输如图像刷新启用SPI的DMA功能可以极大解放CPU。配置DMA通道为SPI的发送和接收服务。注意GPIO速度对于硬件SPI需将相关引脚配置为高速模式如GPIO_Speed_50MHz。9. 常见问题与排查方法SPI通讯失败绝大多数问题出在硬件连接、模式配置和时序上。问题现象可能原因排查方式解决方案完全无数据1. 电源未接通或电压不对。2. CS引脚未正确拉低。3. 主从设备引脚接反MOSI对MOSIMISO对MISO。4. 外设芯片未初始化或处于睡眠模式。1. 用万用表测量电源和地。2. 用示波器或逻辑分析仪抓取CS和SCK信号。3. 核对原理图和接线。1. 确保供电正确。2. 检查代码中CS引脚控制逻辑。3. 交换MOSI/MISO线测试。4. 发送外设的唤醒命令如果有。能发送但接收数据全为0或0xFF1. MISO线连接错误或断路。2. 从设备未正确响应可能模式不对。3. 读取时序错误在错误的时间点采样MISO。1. 测量MISO线电平是否变化。2.重点检查CPOL和CPHA模式是否与外设一致。3. 检查代码中读取数据的指令位置。1. 修复MISO线路。2.逐一尝试SPI四种模式这是最高效的方法。3. 调整软件SPI中读取MISO的代码位置。数据错位或字节错位1. 数据位顺序MSB/LSB设置错误。2. 时钟频率过高从设备跟不上。3. 在帧与帧之间CS信号被意外拉高。1. 用逻辑分析仪解码SPI数据看单个字节内的位顺序。2. 降低SPI时钟分频。1. 修改SPI配置的First Bit设置。2. 降低通讯速率。3. 确保在一次完整的通讯序列中CS保持低电平。偶尔数据错误1. 电源噪声或地线干扰。2. 走线过长信号质量差。3. 软件SPI被高优先级中断打断。1. 观察电源纹波。2. 缩短连接线或使用双绞线。3. 在关键SPI通讯段禁用全局中断。1. 增加电源滤波电容。2. 优化PCB布局SPI线尽量短且远离噪声源。3. 软件SPI通讯时关中断或改用硬件SPI。硬件SPI初始化失败1. 时钟未使能。2. 引脚复用功能未正确映射。3. 与其他外设如I2S冲突。1. 检查__HAL_RCC_SPI1_CLK_ENABLE()是否调用。2. 检查CubeMX中引脚配置。1. 确保SPI和GPIO的时钟已使能。2. 核对数据手册的引脚复用表。万能调试法逻辑分析仪抓包连接逻辑分析仪的通道到SCK、MOSI、MISO、CS。设置正确的协议解码器SPI并配置好模式CPOL, CPHA。触发一次通讯观察解码出的数据是否与你发送/期望接收的一致。这是定位SPI问题最强大的工具。10. 最佳实践与使用建议模式匹配是第一要务拿到新外设芯片第一件事就是查数据手册确认其SPI模式CPOL/CPHA和数据位顺序。可以写一个简单的测试程序循环尝试4种模式去读芯片ID快速确定正确模式。片选(CS)管理要严谨确保在发送命令、地址、数据的整个序列中CS保持有效通常为低电平。帧与帧之间CS应有足够的高电平时间满足芯片的tCSH要求。先低速再高速调试阶段先将SPI时钟频率设到最低如100KHz确保基本通讯成功。然后再逐步提高频率测试稳定性。善用DMA和中断对于流式数据传输如刷屏、读写大文件务必使用DMA。对于需要及时响应的数据接收可以使用SPI接收中断。为软件SPI预留退路即使计划使用硬件SPI也可以在代码中保留一套经过测试的软件SPI函数。当硬件SPI引脚被其他功能占用时可以快速切换到任意GPIO进行模拟。编写设备驱动层将针对特定外设如W25Q、OLED的SPI操作封装成独立的.c/.h文件提供Init,Read,Write,Erase等接口。这提高代码复用性和可读性。注意电源和地线SPI通讯尤其是高速通讯对电源质量敏感。确保主从设备共地良好并在电源引脚附近放置去耦电容。掌握SPI协议的正确使用姿势意味着你能驾驭单片机世界里一大半的常见外设。核心在于硬件连接正确、模式配置匹配、时序控制严谨。从用逻辑分析仪验证第一个正确的SPI波形开始到成功驱动Flash、屏幕、传感器每一步的排错经验都会让你的嵌入式开发能力更加扎实。建议将本文中的代码框架和排查表格保存下来在下次遇到SPI问题时它们能帮你快速定位到症结所在。