02601 · Infineon DPS310 · 精密气压/温度/海拔传感器

02601 DPS310 精密气压高度传感器
兼容 STEMMA QT / Qwiic 完整技术资料

这块DPS310气压模块接到 ESP32-2432S028R(2.8"黄屏/CYD)上,做成一个仿机械气压表风格的仪表盘UI:表盘开机自动以第一次真实读数为中心、上下各留30hPa摆动,不管装在什么海拔都能正常显示;下方两张卡片显示海拔和温度。这份资料整理了芯片规格、接线方法、完整固件代码,以及两轮真实排查记录——一次是模块虚焊导致I2C检测不到,另一次是固件本身发现的"总线异常时显示假数据"问题。2026-07-24:虚焊重新加热修复、固件两处bug修复后,已实物验证气压/温度/海拔读数正常、数值自洽。

1

总览

Overview

DPS310 是 Infineon 出的高精度数字气压传感器,同时能测气压和温度,再用气压值反推海拔高度,常见于无人机定高、气象站、户外手表这类应用。这次把它接到 ESP32-2432S028R(俗称"CYD",板载2.8寸TFT屏)上,屏幕上画一个仿经典机械气压表的表盘(底部留缺口的270°→实际240°扫描指针),指针实时指向当前气压,下面两张卡片分别显示气压数值、海拔和温度。

DPS310

  • Infineon 数字气压传感器(气压+温度)
  • I2C / SPI 双接口可选
  • I2C地址 0x77(默认)/ 0x76(SDO拉低)
  • STEMMA QT / Qwiic 免焊接连接
  • 板载LDO,支持3~5V供电

ESP32-2432S028R(CYD)

  • ESP32-D0WD-V3 双核
  • 板载2.8" ST7789 TFT,240×320
  • CN1扩展口:GND/IO22/IO27/3V3
  • 本项目把CN1当I2C用(SCL/SDA)
  • USB芯片CH340,PlatformIO直接烧录

气压表UI

  • 仿机械气压表表盘,底部留缺口
  • 红/白双色箭头指针,指向实时气压
  • 气压数值卡片(橙色,单位hPa)
  • 海拔+温度卡片(绿色两行)
  • 位带SPI手写驱动,不依赖图形库
2

DPS310 详解

精密数字气压/温度传感器

DPS310 是 Infineon(英飞凌)出的低功耗数字气压传感器,内部集成压阻式气压感应元件和24位ADC,直接通过总线读出已经补偿好的气压(hPa)和温度(℃),库里还提供了根据气压反推海拔的 readAltitude() 函数。

项目参数
测量量气压(hPa)+ 温度(℃),可反推海拔(米)
气压量程典型 300~1200 hPa(据Infineon官方数据手册,覆盖海平面到约9000米高空)
精度相对精度典型 ±0.002 hPa(约±5cm等效高度分辨率),绝对精度典型 ±1 hPa(据数据手册,实际以官方最新版本为准)
总线接口I2C 或 SPI,由模块上 CS 引脚接法决定(本资料用I2C,STEMMA QT版CS内部已处理好)
I2C地址默认 0x77;SDO引脚拉低时为 0x76。本资料固件开机时两个地址都会探测一遍
供电芯片本身工作电压 1.7~3.6V,Adafruit STEMMA QT款板载LDO稳压,VIN支持3~5V输入
封装/接口引脚面板丝印标识:VIN / 3Vo / GND / SCK / SDI / SDO / CS,两侧各带一个STEMMA QT连接器

⚠ VIN 才是电源输入,3Vo 是稳压输出,别接反

VIN 是供电输入脚(3~5V均可,板载LDO会稳压到3.3V给芯片用);3Vo 是LDO稳压后的输出脚,用来给其它3.3V小外设供电用的,不能当作本模块的电源输入。接反的话芯片拿不到电,I2C总线自然什么都扫不到。


CS / SDO 悬空即可(I2C模式)

Adafruit这款STEMMA QT版DPS310板载电路已经处理好了默认I2C模式,只需要接 VIN / GND / SCK(SCL) / SDI(SDA) 四根线,CSSDO 悬空不用管,芯片默认走I2C地址0x77。只有需要切到SPI模式,或者想把I2C地址改成0x76时才需要额外接SDO。

3

ESP32 CYD 详解

ESP32-2432S028R · 2.8" 黄屏

圈内俗称"CYD"(Cheap Yellow Display),主控是ESP32-D0WD-V3双核芯片,板载一块2.8寸ST7789驱动的TFT彩屏(240×320,SPI接口),还带一个CN1四针扩展口,本资料只用了屏幕显示 + CN1接I2C模块这两个功能,跟本工作区里的HC-SR04、I2C扫描、BMM150指南针几个项目共用同一套板子和引脚约定。

项目参数
主控ESP32-D0WD-V3,双核,自带Wi-Fi/蓝牙(本项目未使用)
屏幕2.8" ST7789,240×320,SPI接口,位带(bit-bang)手写驱动
CN1扩展口GND / IO22 / IO27 / 3V3 四针,本资料把 IO22 当SCL、IO27 当SDA 用
USB芯片CH340,Windows下识别为USB-SERIAL CH340
烧录方式PlatformIO命令行 pio run --target upload,或Arduino IDE

CN1 引脚复用提醒

CN1上的IO22/IO27是通用GPIO,本资料当I2C(SCL/SDA)用;同一块板子在别的项目里(HC-SR04测距、I2C扫描、BMM150指南针)也会用到这两个脚,同一时间只能接一种模块,换项目前记得先拔掉旧模块。


⚠ 同一块板子换USB口,COM口号会变

Windows下这块板子的串口号(COMx)会随着USB口/插拔顺序变化,烧录前建议先用 Get-CimInstance Win32_PnPEntity | Where-Object { $_.Name -match "COM\d+" } 确认一下当前口号,再改 platformio.ini 里的 upload_port

4

接线

仅4根线,I2C模式

只用到4根线,DPS310模块接到CYD的CN1扩展口上;CS、SDO两个引脚悬空不用接。

DPS310 模块ESP32 CYD · CN1

VIN3V3
GNDGND
SCKIO22
SDIIO27
CS、SDO 悬空即可(默认I2C模式,地址0x77);千万不要把电源接到 3Vo(那是输出脚,不是输入)

怎么确认地址是不是0x77

接好线之后先跑一遍I2C地址扫描工具(代码见 ESP32 2.8黄屏\AI开发记录\src\main.ino,跟本项目共用同一块CYD板子),屏幕上会直接显示扫到的7位地址。默认应该是0x77;如果SDO被接地会变成0x76,两种情况本资料第6节代码都做了自动探测,不用改代码。

5

气压表UI设计

仿机械气压表风格 · 开机自适应量程 · ✅ 已实物验证

跟纯数字仪表比,气压变化本身很缓慢(一天之内通常只有几个hPa波动),单纯甩一串数字不直观,所以这里仿照传统机械气压表做了一个指针表盘:表盘不是整圆,底部留一个120°的缺口(像大多数真实气压表那样),指针沿240°弧线摆动。指针红色箭头指向当前气压,白色尾部指向反方向,正中心一个黄色轴心圆点。表盘下方两张卡片:橙色大字卡片显示气压数值(hPa),绿色卡片显示海拔(m)和温度(℃)两行。

表盘量程为什么是"开机自适应"而不是写死950~1050

气压表常见的950~1050 hPa量程是按海平面气压设计的,但DPS310读到的是站点气压(不做海拔修正的原始值),海拔越高站点气压越低——实测这台设备所在位置站点气压常年在750~950 hPa区间,如果表盘量程写死950起步,指针会一直卡在最左端不动,看起来像坏了。所以改成开机拿到第一次真实读数后,以该读数为中心(四舍五入取整)、上下各留30 hPa,这样不管装在什么海拔,指针都能落在表盘中段正常摆动。


I2C扫描确认DPS310应答地址0x77
实拍 · 虚焊重新加热修复后,I2C扫描工具确认能稳定扫到0x77(详见第7节调试记录)

指针角度换算(自适应版)

  • 表盘范围:开机第一次读数 ± 30 hPa(共60 hPa跨度)
  • 角度范围:-120°~+120°(共240°,正上方为0°)
  • deg = -120 + (hpa-gaugeMinHpa)/60 * 240
  • 超出范围自动限幅(clamp到0~1),刻度/端点数字随中心值实时计算
  • 每帧只清空指针占用的内圆,表盘外圈/刻度不用重画,减少闪烁

实拍(早期版本)· 这段视频拍摄于表盘量程还写死950~1050时,实测气压771 hPa低于下限,指针被卡在最左侧"950"附近不动——这正是第7节调试记录里发现并修复的表盘量程bug,保留在这里作为bug现场记录。

修复后:实拍 vs 渲染版

修复后用手机实拍过自适应表盘运行画面(气压932 hPa,表盘自动以924为中心,指针落在中段),跟摄像头拍屏幕常见的摩尔纹/反光问题一样,直接实拍不够清晰。所以照本工作区里 U1205 BMM150 项目用过的老办法(实拍有摩尔纹时不重拍,改用代码逐帧渲染一版干净的演示),按固件里 drawDial/drawNeedle/drawPressureCard/drawEnvCard 的坐标和配色,用Python(PIL)逐像素还原了一版"渲染版",下面这张静态截图和动画用的都是实拍时的同一组真实数据(932 hPa、表盘居中924、温度27.3℃,海拔按标准大气压公式反算约699.5m)。


渲染版气压表UI静态截图
渲染版 · 静态截图,数据取自实拍读数(932 hPa / 表盘居中924)
渲染版 · 指针在中心值±18hPa范围内摆动演示,渲染脚本见 07_视频与网页资料\scripts\render_barometer_ui.py

实测数据自洽性验证

实测过多组读数,例如 938 hPa → ALT 653m:代入标准大气压公式 44330 × (1 - (938/1013.25)^0.1903) 反算约646m,跟屏幕显示的653m基本吻合(几米误差是手动验算精度问题);上面渲染版用的932 hPa读数反算约699.5m,同样自洽,说明气压→海拔的换算和显示是正确的。

6

完整代码

PlatformIO · Arduino (ESP32) ✅ 编译/烧录/实物读数均已验证

用 PlatformIO + Arduino核心,官方 Adafruit_DPS310 库连同 Adafruit_Sensor / Adafruit_BusIO 两个依赖的源码一起直接拷进了工程的 lib/ 目录,编译不用联网装库。

代码在本机的位置

下面这份代码就是工程里实际跑的那份,文件在 06_AI开发记录\src\main.ino(同目录 platformio.ini 是编译配置,lib\Adafruit_DPS310\lib\Adafruit_Sensor\lib\Adafruit_BusIO\ 是拷进来的库源码),用 VS Code 打开 06_AI开发记录 这个文件夹,PlatformIO 会自动识别。


已验证 ✅

pio run 编译成功(Flash占用约23.6%,RAM约6.7%),pio run --target upload 烧录到COM20成功,真实DPS310模块的气压/温度/海拔读数已验证正常、数值自洽(见第5节末尾)。这是修复表盘量程bug、修复"总线异常显示假数据"bug之后的最终版本,比第7节调试记录里最早烧录的那版多了两处修复,详见第7节。


platformio.ini
[env:esp32dev]
platform = espressif32@6.13.0
board = esp32dev
framework = arduino
; Arduino-ESP32 核心3.x(ESP-IDF5)在这台机器上链接报错:
;   undefined reference to `esp_bt_controller_mem_release'
; 是工具链BT库版本不匹配的已知问题,跟代码无关。固定用2.0.17这个稳定核心版本绕开。
; (跟 U1205 BMM150 / ESP32 2.8黄屏\AI开发记录 里踩过的坑是同一个,写法照抄。)
platform_packages =
    framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32/releases/download/2.0.17/esp32-2.0.17.zip
upload_port = COM20
monitor_port = COM20
monitor_speed = 115200

src/main.ino
// DPS310 精密气压/温度/海拔传感器 · ESP32-2432S028 (2.8" 黄屏/CYD) · 板载TFT气压表UI
// I2C: SCL=IO22, SDA=IO27 (CN1接口,跟HC-SR04/I2C扫描/BMM150指南针工程共用同一组引脚)
// DPS310 STEMMA QT/Qwiic 版默认I2C地址0x77,若SDO拉低则为0x76,开机时两个都探测一遍。
// 屏幕驱动(位带SPI ST7789 + 5x7点阵字库 + 画圆/线/三角形等图元)照抄BMM150指南针工程里
// 已验证过的 main.ino,没有改动驱动部分;在此基础上把"指南针表盘"换成"气压表表盘"。
// 传感器读数用官方 Adafruit_DPS310 库(连同 Adafruit_Sensor / Adafruit_BusIO 依赖一起
// 拷贝到本工程 lib/ 目录下,不依赖联网下载)。
#include <Wire.h>
#include <math.h>
#include "soc/gpio_struct.h"
#include "Adafruit_DPS310.h"

#define I2C_SCL_PIN 22
#define I2C_SDA_PIN 27

#define LCD_W 240
#define LCD_H 320

// ESP32-2432S028 / CYD 2.8 inch ST7789 internal LCD pins.
#define PIN_SCK  14
#define PIN_SDA  13
#define PIN_CS   15
#define PIN_DC   2
#define PIN_BLK  21

#define SCK_MASK (1UL << PIN_SCK)
#define SDA_MASK (1UL << PIN_SDA)
#define CS_MASK  (1UL << PIN_CS)
#define DC_MASK  (1UL << PIN_DC)
#define BLK_MASK (1UL << PIN_BLK)

#define FAST_HIGH(mask) (GPIO.out_w1ts = (mask))
#define FAST_LOW(mask)  (GPIO.out_w1tc = (mask))

#define SCK_0 FAST_LOW(SCK_MASK)
#define SCK_1 FAST_HIGH(SCK_MASK)
#define SDA_0 FAST_LOW(SDA_MASK)
#define SDA_1 FAST_HIGH(SDA_MASK)
#define CS_0  FAST_LOW(CS_MASK)
#define CS_1  FAST_HIGH(CS_MASK)
#define DC_0  FAST_LOW(DC_MASK)
#define DC_1  FAST_HIGH(DC_MASK)
#define BLK_1 FAST_HIGH(BLK_MASK)

// 16位 RGB565 颜色
#define BLACK    0x0000
#define WHITE    0xFFFF
#define RED      0xF800
#define GREEN    0x07E0
#define BLUE     0x001F
#define CYAN     0x07FF
#define YELLOW   0xFFE0
#define GREY     0x4208
#define DKGREY   0x2104
#define ORANGE   0xFD20
#define NAVY     0x000C
#define SKYBLUE  0x2D7C
#define PANEL_BG 0x18E3   // 深蓝灰,卡片底色
#define RING_BG  0x0841   // 表盘底色,比纯黑亮一点点,有层次感

static const uint8_t FONT[][6] = {
  {' ', 0x00, 0x00, 0x00, 0x00, 0x00},
  {':', 0x00, 0x36, 0x36, 0x00, 0x00},
  {'.', 0x00, 0x60, 0x60, 0x00, 0x00},
  {'-', 0x08, 0x08, 0x08, 0x08, 0x08},
  {'0', 0x3E, 0x51, 0x49, 0x45, 0x3E},
  {'1', 0x00, 0x42, 0x7F, 0x40, 0x00},
  {'2', 0x42, 0x61, 0x51, 0x49, 0x46},
  {'3', 0x21, 0x41, 0x45, 0x4B, 0x31},
  {'4', 0x18, 0x14, 0x12, 0x7F, 0x10},
  {'5', 0x27, 0x45, 0x45, 0x45, 0x39},
  {'6', 0x3C, 0x4A, 0x49, 0x49, 0x30},
  {'7', 0x01, 0x71, 0x09, 0x05, 0x03},
  {'8', 0x36, 0x49, 0x49, 0x49, 0x36},
  {'9', 0x06, 0x49, 0x49, 0x29, 0x1E},
  {'A', 0x7E, 0x11, 0x11, 0x11, 0x7E},
  {'B', 0x7F, 0x49, 0x49, 0x49, 0x36},
  {'C', 0x3E, 0x41, 0x41, 0x41, 0x22},
  {'D', 0x7F, 0x41, 0x41, 0x22, 0x1C},
  {'E', 0x7F, 0x49, 0x49, 0x49, 0x41},
  {'F', 0x7F, 0x09, 0x09, 0x09, 0x01},
  {'G', 0x3E, 0x41, 0x49, 0x49, 0x7A},
  {'H', 0x7F, 0x08, 0x08, 0x08, 0x7F},
  {'I', 0x00, 0x41, 0x7F, 0x41, 0x00},
  {'K', 0x7F, 0x08, 0x14, 0x22, 0x41},
  {'L', 0x7F, 0x40, 0x40, 0x40, 0x40},
  {'M', 0x7F, 0x02, 0x0C, 0x02, 0x7F},
  {'N', 0x7F, 0x04, 0x08, 0x10, 0x7F},
  {'O', 0x3E, 0x41, 0x41, 0x41, 0x3E},
  {'P', 0x7F, 0x09, 0x09, 0x09, 0x06},
  {'R', 0x7F, 0x09, 0x19, 0x29, 0x46},
  {'S', 0x46, 0x49, 0x49, 0x49, 0x31},
  {'T', 0x01, 0x01, 0x7F, 0x01, 0x01},
  {'U', 0x3F, 0x40, 0x40, 0x40, 0x3F},
  {'V', 0x1F, 0x20, 0x40, 0x20, 0x1F},
  {'W', 0x3F, 0x40, 0x38, 0x40, 0x3F},
  {'X', 0x63, 0x14, 0x08, 0x14, 0x63},
  {'x', 0x44, 0x28, 0x10, 0x28, 0x44},
  {'Y', 0x07, 0x08, 0x70, 0x08, 0x07},
  {'Z', 0x61, 0x51, 0x49, 0x45, 0x43},
};

static void spiSendByte(uint8_t value) {
  for (uint8_t i = 0; i < 8; i++) {
    SCK_0;
    if (value & 0x80) { SDA_1; } else { SDA_0; }
    value <<= 1;
    SCK_1;
    SCK_0;
  }
}

static void sendCmd(uint8_t value) { DC_0; spiSendByte(value); }
static void sendData(uint8_t value) { DC_1; spiSendByte(value); }

static void setWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
  sendCmd(0x2A);
  sendData(x0 >> 8); sendData(x0 & 0xFF); sendData(x1 >> 8); sendData(x1 & 0xFF);
  sendCmd(0x2B);
  sendData(y0 >> 8); sendData(y0 & 0xFF); sendData(y1 >> 8); sendData(y1 & 0xFF);
  sendCmd(0x2C);
}

static void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
  if (w <= 0 || h <= 0) return;
  if (x < 0) { w += x; x = 0; }
  if (y < 0) { h += y; y = 0; }
  if (x >= LCD_W || y >= LCD_H || w <= 0 || h <= 0) return;
  if (x + w > LCD_W) w = LCD_W - x;
  if (y + h > LCD_H) h = LCD_H - y;
  setWindow(x, y, x + w - 1, y + h - 1);
  for (uint32_t i = 0; i < (uint32_t)w * h; i++) {
    sendData(color >> 8);
    sendData(color & 0xFF);
  }
}

static void fillScreen(uint16_t color) {
  setWindow(0, 0, LCD_W - 1, LCD_H - 1);
  for (uint32_t i = 0; i < (uint32_t)LCD_W * LCD_H; i++) {
    sendData(color >> 8);
    sendData(color & 0xFF);
  }
}

static const uint8_t *glyphFor(char c) {
  for (uint8_t i = 0; i < sizeof(FONT) / sizeof(FONT[0]); i++) {
    if (FONT[i][0] == c) return &FONT[i][1];
  }
  return &FONT[0][1];
}

static void drawCharBg(uint16_t x, uint16_t y, char c, uint16_t color, uint16_t bg, uint8_t scale) {
  const uint8_t *g = glyphFor(c);
  uint16_t w = 6 * scale;
  uint16_t h = 8 * scale;
  setWindow(x, y, x + w - 1, y + h - 1);
  for (uint8_t py = 0; py < h; py++) {
    uint8_t row = py / scale;
    for (uint8_t px = 0; px < w; px++) {
      uint8_t col = px / scale;
      bool on = false;
      if (col < 5 && row < 7) on = g[col] & (1 << row);
      uint16_t out = on ? color : bg;
      sendData(out >> 8);
      sendData(out & 0xFF);
    }
  }
}

static void drawTextBg(uint16_t x, uint16_t y, const char *text, uint16_t color, uint16_t bg, uint8_t scale) {
  while (*text) {
    drawCharBg(x, y, *text++, color, bg, scale);
    x += 6 * scale;
  }
}

static void drawText(uint16_t x, uint16_t y, const char *text, uint16_t color, uint8_t scale) {
  drawTextBg(x, y, text, color, BLACK, scale);
}

static uint16_t textWidth(const char *text, uint8_t scale) {
  return strlen(text) * 6 * scale;
}

// 居中画一行字,center x 是文字整体的水平中心点
static void drawTextCentered(uint16_t centerX, uint16_t y, const char *text, uint16_t color, uint16_t bg, uint8_t scale) {
  uint16_t w = textWidth(text, scale);
  uint16_t x = (centerX > w / 2) ? centerX - w / 2 : 0;
  drawTextBg(x, y, text, color, bg, scale);
}

// ---------------- 基础图形:点/线/圆/填充圆/三角形 ----------------

static void setPixel(int16_t x, int16_t y, uint16_t color) {
  if (x < 0 || y < 0 || x >= LCD_W || y >= LCD_H) return;
  fillRect(x, y, 1, 1, color);
}

static void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color) {
  int16_t dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
  int16_t dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
  int16_t err = dx + dy, e2;
  for (;;) {
    setPixel(x0, y0, color);
    if (x0 == x1 && y0 == y1) break;
    e2 = 2 * err;
    if (e2 >= dy) { err += dy; x0 += sx; }
    if (e2 <= dx) { err += dx; y0 += sy; }
  }
}

// 稍微加粗的线:主线 + 垂直方向各偏移1px,指针/刻度看起来不会细得像发丝
static void drawThickLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color) {
  drawLine(x0, y0, x1, y1, color);
  float dx = x1 - x0, dy = y1 - y0;
  float len = sqrtf(dx * dx + dy * dy);
  if (len < 1) return;
  int16_t ox = (int16_t)roundf(-dy / len);
  int16_t oy = (int16_t)roundf(dx / len);
  drawLine(x0 + ox, y0 + oy, x1 + ox, y1 + oy, color);
  drawLine(x0 - ox, y0 - oy, x1 - ox, y1 - oy, color);
}

static void drawCircle(int16_t cx, int16_t cy, int16_t r, uint16_t color) {
  int16_t x = r, y = 0, err = 0;
  while (x >= y) {
    setPixel(cx + x, cy + y, color); setPixel(cx + y, cy + x, color);
    setPixel(cx - y, cy + x, color); setPixel(cx - x, cy + y, color);
    setPixel(cx - x, cy - y, color); setPixel(cx - y, cy - x, color);
    setPixel(cx + y, cy - x, color); setPixel(cx + x, cy - y, color);
    y++;
    if (err <= 0) { err += 2 * y + 1; }
    if (err > 0) { x--; err -= 2 * x + 1; }
  }
}

static void fillCircle(int16_t cx, int16_t cy, int16_t r, uint16_t color) {
  for (int16_t dy = -r; dy <= r; dy++) {
    int16_t dx = (int16_t)sqrtf((float)(r * r - dy * dy));
    fillRect(cx - dx, cy + dy, 2 * dx + 1, 1, color);
  }
}

// 圆角矩形卡片(角是简单裁掉的斜角,不是真正圆弧,够用且不慢)
static void fillRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, uint16_t color) {
  fillRect(x + r, y, w - 2 * r, h, color);
  fillRect(x, y + r, r, h - 2 * r, color);
  fillRect(x + w - r, y + r, r, h - 2 * r, color);
  for (int16_t dy = 0; dy <= r; dy++) {
    int16_t dx = (int16_t)sqrtf((float)(r * r - dy * dy));
    fillRect(x + r - dx, y + r - dy, dx, 1, color);
    fillRect(x + w - r, y + r - dy, dx, 1, color);
    fillRect(x + r - dx, y + h - r + dy - 1, dx, 1, color);
    fillRect(x + w - r, y + h - r + dy - 1, dx, 1, color);
  }
}

// 实心三角形(用于指针箭头),扫描线填充
static void fillTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint16_t color) {
  if (y0 > y1) { int16_t t; t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; }
  if (y1 > y2) { int16_t t; t = y1; y1 = y2; y2 = t; t = x1; x1 = x2; x2 = t; }
  if (y0 > y1) { int16_t t; t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; }
  if (y0 == y2) return;
  for (int16_t y = y0; y <= y2; y++) {
    bool secondHalf = y > y1 || y1 == y0;
    int16_t xa = x0 + (int32_t)(x2 - x0) * (y - y0) / (y2 - y0);
    int16_t xb;
    if (secondHalf) {
      xb = (y1 == y2) ? x2 : x1 + (int32_t)(x2 - x1) * (y - y1) / (y2 - y1);
    } else {
      xb = (y0 == y1) ? x1 : x0 + (int32_t)(x1 - x0) * (y - y0) / (y1 - y0);
    }
    if (xa > xb) { int16_t t = xa; xa = xb; xb = t; }
    fillRect(xa, y, xb - xa + 1, 1, color);
  }
}

static void tftInit() {
  pinMode(PIN_SCK, OUTPUT);
  pinMode(PIN_SDA, OUTPUT);
  pinMode(PIN_CS, OUTPUT);
  pinMode(PIN_DC, OUTPUT);
  pinMode(PIN_BLK, OUTPUT);

  SCK_1; CS_1; BLK_1;
  delay(100);
  CS_0;

  sendCmd(0x11);
  delay(120);
  sendCmd(0x3A); sendData(0x05);
  sendCmd(0xC5); sendData(0x1A);
  sendCmd(0x36); sendData(0x00);
  sendCmd(0xB2); sendData(0x05); sendData(0x05); sendData(0x00); sendData(0x33); sendData(0x33);
  sendCmd(0xB7); sendData(0x05);
  sendCmd(0xBB); sendData(0x3F);
  sendCmd(0xC0); sendData(0x2C);
  sendCmd(0xC2); sendData(0x01);
  sendCmd(0xC3); sendData(0x0F);
  sendCmd(0xC4); sendData(0x20);
  sendCmd(0xC6); sendData(0x01);
  sendCmd(0xD0); sendData(0xA4); sendData(0xA1);
  sendCmd(0xE8); sendData(0x03);
  sendCmd(0xE9); sendData(0x09); sendData(0x09); sendData(0x08);

  sendCmd(0xE0);
  sendData(0xD0); sendData(0x05); sendData(0x09); sendData(0x09);
  sendData(0x08); sendData(0x14); sendData(0x28); sendData(0x33);
  sendData(0x3F); sendData(0x07); sendData(0x13); sendData(0x14);
  sendData(0x28); sendData(0x30);

  sendCmd(0xE1);
  sendData(0xD0); sendData(0x05); sendData(0x09); sendData(0x09);
  sendData(0x08); sendData(0x03); sendData(0x24); sendData(0x32);
  sendData(0x32); sendData(0x3B); sendData(0x14); sendData(0x13);
  sendData(0x28); sendData(0x2F);

  sendCmd(0x20); // this 2.8" ST7789 panel needs INVOFF, not INVON
  sendCmd(0x29);
  CS_0;
}

// ---------------- 气压表 UI 布局 ----------------
// 表盘圆心/半径,往下留够两张数据卡片(气压数值 + 海拔/温度)的空间
#define CX        120
#define CY        124
#define R_OUTER   54
#define R_NEEDLE  (R_OUTER - 16)   // 指针红色箭头长度,必须小于下面的清屏半径
#define R_TAIL    18               // 指针白色尾部
#define R_CLEAR   (R_OUTER - 14)   // 每帧只清空这个半径内的区域来擦掉旧指针,刻度圈不用重画

// 仿经典机械气压表:表盘不是整圆,底部留个缺口,指针在表盘范围内摆动。
// 表盘范围不写死成海平面气压(950~1050hPa)——站点气压跟海拔强相关,高海拔地区
// 实测气压可能只有7~800多hPa,写死950下限会导致指针一直卡在最左端。改成开机
// 拿到第一次真实读数后,以该读数为中心(四舍五入取整)、上下各留30hPa,这样不管
// 装在什么海拔的地方,指针都能落在表盘中段正常摆动。
#define GAUGE_SPAN_HPA  60.0f
#define GAUGE_START_DEG (-120)   // 缺口左端(左下方)
#define GAUGE_END_DEG   (120)    // 缺口右端(右下方),总扫过 240 度

Adafruit_DPS310 dps;
bool dpsFound = false;
float gaugeCenterHpa = 1013.25f;  // 开机拿到第一次真实读数后会被重新赋值

static float gaugeMinHpa() { return gaugeCenterHpa - GAUGE_SPAN_HPA / 2.0f; }
static float gaugeMaxHpa() { return gaugeCenterHpa + GAUGE_SPAN_HPA / 2.0f; }

static float pressureToDeg(float hpa) {
  float t = (hpa - gaugeMinHpa()) / GAUGE_SPAN_HPA;
  if (t < 0) t = 0;
  if (t > 1) t = 1;
  return GAUGE_START_DEG + t * (GAUGE_END_DEG - GAUGE_START_DEG);
}

static void polar(float deg, float r, int16_t *x, int16_t *y) {
  float rad = deg * PI / 180.0f;
  float s = sinf(rad), c = -cosf(rad);
  *x = CX + (int16_t)(s * r);
  *y = CY + (int16_t)(c * r);
}

static void drawDial() {
  // 表盘底色 + 双层外圈,比单线圆更有"仪表"质感(只画缺口范围内的弧)
  fillCircle(CX, CY, R_OUTER + 3, DKGREY);
  fillCircle(CX, CY, R_OUTER, RING_BG);

  int16_t px, py;
  int16_t steps = 60;
  for (int16_t i = 0; i <= steps; i++) {
    float deg = GAUGE_START_DEG + (float)i / steps * (GAUGE_END_DEG - GAUGE_START_DEG);
    polar(deg, R_OUTER, &px, &py);
    setPixel(px, py, SKYBLUE);
    polar(deg, R_OUTER + 3, &px, &py);
    setPixel(px, py, GREY);
  }

  // 以表盘中心值(开机时第一次真实读数)为基准,每10hPa一个刻度,共7个刻度
  // (中心±30/±20/±10/中心),两端和正中间三个刻度用黄色高亮
  for (int i = -3; i <= 3; i++) {
    float hpa = gaugeCenterHpa + i * 10.0f;
    float deg = pressureToDeg(hpa);
    bool major = (i == -3 || i == 0 || i == 3);
    int16_t rOut = R_OUTER - 2;
    int16_t rIn = major ? (rOut - 10) : (rOut - 5);
    uint16_t tickColor = major ? YELLOW : SKYBLUE;
    int16_t x0, y0, x1, y1;
    polar(deg, rIn, &x0, &y0);
    polar(deg, rOut, &x1, &y1);
    drawLine(x0, y0, x1, y1, tickColor);
  }

  char buf[16];
  snprintf(buf, sizeof(buf), "%d", (int)(gaugeCenterHpa + 0.5f));
  drawTextCentered(CX, CY - R_OUTER - 15, buf, CYAN, BLACK, 1);
  int16_t lx, ly, rx, ry;
  polar(GAUGE_START_DEG, R_OUTER + 14, &lx, &ly);
  polar(GAUGE_END_DEG, R_OUTER + 14, &rx, &ry);
  snprintf(buf, sizeof(buf), "%d", (int)(gaugeMinHpa() + 0.5f));
  drawTextCentered(lx, ly - 3, buf, WHITE, BLACK, 1);
  snprintf(buf, sizeof(buf), "%d", (int)(gaugeMaxHpa() + 0.5f));
  drawTextCentered(rx, ry - 3, buf, WHITE, BLACK, 1);
}

// 只清空指针占用的内圆区域,表盘外圈/刻度/字母不用每次重画,减少闪烁
static void clearNeedleArea() {
  fillCircle(CX, CY, R_CLEAR, RING_BG);
}

static void drawNeedle(float hpa) {
  float deg = pressureToDeg(hpa);
  float rad = deg * PI / 180.0f;
  float s = sinf(rad), c = -cosf(rad);
  int16_t tipX = CX + (int16_t)(s * R_NEEDLE);
  int16_t tipY = CY + (int16_t)(c * R_NEEDLE);
  int16_t tailX = CX - (int16_t)(s * R_TAIL);
  int16_t tailY = CY - (int16_t)(c * R_TAIL);

  // 指针尾部(灰白)
  drawThickLine(CX, CY, tailX, tailY, WHITE);

  // 指针头部画成三角箭头(红色),比直线更像真实气压表指针
  float perpX = -c, perpY = s;
  float baseX = CX + s * (R_NEEDLE - 14);
  float baseY = CY + c * (R_NEEDLE - 14);
  int16_t leftX  = (int16_t)(baseX + perpX * 5);
  int16_t leftY  = (int16_t)(baseY + perpY * 5);
  int16_t rightX = (int16_t)(baseX - perpX * 5);
  int16_t rightY = (int16_t)(baseY - perpY * 5);
  fillTriangle(tipX, tipY, leftX, leftY, rightX, rightY, RED);
  drawThickLine(CX, CY, (int16_t)baseX, (int16_t)baseY, RED);

  fillCircle(CX, CY, 4, YELLOW);
  drawCircle(CX, CY, 4, DKGREY);
}

#define CARD1_Y (CY + R_OUTER + 18)
#define CARD1_H 32
#define CARD2_Y (CARD1_Y + CARD1_H + 8)
#define CARD2_H 56

// 表盘(drawDial)不在这里画——表盘中心值要等开机拿到第一次真实读数后才知道,
// 见 setup() 里 drawDial() 的调用时机。
static void drawStaticChrome() {
  fillScreen(BLACK);
  fillRect(0, 0, LCD_W, 34, NAVY);
  drawTextCentered(LCD_W / 2, 9, "DPS310 BAROMETER", CYAN, NAVY, 2);
  fillRoundRect(8, CARD1_Y, LCD_W - 16, CARD1_H, 6, PANEL_BG);
  fillRoundRect(8, CARD2_Y, LCD_W - 16, CARD2_H, 6, PANEL_BG);
}

static void showStatus(const char *msg, uint16_t color) {
  fillRect(8, CARD1_Y, LCD_W - 16, CARD1_H, PANEL_BG);
  drawTextCentered(LCD_W / 2, CARD1_Y + (CARD1_H - 16) / 2, msg, color, PANEL_BG, 2);
}

static void drawPressureCard(float hpa) {
  fillRoundRect(8, CARD1_Y, LCD_W - 16, CARD1_H, 6, PANEL_BG);
  char buf[16];
  snprintf(buf, sizeof(buf), "%4d hPa", (int)(hpa + 0.5f));
  drawTextCentered(LCD_W / 2, CARD1_Y + (CARD1_H - 24) / 2, buf, ORANGE, PANEL_BG, 3);
}

// 海拔 + 温度两行,负数、超过三位数的读数都留够宽度不会挤在一起
static void drawEnvCard(float altitudeM, float tempC) {
  fillRect(10, CARD2_Y + 4, LCD_W - 20, CARD2_H - 8, PANEL_BG);
  char altBuf[16], tempBuf[16];
  dtostrf(altitudeM, 7, 1, altBuf);
  dtostrf(tempC, 6, 1, tempBuf);
  char line1[20], line2[20];
  snprintf(line1, sizeof(line1), "ALT %sm", altBuf);
  snprintf(line2, sizeof(line2), "TEMP %sC", tempBuf);
  drawTextBg(14, CARD2_Y + 8, line1, GREEN, PANEL_BG, 2);
  drawTextBg(14, CARD2_Y + 30, line2, GREEN, PANEL_BG, 2);
}

static bool findDPS310() {
  if (dps.begin_I2C(0x77, &Wire)) return true;
  if (dps.begin_I2C(0x76, &Wire)) return true;
  return false;
}

// DPS310数据手册规格:气压量程300~1200hPa,温度量程-40~85℃。读回来的数值
// 只要超出这个物理量程,就一定是I2C通信出了问题(模块被拔掉/接触不良/虚焊)读到的
// 垃圾字节,不是真实传感器数据——曾经实测遇到过"模块拔掉了屏幕却还显示
// TEMP 4999.9℃、ALT -47524m"这种情况,根源是getEvents()本身不检查总线读取
// 是否真正成功(库内部固定return true),必须在这一层自己加合理性校验。
static bool readingIsSane(float hpa, float tempC) {
  return hpa > 300.0f && hpa < 1200.0f && tempC > -40.0f && tempC < 85.0f;
}

// 连接/重连 + 校准表盘中心:开机第一次调用,以及运行中检测到断线/读数异常时
// 也会重新调用一遍,行为跟开机时完全一致(不会残留断线前的假数据)。
static void connectSensor() {
  showStatus("CONNECTING", YELLOW);
  while (!findDPS310()) {
    showStatus("DPS310 NOT FOUND", RED);
    Serial.println("dps310 init failed, retrying...");
    delay(1000);
  }
  dpsFound = true;

  dps.configurePressure(DPS310_64HZ, DPS310_64SAMPLES);
  dps.configureTemperature(DPS310_64HZ, DPS310_64SAMPLES);

  showStatus("CALIBRATING", YELLOW);
  sensors_event_t temp_event, pressure_event;
  while (!dps.temperatureAvailable() || !dps.pressureAvailable()) {
    delay(20);
  }
  dps.getEvents(&temp_event, &pressure_event);
  // 用第一次真实读数(四舍五入取整)当表盘中心,见 pressureToDeg 上方注释
  gaugeCenterHpa = (float)(int)(pressure_event.pressure + 0.5f);
  drawDial();

  showStatus("READY", GREEN);
  delay(400);
}

void setup() {
  Serial.begin(115200);
  tftInit();
  drawStaticChrome();
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  connectSensor();
}

void loop() {
  sensors_event_t temp_event, pressure_event;

  if (!dps.temperatureAvailable() || !dps.pressureAvailable()) {
    delay(20);
    return;
  }

  dps.getEvents(&temp_event, &pressure_event);
  float hpa = pressure_event.pressure;
  float tempC = temp_event.temperature;

  if (!readingIsSane(hpa, tempC)) {
    // 读到了物理上不可能的数值,当作断线处理:不显示这帧数据,重新走一遍
    // connectSensor()(会先卡在"DPS310 NOT FOUND"直到总线恢复正常)
    Serial.printf("reading out of sane range (pressure=%.2f temp=%.2f), "
                  "treating as disconnected\n", hpa, tempC);
    connectSensor();
    return;
  }

  float altitudeM = dps.readAltitude();

  clearNeedleArea();
  drawNeedle(hpa);
  drawPressureCard(hpa);
  drawEnvCard(altitudeM, tempC);

  Serial.printf("pressure=%.2f hPa  temp=%.2f C  altitude=%.2f m\n", hpa, tempC, altitudeM);
  delay(150);
}
7

调试记录:模块检不到I2C

一次完整的排查过程,供参考

固件写好、编译烧录都成功之后,实机测试发现DPS310模块检测不到(屏幕一直显示"DPS310 NOT FOUND")。下面是完整的排查过程,方法本身比结论更值得参考——遇到"模块检测不到"这类问题时,可以照这个顺序排查。

步骤操作 & 结果
① 固件专用探测先烧录本资料的DPS310固件,只尝试0x77/0x76两个地址,结果一直显示"NOT FOUND"——但这个结果本身不能区分"地址错了"还是"总线上根本没设备"。
② 全地址扫描换烧同一块CYD板子上已有的通用I2C扫描工具(ESP32 2.8黄屏\AI开发记录\src\main.ino,扫描1~126全部地址),结果0个设备——排除了"地址选错"这个可能,说明总线上确实没有任何设备应答。
③ 重新检查接线确认CN1排针顺序(GND-IO22-IO27-3V3)和模块引脚(VIN/GND/SCK/SDI)对应关系后重新接线,重新扫描——仍然0个设备。
④ 换用已知良好模块拔下DPS310,把此前已验证工作正常的BMM150模块接到同一组CN1排线上——BMM150能正常扫到地址(0x13)。这一步很关键:证明ESP32一侧、排线、CN1接口全部正常,问题被锁定在DPS310模块本身。
⑤ 供电检查(拍照确认)用户拍照确认:DPS310板上标注"ON"的绿色电源指示灯常亮,说明VIN/GND确实通电正常,板载LDO工作正常——排除了"完全没通电"这个可能。
阶段性结论供电正常 + ESP32/接线正常 + 全地址扫描仍为0个设备,三者叠加当时指向"DPS310芯片本身没有响应I2C总线"。用户说明这块模块的传感器芯片是翻新/拆机件,怀疑是坏片——但实际根因不是芯片损坏,见下面⑥。
⑥ 重新加热焊点用户判断是虚焊(芯片引脚跟PCB焊盘之间没有真正导通,而不是芯片本身损坏),重新给芯片焊盘加热返修。返修后立刻用全地址扫描工具复测,能稳定扫到0x77(见第5节实拍截图)——证明前面①~⑤的排查方向没错(确实是"总线上没设备应答"),只是根因是虚焊而不是坏片/翻新片。
⑦ 烧录正式固件,复测读数换回DPS310正式固件(非扫描工具),串口读到气压/温度/海拔数值,一开始怀疑是"开机热漂移"(气压757→771→825→868→903 hPa,温度26.6→27.4℃,持续爬升近20分钟),跟外部温湿度计(26.1℃)对比后发现传感器温度不降反升,跟单纯热平衡不符,判断另有原因。
⑧ 拔掉芯片做对照实验用户直接把DPS310从测试座上拔出,屏幕却仍在显示数据,且是TEMP 4999.9℃、ALT -47524m这种物理上不可能的数值——证实了固件存在"总线读取失败时仍显示假数据"的bug,而不是传感器本身还在漂移。
⑨ 定位代码问题检查 Adafruit_DPS310 库源码发现:getEvents() 函数不检查底层I2C读取是否真正成功,固定 return truetemperatureAvailable()/pressureAvailable() 只读一个状态位,总线异常时可能间歇性误判为"有新数据"。第7节⑦的持续大幅漂移,很可能就是这段时间连接仍不稳定、偶尔读到错误字节导致的,并非真的热漂移。
⑩ 修复并复测main.ino 里加了两处修复:数值合理性校验(气压300~1200hPa、温度-40~85℃之外的读数一律判定为断线,见第6节 readingIsSane())+ 断线自动重连(connectSensor())。重新烧录后插回芯片,读数(如938 hPa / ALT 653m)用标准大气压公式反算自洽(见第5节),确认问题已解决。

排查方法论小结(适用于任何I2C模块"检测不到/读数异常"的场景)

① 先用全地址扫描工具而不是"只测目标地址"的代码,区分"地址错"和"总线上无设备"两种完全不同的故障;② 检查供电是否正常(电源指示灯、或用万用表量VIN/GND电压);③ 有条件的话,用同一组接线/主控换一个已知良好的同类模块做交叉验证,能最快把故障范围从"主控+接线"还是"待测模块"两个大类里锁定到其中一个;④ 怀疑是虚焊时,重新加热焊点往往比直接判定芯片报废更值得先试;⑤ 读数看起来在"漂移"时,别急着归因为"传感器需要热平衡"——先用物理量程做合理性校验,再用"直接拔掉模块看还显不显示数据"这种对照实验,能快速区分"真实物理变化"和"固件在展示垃圾数据"。


✅ 最终状态

虚焊已通过重新加热修复,固件的表盘量程bug和"总线异常显示假数据"bug均已修复并重新烧录验证,气压/温度/海拔读数正常、多组数据自洽。第6节代码即为此最终版本。

8

常见问题排查

结合第7节调试记录整理
  • I2C扫描不到设备(全地址扫描也是0个):按第7节的顺序排查——先确认电源指示灯亮不亮(板载LDO正常工作的标志),再检查VIN/GND/SCK/SDI接线是否对应正确(不要把VIN错接成3Vo,3Vo是输出脚不是输入),最后有条件的话换一个已知良好的同类/其它I2C模块做交叉验证,缩小故障范围。
  • 换了良好模块交叉验证后依然0个设备:说明问题出在主控/接线一侧,重新检查CN1排针插接是否到底、IO22/IO27是否接反、杜邦线本身是否有断路。
  • 交叉验证证明主控/接线没问题,待测模块仍检测不到:先怀疑虚焊而不是直接判定芯片报废——重新给芯片引脚/焊盘加热返修一次,很多时候就能解决(本资料第7节⑥就是这么修好的),实在修不好再考虑换模块。
  • I2C扫到的地址是0x76而不是0x77:不是坏了,是模块SDO引脚被拉低了,本资料第6节代码里两个地址都会自动探测,不用改代码;如果用的不是本资料这份固件,记得把地址常量换成0x76。
  • 误把3Vo当作电源输入接:3Vo是板载LDO稳压后的输出脚,用来给其它3.3V外设供电,不是本模块的供电输入,见第2节说明。
  • 读数看起来在持续"漂移",一直不稳定:先别急着归因成"传感器还没热平衡"——用第2节的物理量程(气压300~1200hPa、温度-40~85℃)判断是否超出合理范围,超出就是总线读取出错而不是真实变化;如果怀疑固件在显示假数据,可以直接拔掉模块看屏幕还显不显示"数据",显示就说明代码没做断线检测(本资料第6节的readingIsSane() + connectSensor()就是为此加的),详见第7节⑦~⑩。
  • Adafruit_DPS310库的getEvents()返回true,但读数很离谱:这是库本身的已知限制——getEvents()不检查底层I2C读取是否真正成功,固定返回true,不能只凭返回值判断数据有效性,必须自己在应用层加合理性校验(见第6节代码 / 第7节⑨排查记录)。
  • PlatformIO编译报 undefined reference to 'esp_bt_controller_mem_release':默认拉取的Arduino-ESP32核心3.x(ESP-IDF5)跟本机工具链里的BT库版本不匹配,是已知问题,platformio.ini里已经用platform_packages固定了2.0.17这个稳定核心版本绕开。
  • Windows下编译/烧录报中文路径相关的错误:这是STM32(arm-none-eabi-ld)工具链的已知限制;本资料用的ESP32/xtensa工具链目前在中文路径下编译烧录正常,如果换成STM32项目遇到类似问题,需要把工程复制到纯英文路径下再编译。
  • 同一块CYD板子COM口对不上:换了USB口或者重新插拔,Windows分配的COM口号会变,烧录前用PowerShell命令重新确认一下,见第3节。
9

参考资料来源

官方数据手册 / 库源码

第2节的芯片规格参数来自以下公开资料,感兴趣可以查看原文获取更完整的电气特性表:

  • Adafruit DPS310 产品页 — adafruit.com/products/4494
  • Adafruit_DPS310 Arduino库源码 — GitHub,本资料第6节代码里的库就是拷贝自这里,依赖 Adafruit_BusIOAdafruit_Sensor 两个库
  • Infineon DPS310 芯片规格 — 可在 Infineon 官网搜索"DPS310"获取官方数据手册原文,本资料第2节的量程/精度数字以其为准