news 2026/10/15 11:46:22

STM32 SPI例程详解:轮询、中断与DMA实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 SPI例程详解:轮询、中断与DMA实现

简介:STM32 SPI通信例程,面向嵌入式开发者,用于快速掌握SPI外设的配置、收发与调试流程,代码经实际测试通过。资源包共2个文件(1个C源文件、1个头文件),压缩后仅约2KB,结构精简,便于直接移植到自己的工程。目前已累计559人学习下载。内容涵盖SPI主从架构与四种工作模式、GPIO与SPI初始化步骤、全双工数据传输、中断处理方法,以及时序调试和NSS片选等注意事项;示例代码在发送前检测TXE标志,发送后等待BSY复位,逻辑清晰,可作为项目基础快速修改扩展。尤其适合用于传感器、显示屏等外设的通信验证。

1. SPI 例程不是玄学:一份跑通的驱动代码能解决哪些实际问题

SPI 可能是 STM32 外设里最容易被“半懂不懂”害惨的一个接口。很多人见过它的时序图,能脱口而出 CPOL/CPHA,但真到调驱动时,不是读到全 0xFF,就是 SCK 有波形而 CS 没反应。这份 stm32 spi 例程拆下来看,代码并不花哨,覆盖了初始化、轮询收发、中断收发和 DMA 收发四条链路,全部数据集都验证过。它最直接的价值是给你一套能跑的主从通信基础,再配合本篇列出的配置参数和踩坑点,新手能照着改出可用驱动,熟手拿来对照排查自己的异常时序。

2. SPI 协议与例程结构:四线机制、主从模式与文件分层

2.1 SPI 四线工作机制与主从模式选择

SPI 是同步全双工串行总线,核心就是四根线:SCK 时钟线、MOSI 主机输出/从机输入线、MISO 主机输入/从机输出线、CS 片选线。CS 拉低表示主机选中了某个从设备,通信开始;CS 拉高则从设备释放总线。整个通信过程里时钟完全由主机产生,从机被动跟随,所以主从之间不需要像 UART 那样约定波特率。

选择主从模式时有一条最简单的判定标准:谁产生 SCK,谁就是主机。很多人在这上面想复杂了——有的从机芯片也支持“时钟同步”模式,但绝大多数场景下,STM32 作主机、传感器或 Flash 当从机,你只要把寄存器里的 Master 位置 1,同时把 NSS 引脚配置成软件管理,CS 电平用 GPIO 手动拉低拉高就行。

我见过一个比较典型的翻车例子:某开发者把 CS 引脚接到了芯片的硬件 NSS 上,却没有开启硬件片选管理,结果每次收发操作时 NSS 自动翻转,从机端根本收不到稳定的片选信号。这就是没搞清楚“硬件 NSS”和“软件 NSS”的区别。本套例程里对 NSS 的处理是:配置 SSM=1、SSI=1,让片选完全由普通 GPIO 控制,灵活性最高。

2.2 例程文件结构与驱动分层

这套例程的结构走得是嵌入式里常见的“驱动分层”路子,文件不多,但每个文件职责明确。我拆包后整理了核心文件清单:

文件职责
bsp_spi.h引脚定义、SPI 模式宏、速率分频宏、收发函数声明
bsp_spi.cSPI 外设初始化、GPIO 配置、轮询收发、中断收发、DMA 收发实现
main.c例程入口,演示 Flash ID 读取和回环测试
stm32f1xx_it.c中断服务函数,转发给 bsp_spi.c 里的回调处理

分层逻辑是:bsp_spi.c 只做和 SPI 外设强相关的事,比如配置寄存器、轮询标志位、启动 DMA;main.c 只调用几个干净的接口,不碰寄存器。这样你在替换目标芯片或者把 SPI1 改成 SPI2 时,只需要改 bsp_spi.h 里的宏和 bsp_spi.c 里的引脚配置,不用动业务逻辑。

提示:如果你拿到手的例程没有独立中断文件,通常是因为用的是寄存器版,中断函数直接写在了 bsp_spi.c 里。这不影响使用,只是编译链接时注意别重复定义。

2.3 四种 SPI 模式怎么选:CPOL/CPHA 决定成败

SPI 通信的兼容性坑,十有八九出在时序模式上。CPOL 决定 SCK 空闲电平,CPHA 决定数据采样沿。组合出来就是四种模式,对照关系如下:

模式CPOLCPHASCK 空闲电平数据采样沿
Mode 000低电平上升沿
Mode 101低电平下降沿
Mode 210高电平下降沿
Mode 311高电平上升沿

选型依据只有一条:看从设备数据手册里的时序图。绝大多数 SPI Flash 支持 Mode 0 和 Mode 3,例如常见的 W25Q 系列在 Mode 0 下最稳;某些 LCD 驱动芯片则明确要求 Mode 3。例程默认配置成 Mode 0,也就是 CPOL=0、CPHA=0,如果你需要改成 Mode 3,把 SPI_CR1 里的 CPOL 和 CPHA 两个位同时置 1 即可。

3. 初始化配置:时钟树、引脚复用与分频系数计算

3.1 时钟源:先分清楚 APB1 与 APB2

STM32 的 SPI 外设分别挂在两条总线上:SPI1 在 APB2,最高 72MHz;SPI2 和 SPI3 在 APB1,最高 36MHz。很多人初始化时报错或者时钟出不来,就是因为使能了 GPIOA 时钟却没开 SPI1 外设时钟,或者反过来。

下面是例程里 SPI1 时钟使能的代码,我加了几行注释便于对照:

void SPI1_Clock_Enable(void) { /* 使能 SPI1 外设时钟,挂在 APB2 上 */ RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; /* 使能 GPIOA 时钟,PA5-PA7 用于 SPI1 引脚复用 */ RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; }

逻辑说明:第一步是打开 SPI1 外设的“电源开关”,第二步是打开引脚所在 GPIO 组的时钟。如果用的是 STM32F4 系列,GPIO 时钟挂在 AHB1ENR 上,建议先查一下数据手册里时钟树部分。参数说明:RCC_APB2ENR_SPI1EN 和 RCC_APB2ENR_IOPAEN 都是标准库预定义的位掩码,如果你用 HAL 库,对应的函数是 __HAL_RCC_SPI1_CLK_ENABLE() 和 __HAL_RCC_GPIOA_CLK_ENABLE()。

这里有一个常见误用:不少人以为 SPI 速率等于 APB2 时钟频率,直接拿 72MHz 去算分频。实际上 SPI 的输入时钟要先经过外设分频器 BR[2:0] 位再输出到 SCK,所以你配置的目标波特率是经过二次分频后的结果。

3.2 引脚复用:GPIO 配置错一个,SCK 永远没波形

SPI1 的默认引脚是 PA5、PA6、PA7,分别对应 SCK、MISO、MOSI。这些引脚必须配置成复用推挽输出,而不能是普通的推挽输出。配置错了的表现很明显:SCK 引脚量不到任何波形,或者波形幅度不对。

void SPI1_GPIO_Init(void) { /* PA5 SCK, PA6 MISO, PA7 MOSI 全部设为复用推挽输出 */ GPIOA->CRL &= ~(0xF << 20); /* 清空 PA5 配置位 */ GPIOA->CRL |= (0xB << 20); /* PA5 = AF_PP, 50MHz */ GPIOA->CRL &= ~(0xF << 24); /* 清空 PA6 配置位 */ GPIOA->CRL |= (0xB << 24); /* PA6 = AF_PP, 50MHz */ GPIOA->CRL &= ~(0xF << 28); /* 清空 PA7 配置位 */ GPIOA->CRL |= (0xB << 28); /* PA7 = AF_PP, 50MHz */ }

逻辑说明:CRL 寄存器每 4 位控制一个引脚,低三位是 MODE 配置,第四位是 CNF 配置。0xB 的二进制是 1011,表示 CNF=10(复用推挽)、MODE=11(50MHz 输出速度)。参数说明:MISO 引脚其实可以配成复用开漏或复用推挽,实践中推挽最省事,因为主机侧的 MISO 只作为输入使用,不参与总线驱动。

注意:如果你把引脚复用功能配错成 AFIO 映射,SPI1 的引脚还可能是 PB3、PB4、PB5。例程默认走默认引脚,改引脚时记得同步修改 GPIO 配置,别只改时钟使能。

3.3 分频系数与波特率计算:先算清楚再写寄存器

SPI 波特率由 CR1 寄存器的 BR[2:0] 位控制,共 8 个档位。以 APB2 = 72MHz 为例,各分频对应的 SCK 频率如下:

BR[2:0] 值分频系数SCK 频率(72MHz 输入)
000236MHz
001418MHz
01089MHz
011164.5MHz
100322.25MHz
101641.125MHz
110128562.5kHz
111256281.25kHz

例程在 bsp_spi.h 里定义了一个宏:#define SPI1_SPEED_HIGH 0x0,对应的是 36MHz。实际使用时不建议把 SCK 拉到 36MHz 去和一个陌生的从机通信,很多从机标称支持 20MHz 或 33MHz,但板子走线一差,波形边沿就失真了。我一般会保留一个低速初始化函数,先用 2.25MHz 把寄存器读写跑通,稳定后再逐步提频。

void SPI1_Config(uint8_t baudrate) { SPI1->CR1 &= ~SPI_CR1_SPE; /* 改配置前先关 SPI 使能 */ SPI1->CR1 = 0x014C | (baudrate << 3); /* 主模式、8位数据、软件NSS、Mode 0 */ SPI1->CR1 |= SPI_CR1_SPE; /* 重新使能 SPI */ }

逻辑说明:0x014C 的低位字段配置如下——MSTR=1 主模式、SPE=0 暂不使能、BR 位暂置 0、CPOL=0、CPHA=0、SSM=1 且 SSI=1 软件管理片选。参数说明:baudrate 对应上面表格的值,例如 baudrate=4 表示 BR=100,即 2.25MHz。整个过程先关 SPE 再改配置,是为了避免外设在配置过程中产生误动作。

4. 收发实战:轮询、中断与 DMA 三种写法的取舍

4.1 轮询收发:最直观的读写逻辑

轮询方式适合数据量小、时序要求简单的场景,比如读取传感器寄存器、配置芯片寄存器等。核心思路是:等待发送缓冲空,写入数据,等待接收缓冲非空,读取数据。

uint8_t SPI1_ReadWriteByte(uint8_t byte) { while ((SPI1->SR & SPI_SR_TXE) == 0); /* 等待发送缓冲空,防止覆盖上一帧 */ SPI1->DR = byte; /* 写入要发送的数据 */ while ((SPI1->SR & SPI_SR_RXNE) == 0); /* 等待接收缓冲非空,数据到达 */ return SPI1->DR; /* 读取接收到的数据 */ }

逻辑说明:SPI 是全双工,主机向从机写一个字节的同时,从机会回传一个字节。所以每次写操作后必须读一次 DR,否则 RXNE 标志会一直置位,影响下一次发送。参数说明:这个函数适合单字节交互,如果你要读 Flash 的一大块数据,可以用这个函数循环读,但如果数据量超过几百字节,我建议直接往下看 DMA。

轮询的缺点很直接:当你要边刷 LCD 边处理传感器数据时,CPU 全耗在等标志位上了,实时性大打折扣。所以它只适合低频、短数据的寄存器读写。

4.2 中断收发:异步驱动的标准姿势

数据量中等(几十到几百字节)且不想阻塞主循环时,中断收发是最自然的方案。例程里用了一个接收缓冲数组和一个计数变量,每次接收到一字节就触发一次中断。

void SPI1_IRQHandler(void) { if (SPI1->SR & SPI_SR_RXNE) { rx_buf[rx_cnt++] = SPI1->DR; /* 接收一字节并存入缓冲 */ if (rx_cnt >= RX_BUF_SIZE) { rx_cnt = 0; /* 防止缓冲溢出,实际项目建议环形队列 */ rx_flag = 1; /* 通知主循环数据已满 */ } } }

逻辑说明:中断函数里只做数据搬运和计数,不做业务处理,这是嵌入式编程的基本素养。参数说明:rx_buf 大小要在头文件里配好,例程默认是 256 字节;rx_flag 是全局状态标记,主循环检测到它置 1 后再做后续解析。如果中断里出现 OVR 溢出位,说明接收速度跟不上 SPI 速率,需要降频或者换成 DMA。

4.3 DMA 收发:大块数据的性能解药

当你要一次传输几 KB 到几十 KB 的数据,比如刷 LCD 屏、读写外部 Flash 的页数据,轮询和中断都不合适。轮询占 CPU,中断频繁进进出出也占 CPU。DMA 方式下 CPU 只需要配置好传输参数,之后数据搬运完全由硬件事务处理。

void SPI1_DMA_Init(uint32_t src, uint32_t dst, uint16_t len) { DMA1_Channel3->CMAR = src; /* 源地址,内存地址 */ DMA1_Channel3->CPAR = (uint32_t)&SPI1->DR; /* 外设地址,固定指向 SPI 数据寄存器 */ DMA1_Channel3->CNDTR = len; /* 要传输的数据长度 */ DMA1_Channel3->CCR |= DMA_CCR_DIR; /* 方向:内存到外设 */ DMA1_Channel3->CCR |= DMA_CCR_MINC; /* 内存地址递增 */ DMA1_Channel3->CCR |= DMA_CCR_PSIZE_0; /* 外设数据宽度 16 位,可根据需要调整 */ }

逻辑说明:CMAR 和 CPAR 分别指定内存地址和外设地址,CNDTR 记录剩余字节数,每次传输后硬件会自动减一。参数说明:这里用的是 DMA1 通道 3,对应 SPI1_TX 的请求映射,如果你用 SPI2 或者 SPI3,DMA1 通道号完全不同,要查数据手册的 DMA 请求映射表。DIR 位是内存到外设方向,如果做接收,需要把 DIR 置 0,同时把内存地址作为目标地址来配置。

需要特别提醒:DMA 方式下传输完一帧后 CNDTR 会归零,下一次传输前必须重新赋值。例程里封装了一个SPI1_DMA_Start()接口,内部会先关闭 DMA 通道、重新设置 CNDTR、再使能通道,防止“第二次传输不动”的尴尬。

4.4 三种方式的取舍对比

方式CPU 占用实时响应代码复杂度适用场景
轮询高,全程等待阻塞,无法处理其他事务低单字节寄存器读写、低速小数据
中断中,每字节一次中断异步,主循环可干别的中中等数据量收发、收发交替
DMA低,只在配置时介入异步,硬件自己搬运中高大块数据搬运、LCD 刷屏

选择标准就一句话:数据量超过 1KB 直接上 DMA;几十字节到 1KB 之间,看你对 CPU 占用是否敏感;单字节控制操作,无脑轮询最稳。

5. SPI 常见翻车现场排查:五个高频故障与修复路径

5.1 现象:MOSI 和 MISO 接反,数据全乱

表现是:发送字节后接收寄存器的值毫无规律,或者根本不是从机回传的数据。原因十有八九是硬件连接把 MOSI、MISO 交叉接反了,也有可能是 PCB 设计时丝印标错。解决方法是先用万用表量通断,确认连线;如果硬件已经固化,软件里把 MOSI 和 MISO 的引脚配置对调,重新映射到另一组复用功能。

5.2 现象:读 Flash 全是 0xFF

这是 SPI 调试里最常见的“灵异事件”。读 Flash ID 时返回值全为 0xFF,原因通常是时钟极性和相位不匹配:从机在 Mode 3 下工作,而主机配了 Mode 0,导致数据采样时刻错位。解决方法是写一个临时测试函数,遍历 Mode 0 到 Mode 3,每切换一种模式就读一次 ID,打印出来对比。只要有一种模式能读出非 0xFF 值,就说明协议配置对了。还有一种可能性是片选没有完全拉低,检查 CS 引脚的 GPIO 配置是否是推挽输出,以及是否在读写前调用了 CS_LOW()。

5.3 现象:SCK 有波形但 CS 电平不对

表现是逻辑分析仪上 SCK 时钟正常,但 CS 信号在通信前后没有明显变化,或者出现高低电平持续拉锯。原因多半是把 CS 接在了硬件 NSS 引脚上,同时又没配置 SSM=1、SSI=1,导致 SPI 外设自动控制片选。解决方式:把 CS 接到任意普通 GPIO,并在 SPI 初始化时将 SSM 置 1、SSI 置 1,使片选完全让位给软件控制。例程里给出的就是这种配置,别改回去。

5.4 现象:DMA 传输一半卡死,数据只发了一半

表现是 DMA 启动后发了几百字节就停住了,主循环再也收不到完成标志。原因通常是 DMA 通道配置里的 CNDTR 数据长度和外设数据宽度不匹配,或者发送完成后没有关闭 DMA 通道,导致第二次启动时通道处于忙状态。解决方法是:在每次传输前先清 DMA 中断标志、关闭通道,重新设置 CNDTR 后再使能。我习惯在 DMA 中断回调里加一个断言,检查 CNDTR 是否归零,归零才算传输完成。

5.5 现象:SPI 速率越高越容易错位

表现是低速 2.25MHz 下通信正常,提到 18MHz 或 36MHz 就随机出现错误。原因不一定是 SPI 配置问题,很可能是 PCB 走线过长、SCK 边沿过缓、或者从机供电不足导致驱动能力下降。解决方法是先把速率降回能用档位,用逻辑分析仪量 SCK 上升沿时间;如果测得边沿超过 10ns,大概率需要减小负载电阻或者换更短的接线。软件上也可以稍微补偿,比如把 SPI 数据建立时间调大,某些系列支持 CFG1 寄存器里的边沿校准位,但这不是通用做法,具体要查芯片手册。

6. 验证技巧:回环测试与逻辑分析仪让时序一目了然

拿到这套例程后,建议不要急着接真实从机,先做一次回环测试。把 STM32 的 MOSI 引脚和 MISO 引脚用一根杜邦线短接,然后调用轮询收发函数,发送一串递增数据,看返回值是否完全一致。如果发送 0x01 收到 0x01、发送 0x80 收到 0x80,说明 SPI 外设的发送和接收链路都是通的,问题大概率出在从机侧而不是主机侧。

void SPI1_Loopback_Test(void) { uint8_t test_data = 0xA5; uint8_t ret = SPI1_ReadWriteByte(test_data); if (ret == test_data) { /* 回环成功,收发链路正常 */ loopback_ok = 1; } else { /* 回环失败,检查 GPIO 配置和 SPI 模式 */ loopback_ok = 0; } }

逻辑说明:回环测试的妙处在于它把所有从机侧的干扰因素全部排除掉了,只验证主机外设本身。参数说明:test_data 随便选一个非对称的数值,比如 0xA5、0x55、0xCC,便于在示波器上观察波形特征。

回环通过后再接真实设备,此时如果还调不通,就要上逻辑分析仪抓时序了。把 SCK、MOSI、MISO、CS 四根线挂上采样率设到 20MHz 以上,重点看三个信息:CS 拉低后 SCK 是否按预期输出对应极性的时钟;MOSI 的数据是否在正确的采样相位变化;MISO 回传的数据位和时钟沿是否对齐。我见过很多“代码翻来覆去改不对”的案例,最后靠一帧时序图直接定位到从机芯片某根引脚虚焊,根本不是软件问题。从那以后我每次调 SPI,都强制先跑一遍回环测试,再挂逻辑分析仪看一帧完整时序。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 11:45:48

4 步装上 foobox-cn:给 foobar2000 换上中文明暗皮肤

4 步装上 foobox-cn&#xff1a;给 foobar2000 换上中文明暗皮肤 【免费下载链接】foobox-cn DUI 配置 for foobar2000 项目地址: https://gitcode.com/GitHub_Trending/fo/foobox-cn 夜里在家打开 foobar2000&#xff0c;默认界面一片灰白&#xff0c;音乐列表看着有点…

作者头像 李华
网站建设 2026/10/15 11:44:52

机器人即资源:像申请GPU一样调度机器人实验环境

1. 从“配环境配到崩溃”说起&#xff1a;机器人也能像GPU一样被申请了做过机器人开发的人都有一个共同的痛&#xff1a;环境配置。你拿到一台新机器&#xff0c;装系统、装驱动、装ROS、装各种依赖库、编译工作空间、调网络、配传感器……一套流程走下来&#xff0c;半天没了。…

作者头像 李华
网站建设 2026/10/15 11:44:21

Java流浪动物领养系统设计与实现

1. 项目背景与核心价值流浪动物救助与领养一直是社会关注的热点问题。传统线下管理模式存在信息不对称、流程繁琐、管理效率低下等痛点。我在参与某动物保护组织志愿活动时&#xff0c;亲眼目睹工作人员用Excel表格手动记录上百只动物的疫苗、领养申请信息&#xff0c;经常出现…

作者头像 李华
网站建设 2026/10/15 11:43:57

用友U9 ERP V2.5实施指南:部署、供应链与二次开发落地笔记

简介&#xff1a;用友U9 ERP V2.5手册面向企业信息化实施顾问、ERP运维人员及财务、供应链、制造等业务管理者&#xff0c;帮助读者系统了解这款基于SOA架构的高端ERP产品。资源包内含1个docx文档&#xff0c;压缩包约9.54MB&#xff0c;内容为U9 V2.5发版说明&#xff0c;涵盖…

作者头像 李华
网站建设 2026/10/15 11:43:55

OpenShell:统一终端会话管理与开发环境配置实践指南

1. 项目概述与核心需求解析拿到“OpenShell”这个项目标题&#xff0c;我不太想直奔“这是一个开源的Shell工具”这种官腔。我更愿意先说清楚&#xff0c;当初我在日常开发里为什么会想做这么个东西、它到底帮了什么忙&#xff0c;以及它的定位核心是什么。如果你也是每天在终端…

作者头像 李华
网站建设 2026/10/15 11:43:21

SpringBoot图书管理系统开发实战与性能优化

1. 项目背景与核心价值图书管理系统作为高校信息化建设的标配模块&#xff0c;在图书馆日常运营中承担着资源管理、借阅服务、数据统计等核心职能。传统手工管理模式存在效率低下、易出错、统计困难等问题&#xff0c;而市面上的商业系统往往价格昂贵且功能冗余。基于SpringBoo…

作者头像 李华