STM32 SPI协议详解从4根线到W25Q64实战上一篇我们讲了 I2C 是用2根线挂一堆低速设备这篇讲 SPI — 用4根线跑高速。如果说 I2C 是乡镇小巴SPI 就是高速公路无需寻址挂挡就飙。STM32 中最常用的外设Flash芯片 W25Q64/128、0.96寸OLED、2.4G无线 NRF24L01、SD卡、TFT屏全是 SPI。1. SPI 是什么为什么 STM32 必须学SPI (Serial Peripheral Interface) 摩托罗拉发明特点同步、全双工、高速一主多从主控一切没有寻址没有应答靠一根CS片选线选中谁I2C vs SPI 终极对比特性I2CSPI线数2根4根 (SCK/MOSI/MISO/CS) 每多一个从机多一根CS速度100k ~ 400k (慢)STM32可跑 18M~36M甚至72M/236MHz (快)双工半双工全双工收发同时进行寻址7位地址寻址需ACK片选寻址拉低CS就是选中无需ACK抗干扰上拉电阻稍弱推挽输出高速更稳典型OLED(I2C版), MPU6050, AT24C02W25Q64, OLED(SPI版), SD卡, 屏幕选型口诀省IO、低速多设备选I2C要速度、传大文件选SPI。2. 硬件层4根线的本质SCK (Serial Clock)时钟线完全由主机产生从机被动跟随。MOSI (Master Out Slave In)主发从收。MISO (Master In Slave Out)从发主收。CS / NSS / SS片选低电平有效。主机通过单独的CS线选中某个从机。关键区别SPI是推挽输出不需要I2C那样的上拉电阻所以可以跑非常快但也失去了多主机仲裁能力。多从机接法SCK、MOSI、MISO 三根总线并联所有从机共享。CS线每个从机独立一根主机想和谁说话就把谁的CS拉低。3. 协议核心环形移位寄存器模型理解SPI的灵魂就是理解这个图主机和从机内部各有一个8位移位寄存器连成一个环主机把数据写入寄存器主机产生8个时钟脉冲 SCK每来一个脉冲MOSI移1位出去给从机MISO移1位进来给主机8个脉冲后两个寄存器的数据完成了交换所以 SPI 每次收发是同时发生的你HAL_SPI_Transmit其实也会收到数据只是你忽略了你想读从机数据必须发一个空字节0xFF来产生时钟。// STM32硬件SPI的本质就是这个模型 uint8_t SPI_SwapByte(uint8_t send) { while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE)RESET); // 等发送寄存器空 SPI_I2S_SendData(SPI1, send); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE)RESET); // 等接收到 return SPI_I2S_ReceiveData(SPI1); }4. 最难的一关CPOL 与 CPHA 四种模式这是所有SPI教程都绕不开的坎也是SPI不通的头号原因。引入两个概念CPOL (Clock Polarity) 时钟极性SCK在空闲时是高还是低CPOL0空闲低电平CPOL1空闲高电平CPHA (Clock Phase) 时钟相位在第几个边沿采样CPHA0奇数边沿采样 (第一个边沿)CPHA1偶数边沿采样 (第二个边沿)组合成4种模式模式CPOLCPHA空闲电平采样时刻典型器件Mode 000低上升沿采样下降沿发送W25Q64, SD卡, 大部分FlashMode 101低下降沿采样上升沿发送部分AD芯片Mode 210高下降沿采样上升沿发送少见Mode 311高上升沿采样下降沿发送OLED(SSD1306 SPI版), NRF24L01记住两条就够了95%的设备都是 Mode0 或 Mode3。 偷懒技巧Mode0 和 Mode3 都是上升沿采样只是空闲电平不同。很多从机其实两种都兼容。如何看数据手册判断看时序图数据线在哪个边沿保持稳定那个边沿就是采样边沿。5. STM32 SPI外设详解5.1 核心寄存器SPI_CR1 MSTR1主机0从机SPI_CR1 CPOL/CPHA模式选择SPI_CR1 BR[2:0]波特率分频0002分频0014分频...111256分频。SCK频率 fPCLK / 分频SPI_CR1 SPE外设使能SPI_CR1 LSBFIRST0MSB高位在前常用1LSB低位在前SPI_SR TXE发送寄存器空SPI_SR RXNE接收寄存器非空SPI_DR数据寄存器写它发送读它接收STM32F103APB2是72MHzAPB1是36MHz。SPI1挂APB2最高可到36MHzSPI2挂APB1最高到18MHz。5.2 硬件 vs 软件 SPI 对比软件模拟SPI硬件SPI速度慢几百k靠delay快几十MHz靠分频器CPU占用高低可用DMA解放CPU稳定性非常稳F1系列偶有配置问题但比I2C稳得多引脚任意GPIO固定复用SPI1: PA5/6/7SPI2: PB13/14/156. 实战一CubeMX HAL 驱动 W25Q64最经典W25Q64 是 8MByte 的 SPI Flash是 STM32 的最佳外置存储搭档。6.1 硬件连接W25Q64引脚STM32F103 (SPI1)CLKPA5 - SCKDI(MOSI)PA7 - MOSIDO(MISO)PA6 - MISOCSPB6 / PC0 任意GPIO推挽输出软件控制VCC/GND3.3V / GNDWP/HOLD接3.3V (不写保护)6.2 CubeMX配置关键Connectivity - SPI1Mode:Full-Duplex MasterHardware NSS:Disable(我们用软件GPIO控制CS最灵活)Parameter Settings:Frame Format:MotorolaData Size:8 BitsFirst Bit:MSB FirstCPOL: Low, CPHA: 1 Edge → Mode0(W25Q64要求)BaudRate:Prescaler 8(72M/89MHz先跑慢点调通后再加速)GPIO - CS引脚例如 PB6配置为GPIO_Output, Push-Pull, HighClock别忘了开到72MHz6.3 W25Q64 核心驱动HAL版面试级必背W25Q64 操作本质发命令 发地址 传数据。所有操作先发写使能 0x06。// w25q64.h #define W25Q64_CS_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET) #define W25Q64_CS_HIGH() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET) #define CMD_WRITE_ENABLE 0x06 #define CMD_READ_STATUS1 0x05 #define CMD_SECTOR_ERASE 0x20 // 4K擦除 #define CMD_PAGE_PROGRAM 0x02 // 256字节页编程 #define CMD_READ_DATA 0x03 #define CMD_READ_ID 0x9F extern SPI_HandleTypeDef hspi1; // 底层一字节交换 uint8_t SPI1_Swap(uint8_t byte) { uint8_t rx; HAL_SPI_TransmitReceive(hspi1, byte, rx, 1, 100); return rx; } uint8_t W25Q64_ReadStatus(void) { uint8_t status; W25Q64_CS_LOW(); SPI1_Swap(CMD_READ_STATUS1); status SPI1_Swap(0xFF); W25Q64_CS_HIGH(); return status; } void W25Q64_WaitBusy(void) { while(W25Q64_ReadStatus() 0x01); // BUSY位1 表示正在擦/写 } void W25Q64_WriteEnable(void) { W25Q64_CS_LOW(); SPI1_Swap(CMD_WRITE_ENABLE); W25Q64_CS_HIGH(); } // 读ID判断芯片是否正常0xEF4017 W25Q64 uint32_t W25Q64_ReadID(void) { uint32_t id; uint8_t buf[3]; W25Q64_CS_LOW(); SPI1_Swap(CMD_READ_ID); buf[0] SPI1_Swap(0xFF); buf[1] SPI1_Swap(0xFF); buf[2] SPI1_Swap(0xFF); W25Q64_CS_HIGH(); id (buf[0]16)|(buf[1]8)|buf[2]; return id; } // 擦除4K扇区地址必须是4096倍数 void W25Q64_EraseSector(uint32_t addr) { W25Q64_WriteEnable(); W25Q64_CS_LOW(); SPI1_Swap(CMD_SECTOR_ERASE); SPI1_Swap((addr16)0xFF); // W25Q64是24位地址高位先发 SPI1_Swap((addr8)0xFF); SPI1_Swap(addr0xFF); W25Q64_CS_HIGH(); W25Q64_WaitBusy(); } // 页编程最多256字节且不能跨页 void W25Q64_PageProgram(uint32_t addr, uint8_t* data, uint16_t len) { W25Q64_WriteEnable(); W25Q64_CS_LOW(); SPI1_Swap(CMD_PAGE_PROGRAM); SPI1_Swap((addr16)0xFF); SPI1_Swap((addr8)0xFF); SPI1_Swap(addr0xFF); for(uint16_t i0;ilen;i) SPI1_Swap(data[i]); W25Q64_CS_HIGH(); W25Q64_WaitBusy(); } void W25Q64_ReadData(uint32_t addr, uint8_t* data, uint32_t len) { W25Q64_CS_LOW(); SPI1_Swap(CMD_READ_DATA); SPI1_Swap((addr16)0xFF); SPI1_Swap((addr8)0xFF); SPI1_Swap(addr0xFF); for(uint32_t i0;ilen;i) data[i]SPI1_Swap(0xFF); W25Q64_CS_HIGH(); } // 主函数测试 void Test_W25Q64(void) { uint32_t id W25Q64_ReadID(); printf(W25Q64 ID: 0x%X (should be 0xEF4017)\n, id); uint8_t wbuf[] Hello SPI Flash! STM32 NB!; uint8_t rbuf[32]{0}; W25Q64_EraseSector(0x000000); // 擦0地址扇区 W25Q64_PageProgram(0x000000, wbuf, sizeof(wbuf)); W25Q64_ReadData(0x000000, rbuf, sizeof(wbuf)); printf(Read: %s\n, rbuf); }关键坑必须先擦后写Flash写只能把1变0擦除才把0变回1。页编程不能跨页每页256字节地址0xFF后下一页是0x100。擦/写时必须轮询 BUSY 位。7. 实战二软件模拟 SPI0.96寸 OLED SPI版 最常用软件SPI的好处任意引脚且可以模拟各种不标准的SPI时序。#define SPI_SCK_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET) #define SPI_SCK_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET) #define SPI_MOSI_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET) #define SPI_MOSI_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET) #define SPI_MISO_READ() HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6) void SoftSPI_Init(void) { // SCK, MOSI 推挽输出MISO 上拉输入 } uint8_t SoftSPI_SwapByte(uint8_t data) { for(uint8_t i0;i8;i) { // Mode0先放数据上升沿采样 if(data 0x80) SPI_MOSI_HIGH(); else SPI_MOSI_LOW(); data 1; SPI_SCK_HIGH(); // 上升沿从机采样 if(SPI_MISO_READ()) data | 0x01; SPI_SCK_LOW(); // 下降沿主机切换数据 } return data; }8. 实战三SPI DMA 高速刷屏进阶OLED/TFT 刷屏慢用DMAHAL库自带DMA收发CPU只需要启动传输就可以去干别的。// 使用 DMA 发送大块数据例如 1KB 的显存 uint8_t frame[1024]; HAL_SPI_Transmit_DMA(hspi1, frame, 1024); // 此时CPU空闲等传输完成会进回调 void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { // 刷屏完成可以发下一帧 }DMA模式下STM32F103 SPI1可轻松跑到 18MHz刷 0.96寸OLED 几乎无延迟。9. 调试排坑指南CPOL/CPHA配错读出来全是0xFF或0x00。解决对着从机手册时序图一一核对先试Mode0再试Mode3。CS没控制好时序乱套。每次命令前后必须CS LOW - 传输 - CS HIGH。软件NSS千万别开硬件NSS自动模式除非你非常懂。速度过快杜邦线太长9MHz跑不通。CubeMX先设 Prescaler 64或128跑通再慢慢加速。MISO没上拉/浮空读ID一直0xFF。检查MISO引脚配置为 Input / AF。W25Q64写失败忘了写使能0x06忘了等 BUSY没擦除就写跨页写硬件冲突SPI1默认和JTAG复用PA5/6/7上如果接了调试器需要重映射或用SPI2。10. 总结与面试考点问SPI为什么比I2C快答推挽输出驱动强无需上拉电阻等待上升无寻址ACK开销全双工。问SPI有几根线能挂几个设备答4根基础线多一个设备多一根CS理论无限实际受限于IO和布线。问4种模式区别答CPOL定空闲电平CPHA定采样边沿Mode0/3上升沿采样最常用。问如何实现双向传输答靠移位寄存器环主机发一个字节的同时收一个字节。问W25Q64写入流程答写使能0x06 - 擦扇区0x20 - 等BUSY - 页编程0x02 - 等BUSY。系列文章参考野火《SPI—读写串行FLASH》官方文档 4ST社区《SPI原理超详细讲解》时序分析 1STM32CubeMX实战驱动W25Q64完整流程 2本文所有配图均为原创生成代码亲测F103/F407可用。如果文章帮你打通了SPI开源不易