Platform-agnostic C driver for the ST ISM330DHCX — a 6-axis industrial-grade IMU (3-axis accelerometer + 3-axis gyroscope + on-chip temperature sensor) with FIFO, interrupt sources, machine-learning core (MLC), finite state machine (FSM), sensor-hub master, OIS mode, and dual-bus (SPI/I²C) digital interface.
The driver is bus-agnostic: register access goes through two callbacks, readReg and writeReg, that the application wraps around either SPI mode 0/3 (≤ 10 MHz) or I²C Fast-mode+ (≤ 1 MHz).
| Channels | 3-axis accel + 3-axis gyro + temp sensor |
| Accel ranges | ±2 g · ±4 g · ±8 g · ±16 g |
| Gyro ranges | ±125 · ±250 · ±500 · ±1000 · ±2000 dps |
| ODR (both) | 12.5 Hz → 6.667 kHz (accel also has 1.6 Hz low-power) |
| FIFO | 9 kB total, tagged-frame samples, configurable batch rates |
| Bus | SPI (mode 0/3, ≤ 10 MHz) or I²C (≤ 1 MHz) |
| WHO_AM_I | 0x6B |
| Package | LGA-14, 2.5×3 mm |
Activity-detection blocks (tap, free-fall, wake-up, 6D/4D, MLC, FSM, sensor-hub, OIS, pedometer) exist on the chip but are not wrapped by this driver — see Coverage scope.
ISM330DHCx/
├── include/
│ ├── ism330dhcx.h Public API
│ ├── ism330dhcx_types.h HAL, error codes, config + decoded structs, all enums
│ └── ism330dhcx_registers.h Register addresses + bit shifts/masks
├── src/
│ └── ism330dhcx.c Implementation
├── example/
│ └── main.c ESP-IDF I²C polling example
├── test/
│ └── system/
│ └── main.c Hardware-in-the-loop system test (I²C)
├── docs/
│ ├── ism330dhcx_datasheet.pdf
│ └── ism330dhcx_notes.md Datasheet extract: register-bank model, FIFO tags, self-test thresholds
├── CMakeLists.txt Tri-mode (ESP-IDF / Zephyr / plain CMake)
├── Justfile format / format_check / build
├── .clang-format Tabs, Allman, no col limit
├── LICENSE
└── README.md
Everything is namespaced under ism330dhcx*. Returns ISM330DHCXError. Caller allocates the ISM330DHCXDevice and provides the HAL.
ISM330DHCXError ism330dhcxInit(ISM330DHCXDevice* dev, const ISM330DHCXConfig* config, const ISM330DHCXHAL* hal);
ISM330DHCXError ism330dhcxDeinit(ISM330DHCXDevice* dev);
ISM330DHCXError ism330dhcxReset(ISM330DHCXDevice* dev);
ISM330DHCXError ism330dhcxResetHardware(ISM330DHCXDevice* dev); /* requires hal.resetSet */
ISM330DHCXError ism330dhcxReloadCalibration(ISM330DHCXDevice* dev);
ISM330DHCXError ism330dhcxGetWHOAMI(const ISM330DHCXDevice* dev, uint8_t* who_am_i);ISM330DHCXError ism330dhcxSetAccelConfig(ISM330DHCXDevice* dev, const ISM330DHCXAccelConfig* config);
ISM330DHCXError ism330dhcxSetAccelODR(ISM330DHCXDevice* dev, ISM330DHCXAccelODR odr);
ISM330DHCXError ism330dhcxSetAccelFullScale(ISM330DHCXDevice* dev, ISM330DHCXAccelFullScale fs);
ISM330DHCXError ism330dhcxReadAccelRaw(const ISM330DHCXDevice* dev, ISM330DHCXAxesRaw* out);
ISM330DHCXError ism330dhcxReadAccelMilliG(const ISM330DHCXDevice* dev, ISM330DHCXAxesMilli* out);
ISM330DHCXError ism330dhcxSetGyroConfig(ISM330DHCXDevice* dev, const ISM330DHCXGyroConfig* config);
ISM330DHCXError ism330dhcxSetGyroODR(ISM330DHCXDevice* dev, ISM330DHCXGyroODR odr);
ISM330DHCXError ism330dhcxSetGyroFullScale(ISM330DHCXDevice* dev, ISM330DHCXGyroFullScale fs);
ISM330DHCXError ism330dhcxReadGyroRaw(const ISM330DHCXDevice* dev, ISM330DHCXAxesRaw* out);
ISM330DHCXError ism330dhcxReadGyroMilliDPS(const ISM330DHCXDevice* dev, ISM330DHCXAxesMilli* out);
ISM330DHCXError ism330dhcxReadTempRaw(const ISM330DHCXDevice* dev, int16_t* raw);
ISM330DHCXError ism330dhcxReadTempMilliCelsius(const ISM330DHCXDevice* dev, int32_t* milli_celsius);
ISM330DHCXError ism330dhcxReadTimestamp(const ISM330DHCXDevice* dev, uint32_t* ticks);ISM330DHCXError ism330dhcxGetStatus(const ISM330DHCXDevice* dev, ISM330DHCXStatus* status);
ISM330DHCXError ism330dhcxGetInterruptSources(const ISM330DHCXDevice* dev, ISM330DHCXInterruptSources* sources);
ISM330DHCXError ism330dhcxSetFIFOConfig(ISM330DHCXDevice* dev, const ISM330DHCXFIFOConfig* config);
ISM330DHCXError ism330dhcxGetFIFOStatus(const ISM330DHCXDevice* dev, ISM330DHCXFIFOStatus* status);
ISM330DHCXError ism330dhcxFIFOFlush(ISM330DHCXDevice* dev);
ISM330DHCXError ism330dhcxReadFIFOSample(const ISM330DHCXDevice* dev, ISM330DHCXFIFOTag* tag, uint8_t data[6]);
ISM330DHCXError ism330dhcxSetINT1Config(ISM330DHCXDevice* dev, const ISM330DHCXInterruptPinConfig* config);
ISM330DHCXError ism330dhcxSetINT2Config(ISM330DHCXDevice* dev, const ISM330DHCXInterruptPinConfig* config);
ISM330DHCXError ism330dhcxReadINT1(const ISM330DHCXDevice* dev, bool* level); /* requires hal.int1Get */
ISM330DHCXError ism330dhcxReadINT2(const ISM330DHCXDevice* dev, bool* level);
ISM330DHCXError ism330dhcxAccelSelfTest(ISM330DHCXDevice* dev); /* requires hal.delayMs */
ISM330DHCXError ism330dhcxGyroSelfTest(ISM330DHCXDevice* dev);The driver is intentionally scoped to raw IMU acquisition: accelerometer + gyroscope + temperature signal chain, FIFO acquisition, INT1/INT2 routing for data-ready, the per-pin polarity/drive controls, factory self-test, timestamping, and reset/identity. Activity-detection blocks (tap, free-fall, wake-up, 6D/4D), pedometer, sensor-hub master, MLC, FSM, and OIS are out of scope for the gravitometer — the chip exposes them, but this driver does not wrap them. If a future product needs them, add public functions then; the architecture won't change.
The driver doesn't care whether you talk to the chip over SPI or I²C — both buses use the same "register address + N data bytes" pattern, and the chip auto-increments the register pointer for multi-byte transfers when CTRL3_C.IF_INC = 1 (the driver sets this at init).
static int my_read_reg(uint8_t reg, void* data, uint16_t length)
{
return i2c_master_transmit_receive(handle, ®, 1, data, length, timeout_ms);
}
static int my_write_reg(uint8_t reg, const void* data, uint16_t length)
{
/* Pack [reg | data...] and transmit; max 1-byte register address. */
}
const ISM330DHCXHAL hal = { .readReg = my_read_reg, .writeReg = my_write_reg, .delayMs = my_delay };static int my_read_reg(uint8_t reg, void* data, uint16_t length)
{
/* Set the read bit (MSB of address) and clock out (1+length) bytes. */
const uint8_t cmd = reg | 0x80U;
return spi_xfer(&cmd, 1, data, length);
}The chip uses 3- or 4-wire SPI in mode 0 or mode 3 (CPOL = CPHA, both edge configurations work). For 3-wire SPI, the driver leaves CTRL3_C.SIM at its default (4-wire) — flip it via a custom register write if needed.
Drop the repo into components/ISM330DHCx and ESP-IDF builds it automatically.
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/components/ISM330DHCx)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})add_subdirectory(components/ISM330DHCx)
target_link_libraries(my_app PRIVATE ism330dhcx)Or build standalone:
just build
just format_checktest/system/main.c is a hardware-in-the-loop test that covers:
- NULL-pointer / invalid-arg rejection
- Use-before-init / use-after-deinit guards
- Init / deinit / re-init lifecycle and
COMM_FAILpropagation - WHO_AM_I returns 0x6B
- Software reset clears the SW_RESET bit
- Accel ODR/FS round-trip + finite samples
- Gyro ODR/FS round-trip + finite samples
- |g| ≈ 1 g on a stationary device
- Temperature reads in operating range
- Timestamp counter monotonic
- FIFO config/status/flush
- INT routing config writes accepted
- Accelerometer + gyroscope factory self-test
- C11 source; C99 compatible
- Style: Crab Labs C Style Guide — tabs (4-wide), Allman,
camelCasefunctions,PascalCasetypes,ALL_CAPSmacros/enums,snake_casevariables/struct members, units in identifiers - Architecture: Crab Labs Driver Development Guide — HAL via function pointers, caller-allocated device handle, layered internal helpers, no platform headers, no logging, no asserts, no dynamic memory