Skip to content

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

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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions