This guide walks you through implementing the platform callbacks and bringing up a TMF8829 sensor on any microcontroller or host system.
Overview
The driver communicates with the TMF8829 exclusively through a callback table (tmf8829_ops_t). Your job is to implement these callbacks for your platform's bus peripheral, timer, and GPIO. Once that is done, the full driver API (enable, configure, measure, download firmware) works without further platform code.
┌────────────────────────────────────┐
│ Application │
├────────────────────────────────────┤
│ tmf8829_universal_driver │
│ (tmf8829_init, tmf8829_enable, │
│ tmf8829_read_results, ...) │
├────────────────────────────────────┤
│ tmf8829_ops_t (your callbacks) │
├────────────────────────────────────┤
│ Platform HAL / registers │
└────────────────────────────────────┘
Step 1: Include the Driver
Add the library to your CMake project:
add_subdirectory(external/tmf8829_universal_driver)
target_link_libraries(your_firmware PRIVATE tmf8829::tmf8829)
In your source file:
Public API for the portable TMF8829 driver.
Step 2: Implement the Callback Table
You must provide four required callbacks and one or two optional callbacks in a tmf8829_ops_t struct.
Required Callbacks
| Callback | Signature | Purpose |
| read | int (tmf8829_driver_t* drv, uint8_t reg, uint8_t* buf, uint16_t len) | Read len bytes starting at register reg into buf. |
| write | int (tmf8829_driver_t* drv, uint8_t reg, const uint8_t* buf, uint16_t len) | Write len bytes from buf starting at register reg. |
| delay_us | void (uint32_t us) | Busy-wait for at least us microseconds. |
| systick_us | uint32_t (void) | Return a free-running microsecond counter (32-bit wrap is fine). |
Optional Callbacks
| Callback | Signature | Purpose |
| write_pin_enable | void (tmf8829_driver_t* drv, int high) | Drive the sensor enable/power pin high or low. Set to NULL if the enable pin is not controllable (permanently high, e.g. hard wired on the PCB). |
| read_pin_int | int (tmf8829_driver_t* drv) | Read the interrupt GPIO level. Return 1 if asserted, 0 if not. Set to NULL if you poll TMF8829_REG_INT_STATUS instead. |
All callbacks return 0 on success and a negative value on failure (matching the driver's own convention).
Bus Read/Write Details
The drv pointer gives you access to:
- drv->bus — whether the sensor uses I2C or SPI
- drv->i2c_addr — the 7-bit I2C address (only relevant for I2C)
- drv->user_ctx — an opaque void* you set to your peripheral handle
I2C: Perform an I2C memory-read/write with reg as the 8-bit sub-address.
SPI: Prefix with a 2-byte header (TMF8829_SPI_RD_CMD/TMF8829_SPI_WR_CMD + register), manage NSS manually. For reads, discard the one stuff byte returned by the device before the actual payload.
Step 3: Create the Driver Instance
.user_ctx = &my_i2c_handle,
.buffer = sensor_buffer,
.buffer_len = sizeof(sensor_buffer),
};
#define TMF8829_DEFAULT_I2C_ADDR
Definition tmf8829_regs.h:32
@ TMF8829_BUS_I2C
Definition tmf8829_types.h:44
#define TMF8829_MIN_BUFFER_SIZE
Minimum tmf8829_driver_t::buffer_len in bytes.
Definition tmf8829_types.h:116
struct tmf8829_driver tmf8829_driver_t
Definition tmf8829_types.h:33
The buffer is used internally for register staging and firmware download. It must be at least TMF8829_MIN_BUFFER_SIZE bytes. Larger buffers enable faster FIFO downloads.
Step 4: Initialise and Enable
.read = my_read,
.write = my_write,
.delay_us = my_delay_us,
.systick_us = my_systick_us,
.write_pin_enable = my_write_pin_enable,
.read_pin_int = NULL,
};
int tmf8829_enable(tmf8829_driver_t *drv)
Drive the enable pin high and wait for CPU ready.
int tmf8829_init(tmf8829_driver_t *drv, const tmf8829_ops_t *ops)
Validate parameters, bind ops, and reset driver state.
@ TMF8829_OK
Definition tmf8829_types.h:62
struct tmf8829_ops tmf8829_ops_t
Definition tmf8829_types.h:34
tmf8829_enable() performs the power-on sequence: drives enable low (capacitor discharge), then high, and polls until the CPU reports ready.
Step 5: Select the Active Bus Interface
- Warning
- This step is mandatory after every power cycle and is easy to miss. Skipping it will cause all subsequent communication to silently fail and the sensor remains unresponsive.
After or power-on the bootloader starts with both the I2C and SPI interfaces active simultaneously. You must disable the interface you are not using before any other command. This tells the bootloader which interface to route traffic through for the remainder of the session.
For SPI (disable I2C):
int tmf8829_bootloader_i2c_off(tmf8829_driver_t *drv)
Bootloader: TMF8829_BL_CMD_STAT_I2C_OFF.
For I2C (disable SPI):
int tmf8829_bootloader_spi_off(tmf8829_driver_t *drv)
Bootloader: TMF8829_BL_CMD_STAT_SPI_OFF.
This command only works while the bootloader is running (i.e. before tmf8829_download_firmware is called). If the application firmware is already running from a previous session, the call will return an error, which is safe to ignore.
Step 6: Download Firmware
The tmf8829 main firmware is stored in the sensors RAM memory. The RAM is volatile and looses its content when the sensor is powered down. Therefore we need to upload the sensor firmware to the sensor after every power cycle:
#include <tmf8829/tmf8829_fw_source.h>
tmf8829_fw_image_read_fn fw_image_read
Definition tmf8829.h:172
int tmf8829_download_firmware(tmf8829_driver_t *drv, uint32_t image_start_addr, int use_fifo)
Stream firmware through tmf8829_driver_t::fw_image_read and start the RAM application.
#define TMF8829_FW_IMAGE_LOAD_ADDR_DEFAULT
Definition tmf8829_regs.h:404
Or supply your own tmf8829_fw_image_read_fn to read from external flash, a filesystem, etc.
Step 7: Configure and Measure
uint8_t irqs = 0;
if (irqs & TMF8829_INT_RESULT_IRQ) {
}
int tmf8829_get_and_clr_interrupts(tmf8829_driver_t *drv, uint8_t mask, uint8_t *out_pending)
Read TMF8829_REG_INT_STATUS and write-1-to-clear bits in mask.
int tmf8829_read_results(tmf8829_driver_t *drv)
Read a result frame from the FIFO (header via TMF8829_REG_FIFO_STATUS, payload via TMF8829_REG_FIFO).
#define TMF8829_CMD_LOAD_CFG_8X8
Definition tmf8829_regs.h:261
#define TMF8829_CMD_MEASURE
Definition tmf8829_regs.h:257
Multi-Instance Support
Each physical sensor gets its own tmf8829_driver_t. They can share the same tmf8829_ops_t if they are on the same bus type. Differentiate them via user_ctx and i2c_addr:
.i2c_addr = 0x41,
.user_ctx = &hi2c1,
.buffer = buf_a,
.buffer_len = sizeof(buf_a),
};
.i2c_addr = 0,
.user_ctx = &hspi1,
.buffer = buf_b,
.buffer_len = sizeof(buf_b),
};
@ TMF8829_BUS_SPI
Definition tmf8829_types.h:45
Both instances can share the same ops table — the read/write implementations dispatch on drv->bus.
Reference: STM32 HAL Implementation
Below is a condensed reference showing how the callbacks are implemented for an STM32G4 using the HAL.
Per-Instance Context
Since each tmf8829_driver_t carries a user_ctx pointer, we use a small struct to hold both the bus handle and the per-sensor pin assignment:
#include "stm32g4xx_hal.h"
typedef struct {
I2C_HandleTypeDef* hi2c;
SPI_HandleTypeDef* hspi;
GPIO_TypeDef* nss_port;
uint16_t nss_pin;
GPIO_TypeDef* en_port;
uint16_t en_pin;
} tmf8829_platform_ctx_t;
Register map, command/status codes, interrupt masks, frame layout, and bootloader constants.
Platform Callbacks
static void delay_us(uint32_t us) {
const uint32_t start = systick_us();
while ((uint32_t)(systick_us() - start) < us) {}
}
static uint32_t systick_us(void) {
return my_get_us_tick();
}
tmf8829_platform_ctx_t* ctx = (tmf8829_platform_ctx_t*)drv->
user_ctx;
HAL_GPIO_WritePin(ctx->en_port, ctx->en_pin,
high ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
static int i2c_read(
tmf8829_driver_t* drv, uint8_t reg, uint8_t* buf, uint16_t len) {
tmf8829_platform_ctx_t* ctx = (tmf8829_platform_ctx_t*)drv->
user_ctx;
uint16_t dev_addr = (uint16_t)((drv->i2c_addr & 0x7F) << 1);
HAL_StatusTypeDef st = HAL_I2C_Mem_Read(ctx->hi2c, dev_addr, reg,
I2C_MEMADD_SIZE_8BIT, buf, len, 50);
return (st == HAL_OK) ? 0 : -1;
}
static int i2c_write(
tmf8829_driver_t* drv, uint8_t reg,
const uint8_t* buf, uint16_t len) {
tmf8829_platform_ctx_t* ctx = (tmf8829_platform_ctx_t*)drv->
user_ctx;
uint16_t dev_addr = (uint16_t)((drv->i2c_addr & 0x7F) << 1);
HAL_StatusTypeDef st = HAL_I2C_Mem_Write(ctx->hi2c, dev_addr, reg,
I2C_MEMADD_SIZE_8BIT,
(uint8_t*)buf, len, 50);
return (st == HAL_OK) ? 0 : -1;
}
static int spi_read(
tmf8829_driver_t* drv, uint8_t reg, uint8_t* buf, uint16_t len) {
tmf8829_platform_ctx_t* ctx = (tmf8829_platform_ctx_t*)drv->
user_ctx;
HAL_GPIO_WritePin(ctx->nss_port, ctx->nss_pin, GPIO_PIN_RESET);
HAL_SPI_Transmit(ctx->hspi, hdr, 2, 50);
uint8_t stuff = 0;
HAL_SPI_Receive(ctx->hspi, &stuff, 1, 50);
HAL_StatusTypeDef st = HAL_SPI_Receive(ctx->hspi, buf, len, 50);
HAL_GPIO_WritePin(ctx->nss_port, ctx->nss_pin, GPIO_PIN_SET);
return (st == HAL_OK) ? 0 : -1;
}
static int spi_write(
tmf8829_driver_t* drv, uint8_t reg,
const uint8_t* buf, uint16_t len) {
tmf8829_platform_ctx_t* ctx = (tmf8829_platform_ctx_t*)drv->
user_ctx;
HAL_GPIO_WritePin(ctx->nss_port, ctx->nss_pin, GPIO_PIN_RESET);
HAL_StatusTypeDef st = HAL_SPI_Transmit(ctx->hspi, hdr, 2, 100);
if (st == HAL_OK && len > 0 && buf != NULL) {
st = HAL_SPI_Transmit(ctx->hspi, (uint8_t*)buf, len, 100);
}
HAL_GPIO_WritePin(ctx->nss_port, ctx->nss_pin, GPIO_PIN_SET);
return (st == HAL_OK) ? 0 : -1;
}
static int port_read(
tmf8829_driver_t* drv, uint8_t reg, uint8_t* buf, uint16_t len) {
: i2c_read(drv, reg, buf, len);
}
static int port_write(
tmf8829_driver_t* drv, uint8_t reg,
const uint8_t* buf, uint16_t len) {
: i2c_write(drv, reg, buf, len);
}
.read = port_read,
.write = port_write,
.delay_us = delay_us,
.systick_us = systick_us,
.write_pin_enable = write_pin_enable,
.read_pin_int = NULL,
};
void * user_ctx
Definition tmf8829.h:145
tmf8829_bus_t bus
Definition tmf8829.h:141
#define TMF8829_SPI_WR_CMD
Definition tmf8829_regs.h:35
#define TMF8829_SPI_RD_CMD
Definition tmf8829_regs.h:37
Instantiation Example (Two Sensors)
static tmf8829_platform_ctx_t ctx_a = {
.hi2c = &hi2c1,
.en_port = GPIOA, .en_pin = GPIO_PIN_4,
};
static tmf8829_platform_ctx_t ctx_b = {
.hspi = &hspi1,
.nss_port = GPIOB, .nss_pin = GPIO_PIN_6,
.en_port = GPIOC, .en_pin = GPIO_PIN_0,
};
.i2c_addr = 0x41,
.user_ctx = &ctx_a,
.buffer = buf_a,
.buffer_len = sizeof(buf_a),
};
.user_ctx = &ctx_b,
.buffer = buf_b,
.buffer_len = sizeof(buf_b),
};
Troubleshooting
| Symptom | Likely cause |
| tmf8829_init returns TMF8829_E_PARAM | Missing required callback, buffer too small, or invalid I2C address (> 0x7F). |
| tmf8829_enable returns TMF8829_E_TIMEOUT | Enable pin not wired, wrong GPIO port/pin, or sensor not powered. |
| Firmware download times out or all calls fail after power cycle | Interface not selected. tmf8829_bootloader_i2c_off() (SPI) or tmf8829_bootloader_spi_off() (I2C) was not called before the firmware download. See Step 5. |
| Garbage data from reads | SPI: stuff byte not discarded. I2C: address not left-shifted. |
| Firmware download fails | Buffer too small for chunk size, or fw_image_read returns short reads. |
| Clock correction drift | systick_us wraps too fast or has low resolution. Ensure monotonic microsecond accuracy. |