简介基于STM32F103平台的BMI160六轴惯性传感器SPI驱动示例工程以标准外设库为基础面向需要快速移植IMU驱动的嵌入式开发者。工程演示通过SPI接口读取加速度计与陀螺仪原始数据利用FIFO和INT1中断实现批量数据获取相比轮询方式更节省主控资源、提升实时性。压缩包共81个文件以37个h头文件和34个c源文件为主覆盖BMI160寄存器配置、SPI读写、FIFO解析与中断处理同时附有Keil工程文件、README说明、调试日志、串口相关代码和keilkill清理脚本整体仅309KB目录按工程、文档、库文件划分结构直观便于快速定位。例程中留有调试日志输出与数据记录文件方便对比验证读取结果适合在STM32F103上开发运动检测、姿态解算或可穿戴设备的工程师直接参考。已有1906人学习下载具备一定STM32基础、希望掌握传感器高效读取方式的开发者可重点关注。 前几天整理移动硬盘的时候翻出来一个旧工程包stm32f103-spi-bmi160-demo-20201126。看名字就知道这是一份用STM32F103通过SPI接口驱动BMI160六轴传感器的demo程序打包日期是2020年11月26日。当时是为了给一个手持设备做姿态采集验证核心就是把BMI160的加速度和角速度数据通过SPI总线读出来再换算成物理量。东西不大但把SPI通信、传感器寄存器配置、数据换算这一整条链路都走通了对刚接触嵌入式传感器开发的人是个很典型的入门案例。这个demo的参考价值在于不依赖复杂的RTOS或HAL库封装直接用STM32F103标准外设库v3.5那套操作寄存器级别的SPI外设代码结构一眼能看到底。如果你正准备在stm32f103最小系统板上接BMI160、MPU6050这类SPI接口传感器或者想弄明白SPI时序图上的那几条线到底该怎么配这篇文章应该能帮你少走不少弯路。1. 项目概述一个SPI接口的六轴传感器demo1.1 核心需求与代码结构BMI160是博世出品的一款6轴惯性传感器内部集成了三轴加速度计和三轴陀螺仪体积很小功耗也不高常见于无人机、可穿戴设备、手势识别这类场景。这个demo的需求很朴素上电后读取BMI160的加速度和角速度原始值通过串口打印出来方便在PC端观察数据变化。代码结构按标准库的习惯分了几块SPI1接口初始化、BMI160驱动层寄存器读写、初始化配置、数据读取与换算、主循环轮询打印。整个工程没有用到外部中断也没有DMA就是最简单的“主循环里轮询读取”模式。这样做的好处是逻辑直观出了问题很快能定位到具体是哪一行代码导致的。1.2 为什么选SPI而不是IICBMI160本身同时支持IIC和SPI两种接口这也是绝大多数MEMS传感器芯片的标配。上手时很多朋友会纠结到底用哪个我的建议是如果硬件上没有引脚限制优先走SPI。原因有三个。第一个是速率SPI是主从同步通信没有IIC那套应答机制时钟可以拉得更高。BMI160的SPI接口理论上支持到10MHz而IIC通常只能到400kHz快速模式对于需要高频读取姿态数据的场景SPI的带宽优势非常明显。第二个是设备地址问题。IIC每个设备需要唯一地址同一条总线上挂多个相同型号的传感器时就得改地址脚或者加MUX比较麻烦。SPI靠片选信号CS区分设备一根CS管脚对应一个设备多挂一个传感器就是多一根CS线逻辑上清晰得多。第三个是调试效率。用逻辑分析仪抓SPI波形时CS拉低之后SCK、MOSI、MISO四条线的状态一目了然远比IIC那两根线上挤着的地址帧、数据帧好理解。实际开发中SPI出问题往往半小时就能定位IIC出问题经常要对着时序图看半天。2. 硬件接线与SPI时序连接是第一道关卡2.1 STM32侧最小系统与IO分配这个demo的开发环境用的是stm32f103最小系统板没有自己画完整PCB直接买现成的核心板引出PA4到PA7这几个引脚就能跑。BMI160模块买的是现成的贴片转接板板上带了3.3V稳压和电平转换可以直接和STM32的3.3V电平对接不用另外加电平匹配电路。接线方式如下PA5接SCKPA6接MISOPA7接MOSIPA4接CS另外把3.3V和GND接好。这里有个细节需要留意BMI160的CS引脚在SPI模式下必须接高或由MCU控制如果悬空芯片可能误判进入IIC模式。很多demo跑不起来就是CS引脚状态没处理好的原因。还有一个容易被忽略的问题是SCK引脚的复用配置。STM32F103的SPI1在PA5、PA6、PA7但PA5、PA7需要配成复用推挽输出GPIO_Mode_AF_PPPA6是输入浮空GPIO_Mode_IN_FLOATING。我第一次写的时候把三个引脚全配成了通用推挽结果MISO数据读不到查了半天才发现是引脚模式不对。2.2 SPI模式、极性和相位的选择SPI通信有四种模式区别在于CPOL时钟极性和CPHA时钟相位的组合。CPOL决定SCK空闲时是高电平还是低电平CPHA决定数据在SCK上升沿还是下降沿采样。BMI160的数据手册里明确要求使用模式0CPOL0CPHA0。也就是说SCK空闲为低电平在上升沿采样数据。代码里的配置如下SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_16; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStructure);这里把SPI_NSS设为软件管理SPI_NSS_Soft意味着不使用硬件片选全部由GPIO手动控制。SPI_FirstBit_MSB表示MSB先行这是绝大多数SPI传感器共用的规则。如果你在调其他厂家的传感器不要想当然地套用模式0一点要先查数据手册里的SPI时序图。比如某些Flash芯片同时支持模式0和模式3那倒无所谓但有些器件只支持其中一种配错了读回来的数据就是乱的。判断方法也简单用逻辑分析仪抓CS拉低后的第一个字节看数据是否稳定即可。2.3 硬件片选与软件片选怎么选STM32的SPI外设带有硬件NSS引脚可以自动控制片选信号但实际项目里我基本不用一律改成GPIO软件模拟片选。原因是硬件NSS在标准库里的行为偏复杂单主模式下NSS脚的电平状态容易受SPI配置顺序影响调试时反而不直观。软件片选的核心逻辑其实是这样的要和一个设备通信时先把对应的CS引脚拉低等SPI传输完毕后拉回高电平。以这个demo为例#define BMI160_CS_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_4) #define BMI160_CS_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_4)多设备共享同一条SPI总线时软件片选的优势更明显。只要保证同一时间只有一根CS线是低电平其他设备就算是都不认识你的操作行为也不会有冲突。如果你手上有个ESP32之类的平台需要把屏幕和SD卡共享同一个SPI网上讨论“共享SPI哪个好”这类问题本质就是在讨论片选和时间片的调度理解了软件片选这些问题自然就通了。3. 驱动代码落地的关键流程3.1 初始化三步复位、校验、配置BMI160驱动部分我习惯按“软复位、读ID、写配置”三步走。软复位的操作是向命令寄存器0x7E写入0xB6芯片收到后会自动复位所有寄存器到默认值。复位之后要延时一段时间一般50到100毫秒等芯片内部稳定。接着读取芯片ID寄存器0x00BMI160的ID固定是0xD1。如果读不到0xD1先别急着查代码逻辑直接检查硬件连接和SPI初始化。我在调这个demo时ID能读对基本就等于通信链路通了后面配置寄存器才不会瞎折腾。配置部分主要是三个寄存器加速度计配置寄存器0x41、加速度计量程寄存器0x42、陀螺仪配置寄存器0x43、陀螺仪量程寄存器0x44。这个demo里我用的配置是加速度计100Hz输出、±2g量程陀螺仪100Hz输出、±2000dps量程BMI160_WriteReg(0x41, 0x28); // ACC_CONF100Hz输出普通滤波 BMI160_WriteReg(0x42, 0x00); // ACC_RANGE±2g BMI160_WriteReg(0x43, 0x28); // GYR_CONF100Hz输出 BMI160_WriteReg(0x44, 0x00); // GYR_RANGE±2000dps这里0x28和0x00的具体含义不同手册版本写法略有差异建议对照数据手册的寄存器表逐一核对。更重要的是别忘了把芯片从挂起模式切到正常工作模式。BMI160上电默认可能处于suspend状态需要往命令寄存器0x7E写0x11让加速度计进入正常模式写0x15让陀螺仪进入正常模式。如果漏了这一步后面读出来的数据全是零。3.2 读数据的正确姿势字节序与burst读BMI160的加速度数据寄存器从0x0A开始连续6个字节分别对应X、Y、Z轴的低字节和高字节陀螺仪从0x10开始同样是6个字节。这里最需要注意的就是字节序低字节在前高字节在后组合成int16_t时千万别写反。读单个寄存器的方法很直接先拉低CS发送寄存器地址再发送一个空字节0x00接收数据最后拉高CS。写操作则在地址之后紧跟要写的值。SPI是双工的主机发一个字节的同时必然收到一个字节所以读寄存器时MOSI线发的那个字节是无效的只有MISO上返回的数据才是有效值。如果按单字节方式把X、Y、Z都读了每组数据之间CS都要重新拉低拉高效率不高。BMI160支持连续读模式也就是burst read。初始化时直接把CS拉低一次从起始寄存器地址开始连续读6个字节最后再释放CSuint8_t buf[6]; BMI160_CS_LOW(); SPI1_ReadWriteByte(0x0A | 0x80); // 读标记地址0x0A for (uint8_t i 0; i 6; i) { buf[i] SPI1_ReadWriteByte(0x00); } BMI160_CS_HIGH(); int16_t acc_x (int16_t)((buf[1] 8) | buf[0]); int16_t acc_y (int16_t)((buf[3] 8) | buf[2]); int16_t acc_z (int16_t)((buf[5] 8) | buf[4]);这里注意一个细节SPI读操作时地址字节要加上读标记。BMI160的约定是寄存器地址最高位置1表示读操作置0表示写操作。我在读0x0A时用的是0x0A | 0x80写寄存器时就直接发地址本身。这类约定在不同的传感器上未必一样但绝大多数SPI传感器都是类似套路多看手册就能掌握。3.3 把原始值换算成物理量从寄存器读出来的原始值是没有量纲的int16数字必须根据量程换算成有意义的加速度g和角速度dps。换算的原理很简单量程对应的满量程值映射到16位有符号整数的最大值32767。以加速度计±2g量程为例满量程范围是-2g到2g总跨度4g对应的数字量范围是-32768到32767。所以灵敏度就是4g/65536约等于0.061mg/LSB。工程上为了计算方便经常直接用16384这个数因为2g除以32768等于16384分之1g每LSB即16384 LSB代表1g。换算公式为float acc_x_g (float)acc_x / 16384.0f;陀螺仪±2000dps量程的换算同理2000除以32768约等于0.061dps/LSB也就是16.384 LSB代表1dpsfloat gyr_x_dps (float)gyr_x / 16.384f;有朋友问为什么有的代码里写32768有的写32767其实差不了多少对一般姿态显示场景影响不到。真正需要注意的是符号扩展问题int16_t类型转换时不要先转成uint16_t再转float否则负数会变成很大的正数数据曲线看起来就像被腰斩了一样。4. 实测遇到的问题与排查记录4.1 读CHIP_ID卡在0xFF这个现象出现时基本可以判定MCU根本没有和BMI160建立有效通信。可能性有三个CS没拉低、SPI模式配错、MISO线接错。先用示波器或逻辑分析仪抓CS拉低的瞬间确认CS电平确实拉低了。然后检查引脚复用配置PA6作为MISO必须配成输入模式很多初学者在这里配成推挽输出导致数据线冲突。还有一种情况是接线没问题但BIMM160模块的CS引脚被板上的上拉电阻拉高了而你没有用单片机的GPIO去控制它。SPI模式下CS由外部MCU控制但如果CS状态不确定芯片可能自动进入IIC模式SPI自然就失效了。处理方式是把CS引脚明确接到GPIO口再在软件里初始化成输出高电平。4.2 加速度读出来全是0这个坑我在调的时候踩过当时芯片ID读得正正常常但加速度数据死活全是0。后面排查发现是芯片没有从挂起模式切到正常模式。BMI160上电后默认处于suspend状态传感器虽然能响应SPI读写但不会真正采集数据。解决方式就是前面提到的向命令寄存器0x7E写0x11和0x15把加速度计和陀螺仪都切到正常模式。切换后还要延时20毫秒左右等内部电源稳定。如果写了命令依然读不到数据还有一个冷门原因命令寄存器的写入有时序要求两条命令之间需要留出间隔写得太快可能会被芯片忽略。4.3 陀螺仪零漂大数值乱跳陀螺仪读出来有零漂是正常的物理器件的特性决定了它静止时也会有微小的角速度输出。但如果数值乱跳得离谱首先怀疑电源纹波。BMI160这类MEMS传感器对电源比较敏感转接板上的稳压芯片输出如果不干净直接影响传感器内部ADC的参考电压。解决办法是在电源引脚旁边多放一个0.1uF的陶瓷电容再并联一个10uF的钽电容。另一个问题是采样率配得太高。100Hz的ODR配合普通滤波数据还比较平滑如果配到400Hz甚至更高噪声会显著上升。对于静态姿态测量适当降低采样率或者在后端加滑动平均滤波效果立竿见影。4.4 SPI时钟速率该怎么选STM32F103的SPI时钟源是APB2的72MHz通过分频器产生。这个demo里用的分频系数是16SPI时钟为4.5MHz处于BMI160支持的范围内。如果你用的分频系数是4时钟高达18MHz已经超过BMI160的10MHz极限就会出现通信不稳定、数据偶发错误的情况。调试时建议先从低速开始比如分频128或256确认通信正常后再逐步提高速率。很多时候传感器读数的偶发异常不是代码逻辑问题而是SPI时钟太快导致时序裕量不足。我的习惯是验证阶段用1MHz左右稳定工作之后再把速率提到4.5MHz或9MHz。下面整理了一份这个demo调试过程中常见的现象和排查方向方便大家对照现象可能原因排查方向读ID永远是0xFFCS没拉低、SPI模式错、接线错检查CS引脚、检查PA6输入模式读ID永远是0x00MISO短路到地、芯片供电异常量3.3V电压、检查焊接质量加速度全是0芯片在suspend模式写0x7E命令切正常模式数据偶发跳变SPI速率过高、电源纹波大降低分频、加滤波电容陀螺仪零漂大供电不稳、ODR太高滤波、降采样率5. 写在最后两个实际经验这个demo调完之后我自己养成了一个习惯任何新的SPI传感器到手第一件事不是写驱动而是先把数据手册里的寄存器映射和SPI时序图看明白。很多人连寄存器查表都没搞清楚上来就开调最终都是在拿时间换经验。先读CHIP_ID再往下写配置能节省一半以上的排错时间。另一个体会是调试SPI设备时逻辑分析仪比示波器好用得多尤其对片选时序、burst读这类信号逻辑分析仪能直接解析出通信内容快速确认地址帧和数据帧是否按预期发送。如果手边没有逻辑分析仪也可以用延时的方式在关键位置翻转一个GPIO再用示波器看时间点对齐虽然粗糙点但同样能把问题定位出来。最后再分享一个小技巧这个demo是轮询读取数据实际项目里如果需要更高的实时性和更低的主循环占用率可以考虑把BMI160的数据就绪中断引脚INT1接到STM32的外部中断输入上传感器数据准备好后在中断服务函数里读取主循环只负责消费数据后续的扩展方向可以从这里入手。本文还有配套的精品资源点击获取
