简介面向STM32F4系列嵌入式开发者以SPI通信为主题的示例工程包用于学习和验证SPI总线协议、四种工作模式CPOL/CPHA配置以及NSS片选管理方式也适用于连接LCD、EEPROM、传感器等常见SPI外设的场景。压缩包仅9KB共5个文件包含C源文件、头文件、链接脚本、Makefile和启动文件结构精简C与头文件实现SPI初始化、收发函数及错误处理Makefile和启动文件支撑工程编译与板级启动。已有222人学习下载适合刚接触STM32F4或需要参考SPI外设配置的开发者既能对照HAL/LL库接口理解底层时序也能直接借用工程模板进行二次开发减少查阅参考手册的时间成本。整体上提供了一套可直接编译运行的代码骨架便于按具体外设需求扩展。1. STM32F4 SPI 通信的硬件结构与四线时序拆开 SPI.rar 这个工程包里面没有 HAL 库的层层封装main.c、stm32f4xx_conf.h、startup_stm32f4xx.S 和一份 Makefile 就把骨架撑起来了。熟悉标准外设库SPL的工程师一眼能认出这是老式 Makefile 工程最典型的结构——外设驱动文件是空的SPI 相关代码全部等着自己填。有人拿到这类工程第一反应是去装 CubeMX 重新生成但历史项目里的 Makefile 和链接脚本stm32_flash.ld往往已经调好重生成反而会引入一批未知的默认参数。SPI 在 STM32F4 上常排在外设调试的第二梯队UART 通个串口助手很容易SPI 却要看波形才能确认通没通。原因在于四根线SCLK、MOSI、MISO、NSS之间有严格时序关系主从双方只要 CPOL/CPHA 不一致数据就会整体错位。下面的内容从模式判定讲起把寄存器级和 HAL 库两条初始化路径并排写出来再处理多从机片选和上拉电阻这些容易被忽略的细节最后用示波器把实测波形收尾。2. SPI 工作模式与寄存器级初始化路径2.1 四种工作模式与时钟沿的判定方法SPI 协议没有像 UART 那样的帧格式协商机制主从双方必须在初始化前就把时钟极性和相位确定下来。CPOL 决定 SCLK 空闲电平CPOL0 时空闲为低CPOL1 时空闲为高。CPHA 决定数据采样沿CPHA0 在第一个边沿采样CPHA1 在第二个边沿采样。两者组合出四种工作模式SPI 模式CPOLCPHASCLK 空闲电平采样沿常见外设Mode 000低上升沿W25Q128、GD25Q128、SSD1306 OLEDMode 101低下降沿部分音频编解码器Mode 210高下降沿少数传感器Mode 311高上升沿NRF24L01 可用 Mode 0/3对 GD25Q128 这类 SPI NOR Flash数据手册时序图里标注数据在 SCLK 上升沿被锁存对应 Mode 0 或 Mode 3二者只在空闲电平和有效沿序上有差别。大多数驱动直接选 Mode 0如果这个 Flash 模块是从别的分线板上拆来的先查手册再定模式照抄别人的配置很容易踩空。实际调试时还有一个快速判定方法把示波器探针挂在 SCLK 和 MOSI 上发送一个 0x9F读 JEDEC ID指令观察空闲时 SCLK 是低还是高再看 MOSI 第一位翻转发生在 SCLK 上升沿之前还是之后。空闲电平直接对应 CPOL第一位数据的建立时间对应 CPHA比翻手册更快具体抓取方法放在最后一部分展开。2.2 STM32F4 外设分布与 GPIO 复用配置STM32F407 有 SPI1~SPI6 六个 SPI 外设其中 SPI1、SPI4、SPI5、SPI6 挂在 APB2 上SPI2、SPI3 挂在 APB1 上。典型 168MHz 主频下 APB284MHz、APB142MHz因此 SPI1 的 SCLK 最高可以到 42MHzSPI2/SPI3 最高 21MHz。驱动 Flash 和多数传感器根本用不到最高频率反而要主动降速到 10.5MHz 或 5.25MHz减少长走线上的信号反射。外设总线最高 SCLK典型时钟树常用复用引脚SPI1APB242 MHzPA5/PA6/PA7SPI2APB121 MHzPB13/PB14/PB15SPI3APB121 MHzPB3/PB4/PB5、PC10/PC11/PC12SPI4APB242 MHzPE2/PE4/PE5/PE6SPI5APB242 MHzPF7/PF8/PF9SPI6APB242 MHzPG11/PG12/PG13/PG14GPIO 复用引脚需要同时做两件事把引脚模式改成复用推挽再通过 AFR 寄存器选择对应的复用功能编号。比如 PA5/PA6/PA7 的 SPI1 复用编号是 AF5写错编号只会看到 IO 输出恒定时钟或无时钟。时钟使能的顺序也有讲究必须先开 GPIO 所在 AHB1 总线的时钟再开 SPI 外设所在 APB 总线的时钟顺序反过来时硬件可能进入未定义状态。标准外设库这些年已经不更新了近几年的 CubeMX 工程基本都迁移到 HAL/LL 库但底层寄存器行为没有变下面这段配置在两种库下都能找到对应逻辑。#include stm32f4xx.h #include stm32f4xx_gpio.h #include stm32f4xx_rcc.h #include stm32f4xx_spi.h void SPI1_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; /* 先开 GPIOA 时钟再开 SPI1 时钟 */ RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); GPIO_InitStruct.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStruct.GPIO_OType GPIO_OType_PP; GPIO_InitStruct.GPIO_PuPd GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, GPIO_InitStruct); /* PA5SCK, PA6MISO, PA7MOSI 复用为 SPI1AF5 */ GPIO_PinAFConfig(GPIOA, GPIO_PinSource5, GPIO_AF_SPI1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_SPI1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_SPI1); }上面这段把 PA5/PA6/PA7 同时配置成复用推挽并指定 AF5。GPIO_PuPd_NOPULL表示不上拉也不下拉SPI 信号线默认不打开内部上下拉实际板上是否要补上拉电阻取决于从机类型后面第 4 章单独说。时钟顺序在中低端调试器下不会直接报错但容易出现 GPIO 复用功能不生效的假象所以保持先 AHB1 后 APB 的习惯更安全。2.3 SPI_CR1 寄存器与模式参数对照SPI_CR1 里真正需要关心的位集中在低 8 位BR[2:0] 是波特率分频CPOL 在第 1 位CPHA 在第 0 位MSTR 是主从模式选择SPE 是外设使能。BR 分频系数从 2 到 256对应分频值为 2 的 (BR1) 次方。42MHz 的 SPI1 要跑 10.5MHzBR 就写 0b011也就是 16 分频。下面是一段基于标准外设库的完整初始化包含方向、主从、数据位宽、极性、相位、NSS 管理、波特率、MSB 先行和 CRC 多项式void SPI1_Init_Master(void) { SPI_InitTypeDef SPI_InitStruct; SPI_InitStruct.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStruct.SPI_Mode SPI_Mode_Master; SPI_InitStruct.SPI_DataSize SPI_DataSize_8b; SPI_InitStruct.SPI_CPOL SPI_CPOL_Low; /* CPOL0 */ SPI_InitStruct.SPI_CPHA SPI_CPHA_1Edge; /* CPHA0 */ SPI_InitStruct.SPI_NSS SPI_NSS_Soft; SPI_InitStruct.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_16; SPI_InitStruct.SPI_FirstBit SPI_FirstBit_MSB; SPI_InitStruct.SPI_CRCPolynomial 7; SPI_Init(SPI1, SPI_InitStruct); SPI_Cmd(SPI1, ENABLE); } uint8_t SPI1_TransferByte(uint8_t txData) { /* 等待发送缓冲区空 */ while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, txData); /* 等待接收缓冲区非空 */ while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI1); }这段代码里SPI_BaudRatePrescaler_16对 SPI1 来说就是把 84MHz 的 PCLK2 做 16 分频实际 SCLK 约 5.25MHz对 GD25Q128 这类最高支持 120MHz 的 Flash 来说余量足够。SPI_NSS_Soft表示不使用硬件片选后续用 GPIO 手动拉片选。SPI1_TransferByte是标准的双向字节交换先查 TXE 标志再写数据寄存器再查 RXNE 标志读回数据全双工时主设备发送一个字节的同时也会收到从设备返回的一个字节。读回来的数据不能忽略如果不读 DRRXNE 会一直保持为 1新数据到达时 OVR 溢出标志置位DR 不再更新后续读到的全是旧值。3. HAL 库收发实现与 DMA 通道配置3.1 HAL_SPI_Init 与 CubeMX 参数映射CubeMX 生成的代码把上述寄存器配置包装成SPI_HandleTypeDef字段与标准外设库基本一一对应。在 CubeMX 的 SPI1 配置页里选择 Master、8 Bit、Mode 0、Software NSS、Prescaler 16最终生成的初始化如下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; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_16; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial 7; if (HAL_SPI_Init(hspi1) ! HAL_OK) { Error_Handler(); } }HAL 库里NSS字段与硬件 NSS 的关系是许多人在 CubeMX 里踩的第一个坑SPI_NSS_SOFT对应寄存器里 SSM1 且 SSI1片选引脚被释放可以随意映射成 GPIO 使用。如果误选了SPI_NSS_HARD_INPUT硬件 NSS 引脚默认使能GPIO 配置会冲突片选信号被硬件接管后多从机场景直接失控。3.2 三种收发方式的取舍HAL 提供阻塞、中断、DMA 三组收发函数对应关系如下收发方式函数名适用场景主要风险阻塞HAL_SPI_TransmitReceive单字节短命令占用 CPU长数据时被中断打乱中断HAL_SPI_TransmitReceive_IT几十字节以内回调里收尾状态机复杂DMAHAL_SPI_TransmitReceive_DMA刷屏、Flash 读写需要两路 DMA 流配置繁琐阻塞模式下如果对每个字节都调用HAL_SPI_TransmitReceive中途被高优先级中断打断SCLK 会暂停Flash 片选没有释放的话整个命令序列会错乱。短命令没问题大批量读写必须换 DMA。中断模式适合一次发几十个字节的传感器读取DMA 模式适合一次性搬 4KB 以上的数据。uint8_t spiTxBuf[8] {0x9F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; uint8_t spiRxBuf[8] {0}; HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(hspi1, spiTxBuf, spiRxBuf, 8, 100); HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET);HAL_SPI_TransmitReceive的最后一个参数是超时时间单位毫秒。超时后函数返回HAL_TIMEOUT但总线上的数据可能只发了一半所以收到超时错误时要先拉高片选再重新初始化不能直接重试。这里的 100 毫秒对 8 个字节来说绰绰有余对一次 4KB 的 DMA 传输反而偏紧DMA 场景要用HAL_MAX_DELAY配合额外超时中断来做保护。3.3 两路 DMA 的请求映射与配置SPI 需要两个 DMA 吗——这个问题的准确回答是全双工大数据量场景需要纯 TX 刷屏只挂发送流也够用。SPI 是全双工外设只挂 TX DMA 时RX 侧仍然会产生数据如果一直没人读 RXNE数据溢出后会置位 OVR之后 RXNE 状态异常读回来的数据全是旧值收发数据流就断了。所以做 Flash 读写或传感器连续采样TX 和 RX 两路 DMA 建议同时挂上。以 STM32F407 为例SPI1 的 DMA 请求映射如下SPI 请求DMA 流通道SPI1_TXDMA2 Stream3Channel 3SPI1_RXDMA2 Stream0Channel 3SPI2_TXDMA1 Stream4Channel 0SPI2_RXDMA1 Stream3Channel 0void MX_SPI1_DMA_TX_Init(void) { static DMA_HandleTypeDef hdma_tx; __HAL_RCC_DMA2_CLK_ENABLE(); hdma_tx.Instance DMA2_Stream3; hdma_tx.Init.Channel DMA_CHANNEL_3; hdma_tx.Init.Direction DMA_MEMORY_TO_PERIPH; hdma_tx.Init.PeriphInc DMA_PINC_DISABLE; hdma_tx.Init.MemInc DMA_MINC_ENABLE; hdma_tx.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_tx.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; hdma_tx.Init.Mode DMA_NORMAL; hdma_tx.Init.Priority DMA_PRIORITY_HIGH; hdma_tx.Init.FIFOMode DMA_FIFOMODE_DISABLE; HAL_DMA_Init(hdma_tx); __HAL_LINKDMA(hspi1, hdmatx, hdma_tx); }__HAL_LINKDMA把 DMA 句柄挂到hspi1.hdmatx上HAL 库的HAL_SPI_Transmit_DMA内部会读取这个句柄发起传输。FIFOMode 设置成关闭时DMA 按字节直接搬移适合 8 位数据如果要打开 FIFO 做半字对齐传输需要保证源缓冲区地址按 2 字节对齐否则会触发 DMA 传输错误中断。MemInc使能表示源地址自动递增PeriphInc关闭表示外设地址固定为 SPI 数据寄存器。这里有个容易忽略的点SPI1 的 RX 和 TX 分别占用两个独立的 DMA 流但中断优先级配置要错开否则同时触发时会互相打断导致 RX 数据丢失。一般把 RX 流优先级设成比 TX 高因为接收侧是数据入口发送侧丢一个字节最多重发接收侧丢了一个字节整帧就废了。4. 多从机片选管理与软硬件 NSS 的边界4.1 硬件 NSS 的自动模式与 NSS 输出模式STM32F4 SPI 的 NSS 引脚有四种工作状态硬件输入、硬件输出、软件控制、复用 GPIO。SPI_NSS_HARD对应 SSM0片选信号由外部引脚驱动适合做从机SPI_NSS_SOFT对应 SSM1 且 SSI1片选完全由软件决定NSS 引脚释放。主机做主设备时如果用硬件 NSS还必须把 CR2 的 SSOE 位置 1让硬件在 SPI 使能时自动拉低片选、传输结束后拉高。这个自动拉片选看起来很方便实际多从机场景里会有严重副作用SPI 发送期间 SSOE 控制的片选持续拉低如果总线上挂了两个以上从机它们会被同时选中MISO 总线立刻发生写冲突。所以业界实践中硬件片选只用于单从机且从机片选时序宽松的场合任何带多从机的板子最终都会退回到 GPIO 模拟片选。提示把 SPI 配成硬件 NSS 输出还要留意从机的片选时序。部分 Flash 要求 CS 拉高后保持一段时间 tSHSL 才能发起下一次命令硬件自动拉高时序不满足时下一次传输会被忽略此时必须换软件片选。4.2 用 GPIO 模拟片选的多从机结构软件拉片选并不是 Linux 系统里才有的特殊处理在 STM32 裸机工程里同样是主流做法。片选退化为普通 GPIO 之后选哪个从机就是一次普通的GPIO_WriteBit操作。片选的关键时序要求是拉低后至少等待几百纳秒再发第一个字节让从机内部完成片选解码。#define FLASH_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define FLASH_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) uint32_t SPI_Flash_ReadJEDEC_ID(void) { uint8_t txBuf[4] {0x9F, 0x00, 0x00, 0x00}; uint8_t rxBuf[4] {0}; FLASH_CS_LOW(); /* 片选建立时间数据手册要求 tCS 不小于 100ns补几个 NOP 更稳妥 */ __NOP(); __NOP(); __NOP(); __NOP(); HAL_SPI_TransmitReceive(hspi1, txBuf, rxBuf, 4, 100); FLASH_CS_HIGH(); return (rxBuf[1] 16) | (rxBuf[2] 8) | rxBuf[3]; }txBuf的第一个字节是 JEDEC ID 指令后续三个 0x00 用来在从机应答时提供时钟。Flash 从机在收到 0x9F 后从第二个字节开始回填厂商 ID 和容量 ID。W25Q128 返回0xEF 0x40 0x18GD25Q128 返回0xC8 0x40 0x18如果读到的是 0xFF 或者全 0说明时钟极性、相位、波特率三者中至少有一个没对上。多从机时每个从机的片选 GPIO 必须单独占用一个引脚并且初始化时把空闲电平设成推挽高。两个从机共享同一根 MOSI/SCLK 没问题MISO 必须是独立输入引脚除非两个从机都具备片选拉高后 MISO 三态输出的能力。多数 SPI NOR Flash 都满足这个条件共享 MISO 是安全的前提是片选时序正确绝不能在两个从机同时被选中的状态下操作。4.3 上拉电阻与浮空输入的影响SPI 信号线是否需要上拉取决于从机 GPIO 的驱动类型和总线上从机的挂载数量。多数 SPI NOR Flash 模块是推挽输出MISO 不需要额外上拉信号电平由 Flash 直接驱动但 TF 卡在 SPI 模式下常用开漏输出MISO 必须加 4.7k~10k 上拉到 3.3V否则初始化读取 CID/CSD 寄存器时第一个字节经常是 0xFF误判成卡不存在。从机类型SCLKMOSIMISONSS/CSW25Q128 独立模块不需要不需要不需要可加 10k 上拉防误触发TF 卡 SPI 模式可加 10k可加 10k必须 4.7k~10k建议 10k 上拉NRF24L01 模块不需要不需要建议 10k建议 10k多从机共享总线末端加末端加每个从机各加每个 CS 各加还有一个经常被忽略的点SPI 时钟线 SCLK 在空闲状态下如果悬空会给从机引入随机时钟沿导致从机内部状态机错乱。如果从机靠近长排线末端最好在 SCLK 上并联一个小电容到地10~22pF把沿变缓代价是最高可用波特率下降到 10MHz 左右。这个处理在 FPC 排线连接 OLED 或 Flash 模块时尤其有效能直接消除偶发第一位数据错误的问题。5. 最后一步示波器验证 SPI 时序5.1 抓 SCLK 与 MOSI 判定模式是否正确验证 SPI 是否真正跑通最快的方法是双通道示波器。CH1 接 SCLKCH2 接 MOSI触发方式选下降沿触发源设置为 NSS/CS 引脚。发送一排 0x55二进制 01010101再发送一排 0xAA10101010观察波形0x55 模式下相邻电平翻转频率最高能直观看出 SCLK 是上升沿采样还是下降沿采样同时能确认数据位与时钟沿的对应关系。如果 SCLK 空闲电平是低MOSI 数据在上升沿附近保持稳定说明 CPOL0、CPHA0 成立如果看到 MOSI 的数据变化沿刚好压在 SCLK 上升沿上说明 CPHA 反了改成 CPHA1 重试。还有一种半概率故障MOSI 波形正确但读回来的数据整体右移一位这种通常是从机要求 MSB first而主机配成了 LSB first把SPI_FIRSTBIT_MSB改回去即可。5.2 从波形反推寄存器的调试顺序当从机应答持续为 0xFF 时按下面的顺序排查每一步都能从波形上找到对应证据检查 SCLK 频率波形上高电平宽度应大于从机手册要求的最小高电平时间频率过高时把 BR 分频系数加大一级。检查片选时序CS 下降沿到 SCLK 第一个沿的间距应大于手册里的建立时间 tCS通常 1us 足够。检查 MISO 信号完整性MISO 上叠加了明显回沟时说明上拉或端接不匹配。检查从机供电部分 Flash 模块 3.3V 与 5V 混用MISO 电平可能低于主设备高电平阈值。最后可以再确认一点如果主机发出的字节只有 8 位而从机要求 16 位命令部分传感器的寄存器地址就是 16 位示波器上看到的 SCLK 边沿数量会明显少于预期直接去查SPI_DataSize设置。这套从波形反推寄存器的方法比反复改代码重新烧录快得多也适用于用逻辑分析仪排查软件模拟 SPI 的情况——后者只是把 SCLK 换成 GPIO 翻转时序列的判据完全相同。本文还有配套的精品资源点击获取