
简介面向51单片机学习者和嵌入式开发者的BMP085/BMP280气压计读取工程资源围绕I2C通信协议完整实现传感器初始化、读取温度与气压原始数据、换算为实际物理量、结合标准大气模型估算海拔高度并利用传感器内部温度测量进行补偿。压缩包为rar格式共15个文件主要包含C语言源码、Keil工程配置文件、汇编启动文件以及编译生成的hex、obj、list等输出文件整体仅44KB结构轻量便于直接对照学习或二次修改。目前已有813人学习。读者通过该工程可快速掌握I2C时序操作与传感器数据处理流程获得可直接烧录的hex文件参照源码可以系统梳理寄存器配置、数据转换、海拔计算等关键步骤同时对比BMP085与BMP280在寄存器和控制逻辑上的差异为后续传感器选型与电路调试提供实用参考。1. 51 读气压计不是换颗 BMP280 那么简单实际项目里用一个 51 单片机读 BMP085 气压计一直是低成本气象站和高度计的标准做法I2C 读取校准系数、触发一次转换、再用官方整数补偿公式就能得到温度和气压。但 BMP085 已经停产多年很多人最近都在把老代码往 BMP280 上平移却发现这颗“继承者”并不是地址兼容就能跑。BMP280 的控制寄存器、校准系数存放位置、数据字节长度和最终计算公式全都不一样直接替换连读数都会是零或者乱码。这篇从 51 单片机读取 BMP085/BMP280 气压计的完整链路讲起把 I2C 底层驱动、两个型号的读取代码、整数校准公式和换型时的验证技巧一次说明白。不管你是 Proteus 仿真还是实物调试顺着这条路都能走通。2. 读取 BMP085 / BMP280 前先分清 I2C 地址和寄存器差异2.1 SDO 引脚决定 BMP280 是 0x76 还是 0x77BMP085 和 BMP280 都是 7 位 I2C 地址。BMP085 的地址固定在 0x77硬件上没有更改手段。BMP280 多了一个 SDO 引脚这颗引脚的电平决定从机地址SDO 接 VCC 时地址是 0x77SDO 接地时地址是 0x76。板子上看到的 BMP280 模块默认地址往往已经由 SDO 的上拉或者下拉电阻固定多数做成了 0x76但并不能保证。所以代码里不要写死优先用宏定义再在初始化后读一下状态寄存器 0xF3能从机正常回 ACK说明地址找对了。I2C 总线上发送地址时要左移一位最低位是读写方向。读取 BMP085 温度原始值时命令序列是 Start 0xEE0x77 写寄存器地址 Repeat Start 0xEF0x77 读数据 Stop。如果 BMP280 地址是 0x76对应写地址就是 0xEC读地址 0xED。这个细节在 Proteus 仿真时特别容易忽略因为仿真里不接 SDO默认地址是固定的真实板子却可能完全不同。2.2 寄存器地图对照BMP085 和 BMP280 不是一个套路功能BMP085BMP280设备地址0x77 固定0x76 或 0x77校准系数0xAA-0xBF共 22 字节0x88-0x9F共 24 字节状态寄存器无靠延时等待0xF3含 measuring 位测量控制0xF40xF4、0xF5温度原始值0xF6-0xF716 位0xFA-0xFC20 位气压原始值0xF6-0xF819 位0xF7-0xF920 位BMP085 的 0xF6 地址既用来读温度也用来读压力读压力时还要根据 OSS 过采样设置去读 2 到 3 个字节。BMP280 则是连续寄存器 0xF7 到 0xFC把压力 MSB、LSB、XLSB 和温度 MSB、LSB、XLSB 排在一起一次连续读 6 个字节就能同时拿到两组原始值。这个差异直接决定了读取函数不能复用。BMP085 的校准系数里AC1、AC2、AC3 是有符号数AC4、AC5、AC6 是无符号数后面 B1、B2、MB、MC、MD 又全是有符号数。BMP280 的校准是 12 个 16 位值从 0x88 开始的顺序依次是 dig_T1、dig_T2、dig_T3然后 dig_P1 到 dig_P9。结构体看着差不多但排列顺序和有无符号不一样迁移 C 代码时不能照搬结构体。2.3 51 模拟 I2C底层四个函数加一个阻塞读块51 单片常见型号像 STC89C52、AT89S52 没有硬件 I2C主流做法是用 GPIO 模拟。底层需要实现 Start、Stop、SendByte、RecvByte 和两个应答函数。下面这一份最小实现在 12MHz 晶振、6 时钟模式下_nop_()延时约 1usI2C 时钟约 100kHz满足 BMP280 标准模式要求。#include reg51.h #include intrins.h sbit SCL P1^0; sbit SDA P1^1; void I2C_Delay(void) { _nop_(); _nop_(); _nop_(); _nop_(); } void I2C_Start(void) { SDA 1; SCL 1; I2C_Delay(); SDA 0; I2C_Delay(); SCL 0; } void I2C_Stop(void) { SDA 0; SCL 1; I2C_Delay(); SDA 1; } void I2C_SendByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { SDA dat 0x80; dat 1; SCL 1; I2C_Delay(); SCL 0; } } unsigned char I2C_RecvByte(void) { unsigned char i, dat 0; SDA 1; for (i 0; i 8; i) { SCL 1; I2C_Delay(); dat (dat 1) | SDA; SCL 0; I2C_Delay(); } return dat; } bit I2C_GetAck(void) { bit ack; SDA 1; SCL 1; I2C_Delay(); ack SDA; SCL 0; return ack; } void I2C_SendAck(bit ack) { SDA ack; SCL 1; I2C_Delay(); SCL 0; SDA 1; }这段代码有四个点需要额外说明。第一I2C_GetAck()读到的 0 表示从机应答1 表示非应答判断时不要写反。第二I2C_SendAck(0)表示主设备继续读下一字节I2C_SendAck(1)表示读最后一个字节后回非应答。第三SDA 释放时必须先SDA 1否则从机拉不动总线。第四如果 Keil 报SDA dat 0x80的类型警告可以改成SDA dat 0x80 ? 1 : 0不影响逻辑。通用读寄存器函数建议封装成“Start 写地址和寄存器再 Repeat Start 读地址读一字节后回 NACK”。这里必须用 Repeat Start不要让 Stop 再 Start否则一些 I2C 从设备会直接丢掉刚才选中的寄存器指针。unsigned char I2C_ReadReg(unsigned char dev_addr, unsigned char reg) { unsigned char dat; I2C_Start(); I2C_SendByte(dev_addr 1); I2C_GetAck(); I2C_SendByte(reg); I2C_GetAck(); I2C_Start(); I2C_SendByte((dev_addr 1) | 0x01); I2C_GetAck(); dat I2C_RecvByte(); I2C_SendAck(1); I2C_Stop(); return dat; }这里的dev_addr传入 7 位地址例如 0x77。函数内部左移一位后变成 0xEE读地址变成 0xEF。如果调用时直接传 0xEE最终发送地址会被左移成 0xDC整个 I2C 通讯全错。这是移植别人代码时最容易犯的错51、STM32、Arduino 的驱动文件里都见过。3. 51 单片机读取 BMP085校准系数、寄存器与一次完整换算3.1 读取 22 字节校准系数并正确解释符号BMP085 出厂时已经在内部 OTP 里写好了 11 个 16 位校准值每次上电必须重新读取。读取利用 I2C 连续读 22 字节然后按顺序拆到结构体。AC1 到 AC3 用signed shortAC4 到 AC6 用unsigned shortB1、B2、MB、MC、MD 都是signed short。这里最坑的是 AC4 到 AC6如果用int定义后面温度公式会把无符号数当成有符号数整个计算结果直接发散。typedef struct { short ac1, ac2, ac3; unsigned short ac4, ac5, ac6; short b1, b2, mb, mc, md; } BMP085_CAL_t; BMP085_CAL_t bmp085_cal; void BMP085_ReadCalibration(void) { unsigned char buf[22]; unsigned char i; BMP085_ReadBlock(0xAA, buf, 22); bmp085_cal.ac1 (short)((buf[0] 8) | buf[1]); bmp085_cal.ac2 (short)((buf[2] 8) | buf[3]); bmp085_cal.ac3 (short)((buf[4] 8) | buf[5]); bmp085_cal.ac4 (unsigned short)((buf[6] 8) | buf[7]); bmp085_cal.ac5 (unsigned short)((buf[8] 8) | buf[9]); bmp085_cal.ac6 (unsigned short)((buf[10] 8) | buf[11]); bmp085_cal.b1 (short)((buf[12] 8) | buf[13]); bmp085_cal.b2 (short)((buf[14] 8) | buf[15]); bmp085_cal.mb (short)((buf[16] 8) | buf[17]); bmp085_cal.mc (short)((buf[18] 8) | buf[19]); bmp085_cal.md (short)((buf[20] 8) | buf[21]); }buf必须是无符号数组。如果定义了char buf[22]当 buf[0] 最高位是 1 时左移 8 位会先发生符号扩展后面的或运算拿到的字节序就是错的。Keil C51 下这种错不报编译警告查起来很痛苦。BMP085_ReadBlock 是 2.3 节 I2C 底层之上的批量读函数通用实现是发设备地址、寄存器地址再从 0xAA 开始连续读 22 字节最后一字节回 NACK。3.2 写 0xF4 控制寄存器温度 0x2E气压 0x34 到 0xF4BMP085 的测量控制寄存器是 0xF4。写 0x2E 触发温度测量写 0x34、0x74、0xB4、0xF4 触发气压测量对应 OSS 过采样精度 0 到 3。触发后不能立刻读结果必须等待转换完成。BMP085 虽然有 EOC 引脚但很多模块没有引出51 端最常用的方式就是延时。void BMP085_StartTempMeasurement(void) { BMP085_WriteReg(0xF4, 0x2E); } void BMP085_StartPressureMeasurement(unsigned char oss) { BMP085_WriteReg(0xF4, 0x34 (oss 6)); }0x34 (oss 6)这个表达式分别得到 0x34、0x74、0xB4、0xF4和手册一致。注意 oss 必须限制在 0 到 3如果传入 4 以上控制字会变成 0x34 0x40 的组合触发指令无效。温度测量不受 OSS 影响所以每次都固定写 0x2E。3.3 温度压力整数补偿b5 是温度到气压的桥梁BMP085 的温度补偿会产生一个中间量 b5气压补偿也要复用 b5所以需要把 b5 保存到全局变量。下面这段代码是官方补偿公式的 51 整数实现中间变量全部用 longKeil C51 编译后运行时间会长一点但 1Hz 采样频率下完全够用。long BMP085_b5; unsigned short BMP085_GetTemperature(unsigned short ut) { long x1, x2; x1 ((long)ut - bmp085_cal.ac6) * bmp085_cal.ac5 15; x2 ((long)bmp085_cal.mc 11) / (x1 bmp085_cal.md); BMP085_b5 x1 x2; return (unsigned short)((BMP085_b5 8) 4); } unsigned long BMP085_GetPressure(unsigned long up, unsigned char oss) { long x1, x2, x3, b3, b6, p; unsigned long b4, b7; b6 BMP085_b5 - 4000; x1 (long)bmp085_cal.b2 * (b6 * b6 12) 11; x2 (long)bmp085_cal.ac2 * b6 11; x3 x1 x2; b3 ((((long)bmp085_cal.ac1 * 4 x3) oss) 2) 2; x1 (long)bmp085_cal.ac3 * b6 13; x2 (long)bmp085_cal.b1 * (b6 * b6 12) 16; x3 (x1 x2 2) 2; b4 bmp085_cal.ac4 * (unsigned long)(x3 32768) 15; b7 ((unsigned long)(up - b3)) * (50000 oss); if (b7 0x80000000) p (b7 1) / b4; else p (b7 / b4) 1; x1 (p 8) * (p 8); x1 x1 * 3038 16; x2 -7357 * p 16; p p ((x1 x2 3791) 4); return (unsigned long)p; }这段代码有三个需要说明的参数。温度函数返回的是 0.1 摄氏度比如数值 247 表示 24.7°C显示时用整数除法分离整数和小数不要在 51 上直接除以 10.0能省点内存是一点。气压函数的返回值单位是 Pa不是 hPa。101325 Pa 等于 1013.25 hPa如果驱动里直接除 1000会把标准大气压显示成 101.325单位就错了。最后一个细节是b7 0x80000000并不是比较负数而是判断无符号数的高位用于区分官方公式里正负分支不要“优化”成有符号比较。3.4 OSS 等待时间与精度选择BMP085 的 OSS 值直接决定 ADC 转换时间数据手册给出的典型转换时间如下。代码里的延时要比手册多留一点尤其在 Keil 优化等级较高时空循环可能被优化掉。OSS控制字采样次数转换时间00x341 次4.5ms10x742 次7.5ms20xB44 次13.5ms30xF48 次25.5ms实际项目里51 单片机做室内气压采集用 OSS1 就够了功耗和等待时间比较均衡。如果想做高度计建议 OSS2。海拔每升高 8 米气压大约变化 100PaOSS0 时读数抖动可能到十几 Pa高度显示会跳得很厉害。OSS3 的采集频率大概 30Hz在 51 上收益不大除非电池供电才需要压噪声。4. 51 单片机读取 BMP280强制模式、数据寄存器和精确补偿4.1 校准数据0x88 开始连续 24 字节BMP280 的校准系数和 BMP085 完全不是一个布局。0x88 地址开始依次是 dig_T1、dig_T2、dig_T3、dig_P1 到 dig_P9共 12 个 16 位值合计 24 字节。BMP280 没有湿度传感器不需要读 BME280 才有的 0xE1 补校数据读到 0x9F 就可以。typedef struct { unsigned short dig_T1; signed short dig_T2; signed short dig_T3; unsigned short dig_P1; signed short dig_P2, dig_P3, dig_P4; signed short dig_P5, dig_P6, dig_P7; signed short dig_P8, dig_P9; } BMP280_CAL_t; BMP280_CAL_t bmp280_cal; void BMP280_ReadCalibration(void) { unsigned char buf[24]; BMP280_ReadBlock(0x88, buf, 24); bmp280_cal.dig_T1 (unsigned short)((buf[0] 8) | buf[1]); bmp280_cal.dig_T2 (signed short)((buf[2] 8) | buf[3]); bmp280_cal.dig_T3 (signed short)((buf[4] 8) | buf[5]); bmp280_cal.dig_P1 (unsigned short)((buf[6] 8) | buf[7]); bmp280_cal.dig_P2 (signed short)((buf[8] 8) | buf[9]); bmp280_cal.dig_P3 (signed short)((buf[10] 8) | buf[11]); bmp280_cal.dig_P4 (signed short)((buf[12] 8) | buf[13]); bmp280_cal.dig_P5 (signed short)((buf[14] 8) | buf[15]); bmp280_cal.dig_P6 (signed short)((buf[16] 8) | buf[17]); bmp280_cal.dig_P7 (signed short)((buf[18] 8) | buf[19]); bmp280_cal.dig_P8 (signed short)((buf[20] 8) | buf[21]); bmp280_cal.dig_P9 (signed short)((buf[22] 8) | buf[23]); }注意 BMP280 的温度校准只有 dig_T1 是无符号其他全部是有符号压力校准只有 dig_P1 无符号dig_P2 到 dig_P9 有符号。这和 BMP085 的结构体顺序不同直接粘贴旧代码后结果会离谱。读取 BMP280 校准系数时设备地址宏要写成#define BMP280_ADDR 0x77如果模块上 SDO 接地就改成 0x76。4.2 控制寄存器 0xF4forced 模式更适合 51 的单次采集BMP280 的 0xF4 寄存器可以同时设置气压过采样 osrs_p、温度过采样 osrs_t 和模式。因为 51 单片机没有硬件 I2C也不像 STM32 那样适合跑复杂状态机所以不建议用 normal 模式最好用 forced 模式写一次 0xF4 触发一次测量读完成后再触发下一次。这样采样频率可以由程序控制调试时也能清楚看到每个测量周期。#define BMP280_ADDR 0x77 #define BMP280_REG_CTRL_MEAS 0xF4 #define BMP280_REG_STATUS 0xF3 #define BMP280_REG_DATA 0xF7 void BMP280_Trigger(void) { // osrs_p010, osrs_t001, mode01 forced BMP280_WriteReg(BMP280_REG_CTRL_MEAS, 0x45); } bit BMP280_IsMeasuring(void) { unsigned char status I2C_ReadReg(BMP280_ADDR, BMP280_REG_STATUS); return (status 0x09) ? 1 : 0; }0x45 的二进制是 01000101拆开看就是气压过采样 x2温度过采样 x1模式为 forced。如果你的应用需要更高精度把 osrs_p 改成 101再给温度也设成 oversampling x2控制字会变成 0x55如果不想纠结每一位直接使用(5 5) | (1 2) | 1这种写法含义比魔数清楚。读状态寄存器时0xF3 的 bit3 是 measuringbit0 是 im_update定时器里有一直为 1 的现象所以上面用 0x09 同时等两个状态位落下。4.3 20 位原始数据拼接与整数补偿BMP280 的数据寄存器从 0xF7 开始0xF7、0xF8、0xF9 是压力0xFA、0xFB、0xFC 是温度。每个量都是 20 位用 6 字节一次连续读取。void BMP280_ReadRaw(unsigned long *pressure, unsigned long *temperature) { unsigned char d[6]; BMP280_ReadBlock(BMP280_REG_DATA, d, 6); *pressure ((unsigned long)d[0] 12) | ((unsigned long)d[1] 4) | ((unsigned long)d[2] 4); *temperature ((unsigned long)d[3] 12) | ((unsigned long)d[4] 4) | ((unsigned long)d[5] 4); }注意拼接顺序。20 位 ADC 值实际上是把 18 位结果左对齐到 20 位所以 MSB 左移 12 位中间那个字节左移 4 位最后 XLSB 只取高 4 位丢掉低 4 位噪声。有些移植程序直接左移 8 位加两个字节最后算出来的气压低 256 倍正是漏看了 4。温度补偿和气压补偿的官方整数公式比较复杂。在 51 上建议分两个函数实现中间量 t_fine 用全局变量因为计算气压必须用到温度补偿后的 t_fine不能先算气压再算温度。signed long bmp280_t_fine; signed long BMP280_CompensateTemp(signed long adc_T) { signed long var1, var2, T; var1 ((((adc_T 3) - ((signed long)bmp280_cal.dig_T1 1)) * (signed long)bmp280_cal.dig_T2) 11); var2 (((((adc_T 4) - (signed long)bmp280_cal.dig_T1)) * ((adc_T 4) - (signed long)bmp280_cal.dig_T1)) 12); var2 (var2 * (signed long)bmp280_cal.dig_T3) 14; bmp280_t_fine var1 var2; T (bmp280_t_fine * 5 128) 8; return T; } unsigned long BMP280_CompensatePressure(signed long adc_P) { signed long var1, var2; unsigned long p; var1 (bmp280_t_fine 1) - 64000; var2 (((var1 2) * (var1 2)) 11) * (signed long)bmp280_cal.dig_P6; var2 var2 ((var1 * (signed long)bmp280_cal.dig_P5) 1); var2 (var2 2) ((signed long)bmp280_cal.dig_P4 16); var1 ((((signed long)bmp280_cal.dig_P3 * (((var1 2) * (var1 2)) 13)) 3) (((signed long)bmp280_cal.dig_P2 * var1) 1)) 18; var1 ((((signed long)32768 var1) * (signed long)bmp280_cal.dig_P1) 15); if (var1 0) { return 0; } p (((unsigned long)((((signed long)1048576) - adc_P) - (var2 12)))) * 3125; if (p 0x80000000) { p (p 1) / (unsigned long)var1; } else { p (p / (unsigned long)var1) * 2; } var1 ((signed long)bmp280_cal.dig_P9 * (signed long)(((p 3) * (p 3)) 13)) 12; var2 ((signed long)(p 2) * (signed long)bmp280_cal.dig_P8) 13; p (unsigned long)((signed long)p ((var1 var2 (signed long)bmp280_cal.dig_P7) 4)); return p; }这段代码里温度函数返回值是 0.01°C比如 2536 就是 25.36°C。气压函数返回值是 Pa单位换算和 BMP085 一样。移植时最容易写错的是 var2 的第二个乘法少打一个括号都会让中间量溢出51 的 long 是 32 位中间计算尽量避免先乘后移位否则 Keil 可能插入临时量导致代码变大。4.4 一个完整采样循环的调用顺序BMP280 用 forced 模式时正确的调用顺序是上电等待 10ms读校准系数然后每采集一次都执行“触发 等待完成 读原始值 补偿”不能只触发一次然后在循环里反复读否则读到的永远是上一次的转换结果。void BMP280_ReadAll(unsigned long *p, unsigned long *t) { unsigned long raw_p, raw_t; BMP280_Trigger(); while (BMP280_IsMeasuring()); BMP280_ReadRaw(raw_p, raw_t); *t (unsigned long)BMP280_CompensateTemp((signed long)raw_t); *p BMP280_CompensatePressure((signed long)raw_p); }BMP280_IsMeasuring()里加了0x09的状态判断。有些 BMP280 模块上电后第一次读校准时会因为 NVM 忙而返回状态异常所以调用BMP280_ReadCalibration之前最好先延时 100ms不要一复位就去读。真实板子上如果卡在while (BMP280_IsMeasuring())优先检查地址、SDA/SCL 上拉电阻再看 0xF3 寄存器是否一直被拉高。5. 从 BMP085 换到 BMP280 的 4 个踩坑点与验证方法5.1 电平范围51 的 5V 和 BMP280 的 3.3V 不能直连BMP280 是 3.3V 器件SDA 和 SCL 耐受一般不超过 VDD0.3V。很多 51 开发板是 5V 供电P1 和 P3 口上拉后输出高电平接近 5V直接接 BMP280 可能在长时间运行后损坏传感器。常见做法是给 BMP280 单独接 3.3V LDOSDA 和 SCL 用 I2C 电平转换板或者用两个 MOS 管做双向转换。如果只是调试可以用 1k 电阻串联到 51 的 P1 口再把 SDA/SCL 上拉到 3.3V但这样不能保证所有器件都稳定正式项目还是建议加电平转换。5.2 换型后先抓 ACK再看数据从 BMP085 换到 BMP280不要直接改完代码就读数。先用逻辑分析仪抓 SDA 和 SCL 两个通道采样率设 1MHz 以上检查从机地址字节之后的 ACK 位是不是低电平。如果抓到的地址是 0xDC 而不是 0xEE说明调用时把 7 位地址和 8 位写地址混用了。如果地址正确但没有 ACK先查 SDO 引脚和电源。Proteus 仿真里换 BMP280 后还要重新设置器件地址仿真默认有时是 0x76和代码不一致时一样没有 ACK。5.3 气压读数滤波一阶滞后加 5 次滑动平均51 单片机读 BMP280 的原始计算值本身已经很稳定但 I2C 读取过程中偶尔会出现一个跳变值。现象是气压值偶尔从 101325 跳到 101390持续一个采样周期后又恢复。最简单的处理是在采样循环里做一个 5 次滑动平均同时限制单次变化量超过 30Pa 的样本直接丢弃。#define BMP_FILTER_N 5 unsigned long bmp_filter_buf[BMP_FILTER_N]; unsigned char bmp_filter_index; unsigned long BMP_Filter(unsigned long new_value) { unsigned char i; unsigned long sum 0; bmp_filter_buf[bmp_filter_index] new_value; bmp_filter_index (bmp_filter_index 1) % BMP_FILTER_N; for (i 0; i BMP_FILTER_N; i) { sum bmp_filter_buf[i]; } return sum / BMP_FILTER_N; }这个滤波算法在 51 上占用的 RAM 很小5 个 unsigned long 一共 20 字节。滤波后如果还要做高度显示用简化大气方程alt (1 - pow(p / 101325.0, 0.1903)) * 44330但 51 上不要直接调用 pow可以查表或者用定点近似。做高度计的时候BMP280 的温度对气压板有热影响尽量避免把传感器放在 MCU 正上方给传感器留一点透气空间精度比换更高端的传感器更明显。本文还有配套的精品资源点击获取