FS3000-1015 detected on I2C, checksum valid, but output frozen at 415 counts regardless of airflow

Hardware

  • Sensor: FS3000-1015 on a MikroE Air Velocity 2 Click board (MIKROE-6577). I know this isn’t a SparkFun breakout, but it’s the same Renesas FS3000-1015 module your library targets, so I’m hoping the sensor behaviour is familiar to people here.
  • MCU: ESP32-S3-DevKitC-1, Arduino ESP32 core, board profile “ESP32S3 Dev Module”
  • Library: SparkFun FS3000 Arduino Library, range set with setRange(AIRFLOW_RANGE_15_MPS)
  • Wiring: 3V3 to 3V3, GND to GND, GPIO41 to SDA, GPIO42 to SCL. I2C pull-ups are fitted on the Click board. Approx 15cm Dupont leads.

Symptom The sensor enumerates and communicates perfectly, but the measurement never changes. Every single read returns exactly the same frame:

Frame: 0x60 0x01 0x9F 0x00 0x00  chk=OK  |  Raw counts: 415  |  Air velocity: 0.02 m/s

That’s 0x019F = 415 counts, which would be a perfectly plausible still-air reading (zero flow is 409 counts on this part). But it is byte-for-byte identical on every read, with zero noise, and it does not respond to anything:

  • Blowing directly through the sensor’s flow channel, in the direction of the moulded arrow
  • Compressed air through the channel
  • A fingertip pressed on the housing for several seconds (thermal stimulus)
  • Warm breath at close range

A live thermopile should show at least a couple of counts of noise between reads. Mine is frozen solid.

What I’ve ruled out

  • Bus/wiring: I2C scanner finds the device at 0x28, begin() succeeds, and every frame passes checksum (low byte of the 5-byte sum is 0). I’ve also run it on a different pin pair (GPIO20/42) with identical results.
  • Library: the frame dump in my test sketch reads 5 bytes straight off the bus with Wire.requestFrom(0x28, 5), bypassing the library completely. The frozen bytes are what the sensor itself transmits. Library readRaw() agrees with the manual decode.
  • Power: 3.3V confirmed with a multimeter directly at the sensor board’s 3V3/GND pins while running. Board power LED lit. This rules out the “chip parasitically powered through the I2C pull-ups” trap.
  • Latch-up: full cold power cycle (USB removed 30+ seconds), no change. Multiple reboots, no change.
  • Polling rate: tried 5s and 1s intervals, no difference.

Minimal repro sketch No WiFi, no application logic, just the scan plus a raw frame read every second (attached / below). Sample output after several minutes, including during airflow and thermal tests:

Scanning I2C bus...
  Found device at 0x28
FS3000 begin() OK
Frame: 0x60 0x01 0x9F 0x00 0x00  chk=OK  |  library readRaw: 415
Frame: 0x60 0x01 0x9F 0x00 0x00  chk=OK  |  library readRaw: 415
Frame: 0x60 0x01 0x9F 0x00 0x00  chk=OK  |  library readRaw: 415
(identical forever)

Question Is there any initialisation step, mode register, or wake command the FS3000 needs that could explain a valid but never-updating output? My reading of the datasheet is that it free-runs and just streams frames with no configuration at all, which leads me to conclude the measurement core of this particular module is dead (digital interface alive, thermopile/ADC not producing samples) and it should go back as DOA. Before I return it, I’d appreciate a sanity check from anyone who has seen an FS3000 do this. Thanks.

Code below:

/*
Minimal FS3000-1015 test - no WiFi, no web server.
Board: ESP32-S3-DevKitC-1 (Arduino core, "ESP32S3 Dev Module")
Sensor: MikroE Air Velocity 2 Click (FS3000-1015), I2C addr 0x28
Library: SparkFun FS3000 Arduino Library (only used for readRaw comparison;
the frame dump below is direct Wire access)

Wiring: 3V3->3V3, GND->GND, GPIO41->SDA, GPIO42->SCL
(I2C pull-ups are on the Click board)
*/

#include <Wire.h>
#include <SparkFun_FS3000_Arduino_Library.h>

const int I2C_SDA_PIN = 41;
const int I2C_SCL_PIN = 42;

FS3000 airSensor;

void setup() {
Serial.begin(115200);
delay(1000);

Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);

// I2C scan
Serial.println("Scanning I2C bus...");
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0) {
Serial.print("  Found device at 0x");
Serial.println(addr, HEX);
}
}

if (airSensor.begin()) {
airSensor.setRange(AIRFLOW_RANGE_15_MPS);
Serial.println("FS3000 begin() OK");
} else {
Serial.println("FS3000 begin() FAILED");
}
}

void loop() {
// Raw 5-byte frame read, bypassing the library entirely.
// Frame: byte0 = checksum, byte1 = data high, byte2 = data low, byte3-4 filler.
// Low 8 bits of the sum of all 5 bytes should be 0 for a valid frame.
byte frame[5];
int got = 0;
Wire.requestFrom((uint8_t)0x28, (uint8_t)5);
while (Wire.available() && got < 5) {
frame[got++] = Wire.read();
}
byte sum = 0;
for (int i = 0; i < got; i++) sum += frame[i];

Serial.print("Frame:");
for (int i = 0; i < got; i++) {
Serial.print(" 0x");
if (frame[i] < 16) Serial.print("0");
Serial.print(frame[i], HEX);
}
Serial.print("  chk=");
Serial.print(sum == 0 ? "OK" : "BAD");
Serial.print("  |  library readRaw: ");
Serial.println(airSensor.readRaw());

delay(1000);
}