简介基于STM32F103的BMI160惯性测量单元移植示例面向嵌入式开发者以及需要快速集成加速度计与陀螺仪的工程师。工程使用SPI接口读取加速度计和陀螺仪数据并借助FIFO缓冲与INT1中断通知机制降低主控轮询负担、提升采样效率两篇配套博客可辅助理解移植关键流程和调试方法。资源包总共包含八十一个文件其中三十七个C源文件与三十四个头文件构成核心代码另有Keil工程配置、启动汇编、日志记录、数据记录、说明文档及清理脚本等辅助内容整体仅三百零九KB目录层级清晰便于按模块查阅。目前已有1909人学习下载。包内包含标准外设库与CMSIS文件并按用户代码、库文件、工程文件分区读者可跟随代码梳理SPI读写时序、FIFO阈值设置、INT1中断服务等环节掌握传感器驱动移植与低功耗数据采集的实现方法是一份实用的嵌入式开发参考。1. stm32f103-spi-bmi160-demo一个寄存器级六轴读取例程先读 ID 再谈算法拿到 stm32f103-spi-bmi160-demo-20201126.rar 这个压缩包先别急着解压就跑。这类 demo 的真正价值不在界面或工程结构而在于把「STM32F103 用 SPI 从 BMI160 上把加速度和陀螺仪数据读出来」这条最小链路焊死引脚怎么接、SPI 参数设成什么、寄存器按什么顺序写、读回来的 16 位怎么拼。2020 年的例程用今天的主线工具链一样能编译因为寄存器访问方式不随库版本变。它适合三类人自己画板子想绕开 I2C 地址冲突的从 MPU6050 迁过来对 BMI160 SPI 时序不熟的以及拿回 demo 数据不对、想确认是硬件还是配置问题的。我一般先把这套跑通再做姿态融合顺序反了最容易卡在最基础的地方。2. 先读 CHIP_IDSPI 通没通的试金石SPI 有四根线SCK、MOSI、MISO、CS任何一根接错都读不到数据。与其把初始化代码一口气写完再排查不如先做一个最小动作读 BMI160 的 CHIP_ID寄存器 0x00。这颗传感器的 ID 固定是 0xD1只要读回来是这个值就说明电源、地线、四根 SPI 线、片选逻辑和 SPI 外设配置全部对了后续所有寄存器操作可以放心往下写。2.1 引脚接线和 SPI 外设参数先用最常见的 SPI1 引脚举例PA5 接 SCKPA6 接 MISO传感器 SDOPA7 接 MOSI传感器 SDIPA4 做软件片选接 CS。BMI160 是 3.3V 器件不能直接接到 5V 的 IO 上F103 的 PA 口是 5V 容忍但传感器侧必须保证引脚电平不超过 VDDIO。供电上用 3.3V 给 VDD 和 VDDIO旁边放一个 100nF 去耦电容电源纹波大的场合再加一个 1uF 钽电容。信号F103 引脚方向传感器引脚SCKPA5输出SCKMISOPA6输入SDOMOSIPA7输出SDICSPA4输出CSHAL 库的 SPI 初始化里几个参数要刻意固定主模式、8 位数据、MSB First、软件 NSS时钟极性 CPOL0、相位 CPHA0也就是 SPI Mode 0。这是这颗传感器最常用的时序。分频系数先给大一点比如 64 分频让 SCK 落在 1MHz 上下。很多翻车案例是上来就用 9 分频跑到 8MHz结果读到全 0xFF还以为是芯片坏了。hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_64; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB;这段配置里最容易忽略的是 NSS_SOFT。如果使用硬件 NSSCS 由 SPI 外设自动控制而 BMI160 的片选时序要求在整次读事务期间保持低电平硬件 NSS 在单字节传输时会把 CS 拉高导致地址递增读取失效。用软件 NSS 后CS 高低完全由 GPIO 控制行为可预期。2.2 读 ID 的最小函数BMI160 的 SPI 读操作有一个容易踩坑的细节读写标志位在字节最高位。写寄存器时最高位为 0读寄存器时要把最高位置 1所以读地址 0x00 实际发送 0x80。另外SPI 是全双工协议发送地址字节的那个时钟周期里 MISO 返回的数据是无效的要再补一个字节的时钟才能真正拿到寄存器内容。#define CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) uint8_t bmi160_read_reg(uint8_t reg) { uint8_t tx[2] {0, 0}; uint8_t rx[2] {0, 0}; tx[0] reg | 0x80; /* 读标志位放最高位 */ tx[1] 0x00; /* 第二个字节用来产生读时钟 */ CS_LOW(); HAL_SPI_TransmitReceive(hspi1, tx, rx, 2, 10); CS_HIGH(); return rx[1]; /* rx[0] 是地址阶段返回的无效数据 */ }写操作不需要关注返回值但同样要注意标志位。写地址直接用寄存器原值最高位保持 0数据字节紧跟其后一次事务发两个字节。这里我习惯把 CS 的拉低和拉高放在同一个函数里避免在别处忘了恢复片选导致总线挂死。void bmi160_write_reg(uint8_t reg, uint8_t val) { uint8_t tx[2] {0, 0}; tx[0] reg 0x7F; /* 写标志位为 0 */ tx[1] val; CS_LOW(); HAL_SPI_Transmit(hspi1, tx, 2, 10); CS_HIGH(); }读 ID 的动作就是把第一个函数调用一次参数传 0x00。如果返回值是 0xD1通信链路确认打通如果不是不要继续往下初始化越往后排查越难定位。这个最小函数是整套 demo 的地基后面的所有寄存器读写都建立在它上面。2.3 读不到 ID 时先自查三个地方经验里读 ID 失败大概率是三类原因。第一SCK 极性相位不对BMI160 对 Mode 0 是标准支持但个别例程抄了别的传感器的 Mode 3表现是 ID 偶尔对、大部分时间 0xFF。第二MISO 和 MOSI 接反这时 SPI 外设不会报错因为发送和接收都有自己的引脚接反后读回来全是 0。第三CS 引脚没有在事务开始前拉低或者拉低后忘了等 SCK 稳定。前两个问题用万用表量不通最好拿逻辑分析仪抓一下 CS 和 SCK 的时序关系。3. 把初始化写成可复现的寄存器序列PMU、量程与 ODRID 读出来了下一步是让传感器进入正常测量模式。BMI160 上电后并不能立即输出有效数据加速度计和陀螺仪需要分别通过电源管理单元PMU切换到正常模式。初始化顺序有一个约定俗成的规矩先写量程和输出速率配置再下发电源模式命令最后延时等待数据稳定。这个顺序能减少第一次读出异常值的概率。3.1 为什么要先写参数再下电源命令BMI160 的命令寄存器地址是 0x7E写入 0x11 表示把加速度计切到正常模式写入 0x15 表示把陀螺仪切到正常模式。如果先唤醒传感器再去改 0x40、0x41 这些量程配置部分批次的芯片会在切换过程中把新配置丢失或者要等到下一次模式切换才生效。我在实际调试中遇到过好几回先唤醒后改参、结果加速度计一直按 ±2g 输出的情况改成先写配置再唤醒就稳定了。还有一点陀螺仪和加速度计是两个独立的 PMU 单元命令要分别下发不能指望一条命令同时唤醒两个。顺序一般先加速度计后陀螺仪中间不需要额外延时但全部命令发完后要等一段时间常见做法是 50ms让内部时钟稳定。3.2 寄存器初始化序列完整初始化函数可以照下面这个结构写。配置寄存器分别是加速度计配置 0x40、加速度计量程 0x41、陀螺仪配置 0x42、陀螺仪量程 0x43。ODR 值 0x08 对应 200Hz这是姿态解算里比较常用的折中频率静止实验和动态实验都能覆盖。void bmi160_init(void) { uint8_t id bmi160_read_reg(0x00); if (id ! 0xD1) { /* 通信失败点亮错误灯或进入错误处理 */ return; } /* 加速度计200Hz 输出±2g 量程 */ bmi160_write_reg(0x40, 0x08); bmi160_write_reg(0x41, 0x03); /* 陀螺仪200Hz 输出±2000dps 量程 */ bmi160_write_reg(0x42, 0x08); bmi160_write_reg(0x43, 0x04); /* 先配置后唤醒加速度计正常模式 */ bmi160_write_reg(0x7E, 0x11); /* 陀螺仪正常模式 */ bmi160_write_reg(0x7E, 0x15); HAL_Delay(50); }0x40 的低 4 位是 ODR0x08 对应 200Hz高 4 位是带宽/滤波模式保持默认不写。0x41 低 4 位是量程选择0x03 对应 ±2g这个量程下 LSB 灵敏度最高适合静态倾角测量。陀螺仪 0x43 的 0x04 对应 ±2000dps是数据手册里最宽的量程适合剧烈转动如果只做缓慢的姿态变化可以改 0x03 对应 ±1000dps。3.3 量程和 ODR 的选择量程不是越大越好也不是越小越好核心权衡是分辨率。以加速度计为例±2g 时 1g 对应 16384 LSB±16g 时 1g 只剩 2048 LSB同样采集到的整数 1乘以灵敏度后误差会放大 8 倍。做倾角测量、平面校准这种静止或慢速场景±2g 是首选。做跌落检测、剧烈运动识别±16g 能避免传感器削顶失真。寄存器写入值量程灵敏度0x410x03±2g16384 LSB/g0x410x05±4g8192 LSB/g0x410x08±8g4096 LSB/g0x410x0C±16g2048 LSB/g0x430x00±125dps262.4 LSB/dps0x430x01±250dps131.2 LSB/dps0x430x02±500dps65.6 LSB/dps0x430x03±1000dps32.8 LSB/dps0x430x04±2000dps16.4 LSB/dpsODR 同样要匹配应用。200Hz 对大多数手持设备和机器人姿态解算足够滤波后延时约 5ms。如果主控还要跑通信协议栈可以降到 100Hz写 0x07。如果要捕捉快速翻转且 CPU 有余量选 400Hz 以上但要注意 SPI 读取频率必须跟上 ODR否则 FIFO 里堆积的数据会越来越多。3.4 上电后的等待时间怎么给初始化最后那个 HAL_Delay(50) 不是随便写的。BMI160 从命令下达到数据稳定需要经过内部时钟启动、自检、输出稳定几个阶段常见传感器手册给的是毫秒级但实际工程中 20ms 经常不够。我一般用 50ms 起步如果发现前几帧数据有明显跳变就加到 100ms。另一个更可靠的验证方式是连续读三次加速度计数据等连续两次差值小于某个阈值再开始真正使用。4. 连续读加速度和陀螺仪字节拼接、量程换算与数据平滑初始化完成之后核心工作就变成周期性读取。BMI160 的加速度计 X 轴低字节在 0x0A高字节在 0x0BY 和 Z 依次排列占满 0x0A 到 0x0F陀螺仪从 0x10 开始同样是三轴各占两个字节读到 0x15。SPI 接口支持地址自动递增只要 CS 在整个读事务期间保持低电平发一个起始地址就能连续读回 12 字节。4.1 一次事务读完 12 字节的接口读取函数的核心是保证 CS 不中途拉高。HAL 库里如果分开调用 Transmit 和 Receive中间片选由 GPIO 控制只要不在这两个调用之间做别的操作就没问题。先发送读地址 0x0A | 0x80然后连续接收 12 字节最后拉高 CS。注意这里读的是加速度计起始地址但陀螺仪数据紧跟在加速度计后面所以一次事务就能把两个传感器都读完。void bmi160_read_acc_gyr(int16_t *acc, int16_t *gyr) { uint8_t reg 0x0A | 0x80; /* 起始地址读标志位置 1 */ uint8_t buf[12] {0}; CS_LOW(); HAL_SPI_Transmit(hspi1, reg, 1, 10); HAL_SPI_Receive(hspi1, buf, 12, 10); CS_HIGH(); /* 每个轴都是低字节在前高字节在后 */ 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)); gyr[0] (int16_t)((uint16_t)buf[6] | ((uint16_t)buf[7] 8)); gyr[1] (int16_t)((uint16_t)buf[8] | ((uint16_t)buf[9] 8)); gyr[2] (int16_t)((uint16_t)buf[10] | ((uint16_t)buf[11] 8)); }buf[0] 到 buf[5] 是加速度计三轴buf[6] 到 buf[11] 是陀螺仪三轴顺序不能搞混。我用 int16_t 接收原始值是因为 BMI160 输出的是二进制补码最高位是符号位。如果定义成 uint16_t 再直接做除法负角度会被算成一个大正数后续滤波和姿态解算全乱。4.2 把 LSB 换成 g 和 dps拿到原始整数后换算公式就是拿原始值除以灵敏度。加速度计 ±2g 时除 16384陀螺仪 ±2000dps 时除 16.4。这里有个常见错误有人会把陀螺仪灵敏度记成 16.4但在 ±250dps 下也除 16.4结果数值偏大 8 倍。灵敏度必须和量程寄存器里的值一一对应最好在代码里写宏避免每个文件里各写一个数。#define ACC_LSB_PER_G 16384.0f /* 对应 ±2g */ #define GYR_LSB_PER_DPS 16.4f /* 对应 ±2000dps */ float acc_g[3]; float gyr_dps[3]; for (int i 0; i 3; i) { acc_g[i] (float)acc[i] / ACC_LSB_PER_G; gyr_dps[i] (float)gyr[i] / GYR_LSB_PER_DPS; }水平静止放置时Z 轴加速度应该在 16384 LSB 附近换算成 g 就是 1.0 左右X 和 Y 轴接近 0。陀螺仪在完全静止时理论上是 0实际会有几十 LSB 的零漂这是正常现象。如果换算出来的重力明显偏离 1.0 超过 5%先检查量程配置是不是被改过了。4.3 一阶低通滤波怎么加BMI160 原始数据带一定的高频噪声直接用于倾角计算时静止状态下角度会有 ±0.5 度左右的抖动。常见做法是先做一阶低通滤波用当前值的一部分加上一次滤波值的一部分系数之和为 1。系数取 0.2 表示新数据占 20%滤波效果明显但响应变慢取 0.5 则更跟手噪声压制弱一点。float ax_filtered 0.0f, ay_filtered 0.0f, az_filtered 0.0f; void lowpass_update(float ax, float ay, float az) { ax_filtered 0.8f * ax_filtered 0.2f * ax; ay_filtered 0.8f * ay_filtered 0.2f * ay; az_filtered 0.8f * az_filtered 0.2f * az; }滤波系数、ODR 和实际运动带宽要一起考虑。ODR 200Hz 配合 0.2 系数截止频率大概在十几赫兹能保留普通手势动作的频段。如果做机械振动检测这个截止频率会把高频特征吃掉那时应该把 ODR 提高滤波系数调到 0.5 以上。滤波不是越狠越好一定要先确认目标场景的最低有效频带。5. 避坑记录SPI 读 BMI160 最容易翻车的 5 个细节这章把调试中总结的教训按「现象、原因、解决」列清楚。每一条都对应一次真实排查过程很多问题表面是软件 bug实际是信号完整性或寄存器时序问题。5.1 ID 读出 0xFF但接线量起来都是通的现象用万用表量 SCK、MOSI、MISO、CS 都连着正确的引脚但 bmi160_read_reg(0x00) 返回 0xFF。原因大概率是 SPI 时钟跑太快杜邦线又长信号反射导致 SCK 沿抖动另一个常见原因是 SCK 空闲电平不对。解决把分频系数改到 64确认 CPOL0、CPHA0然后把杜邦线剪短到 10cm 以内或者直接焊转接板。绝大多数读 ID 失败都能在这一步解决。5.2 ID 能读到但加速度和陀螺仪全是 0现象CHIP_ID 返回 0xD1但读 0x0A 到 0x15 全是 0x00。原因是 PMU 命令没有真正生效或者命令下发后被后续写操作覆盖。0x7E 是命令寄存器写入是瞬时动作传感器切模式需要时间如果马上又写别的寄存器命令可能被中断。解决初始化函数最后加 50ms 延时期间不写任何寄存器然后读一次 0x03 的状态寄存器确认模式切换完成。5.3 静止时 Z 轴读数不是 1g而是差了几百 LSB现象板子水平放置Z 轴加速度换算后只有 0.95g或者 X、Y 轴有一个不为 0。最常见原因是加速度计没有做零偏补偿。BMI160 出厂有校准但焊接应力会让零点偏移几十到几百 LSB。解决把板子平放在桌面上静止采集 100 帧求平均值把这个平均值作为零偏存下来之后每次读取都先减掉它。这不是传感器坏是机械应力导致的正常偏移。5.4 陀螺仪静止时输出几百 dps而且数值乱跳现象静止状态下陀螺仪 Z 轴输出 300dps稍微动一下变成负 400dps。原因往往是传感器没有真正进入正常模式还在内部自检状态或者电源电压不稳。解决确认 VDD 和 VDDIO 都稳定在 3.3V初始化后延时拉长到 100ms如果仍然跳把 SPI 频率降到 1MHz 以下再看。另一个容易忽略的点是陀螺仪和加速度计要分别下 PMU 命令只写了 0x11 没写 0x15陀螺仪就停留在断电状态读数无意义。5.5 SPI 从 1MHz 提速到 8MHz 后数据偶尔错一帧现象低速跑一切正常把分频系数从 64 改成 8 之后每几十帧出现一次明显的野值用示波器看波形也不是理想方波。原因是 F103 的 SPI 输出边沿和 BMI160 的采样窗口在高速下不匹配杜邦线引入的电容让信号边沿变缓。解决先查 BMI160 手册里 SPI 最高时钟的约束通常不建议超过 10MHz然后缩短走线、降低 MOSI 输出驱动强度或者在 SCK 上并一个 22pF 电容改善振铃。如果走线已经固定最省事的做法是留在 4MHz 以下。6. 用三组定性实验验证数据真的对了数据读出来不等于数据可靠我会在接算法之前跑三个快速实验。第一组是静态称重板子水平放置Z 轴加速度应在 1g 附近X、Y 接近 0板子竖起来对应轴应该变成 1g。第二组是动态转动沿 Z 轴匀速转 90 度用陀螺仪 Z 轴积分角度应接近 90 度误差超过 15 度说明零偏没标定好。第三组是短时重复性静止放 1 分钟记录加速度最大值和最小值波动超过 1% 就检查电源和滤波。int16_t acc[3], gyr[3]; int32_t gyr_bias[3] {0, 0, 0}; uint16_t i; /* 静止采集 100 帧求陀螺仪零偏 */ for (i 0; i 100; i) { bmi160_read_acc_gyr(acc, gyr); gyr_bias[0] gyr[0]; gyr_bias[1] gyr[1]; gyr_bias[2] gyr[2]; HAL_Delay(2); } gyr_bias[0] / 100; gyr_bias[1] / 100; gyr_bias[2] / 100;这个零偏值要在每次上电后重新采集因为温漂会改变它。采集时板子必须绝对静止手扶着都会有影响。之后每次使用陀螺仪都减掉对应的零偏再除以 16.4 换算成 dps积分得到的角度明显稳定。每次换板子我第一件事仍然是跑第 2 章的读 ID线不通就不往下谈融合。希望帮到你。本文还有配套的精品资源点击获取