# `LIS3DH`

Driver for the STMicroelectronics LIS3DH 3-axis MEMS accelerometer.

Communicates over I²C (or any other [Wafer](https://hex.pm/packages/wafer)
transport that implements the `Wafer.I2C` protocol).

## Protocol notes

The LIS3DH uses a standard byte-oriented I²C register protocol. Multi-byte
reads and writes only auto-increment the register address when bit 7 of the
sub-address is set; this driver sets that bit unconditionally on every
transaction, which is harmless for single-byte access and required for
bursts.

## I²C address

The 7-bit address is `0b0011000x` where `x` is the value of the `SA0` pin
(also called `SDO`):

  * `SA0 = GND` → `0x18` (default).
  * `SA0 = VDD` → `0x19`.

## Example

    {:ok, i2c} = Wafer.Driver.Circuits.I2C.acquire(bus_name: "i2c-1", address: 0x18)
    {:ok, acc} = LIS3DH.acquire(conn: i2c)

# `acquire_option`

```elixir
@type acquire_option() ::
  {:conn, Wafer.Conn.t()} | {:verify_who_am_i, boolean()} | {:reboot, boolean()}
```

# `axes`

```elixir
@type axes() :: %{x: float(), y: float(), z: float()}
```

# `t`

```elixir
@type t() :: %LIS3DH{
  conn: Wafer.Conn.t(),
  operating_mode: LIS3DH.Config.operating_mode() | nil,
  range: LIS3DH.Config.range() | nil
}
```

# `who_am_i`

```elixir
@type who_am_i() :: byte()
```

# `acquire`

```elixir
@spec acquire([acquire_option()]) :: {:ok, t()} | {:error, term()}
```

Wrap an existing Wafer connection in a `LIS3DH` struct.

## Options

  * `:conn` (required) — a Wafer connection that implements the `Wafer.I2C`
    protocol, e.g. `Wafer.Driver.Circuits.I2C` or `Wafer.Driver.Fake`.
  * `:verify_who_am_i` (default `true`) — when `true`, read `WHO_AM_I` and
    return `{:error, {:who_am_i_mismatch, got: byte, expected: 0x33}}` if
    the device does not identify as a LIS3DH.
  * `:reboot` (default `false`) — when `true`, set `CTRL_REG5.BOOT` to
    refresh the internal trim registers from non-volatile memory and block
    for 5 ms before returning. Useful after power glitches
    or when you suspect the trim values have been corrupted.

# `configure_accelerometer`

```elixir
@spec configure_accelerometer(
  t(),
  keyword()
) :: {:ok, t()} | {:error, term()}
```

Configure the accelerometer's operating mode, ODR, range, axis enables, and
block-data-update setting. Caches the chosen `:operating_mode` and `:range`
on the struct so subsequent reads can scale samples without re-reading the
config registers.

See `LIS3DH.Config.encode_ctrl_reg_1/1` and
`LIS3DH.Config.encode_ctrl_reg_4/1` for the supported options. `:mode` and
`:odr` are required.

Writes `CTRL_REG4` first (range / HR / BDU), then `CTRL_REG1` (ODR / LPen /
axes), so the device is fully reconfigured before sampling resumes.

# `configure_activity`

```elixir
@spec configure_activity(
  t(),
  keyword()
) :: {:ok, t()} | {:error, term()}
```

Configure sleep-to-wake / return-to-sleep by writing `ACT_THS` and
`ACT_DUR`.

When acceleration falls below `:threshold_mg` for the configured
`:duration`, the device automatically switches to low-power mode at 10 Hz
ODR regardless of the original `CTRL_REG1` / `CTRL_REG4` settings. When
acceleration rises above the threshold, the device restores the original
configuration.

## Options

  * `:threshold_mg` — threshold in milli-g (required). Uses the same LSB
    table as `INT*_THS`. Pass `0` to disable activity detection.
  * `:duration` — `0..255` (required). One LSB corresponds to
    `(8 × duration + 1) / ODR` seconds per datasheet §8.36.

Requires the accelerometer range to be cached on the struct.

# `configure_click`

```elixir
@spec configure_click(
  t(),
  keyword()
) :: {:ok, t()} | {:error, term()}
```

Configure click / double-click / tap detection by writing `CLICK_CFG`,
`CLICK_THS`, `TIME_LIMIT`, `TIME_LATENCY`, and `TIME_WINDOW`.

## Options

  * `:events` — list of `t:LIS3DH.Click.click_event/0` to enable
    (required; pass `[]` to disable all).
  * `:threshold_mg` — threshold in milli-g (required). Same LSB table as
    `INT*_THS`.
  * `:latched` — when `true`, the click interrupt stays high until
    `CLICK_SRC` is read (default `false`).
  * `:time_limit` — `0..127` count of `1/ODR` periods, the max click pulse
    width (required).
  * `:time_latency` — `0..255` count of `1/ODR` periods, the dead time
    after a click (required).
  * `:time_window` — `0..255` count of `1/ODR` periods, the search window
    for the second click of a double-click (default `0`).

Requires the accelerometer range to be cached on the struct.

# `configure_free_fall`

```elixir
@spec configure_free_fall(t(), LIS3DH.Interrupts.pin(), keyword()) ::
  {:ok, t()} | {:error, term()}
```

Configure a free-fall detector on the given interrupt pin.

Free-fall is signalled when the magnitude of acceleration on all three
axes falls below a threshold for a configurable duration (i.e. the device
is in true free fall, ~0 g on every axis).

## Options

  * `:threshold_mg` — threshold in milli-g (default `350`, the AN3308
    recommended value). Lower thresholds trigger more easily.
  * `:duration` — `0..127` count of `1/ODR` periods (default `5`).

# `configure_high_pass_filter`

```elixir
@spec configure_high_pass_filter(
  t(),
  keyword()
) :: {:ok, t()} | {:error, term()}
```

Configure the on-chip high-pass filter via `CTRL_REG2`.

See `LIS3DH.Config.encode_ctrl_reg_2/1` for the supported options.

# `configure_inertial_interrupt`

```elixir
@spec configure_inertial_interrupt(t(), LIS3DH.Interrupts.pin(), keyword()) ::
  {:ok, t()} | {:error, term()}
```

Configure an inertial interrupt (1 or 2) by writing `INT*_CFG`, `INT*_THS`,
and `INT*_DURATION` atomically.

## Options

  * `:mode` — `t:LIS3DH.Interrupts.aoi_mode/0` (default `:or`).
  * `:axes` — list of `t:LIS3DH.Interrupts.axis_event/0` to enable.
  * `:threshold_mg` — non-negative integer threshold in milli-g. The
    LSB size depends on the cached `:range`; this function reads the
    cached value and rounds the threshold to fit.
  * `:duration` — `0..127` count of `1/ODR` periods the condition must
    hold before the interrupt fires (default `0`).

Requires the accelerometer range to be cached on the struct.

# `configure_motion`

```elixir
@spec configure_motion(t(), LIS3DH.Interrupts.pin(), keyword()) ::
  {:ok, t()} | {:error, term()}
```

Configure a motion (wake-up) detector on the given interrupt pin.

Motion is signalled when **any** enabled axis exceeds the threshold for
the configured duration.

## Options

  * `:threshold_mg` — threshold in milli-g (no default, must be specified).
  * `:duration` — `0..127` count of `1/ODR` periods (default `0`).
  * `:axes` — list of `t:LIS3DH.Interrupts.axis_event/0` (default
    `[:x_high, :y_high, :z_high]`).

# `configure_orientation`

```elixir
@spec configure_orientation(t(), LIS3DH.Interrupts.pin(), keyword()) ::
  {:ok, t()} | {:error, term()}
```

Configure 6D or 4D orientation detection on the given interrupt pin.

## Options

  * `:mode` — `:movement` (interrupt fires on transitions between known
    zones) or `:position` (interrupt stays asserted while inside a known
    zone). Default `:position`.
  * `:detection` — `:six_d` (default, all six face-down/face-up directions)
    or `:four_d` (X/Y plane only, Z ignored — for portrait/landscape).
  * `:axes` — list of `t:LIS3DH.Interrupts.axis_event/0` to enable
    (default all six).
  * `:threshold_mg` — threshold in milli-g (no default; the zone half-width
    is typically chosen so two zones don't overlap).
  * `:duration` — `0..127` count of `1/ODR` periods (default `0`).

Writes the configured `INT*_CFG`, `INT*_THS`, `INT*_DURATION` and also
toggles `CTRL_REG5.D4D_INT*` to match the `:detection` choice.

# `default_i2c_address`

```elixir
@spec default_i2c_address() :: 24
```

The default 7-bit I²C address (`0x18`, SA0 pin tied to GND). The alternate
address `0x19` is selected by tying SA0 to VDD.

# `detect_configuration`

```elixir
@spec detect_configuration(t()) :: {:ok, t()} | {:error, term()}
```

Populate the cached `:operating_mode` and `:range` by reading `CTRL_REG1`
and `CTRL_REG4`. Useful after `acquire/1` when the device has already been
configured by some other process.

# `disable_activity`

```elixir
@spec disable_activity(t()) :: {:ok, t()} | {:error, term()}
```

Disable activity detection by writing `0` to `ACT_THS`.

# `disable_auxiliary_adc`

```elixir
@spec disable_auxiliary_adc(t()) :: {:ok, t()} | {:error, term()}
```

Clear `TEMP_CFG_REG.ADC_EN`, disabling all three auxiliary ADC channels.

# `disable_int1_routing`

```elixir
@spec disable_int1_routing(t(), [int1_event]) :: {:ok, t()} | {:error, term()}
when int1_event:
       :click
       | :ia1
       | :ia2
       | :zyxda
       | :adc_drdy_321
       | :fifo_watermark
       | :fifo_overrun
```

Mask out the given routing bits in `CTRL_REG3` (INT1 routing).

# `disable_int2_routing`

```elixir
@spec disable_int2_routing(t(), [int2_event]) :: {:ok, t()} | {:error, term()}
when int2_event: :click | :ia1 | :ia2 | :boot | :activity
```

Mask out the given routing bits in `CTRL_REG6` (INT2 routing).

# `disable_temperature_sensor`

```elixir
@spec disable_temperature_sensor(t()) :: {:ok, t()} | {:error, term()}
```

Clear `TEMP_CFG_REG.TEMP_EN` (leaving `ADC_EN` alone).

# `enable_auxiliary_adc`

```elixir
@spec enable_auxiliary_adc(t()) :: {:ok, t()} | {:error, term()}
```

Enable the on-chip auxiliary ADC by setting `TEMP_CFG_REG.ADC_EN`. The ADC
samples at the configured `CTRL_REG1.ODR`. Requires `:block_data_update`
(`CTRL_REG4.BDU`) to be `:hold` for consistent reads — `configure_accelerometer/2`
defaults to that already.

# `enable_int1_routing`

```elixir
@spec enable_int1_routing(t(), [int1_event]) :: {:ok, t()} | {:error, term()}
when int1_event:
       :click
       | :ia1
       | :ia2
       | :zyxda
       | :adc_drdy_321
       | :fifo_watermark
       | :fifo_overrun
```

OR-in the given routing bits in `CTRL_REG3` (INT1 routing). Leaves the
other bits untouched, so it composes cleanly with `LIS3DH.Sampler` which
also writes the FIFO bits in this register.

Valid `events`: `:click`, `:ia1`, `:ia2`, `:zyxda`, `:adc_drdy_321`,
`:fifo_watermark`, `:fifo_overrun`.

# `enable_int2_routing`

```elixir
@spec enable_int2_routing(t(), [int2_event]) :: {:ok, t()} | {:error, term()}
when int2_event: :click | :ia1 | :ia2 | :boot | :activity
```

OR-in the given routing bits in `CTRL_REG6` (INT2 routing). Preserves the
`INT_POLARITY` bit and any others not in `events`.

Valid `events`: `:click`, `:ia1`, `:ia2`, `:boot`, `:activity`.

# `enable_temperature_sensor`

```elixir
@spec enable_temperature_sensor(t()) :: {:ok, t()} | {:error, term()}
```

Enable the embedded temperature sensor by setting both `TEMP_CFG_REG.ADC_EN`
and `TEMP_CFG_REG.TEMP_EN`. The temperature reading is routed to channel 3
of the auxiliary ADC; read it via `read_temperature/1`.

# `expected_who_am_i`

```elixir
@spec expected_who_am_i() :: 51
```

The expected `WHO_AM_I` value (`0x33`) returned by an unmodified LIS3DH.

# `power_off`

```elixir
@spec power_off(t()) :: {:ok, t()} | {:error, term()}
```

Set `CTRL_REG1.ODR` to `0000` (power-down mode), preserving the other
fields.

# `power_on`

```elixir
@spec power_on(t(), LIS3DH.Config.odr()) :: {:ok, t()} | {:error, term()}
```

Set `CTRL_REG1.ODR` to a non-zero rate without changing the other fields,
bringing the sensor out of power-down. Equivalent to a write to `CTRL_REG1`
with the chosen ODR while preserving the LPen and axis enable bits.

# `read_accelerometer`

```elixir
@spec read_accelerometer(t()) :: {:ok, axes()} | {:error, term()}
```

Read the accelerometer x/y/z sample and return scaled values in m/s².

Requires `:operating_mode` and `:range` to be cached on the struct — call
`configure_accelerometer/2` or `detect_configuration/1` first.

# `read_auxiliary_adc`

```elixir
@spec read_auxiliary_adc(t(), 1 | 2 | 3) :: {:ok, float()} | {:error, term()}
```

Read auxiliary ADC channel 1, 2, or 3 and return the absolute voltage in
millivolts.

The chip's ADC input range is centred on 1200 mV with a
±400 mV span, so the returned value is in
`800..1600` mV.
ADC resolution depends on the operating mode (10-bit in normal /
high-resolution, 8-bit in low-power), so this function requires
`:operating_mode` to be cached on the struct.

# `read_click_source`

```elixir
@spec read_click_source(t()) :: {:ok, LIS3DH.Click.source_flags()} | {:error, term()}
```

Read the `CLICK_SRC` register and decode it. Reading clears the latched
flags if `LIR_Click` was set during configure.

# `read_interrupt_source`

```elixir
@spec read_interrupt_source(t(), LIS3DH.Interrupts.pin()) ::
  {:ok, LIS3DH.Interrupts.source_flags()} | {:error, term()}
```

Read the `INT*_SRC` register. Reading clears the latched flags if latching
is enabled (`LIR_INTx` in `CTRL_REG5`).

# `read_reference`

```elixir
@spec read_reference(t()) :: {:ok, integer()} | {:error, term()}
```

Read the `REFERENCE` register. With `:normal_with_reset` HPF mode (the
default after power-up), this read also resets the high-pass filter's
internal state.

# `read_temperature`

```elixir
@spec read_temperature(t()) :: {:ok, float()} | {:error, term()}
```

Read the embedded temperature sensor on auxiliary ADC channel 3 and return
the **delta** temperature in °C, relative to the 25 °C factory
calibration point (i.e. add `25.0` for the absolute reading).

Only the `OUT_ADC3_H` byte carries temperature data — sensitivity is
`1 LSB/°C` and resolution is 8-bit regardless of operating mode
(datasheet §3.2). The full 16-bit word is still read so `BDU=:hold`
unlatches cleanly.

Requires the temperature sensor to be enabled via
`enable_temperature_sensor/1`.

# `reboot`

```elixir
@spec reboot(t()) :: {:ok, t()} | {:error, term()}
```

Refresh the internal trim registers from non-volatile memory by setting
`CTRL_REG5.BOOT`. Blocks for 5 ms to give the device time
to finish the boot sequence before returning.

# `set_4d_detection`

```elixir
@spec set_4d_detection(t(), LIS3DH.Interrupts.pin(), boolean()) ::
  {:ok, t()} | {:error, term()}
```

Toggle 4D detection for the given pin via `CTRL_REG5.D4D_INT1` /
`D4D_INT2`. 4D restricts 6D detection to the X/Y plane (Z position
ignored). Has no effect unless `INT*_CFG.6D` is also set.

# `set_interrupt_latching`

```elixir
@spec set_interrupt_latching(t(), LIS3DH.Interrupts.pin(), boolean()) ::
  {:ok, t()} | {:error, term()}
```

Toggle interrupt latching for the given pin via `CTRL_REG5.LIR_INT1` /
`LIR_INT2`. When latched, the interrupt pin stays asserted until the
corresponding `INT*_SRC` register is read.

# `set_interrupt_polarity`

```elixir
@spec set_interrupt_polarity(t(), :active_high | :active_low) ::
  {:ok, t()} | {:error, term()}
```

Set the active level for both INT pins via `CTRL_REG6.INT_POLARITY`.

`polarity` is `:active_high` (default after reset) or `:active_low`.

# `set_self_test`

```elixir
@spec set_self_test(t(), LIS3DH.Config.self_test_mode()) ::
  {:ok, t()} | {:error, term()}
```

Set the `CTRL_REG4.ST` self-test field while preserving the other bits.

The recommended self-test procedure (per ST application note AN3308) is:

  1. Power up the device and `configure_accelerometer/2` for normal mode,
     ±2g, 50 Hz, BDU=`:hold`.
  2. Wait for stable output (≥ a few ODR periods) and average several
     baseline samples.
  3. Call `set_self_test(acc, :self_test_0)` and wait for the documented
     turn-on time (90 ms typical).
  4. Average several test samples; the per-axis delta vs. the baseline
     must fall within the limits in datasheet table 4.
  5. Restore with `set_self_test(acc, :off)`.
  6. Optionally repeat with `:self_test_1` for the alternate direction.

This helper just toggles the ST field; the user owns the timing,
averaging, and pass/fail check.

# `who_am_i`

```elixir
@spec who_am_i(t()) :: {:ok, who_am_i()} | {:error, term()}
```

Read the device's `WHO_AM_I` register.

# `write_reference`

```elixir
@spec write_reference(t(), integer()) :: {:ok, t()} | {:error, term()}
```

Write the `REFERENCE` register (used as the HPF reference in `:reference` mode).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
