Skip to content

API guide

How to do the common jobs with the driver: identify the part, start a gas or a mixture, read, average, sleep and handle errors.

Every snippet on this page is compiled by the host CI from tests/docs/api_tasks.cpp, with Flow standing for sfm::Driver<MyI2c>. The API reference lists every member.

The shape of the API

One sfm::Driver<I2cT> object per sensor, constructed with your port, the sensor's address and optionally the part family. Every command returns sfm::DriverResult<T>, never throws and never allocates.

Task Calls
Identify the part ReadProductIdentifier(), KnownVariant(), PartFamily(), AssumeVariant()
Measure StartContinuous(), ReadMeasurement(), ReadFlowOnly(), Stop()
Mixtures StartContinuous(gas, o2_permille), UpdateConcentration()
Configure ConfigureAveraging(), ReadScaleOffsetUnit(), SetAddress()
Power EnterSleep(), ExitSleep(), SoftReset()
State ActiveGas(), Measuring(), ActiveScaling(), Address()
No bus access DecodeMeasurement(), EncodeWord(), DecodeWords(), sfm::Crc8(), sfm::StartCommandFor(), sfm::SupportsGas()
SFx6000 only ReadTemperature()

A DriverResult<T> has value and error. It converts to bool (true when error is None), and sfm::ToString(error) names the error for a log.

Identify the part

const auto id = flow.ReadProductIdentifier();  // idle mode only: before StartContinuous() or after Stop()
if (!id) {
  std::printf("no reply: %s\n", sfm::ToString(id.error).data());
  return false;
}
std::printf("product 0x%08lX, %s, %.0f slm full scale\n", static_cast<unsigned long>(id.value.product_id),
            sfm::ToString(id.value.variant).data(), static_cast<double>(sfm::FullScaleSlm(id.value.variant)));

The sensor answers only in idle mode: before the first start, or after Stop(). The result sets the variant and the family the driver uses for every later start. Gases and part families explains why that matters.

Start a measurement

if (!flow.ConfigureAveraging(0)) {               // idle only: average-until-read (the default)
  return false;
}
i2c.DelayMs(1);

sfm::Gas gas = sfm::Gas::CO2;
if (!sfm::SupportsGas(flow.KnownVariant(), gas)) {
  gas = sfm::Gas::Air;                           // SFM4300-50: O2, Air and Air/O2 only
}
if (const auto r = flow.StartContinuous(gas); !r) {
  std::printf("start failed: %s\n", sfm::ToString(r.error).data());
  return false;
}
const sfm::Scaling& s = flow.ActiveScaling();    // read from the sensor for this gas
std::printf("scale %d, offset %d, unit 0x%04X\n", s.scale, s.offset, s.unit);

StartContinuous() checks the gas against the known variant, reads the scale factor, offset and unit for that gas (so the sensor must be idle), sends the start command and waits 12 ms for the first result. To change the gas, call it again: when this driver started the running measurement (Measuring()), it sends Stop() first. A measurement it did not start (the sensor kept measuring across an MCU reset) needs an explicit Stop().

Read

const auto m = flow.ReadMeasurement();
if (m.error == sfm::DriverError::NoData) {
  return;                                        // no new sample since the last read: skip this tick
}
if (!m) {
  ++errors;                                      // Crc: noise, missing pull-ups, a partial read
  return;
}
const float slm = m.value.flow;                  // (raw - offset) / scale
const float t_c = m.value.temperature_c;         // sensor silicon temperature, SF06 parts only
const bool fixed_n = m.value.status.fixed_n_averaging();

One read is one 9-byte transfer and three CRC checks. NoData is normal: the sensor NACKs a read while it has no new result. Call it at your control-loop rate, up to 2 kHz; Measurement and averaging explains what each read returns in each averaging mode.

When you need only the flow, read one word instead of three:

const auto f = flow.ReadFlowOnly();              // 3 bytes instead of 9: no temperature, no status
if (f) {
  use_flow(f.value);
}

ReadFlowOnly() does not refresh the status or the temperature.

Measure a mixture

if (!flow.StartContinuous(sfm::Gas::AirO2Mix, 400)) {  // 40.0 % O2 in air, in per mille (0 to 1000)
  return false;
}
// Later, while measuring: no restart, no 12 ms gap. At most once per millisecond.
if (!flow.UpdateConcentration(600)) {                   // 60.0 % O2
  return false;
}

The argument is the O2 volume fraction in per mille (0 to 1000); the driver refuses larger values with InvalidParameter (the sensor would stop measuring). UpdateConcentration() changes it without the 12 ms restart of a new start command. Send it at most once per millisecond, and do not read in between its two commands.

Average on the sensor

if (!flow.Stop()) {                              // to idle; waits the 0.5 ms the sensor needs
  return false;
}
if (!flow.ConfigureAveraging(4)) {               // fixed N = 4: one value every 2 ms (500 Hz)
  return false;
}
if (!flow.StartContinuous(sfm::Gas::Air)) {
  return false;
}

Averaging is configured in idle mode and holds until the next reset. 0 is average-until-read (the default), 1 to 128 is fixed-N.

Sleep and reset

if (!flow.Stop()) {                              // continuous measurement to idle (waits 0.5 ms)
  return false;
}
if (!flow.EnterSleep()) {                        // SF06 only, from idle: 0.4 uA typical
  return false;
}
// ... later
if (!flow.ExitSleep()) {                         // a write header wakes it; polls until it ACKs
  return false;
}
if (const auto r = flow.SoftReset(); !r) {       // general call 0x06, then 20 ms (SFx6000: 30 ms)
  return false;                                  // NotSupported: the port has no WriteGeneralCallImpl()
}

Sleep needs idle mode and is SF06-only. The sensor does not acknowledge the wake-up write; ExitSleep() polls the address in 5 ms steps for up to 20 ms. A soft reset is a general-call write that resets every device on the bus that supports it; after it, averaging is back to average-until-read and the sensor is idle.

Read from an interrupt or DMA

// frame: the 9 bytes your interrupt or DMA handler read after IRQn went low
const auto m = Flow::DecodeMeasurement(flow.PartFamily(), flow.ActiveScaling(), frame);
if (m) {
  use_flow(m.value.flow);
}

DecodeMeasurement() is static and touches no bus: it checks the CRCs and scales the frame with the scaling of the active gas. Read the frame when IRQn falls; see Measurement and averaging.

Handle errors

const auto r = flow.StartContinuous(sfm::Gas::N2O);
switch (r.error) {
  case sfm::DriverError::None:
    return true;
  case sfm::DriverError::VariantUnknown:         // N2O's start code means CO2 on SFx6000 parts
    (void)flow.ReadProductIdentifier();          // learn the family, then try again
    return flow.StartContinuous(sfm::Gas::N2O).ok();
  case sfm::DriverError::UnsupportedGas:         // this variant has no N2O table (SFM4300-50)
    return flow.StartContinuous(sfm::Gas::Air).ok();
  default:                                       // BusWrite, BusRead, UnexpectedUnit, Crc, NotInitialized
    std::printf("start failed: %s\n", sfm::ToString(r.error).data());
    return false;
}
Error Meaning What to do
NoData A result-buffer read was NACKed: no new sample yet, the sensor is idle, or nothing answered Skip the tick; if it persists, check the start and the address
BusRead The reply of the identifier or scaling command was NACKed Wiring, the address, or the sensor is measuring (these need idle mode)
UnexpectedUnit The scaling reply cannot convert: scale factor 0, or a unit other than 0x0148 on a known SF06 part The variant assumed does not match the part, or the reply is corrupt
Crc A received word failed its CRC-8 Count it; persistent errors mean noise, weak pull-ups or a port that returns partial reads
BusWrite A command write was NACKed or failed Address, wiring, or the sensor is measuring when the command needs idle
NotInitialized The port's EnsureInitialized() returned false Bus set-up in your port
UnsupportedGas The known variant has no table for this gas Use a gas from the table
VariantUnknown The gas's start code depends on the family and the part is not known ReadProductIdentifier() or AssumeVariant() first
InvalidParameter Averaging over 128, O2 fraction over 1000 ‰ Fix the argument
NotSupported The command does not exist on this family, or the port has no general-call write See Gases and part families

SFx6000 temperature

if (flow.PartFamily() == sfm::Family::Sfx6000) {
  const auto t = flow.ReadTemperature();         // output-buffer pointer, while measuring
  if (!t) {
    return false;
  }
  use_flow(t.value);
}

On SFx6000 parts the second word of the measurement frame is reserved, so Measurement::temperature_valid is false; ReadTemperature() moves the output-buffer pointer, reads one word and moves it back. On SF06 parts it returns NotSupported: the temperature is in every frame.

Next: Configuration.