深入掌握STM32F4的SPI主从通信:从原理到实战的完整工程实践
在嵌入式系统开发中,当你需要读取一个高精度ADC、驱动一块OLED屏幕,或者与多路传感器高速交互时,SPI(Serial Peripheral Interface)几乎总是首选方案。它不像I²C那样受限于地址冲突和低速瓶颈,也不像UART只能半双工传输——SPI以其全双工、高速、同步串行的特点,在工业控制、物联网节点、音频处理等领域大放异彩。
而面对复杂的寄存器配置与引脚映射,我们早已不必再“手撸”每一个位域。ST官方推出的STM32CubeMX + HAL库组合,让开发者能够通过图形化界面快速完成外设初始化,把精力真正聚焦于应用逻辑本身。
本文将以STM32F4系列微控制器为例,带你系统性地走完一条完整的SPI开发路径:
从底层通信机制讲起,深入剖析主/从模式的工作细节;
结合STM32CubeMX 配置流程,一步步搭建可运行的工程框架;
最后通过真实场景案例(如读取MAX6675温度传感器),展示如何实现稳定可靠的SPI数据交互。
全程无AI感模板句式,只有工程师之间的“对话式”讲解——就像你在调试板子时旁边坐着一位经验丰富的同事。
SPI到底是什么?别被术语吓住
先抛开那些手册里的专业定义,咱们用最直白的话说清楚:
SPI就是一个“对讲机协议”:主设备说话,从设备听着;同时从设备也能回话,主设备也在听。
它有四根线:
-SCK:时钟线 —— 主设备打拍子,大家跟着节奏传数据;
-MOSI:我发你收 —— Master Out, Slave In;
-MISO:你发我收 —— Master In, Slave Out;
-NSS:叫谁起来回答 —— 片选信号,拉低表示“我要跟你聊”。
通信永远由主设备发起。你想跟哪个从机说话,就把它的NSS拉低,然后开始敲SCK。每敲一次,双方就交换一位数据。8次之后,各得一个字节。
听起来简单?但实际用起来常踩坑。比如:
- 数据错位?
- 接收不到回应?
- 波特率太高通信失败?
这些问题往往出在四个关键点上:模式选择(Mode 0~3)、NSS控制方式、波特率匹配、以及主从角色理解不清。
下面我们一个个拆解。
主模式 vs 从模式:谁掌控时钟,谁就是老大
核心区别一句话总结:
主设备产生SCK时钟,从设备只能被动响应。
这意味着:
- 主设备可以随时发起通信;
- 从设备不能主动发送数据,必须等主设备“喂”时钟才能吐出数据;
- SCK的极性(空闲是高还是低)和相位(在上升沿还是下降沿采样)必须主从一致。
这四个组合形成了SPI的四种工作模式:
| Mode | CPOL (时钟极性) | CPHA (时钟相位) | 常见设备 |
|---|---|---|---|
| 0 | 0(空闲为低) | 0(第一个边沿采样) | MAX6675, nRF24L01 |
| 1 | 0 | 1(第二个边沿采样) | 部分Flash |
| 2 | 1(空闲为高) | 0 | 少见 |
| 3 | 1 | 1 | DAC芯片 |
举个例子:MAX6675要求使用Mode 0—— 即SCK空闲为低电平,在上升沿采样数据。如果你配成了Mode 1,那每个bit都会偏半个周期,结果必然是乱码。
所以第一步,看清楚你的外设手册写了什么模式!
STM32F4上的SPI外设有哪些硬实力?
STM32F4系列通常集成多个SPI接口(如SPI1~SPI6),分布在APB1或APB2总线上。以最常见的STM32F407VG为例:
- SPI1接在 APB2 上,最高时钟可达84MHz → SCK最大约42MHz(PCLK/2)
- SPI2/3在 APB1 上,最大频率为42MHz → SCK理论最快21MHz
- 支持8位或16位数据帧
- 可配置为全双工 / 半双工 / 发送-only / 接收-only
- 内建DMA请求通道,适合大数据量传输
- 支持硬件CRC校验(安全关键系统可用)
这些特性让它不仅能对接普通传感器,还能用于LCD刷新、SD卡读写甚至音频流传输。
更重要的是,它支持两种NSS管理方式:
-软件NSS:你自己用GPIO控制片选,灵活但需手动操作;
-硬件NSS:外设自动检测NSS引脚状态,适用于从机自动响应;
对于大多数应用场景,推荐主设备使用软件NSS,避免硬件误触发。
手把手教你用STM32CubeMX配置SPI主设备
现在进入实战环节。假设我们要用SPI2作为主设备,连接一个MAX6675热电偶模块。
第一步:创建项目并选择芯片
打开STM32CubeMX,新建项目,搜索STM32F407VG并选中。
第二步:启用SPI2
在Pinout图中找到SPI2,默认会分配如下引脚:
- PB13 → SCK
- PB14 → MISO
- PB15 → MOSI
这些都是复用功能AF5,CubeMX会自动设置好。
⚠️ 注意:不要改动这些引脚的功能!否则通信会失败。
第三步:配置SPI参数
点击右侧Configuration面板中的SPI2,进入参数设置页:
| 参数 | 设置值 | 说明 |
|---|---|---|
| Mode | Full Duplex Master | 全双工主模式 |
| Frame Format | Motorola | 大多数设备都用这个 |
| Data Size | 8 bits | MAX6675每次读16位,但我们分两次8位传 |
| Clock Polarity | Low | 对应CPOL=0,Mode 0/1 |
| Clock Phase | 1 Edge | 对应CPHA=0,Mode 0/2 → 合起来就是Mode 0 |
| NSS Signal | Software | 使用软件控制片选 |
| Baud Rate Prescaler | 16 | PCLK1 = 84MHz → SCK = 84/16 ≈ 5.25MHz,低于MAX6675的4MHz上限?等等…… |
等等!这里有个陷阱!
虽然我们设了分频16得到5.25MHz,但MAX6675最大只支持4MHz!继续往下看怎么解决。
回到Clock Configuration标签页,你会发现PCLK1其实是42MHz(因为APB1预分频为2)。所以实际SCK频率是:
42MHz / 16 = 2.625MHz ✅ 符合规格完美避开超频风险。
第四步:配置DMA(可选)
如果希望SPI后台传输不占用CPU,可以开启DMA:
- 在SPI2配置里勾选 Rx DMA Request 和 Tx DMA Request;
- CubeMX会自动生成hdma_spi2_rx和hdma_spi2_tx结构体;
这样后续可以用HAL_SPI_TransmitReceive_DMA()实现零等待通信。
第五步:生成代码
选择工具链(Keil MDK / SW4STM32 / Makefile等),生成工程。
生成后你会看到:
-main.c中已有MX_SPI2_Init()调用;
-spi.c文件包含SPI初始化函数;
-gpio.c完成了引脚复用配置;
一切就绪,只差写应用代码了。
主设备代码实战:读取MAX6675温度数据
MAX6675是一个K型热电偶数字转换器,通过SPI输出16位温度数据,其中高2位无效,中间12位为温度值(0.25°C/LSB),最低位用于判断是否开路。
我们来写一段轮询方式的读取代码。
硬件连接补充
除了SPI三线,还需要一个GPIO控制NSS:
- PC4 → 连接到MAX6675的CS脚
记得在CubeMX中将PC4设为GPIO_Output。
代码实现
// 定义片选引脚 #define MAX6675_CS_LOW() HAL_GPIO_WritePin(GPIOC, GPIO_PIN_4, GPIO_PIN_RESET) #define MAX6675_CS_HIGH() HAL_GPIO_WritePin(GPIOC, GPIO_PIN_4, GPIO_PIN_SET) // 读取温度值函数 float Read_MAX6675_Temperature(void) { uint8_t txData[2] = {0}; // 发送空数据触发读取 uint8_t rxData[2] = {0}; // 拉低片选,启动通信 MAX6675_CS_LOW(); // 延迟至少100ns(确保建立时间) for(volatile int i = 0; i < 10; i++); // 发送两个字节,接收两个字节 if (HAL_SPI_TransmitReceive(&hspi2, txData, rxData, 2, 100) == HAL_OK) { // 组合16位数据 uint16_t raw = (rxData[0] << 8) | rxData[1]; // 判断是否开路 if (raw & 0x0004) { return -999.0f; // 开路错误 } // 提取12位温度数据(bit 3~14) raw >>= 3; return (float)(raw) * 0.25f; } else { return -998.0f; // 通信失败 } // 释放片选 MAX6675_CS_HIGH(); }关键点解析
为什么要发空数据?
因为SPI是同步通信,主机必须提供SCK才能让从机输出数据。发送0x00只是为了让时钟跑起来。为什么接收两个字节?
MAX6675每次输出16位,高位先出。所以我们需要连续接收两字节。延迟有必要吗?
是的。有些传感器要求CS建立时间(setup time),加一点小延时更稳妥。浮点运算会影响性能吗?
如果频繁调用,建议改为定点计算或查表优化。
如何配置STM32作为SPI从设备?
有时候你也可能需要让STM32当“配角”,比如做数据中转站、协处理器或调试探针。
这时就要把它设为从模式。
CubeMX配置要点
- SPI Mode 设为Slave
- NSS Signal 推荐设为Hardware Input
→ 当主设备拉低NSS时,STM32自动感知并准备接收数据 - 开启中断或DMA,以便及时响应
示例:从设备接收一包数据
uint8_t slaveRxBuffer[10]; volatile uint8_t spiReady = 0; // 在 main() 中初始化后启动接收中断 HAL_SPI_Receive_IT(&hspi2, slaveRxBuffer, 10); // 中断回调函数 void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi == &hspi2) { spiReady = 1; // 标志数据已收到 } } // 主循环中处理 while (1) { if (spiReady) { // 处理接收到的数据 Process_Data(slaveRxBuffer, 10); spiReady = 0; // 重新启动下一次接收 HAL_SPI_Receive_IT(&hspi2, slaveRxBuffer, 10); } }⚠️ 注意事项:
- 从设备不能主动发数据,除非主设备持续提供SCK;
- 若使用DMA,需注意缓冲区大小与溢出保护;
- 接收过程中不要关闭SPI,否则会导致主设备读取异常;
实际设计中的五大“坑点”与应对秘籍
别以为生成代码就能一次成功。以下是我在项目中踩过的坑,帮你提前避雷:
❌ 坑点1:SCK频率超过从设备承受范围
现象:偶尔能读到数据,大部分时候乱码。
原因:主设备太快,从设备跟不上。
✅解决方案:仔细核对外设手册允许的最大SCK频率,适当加大预分频值。宁可慢一点,也要稳。
❌ 坑点2:MISO悬空导致干扰
现象:未选中从机时MISO线上出现毛刺,影响其他设备。
✅解决方案:给MISO加弱上拉电阻,或在软件中禁止空闲时SPI输出。
❌ 坑点3:多从机共用NSS造成误选
现象:本想读A传感器,结果B也响应了。
✅解决方案:每个从设备使用独立的GPIO控制NSS,绝不共享。
❌ 坑点4:DMA传输完成后未重新启动
现象:第一次传输正常,第二次卡死。
✅解决方案:在DMA完成回调中重新调用HAL_SPI_Receive_DMA(),形成循环缓冲。
❌ 坑点5:电源噪声导致通信不稳定
现象:上电初期通信失败,复位后恢复正常。
✅解决方案:每个SPI器件旁加0.1μF陶瓷去耦电容,靠近VCC引脚放置。
进阶技巧:提升效率的三种模式对比
| 方式 | CPU占用 | 适用场景 | 推荐指数 |
|---|---|---|---|
| 轮询 Polling | 高 | 简单任务、少量数据 | ★★☆☆☆ |
| 中断 Interrupt | 中 | 实时性要求较高 | ★★★★☆ |
| DMA | 极低 | 大批量数据(如音频、图像) | ★★★★★ |
例如你要做音频播放,每秒要传几十KB数据,用轮询等于让CPU罢工。换成DMA+内存缓冲,轻松实现后台播放。
总结一下:你该记住的关键点
- SPI是主从架构,时钟由主设备产生;
- Mode 0 最常见,务必确认主从模式一致;
- STM32CubeMX极大简化配置过程,但不能忽视底层逻辑;
- 波特率要合规,NSS要可控,信号完整性要保障;
- 优先使用HAL库API,复杂场景考虑DMA+中断组合;
- 调试时善用逻辑分析仪抓波形,比猜快十倍;
如果你正在做一个涉及多个SPI设备的项目,不妨试试把这些知识点套进去:
- 用CubeMX一次性配置所有外设;
- 给每个从设备分配独立CS引脚;
- 关键通信加上超时重试机制;
- 高频操作启用DMA降低负载;
你会发现,原本令人头疼的通信问题,变得像搭积木一样清晰可控。
如果你在实现过程中遇到了具体问题——比如“为什么读回来全是0xFF?”、“DMA传输卡住了怎么办?”——欢迎在评论区留言,我们可以一起排查。
毕竟,每一个成功的SPI通信背后,都曾有过无数次SCK的跳动和一次又一次的调试尝试。