Bergsonne Labs

Driver Development

Guidelines and patterns for writing new tile drivers. These sections cover design considerations, documentation standards, private helper conventions, and the contribution checklist to verify before submitting a driver.

Design Considerations

As the library grows to 75+ drivers, these principles keep the codebase maintainable and the user experience consistent:

Memory Footprint

Many tiles will run on small MCUs (32KB flash, 8KB RAM). Keep drivers lean — no dynamic allocation, no large buffers. The tile_t handle is intentionally small (typically 6 bytes). Use stack-local buffers for temporary data.

Blocking, Callbacks & Async

Today, all driver functions are blocking — they call the HAL, wait for the bus transfer to complete, and return with the result. This is the simplest model and works well for most applications.

For better CPU utilization, your HAL implementation can use DMA transparently — the driver still blocks, but the CPU sleeps during the transfer instead of busy-waiting. Same API, better power and throughput. This is the recommended first optimization.

Looking ahead, the tile_t handle includes reserved fields (flags, callback, cb_ctx) for a future event-driven model. The vision: register a callback with a driver function like on_ready(), and a central tiles_process() function dispatches callbacks in your main loop when events occur (data ready, transfer complete, etc.). This enables non-blocking patterns without requiring users to write ISR code.

For the most demanding use cases (high-speed sensor streaming, camera tiles), true async DMA and direct ISR access will be available through the HAL. These are advanced patterns — most applications will never need them.

Thread Safety

Drivers are not thread-safe by design. Two threads calling the same tile instance concurrently will produce undefined behavior. If you need concurrent access, protect calls with a mutex in your application or HAL layer. Different tile instances on different buses can be used concurrently without synchronization.

Multi-Protocol Tiles

The HAL supports I2C, SPI, QSPI, and I3C via bus flags and dedicated function pointers (see the HAL section above). Some tiles are I2C-only, some SPI-only, and some support both. The instance mapping table in each driver header documents which instance numbers correspond to which bus and address. The id field in tile_t is bus-agnostic — it holds an I2C address or SPI CS index depending on the instance.

Dual-Mode Interface Init

Some ICs share physical pins between I2C and SPI (e.g., SCL/SCLK and SDA/SDI on the ICM-20948). At power-on, these chips typically default to I2C and can misinterpret early SPI traffic as garbled I2C. When a tile supports both interfaces, the driver's SPI init path must explicitly disable the I2C interface early in the sequence (e.g., setting an I2C_IF_DIS bit) to prevent bus contention. This is a configure-once-at-init decision — runtime switching between protocols is not currently supported. Tiles include internal pull-ups on configurable pads to ensure a clean default state at power-on and prevent startup ambiguity. Document any interface-specific init requirements and pad configurations in the driver header.

Interrupts & Data-Ready Pins

Many sensor tiles have interrupt output pins (data ready, FIFO watermark, threshold alerts, motion events). Drivers should provide a _data_ready() function that reads the status register, and document which interrupt pins the chip supports.

GPIO/ISR wiring is host-platform-specific — the future tiles_process() system will use these pins to set flags and dispatch callbacks automatically. For now, users who need interrupt-driven behavior can wire the GPIO ISR themselves and call _data_ready() or read data directly from the callback.

Power Management

Tiles that support low-power modes should implement _sleep() and _wake() functions that update the instance state accordingly. Document current consumption in each mode.

Documentation Standards

Driver headers are the primary documentation source. The website's API reference pages are auto-generated from Doxygen comments in the headers, so thorough documentation directly improves the online docs.

File-Level Comment

Every header starts with a block that identifies the tile, its IC, key specs, and a quick-start example. For standalone use, include the link(s) for on-board ICs as well as essential information on the available interface(s).

/**
 * @file   tile_sense_i_9.h
 * @brief  tile driver for the Sense.I.9 tile (ICM-20948).
 *
 * 9-axis IMU: 3-axis accelerometer, 3-axis gyroscope, 3-axis
 * magnetometer (AK09916 die). I2C interface, 1 MHz fast-mode+.
 *
 * Datasheet: https://invensense.tdk.com/...
 *
 * @note   I2C default address: 0x69 (instance 0, AD0 floating)
 */

Function Documentation

Document every public function with @brief, @param, @return, and a @code example where helpful. Include conversion formulas, units, and practical context — not just parameter descriptions.

/**
 * @brief  Read raw accelerometer data for all three axes.
 *
 * Returns signed 16-bit values in sensor frame. To convert to g,
 * divide by the sensitivity for the current range setting:
 *   ±2g → 16384 LSB/g,  ±4g → 8192,  ±8g → 4096,  ±16g → 2048
 *
 * @param  inst   Instance handle from tile_sense_i_9_init()
 * @param  accel  Output buffer for [x, y, z] — must hold 3 int16_t
 *
 * @code
 *   int16_t accel[3];
 *   tile_sense_i_9_get_raw_accels(imu, accel);
 *   float x_g = accel[0] / 16384.0f;  /* at ±2g range */
 * @endcode
 */
void tile_sense_i_9_get_raw_accels(tile_t* tile, int16_t* accel);

Enum Documentation

Document enum values with units, sensitivities, and practical implications. The user writing the application code should understand which value to choose without needing to open the datasheet.

/**
 * @brief  Accelerometer full-scale range.
 *
 * Higher ranges measure faster motion; lower ranges give finer
 * resolution. The sensitivity (LSB/g) halves with each step.
 */
typedef enum {
    SENSE_I_9_ACCEL_2G  = 0x00,  /**< ±2g  — 16384 LSB/g (default) */
    SENSE_I_9_ACCEL_4G  = 0x01,  /**< ±4g  — 8192 LSB/g            */
    SENSE_I_9_ACCEL_8G  = 0x02,  /**< ±8g  — 4096 LSB/g            */
    SENSE_I_9_ACCEL_16G = 0x03,  /**< ±16g — 2048 LSB/g            */
} sense_i_9_accel_range_t;

Private Bus Helpers

Every driver defines static helper functions that wrap the HAL calls with the instance's device ID pre-filled. This keeps the public API implementations clean and readable. The pattern is the same regardless of bus — I2C, SPI, or QSPI — just swap the HAL function pointer. Here's the I2C version (the most common):

/* Private I2C helpers — keep the public API clean */

static void chip_write(tile_t* tile, uint8_t reg, uint8_t value) {
    tile->hal->i2c_write(tile->hal->handle, tile->id, reg, &value, 1);
}

static uint8_t chip_read(tile_t* tile, uint8_t reg) {
    uint8_t value = 0;
    tile->hal->i2c_read(tile->hal->handle, tile->id, reg, &value, 1);
    return value;
}

static void chip_read_buf(tile_t* tile, uint8_t reg,
                           uint8_t* buf, uint16_t len) {
    tile->hal->i2c_read(tile->hal->handle, tile->id, reg, buf, len);
}

For SPI tiles, the pattern is identical but calls hal->spi_read() / hal->spi_write() instead. For chips with 16-bit registers, big-endian byte order, or multi-register reads, add specialized helpers (e.g., chip_read16(), chip_write16()) in the private section.

Contribution Checklist

Before submitting a new driver, verify:

Structure

  • Header and source follow the standard layout
  • File named tile_<family>_<name>.h / .c
  • Includes: tiles.h and stdint.h — nothing else host-platform-specific
  • Include guard matches file path

API

  • find() implemented — stateless probe by instance number
  • init() implemented — populates tile_t with READY or ERROR state
  • All public functions take tile_t as first parameter
  • No global/static mutable state (all state lives in the instance or is const)
  • Instance ID table with resolve_id() helper

Documentation

  • File-level @file, @brief, @note with datasheet link
  • Instance-to-address mapping table in header comment
  • Every public function has @brief, @param, @return
  • At least one @code example in the file-level comment
  • Enums documented with units/sensitivity/practical guidance

Versioning

  • Driver version defines (MAJOR, MINOR, PATCH) in header
  • TILES_CHECK_VERSION() call with minimum framework requirement
  • Version bump follows semver rules (patch/minor/major)

Quality

  • Compiles with -Wall -Wextra -Werror (no warnings)
  • Tested on hardware (at minimum: find → init → read one value)
  • No host-platform-specific includes or types
  • Device identity verification in init() (WHO_AM_I, known register default, or documented as unavailable)