Skip to content

Sensors

How the firmware drives, samples, validates, and publishes data from the five on-board sensor chips: the ADS1115 (four-channel analog-to-digital converter), the INA228 (battery monitor), the DS18B20 (alternator temperature probe), the BMP390 (barometric pressure and board temperature), and the LSM6DSOX (inertial measurement unit, IMU). External data sources (VE.Direct, NMEA2000, NMEA0183) are covered elsewhere.

The shared I²C bus runs at standard Fast-mode speed (400 kHz) with a short bus timeout, so a wedged device costs milliseconds, not a hung loop. Every sample that passes validation stamps a freshness timestamp (MARK_FRESH(IDX_*)); consumers check staleness against those stamps rather than trusting the last value forever.


ADS1115 — Four-Channel Analog Converter (I²C 0x48)

Configured for a wide input range and its maximum sample rate, in single-shot triggered mode. The library wrapper is used only for setup and conversion triggering; conversion-register reads are done as raw two-phase I²C transactions so the firmware can avoid the library's blocking poll and time bus stalls in microseconds. Search 5_functions.ino for _ReadAnalogInputs_inner.

Non-blocking state machine

A three-state machine drives each conversion without blocking the loop:

  • Idle — select the next channel, trigger a conversion, note the time.
  • Wait — a time-based ready check (a few milliseconds, with margin over the theoretical conversion time); a timeout retries the same channel.
  • Read result — raw register read with microsecond timing, channel-specific scaling, a sanity-band check, then the freshness stamp. Repeated consecutive failures set ADS1115Disconnected so a dead chip cannot stall the system.

Sample sequence

The channel order interleaves alternator current between every other measurement:

adsSeq = { CH1, CH0, CH1, CH2, CH1, CH3 }

CH1 (alternator current) gets half the slots — roughly two hundred samples per second, since it feeds the fastest control loop — while battery voltage, RPM, and the thermistor each refresh several tens of times per second.

What each channel measures

Ch Variable Source Notes
0 BatteryV (V) High-ratio resistor divider Used only for the cross-sensor disagreement check on the safeties page
1 MeasuredAmps (A) Hall-effect current clamp, mid-rail centered Scale selected by AmpSensorRange; optional sign flip (InvertAltAmps); user offset (AlternatorCOffset) and auto-zero offset (DynamicAltCurrentZero) subtracted
2 RPM LM2907 frequency-to-voltage converter (see LM2907 Resistor Mod) Scaled by RPMScalingFactor; very low readings forced to zero
3 temperatureThermistor (°F) NTC thermistor via op-amp chain Converted by the Beta-equation helper — search 5_functions.ino for thermistorTempC. The thermistor constants (R_fixed, R0, Beta, T0_C) are user settings persisted in NVS

Each channel has its own plausibility band; readings outside it are discarded and never stamped fresh.

Why CH1 is special

CH1 is the heartbeat of the control loop. Every confirmed sample:

  1. Lands in a large PSRAM ring for history and statistics (ch1_record in 7_functions.ino).
  2. Updates the smoothing filters consumed by the current-excess supervisor and the current loop (exponential and moving-average variants, so the user-selectable signal sources are always populated).
  3. Updates session and lifetime peak-current watermarks.
  4. Sets a freshness flag that gates one pass of the control function — the control loop runs per fresh sample, not on a wall-clock timer.

Performance counters (I²C error count, slow-read count, worst-case timings) are published to the dashboard so a marginal bus shows up as numbers rather than unexplained stalls.


INA228 — Battery Voltage and Current Monitor (I²C 0x40)

A high-resolution delta-sigma converter measuring battery bus voltage (IBV) and shunt current (Bcur). It is the single source of truth for all control and safety voltage decisions, and it also drives the hardware overvoltage backup: its alert pin physically pulls the field enable line down with no firmware involvement (see Safeties and Protections).

Two sampling modes

The chip is reconfigured on the field-state edge (inaFastModeActive):

  • Field on — fast mode: short conversions, light averaging, polled every few milliseconds. Feeds the voltage loop, the rate-of-rise prediction, and load-dump detection.
  • Field off — slow mode: long conversions, heavy averaging, polled about once a second. Quiet idle monitoring, and a heavily averaged input to the hardware alert threshold.

Reconfiguration costs two register writes on the transition edge only. On the switch to fast mode, the smoothed-voltage filter is reseeded so the voltage loop starts from reality, not a stale average.

Per-read pipeline

Each read fetches bus voltage and shunt voltage, validates both (range and not-a-number checks), then derives current from the user's configured shunt resistance (ShuntResistanceMicroOhm), applies the calibration offset (BatteryCOffset), optional inversion (InvertBattAmps), and the auto-learned gain factor (DynamicShuntGainFactor). Only validated reads stamp freshness. Derived per read:

  • IBV_filtered — a time-constant-aware exponential smooth used for display and the voltage-loop error term (getFiltV()).
  • g_dBcur_dt — battery-current slew rate in amps per second, the input to load-dump detection.
  • Watermarks — session and lifetime voltage extremes, plus per-ignition-cycle high/low tracking.

The hardware overvoltage threshold (VoltageHardwareLimit) is reprogrammed into the chip whenever the user changes BulkVoltage — search 5_functions.ino for updateINA228OvervoltageThreshold.

Accessor convention

getBatteryVoltage()  → IBV            (control + safety source of truth)
getBatteryCurrent()  → Bcur, or the Victron value if the user opts in (display/accounting only)
getFiltV()           → smoothed IBV   (display, never safety)

BatteryV from the ADS1115 is never a control or safety signal — it exists only so two independent sensors can cross-check each other.


DS18B20 — Alternator Temperature (OneWire)

Runs entirely in its own task (TempTask) on the second CPU core, because a full conversion takes most of a second — running it in the main loop would disrupt control timing. The task feeds a heartbeat that the main core watches (the T4 protection on the safeties page).

The per-iteration sequence: heartbeat, a few cooperative gates (over-the-air-update in progress, hardware-absent flag, core-busy flag), re-enumeration if the sensor was lost, an adaptive poll cadence (slower after success, faster retry after failure), a non-blocking conversion wait, then the scratchpad read.

Five-check validation

Every raw reading must pass all five before it is accepted:

Check What it catches Failure counter
CRC of the scratchpad (one immediate retry) Bus noise, marginal pull-up tempCrcFailCount (tempCrcRecoveredCount on retry success)
All-bytes-0xFF Sensor physically disconnected tempAllFFCount
Power-on reset signature Sensor rebooted and is returning its placeholder value tempPowerOn85Count
Resolution-config drift Sensor lost its configured resolution; firmware rewrites it and re-converts tempResolutionFixCount
Sanity range Impossible temperatures tempOutOfRangeCount

A passing read updates AlternatorTemperatureF, the session and lifetime maxima, and the freshness stamp. Persistent failures queue a throttled console warning.

Every counter above is exposed on the dashboard Stats panel — a marginal crimp or noisy bus shows its specific failure mode rather than a generic "temperature stale." A rising recovered-CRC count with a steady temperature is an early noise warning before anything goes stale.


BMP390 — Board Temperature and Barometric Pressure (I²C)

The barometer (a BMP390 part — driven through a BMP388-compatible library, so code symbols read bmp388) runs heavy pressure oversampling with infinite-impulse-response filtering, in forced one-shot mode inside the same non-blocking state machine as the ADS1115: trigger a conversion every several seconds, poll for completion, validate plausible pressure and temperature ranges, convert, stamp fresh. The first sample after boot is discarded, and a long timeout with a throttled warning covers a wedged conversion.

Use sites: ambientTemp (displayed as board temperature — the sensor sits on the powered board, so it reads warmer than true ambient) feeds the alternator cooling model and cold-weather logic; baroPressure is logged for trip analytics and weather correlation.


LSM6DSOX — Six-Axis IMU (I²C)

Accelerometer and gyroscope in hardware-FIFO mode, drained from the main loop after the control function so an IMU bus stall can never delay control-critical reads.

Initialization (imuInit() in 4_functions.ino)

Hard-fails — sets imuEnabled = false rather than limping — on any of: no I²C acknowledge at the address, a wrong chip-identity register (WHO_AM_I), or any failure configuring sensor enables, output data rates, FIFO batching rates, or continuous-FIFO mode. The accelerometer runs at roughly a hundred samples per second (margin over the tens-of-milliseconds slam pulses it must catch); the gyro at about half that, sufficient for hull roll and pitch dynamics. After configuration the init reads a few FIFO entries and prints their decoded sensor tags as an empirical self-check.

Draining (drainIMUFifo())

Polled on a short interval, pulling a bounded handful of FIFO entries per poll (MAX_FIFO_DRAIN_PER_POLL) so the cost per loop pass is capped. Each entry carries a tag identifying accelerometer, gyro, or temperature data; samples land in a PSRAM ring with separate accel and gyro pointers. An error tracker (imuRecordI2CError()) watches a sliding window and auto-disables the IMU if errors accumulate, with a console message.

Windowed motion metrics (updateAccelMetrics())

Aggregated over a configurable window when motion sensing is enabled (on enable, stale FIFO backlog is flushed first):

  • Attitude — heel and pitch via a complementary filter of accel and gyro; yaw rate direct from the gyro.
  • Slam detection — vertical and total acceleration peaks.
  • Comfort estimates — a motion-sickness index based on frequency-weighted vertical acceleration (Lawther and Griffin's published model), an estimated seasickness percentage, and a heuristic anchorage comfort score.
  • Deviation stats — peak heel, pitch, and heading swing versus a rolling mean.
  • Lifetime event counters — capsize, pitchpole, and slam counts against fixed thresholds (CAPSIZE_THRESHOLD_DEG, PITCHPOLE_THRESHOLD_DEG, SLAM_THRESHOLD_G), persisted in NVS.

These feed both the live tipping display and the slower summary statistics on the dashboard.


Data Freshness (the DataIndex enum)

Every sensor source has an index into a timestamp table; MARK_FRESH stamps it and IS_STALE compares against a default timeout, with safety-critical consumers (current and temperature staleness protections) applying their own stricter windows. The roster, in Xregulator.ino near the DataIndex enum:

Group Indices Use
Electrical battery voltage (both sensors), battery current, alternator current Control and safety; stale alternator current is itself a field-cut reason
Engine RPM Ceiling table, RPM gate
Thermal alternator probe, thermistor Whichever is selected (TempSource) drives derating and the staleness protection
Environment barometric pressure, board temperature Analytics, cooling model
Motion IMU Tipping and comfort metrics
Derived duty cycle, field telemetry, state of charge, charge-time estimates, and similar Dashboard display
External NMEA position/wind/speed fields, Victron voltage and current Dashboard display, greyed out when stale

Disconnect Flags

Each device has a runtime "disconnected" flag so a hardware failure degrades gracefully instead of crashing the loop:

Flag Set by Effect
ADS1115Disconnected Consecutive bus failures (automatic) or user toggle The whole ADS block is skipped
INADisconnected User toggle INA reads skipped; values freeze, hardware alert disabled
imuEnabled = false Init failure or error-rate trip (automatic), or user toggle FIFO drain early-returns
Temperature task Self-recovers via re-enumeration retries The staleness protection cuts the field if readings stop

The manual toggles exist mainly for bench testing; the fake-data injection path (ReadAnalogInputs_Fake) covers most development scenarios more cleanly.