Compare commits

..
4 changed files with 595 additions and 2 deletions

No files matched your search

+436
View File
@@ -0,0 +1,436 @@
# LSM6DS External Component Bug Investigation
## Purpose
This document records the investigation and correction of an LSM6DS
external-component bug found while diagnosing recurring ESP32-C6
interrupt-watchdog resets in the RV level sensor.
It separates confirmed driver bugs, supporting diagnostic evidence,
fixes made in the `rv-lsm6ds` branch, validation results, and the
still-unproven relationship between these bugs and the
interrupt-watchdog resets.
A concise upstream bug report is included at the end.
------------------------------------------------------------------------
## 1. Test environment
### Hardware
- Seeed Studio XIAO ESP32-C6
- Modulino Movement / LSM6DSOXTR IMU
- I2C address: `0x6A`
- I2C SDA: GPIO22
- I2C SCL: GPIO23
### Software
- ESPHome 2026.8.2
- ESP-IDF framework
- External component fork:
`https://git.mr2web.net/mr2web/esphome-lsm6ds.git`
- Working branch: `rv-lsm6ds`
Relevant configuration:
``` yaml
lsm6ds_i2c:
id: imu
i2c_id: i2c_bus
address: 0x6A
accel_range: 2G
gyro_range: 250DPS
accel_odr: 104HZ
gyro_odr: "OFF"
update_interval: 10ms
```
The accelerometer intentionally remains at 104 Hz. High-rate sampling is
required by the RV leveling application because lower sampling
previously produced vibration/aliasing-related drift.
------------------------------------------------------------------------
## 2. Original investigation context
The RV level sensor experienced recurring hangs followed by ESP32-C6
interrupt-watchdog resets:
``` text
Reason: Interrupt wdt
Crashed core: 0
```
During a live failure, ESPHome API/entity updates stopped while the
device continued responding to ping. It later rebooted and reported the
watchdog reset.
The leveling calculation uses accelerometer data only. Gyroscope and
temperature data are not required for leveling.
Inspection of the external component showed that its polling `update()`
function reads `STATUS_REG` and conditionally reads/publishes gyroscope,
accelerometer and temperature data. At a 10 ms polling interval this is
a high-frequency I2C path.
------------------------------------------------------------------------
## 3. Adding support for ODR OFF
The C++ component already defined:
``` cpp
LSM6DS_ODR_OFF = 0x00
```
but the ESPHome configuration schema did not expose `OFF`.
This was corrected so YAML could use:
``` yaml
gyro_odr: "OFF"
```
`OFF` is quoted because an unquoted YAML value may be interpreted as
boolean false.
Commit:
``` text
41cb715d6 lsm6ds: expose OFF output data rate
```
------------------------------------------------------------------------
## 4. Disabled-sensor read guards
After configuring `gyro_odr: "OFF"`, the driver continued publishing
repeated frozen gyroscope values.
A guard was added so a disabled sensor would not be read even if the
status test appeared true:
``` cpp
if (this->gyro_odr_ != LSM6DS_ODR_OFF && ...) {
// gyro read
}
if (this->accel_odr_ != LSM6DS_ODR_OFF && ...) {
// accelerometer read
}
```
Commit:
``` text
3b86faa44 lsm6ds: skip reads for disabled sensors
```
After this change:
- gyroscope publications stopped;
- temperature continued to update;
- accelerometer publications unexpectedly disappeared;
- the application reported `IMU DATA INVALID`.
Because temperature was still read successfully, the IMU and I2C
communication were still functioning. This pointed to the interpretation
of `STATUS_REG`.
------------------------------------------------------------------------
## 5. STATUS_REG diagnostic
Temporary instrumentation was added after reading register `0x1E`:
``` cpp
uint8_t status;
if (!this->read_register(0x1E, &status, 1))
return;
ESP_LOGW(TAG, "STATUS=0x%02X accel_odr=0x%02X gyro_odr=0x%02X",
status,
static_cast<uint8_t>(this->accel_odr_),
static_cast<uint8_t>(this->gyro_odr_));
```
Diagnostic commits:
``` text
e2d4b947e lsm6ds: add temporary status diagnostics
53218bd4d lsm6ds: raise temporary status diagnostic to warning
```
With:
``` text
accel_odr = 0x04 (104 Hz)
gyro_odr = 0x00 (OFF)
```
the device repeatedly reported:
``` text
STATUS=0x01 accel_odr=0x04 gyro_odr=0x00
STATUS=0x05 accel_odr=0x04 gyro_odr=0x00
```
`0x02` was not observed.
`0x05` is `0x01 | 0x04` and appeared when temperature data was also
available. This demonstrated that `0x01` was the accelerometer-ready
flag, not the gyroscope-ready flag.
------------------------------------------------------------------------
## 6. Confirmed STATUS_REG bug
Correct mapping:
Bit Mask Meaning
----- -------- ---------------------------------------
0 `0x01` XLDA --- accelerometer data available
1 `0x02` GDA --- gyroscope data available
2 `0x04` TDA --- temperature data available
The external component had XLDA and GDA reversed.
Previous logic was effectively:
``` cpp
if (status & 0x01) { // GDA - incorrect
// read gyroscope
}
if (status & 0x02) { // XLDA - incorrect
// read accelerometer
}
```
Corrected implementation:
``` cpp
if (this->gyro_odr_ != LSM6DS_ODR_OFF && (status & 0x02)) { // GDA
// read gyroscope
}
if (this->accel_odr_ != LSM6DS_ODR_OFF && (status & 0x01)) { // XLDA
// read accelerometer
}
```
Temperature remains:
``` cpp
if (status & 0x04) { // TDA
// read temperature
}
```
The temporary diagnostic logging was removed in the final correction.
Commit:
``` text
7adb22de2 lsm6ds: fix swapped accel and gyro status bits
```
------------------------------------------------------------------------
## 7. Validation
The firmware containing commit `7adb22de2` was deployed with:
``` text
Accelerometer ODR: 104 Hz
Gyroscope ODR: OFF
Polling interval: 10 ms
```
After correction:
- accelerometer X/Y/Z publishes normally;
- gyroscope values no longer publish;
- temperature continues to update;
- raw and calibrated pitch/roll publish normally;
- orientation calibration reports READY;
- RV Level diagnostic reports `OK`;
- activation from INACTIVE to ACTIVE works normally.
This confirms the XLDA/GDA mask error independently of the watchdog
investigation.
------------------------------------------------------------------------
## 8. Relevant commit history
``` text
7adb22de2 lsm6ds: fix swapped accel and gyro status bits
53218bd4d lsm6ds: raise temporary status diagnostic to warning
e2d4b947e lsm6ds: add temporary status diagnostics
3b86faa44 lsm6ds: skip reads for disabled sensors
41cb715d6 lsm6ds: expose OFF output data rate
```
Branch: `rv-lsm6ds`
Repository: `https://git.mr2web.net/mr2web/esphome-lsm6ds.git`
------------------------------------------------------------------------
## 9. Interrupt-watchdog relationship
The STATUS_REG bug was discovered while investigating recurring ESP32-C6
resets reporting:
``` text
Reason: Interrupt wdt
Crashed core: 0
```
Earlier firmware polled the LSM6DS every 10 ms with both accelerometer
and gyroscope configured at high rate. The leveling application requires
accelerometer data but not gyroscope data.
The corrected firmware removes unnecessary gyroscope activity and fixes
incorrect status-bit handling.
### Confirmed
1. XLDA and GDA status masks were reversed.
2. `0x01` is XLDA and `0x02` is GDA.
3. The configuration schema did not expose the already-supported C++
`LSM6DS_ODR_OFF` value.
4. A sensor configured with ODR OFF should not be read by the polling
path.
5. With corrected masks, accelerometer at 104 Hz and gyroscope OFF
operate as expected.
### Not yet proven
It has **not** been established that the reversed status bits,
unnecessary gyroscope path, or their combined workload caused the
ESP32-C6 interrupt-watchdog resets.
The corrected firmware is being left unchanged for an endurance test.
------------------------------------------------------------------------
## 10. Exact endurance-test firmware artifacts
Exact ELF preserved on the RockPi:
``` text
/srv/docker/esphome/firmware-archive/rv-level/2026-09-20_2148/firmware.elf
```
SHA-256:
``` text
dca98d924fb09c63646d59a1ed7a10e48790692ae67c51bf260bd0275b2d5171
```
Corresponding OTA image:
``` text
/srv/docker/esphome/firmware-archive/rv-level/2026-09-20_2148/firmware.ota.bin
```
SHA-256:
``` text
1e7dc78e4b40fe7b1f7f31294c70968b5cf6736840e4fd37926a64afd1477d67
```
These came from the ESPHome remote build generated at approximately
21:48 on 2026-09-20.
If another watchdog reset occurs, its `PC` and `BT` addresses should be
decoded against this exact `firmware.elf`, not a rebuilt ELF.
------------------------------------------------------------------------
# Upstream Bug Report
## LSM6DS STATUS_REG XLDA/GDA masks are reversed
The LSM6DS base component has the XLDA and GDA bits of `STATUS_REG`
(`0x1E`) reversed.
Affected logic treated:
``` cpp
status & 0x01 // GDA
status & 0x02 // XLDA
```
Correct mapping:
``` text
bit 0 / 0x01 = XLDA — accelerometer data available
bit 1 / 0x02 = GDA — gyroscope data available
bit 2 / 0x04 = TDA — temperature data available
```
## Reproduction
Test environment:
- LSM6DSOXTR
- ESP32-C6
- ESPHome 2026.8.2
- I2C
- accelerometer ODR: 104 Hz
- gyroscope ODR: OFF
- polling interval: 10 ms
Temporary logging with:
``` text
accel_odr=0x04
gyro_odr=0x00
```
repeatedly produced:
``` text
STATUS=0x01 accel_odr=0x04 gyro_odr=0x00
STATUS=0x05 accel_odr=0x04 gyro_odr=0x00
```
`0x02` was not observed.
This is consistent with bit 0 being XLDA and bit 2 being TDA.
## Fix
``` cpp
if (this->gyro_odr_ != LSM6DS_ODR_OFF && (status & 0x02)) { // GDA
...
}
if (this->accel_odr_ != LSM6DS_ODR_OFF && (status & 0x01)) { // XLDA
...
}
```
After correction with accelerometer at 104 Hz and gyroscope OFF:
- accelerometer values update normally;
- no gyroscope values are read/published;
- temperature continues to update normally.
Fix commit:
``` text
7adb22de2 lsm6ds: fix swapped accel and gyro status bits
```
Related work also exposes `OFF` as an ODR configuration option and
prevents reads for an output whose ODR is OFF. Those changes are
separable from the XLDA/GDA correction.
No claim is currently made that the status-bit bug caused the separately
observed ESP32-C6 interrupt-watchdog resets.
+156
View File
@@ -0,0 +1,156 @@
# Bug: LSM6DS STATUS_REG XLDA/GDA masks are reversed
## Summary
The LSM6DS base component has the accelerometer and gyroscope data-ready
bits of `STATUS_REG` (`0x1E`) reversed.
The affected implementation treats:
``` cpp
status & 0x01 // GDA - gyroscope data available
status & 0x02 // XLDA - accelerometer data available
```
The correct mapping is:
Bit Mask Meaning
----- -------- ---------------------------------------
0 `0x01` XLDA --- accelerometer data available
1 `0x02` GDA --- gyroscope data available
2 `0x04` TDA --- temperature data available
## Test environment
- LSM6DSOXTR
- Seeed Studio XIAO ESP32-C6
- ESPHome 2026.8.2
- ESP-IDF
- I2C
- Accelerometer ODR: 104 Hz
- Gyroscope ODR: OFF
- Polling interval: 10 ms
Relevant configuration:
``` yaml
lsm6ds_i2c:
id: imu
i2c_id: i2c_bus
address: 0x6A
accel_range: 2G
gyro_range: 250DPS
accel_odr: 104HZ
gyro_odr: "OFF"
update_interval: 10ms
```
## Reproduction and diagnostic evidence
To determine which data-ready bits were actually being asserted,
temporary logging was added immediately after reading `STATUS_REG`:
``` cpp
uint8_t status;
if (!this->read_register(0x1E, &status, 1))
return;
ESP_LOGW(TAG, "STATUS=0x%02X accel_odr=0x%02X gyro_odr=0x%02X",
status,
static_cast<uint8_t>(this->accel_odr_),
static_cast<uint8_t>(this->gyro_odr_));
```
With:
``` text
accel_odr=0x04
gyro_odr=0x00
```
the device repeatedly reported:
``` text
STATUS=0x01 accel_odr=0x04 gyro_odr=0x00
STATUS=0x05 accel_odr=0x04 gyro_odr=0x00
```
`0x02` was not observed.
`0x05` is `0x01 | 0x04`, corresponding to accelerometer data plus
temperature data being available.
This behavior is consistent with:
``` text
0x01 = XLDA
0x02 = GDA
0x04 = TDA
```
rather than the mapping used by the component.
## Fix
The data-ready tests should be:
``` cpp
if (this->gyro_odr_ != LSM6DS_ODR_OFF && (status & 0x02)) { // GDA
// read gyroscope
}
if (this->accel_odr_ != LSM6DS_ODR_OFF && (status & 0x01)) { // XLDA
// read accelerometer
}
```
Temperature handling remains unchanged:
``` cpp
if (status & 0x04) { // TDA
// read temperature
}
```
## Validation
After correcting the masks, with the accelerometer configured at 104 Hz
and the gyroscope disabled:
- accelerometer X/Y/Z values update normally;
- no gyroscope values are read/published;
- temperature continues to update normally;
- downstream pitch/roll calculations operate normally.
The correction is present in:
``` text
7adb22de2 lsm6ds: fix swapped accel and gyro status bits
```
## Related changes
During the investigation two related improvements were also made:
``` text
41cb715d6 lsm6ds: expose OFF output data rate
3b86faa44 lsm6ds: skip reads for disabled sensors
```
Diagnostic commits used to establish the register behavior were:
``` text
e2d4b947e lsm6ds: add temporary status diagnostics
53218bd4d lsm6ds: raise temporary status diagnostic to warning
```
These changes are separable from the XLDA/GDA mask correction itself.
## Scope
This bug was discovered while investigating a separate ESP32-C6
interrupt-watchdog issue.
No claim is currently made that the reversed XLDA/GDA masks caused those
watchdog resets. The status-register mapping error is independently
reproducible and confirmed by the data-ready behavior described above.
@@ -50,6 +50,7 @@ GYRO_RANGE_OPTIONS = {
LSM6DSODR = lsm6ds_ns.enum("LSM6DSODR") LSM6DSODR = lsm6ds_ns.enum("LSM6DSODR")
ODR_OPTIONS = { ODR_OPTIONS = {
"OFF": LSM6DSODR.LSM6DS_ODR_OFF,
"12.5HZ": LSM6DSODR.LSM6DS_ODR_12_5HZ, "12.5HZ": LSM6DSODR.LSM6DS_ODR_12_5HZ,
"26HZ": LSM6DSODR.LSM6DS_ODR_26HZ, "26HZ": LSM6DSODR.LSM6DS_ODR_26HZ,
"52HZ": LSM6DSODR.LSM6DS_ODR_52HZ, "52HZ": LSM6DSODR.LSM6DS_ODR_52HZ,
@@ -101,7 +101,7 @@ void LSM6DSComponent::update() {
if (!this->read_register(0x1E, &status, 1)) if (!this->read_register(0x1E, &status, 1))
return; return;
if (status & 0x01) { // GDA - Gyroscope data available if (this->gyro_odr_ != LSM6DS_ODR_OFF && (status & 0x02)) { // GDA - Gyroscope data available
uint8_t data[6]; uint8_t data[6];
if (this->read_register(0x22, data, 6)) { if (this->read_register(0x22, data, 6)) {
int16_t gx = encode_uint16(data[1], data[0]); int16_t gx = encode_uint16(data[1], data[0]);
@@ -116,7 +116,7 @@ void LSM6DSComponent::update() {
} }
} }
if (status & 0x02) { // XLDA - Accelerometer data available if (this->accel_odr_ != LSM6DS_ODR_OFF && (status & 0x01)) { // XLDA - Accelerometer data available
uint8_t data[6]; uint8_t data[6];
if (this->read_register(0x28, data, 6)) { if (this->read_register(0x28, data, 6)) {
int16_t ax = encode_uint16(data[1], data[0]); int16_t ax = encode_uint16(data[1], data[0]);