
简介这是一份围绕STM32F429芯片SPI外设的完整例程包主要面向嵌入式系统开发者和正在学习STM32的进阶人员重点解决SPI主从模式配置、双工数据收发以及外设时序匹配等实际工程问题。压缩包共包含1445个文件绝大多数为C语言源文件和头文件同时配合有汇编启动文件、SVD器件描述文件以及编译好的静态库文件等整体大小84.34MB文件目录结构清楚方便驱动代码的查阅、移植与二次开发。这套例程目前已获得237人学习下载适合正在调试SPI通信或准备切换硬件平台的工程师使用。例程主体覆盖GPIO引脚初始化、SPI时钟源选择、数据帧格式设计、软件或硬件片选管理以及完整的发送接收流程并额外提供STemWin图形界面库和GCC、IAR编译环境对应的工程版本既能直接烧录到开发板验证基本功能也能作为上层应用的底层接口模板节省项目前期搭建时间。1. 为什么 STM32F429 的 SPI 例程值得重新过一遍STM32F429 单片机的外设例程里SPI 是最容易“配置成功但数据不对”的外设之一。F429 最多提供 6 个 SPI 接口支持全双工、半双工以及 DMA 传输但如果 GPIO 复用映射、NSS 管理模式或 CPOL/CPHA 有一处不对读回来的数据要么全 0xFF要么直接卡死在 BSY 标志上。这套例程通常还附带 STemWin 图形库很多人在跑 UI 之前先把 SPI 驱动的屏或 Flash 调崩。适合刚点亮 F429、想从标准外设库迁移到 HAL 或 CubeMX 的工程师也适合从 51 单片机转过来、需要理顺 SPI 时序和片选逻辑的人。下面对着例程代码把 SPI 的硬骨头逐个拆开。2. SPI 外设骨架时钟树、四种模式与硬件/软件片选2.1 同步串行和异步串行的区别以及为什么选 SPISPI 和 UART/I2C 最根本的差异在时钟来源。UART 是异步协议收发双方必须约定波特率靠起始位对齐字节任一边晶振偏差过大就会出现乱码。I2C 虽然有时钟线但属于半双工设备通过地址寻址速度受上拉电阻和线电容限制。SPI 是同步全双工SCK 由主机产生从机不需要精确时钟只看 SCK 边沿采样。正是这个特性让 SPI 成为单片机接 Flash、SD 卡、ADC、显示驱动最顺手的接口。特性SPIUARTI2C同步方式同步SCK 由主机产生异步需约定波特率同步SCK 由主设备产生数据线MOSI/MISO 独立全双工TX/RX 独立全双工SDA 双向半双工设备寻址片选 NSS无地址点对点无地址7/10 位地址接线数量4 线加地线2 线加地线2 线加地线典型速率几 Mbps 到几十 Mbps与波特率相关100k/400k/1MF429 的 SPI 最高可跑到 APB 时钟的一半。SPI1 挂在 APB2 上SPI2、SPI3 挂在 APB1 上APB1 和 APB2 的时钟源还可以通过 RCC 的分频器单独调整。例程里通常不做极限速率重点是把分频系数和 CPOL/CPHA 配对通信才能稳定。2.2 时钟极性和相位CPOL/CPHA 的四种模式SPI 通信时序有两个独立参数CPOL 决定 SCK 空闲电平CPHA 决定数据在哪个边沿采样。模式 0 是大多数器件默认的做法空闲低电平第一个边沿采样数据。模式 3 也常见空闲高电平第一个边沿采样。选哪个不靠偏好而是看从设备数据手册里的时序图。例程中默认给模式 0如果接的传感器要求模式 3不改配置就会出现“SCK 有波形数据全错”的现象。SPI 模式CPOLCPHA空闲电平数据采样边沿Mode 000低第一个边沿Mode 101低第二个边沿Mode 210高第一个边沿Mode 311高第二个边沿下面是一段基于标准外设库的 SPI2 主模式初始化和例程给出的代码风格一致。void SPI2_Init_Master(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_NOPULL; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_PinAFConfig(GPIOB, GPIO_PinSource13, GPIO_AF_SPI2); GPIO_PinAFConfig(GPIOB, GPIO_PinSource14, GPIO_AF_SPI2); GPIO_PinAFConfig(GPIOB, GPIO_PinSource15, GPIO_AF_SPI2); SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_16; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI2, SPI_InitStructure); SPI_Cmd(SPI2, ENABLE); }这段代码里最容易漏的是GPIO_PinAFConfig。F429 的引脚默认是普通 GPIO只有配置成复用模式并指定GPIO_AF_SPI2SCK、MOSI、MISO 才会连通到 SPI2 外设。SPI_NSS_Soft表示不用硬件片选NSS 引脚可以空出来或留给普通 GPIO 控制。分频系数 16 对应 APB1 时钟除以 16若 APB145MHzSCK 约为 2.8125MHz这个速率对绝大多数传感器和 Flash 都够用。2.3 硬件片选和软件片选例程里该用哪种“spi 硬件片选与软件片选”是很多人踩过坑的地方。硬件 NSS 由 SPI 外设自动拉低选中从机发送结束自动释放省一次 GPIO 操作但主机模式下需要额外处理 SSOE 和 NSSP 等位稍不留神就会触发 MODF 错误把 SPI 从主机变成从机。软件 NSS 就是把一个普通 GPIO 当片选传输前拉低传输后拉高完全由代码控制多从机时只要多占用几个 GPIO 就能扩展。例程里更常见的是软件 NSS因为可读性好也方便在片选前后插入延时或调试逻辑。如果非要硬件 NSS常见做法是把SPI_NSS设为SPI_NSS_Hard并配置 NSS 引脚为复用功能同时使能 SSOE。但有些从机要求 CS 在两次传输之间保持低电平硬件 NSS 自动释放会把命令拆散这时只能改回软件 NSS。结论是连续多字节传输场景软件片选更可控也更符合 Modbus 单片机帧接收这类要求完整数据帧的读写方式。3. 轮询到 DMASPI 收发代码这样写才不丢数据3.1 TXE、RXNE、BSY三个标志决定传输节奏SPI 状态寄存器中最常用的标志有三个。TXE 表示发送缓冲空可以向SPI_DR写入下一个字节RXNE 表示接收缓冲非空可以读走数据BSY 表示 SPI 忙传输尚未结束。轮询发送一个字节的标准流程是等 TXE写 DR等 RXNE读 DR。很多人忘记最后等 BSY 清零连续传输时提前拉高 NSS最后一个字节被截断。BSY 在最后一个数据位结束后还会维持一段时间具体时长由 APB 时钟和分频决定不能靠延时硬凑应该轮询标志位。uint8_t SPI2_TransferByte(uint8_t byte) { while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI2, byte); while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI2); } void SPI2_WriteRead(uint8_t *txbuf, uint8_t *rxbuf, uint16_t len) { uint16_t i; for (i 0; i len; i) { rxbuf[i] SPI2_TransferByte(txbuf[i]); } while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_BSY) SET); }这段代码在 STM32F429 主频 168MHz、SPI2 挂在 APB1 的场景下可以直接使用。外层循环控制传输长度内层SPI2_TransferByte完成一个字节的全双工交换。SPI 是全双工读和写是同一个动作发送指令字节的同时前一个字节的应答已经进入移位寄存器。txbuf是发送缓冲区rxbuf是接收缓冲区len是要传输的字节数两个缓冲区都要由调用方预先分配好。3.2 中断收发把等待时间还给 CPU轮询模式在传输大量数据时会占满 CPU。1MHz SCK 下传 1KB 数据大约需要 8ms如果主循环还有其他任务就会白白浪费算力。中断方式下每个字节传输完成触发一次中断在中断里读写SPI_DR。RXNE 中断适合接收不定长数据每次收一个字节TXE 中断适合批量发送但发送完最后一批后要关闭 TXE 中断否则会不停进中断。例程里如果嵌入了 STemWin 图形库主循环本身很忙SPI 用中断代替轮询是常见优化。传输方式CPU 占用实时性适用场景轮询高整个传输期间占 CPU高无调度延迟短包命令、调试中断中等每字节一次中断较高受中断优先级影响不定长接收、低速外设DMA低传输结束后一次中断高依赖 DMA 优先级刷屏、大批量读写中断服务例程的基本框架如下。void SPI2_IRQHandler(void) { if (SPI_I2S_GetITStatus(SPI2, SPI_I2S_IT_RXNE) ! RESET) { rxbuf[rx_cnt] SPI_I2S_ReceiveData(SPI2); if (rx_cnt rx_len) SPI_I2S_ITConfig(SPI2, SPI_I2S_IT_RXNE, DISABLE); } }中断处理函数先判断中断标志再读取数据寄存器。rx_cnt必须在主程序中初始化为 0rx_len是期望接收长度。每收满一帧就关闭 RXNE 中断避免缓冲区越界。注意 STM32F429 的 SPI2 中断入口名是SPI2_IRQHandlerSPI1 对应SPI1_IRQHandler在标准外设库工程里需要在stm32f4xx_it.c中开放同名函数。3.3 DMA接收方向最好开发送方向看场景“spi 需要两个 dma 吗”这个疑问结论是接收方向必须开 DMA发送方向看数据量。SPI 接收如果不及时读走 RXNE下一个字节到达会产生 OVR 溢出错误数据直接丢。用 DMA 接收字节自动从外设搬到内存CPU 只在 DMA 传输完成中断里处理整包数据。发送方向如果只是发寄存器命令每包几个字节轮询就够如果是刷屏、写 Flash一次几百字节用 DMA 发送更合理。F429 的 SPI2 挂在 DMA1 上发送和接收分别占两条数据流。CubeMX 生成工程后在 SPI2 的 DMA Settings 里添加 TX 和 RX 通道即可HAL 库会自动处理请求使能。如果手写标准外设库则要配置 DMA_InitTypeDef 并通过SPI_I2S_DMACmd开启 DMA 请求。DMA_InitTypeDef DMA_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE); DMA_DeInit(DMA1_Stream4); DMA_InitStructure.DMA_Channel DMA_Channel_0; DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)SPI2-DR; DMA_InitStructure.DMA_Memory0BaseAddr (uint32_t)txbuf; DMA_InitStructure.DMA_DIR DMA_DIR_MemoryToPeripheral; DMA_InitStructure.DMA_BufferSize tx_len; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_FIFOMode DMA_FIFOMode_Disable; DMA_Init(DMA1_Stream4, DMA_InitStructure);这段代码的关键在于DMA_PeripheralBaseAddr直接指向SPI2-DR外设地址不自增内存地址随传输递增。DMA1 的 Stream4 Channel0 对应 SPI2_TXStream5 Channel0 对应 SPI2_RX这个映射关系在芯片手册里有明确标注换到其他 SPI 接口时要重新查表。初始化完成后还要调用SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_TX | SPI_I2S_DMAReq_RX, ENABLE)使能外设请求。最容易犯的错是 DMA 时钟和外设时钟顺序不对必须先开 DMA 时钟再开 SPI 时钟否则 DMA 请求覆盖率不够第一个字节会丢。4. 多从机、软件 NSS 与 SPI 时序排查实测中的边界条件4.1 两根普通 GPIO 就能管理多从机SPI 主机接多个从机时常见做法是每个从机独立占用一个 NSS 引脚SCK、MOSI、MISO 三根线并联。从机的 MISO 在没有被选中时必须输出高阻否则多个从机同时在 MISO 上驱动数据轻则读数错误重则烧引脚。软件 NSS 的管理逻辑很简单传输前拉低目标从机的片选传输结束后拉高中间不要操作其他从机的片选。例程中如果遇到 MISO 数据被拉死先查是不是没接上拉电阻或者某个从机没有真正进入高阻态。#define SPI2_CS1_PORT GPIOB #define SPI2_CS1_PIN GPIO_Pin_12 #define SPI2_CS2_PORT GPIOB #define SPI2_CS2_PIN GPIO_Pin_9 void SPI2_Select(uint8_t cs_index) { GPIO_SetBits(SPI2_CS1_PORT, SPI2_CS1_PIN); GPIO_SetBits(SPI2_CS2_PORT, SPI2_CS2_PIN); if (cs_index 1) GPIO_ResetBits(SPI2_CS1_PORT, SPI2_CS1_PIN); else GPIO_ResetBits(SPI2_CS2_PORT, SPI2_CS2_PIN); } void SPI2_Release(void) { GPIO_SetBits(SPI2_CS1_PORT, SPI2_CS1_PIN); GPIO_SetBits(SPI2_CS2_PORT, SPI2_CS2_PIN); }这段代码先把所有片选置高再单独拉低要访问的路从根上避免两路设备同时选中。SPI2_Select和SPI2_Release必须成对调用如果中间某个分支提前 return会留下一个从机一直处于选中状态。实际调试时可以把片选动作包在临界区里防止被中断打断造成片选错乱。4.2 硬件 NSS 和 MODF 错误的因果关系配置SPI_NSS_Hard时主模式的 NSS 引脚受外设控制一旦外部信号把 NSS 拉低外设会认为发生多主机冲突立即置位 MODF 并自动关闭 SPI。F429 的 SPI2 如果开启硬件 NSS又把 NSS 引脚接到外部上拉就会出现间歇性通信失败断点停在SPI_I2S_GetFlagStatus的死循环里。解决方法是优先使用SPI_NSS_Soft并且不要把 NSS 引脚初始化为复用功能留作普通 GPIO。软件 NSS 模式下SPI 外设的 NSS 输入被内部拉高不会引发 MODF片选完全由软件控制。SPI 外设SCK 可选引脚MISO 可选引脚MOSI 可选引脚所在总线SPI2PB13 / PI1PB14 / PI2PB15 / PI3APB1SPI1PA5 / PB3PA6 / PB4PA7 / PB5APB2SPI3PB3 / PC10PB4 / PC11PB5 / PC12APB1查表时要注意同一引脚可能同时属于多个外设例如 PB3、PB4 既能做 SPI1 也能做 SPI3这时就要通过GPIO_PinAFConfig指定正确的GPIO_AF_SPIx否则两个外设的信号会互相干扰。CubeMX 可以自动规避冲突手写代码时经常在这里配错。4.3 逻辑分析仪看 SPI 时序要盯哪几个点如果例程运行后收不到数据第一步不是改代码而是用逻辑分析仪抓 SCK、MOSI、NSS 三根线。先看 SCK 是否正常翻转没有翻转大概率是 GPIO 复用或 APB 时钟问题。有 SCK 但 MOSI 无数据重点查分频系数和SPI_FirstBit。数据有但读回不对对照从设备手册确认采样沿。SPI 模式 0 的波形特征是 SCK 空闲为低数据在 SCK 上升沿稳定主机也在上升沿采样。如果逻辑分析仪显示数据在下降沿才变化而设置的是模式 0说明时序图看反了多半应改成模式 3。用连续发送0x55的波形能快速判断 MSB/LSB、极性和相位是否正确。很多人搜“spi 时序”往往遇到的是“SCK 有波形但 MOSI 不变”这类问题九成出在SPI_Direction被配成了只发送或只接收。4.4 从设备没有应答检查 GPIO 复用和 AF 映射STM32F429 的 SPI 引脚不是固定一组以 SPI2 为例PB13/PB14/PB15 是一组PI1/PI2/PI3 是另一组。CubeMX 生成工程会自动配置GPIO_PinAFConfig手写寄存器时少了这一步GPIO 就仍然工作在普通推挽模式SPI 外设信号根本到不了引脚。除了复用功能还要确认时钟SPI1 在 APB2 上SPI2/SPI3 在 APB1 上SPI4/SPI5/SPI6 在 APB2 上。很多人把 SPI2 的初始化抄成 SPI1导致RCC_APB1Periph_SPI2没有使能SPI 寄存器写了无数遍都没反应。排查时可以直接读RCC-APB1ENR或看SPI2-CR1是否已经置为可操作状态。5. 回环自检法不接外设也能验证 SPI 配置把 MOSI 和 MISO 短接是调试 SPI 最省钱的办法。不需要接任何从设备只要一根杜邦线就能验证 GPIO 复用、数据帧格式、时钟极性、分频系数是否有效。STM32F429 的 SPI2 在 PB15 和 PB14 上物理短接后主机发什么就能读回什么。如果回环读回的不是发送值说明配置一定有问题而且问题范围可以缩小要么是 GPIO 复用映射没有生效要么是 CPOL/CPHA 采样沿不对要么是 MISO 和 MOSI 引脚搞反。uint8_t SPI2_LoopbackTest(void) { uint8_t test_data[8] {0xA5, 0x5A, 0x01, 0xFE, 0x55, 0xAA, 0x80, 0x7F}; uint8_t rx_data[8]; uint8_t i; SPI2_Select(1); for (i 0; i 8; i) { rx_data[i] SPI2_TransferByte(test_data[i]); } SPI2_Release(); for (i 0; i 8; i) { if (rx_data[i] ! test_data[i]) return 1; } return 0; }测试数组里特意放了 0xA5、0x55、0xFE 这类特征数据可以同时暴露位序和相位问题。执行过程是先选中从机连续交换 8 个字节最后释放片选。如果返回值是 1就在调试器里对比test_data和rx_data。两个数组完全一致说明 SPI 配置正确问题在外部设备侧只有一个 bit 翻转先怀疑电气连接和共地。实际经验是 MISO 总是读到 0x00 或 0xFF多数不是 SPI 配置而是 GPIO 初始化没把引脚设成GPIO_Mode_AF或者 MOSI/MISO 接反。回环测试还可以用来验证波特率分频是否过快。把SPI_BaudRatePrescaler从 16 改成 4实测 SCK 频率提升后回环数据开始出错说明总线上负载或信号质量跟不上。这时计算实际波特率SPI2 挂在 APB1 上若 PCLK145MHz预分频 16则 SCK 等于 45MHz 除以 16结果是 2.8125MHz改成 4 就是 11.25MHz。很多从设备数据手册标称最大 SCK 是 10MHz超过这个值后读回数据会随机出错回环测试能提前暴露这个风险。把回环测试放在上电自检里每次复位后先确认 SPI 硬件链路正常再进入业务逻辑比在外设读写失败后查半天要高效得多。短接 MOSI/MISO 时记得先断开外部从设备否则测试结果会混入从设备的输出。回环通过之后再接传感器或 Flash刷新率和稳定性的问题就是另一套排查思路了。本文还有配套的精品资源点击获取