简介:这份资源是面向STM32开发者、嵌入式初学者及需要实现SPI从机通信的工程师的HAL库代码示例包,聚焦于STM32G0系列微控制器上SPI从机接收功能的完整工程实现。包内共1138个文件,以662个C源文件与260个头文件为主体,涵盖HAL库驱动、外设初始化与业务逻辑代码;另有103个汇编文件、73个文本说明、24个IAR链接配置及Keil工程文件等,压缩包约9.21MB,工程结构完整,可直接导入编译调试。资源围绕SPI从机接收实验展开,读者可借此理解SPI总线在实际应用中的时序与数据交互方式,掌握STM32 HAL库的编程方法与从机端配置要点,并参考其目录组织与驱动分层思路进行二次开发。目前已有238人学习下载,适合希望快速上手STM32 SPI从机通信、对照代码排查配置问题的开发者参考使用。
1. 从一份只有文件名的压缩包说起:stm32 spi hal 接收端到底该怎么落地
拿到stmg0-spi-receive.rar这个包的时候,我第一反应是:名字起得够直白,stm32 spi hal接收端,连猜都不用猜。但真拆开跑起来,才发现 SPI 从机接收这件事,坑比想象中多。很多教程只讲主机发、从机收的“理想链路”,一到真实工程就翻车——时钟极性对不上、NSS 管理方式选错、DMA 和中断打架,数据要么收不全,要么直接卡死在HAL_SPI_Receive里出不来。
这份资源适合两类人:一是刚上手 STM32 HAL 库、想找一个能直接跑的 SPI 从机接收例程的新手;二是已经在用标准库或寄存器、准备迁移到 HAL 的熟手,想看看 HAL 下接收链路该怎么组织。它不是一个“教学 PPT 式”的 Demo,而是一个可以烧进去、用逻辑分析仪抓波形、再按自己需求改的工程骨架。下面我按“能干什么 → 怎么跑 → 怎么改”的顺序,把这份包里的东西拆开讲。
2. 工程结构与接收链路:从时钟配置到中断回调的完整路径
2.1 目录里有什么,每个文件负责哪一段
这类 STM32 HAL 工程,目录结构基本是固定的。以stmg0-spi-receive为例,常见做法是分成三层:CubeMX 生成的初始化层、HAL 库层、用户应用层。我一般会先看Core/Src和Core/Inc,因为接收逻辑的入口和回调都在这。
stmg0-spi-receive/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── stm32g0xx_hal_conf.h │ │ └── stm32g0xx_it.h │ └── Src/ │ ├── main.c // 初始化 + 主循环 │ ├── stm32g0xx_it.c // 中断服务函数 │ └── stm32g0xx_hal_msp.c // 底层引脚/时钟使能 ├── Drivers/ │ ├── STM32G0xx_HAL_Driver/ │ └── CMSIS/ ├── MDK-ARM/ 或 STM32CubeIDE/ └── stmg0-spi-receive.ioc // CubeMX 工程文件main.c里做三件事:HAL_Init()、SystemClock_Config()、MX_SPI1_Init()。接收动作通常不在主循环里轮询,而是靠中断或 DMA 触发。stm32g0xx_it.c里放SPI1_IRQHandler,它只调用HAL_SPI_IRQHandler(&hspi1),真正的数据处理在HAL_SPI_RxCpltCallback里。这个分层的好处是:中断服务函数保持极短,业务逻辑不阻塞后续接收。
提示:如果你拿到的包是 Keil 工程,先确认
Drivers文件夹完整,很多人只拷了Core,编译时满屏undefined reference to HAL_SPI_Init。
2.2 SPI 从机初始化的关键参数:CPOL、CPHA 和 NSS
SPI 接收能不能通,八成取决于初始化参数和主机是否匹配。HAL 里SPI_HandleTypeDef的几个字段必须和主机端严格一致,否则你收到的就是错位数据。
/* SPI1 参数配置,从机模式,接收方向 */ hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_SLAVE; // 从机模式 hspi1.Init.Direction = SPI_DIRECTION_2LINES;// 全双工,接收时也要给时钟 hspi1.Init.DataSize = SPI_DATASIZE_8BIT; // 数据宽度 8 位 hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0,空闲低电平 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0,第一个边沿采样 hspi1.Init.NSS = SPI_NSS_SOFT; // 软件管理 NSS hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2; // 从机下分频影响不大 hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; // 高位先出 if (HAL_SPI_Init(&hspi1) != HAL_OK) { Error_Handler(); }这里有几个参数说明。Mode设为SPI_MODE_SLAVE后,时钟由主机提供,从机不需要关心波特率,但BaudRatePrescaler仍要填一个合法值。NSS用SPI_NSS_SOFT时,需要手动把 NSS 引脚拉低或通过__HAL_SPI_ENABLE控制;如果硬件上 NSS 已经接地,用SPI_NSS_HARD_INPUT更省事。CLKPolarity和CLKPhase必须和主机一致,常见组合是LOW/1EDGE或HIGH/2EDGE,对不上就是玄学错位。
2.3 接收启动与回调:中断方式怎么接、DMA 方式怎么切
初始化完只是把外设配好,真正开始接收要调用HAL_SPI_Receive_IT或HAL_SPI_Receive_DMA。中断方式适合小数据量、低频场景;DMA 适合连续大批量接收,能解放 CPU。
/* 在 main 初始化完成后启动一次接收 */ uint8_t rx_buf[32]; /* 中断方式:收到 32 字节后进回调 */ HAL_SPI_Receive_IT(&hspi1, rx_buf, sizeof(rx_buf)); /* 回调函数里处理数据,并重新启动接收 */ void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { /* 此时 rx_buf 里是完整一帧,做解析或置标志 */ process_spi_frame(rx_buf, sizeof(rx_buf)); /* 重新挂起接收,否则只能收一次 */ HAL_SPI_Receive_IT(&hspi1, rx_buf, sizeof(rx_buf)); } }逻辑说明:HAL_SPI_Receive_IT是“一次性”的,收完指定长度就进回调,不会自动续接。所以回调里必须再次调用,否则你只能收到第一帧。参数rx_buf要保证在接收期间不被释放或改写,局部数组如果出了作用域就会踩内存。DMA 方式同理,把_IT换成_DMA,并在HAL_SPI_RxCpltCallback里重新挂起。如果数据长度不固定,常见做法是配合HAL_SPIEx_EnableSSN或改用HAL_SPI_Receive_IT逐字节接收,在回调里判断帧头帧尾。
3. 跑起来:从 CubeMX 配置到逻辑分析仪抓波形的实操步骤
3.1 用 CubeMX 打开 .ioc,先核对这三处
包里如果带了.ioc文件,直接用 STM32CubeMX 打开,比手动改代码快。打开后先看三处:SPI1 的 Mode 是不是Slave、NSS 是不是Disable或Input、GPIO 里 SCK/MISO/MOSI 有没有被正确分配。很多人移植时只改了代码,忘了 CubeMX 里 SPI 还挂在主机模式,结果时钟死活不动。
配置完成后点Project Manager,工具链选你用的 IDE,勾选Generate peripheral initialization as a pair of .c/.h files。这样生成的spi.c和spi.h会把初始化独立出来,方便你后面改参数不影响main.c。
3.2 编译烧录:Keil 和 CubeIDE 的差异点
Keil 工程直接打开.uvprojx,确认Options for Target里晶振频率和Target芯片型号一致。STM32G0 系列如果选错型号,下载会报Flash Download failed。CubeIDE 工程则检查Debug配置里的 ST-Link 是否识别到芯片。
烧录前建议先把主机端准备好。主机可以用另一块 STM32 跑 SPI 发送,也可以用树莓派或逻辑分析仪模拟。主机端参数必须和从机一致:8 位、MSB、CPOL/CPHA 匹配、NSS 拉低有效。
# 以 STM32CubeProgrammer CLI 为例,烧录从机固件 STM32_Programmer_CLI -c port=SWD -w stmg0-spi-receive.bin 0x08000000 -v -rst这条命令里-c port=SWD指定调试口,-w写固件,0x08000000是 G0 系列 Flash 起始地址,-v校验,-rst写完复位。如果你用 IDE 直接点下载,忽略这条。
3.3 验证接收:先看波形,再看内存
烧录后别急着看串口打印,先上逻辑分析仪。抓 SCK、MOSI、NSS 三根线。主机发0x55,从机rx_buf里应该出现0x55。如果 SCK 有波形但rx_buf全是 0,检查HAL_SPI_Receive_IT是否在主机发数据前就调用了。如果数据错位一位,基本是 CPHA 设反了。
没有逻辑分析仪的话,可以在回调里翻转一个 GPIO,用示波器看回调有没有进。我一般会在HAL_SPI_RxCpltCallback里加一句HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin),回调进了灯就闪,比打印快。
注意:调试 SPI 从机时,主机不要连续发太快。从机中断处理如果耗时过长,下一帧 NSS 拉低时可能还没重新挂起接收,导致丢帧。常见做法是把处理逻辑放到主循环,回调里只置标志。
4. 避坑与排查:SPI 从机接收最容易翻车的五个点
4.1 现象:HAL_SPI_Receive_IT返回 HAL_BUSY
原因通常是上一次接收还没完成,或者 SPI 处于禁用状态。HAL 库对同一外设的接收句柄有状态锁,hspi->State不是HAL_SPI_STATE_READY时直接返回HAL_BUSY。解决方法是检查是否在回调里重复调用了接收,或者主机 NSS 一直拉低导致从机认为总线忙。可以在调用前打印hspi1.State,确认状态。
4.2 现象:收到数据但全是 0xFF 或 0x00
先查 CPOL/CPHA。CPHA 设错时,采样点落在数据跳变沿上,读到的就是固定电平。其次查 MISO/MOSI 是否接反。从机接收看的是 MOSI,如果主机和从机都按“自己视角”接线,很容易把 MISO 当 MOSI。用万用表通断档确认一下,比猜快。
4.3 现象:只能收第一帧,后面没反应
这是最典型的“忘了重新挂起”。HAL_SPI_Receive_IT不是循环接收,收完就停。必须在HAL_SPI_RxCpltCallback里再次调用。如果用了 DMA,还要检查 DMA 通道是否被关闭,HAL_SPI_Receive_DMA内部会配置 DMA,回调后同样要重新调用。
4.4 现象:数据偶尔错一位,概率性出现
多半是时钟相位和主机不完全匹配,或者从机中断优先级太低被其他中断打断。SPI 接收对时序敏感,如果系统里有更高优先级中断长时间占用 CPU,从机可能来不及读 DR 寄存器。解决方法是把 SPI 中断优先级设高,或者改用 DMA 减少 CPU 介入。另外检查主机 SCK 频率,从机在 HAL 下虽然不限制波特率,但 CPU 主频太低时高速 SCK 仍可能丢数据。
4.5 现象:编译报错HAL_SPI_MspInit重复定义
CubeMX 生成的stm32g0xx_hal_msp.c里已经有HAL_SPI_MspInit,如果你手动又写了一个,链接就会报重复。解决方法是只保留一个,通常保留 CubeMX 生成的,在里面加 GPIO 和时钟使能。如果自己写,就把 CubeMX 生成的那份删掉或改名。
5. 二次开发:把接收例程改成你自己的协议解析器
5.1 从固定长度到变长帧:状态机接收法
例程里收的是固定 32 字节,实际项目里帧长往往不固定。我一般会把接收拆成“逐字节中断 + 状态机”,在回调里判断帧头、长度、校验。这样不依赖固定长度,也不怕主机发一半停住。
typedef enum { WAIT_HEAD, GET_LEN, GET_DATA, GET_CRC } rx_state_t; static rx_state_t state = WAIT_HEAD; static uint8_t frame[64]; static uint8_t idx = 0, expect_len = 0; void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { uint8_t byte = frame[0]; // 单字节接收时暂存 switch (state) { case WAIT_HEAD: if (byte == 0xAA) { frame[idx++] = byte; state = GET_LEN; } break; case GET_LEN: expect_len = byte; frame[idx++] = byte; state = GET_DATA; break; case GET_DATA: frame[idx++] = byte; if (idx >= expect_len + 2) state = GET_CRC; break; case GET_CRC: frame[idx++] = byte; if (crc_check(frame, idx)) process_frame(frame, idx); idx = 0; state = WAIT_HEAD; break; } HAL_SPI_Receive_IT(&hspi1, &byte, 1); // 继续收下一个字节 } }这段代码的关键是每次只收 1 字节,回调里跑状态机。参数frame要足够大,expect_len来自帧内长度字段,crc_check自己实现。缺点是每字节进一次中断,高频率下 CPU 占用高;优点是逻辑清晰,不怕变长。如果数据量大,可以改成 DMA 收固定块,再在块内跑状态机。
5.2 双缓冲接收:避免处理数据时丢帧
当主机连续发送、帧间隔很短时,单缓冲会在处理上一帧时丢掉下一帧。常见做法是准备两个缓冲区,DMA 收满一个就切换,回调里处理另一个。
uint8_t buf_a[64], buf_b[64]; uint8_t *active_buf = buf_a; void start_receive(void) { HAL_SPI_Receive_DMA(&hspi1, active_buf, sizeof(buf_a)); } void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (active_buf == buf_a) { process_frame(buf_a, sizeof(buf_a)); active_buf = buf_b; } else { process_frame(buf_b, sizeof(buf_b)); active_buf = buf_a; } HAL_SPI_Receive_DMA(&hspi1, active_buf, sizeof(buf_a)); }这里active_buf指向当前 DMA 目标,回调里先处理已收满的,再切换。注意 DMA 传输期间不要改写active_buf指向的内容,否则数据竞争。如果处理时间可能超过一帧时间,双缓冲也不够,得用环形缓冲队列。
5.3 验证改造是否成功:三个必查项
改完代码后,我习惯按三步验证。第一,用逻辑分析仪确认 NSS、SCK 波形和主机一致,没有毛刺。第二,在回调里加计数器,主机发 100 帧,看从机计数是不是 100,丢帧就查缓冲和优先级。第三,故意发一帧错误 CRC,看状态机能不能回到WAIT_HEAD,不会卡死。这三步走完,基本能确定接收链路稳了。
从那以后我每次改 SPI 从机接收,都强制先跑一遍“单字节回环 + 逻辑分析仪抓 NSS”再往下写协议,不然调到最后都不知道是时序问题还是代码问题。希望这份拆解能帮你把stmg0-spi-receive跑通,再按自己的协议改出能用的接收端。
本文还有配套的精品资源,点击获取