-
-
Notifications
You must be signed in to change notification settings - Fork 98
Implement analogWrite() PWM for LN882H using Advanced Timers #369
Description
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
- libretiny_pwm with ln882x board incorrectly uses analogWrite esphome/esphome#11552 —
libretiny_pwmfails on LN882H due to missinganalogWrite - Support for LN882H esphome/feature-requests#3111 — LN882H support request
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.