简介:基于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接口的六轴传感器demo
1.1 核心需求与代码结构
BMI160是博世出品的一款6轴惯性传感器,内部集成了三轴加速度计和三轴陀螺仪,体积很小,功耗也不高,常见于无人机、可穿戴设备、手势识别这类场景。这个demo的需求很朴素:上电后读取BMI160的加速度和角速度原始值,通过串口打印出来,方便在PC端观察数据变化。
代码结构按标准库的习惯分了几块:SPI1接口初始化、BMI160驱动层(寄存器读写、初始化配置)、数据读取与换算、主循环轮询打印。整个工程没有用到外部中断,也没有DMA,就是最简单的“主循环里轮询读取”模式。这样做的好处是逻辑直观,出了问题很快能定位到具体是哪一行代码导致的。
1.2 为什么选SPI而不是IIC
BMI160本身同时支持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接SCK,PA6接MISO,PA7接MOSI,PA4接CS,另外把3.3V和GND接好。这里有个细节需要留意,BMI160的CS引脚在SPI模式下必须接高或由MCU控制,如果悬空,芯片可能误判进入IIC模式。很多demo跑不起来,就是CS引脚状态没处理好的原因。
还有一个容易被忽略的问题是SCK引脚的复用配置。STM32F103的SPI1在PA5、PA6、PA7,但PA5、PA7需要配成复用推挽输出(GPIO_Mode_AF_PP),PA6是输入浮空(GPIO_Mode_IN_FLOATING)。我第一次写的时候把三个引脚全配成了通用推挽,结果MISO数据读不到,查了半天才发现是引脚模式不对。
2.2 SPI模式、极性和相位的选择
SPI通信有四种模式,区别在于CPOL(时钟极性)和CPHA(时钟相位)的组合。CPOL决定SCK空闲时是高电平还是低电平,CPHA决定数据在SCK上升沿还是下降沿采样。BMI160的数据手册里明确要求使用模式0:CPOL=0,CPHA=0。也就是说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寄存器0x00,BMI160的ID固定是0xD1。如果读不到0xD1,先别急着查代码逻辑,直接检查硬件连接和SPI初始化。我在调这个demo时,ID能读对基本就等于通信链路通了,后面配置寄存器才不会瞎折腾。
配置部分主要是三个寄存器:加速度计配置寄存器0x41、加速度计量程寄存器0x42、陀螺仪配置寄存器0x43、陀螺仪量程寄存器0x44。这个demo里我用的配置是加速度计100Hz输出、±2g量程,陀螺仪100Hz输出、±2000dps量程:
BMI160_WriteReg(0x41, 0x28); // ACC_CONF,100Hz输出,普通滤波 BMI160_WriteReg(0x42, 0x00); // ACC_RANGE,±2g BMI160_WriteReg(0x43, 0x28); // GYR_CONF,100Hz输出 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个字节,最后再释放CS:
uint8_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代表1dps:
float 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里用的分频系数是16,SPI时钟为4.5MHz,处于BMI160支持的范围内。如果你用的分频系数是4,时钟高达18MHz,已经超过BMI160的10MHz极限,就会出现通信不稳定、数据偶发错误的情况。
调试时建议先从低速开始,比如分频128或256,确认通信正常后再逐步提高速率。很多时候传感器读数的偶发异常,不是代码逻辑问题,而是SPI时钟太快导致时序裕量不足。我的习惯是验证阶段用1MHz左右,稳定工作之后再把速率提到4.5MHz或9MHz。
下面整理了一份这个demo调试过程中常见的现象和排查方向,方便大家对照:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 读ID永远是0xFF | CS没拉低、SPI模式错、接线错 | 检查CS引脚、检查PA6输入模式 |
| 读ID永远是0x00 | MISO短路到地、芯片供电异常 | 量3.3V电压、检查焊接质量 |
| 加速度全是0 | 芯片在suspend模式 | 写0x7E命令切正常模式 |
| 数据偶发跳变 | SPI速率过高、电源纹波大 | 降低分频、加滤波电容 |
| 陀螺仪零漂大 | 供电不稳、ODR太高 | 滤波、降采样率 |
5. 写在最后:两个实际经验
这个demo调完之后,我自己养成了一个习惯:任何新的SPI传感器到手,第一件事不是写驱动,而是先把数据手册里的寄存器映射和SPI时序图看明白。很多人连寄存器查表都没搞清楚,上来就开调,最终都是在拿时间换经验。先读CHIP_ID再往下写配置,能节省一半以上的排错时间。
另一个体会是,调试SPI设备时逻辑分析仪比示波器好用得多,尤其对片选时序、burst读这类信号,逻辑分析仪能直接解析出通信内容,快速确认地址帧和数据帧是否按预期发送。如果手边没有逻辑分析仪,也可以用延时的方式在关键位置翻转一个GPIO,再用示波器看时间点对齐,虽然粗糙点,但同样能把问题定位出来。
最后再分享一个小技巧:这个demo是轮询读取数据,实际项目里如果需要更高的实时性和更低的主循环占用率,可以考虑把BMI160的数据就绪中断引脚(INT1)接到STM32的外部中断输入上,传感器数据准备好后在中断服务函数里读取,主循环只负责消费数据,后续的扩展方向可以从这里入手。
本文还有配套的精品资源,点击获取