
简介面向嵌入式开发者的博世BMI160陀螺仪自写驱动参考代码聚焦STM32F030平台与传感器之间的I2C通信集成适合已具备单片机外设开发基础、希望快速打通六轴姿态采集链路的工程师学习借鉴。压缩包共3个文件约6KB以C源码与头文件为主一份实现寄存器读写、初始化与数据读取等核心逻辑两份头文件分别承载寄存器宏定义、函数原型及灵敏度、滤波等参数配置便于按需裁剪移植。目前已有1887人学习下载说明其在低成本惯性测量场景中具有一定参考价值。代码虽未覆盖中断处理、节能模式与数据融合等高级特性但清晰展示了BMI160的通信流程与驱动分层思路读者可据此补充卡尔曼滤波、姿态解算或功耗优化作为无人机、机器人及物联网设备原型开发的起步骨架。1. 从一颗焊歪的陀螺仪说起BMI160 自写驱动到底解决什么问题去年帮朋友调一块六轴姿态板硬件同事把 BMI160 焊上去飞线接好 I2C上电一读CHIP_ID寄存器返回0xD1心里一喜——芯片活着。可接下来读陀螺仪三轴数据全是0x00加速度计倒是偶尔蹦几个数。翻遍手头现成的驱动库要么只支持 SPI要么把 BMI160 和 BMI270 的寄存器地址混着用初始化流程里少了一步CMD寄存器解锁数据就永远出不来。最后干脆自己对着数据手册把驱动重写了一遍从复位、配置量程、使能传感器到读 FIFO一条链路走通板子才真正跑起来。这份「博世 BMI160 自写驱动」就是那次折腾的产物一套不依赖厂商闭源库、不绑定特定 RTOS 的轻量驱动覆盖 I2C/SPI 两种总线把 BMI160 的初始化、单次读取、FIFO 批量读取、中断配置都拆成了可读可改的函数。适合正在做姿态解算、可穿戴设备、云台或平衡车的开发者——尤其是那些被现成库的「黑匣子」初始化流程坑过、想自己掌控每一行寄存器操作的人。下面按「这驱动怎么组织 → 怎么跑起来 → 怎么改」的顺序拆代码都能直接抄。2. 驱动架构拆解三层分离与寄存器映射表2.1 为什么把总线层和芯片层拆开BMI160 的坑在于它同时支持 I2C 和 SPI而两种总线下的寄存器读写时序完全不同I2C 是「设备地址 寄存器地址 数据」SPI 是「片选拉低 读写位 寄存器地址 数据」且 SPI 读的时候要额外发一个 dummy byte。如果驱动里到处if (bus I2C)代码会烂成一锅粥。我采用的做法是三层结构最底下是总线抽象层只暴露read_reg/write_reg/read_burst/write_burst四个函数指针中间是芯片操作层所有 BMI160 的寄存器地址、位域定义、初始化序列都在这一层最上面是应用接口层给上层提供bmi160_init、bmi160_get_gyro、bmi160_get_accel这类语义化函数。总线层通过一个结构体注入换 MCU 或换总线只改这一层。/* bmi160_bus.h —— 总线抽象层四个函数指针搞定 I2C/SPI 切换 */ typedef struct { int8_t (*read_reg)(uint8_t dev_addr, uint8_t reg, uint8_t *data); int8_t (*write_reg)(uint8_t dev_addr, uint8_t reg, uint8_t data); int8_t (*read_burst)(uint8_t dev_addr, uint8_t reg, uint8_t *buf, uint16_t len); int8_t (*write_burst)(uint8_t dev_addr, uint8_t reg, const uint8_t *buf, uint16_t len); uint8_t dev_addr; /* I2C 从机地址SPI 模式下填片选编号 */ } bmi160_bus_t;这段结构体的关键在于dev_addr字段的双重语义I2C 下它是 7 位从机地址BMI160 默认0x68或0x69取决于 SDO 引脚电平SPI 下它退化成片选编号由具体实现去拉对应的 GPIO。函数指针返回int8_t约定0成功、负值失败这样上层不用关心底层是 HAL 库还是寄存器直写。2.2 寄存器映射表把数据手册搬进头文件自写驱动最费时但也最值钱的部分是把数据手册里的寄存器地址和位定义整理成一张表。BMI160 的寄存器不算多但分散在0x00到0x7F之间且很多寄存器是「写命令」和「读状态」共用地址容易搞混。/* bmi160_reg.h —— 只列关键寄存器完整表按数据手册补全 */ #define BMI160_REG_CHIP_ID 0x00 #define BMI160_REG_CMD 0x7E /* 命令寄存器软复位/使能传感器都走这里 */ #define BMI160_REG_GYRO_X_L 0x0C /* 陀螺仪 X 轴低字节连续 6 字节为三轴 */ #define BMI160_REG_ACCEL_X_L 0x12 /* 加速度计 X 轴低字节 */ #define BMI160_REG_FIFO_LENGTH 0x24 #define BMI160_REG_FIFO_DATA 0x26 #define BMI160_REG_ACCEL_CONF 0x40 /* 加速度计量程与输出速率 */ #define BMI160_REG_GYRO_CONF 0x42 /* 陀螺仪量程与输出速率 */ #define BMI160_REG_INT_EN 0x51 #define BMI160_REG_INT_MAP 0x55 /* 命令寄存器常用值 */ #define BMI160_CMD_SOFTRESET 0xB6 #define BMI160_CMD_ACCEL_NORMAL 0x11 #define BMI160_CMD_GYRO_NORMAL 0x15这里有个血泪经验CMD寄存器0x7E写入后不是立即生效芯片需要几十毫秒完成内部状态切换写完必须轮询STATUS寄存器0x1B的对应位等它置位才能继续下一步。很多现成库直接delay(100)糊弄过去在低功耗场景下要么等太久、要么偶尔失败自写驱动就该老老实实轮询。2.3 初始化序列顺序错了数据永远出不来BMI160 上电后的默认状态是「挂起」加速度计和陀螺仪都不采样。初始化必须按固定顺序软复位 → 等待复位完成 → 配置量程和输出速率 → 发命令使能传感器 → 等待使能完成 → 可选配置 FIFO 和中断。顺序颠倒比如先使能再配量程量程配置会被忽略。/* bmi160_init.c —— 初始化主流程 */ int8_t bmi160_init(const bmi160_bus_t *bus) { uint8_t chip_id 0; uint8_t status 0; int retry 0; /* 1. 读 CHIP_ID 确认芯片在线BMI160 固定为 0xD1 */ if (bus-read_reg(bus-dev_addr, BMI160_REG_CHIP_ID, chip_id) ! 0) return -1; if (chip_id ! 0xD1) return -2; /* 2. 软复位然后轮询 STATUS 等待复位完成 */ bus-write_reg(bus-dev_addr, BMI160_REG_CMD, BMI160_CMD_SOFTRESET); do { bus-read_reg(bus-dev_addr, BMI160_REG_STATUS, status); if (retry 100) return -3; /* 超时保护约 100ms */ } while (status 0x01); /* bit0 为复位中标志 */ /* 3. 配置加速度计±4g输出速率 100Hz */ bus-write_reg(bus-dev_addr, BMI160_REG_ACCEL_CONF, 0x28); /* 4. 配置陀螺仪±500dps输出速率 100Hz */ bus-write_reg(bus-dev_addr, BMI160_REG_GYRO_CONF, 0x28); /* 5. 使能加速度计和陀螺仪各自轮询状态位 */ bus-write_reg(bus-dev_addr, BMI160_REG_CMD, BMI160_CMD_ACCEL_NORMAL); retry 0; do { bus-read_reg(bus-dev_addr, BMI160_REG_STATUS, status); if (retry 100) return -4; } while (!(status 0x10)); /* bit4 为加速度计就绪 */ bus-write_reg(bus-dev_addr, BMI160_REG_CMD, BMI160_CMD_GYRO_NORMAL); retry 0; do { bus-read_reg(bus-dev_addr, BMI160_REG_STATUS, status); if (retry 100) return -5; } while (!(status 0x20)); /* bit5 为陀螺仪就绪 */ return 0; }逐段说明第 1 步读CHIP_ID是「验明正身」返回0xD1才继续否则可能是焊接虚焊或地址配错第 2 步软复位后轮询STATUS的 bit0这里用retry计数做超时避免死循环卡死主循环第 3、4 步的0x28是量程和速率的组合值具体位域要查数据手册的ACC_CONF和GYR_CONF定义改量程就改这个字节第 5 步使能后分别等 bit4 和 bit5两个传感器是独立使能的不能只等一个。提示STATUS寄存器的位定义在不同批次手册里略有差异落地前一定对照手头芯片的版本号确认别照抄网上的旧表。3. 跑起来从裸机到 Linux 的三条落地路径3.1 裸机 MCU用 HAL 库填总线层最常见的场景是 STM32 这类 MCUI2C 用 HAL 库现成的HAL_I2C_Mem_Read/HAL_I2C_Mem_Write把四个函数指针填好就能跑。下面以 I2C 为例注意HAL_I2C_Mem_Read的参数顺序是「设备地址、寄存器地址、地址长度、数据缓冲、长度、超时」。/* bmi160_bus_i2c.c —— STM32 HAL 下的总线层实现 */ #include bmi160_bus.h #include i2c.h /* CubeMX 生成的 I2C 句柄 */ static int8_t i2c_read_reg(uint8_t addr, uint8_t reg, uint8_t *data) { return (HAL_I2C_Mem_Read(hi2c1, addr 1, reg, I2C_MEMADD_SIZE_8BIT, data, 1, 100) HAL_OK) ? 0 : -1; } static int8_t i2c_write_reg(uint8_t addr, uint8_t reg, uint8_t data) { return (HAL_I2C_Mem_Write(hi2c1, addr 1, reg, I2C_MEMADD_SIZE_8BIT, data, 1, 100) HAL_OK) ? 0 : -1; } static int8_t i2c_read_burst(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) HAL_OK) ? 0 : -1; } static int8_t i2c_write_burst(uint8_t addr, uint8_t reg, const uint8_t *buf, uint16_t len) { return (HAL_I2C_Mem_Write(hi2c1, addr 1, reg, I2C_MEMADD_SIZE_8BIT, (uint8_t *)buf, len, 100) HAL_OK) ? 0 : -1; } const bmi160_bus_t bmi160_bus_i2c { .read_reg i2c_read_reg, .write_reg i2c_write_reg, .read_burst i2c_read_burst, .write_burst i2c_write_burst, .dev_addr 0x68, /* SDO 接地为 0x68接 VCC 为 0x69 */ };关键点addr 1是因为 HAL 库要的是 8 位地址含读写位而 BMI160 手册给的是 7 位地址超时统一给 100msI2C 速率 400kHz 下足够dev_addr填0x68还是0x69取决于硬件上 SDO 引脚接哪接错了读CHIP_ID会返回0xFF或超时。主函数里初始化后读一次数据验证/* main.c 片段 */ bmi160_bus_t bus bmi160_bus_i2c; if (bmi160_init(bus) ! 0) { printf(BMI160 init failed\r\n); while (1); } int16_t gyro[3], accel[3]; bmi160_get_gyro(bus, gyro); /* 单位LSB需按量程换算成 dps */ bmi160_get_accel(bus, accel); printf(gyro: %d %d %d\r\n, gyro[0], gyro[1], gyro[2]);bmi160_get_gyro内部就是一次 6 字节 burst 读从GYRO_X_L开始连续读再拼成三个int16_t。量程 ±500dps 时换算系数是500.0 / 32768.0乘一下就是真实角速度。3.2 Linux 用户态i2c-dev 直接操作如果板子跑 Linux不想写内核驱动用/dev/i2c-N字符设备最省事。总线层换成ioctl(I2C_RDWR)即可芯片层代码一行不用改。/* bmi160_bus_linux.c —— Linux i2c-dev 实现 */ #include linux/i2c-dev.h #include sys/ioctl.h #include fcntl.h #include unistd.h static int i2c_fd -1; static int8_t linux_read_reg(uint8_t addr, uint8_t reg, uint8_t *data) { struct i2c_msg msgs[2] { { .addr addr, .flags 0, .len 1, .buf reg }, { .addr addr, .flags I2C_M_RD, .len 1, .buf data }, }; struct i2c_rdwr_ioctl_data pkt { .msgs msgs, .nmsgs 2 }; return (ioctl(i2c_fd, I2C_RDWR, pkt) 2) ? 0 : -1; }i2c_msg数组的写法是「先写寄存器地址再读数据」两次传输之间不发 STOP这是 I2C 读寄存器的标准时序。i2c_fd在main里用open(/dev/i2c-1, O_RDWR)打开再用ioctl(fd, I2C_SLAVE, 0x68)绑定从机地址。注意 Linux 下addr是 7 位地址不要左移。3.3 SPI 模式多一个 dummy byteSPI 下读寄存器要发「读命令 寄存器地址 dummy」写则是「写命令 寄存器地址 数据」。读命令是0x80 | reg写命令是reg 0x7F。burst 读时 dummy 只发一次后续连续时钟即可。static int8_t spi_read_reg(uint8_t cs, uint8_t reg, uint8_t *data) { uint8_t tx[3] { 0x80 | reg, 0x00, 0x00 }; uint8_t rx[3] { 0 }; cs_low(cs); spi_transfer(tx, rx, 3); /* 第 3 字节才是有效数据 */ cs_high(cs); *data rx[2]; return 0; }0x80 | reg的 bit7 是读标志这是 BMI160 SPI 协议的约定rx[2]才是寄存器值rx[1]是 dummy。SPI 速率可以跑到 10MHz比 I2C 快得多FIFO 批量读时优势明显。4. 避坑与排查五个真实翻车现场4.1 读 CHIP_ID 返回 0xFF 或超时现象初始化第一步就失败read_reg返回负值或读到0xFF。原因九成是硬件问题——I2C 上拉电阻没焊、SDO 引脚电平与代码里的dev_addr不匹配、或者芯片供电不足BMI160 要求 1.71V~3.6V某些板子用 1.8V 但上拉到了 3.3V 导致电平不兼容。解决先用示波器看 SCL/SDA 有没有波形再量 SDO 引脚电压确认地址最后查供电。软件上把dev_addr的0x68和0x69都试一遍能读到0xD1就说明地址对了。4.2 初始化返回成功但数据恒为 0现象bmi160_init返回 0读陀螺仪三轴全是 0。原因最常见的是量程配置字节写错比如把GYR_CONF的 bit4~6 量程位和 bit0~3 输出速率位搞反芯片进了非法配置直接不采样其次是使能命令发了但没等STATUS就绪就读数据读到的是空寄存器。解决把ACCEL_CONF和GYRO_CONF的写入值打印出来对照手册逐位核对确保使能后轮询到对应状态位再读数据。我一般会在初始化末尾加一句「读一次数据并打印」数据非零才算初始化真正成功。4.3 FIFO 读出来的数据错位现象用 FIFO 批量读时解析出的三轴数据偶尔跳变或整体偏移。原因BMI160 的 FIFO 帧头有 1 字节 header里面编码了帧类型和长度直接按固定 6 字节切会错位另外FIFO_LENGTH寄存器读的是当前 FIFO 里的字节数读之前必须先读它读完数据后还要更新。解决先读FIFO_LENGTH拿到字节数再 burst 读FIFO_DATA解析时按 header 的 bit 判断是加速度计帧还是陀螺仪帧不能假设固定顺序。FIFO 这块建议先关掉用单次读把数据链路调通再开。4.4 中断配置后不进中断现象配了INT_EN和INT_MAP数据就绪中断死活不触发。原因BMI160 的中断引脚是推挽/开漏可配的默认可能是高阻态另外中断映射寄存器INT_MAP要把具体中断源如INT1_DRDY映射到物理引脚只使能不映射等于没配。解决检查INT_MAP是否把数据就绪中断映射到了 INT1 或 INT2确认 MCU 侧引脚配置为上升沿或高电平触发与芯片输出极性一致用万用表量中断引脚静态电平排除硬件虚焊。4.5 低功耗模式下唤醒后数据异常现象芯片进低功耗再唤醒读数漂移或量程变了。原因BMI160 进低功耗CMD写0x12/0x14会关闭部分内部电路唤醒后需要重新配置量程且唤醒命令后同样要轮询状态位。解决把「配置量程」和「使能传感器」封装成一个可重复调用的函数每次从低功耗唤醒都完整走一遍别指望寄存器保持原值。5. 二次开发把驱动改成你自己的姿态采集节点5.1 加一层数据换算与校准驱动层只给原始 LSB 值实际项目里要换算成物理单位并做零偏校准。我一般在上层加一个bmi160_data_t结构体和换算函数量程系数做成宏换量程只改一处。/* bmi160_convert.h —— 物理量换算与简易零偏校准 */ #define GYRO_LSB_PER_DPS 65.5f /* ±500dps 量程下32768/500 */ #define ACCEL_LSB_PER_G 8192.0f /* ±4g 量程下32768/4 */ typedef struct { float gyro_dps[3]; float accel_g[3]; float gyro_bias[3]; /* 上电静止时采样求平均得到 */ } bmi160_data_t; static inline void bmi160_convert(const int16_t *raw_gyro, const int16_t *raw_accel, bmi160_data_t *out) { for (int i 0; i 3; i) { out-gyro_dps[i] raw_gyro[i] / GYRO_LSB_PER_DPS - out-gyro_bias[i]; out-accel_g[i] raw_accel[i] / ACCEL_LSB_PER_G; } }GYRO_LSB_PER_DPS和ACCEL_LSB_PER_G必须和初始化时写的量程字节严格对应改量程忘了改系数是新手最常犯的错。零偏校准的做法是上电后静止采 200 组求平均存进gyro_bias之后每次换算减掉。5.2 用定时器驱动采样别在主循环里裸读裸机项目里最常见的错误是在while(1)里直接读传感器采样率随主循环负载波动做姿态解算时积分时间不准。正确做法是用定时器中断或 RTOS 任务固定周期读一次把数据塞进环形缓冲解算任务从缓冲取。改造点裸机方案RTOS 方案采样触发定时器中断置标志位独立任务 vTaskDelayUntil数据传递环形缓冲 读写指针消息队列解算位置主循环轮询标志位独立解算任务优先级低于采样采样率100Hz~200Hz同左按解算需求定采样率不是越高越好BMI160 陀螺仪最高 3200Hz但姿态解算用 100Hz~200Hz 足够太高反而增加总线负载和功耗。我一般把采样率设成解算频率的整数倍方便做滑动平均。5.3 验证方法三步确认驱动真的通了改完驱动别急着上整机按三步验证第一步读CHIP_ID等于0xD1确认通信链路第二步静止放置读 100 组数据三轴加速度计应该只有重力轴约1g对应8192LSB另外两轴接近 0方差小于几个 LSB第三步绕单轴匀速转动对应陀螺仪轴输出稳定非零另外两轴接近 0。三步都过驱动才算可信。注意第二步的方差检查很关键如果静止时数据抖动超过几十个 LSB多半是电源噪声或 I2C 速率过高导致采样不稳先降速到 100kHz 再试。5.4 一个具体技巧把寄存器配置做成表驱动二次开发时经常要试不同量程和输出速率组合硬编码在初始化函数里改起来烦。我的习惯是把配置项做成一张表初始化时遍历写入改配置只改表。typedef struct { uint8_t reg; uint8_t val; } reg_cfg_t; static const reg_cfg_t init_table[] { { BMI160_REG_ACCEL_CONF, 0x28 }, /* ±4g, 100Hz */ { BMI160_REG_GYRO_CONF, 0x28 }, /* ±500dps, 100Hz */ { BMI160_REG_INT_EN, 0x10 }, /* 使能数据就绪中断 */ { BMI160_REG_INT_MAP, 0x01 }, /* 映射到 INT1 */ }; for (size_t i 0; i sizeof(init_table)/sizeof(init_table[0]); i) bus-write_reg(bus-dev_addr, init_table[i].reg, init_table[i].val);表驱动的好处是配置和流程分离加一个新寄存器只加一行不用动初始化逻辑坏处是表里的值没有语义得靠注释说明。我一般会在表旁边留一列注释写清楚每个值的含义半年后回来看不至于抓瞎。从那以后我每次写传感器驱动都强制先跑「读 ID → 静止方差 → 单轴转动」这三步验证再往下写业务逻辑省得后面数据不对时在解算算法里瞎找原因。这套 BMI160 驱动的分层结构和寄存器表换个传感器型号也基本能照搬希望帮到你。本文还有配套的精品资源点击获取