传感器与可信数据 / I²C 双传感器实验发布 内容更新

两个 I²C 设备地址一样怎么办?让两块 SHT40 各走一条总线

固定两块不可软件改址的0x44 SHT40,在S3上分别用Wire和Wire1采集;保留各自序列号、错误与恢复,避免扫描一次ACK就当双设备成功。

读完能做什么
得到两条总线上同地址传感器的完整采集程序,按通道核身份、当前有效性、错误与恢复,而不靠扫描地址数量猜成功。
开始之前
已完成P06扫描和S01单传感器读取,能核指定板GPIO、3.3V与桥USB。
所需器材
官方DevKitC-1 v1.1 / WROOM-1-N8R8,桥USB数据线、两块Adafruit SHT40 PID4885,可靠焊接排针/端子与≤20cm短线、每模块SDA支路低压SPST;无第二电源或5V上拉
版本组合
开发板:DevKitC-1 v1.1 N8R8,桥UART0/115200,CDC关闭工具链:Arduino CLI1.5.1 / core3.3.12;完整8MB/QIO/OPI FQBN驱动:Sensirion I2C SHT4x1.1.2 / Core0.7.3;两独立实例总线:Wire0 GPIO8/9;Wire1 GPIO17/18;各100kHz/0x44,顺序采样
适用范围

仅指定S3 N8R8与两块PID4885;两硬件控制器、两驱动/状态,未做实板、电气、I2C或CRC坏帧仿真;没有同步采样或完全故障隔离保证。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/dual-i2c-root.md;具体模块、板、固定Wire/驱动源码与输入SHA验证日期:
构建 / 配置已通过
docs/esp32-evidence/dual-i2c-root-build.json;实际编译Exit0,app309968B对象:完整S3 N8R8 FQBN版本:CLI1.5.1 / core3.3.12 / SHT4x1.1.2 / Core0.7.3验证日期:
仿真已通过
docs/esp32-evidence/dual-i2c-root-host.json;11组同源纯状态输入,非I2C/CRC/电气/调度仿真验证日期:
本站实板待验证
没有本套硬件;未扫描、烧录、读真实serial或完成A/B断线与恢复

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / I²C 双传感器实验

扫描器只看到0x44,不能说明只接了一块

把两块同地址传感器接在一起,扫描器仍可能只列一次0x44。ACK说明这个地址有人应答,没有响应者数量字段;读数据时开漏线路可能组合两块的输出,也可能因内容相同而看似正常。因此一条CRC正确的温湿度记录不能独立证明来自唯一一块模块,更不能靠“读数有变化”验收双传感器。

本篇保留S09原任务,固定两块Adafruit PID4885 SHT40。它们的0x44不能由软件改成0x45;驱动begin里的地址只是主机寻址目标。其他SHT4x后缀可能出厂另配地址,但那不表示当前SKU有软件改址命令。成品采用S3两个硬件I²C控制器,让两个0x44分别拥有总线、驱动、串号和状态。

做到这里,你应该看到
能解释“扫描一个地址”与“唯一响应器件”为什么不同。

参考:[1] Adafruit PID4885:地址不可改、3V输出、已有10k上拉[2] Sensirion SHT4x V6.4:出厂地址料号、CRC、命令与换算[9] NXP UM10204 Rev7:开漏总线、ACK与寻址

02 / I²C 双传感器实验

先看器件能否选地址,再考虑第二总线或多路器

如果具体模块真的有ADDR焊盘/引脚,先按它的手册设置地址,再给驱动相应地址;如果是出厂地址选项,须选另一个完整料号,不能靠改常量变出硬件功能。本例固定两块PID4885,所以不走改地址这条路。

S3有两个硬件I²C控制器,core3.3.12提供Wire和Wire1,分别对应bus0/bus1。两块正好可各占一条;不能创建两个都指向0的TwoWire对象便声称分离。引脚不足、芯片只有一个控制器或器件更多时,才另选可寻址器件,或依据具体多路器手册设计通道选择;本下载只实现两总线方案,不把多路器留作含糊的必做步骤。

真实条件本例的判断
模块手册允许ADDR选择按真实引脚/焊盘设置并扫描确认
只有出厂地址后缀选项换对应完整料号,不能软件变更
两块PID4885 + S3两控制器分别用Wire与Wire1
更多同地址器件/控制器不足另设计有手册的多路器或改器件组合

参考:[1] Adafruit PID4885:地址不可改、3V输出、已有10k上拉[2] Sensirion SHT4x V6.4:出厂地址料号、CRC、命令与换算[3] Arduino3.3.12 Wire:编号控制器、begin和事务超时[4] Arduino3.3.12 Wire头:TwoWire接口与Wire1声明[5] ESP-IDF5.5.5 S3能力:硬件I2C控制器数量

03 / I²C 双传感器实验

电源地共享,SDA和SCL必须分开

模块A的VIN/GND接板3V3/J1-1与GND/J1-22,SDA/SCL接GPIO8/J1-12、GPIO9/J1-15;模块B电源地相同,SDA/SCL改为GPIO17/J1-10、GPIO18/J1-11。两个模块3V/3Vo都是稳压输出,留空;不把两路稳压输出并在一起。

PID4885自带上拉,先用每总线一块模块、≤20cm短线、100kHz,不先额外并联电阻。可靠焊好排针或固定端子,完全断电后逐端追线;两个STEMMA QT口不能再相互菊花链接回去,否则总线又被连在一起。GPIO17/18本例不启用UART1,桥UART0仍用43/44;N8R8保留35–37和RGB38都不参与。

先让每块分别单独接GPIO8/9,按P06/S01读到0x44和自己的序列号,再断电按表分线。只接一条桥USB供电,不同时外加5V/第二USB,不使用5V I²C上拉。给模块贴A/B和位置标签,串号用于追踪具体芯片,不把标签或地址当型号鉴定/精度校准。

DevKit模块A模块B
3V3/J1-1VINVIN
GND/J1-22GNDGND
GPIO8/9SDA/SCL不接
GPIO17/18不接SDA/SCL
模块3V/3Vo留空留空
做到这里,你应该看到
A/B有不同数据线端点,图、表、代码中的8/9/17/18一致。

参考:[1] Adafruit PID4885:地址不可改、3V输出、已有10k上拉[3] Arduino3.3.12 Wire:编号控制器、begin和事务超时[8] 指定DevKitC-1 v1.1排针、桥USB和N8R8限制

04 / I²C 双传感器实验

先固定依赖编译,再验证当前串口程序

环境锁为Arduino CLI1.5.1/core3.3.12,Sensirion I2C SHT4x1.1.2/Core0.7.3。下载包sketch.yaml固定全部版本且没有default_profile;实际构建复用全局已安装core/库,不为相同profile重复下载SDK。README给出安装、完整FQBN、upload和monitor命令,新机器也能显式profile重建这套组合。

先编译完整.ino和channel_state.h,再选插拔确认的桥口、关闭监视器后上传,必要时BOOT+EN进下载,结束释放BOOT再EN。115200里要出现S09启动行和两通道记录;编译通过、上传工具退出或串口仍有旧数值,都不单独证明当前程序和接线通过。本站实际构建与主机逻辑已执行,实板、扫描和器件行为尚未执行。

代码 / 配置
arduino-cli lib install 'Sensirion Core@0.7.3'
arduino-cli lib install --no-deps 'Sensirion I2C SHT4x@1.1.2'
arduino-cli compile --fqbn 'esp32:esp32:esp32s3:FlashSize=8M,PSRAM=opi,FlashMode=qio,PartitionScheme=default_8MB,CDCOnBoot=default,USBMode=hwcdc' examples/esp32/esp32-i2c-address-conflict

参考:[3] Arduino3.3.12 Wire:编号控制器、begin和事务超时[4] Arduino3.3.12 Wire头:TwoWire接口与Wire1声明[6] Sensirion SHT4x1.1.2:实例总线、序列号与高精度读取[7] Sensirion Core0.7.3:帧长与CRC错误返回[8] 指定DevKitC-1 v1.1排针、桥USB和N8R8限制

05 / I²C 双传感器实验

完整程序让每路拥有自己的驱动、串号和错误

每个Channel持有自己的Sensirion驱动与State,只在自己的Wire对象上begin。总线初始化失败下一轮再试;尚无有效serial时先serialNumber,成功后才测量。总线begin失败的-1是本应用哨兵,输出app_detail而不冒充库返回码。ID_ERROR不冒用上次身份,READ_ERROR会清serial_known,恢复后先重读串号;因此换模块后看到RECOVERED也要核serial是否改变。

固定驱动高精度命令为0xFD,等待10ms后读取;Core验证数据字CRC,错误不进入accept。每轮浮点初始化NAN,失败时当前T/RH仍nan,串号未知打印NA,另保留实际error和driver_detail。程序不拿上一次好温湿度再次标成新样本,也不启动加热器。

采样每2000ms一轮,A调用完才执行B,是顺序而非同步。两条硬件总线/驱动隔离地址和部分电气错误,不会隔离共享电源、同一个主线程或程序崩溃;A耗时会推迟B。20ms是Wire事务超时设置,不是整个loop的20ms实时保证。没有其他任务或ISR并发访问总线。

代码 / 配置
// SPDX-License-Identifier: MIT
// S09: two fixed-address SHT40 modules on two separate S3 I2C controllers.
#include <Arduino.h>
#include <Wire.h>
#include <SensirionI2cSht4x.h>
#include "channel_state.h"
struct Channel {
 const char* label;TwoWire* bus;int sda,scl;SensirionI2cSht4x driver;dual_i2c::State state;
 bool bus_ready=false,serial_known=false;uint32_t serial=0;
 Channel(const char* n,TwoWire& w,int d,int c):label(n),bus(&w),sda(d),scl(c){}
};
Channel a("A",Wire,8,9),b("B",Wire1,17,18);
constexpr uint32_t PERIOD_MS=2000;uint32_t last_cycle=0;
static bool begin_bus(Channel& c){
 bool ok=c.bus->begin(c.sda,c.scl,100000);c.bus->setTimeOut(20);
 c.driver.begin(*c.bus,SHT40_I2C_ADDR_44);return ok;
}
static void report(Channel& c){
 char identity[12]="NA";if(c.serial_known)snprintf(identity,sizeof identity,"%08lx",(unsigned long)c.serial);
 const uint32_t now=millis();
 Serial.printf("channel=%s bus=%s sda=%d scl=%d address=0x44 serial=%s uptime_ms=%lu attempts=%lu good=%lu bad=%lu streak=%u state=%s error=%d T_C=%.2f RH_raw=%.2f RH_pct=%.2f clipped=%u good_age_ms=%lld\n",
 c.label,c.label[0]=='A'?"I2C0":"I2C1",c.sda,c.scl,identity,(unsigned long)now,(unsigned long)c.state.attempts,(unsigned long)c.state.good,(unsigned long)c.state.bad,c.state.consecutive_bad,dual_i2c::name(c.state.status),c.state.error,c.state.temperature,c.state.rh_raw,c.state.rh,c.state.clipped,(long long)c.state.age(now));
 if(c.state.status==dual_i2c::Status::BUS_ERROR)Serial.printf("channel=%s app_detail=bus_begin_failed; retry next cycle\n",c.label);
 else if(c.state.error){char detail[96];errorToString(c.state.error,detail,sizeof detail);Serial.printf("channel=%s driver_detail=%s\n",c.label,detail);}
}
static void sample(Channel& c){
 if(!c.bus_ready)c.bus_ready=begin_bus(c);
 if(!c.bus_ready){c.serial_known=false;c.state.fail(dual_i2c::Status::BUS_ERROR,-1);report(c);return;}
 if(!c.serial_known){
  uint32_t serial=0;int16_t error=c.driver.serialNumber(serial);
  if(error){c.state.fail(dual_i2c::Status::ID_ERROR,error);report(c);return;}
  c.serial=serial;c.serial_known=true;
 }
 float t=NAN,rh=NAN;int16_t error=c.driver.measureHighPrecision(t,rh);
 if(error){c.serial_known=false;c.state.fail(dual_i2c::Status::READ_ERROR,error);}
 else if(!c.state.accept(millis(),t,rh))c.serial_known=false;
 report(c);
}
void setup(){
 Serial.begin(115200);a.bus_ready=begin_bus(a);b.bus_ready=begin_bus(b);last_cycle=millis();
 Serial.println("S09 core3.3.12 SHT4x1.1.2 Core0.7.3; A Wire0 SDA8/SCL9, B Wire1 SDA17/SCL18, each100kHz/0x44; sequential not simultaneous.");
}
void loop(){
 const uint32_t now=millis();
 if(uint32_t(now-last_cycle)>=PERIOD_MS){last_cycle=now;sample(a);sample(b);}
 delay(1);
}
做到这里,你应该看到
能把一次错误归到具体通道、阶段、代码和身份,而不是只看最后一个浮点数。

参考:[3] Arduino3.3.12 Wire:编号控制器、begin和事务超时[4] Arduino3.3.12 Wire头:TwoWire接口与Wire1声明[6] Sensirion SHT4x1.1.2:实例总线、序列号与高精度读取[7] Sensirion Core0.7.3:帧长与CRC错误返回

06 / I²C 双传感器实验

错误清当前值,恢复必须等自己的新好样本

channel_state.h不依赖Arduino,真实固件和主机检查共用它。WAITING没有成功样本时good_age_ms=-1;首次即成功为VALID,任一故障后首个新好样本RECOVERED(含之前未成功过),下一次转VALID。错误保留累计good/bad、连续streak和上次成功年龄,但清当前温湿度为NAN。

湿度保留RH_raw,展示RH_pct裁边0…100并给clipped,不能把裁边说成更准。uint32差值能处理正常回绕,但若连续缺少成功值跨过整个计数周期,年龄不再是完整离线时长。计数和年龄只是诊断字段,不是器件自检或长期可靠性认证。

主机实际检查11组同源输入:初始/正常、错误清值、通道状态隔离、恢复、两端RH裁边、NaN、计数、ID/BUS错误、年龄回绕和连续错误饱和。这没有执行I²C、真实CRC坏帧、电平或调度;图中的状态箭头也不是波形或物理恢复结果。

代码 / 配置
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
#include <math.h>
namespace dual_i2c {
enum class Status { WAITING, BUS_ERROR, ID_ERROR, READ_ERROR, INVALID, VALID, RECOVERED };
inline const char* name(Status s) {
 switch(s){case Status::WAITING:return "WAITING";case Status::BUS_ERROR:return "BUS_ERROR";case Status::ID_ERROR:return "ID_ERROR";case Status::READ_ERROR:return "READ_ERROR";case Status::INVALID:return "INVALID";case Status::VALID:return "VALID";case Status::RECOVERED:return "RECOVERED";}return "UNKNOWN";
}
struct State {
 Status status=Status::WAITING;uint32_t attempts=0,good=0,bad=0,last_good=0;uint16_t consecutive_bad=0;
 bool had_fault=false,seen=false,clipped=false;int16_t error=0;
 float temperature=NAN,rh_raw=NAN,rh=NAN;
 void fail(Status why,int16_t code){
  ++attempts;++bad;if(consecutive_bad<65535)++consecutive_bad;status=why;error=code;had_fault=true;
  temperature=NAN;rh_raw=NAN;rh=NAN;clipped=false;
 }
 bool accept(uint32_t now,float t,float h){
  if(!isfinite(t)||!isfinite(h)){fail(Status::INVALID,0);return false;}
  ++attempts;++good;status=had_fault?Status::RECOVERED:Status::VALID;error=0;had_fault=false;seen=true;last_good=now;consecutive_bad=0;
  temperature=t;rh_raw=h;rh=h<0?0:h>100?100:h;clipped=rh!=rh_raw;return true;
 }
 int64_t age(uint32_t now)const{return seen?int64_t(uint32_t(now-last_good)):-1;}
};
}

参考:[2] Sensirion SHT4x V6.4:出厂地址料号、CRC、命令与换算[6] Sensirion SHT4x1.1.2:实例总线、序列号与高精度读取[7] Sensirion Core0.7.3:帧长与CRC错误返回

07 / I²C 双传感器实验

每条总线都做一次已有样本后的断线恢复

正常时保存两路各10周期,核各自bus/pins/0x44/serial和good计数;不能只因两个温度很接近就认定读到了两块。断电在各模块SDA支路预装低压SPST,上电有好样本后只打开A,记录A的ID_ERROR/READ_ERROR与当前nan,观察B后续是否仍在尝试、实际周期是否变长。

闭合A后等自己的新好样本RECOVERED,核serial与随后VALID,再对B重复;各自bad计数应对应各自试验,真实错误码以现场为准。不要带电拔VCC/GND,也不要把冷启动缺模块代替运行后断线。另做A缺/B连与B缺/A连冷启动,区分控制器初始化成功和器件应答。

把日期、板修订、依赖、条件、通道、serial、序号、状态、错误、年龄与结果填入CSV,不填网络示例值。本站无这套器件的扫描、读数、断线或恢复记录,CSV只有表头;温湿度精度、摆放与气流影响另按S01方法记录。

做到这里,你应该看到
A故障与B故障有分开的原始日志、条件和恢复检查,不预填“完全不受影响”。

参考:[1] Adafruit PID4885:地址不可改、3V输出、已有10k上拉[2] Sensirion SHT4x V6.4:出厂地址料号、CRC、命令与换算[3] Arduino3.3.12 Wire:编号控制器、begin和事务超时[8] 指定DevKitC-1 v1.1排针、桥USB和N8R8限制

08 / I²C 双传感器实验

两路同错先查共享处,一路同错再查该支路

两路都错误先核桥串口是否真在运行S09、3V3/GND、供电稳定和是否把QT口又接回一起;一路持续ID_ERROR/READ_ERROR先断电核那一路SDA/SCL、开关、短线和模块SKU。保留完整driver_detail,再恢复最小接线/已锁版本,不随机改地址或同时换库。

闭合开关后等下一轮;极端总线/供电故障可断电重启恢复,本例不承诺自动清总线或硬实时期限。编译、同源状态检查与这些物理诊断分开验收。完成这篇后可接S08的数据质量规则或继续记录/联网,但不能让平滑或网络层把某一路失效藏起来。

参考:[1] Adafruit PID4885:地址不可改、3V输出、已有10k上拉[3] Arduino3.3.12 Wire:编号控制器、begin和事务超时[6] Sensirion SHT4x1.1.2:实例总线、序列号与高精度读取[7] Sensirion Core0.7.3:帧长与CRC错误返回[8] 指定DevKitC-1 v1.1排针、桥USB和N8R8限制

BACK TO THE SOURCE

资料来源与版本说明

实读厂家固定地址说明、SHT4x V6.4、NXP总线手册及固定core/驱动实现;实际构建和同源纯状态输入与物理试验分开。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. Adafruit PID4885:地址不可改、3V输出、已有10k上拉
  2. Sensirion SHT4x V6.4:出厂地址料号、CRC、命令与换算
  3. Arduino3.3.12 Wire:编号控制器、begin和事务超时
  4. Arduino3.3.12 Wire头:TwoWire接口与Wire1声明
  5. ESP-IDF5.5.5 S3能力:硬件I2C控制器数量
  6. Sensirion SHT4x1.1.2:实例总线、序列号与高精度读取
  7. Sensirion Core0.7.3:帧长与CRC错误返回
  8. 指定DevKitC-1 v1.1排针、桥USB和N8R8限制
  9. NXP UM10204 Rev7:开漏总线、ACK与寻址