Platform Abstraction Layer
Tile drivers are platform-agnostic. They never call hardware directly — instead, they use a tiles_hal_t struct that you populate with function pointers for your platform's I2C, SPI, and timing primitives.
Once you implement these functions, every tile driver in the library works on your hardware without modification. Below are ready-to-use implementations for Arduino, ESP-IDF, and STM32.
The Interface
typedef struct {
/* I2C */
int (*i2c_read)(void* handle, uint8_t addr, uint8_t reg,
uint8_t* data, uint16_t len);
int (*i2c_write)(void* handle, uint8_t addr, uint8_t reg,
const uint8_t* data, uint16_t len);
int (*i2c_is_ready)(void* handle, uint8_t addr);
/* SPI — set to NULL if not used */
int (*spi_read)(void* handle, uint8_t cs, uint8_t reg,
uint8_t* data, uint16_t len);
int (*spi_write)(void* handle, uint8_t cs, uint8_t reg,
const uint8_t* data, uint16_t len);
/* Shared */
void (*delay_ms)(uint32_t ms);
void (*on_error)(struct tile t, const char* msg); /* optional */
void* handle; /* opaque — your platform's bus handle */
} tiles_hal_t;Function Pointer Contract
- i2c_read
- Read
lenbytes starting at registerregfrom the device at 7-bitaddr. Return 0 on success. - i2c_write
- Write
lenbytes to registerregat the device at 7-bitaddr. Return 0 on success. - i2c_is_ready
- Check whether a device ACKs at
addr. Return 0 if present. Used by_find()functions. - delay_ms
- Block for at least
msmilliseconds. Used during chip resets and initialization sequences. - handle
- Opaque pointer passed as the first argument to every callback. Store your platform's I2C peripheral handle here.
Platform Implementations
ArduinoWire library
#include "tiles_hal.h"
#include <Wire.h>
static int arduino_i2c_read(void* handle, uint8_t addr, uint8_t reg,
uint8_t* data, uint16_t len) {
(void)handle;
Wire.beginTransmission(addr);
Wire.write(reg);
if (Wire.endTransmission(false) != 0) return -1;
Wire.requestFrom(addr, (uint8_t)len);
for (uint16_t i = 0; i < len; i++) {
if (!Wire.available()) return -1;
data[i] = Wire.read();
}
return 0;
}
static int arduino_i2c_write(void* handle, uint8_t addr, uint8_t reg,
const uint8_t* data, uint16_t len) {
(void)handle;
Wire.beginTransmission(addr);
Wire.write(reg);
for (uint16_t i = 0; i < len; i++) Wire.write(data[i]);
return Wire.endTransmission() == 0 ? 0 : -1;
}
static int arduino_i2c_is_ready(void* handle, uint8_t addr) {
(void)handle;
Wire.beginTransmission(addr);
return Wire.endTransmission() == 0 ? 0 : -1;
}
static void arduino_delay_ms(uint32_t ms) {
delay(ms);
}
void tiles_hal_arduino_init(tiles_hal_t* hal) {
hal->i2c_read = arduino_i2c_read;
hal->i2c_write = arduino_i2c_write;
hal->i2c_is_ready = arduino_i2c_is_ready;
hal->delay_ms = arduino_delay_ms;
hal->handle = NULL; /* Wire is global */
}ESP-IDFI2C Master driver (v5+)
#include "tiles_hal.h"
#include "driver/i2c_master.h"
static int esp_i2c_read(void* handle, uint8_t addr, uint8_t reg,
uint8_t* data, uint16_t len) {
i2c_master_dev_handle_t dev = (i2c_master_dev_handle_t)handle;
return i2c_master_transmit_receive(dev, ®, 1, data, len, 100)
== ESP_OK ? 0 : -1;
}
static int esp_i2c_write(void* handle, uint8_t addr, uint8_t reg,
const uint8_t* data, uint16_t len) {
i2c_master_dev_handle_t dev = (i2c_master_dev_handle_t)handle;
uint8_t buf[1 + len];
buf[0] = reg;
memcpy(&buf[1], data, len);
return i2c_master_transmit(dev, buf, 1 + len, 100)
== ESP_OK ? 0 : -1;
}
static int esp_i2c_is_ready(void* handle, uint8_t addr) {
i2c_master_dev_handle_t dev = (i2c_master_dev_handle_t)handle;
uint8_t dummy;
return i2c_master_receive(dev, &dummy, 1, 10)
== ESP_OK ? 0 : -1;
}
static void esp_delay_ms(uint32_t ms) {
vTaskDelay(pdMS_TO_TICKS(ms));
}
void tiles_hal_esp32_init(tiles_hal_t* hal,
i2c_master_dev_handle_t dev) {
hal->i2c_read = esp_i2c_read;
hal->i2c_write = esp_i2c_write;
hal->i2c_is_ready = esp_i2c_is_ready;
hal->delay_ms = esp_delay_ms;
hal->handle = (void*)dev;
}STM32HAL with DMA (reference implementation)
#include "tiles_hal.h"
/* STM32 HAL reference implementation (tiles_hal_stm32.c)
* Uses DMA with blocking-fallback and 5ms timeout. */
void tiles_hal_stm32_init(tiles_hal_t* hal,
I2C_HandleTypeDef* hi2c) {
hal->i2c_read = stm32_i2c_read; /* DMA + fallback */
hal->i2c_write = stm32_i2c_write; /* DMA + fallback */
hal->i2c_is_ready = stm32_i2c_is_ready;
hal->delay_ms = stm32_delay_ms; /* HAL_Delay() */
hal->handle = (void*)hi2c;
}Using a Driver
After initializing the HAL for your platform, all tile drivers use the same API regardless of hardware:
#include "tiles_hal.h"
#include "tile_inst.h"
#include "tile_sense_i_9.h"
tiles_hal_t hal;
/* Pick ONE — whichever matches your platform */
tiles_hal_arduino_init(&hal); /* Arduino */
tiles_hal_esp32_init(&hal, i2c_dev); /* ESP-IDF */
tiles_hal_stm32_init(&hal, &hi2c1); /* STM32 */
/* From here on, every tile driver works the same */
tile_t imu = tile_sense_i_9_init(&hal, 0); /* instance 0 = default addr */
if (tile_is_ready(imu)) {
int16_t accel[3];
tile_sense_i_9_get_raw_accels(imu, accel);
}Porting Tips
- Address format: Tile drivers use 7-bit I2C addresses throughout. If your platform expects 8-bit (left-shifted), shift in your HAL callbacks.
- Timeout: Keep I2C timeouts short (5–10 ms) to avoid blocking other tasks, especially if running BLE or RTOS.
- Thread safety: If multiple tasks access the same I2C bus, add a mutex in your HAL callbacks.
- Multiple buses: Create separate
tiles_hal_tinstances for each I2C bus, each with its own handle.
