3. 传感器接口基础与驱动移植

传感器采集,是无线MCU项目的起点,也是功耗优化的第一道关卡。很多新手一上来就调无线协议、搞低功耗算法,结果发现数据根本不准,或者采集流程就把电耗光了。嗯,这章我们就先把地基打牢。

本章核心脉络: 初始化流程 → 单次/连续采集模式 → 原始值转物理量 → 采样率与分辨率权衡
传感器数据采集知识体系 初始化流程 GPIO/时钟/寄存器 采集模式 单次 vs 连续 数据解析 原始值→物理量 权衡决策 采样率×分辨率 核心原则:先初始化 → 选模式 → 解析数据 → 权衡参数 每一步都直接影响功耗和数据的可用性

3.1 传感器初始化流程

初始化传感器,说白了就是让MCU和传感器「对上话」。我见过不少工程师直接复制例程里的初始化代码,换了个传感器型号就跑不通——因为不同传感器的上电时序、寄存器地址、通信速率都不一样。

一个标准的初始化流程,通常包含这几步:

  1. 硬件引脚配置——GPIO方向、上下拉、开漏/推挽
  2. 时钟使能——给传感器模块和通信外设(I²C/SPI/UART)提供时钟
  3. 通信接口初始化——波特率、极性、相位、地址等
  4. 传感器上电/复位——有些传感器需要延时等待内部振荡器稳定
  5. 读取ID/版本寄存器——确认通信正常,防止焊错芯片
  6. 配置工作模式——量程、分辨率、采样率、滤波等
  7. 触发校准(可选)——比如加速度计的零偏校准
我的习惯: 初始化完成后,一定会回读一次配置寄存器做校验。曾经有个项目,I²C总线上的电容负载太大,写进去的配置值被「吞掉」了,不回读根本发现不了。

下面是一个典型的I²C温湿度传感器初始化代码片段:

// 传感器初始化函数
// 返回 0=成功,非0=失败
int sensor_init(void) {
    uint8_t id;
    
    // 1. 配置GPIO:SCL/SDA 开漏输出,带上拉
    GPIO_InitTypeDef gpio = {
        .Pin = SCL_PIN | SDA_PIN,
        .Mode = GPIO_MODE_OUTPUT_OD,
        .Pull = GPIO_PULLUP,
        .Speed = GPIO_SPEED_FREQ_HIGH
    };
    HAL_GPIO_Init(GPIOB, &gpio);
    
    // 2. 使能I²C时钟
    __HAL_RCC_I2C1_CLK_ENABLE();
    
    // 3. 初始化I²C外设:400kHz标准模式
    hi2c1.Instance = I2C1;
    hi2c1.Init.ClockSpeed = 400000;
    hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
    HAL_I2C_Init(&hi2c1);
    
    // 4. 传感器上电延时:等待内部振荡器稳定
    HAL_Delay(10);  // 数据手册要求至少5ms
    
    // 5. 读取设备ID,确认通信正常
    if (HAL_I2C_Mem_Read(&hi2c1, SENSOR_ADDR, 
                         REG_DEVICE_ID, 1, &id, 1, 100) != HAL_OK) {
        return -1;  // 通信失败
    }
    if (id != EXPECTED_ID) {
        return -2;  // ID不匹配,可能焊错芯片
    }
    
    // 6. 配置量程和分辨率
    uint8_t config = 0x84;  // 量程±2g,高分辨率模式
    HAL_I2C_Mem_Write(&hi2c1, SENSOR_ADDR, 
                      REG_CONFIG, 1, &config, 1, 100);
    
    // 7. 回读校验
    uint8_t check;
    HAL_I2C_Mem_Read(&hi2c1, SENSOR_ADDR, 
                     REG_CONFIG, 1, &check, 1, 100);
    if (check != config) {
        return -3;  // 配置写入失败
    }
    
    return 0;  // 初始化成功
}

3.2 单次采集 vs 连续采集模式

这两种模式的选择,直接决定了你的系统功耗。你想想看,如果只是每分钟测一次温度,却让传感器一直连续采集,那电池能撑多久?

对比项 单次采集模式 连续采集模式
触发方式 每次手动触发一次转换 传感器内部自动循环转换
功耗特点 采集时工作,其余时间休眠 持续工作,功耗较高
适用场景 低频采集(秒级/分钟级) 高频采集(毫秒级)或需要实时数据流
MCU参与度 每次采集需唤醒MCU发起指令 可配合FIFO/DMA,MCU可深度休眠
数据延迟 有触发到就绪的等待时间 数据持续更新,延迟低
我曾经踩过的坑: 在一个电池供电的温湿度记录仪项目中,我用了连续采集模式,想着反正传感器功耗才几十微安。结果没算上MCU频繁读取数据、处理中断的功耗,整机电流从预期的10μA飙到了200μA。后来改成单次采集+深度休眠,才降到3μA。

单次采集的典型流程:

// 单次采集示例:触发一次转换,等待完成,读取数据
uint8_t sensor_read_once(int16_t *raw_temp, int16_t *raw_hum) {
    uint8_t cmd = 0x2C;  // 触发单次温湿度转换命令
    uint8_t data[4];
    
    // 1. 发送触发命令
    HAL_I2C_Master_Transmit(&hi2c1, SENSOR_ADDR, &cmd, 1, 100);
    
    // 2. 等待转换完成(数据手册典型值7ms,最大值12ms)
    HAL_Delay(10);  // 保守等待
    
    // 3. 读取4字节数据(温度高/低字节 + 湿度高/低字节)
    HAL_I2C_Master_Receive(&hi2c1, SENSOR_ADDR, data, 4, 100);
    
    // 4. 拼接原始值
    *raw_temp = (data[0] << 8) | data[1];
    *raw_hum  = (data[2] << 8) | data[3];
    
    return 0;
}

连续采集模式则不同。传感器自己干活,MCU可以睡大觉。配合FIFO缓冲区,MCU每隔一段时间醒来批量取数据就行。这在需要高频采集(比如加速度计200Hz采样)时特别有用。

// 连续采集模式配置(以加速度计为例)
void accel_continuous_mode_init(void) {
    uint8_t config;
    
    // 设置输出数据率为200Hz
    config = 0x90;  // ODR=200Hz, 正常模式
    HAL_I2C_Mem_Write(&hi2c1, ACCEL_ADDR, 
                      REG_CTRL1, 1, &config, 1, 100);
    
    // 使能FIFO,设置水印为16个样本
    config = 0x40 | 0x0F;  // FIFO使能,水印=16
    HAL_I2C_Mem_Write(&hi2c1, ACCEL_ADDR, 
                      REG_FIFO_CTRL, 1, &config, 1, 100);
    
    // 使能数据就绪中断(可选,用于唤醒MCU)
    config = 0x01;
    HAL_I2C_Mem_Write(&hi2c1, ACCEL_ADDR, 
                      REG_INT_EN, 1, &config, 1, 100);
}

3.3 数据格式解析:原始值转物理量

传感器给你的原始值,说白了就是一串二进制数。你得知道它的「编码规则」,才能换算成温度、湿度、加速度这些物理量。

常见的原始值格式有这几种:

  • 无符号整数——直接映射,比如光强度传感器
  • 有符号整数(补码)——加速度计、陀螺仪常用
  • 定点小数——高几位是整数,低几位是小数
  • IEEE 754浮点——部分高精度传感器直接输出float

转换公式通常藏在数据手册里。我习惯先把公式整理成宏定义,方便复用:

// 温湿度传感器SHT30的转换公式
// 温度:T = -45 + 175 * (raw / 65535.0)
// 湿度:RH = 100 * (raw / 65535.0)

#define SHT30_TEMP(raw)  (-45.0f + 175.0f * (raw) / 65535.0f)
#define SHT30_HUM(raw)   (100.0f * (raw) / 65535.0f)

// 加速度计LSM6DSO的转换公式(±2g量程)
// 加速度 = raw * 0.061 (mg/LSB)
#define LSM6DSO_ACCEL(raw)  ((raw) * 0.061f)

// 使用示例
int16_t raw_temp = 15240;
float temp_c = SHT30_TEMP(raw_temp);
// 结果:-45 + 175 * 15240 / 65535 ≈ -4.3°C
关键点: 转换时注意数据类型。int16_t转float时先做强制类型转换,避免整数除法截断。我见过有人写 raw / 65535,两个整数相除结果永远是0。

还有一个容易忽略的问题——字节序。很多传感器默认是大端(Big-Endian),而ARM Cortex-M系列MCU是小端(Little-Endian)。直接memcpy会得到错误的值。

// 字节序转换示例:大端转小端
int16_t big_endian_to_little(uint8_t hi, uint8_t lo) {
    return (int16_t)((hi << 8) | lo);
}

// 或者用宏
#define BE16_TO_CPU(hi, lo)  ((int16_t)(((uint16_t)(hi) << 8) | (lo)))

3.4 采样率与分辨率的权衡

这是传感器选型和配置中最需要动脑子的一步。采样率和分辨率,本质上是在跟功耗、数据量、噪声做博弈。

参数 提高带来的好处 提高带来的代价
采样率 ↑ 能捕捉更快的变化信号 功耗↑、数据量↑、MCU处理负担↑
分辨率 ↑ 测量精度更高,细节更丰富 转换时间↑、噪声可能↑、功耗↑

为什么会这样?因为高分辨率意味着ADC需要更多时间做逐次逼近,或者需要过采样+数字滤波。这段时间里,传感器和MCU都得醒着。

我的经验法则: 对于温度、湿度这类变化缓慢的信号,10位分辨率+1Hz采样率就足够了。对于振动监测、音频采集,才需要12-16位+千赫兹级采样率。别为了「看起来好看」而盲目上高参数。

实际项目中,我一般这样决策:

  1. 先确定信号带宽——根据物理现象估算最高频率成分
  2. 采样率取2-5倍带宽——满足奈奎斯特,留余量
  3. 分辨率看动态范围需求——每增加1位,动态范围增加6dB
  4. 算功耗预算——采样率×单次转换能耗=采集功耗
  5. 如果功耗超标——降低采样率,或者用FIFO批量模式

举个例子。做一个结构健康监测的加速度采集:

  • 桥梁振动频率一般在0.5-20Hz → 采样率取100Hz(5倍)
  • 需要检测0.1g的微小振动 → 分辨率至少12位(±2g量程下约1mg/LSB)
  • 电池供电,要求续航1年 → 算下来每天只能采集30分钟
  • 最终方案:100Hz采样,12位分辨率,每天定时采集30分钟,其余时间深度休眠
注意: 高分辨率不等于高精度。分辨率是ADC能分辨的最小变化,精度是测量值与真实值的偏差。有些16位传感器,因为噪声和温漂,实际有效精度可能只有12位。别被数据手册上的「16位」忽悠了。

嗯,传感器采集这块,说白了就是「够用就好」。把初始化流程走稳,选对采集模式,算对物理量,再根据实际需求权衡采样率和分辨率——这套基本功打扎实了,后面的低功耗调优才能有的放矢。