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.