STM32 SPI驱动BMI088:双CS配置与RT-Thread移植实战 上周帮朋友调一块四轴飞控遇到一个很典型的现象BMI088 挂在 STM32 的 SPI1 上读加速度计 ID 正常读陀螺仪 ID 全是 0xFF换一块芯片也一样。第一反应不是怀疑芯片坏了而是怀疑对 BMI088 的从机结构理解错了。后来把逻辑分析仪接到片选引脚上才发现问题根本不是芯片而是驱动里把两个从机的 CS 搞混了。这篇文章就从这个实际踩坑场景说起结合 RT-Thread 的 SPI 设备框架把 STM32F04ZGT6 通过 SPI 驱动 BMI088 的流程完整拆开讲一遍。先说明一下标题里的 STM32F04ZGT6 并不是 ST 官方正式型号按资源量看大概率是 STM32F407ZGT6 的笔误后文按 F407 的 SPI 资源来讲核心流程在 STM32F0 等其他系列上同样适用。文章会包含 CubeMX 配置、RT-Thread 侧设备注册、寄存器初始化、六轴原始数据读取和灵敏度换算工程结构也一并列出来适合已经跑通 RT-Thread 基础工程、但第一次接触 BMI088 的开发者直接对照着抄。1. 为什么要在双 CS 上纠缠先把 BMI088 的从机关系看清1.1 加速度计和陀螺仪其实是两个独立的 SPI 从机BMI088 虽然是一颗六轴传感器但内部物理上是两颗独立的裸片封装在一起加速度计部分和陀螺仪部分分别拥有自己的寄存器映射、自己的状态机、自己的 SPI 从机地址。对外只共享 SCK、MOSI、MISO 三根 SPI 总线信号而 CS 引脚则有两个CSB1 管加速度计CSB2 管陀螺仪。这就带来一个和普通 SPI 传感器完全不同的使用习惯你要把 BMI088 当成“挂在同一条 SPI 总线上的两个从设备”来操作而不是当成一个芯片。实际操作时拉低加速度计的 CS 只能访问加速度计寄存器拉低陀螺仪的 CS 只能访问陀螺仪寄存器两条寄存器地址互不相通。我在最开始调飞控时犯的错就是只用一个 CS 引脚结果读加速度计的 chip ID 是正确的 0x1E读陀螺仪 chip ID 时同一个地址读到的是 0x00一度怀疑是芯片型号买错了。后来查数据手册才确认陀螺仪的 chip ID 是 0x0F必须由第二个 CS 引脚拉低后才能读到。1.2 双 CS 结构对 RT-Thread 设备模型带来的影响RT-Thread 的 SPI 驱动框架天然支持“一条 SPI 总线下挂多个 SPI 设备”的场景每个设备通过rt_spi_device结构体描述并拥有独立的片选引脚。所以正确做法不是把 BMI088 注册成一个 SPI 设备而是注册成两个设备例如bmi088_acc和bmi088_gyro。如果只注册一个设备就会遇到一个很尴尬的局面RT-Thread 的 SPI 消息模型在发送前会自动拉低片选发送结束后拉高片选你很难在一次调用里同时操作两个 CS。就算强行手动切换 CS也会和框架的片选状态管理冲突导致后续事务的片选时序错乱。用两个设备对象分别管理两个 CS是让代码和 RT-Thread 框架配合得最舒服的方案也能天然利用 SPI 总线的互斥锁避免多线程同时访问时两个数据流互相干扰。这里还要记住一个前提两个 SPI 设备共享同一条总线但 RT-Thread 的总线锁会保证同一时刻只有一个设备占用 SPI1所以我们不需要在应用层额外加锁。2. CubeMX 那一步就把 SPI 引脚和片选策略定下来后面代码不会乱2.1 SPI 引脚分配和参数设置建议以 STM32F407ZGT6 为例最常用的做法是把 BMI088 接到 SPI1引脚分配如下信号STM32 引脚说明SCKPA5SPI1_SCKMISOPA6SPI1_MISO接 BMI088 SDOMOSIPA7SPI1_MOSI接 BMI088 SDIACC_CSPA4接 BMI088 CSB1加速度计片选GYRO_CSPE4接 BMI088 CSB2陀螺仪片选在 CubeMX 里把 SPI1 配成全双工主机模式Frame Format 选 8 bitFirst Bit 选 MSB FirstCPOL 设 LowCPHA 设 1 Edge。这组参数就是标准的 SPI Mode 0BMI088 只支持 Mode 0不要用 Mode 3否则时序完全对不上。时钟分频建议先保守一点。F407 的 SPI1 挂在 APB2 上最高 84MHzCubeMX 里预分频选 32得到 2.625MHz。BMI088 手册标称 SPI 最高可达 10MHz但实际飞线环境、杜邦线长度、模块供电纹波都会影响高速稳定性先把速率压到 2MHz 左右把数据调通再逐步提高。我实测在 5.25MHz 下用短杜邦线连接还能稳定工作但是如果传感器模块离 MCU 超过 10cm最好留在 2.1MHz 或 2.6MHz。2.2 软件片选比硬件片选更适合这个场景CubeMX 里 SPI 的 NSS 设置有两种选择硬件片选和软件片选。驱动 BMI088 时强烈建议把 NSS 配成 Software然后用两个普通 GPIO 做 CS。原因在于 BMI088 需要两个独立片选STM32 的硬件 NSS 只能输出一个 SPI 外设的 NSS 信号根本管不过来。即使你愿意把其中一个 CS 映射到 SPI 外设的 NSS 引脚RT-Thread 的通用 SPI 驱动也会在内部用软件 CS 逻辑管理片选引脚和硬件 NSS 同时存在反而会造成重复控制甚至出现驱动释放 CS 时硬件又把电平拉低的问题。软件片选还有一个好处CS 的拉低、拉高时机完全由驱动代码控制可以确保在“发送寄存器地址”和“连续读取多个字节”的整个事务期间 CS 保持低电平。这对 BMI088 这类需要 burst read 的传感器非常关键一个完整读取事务中间如果 CS 被拉高数据就会变成无效值。CubeMX 里把 PA4、PE4 这两根 CS 脚配置成 GPIO 推挽输出初始电平设为 High就完成了硬件准备。3. RT-Thread 侧把加速度计与陀螺仪注册成两个 SPI 设备3.1 先确认 SPI 总线已经在 RT-Thread 中注册成功在 RT-Thread Studio 里打开 RT-Thread Settings在组件中的设备驱动分类下勾选 SPI 总线驱动。如果使用的是 ENV 方式则在 menuconfig 中选中RT-Thread Components → Device Drivers → Using SPI Bus/Device device drivers编译下载后在 MSH 控制台输入list_device能看到spi1这条总线设备。如果看不到说明 STM32 BSP 的 board 配置里没有使能 SPI1需要检查board/Kconfig或drv_spi.c中的外设使能配置。注意这里出现的是总线设备spi1不是传感器设备传感器设备要应用自己去挂载。3.2 挂载两个 SPI 设备并配置通信参数RT-Thread 的 STM32 BSP 中提供了rt_hw_spi_device_attach函数可以把一个 SPI 设备挂到总线上并指定一个 GPIO 作为片选。我们调用两次分别挂载加速度计和陀螺仪#include rtthread.h #include rtdevice.h #include drv_spi.h #include board.h #define BMI088_ACC_CS_PIN GET_PIN(A, 4) #define BMI088_GYRO_CS_PIN GET_PIN(E, 4) static struct rt_spi_device *spi_acc_dev; static struct rt_spi_device *spi_gyro_dev; static struct rt_spi_configuration bmi088_spi_cfg { .mode RT_SPI_MODE_0 | RT_SPI_MSB, .data_width 8, .max_hz 2500 * 1000, }; static int bmi088_spi_device_init(void) { rt_err_t res; res rt_hw_spi_device_attach(spi1, bmi088_acc, (rt_base_t)BMI088_ACC_CS_PIN); if (res ! RT_EOK) { rt_kprintf(attach bmi088_acc failed\n); return res; } res rt_hw_spi_device_attach(spi1, bmi088_gyro, (rt_base_t)BMI088_GYRO_CS_PIN); if (res ! RT_EOK) { rt_kprintf(attach bmi088_gyro failed\n); return res; } spi_acc_dev (struct rt_spi_device *)rt_device_find(bmi088_acc); spi_gyro_dev (struct rt_spi_device *)rt_device_find(bmi088_gyro); if (spi_acc_dev RT_NULL || spi_gyro_dev RT_NULL) { return -RT_ERROR; } rt_spi_configure(spi_acc_dev, bmi088_spi_cfg); rt_spi_configure(spi_gyro_dev, bmi088_spi_cfg); return RT_EOK; } INIT_BOARD_EXPORT(bmi088_spi_device_init);这里INIT_BOARD_EXPORT会把初始化放到板级初始化阶段执行等 main 线程跑起来时两个 SPI 设备已经可以使用了。max_hz对应驱动层的分频配置RT-Thread 会根据这个值计算分频系数所以必须设置否则可能使用默认速度或异常速度。如果你的 BSP 中rt_hw_spi_device_attach的函数签名不同以你本机drv_spi.h里的原型为准但思路完全一致。挂载完成后在 MSH 里执行list_device应该能看到bmi088_acc和bmi088_gyro这两个设备挂载在spi1下。4. 初始化序列要按 BMI088 的寄存器顺序来顺序错了 ID 都读不对4.1 加速度计初始化软复位、SPI 模式、上电、量程BMI088 的加速度计初始化有一个固定顺序不能跳步。推荐按下面的代码顺序操作#define ACC_SOFTRESET_REG 0x7E #define ACC_IF_CONF_REG 0x7D #define ACC_PWR_CONF_REG 0x53 #define ACC_PWR_CTRL_REG 0x54 #define ACC_ODR_REG 0x40 #define ACC_RANGE_REG 0x41 #define ACC_SOFTRESET_CMD 0xB6 #define ACC_IF_SPI_MODE 0x04 #define ACC_PWR_NORMAL 0x00 #define ACC_ENABLE 0x01 #define ACC_ODR_100HZ 0xA0 #define ACC_RANGE_6G 0x01 static rt_err_t bmi088_accel_init(void) { rt_uint8_t id 0; bmi088_write_reg(spi_acc_dev, ACC_SOFTRESET_REG, ACC_SOFTRESET_CMD); rt_thread_mdelay(2); bmi088_write_reg(spi_acc_dev, ACC_IF_CONF_REG, ACC_IF_SPI_MODE); bmi088_write_reg(spi_acc_dev, ACC_PWR_CONF_REG, ACC_PWR_NORMAL); bmi088_write_reg(spi_acc_dev, ACC_PWR_CTRL_REG, ACC_ENABLE); bmi088_write_reg(spi_acc_dev, ACC_ODR_REG, ACC_ODR_100HZ); bmi088_write_reg(spi_acc_dev, ACC_RANGE_REG, ACC_RANGE_6G); bmi088_read_reg(spi_acc_dev, 0x00, id); if (id ! 0x1E) { rt_kprintf(accel chip id error: 0x%02X\n, id); return -RT_ERROR; } return RT_EOK; }第一步软复位写入 0x7E命令字是 0xB6。软复位后芯片需要一点时间恢复必须延时 2ms否则后续寄存器写入可能直接被丢弃。第二步写 0x7D 为 0x04把加速度计接口切到 SPI 模式。然后打开电源控制寄存器最后配置 ODR 和量程。这里有两个容易踩的细节一是 0x7D 的 SPI 模式配置必须在软复位之后重新写一次因为软复位会清掉接口配置二是 0x53 和 0x54 两个电源寄存器各管一部分很多驱动只写 0x54 不写 0x53结果芯片仍处于挂起状态数据永远读不到有效值。这部分如果不确定最直接的办法就是读回 chip ID加速度计读出来必须是 0x1E。4.2 陀螺仪初始化同样有独立软复位和电源控制陀螺仪的寄存器地址和加速度计不同初始化命令也不同不能复用加速度计的代码。陀螺仪的标准初始化序列如下#define GYRO_SOFTRESET_REG 0x14 #define GYRO_LPM1_REG 0x11 #define GYRO_BW_REG 0x15 #define GYRO_RANGE_REG 0x0E #define GYRO_SOFTRESET_CMD 0xB6 #define GYRO_NORMAL_MODE 0x00 #define GYRO_BW_532_2000 0x80 #define GYRO_RANGE_2000DPS 0x00 static rt_err_t bmi088_gyro_init(void) { rt_uint8_t id 0; bmi088_write_reg(spi_gyro_dev, GYRO_SOFTRESET_REG, GYRO_SOFTRESET_CMD); rt_thread_mdelay(2); bmi088_write_reg(spi_gyro_dev, GYRO_LPM1_REG, GYRO_NORMAL_MODE); bmi088_write_reg(spi_gyro_dev, GYRO_BW_REG, GYRO_BW_532_2000); bmi088_write_reg(spi_gyro_dev, GYRO_RANGE_REG, GYRO_RANGE_2000DPS); bmi088_read_reg(spi_gyro_dev, 0x00, id); if (id ! 0x0F) { rt_kprintf(gyro chip id error: 0x%02X\n, id); return -RT_ERROR; } return RT_EOK; }陀螺仪的软复位寄存器是 0x14不是加速度计的 0x7E。很多资料写“BMI088 软复位命令是 0xB6”初学者容易把加速度计的 0x7E 也用在陀螺仪上结果陀螺仪一直不复位初始化失败。陀螺仪的量程寄存器 0x0E 写 0x00 表示 ±2000°/s这是工程中最常用的量程适合飞控、机器人姿态估算。如果后续需要更高分辨率可以改成 0x04 对应 ±125°/s但灵敏度换算表也要跟着变。带宽寄存器 0x15 写 0x80对应采样率 2000Hz 下约 532Hz 带宽适合高速运动场景。如果你更关注低噪声可以改成 0xAA 这类低带宽配置但响应会有明显延迟。4.3 寄存器读写函数封装上面代码里的bmi088_write_reg和bmi088_read_reg是基础函数。BMI088 读操作要求在寄存器地址最高位置 1也就是写入地址时要 | 0x80写操作不需要。这是因为 BMI088 的 SPI 读命令固定带读标志位漏掉这一位会导致读出来的是上一个寄存器的内容或全 0xFF。static rt_err_t bmi088_read_reg(struct rt_spi_device *dev, rt_uint8_t reg, rt_uint8_t *val) { rt_uint8_t cmd reg | 0x80; return rt_spi_send_then_recv(dev, cmd, 1, val, 1); } static rt_err_t bmi088_write_reg(struct rt_spi_device *dev, rt_uint8_t reg, rt_uint8_t val) { rt_uint8_t cmd[2] {reg, val}; return rt_spi_send(dev, cmd, 2); }这里的rt_spi_send_then_recv会保证在发送地址和接收数据期间 CS 保持拉低这是 RT-Thread SPI 框架内置的设备接口很适合 BMI088 的寄存器读取。如果使用rt_spi_send和rt_spi_recv分开调用CS 在两次调用之间可能被释放导致读取中断。5. 写一个能直接用的六轴读取模块数据、换算、MSH 调试5.1 连续读取 6 字节原始数据BMI088 的加速度计数据寄存器从 0x12 开始陀螺仪从 0x02 开始都是按 X_LSB、X_MSB、Y_LSB、Y_MSB、Z_LSB、Z_MSB 的顺序排列。读取时直接连续读 6 个字节即可RT-Thread 的rt_spi_send_then_recv天然支持这种 burst read#define ACC_DATA_START_REG 0x12 #define GYRO_DATA_START_REG 0x02 void bmi088_read_raw(int16_t acc[3], int16_t gyro[3]) { rt_uint8_t buf[6]; bmi088_read_burst(spi_acc_dev, ACC_DATA_START_REG, buf, 6); acc[0] (int16_t)((rt_uint16_t)buf[0] | ((rt_uint16_t)buf[1] 8)); acc[1] (int16_t)((rt_uint16_t)buf[2] | ((rt_uint16_t)buf[3] 8)); acc[2] (int16_t)((rt_uint16_t)buf[4] | ((rt_uint16_t)buf[5] 8)); bmi088_read_burst(spi_gyro_dev, GYRO_DATA_START_REG, buf, 6); gyro[0] (int16_t)((rt_uint16_t)buf[0] | ((rt_uint16_t)buf[1] 8)); gyro[1] (int16_t)((rt_uint16_t)buf[2] | ((rt_uint16_t)buf[3] 8)); gyro[2] (int16_t)((rt_uint16_t)buf[4] | ((rt_uint16_t)buf[5] 8)); }注意一个小端字节序问题BMI088 输出的数据低字节在前高字节在后用左移拼起来即可。转换成int16_t后原始值范围是 -32768 到 327670g 或 0°/s 时的输出接近 0而不是某个固定中间值。5.2 原始值到物理量的灵敏度换算拿到原始值后要换算成 g 和 °/s。不同量程对应的灵敏度不同换算系数不能写死。我常用的两套量程参数如下加速度计量程灵敏度 LSB/g每 LSB 对应加速度(g)±3g109200.0000916±6g54600.0001832±12g27300.0003663±24g13650.0007326陀螺仪量程灵敏度 LSB/(°/s)每 LSB 对应角速度(°/s)±125°/s262.40.00381±250°/s131.20.00762±500°/s65.60.01525±1000°/s32.80.0305±2000°/s16.40.061在本文的初始化配置下加速度计量程是 ±6g灵敏度为 5460 LSB/g陀螺仪量程是 ±2000°/s灵敏度为 16.4 LSB/(°/s)。所以换算代码可以这样写#define ACC_SENSITIVITY_6G 5460.0f #define GYRO_SENSITIVITY_2000 16.4f void bmi088_read_scaled(float acc_g[3], float gyro_dps[3]) { int16_t acc_raw[3]; int16_t gyro_raw[3]; int i; bmi088_read_raw(acc_raw, gyro_raw); for (i 0; i 3; i) { acc_g[i] acc_raw[i] / ACC_SENSITIVITY_6G; gyro_dps[i] gyro_raw[i] / GYRO_SENSITIVITY_2000; } }这里必须使用浮点运算或者用精度足够高的定点换算。如果直接拿原始值当物理量用姿态解算出来的角度会完全偏掉。对性能敏感的场景也可以把除法换成乘法例如gyro_dps raw * 0.061f效果一样。5.3 用 MSH 命令快速验证整条链路调通后加一个 MSH 命令来实时观察数据是最直接的验证方式。把下面这段代码放到工程里编译下载后在终端输入bmi088_test就能看到加速度计和陀螺仪的原始值以及换算后的物理量static void bmi088_test(void) { int16_t acc_raw[3], gyro_raw[3]; float acc_g[3], gyro_dps[3]; bmi088_read_raw(acc_raw, gyro_raw); bmi088_read_scaled(acc_g, gyro_dps); rt_kprintf(acc raw: %6d %6d %6d gyro raw: %6d %6d %6d\n, acc_raw[0], acc_raw[1], acc_raw[2], gyro_raw[0], gyro_raw[1], gyro_raw[2]); rt_kprintf(acc(g): %8.4f %8.4f %8.4f gyro(dps): %8.2f %8.2f %8.2f\n, acc_g[0], acc_g[1], acc_g[2], gyro_dps[0], gyro_dps[1], gyro_dps[2]); } MSH_CMD_EXPORT(bmi088_test, bmi088 spi raw data test);板子静止放在桌面上时Z 轴加速度计换算结果应该接近 1gX、Y 轴接近 0g陀螺仪三轴接近 0°/s。如果静止时陀螺仪数值出现上百的量级优先检查量程配置和换算系数是否匹配。6. 调试实录SPI 常见问题与 CS 时序上的坑6.1 读回全是 0xFF 或 0x00先查波形而不是改代码很多人在 BMI088 上碰到的第一个问题就是读出来的 ID 是 0xFF 或 0x00然后开始怀疑代码写错了。这里我给一个固定的排查顺序第一步用逻辑分析仪抓 SPI 波形确认 CS 拉低时 SCK 上是否真的有时钟MOSI 上发送的地址是否带着 0x80。如果 CS 没拉低问题在 CS GPIO 配置或 RT-Thread 设备挂载如果 CS 正常但 MOSI 地址不对检查 SPI Mode 和字节序设置。第二步确认硬件接线。BMI088 模块上通常标注 SDO、SDI、SCK、CSB1、CSB2 五个信号SDO 对应 MISOSDI 对应 MOSI两个 CS 千万不要接反。我这里调过的几块 BMI088 模块CSB1 和 CSB2 的丝印位置很容易看错一旦接反读到陀螺仪 ID 就是 0xFF。第三步降低 SPI 时钟速度。用杜邦线连接时高频信号反射会直接导致 MISO 回传数据错位表现为时而正确时而全 FF。把max_hz从 10MHz 降到 2MHz 再试很多时候问题直接消失。6.2 两个 CS 互相串扰的问题双 CS 设备最容易出现的坑是“读加速度计数据时陀螺仪 CS 出现毛刺”或者反过来。原因是 RT-Thread 的 GPIO 输出配置不对。如果在 CubeMX 里把 CS 引脚配成了复用推挽输出或者初始化时没有把初始电平置高RT-Thread 驱动在设备挂载时可能会产生一次短暂的低电平脉冲把另一个从机意外选中。解决方法是确保两个 CS 引脚在进入 RT-Thread 框架之前就是 GPIO 推挽输出且初始电平为 High。CubeMX 里对应 GPIO mode 选 Output Push PullGPIO level 选 HighGPIO Speed 建议选 High 或 Very High。这样驱动在第一次控制 CS 之前两个从机都处于未选中状态。另一个容易忽略的点是rt_hw_spi_device_attach挂载设备时驱动会立刻将 CS 引脚置为释放状态。如果 BSP 内部使用的是和 CubeMX 不同的 GPIO 初始化顺序可能在 main 函数还没执行时就出现 CS 抖动。遇到这种情况可以在bmi088_spi_device_init被INIT_BOARD_EXPORT调用之前先把两个 CS GPIO 写高一次。6.3 初期不要用 DMA先把阻塞模式跑通有个高频问题BMI088 用 SPI 读取需不需要开 DMA需不需要两个 DMA答案很简单不需要。BMI088 单次读取加速度计或陀螺仪数据总共才 6 字节CPU 阻塞读写耗时几十微秒对于 1kHz 的 IMU 采样频率来说完全够用。开 DMA 反而会引入缓存一致性、DMA 通道分配、RT-Thread 中断上下文等额外问题初期调通阶段得不偿失。如果后续要做高频采样并把 CPU 让给姿态解算可以考虑用 DMA但前提是先用阻塞模式把数据正确性和时序验证通过。RT-Thread 的rt_spi_send_then_recv也支持配置 DMA 通道后的底层自动调用但那是驱动移植层面的优化应用层代码不需要改变。我的建议是把精力先放在传感器数据准确性上等姿态解算跑通了再回来优化性能。还有一个和中断相关的注意事项BMI088 初始化过程中的延时使用rt_thread_mdelay会主动释放 CPU 给其他线程调度这没问题。但不要在中断服务函数里直接调用bmi088_read_raw因为 SPI 传输过程中可能会等待总线锁中断上下文里等待锁容易造成优先级翻转甚至死锁。7. 完整工程组织和后续接入 Sensor 框架的方向7.1 建议的工程文件结构下面是我在实际工程中使用的文件组织方式核心代码集中在一个bmi088_bsp.c里比较适合 RT-Thread Studio 工程applications/ ├── main.c ├── bmi088_bsp.c ├── bmi088_bsp.h └── SConscript board/ ├── CubeMX_Config/ │ ├── spi.c │ └── gpio.c └── board.c libraries/ └── HAL_Drivers/ └── drv_spi.cbmi088_bsp.h中声明bmi088_init、bmi088_read_raw、bmi088_read_scaled三个接口即可具体实现全部放在.c文件里。这样 main 函数只需要调用初始化函数然后在自己的线程里读取数据结构非常干净。7.2 从原始数据到 RT-Thread Sensor 框架如果你的项目想通过 RT-Thread Sensor 框架统一管理多个传感器可以基于上面的原始驱动再写两个简单的 sensor 设备一个加速度计 sensor一个陀螺仪 sensor。RT-Thread 的 sensor 设备框架会把get_data回调理解为每次上报一组原始数据你只需要在回调函数里调用bmi088_read_raw然后填充struct rt_sensor_data中的data.acc或data.gyro字段即可。这样做的好处是上层应用可以通过统一的 sensor API 读取数据也可以利用 RT-Thread 提供的 sensor 工具命令直接查看设备状态。但要注意Sensor 框架的数据单位一般要求是 m/s² 和 °/s所以换算时不能直接填原始值要用前面章节里的换算系数算出物理量后填入。如果你的项目后续要做姿态解算我的建议是不要过早引入滤波。先把原始数据读出来放在一个固定线程中以 1kHz 频率打印确认静止数据稳定、翻转数据响应迅速再去做低通滤波或互补滤波。很多情况下数据异常不是算法问题而是 SPI 配置或者换算系数的问题先把底层数据质量做好上层算法才不会浪费调试时间。