Skip to content

Implement analogWrite() PWM for LN882H using Advanced Timers #369

@Nouxii

Description

@Nouxii

Summary

analogWrite() is not implemented for the LN882H platform, which means ESPHome's libretiny_pwm component fails to compile with:

undefined reference to 'analogWrite'

This affects anyone trying to do LED dimming or any PWM output on LN882H devices in ESPHome.

Working solution

I've built a working ESPHome external component that bypasses libretiny_pwm and calls the LN882H SDK's Advanced Timer API directly:

https://github.com/Nouxii/esphome-ln882h-pwm

It's tested on an LN882HKI (QFN32, 2MB) running ESPHome 2026.3.1 with LibreTiny v1.12.1. Hardware PWM dimming works reliably with up to 6 channels.

Proposed fix

The proper fix would be adding analogWrite() to cores/lightning-ln882h/arduino/src/wiring_analog.c. The existing file already has analogRead — PWM would be added alongside it.

The implementation uses the 6 Advanced Timer channels (ADV_TIMER_0_BASE through ADV_TIMER_5_BASE) and maps GPIOs via AFIO, based on how OpenBeken implements PWM for LN882H ([source](https://github.com/openshwprojects/OpenBK7231T_App/blob/main/src/hal/ln882h/hal_pins_ln882h.c)).

Here's the proposed code to append to wiring_analog.c:

/* ==================== PWM / analogWrite ==================== */

#include "hal/hal_adv_timer.h"
#include "hal/hal_clock.h"

#define PWM_MAX_CHANNELS 6
#define PWM_DEFAULT_FREQ 1000
#define PWM_RESOLUTION   255

typedef struct {
    uint32_t gpio;
    uint32_t frequency;
    uint32_t timer_base;
} pwm_channel_t;

static pwm_channel_t pwm_channels[PWM_MAX_CHANNELS];
static bool pwm_initialized = false;

static uint32_t get_adv_timer_base(uint8_t ch) {
    switch (ch) {
        case 0: return ADV_TIMER_0_BASE;
        case 1: return ADV_TIMER_1_BASE;
        case 2: return ADV_TIMER_2_BASE;
        case 3: return ADV_TIMER_3_BASE;
        case 4: return ADV_TIMER_4_BASE;
        case 5: return ADV_TIMER_5_BASE;
        default: return ADV_TIMER_0_BASE;
    }
}

static void pwm_ensure_init(void) {
    if (pwm_initialized) return;
    for (uint8_t i = 0; i < PWM_MAX_CHANNELS; i++) {
        pwm_channels[i].gpio = (uint32_t)-1;
        pwm_channels[i].frequency = PWM_DEFAULT_FREQ;
        pwm_channels[i].timer_base = get_adv_timer_base(i);
    }
    pwm_initialized = true;
}

static int8_t pwm_get_channel(uint32_t gpio) {
    pwm_ensure_init();
    for (uint8_t i = 0; i < PWM_MAX_CHANNELS; i++) {
        if (pwm_channels[i].gpio == gpio) return I;
    }
    for (uint8_t i = 0; i < PWM_MAX_CHANNELS; i++) {
        if (pwm_channels[i].gpio == (uint32_t)-1) {
            pwm_channels[i].gpio = gpio;
            return I;
        }
    }
    return -1;
}

static void pwm_configure_timer(uint8_t ch, uint32_t frequency) {
    uint32_t reg_base = pwm_channels[ch].timer_base;
    adv_tim_init_t_def adv_tim_init;
    memset(&adv_tim_init, 0, sizeof(adv_tim_init));

    if (frequency >= 10000) {
        adv_tim_init.adv_tim_clk_div = 0;
        adv_tim_init.adv_tim_load_value =
            (uint32_t)(hal_clock_get_apb0_clk() * 1.0 / frequency - 2);
    } else {
        adv_tim_init.adv_tim_clk_div =
            (uint32_t)(hal_clock_get_apb0_clk() / 1000000) - 1;
        adv_tim_init.adv_tim_load_value = 1000000 / frequency - 2;
    }

    adv_tim_init.adv_tim_cmp_a_value = 0;
    adv_tim_init.adv_tim_dead_gap_value = 0;
    adv_tim_init.adv_tim_dead_en = ADV_TIMER_DEAD_DIS;
    adv_tim_init.adv_tim_cnt_mode = ADV_TIMER_CNT_MODE_INC;
    adv_tim_init.adv_tim_cha_inv_en = ADV_TIMER_CHA_INV_EN;

    hal_adv_tim_init(reg_base, &adv_tim_init);
    pwm_channels[ch].frequency = frequency;
}

void analogWrite(pin_size_t pinNumber, int value) {
    pinCheckGetInfo(pinNumber, PIN_GPIO, );

    if (value <= 0) {
        hal_gpio_pin_reset(GPIO_GET_BASE(pin->gpio), GPIO_GET_PIN(pin->gpio));
        return;
    }
    if (value >= PWM_RESOLUTION) {
        hal_gpio_pin_set(GPIO_GET_BASE(pin->gpio), GPIO_GET_PIN(pin->gpio));
        return;
    }

    int8_t ch = pwm_get_channel(pin->gpio);
    if (ch < 0) return;

    pwm_configure_timer(ch, pwm_channels[ch].frequency);

    hal_gpio_pin_afio_select(
        GPIO_GET_BASE(pin->gpio),
        GPIO_GET_PIN(pin->gpio),
        (afio_function_t)(ADV_TIMER_PWM0 + ch * 2)
    );
    hal_gpio_pin_afio_en(GPIO_GET_BASE(pin->gpio), GPIO_GET_PIN(pin->gpio), HAL_ENABLE);

    uint32_t load = hal_adv_tim_get_load_value(pwm_channels[ch].timer_base) + 2;
    uint32_t compare = (uint32_t)(load * value / PWM_RESOLUTION);
    hal_adv_tim_set_comp_a(pwm_channels[ch].timer_base, compare);

    hal_adv_tim_a_en(pwm_channels[ch].timer_base, HAL_ENABLE);
}

void analogWriteResolution(int bits) {
    // TODO
}

void analogWriteFrequency(pin_size_t pinNumber, uint32_t frequency) {
    pinCheckGetInfo(pinNumber, PIN_GPIO, );
    int8_t ch = pwm_get_channel(pin->gpio);
    if (ch < 0) return;
    pwm_channels[ch].frequency = frequency;
    pwm_configure_timer(ch, frequency);
}

Related issues

Hardware tested

  • Chip: LN882HKI (QFN32, 2MB flash)
  • Board: generic-ln882hki
  • Device: YSR-MINI-01 WiFi LED strip controller
  • ESPHome: 2026.3.1
  • LibreTiny: v1.12.1

Happy to submit a PR if this approach looks good to the maintainers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions