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反而简单——空闲时时钟是高还是低,一目了然。
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)。
3.2 I2C多从机通信
I2C只用两根线(SCL、SDA),就能挂多个设备。但多从机通信时,地址冲突、总线锁死、时序不匹配……问题比SPI多得多。
3.2.1 多从机地址管理
每个I2C从机都有一个7位或10位地址。7位地址最常见,范围0x00~0x7F。但有些地址是保留的,比如0x00是广播地址,0x01~0x07用于特殊用途。
实际项目中要注意:
- 同型号传感器地址可能相同,需要通过ADDR引脚改地址
- 7位地址左移1位才是HAL库用的8位地址(因为最后一位是读写位)
- 地址冲突时,两个从机都会应答,数据就乱了
3.2.2 多从机通信流程
I2C多从机通信的核心是:每次只和一个从机对话。主机通过发送起始条件+从机地址来选定通信对象,其他从机看到地址不匹配,就保持沉默。
典型的多从机读写流程:
- 主机发送起始条件(S)
- 主机发送7位从机地址+写位(0)
- 对应从机应答(ACK)
- 主机发送寄存器地址
- 从机应答
- 主机发送重启条件(Sr)
- 主机发送7位从机地址+读位(1)
- 从机应答并发送数据
- 主机发送非应答(NACK)+停止条件(P)
HAL库封装了这些细节,用HAL_I2C_Mem_Read()和HAL_I2C_Mem_Write()就能搞定。但要注意:连续读写多个从机时,每次都要重新发起起始条件,不能把两个从机的数据放在一个事务里。
3.2.3 总线锁死与恢复
I2C总线锁死,说白了就是SDA被某个设备一直拉低。原因通常是:从机还没处理完数据,主机就发了停止条件;或者主机在传输过程中被复位了,从机还在等待时钟。
恢复方法:
- 硬件复位:把SCL和SDA的GPIO拉高,再重新初始化
- 软件恢复:在SCL上发9个时钟脉冲,让从机释放SDA
- 重新初始化I2C外设
3.3 外设时序分析仪使用
写驱动最怕什么?时序不对。代码看着没问题,设备就是不工作。这时候,时序分析仪(逻辑分析仪)就是你的眼睛。
3.3.1 抓取时序的基本步骤
- 连接探头:SCL/SDA或SCK/MOSI/MISO/CS接到分析仪通道
- 设置采样率:至少是信号频率的4倍,建议8~10倍
- 设置触发条件:比如SPI的CS下降沿,或I2C的起始条件
- 运行程序,抓取波形
- 解码分析:分析仪会自动解析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,主机要支持时钟延展
3.5 本章知识体系
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速索引,遇到问题时对照着查。
这张图把SPI、I2C和故障排查三大块串在一起。左边是SPI的四线模式和DMA,右边是I2C的多从机通信,下面是工具和排查方法。写驱动时遇到问题,先定位是哪个模块,再对照清单一步步查。