defmodule LIS3DH do @moduledoc """ 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) """ import Bitwise alias LIS3DH.Click alias LIS3DH.Config alias LIS3DH.Interrupts alias LIS3DH.Registers alias Wafer.Chip alias Wafer.Conn defstruct conn: nil, operating_mode: nil, range: nil @type t :: %__MODULE__{ conn: Conn.t(), operating_mode: Config.operating_mode() | nil, range: Config.range() | nil } @type who_am_i :: byte @type axes :: %{x: float, y: float, z: float} @type acquire_option :: {:conn, Conn.t()} | {:verify_who_am_i, boolean} | {:reboot, boolean} @behaviour Wafer.Conn @default_i2c_address 0x18 @expected_who_am_i 0x33 @boot_delay_ms 5 @ctrl_reg_5_boot_bit 7 @gravity_ms2 9.80665 @out_x_l 0x28 @out_adc1_l 0x08 @aux_adc_center_mv 1200 @aux_adc_span_mv 400 @temp_en_bit 6 @adc_en_bit 7 @doc """ The default 7-bit I²C address (`0x18`, SA0 pin tied to GND). The alternate address `0x19` is selected by tying SA0 to VDD. """ @spec default_i2c_address() :: 0x18 def default_i2c_address, do: @default_i2c_address @doc """ The expected `WHO_AM_I` value (`0x33`) returned by an unmodified LIS3DH. """ @spec expected_who_am_i() :: 0x33 def expected_who_am_i, do: @expected_who_am_i @doc """ 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 #{@boot_delay_ms} ms before returning. Useful after power glitches or when you suspect the trim values have been corrupted. """ @impl Wafer.Conn @spec acquire([acquire_option]) :: {:ok, t} | {:error, term} def acquire(opts) when is_list(opts) do with {:ok, conn} <- fetch_conn(opts), acc = %__MODULE__{conn: conn}, {:ok, acc} <- maybe_reboot(acc, opts) do maybe_verify_who_am_i(acc, opts) end end @doc """ Read the device's `WHO_AM_I` register. """ @spec who_am_i(t) :: {:ok, who_am_i} | {:error, term} def who_am_i(%__MODULE__{} = acc) do with {:ok, <>} <- Registers.read_who_am_i(acc) do {:ok, id} end end @doc """ Refresh the internal trim registers from non-volatile memory by setting `CTRL_REG5.BOOT`. Blocks for #{@boot_delay_ms} ms to give the device time to finish the boot sequence before returning. """ @spec reboot(t) :: {:ok, t} | {:error, term} def reboot(%__MODULE__{} = acc) do with {:ok, acc} <- Registers.update_ctrl_reg_5(acc, fn <> -> <> end) do Process.sleep(@boot_delay_ms) {:ok, acc} end end @doc """ 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. """ @spec configure_accelerometer(t, keyword) :: {:ok, t} | {:error, term} def configure_accelerometer(%__MODULE__{} = acc, opts) when is_list(opts) do mode = Keyword.fetch!(opts, :mode) range = Keyword.get(opts, :range, 2) ctrl_reg_1 = Config.encode_ctrl_reg_1(opts) ctrl_reg_4 = Config.encode_ctrl_reg_4(opts) with {:ok, acc} <- Registers.write_ctrl_reg_4(acc, ctrl_reg_4), {:ok, acc} <- Registers.write_ctrl_reg_1(acc, ctrl_reg_1) do {:ok, %{acc | operating_mode: mode, range: range}} end end @doc """ 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. """ @spec detect_configuration(t) :: {:ok, t} | {:error, term} def detect_configuration(%__MODULE__{} = acc) do with {:ok, ctrl_reg_1} <- Registers.read_ctrl_reg_1(acc), {:ok, ctrl_reg_4} <- Registers.read_ctrl_reg_4(acc) do %{lpen: lpen} = Config.decode_ctrl_reg_1(ctrl_reg_1) %{hr: hr, range: range} = Config.decode_ctrl_reg_4(ctrl_reg_4) mode = Config.operating_mode(lpen, hr) {:ok, %{acc | operating_mode: mode, range: range}} end end @doc """ 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. """ @spec read_accelerometer(t) :: {:ok, axes} | {:error, term} def read_accelerometer(%__MODULE__{operating_mode: nil}), do: {:error, :operating_mode_not_set} def read_accelerometer(%__MODULE__{range: nil}), do: {:error, :range_not_set} def read_accelerometer(%__MODULE__{operating_mode: mode, range: range} = acc) do with {:ok, <>} <- Chip.read_register(acc, @out_x_l, 6) do {:ok, %{x: scale(x, mode, range), y: scale(y, mode, range), z: scale(z, mode, range)}} end end @doc """ 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. """ @spec power_on(t, Config.odr()) :: {:ok, t} | {:error, term} def power_on(%__MODULE__{} = acc, odr) do Registers.update_ctrl_reg_1(acc, fn <> -> odr_code = encode_odr!(odr) <> end) end @doc """ Set `CTRL_REG1.ODR` to `0000` (power-down mode), preserving the other fields. """ @spec power_off(t) :: {:ok, t} | {:error, term} def power_off(%__MODULE__{} = acc) do Registers.update_ctrl_reg_1(acc, fn <> -> <> end) end @doc """ 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. """ @spec enable_auxiliary_adc(t) :: {:ok, t} | {:error, term} def enable_auxiliary_adc(%__MODULE__{} = acc) do Registers.update_temp_cfg_reg(acc, fn <> -> <> end) end @doc "Clear `TEMP_CFG_REG.ADC_EN`, disabling all three auxiliary ADC channels." @spec disable_auxiliary_adc(t) :: {:ok, t} | {:error, term} def disable_auxiliary_adc(%__MODULE__{} = acc) do Registers.update_temp_cfg_reg(acc, fn <> -> <> end) end @doc """ 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`. """ @spec enable_temperature_sensor(t) :: {:ok, t} | {:error, term} def enable_temperature_sensor(%__MODULE__{} = acc) do Registers.update_temp_cfg_reg(acc, fn <<_byte>> -> <<1 <<< @adc_en_bit ||| 1 <<< @temp_en_bit>> end) end @doc "Clear `TEMP_CFG_REG.TEMP_EN` (leaving `ADC_EN` alone)." @spec disable_temperature_sensor(t) :: {:ok, t} | {:error, term} def disable_temperature_sensor(%__MODULE__{} = acc) do Registers.update_temp_cfg_reg(acc, fn <> -> <> end) end @doc """ Read auxiliary ADC channel 1, 2, or 3 and return the absolute voltage in millivolts. The chip's ADC input range is centred on #{@aux_adc_center_mv} mV with a ±#{@aux_adc_span_mv} mV span, so the returned value is in `#{@aux_adc_center_mv - @aux_adc_span_mv}..#{@aux_adc_center_mv + @aux_adc_span_mv}` 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. """ @spec read_auxiliary_adc(t, 1 | 2 | 3) :: {:ok, float} | {:error, term} def read_auxiliary_adc(%__MODULE__{operating_mode: nil}, _channel), do: {:error, :operating_mode_not_set} def read_auxiliary_adc(%__MODULE__{operating_mode: mode} = acc, channel) when channel in 1..3 do address = @out_adc1_l + (channel - 1) * 2 with {:ok, <>} <- Chip.read_register(acc, address, 2) do {:ok, scale_aux_adc(raw, mode)} end end @doc """ 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`. """ @spec read_temperature(t) :: {:ok, float} | {:error, term} def read_temperature(%__MODULE__{} = acc) do with {:ok, <>} <- Chip.read_register(acc, @out_adc1_l + 4, 2) do {:ok, (raw >>> 8) * 1.0} end end # Data is left-justified — meaningful bits at the MSB end. Arithmetic right # shift recovers the native signed N-bit value, then we scale by the # per-mode mg/LSB and convert mg → m/s². defp scale(raw, mode, range) do shift = 16 - Config.native_width(mode) sensitivity_mg = Config.sensitivity(mode, range) (raw >>> shift) * sensitivity_mg * @gravity_ms2 / 1000 end # Aux ADC is left-justified 10-bit (HR/Normal) or 8-bit (low-power) signed. # Recover the N-bit signed value, then map ±full-scale → ±@aux_adc_span_mv # added to the @aux_adc_center_mv centre. defp scale_aux_adc(raw, mode) do width = Config.aux_adc_width(mode) shift = 16 - width full_scale = 1 <<< (width - 1) @aux_adc_center_mv + (raw >>> shift) * @aux_adc_span_mv / full_scale end defp encode_odr!(odr) do Map.fetch!( %{ :power_down => 0b0000, 1 => 0b0001, 10 => 0b0010, 25 => 0b0011, 50 => 0b0100, 100 => 0b0101, 200 => 0b0110, 400 => 0b0111, 1600 => 0b1000, 1344 => 0b1001, 5376 => 0b1001 }, odr ) end @doc """ Configure the on-chip high-pass filter via `CTRL_REG2`. See `LIS3DH.Config.encode_ctrl_reg_2/1` for the supported options. """ @spec configure_high_pass_filter(t, keyword) :: {:ok, t} | {:error, term} def configure_high_pass_filter(%__MODULE__{} = acc, opts \\ []) do Registers.write_ctrl_reg_2(acc, Config.encode_ctrl_reg_2(opts)) end @doc """ 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. """ @spec read_reference(t) :: {:ok, integer} | {:error, term} def read_reference(%__MODULE__{} = acc) do with {:ok, <>} <- Registers.read_reference(acc) do {:ok, value} end end @doc "Write the `REFERENCE` register (used as the HPF reference in `:reference` mode)." @spec write_reference(t, integer) :: {:ok, t} | {:error, term} def write_reference(%__MODULE__{} = acc, value) when value in -128..127 do Registers.write_reference(acc, <>) end @doc """ 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`). """ @spec configure_free_fall(t, Interrupts.pin(), keyword) :: {:ok, t} | {:error, term} def configure_free_fall(%__MODULE__{} = acc, pin, opts \\ []) do configure_inertial_interrupt(acc, pin, mode: :and, axes: [:x_low, :y_low, :z_low], threshold_mg: Keyword.get(opts, :threshold_mg, 350), duration: Keyword.get(opts, :duration, 5) ) end @doc """ 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]`). """ @spec configure_motion(t, Interrupts.pin(), keyword) :: {:ok, t} | {:error, term} def configure_motion(%__MODULE__{} = acc, pin, opts) do configure_inertial_interrupt(acc, pin, mode: :or, axes: Keyword.get(opts, :axes, [:x_high, :y_high, :z_high]), threshold_mg: Keyword.fetch!(opts, :threshold_mg), duration: Keyword.get(opts, :duration, 0) ) end @doc """ 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. """ @spec configure_orientation(t, Interrupts.pin(), keyword) :: {:ok, t} | {:error, term} def configure_orientation(%__MODULE__{} = acc, pin, opts) do aoi_mode = case Keyword.get(opts, :mode, :position) do :movement -> :six_d_movement :position -> :six_d_position end detection = Keyword.get(opts, :detection, :six_d) axes = Keyword.get(opts, :axes, [:x_high, :x_low, :y_high, :y_low, :z_high, :z_low]) with {:ok, acc} <- configure_inertial_interrupt(acc, pin, mode: aoi_mode, axes: axes, threshold_mg: Keyword.fetch!(opts, :threshold_mg), duration: Keyword.get(opts, :duration, 0) ) do set_4d_detection(acc, pin, detection == :four_d) end end @doc """ 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. """ @spec configure_activity(t, keyword) :: {:ok, t} | {:error, term} def configure_activity(%__MODULE__{range: nil}, _opts), do: {:error, :range_not_set} def configure_activity(%__MODULE__{range: range} = acc, opts) do threshold_mg = Keyword.fetch!(opts, :threshold_mg) duration = Keyword.fetch!(opts, :duration) unless is_integer(duration) and duration in 0..255, do: raise(ArgumentError, "invalid duration: #{inspect(duration)} (must be 0..255)") ths = Interrupts.encode_threshold!(threshold_mg, range) with {:ok, acc} <- Registers.write_act_ths(acc, ths) do Registers.write_act_dur(acc, <>) end end @doc "Disable activity detection by writing `0` to `ACT_THS`." @spec disable_activity(t) :: {:ok, t} | {:error, term} def disable_activity(%__MODULE__{} = acc) do Registers.write_act_ths(acc, <<0>>) end @doc """ 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. """ @spec configure_click(t, keyword) :: {:ok, t} | {:error, term} def configure_click(%__MODULE__{range: nil}, _opts), do: {:error, :range_not_set} def configure_click(%__MODULE__{range: range} = acc, opts) do events = Keyword.fetch!(opts, :events) threshold_mg = Keyword.fetch!(opts, :threshold_mg) latched = Keyword.get(opts, :latched, false) time_limit = Keyword.fetch!(opts, :time_limit) time_latency = Keyword.fetch!(opts, :time_latency) time_window = Keyword.get(opts, :time_window, 0) unless is_integer(time_limit) and time_limit in 0..127, do: raise(ArgumentError, "invalid time_limit: #{inspect(time_limit)} (must be 0..127)") unless is_integer(time_latency) and time_latency in 0..255, do: raise(ArgumentError, "invalid time_latency: #{inspect(time_latency)} (must be 0..255)") unless is_integer(time_window) and time_window in 0..255, do: raise(ArgumentError, "invalid time_window: #{inspect(time_window)} (must be 0..255)") with {:ok, acc} <- Registers.write_click_ths(acc, Click.encode_click_ths!(threshold_mg, range, latched)), {:ok, acc} <- Registers.write_time_limit(acc, <>), {:ok, acc} <- Registers.write_time_latency(acc, <>), {:ok, acc} <- Registers.write_time_window(acc, <>) do Registers.write_click_cfg(acc, Click.encode_click_cfg(events)) end end @doc """ Read the `CLICK_SRC` register and decode it. Reading clears the latched flags if `LIR_Click` was set during configure. """ @spec read_click_source(t) :: {:ok, Click.source_flags()} | {:error, term} def read_click_source(%__MODULE__{} = acc) do with {:ok, byte} <- Registers.read_click_src(acc) do {:ok, Click.decode_click_src(byte)} end end @doc """ 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. """ @spec configure_inertial_interrupt(t, Interrupts.pin(), keyword) :: {:ok, t} | {:error, term} def configure_inertial_interrupt(%__MODULE__{range: nil}, _pin, _opts), do: {:error, :range_not_set} def configure_inertial_interrupt(%__MODULE__{range: range} = acc, pin, opts) when pin in [:int1, :int2] do cfg = Interrupts.encode_int_cfg(opts) ths = Interrupts.encode_threshold!(Keyword.get(opts, :threshold_mg, 0), range) dur = Interrupts.encode_duration!(Keyword.get(opts, :duration, 0)) {cfg_w, ths_w, dur_w} = case pin do :int1 -> {&Registers.write_int1_cfg/2, &Registers.write_int1_ths/2, &Registers.write_int1_duration/2} :int2 -> {&Registers.write_int2_cfg/2, &Registers.write_int2_ths/2, &Registers.write_int2_duration/2} end with {:ok, acc} <- ths_w.(acc, ths), {:ok, acc} <- dur_w.(acc, dur) do cfg_w.(acc, cfg) end end @doc """ Read the `INT*_SRC` register. Reading clears the latched flags if latching is enabled (`LIR_INTx` in `CTRL_REG5`). """ @spec read_interrupt_source(t, Interrupts.pin()) :: {:ok, Interrupts.source_flags()} | {:error, term} def read_interrupt_source(%__MODULE__{} = acc, pin) when pin in [:int1, :int2] do reader = if pin == :int1, do: &Registers.read_int1_src/1, else: &Registers.read_int2_src/1 with {:ok, byte} <- reader.(acc) do {:ok, Interrupts.decode_int_src(byte)} end end @doc """ 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`. """ @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 def enable_int1_routing(%__MODULE__{} = acc, events) when is_list(events) do mask = int1_routing_mask(events) Registers.update_ctrl_reg_3(acc, fn <> -> <> end) end @doc "Mask out the given routing bits in `CTRL_REG3` (INT1 routing)." @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 def disable_int1_routing(%__MODULE__{} = acc, events) when is_list(events) do mask = int1_routing_mask(events) Registers.update_ctrl_reg_3(acc, fn <> -> <> end) end @doc """ 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`. """ @spec enable_int2_routing(t, [int2_event]) :: {:ok, t} | {:error, term} when int2_event: :click | :ia1 | :ia2 | :boot | :activity def enable_int2_routing(%__MODULE__{} = acc, events) when is_list(events) do mask = int2_routing_mask(events) Registers.update_ctrl_reg_6(acc, fn <> -> <> end) end @doc "Mask out the given routing bits in `CTRL_REG6` (INT2 routing)." @spec disable_int2_routing(t, [int2_event]) :: {:ok, t} | {:error, term} when int2_event: :click | :ia1 | :ia2 | :boot | :activity def disable_int2_routing(%__MODULE__{} = acc, events) when is_list(events) do mask = int2_routing_mask(events) Registers.update_ctrl_reg_6(acc, fn <> -> <> end) end @doc """ Set the active level for both INT pins via `CTRL_REG6.INT_POLARITY`. `polarity` is `:active_high` (default after reset) or `:active_low`. """ @spec set_interrupt_polarity(t, :active_high | :active_low) :: {:ok, t} | {:error, term} def set_interrupt_polarity(%__MODULE__{} = acc, polarity) when polarity in [:active_high, :active_low] do bit = if polarity == :active_low, do: 1 <<< 1, else: 0 Registers.update_ctrl_reg_6(acc, fn <> -> <<(byte &&& bnot(1 <<< 1) &&& 0xFF) ||| bit>> end) end @doc """ 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. """ @spec set_interrupt_latching(t, Interrupts.pin(), boolean) :: {:ok, t} | {:error, term} def set_interrupt_latching(%__MODULE__{} = acc, pin, latched?) when pin in [:int1, :int2] do bit = if pin == :int1, do: 3, else: 1 update_bit(acc, &Registers.update_ctrl_reg_5/2, bit, latched?) end @doc """ 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. """ @spec set_4d_detection(t, Interrupts.pin(), boolean) :: {:ok, t} | {:error, term} def set_4d_detection(%__MODULE__{} = acc, pin, enabled?) when pin in [:int1, :int2] do bit = if pin == :int1, do: 2, else: 0 update_bit(acc, &Registers.update_ctrl_reg_5/2, bit, enabled?) end defp int1_routing_mask(events) do map = %{ click: 1 <<< 7, ia1: 1 <<< 6, ia2: 1 <<< 5, zyxda: 1 <<< 4, adc_drdy_321: 1 <<< 3, fifo_watermark: 1 <<< 2, fifo_overrun: 1 <<< 1 } Enum.reduce(events, 0, fn event, acc -> acc ||| Map.fetch!(map, event) end) end defp int2_routing_mask(events) do map = %{ click: 1 <<< 7, ia1: 1 <<< 6, ia2: 1 <<< 5, boot: 1 <<< 4, activity: 1 <<< 3 } Enum.reduce(events, 0, fn event, acc -> acc ||| Map.fetch!(map, event) end) end defp update_bit(acc, updater, bit, true), do: updater.(acc, fn <> -> <> end) defp update_bit(acc, updater, bit, false), do: updater.(acc, fn <> -> <> end) @doc """ 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. """ @spec set_self_test(t, Config.self_test_mode()) :: {:ok, t} | {:error, term} def set_self_test(%__MODULE__{} = acc, mode) do st_code = Config.self_test_code(mode) Registers.update_ctrl_reg_4(acc, fn <> -> <<(byte &&& bnot(0b110) &&& 0xFF) ||| st_code <<< 1>> end) end defp fetch_conn(opts) do case Keyword.fetch(opts, :conn) do {:ok, conn} -> {:ok, conn} :error -> {:error, "`:conn` option is required"} end end defp maybe_reboot(acc, opts) do if Keyword.get(opts, :reboot, false), do: reboot(acc), else: {:ok, acc} end defp maybe_verify_who_am_i(acc, opts) do if Keyword.get(opts, :verify_who_am_i, true), do: verify_who_am_i(acc), else: {:ok, acc} end defp verify_who_am_i(acc) do case who_am_i(acc) do {:ok, @expected_who_am_i} -> {:ok, acc} {:ok, got} -> {:error, {:who_am_i_mismatch, got: got, expected: @expected_who_am_i}} {:error, _} = error -> error end end end defimpl Wafer.Chip, for: LIS3DH do @moduledoc """ `Wafer.Chip` implementation that sets bit 7 (auto-increment) of the sub-address on every read and write, satisfying the LIS3DH's requirement for multi-byte transfers without breaking single-byte access. """ import Bitwise alias Wafer.I2C @auto_increment 0x80 def read_register(%LIS3DH{conn: inner}, address, bytes) when is_integer(address) and address in 0..0x7F and is_integer(bytes) and bytes > 0 do with {:ok, data, _inner} <- I2C.write_read(inner, <
>, bytes, []) do {:ok, data} end end def read_register(_conn, address, bytes) do {:error, "Invalid argument: address=#{inspect(address)} bytes=#{inspect(bytes)} " <> "(address must be in 0..0x7F, bytes must be a positive integer)"} end def write_register(%LIS3DH{conn: inner} = conn, address, data) when is_integer(address) and address in 0..0x7F and is_binary(data) and byte_size(data) > 0 do with {:ok, inner} <- I2C.write(inner, <
>, []) do {:ok, %{conn | conn: inner}} end end def write_register(_conn, address, data) do {:error, "Invalid argument: address=#{inspect(address)} data=#{inspect(data)} " <> "(address must be in 0..0x7F, data must be a non-empty binary)"} end def swap_register(conn, address, data) when is_binary(data) do with {:ok, old} <- read_register(conn, address, byte_size(data)), {:ok, conn} <- write_register(conn, address, data) do {:ok, old, conn} end end def swap_register(_conn, _address, data), do: {:error, "Invalid argument: data must be a binary, got #{inspect(data)}"} end