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.
Figure description and sources
From top to bottom. Your application talks to one sfm::Driver
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¶
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
falseand let the driver turn it intoNoData. Do not retry inside the port. - Read exactly
lenbytes. A partial read hands the driver words with stale CRCs:Crcerrors that come and go. - Support
len == 0onWrite().ExitSleep()probes the address with an empty write until the sensor acknowledges it. A port that cannot send an address-only write makesExitSleep()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¶
- Build it: the port on this page is compiled by the host CI from
tests/docs/, with the driver instantiated over it. - Call
ReadProductIdentifier(): a decodedVariantproves raw writes, 18-byte reads and CRC handling. - Start air and read for a second:
NoDatashould be rare at 1 kHz polling and frequent at 5 kHz;Crcshould stay at zero. - Call
EnterSleep()thenExitSleep(): 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.