第一章:SPI/I2C总线驱动开发——传感器通信的“任督二脉”

各位同学,大家好。我是你们的老朋友,一个在汽车电子圈摸爬滚打了十几年的嵌入式工程师。今天咱们正式开始《汽车传感器芯片驱动与算法移植实战》的第一章。

这一章,我打算聊聊传感器通信中最基础、也最绕不开的两个协议:SPI和I2C。说白了,这就是传感器芯片和主控芯片之间“对话”的两种方言。你想想看,一个加速度传感器、一个温度传感器,它们怎么把数据告诉你?靠的就是这两条总线。

我个人习惯,在开始任何驱动开发之前,先把协议吃透。不然你代码写得再花哨,时序不对,芯片就是不搭理你。嗯,咱们今天就把它彻底搞明白。

第一章:SPI/I2C总线驱动开发知识体系 传感器通信协议 SPI 协议 4线制:SCK/MOSI/MISO/CS CPOL/CPHA 模式配置 全双工、高速(>10MHz) I2C 协议 2线制:SCL/SDA 多主机仲裁、7位/10位地址 半双工、中低速(100k~1MHz) 对比选择:速度 vs 引脚数 vs 复杂度

1.1 SPI vs I2C:先来个直观对比

很多初学者会问:到底用SPI还是I2C?我的回答是:看场景。我做过一个胎压监测项目,传感器需要高速连续输出数据,那必须上SPI。但如果是温度采集,几秒钟才读一次,I2C就够用了,还省引脚。

下面这张表,是我自己总结的,你直接拿去用:

对比项 SPI I2C
信号线数量 4根(SCK, MOSI, MISO, CS) 2根(SCL, SDA)
通信方式 全双工 半双工
速度 通常10MHz~50MHz 标准100kHz,快速400kHz,高速3.4MHz
从机选择 片选线CS,一对一或一对多 地址寻址,最多127个从机
硬件复杂度 简单,无地址概念 稍复杂,需地址仲裁
典型传感器 加速度计、陀螺仪、磁力计 温度传感器、湿度传感器、光照传感器
我的经验之谈: 在汽车电子领域,SPI更受青睐。为什么?因为抗干扰能力强,速度快。我曾经在一个ECU项目中,I2C总线被电机噪声干扰得数据全乱,换成SPI后问题迎刃而解。当然,I2C也有它的优势——引脚少,布线简单。

1.2 SPI模式配置:CPOL和CPHA,别搞反了

SPI有四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。很多新手在这里栽跟头。我刚开始也犯过傻,拿着一个传感器手册,对着时序图看了半天,结果配置反了,读出来的数据全是0xFF。

咱们来拆解一下:

  • CPOL:空闲时时钟电平。CPOL=0,空闲低电平;CPOL=1,空闲高电平。
  • CPHA:数据采样边沿。CPHA=0,第一个边沿采样;CPHA=1,第二个边沿采样。

四种模式组合如下:

模式 CPOL CPHA 采样边沿
000上升沿采样
101下降沿采样
210下降沿采样
311上升沿采样
避坑指南: 我曾经在一个项目中,传感器手册写的是“Mode 0”,但我配置成了“Mode 3”。结果数据偶尔对、偶尔错,排查了两天才发现。所以我的建议是:拿到传感器后,先用逻辑分析仪抓一下时序,确认CPOL和CPHA,再写代码。

1.3 I2C时序与多主机仲裁

I2C的时序比SPI复杂一些。它只有两根线:SCL(时钟)和SDA(数据)。每次通信以START条件开始(SCL高时SDA下降沿),以STOP条件结束(SCL高时SDA上升沿)。

多主机仲裁是I2C的一个亮点。当两个主机同时发起通信时,谁先拉低SDA,谁就赢得仲裁。另一个主机检测到SDA与自身输出不一致,就会自动释放总线。嗯,这个机制在汽车多ECU系统中特别有用。

我记得有一次调试一个车身控制模块,两个MCU通过I2C共享一个温度传感器数据。其中一个MCU偶尔会“抢话”,导致数据错乱。后来我仔细检查了仲裁逻辑,发现是其中一个MCU的SDA驱动配置成了推挽输出,而不是开漏输出。改成开漏后,问题解决。

警告: I2C总线必须外接上拉电阻!我见过有人直接把两个MCU的SDA和SCL引脚连在一起,没加上拉电阻,结果通信时好时坏。上拉电阻一般选4.7kΩ,总线电容大时选2.2kΩ。

1.4 基于寄存器级的读写驱动实现

好了,理论讲完了,咱们来点实际的。下面是我写的一个SPI读写寄存器的驱动代码,基于STM32平台。注意,我直接操作寄存器,没用HAL库。为什么?因为寄存器级代码更可控,性能更高,在汽车电子中这是基本要求。

// SPI寄存器级读写驱动示例(基于STM32F4)
// 作者:资深嵌入式工程师

#include "stm32f4xx.h"

// SPI1初始化:模式0,8位数据,主模式
void SPI1_Init(void) {
    // 使能SPI1时钟
    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
    
    // 配置GPIO:PA5(SCK), PA7(MOSI), PA6(MISO)
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    GPIOA->MODER |= (2 << 10) | (2 << 12) | (2 << 14);  // AF模式
    GPIOA->AFR[0] |= (5 << 20) | (5 << 24) | (5 << 28); // AF5: SPI1
    
    // 配置SPI控制寄存器1
    SPI1->CR1 = 0;  // 先清零
    SPI1->CR1 |= SPI_CR1_MSTR;      // 主模式
    SPI1->CR1 |= SPI_CR1_BR_2;      // 波特率分频:fPCLK/64
    SPI1->CR1 |= SPI_CR1_SSM;       // 软件从机管理
    SPI1->CR1 |= SPI_CR1_SSI;       // 内部从机选择高电平
    
    // 模式0:CPOL=0, CPHA=0
    SPI1->CR1 &= ~SPI_CR1_CPOL;     // CPOL=0
    SPI1->CR1 &= ~SPI_CR1_CPHA;     // CPHA=0
    
    // 8位数据格式
    SPI1->CR1 &= ~SPI_CR1_DFF;      // 8位
    
    // 使能SPI
    SPI1->CR1 |= SPI_CR1_SPE;
}

// SPI读写一个字节
uint8_t SPI1_ReadWriteByte(uint8_t data) {
    // 等待发送缓冲区空
    while(!(SPI1->SR & SPI_SR_TXE));
    SPI1->DR = data;  // 发送数据
    
    // 等待接收缓冲区非空
    while(!(SPI1->SR & SPI_SR_RXNE));
    return (uint8_t)SPI1->DR;  // 读取接收数据
}

// 读取传感器寄存器(示例:读地址0x10)
uint8_t Sensor_ReadReg(uint8_t regAddr) {
    uint8_t result;
    
    // 拉低片选(假设CS接PA4)
    GPIOA->BSRR = (1 << (4 + 16));  // 低电平
    
    // 发送读命令(假设读命令为0x80 | regAddr)
    SPI1_ReadWriteByte(0x80 | regAddr);
    
    // 读取数据
    result = SPI1_ReadWriteByte(0x00);  // 发送任意数据,接收从机数据
    
    // 拉高片选
    GPIOA->BSRR = (1 << 4);  // 高电平
    
    return result;
}
代码说明: 这段代码直接操作SPI寄存器,没有使用任何库函数。注意看片选控制,我用了GPIO的BSRR寄存器来快速拉高拉低,比用HAL_GPIO_WritePin快得多。在汽车传感器读取中,每个微秒都很宝贵。

I2C的寄存器级驱动类似,但多了起始条件、停止条件和应答检测。这里我给出一个I2C写寄存器的核心片段:

// I2C写寄存器(伪代码,基于寄存器操作)
void I2C_WriteReg(uint8_t devAddr, uint8_t regAddr, uint8_t data) {
    // 发送起始条件
    I2C1->CR1 |= I2C_CR1_START;
    while(!(I2C1->SR1 & I2C_SR1_SB));  // 等待起始条件发送完成
    
    // 发送设备地址(写)
    I2C1->DR = devAddr << 1;  // 左移1位,最低位为0表示写
    while(!(I2C1->SR1 & I2C_SR1_ADDR));  // 等待地址发送完成
    (void)I2C1->SR2;  // 清除ADDR标志
    
    // 发送寄存器地址
    I2C1->DR = regAddr;
    while(!(I2C1->SR1 & I2C_SR1_TXE));
    
    // 发送数据
    I2C1->DR = data;
    while(!(I2C1->SR1 & I2C_SR1_TXE));
    
    // 发送停止条件
    I2C1->CR1 |= I2C_CR1_STOP;
}
小技巧: 在I2C驱动中,别忘了在读取ADDR标志后,要读一次SR2寄存器来清除标志。这是STM32的硬件设计,很多人会漏掉这一步,导致后续通信卡死。我当年就因为这个bug,调了整整一个下午。

1.5 本章小结

好了,咱们把SPI和I2C的核心知识点过了一遍。从协议对比、模式配置,到寄存器级驱动实现,每一步我都结合了自己的实际项目经验。说白了,驱动开发没有捷径,就是多写、多测、多看时序图。

下一章,我会带大家深入一个具体的传感器驱动案例——从零开始写一个加速度计的驱动。到时候咱们会用到今天讲的SPI知识,还会涉及中断和DMA。嗯,敬请期待。


专注资料整理