2、硬件接口与通信协议:I2C协议详解、SPI协议详解、传感器寄存器读写实战
好,咱们进入正题。做可穿戴设备,传感器是核心,而传感器怎么跟主控芯片“说话”?靠的就是通信协议。说白了,就是定好规矩,谁先讲、讲多快、讲完怎么收尾。
我这些年调过的传感器,少说也有几十种。最常用的就是I2C和SPI。这两个协议,你玩透了,市面上九成的传感器都能搞定。今天我就把压箱底的经验掏出来,跟你聊聊。
2.1 I2C协议详解
I2C,全称是Inter-Integrated Circuit。我习惯叫它“I方C”。它最大的特点就是省引脚——只需要两根线:SCL(时钟线)和SDA(数据线)。
你想想看,一个主控芯片要挂好几个传感器,如果用SPI,每个传感器得多占一根片选线。但I2C呢?两根线搞定所有。这就是为什么心率血氧传感器几乎清一色用I2C——省引脚啊!
2.1.1 物理层与电气特性
- SCL:时钟线,由主设备控制。我一般用400kHz,这是快速模式。老一点的传感器可能只支持100kHz标准模式。
- SDA:数据线,双向传输。主设备发地址、写数据,从设备回数据。
- 上拉电阻:这个很重要。两根线必须接上拉电阻到VCC。我常用的阻值是4.7kΩ。如果总线电容大,可以换成2.2kΩ。
2.1.2 通信时序
I2C的通信流程,我总结成四步走:
- 起始条件:SCL高电平时,SDA从高变低。这是“我要开始说话了”的信号。
- 发送从设备地址:7位地址 + 1位读写位。比如MAX30102心率传感器的地址是0x57(7位),左移一位变成0xAE(写)或0xAF(读)。
- 数据传输:每8位数据后跟一个应答位(ACK)。从设备拉低SDA表示“收到了”。
- 停止条件:SCL高电平时,SDA从低变高。表示“我说完了”。
这里有个细节:地址发送后,如果从设备没应答(SDA没被拉低),说明设备没在线或者地址错了。我调试时经常用逻辑分析仪抓这个信号,一眼就能看出问题。
2.1.3 读写操作示例
以MAX30102为例,写一个寄存器:
// 写寄存器:先发设备地址(写),再发寄存器地址,最后发数据
uint8_t write_reg(uint8_t reg_addr, uint8_t data) {
i2c_start(); // 起始条件
i2c_send_byte(0xAE); // 设备地址 + 写位
if (!i2c_wait_ack()) return 1; // 等待应答
i2c_send_byte(reg_addr); // 寄存器地址
i2c_wait_ack();
i2c_send_byte(data); // 要写入的数据
i2c_wait_ack();
i2c_stop(); // 停止条件
return 0;
}
读寄存器稍微复杂一点:要先写寄存器地址,再重新发起始条件,然后读数据。
// 读寄存器:先写寄存器地址,再重新开始读
uint8_t read_reg(uint8_t reg_addr) {
uint8_t data;
i2c_start();
i2c_send_byte(0xAE); // 写地址
i2c_wait_ack();
i2c_send_byte(reg_addr); // 指定寄存器
i2c_wait_ack();
i2c_start(); // 重新起始条件
i2c_send_byte(0xAF); // 读地址
i2c_wait_ack();
data = i2c_read_byte(); // 读取数据
i2c_send_nack(); // 发送非应答
i2c_stop();
return data;
}
2.2 SPI协议详解
SPI,全称Serial Peripheral Interface。它比I2C快得多,但多占引脚。SPI需要四根线:
- SCLK:串行时钟,由主设备产生
- MOSI:主设备输出,从设备输入
- MISO:主设备输入,从设备输出
- CS:片选信号,低电平有效
为什么可穿戴设备里SPI用得少?因为心率血氧传感器数据量不大,I2C的400kHz完全够用。但如果你做的是高采样率的加速度计或陀螺仪,SPI就派上用场了。
2.2.1 四种模式
SPI有四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。我刚开始做的时候,被这个搞晕过。
| 模式 | CPOL | CPHA | 说明 |
|---|---|---|---|
| 模式0 | 0 | 0 | 空闲时SCLK为低,第一个边沿采样 |
| 模式1 | 0 | 1 | 空闲时SCLK为低,第二个边沿采样 |
| 模式2 | 1 | 0 | 空闲时SCLK为高,第一个边沿采样 |
| 模式3 | 1 | 1 | 空闲时SCLK为高,第二个边沿采样 |
我个人习惯:先看传感器数据手册,找到“SPI Timing Diagram”那张图。图上会标清楚数据是在时钟上升沿还是下降沿采样。照着配就行,别自己猜。
2.2.2 SPI读写示例
SPI的读写是同步的。你发一个字节的同时,会收到一个字节。所以读操作其实是“假读真写”——你得发一个空字节来产生时钟。
// SPI读写一个字节
uint8_t spi_transfer(uint8_t data) {
// 发送数据的同时接收数据
SPI_DR = data; // 写入数据寄存器
while(!(SPI_SR & SPI_SR_TXE)); // 等待发送完成
while(!(SPI_SR & SPI_SR_RXNE));// 等待接收完成
return SPI_DR; // 返回接收到的数据
}
// 读寄存器:先发寄存器地址(带读标志),再读数据
uint8_t spi_read_reg(uint8_t reg_addr) {
uint8_t data;
CS_LOW(); // 拉低片选
spi_transfer(reg_addr | 0x80); // 地址 + 读标志(最高位置1)
data = spi_transfer(0x00); // 发空字节,收数据
CS_HIGH(); // 拉高片选
return data;
}
2.3 传感器寄存器读写实战
理论说完了,咱们来点实战。以MAX30102心率血氧传感器为例,我带你走一遍完整的寄存器读写流程。
2.3.1 初始化流程
传感器上电后,第一步是复位。然后配置工作模式、采样率、LED电流等参数。
// MAX30102初始化
void max30102_init(void) {
// 1. 复位传感器
write_reg(0x09, 0x40); // 写0x40到模式配置寄存器,触发复位
delay_ms(100); // 等待复位完成
// 2. 设置中断使能
write_reg(0x02, 0x00); // 关闭所有中断,先清干净
// 3. 配置FIFO
write_reg(0x08, 0x0F); // FIFO配置:采样平均=1,FIFO满时触发中断
// 4. 设置模式
write_reg(0x09, 0x03); // 模式:心率+血氧(SpO2模式)
// 5. 配置采样率与LED电流
write_reg(0x0A, 0x43); // SpO2配置:采样率400Hz,LED脉宽411μs
write_reg(0x0C, 0x24); // LED1(红光)电流:6.4mA
write_reg(0x0D, 0x24); // LED2(红外)电流:6.4mA
// 6. 使能传感器
write_reg(0x09, 0x03); // 再次确认模式,开始工作
}
2.3.2 读取FIFO数据
传感器采集到的数据存在FIFO里。我们需要连续读取多个字节。
// 读取FIFO数据(一次读6个字节:2字节红光 + 2字节红外 + 2字节备用)
void max30102_read_fifo(uint32_t *red, uint32_t *ir) {
uint8_t buffer[6];
// 从FIFO数据寄存器(0x07)开始连续读
for(int i = 0; i < 6; i++) {
buffer[i] = read_reg(0x07);
}
// 组装数据(注意:传感器数据是大端格式)
*red = ((uint32_t)buffer[0] << 16) |
((uint32_t)buffer[1] << 8) |
(uint32_t)buffer[2];
*ir = ((uint32_t)buffer[3] << 16) |
((uint32_t)buffer[4] << 8) |
(uint32_t)buffer[5];
}
2.3.3 调试技巧
调试传感器通信,我推荐两个工具:
- 逻辑分析仪:几十块钱的USB逻辑分析仪就够用。抓取SCL和SDA波形,看地址、数据、应答位对不对。
- 串口打印:把读到的寄存器值通过串口打印出来。我习惯在初始化后,把关键寄存器的值读回来打印,确认写入成功。
举个例子,初始化后打印寄存器0x09的值:
uint8_t mode = read_reg(0x09);
printf("Mode register: 0x%02X\n", mode);
// 如果打印出来是0x03,说明模式配置成功
如果打印出来是0x40,说明传感器还在复位状态。这时候就要检查复位时序了。
好了,I2C和SPI的要点就这些。说白了,通信协议就是定好规矩,你按规矩来,传感器就乖乖听话。下一章咱们聊聊数据处理的那些坑——嗯,那才是真正让人头大的地方。