☰
STM32F103通过SPI驱动BMI160六轴传感器:时序、初始化与数据读取详解
2026/10/14 18:03:33 网站建设 项目流程

简介:这是一份基于STM32F103平台移植BMI160六轴惯性传感器的完整工程例程,适用于嵌入式开发者学习SPI通信与IMU数据采集。项目使用SPI接口读取加速度计和陀螺仪数据,并启用FIFO与INT1中断机制,在降低主控资源占用同时保证采样效率,适合希望快速上手BMI160驱动的中初级开发者参考。压缩包共81个文件,整体仅309KB,以37个.h头文件和34个.c源文件为核心,涵盖BMI160驱动、SPI配置、系统时钟及USART等模块;此外还包含Keil工程文件(uvprojx/uvoptx)、CMSIS标准库、两个调试日志、一个数据记录文件及README说明,便于对照代码和日志理解时序与异常处理。已有1909人浏览学习。通过该工程可掌握基于寄存器操作的传感器驱动写法、FIFO批次读取与中断引脚联动策略,并可直接作为STM32F103项目的BMI160应用基础,节省前期调驱动的时间。

1. 拿着这份 STM32F103 的 SPI + BMI160 工程,我先把话放在前边

解压stm32f103-spi-bmi160-demo-20201126.rar的那一刻,我最关心的不是“代码能不能编译”,而是“SPI 时序有没有踩对”。BMI160 是一颗六轴惯性传感器,绝大多数现成例程默认走 IIC,但放在 STM32F103 上,IIC 的速率上不去,连续读 12 字节数据会被应答时序拖慢;SPI 的优势是一次 CS 拉低就能把加速度计和陀螺仪的数据全搬回来。这份 demo 的真正价值,不是那几行初始化代码,而是它把片选怎么拉、地址字节的读写位怎么拼、量程和 ODR 怎么配这几件最容易翻车的事提前做完了。它适合的是做手持姿态检测、低成本倾角报警、计步器或者动作识别,又不想从零啃数据手册的人。你先打开工程,跳着看 main 函数和 SPI 初始化,跑通一次再谈优化。

2. 拆解工程结构:先把 SPI1 初始化和传感器配置顺序对上号

2.1 工程里真正会影响你改动的文件就三个

解压之后,标准的 STM32F103 工程骨架基本长这样:SystemInit管时钟,stm32f10x外设库管寄存器映射,然后是一个放传感器驱动的bmi160.c、一个放 SPI 初始化和收发封装的bsp_spi.c,最后main.c负责把两者串起来。我习惯第一时间打开的是bmi160.h,看它的寄存器地址宏写得齐不齐。

如果驱动源码里把寄存器地址写成裸数字,比如0x41、0x7E直接嵌在函数里,那后面你调量程、换 ODR 时只能满文件搜索,非常痛苦。我的做法是先把bmi160.h里的寄存器宏表补齐,哪怕工程里已经有一份,也会对着数据手册核对一遍。因为 SPI 模式下的 BMI160 对寄存器地址字节的最高位有特殊约定,这个在头文件里不写清楚,后面读数据就会拿到一堆 0xFF。

另外一个值得留意的文件是main.c里的初始化顺序。BMI160 上电后加速度计默认在 normal 模式,陀螺仪却在 suspend 状态。很多 demo 只初始化了加速度计,陀螺仪数据读出来永远是 0。这份工程里如果初始化函数把量程配置、电源模式切换、数据就绪检查分成了几个独立函数,那基本就是按正确顺序写的。如果看到的是一个函数里连续写十几个寄存器,我建议你别急着跑,先确认它有没有做软复位和 ID 检查。

2.2 SPI1 引脚配置:SCLK 别一上来就拉满

先看最常见的引脚映射:SPI1 的 SCK 在 PA5,MISO 在 PA6,MOSI 在 PA7。片选 CS 一般用普通 GPIO,常见接 PA4 或者拉高外部上拉。我习惯用 GPIO 操作 CS,不用 SPI 硬件的 NSS,因为 NSS 引脚在 F103 上自动拉低拉高的时机跟传感器要求的片选时序经常对不上,容易造成最后一次传输丢数据。

下面是工程里最典型的一段 SPI1 初始化代码,我加上了注释:

// bsp_spi.c — 初始化 SPI1 为主机模式,速度先降下来 void SPI1_Config(void) { GPIO_InitTypeDef gpio; SPI_InitTypeDef spi; // 使能 SPI1 和 GPIOA 的时钟,SPI1 挂在 APB2 上 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA, ENABLE); // SCK=PA5, MOSI=PA7 配置为复用推挽输出 GPIO_StructInit(&gpio); gpio.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); // MISO=PA6 配置为浮空输入 gpio.GPIO_Pin = GPIO_Pin_6; gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &gpio); // CS=PA4 配置为推挽输出,初始化后拉高 gpio.GPIO_Pin = GPIO_Pin_4; gpio.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &gpio); GPIO_SetBits(GPIOA, GPIO_Pin_4); // SPI1 主机模式,8 位数据,极性/相位先用模式 0 SPI_StructInit(&spi); spi.SPI_Direction = SPI_Direction_2Lines_FullDuplex; spi.SPI_Mode = SPI_Mode_Master; spi.SPI_DataSize = SPI_DataSize_8b; spi.SPI_CPOL = SPI_CPOL_Low; spi.SPI_CPHA = SPI_CPHA_1Edge; spi.SPI_NSS = SPI_NSS_Soft; spi.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; SPI_Init(SPI1, &spi); SPI_Cmd(SPI1, ENABLE); }

代码逻辑不复杂:开时钟、配引脚、配 SPI 参数。关键在于分频系数。F103 里 SPI1 挂在 APB2 上,主频 72MHz,所以分频 16 时 SCLK 是 4.5MHz,而不是很多人以为的 72 除以 16。这个速率对 BMI160 来说完全够用,先把时序跑通再说提速。

参数说明:SPI_BaudRatePrescaler可以改,改到SPI_BaudRatePrescaler_4后 SCLK 是 9MHz,改到SPI_BaudRatePrescaler_2就是 18MHz。但我不建议一上来就拉到 9MHz 以上,因为布线稍长、CS 和 SCLK 的斜率不一致时,采到的数据可能随机出错。调试新板子我一般固定在 4.5MHz,等逻辑分析仪确认波形干净再提速。

另外一个容易出错的地方是 SPI 的极性和相位。BMI160 的手册写的是支持 SPI 模式 0 或者模式 3,但很多驱动模板默认写SPI_CPOL_Low / SPI_CPHA_1Edge,也就是模式 0。如果你读到芯片 ID 一直不对,可以把这两行改成SPI_CPOL_High / SPI_CPHA_2Edge再试一次,大概率是这里的问题。

2.3 BMI160 初始化顺序:软复位、ID 检查、量程配置、使能陀螺仪

BMI160 的初始化流程看着简单,但顺序错了就很玄学。正确顺序一般是:先做软复位清状态,再读芯片 ID 确认 SPI 链路通,然后配加速度计量程和 ODR,配陀螺仪量程,最后把陀螺仪从 suspend 切到 normal。

// bmi160.c — 初始化核心流程 void BMI160_Init(void) { uint8_t id = 0; uint8_t cmd = 0xB6; // 软复位命令字 uint8_t acc_conf = 0x28; // ACC 输出速率 100Hz,带宽默认 uint8_t acc_range = 0x00; // 加速度计量程 ±2g // 第 1 步:确认 SPI 链路正常,读 CHIP_ID,固定值 0xD1 BMI160_ReadReg(0x00, &id, 1); if (id != 0xD1) { // 卡在这里,先查 CS 电平、SPI 极性和焊接 while (1); } // 第 2 步:软复位,清理内部状态 BMI160_WriteReg(0x7E, &cmd, 1); delay_ms(10); // 复位后要等内部初始化完成 // 第 3 步:配置加速度计,range=±2g,ODR=100Hz BMI160_WriteReg(0x41, &acc_range, 1); BMI160_WriteReg(0x40, &acc_conf, 1); // 第 4 步:配置陀螺仪,量程 ±2000dps,ODR=100Hz uint8_t gyr_range = 0x00; uint8_t gyr_conf = 0x28; BMI160_WriteReg(0x43, &gyr_range, 1); BMI160_WriteReg(0x42, &gyr_conf, 1); // 第 5 步:把陀螺仪从 suspend 切到 normal cmd = 0x11; BMI160_WriteReg(0x7E, &cmd, 1); delay_ms(5); }

这段代码里的关键点有两个。第一个是软复位。上电后芯片内部状态可能是未知的,不先写 0xB6 复位,后面配置寄存器可能被内部状态机吃掉。第二个是陀螺仪使能命令,很多工程漏了这一步,导致加速度计正常、陀螺仪读数全零,而且没有任何报错,排查起来最耗时间。

参数说明:acc_range为 0 对应 ±2g,如果要测倾角,±2g 够用;做剧烈运动项目建议改成 ±8g 或 ±16g,也就是把值改成 0x02 或 0x03。gyr_range为 0 对应 ±2000dps,机器人和手持设备常用。ODR 那个字节的具体含义要看数据手册中的 ODR 映射表,不同驱动库封装的写法不太一样,但前提是要保证加速度计和陀螺仪的输出速率一致,后面做姿态融合时优先级才不用猜。

3. SPI 读写 BMI160 的底子:地址格式、burst 读与 CS 时序

3.1 地址字节的读写位,是最容易踩的坑

SPI 模式下 BMI160 的寄存器地址只有 7 位,最高位不代表地址,代表读写方向。读操作要把最高位置 1,写操作保持为 0。很多第一次调 BMI160 的开发者都是栽在这里,直接拿数据手册里的寄存器地址去读,结果永远得到 0xFF。

工程里的读写函数一般是这么封装的:

// bsp_spi.c — BMI160 寄存器读写底层封装 void BMI160_WriteReg(uint8_t reg, uint8_t val) { CS_LOW(); // 片选拉低,开始一次传输 SPI_SendByte(SPI1, reg & 0x7F); // 写操作,最高位是 0 SPI_SendByte(SPI1, val); // 写入数据 CS_HIGH(); // 片选拉高,结束传输 } void BMI160_ReadReg(uint8_t reg, uint8_t *buf, uint8_t len) { CS_LOW(); SPI_SendByte(SPI1, reg | 0x80); // 读操作,最高位置 1 while (len--) { *buf++ = SPI_RecvByte(SPI1); // 连续读 len 个字节 } CS_HIGH(); }

逻辑说明:reg & 0x7F是把寄存器地址限制在 7 位内,防止高位干扰;reg | 0x80则是显式加上读标志。我建议所有底层读写都走这两个函数,不要在外面拼地址。之前我见过一个功能完整的项目,初始化没问题,数据读取却偶尔跳变,最后发现是某处代码直接用reg当发送字节,读操作没有置位最高位,CS 拉低后 MISO 上根本没有正确返回值,读到的全是缓存里的旧数据。

参数说明:CS_LOW()和CS_HIGH()是对 PA4 的位操作。值得注意的是,在 ReadReg 函数里,CS 在整个读过程保持低电平,包括发送地址字节和接收数据字节。有些实现会每读一个字节就拉高一次 CS,那样地址自动递增机制就失效了。

3.2 一次 burst 读,把加速度计和陀螺仪 12 字节全拿回来

BMI160 的数据寄存器是连续的,加速度计从 0x04 开始,依次是 X 低字节、X 高字节、Y 低字节、Y 高字节、Z 低字节、Z 高字节;陀螺仪从 0x0A 开始,同样是小端排列。把这两组数据合并成 12 字节一次读出,是最推荐的做法。

// bmi160.c — 一次性读取加速度计和陀螺仪原始数据 void BMI160_ReadSensor(int16_t acc[3], int16_t gyr[3]) { uint8_t buf[12]; uint8_t reg = 0x04; // ACC_X_L,起始地址 CS_LOW(); SPI_SendByte(SPI1, reg | 0x80); // 发送起始地址,读标志置位 for (int i = 0; i < 12; i++) { buf[i] = SPI_RecvByte(SPI1); // 连续读,地址自动递增 } CS_HIGH(); // 小端拼接:低字节在前,高字节在后 acc[0] = (int16_t)(buf[1] << 8 | buf[0]); acc[1] = (int16_t)(buf[3] << 8 | buf[2]); acc[2] = (int16_t)(buf[5] << 8 | buf[4]); gyr[0] = (int16_t)(buf[7] << 8 | buf[6]); gyr[1] = (int16_t)(buf[9] << 8 | buf[8]); gyr[2] = (int16_t)(buf[11] << 8 | buf[10]); }

逻辑说明:发送起始地址之后,BMI160 在时钟的每个边沿自动把地址加 1,所以只要 CS 不拉高,就可以连续把 12 个字节都读出来。这种 burst 读比每个寄存器单独拉一次 CS 的写法性能好很多,也更稳定,因为它减少了片选翻转的次数,SPI 主从双方的时序窗口更宽裕。

参数说明:buf数组长度必须是 12,少了读不全,多了会把下一个寄存器的值也读出来。如果你只关心加速度计,可以把长度改成 6,但后面拼接陀螺仪的数组下标要对齐。我见过有人把长度改成 6 后忘了改拼接代码,陀螺仪数据错位了两个字节,静止时数值不为零,查了很久才发现是下标越界了。

3.3 CS 时序与字节对齐:为什么丢字节总是最后一个

SPI 丢数据最常见的现象是连续读数时,最后一次数据变成了 0xFF 或者上一次的值。归根结底是 CS 拉高的时机早了或者晚了。

我一般会在 CS 拉高前加一个极短的延时,或者干脆不延时,但保证发送完最后一个字节后等待 SPI 的 BSY 标志清除再拉高 CS。F103 硬件 SPI 在连续传输时,最后一个字节的接收完成标志和 RXNE 事件之间有一个小的延迟,如果直接拉高 CS,传感器端可能还在输出最后一位,时序就破了。

工程里遇到这种问题,我建议先把 SCLK 降到 2.25MHz 试一下,也就是分频系数设为 32。如果问题消失,说明是高速下的建立时间不够;如果问题还在,就重点检查 CS 信号和 SCLK 之间的相对延迟。用 GPIO 模拟 CS 的好处是可以在拉高前强制做一次while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET);,这个方法比软件延时可靠得多。

4. 把原始寄存器值变成可用的物理量:量程、符号、单位换算

4.1 从两个字节拼出有符号数,别用 char

BMI160 输出的原始数据是 16 位有符号数,小端存储。上一步的拼接代码用了buf[1] << 8 | buf[0],这里有个隐藏问题:如果buf[1]被声明为uint8_t,左移后需要转成int16_t才能保持符号位。如果用的是char,在某些编译器上会先做符号扩展,导致负数变成很大的正数。

// bmi160.c — 符号扩展校验 int16_t raw = (int16_t)((uint16_t)buf[1] << 8 | buf[0]);

逻辑说明:这里先把高字节转成uint16_t再左移,避免了高字节是负数时被错误扩展。然后整个表达式转成int16_t,让两个字节拼接后的结果带上符号。静态时加速度计 Z 轴读数应该是 1g 左右的加速度,即 LSB 值约为 16384,如果是 -30000 或者 30000 附近,说明量程配置和换算系数对不上。

参数说明:原始值本身没有单位,必须先确认量程才能换算。加速度计量程 ±2g 时,1g 对应 16384 LSB;±4g 时对应 8192 LSB;±8g 时对应 4096 LSB;±16g 时对应 2048 LSB。换算时一定要用对应量程的分辨率,否则后续姿态角度会差一倍。

4.2 加速度计换算:静止时输出就是 1g

把原始值变成物理量的代码很简单,关键是要把量程参数做成宏,方便后面改动。

// bmi160.c — 加速度计原始值转物理值 #define ACC_RANGE_2G_LSB (16384.0f) // 1g 对应的 LSB 数 #define ACC_RANGE_4G_LSB (8192.0f) #define ACC_RANGE_8G_LSB (4096.0f) #define ACC_RANGE_16G_LSB (2048.0f) float acc_x_g = (float)acc[0] / ACC_RANGE_2G_LSB; float acc_y_g = (float)acc[1] / ACC_RANGE_2G_LSB; float acc_z_g = (float)acc[2] / ACC_RANGE_2G_LSB;

逻辑说明:除以 LSB 数以后,X、Y 轴在水平静止时接近 0,Z 轴接近 1。如果 Z 轴读数是 -1 左右,说明传感器上下表面装反了,不需要改代码,只在结构安装时调整方向就行。注意这里用的是浮点数,F103 没有硬件 FPU,如果要大量计算建议改成定点数,但 demo 阶段直接浮点最直观。

参数说明:如果工程初始化的量程是 ±8g,上面代码也用ACC_RANGE_8G_LSB。不同量程的分辨率差别很大,不要在代码里写死 16384,否则你后面换量程时会忘记更新这里。

4.3 陀螺仪换算:先看量程再看单位

陀螺仪的原始值代表角速度,量程不同则比例不同。以 ±2000dps 量程为例,满量程对应的 LSB 数是 32768,所以比例是 2000 除以 32768,约等于 0.061 度每秒每 LSB。但在做姿态融合时,很多上位机或者算法模块要求输入的是弧度每秒,所以还要先把度转换成弧度,乘以 0.0174533。

// bmi160.c — 陀螺仪原始值转弧度每秒 #define GYR_RANGE_2000_DPS_LSB (32768.0f) #define DPS_TO_RAD (0.0174533f) float gyr_x_rad = (float)gyr[0] / GYR_RANGE_2000_DPS_LSB * 2000.0f * DPS_TO_RAD; float gyr_y_rad = (float)gyr[1] / GYR_RANGE_2000_DPS_LSB * 2000.0f * DPS_TO_RAD; float gyr_z_rad = (float)gyr[2] / GYR_RANGE_2000_DPS_LSB * 2000.0f * DPS_TO_RAD;

逻辑说明:gyr[0]除以 32768 得到的是满量程的百分比,乘以 2000 就是度数每秒,再乘以DPS_TO_RAD转成弧度每秒。静止时三个轴的换算值应该接近 0,如果有一个轴稳定输出一个非零值,比如 0.3 rad/s,多半是硬件安装有残余应力或者焊接时传感器本体受力变形。

参数说明:后面的 2000 必须和初始化时写入的gyr_range一致。如果初始化配置的是 ±500dps,这里的 2000 也要改成 500,否则角速度数值会整体偏大 4 倍。这个坑特别隐蔽,因为静止时数值还是接近 0,只有转动起来才发现姿态发散比较快。

4.4 静止检测的简单阈值方法

拿到加速度计和陀螺仪的物理值后,第一件事不是直接送滤波算法,而是先判断传感器当前是否静止。这个判断在姿态融合里很重要,不然静止时陀螺仪的微小漂移会被积分放大。

// bmi160.c — 静止检测 #define GYRO_STATIC_THRESHOLD (0.02f) // rad/s,小于此值认为静止 #define ACC_MAG_DEVIATION (0.1f) // 加速度模长与 1g 的偏差阈值 int BMI160_IsStatic(float ax, float ay, float az, float gx, float gy, float gz) { float acc_mag = sqrtf(ax * ax + ay * ay + az * az); // 陀螺仪三轴角速度都要接近 0,同时加速度模长接近 1g if (fabsf(gx) < GYRO_STATIC_THRESHOLD && fabsf(gy) < GYRO_STATIC_THRESHOLD && fabsf(gz) < GYRO_STATIC_THRESHOLD && fabsf(acc_mag - 1.0f) < ACC_MAG_DEVIATION) { return 1; } return 0; }

逻辑说明:静止时加速度计三轴合力的模长等于 1g,陀螺仪三轴角速度接近 0。两个条件同时满足才判定静止。只判断陀螺仪是不够的,因为匀速转动时陀螺仪也有输出,但加速度计模长仍然接近 1g;反过来,加速度计在振动环境下模长不稳定,但陀螺仪数值可能在 0 附近。两者配合才能覆盖多数实际场景。

参数说明:GYRO_STATIC_THRESHOLD设成 0.02,是给手持设备留了 1 度每秒左右的余量,太严容易误判,太松又会把转动也当成静止。ACC_MAG_DEVIATION设成 0.1,可以过滤掉大多数由机械振动导致的加速度波动。这两个阈值在 demo 里可以先用,真正做产品时要根据结构件的固有振动频段重新调。

5. SPI + BMI160 常见问题排查:五条踩坑记录

5.1 芯片 ID 读到 0xFF,SPI 链路完全不通

现象:初始化函数卡在 ID 检查那一步,读出来的值一直是 0xFF。

原因:最常见的是 SPI 模式不匹配。BMI160 支持 SPI 模式 0 或模式 3,但有的板卡原理图把 CS 接在 PA4,SCK 走线经过长排线,信号斜率变缓后,模式 0 的采样点正好落在数据建立不稳定的位置。另外,如果 CS 没有拉低或者拉低时间不够,传感器根本不会进入响应状态,读到的自然是 0xFF。

解决:先用示波器看 CS、SCK、MOSI 三个信号的波形,确认 CS 确实拉低了。然后在初始化函数里把 SPI 改成模式 3,也就是SPI_CPOL_High / SPI_CPHA_2Edge,再读一次 ID。我遇到过的案例中,半数以上是 SPI 模式问题,剩下的是 CS 引脚复用冲突,PA4 同时被别的外设占用了。

5.2 加速度计正常,陀螺仪永远输出 0

现象:加速度计读数正常,转动板子时陀螺仪三个轴纹丝不动。

原因:BMI160 上电后陀螺仪处于 suspend 状态,需要通过命令寄存器切到 normal。很多初始化代码错误地认为配了量程就代表使能了,量程寄存器只是设定范围,电源模式是独立的控制流程。

解决:在初始化函数最后显式写一次0x7E寄存器,命令字用工程头文件里定义好的BMI160_CMD_GYRO_NORMAL,或者参考数据手册中电源模式切换的命令值。写完命令后加 5ms 以上延时,再用状态寄存器确认切换成功。

5.3 静止时数据跳变,数值忽大忽小

现象:板子放在桌面上,串口打印的加速度原始值会不定期跳变几百甚至上千。

原因:如果 SPI 时序本身没问题,那大概率是供电纹波或者传感器电源引脚缺去耦电容。BMI160 内部有数字逻辑,电源纹波大时数据输出会抖动。另一个常被忽略的原因是 SPI 连续读时用单字节读函数,CS 反复拉高拉低,每次传输之间传感器内部状态还没有完全稳定。

解决:先用示波器看传感器 VDD 和 VDDIO 的纹波,纹波超过 50mV 就在电源入口加 1uF 和 100nF 两级电容。然后把所有数据读取改成 burst 读,一次拉低 CS 读完 12 字节,减少片选翻转次数。这个方法在很多项目里都能显著减少数据跳变。

5.4 读出来数据全是 0,但 ID 能读到

现象:ID 检查通过,读数据寄存器却全为 0。

原因:这个现象大概率是寄存器地址写错了。数据寄存器起始地址是 0x04,但如果你用的是单字节读函数,且每次读之前单独发地址,必须保证每个地址都有正确的读写标志位。另一种情况是代码里把数据读到了同一个数组下标,覆盖了每次读到的值,看起来像全是 0。

解决:先用调试器读 0x04 到 0x0F 的原始值,确认寄存器内容是否为 0。再用 burst 读函数,把地址递减逻辑交给芯片自己处理。如果寄存器内容确实是 0,检查传感器的电源模式和量程设置,看是否在配置过程中被意外的写操作重置了。

5.5 数据能读,但更新频率明显和配置对不上

现象:配置 ODR 为 100Hz,串口实测只有 25Hz 左右。

原因:BMI160 的数据就绪中断不一定配置到了正确引脚,如果你只是轮询读取,没有检查STATUS寄存器里的数据就绪位,可能会读到新旧数据混在一起,导致频率看起来对不上。另外,如果 SPI 读速度太慢,读一次传感器要占用超过一个数据周期,轮询频率自然被程序阻塞拉低。

解决:在每次读取前检查STATUS寄存器的数据就绪位,只有就绪时才发起 burst 读。如果还慢,就检查delay_ms是不是塞得太长。F103 的 SPI 在 4.5MHz 下读 12 字节只需约 20 微秒,延迟大头通常在主循环的其它逻辑上。

6. 验证 SPI 链路是否真的跑对:串口打印原始数据再进算法

很多项目把姿态滤波算法写完才发现数据源有问题,然后来回折腾,浪费大量时间。我的习惯是搭一条最原始的调试链路:STM32F103 通过 SPI 读 BMI160,把原始值发到串口,上位机不管三七二十一,先画一条波形出来看。

第一步是打开工程里的串口初始化,确认波特率和上位机一致。F103 的串口 1 可以直出 TTL,接一个 USB 转串口模块就能看到数据。建议波特率用 115200,打印一次 12 字节数据加换行符,时间开销约 1ms,不会影响 100Hz 的读取频率。

第二步是排查时只打印原始值,不打换算后的物理量。因为看原始值能直观发现符号位问题、字节拼接问题和量程错误。比如静止时 Z 轴原始值应该在 16000 附近,如果打印出来是 -30000 左右,说明符号扩展出了问题,换算成 g 数反而不容易看出来。

第三步是用逻辑分析仪抓 CS、SCK、MISO 三条线,确认一次 burst 读的波形是否干净。重点看 CS 拉低后地址字节的读写位是否为 1,MISO 上 12 字节数据是否连续无间隙。如果有间隙,说明 SPI 的连续传输配置不对,或者代码里在收发之间加了多余延时。

我自己的习惯是,在新板子第一次跑 BMI160 时,先只调通 SPI 读 ID,然后打印原始数据,再上滤波和算法,不走捷径。上一次偷懒跳过波形检查,结果数据在高速转动时偶发抖动,查了两天才发现是 CS 信号线上有一个 100pF 的电容让边沿变缓,低速没问题,高速就采样错误。从那以后,我每次接新传感器都会强制走一遍原始数据打印加波形验证这个流程。希望这个顺序也能帮你少走点弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询