4、基础传感器驱动:I2C/SPI通信协议回顾、IMU传感器数据读取、麦克风数据采集、红外传感器数据读取
好,咱们进入第四章。这一章内容比较实在,说白了就是要把智能音箱的“耳朵”和“触角”都调通。我见过不少同学,算法写得飞起,结果传感器数据读出来全是乱码,那真是欲哭无泪。所以,咱们先把底子打牢。
4.1 通信协议回顾:I2C vs SPI
先聊聊通信协议。I2C和SPI,这两个是嵌入式世界里的“普通话”和“方言”。
I2C(Inter-Integrated Circuit),我习惯叫它“两线制”。就两根线:SCL(时钟)和SDA(数据)。它最大的好处是省引脚,一条总线上能挂几十个设备,每个设备有个唯一地址。但代价是什么?速度慢。标准模式100kHz,快速模式400kHz,高速模式也就3.4MHz。而且,它有个“开漏输出”的特性,所以必须加上拉电阻。我在项目中遇到过,有人忘了加上拉电阻,结果通信时好时坏,查了半天才发现是这个问题。
SPI(Serial Peripheral Interface),这个就“豪放”多了。四根线:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。它是全双工的,收发可以同时进行,速度也快,轻松上几十MHz。但缺点也明显:每多一个设备,就得占用一个CS引脚。所以,如果你的主控引脚多,追求速度,那就用SPI;如果引脚紧张,设备又多,那就用I2C。
| 特性 | I2C | SPI |
|---|---|---|
| 引脚数量 | 2(SCL, SDA) | 4(SCLK, MOSI, MISO, CS) |
| 通信方式 | 半双工 | 全双工 |
| 速度 | 慢(100kHz-3.4MHz) | 快(可达几十MHz) |
| 多设备支持 | 通过地址,一条总线挂多个 | 每个设备一个CS引脚 |
| 典型应用 | IMU、温度传感器、EEPROM | 显示屏、SD卡、高速ADC |
我的小建议: 在智能音箱里,IMU传感器(比如MPU6050)通常用I2C,因为数据更新率不需要太高。而麦克风的PDM接口,本质上是SPI的变种,所以会用到SPI的高速特性。
4.2 IMU传感器:加速度计与陀螺仪数据读取
IMU,全称Inertial Measurement Unit,惯性测量单元。它里面通常集成了加速度计和陀螺仪。加速度计测的是“线性加速度”,说白了就是物体在X、Y、Z三个轴上的运动。陀螺仪测的是“角速度”,也就是物体绕三个轴的旋转。
我选MPU6050来举例,这颗芯片太经典了,几乎人手一块。它通过I2C接口输出数据。
初始化流程大致如下:
- 上电后,等待100ms,让芯片稳定。
- 通过I2C写寄存器0x6B(电源管理寄存器),写入0x00,唤醒芯片。
- 配置加速度计量程,比如±2g、±4g、±8g、±16g。寄存器是0x1C。
- 配置陀螺仪量程,比如±250°/s、±500°/s、±1000°/s、±2000°/s。寄存器是0x1B。
- 配置数字低通滤波器(DLPF),寄存器是0x1A,用来滤除高频噪声。
读取数据时,直接从寄存器0x3B开始,连续读取14个字节。这14个字节依次是:加速度计X高8位、X低8位、Y高8位、Y低8位、Z高8位、Z低8位、温度高8位、温度低8位、陀螺仪X高8位、X低8位、Y高8位、Y低8位、Z高8位、Z低8位。
代码示例(伪代码,基于STM32 HAL库):
// 初始化MPU6050
void MPU6050_Init(void) {
uint8_t data;
// 唤醒芯片
data = 0x00;
HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR, 0x6B, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
// 配置加速度计量程为±2g
data = 0x00;
HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR, 0x1C, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
// 配置陀螺仪量程为±250°/s
data = 0x00;
HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR, 0x1B, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
}
// 读取原始数据
void MPU6050_Read_Raw(int16_t* accel, int16_t* gyro) {
uint8_t buf[14];
HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, 0x3B, I2C_MEMADD_SIZE_8BIT, buf, 14, 100);
accel[0] = (int16_t)((buf[0] << 8) | buf[1]); // Accel X
accel[1] = (int16_t)((buf[2] << 8) | buf[3]); // Accel Y
accel[2] = (int16_t)((buf[4] << 8) | buf[5]); // Accel Z
gyro[0] = (int16_t)((buf[8] << 8) | buf[9]); // Gyro X
gyro[1] = (int16_t)((buf[10] << 8) | buf[11]); // Gyro Y
gyro[2] = (int16_t)((buf[12] << 8) | buf[13]); // Gyro Z
}
注意: 读出来的原始数据是16位有符号整数。要转换成物理单位,需要除以对应的灵敏度。比如加速度计±2g时,灵敏度是16384 LSB/g。所以实际加速度 = 原始值 / 16384 (g)。陀螺仪±250°/s时,灵敏度是131 LSB/°/s。这个换算关系,我曾经在调试时搞错过,结果手势识别完全不准,折腾了两天才发现是单位没换算对。
4.3 麦克风数据采集:PDM与I2S接口
智能音箱的核心是语音交互,所以麦克风数据采集是重中之重。目前主流数字麦克风有两种接口:PDM和I2S。
PDM(Pulse Density Modulation),脉冲密度调制。它只有两根线:CLK(时钟)和DATA(数据)。麦克风内部有一个1-bit的Σ-Δ调制器,输出的是1位数据流,密度代表模拟信号的幅度。说白了,就是通过“0”和“1”的密度来模拟声音波形。它的优点是接口简单,但缺点是数据需要经过“抽取滤波”才能变成标准的PCM数据。这个滤波过程,通常由MCU的硬件PDM模块或者软件算法完成。
I2S(Inter-IC Sound),集成电路内置音频总线。它有三根线:BCLK(位时钟)、LRCLK(左右声道选择)、DIN/DOUT(数据)。它直接输出标准的PCM数据,比如16位或24位。I2S的优点是数据可以直接用,不需要额外处理,但需要更多的引脚。
我个人习惯,在智能音箱里,如果MCU有硬件PDM模块,我会优先用PDM麦克风,因为可以省引脚,而且硬件滤波效果不错。如果MCU没有PDM模块,那就老老实实用I2S麦克风,比如INMP441。
以INMP441为例,它的I2S接口配置如下:
- BCLK:位时钟,频率 = 采样率 × 位宽 × 通道数。比如48kHz采样率、16位、双通道,BCLK = 48k × 16 × 2 = 1.536MHz。
- LRCLK:左右声道选择,频率等于采样率,即48kHz。
- DOUT:数据输出,在BCLK的上升沿或下降沿输出数据。
代码示例(基于ESP32的I2S驱动):
// I2S配置结构体
i2s_config_t i2s_config = {
.mode = I2S_MODE_MASTER | I2S_MODE_RX, // 主模式,接收
.sample_rate = 48000, // 采样率48kHz
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // 16位
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // 单声道
.communication_format = I2S_COMM_FORMAT_I2S, // I2S格式
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // 中断优先级
.dma_buf_count = 8, // DMA缓冲区数量
.dma_buf_len = 256 // 每个缓冲区长度
};
// 安装I2S驱动
i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
// 读取音频数据
int16_t audio_buffer[256];
size_t bytes_read;
i2s_read(I2S_NUM_0, audio_buffer, sizeof(audio_buffer), &bytes_read, portMAX_DELAY);
关键点: 读取音频数据时,一定要用DMA(直接内存访问),否则CPU会被频繁中断,导致系统卡顿。我见过有人用轮询方式读I2S,结果系统响应延迟高得离谱,语音唤醒根本没法用。
4.4 红外传感器:距离与手势数据读取
红外传感器在智能音箱里,主要用来做“接近检测”和“手势识别”。比如,你把手靠近音箱,它自动亮屏;你挥手,它切换歌曲。
常用的红外传感器有:
- VL53L0X:ST公司的飞行时间(ToF)传感器,测距精度高,最远2米。通过I2C接口读取距离数据。
- APDS-9960:Broadcom的环境光、距离和手势传感器。它通过I2C接口,不仅能测距离,还能识别上下左右四种手势。
以APDS-9960为例,它的手势识别原理是:内部有四个光电二极管,分别朝向上下左右四个方向。当你挥手时,四个二极管接收到的红外光强度变化不同,通过算法判断手势方向。
初始化APDS-9960的步骤:
- 写寄存器0x80(使能寄存器),设置位0为1,开启电源。
- 写寄存器0x80,设置位1为1,开启接近检测。
- 写寄存器0x80,设置位6为1,开启手势引擎。
- 配置手势参数,比如手势退出阈值、手势增益等。
读取手势数据时,轮询寄存器0xE7(手势状态寄存器),如果位0为1,表示有手势数据可用。然后读取寄存器0xFC(手势数据寄存器),得到手势方向:0x00表示无手势,0x01表示向上,0x02表示向下,0x03表示向左,0x04表示向右。
代码示例:
// 初始化APDS-9960
void APDS9960_Init(void) {
uint8_t data;
// 开启电源
data = 0x01;
HAL_I2C_Mem_Write(&hi2c1, APDS9960_ADDR, 0x80, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
HAL_Delay(10);
// 开启接近检测和手势引擎
data = 0x43; // 位0:电源, 位1:接近, 位6:手势
HAL_I2C_Mem_Write(&hi2c1, APDS9960_ADDR, 0x80, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
}
// 读取手势
uint8_t APDS9960_Read_Gesture(void) {
uint8_t status, gesture;
// 读取手势状态
HAL_I2C_Mem_Read(&hi2c1, APDS9960_ADDR, 0xE7, I2C_MEMADD_SIZE_8BIT, &status, 1, 100);
if (status & 0x01) {
// 读取手势数据
HAL_I2C_Mem_Read(&hi2c1, APDS9960_ADDR, 0xFC, I2C_MEMADD_SIZE_8BIT, &gesture, 1, 100);
return gesture;
}
return 0; // 无手势
}
避坑指南: 我曾经在调试APDS-9960时,发现手势识别总是误触发。后来仔细看数据手册才发现,传感器对环境光很敏感,尤其是阳光中的红外成分。所以,在室外或者强光环境下,需要调整传感器的增益和阈值。另外,传感器的安装位置也很重要,最好用遮光罩挡住侧面光线,只让正面光线进入。
好了,这一章的内容就到这里。I2C和SPI是基本功,IMU、麦克风、红外传感器是智能音箱的“感官”。把这些驱动调通,后面的手势识别算法才有用武之地。下一章,咱们开始玩点高级的——传感器数据融合与滤波。