简介:资源围绕博世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 的取舍
两种接口驱动包都支持,选哪个取决于你的采样率和布线。
| 维度 | I2C | SPI |
|---|---|---|
| 引脚数 | 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_CONF | 0x40 | 0x2A | 高 4 位带宽 OSR4,低 4 位 ODR 200 Hz |
| ACC_RANGE | 0x41 | 0x05 | ±4 g,灵敏度 8192 LSB/g |
| GYR_CONF | 0x42 | 0x28 | 正常滤波,ODR 100 Hz |
| GYR_RANGE | 0x43 | 0x02 | ±500 dps,灵敏度 65.6 LSB/dps |
| CMD | 0x7E | 0x11 / 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>