最近在调 STM32C5 驱动 LSM6DSVE,先把陀螺仪这块单独跑通了。LSM6DSVE 是 ST 的六轴惯性传感器,一颗芯片同时带三轴加速度计和三轴陀螺仪,I2C 和 SPI 都能接,典型用在云台、手势识别、姿态解算这些场景。这篇先只盯一件事:用 STM32C5 通过 I2C 轮询读取陀螺仪数据,并把原始寄存器值换算成每秒多少度(dps)。适合刚接触 LSM6DS 系列传感器、想快速看到角速度数据的开发者,也适合想对比“轮询、中断、DMA”三种读取方式区别的朋友。整个工程基于 STM32CubeIDE + HAL 库,代码逻辑不难,但有几个坑值得记一下:高低字节错位、FS 量程配置后灵敏度搞错、I2C 无应答等,我会把排查过程一起写出来。
提示:本文按 ST 六轴传感器常用的寄存器映射来写,LSM6DSVE 的实际寄存器地址和部分配置位请以官方数据手册为准,不同子型号会有差异,但整体流程和排查思路是通用的。
1. 先搞清楚:轮询、中断、DMA 怎么选
1.1 三种读取方式到底是什么
六轴传感器取数据的方式,本质上是“MCU 怎么知道传感器有新数据了”。轮询就是 MCU 主动去问传感器:数据准备好了吗?准备好了我就读。中断则是传感器主动喊 MCU:我有新数据了,你来取。DMA 一般配合 FIFO 使用,传感器把多次采样先存进内部 FIFO,MCU 再通过 DMA 成批搬走,最省 CPU。
很多初学者一上来就选中断,觉得比轮询高级,其实在小数据量场景下轮询完全够用,代码也最简单。轮询的思路很直白:读状态寄存器,看数据就绪位,置 1 就去读数据寄存器,读完再回到循环。这个流程里没有任何回调函数、优先级、标志位竞争的问题,对于把传感器驱动跑通来说,是最不容易出错的一种方式。
我个人的习惯是:硬件验证阶段统一用轮询,先把传感器通信、寄存器读写、数据拼接这些底层机制彻底吃透,等产品要上正式功能了,再根据实际 ODR 和功耗要求切换到中断或 FIFO。这篇作为系列第一篇,正好把轮询这条路走完整。
1.2 轮询方式适合什么场景
轮询适合对实时性要求不极端、输出数据率不太高的场景。举个例子,陀螺仪配置成 104Hz 输出,也就是每秒钟产生 104 组三轴数据,主循环跑到 1kHz 的话,每进一次循环查一次状态位,完全来得及,数据不会丢。如果 ODR 拉到 6.66kHz,主循环稍微卡顿一下,读的速度跟不上传感器内部更新的速度,新数据就可能覆盖旧数据,读出来就会跳变。
另外,低功耗产品里不建议用傻轮询。MCU 一直在跑,I2C 一直在访问,功耗自然压不下来。这时候更适合用中断唤醒,或者干脆让传感器数据进 FIFO,MCU 睡够了再批量读。所以轮询不是“low”,而是有它明确的适用边界。这篇先把功能跑通,后续文章再单独说 FIFO 和中断怎么做。
2. 硬件连接与 CubeMX 工程准备
2.1 LSM6DSVE 的引脚和 I2C 地址
LSM6DSVE 硬件上不复杂,核心引脚就这么几个:VDD 供电、GND 地、SCL 时钟、SDA 数据,另外还有一个 SA0 地址选择引脚。SA0 电平不同,I2C 设备地址会不一样,通常接地时地址是 0x6A,接 VDD 时是 0x6B,具体以数据手册为准。如果板子上 SA0 是悬空或者通过电阻配置,焊接前先确认清楚,否则后面读 WHO_AM_I 可能莫名其妙失败。
供电方面,LSM6DSVE 工作电压范围比较宽,常见是 1.71V 到 3.6V,和 STM32C5 的 3.3V 电平直接对接没问题。不过 SCL 和 SDA 是开漏结构,必须接上拉电阻,一般用 4.7kΩ 比较稳。如果传感器模块上已经自带上拉,就不用再外接;如果是自己画的板子,漏了上拉电阻,I2C 通信会时好时坏,这是第一类经典坑。
陀螺仪数据是通过 I2C 读出来的寄存器值,寄存器本身是传感器内部的状态存储单元,你可以把它理解成传感器的“内存”。MCU 往这些寄存器写配置,或者从这些寄存器读数据。所有功能,比如量程设置、数据率设置、数据就绪标志,都映射在寄存器上,这就是 I2C 驱动传感器的最底层逻辑。
2.2 CubeMX 里怎么配置 STM32C5 的 I2C
我用的开发环境是 STM32CubeIDE 加 HAL 库,因为 STM32C5 这种相对新的芯片,用 CubeMX 生成底层初始化代码最省事,手写寄存器不仅效率低,还容易踩时钟配置的坑。
打开 CubeMX 后,选好具体的 STM32C5 型号,在 Pinout 视图里把 I2C1 的 SCL 和 SDA 引脚分配给对应的引脚,然后到 Connectivity -> I2C1 里配置:
- I2C Speed Mode:Standard Mode 或 Fast Mode,为了稳妥,初期先用 100kHz,通信稳定后再调到 400kHz。
- Duty Cycle:如果是 Fast Mode,可以选 2:1。
- 其他参数保持默认,HAL 库会自动填好时序寄存器。
时钟树方面,STM32C5 的外设时钟用 CubeMX 自动生成就行,手动改出现异常的几率更高。生成工程后,I2C1 的初始化代码已经在MX_I2C1_Init()里了,HAL 库的HAL_I2C_Mem_Read()和HAL_I2C_Mem_Write()就是我们这次读写传感器寄存器的两个核心函数。
2.3 建议优先用硬件 I2C
硬件 I2C 和软件 I2C 的区别,简单说就是一个用芯片内部的外设模块自动产生时序,一个靠 GPIO 翻转模拟时序。硬件 I2C 有硬件时钟发生器、ACK 检测、超时机制,稳定性更好,代码也更简洁。软件 I2C 的优势是随便哪两个 GPIO 都能用,但不推荐在 STM32C5 这种硬件资源充足的 MCU 上为了省一个外设去折腾。
用 HAL 库读寄存器时,最常见的调用是:
HAL_I2C_Mem_Read(&hi2c1, dev_addr, reg_addr, I2C_MEMADD_SIZE_8BIT, buffer, len, timeout);
这个函数会自动完成“先发设备地址 + 寄存器地址,再发设备地址 + 读数据”的 I2C 复合操作,开发者不用管底层的 START、STOP、ACK 细节。后面代码里我会大量用这个函数。
3. LSM6DSVE 寄存器配置与初始化流程
3.1 第一步:读 WHO_AM_I 确认通信正常
拿到传感器第一件事不是急着配寄存器,而是先读 WHO_AM_I 寄存器。这个寄存器存放着芯片的身份标识,相当于传感器的“身份证号”。如果 MCU 和传感器之间通信有问题,第一步就暴露出来了,不用等到后面数据异常再回头查线。
参照常见 ST 六轴传感器的寄存器布局,WHO_AM_I 的地址通常在 0x0F。读取代码可以这样写:
uint8_t who_am_i = 0; HAL_StatusTypeDef status = HAL_I2C_Mem_Read(&hi2c1, LSM6DSVE_ADDR << 1, 0x0F, I2C_MEMADD_SIZE_8BIT, &who_am_i, 1, 100); if (status != HAL_OK) { printf("I2C read error\r\n"); } else { printf("WHO_AM_I = 0x%02X\r\n", who_am_i); }注意 HAL 库里的设备地址需要左移一位,因为HAL_I2C_Mem_Read()的地址参数是 8 位地址格式,0x6A 要写成0x6A << 1。
读出来如果和手册一致,说明接线、供电、I2C 地址都对。如果不一致,优先查地址位和上拉电阻,不要急着怀疑代码逻辑。那个值具体是多少,不同批次或者子型号可能有差异,以你自己数据手册为准,我不在这里给死一个数字,因为就得养成翻手册的习惯。
3.2 软件复位和 BDU 块数据更新
确认通信没问题后,先做一次软件复位,让传感器内部寄存器回到默认状态,避免上一次配置残留影响初始化。复位操作一般是在 CTRL3_C 寄存器里把 SW_RESET 位写 1,然后等待这个位自动清 0。
这个等待一定要做,不等待就继续配置,寄存器写入可能被跳过,导致后面量程、ODR 永远是默认值。代码上可以轮询等待,比如最多等 10ms:
uint8_t ctrl3_c = 0x01; HAL_I2C_Mem_Write(&hi2c1, LSM6DSVE_ADDR << 1, 0x12, I2C_MEMADD_SIZE_8BIT, &ctrl3_c, 1, 100); for (uint8_t i = 0; i < 50; i++) { HAL_I2C_Mem_Read(&hi2c1, LSM6DSVE_ADDR << 1, 0x12, I2C_MEMADD_SIZE_8BIT, &ctrl3_c, 1, 100); if ((ctrl3_c & 0x01) == 0) { break; } HAL_Delay(1); }复位之后要开启 BDU,也就是 Block Data Update。BDU 的作用是:传感器内部数据更新时,不会清除当前正在读取的寄存器,而是等 MCU 读完一个数据组的所有字节后,才让新数据覆盖。没有 BDU,如果 MCU 正在读高字节的时候传感器恰好更新了数据,高低字节就可能来自不同时刻的采样,算出来的角速度会出现偶发跳变。这个说法有点绕,用大白话讲就是:它防止你“读到一半被插队”。所以轮询读取时,BDS 一定要开。
在 CTRL3_C 里,BDU 位一般是在 bit 6,IF_INC 自动地址递增在 bit 2。IF_INC 也应该打开,这样连续读取多字节寄存器时,地址可以自动增加,一次 I2C 请求就能读完 6 字节数据,不然每次只能按单字节地址读。
3.3 陀螺仪量程和输出数据率怎么配
陀螺仪的核心寄存器是 CTRL2_G,负责控制陀螺仪的开启、输出数据率和满量程。在常见 LSM6DS 系列里,这个寄存器的高 4 位是 ODR_G,低 3 位是 FS_G,bit 7 是开启陀螺仪。具体每个值对应多少 Hz 或者多少 dps,跟着数据手册查表走。
一般 ODR 我建议初期不要选太高,104Hz 或 208Hz 就够调试了。原因很简单:串口打印一秒钟出几百行数据,人眼根本看不过来,调试阶段反而低数据率更清晰。量程方面,姿态解算常用 ±2000dps,如果只是测转动,这个量程宽容度最好。不同量程对应的灵敏度是不一样的,这个先记住,后面换算会用到。
以常见的 LSM6DS 系列为例,ODR 和量程的对应关系大致是:
| ODR_G 值 | 输出数据率 |
|---|---|
| 0x0 | 关闭 |
| 0x3 | 104 Hz |
| 0x4 | 208 Hz |
| 0x5 | 416 Hz |
| 0x6 | 833 Hz |
| 0x7 | 1.66 kHz |
| FS_G 值 | 陀螺仪满量程 |
|---|---|
| 0x0 | ±245 dps |
| 0x1 | ±500 dps |
| 0x2 | ±1000 dps |
| 0x3 | ±2000 dps |
具体位宽和编码,不同子型号会有出入,配置前一定先看 LSM6DSVE 手册的寄存器表格。如果手册上和这张表不一样,以手册为准,这里重点理解配置逻辑,而不是死记数值。
3.4 初始化函数的完整示例
把前面说的综合起来,初始化的流程就是:读 WHO_AM_I 确认通信,软件复位,等复位完成,再配置 CTRL3_C 开启 BDU 和 IF_INC,最后写 CTRL2_G 配置陀螺仪 ODR 和量程。下面是 HAL 库风格的示例,注释我尽量写清楚:
#define LSM6DSVE_ADDR 0x6A #define LSM6DSVE_REG_WHO_AM_I 0x0F #define LSM6DSVE_REG_CTRL1_XL 0x10 #define LSM6DSVE_REG_CTRL2_G 0x11 #define LSM6DSVE_REG_CTRL3_C 0x12 #define LSM6DSVE_REG_STATUS_REG 0x1E #define LSM6DSVE_REG_OUTX_L_G 0x22 uint8_t LSM6DSVE_Init(void) { uint8_t data = 0; // 1. 读 WHO_AM_I if (HAL_I2C_Mem_Read(&hi2c1, LSM6DSVE_ADDR << 1, LSM6DSVE_REG_WHO_AM_I, I2C_MEMADD_SIZE_8BIT, &data, 1, 100) != HAL_OK) { return 1; } printf("WHO_AM_I: 0x%02X\r\n", data); // 2. 软件复位 data = 0x01; HAL_I2C_Mem_Write(&hi2c1, LSM6DSVE_ADDR << 1, LSM6DSVE_REG_CTRL3_C, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); for (uint8_t i = 0; i < 50; i++) { HAL_I2C_Mem_Read(&hi2c1, LSM6DSVE_ADDR << 1, LSM6DSVE_REG_CTRL3_C, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); if ((data & 0x01) == 0) { break; } HAL_Delay(1); } // 3. CTRL3_C: 开启 BDU 和 IF_INC,地址自动递增 data = (0x01 << 6) | (0x01 << 2); HAL_I2C_Mem_Write(&hi2c1, LSM6DSVE_ADDR << 1, LSM6DSVE_REG_CTRL3_C, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); // 4. CTRL2_G: 配置陀螺仪 208Hz, ±2000dps data = (0x04 << 4) | (0x03); HAL_I2C_Mem_Write(&hi2c1, LSM6DSVE_ADDR << 1, LSM6DSVE_REG_CTRL2_G, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); return 0; }写 CTRL2_G 的时候注意,ODR 位和 FS 位到底占哪几位,以及需不需要把 FIFO 或 I3C 接口关掉,这些都要对着手册的寄存器描述核一遍。我在实际调试时吃过一次亏,把 ODR 和 FS 的位置搞反了,结果陀螺仪完全没有输出,浪费了半天。
4. 轮询读取陀螺仪数据的完整实现
4.1 怎么判断数据准备好了
传感器配置好之后,并不是随时都能从输出寄存器里读数据,必须等它完成一次采样并锁存之后才去读,否则读出来的可能是旧数据。判断数据是否就绪,靠 STATUS_REG 寄存器里的 GDA 位,也就是陀螺仪数据可用标志。
如果 GDA 为 1,表示当前陀螺仪三轴输出寄存器里的数据是完整的、新的一帧;读走之后,这个标志位会自动清 0。轮询的核心就是不断读 STATUS_REG,看到 GDA 置 1 后再去读 OUTX_L_G 那一片寄存器。
uint8_t status = 0; do { HAL_I2C_Mem_Read(&hi2c1, LSM6DSVE_ADDR << 1, LSM6DSVE_REG_STATUS_REG, I2C_MEMADD_SIZE_8BIT, &status, 1, 100); } while ((status & 0x02) == 0); // GDA 位,具体位置查手册这里位的位置我按常见系列写的是 bit 1,对应 GDA。实际 LSM6DSVE 有可能用别的位,或者和 XLDA 排列不一样,所以这个位掩码一定要对照手册改。还有一种做法是不看状态位,固定延时就绪,比如 ODR 是 208Hz,就每隔 5ms 读一次。这种固定延时做法也能跑,但对主循环时序有要求,稍微卡顿就丢数据,所以我更推荐读状态位。
4.2 一次读完 6 个字节并合成 int16_t
陀螺仪三轴数据寄存器是 OUTX_L_G、OUTX_H_G、OUTY_L_G、OUTY_H_G、OUTZ_L_G、OUTZ_H_G。每个轴两个字节,低字节在前。因为初始化时开了 IF_INC,所以 I2C 读数据时可以连续读 6 个字节,地址会自动递增,效率最高,也不容易出现高低字节错位。
读取代码可以封装成一个函数:
typedef struct { int16_t x; int16_t y; int16_t z; } gyro_raw_t; uint8_t LSM6DSVE_ReadGyro(gyro_raw_t *gyro) { uint8_t buf[6]; if (HAL_I2C_Mem_Read(&hi2c1, LSM6DSVE_ADDR << 1, LSM6DSVE_REG_OUTX_L_G, I2C_MEMADD_SIZE_8BIT, buf, 6, 100) != HAL_OK) { return 1; } gyro->x = (int16_t)((buf[1] << 8) | buf[0]); gyro->y = (int16_t)((buf[3] << 8) | buf[2]); gyro->z = (int16_t)((buf[5] << 8) | buf[4]); return 0; }这里有个细节容易踩坑:高字节和低字节合成时,要先把高字节左移 8 位再与低字节相或。如果顺序搞反,或者搞成(buf[0] << 8) | buf[1],读出来的数不仅数值不对,正负号也会乱掉。另外,合成后一定要强转成 int16_t,因为陀螺仪的原始输出是有符号数,转成 unsigned 会导致负角速度变成很大的正数,后面无论怎么换算都是错的。
4.3 原始值到底怎么换算成实际角速度
这一步是很多新手最迷惑的地方。传感器返回的原始值只是一个整数,和真实的角速度之间隔着灵敏度。灵敏度可以理解成“一个 LSB 代表多少度每秒”。以 ±2000dps 量程为例,常见灵敏度是 16.4 LSB/dps,也就是 1 dps 对应 16.4 个 ADC 计数。换算公式很简单:
实际角速度(dps) = 原始整数 / 灵敏度(LSB/dps)
比如原始值读出来是 1640,量程 ±2000dps,那么实际角速度就是 1640 / 16.4 = 100 dps。如果原始值是 -1640,那就是 -100 dps,表示反转方向。
换算代码可以这样写:
#define GYRO_SENSITIVITY 16.4f // ±2000dps 时的 LSB/dps float gyro_x_dps = (float)gyro.x / GYRO_SENSITIVITY; float gyro_y_dps = (float)gyro.y / GYRO_SENSITIVITY; float gyro_z_dps = (float)gyro.z / GYRO_SENSITIVITY;量程不同,灵敏度也不同。比如 ±500dps 的时候灵敏度会更高,意味着分辨率更好,但能测的最大角速度变低了。所以选量程要在“量程够大”和“分辨率够细”之间权衡。工程里最好把灵敏度做成宏定义,和量程一起管理,别在代码里到处写裸的 16.4,否则后期改量程时很容易漏改。
4.4 主循环里的轮询逻辑怎么组织
在主循环里组织轮询时,要避免一个常见错误:每次循环都去读数据寄存器,而不看数据是否就绪。这样读到的数据可能是上一帧的重复,也可能读到一半被新数据覆盖,毫无时序保证。正确的逻辑是:先查状态位,就绪了再读,没就绪就继续做别的事。
简单的主循环示例:
while (1) { uint8_t status = 0; HAL_I2C_Mem_Read(&hi2c1, LSM6DSVE_ADDR << 1, LSM6DSVE_REG_STATUS_REG, I2C_MEMADD_SIZE_8BIT, &status, 1, 100); if (status & 0x02) // GDA = 1 { gyro_raw_t gyro; LSM6DSVE_ReadGyro(&gyro); float gx = (float)gyro.x / GYRO_SENSITIVITY; float gy = (float)gyro.y / GYRO_SENSITIVITY; float gz = (float)gyro.z / GYRO_SENSITIVITY; printf("%.2f, %.2f, %.2f\r\n", gx, gy, gz); } // 其他任务 }调试阶段不建议在循环里加太长的 HAL_Delay,否则主循环被拖慢,高 ODR 下会丢帧。如果确实需要控制串口打印频率,可以简单加一个计数,比如每读 50 帧打印一次,既不会拖垮循环,又能看数据趋势。
还有一种更好的写法是加上超时控制,避免 I2C 异常时卡死在 do-while 里:
uint32_t timeout = 1000; while ((status & 0x02) == 0 && timeout > 0) { HAL_I2C_Mem_Read(&hi2c1, LSM6DSVE_ADDR << 1, LSM6DSVE_REG_STATUS_REG, I2C_MEMADD_SIZE_8BIT, &status, 1, 100); timeout--; }这样万一传感器掉了,主程序也不会一直傻等状态位,至少能在串口上看到异常提示。
5. 调试过程中踩过的坑与排查实录
5.1 I2C 通信不稳定的几个典型原因
我这个工程第一次上电,串口打印出来的 WHO_AM_I 全是 0xFF,一看就是 I2C 总线根本没有设备响应。排查顺序是这样的:先量供电,确认 VDD 有 3.3V;再用示波器看 SCL 和 SDA 波形,发现 SCL 有信号但 SDA 一直被拉高,也就是没有 ACK。最后查到是 I2C 上拉电阻没焊,补上 4.7kΩ 电阻后通信恢复正常。
另一个典型问题是设备地址搞错。如果用错误的地址去访问,I2C 主机会收到 NACK,HAL 库的HAL_I2C_Mem_Read()返回HAL_ERROR。如果拿到的模块原理图上 SA0 接法不确定,可以分别试 0x6A 和 0x6B,哪个能读回正确 WHO_AM_I 就用哪个。
I2C 速率也不是越高越好。我在 400kHz 下遇到过偶发 NACK,降回 100kHz 后问题消失。虽然传感器标称支持 400kHz,但板子走线、上拉电容、传感器模块质量都会影响极限速率。工程上稳定优先,调试阶段用 100kHz,量产前再决定要不要提速。
5.2 数据全 0 或者漂移严重是怎么回事
配完寄存器后,串口打印出来的陀螺仪三轴数据全是 0,这种问题大多数是陀螺仪没有真正开启。CTRL2_G 寄存器里 ODR 位如果写成了 0,陀螺仪就处于关闭状态,即使读数据寄存器,拿到的也是初始值 0。所以配置完成后,最好回读一次 CTRL2_G,确认写入值确实生效。
还有一种情况是数据不全是 0,但零漂很大,比如板子静止时 Z 轴读出来有几十 dps。这个对于 MEMS 陀螺仪来说不算异常,传感器出厂会有零偏,温度变化也会放大零偏。解决办法是在应用层做零偏校准:上电后静止采集 100 帧,求平均值,然后从实时数据里减去这个偏移。
校准代码思路很简单:
float offset_x = 0, offset_y = 0, offset_z = 0; for (int i = 0; i < 100; i++) { gyro_raw_t gyro; LSM6DSVE_ReadGyro(&gyro); offset_x += (float)gyro.x / GYRO_SENSITIVITY; offset_y += (float)gyro.y / GYRO_SENSITIVITY; offset_z += (float)gyro.z / GYRO_SENSITIVITY; HAL_Delay(10); } offset_x /= 100; offset_y /= 100; offset_z /= 100;采完静态零偏后,每次读到角速度先减去对应偏移,再做积分或者其他计算,姿态解算的精度会明显提升。
5.3 读数偶发跳变,高低字节拼接错位
有段时间数据一个劲儿地跳,比如静止时 Z 轴偶尔蹦出几百。一开始怀疑是电源噪声,后来发现是没开 BDU 导致的高低字节错位。传感器内部数据更新频率高,MCU 读低字节的时候刚好碰到新数据写入,高字节就变成了新数据的高字节,最终拼出的值自然就错了。
开了 BDU 之后,这类跳变几乎绝迹。所以强烈建议:LSM6DSVE 初始化时一定把 BDU 置位,轮询读取也不例外。还有一个相关注意事项是,读轴数据时最好连续读 6 字节,中途不要用两三次独立的 Mem_Read 分头读,因为那样不是原子操作,更容易被内部更新打断。
5.4 故障排查速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| WHO_AM_I 读不出来 | 接线错误、无上拉、地址不对 | 查供电、补上拉、换地址重试 |
| 读回 0xFF | SDA 无 ACK,设备未响应 | 示波器查波形,确认地址 |
| 数据全 0 | 陀螺仪未开启、ODR 配错 | 回读 CTRL2_G 确认配置 |
| 静止时读数偏大 | 零偏未校准 | 做静态零偏校正 |
| 偶发跳变 | BDU 未开启 | 设置 CTRL3_C 的 BDU 位 |
| 换算值明显偏大/偏小 | 灵敏度用错,量程和灵敏度不匹配 | 按 FS 实际值查表 |
| 打印数据重复 | 没有检查 GDA,读到旧数据 | 先等 GDA 置位再读 |
这个表是实际调试中最常见的几类问题,基本覆盖了轮询读取陀螺仪的大部分坑。遇到问题先对着表格从头过一遍,往往比反复改代码更有效率。
6. 后续扩展方向与个人体会
这一篇只实现了轮询读陀螺仪,但 LSM6DSVE 还有不少功能值得继续做下去。比如加速度计的数据还没配,CTRL1_XL 对应的是加速度计配置,配置好之后六轴数据就齐了,可以做摔倒检测或者倾角计算。再比如内置 FIFO,如果 ODR 很高,主循环来不及读,数据可以先存在 FIFO 里,等攒了一批再批量读取,CPU 占用率会低很多。
就我个人体会,轮询读陀螺仪这件事,看起来不起眼,但它把整个 I2C 传感器调试链路都串起来了。把这条链路弄通,后面加中断、加 FIFO、加姿态解算,都是在现有的基础上做增量。如果一上来就追求最复杂的方案,出了问题连最基本的通信都没验证,反而更难排查。这篇先到这里,下一篇我会继续写加速度计的数据读取和校准,把 LSM6DSVE 的六轴数据凑齐。