第一章:SPI/I2C总线驱动开发——传感器通信的“任督二脉”
各位同学,大家好。我是你们的老朋友,一个在汽车电子圈摸爬滚打了十几年的嵌入式工程师。今天咱们正式开始《汽车传感器芯片驱动与算法移植实战》的第一章。
这一章,我打算聊聊传感器通信中最基础、也最绕不开的两个协议:SPI和I2C。说白了,这就是传感器芯片和主控芯片之间“对话”的两种方言。你想想看,一个加速度传感器、一个温度传感器,它们怎么把数据告诉你?靠的就是这两条总线。
我个人习惯,在开始任何驱动开发之前,先把协议吃透。不然你代码写得再花哨,时序不对,芯片就是不搭理你。嗯,咱们今天就把它彻底搞明白。
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个从机 |
| 硬件复杂度 | 简单,无地址概念 | 稍复杂,需地址仲裁 |
| 典型传感器 | 加速度计、陀螺仪、磁力计 | 温度传感器、湿度传感器、光照传感器 |
1.2 SPI模式配置:CPOL和CPHA,别搞反了
SPI有四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。很多新手在这里栽跟头。我刚开始也犯过傻,拿着一个传感器手册,对着时序图看了半天,结果配置反了,读出来的数据全是0xFF。
咱们来拆解一下:
- CPOL:空闲时时钟电平。CPOL=0,空闲低电平;CPOL=1,空闲高电平。
- CPHA:数据采样边沿。CPHA=0,第一个边沿采样;CPHA=1,第二个边沿采样。
四种模式组合如下:
| 模式 | CPOL | CPHA | 采样边沿 |
|---|---|---|---|
| 0 | 0 | 0 | 上升沿采样 |
| 1 | 0 | 1 | 下降沿采样 |
| 2 | 1 | 0 | 下降沿采样 |
| 3 | 1 | 1 | 上升沿采样 |
1.3 I2C时序与多主机仲裁
I2C的时序比SPI复杂一些。它只有两根线:SCL(时钟)和SDA(数据)。每次通信以START条件开始(SCL高时SDA下降沿),以STOP条件结束(SCL高时SDA上升沿)。
多主机仲裁是I2C的一个亮点。当两个主机同时发起通信时,谁先拉低SDA,谁就赢得仲裁。另一个主机检测到SDA与自身输出不一致,就会自动释放总线。嗯,这个机制在汽车多ECU系统中特别有用。
我记得有一次调试一个车身控制模块,两个MCU通过I2C共享一个温度传感器数据。其中一个MCU偶尔会“抢话”,导致数据错乱。后来我仔细检查了仲裁逻辑,发现是其中一个MCU的SDA驱动配置成了推挽输出,而不是开漏输出。改成开漏后,问题解决。
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;
}
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;
}
1.5 本章小结
好了,咱们把SPI和I2C的核心知识点过了一遍。从协议对比、模式配置,到寄存器级驱动实现,每一步我都结合了自己的实际项目经验。说白了,驱动开发没有捷径,就是多写、多测、多看时序图。
下一章,我会带大家深入一个具体的传感器驱动案例——从零开始写一个加速度计的驱动。到时候咱们会用到今天讲的SPI知识,还会涉及中断和DMA。嗯,敬请期待。