
简介面向STM32F030等嵌入式开发者的BMI160陀螺仪自写驱动参考包用于快速理解三轴陀螺仪通过I2C总线进行寄存器配置与数据读取的基本流程满足无人机、机器人等场景的角速度测量需求。压缩包共3个文件包括BMI160.h、BMI160_prm.h两个头文件及BMI160.c源文件整体仅6KB结构精简便于逐行研读。头文件定义了寄存器宏、参数结构体及驱动函数接口参数文件可配置灵敏度系数、数据滤波等关键选项源文件则实现了初始化、读写寄存器等基础功能但通讯所用的I2C初始化需由用户自行完成。作者明确注明驱动功能尚不完整开发者可在此框架上补充中断触发、FIFO缓冲、低功耗策略或姿态解算算法以适配完整应用需求。目前已有1887人学习下载适合需要从零编写BMI160驱动的嵌入式初学者参考。1. 为什么自己写驱动一份能在一小时内跑通的 BMI160 加速度计与陀螺仪驱动拿到 BMI160 模块时最容易踩的坑是原厂 SDK 功能太全磁力计、计步器、活动识别、姿态引擎全给你塞进来几千行代码改一个接口要翻半天。而实际做一个惯性测量项目往往只需要加速度计和陀螺仪的数据。这份驱动就是冲着“最小可用”来的上电校验、软复位、陀螺仪与加速度计数据读取、单位换算、FIFO 批量读取、中断映射全部集中在几个文件里。移植到新平台只需要替换两个 I2C 读写回调不需要动驱动核心逻辑。适合两类人手里有 BMI160 模块、想快速出数据验证的同学以及想把“看数据手册写外设驱动”这条路走一遍的嵌入式开发者。2. 初始化流程寄存器地图与最小驱动骨架2.1 先看清楚 BMI160 是什么、通信上要注意什么BMI160 是一颗六轴惯性传感器内部集成了三轴加速度计和三轴陀螺仪并且支持外接磁力计做九轴融合。数字接口支持 I2C 和 SPI片内还有 FIFO 和中断引擎可以在 MCU 睡眠时继续采集数据等 MCU 醒来一次性搬走。这颗芯片默认 I2C 地址由 SDO 引脚决定SDO 接低电平是 0x68接高电平是 0x69。上电后第一件事不是读数据而是读 CHIP_ID 确认 I2C 通路正常这一步能过滤掉至少一半的硬件接线问题。芯片内部的寄存器可以分成几组状态与 ID 寄存器、加速度计配置寄存器、陀螺仪配置寄存器、数据输出寄存器、FIFO 与中断相关寄存器、命令寄存器。驱动设计上最忌讳“每个函数各自直接操作寄存器”项目一大就乱。我的做法是先定义一张寄存器映射头文件把所有地址和关键命令值集中管理然后对上层只暴露设备句柄和读写函数指针。这样以后换平台、换传感器型号改动被限制在一个很小的范围内。2.2 寄存器地图驱动里最常见的十几个地址下面这张表是驱动里最常用的一组寄存器。不同批次数据手册的地址常量命名可能有一点差异但功能位置基本一致拿到芯片后建议对照你自己的 datasheet 核对一遍。驱动代码里所有地址都集中在 bmi160_regs.h就算某个地址不同也只需要改头文件。地址寄存器作用0x00CHIP_ID芯片 IDBMI160 固定读出 0xD10x0BACCEL_RANGE加速度计量程配置0x0DGYRO_RANGE陀螺仪量程配置0x12~0x17ACCD_X/Y/Z加速度三轴原始数据每轴 2 字节0x18~0x1DGYRO_X/Y/Z陀螺仪三轴原始数据每轴 2 字节0x7ECMD命令寄存器写命令触发软复位、电源模式切换等操作数据寄存器读取时利用了 I2C 的地址自动递增特性从 0x12 连续读 6 个字节就能一次拿到加速度 X、Y、Z 的完整 16 位数据从 0x18 连续读 6 个字节就是陀螺仪三轴。这种读法比一轴一轴读更重要不只是速度问题而是能保证一次采样里三个轴来自同一时刻。后面拆数据读取时会细说。2.3 最小初始化流程先跑通 ID 校验、软复位、电源模式初始化顺序有讲究。直接上电就读数据大概率拿到的是加速度计和陀螺仪休眠状态下的旧值。标准流程是读 CHIP_ID 校验链路写软复位命令等芯片重启完成再分别把加速度计和陀螺仪切到 normal 模式最后配置量程。下面这段代码是驱动核心 bmi160_init 的简化版也是整个驱动最开始跑的流程。int bmi160_init(bmi160_t *dev) { uint8_t id 0; if (!dev || !dev-read || !dev-write || !dev-delay_ms) { return BMI160_ERR_PARAM; } // 1. 读 CHIP_ID确认 I2C/SPI 通路和器件地址都正确 dev-read(dev-addr, BMI160_REG_CHIP_ID, id, 1); if (id ! BMI160_CHIP_ID_VALUE) { return BMI160_ERR_CHIP_ID; } // 2. 软复位写 0xB6 到 CMD 寄存器芯片内部自动恢复默认值 uint8_t cmd BMI160_CMD_SOFTRESET; dev-write(dev-addr, BMI160_REG_CMD, cmd, 1); dev-delay_ms(20); // 复位完成后需要等芯片内部稳定 // 3. 加速度计切换到 normal 模式命令值 0x11 cmd BMI160_CMD_ACCEL_PM_NORMAL; dev-write(dev-addr, BMI160_REG_CMD, cmd, 1); dev-delay_ms(10); // 4. 陀螺仪切换到 normal 模式命令值 0x15 cmd BMI160_CMD_GYRO_PM_NORMAL; dev-write(dev-addr, BMI160_REG_CMD, cmd, 1); dev-delay_ms(10); // 5. 设置量程加速度计 ±2g陀螺仪 ±2000dps uint8_t range BMI160_ACCEL_RANGE_2G; dev-write(dev-addr, BMI160_REG_ACCEL_RANGE, range, 1); range BMI160_GYRO_RANGE_2000DPS; dev-write(dev-addr, BMI160_REG_GYRO_RANGE, range, 1); return BMI160_OK; }参数说明dev 是驱动句柄四个函数指针分别是读、写、毫秒延时read/write 的第一个参数 addr 是 I2C 从机地址。这个设计的核心是“平台无关”STM32、Linux 用户态、RT-Thread 设备框架都能用同一套 bmi160.c。CHIP_ID 读出不对时不需要继续往下走先检查接线和地址。软复位命令写入后一定要等芯片内部要重新加载出厂校准参数常见驱动只等 1~2 毫秒我习惯等 20 毫秒更稳在低功耗项目里这个耗时也不算什么。写完这段再补充一个容易被忽略的细节命令行里写 CMD 寄存器不能像普通寄存器一样直接赋值命令值是一次性的。如果你用调试器连续往 0x7E 写两次相同命令第二次不一定生效因为命令寄存器被读取或写入后就会自清零。这也是我看到很多新手代码里“明明写了 normal 模式读 PMU_STATUS 还是 suspend”的原因。常见做法是写完命令后延时再读电源状态寄存器确认模式已经切过去确认不了就返回错误码避免后面拿着空数据算半天。3. 核心数据链路原始值读取、单位换算与数据校验3.1 一次连续读六字节别一轴一轴读数据读取是整个驱动里最直观的部分但细节很多。BMI160 的数据寄存器是 16 位有符号数低字节在前高字节在后。读取加速度时要先申请一个 6 字节的缓冲区从 0x12 地址连续读寄存器地址会自动递增依次填好 X 低、X 高、Y 低、Y 高、Z 低、Z 高。代码看起来挺简单真正容易翻车的是符号拼接。int bmi160_read_accel_raw(bmi160_t *dev, int16_t acc[3]) { uint8_t buf[6] {0}; int ret; ret dev-read(dev-addr, BMI160_REG_ACCEL_DATA, buf, 6); if (ret ! 0) { return BMI160_ERR_IO; } // 低字节在前高字节在后需要先按无符号拼好再转有符号 acc[0] (int16_t)((uint16_t)buf[0] | ((uint16_t)buf[1] 8)); acc[1] (int16_t)((uint16_t)buf[2] | ((uint16_t)buf[3] 8)); acc[2] (int16_t)((uint16_t)buf[4] | ((uint16_t)buf[5] 8)); return BMI160_OK; }逻辑说明buf[1] 是高字节如果直接写成int16_t x buf[0] | (buf[1] 8)在某些编译器里 buf[1] 会被提升为 int左移 8 位后符号位可能已经错位最后赋给 int16_t 时得到的负数和你预期的不一样。先用 uint16_t 无符号拼接再整体强转成 int16_t两字节补码才能正确还原。陀螺仪读取函数和加速度完全对称只是起始地址变成 0x18代码里可以共用一个内部读函数只传不同的起始地址。这里还需要知道为什么强调一次读 6 个字节比三次各读 2 个字节好BMI160 内部三轴是同一时刻采样的如果分三次读第一次读 X 和第三次读 Z 之间可能已经隔了好几个采样周期芯片在运动时算出的姿态就会出现明显的轴间错位。连续读虽然也不能保证和芯片采样的绝对同时但能大幅缩短三次读取的时间窗口在低速运动场景下基本够用。3.2 从原始数到物理单位量程直接决定 LSB 灵敏度原始数据只有转换到物理单位才有意义。加速度转换公式是g raw / acc_lsb陀螺仪是dps raw / gyro_lsb。这个 lsb 不是固定值它跟着量程档位走。BMI160 加速度计常见量程和对应灵敏度如下表陀螺仪类似。加速度量程灵敏度 LSB/g±2g16384±4g8192±8g4096±16g2048陀螺仪量程灵敏度 LSB/(dps)±2000dps16.4±1000dps32.8±500dps65.5±250dps131.2±125dps262.4单位换算函数不宜每次都查表初始化时把当前量程对应的 lsb 提前存进 dev 结构体后续转换就是一次除法。下面这段是加速度计的换算实现陀螺仪同理。static float bmi160_acc_raw_to_g(int16_t raw, float lsb) { return (float)raw / lsb; } int bmi160_read_accel_g(bmi160_t *dev, float acc_g[3]) { int16_t raw[3] {0}; int ret; ret bmi160_read_accel_raw(dev, raw); if (ret ! BMI160_OK) { return ret; } acc_g[0] bmi160_acc_raw_to_g(raw[0], dev-acc_lsb); acc_g[1] bmi160_acc_raw_to_g(raw[1], dev-acc_lsb); acc_g[2] bmi160_acc_raw_to_g(raw[2], dev-acc_lsb); return BMI160_OK; }参数说明dev-acc_lsb 在 init 阶段根据量程寄存器赋值量程切到 ±16g 时如果还拿 ±2g 的 16384 去算结果会差 8 倍这种错误在静止时看不出来转动机器后角度计算全是乱的。我额外加了一个bmi160_set_acc_range()函数每次写量程寄存器后立即同步更新 dev-acc_lsb避免配置和数据解析两套量程不匹配。陀螺仪静止时的读数理论上接近 0但实际会有零偏这个放到后面避坑部分讲。3.3 数据就绪判断别把上一帧旧数据当新数据用另一个常见问题是读完数据不检查数据是否更新过。BMI160 的数据寄存器是“读到就锁存”的典型设计如果你读的速度比芯片采样速度快第二次拿到的还是上一次的旧值。高速运动场景下拿旧数据做姿态解算会出现台阶式跳变。驱动里一般提供一个bmi160_data_ready()函数轮询状态寄存器里的数据就绪位。BMI160 不同手册版本里 DRDY 位的定义略有不同我在代码里把它做成宏让你按自己的 datasheet 调整例如int bmi160_wait_data_ready(bmi160_t *dev, uint32_t timeout_ms) { uint8_t status 0; uint32_t tick dev-timestamp_ms(); while (dev-timestamp_ms() - tick timeout_ms) { dev-read(dev-addr, BMI160_REG_STATUS, status, 1); if (status BMI160_DRDY_MASK) { return BMI160_OK; } dev-delay_ms(1); } return BMI160_ERR_TIMEOUT; }逻辑说明每次读取原始数据前先调这个函数能保证拿到的是一帧完整的新数据。timeout 参数很重要I2C 挂在共享总线上时如果传感器没供电或地址错误读操作可能一直不成功没有超时机制的驱动会让整个任务卡死在轮询里。实际项目里我一般把超时设成 100 毫秒超过就返回错误码上层可以据此做异常处理而不是拿坏数据继续算。4. 进阶玩法FIFO 缓存与中断唤醒配置4.1 什么时候该用 FIFO很多人在驱动里只读寄存器忽略了 BMI160 内置的 FIFO。这里有一个典型场景低功耗环境监测设备MCU 大部分时间在睡眠传感器一直在跑等 MCU 醒来后一次性读出这段时间的数据。如果不用 FIFOMCU 要么频繁醒来读数据浪费功耗要么错过采样点数据不连续。BMI160 的 FIFO 有约 1KB 缓存空间可以把加速度计、陀螺仪数据按帧缓存进去还能设置水位线存到一定字节数就通过中断引脚通知 MCU 来取。对做姿态解算的人来说FIFO 还能保证每一帧数据都是完整的三轴采样不会读到一半被上下文切换打断。FIFO 配置可以从驱动里拆成一个独立函数。配置项包括哪些数据源写入 FIFO、FIFO 是否使用 16 位数据、中断触发模式是水位线还是满。下面代码展示的是最常见的“加速度计和陀螺仪都进 FIFO存到 100 字节时触发中断”的配置写法具体寄存器位定义放在 bmi160_regs.h 里方便对照手册调整。int bmi160_fifo_config(bmi160_t *dev, bmi160_fifo_cfg_t *cfg) { uint8_t f0 0; uint8_t f1 0; // FIFO_CONFIG_0选择写入 FIFO 的数据源这里 acc 和 gyro 都写 // 位定义bit0accel, bit1gyro, bit616bit 数据 f0 BMI160_FIFO_ACC_EN | BMI160_FIFO_GYRO_EN | BMI160_FIFO_16BIT; dev-write(dev-addr, BMI160_REG_FIFO_CONFIG0, f0, 1); // FIFO_CONFIG_1设置水位线低字节超过该字节数后触发中断 f1 (uint8_t)(cfg-watermark_bytes 0xFF); dev-write(dev-addr, BMI160_REG_FIFO_CONFIG1, f1, 1); return BMI160_OK; }参数说明cfg-watermark_bytes 是中断触发阈值单位是字节。如果 acc 和 gyro 都用 16 位数据每帧 6612 字节100 字节大约能存 8 帧。FIFO 配置有个注意点修改 FIFO 配置前最好先发 FIFO flush 命令把积压的旧数据清掉否则新配置会从残留数据后面开始写解析时第一帧可能不完整。4.2 把 FIFO 中断引出来中断映射与中断处理BMI160 有两个可编程中断引脚 INT1 和 INT2可以映射多种中断源。驱动里常见的组合是数据就绪中断走 INT1 唤醒 MCUFIFO 水位线中断走 INT2 触发批量读取。和 FIFO 配置一样中断映射寄存器的位定义需要按 datasheet 填下面是逻辑模板。int bmi160_fifo_int_enable(bmi160_t *dev, uint8_t int_pin) { uint8_t int_map 0; // 开启 FIFO watermark 中断并映射到指定引脚 int_map | BMI160_INT_MAP_FIFO_WM; dev-write(dev-addr, BMI160_REG_INT_MAP, int_map, 1); return BMI160_OK; }中断服务函数里不要直接解析数据正确做法是只置一个标志位把 FIFO 读取放到主循环或者任务里。FIFO 数据读取流程分三步先读 FIFO 长度寄存器知道有多少字节再连续读取这些字节最后按帧格式解析。FIFO 里的数据不是从 0x12 这类固定寄存器读的而是从 FIFO 数据寄存器连续读。解析时先看帧头BMI160 常见的帧头会区分当前帧是加速度、陀螺仪还是无效数据解析伪代码如下。while (fifo_len 12) { read_fifo(header, 1); // 读帧头 if (header BMI160_FIFO_HEAD_ACC) { read_fifo(acc_raw[0], 6); // 读加速度数据 } else if (header BMI160_FIFO_HEAD_GYRO) { read_fifo(gyro_raw[0], 6); // 读陀螺仪数据 } else { break; // 无效帧或格式不对停止解析 } fifo_len - 12; }逻辑说明这个循环假设 acc 和 gyro 帧都固定 6 字节数据加上 1 字节帧头共 7 字节两帧在一起读时一次消耗 12 字节。实际 BMI160 的 FIFO 帧格式有几种模式16 位和 8 位数据长度不同且可能带时间戳。驱动里我把帧头宏集中定义你拿到芯片后第一件事是拿数据手册确认你选的模式对应的帧头值别直接照抄网上代码。FIFO 解析最容易翻车的就是这里数据流看起来全对但帧头判断条件写反所有帧被当成无效帧丢掉。4.3 FIFO 在姿态解算里的用法建议FIFO 不只是低功耗的产物。跑姿态解算时如果每次 I2C 读 6 字节然后送进解算主循环哪怕有一点抖动采样时间间隔就不均匀互补滤波和四元数都会受影响。用 FIFO 批量读回几十帧数据后再按固定采样周期喂给解算算法输入数据就规律得多。常见做法是把 FIFO 水位线按 10 帧设置对应 120 字节中断后一次搬回所有帧在 MCU 端按 100Hz 的时间戳处理。这一步不改传感器只改数据读取方式但对最终姿态稳定性提升很大。5. 常见问题排查五条最让人翻车的血泪经验5.1 读 CHIP_ID 一直返回 0xFF 或 0x00现象初始化第一步就失败CHIP_ID 寄存器读出来不是 0xD1而是 0xFF 或者 0x00。原因分两类I2C 总线上根本没有设备应答或者地址选错了。BMI160 的 SDO 引脚决定地址很多模块上 SDO 已经默认接好有些接高有些接低用 0x68 去扫 0x69 的设备当然读不到。另一个原因是模块供电没稳定芯片还在上电复位中I2C 从机没起来。解决先用 I2C 扫描工具把总线上所有地址打一遍确认设备实际挂在哪个地址再检查模块 VDDIO 和 VDD 供电时序有些开发板的 IO 电平转换电路在上电瞬间会拉低 SDO。调试阶段我习惯在 init 函数里把读到的 id 通过串口打印出来不直接返回错误这样能看到“读回来的是 0xD1 但程序还是报错”这种诡异情况多半是 I2C 读写函数本身有问题比如寄存器地址参数传成了设备地址。5.2 读完数据全是 0 或固定小值现象初始化返回 OK但加速度计三个轴读出来要么全是 0要么是个不变化的小数。原因加速度计或陀螺仪根本没进入 normal 模式。BMI160 上电默认是 suspend 状态只写量程寄存器不会唤醒测量核心必须先往 CMD 寄存器写电源模式命令。如果写完命令后立刻读数据芯片还在切换模式的延时窗口里读到的还是旧值。解决写完 0x11/0x15 后至少等 10 毫秒再开始读数据。我一般会加一个确认步骤读 PMU_STATUS检查对应 bit 是否变成 normal mode。确认不了就返错不要拿 0 数据继续跑。这个坑在低功耗项目里尤其明显因为驱动为了省电会在空闲时把传感器切回 suspend唤醒后忘等延时数据就周期性出现一帧 0。5.3 换算成 g 之后数值差了好几倍现象芯片水平放置时Z 轴加速度读数不是 1g 左右而是 0.25g 或者 4g。原因加速度量程是 ±16g但换算时还在用 ±2g 的 16384 LSB/g。BMI160 默认量程不一定是 ±2g如果你初始化时没写量程寄存器它按默认量程跑但换算函数里写死了一个灵敏度常数两边就错位了。解决把灵敏度作为设备状态的一部分量程一变lsb 跟着变。我在驱动里封装bmi160_set_acc_range()内部写入量程寄存器后立即更新 dev-acc_lsb所有换算函数统一从结构体取值不允许在换算函数里写死常量。检查方法很简单芯片平放桌面Z 轴换算结果应在 1g 附近X、Y 轴接近 0。5.4 原始数据符号不对显示的小数对负数全错现象静止时数据看起来正常但翻转传感器后原本应该是负数的方向变成了一个很大的数比如 -1g 变成了 65535 或者 32767。原因16 位补码拼接时没有正确处理符号位。(uint8_t)0xFF 8在 int 下等于 0xFF00直接和低字节拼接后赋给 int16_t符号位被当成数据位了。这个坑在 gcc 和 keil 下表现还不完全一样gcc 的整型提升规则会先转成 int再截断回 int16_t结果通常也错但错得更隐蔽。解决统一用(int16_t)((uint16_t)buf[0] | ((uint16_t)buf[1] 8))这种无符号拼接再强转的写法。另外在测试时把每个轴都旋转 90 度验证一下正负号不要只看静止数据。因为加速度计静止时 X 和 Y 都是 0只有 Z 有值符号问题要转动后才能暴露。5.5 陀螺仪静止时数据一直乱跳现象传感器放在桌面上不动陀螺仪 Z 轴读出来的角速度不是 0而是来回跳的 ±2 dps 甚至更大。原因第一这是正常的零偏不是驱动 bug第二可能是温度变化导致的温漂第三如果数据跳得超过 5 dps可能量程配置和芯片实际配置不一致或者 I2C 读到了未更新完的中间状态。陀螺仪的零偏跟芯片个体有关出厂校准值在寄存器里但温度一变又会漂。解决上电初始化完成后让芯片静止连续采集 100 组陀螺仪原始值求平均作为角速度零偏后续每次读数都减去这个零偏。我习惯把零偏校正放到驱动里做成一个独立接口bmi160_gyro_offset_calibrate()不把校准逻辑散落在应用层。注意校准时机必须在芯片完全静止时进行如果用户拿着板子校准算出来的零偏会把正常运动信号也吃掉。6. 二次开发换平台、加 SPI、接姿态解算驱动设计里最值得复用的是那个“函数指针注入”的思路。移植到 STM32 时只需要写一个符合读写回调签名的函数比如用 HAL 库实现static int8_t platform_i2c_read(uint8_t addr, uint8_t reg, uint8_t *buf, uint16_t len) { return HAL_I2C_Mem_Read(hi2c1, addr 1, reg, I2C_MEMADD_SIZE_8BIT, buf, len, 100); }参数说明addr 是 BMI160 的 7 位地址HAL 里要左移 1 位变成 8 位地址I2C_MEMADD_SIZE_8BIT 表示寄存器地址是单字节。写函数用 HAL_I2C_Mem_Write 对称实现。换到 Linux 用户态就把函数体改成 ioctl 操作 /dev/i2c-x 设备换到 RTOS 就加个互斥锁。核心的 bmi160.c 完全不用动这是这套驱动最值得保留的骨架。加 SPI 支持也是同一套思路。SPI 模式下读寄存器时要先发送寄存器地址地址最高位写 1 表示读写寄存器地址最高位是 0。驱动核心不关心底层是 I2C 还是 SPI回调函数内部实现即可甚至可以在同一份代码里通过编译宏切换两种总线。接姿态解算时把 bmi160_read_accel_g 和 bmi160_read_gyro_dps 的输出直接喂给互补滤波或四元数算法注意保持固定的调用周期。从那以后我每次移植 IMU 驱动都会强制跑一遍“ID 校验、量程换算、静止零偏校准”这三个测试确认通过才继续往下做这套动作帮我省掉了大量定位问题的时间。希望帮到你。本文还有配套的精品资源点击获取