STM32F103 I2C 驱动 BMP280 气压温度传感器实战:校准补偿公式拆解、IIR 滤波整定与海拔换算实测

文章目录

摘要

无人机定高、气象站和室内导航都依赖气压传感器,但 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 后数据稳了,结果爬楼梯时高度变化慢得不像话。这些经验都会在本文的实测数据和故障排查里展开。

读完本文,你将掌握:

  1. BMP280 的寄存器体系与 24 字节校准参数的正确读取方法;
  2. 官方补偿公式的逐项含义与代码落地;
  3. CubeMX 配置中每个选项对应的寄存器位;
  4. 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 外设

  1. Pinout 视图搜索 PB6/PB7,将 PB6 设为 I2C1_SCL、PB7 设为 I2C1_SDA;
  2. Connectivity → I2C1 → Mode 选 I2C;
  3. 参数区: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。

总结

核心要点回顾

  1. BMP280 的准确读数强依赖 24 字节校准参数------不读校准直接套通用系数,温度可偏 6℃ 以上(实测 +6.6℃);
  2. I2C 地址由 SDO 引脚决定(接地 0x76 / 接高 0x77),SDO 悬空是"时好时坏"故障的头号原因;
  3. 初始化有严格顺序:上电等 2 ms → 软复位等 2 ms → 读校准 → 配 CTRL_MEAS/CONFIG,顺序颠倒会出现 -40℃ 类症状;
  4. IIR 滤波是噪声与延迟的权衡:x16 降噪 86% 但阶跃响应慢到 4.0 s,按场景选系数比追求"最大滤波"更重要;
  5. 海拔换算必须处理海平面气压 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 枚举名不变

参考资料

相关推荐
索端阳1 小时前
I.MX6ULL 裸机开发:LCD Framebuffer 与 RGB888 学习笔记
笔记·vscode·嵌入式硬件·学习
Amos zhang1 小时前
AWTK 开发实践(三):AMT630HV100 SDK 移植调用链分析
单片机·嵌入式·awtk
数字新视界1 小时前
国产动环系统技术原理与应用实践的深入剖析
嵌入式硬件·物联网·系统架构·机房管理·动力与环境监控系统
新思维软件2 小时前
基于单片机的激光防盗报警器语音报警系统:STM32 激光围栏 + ESP32-CAM 图像留痕全链路实现
stm32·单片机·物联网·物联网开发
恶魔泡泡糖2 小时前
stm32F103C8T6标准库串口接收之控制LED灯3
stm32·单片机·嵌入式硬件
新思维软件2 小时前
基于无线传输的地衡系统:三节点 LoRa 组网与 MQTT 上云的分布式称重方案
分布式·stm32·单片机·嵌入式硬件·物联网·物联网开发
芯祥联2 小时前
STM32 上跑 MQTT 怎么选?嵌入式 MQTT 客户端 C 语言实现对比
c语言·stm32·单片机·嵌入式硬件·网络协议·开源·broker
Brilliantwxx11 小时前
【STM32】 NVIC 与 EXTI 中断 · 超详细源码级解析
stm32·单片机·嵌入式硬件
加载力科技12 小时前
硬盘咔咔响是坏盘信号!赶在数据丢失前克隆换盘:不重装系统、不丢文件
windows·嵌入式硬件·系统安全·策略模式·命令模式·硬盘