Skip to content

esp32_ledc

LEDC: the ESP32’s PWM generator.

Eight high-speed channels sharing four timers. A timer sets the period; a channel picks a timer and a duty. Despite the name it is a general PWM peripheral — servos, motor drivers and buzzers all use it.

This drives the high-speed channels only. The low-speed ones exist (LEDC_LSCH0_CONF0_REG at offset 0xA0) and differ in that they can run from the slow clock while the CPU sleeps. Nothing here sleeps yet, and a second set of channels with subtly different registers is a second set of things to get wrong.

LEDC_HSCHn_DUTY_REG does not hold the duty. It holds duty << 4: bits [24:4] are the integer part and the low four bits are a fraction the hardware uses to dither between two lpoint values. Writing a duty directly gives one sixteenth of the intended output — a signal that looks plausible on a scope and is wrong everywhere.

Quoted from ledc_reg.h:

reg_lpoint_hsch0 = (reg_hpoint_hsch0[19:0] + reg_duty_hsch0[24:4]) … The least four bits in this register represent the decimal part

[Channel::set_duty] takes a plain duty and does the shift.

From soc/ledc_reg.h, soc/soc.h, soc/dport_reg.h and soc/gpio_sig_map.h, read rather than recalled:

DR_REG_LEDC_BASE 0x3ff59000
HS channel stride 0x14 — five registers each
HS timer stride 0x08
LEDC_TICK_SEL_HSTIMERn bit 25, 1 = APB, 0 = REF_TICK
LEDC_HS_SIG_OUT0_IDX 71
DPORT_LEDC_CLK_EN BIT(11)
struct Channel { /* private fields */ }

One of the eight high-speed channels.

struct Timer { /* private fields */ }

One of the four high-speed timers.

const fn divider_for(freq_hz: u32, res_bits: u8) -> Option<u32>

Q10.8 timer divider for freq_hz at res_bits resolution, or None if the combination is out of range.

freq = APB / (div/256 * 2^res), so div = APB * 256 / (freq * 2^res).

Returning None rather than clamping is deliberate: a clamped divider silently produces a different frequency, and a servo driven at the wrong frequency moves to the wrong place rather than failing.

const fn duty_for_percent(pct: u8, res_bits: u8) -> u32

Duty steps for a percentage, rounded to nearest and clamped at 100.

const fn freq_for(div: u32, res_bits: u8) -> u32

The frequency a divider actually produces, for reporting what was asked against what was got.

const fn max_duty(res_bits: u8) -> u32

Duty steps at res_bits resolution: full scale, i.e. always-on.

const APB_HZ: u32;

APB clock, the only source this driver uses.

const CHANNELS: u8;

High-speed channels.

const MAX_RES_BITS: u8;

Widest duty resolution the timer’s DUTY_RES field allows for a period that still fits HPOINT.

const TIMERS: u8;

High-speed timers.