lis2dh12 (1.0.14)

Published 2026-08-04 13:10:16 +00:00 by faicel in homeiot/lis2dh12

Installation

[registries.forgejo]
index = "sparse+" # Sparse index
# index = "" # Git

[net]
git-fetch-with-cli = true
cargo add lis2dh12@1.0.14 --registry forgejo

About this package

no_std I2C driver for the STMicroelectronics LIS2DH12 accelerometer

LIS2DH12 Accelerometer Driver

no_std Rust driver for the STMicroelectronics LIS2DH12 3-axis accelerometer over I2C.

Design

This crate uses a shared-bus friendly API:

  • the public driver type is lis2dh12::i2c::Lis2dh12
  • the instance stores sensor state
  • runtime operations receive &mut i2c at call time

This makes the driver easier to use on embedded targets where the I2C peripheral is shared with other devices.

Features

  • no_std
  • I2C driver built on embedded-hal 1.0
  • shared-bus friendly public API
  • acceleration readout
  • configurable range, data rate, and power mode
  • threshold/motion interrupts on INT1 and INT2
  • FIFO support
  • temperature sensor support
  • built-in self-test
  • click / double-click detection
  • free-fall detection
  • activity / inactivity detection

Basic usage

use embedded_hal::delay::DelayNs;
use lis2dh12::{i2c::Lis2dh12, DataRate, Lis2dh12Config, Range, SlaveAddr};

# fn demo<I2C, E>(i2c: &mut I2C, delay: &mut impl DelayNs) -> Result<(), lis2dh12::Lis2dh12Error<E>>
# where
#     I2C: embedded_hal::i2c::I2c<Error = E>,
#     E: core::fmt::Debug,
# {
let mut accel = Lis2dh12::new(i2c, SlaveAddr::Low, Lis2dh12Config::default(), delay)?;

accel.set_range(i2c, Range::G2)?;
accel.set_data_rate(i2c, DataRate::Hz_100)?;

let sample = accel.read_acceleration(i2c, delay)?;

let _ = sample;
# Ok(())
# }

Blocking and non-blocking reads

Initialization and blocking reads need a DelayNs implementation:

  • Lis2dh12::new(..., delay) waits 5 ms after CTRL_REG5.BOOT before touching registers again.
  • read_acceleration(i2c, delay) waits until STATUS_REG.ZYXDA is ready, then reads the sample.
  • read_temperature(i2c, delay) and read_temperature_raw(i2c, delay) wait until STATUS_REG_AUX.TDA is ready.

The blocking data-ready timeout is derived from the configured ODR: at least two sample periods, with a 5 ms minimum. If no fresh sample arrives in time, these methods return Lis2dh12Error::DataReadyTimeout. If the device is in DataRate::PowerDown, they return Lis2dh12Error::InvalidConfiguration without polling.

For event loops or RTOS tasks that must not block on sensor readiness, use the non-blocking API:

if accel.is_data_ready(i2c)? {
    if let Some(sample) = accel.try_read_acceleration(i2c)? {
        let _ = sample;
    }
}

if let Some(temp) = accel.try_read_temperature(i2c)? {
    let _ = temp.celsius;
}

Multi-register configuration

High-level helpers that configure several registers, such as motion detection, FIFO, click/tap, free-fall, activity, self-test setup, and timing helpers, are not atomic I2C transactions. If an I2C operation fails in the middle of one of these methods, the sensor may keep a partial configuration.

The driver orders critical helpers so the effective enable/routing write happens last where practical. For example, FIFO mode/watermark is written before FIFO_EN, and interrupt polarity/configuration is prepared before routing interrupt sources to INT1/INT2. This reduces the chance of enabling an incomplete configuration, but it is not a rollback guarantee.

After an error from a high-level multi-register helper, recover by reinitializing the device with Lis2dh12::new(..., delay) or by applying the full intended configuration sequence again before relying on the sensor state.

I2C addresses

The LIS2DH12 supports two I2C addresses depending on the SA0/SDO pin state:

  • 0x18: SlaveAddr::Low
  • 0x19: SlaveAddr::High

The application must pass the expected address to Lis2dh12::new. Do not use WHO_AM_I = 0x33 as an automatic model detector: ST reuses that value across related parts such as LIS2DH12, LIS3DH, and LIS3DE. new still checks WHO_AM_I as a sanity check after the caller-selected address responds.

Example:

use embedded_hal::delay::DelayNs;
use lis2dh12::{i2c::Lis2dh12, Lis2dh12Config, SlaveAddr};

# fn demo<I2C, E>(i2c: &mut I2C, delay: &mut impl DelayNs) -> Result<(), lis2dh12::Lis2dh12Error<E>>
# where
#     I2C: embedded_hal::i2c::I2c<Error = E>,
#     E: core::fmt::Debug,
# {
let low = Lis2dh12::new(i2c, SlaveAddr::Low, Lis2dh12Config::default(), delay)?;
let _ = low.address();
# Ok(())
# }

Interrupt / motion configuration

use lis2dh12::{
    i2c::Lis2dh12, InterruptPin, Lis2dh12Config, MotionConfig, SlaveAddr,
};

# fn demo<I2C, E>(i2c: &mut I2C, delay: &mut impl embedded_hal::delay::DelayNs) -> Result<(), lis2dh12::Lis2dh12Error<E>>
# where
#     I2C: embedded_hal::i2c::I2c<Error = E>,
#     E: core::fmt::Debug,
# {
let config = Lis2dh12Config::default().with_interrupt_active_low(false);
let mut accel = Lis2dh12::new(i2c, SlaveAddr::Low, config, delay)?;

let motion = MotionConfig::vibration_default(6, 1);
accel.configure_motion_detection(i2c, InterruptPin::Int1, motion)?;

let source = accel.read_motion_interrupt(i2c, InterruptPin::Int1)?;
let _ = source.motion;
# Ok(())
# }

For lower-level interrupt setup, the public API also exposes:

  • configure_interrupt
  • set_interrupt_threshold
  • set_interrupt_duration
  • enable_interrupt_pin
  • read_interrupt_source
  • clear_interrupt_flags

Interrupt polarity is global on the LIS2DH12: CTRL_REG6.INT_POLARITY affects both INT1 and INT2. Lis2dh12Config::default() keeps the datasheet default, active-high. Use Lis2dh12Config::with_interrupt_active_low(active_low) during init, or set_interrupt_polarity(active_low) for an explicit runtime change, before routing any interrupt source. Per-source helpers only route or unroute events; they do not change polarity.

FIFO

use lis2dh12::{i2c::Lis2dh12, Acceleration, FifoConfig, FifoMode, FIFO_CAPACITY, Lis2dh12Config, SlaveAddr};

# fn demo<I2C, E>(i2c: &mut I2C, delay: &mut impl embedded_hal::delay::DelayNs) -> Result<(), lis2dh12::Lis2dh12Error<E>>
# where
#     I2C: embedded_hal::i2c::I2c<Error = E>,
#     E: core::fmt::Debug,
# {
let mut accel = Lis2dh12::new(i2c, SlaveAddr::Low, Lis2dh12Config::default(), delay)?;

accel.configure_fifo(i2c, FifoConfig::new(FifoMode::Stream).with_watermark(16))?;

let mut samples = [Acceleration::new(0.0, 0.0, 0.0); FIFO_CAPACITY];
let count = accel.read_fifo(i2c, &mut samples)?;
let status = accel.fifo_status(i2c)?;

let _ = (count, status.empty);
# Ok(())
# }

Temperature sensor

use embedded_hal::delay::DelayNs;
use lis2dh12::{i2c::Lis2dh12, Lis2dh12Config, SlaveAddr};

# fn demo<I2C, E>(i2c: &mut I2C, delay: &mut impl DelayNs) -> Result<(), lis2dh12::Lis2dh12Error<E>>
# where
#     I2C: embedded_hal::i2c::I2c<Error = E>,
#     E: core::fmt::Debug,
# {
let mut accel = Lis2dh12::new(i2c, SlaveAddr::Low, Lis2dh12Config::default(), delay)?;

accel.enable_temperature_sensor(i2c)?;

if accel.temperature_status(i2c)?.data_available {
    let temp = accel.read_temperature(i2c, delay)?;
    let _ = temp.celsius;
}

if let Some(temp) = accel.try_read_temperature(i2c)? {
    let _ = temp.celsius;
}
# Ok(())
# }

Self-test

use embedded_hal::delay::DelayNs;
use lis2dh12::{i2c::Lis2dh12, Lis2dh12Config, SelfTestMode, SlaveAddr};

# fn demo<I2C, E>(i2c: &mut I2C, delay: &mut impl DelayNs) -> Result<(), lis2dh12::Lis2dh12Error<E>>
# where
#     I2C: embedded_hal::i2c::I2c<Error = E>,
#     E: core::fmt::Debug,
# {
let mut accel = Lis2dh12::new(i2c, SlaveAddr::Low, Lis2dh12Config::default(), delay)?;

let result = accel.run_self_test(i2c, delay, SelfTestMode::Test0)?;
let _ = result.passed;
# Ok(())
# }

Click / tap

use lis2dh12::{
    i2c::Lis2dh12, ClickConfig, ClickTiming, InterruptPin, Lis2dh12Config, SlaveAddr,
};

# fn demo<I2C, E>(i2c: &mut I2C, delay: &mut impl embedded_hal::delay::DelayNs) -> Result<(), lis2dh12::Lis2dh12Error<E>>
# where
#     I2C: embedded_hal::i2c::I2c<Error = E>,
#     E: core::fmt::Debug,
# {
let mut accel = Lis2dh12::new(i2c, SlaveAddr::Low, Lis2dh12Config::default(), delay)?;

accel.configure_click(i2c, ClickConfig::new().with_x_single(true).with_x_double(true))?;
accel.set_click_threshold(i2c, 20, true)?;
accel.set_click_timing(
    i2c,
    ClickTiming::new()
        .with_time_limit(10)
        .with_time_latency(20)
        .with_time_window(50)
        .with_latch(true),
)?;
accel.enable_click_interrupt(i2c, InterruptPin::Int1, true)?;

let source = accel.read_click_source(i2c)?;
let _ = source.active;
# Ok(())
# }

Free-fall and activity detection

use lis2dh12::{
    i2c::Lis2dh12, ActivityConfig, FreeFallConfig, InterruptPin, Lis2dh12Config, SlaveAddr,
};

# fn demo<I2C, E>(i2c: &mut I2C, delay: &mut impl embedded_hal::delay::DelayNs) -> Result<(), lis2dh12::Lis2dh12Error<E>>
# where
#     I2C: embedded_hal::i2c::I2c<Error = E>,
#     E: core::fmt::Debug,
# {
let mut accel = Lis2dh12::new(i2c, SlaveAddr::Low, Lis2dh12Config::default(), delay)?;

accel.configure_free_fall(i2c, InterruptPin::Int1, FreeFallConfig::recommended_2g())?;
accel.enable_free_fall_interrupt(i2c, InterruptPin::Int1, true)?;

accel.configure_activity(i2c, ActivityConfig::new(16, 4))?;
accel.enable_activity_interrupt(i2c, true)?;
# Ok(())
# }

Notes

  • This crate currently focuses on the I2C path.
  • The public API is intentionally instance-based and shared-bus friendly.
  • Internally the crate uses an attached driver layer, but that is not part of the public API.

Tests

./scripts/test.sh
cargo deny check
./scripts/check-version-sync.sh
./scripts/test-resolve-cargo-registry.sh
./scripts/test-release-branch-name.sh

./scripts/test.sh runs version sync, CI helper unit tests, then cargo test.

cargo deny check enforces dependency advisories, allowed licenses, bans, and crate sources (see deny.toml). The same check runs in Forgejo CI.

./scripts/check-version-sync.sh fails if Cargo.toml version does not match the latest ## [x.y.z] heading in CHANGELOG.md. Forgejo CI runs the same gate via the reusable workflow rust_crate_checks.yml from central_ci. When releasing, bump both files to the same version in the same commit.

Publish / registries

Branch Tag Registry
dev vX.Y.Z-rc forgejorc (unstable)
staging — (merge gate only) —
main vX.Y.Z (+ ensures release/X.Y) forgejo (stable)

Cargo.toml stays at X.Y.Z. The registry (not the version) separates stable from unstable. Bump Cargo.toml + CHANGELOG before each publish (a version can be published once per registry). On first stable publish of a minor line, CI creates release/X.Y for future hotfixes. Details: .forgejo/README.md.

CI security note

Workflows live under .forgejo/. Branch protection on main uses Protected file patterns .gitea/**/*.yml;.forgejo/**/*.yml, so those workflows cannot be changed by a direct push. That compensates for a single RUNNER_TOKEN. See .forgejo/README.md.

Documentation

For device-level details, see the official STMicroelectronics LIS2DH12 datasheet.

Dependencies

ID Version
accelerometer ^0.12
embedded-hal ^1.0.0
embedded-hal-mock ^0.11.1
Details
Cargo
2026-08-04 13:10:16 +00:00
2
652 KiB
Assets (1)
Versions (11) View all
1.0.14 2026-08-04
1.0.13 2026-08-04
1.0.12 2026-08-03
1.0.11 2026-08-03
1.0.10 2026-08-03