STM32 SPI通信协议详解:从原理到实战驱动LCD与Flash

STM32 SPI通信协议详解:从原理到实战驱动LCD与Flash
1. 项目概述从零到一掌握STM32的SPI通信如果你正在用STM32做项目大概率绕不开和各种传感器、存储芯片或者显示屏打交道。这时候SPISerial Peripheral Interface通信协议就成了你的得力助手。它不像UART那样需要复杂的波特率匹配也不像I2C那样受限于总线上拉电阻和地址冲突SPI以其全双工、高速、协议简单的特点在芯片间短距离通信中占据着重要地位。我最初接触SPI时也被其主从模式、时钟极性和相位这些概念绕得有点晕但一旦理清你会发现用它驱动一块LCD屏或者读写一颗Flash芯片效率非常高。这篇笔记就是把我这些年用STM32的SPI外设趟过的路、踩过的坑系统地梳理一遍目标是让你看完后不仅能理解SPI的原理更能独立配置和调试STM32的SPI解决实际项目中遇到的大部分通信问题。简单来说SPI是一种同步的、全双工的串行通信总线。它最少需要四根线SCK时钟、MOSI主出从入、MISO主入从出、CS片选。STM32内部集成了硬件SPI外设我们只需要通过配置寄存器或者使用HAL/LL库就能轻松实现通信把CPU从繁重的模拟时序中解放出来。无论是读取温湿度传感器的数据还是向SD卡写入文件亦或是驱动一块高分辨率的TFT屏幕SPI都是核心桥梁。接下来我们就从最根本的原理开始一步步拆解STM32 SPI外设的使用方法。2. SPI通信协议核心原理深度解析2.1 SPI的物理连接与四种工作模式SPI的物理连接非常直观。一个主设备通常是我们的STM32可以连接多个从设备如W25Q64 Flash、ILI9341 LCD等。每个从设备都需要一根独立的片选线CS/NSS。当主设备需要和某个从设备通信时就将其对应的CS线拉低有效其他从设备的CS线保持高电平。SCK时钟由主设备产生并输出给所有从设备数据则在MOSI和MISO线上同步传输。SPI协议的精髓或者说最容易让人困惑的地方在于其时钟极性CPOL和时钟相位CPHA的四种组合也就是常说的SPI模式0-3。CPOL (Clock Polarity)时钟极性。它定义了SCK时钟线在空闲状态时的电平。CPOL0SCK空闲时为低电平。CPOL1SCK空闲时为高电平。CPHA (Clock Phase)时钟相位。它定义了数据在时钟的哪个边沿被采样捕获。CPHA0数据在时钟的第一个边沿如果CPOL0则是上升沿如果CPOL1则是下降沿被采样在下一个边沿切换。CPHA1数据在时钟的第二个边沿被采样在第一个边沿切换。这四种模式的波形需要仔细理解模式0 (CPOL0 CPHA0)空闲时SCK为低电平数据在SCK的上升沿被采样下降沿切换。这是最常用的模式很多传感器和存储芯片默认于此。模式1 (CPOL0 CPHA1)空闲时SCK为低电平数据在SCK的下降沿被采样上升沿切换。模式2 (CPOL1 CPHA0)空闲时SCK为高电平数据在SCK的下降沿被采样上升沿切换。模式3 (CPOL1 CPHA1)空闲时SCK为高电平数据在SCK的上升沿被采样下降沿切换。注意主设备和从设备的SPI模式必须完全一致否则无法正确通信。通常从设备的数据手册会明确说明其支持的SPI模式我们在配置STM32时必须与之匹配。一个快速记忆方法是看数据手册时序图里数据线如MOSI在SCK的哪个边沿是稳定的即采样边沿哪个边沿是变化的即切换边沿。2.2 数据帧格式与传输过程SPI以数据帧为单位进行传输一帧通常是一个字节8位或两个字节16位具体取决于STM32 SPI外设的配置DATASIZE。传输总是由主设备发起。当主设备通过MOSI线发送一位数据时从设备也会同时通过MISO线向主设备发送一位数据。因此SPI是全双工的发送和接收同时进行。传输过程可以这样理解主设备内部有一个移位寄存器。当我们把要发送的数据写入发送数据寄存器SPI_DR后硬件会在SCK时钟的驱动下将这个数据一位一位地从MOSI引脚移出。同时MISO引脚上的数据也会被一位一位地移入同一个移位寄存器。当一帧数据比如8位全部移完移位寄存器里的内容就是接收到的数据我们可以从接收数据寄存器同样是SPI_DR但读操作对应接收中读取它。这里有一个关键点即使主设备没有数据要发送只要它产生SCK时钟从设备就会持续发送数据。因此在实际操作中如果我们只想读取从设备的数据主设备通常需要发送一个“哑元”字节比如0xFF或0x00来产生时钟从而触发从设备的发送。这个“哑元”字节的具体值有时需要根据从设备特性确定有些器件在时钟边沿采样命令或地址此时发送的就不能是任意值。2.3 片选信号硬件与软件控制片选CS/NSS信号用于选择目标从设备。STM32的SPI外设提供了两种NSS管理模式硬件NSS从模式选择将STM32配置为SPI从设备时使用此模式。NSS引脚作为输入由外部主设备控制其电平。当外部主设备拉低这个引脚时本STM32被选为从设备。软件NSS管理这是STM32作为主设备时最常用的方式。我们将NSS引脚配置为普通的GPIO输出例如PA4或PA15具体看芯片数据手册然后在代码中手动控制其电平。在通信开始时拉低对应的GPIO通信结束后再拉高。这种方式非常灵活可以轻松控制多个从设备。实操心得我强烈建议在绝大多数主设备应用中使用软件控制NSS。原因有三一是硬件NSS模式在某些情况下如多主系统逻辑复杂容易出错二是软件控制可以更精确地控制片选信号的时序例如在发送命令和数据序列之间可以保持片选有效而硬件NSS可能在每帧数据传输间隙产生不必要的跳变三是软件控制便于调试你可以随时在代码中插入延时或打印信息来观察片选信号。3. STM32 SPI外设的配置与初始化详解3.1 使用STM32CubeMX进行图形化配置对于初学者或者追求开发效率的工程师STM32CubeMX是配置SPI的利器。我们以STM32F103C8T6的SPI1为例演示关键配置步骤。首先在Pinout Configuration标签页下找到SPI1。将模式Mode设置为Full-Duplex Master全双工主模式。此时CubeMX会自动分配引脚PA5为SCKPA6为MISOPA7为MOSI。PA4通常被分配为NSS但如前所述我们更倾向于软件控制所以可以在这里将PA4的GPIO output level先设为High上拉然后在代码中手动控制。或者你也可以在Pinout视图里直接将PA4重新配置为GPIO_Output。接下来进入Configuration标签页下的SPI1参数设置Basic Parameters:Prescaler (波特率预分频器)这是决定SCK时钟频率的关键。SCK频率 APBx总线时钟/Prescaler。例如APB2时钟为72MHz选择8分频则SCK为9MHz。需要根据从设备支持的最高时钟频率来设置留有一定余量。Data Size (数据大小)选择8 Bits或16 Bits与从设备匹配。First Bit (首比特)选择MSB First最高位先传或LSB First最低位先传。绝大多数器件都是MSB First。Advanced Parameters:Clock Polarity和Clock Phase根据从设备手册选择对应的模式Mode 0-3。NSS Signal Type选择Software软件控制。CRC Calculation除非通信协议要求CRC校验否则保持Disable。其他TI Mode通常禁用Standard SPI模式。配置完成后生成代码。CubeMX会帮我们生成MX_SPI1_Init()函数完成GPIO和SPI外设的初始化。3.2 直接寄存器配置与HAL/LL库函数解析如果你不用CubeMX或者想更深入地理解底层就需要直接操作寄存器或调用HAL/LL库。HAL库方式 初始化后核心的发送接收函数是HAL_SPI_TransmitReceive()。这是一个阻塞式函数会等待整个数据传输完成才返回。uint8_t txData[2] {0x90, 0x00}; // 要发送的命令/数据 uint8_t rxData[2] {0}; // 接收缓冲区 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // 拉低片选 HAL_SPI_TransmitReceive(hspi1, txData, rxData, 2, 1000); // 超时时间1000ms HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 拉高片选还有HAL_SPI_Transmit()只发不收、HAL_SPI_Receive()只收不发内部会发送哑元数据等变体。LL库方式 LL库更接近寄存器效率更高。基本流程如下// 1. 使能SPI时钟配置GPIO略 // 2. 配置SPI参数 LL_SPI_SetBaudRatePrescaler(SPI1, LL_SPI_BAUDRATEPRESCALER_DIV8); LL_SPI_SetDataWidth(SPI1, LL_SPI_DATAWIDTH_8BIT); LL_SPI_SetClockPolarity(SPI1, LL_SPI_POLARITY_LOW); LL_SPI_SetClockPhase(SPI1, LL_SPI_PHASE_1EDGE); LL_SPI_SetTransferDirection(SPI1, LL_SPI_FULL_DUPLEX); LL_SPI_SetNSSMode(SPI1, LL_SPI_NSS_SOFT); LL_SPI_SetMode(SPI1, LL_SPI_MODE_MASTER); LL_SPI_Enable(SPI1); // 使能SPI // 3. 发送接收数据 LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_4); // 片选拉低 while(!LL_SPI_IsActiveFlag_TXE(SPI1)); // 等待发送缓冲区空 LL_SPI_TransmitData8(SPI1, cmd); // 发送命令字节 while(!LL_SPI_IsActiveFlag_RXNE(SPI1)); // 等待接收缓冲区非空 data LL_SPI_ReceiveData8(SPI1); // 读取接收到的数据 LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_4); // 片选拉高注意事项使用LL库或寄存器操作时必须严格遵循“先检查标志位再操作数据寄存器”的顺序。特别是读取数据时一定要在RXNE接收缓冲区非空标志置位后再去读DR寄存器否则可能读到旧数据或导致错误。3.3 关键参数计算与选择依据波特率预分频器这是影响通信速度最直接的参数。计算公式SPI_SCK fPCLK / Prescaler。fPCLK是SPI所在APB总线的时钟频率。选择时必须小于等于从设备手册标明的最大SCK频率。例如W25Q64 Flash最大支持104MHz在Fast Read模式下如果你的fPCLK是80MHz分频系数至少设为2得到40MHz以保证稳定。对于长线传输或干扰环境还应进一步降低速率。数据大小8位是最通用的。有些16位精度的ADC或DAC芯片可能需要16位模式。注意在16位模式下每次读写操作都是以16位为单位。时钟极性与相位这是通信的基石必须与从设备100%匹配。没有妥协的余地。仔细阅读从设备数据手册的时序图部分。NSS管理如前所述主模式下强烈推荐软件管理。从模式下则必须使用硬件NSS输入。4. SPI通信的进阶应用与DMA驱动4.1 使用DMA提升SPI传输效率当需要传输大量数据时比如刷新整个LCD屏发送数万个字节的像素数据如果使用阻塞式查询或中断方式CPU会被长时间占用在等待SPI传输完成上。此时DMA直接存储器访问就是救星。DMA可以在不占用CPU的情况下自动将内存中的数据搬运到SPI数据寄存器发送出去或者将接收到的数据搬运到指定内存。配置SPI DMA发送的步骤以HAL库为例在CubeMX中使能SPI对应的DMA通道SPI_TX。通常选择Memory To Peripheral模式数据宽度根据SPI数据大小选择Byte或Half Word。生成代码后使用HAL_SPI_Transmit_DMA()函数启动传输。uint8_t lcdBuffer[320*240*2]; // 假设屏幕缓冲区 HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit_DMA(hspi1, lcdBuffer, sizeof(lcdBuffer)); // 此时CPU可以继续执行其他任务 // 传输完成后会进入SPI发送完成的DMA回调函数 HAL_SPI_TxCpltCallback()在传输完成回调函数中拉高片选或者准备下一次传输。踩坑实录STM32H750 DMA驱动SPI LCD问题这个问题非常典型。现象可能是屏幕显示花屏、错位或部分不更新。原因和排查点包括内存对齐DMA对源地址内存地址有对齐要求。确保发送缓冲区如lcdBuffer的地址是4字节对齐的对于32位系统。可以使用__attribute__((aligned(4)))定义数组或者使用malloc分配对齐的内存。Cache一致性如果使用了STM32H7系列的CacheD-Cache而lcdBuffer位于Cacheable的内存区域如AXI SRAM在DMA传输前必须清理CleanCache。因为CPU写入lcdBuffer的数据可能还留在Cache里并未真正写入物理内存DMA直接从物理内存取数据就会是旧数据。使用SCB_CleanDCache_by_Addr()函数清理缓冲区对应的Cache行。DMA流/通道配置确保DMA的数据流Stream和通道Channel配置正确优先级设置合理没有与其他外设的DMA冲突。SPI时钟与LCD时序过高的SPI时钟可能导致LCD控制器来不及处理。尝试降低SPI波特率。同时检查LCD的WRX写使能等控制信号时序是否满足要求。4.2 单线双向半双工SPI模式有些器件为了节省引脚采用了单线双向SPI。例如某些ADC芯片它只有一根数据线SIO在特定时段用于主机发送命令另一时段用于从机返回数据。STM32的SPI外设支持这种模式通过配置BIDIMODE和BIDIOE位。在单线双向模式下同一根引脚通常是MOSI引脚会在不同时间扮演输出或输入的角色。操作流程一般是配置SPI为“只发送”模式BIDIOE1主机通过该引脚发送命令。发送完命令后切换SPI为“只接收”模式BIDIOE0并将该引脚配置为浮空输入或根据从设备要求配置上拉然后主机发送时钟从机通过同一根线传回数据。这种模式对时序控制要求更精细需要严格按照从设备数据手册的指令序列来操作GPIO模式和SPI方向。4.3 多从机SPI系统设计当一个STM32需要连接多个SPI从设备时设计上需要注意独立的片选线每个从设备使用一根独立的GPIO作为片选。这是最清晰、干扰最小的方式。菊花链Daisy Chain如果所有从设备都支持菊花链模式可以将它们的MISO和MOSI首尾相接。主机只用一个片选控制整个链。数据像接力一样从一个器件传到下一个。这种方式节省GPIO但协议复杂所有器件必须同时参与传输灵活性差。SPI开关/多路复用器使用专用的模拟开关芯片通过少量GPIO控制将主SPI路由到不同的从设备。适用于从设备数量非常多的情况。5. 典型器件驱动实战与调试技巧5.1 驱动W25Qxx系列SPI FlashW25Q6464Mbit是常用的SPI NOR Flash。驱动它有几个关键点初始化通常模式为Mode 0 MSB first。上电后Flash可能处于“写保护”或“挂起”状态需要先发送0xABRelease Power-down或0x660x99Enable Reset指令。读数据使用0x03Read Data指令后面跟24位地址。然后SPI时钟就会读回数据。注意发送读指令和地址后从设备需要一点时间tV才能输出有效数据因此最好在发送地址后稍微延迟一下再开始读时钟或者先发送一个哑元字节。写使能与页编程写操作前必须先发送0x06Write Enable指令。页编程指令是0x02后跟地址和数据。Flash按页通常256字节编程跨页写入需要分多次。等待忙状态写、擦除操作后Flash内部需要时间完成。可以通过读状态寄存器10x05的BUSY位来判断。在忙状态除了读状态寄存器指令其他指令都会被忽略。// 读取W25Q64的ID0xEF, 0x40, 0x16 uint8_t cmd_read_id[4] {0x9F, 0x00, 0x00, 0x00}; // 0x9F是读ID指令 uint8_t id_buf[3] {0}; HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, cmd_read_id, 1, 100); // 发送指令 HAL_SPI_Receive(hspi1, id_buf, 3, 100); // 接收3字节ID HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET);5.2 驱动ILI9341 TFT LCD屏ILI9341是320x240的SPI接口LCD控制器。驱动它涉及初始化序列、写命令/写数据、GRAM操作。接口模式通常支持4线SPI即标准SPI带DC命令/数据选择线和3线SPI9位数据帧最高位表示命令/数据。4线SPI更常见。DC引脚这是一根额外的GPIO用于区分发送的是命令DC0还是数据DC1。例如设置显示区域0x2A和0x2B命令后跟着发送的数据就是坐标值。初始化需要发送一长串初始化命令序列这些序列通常由屏厂提供用于设置伽马、驱动方向、颜色格式等。刷屏优化刷屏是连续写入GRAM数据0x2C命令。这是DMA最能发挥作用的场景。将整个帧缓冲区的数据通过DMA源源不断地发送出去CPU只需在开始和结束时干预。5.3 SPI通信调试技巧与常见问题排查当SPI通信失败时可以按照以下步骤排查检查物理连接这是第一步也是最容易出错的一步。确保SCK、MOSI、MISO、CS四根线或三线连接正确、牢固。用万用表测量是否有短路、断路。用逻辑分析仪抓取波形这是最强大的调试工具。将探头连接到SPI的四根线上设置正确的触发条件如CS下降沿。观察片选CS是否在数据帧传输期间保持低电平是否有毛刺或意外跳变时钟SCK频率是否符合预期极性空闲电平和相位数据采样边沿是否正确数据线MOSI/MISO主设备发送的数据是否正确从设备是否有数据回送数据是在正确的时钟边沿稳定对比分析将抓到的波形与从设备数据手册的时序图逐一对比任何不符都可能是问题根源。检查软件配置模式匹配反复确认CPOL和CPHA设置与从设备要求是否一字不差。时钟频率是否超过从设备最大允许频率尝试降低分频系数。数据顺序MSB/LSB设置是否正确NSS信号软件控制时GPIO初始化是否正确推挽输出拉低和拉高的时机是否在数据帧传输的首尾检查从设备状态有些器件如Flash上电后需要特定指令唤醒或解锁。检查从设备是否处于忙状态通过读状态寄存器。确认从设备的供电电压和电平是否与STM32匹配都是3.3V。简化测试先尝试最简单的单字节回环测试将STM32的MOSI和MISO短接发送一个数据看接收到的数据是否相同。这可以排除软件配置和STM32自身SPI外设的问题。编写一个最简单的发送函数只发送几个已知的字节用逻辑分析仪观察波形。常见问题速查表现象可能原因排查建议完全无波形SPI未使能GPIO配置错误片选未拉低检查外设时钟、GPIO复用功能、CS引脚电平有SCK但无数据数据线接反MOSI/MISO从设备未响应交换MOSI/MISO线检查从设备电源和使能数据错误错位MSB/LSB设置错误时钟相位错误核对数据手册时序图调整CPHA只能读/只能写半双工/全双工模式混淆从设备特定模式确认从设备是否支持全双工检查读写指令高速时通信失败时钟频率过高信号完整性差过冲、振铃降低波特率缩短走线在SCK上加小电阻串联阻尼DMA传输数据错乱内存Cache不一致缓冲区地址未对齐清理D-Cache确保缓冲区地址对齐调试SPI耐心和细致的观察是关键。从电源、时钟、基本配置这些底层因素开始逐步向上层应用排查结合逻辑分析仪这个“眼睛”大部分问题都能迎刃而解。掌握了这些你就能让STM32的SPI外设稳定可靠地为你服务连接起更广阔的外部世界。