零知派——STM32+BMP180结合旋转编码器多页仪表盘+ESP-01无线上位机,三级蜂鸣告警全链路IoT气象监控系统
随着物联网技术向消费端和教育端快速渗透,低成本嵌入式气象监测节点的需求持续升温
目录
Q1:编译报错 invalid conversion from 'const uint8_t*' to 'uint8*'
项目概述
本项目基于零知派标准板,主控芯片 STM32F103RBT6(72MHz Cortex-M3),通过 SoftWire 软件 I²C(SDA=A4, SCL=A5)驱动 BMP180 气压传感器,经 SPI 接口驱动 ST7789 240×240 TFT 显示屏,配合 EC11 旋转编码器实现人机交互,转动即响应,停转后稳定判定,短按切换亮色/深色双主题,使用ESP-01 AP 热点+Network Kit 短指令控制进行物联网驱动
项目难点
问题描述:ESP-01 +IPD 帧被 SEND OK/Recv N bytes 噪声污染导致指令解析崩溃
解决方案: 检测到 SEND OK/Recv 时提前清洗缓冲区,只保留 +IPD 之后的内容;handleClientCmd精确定位数据载荷起始位置
一、系统接线部分
1.1 硬件清单
| 序号 | 名称 | 规格型号 | 数量 |
|---|---|---|---|
| 1 | 主控开发板 | 零知派标准板(STM32F103RBT6) | 1 |
| 2 | 气压传感器 | BMP180 模块(GY-68) | 1 |
| 3 | TFT 显示屏 | ST7789 240×240 SPI | 1 |
| 4 | 旋转编码器 | EC11 五脚带按键 | 1 |
| 5 | WiFi 模块 | ESP-01(ESP8266) | 1 |
| 6 | 3.3V LDO | AMS1117-3.3 模块 | 1 |
| 7 | 无源蜂鸣器 | 5V 无源,直径12mm | 1 |
| 8 | 杜邦线 | 公对母 20cm | 若干 |
1.2 接线方案表
严格按照代码中的宏定义进行接线,不得随意更改,否则编码器中断和 ADC 会失效
①BMP180
| BMP180 引脚 | 零知派引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 严禁接5V |
| GND | GND | 共地 |
| SDA | A4(PC4) | SoftWire 数据线 |
| SCL | A5(PC5) | SoftWire 时钟线 |
②EC11 旋转编码器
| 编码器引脚 | 零知派引脚 | 代码定义 |
|---|---|---|
| CLK(A相) | D6(PA8) | #define ENC_CLK 6 |
| DT(B相) | D12(PA6) | #define ENC_DT 12 |
| SW(按键) | D14(PB8) | #define ENC_SW 14 |
| VCC | 3.3V | — |
| GND | GND | — |
③ESP-01(USART2)
| ESP-01 引脚 | 零知派引脚 | 说明 |
|---|---|---|
| TX | D0(PA3=USART2 RX) | ESP发→STM32收 |
| RX | D1(PA2=USART2 TX) | STM32发→ESP收 |
| VCC | 独立3.3V LDO | 不可用STM32 3.3V引脚 |
| GND | 共地 | — |
| CH_PD | 3.3V | 必须拉高,否则模块不启动 |
④无源蜂鸣器
| 蜂鸣器引脚 | 零知派引脚 | 代码定义 |
|---|---|---|
| + | D3(PA1,Timer2 CH2) | #define BUZZER_PIN 3 |
| - | GND | — |
请注意:ST7789显示屏直插零知派标准板TFT引脚,无需单独接线
1.3 具体接线图
ESP-01模块的CH_PD 引脚悬空时模块不工作,请接入3.3V拉高CH_PD 引脚

1.4 接线实物图

二、安装与使用部分
2.1 开源平台-输入"BMP180"并搜索-代码下载自动打开

2.2 连接-验证-上传

2.3 调试-串口监视器

三、代码讲解部分
本项目代码结构:BMP180_IoT_Station主程序、config全局配置、alarm_buzzer蜂鸣器驱动、display_ui显示模块、sensor_data气压传感器数据、wifi_esp01无线传输驱动
3.1 环形缓冲区 + 时序读取
/******************************************************************************
* 文件: sensor_data.cpp
* 功能: BMP180 采样 + EMA 滤波 + 环形缓冲
******************************************************************************/
#include "sensor_data.h"
#include <Adafruit_BMP085.h>
// ── 全局变量定义 ──────────────────────────────────
float curTemp = 25.0f;
float curPres = 101325.0f;
float curAlt = 0.0f;
float emaTemp = 25.0f;
float emaPres = 101325.0f;
float emaAlt = 0.0f;
bool dataOK = false;
float tBuf[BUF_LEN];
float pBuf[BUF_LEN];
float aBuf[BUF_LEN];
uint8_t bufHead = 0;
uint8_t bufCnt = 0;
static Adafruit_BMP085 bmp;
// ── 环形缓冲 ─────────────────────────────────────
void pushBuf(float t, float p, float a) {
tBuf[bufHead] = t;
pBuf[bufHead] = p;
aBuf[bufHead] = a;
bufHead = (bufHead + 1) % BUF_LEN;
if (bufCnt < BUF_LEN) bufCnt++;
}
float getBuf(float *buf, uint8_t i) {
uint8_t start = (bufCnt < BUF_LEN) ? 0 : bufHead;
return buf[(start + i) % BUF_LEN];
}
// ── 初始化 ───────────────────────────────────────
void sensorInit() {
Serial.println(F("[BMP] Initializing BMP180..."));
if (!bmp.begin()) {
Serial.println(F("[BMP] ERROR: BMP180 not found! Check SDA=A4 SCL=A5"));
// 调用者负责错误处理(屏幕显示)
while (1) delay(1000);
}
Serial.println(F("[BMP] BMP180 OK"));
// 预热 3 次,EMA 从真实值启动,避免指数跳变
for (uint8_t i = 0; i < 3; i++) {
sampleBMP180();
delay(200);
}
// 用第一次真实值覆盖初始 EMA
emaTemp = curTemp;
emaPres = curPres;
emaAlt = curAlt;
Serial.print(F("[BMP] Preheat done: T="));
Serial.print(emaTemp, 1);
Serial.print(F("C P="));
Serial.print(emaPres / 100.0f, 1);
Serial.print(F("hPa A="));
Serial.print(emaAlt, 1);
Serial.println(F("m"));
}
// ── 采样(带合理性校验 + EMA)────────────────────
void sampleBMP180() {
float t = bmp.readTemperature();
float p = bmp.readPressure();
float a = bmp.readAltitude();
// 合理性校验:超出范围视为无效帧
if (t < -40.0f || t > 85.0f || p < 80000.0f || p > 120000.0f) {
Serial.print(F("[BMP] INVALID sample: T="));
Serial.print(t, 1);
Serial.print(F(" P="));
Serial.println(p / 100.0f, 1);
return;
}
curTemp = t;
curPres = p;
curAlt = a;
// EMA 低通滤波:y[n] = α·x[n] + (1-α)·y[n-1]
emaTemp = EMA_ALPHA * t + (1.0f - EMA_ALPHA) * emaTemp;
emaPres = EMA_ALPHA * p + (1.0f - EMA_ALPHA) * emaPres;
emaAlt = EMA_ALPHA * a + (1.0f - EMA_ALPHA) * emaAlt;
dataOK = true;
pushBuf(emaTemp, emaPres, emaAlt);
Serial.print(F("[BMP] T=")); Serial.print(emaTemp, 1);
Serial.print(F("C P=")); Serial.print(emaPres / 100.0f, 1);
Serial.print(F("hPa A=")); Serial.print(emaAlt, 1);
Serial.println(F("m"));
}
写入 O(1),按序读取 O(1),内存固定占用 32×3×4=384 Byte。bufCnt < BUF_LEN 时数据从下标0连续存,满后 start=bufHead(最老数据所在位置),getBuf 通过模运算对外呈现线性时间序列,调用方无需感知环形结构。三路传感器共用同一写指针,时序完全对齐
3.2 编码器格雷码查表
旋转编码器的A/B两相信号呈正交(相位差90°),本项目采用两级解耦架构来消除硬件抖动并防止中断阻塞主任务
static const int8_t encTable[16] = {
0,-1, 1, 0,
1, 0, 0,-1,
-1, 0, 0, 1,
0, 1,-1, 0
};
void updateEncoder() { // 中断服务函数
uint8_t clk = digitalRead(ENC_CLK);
uint8_t dt = digitalRead(ENC_DT);
uint8_t enc = (clk << 1) | dt;
// 前后4位状态拼成索引,直接查表得方向
int8_t dir = encTable[(lastEncoded << 2) | enc];
if (dir != 0) {
accSteps += dir; // 中断里只累积,不翻页
encDirty = true;
lastEncTime = millis();
}
lastEncoded = enc;
}
static void checkEncoder() { // 主循环里每帧调用
if (!encDirty) return;
if (millis() - lastEncTime < 120) return; // 还在转,等待
encDirty = false;
if (accSteps >= 2) { encCW = true; accSteps = 0; }
else if (accSteps <= -2) { encCCW = true; accSteps = 0; }
else { accSteps = 0; } // 抖动丢弃
}
主循环停转判定后才翻页,STABLE_MS=120ms 窗口防止连转时多次触发;STEP_THR=2 过滤机械抖动产生的单步噪声
3.3 ESP-01 非阻塞 AT 状态机
ESP-01的AT指令初始化共需发送7条指令,若简单顺序发送,其中任何一条因超时重试都会导致主循环长时间阻塞。本项目用状态机将所有初始化过程拆分为离散步骤
const char *wfStateName(WiFiState s) {
switch (s) {
case WF_IDLE: return "IDLE";
case WF_RST: return "RST";
case WF_ECHO_OFF: return "ECHO_OFF";
case WF_MODE: return "CWMODE";
case WF_SAP: return "CWSAP";
case WF_CIPMUX: return "CIPMUX";
case WF_SERVER: return "CIPSERVER";
case WF_READY: return "READY";
case WF_ERROR: return "ERROR";
default: return "?";
}
}
// ── AT 状态机主循环(每帧调用)──────────────────
void wifiTick() {
// 读取所有可用字节
while (ESP_SERIAL.available()) {
char c = (char)ESP_SERIAL.read();
esp01Buf += c;
// 防溢出
if (esp01Buf.length() > 300) {
esp01Buf = esp01Buf.substring(150);
}
}
uint32_t now = millis();
// ── 连接事件检测(在任何状态下都检测)──────
if (wfState == WF_READY) {
if (esp01Buf.indexOf("SEND OK") >= 0 ||
esp01Buf.indexOf("Recv ") >= 0) {
// 只保留 +IPD 之后的内容
int ipdPos = esp01Buf.indexOf("+IPD");
if (ipdPos > 0) {
esp01Buf = esp01Buf.substring(ipdPos);
} else if (ipdPos < 0) {
esp01Buf = ""; // 没有 +IPD,全部是噪声
}
}
if (esp01Buf.indexOf("+IPD") >= 0) {
if (!wfClientOK) {
wfClientOK = true;
Serial.println(F("[WiFi] Client CONNECTED"));
lastPush = 0; // 立即推送一帧欢迎数据
}
handleClientCmd();
esp01Buf = "";
}
if (esp01Buf.indexOf("CLOSED") >= 0) {
wfClientOK = false;
Serial.println(F("[WiFi] Client DISCONNECTED"));
esp01Buf = "";
}
if (esp01Buf.indexOf("CONNECT") >= 0 && esp01Buf.indexOf("+IPD") < 0) {
wfClientOK = true;
Serial.println(F("[WiFi] New connection"));
esp01Buf = "";
}
return;
}
// ── AT 初始化状态机 ───────────────────────
switch (wfState) {
case WF_IDLE:
Serial.println(F("[WiFi] State: IDLE → sending RST"));
delay(800); // 等模块上电稳定
espSend("AT+RST");
wfTimer = now;
wfState = WF_RST;
wfRetry = 0;
break;
case WF_RST:
if (esp01Buf.indexOf("ready") >= 0 || esp01Buf.indexOf("WIFI GOT IP") >= 0 || esp01Buf.indexOf("ATE0") >= 0) {
Serial.println(F("[WiFi] RST OK → ATE0 (disable echo)"));
esp01Buf = "";
delay(300);
espSend("ATE0");
wfTimer = now;
wfState = WF_ECHO_OFF;
wfRetry = 0;
} else if (now - wfTimer > WF_TIMEOUT) {
wfRetry++;
Serial.print(F("[WiFi] RST timeout, retry "));
Serial.print(wfRetry);
Serial.print(F("/"));
Serial.println(WF_MAX_RETRY);
Serial.print(F("[WiFi] RX buf: ["));
Serial.print(esp01Buf);
Serial.println(F("]"));
if (wfRetry >= WF_MAX_RETRY) {
Serial.println(F("[WiFi] ERROR: ESP-01 not responding"));
Serial.println(F("[WiFi] Check: VCC=3.3V? CH_PD=3.3V? TX/RX not swapped?"));
wfTimer = now;
wfState = WF_ERROR;
return;
}
espSend("AT+RST");
wfTimer = now;
esp01Buf = "";
}
break;
case WF_ECHO_OFF:
if (esp01Buf.indexOf("OK") >= 0 || esp01Buf.indexOf("ATE0") >= 0) {
Serial.println(F("[WiFi] Echo OFF OK → CWMODE=2"));
esp01Buf = "";
delay(100);
espSend("AT+CWMODE=2");
wfTimer = now;
wfState = WF_MODE;
wfRetry = 0;
} else if (now - wfTimer > 1500) {
// ATE0 失败也继续(有些固件版本无回复)
Serial.println(F("[WiFi] ATE0 no OK, continue anyway"));
esp01Buf = "";
espSend("AT+CWMODE=2");
wfTimer = now;
wfState = WF_MODE;
wfRetry = 0;
}
break;
case WF_MODE:
if (esp01Buf.indexOf("OK") >= 0 || esp01Buf.indexOf("no change") >= 0) {
Serial.println(F("[WiFi] CWMODE=2 OK → CWSAP"));
esp01Buf = "";
delay(100);
espSend("AT+CWSAP=\"BMP180_Station\",\"12345678\",6,3");
wfTimer = now;
wfState = WF_SAP;
wfRetry = 0;
} else if (now - wfTimer > WF_TIMEOUT) {
wfRetry++;
Serial.print(F("[WiFi] CWMODE timeout retry "));
Serial.println(wfRetry);
Serial.print(F("[WiFi] RX: ["));
Serial.print(esp01Buf);
Serial.println(F("]"));
if (wfRetry >= WF_MAX_RETRY) {
wfTimer = now;
wfState = WF_ERROR;
return;
}
espSend("AT+CWMODE=2");
wfTimer = now;
esp01Buf = "";
}
break;
case WF_SAP:
if (esp01Buf.indexOf("OK") >= 0) {
Serial.println(F("[WiFi] CWSAP OK → CIPMUX=1"));
Serial.println(F("[WiFi] AP: SSID=BMP180_Station ch=6 enc=WPA2"));
esp01Buf = "";
delay(100);
espSend("AT+CIPMUX=1");
wfTimer = now;
wfState = WF_CIPMUX;
wfRetry = 0;
} else if (now - wfTimer > WF_TIMEOUT) {
wfRetry++;
Serial.print(F("[WiFi] CWSAP timeout retry "));
Serial.println(wfRetry);
Serial.print(F("[WiFi] RX: ["));
Serial.print(esp01Buf);
Serial.println(F("]"));
if (wfRetry >= WF_MAX_RETRY) {
wfTimer = now;
wfState = WF_ERROR;
return;
}
espSend("AT+CWSAP=\"BMP180_Station\",\"12345678\",6,3");
wfTimer = now;
esp01Buf = "";
}
break;
case WF_CIPMUX:
if (esp01Buf.indexOf("OK") >= 0 || esp01Buf.indexOf("no change") >= 0) {
Serial.println(F("[WiFi] CIPMUX=1 OK → CIPSERVER"));
esp01Buf = "";
delay(100);
espSend("AT+CIPSERVER=1,8080");
wfTimer = now;
wfState = WF_SERVER;
wfRetry = 0;
} else if (now - wfTimer > WF_TIMEOUT) {
wfRetry++;
Serial.print(F("[WiFi] CIPMUX timeout retry "));
Serial.println(wfRetry);
if (wfRetry >= WF_MAX_RETRY) {
wfTimer = now;
wfState = WF_ERROR;
return;
}
espSend("AT+CIPMUX=1");
wfTimer = now;
esp01Buf = "";
}
break;
case WF_SERVER:
if (esp01Buf.indexOf("OK") >= 0 || esp01Buf.indexOf("no change") >= 0) {
Serial.println(F("[WiFi] ============================"));
Serial.println(F("[WiFi] READY! TCP Server started"));
Serial.println(F("[WiFi] Connect to WiFi: BMP180_Station"));
Serial.println(F("[WiFi] Password: 12345678"));
Serial.println(F("[WiFi] Then TCP connect: 192.168.4.1:8080"));
Serial.println(F("[WiFi] Send GET_DATA to receive JSON"));
Serial.println(F("[WiFi] ============================"));
esp01Buf = "";
wfState = WF_READY;
} else if (now - wfTimer > WF_TIMEOUT) {
wfRetry++;
Serial.print(F("[WiFi] CIPSERVER timeout retry "));
Serial.println(wfRetry);
Serial.print(F("[WiFi] RX: ["));
Serial.print(esp01Buf);
Serial.println(F("]"));
if (wfRetry >= WF_MAX_RETRY) {
wfTimer = now;
wfState = WF_ERROR;
return;
}
espSend("AT+CIPSERVER=1,8080");
wfTimer = now;
esp01Buf = "";
}
break;
case WF_ERROR:
// 每30s尝试重新初始化一次
if (now - wfTimer > 30000) {
Serial.println(F("[WiFi] Auto retry after 30s..."));
wifiInit();
wfTimer = now;
}
break;
default: break;
}
}
每个状态设 4000ms 超时 + 最多3次重试,失败后进 WF_ERROR,30s 后自动重启
3.4 三级告警非阻塞蜂鸣器 FSM
FSM将蜂鸣器行为离散化,三个状态构成闭环。buzTick()被主循环高频调用,利用millis()获取绝对时间戳,与上次状态切换的时间比较,满足条件时才允许状态迁移
enum BuzState { BUZ_IDLE, BUZ_ON, BUZ_OFF };
struct BuzFSM {
BuzState state = BUZ_IDLE;
uint8_t beepsDone = 0;
uint8_t beepsReq = 0;
uint32_t timer = 0;
uint16_t onMs = 80;
uint16_t offMs = 120;
uint32_t cooldown = 0;
uint16_t coolMs = 3000;
};
uint8_t evalAlarmLevel() {
float t = emaTemp;
float p = emaPres / 100.0f;
uint8_t lv = 0;
if (t >= alm.tempWarn3) lv = max(lv, (uint8_t)3);
else if (t >= alm.tempWarn2) lv = max(lv, (uint8_t)2);
else if (t >= alm.tempWarn1) lv = max(lv, (uint8_t)1);
if (p <= alm.presLow3) lv = max(lv, (uint8_t)3);
else if (p <= alm.presLow2) lv = max(lv, (uint8_t)2);
else if (p <= alm.presLow1) lv = max(lv, (uint8_t)1);
if (p >= alm.presHigh3) lv = max(lv, (uint8_t)3);
else if (p >= alm.presHigh2) lv = max(lv, (uint8_t)2);
else if (p >= alm.presHigh1) lv = max(lv, (uint8_t)1);
return lv;
}
uint16_t almColor(uint8_t lv) {
if (lv == 3) return COL_WARN3;
if (lv == 2) return COL_WARN2;
if (lv == 1) return COL_WARN1;
return 0;
}
void buzTick() {
if (almMuted || almLevel == 0) {
if (buz.state != BUZ_IDLE) {
// 使用 analogWrite(0) 释放 STM32 PWM 定时器通道
analogWrite(BUZZER_PIN, 0);
buz.state = BUZ_IDLE;
}
if (almMuted) return; // 若处于静音状态,直接返回拦截后续触发
}
uint32_t now = millis();
switch (buz.state) {
case BUZ_IDLE:
if (almLevel > 0 && now >= buz.cooldown) {
buz.beepsReq = almLevel;
buz.onMs = (almLevel == 3) ? 60 : 90;
buz.offMs = (almLevel == 3) ? 60 : 130;
buz.coolMs = (almLevel == 1) ? 4000 :
(almLevel == 2) ? 2500 : 1200;
buz.beepsDone = 0;
buz.state = BUZ_ON;
buz.timer = now;
analogWrite(BUZZER_PIN, BUZZER_VOLUME); // 启动 PWM
}
break;
case BUZ_ON:
if (now - buz.timer >= buz.onMs) {
analogWrite(BUZZER_PIN, 0); // 核心修复:替换 digitalWrite
buz.beepsDone++;
buz.state = BUZ_OFF;
buz.timer = now;
}
break;
case BUZ_OFF:
if (buz.beepsDone >= buz.beepsReq) {
buz.cooldown = now + buz.coolMs;
buz.state = BUZ_IDLE;
} else if (now - buz.timer >= buz.offMs) {
analogWrite(BUZZER_PIN, BUZZER_VOLUME);
buz.state = BUZ_ON;
buz.timer = now;
}
break;
}
}
这种“轮询-判断-执行”模式避免了delay()带来的CPU空转,使蜂鸣器能与采样、显示等其他任务并行执行
3.5 双标志驱动精确局部刷新
// ── 顶栏 ─────────────────────────────────────────
static const char *pageTitle[] = {
"BMP180 Station", "Temperature", "Pressure", "Altitude"
};
void drawTopBar(bool full) {
if (!full) {
// 局部刷新:页码点 + WiFi状态点 + 告警角标
for (int8_t i = 0; i < PAGE_CNT; i++) {
int16_t dx = 152 + i * 11;
uint16_t dc = (i == curPage) ? cText() : cMuted();
tft.fillCircle(dx, 14, (i == curPage) ? 4 : 2, dc);
}
// WiFi 状态点
uint16_t wCol = (wfState == WF_READY) ? (wfClientOK ? cA1() : cA3()) :
(wfState == WF_ERROR) ? COL_WARN3 : cMuted();
tft.fillCircle(233, 14, 4, wCol);
// 告警角标
if (almLevel > 0) {
tft.fillTriangle(200, 20, 207, 7, 214, 20, almColor(almLevel));
tft.setFont(NULL); tft.setTextSize(1);
tft.setTextColor(0x0000);
tft.setCursor(204, 11);
tft.print(almLevel);
} else {
tft.fillTriangle(200, 20, 207, 7, 214, 20, cCard());
}
return;
}
tft.fillRect(0, 0, SCR_W, TOP_H, cCard());
tft.drawFastHLine(0, TOP_H - 1, SCR_W, cBorder());
uint16_t barCol[] = { cA1(), cA1(), cA2(), cA3() };
tft.fillRect(0, 4, 3, TOP_H - 8, barCol[curPage]);
tft.setFont(&FreeSans9pt7b);
tft.setTextColor(cText());
tft.setCursor(10, 20);
tft.print(pageTitle[curPage]);
// 页码指示点
for (int8_t i = 0; i < PAGE_CNT; i++) {
int16_t dx = 152 + i * 11;
uint16_t dc = (i == curPage) ? cText() : cMuted();
tft.fillCircle(dx, 14, (i == curPage) ? 4 : 2, dc);
}
// WiFi 状态:绿=就绪有客户端 青=就绪无客户端 灰=初始化中 红=错误
uint16_t wCol = (wfState == WF_READY) ? (wfClientOK ? cA1() : cA3()) :
(wfState == WF_ERROR) ? COL_WARN3 : cMuted();
tft.fillCircle(233, 14, 4, wCol);
// 告警角标
if (almLevel > 0) {
tft.fillTriangle(200, 20, 207, 7, 214, 20, almColor(almLevel));
tft.setFont(NULL); tft.setTextSize(1);
tft.setTextColor(0x0000);
tft.setCursor(204, 11);
tft.print(almLevel);
}
}
// ── 底栏 ─────────────────────────────────────────
void drawBottomBar(bool full) {
if (!full) return;
tft.fillRect(0, BOT_Y, SCR_W, BOT_H, cCard());
tft.drawFastHLine(0, BOT_Y, SCR_W, cBorder());
tft.setFont(NULL); tft.setTextSize(1);
tft.setTextColor(cMuted());
tft.setCursor(8, BOT_Y + 18);
tft.print("TURN:page SW:theme"); // 去掉 /mute,图标代替
// 静音图标放在底栏右侧 x=172,垂直居中
int16_t mx = 172, my = BOT_Y + 5;
tft.fillRect(mx - 2, my, 20, 18, cCard()); // 先擦
uint16_t spkCol = almMuted ? COL_WARN2 : cMuted();
// 喇叭主体(小矩形)
tft.fillRect(mx, my + 5, 5, 7, spkCol);
// 扩音三角(右侧三条边)
tft.drawLine(mx + 5, my + 5, mx + 11, my + 2, spkCol);
tft.drawLine(mx + 5, my + 11, mx + 11, my + 15, spkCol);
tft.drawLine(mx + 11, my + 2, mx + 11, my + 15, spkCol);
// 静音斜线
if (almMuted) {
tft.drawLine(mx, my + 15, mx + 14, my + 2, COL_WARN3);
tft.drawLine(mx + 1, my + 15, mx + 15, my + 2, COL_WARN3);
}
// 数据状态点
tft.fillCircle(230, BOT_Y + 14, 4, dataOK ? cA3() : cMuted());
}
顶栏底栏在非切换帧完全不触碰;内容区按精确脏区更新,每帧 SPI 传输量极小,无全屏闪烁
系统流程图

BMP180库 API
11个校准系数:BMP180 出厂时每颗芯片在不同温度压力下测试,将误差补偿系数写入片内 E²PROM(0xAA~0xBF),再结合原始ADC采样值进行浮点运算,才能得到精确的物理气压和温度值
int32_t Adafruit_BMP085::readPressure(void) {
int32_t UT, UP, B3, B5, B6, X1, X2, X3, p;
uint32_t B4, B7;
UT = readRawTemperature();
UP = readRawPressure();
#if BMP085_DEBUG == 1
// use datasheet numbers!
UT = 27898;
UP = 23843;
ac6 = 23153;
ac5 = 32757;
mc = -8711;
md = 2868;
b1 = 6190;
b2 = 4;
ac3 = -14383;
ac2 = -72;
ac1 = 408;
ac4 = 32741;
oversampling = 0;
#endif
B5 = computeB5(UT);
#if BMP085_DEBUG == 1
Serial.print("X1 = ");
Serial.println(X1);
Serial.print("X2 = ");
Serial.println(X2);
Serial.print("B5 = ");
Serial.println(B5);
#endif
// do pressure calcs
B6 = B5 - 4000;
X1 = ((int32_t)b2 * ((B6 * B6) >> 12)) >> 11;
X2 = ((int32_t)ac2 * B6) >> 11;
X3 = X1 + X2;
B3 = ((((int32_t)ac1 * 4 + X3) << oversampling) + 2) / 4;
#if BMP085_DEBUG == 1
Serial.print("B6 = ");
Serial.println(B6);
Serial.print("X1 = ");
Serial.println(X1);
Serial.print("X2 = ");
Serial.println(X2);
Serial.print("B3 = ");
Serial.println(B3);
#endif
X1 = ((int32_t)ac3 * B6) >> 13;
X2 = ((int32_t)b1 * ((B6 * B6) >> 12)) >> 16;
X3 = ((X1 + X2) + 2) >> 2;
B4 = ((uint32_t)ac4 * (uint32_t)(X3 + 32768)) >> 15;
B7 = ((uint32_t)UP - B3) * (uint32_t)(50000UL >> oversampling);
#if BMP085_DEBUG == 1
Serial.print("X1 = ");
Serial.println(X1);
Serial.print("X2 = ");
Serial.println(X2);
Serial.print("B4 = ");
Serial.println(B4);
Serial.print("B7 = ");
Serial.println(B7);
#endif
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;
#if BMP085_DEBUG == 1
Serial.print("p = ");
Serial.println(p);
Serial.print("X1 = ");
Serial.println(X1);
Serial.print("X2 = ");
Serial.println(X2);
#endif
p = p + ((X1 + X2 + (int32_t)3791) >> 4);
#if BMP085_DEBUG == 1
Serial.print("p = ");
Serial.println(p);
#endif
return p;
}
写0xF4=0x34+(oss<<6)触发气压转换,等26ms,读24bit UP,7步补偿算法
四、项目结果演示
4.1 操作流程
①硬件上电与初始化

上电,显示启动画面(WiFi热点信息/串口配置),约2秒进入主界面;主界面显示三路数据卡片 + 右侧趋势三角形 + 底部阈值提示行
②手机/电脑连接WiFi

在手机WiFi设置中搜索BMP180_Station,密码12345678;打开TCP调试工具(iOS系统App Store安装NetWork Kit);新建TCP Client连接:192.168.4.1 端口:8080

③基本数据查看

顺时针旋转编码器,翻页到温度详情(蓝色圆弧+历史波形);继续旋转:气压详情(橙色)→ 海拔详情(紫色)→ 回主界面
④主题切换与静音功能

短按编码器:深色/亮色主题切换,全屏重绘;长按编码器500ms:静音/取消静音,底栏喇叭图标切换
⑤远程指令控制
手机连WiFi BMP180_Station,Network Kit 连接 192.168.4.1:8080,发送 m1 远程静音,t1:28 降低告警阈值测试告警触发,pg:1 远程切页

手心捂住BMP180:约10~15秒后温度升到31°C,蜂鸣器单鸣,顶栏出现告警三角
4.2 视频演示
基于零知派标准板 + BMP180的 IoT 气象监控系统
完整演示基于零知派标准板(STM32F103RBT6)的 IoT 气象监控系统。演示内容包括:上电启动序列、主界面三路数据实时显示、旋转编码器多页切换(含水平滑入动画)、温度/气压/海拔三个详情页的圆弧仪表盘与历史波形、深色/亮色主题切换、手捂传感器触发三级蜂鸣告警(含顶栏角标变化)、长按静音操作、手机连接WiFi热点并通过Network Kit发送短指令远程调阈值和控制页面
五、工作原理讲解
BMP180 在单芯片内集成了压阻式 MEMS 传感单元、温度敏感电阻、24bit ADC 和校准 E²PROM,通过 I²C 固定地址 0x77 与主控通信

5.1 关键寄存器映射
| 地址 | 名称 | 说明 |
|---|---|---|
| 0xD0 | chip_id | 固定值 0x55,用于识别芯片 |
| 0xE0 | soft_reset | 写 0xB6 触发软复位 |
| 0xF4 | ctrl_meas | bit7:6=OSS过采样,写触发转换命令 |
| 0xF3 | status | bit3=SCO,转换进行中为1 |
| 0xF6~0xF7 | out_msb/lsb | 温度原始值 UT(16bit) |
| 0xF6~0xF8 | out_msb/lsb/xlsb | 气压原始值 UP(19bit) |
| 0xAA~0xBF | calib_data | 11个16bit校准系数(AC1~MD) |

过采样设置
本项目使用默认 BMP085_ULTRAHIGHRES,oss=3

I2C 传输地址
BMP180 模块地址如下所示,器件地址的最低有效位用于区分读(1)和写(0)操作,对应读地址 0xEF、写地址 0xEE

I2C 传输协议
起始后主机发 7 位从机地址 + 1 位 R/W 读写位(就是前文 BMP180 地址最低位);从机被寻址后,在第 9 个 SCL 时钟拉低 SDA 回复 ACK 应答

5.2 温度补偿气压计算流程
①温度测量
微控制器向BMP180的控制寄存器(地址0xF4)写入0x2E,启动温度测量,等待4.5ms数据有效时间(超低功耗模式),从0xF6 0xF7读取16位原始温度值UT

- T=(B5+8)/16×10(单位:0.1°C,除以10转为℃)
②压力测量
向控制寄存器写入启动命令0x34 + (OSS << 6)(OSS过采样率移位后填入高两位bit7~bit6),等待基于OSS值对应的时间(4.5ms至25.5ms)后,从0xF6、0xF7、0xF8三字节读取20位原始压力值UP(高16位+低4位)。再结合B5和校准系数计算真实压力P(单位Pa)

- P=(B7×2)/B4×100/2
③海拔高度换算
在得到准确的大气压P(单位Pa)后,利用国际标准大气压公式推算相对于海平面的高度

P0 为海平面标准大气压,通常设为101325Pa(即1013.25hPa)
5.3 旋转编码器使用原理
本项目用格雷码查表法消除机械抖动误判:将前后状态各2bit拼成4bit索引,查16元素表直接获得方向(+1/-1/0),单次查表无分支,极适合中断
| 顺时针状态序列 | 索引 | 结果 |
|---|---|---|
| 11→01→00→10 | 对应表项 | +1 |
| 逆时针反向 | 对应表项 | -1 |
| 抖动(11→10→11) | 对应表项 | 0 |
EC11 输出两路相差 90° 的正交方波(A相/CLK,B相/DT)
六、常见问题解答(FAQ)
Q1:编译报错 invalid conversion from 'const uint8_t*' to 'uint8*'
A:这是 Adafruit BusIO 与零知派 SoftWire 的 API 兼容问题。修改 Adafruit_I2CDevice.cpp:两处 _wire->write 调用去掉返回值判断并加 (uint8_t*) 强转;requestFrom 去掉第三参数;Adafruit_SPIDevice.cpp 的 buffer 版 transfer 改为 for 循环
Q2:Network Kit 发指令无响应或崩溃
A:①确认连接热点 BMP180_Station 后 IP 获取成功;②TCP 连接到 192.168.4.1:8080;③指令末尾加换行符(Network Kit 设置发送后自动追加 \n);④查看串口调试信息,观察 [CMD] payload: 打印的内容是否正确
项目资源整合
Adafruit BMP085/BMP180 库: adafruit/Adafruit-BMP085-Library
BMP180 数据手册: BMP180.pdf
更多推荐


所有评论(0)