随着物联网技术向消费端和教育端快速渗透,低成本嵌入式气象监测节点的需求持续升温     

目录

一、系统接线部分

1.1 硬件清单

1.2 接线方案表

1.3 具体接线图

1.4 接线实物图

二、安装与使用部分

三、代码讲解部分

3.1 环形缓冲区 + 时序读取

3.2 编码器格雷码查表

3.3 ESP-01 非阻塞 AT 状态机

3.4 三级告警非阻塞蜂鸣器 FSM

3.5 双标志驱动精确局部刷新

四、项目结果演示

4.1 操作流程

4.2 视频演示

五、工作原理讲解

5.1 关键寄存器映射

5.2 温度补偿气压计算流程

5.3 旋转编码器使用原理

六、常见问题解答(FAQ)

Q1:编译报错 invalid conversion from 'const uint8_t*' to 'uint8*' 

Q2:Network Kit 发指令无响应或崩溃


项目概述

        本项目基于零知派标准板,主控芯片 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

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐