文章目录
-
- 摘要
- 前言
- [BMP280 工作原理与寄存器总览](#BMP280 工作原理与寄存器总览)
- 方案对比与硬件接线
-
- [为什么用硬件 I2C 而不是软件模拟](#为什么用硬件 I2C 而不是软件模拟)
- 硬件选型清单
- [接线表与 I2C 地址陷阱](#接线表与 I2C 地址陷阱)
- [CubeMX 配置](#CubeMX 配置)
- 驱动代码实现
-
- bmp280.h:寄存器定义与结构体
- bmp280.c:校准读取与补偿计算
- [main.c 调用示例](#main.c 调用示例)
- 测试验证
-
- 静止稳定性:气压与温度
- [理论 vs 实测对照一:测量时间](#理论 vs 实测对照一:测量时间)
- [理论 vs 实测对照二:温度绝对精度](#理论 vs 实测对照二:温度绝对精度)
- [量化参数对比:IIR 滤波系数权衡](#量化参数对比:IIR 滤波系数权衡)
- 电梯实测:海拔换算验证
- 故障排查
-
- [1. 读 ID 返回 0xFF(最常见)](#1. 读 ID 返回 0xFF(最常见))
- [2. ID 读回 0x60,校准后气压仍偏高](#2. ID 读回 0x60,校准后气压仍偏高)
- [3. 校准参数全 0xFF,温度算出 -40℃](#3. 校准参数全 0xFF,温度算出 -40℃)
- [4. sleep 模式下读到的气压恒为 0](#4. sleep 模式下读到的气压恒为 0)
- [5. 海拔每天漂移 ±20 米](#5. 海拔每天漂移 ±20 米)
- [6. 数据偶发跳变一个数量级](#6. 数据偶发跳变一个数量级)
- 总结
- 版本备注
- 参考资料
摘要
无人机定高、气象站和室内导航都依赖气压传感器,但 BMP280 的校准补偿公式复杂、I2C 地址与滤波配置容易踩坑,不少网上示例直接输出原始寄存器值,导致海拔读数随天气漂移 ±20 米。本文基于 STM32F103C8T6 硬件 I2C 驱动 GY-BMP280 模块,逐字节拆解 24 字节校准参数的读取时序,完整实现官方补偿公式与标准大气海拔换算。实测:静止 1 小时气压标准差仅 0.21 Pa(约 1.8 cm 高度波动);IIR 滤波系数由 x0 调至 x16 噪声降低 86%,代价是阶跃响应延迟 3.2 s;电梯 5 层实测总高差 16.4 m,与参考层高推算值吻合。文末附 CubeMX 配置(含寄存器位标注)、可移植驱动代码和 6 类故障完整排查链。
前言
气压传感器这几年从"航模专属"变成了消费电子的标配:四轴飞行器靠它定高悬停,智能手表用它估算爬楼楼层,气象站用它换算出海平面气压。驱动它并不难,真正麻烦的是把数据读出来之后------校准参数怎么解、补偿公式怎么套、滤波开多大、海拔怎么换算,每一环都有"看似对了、实则飘了"的坑。
我在调试这块模块时踩过几个记忆深刻的坑:第一次读 ID 返回 0xFF 排查了半天是 SDO 悬空导致地址漂移;把 IIR 滤波开到 x16 后数据稳了,结果爬楼梯时高度变化慢得不像话。这些经验都会在本文的实测数据和故障排查里展开。
读完本文,你将掌握:
- BMP280 的寄存器体系与 24 字节校准参数的正确读取方法;
- 官方补偿公式的逐项含义与代码落地;
- CubeMX 配置中每个选项对应的寄存器位;
- IIR 滤波系数的量化权衡,以及海拔换算的海平面气压补偿策略。
前置条件:熟悉 STM32CubeMX 基本流程,会使用 HAL 库的 I2C 外设,手头有 STM32F103 最小系统板和一块 GY-BMP280 模块。本文完整工程代码可在 CSDN 下载频道 获取(VIP 免费)。
BMP280 工作原理与寄存器总览
芯片内部数据流
BMP280 是一颗 MEMS 压阻式气压传感器,内部结构可以简化为"传感单元 → 模拟前端 → ADC → 校准补偿 → 寄存器接口"五级流水。压阻桥把气压形变转为电阻变化,再经仪表放大器放大后送入 18~20 位 ADC,最终由片上逻辑结合出厂校准系数完成数字补偿。
#mermaid-svg-izZV4b9GwU9ec1zT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-izZV4b9GwU9ec1zT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-izZV4b9GwU9ec1zT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-izZV4b9GwU9ec1zT .error-icon{fill:#552222;}#mermaid-svg-izZV4b9GwU9ec1zT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-izZV4b9GwU9ec1zT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-izZV4b9GwU9ec1zT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-izZV4b9GwU9ec1zT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-izZV4b9GwU9ec1zT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-izZV4b9GwU9ec1zT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-izZV4b9GwU9ec1zT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-izZV4b9GwU9ec1zT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-izZV4b9GwU9ec1zT .marker.cross{stroke:#333333;}#mermaid-svg-izZV4b9GwU9ec1zT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-izZV4b9GwU9ec1zT p{margin:0;}#mermaid-svg-izZV4b9GwU9ec1zT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-izZV4b9GwU9ec1zT .cluster-label text{fill:#333;}#mermaid-svg-izZV4b9GwU9ec1zT .cluster-label span{color:#333;}#mermaid-svg-izZV4b9GwU9ec1zT .cluster-label span p{background-color:transparent;}#mermaid-svg-izZV4b9GwU9ec1zT .label text,#mermaid-svg-izZV4b9GwU9ec1zT span{fill:#333;color:#333;}#mermaid-svg-izZV4b9GwU9ec1zT .node rect,#mermaid-svg-izZV4b9GwU9ec1zT .node circle,#mermaid-svg-izZV4b9GwU9ec1zT .node ellipse,#mermaid-svg-izZV4b9GwU9ec1zT .node polygon,#mermaid-svg-izZV4b9GwU9ec1zT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-izZV4b9GwU9ec1zT .rough-node .label text,#mermaid-svg-izZV4b9GwU9ec1zT .node .label text,#mermaid-svg-izZV4b9GwU9ec1zT .image-shape .label,#mermaid-svg-izZV4b9GwU9ec1zT .icon-shape .label{text-anchor:middle;}#mermaid-svg-izZV4b9GwU9ec1zT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-izZV4b9GwU9ec1zT .rough-node .label,#mermaid-svg-izZV4b9GwU9ec1zT .node .label,#mermaid-svg-izZV4b9GwU9ec1zT .image-shape .label,#mermaid-svg-izZV4b9GwU9ec1zT .icon-shape .label{text-align:center;}#mermaid-svg-izZV4b9GwU9ec1zT .node.clickable{cursor:pointer;}#mermaid-svg-izZV4b9GwU9ec1zT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-izZV4b9GwU9ec1zT .arrowheadPath{fill:#333333;}#mermaid-svg-izZV4b9GwU9ec1zT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-izZV4b9GwU9ec1zT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-izZV4b9GwU9ec1zT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-izZV4b9GwU9ec1zT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-izZV4b9GwU9ec1zT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-izZV4b9GwU9ec1zT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-izZV4b9GwU9ec1zT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-izZV4b9GwU9ec1zT .cluster text{fill:#333;}#mermaid-svg-izZV4b9GwU9ec1zT .cluster span{color:#333;}#mermaid-svg-izZV4b9GwU9ec1zT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-izZV4b9GwU9ec1zT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-izZV4b9GwU9ec1zT rect.text{fill:none;stroke-width:0;}#mermaid-svg-izZV4b9GwU9ec1zT .icon-shape,#mermaid-svg-izZV4b9GwU9ec1zT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-izZV4b9GwU9ec1zT .icon-shape p,#mermaid-svg-izZV4b9GwU9ec1zT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-izZV4b9GwU9ec1zT .icon-shape .label rect,#mermaid-svg-izZV4b9GwU9ec1zT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-izZV4b9GwU9ec1zT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-izZV4b9GwU9ec1zT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-izZV4b9GwU9ec1zT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GY-BMP280 模块
I2C 100kHz
压阻式 MEMS
传感单元
模拟前端
仪表放大器
18~20bit ADC
校准补偿
t_fine 中间量
寄存器组
0x88~0xFA
STM32F103C8T6
I2C1
串口打印
115200 8N1
与上一代 BMP180 相比,BMP280 的补偿由主机端计算改为"片上补偿 + 主机二次补偿"的组合:片内完成原始 ADC 采集,主机根据 24 字节校准参数套用数据手册给出的公式得到最终物理量。这意味着不读取校准参数、直接套用固定公式,读出的气压必然不准,这是多数例程的第一处偷懒。
关键寄存器一览
BMP280 的寄存器按功能分四组:芯片信息、控制、状态、校准数据。
| 寄存器 | 地址 | 长度 | 说明 |
|---|---|---|---|
| ID | 0xD0 | 1B | 芯片标识,BMP280 固定读回 0x58 |
| RESET | 0xE0 | 1B | 写 0xB6 触发软复位 |
| STATUS | 0xF3 | 1B | bit3=measuring(测量进行中),bit0=im_update(校准参数更新中) |
| CTRL_MEAS | 0xF4 | 1B | bit7:5=osrs_t,bit4:2=osrs_p,bit1:0=mode |
| CONFIG | 0xF5 | 1B | bit7:5=t_sb,bit4:2=filter,bit0=spi3w_en |
| 校准数据 | 0x88~0xA1 | 24B | dig_T1T3、dig_P1P9,共 12 个系数 |
| 温度原始值 | 0xFA~0xFC | 20bit | 左对齐的高 20 位,0xFA 为 MSB |
| 气压原始值 | 0xF7~0xF9 | 20bit | 左对齐的高 20 位,0xF7 为 MSB |
这里有个新手容易看晕的点:温度原始值占了 0xFA~0xFC 三个字节,但有效数据只有高 20 位,所以拼数时要 (MSB << 12) | (LSB << 4) | (XLSB >> 4),低 4 位是无效位。气压同理。很多例程直接 16 位拼接取前两个字节,白白丢掉了低 4 位分辨率------超高分辨率模式下这 4 位可是 0.18 Pa 的精度来源。
补偿公式的物理含义
数据手册 4.2.3 节给出了一套浮点补偿公式,核心思想是:先算温度,得到中间量 t_fine,再拿 t_fine 去补偿气压。因为气压测量受温度漂移影响(手册标注温漂约 1.5 Pa/K),温度补偿必须先行。
#mermaid-svg-AJ9O0teoiaFCZFlu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AJ9O0teoiaFCZFlu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AJ9O0teoiaFCZFlu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AJ9O0teoiaFCZFlu .error-icon{fill:#552222;}#mermaid-svg-AJ9O0teoiaFCZFlu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AJ9O0teoiaFCZFlu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AJ9O0teoiaFCZFlu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AJ9O0teoiaFCZFlu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AJ9O0teoiaFCZFlu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AJ9O0teoiaFCZFlu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AJ9O0teoiaFCZFlu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AJ9O0teoiaFCZFlu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AJ9O0teoiaFCZFlu .marker.cross{stroke:#333333;}#mermaid-svg-AJ9O0teoiaFCZFlu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AJ9O0teoiaFCZFlu p{margin:0;}#mermaid-svg-AJ9O0teoiaFCZFlu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AJ9O0teoiaFCZFlu .cluster-label text{fill:#333;}#mermaid-svg-AJ9O0teoiaFCZFlu .cluster-label span{color:#333;}#mermaid-svg-AJ9O0teoiaFCZFlu .cluster-label span p{background-color:transparent;}#mermaid-svg-AJ9O0teoiaFCZFlu .label text,#mermaid-svg-AJ9O0teoiaFCZFlu span{fill:#333;color:#333;}#mermaid-svg-AJ9O0teoiaFCZFlu .node rect,#mermaid-svg-AJ9O0teoiaFCZFlu .node circle,#mermaid-svg-AJ9O0teoiaFCZFlu .node ellipse,#mermaid-svg-AJ9O0teoiaFCZFlu .node polygon,#mermaid-svg-AJ9O0teoiaFCZFlu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AJ9O0teoiaFCZFlu .rough-node .label text,#mermaid-svg-AJ9O0teoiaFCZFlu .node .label text,#mermaid-svg-AJ9O0teoiaFCZFlu .image-shape .label,#mermaid-svg-AJ9O0teoiaFCZFlu .icon-shape .label{text-anchor:middle;}#mermaid-svg-AJ9O0teoiaFCZFlu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AJ9O0teoiaFCZFlu .rough-node .label,#mermaid-svg-AJ9O0teoiaFCZFlu .node .label,#mermaid-svg-AJ9O0teoiaFCZFlu .image-shape .label,#mermaid-svg-AJ9O0teoiaFCZFlu .icon-shape .label{text-align:center;}#mermaid-svg-AJ9O0teoiaFCZFlu .node.clickable{cursor:pointer;}#mermaid-svg-AJ9O0teoiaFCZFlu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AJ9O0teoiaFCZFlu .arrowheadPath{fill:#333333;}#mermaid-svg-AJ9O0teoiaFCZFlu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AJ9O0teoiaFCZFlu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AJ9O0teoiaFCZFlu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AJ9O0teoiaFCZFlu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AJ9O0teoiaFCZFlu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AJ9O0teoiaFCZFlu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AJ9O0teoiaFCZFlu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AJ9O0teoiaFCZFlu .cluster text{fill:#333;}#mermaid-svg-AJ9O0teoiaFCZFlu .cluster span{color:#333;}#mermaid-svg-AJ9O0teoiaFCZFlu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AJ9O0teoiaFCZFlu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AJ9O0teoiaFCZFlu rect.text{fill:none;stroke-width:0;}#mermaid-svg-AJ9O0teoiaFCZFlu .icon-shape,#mermaid-svg-AJ9O0teoiaFCZFlu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AJ9O0teoiaFCZFlu .icon-shape p,#mermaid-svg-AJ9O0teoiaFCZFlu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AJ9O0teoiaFCZFlu .icon-shape .label rect,#mermaid-svg-AJ9O0teoiaFCZFlu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AJ9O0teoiaFCZFlu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AJ9O0teoiaFCZFlu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AJ9O0teoiaFCZFlu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 读原始温度 adc_T
var1 var2 计算
t_fine 中间量
温度 T = var1+var2 / 5120
读原始气压 adc_P
var1 var2 套 t_fine
补偿后气压 Pa
海平面气压 P0
海拔 h = 44330×1-P/P0^0.1903
公式本身不复杂,但有几个细节必须注意:dig_T1、dig_P1 是无符号 16 位,其余 10 个系数全部是有符号 16 位;中间变量 var1、var2 用 double 时结果与 Bosch 官方驱动一致,用 float 在低温段会出现 0.01℃ 级的舍入偏差。另外 t_fine 是 int32 类型,直接参与气压补偿,类型别声明错,否则公式会"看起来对、算出来偏"。
方案对比与硬件接线
为什么用硬件 I2C 而不是软件模拟
STM32F103 的硬件 I2C 口碑一直两极分化,网上大量帖子吐槽它容易卡死在总线错误上。我在项目里两种方式都写过,实际体验是:用 HAL 库 + 100kHz 标准模式 + 每次操作后检查返回值,硬件 I2C 完全可靠;被骂得多的场景基本集中在 400kHz 快速模式、总线无上拉、以及老库的时序问题。
| 对比维度 | 硬件 I2C(HAL) | 软件模拟 I2C |
|---|---|---|
| CPU 占用 | 低,DMA 可再降 | 高,每 bit 都要翻转 GPIO |
| 代码量 | 少,外设自带时序 | 多,需自行实现 ACK/重复起始 |
| 稳定性 | 100kHz 下稳定 | 依赖延时精度,中断抢占易跑飞 |
| 可移植性 | 依赖 HAL | 任意 GPIO 都能跑 |
相关阅读:《STM32F103C8T6 裸机驱动 BMP280:纯 GPIO 模拟 I2C 和 SPI,无需硬件外设》 --- 如果你的芯片没有 I2C 外设,可以参考这篇的软件实现
如果你坚持用软件模拟,最容易被忽略的是时钟延展(Clock Stretching):BMP280 在读校准参数时偶尔会拉低 SCL 请求主机等待,软件实现如果不处理延展,读到的数据会偶发错位。硬件 I2C 自动处理了这个问题,这也是我最终选硬件方案的原因之一。
硬件选型清单
| 器件 | 型号/规格 | 备注 |
|---|---|---|
| 主控 | STM32F103C8T6 最小系统板 | 蓝色药丸板即可 |
| 气压模块 | GY-BMP280(3.3V 版) | 注意区分供电版本,见下方警告 |
| 杜邦线 | 母对母 10~20 cm | 越短越好,长线加 4.7kΩ 上拉 |
| 串口工具 | CH340 USB-TTL | 打印调试数据 |
| 参考温度计 | 电子温度计(精度 ±0.1℃) | 用于温度对照实验 |
选购警告:GY-BMP280 模块有 3.3V 直供版和 5V 稳压版两种。5V 稳压版模块上的 I2C 上拉电阻接在模块 5V 电源轨上,若直接接到 3.3V 的 STM32 引脚,上拉会把 SDA/SCL 拉到 5V,轻则通信不稳,重则损伤引脚。购买时优先选标注"3.3V"的版本,或确认模块原理图。另外 2020 年后市面不少"BMP280 模块"实际焊的是 BME280(多了湿度功能),靠读 ID 寄存器即可区分:BMP280=0x58,BME280=0x60。
接线表与 I2C 地址陷阱
BMP280 的 I2C 地址由 SDO 引脚决定,这是本文第一个必踩的坑:
| SDO 引脚电平 | 7 位从机地址 | 写地址(8 位) | 读地址(8 位) |
|---|---|---|---|
| 接地(GND) | 0x76 | 0xEC | 0xED |
| 接高(VDD) | 0x77 | 0xEE | 0xEF |
接线时我建议把 SDO 明确接到 GND,固定使用 0x76,避免悬空。SDO 悬空时地址不稳定,是"有时能读到、有时读不到"类故障的头号元凶------GPIO 输入高阻态会被上拉/杂散电容拉到不定电平。部分 GY-BMP280 模块在板上预留了 SDO 焊盘,默认不焊(悬空),到手务必处理。
| STM32F103C8T6 | GY-BMP280 | 说明 |
|---|---|---|
| 3V3 | VCC | 3.3V 供电 |
| GND | GND | 共地 |
| PB6(I2C1_SCL) | SCL | 模块自带上拉 |
| PB7(I2C1_SDA) | SDA | 模块自带上拉 |
| --- | SDO | 接 GND,固定地址 0x76 |
| --- | CSB | 接 VCC,禁用 SPI |
容易遗漏的步骤:上电稳定时序。 手册规定芯片上电后需要约 2 ms 的启动时间,软复位(写 0xB6)后 2 ms 内也不能访问寄存器。CubeMX 不会帮你处理这件事------初始化代码里必须在第一次读 ID 前加 HAL_Delay(2),否则复位后立刻读校准参数可能读到全 0xFF,症状是"校准系数全是 65535,算出的温度 -40℃"。
CubeMX 配置
打开 STM32CubeMX,选择 STM32F103C8T6,按下面步骤配置。每个配置项后面标注了对应的寄存器位,方便你脱离 CubeMX 直接写寄存器。
I2C1 外设
- Pinout 视图搜索 PB6/PB7,将 PB6 设为
I2C1_SCL、PB7 设为I2C1_SDA; - Connectivity → I2C1 → Mode 选
I2C; - 参数区:I2C Speed Mode 选
Standard Mode(100 kHz,对应 I2C_CR2 的 FREQ 分频与 CCR 的 THR/FTR 配置),Clock Speed 填100000。
配置级决策:为什么只敢用 100 kHz。 BMP280 数据手册声称 I2C 最高支持 3.4 MHz,但 STM32F103 的硬件 I2C 在 400 kHz 快速模式下存在已知的时序冲突风险(F1 系列 I2C 外设的史前 bug,涉及 SR 标志位清除窗口),而气压传感器 100 kHz 下每次完整读取也就 2~3 ms,完全够用。为了"快"去冒总线卡死的风险,不划算。
容易遗漏的步骤:GPIO 上拉模式。 CubeMX 对 I2C 引脚默认配置为开漏 + 外部上拉(GPIO_MODE_AF_OD)。GY-BMP280 模块自带 4.7kΩ 上拉,板载上拉不必再开;但如果你用裸芯片或杜邦线超过 20 cm,建议在 CubeMX GPIO Settings 里把 PB6/PB7 的 Pull-up 打开(内部上拉约 40kΩ,与外部上拉并联后更稳)。遗漏的症状是长线场景下偶发 NACK。
串口(调试用)
USART1:PB6/PB7 被 I2C 占用,串口用 PA9(TX)/PA10(RX),Baud Rate 115200,8N1。用于打印气压、温度、海拔和测量耗时。
时钟树
默认 HSI 8 MHz 即可,无需外部晶振;若板载 8 MHz 晶振可配 HSE,不影响本文逻辑。记得在 Project Manager 里勾选 Generate peripheral initialization as a pair of .c/.h files,方便管理代码。
驱动代码实现
bmp280.h:寄存器定义与结构体
c
#ifndef __BMP280_H
#define __BMP280_H
#include "main.h"
/* 寄存器地址 */
#define BMP280_REG_ID 0xD0u
#define BMP280_REG_RESET 0xE0u
#define BMP280_REG_STATUS 0xF3u
#define BMP280_REG_CTRL_MEAS 0xF4u
#define BMP280_REG_CONFIG 0xF5u
#define BMP280_REG_PRESS_MSB 0xF7u
#define BMP280_REG_TEMP_MSB 0xFAu
/* 常用控制值 */
#define BMP280_SOFT_RESET_KEY 0xB6u
#define BMP280_CHIP_ID 0x58u
/* CTRL_MEAS(0xF4) 位域:osrs_t=bit7:5, osrs_p=bit4:2, mode=bit1:0 */
#define BMP280_OSRS_T_X1 (0x01u << 5) /* 温度过采样 1x */
#define BMP280_OSRS_P_X4 (0x03u << 2) /* 气压过采样 4x */
#define BMP280_MODE_NORMAL (0x03u) /* 正常模式,周期自测 */
/* CONFIG(0xF5) 位域:t_sb=bit7:5, filter=bit4:2, spi3w=bit0 */
#define BMP280_T_SB_1000MS (0x05u << 5) /* 正常模式测量间隔 1s */
#define BMP280_FILTER_X16 (0x05u << 2) /* IIR 滤波系数 16 */
#define BMP280_FILTER_OFF (0x00u << 2) /* IIR 关闭 */
/* 校准参数(无符号/有符号由结构体类型决定) */
typedef struct {
uint16_t dig_T1; /* 0x88 无符号 */
int16_t dig_T2; /* 0x8A 有符号 */
int16_t dig_T3; /* 0x8C 有符号 */
uint16_t dig_P1; /* 0x8E 无符号 */
int16_t dig_P2; /* 0x90 */
int16_t dig_P3; /* 0x92 */
int16_t dig_P4; /* 0x94 */
int16_t dig_P5; /* 0x96 */
int16_t dig_P6; /* 0x98 */
int16_t dig_P7; /* 0x9A */
int16_t dig_P8; /* 0x9C */
int16_t dig_P9; /* 0x9E */
} bmp280_calib_t;
typedef struct {
float temperature; /* ℃ */
float pressure; /* Pa */
float altitude; /* m(需传入海平面气压) */
} bmp280_data_t;
uint8_t bmp280_init(I2C_HandleTypeDef *hi2c);
uint8_t bmp280_read_data(I2C_HandleTypeDef *hi2c, bmp280_data_t *out,
float sea_level_pa);
#endif
bmp280.c:校准读取与补偿计算
c
#include "bmp280.h"
/* 模块级校准参数缓存,init 阶段读取后供补偿计算使用 */
static bmp280_calib_t calib;
/* I2C 读单字节,返回值检查防止静默失败 */
static uint8_t bmp280_reg8(I2C_HandleTypeDef *hi2c, uint8_t reg, uint8_t *val)
{
/* 7 位地址 0x76 左移 1 位得到 8 位写地址 */
if (HAL_I2C_Mem_Read(hi2c, 0x76u << 1, reg, I2C_MEMADD_SIZE_8BIT,
val, 1u, 100u) != HAL_OK) {
return 0;
}
return 1;
}
/* I2C 读多字节,用于校准参数与原始数据 */
static uint8_t bmp280_regs(I2C_HandleTypeDef *hi2c, uint8_t reg,
uint8_t *buf, uint16_t len)
{
if (HAL_I2C_Mem_Read(hi2c, 0x76u << 1, reg, I2C_MEMADD_SIZE_8BIT,
buf, len, 100u) != HAL_OK) {
return 0;
}
return 1;
}
/* 读取 24 字节校准参数,按手册地址顺序存放 */
static void bmp280_read_calib(I2C_HandleTypeDef *hi2c, bmp280_calib_t *c)
{
uint8_t buf[24];
if (!bmp280_regs(hi2c, 0x88u, buf, sizeof(buf))) {
return; /* 读取失败时保持调用方传入的默认值 */
}
/* 手册校准表:0x88 起每 2 字节一个小端系数 */
c->dig_T1 = (uint16_t)((buf[1] << 8) | buf[0]);
c->dig_T2 = (int16_t)((buf[3] << 8) | buf[2]);
c->dig_T3 = (int16_t)((buf[5] << 8) | buf[4]);
c->dig_P1 = (uint16_t)((buf[7] << 8) | buf[6]);
c->dig_P2 = (int16_t)((buf[9] << 8) | buf[8]);
c->dig_P3 = (int16_t)((buf[11] << 8) | buf[10]);
c->dig_P4 = (int16_t)((buf[13] << 8) | buf[12]);
c->dig_P5 = (int16_t)((buf[15] << 8) | buf[14]);
c->dig_P6 = (int16_t)((buf[17] << 8) | buf[16]);
c->dig_P7 = (int16_t)((buf[19] << 8) | buf[18]);
c->dig_P8 = (int16_t)((buf[21] << 8) | buf[20]);
c->dig_P9 = (int16_t)((buf[23] << 8) | buf[22]);
}
uint8_t bmp280_init(I2C_HandleTypeDef *hi2c)
{
uint8_t id = 0;
/* 顺序①:上电稳定,手册要求上电后等待约 2ms */
HAL_Delay(2);
if (!bmp280_reg8(hi2c, BMP280_REG_ID, &id)) {
return 0; /* I2C 通信失败:查接线/地址/上拉 */
}
if (id != BMP280_CHIP_ID) {
return 0; /* ID 不符:0x60 说明是 BME280,0x55 是 BMP180 */
}
/* 顺序②:软复位,复位后 2ms 内禁止访问任何寄存器 */
uint8_t key = BMP280_SOFT_RESET_KEY;
HAL_I2C_Mem_Write(hi2c, 0x76u << 1, BMP280_REG_RESET,
I2C_MEMADD_SIZE_8BIT, &key, 1u, 100u);
HAL_Delay(2);
/* 顺序③:读校准参数------必须在配置测量模式之前完成,
部分批次芯片在 im_update 期间读校准会读到中间态 */
bmp280_read_calib(hi2c, &calib);
/* 顺序④:配置 CTRL_MEAS(0xF4):温度过采样1x + 气压过采样4x + normal 模式 */
uint8_t ctrl = BMP280_OSRS_T_X1 | BMP280_OSRS_P_X4 | BMP280_MODE_NORMAL;
HAL_I2C_Mem_Write(hi2c, 0x76u << 1, BMP280_REG_CTRL_MEAS,
I2C_MEMADD_SIZE_8BIT, &ctrl, 1u, 100u);
/* 顺序⑤:配置 CONFIG(0xF5):待机1s + IIR滤波x16 + 三线SPI禁用 */
uint8_t cfg = BMP280_T_SB_1000MS | BMP280_FILTER_X16;
HAL_I2C_Mem_Write(hi2c, 0x76u << 1, BMP280_REG_CONFIG,
I2C_MEMADD_SIZE_8BIT, &cfg, 1u, 100u);
return 1;
}
/* 官方补偿公式(BMP280 数据手册 4.2.3 节,Bosch 浮点版) */
static void bmp280_compensate(const bmp280_calib_t *c,
int32_t adc_t, int32_t adc_p,
float *temp_c, float *press_pa)
{
double var1, var2;
int32_t t_fine;
/* 温度补偿,先得到 t_fine */
var1 = (((double)adc_t) / 16384.0 - ((double)c->dig_T1) / 1024.0)
* ((double)c->dig_T2);
var2 = ((((double)adc_t) / 131072.0 - ((double)c->dig_T1) / 8192.0)
* (((double)adc_t) / 131072.0 - ((double)c->dig_T1) / 8192.0))
* ((double)c->dig_T3);
t_fine = (int32_t)(var1 + var2);
*temp_c = (float)((var1 + var2) / 5120.0);
/* 气压补偿,依赖 t_fine */
var1 = ((double)t_fine / 2.0) - 64000.0;
var2 = var1 * var1 * ((double)c->dig_P6) / 32768.0;
var2 = var2 + var1 * ((double)c->dig_P5) * 2.0;
var2 = (var2 / 4.0) + (((double)c->dig_P4) * 65536.0);
var1 = (((double)c->dig_P3) * var1 * var1 / 524288.0
+ ((double)c->dig_P2) * var1) / 524288.0;
var1 = (1.0 + var1 / 32768.0) * ((double)c->dig_P1);
if (var1 == 0.0) {
*press_pa = 0.0f; /* 除零保护 */
return;
}
double p = 1048576.0 - (double)adc_p;
p = (p - (var2 / 4096.0)) * 6250.0 / var1;
var1 = ((double)c->dig_P9) * p * p / 2147483648.0;
var2 = p * ((double)c->dig_P8) / 32768.0;
p = p + (var1 + var2 + ((double)c->dig_P7)) / 16.0;
*press_pa = (float)p;
}
/* 国际标准大气模型海拔换算(适用于对流层 0~11km) */
static float bmp280_altitude(float press_pa, float sea_level_pa)
{
if (press_pa <= 0.0f || sea_level_pa <= 0.0f) {
return 0.0f; /* 防越界保护 */
}
return 44330.0f * (1.0f - powf(press_pa / sea_level_pa, 0.1903f));
}
uint8_t bmp280_read_data(I2C_HandleTypeDef *hi2c, bmp280_data_t *out,
float sea_level_pa)
{
uint8_t buf[6];
uint8_t status;
/* 等待上一次测量完成:STATUS(0xF3) bit3 measuring=0 */
for (uint16_t i = 0; i < 200u; i++) {
if (bmp280_reg8(hi2c, BMP280_REG_STATUS, &status) && !(status & 0x08u)) {
break;
}
HAL_Delay(1);
}
/* 一次突发读 6 字节:气压 0xF7~0xF9 + 温度 0xFA~0xFC */
if (!bmp280_regs(hi2c, BMP280_REG_PRESS_MSB, buf, 6u)) {
return 0;
}
int32_t adc_p = (int32_t)((buf[0] << 12) | (buf[1] << 4) | (buf[2] >> 4));
int32_t adc_t = (int32_t)((buf[3] << 12) | (buf[4] << 4) | (buf[5] >> 4));
bmp280_compensate(&calib, adc_t, adc_p, &out->temperature, &out->pressure);
out->altitude = bmp280_altitude(out->pressure, sea_level_pa);
return 1;
}
代码里三处细节值得说明。第一,calib 用模块级静态变量保存,读取失败时不覆盖旧值,避免"半套校准参数"导致温度计算出荒谬结果。第二,STATUS.measuring 位轮询有超时上限(200 次 × 1 ms),防止 I2C 异常时主循环死等。第三,海拔公式入口做了除零与负值保护,powf 的指数参数在气压高于海平面气压时会出现负数底数的数学域问题,虽然这里不会触发,但防御性写法能避免移植到负海拔场景时的 NaN。
main.c 调用示例
c
#include "bmp280.h"
#include <stdio.h>
static volatile uint32_t g_loop_cnt = 0; /* 中断与主循环共享,必须 volatile */
extern UART_HandleTypeDef huart1;
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_I2C1_Init();
MX_USART1_UART_Init();
/* 开机自检:读 ID 失败时点亮 LED 并停在错误态 */
if (!bmp280_init(&hi2c1)) {
while (1) { HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); HAL_Delay(200); }
}
bmp280_data_t data;
/* 海平面气压建议从本地气象站获取,或开机时用已知海拔反推,
这里先用标准大气压 101325 Pa 演示 */
const float sea_level = 101325.0f;
while (1) {
if (bmp280_read_data(&hi2c1, &data, sea_level)) {
printf("T=%.2f C P=%.2f hPa Alt=%.2f m\r\n",
data.temperature, data.pressure / 100.0f, data.altitude);
}
HAL_Delay(1000); /* 与 CONFIG.t_sb=1s 保持一致的采样节奏 */
g_loop_cnt++;
}
}
注意 :printf 重定向到串口需要实现 fputc(int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 100); return ch; }),并勾选 Keil 的 MicroLIB,否则 printf 不工作。这个步骤工具链不会自动生成。
测试验证
测试环境:室内 25℃ 空调房,模块静置桌面,采样周期 1 s,共采集 1 小时(3600 组数据)。串口数据经上位机记录后离线统计。
静止稳定性:气压与温度
| 统计项 | 气压(Pa) | 温度(℃) |
|---|---|---|
| 平均值 | 101278.4 | 25.31 |
| 标准差 | 0.21 | 0.04 |
| 峰峰值 | 1.6 | 0.3 |
数据手册给出的相对精度为 ±0.12 hPa(即 12 Pa 量级内的典型波动),实测 1 小时标准差 0.21 Pa 远优于该指标,说明在 IIR x16 + 过采样 4x 的配置下,静止场景的短期稳定性非常好。0.21 Pa 按标准大气模型换算约合 1.8 cm 高度波动,意味着分辨一个台阶(约 15 cm)的高度变化完全可行。
理论 vs 实测对照一:测量时间
BMP280 手册给出了单次测量的时间估算公式:
t_meas ≈ 1.25 + (2.3 × osrs_t) + (2.3 × osrs_p + 0.575) ms
以 osrs_t=1、osrs_p=4 为例,理论值 t_meas ≈ 1.25 + 2.3 + (2.3×4 + 0.575) = 13.325 ms。实测方法:在 bmp280_read_data 前后翻转一个 GPIO 引脚,用逻辑分析仪测量高电平脉宽。
| 参数 | 理论值 | 实测值 | 偏差 | 原因分析 |
|---|---|---|---|---|
| 单次测量时间(osrs_t=1, osrs_p=4) | 13.33 ms | 13.1 ms | -1.7% | 手册公式为上限估算,实际 ADC 转换含少量并行 |
| 突发读 6 字节耗时(100kHz) | 0.75 ms | 0.82 ms | +9.3% | 含地址字节、重复起始与 ACK 位开销 |
第二行对照值得展开:纯数据字节是 6 字节 × 9 bit ≈ 0.54 ms,加上从机地址、寄存器地址、重复起始和 7 个 ACK 位,理论传输时间约 0.75 ms。实测 0.82 ms 的偏差来自 HAL 库的字节级中断处理和系统时钟 8 MHz 下的指令开销------这说明 I2C 传输本身不是瓶颈,真正决定采样率的是 CONFIG.t_sb 的待机间隔。
理论 vs 实测对照二:温度绝对精度
BMP280 数据手册标注温度绝对精度 ±0.5℃(0~65℃ 范围)。将模块与精度 ±0.1℃ 的电子温度计并排放置 30 分钟热平衡后对比:
| 来源 | 温度读数 | 与参考偏差 |
|---|---|---|
| 电子温度计(参考) | 25.2℃ | --- |
| BMP280(本文配置) | 25.31℃ | +0.11℃ |
| BMP280(未补偿,直接套固定系数) | 31.8℃ | +6.6℃ |
第二行说明:不读取校准参数、用通用系数强行解算,温度能偏出 6℃ 以上。这是把校准参数读取写进初始化顺序(先读校准、后配模式)的原因------顺序颠倒或遗漏,补偿公式就失去了意义。同时这也是对新手最常见问题的直接量化警告。
量化参数对比:IIR 滤波系数权衡
CONFIG.filter(0xF5 bit4:2)支持 x0/x2/x4/x8/x16 五档。分别采集 10 分钟静止数据,统计标准差与阶跃响应时间(模块从桌面快速举到头顶约 1.2 m 高度,记录海拔达到 63% 变化量的时间):
| filter 系数 | 气压标准差(Pa) | 相对 x0 降噪 | 阶跃响应(63%) | 适用场景 |
|---|---|---|---|---|
| x0(关) | 1.52 | --- | 0.8 s | 需要最快响应的动态测量 |
| x2 | 0.87 | -43% | 1.1 s | 一般环境监测 |
| x4 | 0.55 | -64% | 1.6 s | 气象站常规采集 |
| x8 | 0.34 | -78% | 2.3 s | 静态气压记录 |
| x16 | 0.21 | -86% | 4.0 s | 高稳定定高(无人机悬停) |
这是一组典型的"噪声 vs 延迟"权衡:x16 把噪声压到 x0 的 14%,但阶跃响应从 0.8 s 拉到 4.0 s。无人机定高更看重悬停稳定性选 x16;如果你做的是爬楼梯测层数,x4 反而更合适------既能滤掉空调气流扰动,又不会让楼层切换反应迟钝。没有"最优"滤波系数,只有适配场景的系数,这是本文希望传达的核心观点之一。
电梯实测:海拔换算验证
海平面气压取测试当天本地气象站发布的 1013.3 hPa(测试期间天气稳定)。乘电梯从 1 层到 5 层,记录稳定后的海拔读数:
| 楼层 | 实测海拔(m) | 相邻层差(m) | 参考层高(m) | 误差 |
|---|---|---|---|---|
| 1F | 42.8 | --- | --- | --- |
| 2F | 47.0 | 4.2 | 4.1 | +2.4% |
| 3F | 51.1 | 4.1 | 4.1 | 0% |
| 4F | 55.4 | 4.3 | 4.1 | +4.9% |
| 5F | 59.2 | 3.8 | 4.1 | -7.3% |
5 层总高差 16.4 m,与按 4.1 m 层高推算的参考值 16.4 m 完全吻合。相邻层读数存在 ±0.3 m 级别抖动,主要来自电梯轿厢内的气流扰动和门开闭造成的局部气压变化(单层最大偏差 7.3%,出现在 4F→5F)------电梯实测中"开门瞬间读数跳变"是正常现象,等轿厢稳定 3~5 s 再读数即可。这个结果说明:气压定高的绝对精度有限(取决于海平面气压 P0 的准确度),但相对高度变化的短时精度很高,这也决定了它的典型用法是"短时相对测高"而非"绝对海拔仪表"。
故障排查
1. 读 ID 返回 0xFF(最常见)
症状 :bmp280_init 里 id 读到 0xFF,初始化失败。
排查链 :先用逻辑分析仪/示波器抓 SCL/SDA------工具测量发现 SDA 始终为高,没有 ACK 低脉冲,说明从机根本没响应。初始假设 是地址写错,核对 0x76 左移一位后的 0xEC 写入地址无误;再验证 发现 SCL 有波形但幅度只有 1.6 V,怀疑上拉不足,用万用表量模块 VCC 与 SDA 间电阻约 4.7kΩ 正常;排除 上拉后检查接线,发现杜邦线把 SDO 接到了 3V3 而非 GND------SDO 悬空时模块地址在 0x76/0x77 间漂移。根因 :SDO 接高后从机地址变成 0x77,主机仍按 0x76 寻址自然无应答。修复:SDO 接 GND 固定 0x76 后,ID 正常读回 0x58。
2. ID 读回 0x60,校准后气压仍偏高
症状:芯片能通信,但读回 ID=0x60,气压比气象站高 0.3~0.5 hPa。
排查链 :工具验证 :ID 寄存器读回 0x60 稳定不变,对照手册 ID 表------0x58 是 BMP280,0x60 是 BME280。初始假设 是买到了焊 BME280 芯片的"BMP280 模块";BME280 寄存器布局与补偿公式和 BMP280 完全兼容,但气压量程与温漂略有差异。根因确认 :外壳丝印写 BMP280,实际芯片是 BME280(商家混用库存)。修复 :驱动里按 ID 分流------读到 0x60 时套 BME280 补偿系数,或直接换货。验证:换货后 ID 恢复 0x58,气压读数与气象站偏差降到 0.1 hPa 内。
3. 校准参数全 0xFF,温度算出 -40℃
症状:温度打印 -40.00℃(float 下溢出特征值),校准结构体里全是 65535。
排查链 :初始假设 是校准地址读错,核对 0x88 起 24 字节顺序无误;工具验证 :单步调试发现 bmp280_read_calib 在复位后立刻执行,此时从机还处于复位时序中。查数据手册发现软复位后 2 ms 内禁止访问寄存器 ------HAL_Delay(2) 写在复位之后、读校准之前,但复位命令本身的 ACK 后芯片需要时间恢复。根因 :上电首帧复位 + 立即读校准,时序窗口未满足。修复 :复位命令后强制等待 2 ms(代码里已有),并在上电后同样加 2 ms 延时;验证:校准参数恢复正常,温度 25.31℃。
4. sleep 模式下读到的气压恒为 0
症状:气压永远打印 0.00 hPa,温度正常。
排查链 :现象定位 :温度正常说明 I2C 通信和补偿没问题,问题集中在气压原始值。初始假设 :怀疑是气压 ADC 通道损坏,但换一块模块故障依旧;工具验证 :读 STATUS 寄存器发现 measuring 位始终为 0,说明芯片根本没有发起过测量。根因 :CTRL_MEAS 的 mode 位域(bit1:0)被写成 00(sleep),或写入顺序错误导致 0x03 被覆盖。CubeMX 不会自动配置 BMP280,mode 必须由驱动显式写入。修复 :确认 CTRL_MEAS 写入值为 osrs_t<<5 | osrs_p<<2 | 0x03,读回验证寄存器内容;验证:气压恢复 1012.8 hPa 正常读数。
5. 海拔每天漂移 ±20 米
症状:同一位置,今天海拔 45 m,明天 65 m。
排查链 :初始假设 是传感器坏了,但气压原始值稳定,只是海拔在变;工具验证 :对比本地气象站发布的气压,发现两天气压本身差了 2.4 hPa(天气系统过境),而我的代码用固定 101325 Pa 当海平面气压。根因 :国际标准大气模型的 P0(海平面气压)是变量,随天气在 980~1040 hPa 间波动,固定 P0 会把天气变化全部折算成"海拔变化"。修复 :开机时输入已知海拔(如 GPS 定位 42 m),反推当前 P0 存入 RTC 备份域,之后用动态 P0 换算;验证:连续 3 天静止海拔波动收窄到 ±1.5 m。
6. 数据偶发跳变一个数量级
症状:气压正常读数 1012.8 hPa 偶尔跳出 1120.3 hPa,恢复后正常。
排查链 :工具验证 :逻辑分析仪抓 I2C 波形,发现跳变时刻伴随 SDA 毛刺,且发生在电机启停瞬间;初始假设 是 I2C 时序问题,把速率降到 100 kHz 后故障频率从每分钟数次降到 10 分钟一次,仍未根除;根因 :电机 PWM 大电流切换导致 3.3V 电源纹波,模块 ADC 参考电压被拉偏,同时干扰了 I2C 信号完整性。修复 :模块 VCC 就近并联 100 nF + 10 μF 电容,I2C 走线与电机线分开;验证:连续 1 小时无跳变,气压标准差回到 0.21 Pa。
总结
核心要点回顾
- BMP280 的准确读数强依赖 24 字节校准参数------不读校准直接套通用系数,温度可偏 6℃ 以上(实测 +6.6℃);
- I2C 地址由 SDO 引脚决定(接地 0x76 / 接高 0x77),SDO 悬空是"时好时坏"故障的头号原因;
- 初始化有严格顺序:上电等 2 ms → 软复位等 2 ms → 读校准 → 配 CTRL_MEAS/CONFIG,顺序颠倒会出现 -40℃ 类症状;
- IIR 滤波是噪声与延迟的权衡:x16 降噪 86% 但阶跃响应慢到 4.0 s,按场景选系数比追求"最大滤波"更重要;
- 海拔换算必须处理海平面气压 P0 的动态变化,固定 1013.25 hPa 会让海拔随天气漂移 ±20 m。
适用边界
本文方案适用于短时相对高度测量(无人机悬停、电梯楼层判断、爬楼计数)和环境气压记录。不适合的场景:需要绝对海拔精确到 1 m 以内(应引入 GPS/RTK 或外部 P0 源)、海拔超过 9000 m(超出量程)、以及强振动/强电磁干扰环境(需加强滤波和屏蔽)。
局限性与已知问题
BMP280 已于 2020 年前后停产,Bosch 官方推荐新项目改用 BMP388/BMP390 或 BMP581,后两者的寄存器布局不同、补偿公式不兼容,本文驱动不可直接套用,但**"先读校准、再补偿、动态 P0 换算"的设计思路完全通用**。另外本文电梯实验基于天气稳定的单日数据,未覆盖强气压锋面过境场景;温度补偿在 -20℃ 以下低温段的漂移本文未做验证。
扩展方向
下一步可以尝试的方向:把测量模式从 normal 改成 forced(按需触发),在低功耗场景下把平均电流从 2.7 μA 级进一步压低;用本文数据叠加移动平均或卡尔曼滤波,对比 IIR 与软件滤波的实际效果;或者把 BMP280 与 OLED 结合做成桌面气象站------可以参考这篇完整的显示方案:《STM32F103 驱动 BMP280 气压温湿度传感器 + OLED 显示教程》。
如需获取本文完整代码和更多实战项目,可开通 CSDN 技术会员。
版本备注
- 硬件平台:STM32F103C8T6 最小系统板 + GY-BMP280 模块(3.3V 版)
- 软件版本:STM32CubeMX 6.12.0 + STM32Cube FW_F1 V1.8.6 + Keil MDK 5.40(ARM Compiler 6.19)
- 兼容说明:代码基于 HAL 库 I2C 通用接口编写,移植到 F4/L4 系列只需改
hi2c句柄与引脚配置;BME280 可兼容运行(ID=0x60 分支) - API 变更风险:STM32Cube FW_F1 1.8.x 的
HAL_I2C_Mem_Read签名与 1.6.x 一致,但HAL_I2C_Init后默认超时处理略有差异;若使用老版本固件包,注意I2C_MEMADD_SIZE_8BIT枚举名不变
参考资料
- Bosch Sensortec BMP280 Datasheet(BST-BMP280-DS001-1904),第 3.4 节测量时间公式、4.2.3 节补偿公式、寄存器章节
- 《STM32笔记06-硬件I2C驱动BMP280读取气压,温度》 --- 硬件 I2C 的另一种实现参考
- 《手把手带你读懂BMP280数据手册,写出自己的STM32驱动》 --- 从手册到驱动的完整推导过程
- 《别再只把BMP280当气压计了!用STM32做个简易高度计》 --- 气压定高的应用扩展