2026/10/29 17:16:09

LPS25/LPS22气压传感器:选型、寄存器驱动与Arduino/树莓派实战

LPS25/LPS22气压传感器:选型、寄存器驱动与Arduino/树莓派实战 LPS25 和 LPS22 这两颗气压传感器在创客项目里的出场率一直不低——桌面气象站、无人机定高模块、可穿戴楼层计甚至一些室内的定位实验都能看到它们的身影。它们便宜、体积小、I2C 直接能读看起来接上线就能出数但真正把它用稳的人并不多。我见过太多情况数据能读出来可数值一直不动能动了但跳得厉害不跳了一算高度又飘到离谱。问题往往不在代码写得多烂而在于没搞清这两颗芯片的工作方式以及 MEMS 气压计对物理环境有多敏感。这篇内容面向的是已经上手或准备上手 LPS25/LPS22 系列气压传感器的开发者包括用 Arduino 做原型的硬件爱好者也包括用树莓派跑 Python 采集脚本的数据玩家。我会把两颗芯片的差异、寄存器层面的读取逻辑、Arduino 与 Python 两套可直接抄的裸寄存器驱动、以及实际踩过的坑一条条摊开讲。文中代码不依赖任何第三方驱动库读完你就能自己判断读出来的数到底对不对。1. 两颗芯片的差异在哪选型时先看这三件事型号放在一起看很容易觉得它们差不多都是 260 到 1260 hPa 的绝对压力量程都是 I2C/SPI 双接口都有 32 帧 FIFO都能顺带测温度。但真正落到项目里选错一颗的代价可能是结构要重开、功耗预算超标或者精度怎么调都差一口气。选型时最该盯的不是谁的参数更漂亮而是这三件事精度与噪声的匹配度、封装与开孔工艺的可行性、以及采样节奏和功耗的配合。1.1 量程、精度与噪声参数表背后的真实含义先看量程。260 到 1260 hPa 这个区间覆盖了从海拔约 5000 米到海平面以下的绝大部分日常场景做室内、户外、无人机都没问题。要注意的是它是绝对压力测量不是表压所以读数是大气压本身而不是比环境高多少。这一点决定了你后面算高度必须自己选基准芯片不会帮你做。精度方面两颗的绝对精度典型值都在 ±0.1 hPa 量级换算成高度大约是 ±0.8 米。这个数字看起来很美但它成立有前提温度在芯片的补偿范围内、供电干净、没有明显的板级应力。实际在自制板上我通常按 ±0.3 到 ±0.5 hPa 来预估留够余量。噪声才是真正拉开体验差距的地方。官方给出的指标是用等效高度噪声表示的单位是厘米级。这个参数直接决定了你能分辨多小的气压变化如果噪声当量是 6 cm那你做楼层检测时把阈值设在 3 米就是安全的如果实际噪声因为电源纹波翻了三倍阈值就得往上抬。很多人抱怨高度一直抖本质是把理论噪声当成了实际噪声。对比项LPS25HLPS22HB压力量程260–1260 hPa260–1260 hPa绝对精度典型±0.1 hPa±0.1 hPa输出分辨率24 位4096 LSB/hPa24 位4096 LSB/hPa噪声水平官方给出厘米级高度当量低噪声模式下更优同为厘米级输出速率 ODR1 / 7 / 12.5 / 25 Hz1 / 10 / 25 / 50 / 75 Hz温度输出12 位480 LSB/°C16 位100 LSB/°C封装尺寸2.5 × 2.5 × 1.0 mm2.0 × 2.0 × 0.76 mmFIFO 深度32 帧32 帧工作电压1.7–3.6 V1.7–3.6 VI2C 地址0x5C / 0x5D0x5C / 0x5D表格里的数值是典型值不同批次的器件手册可能有微调做正式产品时一定要拿对应版本的手册核对一遍。但趋势是清楚的LPS22HB 是更新的一代封装更小、ODR 档位更细、多了可配置的低通滤波适合对体积和噪声都敏感的场景LPS25H 的 ODR 档位更适合低功耗间歇采样做电池供电的长期监测更顺手。1.2 封装开孔与贴片工艺MEMS 气压计最容易被忽略的物理前提这一条我必须单独拎出来说因为它毁掉的项目比代码 bug 多得多。这类传感器是带开孔的 MEMS 器件压力敏感膜上方有一个进压孔。孔的对面就是那层几微米厚的硅膜任何东西堵住它读数就会卡死在一个值附近而且不会报错。常见的三种堵法一是贴片时助焊剂飞溅残留在孔口形成一层薄膜二是结构设计时在上面盖了不透气的双面胶或者灌了密封胶三是外壳打孔位置偏了或者孔径太小、孔道太长导致压力响应严重滞后。第三种最隐蔽因为静态读数是对的只有气压快速变化时才暴露——比如电梯里上下楼你的曲线会明显拖后。正确做法是PCB 布局时在传感器正上方的外壳开一个直径不小于 1 mm 的通孔孔道尽量短贴片后用无水乙醇配合软毛刷轻扫孔口别用超声波清洗超声能量可能直接打坏硅膜组装时确保孔周围有一圈透气间隙不要用胶完全包死。我见过一个案例整机装配完成后气压读数比实际低了 40 hPa拆开一看是防水透气膜贴反了换了方向立刻恢复。1.3 三种典型场景下的取舍逻辑桌面气象站 / 室内环境监测采样频率要求不高1 Hz 足够两颗都能胜任。这时候优先选 LPS22HB因为它有更细的 ODR 档位和可配置低通滤波能直接把高频噪声压掉一部分省得后面写软件滤波。电池供电的长期记录仪核心矛盾是功耗。这时 LPS25H 更合适——它的单次测量模式配合 1 Hz 甚至更低的唤醒节奏平均电流可以压到微安级。配合 FIFO 攒够数据再唤醒主控读取能进一步省电。无人机 / 高度变化检测需要快速响应ODR 要往高了设。LPS22HB 的 75 Hz 和低噪声模式在这里优势明显而且封装小、重量轻对载具影响小。但要注意高 ODR 下噪声也会上升得配合滤波一起用。选型没有绝对答案把我需要多快、多准、多省电这三个问题先回答清楚答案基本就出来了。2. 寄存器层面的工作方式气压读数是怎么一步步出来的很多人用现成的库readPressure()一调就出数感觉这芯片很简单。但一旦库不工作、或者数值明显不对就完全无从下手。原因在于这类传感器不是上电就吐数据的它有一整套状态机上电复位、配置速率、等待转换、读状态位、拼字节、换算。任何一步走岔读出来的都是垃圾。这一章把这套流程拆开。2.1 上电后的第一件事确认 WHO_AM_I所有 I2C 器件的调试都应该从这一条开始。LPS25H 的 WHO_AM_I 寄存器在 0x0F返回值是0xBDLPS22HB 同样在 0x0F返回0xB1。读不到这两个值后面的所有努力都是白费。读不到通常有三种原因地址错了、总线没通、器件没供电。地址方面两颗芯片都用 SA0/SDO 引脚决定最低位接低电平是 0x5C接高电平是 0x5D。很多模块板上这个引脚悬空导致地址不确定——悬空在 CMOS 输入上是个大忌电平会随机漂。拿到一块新模块先跑一遍 I2C 扫描确认 0x5C 和 0x5D 哪个有响应比盲猜快得多。还有一种情况是器件被锁在了 SPI 模式。这两颗芯片的片选引脚CS如果被拉低芯片会进入 SPI 状态此时 I2C 完全不响应。所以用 I2C 时CS 必须接高电平。我就栽过这个跟头模块上 CS 和 GND 之间有个默认贴片电阻看着像上拉其实是下拉找了两个小时才发现。2.2 ODR、BDU 和单次测量采样节奏决定了噪声与功耗ODR 是输出数据率配置在控制寄存器 1 的高三位。LPS25H 的档位是 1 / 7 / 12.5 / 25 HzLPS22HB 是 1 / 10 / 25 / 50 / 75 Hz。设置 ODR 之前必须先让芯片进入工作状态——LPS25H 的控制寄存器 1 最高位是 PDPower Down置 1 才会开始转换LPS22HB 没有独立的 PD 位ODR 非零就代表工作。BDUBlock Data Update这个位特别值得说。它的作用是当一组数据的高字节和低字节还没被完整读走时禁止刷新输出寄存器。不设 BDU你可能会读到新的高字节 旧的低字节这种撕裂组合表现为偶尔蹦出一个离谱的值。我从第一次用这类传感器起就把 BDU 打开再没遇到过野值。单次测量模式在低功耗场景很有用。做法是把 ODR 设为 0、PD 设为 0然后置位 ONE_SHOT 触发一次转换等状态位显示数据就绪后读走芯片自动回到待机。这种按需唤醒的节奏比一直开着转换省电一个数量级。2.3 24 位补码的拼接顺序与 4096 LSB/hPa 的换算压力输出是三个字节顺序是XL最低、L、H最高小端排列。正确的拼接方式是int32_t raw ((int32_t)buf[2] 16) | ((int32_t)buf[1] 8) | buf[0];注意这里千万不能写成大端顺序也别把 buf[0] 当成高字节。我见过不少自己写驱动的代码在这里翻车症状是气压值看起来很稳定但完全对不上真实值。换算系数是4096 LSB/hPa也就是float pressure_hPa raw / 4096.0f;为什么是 4096因为满量程 260–1260 hPa 是 1000 hPa 跨度而 24 位输出的有效位数和内部 ADC 的增益共同决定了这个比例。实际使用时不需要推导记住 4096 就行。还有一个隐藏细节多字节连续读的地址自增方式两颗芯片不一样。LPS25H 要求把子地址的最高位置 1即reg | 0x80芯片才会在连续读时自动递增寄存器指针LPS22HB 则是通过控制寄存器 2 的 IF_ADD_INC 位bit4来控制。这一点如果不注意LPS25H 会一直返回同一个寄存器的值你读到的三个字节其实完全一样换算出来的结果自然莫名其妙。2.4 温度通道不是附属品它决定了绝对精度温度输出紧跟在压力后面LPS25H 从 0x2B 开始两个字节LPS22HB 从 0x29 开始。但两者的格式完全不同LPS25H12 位有符号放在 16 位的低 12 位里需要先右移 4 位再按T 42.5 raw / 480换算。LPS22HB16 位有符号直接按T raw / 100换算。这个差异如果搞混温度会差出几十度但气压读数看起来还正常——因为气压的内部补偿是芯片自己做的用的是内部温度不依赖你读出来的这个值。所以很多人干脆忽略温度直到做温度补偿或者故障判断时才发现读出来是错的。我的建议是始终把温度一起读出来并记录。它有三个用处一是判断芯片是否处于正常工作温区二是做零点漂移的长期观察三是当气压数据出现异常时温度往往能提供线索比如芯片被附近功率器件烤热了温度会明显偏高气压也会跟着偏移。3. Arduino 端裸寄存器驱动不装库也能从零跑通依赖第三方库最大的问题是出问题时你只能去翻库的源码。而这类传感器的驱动其实很短自己写一遍后面调试会轻松太多。这一章给出一套完整的、不依赖任何库的实现AVR 和 ESP32 平台都能跑只要把 I2C 引脚接对。3.1 接线、地址与总线上拉接线只有四根VCC、GND、SDA、SCL。供电范围 1.7–3.6 V如果你用的是 5 V 主控务必确认模块板上有没有电平转换或者 LDO——直接灌 5 V 大概率烧芯片。多数模块板支持 3.3 V 和 5 V 输入是因为板上带了稳压但传感器本体仍然是 3.3 V 域。上拉电阻是另一个必须确认的点。很多模块板已经焊了 4.7 kΩ 或 10 kΩ 上拉如果你再接一块带同样上拉的模块并联后阻值会变小某些情况下反而影响信号质量。总线长度超过 20 cm 时建议把速率降到 100 kHz或者把上拉减小到 2.2 kΩ 增强驱动能力。地址由 SA0 引脚决定。模块上通常有丝印标注没标的就用扫描程序确认。#include Wire.h void scanBus() { for (uint8_t addr 1; addr 127; addr) { Wire.beginTransmission(addr); if (Wire.endTransmission() 0) { Serial.print(found 0x); Serial.println(addr, HEX); } } }扫描到 0x5C 或 0x5D就说明器件在线。3.2 最小驱动的四个函数整个驱动只需要四个函数写寄存器、读单字节、连续读多字节、以及一个初始化。核心难点在连续读——前面说过LPS25H 需要在子地址最高位置 1。#include Wire.h static uint8_t ADDR 0x5C; static uint8_t CHIP 0; // 0未知 1LPS25H 2LPS22HB void regWrite(uint8_t reg, uint8_t val) { Wire.beginTransmission(ADDR); Wire.write(reg); Wire.write(val); Wire.endTransmission(); } uint8_t regRead(uint8_t reg) { Wire.beginTransmission(ADDR); Wire.write(reg); Wire.endTransmission(false); Wire.requestFrom((uint8_t)ADDR, (uint8_t)1); return Wire.available() ? Wire.read() : 0xFF; } void regReadBurst(uint8_t reg, uint8_t *buf, uint8_t len) { Wire.beginTransmission(ADDR); if (CHIP 1) reg | 0x80; // LPS25H子地址最高位置 1 才自增 Wire.write(reg); Wire.endTransmission(false); Wire.requestFrom((uint8_t)ADDR, len); for (uint8_t i 0; i len Wire.available(); i) { buf[i] Wire.read(); } }Wire.endTransmission(false)里的false很关键它表示发送完寄存器地址后不发送停止位而是保持总线占用紧接着发起读操作。这是 I2C 的重复起始机制。如果这里写成true很多平台上会读不到数据因为器件认为事务已经结束。3.3 自动识别型号并输出气压、温度与相对高度把识别逻辑和读取逻辑串起来就成了一个能同时兼容两颗芯片的完整程序bool sensorInit() { uint8_t who regRead(0x0F); if (who 0xBD) { CHIP 1; regWrite(0x21, 0x00); // CTRL_REG2清 FIFO 配置 regWrite(0x20, 0xB4); // PD1, ODR12.5Hz, BDU1 } else if (who 0xB1) { CHIP 2; regWrite(0x11, 0x10); // CTRL_REG2IF_ADD_INC1 regWrite(0x10, 0x32); // ODR25Hz, BDU1 } else { return false; } delay(10); return true; } bool readPT(float p_hPa, float t_c) { uint8_t buf[5]; uint8_t reg (CHIP 1) ? 0x28 : 0x26; regReadBurst(reg, buf, 5); int32_t rawP ((int32_t)buf[2] 16) | ((int32_t)buf[1] 8) | buf[0]; p_hPa rawP / 4096.0f; if (CHIP 1) { int16_t rawT (int16_t)(((uint16_t)buf[4] 8) | buf[3]); rawT 4; // 取高 12 位 t_c 42.5f rawT / 480.0f; } else { int16_t rawT (int16_t)(((uint16_t)buf[4] 8) | buf[3]); t_c rawT / 100.0f; } return true; } float toAltitude(float p_hPa, float p0_hPa) { return 44330.0f * (1.0f - pow(p_hPa / p0_hPa, 1.0f / 5.255f)); }这段代码里有两个细节值得注意。第一rawP用int32_t接收但实际 260–1260 hPa 对应的最大值约 516 万远小于 2^23所以不会出现负数用有符号类型只是为了移位安全。第二温度拼接必须先转成uint16_t再拼否则符号位会在移位时被污染。主循环里加上滤波和输出void loop() { float p, t; if (readPT(p, t)) { float alt toAltitude(p, 1013.25f); Serial.print(p, 2); Serial.print( hPa, ); Serial.print(t, 2); Serial.print( C, ); Serial.print(alt, 2); Serial.println( m); } delay(500); }3.4 采样节奏与串口输出的取舍串口打印本身是有成本的。在 AVR 平台上Serial.print一个浮点数大约要几毫秒如果循环里没有延时你会把大量时间花在格式化和发送上采样节奏完全被串口拖住。更麻烦的是串口输出会引入电源波动而这类传感器对电源噪声相当敏感你会看到气压值随打印频率出现周期性抖动。我的做法是采集和输出分离。用一个固定周期的定时器或者millis()差值判断来控制采样采样结果存进一个环形缓冲区主循环慢慢往外打印。如果只是调试把输出降到 1 秒一次并在两次输出之间做几次采样取平均数据会干净很多。另外 ESP32 平台上有个坑默认的Wire库对时钟拉伸支持不完整某些批次的传感器在转换期间会拉低 SCL导致总线卡死。解决办法是把 I2C 速率降到 100 kHz 并加上超时重试Wire.setTimeOut(50); // 单位毫秒ESP32 核心支持AVR 平台没有这个接口可以自己维护一个失败计数连续失败超过阈值就重新初始化总线。4. 树莓派加 Python把散点数据变成能判断趋势的曲线Arduino 适合做实时控制和原型验证但要做长时间记录、趋势分析、甚至接个网页看曲线树莓派加 Python 更顺手。这一章给出一套能同时兼容两颗芯片的 Python 读取类以及配套的采集、落盘和滤波方案。4.1 I2C 使能与总线权限树莓派默认关闭 I2C需要在配置工具里打开。打开之后用i2cdetect确认设备出现在总线上sudo apt install -y i2c-tools python3-smbus sudo raspi-config nonint do_i2c 0 i2cdetect -y 1看到5c或5d就说明通了。如果显示UU表示该地址已被内核驱动占用需要把对应的驱动从/boot/config.txt里注释掉或者换用i2c-dev的裸访问方式。普通用户访问/dev/i2c-1需要加入i2c组sudo usermod -aG i2c $USER重新登录后生效。别图省事一直用sudo跑脚本权限问题会被掩盖后面部署成服务时又要重新踩一遍。4.2 一个能同时兼容两颗芯片的读取类Python 侧我用smbus2的i2c_msg接口因为它能明确控制重复起始比read_i2c_block_data更贴近底层也更稳。import time from smbus2 import SMBus, i2c_msg class Lps2x: WHO {0xBD: LPS25H, 0xB1: LPS22HB} REG { LPS25H: dict(ctrl10x20, ctrl20x21, press_xl0x28), LPS22HB: dict(ctrl10x10, ctrl20x11, press_xl0x26), } def __init__(self, bus1, addr0x5C): self.i2c SMBus(bus) self.addr addr who self._read(0x0F, 1)[0] self.chip self.WHO.get(who) if self.chip is None: raise RuntimeError(fWHO_AM_I0x{who:02X}未识别的器件) self.r self.REG[self.chip] self._configure() def _read(self, reg, n): if self.chip LPS25H and n 1: reg | 0x80 w i2c_msg.write(self.addr, [reg]) r i2c_msg.read(self.addr, n) self.i2c.i2c_rdwr(w, r) return list(r) def _write(self, reg, val): self.i2c.write_byte_data(self.addr, reg, val) def _configure(self): if self.chip LPS25H: self._write(self.r[ctrl2], 0x00) self._write(self.r[ctrl1], 0xB4) # PD1, 12.5Hz, BDU1 else: self._write(self.r[ctrl2], 0x10) # IF_ADD_INC1 self._write(self.r[ctrl1], 0x32) # 25Hz, BDU1 time.sleep(0.02) def read(self): b self._read(self.r[press_xl], 5) raw_p (b[2] 16) | (b[1] 8) | b[0] p raw_p / 4096.0 if self.chip LPS25H: raw_t int.from_bytes(bytes([b[3], b[4]]), little, signedTrue) 4 t 42.5 raw_t / 480.0 else: raw_t int.from_bytes(bytes([b[3], b[4]]), little, signedTrue) t raw_t / 100.0 return p, t_read里那句reg | 0x80就是 LPS25H 的地址自增开关只有在读多字节时才需要加。_configure里两颗芯片写入的值也不一样别抄错。4.3 长时间采集与 CSV 落盘长期记录最怕的是脚本跑崩了数据全丢。我一般用边采边写、立即刷盘的方式即使进程被 kill已经写下的数据还在import csv, time from datetime import datetime def log_loop(sensor, path, interval1.0, durationNone): t0 time.time() with open(path, a, newline) as f: w csv.writer(f) if f.tell() 0: w.writerow([timestamp, pressure_hPa, temperature_C]) while True: if duration and time.time() - t0 duration: break try: p, t sensor.read() w.writerow([datetime.now().isoformat(timespecseconds), f{p:.3f}, f{t:.2f}]) f.flush() except OSError as e: print(读取失败稍后重试:, e) time.sleep(1.0) time.sleep(interval)OSError这个捕获不能省。I2C 总线在长线、干扰或者器件瞬间掉电时会出现读失败一次异常就退出会让整晚的数据白采。捕获之后等一秒重试通常能自动恢复。4.4 滤波中值加滑动平均的组合拳气压数据的噪声是双峰的大部分时间