☰
BMI160六轴IMU驱动移植:I2C初始化与FIFO中断调试
2026/10/26 12:18:24 网站建设 项目流程

简介:资源围绕博世BMI160六轴惯性测量单元的官方驱动与数据手册展开,面向嵌入式驱动开发及传感器应用工程师。BMI160集成三轴加速度计与三轴陀螺仪,常见于移动设备、无人机、物联网终端与智能穿戴的运动追踪、姿态检测和方向感知。资源包共17个文件、约2.22MB,主体是c与h源码,包含驱动实现、寄存器定义、结构体与函数原型;另有一份PDF数据手册、README集成说明及若干工程索引类附属文件,便于了解编译方式与依赖要求。目前已有1059人学习下载,从寄存器级裸机驱动到系统集成各阶段均可参考。读者据此可完成传感器初始化、量程与输出速率配置、数据读取与校准,并对照手册核对电气特性、接口协议、引脚定义及误差模型,减少从零摸索的调试成本。

1. BMI160 调不通的那半天,问题往往不在算法里

BMI160 这颗六轴 IMU 最关键、也最容易卡住的地方,并不在“能不能读到寄存器”,而在“能不能拿到一条可信的数据”。板子焊回来,I2C 上有波形,chip id 也能对上 0xD1,可加速度计的读数纹丝不动,或者抖得像心电图——我第一次碰它的时候就在这个状态里耗了大半天,最后发现是 PMU 模式没切、FIFO 模式还停在 bypass。这份资源正好瞄准了这一段:原厂驱动包(C 源码,寄存器封装、量程/ODR 配置、FIFO 拆帧都在里面)加数据手册。它适合手上已有 MCU 工程、要把 BMI160 真正跑起来做姿态采集、可穿戴或云台控制的人。接下来的顺序是:包里有什么、怎么接到自己的总线上、怎么出数、怎么进 FIFO 和中断、哪里会翻车、最后怎么判断数据可信。

2. 拆开资源包:驱动层、寄存器层和手册各自负责哪一段

2.1 驱动的三层结构,改哪一层心里要有数

原厂这套 Sensor API 的组织方式很典型,分成三层,把这三层分清,后面调试基本不会打乱顺序。

层典型内容谁负责你会不会动它
总线适配层单寄存器写、连续读、微秒/毫秒延时你必动,每个平台都不一样
器件抽象层量程、ODR、滤波器、PMU 模式、FIFO 配置驱动包一般不动,只调参数
数据层原始值读取、FIFO 拆帧、sensortime 提取驱动包偶尔改,比如自定义缓冲区

中间那层是驱动的价值所在:bmi160_set_sens_conf()一次调用背后其实是往 0x40、0x41、0x42、0x43 写四组值,再往 0x7E 发命令切 PMU 模式,然后轮询 0x03 确认生效。自己从零写这段大概要一两天,用现成的只要对一遍参数名。最上面那层是唯一没法直接复用的部分——你的 I2C 可能是软件模拟的,也可能是硬件外设加 DMA,驱动包不可能替你决定。

移植的最小集合就三个函数:一次写、一次连续读、一个延时。返回值沿用驱动里的错误码宏,0 表示成功,非 0 直接打出来对着头文件查,比猜快得多。

/* bmi160_port.c —— 总线适配层只做三件事:写寄存器、连续读、延时 */ #include "bmi160.h" static int8_t bmi160_bus_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len) { if (i2c_start((dev_addr << 1) | 0) != 0) return BMI160_E_COM_FAIL; i2c_write(reg_addr); while (len--) { if (i2c_write(*data++) != 0) return BMI160_E_COM_FAIL; } i2c_stop(); return BMI160_OK; } static int8_t bmi160_bus_read(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len) { if (i2c_start((dev_addr << 1) | 0) != 0) return BMI160_E_COM_FAIL; i2c_write(reg_addr); /* 重复起始,中间不能插 stop,否则寄存器地址会丢 */ if (i2c_start((dev_addr << 1) | 1) != 0) return BMI160_E_COM_FAIL; while (len--) { *data++ = i2c_read(len != 0); /* 最后一字节回 NACK,让从机释放 SDA */ } i2c_stop(); return BMI160_OK; } static void bmi160_bus_delay_us(uint32_t us) { delay_us(us); }

这段代码里两个地方最容易写错。一是重复起始,很多软件 I2C 的i2c_start内部会先发一个 stop,读寄存器就永远拿到 0xFF。二是最后一个字节的 ACK/NACK,全程 ACK 会让某些主控在收尾时把总线拉死,表现为“第一次能读,第二次卡住”。延时函数的精度不用太苛刻,驱动里主要用它等软复位和 PMU 切换,给个 10% 误差无所谓。

然后就是把三个函数挂进设备结构体:

struct bmi160_dev sensor = {0}; sensor.id = BMI160_I2C_ADDR; /* SDO 接地 0x68,接 VDDIO 是 0x69 */ sensor.interface = BMI160_I2C_INTF; sensor.read = bmi160_bus_read; sensor.write = bmi160_bus_write; sensor.delay_ms = bmi160_bus_delay_ms;

id这一项不是随便填的,它决定驱动发出去的从机地址。板子上 SDO 引脚悬空是最常见的翻车点:悬空电平不确定,有时能通有时不能,换个批次的板子就复现不了,所以硬件上必须明确拉高或拉低。

2.2 Datasheet 里真正要反复翻的四块内容

手册几百页,实际调试会翻来覆去看的就那么几块,剩下的等出问题再查。

手册内容什么时候用关键信息
引脚定义与供电范围画板、飞线、怀疑供电VDD 与 VDDIO 是两路独立电源,电平不匹配会读不到
寄存器地图驱动没封装的功能要自己写数据寄存器、FIFO、中断映射的地址
FIFO 与中断章节从轮询切到中断驱动头字节格式、水印单位、中断使能与映射
时序与上电参数通信不稳定、偶发丢包上电到可通信的等待时间、总线最高速率

供电那部分值得单独说一句:VDD 和 VDDIO 是分开的,VDDIO 决定 IO 电平。有人把 VDDIO 直接接到 3.3V 而 MCU 是 1.8V 的 IO,通信能通但偶尔误码,这种问题用示波器看波形是“勉强能看”,用逻辑分析仪抓数据才会发现高低电平裕量不够。

2.3 移植前先定总线:I2C 与 SPI 的取舍

两种接口驱动包都支持,选哪个取决于你的采样率和布线。

维度I2CSPI
引脚数2(SDA/SCL)4(CS/SCK/MOSI/MISO)
典型速率400 kHz几 MHz 到 10 MHz
多设备地址区分,一条总线挂多颗每颗一个片选
常见坑上拉电阻、地址冲突、总线被拉死片选时序、MISO 空闲电平

FIFO 一次读几百字节的场景,I2C 400 kHz 下大概几毫秒,还在可接受范围;如果要高 ODR 加实时读取原始数据,SPI 更稳。走 SPI 时 0x7E 命令寄存器依然要写,但读数据可以连续 burst,不需要每字节重复发地址。

一个容易忽略的细节:CSB 引脚在 I2C 模式下必须拉高。它悬空时芯片可能识别成 SPI 从机,表现就是 I2C 地址怎么都不应答,很多人卡在这里然后怀疑焊接。

3. 从软复位到出第一个数:初始化与量程配置的实操路径

3.1 上电时序、软复位和 chip id 校验

初始化这段代码不长,但顺序错一步就是白忙。

struct bmi160_dev sensor = {0}; uint8_t chip_id = 0; int8_t rslt; /* 1. 上电稳定,手册给的最小等待时间别省,省了就是偶发读不到 */ bmi160_bus_delay_ms(10); /* 2. init 内部流程:读 chip id → 软复位(0x7E 写 0xB6) → 等复位完成 → 再读 chip id */ rslt = bmi160_init(&sensor); if (rslt != BMI160_OK) { /* 把 rslt 打出来,通信类错误和 ID 不匹配是两码事 */ } /* 3. 想自己确认一次,单独读 0x00,正常值固定为 0xD1 */ rslt = bmi160_get_regs(0x00, &chip_id, 1, &sensor);

bmi160_init()内部已经包含了软复位,不需要在外面再手动写 0x7E。软复位的作用是把上一次运行留下的配置清干净——调试期间反复烧录固件,芯片是不掉电的,残留的 PMU 模式和 FIFO 状态会让新固件读到莫名其妙的值,这个坑我在第 5 章还会提。

复位之后如果马上配置量程,有概率写不进去。手册里复位时间写得很短,但实机上留 20 到 50 毫秒更保险,尤其在电源爬升比较慢的板子上。

3.2 量程与 ODR:把想要的参数翻译成寄存器值

参数不是随便填的,量程选小了会削顶,选大了分辨率浪费。以常见的姿态采集为例:±4g 够覆盖手持晃动和一般振动,±500 dps 够覆盖快速转动又不至于把零偏噪声放大。

/* 加速度计:±4g,ODR 200Hz,OSR4 滤波 */ sensor.accel_cfg.odr = BMI160_ACCEL_ODR_200HZ; sensor.accel_cfg.range = BMI160_ACCEL_RANGE_4G; sensor.accel_cfg.bw = BMI160_ACCEL_BW_OSR4_AVG1; sensor.accel_cfg.power = BMI160_ACCEL_NORMAL_MODE; /* 陀螺仪:±500dps,ODR 100Hz,正常滤波 */ sensor.gyr_cfg.odr = BMI160_GYRO_ODR_100HZ; sensor.gyr_cfg.range = BMI160_GYRO_RANGE_500_DPS; sensor.gyr_cfg.bw = BMI160_GYRO_BW_NORMAL_MODE; sensor.gyr_cfg.power = BMI160_GYRO_NORMAL_MODE; /* 一次把四组配置和 PMU 模式全写下去 */ rslt = bmi160_set_sens_conf(&sensor);

这几个宏背后对应的真实寄存器值,最好背下来,因为一旦驱动不生效,你得靠直接读写寄存器来定位:

寄存器地址本例取值含义
ACC_CONF0x400x2A高 4 位带宽 OSR4,低 4 位 ODR 200 Hz
ACC_RANGE0x410x05±4 g,灵敏度 8192 LSB/g
GYR_CONF0x420x28正常滤波,ODR 100 Hz
GYR_RANGE0x430x02±500 dps,灵敏度 65.6 LSB/dps
CMD0x7E0x11 / 0x15加速度计进 normal、陀螺仪进 normal

陀螺仪从 suspend 切到 normal 有个启动时间,手册里是几十毫秒量级,用 fast start 模式会短一些。这段时间读出来的陀螺仪数据是无效的,常见做法是切完模式后延时 100 毫秒再开始采,或者直接轮询 0x03(PMU_STATUS)等对应位变成 normal。省掉这一步的表现是“前几十帧陀螺仪数据全是 0 或者乱跳”,很容易误判成驱动有问题。

3.3 一次 burst 读 12 字节并换算成 g 与 dps

读数据有两个原则:一次读完、按 LSB 在前拼。

struct bmi160_sensor_data accel, gyro; /* 加速度 0x12~0x17、陀螺仪 0x0C~0x11 一次读完,别分两次读 */ rslt = bmi160_get_sensor_data(BMI160_ACCEL_SEL | BMI160_GYRO_SEL | BMI160_TIME_SEL, &accel, &gyro, &sensor);

不同版本的驱动包入参略有差异,有的把 sensortime 放在结构体字段里,有的作为单独出参,编译报错时对着bmi160.h的声明改一下就行,不用怀疑是自己写错了。

换算这一步纯粹是除法,但系数一定跟量程对齐:

/* ±4g 对应 8192 LSB/g,±500dps 对应 65.6 LSB/dps */ float ax_g = accel.x / 8192.0f; float ay_g = accel.y / 8192.0f; float az_g = accel.z / 8192.0f; float gx_dps = gyro.x / 65.536f; float gy_dps = gyro.y / 65.536f; float gz_dps = gyro.z / 65.536f; /* 温度:0x20/0x21,T = 23 + raw/512,单位摄氏度 */

驱动内部读 12 个字节是一次连续传输,中间不会插 stop,这样加速度和陀螺仪是同一时刻的快照。如果分两次调用分别读,中间隔了几百微秒,做姿态融合时两者不同步,表现为快速转动下角度明显滞后——这种误差不会报错,只会让你怀疑滤波算法。

另外,结构体打包要确认对齐。有些平台的编译器会在int16_t x, y, z之间插填充字节,直接拿结构体指针去接 DMA 缓冲区会错位,结果就是 x 轴的数据跑到 y 轴上。稳妥的做法是定义uint8_t buf[12],读完再手工拼。

4. 把轮询换成 FIFO 中断:1024 字节缓冲怎么用才不丢帧

4.1 FIFO 模式、header 与水印的配置顺序

轮询读数据在低速率下没问题,一旦 ODR 上到 200 Hz 以上,主循环稍微忙一点就会漏采样。BMI160 内置 1024 字节 FIFO,配合中断引脚可以把 CPU 占用压下来。

配置顺序有讲究:先开数据源,再切模式。

uint8_t fifo_buf[256]; /* 1. 先使能 FIFO 里的加速度、陀螺仪和头字节,此时还在 bypass 模式,不产生数据 */ bmi160_set_fifo_config(BMI160_FIFO_ACCEL | BMI160_FIFO_GYRO | BMI160_FIFO_HEADER, BMI160_ENABLE, &sensor); /* 2. 再切到 FIFO 模式,顺序反过来会先灌进一帧没有头字节的数据 */ bmi160_set_fifo_config(BMI160_FIFO_MODE, BMI160_ENABLE, &sensor); /* 3. 水印:字段以 4 字节为一个单位,先按 0x40 估,实机再按中断频率微调 */ bmi160_set_fifo_wm(0x40, &sensor); /* 4. 把水印中断使能并映射到 INT1 */ bmi160_set_intr_fifo_wm(BMI160_ENABLE, &sensor);

还有一处:FIFO_DOWNS(0x45)默认是0x88,表示加速度和陀螺仪都不降采样。有人为了省带宽把它改成 2 帧抽 1,结果 FIFO 里的帧率和 ODR 不再一致,算出来的时间戳全错,而且这个错误不会报任何异常,只是姿态角慢慢偏。改这一位之前先想清楚时间戳怎么对。

4.2 头字节逐帧解析与常见错位

用驱动包的bmi160_get_fifo_data()加bmi160_extract_accel()最省事,但如果要自己拆,头字节的规则得记住。

uint16_t i = 0; while (i < fifo_len) { uint8_t header = fifo_buf[i++]; if ((header & 0x80) == 0) { /* 无效帧 */ if (header & 0x40) continue; /* 配置变更帧,无载荷,跳过 */ break; /* 其余是 skip frame,FIFO 曾溢出丢过数据 */ } if (header & 0x04) { /* 6 字节加速度,LSB 在前 */ int16_t ax = (int16_t)(fifo_buf[i] | (fifo_buf[i + 1] << <p> <a href="https://download.csdn.net/download/zhipun/9670100" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询