简介:这份资源面向学习STM32嵌入式开发与传感器应用的初学者及进阶者,围绕BMP180气压计的气压、海拔高度与温度测量展开,提供从硬件仿真到软件源码的完整实践方案。压缩包共406个文件、约30.52MB,以c与h源码文件为主体,配合Proteus工程文件、hex固件、工程配置与说明文档,覆盖驱动编写、通信协议实现与仿真验证各环节。已有357人学习下载,说明该案例在同类项目中具备一定参考价值。读者可从中获得STM32与BMP180通过I2C或SPI交互的完整代码框架,理解气压数据校准、巴尔末公式换算海拔及温度处理的实现思路,并借助Proteus仿真验证电路与逻辑的正确性;配套资料还包含原理图与设计说明,便于对照源码梳理模块职责,适合课程设计、毕业设计及自学嵌入式数据采集的读者参考。
1. 从一块气压计板子说起:这个仿真项目到底能干什么
前阵子帮人看一个气压海拔测量的课设,板子焊好了,屏也亮了,结果读出来的气压值一直在跳,海拔算出来能从一楼飘到三十楼。翻来覆去查了两天,最后发现是 I2C 时序里少了一个应答位。这种玄学问题,如果一开始就在 Proteus 里把传感器和单片机跑通,能省掉一大半返工。这个项目就是干这个的:基于 STM32 单片机,通过 I2C 挂一颗 BMP180 气压传感器,把气压、温度、海拔三组数据算出来并显示,配套 Proteus 仿真工程、源码和全套资料。适合正在做传感器采集课设、想搞懂 I2C 时序和气压补偿算法、或者需要一个能直接跑起来的最小工程骨架的开发者。它不追求花哨,胜在链路完整:从底层总线到寄存器读写,再到补偿计算和显示,一条线拉通。
2. 技术选型与工程结构:为什么是 STM32 加 BMP180 这套组合
2.1 选型逻辑:一颗传感器把 I2C 和浮点运算都练到了
BMP180 是博世的一款数字气压传感器,走 I2C 接口,内部带温度补偿,输出未补偿的气压和温度原始值,需要主机侧用一组出厂标定系数做二次计算才能得到真实气压和海拔。这个特性决定了它不是「读出来就能用」的傻瓜器件,而是逼着你去理解补偿算法。对学习型项目来说,这恰恰是价值所在:你会真正碰到寄存器地址、标定系数 EEPROM、过采样设置这些实打实的东西。
STM32 这边,选型上常见做法是用 STM32F103 系列,Cortex-M3 内核,主频 72MHz,带硬件 I2C 外设,Flash 和 RAM 足够跑这套逻辑,价格也友好。项目里 I2C 既可以用硬件外设,也可以用 GPIO 软件模拟,后者在 Proteus 仿真里更稳,因为仿真器对硬件 I2C 的时序支持偶尔会翻车。我一般建议初学阶段先用软件 I2C,时序完全可控,出问题好定位。
海拔的计算依赖气压,公式里带对数和幂运算,所以工程里会引入 math 库,用浮点跑。72MHz 的 M3 跑这点运算毫无压力,但如果你的工程对实时性有要求,也可以改成定点近似,这是后话。
2.2 目录结构:一份能直接导入的工程长什么样
拿到压缩包解压后,结构大致是这样,不同人整理习惯略有差异,但核心几块跑不掉:
BMP180_STM32_Proteus/ ├── Doc/ # 原理说明、BMP180 数据手册、接线图 ├── Firmware/ # STM32 标准外设库或 HAL 库 ├── Hardware/ │ ├── bmp180.c # 传感器驱动:初始化、读原始值、补偿计算 │ ├── bmp180.h │ ├── i2c_soft.c # 软件 I2C 时序实现 │ ├── i2c_soft.h │ ├── oled.c # 显示驱动(常见为 0.96 寸 OLED 或 LCD1602) │ └── oled.h ├── User/ │ ├── main.c # 主循环:初始化 + 周期采集 + 显示 │ ├── stm32f10x_it.c # 中断服务 │ └── system_stm32f10x.c # 时钟配置 ├── Project/ │ └── xxx.uvprojx # Keil 工程文件 └── Proteus/ └── xxx.pdsprj # 仿真工程,含 STM32 模型和 BMP180 模型这个结构的关键在于Hardware层和User层的分离:驱动只管和芯片打交道,主循环只管调度。二次开发时你要换显示方式,只动oled.c;要换传感器型号,只动bmp180.c,互不牵连。这种分层不是形式主义,是让你改一处不炸一片。
2.3 仿真环境搭建:Proteus 里怎么把板子「搭」出来
Proteus 仿真工程里需要放三样东西:STM32 单片机模型、BMP180 传感器模型、显示器件。接线就四根:VCC、GND、SCL、SDA,SCL 和 SDA 各挂一个上拉电阻到 VCC,常见取 4.7k 或 10k。BMP180 的 I2C 地址是 0x77(7 位地址),写操作时左移一位变成 0xEE,读操作是 0xEF,这个细节后面代码里会反复出现。
仿真里还有一个坑:STM32 模型需要加载编译出来的 hex 文件,路径在模型属性里设置。如果你改了代码重新编译,记得在 Proteus 里重新加载一次 hex,否则跑的还是旧程序,现象对不上会让你怀疑人生。另外仿真时钟要和工程里的时钟配置一致,常见是 8MHz 外部晶振经 PLL 倍频到 72MHz,配错了串口和延时全乱。
提示:Proteus 的 BMP180 模型对 I2C 时序的容忍度比真实器件低,软件 I2C 的延时函数别写得太激进,适当加长能显著提高仿真成功率。
3. 核心驱动拆解:从 I2C 时序到气压海拔的完整计算链
3.1 软件 I2C 的四个基本动作
软件 I2C 的本质是用 GPIO 电平变化模拟总线协议,核心就四个动作:起始、停止、写一个字节、读一个字节。下面这段是典型的实现骨架,引脚宏定义按你的实际接线改:
// i2c_soft.c 软件 I2C 基本时序 #define SCL_H GPIO_SetBits(GPIOB, GPIO_Pin_6) #define SCL_L GPIO_ResetBits(GPIOB, GPIO_Pin_6) #define SDA_H GPIO_SetBits(GPIOB, GPIO_Pin_7) #define SDA_L GPIO_ResetBits(GPIOB, GPIO_Pin_7) #define SDA_IN GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7) void I2C_Start(void) { SDA_H; SCL_H; Delay_us(4); SDA_L; Delay_us(4); // SCL 高时 SDA 下降沿 = 起始 SCL_L; } void I2C_Stop(void) { SDA_L; SCL_H; Delay_us(4); SDA_H; Delay_us(4); // SCL 高时 SDA 上升沿 = 停止 } // 写一个字节,返回从机应答位:0 表示应答 uint8_t I2C_WriteByte(uint8_t dat) { uint8_t i, ack; for (i = 0; i < 8; i++) { if (dat & 0x80) SDA_H; else SDA_L; dat <<= 1; SCL_H; Delay_us(4); SCL_L; Delay_us(4); } SDA_H; // 释放 SDA 等从机拉低 SCL_H; Delay_us(4); ack = SDA_IN; // 读应答 SCL_L; return ack; }逻辑说明:起始和停止靠 SCL 高电平期间 SDA 的跳变来区分,这是 I2C 协议的硬规定。写字节是高位先出,每发一位拉一次时钟,8 位发完后第 9 个时钟周期释放 SDA,让从机把线拉低表示应答。Delay_us(4)是给总线留的建立保持时间,仿真里可以适当加大到 10us 更稳。参数上唯一要动的就是引脚宏和延时,换板子只改这两处。
3.2 BMP180 初始化与标定系数读取
BMP180 上电后第一件事不是读气压,而是从 0xAA 开始的 EEPROM 里读出 11 个标定系数,这些系数是每颗芯片出厂单独校准的,补偿计算全靠它们。地址分布是固定的:
| 寄存器地址 | 系数 | 类型 |
|---|---|---|
| 0xAA | AC1 | int16 |
| 0xAC | AC2 | int16 |
| 0xAE | AC3 | int16 |
| 0xB0 | AC4 | uint16 |
| 0xB2 | AC5 | uint16 |
| 0xB4 | AC6 | uint16 |
| 0xB6 | B1 | int16 |
| 0xB8 | B2 | int16 |
| 0xBA | MB | int16 |
| 0xBC | MC | int16 |
| 0xBE | MD | int16 |
读取时注意高低字节顺序,BMP180 是大端,高字节在前。代码里通常写一个BMP180_Read2Byte(addr)把两个字节拼成 16 位,再按有无符号强转。这一步错了,后面算出来的气压会离谱到没边,而且不会报错,属于典型的黑匣子问题。
// bmp180.c 读标定系数 void BMP180_ReadCalibration(void) { cal.AC1 = (int16_t)BMP180_Read2Byte(0xAA); cal.AC2 = (int16_t)BMP180_Read2Byte(0xAC); cal.AC3 = (int16_t)BMP180_Read2Byte(0xAE); cal.AC4 = BMP180_Read2Byte(0xB0); // 无符号 cal.AC5 = BMP180_Read2Byte(0xB2); cal.AC6 = BMP180_Read2Byte(0xB4); cal.B1 = (int16_t)BMP180_Read2Byte(0xB6); cal.B2 = (int16_t)BMP180_Read2Byte(0xB8); cal.MB = (int16_t)BMP180_Read2Byte(0xBA); cal.MC = (int16_t)BMP180_Read2Byte(0xBC); cal.MD = (int16_t)BMP180_Read2Byte(0xBE); }参数说明:AC4、AC5、AC6 在数据手册里定义为无符号,其余为有符号,强转类型必须严格对应,否则负系数会被解释成大正数,补偿结果直接崩。这是我在真实项目里踩过的坑,仿真里因为模型系数固定,反而不容易暴露,但换到实物就现原形。
3.3 温度与气压的补偿计算
BMP180 的补偿算法分两步:先算温度,再用温度去算气压。原始温度 UT 读自 0x2E 寄存器,原始气压 UP 读自 0xF4,过采样设置 oss 决定精度和转换时间。下面这段是数据手册给出的标准算法,工程里基本照抄:
// bmp180.c 温度补偿 void BMP180_Calculate(void) { int32_t X1, X2, B5, B6, X3, B3, p; uint32_t B4, B7; // ---- 温度 ---- X1 = ((int32_t)ut - cal.AC6) * cal.AC5 >> 15; X2 = ((int32_t)cal.MC << 11) / (X1 + cal.MD); B5 = X1 + X2; temperature = (B5 + 8) >> 4; // 单位 0.1℃,除以 10 得摄氏度 // ---- 气压 ---- B6 = B5 - 4000; X1 = (cal.B2 * (B6 * B6 >> 12)) >> 11; X2 = cal.AC2 * B6 >> 11; X3 = X1 + X2; B3 = (((int32_t)cal.AC1 * 4 + X3) << oss) + 2) >> 2; X1 = cal.AC3 * B6 >> 13; X2 = (cal.B1 * (B6 * B6 >> 12)) >> 16; X3 = ((X1 + X2) + 2) >> 2; B4 = cal.AC4 * (uint32_t)(X3 + 32768) >> 15; B7 = ((uint32_t)up - B3) * (50000 >> oss); if (B7 < 0x80000000) p = (B7 * 2) / B4; else p = (B7 / B4) * 2; X1 = (p >> 8) * (p >> 8); X1 = (X1 * 3038) >> 16; X2 = (-7357 * p) >> 16; pressure = p + ((X1 + X2 + 3791) >> 4); // 单位 Pa }逻辑说明:oss是过采样设置,取值 0 到 3,对应内部转换次数 1、2、4、8 次,精度递增但转换时间也递增。仿真里用 oss=0 就够,实物想稳一点用 oss=1 或 2。B5是温度中间量,气压计算全程依赖它,所以顺序不能颠倒。B7那个分支是为了防止 32 位溢出,别嫌它丑,删了在低气压场景会算错。最终pressure单位是帕斯卡,除以 100 得百帕。
3.4 海拔换算与显示刷新
拿到气压后,海拔用国际气压高度公式算:
// 海拔 = 44330 * (1 - (P / P0)^(1/5.255)) // P0 为海平面标准气压 101325 Pa float BMP180_GetAltitude(int32_t p) { return 44330.0f * (1.0f - powf((float)p / 101325.0f, 0.190295f)); }参数说明:0.190295就是1/5.255,写成常量省一次除法。P0如果你知道当地基准气压,可以换成实测值,海拔会更准;不知道就用标准值,误差在几十米量级,课设够用。主循环里按「读原始值 → 补偿计算 → 换算海拔 → 刷屏」的顺序跑,中间加个几百毫秒延时,别刷太快,OLED 刷新本身也耗时。
注意:
powf来自 math.h,Keil 里记得勾选使用微库(Use MicroLIB),否则浮点库链接可能报错,这是新手最容易卡住的一步。
4. 避坑与排查:仿真跑不通时先看这几条
4.1 读出来全是 0 或 0xFF
现象:串口或屏幕显示的气压温度恒定不变,要么全 0 要么全 0xFF。原因多半是 I2C 根本没通,从机没应答。先查接线,SCL 和 SDA 有没有接反,上拉电阻有没有漏;再查地址,0x77 左移后是 0xEE,写寄存器时别把读写位搞混。解决:在I2C_WriteByte后打印应答位,应答为 1 就是没通,逐段缩短延时再试。
4.2 气压值合理但海拔乱跳
现象:气压看着正常,海拔一会儿正一会儿负。原因通常是P0用了错误的值,或者气压单位没统一——补偿算出来是 Pa,有人当百帕直接代进公式,差了两个数量级。解决:打印中间变量pressure,确认量级在 100000 附近,再代公式。
4.3 仿真里正常,烧到实物就偏
现象:Proteus 里数据漂亮,实物上温度差好几度。原因是仿真模型的标定系数是理想值,实物每颗芯片系数不同,而你如果没读 EEPROM 直接用了硬编码系数,必然偏。解决:确认BMP180_ReadCalibration真的被调用,且读出的系数打印出来和手册典型值在同一量级。
4.4 改了代码仿真没变化
现象:明明改了 main.c,Proteus 跑出来还是老现象。原因是 hex 没重新加载。解决:Keil 重新编译确认生成新 hex,Proteus 里双击单片机模型,把 Program File 重新指一遍,或者干脆重启仿真。
4.5 浮点运算结果异常
现象:海拔算出来是nan或极大值。原因多是 math 库没链接、微库没勾,或者powf的底数传了负数(气压算错导致)。解决:先确认气压为正,再检查 Keil 的 Target 选项里 Use MicroLIB 是否勾选。
5. 二次开发:把这套骨架改成你自己的采集节点
这套工程最大的价值不是「能测气压」,而是它给了一个干净的传感器接入模板。你要做的改造,基本集中在三个位置。
第一处是换传感器。如果你手头是 BMP280 或 SPL06,接口还是 I2C,那i2c_soft.c一行不用动,只重写bmp180.c里的初始化、读原始值和补偿函数,保持对外暴露GetPressure()、GetTemperature()两个接口不变,主循环完全不用改。这就是分层的好处,接口稳定,实现随便换。
第二处是加数据上报。课设常见要求是把数据传到上位机,你可以在主循环里加一段串口发送,把三个值按固定格式打包:
// main.c 周期上报,格式:T=25.3,P=1013.2,H=52.1 printf("T=%.1f,P=%.1f,H=%.1f\r\n", temperature / 10.0f, pressure / 100.0f, altitude);注意temperature单位是 0.1℃,所以除以 10;pressure是 Pa,除以 100 得百帕。串口重定向fputc要自己实现,标准库和 HAL 写法不同,照你用的库来。
第三处是加滤波。单次采样的气压噪声不小,海拔跟着抖。最简单的做法是滑动平均,开一个长度 8 的环形缓冲,每次采完压进去取均值,代码不到十行,效果立竿见影。想再讲究点可以用一阶低通,系数取 0.1 到 0.3 之间,响应和 smooth 之间自己权衡。
验证方法上,我习惯先在仿真里把 oss 从 0 调到 3,看气压值是否随精度提升而收敛;再人为改P0看海拔是否线性平移,确认公式链路没错;最后烧实物,拿手机的气压计 App 做对照,差值在合理范围就算通过。
改造时有个习惯我一直保持:每动一处驱动,先在main里单独打印这一处的原始值,确认对了再往下接。早年我图省事,一口气把驱动、补偿、显示全改完再上电,结果一个字节序错误让我查了一整晚。从那以后,我每次接新传感器都强制走一遍「先读 ID、再读原始值、最后算物理量」的三段验证,一步不对就停在那一步。这套工程的资料里带了原理说明和接线图,照着把三段验证跑通,剩下的就是往你的业务上套。希望帮到你。
本文还有配套的精品资源,点击获取