Skip to content

TLSR825x: 5 device_db entries use a battery pin with no ADC channel -> bogus battery readings #487

Description

@epsilonrt

Summary

device_db.yaml contains 5 Telink entries whose BT<pin> token points at a GPIO that has
no ADC channel on the TLSR825x. The reported battery percentage is then meaningless, and the
failure is completely silent — no error, no warning.

The symptom is not deterministic. An invalid pin leaves gpio_no = 0, which selects no
channel at all; the resulting reading is undefined and saturates either low or high depending
on the pin. I measured both cases on the same PCB family, using the same partially discharged
cell:

Unit BT pin Before fix After BTB5 Cell (multimeter) Δ
_TZ3000_itb0omhv (TS0041) #1 C2 stuck 0% 83% → 2830 mV 2.860 V −30 mV
_TZ3000_tzvbimpq (TS0042) #1 D2 stuck 100% 89% → 2890 mV 2.860 V +30 mV
_TZ3000_tzvbimpq (TS0042) #2 D2 0% * 93% → 2930 mV 2.956 V −26 mV
_TZ3000_itb0omhv (TS0041) #2 C2 stuck 0% 100% (fresh cell, clamped) >3.0 V

After the fix, the three units measured against a partially discharged cell track its real
voltage within ±30 mV (chip-to-chip ADC spread), i.e. about 3 percentage points. The fourth was
tested with a fresh cell: 100% is ambiguous with the cluster default on its own, but the
transition 0% → 100% is not — a broken reading stays at 0%.

* Note the two TS0042 units are the same model, same manufacturer name, same BTD2 config
string
, yet one saturated at 100% and the other at 0%. That is strong evidence the reading is
genuinely undefined rather than merely miscalibrated. (Caveat: on unit #2 the 0% was observed
over ~97 s and did not survive to a periodic refresh before I applied the fix, so treat it as
strongly suggestive rather than proven.)

Worth noting the stuck-100% case is the more dangerous: it never raises a low-battery alert, so
the device simply dies one day with no warning. A stuck 0% at least gets noticed.

I traced this on the TS0041, confirmed the root cause in the Telink SDK, fixed it by changing
one token, and then reproduced the whole thing on the TS0042 — different entry, different pin,
different symptom, same fix.

Root cause

battery_init() always uses VBAT mode:

hal_adc_init(HAL_ADC_INPUT_VBAT, battery->pin);

On the 8258 the pin is genuinely used — unlike newer cores which discard it
(proj/drivers/drv_adc.c):

#elif defined(MCU_CORE_8258) || defined(MCU_CORE_8278)
    } else if (mode == DRV_ADC_VBAT_MODE) {
        adc_vbat_init(pin);          // B91 / TL321X / TL323X do `(void)pin;`
    }

And adc_vbat_init()adc_vbat_pin_init() (platform/chip_8258/adc.c, SDK V3.6.8.6):

void adc_vbat_pin_init(GPIO_PinTypeDef pin)
{
    gpio_set_func(pin, AS_GPIO);
    gpio_set_input_en(pin, 0);
    gpio_set_output_en(pin, 1);     // pin driven as OUTPUT
    gpio_write(pin, 1);             // forced HIGH -> sits at VDD
    for (i = 0; i < 10; i++)
        if (pin == ADC_GPIO_tab[i]) { gpio_no = i+1; break; }
    adc_set_input_mode(ADC_MISC_CHN, DIFFERENTIAL_MODE);
    adc_set_ain_channel_differential_mode(ADC_MISC_CHN, gpio_no, GND);
}

So VBAT mode drives the chosen pin high and measures that pad differentially against GND.
The pin must therefore (a) be free of any external load, and (b) exist in the ADC
table
:

const GPIO_PinTypeDef ADC_GPIO_tab[10] = {
    GPIO_PB0, GPIO_PB1, GPIO_PB2, GPIO_PB3,
    GPIO_PB4, GPIO_PB5, GPIO_PB6, GPIO_PB7,
    GPIO_PC4, GPIO_PC5
};

C2, D2 and D4 are not in that table. The loop finds no match, gpio_no stays 0
— not a valid channel, the valid range being 1..10 — and the ADC ends up sampling nothing
meaningful. Note the lookup has no failure path: gpio_no = 0 is passed on silently.

What the reading then becomes is undefined, and both clamps in battery_get_status() can be
hit depending on the pin:

if (voltage_mv < battery->voltage_min)        status.charge = 0;              // observed on C2
else if (voltage_mv > battery->voltage_max)   status.charge = charge_range;   // observed on D2

Hence the two opposite symptoms in the table above. Note the pin does not determine which
clamp is hit: both BTD2 units are the same model with the same config string, and one
saturated high while the other saturated low.

Affected entries

Entry BT pin In ADC_GPIO_tab? Status in db
REMOTE_TUYA_SWITCH_TS0041 C2 mostly_supported
REMOTE_TUYA_SWITCH_TS0042 D2 mostly_supported
REMOTE_TUYA_SWITCH_TS0043 D4 mostly_supported
REMOTE_TUYA_SWITCH_TS0043_1 D4 fully_supported
REMOTE_IHSENO_TS0044 D2 fully_supported

The other 9 Telink entries all use B5 or C5 and are fine.

The four REMOTE_TUYA_SWITCH_* entries all come from the pinout contributed in #293, which
documented switch and LED pins but no battery pin — a switch or LED pin appears to have
been reused to fill the field. Note two of the affected entries are currently marked
fully_supported.

Fix (verified)

For REMOTE_TUYA_SWITCH_TS0041:

- config_str: itb0omhv;TS0041-MOES;SC2u;IC4;BTC2;M;
+ config_str: itb0omhv;TS0041-MOES;SC2u;IC4;BTB5;M;

I inspected the PCB: TX, RX, B5, B4 and A0 have no pads at all on this board,
so B5 is both ADC-capable and completely unloaded — ideal for VBAT mode.

Verification — a healthy 3.0 V cell is useless here, since it produces 3000 mV / 100%,
identical to the cluster defaults. I used a partially discharged cell instead:

Cell, open circuit (multimeter) 2.86 V
battery reported after fix 83 %

Back-calculation: 83% → charge_ZCL = 166V = 2000 + 166 × 1000/200 = 2830 mV, i.e.
2.83 V under load for 2.86 V open circuit. Neither the defaults (100%) nor the failure mode
(0%) can produce 83%.

Before the fix this device reported 0% at every 5-minute refresh.

Verified on two of the five entries (TS0041 itb0omhv, TS0042 tzvbimpq), across four
physical units, two of each. The remaining three are untested; REMOTE_TUYA_SWITCH_TS0043* is
the same PCB, so BTB5 is very likely correct there too, but that should be confirmed by
someone with the hardware.

The TS0041 populates only S2 and the TS0042 only S1/S3 — same board, different front-faces, as
documented in #293.

Unrelated side note: itb0omhv is not always a 1-gang

REMOTE_TUYA_SWITCH_TS0041 describes _TZ3000_itb0omhv as a 1-gang, and #293 notes it "uses
only S2 / S2 LED". One of my two units has three push-buttons actually populated and wired
(verified on the board: S1=D2, S2=C2, S3=C3, LEDs D4/C4/D7) — the stock Tuya firmware simply
never reads S1 and S3, so two physical buttons are dead out of the box.

Writing a 3-gang config string turns it into a fully working 3-gang:

pkeqinnt;TS0043-MOES;SD2u;ID4;SC2u;IC4;SC3u;ID7;BTB5;M;

Three endpoints, three genOnOff/genLevelCtrl client pairs, three multistate inputs. Nothing
to fix in the firmware — it is a nice illustration that the gang count is a config line, not a
product property. It might be worth a note in supported_devices.md that this variant ships in
several front-face versions on a shared PCB.

Suggested guard

A schema check would close this whole class of bug: for any mcu matching TLSR825x, the
BT pin must be in {B0..B7, C4, C5}. Roughly ten lines against device_db.yaml.

Two minor notes

  1. Silent failure. When the BT pin has no ADC channel, nothing is logged anywhere —
    gpio_no = 0 propagates through the whole stack. A build-time or boot-time warning would
    have saved a long investigation.
  2. Z2M UI, device_config field. It applies on change, with no Save button — on an
    attribute that writes to NV and reboots the device. A confirmation step would be safer.

Bonus: OTA conversion of _TZ3000_itb0omhv works (no disassembly needed)

I could not find any report of an OTA conversion for this variant: #293 contains only
wired-flash reports (two of them experienced as bricking), and #424 is a documentation
question from someone following the wired procedure. I may well have missed something —
Discord in particular — so I am not claiming a first, just adding a data point with
reproducible conditions:

  • image tlc_switch-1.1.2-8542fc05-from_tuya.zigbee (imageType 54179 = stock image type)
  • Z2M 2.13.0-1, index index_end_device.json, both converters installed
  • LQI 220 (device moved next to the coordinator), fresh CR2032
  • image_block_response_delay 250 → 50 ms
  • transfer completed in 383 s

Two things that were not obvious:

  • The device is a sleeping end device: it must be woken immediately before clicking
    Check for updates, and kept awake until the transfer starts. A first attempt failed
    because the presses came 25 s after the 60 s window had closed. Once the transfer is
    running, the device polls autonomously and no further presses are needed.
  • After flashing, Z2M refreshed the identifiers (modelId, manufName, appVersion)
    during configure, but not the endpoint descriptorsoutClusterList stayed as the
    stock [genOta, genTime], so no client clusters and no binding possible. Neither
    device_announce, nor the automatic configure, nor a manual Reconfigure fixed it.
    Only a factory reset followed by a clean re-join forced the coordinator to re-read the
    descriptors. It might be worth making that an explicit step in updating.md.

After the clean re-join, endpoint 1 exposes genOta, genOnOff and genLevelCtrl as output
clusters, the phantom ep2-4 are gone, and direct binding works (short press = toggle, hold =
level ramp, release = stop).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions