
简介一份基于STM32G0系列HAL库的SPI从机接收代码示例面向正在学习STM32硬件抽象层开发、需要快速上手SPI通信的嵌入式开发者。资源包共1138个文件约9.21MB以C源程序662个和头文件260个为核心涵盖HAL库驱动、应用逻辑与寄存器映射同时提供汇编启动文件、链接脚本、Keil工程文件及IOC配置等便于在真实工程中直接对照学习。已有238人浏览学习可作为从零配置SPI从机、理解中断接收流程和回调机制的参考。通过阅读代码读者能掌握STM32G0系列HAL库的SPI从机初始化、收发缓冲管理与错误处理思路并借助工程配置文件快速迁移到自己的板卡。1. 用 G0 调 SPI 接收别急着写代码先吃透这份最小工程调试一块 SPI 接口的从机传感器时示波器上 CS 和 SCLK 时序都正常寄存器却读回一整片 0xFF。折腾到晚上问题定位到 STM32G0 的 SPI 接收配置细节——从老平台迁移过来的惯性写法在 G0 上并不完全通用。这个 stmg0-spi-receive 资源包就是围绕这个现场拆出来的基于 HAL 库的 G0 SPI 接收最小工程包含阻塞、中断、DMA 三种接收接口的落地调法、从机模式下的时序注意点、以及几类高频问题的排查记录。适合正在调 SPI 从机协议栈、或从 F1/F4 换到 G0 后发现接收行为不一致的嵌入式开发者新手也能顺着工程结构完整走一遍接收链路。2. 解压后先看四件事工程结构、G0 平台差异与接收接口选型2.1 工程目录一条接收链路的骨架先把压缩包解开目录结构大致是这样stmg0-spi-receive/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ └── spi_receive.h │ └── Src/ │ ├── main.c │ ├── spi_receive.c │ └── system_stm32g0xx.c ├── Drivers/ │ ├── CMSIS/ │ └── STM32G0xx_HAL_Driver/ └── stmg0-spi-receive.iocCore/Src/main.c 是 CubeMX 生成的初始化与主循环入口spi_receive.c 是抽出来的接收逻辑模块.ioc 文件则记录了 CubeMX 工程配置可以随时打开重新生成代码。Drivers 里放的是 STM32G0 系列专用的 HAL 驱动这一点要特别注意不要图省事直接从 F1 工程里复制 HAL 库顶替G0 的寄存器定义与外设位字段和 F1 不是一套编译时大概率会在底层报错。我一般先确认 .ioc 用的芯片型号和自己手上的封装一致再去看 spi_receive.c 里封装了哪些对外函数。资源里如果自带 README 或者注释块优先读注释那通常是作者记录的调试环境与踩坑摘要。2.2 G0 的 SPI 与 F1/F4 的差异迁移时最容易踩的字段从 SPL 库时代迁移到 G0 HAL 库的人最先撞上的问题往往是SPI_InitTypeDef里没有SPI_Direction这个成员。F1 的 SPL 库里用它来设定双线全双工、双线只收、单线半双工到了 G0 的 HAL 库这个字段被拆进了Init.Direction取值变成了SPI_DIRECTION_2LINES、SPI_DIRECTION_2LINES_RXONLY、SPI_DIRECTION_1LINE三个宏。如果直接拿老代码编译会得到 undefined identifier而不是一个温和的警告。时钟配置也要重新核对。G0 系列的 SYSCLK 与 APB 分频关系跟 F1 不完全一样SPI 外设挂在 APB 总线上APB 分频系数直接影响 SPI 外设可用的时钟上限。以下代码段是 CubeMX 生成后常见的时钟初始化写法RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSI; RCC_ClkInitStruct.SYSCLKSource RCC_SYSCLKSOURCE_HSI; RCC_ClkInitStruct.APB1CLKDivider RCC_APB1_DIV1;这段配置把系统时钟源设为 HSIAPB1 分频设为 1 分频。对 SPI 接收而言APB1 分频太大会直接限制 SPI 外设最高工作频率导致同样的 SPI 分频系数在不同芯片上拿到完全不同的实际波特率。所以拿到陌生 G0 板卡时我一般先读 RCC 配置确认 APB 分频不是默认的 4 分频或 8 分频再谈后面的 SPI 参数。2.3 三种 HAL 接收接口按场景选型不是越高级越好HAL 库为 SPI 接收提供了三套并行接口选型的核心不是“哪个技术更先进”而是“当前阶段要验证什么”。下面的表可以直接对着选。接口阻塞行为CPU 占用适用场景错误处理路径HAL_SPI_Receive阻塞等待接收完成高短帧、调试期验证返回值是 HAL_OK / HAL_TIMEOUT / HAL_ERRORHAL_SPI_Receive_IT非阻塞中断服务收完低不定长、事件驱动错误状态在回调函数里查句柄HAL_SPI_Receive_DMA非阻塞DMA 搬运极低批量、连续、高吞吐检查半传输与完成回调阻塞接口适合第一次把链路跑通因为它把时序细节都藏进了函数内部返回值直观中断接口适合明确知道一帧有多长、且不想让 CPU 空转的场景DMA 接口则是在数据量大到中断频繁抢占主循环时才需要引入。调通之前不要直接上 DMA否则出错时你很难分辨是 DMA 通道配置问题还是 SPI 外设本身的问题。接收之前有一句状态检查很值得保留if (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_RXNE) SET) { /* RXNE1 表示接收数据寄存器里还有上一帧数据没被取走 */ }这句检查不参与正常流程但它能把问题拆成“硬件根本没收到”和“收到了但我没取走”两类。我排查接收问题时第一步就是打印这个标志位比盲目调整参数快得多。RXNE 是硬件状态寄存器里的真实位不经过中断回调它的状态比任何软件标志都可信。2.4 编译前先核对 HAL 版本与代码生成工具解压之后别急着按编译快捷键。用旧版 CubeMX 打开 .ioc 文件可能提示版本过低生成出来的 HAL 驱动与新库不兼容即便不重新生成代码直接编译源码也要确认 HAL 配置头文件里的模块开关是打开状态。/* stm32g0xx_hal_conf.h 中确认以下宏没有被注释 */ #define HAL_SPI_MODULE_ENABLED这个宏控制的是整个 HAL_SPI 模块的编译开关。如果它被注释或屏蔽所有HAL_SPI_开头的函数会全部编译失败报错还都是一堆“undefined reference”。遇到这种批量报错不要一个个去补函数先去翻 hal_conf.h。我见过有人在这里卡了一天最后发现只是开关没打开。3. 从 CubeMX 到调通阻塞与中断接收的完整落地步骤3.1 CubeMX 配置时钟、引脚与寄存器参数一页说清在 CubeMX 中先把具体型号选准不同封装的 G0 引脚数差异很大如果实际板子上 SPI 引脚和封装对不上后面会浪费大量时间。时钟树里先把 HSI 设为系统时钟源APB 分频做 1:1让 SPI 外设拿到一个相对宽裕的时钟上限。引脚部分需要确认四根线SCK、MISO、MOSI、CS如果使用硬件片选则必须配置。SPI 参数页是接收能否成功的核心下面这段是 CubeMX 生成代码里典型的 SPI1 从机初始化static void MX_SPI1_Init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_SLAVE; /* 从机模式 */ hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; /* 8bit 数据帧 */ hspi1.Init.CLKPolarity SPI_POLARITY_LOW; /* CPOL0 */ hspi1.Init.CLKPhase SPI_PHASE_1EDGE; /* CPHA0 */ hspi1.Init.NSS SPI_NSS_SOFT; /* 软件 NSS 管理 */ hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_2; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; /* 高位先出 */ hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; if (HAL_SPI_Init(hspi1) ! HAL_OK) { Error_Handler(); } }这里几个参数的实际意义要搞清楚。Mode SPI_MODE_SLAVE决定 SPI 外设不产生时钟只被动采样采样完全由主机的 SCLK 驱动。BaudRatePrescaler在主从模式下不一样主机模式下它决定 SCLK 频率从机模式下它并不会限制接收但会影响从机内部对时钟的容忍上限主机把 SCLK 拉太高时从机可能采错。NSS SPI_NSS_SOFT表示从机不依赖物理 CS 引脚的电平来界定一帧这在多字节接收时容易出问题后面第四章细说。3.2 阻塞接收把硬件链路走通的最近路径配置生成后第一步要做的不是写协议逻辑而是用阻塞接收把硬件链路验证一遍。下面的代码可以直接放进主循环uint8_t rx_buf[8] {0}; HAL_StatusTypeDef status; /* 参数SPI 句柄、接收缓冲、数据项个数、超时毫秒数 */ status HAL_SPI_Receive(hspi1, rx_buf, 8, 200); if (status HAL_OK) { /* rx_buf 里已经拿到 8 个字节 */ } else if (status HAL_TIMEOUT) { /* 200ms 内没有收满 8 字节优先排查主机是否发了时钟 */ } else { /* 参数错误或硬件错误先看 hspi1-ErrorCode */ }阻塞接收的底层逻辑是轮询 RXNE 标志SPI 外设每收到一个字节RXNE 置位HAL 函数把数据寄存器里的值搬进 rx_buf直到收满 8 个字节或超时。如果 HAL_OK 但数据全是 0xFF说明时钟采样沿有问题如果 HAL_TIMEOUT说明主机可能根本没发起传输或者 CS 引脚没有正确使能从机。判断这两个方向的关键就在返回值本身非常直观。接收之前可能残留上一个帧的数据这在热插拔从机、主机提前发起时钟时经常发生。我习惯在每次接收前把残留字节清掉uint8_t tmp; while (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_RXNE) SET) { tmp *(volatile uint8_t *)hspi1.Instance-RXDR; }这段代码通过直接读取硬件数据寄存器 RXDR 来消费掉残留数据volatile关键字防止编译器优化掉这次读操作。清残留不是 HAL 标准接口要求但能显著减少“偶尔多收一个字节”这类诡异现象。提示阻塞接收超时时间设多长取决于主机帧节奏。如果主机每 500ms 发一帧Timeout 至少留到 800ms否则阻塞接口会提前退出把后半帧丢掉。3.3 中断接收状态机动起来但要管好回调里的副作用链路验证通过后可以把接收逻辑切成中断方式释放 CPU 去处理其他任务。启动中断接收与回调代码如下if (HAL_SPI_GetState(hspi1) HAL_SPI_STATE_READY) { /* 状态机空闲时才提交新的接收任务 */ HAL_SPI_Receive_IT(hspi1, rx_buf, 64); } else { /* 上一次还没结束不应重复提交 */ } /* 接收完成回调由 HAL 在中断上下文里调用 */ void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { spi_frame_ready 1; /* 只置标志不做数据处理 */ } }HAL_SPI_Receive_IT一次只能提交一个接收任务底层状态机会从 READY 切到 BUSY收满 64 个数据项后回到 READY期间重复调用会返回HAL_BUSY。回调函数运行在中断上下文里面不能做耗时操作不能调用 HAL_Delay也不适合直接解析协议最稳妥的做法是只置一个标志位让主循环去处理数据。中断接收比阻塞更接近真实产品形态但它引入了一个新的复杂度主循环和中断回调共享变量。spi_frame_ready这个标志位如果只在主循环里清零而中断和主循环同时访问它就可能出现标志被覆盖的竞态问题。最简单的规避方式是在回调里只做置位、主循环里只做消费且用volatile修饰共享标志。4. SPI 接收常见问题排查四个翻车现场的现象、原因与解法4.1 读回一屏 0xFF时钟极性/相位不匹配现象主机一帧发完从机接收缓冲里全是 0xFF一个有效字节都没有。原因主从双方 CPOL 与 CPHA 不匹配。CPOL 决定空闲电平是高还是低CPHA 决定采样沿是第一个边沿还是第二个边沿。两组位组合出四种 SPI 模式只要有一方不一致采样时刻就落在 bit 数据以外读到的全是空闲电平。解决先固定主机侧的模式再去翻从机芯片数据手册里的时序图确认它在哪个边沿采样数据。绝大多数 SPI 器件默认工作在模式 0即下列参数hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE;对照示波器确认空闲时 SCLK 是低电平、第一个时钟边沿开始采样基本就能对上。有逻辑分析仪的话直接抓包看比反复改参数重新编译高效得多。4.2 前几字节对、后面全乱从机 NSS 管理方式不能想当然现象连续接收多字节时前 1-2 个字节正确后面的字节错位、丢失或出现多余数据。原因从机使用了软件 NSS 管理SPI_NSS_SOFT从机内部不感知物理 CS 引脚的电平变化。主机一侧的片选信号如果在一次传输中途出现短暂拉高或者帧长度和从机设定的接收长度不一致从机就会丢失帧边界进而错位。解决从机优先使用硬件 NSS 模式——SPI_NSS_HARD并确认 CS 引脚在 CubeMX 里复用为 SPI 的 NSS 功能而不是普通 GPIO。硬件 NSS 会把低电平持续期间视为一个完整的传输窗口从机只在窗口内采样天然规避了帧边界漂移。如果引脚资源紧张必须坚持软件 NSS那要保证主机的 CS 在整个 PDU 传输期间保持稳定不在中间拉高。4.3 中断接收只在第一次生效回调置位后没人清状态现象第一次调用HAL_SPI_Receive_IT能正常收满主循环处理完数据后再调第二次收不到任何数据了。原因典型的 HAL 状态机卡死。回调函数里直接再次调用HAL_SPI_Receive_IT会把新的接收任务叠在旧任务上而底层状态还没有完全回到 READY重复提交返回HAL_BUSY后无人处理后续不再有中断触发。解决回调里只置位spi_frame_ready主循环消费完数据后主动清除标志再显式提交下一轮接收。加一条打印确认返回值不是HAL_BUSYHAL_StatusTypeDef st HAL_SPI_Receive_IT(hspi1, rx_buf, 64); if (st ! HAL_OK) { /* 打印错误码重点看是不是 HAL_BUSY */ }从那以后我每次在中断接收里发现“只能工作一次”的问题都会先检查有没有在中断上下文里重复驱动 HAL 状态机。4.4 MISO 引脚电平钉死复用模式选错的典型症状现象SCLK 和 MOSI 波形正常MISO 却固定为高或低接收数据全 0 或全 0xFF。原因MISO 引脚没有正确配置为 SPI 复用功能。G0 的引脚复用表里一个引脚往往挂了多个外设功能AF 编号选错时不会报编译错误只是信号根本没连到 SPI 外设上。解决回 CubeMX 检查引脚颜色是否变成了 SPI 对应的复用色并对照数据手册的 AF 映射表确认引脚编号。有个快速验证办法初始化之后读一下 GPIO 的复用功能寄存器确认 AF 编号和预期一致。这类问题最迷惑人的地方在于电压、波形看起来都对实际上数据通路根本没接通属于典型的硬件配置型玄学问题只能靠严格走一遍 AF 映射表来排除。5. 收尾DMA 接收与一条回环验证命令5.1 什么时候把中断换成 DMA中断接收够用的时候不要急着上 DMA。DMA 的价值体现在持续大流量接收中断每收一字节都要进一次中断服务函数即便逻辑简单频繁进出也会占用 CPU 时间换成 DMA 后数据从 SPI 外设寄存器直接搬到内存CPU 几乎不参与搬运过程。单次启动 DMA 接收的代码比中断版更短HAL_SPI_Receive_DMA(hspi1, rx_buf, 128);但代价是调试复杂度上升。DMA 接收出现问题时你要同时查 SPI 状态、DMA 通道配置和内存缓冲三个环节所以我只建议在中断接收验证通过之后做这个升级。回调逻辑与中断版一致只是 HAL 会额外提供半传输回调适合做双缓冲处理——前半段收满时处理前半段后半段收满时处理后半段CPU 和 DMA 并行工作。5.2 拿到陌生 G0 板卡时我每次先做一遍回环验证在接任何真实从机设备之前我会先把 SPI 主机侧的 MOSI 与 MISO 用杜邦线短接构成一个板载回环。然后以主机模式发送一组固定字节同时启动接收校验收发数据是否一致uint8_t txbuf[2] {0xA5, 0x5A}; uint8_t rxbuf[2] {0x00, 0x00}; HAL_SPI_TransmitReceive(hspi1, txbuf, rxbuf, 2, 100); if (rxbuf[0] txbuf[0] rxbuf[1] txbuf[1]) { /* 主机侧 SPI 链路正常再接入从机设备 */ }HAL_SPI_TransmitReceive在主机模式下同时完成发送和接收两个动作回环短接时发出的字节会原样回到接收路径。这一步能把问题边界压缩到“主机这半边”如果回环都收不到说明板卡 SPI 初始化有问题如果回环正常但接从机后失败问题大概率在主从之间的极性、片选或从机配置上。我从那以后每次拿到 G0 板子都强制先走一遍回环、看一眼 RXNE、翻一遍 AF 映射表三遍下来才允许自己动业务逻辑。希望帮到你。本文还有配套的精品资源点击获取