Skip to content

Porting

Run the driver on a new MCU by writing one class: the I2C interface.

The driver never calls your SDK directly. It calls four methods, plus one optional, on a class you derive from sfm::I2cInterface<Self> (in inc/sfm_i2c_interface.hpp). The base class uses CRTP: the calls are resolved at compile time, so there are no virtual functions, no vtable and nothing to allocate.

Layer diagram: the application calls the hardware-agnostic sfm::Driver and its commands, which use the CRTP I2C interface, which calls the platform port you write, which drives the I2C bus of the Sensirion SFM4300 or another SF06-family flow meter.Layer diagram: the application calls the hardware-agnostic sfm::Driver and its commands, which use the CRTP I2C interface, which calls the platform port you write, which drives the I2C bus of the Sensirion SFM4300 or another SF06-family flow meter.
Three layers, one of them yours
Figure description and sources

From top to bottom. Your application talks to one sfm::Driver object per sensor, whose public commands are listed from inc/sfm_driver.hpp. The core turns each call into 16-bit SF06 commands, checks the CRC-8 of every word it reads and converts raw counts with the scaling the sensor reported. The communication interface (sfm::I2cInterface in inc/sfm_i2c_interface.hpp) is a CRTP base: four methods are required, general call is optional. The platform port is the only code you write. Below it is the sensor.

Drawn from: inc/sfm_driver.hpp Driver public commands; inc/sfm_i2c_interface.hpp I2cInterface; figure module docs/diagrams/figures/driver-layers.py.

What you implement

Method Required Contract
Write(addr7, data, len) yes START, address + W, len bytes, STOP. No register byte. len == 0 is an address-only probe. Return false on a NACK or bus error
Read(addr7, out, len) yes START, address + R, len bytes (NACK the last), STOP. Return false on an address NACK: that is the sensor's "no new sample"
DelayMs(ms) yes Block for at least ms milliseconds
EnsureInitialized() yes Bring the bus up if needed; return true when it is usable. Called at the start of every command
WriteGeneralCallImpl(byte) no Write one byte to the general-call address 0x00. Without it SoftReset() returns NotSupported

The port

my_i2c.hpp
class MyI2c : public sfm::I2cInterface<MyI2c> {
public:
  // Raw write: START, address + W, the bytes, STOP. No register byte. len == 0 is an
  // address-only probe (ExitSleep() uses it). Return false on a NACK or bus error.
  bool Write(uint8_t addr7, const uint8_t* data, std::size_t len) noexcept {
    return platform_i2c_write(addr7, data, len);
  }

  // Raw read: START, address + R, len bytes, STOP. Return false on a NACK: the sensor
  // NACKs its address while it has no new sample, and the driver reports NoData.
  bool Read(uint8_t addr7, uint8_t* out, std::size_t len) noexcept {
    return platform_i2c_read(addr7, out, len);
  }

  // Blocking wait: start-up (12 ms), soft reset (20 or 30 ms), wake-up polling (5 ms steps).
  void DelayMs(uint32_t ms) noexcept { platform_delay_ms(ms); }

  // Bring the bus up on first use; every command checks it.
  bool EnsureInitialized() noexcept {
    if (!ready_) {
      ready_ = platform_i2c_init();
    }
    return ready_;
  }

  // Optional: one byte to the general-call address 0x00. Without it SoftReset() returns NotSupported.
  bool WriteGeneralCallImpl(uint8_t byte) noexcept {
    return platform_i2c_write(sfm::addr::kGeneralCall, &byte, 1);
  }

private:
  bool ready_ = false;
};

Rules the transfers must follow:

  • One transaction per call. A command (two or five bytes) and a read (3, 9 or 18 bytes) are each one START to STOP transfer. Do not split them and do not add a register byte.
  • Report the NACK. Reads of the result buffer are NACKed whenever no new sample is ready. Return false and let the driver turn it into NoData. Do not retry inside the port.
  • Read exactly len bytes. A partial read hands the driver words with stale CRCs: Crc errors that come and go.
  • Support len == 0 on Write(). ExitSleep() probes the address with an empty write until the sensor acknowledges it. A port that cannot send an address-only write makes ExitSleep() fail.
  • Keep timeouts short. A clock-stretching or stuck bus should fail the call, not block the control loop.

Bus settings

Setting Value
Clock 100 kHz to 1 MHz; 400 kHz is the typical choice
Pull-ups External, on SDA and SCL. MCU internal pull-ups (tens of kΩ) are too weak above about 100 kHz
Address 7-bit, from the ADDR strap

Reference implementation

The ESP32 examples use hf_sfm_esp_i2c.hpp: SfmEspIdfI2c<port, sda, scl, hz>, a template port on the ESP-IDF driver/i2c.h API with FreeRTOS delays and the general-call write. Call its static Install() once before the first driver call. Start from it for any ESP-IDF target.

Check your port

  1. Build it: the port on this page is compiled by the host CI from tests/docs/, with the driver instantiated over it.
  2. Call ReadProductIdentifier(): a decoded Variant proves raw writes, 18-byte reads and CRC handling.
  3. Start air and read for a second: NoData should be rare at 1 kHz polling and frequent at 5 kHz; Crc should stay at zero.
  4. Call EnterSleep() then ExitSleep(): success proves the address-only write.

Threads

The driver is not thread-safe. Give each sensor one owner task, or guard every call on a driver object, and the port it shares with other devices on the bus, with one mutex.

Next: Examples.