简介一套以STM32单片机为核心、BMP180气压传感器为主角的测量方案工程包面向嵌入式初学者与电子设计竞赛备赛者可完整演示气压、海拔高度与温度的采集、换算和显示流程。包内共406个文件以HAL库源码h/c、编译输出hex/axf、Keil工程uvprojx、Proteus仿真pdsprj为主另含原理图、PDF文档与WMV演示视频整体约30.52MB结构清晰便于对照学习。已有353人学习或下载。其中大量.h/.c源文件覆盖传感器驱动与显示驱动Proteus工程可直接运行便于无硬件环境时先行验证。资料提供从传感器校准、I2C读写到大气压海拔公式换算的完整代码路径并能对照Proteus仿真电路验证LCD显示、主控引脚接线和调试过程配套文档与演示视频进一步降低了上手指引难度适合作为课程设计或毕业设计的基础模板也适合逐步对照练习。1. 从“量个温度”到“算准海拔”这套STM32BMP180仿真资料到底能练什么海拔这个数在户外手表、航模飞控、气象站里都会出现但STM32内部没有海拔寄存器BMP180给出的也不是现成的海拔——它只能测气压海拔要靠气压换算公式一步步推出来。这套基于STM32的BMP180气压计海拔高度温度测量Proteus仿真资源把I2C驱动、传感器补偿、气压转海拔、仿真联调整条链路都打包在源码和仿真工程里你做完它不只是得到一组温度和海拔数字而是把“传感器原始数据→补偿计算→工程化显示”这条完整能力练了一遍。适合正在做课程设计、想低成本验证传感器逻辑再上真机的工程师。仿真器里跑通了移植到板子上会少踩很多坑。2. BMP180测量原理原始数据如何变成可用的气压和海拔BMP180直接读出来的并不是温度和气压是两个叫UT和UP的原始值。UT是未补偿的温度原始值UP是未补偿的气压原始值不经过补偿直接用温度会偏几摄氏度、气压偏差甚至超过几十百帕海拔更没法看。这章先把芯片内部的测量管道和补偿公式讲清楚源码里那些看着像魔法的移位和乘加算式就有了依据。2.1 测量管道从寄存器配置到UT/UP原始值BMP180内部由压力感应单元、温度感应单元、ADC和EEPROM四部分组成。EEPROM里存放11个出厂校准系数地址从0xAA到0xBF共22字节。每次测量流程分两步先往控制寄存器0xF4写转换命令等芯片转换完成再从结果寄存器读出原始值。下面这张寄存器表是驱动代码的基础。寄存器地址名称说明0xF4控制寄存器写入转换命令选择测温度还是测气压0xF6结果寄存器MSB转换结果高字节0xF7结果寄存器LSB转换结果低字节0xF8结果寄存器XLSB气压测量比温度测量多出的低字节0xAA - 0xBF校准EEPROM22字节正好放11个16位系数控制寄存器写0x2E启动温度转换写0x34加上过采样位启动气压转换。BMP180的加压ADC支持4档过采样率oss命令值和转换时间如下表。oss转换时间功耗适用场景04.5 ms最低快速刷新场景17.5 ms低常规测量213.5 ms中多数场景的折中选择325.5 ms高高精度、低刷新率场景温度结果只有16位读0xF6和0xF7两个字节就够了。气压结果最多20位要读0xF6、0xF7、0xF8三个字节其中XLSB的低3位在oss为0时是无效位需要右移丢弃。很多人第一次写驱动时忽略了这个细节把多余的位也拼进UP参与换算算出来的气压值会差一大截。2.2 补偿公式BMP180厂家给的转换链路为什么要补偿压阻式传感器的输出会随温度明显漂移BMP180出厂时把温漂和非线性系数烧录进EEPROM软件上电后读出来参与计算。温度补偿的核心是算出中间变量B5气压补偿再拿B5和B6做第二组修正。公式看起来复杂本质就是几组乘加和移位运算全部是定点整数计算不需要浮点很适合不带FPU的STM32F103。typedef struct { int16_t ac1, ac2, ac3; uint16_t ac4, ac5, ac6; int16_t b1, b2; int16_t mb, mc, md; } bmp180_calib_t; static int32_t g_b5 0; // 输入UT原始值返回温度值单位0.1℃同时更新g_b5供气压计算使用 int32_t bmp180_get_temperature(int32_t ut, bmp180_calib_t *c) { int32_t x1 ((ut - c-ac6) * (int32_t)c-ac5) 15; int32_t x2 ((int32_t)c-mc 11) / (x1 c-md); g_b5 x1 x2; return (g_b5 8) 4; }逻辑说明x1先把UT减去AC6零点偏移再用AC5放大x2修正高温区的非线性B5是两者之和。返回值的单位是0.1℃比如要显示26.5℃时这个函数返回265。g_b5是全局中间量气压补偿公式还要再次用到B5所以必须把它从温度函数里带出来这是很多例程里用全局变量的原因。// 输入UP原始值、B5和oss返回气压值单位0.1Pa int32_t bmp180_get_pressure(int32_t up, int32_t b5, int32_t oss, bmp180_calib_t *c) { int32_t b6, x1, x2, x3, b3, p; uint32_t b4, b7; b6 b5 - 4000; x1 ((int32_t)c-b2 * ((b6 * b6) 12)) 11; x2 ((int32_t)c-ac2 * b6) 11; x3 x1 x2; b3 ((((int32_t)c-ac1 * 4 x3) oss) 2) 2; x1 ((int32_t)c-ac3 * b6) 13; x2 ((int32_t)c-b1 * ((b6 * b6) 12)) 16; x3 ((x1 x2) 2) 2; b4 (c-ac4 * (uint32_t)(x3 32768)) 15; b7 ((uint32_t)up - (uint32_t)b3) * (50000 oss); p (int32_t)((b7 * 2) / b4); if (p 101110) { p p 2 * ((int32_t)((p - 101110) * (p - 101110) 16)) 1; } return p; // 单位0.1Pa除以1000就是hPa }逻辑说明B5先减去4000得到B6后面所有气压补偿都以B6为基准。B3、B4、B7是从校准系数反推的中间量最后p就是0.1Pa单位的真实气压。当气压超过约10111.0 hPa时需要做二次修正因为此时传感器输出呈现轻微非线性芯片手册明确要求补这一段。oss出现在B3和50000的右移里原因是不同oss下UP的位数不同需要按同样的比例缩回到统一尺度。2.3 海拔换算气压高度公式与本地气压校准拿到气压之后用国际标准大气的压力高度公式换算海拔。这个公式假设海平面气压P0为1013.25 hPa海平面温度15℃它不是一个纯几何高度而是“气压高度”但在地面附近已经足够工程使用。#include math.h // 输入当前气压p(hPa)和参考海平面气压p0(hPa)返回海拔单位米 float bmp180_altitude(float p, float p0) { return 44330.77f * (1.0f - powf(p / p0, 1.0f / 5.255f)); }参数说明p0是本地海平面气压不是随便填的1013.25。如果做课程设计只验证功能用1013.25没问题如果真要看现场海拔要把p0设成当地气象站发布的实时海平面气压否则系统误差能到几十米。还有一个常见场景是测相对高度比如从一楼到六楼的楼层差那就在一楼读一次气压作为p0之后所有海拔输出都是相对一楼的差值影响零漂的是气压短时波动而不是海平面基准这样反而更准。气压与高度的换算关系大致是气压每差1 hPa对应约8.4米高度差。这个数值决定了海拔对气压噪声很敏感第5章里会专门讲滤掉这种抖动的方法。3. STM32裸机I2C驱动BMP180的数据从底层读上来寄存器配置和公式看明白之后真正要动手写的核心就是I2C通信。这一章把驱动拆成几个模块从引脚选择到主循环调度逐步落地。3.1 硬件I2C还是模拟I2CProteus仿真下的选择STM32F103内置了硬件I2C外设代码量更少但要处理EV5、EV7这些事件标志还需要状态机轮询或中断配合。Proteus的模型对硬件I2C时序的模拟与真实硅片存在差异仿真里经常出现读回全0xFF、或者卡在等待标志的玄学问题。相比之下模拟I2C用普通GPIO翻转来产生时序一切由代码控制在Proteus里稳定移植到真实MCU也不需要改。我一般建议这个场景直接上模拟I2C所以SCL和SDA要选普通GPIO。常用引脚是PB6和PB7配置为开漏输出外部各接一个4.7kΩ上拉到3.3V。开漏输出是为了符合I2C总线协议总线空闲时被上拉电阻拉高任一设备都能把总线拉低这样多设备共享总线不会互相短路。3.2 读补偿系数上电后第一件事BMP180上电后第一件事是把EEPROM里的11个校准系数读进内存之后每次计算都用它们。EEPROM数据的字节序是大端STM32是小端读回来要用移位拼接不能直接memcpy。// 从地址0xAA连续读取22字节EEPROM解析到结构体 void bmp180_read_calib(bmp180_calib_t *c) { uint8_t buf[22]; i2c_read_regs(BMP180_ADDR, 0xAA, buf, 22); c-ac1 (int16_t)((buf[0] 8) | buf[1]); c-ac2 (int16_t)((buf[2] 8) | buf[3]); c-ac3 (int16_t)((buf[4] 8) | buf[5]); c-ac4 (uint16_t)((buf[6] 8) | buf[7]); c-ac5 (uint16_t)((buf[8] 8) | buf[9]); c-ac6 (uint16_t)((buf[10] 8) | buf[11]); c-b1 (int16_t)((buf[12] 8) | buf[13]); c-b2 (int16_t)((buf[14] 8) | buf[15]); c-mb (int16_t)((buf[16] 8) | buf[17]); c-mc (int16_t)((buf[18] 8) | buf[19]); c-md (int16_t)((buf[20] 8) | buf[21]); }逻辑说明一次连续读22字节比单字节重复读22次效率高得多出错概率也小。i2c_read_regs的封装行为是先发设备地址加写位再发寄存器地址0xAA然后重发设备地址加读位连续接收22字节最后一个字节由主机回NACK表示接收结束。AC1、AC2、AC3、B1、B2、MB、MC、MD是有符号数AC4、AC5、AC6是无符号数解析时强制类型转换不能搞错否则后面公式会算出负温度。3.3 触发测量与读取结果时序这件事不能想当然读取UT和UP的流程非常固定写控制寄存器触发转换等待转换完成再读结果。等待时间必须大于数据手册的转换时间而且不能用裸延时代替状态查询因为不同oss下转换时间相差5倍以上。#define BMP180_ADDR 0x77 #define BMP180_CTRL 0xF4 #define CMD_TEMP 0x2E #define CMD_PRES_BASE 0x34 // 读取未补偿温度UT int32_t bmp180_read_ut(void) { uint8_t buf[2]; i2c_write_reg(BMP180_ADDR, BMP180_CTRL, CMD_TEMP); delay_ms(5); // oss0时转换4.5ms留1ms余量 i2c_read_regs(BMP180_ADDR, 0xF6, buf, 2); return ((int32_t)buf[0] 8) | buf[1]; } // 读取未补偿气压UPoss决定有效位宽 int32_t bmp180_read_up(uint8_t oss) { uint8_t buf[3]; uint32_t up; i2c_write_reg(BMP180_ADDR, BMP180_CTRL, CMD_PRES_BASE | (oss 6)); delay_ms(oss 0 ? 5 : oss 1 ? 8 : oss 2 ? 14 : 26); i2c_read_regs(BMP180_ADDR, 0xF6, buf, 3); up ((uint32_t)buf[0] 16) | ((uint32_t)buf[1] 8) | buf[2]; return (int32_t)(up (8 - oss)); // 去掉无效低位保留有效16oss位 }逻辑说明第一条命令里(oss 6)是把过采样率拼到控制寄存器的bit6和bit7比如oss为2时控制寄存器写入0x34 | 0x80 0xB4。第二条位移操作up (8 - oss)是整个函数里最容易抄错的地方oss0时右移8位24位数据只保留高16位oss为3时右移5位保留高19位。如果漏掉这个右移UP数值会被放大气压结果会系统性偏大。3.4 主循环调度多久测一次、怎么算平均主循环的调度间隔建议设在500ms左右。BMP180一次完整测量至少要4.5ms加25.5ms哪怕连续转换也没有压力但气压本身是慢变量10Hz的刷新率完全够用还能给串口打印和显示留出余量。另一个原因是BMP180在连续测量时自身会轻微发热温度数据会缓慢漂移测量间隔太短会把这种漂移当作真实温度变化。int main(void) { bmp180_calib_t calib; int32_t ut, up, t, p; float alt; uint8_t oss 2; // 折中选择 systick_init(); uart1_init(115200); i2c_gpio_init(); bmp180_read_calib(calib); while (1) { ut bmp180_read_ut(); t bmp180_get_temperature(ut, calib); // 内部更新g_b5 up bmp180_read_up(oss); p bmp180_get_pressure(up, g_b5, oss, calib); alt bmp180_altitude((float)p / 1000.0f, 1013.25f); printf(T%.1f°C P%.2fhPa Alt%.1fm\r\n, t / 10.0f, p / 1000.0f, alt); delay_ms(500); } }逻辑说明温度值和气压值是整数运算转换单位时再转浮点避免在补偿公式里引入浮点误差。海拔公式里两个参数都是hPa而bmp180_get_pressure返回的是0.1Pa把p除以1000.0f就是hPa。printf能直接送到串口的前提是重定向了fputc到USART1这个在课程设计的工程里通常已经写好没有的话补一个很短的fputc函数即可。4. 在Proteus里跑起来连接、配置与仿真验证驱动代码写完编译通过接下来是Proteus搭建和联调。这一章按步骤走多数仿真翻车都发生在元件连接和时钟配置上。4.1 元件清单与电路连接要点先看元件清单和引脚分配这部分和真实硬件电路高度一致直接照做就行。元件型号/值数量说明STM32主控STM32F103C81采用LQFP48封装气压传感器BMP1801Proteus元件库里直接搜上拉电阻4.7kΩ2SCL、SDA各一个虚拟终端Virtual Terminal1观察串口输出电源3.3V / 5V各1仿真电源源STM32引脚连接到说明PB6BMP180的SCLI2C时钟线PB7BMP180的SDAI2C数据线PA9虚拟终端RXDUSART1的TX3.3VBMP180 VCC上拉电阻供电GNDBMP180 GND公共地Proteus里的BMP180模型SDO引脚要接地接地后I2C地址固定为0x77如果SDO悬空地址会漂移到0x76代码里写的0x77就找不到了。这是最常见的第一坑。SCL和SDA上拉电阻不能省Proteus虽然对高阻态比真实器件宽容但浮空总线上读回的数据经常是0xFF看起来像芯片损坏。4.2 工程配置加载HEX、设置晶振和虚拟终端整个配置过程四步走每一步出错都有明确的表现。在Keil工程里编译生成HEX文件。打开Options for Target在Output页勾选Create HEX File重新编译。生成的hex文件在工程目录的Objects或者输出文件夹里。双击Proteus图纸上的STM32F103C8在Edit Component对话框的Program File中选择生成的hexClock Frequency设为8MHz。从元件库添加Virtual Terminal把PA9接到它的RXD引脚。双击虚拟终端Baud Rate设为115200与代码一致。点击左下角运行按钮虚拟终端窗口应输出温度和气压数据。晶振配置是仿真里最容易埋雷的点代码里如果用了标准库的SystemInit默认是8MHz外部晶振倍频到72MHz那Proteus芯片属性里的XTAL就必须写8MHz。如果芯片属性默认是1MHz或4MHz系统时钟算出来的波特率会偏差很大串口输出乱码。如果代码改用了内部RC时钟那芯片属性又要改对应频率不能照抄默认值。4.3 仿真验证怎么确认数据和真实硬件一致仿真验证分三步每步一个明确目标。第一步先确认串口有数据且频率稳定重启仿真后每秒能收到2组左右的数据第二步看数值范围正常室温下温度在25℃附近、气压在1013hPa附近、海拔在0附近第三步在代码里临时把P0从1013.25改成980.00海拔应跑到300米以上这能确认海拔公式链路没写错。Proteus里的BMP180模型输出的是固定参考数据不像真机会随环境温度变化而变化。因此仿真阶段验证的重点是通信链路和计算链路是否正确而不是数值是否贴合现场环境。真机移植后气压值出现几hPa的偏移很正常那不是驱动的错是本地海平面气压P0和模型气压基准不同导致的校准一下就好。5. 常见问题排查Proteus仿真BMP180最容易翻车的五个坑这一章全部是实际调试中的血泪经验每一条都按现象、原因、解决三个角度展开可以直接对照排查。5.1 数据全为0或全为0xFF现象虚拟终端输出T0、P0或者T255、P255完全不像正常数据。原因有两类I2C地址错或者SDAO。地址错最常见是把7位地址0x77写成了0xEE0xEE是8位写地址模拟I2C函数里如果再左移一次就会变成0xDC从机根本不回应。另一个原因是SCL和SDA没有上拉电阻Proteus仿真下总线浮空读回的数据多为0xFF。解决确认代码里用的是7位0x77在i2c读写函数内部再左移拼读写位给两条总线加上4.7kΩ上拉SDO接地。5.2 温度正常但气压数值恒定不动现象温度随代码修改或环境设置变化气压读数始终是同一个数。原因气压控制寄存器命令写错没有把oss移位到正确位置或者等待转换时间不够读到的是上一次缓存结果。气压转换命令的值是0x34加上oss左移6位写成CMD_PRES_BASE | oss的话oss1实际发出去的命令等于0x35芯片执行的可能是未定义模式。解决确认命令是0x34 | (oss 6)切换oss后把等待时间按2.1节的表加上去调试时先用oss0跑通基础链路再调其他档位。5.3 海拔跳变剧烈数值在0米和几十米之间来回跳现象温度和气压看着正常海拔却像过山车几秒内波动十几二十米。原因气压差1hPa对应约8.4米高度差BMP180原始气压本身的短时噪声在±0.3hPa左右乘上8.4米就变成两三米级别的抖动再加上第2.3节公式里的幂运算在P接近P0时对噪声更敏感实际显示就容易放大到几十米。解决对气压先做滑动平均再换算海拔不要对海拔平均值滤波具体的滤波代码在第6章给出。还有一个治本的做法是把500ms测量间隔加大到1秒气压短时噪声被时间平均掉一部分。5.4 虚拟终端输出乱码现象串口窗口能看到字符但不是完整的“Txx.x”而是菱形块和乱码混合。原因波特率失配或系统主频不对。代码和虚拟终端都设115200但STM32实际运行的主频不是72MHz偏差超过2%就会乱码。原因是Proteus芯片属性里的XTAL频率和代码SystemInit假设的8MHz外部晶振不一致。解决先确认芯片属性XTAL是8MHz再确认代码里没有把RCC配置成HSI内部时钟。乱码还有一个容易被忽略的场景如果fputc里用了查询发送标志而发送标志在某些情况下没被正确清零数据会错位表现为每帧的第一个字符乱码。5.5 仿真运行一段时间后BMP180无响应现象前几分钟数据正常跑着跑着读回全是0xFF重启仿真后又正常。原因模拟I2C的时序完全依赖延时函数如果主循环里加入了printf、LCD刷新这类耗时操作偶发地打断了I2C时序总线上会留下半截传输从机状态机卡死。Proteus的I2C模型不像真实硬件那样有超时复位机制卡住之后会一直保持错误状态。解决每次发起新的I2C传输前先执行一遍总线恢复序列——把SCL和SDA都拉高再给SCL来9个脉冲让挂在总线上的从机复位状态机。把这个序列放在i2c_start之前调用仿真稳定性会明显提升真机上也是有效的保底手段。6. 进阶把“能读数”变成“数据可信”——校准、滤波与显示仿真跑通了数据也出来了这个时候才算走完一半。真正让海拔数据可信还需要解决P0校准、气压滤波和显示这三个问题。先做P0校准。如果你在海拔200米的地方拿着代码默认的1013.25当海平面气压读出的海拔会偏低或者偏高误差在几十米量级。最直接的做法是用手机GPS看一下你所在位置的海拔h再用BMP180读出当前气压P反推P0P0 P / powf(1 - h / 44330.77f, 5.255f)。算出来的值填到代码的P0参数里海拔输出就会明显更合理。如果只想测相对高度把起点气压作为P0即可。再处理气压滤波。BMP180原始气压的短时噪声经过海拔公式放大后会看到±几米的波动。滑动平均是成本最低的平滑方案我通常对单次气压值做8点平均#define FILTER_N 8 static float filter_buf[FILTER_N]; static uint8_t filter_idx 0; float pressure_filter(float new_p) { static float sum 0.0f; sum - filter_buf[filter_idx]; // 减掉最旧值 filter_buf[filter_idx] new_p; // 更新当前位置 sum new_p; // 加入新值 filter_idx (filter_idx 1) % FILTER_N; return sum / FILTER_N; }逻辑说明环形缓冲区里始终保留最近8次气压值sum是它们的累加和每来一个新值就替换最旧的那个返回平均值。注意滤波要作用在气压上不要作用在海拔上。海拔换算是一个非线性幂函数对滤波后的气压再做幂运算误差比先算海拔再平均要小。这个细节是我当时调试时反复对比发现的顺序不同结果能差好几米。显示方面课程设计通常要求LCD直观展示。Proteus里建议用LCD1602接线简单八根数据线直接连到STM32的PA0到PA7RS、RW、EN各占一个引脚。如果手头有带I2C接口的OLED模块可以和BMP180共用同一条I2C总线OLED地址0x3CBMP180地址0x77不会冲突只要代码里根据设备地址区分访问对象就行。最后还有一个我个人的习惯每次拿到一种新传感器先花10分钟把数据手册里的补偿公式逐行对照一遍再烧一个固定已知气压的测试程序验证驱动最后才进仿真调界面。第一次看到海拔在正负20米跳的时候以为是传感器质量差后来发现是没滤波没校准从那时候起我每次做传感器项目都强制先走一遍“读寄存器、算补偿、验数据”的闭环能省下大量查“为什么数据不对”的时间。希望帮到你。本文还有配套的精品资源点击获取