做嵌入式采集的朋友应该对“OV7670不带FIFO”这种板子印象很深——摄像头数据一帧接一帧往外吐,MCU哪怕慢半拍,画面就花掉。后来大家都学聪明了,想方设法给摄像头前面加一块FIFO缓存,先攒着、再统一搬走,省心得多。惯性传感器这边其实有同样的思路,而且LSM6DSL把它做得很彻底。
LSM6DSL这颗六轴MEMS芯片内置了一块3KB的FIFO,支持连续模式(Continuous Mode)。传感器按固定ODR持续采样,数据不急着让MCU拿走,而是先存在内部FIFO里,攒到一定水位再一次性通知MCU批量读取。这种方案对STM32平台上的低功耗数据流项目来说,收益是数量级的。这篇文章就把LSM6DSL的FIFO连续模式掰开讲清楚:硬件机制、寄存器配置、STM32工程接入、MEMS库使用、批量读取流程,以及我实际调试中踩过的坑,一次说透。
1. FIFO连续模式到底解决了什么问题
1.1 传统“每样本中断”方案的成本
很多人第一次用LSM6DSL,都是照着官方的DataLog例程写:开一个数据就绪中断,每个ODR周期进一次中断,在中断里把加速度和陀螺仪的原始值读出来。这写法在100Hz采样率时问题不大,但一旦把ODR拉高,事情就变了。
LSM6DSL的加速度计和陀螺仪最高都能跑到6.66kHz。如果你在做振动采集或手势识别,ODR设到1.66kHz很常见。这意味着MCU每0.6毫秒就要被中断一次,每次中断还要通过I2C或SPI去读6个寄存器。CPU被切成碎片,其他任务根本排不进去;I2C总线也被频繁占用,如果总线上还挂了屏幕、Flash或者第二个传感器,时序就很容易乱。更头疼的是功耗:CPU每次唤醒、读数据、处理、再睡过去,来回折腾的电流远高于待机电流,电池供电的设备根本扛不住。
实时性要求很高的闭环控制(比如自平衡车、无人机姿态环)确实需要逐样本响应,但大量数据采集类应用根本不需要每一笔数据都立刻处理。
1.2 连续模式是怎么把问题化解掉的
FIFO连续模式的核心思路,就是把“传感器产生数据”和“MCU处理数据”这两件事解耦。传感器以固定ODR往FIFO里写数据,FIFO满的时候覆盖最旧的数据继续写;MCU不用理会每一笔数据,等FIFO里的数据攒到指定水位之后,中断一次,然后一次性批量读走一大块。
打个比方:没有FIFO时,相当于快递员每送一件快递就打一次电话,你一天下来手机响个不停;开了FIFO连续模式,快递员把快递都放快递柜里,攒够一定数量才打电话通知你去取一次。取的时候虽然一次性搬了一堆,但被打扰的次数从几十次降到一次。
用数据说话:六轴同时开启、ODR设为1.66kHz的情况下,每样本点包含6个16位数据字(加速度3个、陀螺仪3个)。如果不做批处理,MCU每秒被中断1660次;把FIFO水印设为96个字(正好16个完整六轴样本),大约9.6毫秒才触发一次中断,每秒约104次;如果把水印设到1024字再读,每秒中断次数降到10次左右。光是这个数量级的变化,就足够让主循环和功耗表现完全不同。
1.3 哪些场景最适合用FIFO连续模式
从我接触过的项目看,最典型的是这几类:
- 可穿戴设备里的睡眠监测、运动识别,需要长时间连续采集加速度数据,MCU大部分时间应该睡大觉;
- 结构健康监测、旋转机械振动监测,采样率要1kHz以上,数据吞吐量很大;
- 物流运输冲击记录、跌落检测,既要连续记录又要事后回看事件前后一段波形;
- 和“智能鱼缸”“智能台灯”这类项目配合时,用LSM6DSL做倾斜检测、水流扰振判断,FIFO连续模式可以让你在后台持续记录物理量变化,不至于漏掉异常瞬间。
反过来说,如果系统里只有一个固定频率的低速率传感器,比如仅用加速度计做10Hz的倾斜开关判断,那FIFO的价值就体现不出来。这种场景正常读寄存器就够了,没必要为了用FIFO而用FIFO。
2. 寄存器与硬件机制拆解:连续模式不是简单“写入FIFO”
2.1 FIFO相关寄存器总览
LSM6DSL的FIFO不是一块简单的“数据仓库”,它由一组控制寄存器和状态寄存器共同管理。控制部分集中在FIFO_CTRL1到FIFO_CTRL5,状态部分则是FIFO_STATUS1、FIFO_STATUS2和FIFO_STATUS3。
大致职责如下:
| 寄存器 | 作用 |
|---|---|
| FIFO_CTRL1 | 水印阈值的低8位 |
| FIFO_CTRL2 | 水印阈值的高位部分,以及水印中断极性等设置 |
| FIFO_CTRL3 | FIFO温度使能、加速度/陀螺解算配置、FIFO ODR、FIFO模式 |
| FIFO_CTRL4 | 外部传感器批处理、只读FIFO、触发源等高级配置 |
| FIFO_CTRL5 | FIFO模式位与数据率相关配置的补充 |
| FIFO_STATUS1 | FIFO中当前未读数据字的低8位 |
| FIFO_STATUS2 | FIFO当前数据字高位,以及满、溢出、水印中断标志 |
| FIFO_STATUS3 | 各轴在FIFO中的样本数量统计 |
这里有个关键概念:FIFO_STATUS里的深度单位是“16位数据字”,不是“样本数”。LSM6DSL的FIFO总容量是3KB,换算下来是1536个16位字。如果只存加速度数据,一个字对应一个轴的一次采样,一帧三轴数据占3个字;如果六轴同时进FIFO,一个完整样本点占6个字。这个单位问题后面算水印时特别容易踩坑。
2.2 六种FIFO模式对比
LSM6DSL的FIFO模式远不止连续模式一种。FIFO_CTRL里的FMode位段可以配置出六种行为:
| 模式 | 数据满后的行为 | 典型用途 |
|---|---|---|
| Bypass | FIFO不参与,数据直接输出 | 简单读取场景 |
| FIFO | 满后停止采样 | 采集一段固定长度数据后离线处理 |
| Continuous | 满后覆盖最旧数据,继续写 | 长时间连续监控 |
| Continuous-to-FIFO | 先连续运行,触发后转FIFO模式 | 保存触发前一段时间的数据 |
| Bypass-to-FIFO | 先旁路,触发后开始往FIFO写 | 只记录事件发生后的数据 |
| Bypass-to-Continuous | 先旁路,触发后转连续模式 | 事件发生后进入持续记录 |
很多初学者以为“连续模式”就是“传感器连续采样”,其实Bypass模式下传感器也一直在采样。连续模式的真正含义,是FIFO满之后不停止接收新数据,而是把最旧的数据覆盖掉。这样MCU永远不会面对一个被新数据堵死的FIFO。
正因为是“丢旧保新”,连续模式特别适合那些MCU无法保证实时读取的场景:哪怕MCU被别的任务耽误了几百毫秒,读到的永远是最近一段时间内的数据,而不是采满后就卡死在那。
2.3 连续模式里的批处理、解算与触发
如果说“连续模式”解决了数据不断产生的问题,那LSM6DSL给的几个附加功能就是真正拉开体验差距的地方。
第一个是批处理(Batch)。加速度计数据、陀螺仪数据、温度传感器数据、外部传感器数据,甚至计步器等传感器输出结果,都可以统一写入FIFO。你可以把外部ADC、I2C子板的数据通过辅助接口送进来,和惯性数据打包成同一时间轴。这个功能在做多传感器融合记录时非常有用,省去了MCU自己去做时间戳对齐的麻烦。
第二个是解算(Decimation)。FIFO允许对进入FIFO的数据做N倍抽取。比如陀螺仪需要1.66kHz的采样率,但加速度计你只想记录208Hz,则可以单独配置加速度计数据在写入FIFO前先做8倍抽取。这样FIFO不会因为高ODR被迅速塞满,批次读取间隔也可以拉得更长。
第三个是触发模式切换。配合连续转FIFO模式,可以实现类似示波器“预触发”的效果:传感器一直以连续模式运行,某个事件(比如外部引脚触发、内部状态机检测到跌倒特征)发生后切到FIFO模式,把触发之前的波形留在FIFO里。这对做跌落记录、冲击检测特别有用,拿到手的不仅是事件后的数据,还有事件发生前那一小段时间的完整波形,定位根因容易得多。
3. STM32侧的准备:硬件连接与MEMS库工程搭建
3.1 I2C还是SPI:从硬件上先砍一刀
STM32接LSM6DSL通常用I2C或SPI。I2C只需要两根线,接线少,大部分低速率场景都够用;SPI四根线,但速度能到10MHz,适合高ODR、大数据量持续采集。
| 对比项 | I2C | SPI |
|---|---|---|
| 引脚数 | 2(SCL、SDA) | 4(CS、SCK、MOSI、MISO) |
| 速度 | 最高约1MHz | 最高约10MHz |
| 地址 | 需要SA0引脚决定地址 | 无地址概念 |
| 典型场景 | 低功耗传感器节点、总线挂载多设备 | 振动采集、高吞吐记录 |
我个人的习惯是:项目不着急、引脚紧张,优先I2C;对吞吐和确定性有要求,直接SPI。注意LSM6DSL的I2C地址是0x6A还是0x6B,由SA0引脚的电平决定。很多朋友第一次上电,读WHO_AM_I读不到正确值,查了一圈发现是SA0悬空或者电平不对,地址填反了。STM32的I2C外设本身就支持7位地址,地址枚举时两个都试一下是排查第一步。
驱动电压方面,既然配STM32,绝大多数情况是3.3V电平,直接连没问题。I2C上拉电阻用4.7k比较常见,如果总线速度快或者线长,可以换成2.2k,但要注意总线上所有设备的输入电平要求。
3.2 X-CUBE-MEMS1库能替你做什么
ST官方的MEMS传感器驱动库就是X-CUBE-MEMS1,支持包括LSM6DSL在内的一大票传感器。你可以从STM32CubeMX的软件包管理器里下载,也可以直接在GitHub上找ST的MEMS驱动仓库。库的结构很简单,核心就是一份lsm6dsl_reg.c和lsm6dsl_reg.h。
这个库最大的价值,是把底层的寄存器读写封装成了语义清晰的API。你不需要每次翻数据手册去抠FIFO_CTRL1的第几位,而是直接调用lsm6dsl_fifo_mode_set、lsm6dsl_fifo_watermark_set这样的函数。ST的驱动还分了两层:平台无关的传感器操作层和你自己实现的下层读写函数。库内部不会直接操作你的I2C/SPI外设,而是通过注册好的读写回调函数去访问硬件。
对应到STM32工程里,你需要自己写两个平台函数,并在初始化时把它们登记到stmdev_ctx_t结构体里:
int32_t platform_write_i2c(void *handle, uint8_t reg, const uint8_t *bufp, uint16_t len) { return HAL_I2C_Mem_Write((I2C_HandleTypeDef *)handle, LSM6DSL_I2C_ADDR, reg, I2C_MEMADD_SIZE_8BIT, (uint8_t *)bufp, len, 100); } int32_t platform_read_i2c(void *handle, uint8_t reg, uint8_t *bufp, uint16_t len) { return HAL_I2C_Mem_Read((I2C_HandleTypeDef *)handle, LSM6DSL_I2C_ADDR, reg, I2C_MEMADD_SIZE_8BIT, bufp, len, 100); }这段代码有个细节:HAL_I2C_Mem_Write的第一个地址参数如果是8位寄存器地址,用I2C_MEMADD_SIZE_8BIT即可;LSM6DSL的寄存器地址都是8位,这个参数不用改。
初始化时这样绑定:
stmdev_ctx_t dev_ctx; dev_ctx.write_reg = platform_write_i2c; dev_ctx.read_reg = platform_read_i2c; dev_ctx.handle = &hi2c1;完成之后,后续所有传感器操作都不再和你用的具体总线有关,库内部会自动调用这两个函数。
3.3 CubeMX工程初始化要点
在CubeMX里,如果你用I2C,就先把I2C外设配成标准模式,速度建议先保守选400kHz;如果后面发现读取耗时太长再调。用SPI的话,配成Mode 0或者Mode 3都行,LSM6DSL的数据手册里明确支持,但千万记得把时钟极性、相位和手册对上。
中断引脚方面,LSM6DSL的INT1和INT2可以映射多种中断事件。把其中一个引脚引出到STM32的GPIO,并配置为外部中断输入。CubeMX里这个引脚要设置成上拉输入,因为传感器开漏输出时需要有外部上拉才能产生可靠电平跳变。
工程建好后,第一件事不是急着配FIFO,而是先读WHO_AM_I。LSM6DSL的WHO_AM_I值是0x6A,读不到这个值,后续配置全是白搭。通了之后再做传感器基础配置,然后是FIFO配置,顺序不要乱。
4. FIFO连续模式配置:寄存器写入顺序与中断配合
4.1 推荐的配置顺序
FIFO连续模式不是写一个寄存器就能跑起来的。我踩过几次坑之后,总结出一套稳定的配置顺序:
第一步,软复位传感器,等待复位完成。注意LSM6DSL的软复位完成后,所有寄存器恢复默认值,不能用完就立刻写FIFO寄存器,需要延时或者轮询复位完成位。
第二步,配置加速度计和陀螺仪的基础参数:ODR、满量程。这里有个新手特别容易忽略的点:如果加速度计和陀螺仪的ODR没开,FIFO里永远不会有数据。FIFO的ODR决定的是“传感器数据写入FIFO”的节奏,但数据源本身必须工作在相应ODR下。
第三步,配置FIFO ODR、水印阈值、是否开启解算。
第四步,配置FIFO模式为连续模式。
第五步,最后才开启中断使能。
这个顺序的意义在于:中断一定要放在最后开。如果你先把中断开了,再去慢慢配传感器和FIFO,配置过程中FIFO可能已经产生数据、水银条件满足,中断触发在预料之外,代码还没准备好读取流程就直接卡进中断,看起来就像死机了一样。
用MEMS库配置的典型代码是这样:
uint8_t whoamI; lsm6dsl_device_id_get(&dev_ctx, &whoamI); if (whoamI != LSM6DSL_ID) { // 错误处理 } lsm6dsl_reset_set(&dev_ctx, PROPERTY_ENABLE); // 等待软复位完成,可延时或轮询 lsm6dsl_xl_data_rate_set(&dev_ctx, LSM6DSL_XL_ODR_1k66Hz); lsm6dsl_xl_full_scale_set(&dev_ctx, LSM6DSL_2g); lsm6dsl_gy_data_rate_set(&dev_ctx, LSM6DSL_GY_ODR_1k66Hz); lsm6dsl_gy_full_scale_set(&dev_ctx, LSM6DSL_2000dps); lsm6dsl_fifo_data_rate_set(&dev_ctx, LSM6DSL_FIFO_1k66Hz); lsm6dsl_fifo_watermark_set(&dev_ctx, 96); lsm6dsl_fifo_mode_set(&dev_ctx, LSM6DSL_FIFO_CONTINUOUS_MODE); // 最后开中断 lsm6dsl_fifo_wtm_ia_set(&dev_ctx, LSM6DSL_INT1_PIN);不同版本的库,中断映射函数名可能略有差异,有的版本叫lsm6dsl_int1_fifo_threshold_set,有的叫lsm6dsl_fifo_wtm_ia_set。不用死记,用的时候在头文件里搜“fifo”“wtm”“threshold”这些关键词就能找到对应函数。
4.2 水印到底怎么算
水印阈值是FIFO连续模式里最需要动脑子的一项。很多人的第一个坑就是在这里。FIFO_STATUS里的数据深度单位是“16位字”,水印的设置也是以“字”为单位,而不是“样本点”。
六轴同时开启时,一个完整样本点包含加速度X/Y/Z和陀螺仪X/Y/Z共6个16位字。所以水印设置96,意味着FIFO里攒了96个字,也就是16个完整样本点时触发中断。如果只开启加速度计,一个样本点只占3个字,那水印96字就代表32个样本点。
中断触发周期可以这样估算:
中断周期 ≈ 水印字数 / (每样本字数 × ODR)
举个具体例子:
| ODR | 启用轴 | 水印 | 每样本字数 | 约多少毫秒触发一次 |
|---|---|---|---|---|
| 1.66kHz | 六轴 | 96 | 6 | 约9.6ms |
| 1.66kHz | 六轴 | 384 | 6 | 约38.5ms |
| 1.66kHz | 六轴 | 1024 | 6 | 约102.8ms |
| 208Hz | 三轴 | 96 | 3 | 约153.8ms |
水印设得越大,MCU唤醒频率越低,但每次读取的数据量越大,而且数据实时性越差。如果你的应用既要低功耗又要及时响应,比如检测到异常后需要快速停止,就要在两者之间取平衡。
另外一个容易忽视的支出:如果你把温度、计步结果或者外部传感器数据也批处理进FIFO,每一笔入队都会占额外的字,水印对应的实际“惯性样本数”就会变少。计算水印时一定要把这些额外项目算进去。
4.3 中断引脚与中断标志的处理方式
配置完FIFO之后,MCU需要有个途径知道“水印到了”。最简单粗暴的办法是主循环里不断轮询FIFO深度或水印标志,但这又回到了“频繁查状态”的老路,只适合调试阶段。正式工程中我建议用中断:把INT1或INT2接到STM32的EXTI引脚,水印触发后进入中断回调,在回调里只做一个动作——置一个全局标志。
volatile uint8_t fifo_flag = 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == LSM6DSL_INT1_Pin) { fifo_flag = 1; } }主循环里再根据标志去批量读数据:
while (1) { if (fifo_flag) { fifo_flag = 0; lsm6dsl_fifo_consume(); } // 其他任务 }这里有个重要原则:不要在中断服务函数里直接做I2C或SPI传输。STM32的HAL库I2C读操作占用时间不短,如果在中断里做,可能导致中断嵌套、总线冲突,甚至卡死。实践中最稳的就是把读取和处理全部放到主循环,中断只负责“提醒”。
5. 批量读取数据:从FIFO到用户数据的完整流程
5.1 读状态、读块数据的标准流程
FIFO连续模式配置好之后,读取流程比很多人想象中简单,但要养成固定套路。我每次都是三步走:
第一步,读FIFO状态,确认当前FIFO里有多少未读数据字。对应库函数一般是lsm6dsl_fifo_data_level_get。
第二步,根据应用需要,决定本次读多少个样本。比如水印设了96字,那就准备读16个六轴样本;如果该批次没读干净,也不能立即关中断,要循环把剩余数据读完。
第三步,逐样本调用lsm6dsl_fifo_raw_data_get,一次拿回一个样本的6个原始int16值。
为什么推荐一次读一个样本(12字节),而不是一次性把整个FIFO读进一个大数组?这要看MEMS库底层的实现。lsm6dsl_fifo_raw_data_get内部会对FIFO状态做相应处理,按样本为单位读取,语义最干净。如果你追求极致的吞吐量,完全可以自己用平台函数直接突发读一大块,但边界判断要自己处理好,否则很容易把FIFO中正在写入的半个样本读出来,数据对齐就乱了。
I2C突发读的效率和单字节读完全不是一个量级。单字节读每读一字节都要重新发送寄存器地址,I2C总线上要多出大量地址数据帧;突发读只需要一个起始地址,后续数据连续传输。LSM6DSL支持I2C突发读,用MEMS库时它会自动处理。
5.2 数据解包与工程单位换算
FIFO里存的全是16位带符号原始值。拿到手先要用int16_t类型去接收,不要用uint16_t,否则负数会被当成大正数,符号扩展直接出错。
LSM6DSL在不同满量程下的灵敏度不一样。以加速度计±2g为例,1 LSB对应0.061mg;陀螺仪±125dps时,1 LSB对应4.375mdps。如果你用的是其它满量程档位,灵敏度要对查数据手册。
| 加速度满量程 | 灵敏度(mg/LSB) |
|---|---|
| ±2g | 0.061 |
| ±4g | 0.122 |
| ±8g | 0.244 |
| ±16g | 0.488 |
| 陀螺仪满量程 | 灵敏度(mdps/LSB) |
|---|---|
| ±125dps | 4.375 |
| ±250dps | 8.75 |
| ±500dps | 17.50 |
| ±1000dps | 35.00 |
| ±2000dps | 70.00 |
换算公式很简单:
float ax_mg = (float)raw_acc_x * 0.061f; float gy_mdps = (float)raw_gyro_x * 4.375f;实际工程中,如果只是判断倾斜、跌倒或振动特征,不一定要换算成物理单位,直接用原始值做阈值判断反而更快。但如果要做姿态解算、数据归档或跨设备对比,物理单位换算不能省。
5.3 一个可以直接改用的读取函数
我把自己常用的FIFO消费函数放这里,稍微改改就能用。
typedef struct { float ax_mg, ay_mg, az_mg; float gx_mdps, gy_mdps, gz_mdps; } imu_sample_t; #define ACC_SENSITIVITY 0.061f // ±2g #define GYRO_SENSITIVITY 4.375f // ±125dps void lsm6dsl_fifo_consume(void) { uint16_t level = 0; int16_t raw[6]; imu_sample_t sample; lsm6dsl_fifo_data_level_get(&dev_ctx, &level); if (level < 6) { return; } uint16_t n_samples = level / 6; // 六轴模式,每样本6字 if (n_samples > 32) { n_samples = 32; } for (uint16_t i = 0; i < n_samples; i++) { lsm6dsl_fifo_raw_data_get(&dev_ctx, raw, 6); sample.ax_mg = raw[0] * ACC_SENSITIVITY; sample.ay_mg = raw[1] * ACC_SENSITIVITY; sample.az_mg = raw[2] * ACC_SENSITIVITY; sample.gx_mdps = raw[3] * GYRO_SENSITIVITY; sample.gy_mdps = raw[4] * GYRO_SENSITIVITY; sample.gz_mdps = raw[5] * GYRO_SENSITIVITY; // 这里把 sample 交给你的业务逻辑 process_imu_sample(&sample); } }这段代码有几个地方可以根据项目调整:MAX_SAMPLES限定了单次最多处理32个样本,避免FIFO里积压了海量数据时一次处理太久;process_imu_sample是你的回调,可以把样本存到环形缓冲区、实时解析、或者通过无线模块发出去,都行。
在调试阶段,我建议先不要着急开中断,直接把lsm6dsl_fifo_consume放在while(1)里轮询跑一遍,确认水印中断的触发时机和FIFO深度读数一致后,再切换到中断模式。这样即使出问题,你也能把范围缩小到中断配置上。
6. 实测中的坑与低功耗调优方向
6.1 我踩过的几个典型坑
这里整理一份问题排查表,基本覆盖了FIFO连续模式最常见的翻车点。
| 现象 | 根本原因 | 解决办法 |
|---|---|---|
| FIFO里一直没有数据 | 只配了FIFO的ODR,没开加速度/陀螺的ODR | 先确认XL和GY的ODR已配置,传感器本身先跑起来 |
| 中断触发频率比预期高一倍 | 把水印当成了“样本数”,实际单位是“字” | 六轴模式水印除以6才是样本数,按字数重算 |
| 读出的数据最后几帧乱掉 | 读取过程中FIFO持续写入,边界帧被切成半个样本 | 用库的fifo_raw_data_get按样本读,别自己裸读大块 |
| 中断回调里用HAL_I2C死机 | 在中断上下文做阻塞式I2C传输 | 中断里只置标志位,读取放主循环 |
| WHO_AM_I读不到0x6A | I2C地址或SPI读位写错 | 检查SA0引脚电平,SPI读时寄存器地址最高位置1 |
| 配置完FIFO但水印标志不置位 | 中断映射函数参数指向了错误的INT引脚 | 查数据手册确认水印事件能不能映射到你用的引脚 |
其中“在中断里做I2C”这个坑,我见过太多人掉进去了。HAL库的I2C读操作如果带超时参数,阻塞时间在几百微秒到几毫秒之间,放在中断服务函数里不仅拖慢中断响应,还可能因为优先级问题导致低优先级任务饿死。真要在高实时性场景下做,应该用DMA配合I2C,但那已经是另一个层面的工程问题了。
6.2 低功耗调优:把唤醒频率压到接近“零”
连续模式对低功耗项目的贡献,最直观的体现就是唤醒频率。
以一个90%时间在睡眠的可穿戴设备为例。如果采用传统方式:加速度计100Hz ODR,每个样本都触发中断,意味着MCU每秒被唤醒100次;假设每次唤醒后处理数据耗时2ms,那每秒里MCU处于活动状态的时间是200ms,活动占空比20%。这还没算短时间反复唤醒带来的额外功耗。
改用FIFO连续模式后:同样100Hz,水印设30字(三轴模式,10个样本),意味着每100ms才唤醒一次。每次读取10个样本并处理,总耗时和之前的单样本处理差不多。活动占空比从20%直线下降到2%。如果再把水印拉高到90字(30个样本),那就是300ms唤醒一次,活动占空比进一步降到0.7%。唤醒频率和数据处理时延之间,是纯工程调优,取决于你的业务容忍度。
STM32侧配合低功耗,可以这样设计:配置好FIFO和中断之后,主循环里执行WFI指令进入睡眠模式;水印中断到来时,EXTI唤醒MCU,执行数据读取和处理,处理完继续睡。
while (1) { __WFI(); // 进入睡眠,FIFO水印中断会唤醒 if (fifo_flag) { fifo_flag = 0; lsm6dsl_fifo_consume(); } }这套写法的前提是,FIFO水印中断在唤醒MCU后能正常置起标志位,且WFI之前确认没有pending的中断。如果中断处理不当,WFI可能直接被一个未处理的中断唤回导致空跑,那样功耗反而更高。
6.3 长期稳定运行的几个经验做法
项目做到后期,关注的已经不是“能不能跑通”,而是“能不能长时间稳定不出幺蛾子”。这里分享几个我实际项目里沉淀下来的做法。
第一,水印阈值别顶到FIFO最大容量。如果水印设得太靠近容量上限,在某些异常节奏下,FIFO可能在MCU读到之前就发生溢出。溢出意味着数据被覆盖,虽然连续模式允许丢旧数据,但如果你在事后分析时发现时间轴上有一小段断层,很难从代码上定位是覆盖导致的还是总线丢包导致的。留出20%到30%余量,诊断压力会小很多。
第二,调试阶段一定要加日志。不用很复杂,在每批读出来的数据上打一个批次序号,或者记录每次中断的时间戳。数据出现断层时,通过日志可以快速判断是中断丢失、FIFO溢出、还是无线传输丢包。很多人一上来就盯着原始波形找问题,忽略了最基础的流程埋点,往往折腾半天才发现是中断配置问题。
第三,软复位会把FIFO清空。如果你在固件里做了在线升级或者异常恢复逻辑,复位前一定要先读走FIFO里剩余的数据。否则用户会反馈“记录断了一截”,但你自己复现不了,因为正常调试流程里没有故意复位这个过程。
第四,多设备共享I2C总线时,FIFO突发读会一下子占用总线几十毫秒。如果总线上还有显示屏、温湿度传感器或者其他从机,建议给FIFO读取预留一个专用时间片,或者干脆用SPI把LSM6DSL独立出来。这样就算读取时占用总线再久,也不会拖累其它周期性传感器。
最后说一句我在实际项目中养成的小习惯:不管用不用FIFO,都把水印、每样本字数、ODR这些参数做成宏定义放在文件头部,调参时只改宏,不用翻函数体。FIFO连续模式本身不复杂,复杂的是参数之间互相耦合,把参数集中管理能省掉大量排错时间。