3. STM32基础外设驱动实战(SPI与I2C)

SPI和I2C,这两个总线在嵌入式AI项目里太常用了。传感器采集、显示驱动、存储扩展,几乎都离不开它们。但说实话,很多人写驱动就是对着寄存器一顿操作,能跑就行。真到了项目里,时序对不上、DMA卡死、多从机冲突……问题一个接一个。

这一章,咱们就聊聊怎么把这些外设用扎实。我会结合自己踩过的坑,把SPI四线模式、DMA传输、I2C多从机通信这些硬骨头啃下来。

3.1 SPI四线模式与DMA传输

SPI全双工、速度快,结构也简单。但很多人一开始就被它的四种模式搞晕了。其实没那么复杂。

3.1.1 四线模式详解

SPI四线分别是:SCK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。关键就在时钟极性(CPOL)和时钟相位(CPHA)的组合上。

模式 CPOL CPHA 数据采样边沿 空闲时钟电平
模式0 0 0 第一个跳变沿(上升沿) 低电平
模式1 0 1 第二个跳变沿(下降沿) 低电平
模式2 1 0 第一个跳变沿(下降沿) 高电平
模式3 1 1 第二个跳变沿(上升沿) 高电平

怎么选? 看从机的手册。大多数传感器和Flash芯片默认用模式0或模式3。我个人习惯先看手册里时序图,找到数据是在哪个边沿被锁存的,再定CPHA。CPOL反而简单——空闲时时钟是高还是低,一目了然。

小技巧: 调试时如果读回来的数据全是0xFF或0x00,多半是模式配错了。用逻辑分析仪抓一下SCK和MOSI,看看数据跳变沿对不对得上。

3.1.2 DMA传输实战

SPI用轮询方式收发数据,CPU会被死死拖住。尤其是AI推理时,传感器数据量一大,CPU全花在等SPI上了。这时候DMA就派上用场了。

DMA传输的核心思路:CPU配置好传输参数,然后撒手不管。数据从外设到内存,或者从内存到外设,全由DMA控制器搞定。传输完了再给你一个中断。

下面是一个SPI+DMA接收数据的典型配置流程:

// 1. 使能DMA时钟
__HAL_RCC_DMA1_CLK_ENABLE();

// 2. 配置DMA句柄
hdma_spi1_rx.Instance = DMA1_Channel2;
hdma_spi1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_spi1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi1_rx.Init.Mode = DMA_NORMAL;
HAL_DMA_Init(&hdma_spi1_rx);

// 3. 将DMA句柄关联到SPI句柄
__HAL_LINKDMA(&hspi1, hdmarx, hdma_spi1_rx);

// 4. 启动DMA接收(同时发送0x00以产生时钟)
uint8_t rx_buffer[256];
HAL_SPI_Receive_DMA(&hspi1, rx_buffer, 256);

这里有个坑:SPI的DMA接收必须同时发送数据来产生时钟。如果你只收不发,时钟不会自己跑起来。所以HAL库内部其实会发一个虚拟数据(通常是0x00或0xFF)。

我曾经踩过的坑: 有一次用SPI+DMA读ADXL345加速度计,数据总是错位。查了半天,发现是DMA传输完成中断里又调了HAL_SPI_Receive_DMA,导致DMA配置被覆盖。正确的做法是在中断里设一个标志位,主循环里再启动下一次传输。

3.2 I2C多从机通信

I2C只用两根线(SCL、SDA),就能挂多个设备。但多从机通信时,地址冲突、总线锁死、时序不匹配……问题比SPI多得多。

3.2.1 多从机地址管理

每个I2C从机都有一个7位或10位地址。7位地址最常见,范围0x00~0x7F。但有些地址是保留的,比如0x00是广播地址,0x01~0x07用于特殊用途。

实际项目中要注意:

  • 同型号传感器地址可能相同,需要通过ADDR引脚改地址
  • 7位地址左移1位才是HAL库用的8位地址(因为最后一位是读写位)
  • 地址冲突时,两个从机都会应答,数据就乱了
经验之谈: 我建议在系统初始化时,逐个扫描I2C总线上的设备地址。用HAL_I2C_IsDeviceReady()函数,能快速确认哪些地址有应答。这样既能发现地址冲突,也能检测设备是否正常上电。

3.2.2 多从机通信流程

I2C多从机通信的核心是:每次只和一个从机对话。主机通过发送起始条件+从机地址来选定通信对象,其他从机看到地址不匹配,就保持沉默。

典型的多从机读写流程:

  1. 主机发送起始条件(S)
  2. 主机发送7位从机地址+写位(0)
  3. 对应从机应答(ACK)
  4. 主机发送寄存器地址
  5. 从机应答
  6. 主机发送重启条件(Sr)
  7. 主机发送7位从机地址+读位(1)
  8. 从机应答并发送数据
  9. 主机发送非应答(NACK)+停止条件(P)

HAL库封装了这些细节,用HAL_I2C_Mem_Read()和HAL_I2C_Mem_Write()就能搞定。但要注意:连续读写多个从机时,每次都要重新发起起始条件,不能把两个从机的数据放在一个事务里。

3.2.3 总线锁死与恢复

I2C总线锁死,说白了就是SDA被某个设备一直拉低。原因通常是:从机还没处理完数据,主机就发了停止条件;或者主机在传输过程中被复位了,从机还在等待时钟。

恢复方法:

  • 硬件复位:把SCL和SDA的GPIO拉高,再重新初始化
  • 软件恢复:在SCL上发9个时钟脉冲,让从机释放SDA
  • 重新初始化I2C外设
我曾经遇到的情况: 有一次系统频繁掉电重启,I2C总线经常锁死。后来发现是掉电瞬间,从机还在处理数据,主机已经复位了。解决方案是在I2C初始化前,先做一次软件恢复——把SCL和SDA配置成GPIO输出,手动拉高拉低几次,再切回I2C功能。从此再没锁死过。

3.3 外设时序分析仪使用

写驱动最怕什么?时序不对。代码看着没问题,设备就是不工作。这时候,时序分析仪(逻辑分析仪)就是你的眼睛。

3.3.1 抓取时序的基本步骤

  1. 连接探头:SCL/SDA或SCK/MOSI/MISO/CS接到分析仪通道
  2. 设置采样率:至少是信号频率的4倍,建议8~10倍
  3. 设置触发条件:比如SPI的CS下降沿,或I2C的起始条件
  4. 运行程序,抓取波形
  5. 解码分析:分析仪会自动解析SPI/I2C协议,显示数据内容

3.3.2 常见时序问题

问题现象 可能原因 波形特征
数据全是0xFF SPI模式不匹配,或MISO没接好 MISO一直高电平
I2C无应答 地址错误,或从机未上电 SDA在应答位保持高
数据错位 时钟相位不对,或DMA传输长度错误 数据边沿和时钟边沿错开
总线锁死 SDA被从机拉低 SDA持续低电平
我的习惯: 写驱动前,先用分析仪抓一下设备上电后的默认输出。很多传感器上电后会发一串数据,通过这个能确认设备是否正常工作,也能验证你的接线对不对。

3.4 常见总线故障排查

故障排查,说白了就是三板斧:看波形、查配置、测电平。

3.4.1 SPI故障排查清单

  • 检查电平转换: 3.3V的MCU接5V的SPI设备,需要电平转换芯片
  • 检查CS控制: 多个SPI设备共用总线时,CS必须独立控制,不能同时选中多个
  • 检查时钟极性: 用分析仪看SCK空闲时是高还是低,和手册对比
  • 检查数据顺序: MSB first还是LSB first?很多设备默认MSB first

3.4.2 I2C故障排查清单

  • 检查上拉电阻: SCL和SDA必须有上拉电阻,典型值4.7kΩ
  • 检查总线电容: 总线太长或设备太多,会导致信号边沿变缓
  • 检查地址冲突: 用扫描程序确认每个地址只有1个设备应答
  • 检查时钟延展: 有些从机在忙时会拉低SCL,主机要支持时钟延展
一句话总结: 总线故障,90%是硬件问题(接线、电平、上拉),9%是配置问题(模式、地址、速率),只有1%是芯片坏了。先查硬件,再查软件,别一上来就怀疑芯片。

3.5 本章知识体系

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速索引,遇到问题时对照着查。

第3章 知识体系:SPI与I2C外设驱动实战 SPI 四线模式与DMA 四线模式配置 DMA传输配置 CPOL/CPHA选择 中断与回调 I2C 多从机通信 地址管理与扫描 多从机读写流程 总线锁死恢复 上拉电阻与电容 工具与故障排查 时序分析仪使用 SPI/I2C故障排查清单

这张图把SPI、I2C和故障排查三大块串在一起。左边是SPI的四线模式和DMA,右边是I2C的多从机通信,下面是工具和排查方法。写驱动时遇到问题,先定位是哪个模块,再对照清单一步步查。