From ee33b25ff0f11571cd58792b31b9d9be94ffdb64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Mon, 19 Jan 2026 15:04:27 +0000 Subject: [PATCH 1/9] machine/esp32: add PWM support using LEDC peripheral Add PWM support for ESP32 using the LEDC (LED Control) peripheral. Features: - 4 high-speed timers (PWM0-PWM3) with glitch-free duty updates - Up to 8 channels shared across timers - Configurable duty resolution (1-20 bits) - Flexible frequency control via period setting - Any GPIO can be used via GPIO matrix routing The implementation follows the standard TinyGo PWM interface with Configure, Channel, Set, SetPeriod, Top, and SetInverting methods. Co-authored-by: Ona --- src/examples/pwm/esp32.go | 12 + src/machine/machine_esp32_pwm.go | 415 +++++++++++++++++++++++++++++++ 2 files changed, 427 insertions(+) create mode 100644 src/examples/pwm/esp32.go create mode 100644 src/machine/machine_esp32_pwm.go diff --git a/src/examples/pwm/esp32.go b/src/examples/pwm/esp32.go new file mode 100644 index 0000000000..3b96a4de2d --- /dev/null +++ b/src/examples/pwm/esp32.go @@ -0,0 +1,12 @@ +//go:build esp32 +// +build esp32 + +package main + +import "machine" + +var ( + pwm = machine.PWM0 // Use high-speed timer 0 + pinA = machine.GPIO2 // Built-in LED on many ESP32 boards + pinB = machine.GPIO4 // Another GPIO for testing +) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go new file mode 100644 index 0000000000..bdbe08562d --- /dev/null +++ b/src/machine/machine_esp32_pwm.go @@ -0,0 +1,415 @@ +//go:build esp32 +// +build esp32 + +package machine + +import ( + "device/esp" + "runtime/volatile" + "unsafe" +) + +// PWM is one PWM peripheral, which consists of a timer and the associated +// channels. There are 4 high-speed timers available (PWM0-PWM3), each can +// drive up to 8 channels total (shared across all timers). +// +// The ESP32 LEDC peripheral is used for PWM generation. It provides flexible +// frequency and duty cycle control with configurable resolution (1-20 bits). +type PWM struct { + num uint8 // Timer number (0-3 for high-speed) +} + +// pwmChannelPins tracks which pin is assigned to each channel (0-7) +// Initialized to NoPin (0xff) to indicate unused channels +var pwmChannelPins [8]Pin + +// Hardware PWM peripherals available on ESP32. +// These use the high-speed LEDC timers for glitch-free PWM updates. +var ( + PWM0 = &PWM{num: 0} + PWM1 = &PWM{num: 1} + PWM2 = &PWM{num: 2} + PWM3 = &PWM{num: 3} +) + +// LEDC peripheral constants +const ( + // Clock enable bit in DPORT.PERIP_CLK_EN for LEDC + ledcClockEnable = 1 << 11 + + // GPIO matrix output signal numbers for LEDC high-speed channels + ledcHSSignalOut0 = 71 + ledcHSSignalOut1 = 72 + ledcHSSignalOut2 = 73 + ledcHSSignalOut3 = 74 + ledcHSSignalOut4 = 75 + ledcHSSignalOut5 = 76 + ledcHSSignalOut6 = 77 + ledcHSSignalOut7 = 78 + + // APB clock frequency (used by high-speed LEDC) + apbClockFreq = 80000000 // 80 MHz + + // Maximum values + maxDivider = 0x3FFFF // 18-bit divider + maxResolution = 20 // Maximum duty resolution in bits + minResolution = 1 // Minimum duty resolution in bits +) + +// LEDC register bit positions and masks for timer configuration +const ( + // HSTIMER_CONF register + timerDivNumPos = 5 // DIV_NUM position (bits 5-22) + timerDivNumMask = 0x3FFFF << timerDivNumPos + timerLimPos = 0 // LIM position (bits 0-4), resolution = LIM + 1 + timerLimMask = 0x1F + timerPausePos = 23 // PAUSE bit + timerPauseMask = 1 << timerPausePos + timerRstPos = 24 // RST bit + timerRstMask = 1 << timerRstPos + timerTickSelPos = 25 // TICK_SEL bit (0=REF_TICK, 1=APB_CLK) + timerTickSelMask = 1 << timerTickSelPos +) + +// LEDC register bit positions and masks for channel configuration +const ( + // HSCH_CONF0 register + chanTimerSelPos = 0 // TIMER_SEL position (bits 0-1) + chanTimerSelMask = 0x3 + chanSigOutEnPos = 2 // SIG_OUT_EN bit + chanSigOutEnMask = 1 << chanSigOutEnPos + chanIdleLvPos = 3 // IDLE_LV bit + chanIdleLvMask = 1 << chanIdleLvPos + chanClkEnPos = 31 // CLK_EN bit + chanClkEnMask = 1 << chanClkEnPos + + // HSCH_CONF1 register + chanDutyStartPos = 31 // DUTY_START bit + chanDutyStartMask = 1 << chanDutyStartPos + + // HSCH_DUTY register - duty value uses bits 0-24 (25 bits total) + // The lower 4 bits are fractional, upper 20 bits are integer + chanDutyFracBits = 4 +) + +// pwmState holds the current configuration for each PWM timer +type pwmState struct { + resolution uint8 // Current duty resolution in bits + configured bool // Whether the timer has been configured +} + +var pwmStates [4]pwmState + +// Configure enables and configures this PWM peripheral. +// The period is specified in nanoseconds. A period of 0 will select a default +// period suitable for LED dimming (~1kHz). +func (pwm *PWM) Configure(config PWMConfig) error { + // Enable LEDC peripheral clock + esp.DPORT.PERIP_CLK_EN.SetBits(ledcClockEnable) + // Clear reset bit + esp.DPORT.PERIP_RST_EN.ClearBits(ledcClockEnable) + + // Calculate timer configuration + divider, resolution, err := pwm.calculateConfig(config.Period) + if err != nil { + return err + } + + // Store the resolution for duty cycle calculations + pwmStates[pwm.num].resolution = resolution + pwmStates[pwm.num].configured = true + + // Get timer configuration register + timerConf := pwm.timerConf() + + // Reset the timer first + timerConf.SetBits(timerRstMask) + + // Configure timer: + // - Use APB_CLK (80MHz) as clock source + // - Set divider + // - Set resolution (LIM = resolution - 1) + var conf uint32 + conf |= timerTickSelMask // APB_CLK + conf |= (divider << timerDivNumPos) & timerDivNumMask // Divider + conf |= uint32(resolution-1) & timerLimMask // Resolution + timerConf.Set(conf) + + return nil +} + +// calculateConfig determines the optimal divider and resolution for a given period. +// Returns divider, resolution, and any error. +func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { + if period == 0 { + // Default: ~1kHz with 13-bit resolution (good for LEDs) + // period = 1e9 / 1000 = 1,000,000 ns + period = 1000000 + } + + // Formula: period_ns = (2^resolution * divider) / 80MHz * 1e9 + // period_ns = (2^resolution * divider) * 12.5 + // divider = period_ns / (2^resolution * 12.5) + // divider = period_ns * 80 / (2^resolution * 1000) + + // Try to find the highest resolution that gives a valid divider + for resolution := uint8(maxResolution); resolution >= minResolution; resolution-- { + // Calculate divider for this resolution + // divider = period * 80MHz / (2^resolution * 1e9) + // To avoid overflow: divider = period * 80 / (2^resolution * 1000) + resolutionValue := uint64(1) << resolution + divider := (period * 80) / (resolutionValue * 1000) + + if divider == 0 { + // Period too short for this resolution, try lower resolution + continue + } + + if divider <= maxDivider { + return uint32(divider), resolution, nil + } + } + + return 0, 0, ErrPWMPeriodTooLong +} + +// Channel returns a PWM channel for the given pin. If the pin is already +// configured for this PWM peripheral, the same channel is returned. +// The pin is configured for PWM output. +func (pwm *PWM) Channel(pin Pin) (uint8, error) { + if !pwmStates[pwm.num].configured { + // Timer not configured, configure with default period + if err := pwm.Configure(PWMConfig{}); err != nil { + return 0, err + } + } + + // Check if this pin is already assigned to a channel + for ch := uint8(0); ch < 8; ch++ { + if pwmChannelPins[ch] == pin { + return ch, nil + } + } + + // Find an available channel + for ch := uint8(0); ch < 8; ch++ { + if pwmChannelPins[ch] == NoPin || pwmChannelPins[ch] == 0 { + // Found an available channel + pwmChannelPins[ch] = pin + + // Configure the GPIO for PWM output through GPIO matrix + signal := uint32(ledcHSSignalOut0 + ch) + pin.configure(PinConfig{Mode: PinOutput}, signal) + + // Configure the channel + chanConf0 := pwm.channelConf0(ch) + + // Set channel configuration: + // - Enable clock + // - Select this timer + // - Enable signal output + // - Idle level low + var conf uint32 + conf |= chanClkEnMask // Enable clock + conf |= uint32(pwm.num) & chanTimerSelMask // Select timer + conf |= chanSigOutEnMask // Enable output + chanConf0.Set(conf) + + // Initialize duty to 0 + pwm.channelDuty(ch).Set(0) + + // Set HPOINT to 0 (start of cycle) + pwm.channelHpoint(ch).Set(0) + + // Trigger duty update + pwm.channelConf1(ch).SetBits(chanDutyStartMask) + + return ch, nil + } + } + + return 0, ErrInvalidOutputPin +} + +// Set updates the channel value. This is used to control the channel duty +// cycle. For example, to set it to a 25% duty cycle, use: +// +// pwm.Set(channel, pwm.Top() / 4) +// +// pwm.Set(channel, 0) will set the output to low and pwm.Set(channel, +// pwm.Top()) will set the output to high, assuming the output isn't inverted. +func (pwm *PWM) Set(channel uint8, value uint32) { + if channel >= 8 { + return + } + + resolution := pwmStates[pwm.num].resolution + maxValue := uint32((1 << resolution) - 1) + + // Clamp value to valid range + // Note: Setting duty to exactly 2^resolution causes hardware overflow + if value > maxValue { + value = maxValue + } + + // The duty register uses 4 fractional bits, so shift left by 4 + dutyValue := value << chanDutyFracBits + + // Set the duty value + pwm.channelDuty(channel).Set(dutyValue) + + // Trigger duty update by setting DUTY_START bit + pwm.channelConf1(channel).SetBits(chanDutyStartMask) +} + +// SetPeriod updates the period of this PWM peripheral in nanoseconds. +// To set a particular frequency, use the following formula: +// +// period = 1e9 / frequency +// +// SetPeriod will try to maintain the current duty cycle ratio when changing +// the period. +func (pwm *PWM) SetPeriod(period uint64) error { + // Calculate new configuration + divider, resolution, err := pwm.calculateConfig(period) + if err != nil { + return err + } + + oldResolution := pwmStates[pwm.num].resolution + pwmStates[pwm.num].resolution = resolution + + // Update timer configuration + timerConf := pwm.timerConf() + + // Read current config, update divider and resolution + conf := timerConf.Get() + conf &^= timerDivNumMask | timerLimMask + conf |= (divider << timerDivNumPos) & timerDivNumMask + conf |= uint32(resolution-1) & timerLimMask + timerConf.Set(conf) + + // If resolution changed, we may need to scale duty values + if resolution != oldResolution { + // Scale all active channel duty values + for ch := uint8(0); ch < 8; ch++ { + if pwmChannelPins[ch] != NoPin && pwmChannelPins[ch] != 0 { + // Read current duty (includes fractional bits) + currentDuty := pwm.channelDuty(ch).Get() >> chanDutyFracBits + + // Scale to new resolution + if oldResolution > resolution { + currentDuty >>= (oldResolution - resolution) + } else { + currentDuty <<= (resolution - oldResolution) + } + + // Apply new duty + pwm.Set(ch, currentDuty) + } + } + } + + return nil +} + +// Top returns the current counter top, for use in duty cycle calculation. +// The value returned is (2^resolution - 1), which is the maximum value +// that can be passed to Set(). +func (pwm *PWM) Top() uint32 { + resolution := pwmStates[pwm.num].resolution + if resolution == 0 { + resolution = 13 // Default resolution + } + return (1 << resolution) - 1 +} + +// Counter returns the current counter value of the timer. +// This may be useful for debugging. +func (pwm *PWM) Counter() uint32 { + return pwm.timerValue().Get() +} + +// Period returns the current period in nanoseconds. +func (pwm *PWM) Period() uint64 { + conf := pwm.timerConf().Get() + divider := (conf & timerDivNumMask) >> timerDivNumPos + resolution := (conf & timerLimMask) + 1 + + // period_ns = (2^resolution * divider) * 12.5 + // period_ns = (2^resolution * divider) * 1000 / 80 + resolutionValue := uint64(1) << resolution + return resolutionValue * uint64(divider) * 1000 / 80 +} + +// SetInverting sets whether to invert the output of this channel. +// Without inverting, a 25% duty cycle would mean the output is high for 25% of +// the time and low for the rest. Inverting flips the output as if a NOT gate +// was placed at the output, meaning that the output would be 25% low and 75% +// high with a duty cycle of 25%. +func (pwm *PWM) SetInverting(channel uint8, inverting bool) { + if channel >= 8 { + return + } + + chanConf0 := pwm.channelConf0(channel) + if inverting { + chanConf0.SetBits(chanIdleLvMask) + } else { + chanConf0.ClearBits(chanIdleLvMask) + } +} + +// Enable enables or disables the PWM output for all channels on this timer. +func (pwm *PWM) Enable(enable bool) { + timerConf := pwm.timerConf() + if enable { + timerConf.ClearBits(timerPauseMask) + } else { + timerConf.SetBits(timerPauseMask) + } +} + +// Register access helpers + +// timerConf returns the configuration register for this timer. +func (pwm *PWM) timerConf() *volatile.Register32 { + // HSTIMER0_CONF is at offset 0x140, each timer is 0x8 bytes apart + base := uintptr(unsafe.Pointer(&esp.LEDC.HSTIMER0_CONF)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(pwm.num)*0x8)) +} + +// timerValue returns the value register for this timer. +func (pwm *PWM) timerValue() *volatile.Register32 { + // HSTIMER0_VALUE is at offset 0x144, each timer is 0x8 bytes apart + base := uintptr(unsafe.Pointer(&esp.LEDC.HSTIMER0_VALUE)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(pwm.num)*0x8)) +} + +// channelConf0 returns the CONF0 register for the given channel. +func (pwm *PWM) channelConf0(ch uint8) *volatile.Register32 { + // HSCH0_CONF0 is at offset 0x0, each channel is 0x14 bytes apart + base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_CONF0)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) +} + +// channelHpoint returns the HPOINT register for the given channel. +func (pwm *PWM) channelHpoint(ch uint8) *volatile.Register32 { + // HSCH0_HPOINT is at offset 0x4, each channel is 0x14 bytes apart + base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_HPOINT)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) +} + +// channelDuty returns the DUTY register for the given channel. +func (pwm *PWM) channelDuty(ch uint8) *volatile.Register32 { + // HSCH0_DUTY is at offset 0x8, each channel is 0x14 bytes apart + base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_DUTY)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) +} + +// channelConf1 returns the CONF1 register for the given channel. +func (pwm *PWM) channelConf1(ch uint8) *volatile.Register32 { + // HSCH0_CONF1 is at offset 0xC, each channel is 0x14 bytes apart + base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_CONF1)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) +} From a58f5559dd7751d0107b05ec973ee566a756c789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 20 Jan 2026 15:38:29 +0000 Subject: [PATCH 2/9] machine/esp32: fix PWM duty cycle not working Fix several issues with the LEDC PWM implementation: 1. Timer reset: The reset bit must be set then cleared (not just set) 2. CONF1 register: For non-fading operation, duty_cycle and duty_num must be set to 1, and duty_inc must be enabled 3. Duty update: Properly configure CONF1 when updating duty values These fixes align with the ESP-IDF LEDC driver implementation. Co-authored-by: Ona --- src/machine/machine_esp32_pwm.go | 34 +++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index bdbe08562d..bfc4ccf7e8 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -84,6 +84,14 @@ const ( chanClkEnMask = 1 << chanClkEnPos // HSCH_CONF1 register + chanDutyScalePos = 0 // DUTY_SCALE position (bits 0-9) + chanDutyScaleMask = 0x3FF + chanDutyCyclePos = 10 // DUTY_CYCLE position (bits 10-19) + chanDutyCycleMask = 0x3FF << chanDutyCyclePos + chanDutyNumPos = 20 // DUTY_NUM position (bits 20-29) + chanDutyNumMask = 0x3FF << chanDutyNumPos + chanDutyIncPos = 30 // DUTY_INC bit + chanDutyIncMask = 1 << chanDutyIncPos chanDutyStartPos = 31 // DUTY_START bit chanDutyStartMask = 1 << chanDutyStartPos @@ -122,9 +130,6 @@ func (pwm *PWM) Configure(config PWMConfig) error { // Get timer configuration register timerConf := pwm.timerConf() - // Reset the timer first - timerConf.SetBits(timerRstMask) - // Configure timer: // - Use APB_CLK (80MHz) as clock source // - Set divider @@ -133,6 +138,9 @@ func (pwm *PWM) Configure(config PWMConfig) error { conf |= timerTickSelMask // APB_CLK conf |= (divider << timerDivNumPos) & timerDivNumMask // Divider conf |= uint32(resolution-1) & timerLimMask // Resolution + + // Reset the timer (set rst=1, then rst=0) + timerConf.Set(conf | timerRstMask) timerConf.Set(conf) return nil @@ -221,8 +229,14 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { // Set HPOINT to 0 (start of cycle) pwm.channelHpoint(ch).Set(0) - // Trigger duty update - pwm.channelConf1(ch).SetBits(chanDutyStartMask) + // Configure CONF1 for non-fading operation and trigger duty update + // duty_scale=0, duty_cycle=1, duty_num=1, duty_inc=1, duty_start=1 + var conf1 uint32 + conf1 |= 1 << chanDutyCyclePos // duty_cycle = 1 + conf1 |= 1 << chanDutyNumPos // duty_num = 1 + conf1 |= chanDutyIncMask // duty_inc = 1 + conf1 |= chanDutyStartMask // duty_start = 1 + pwm.channelConf1(ch).Set(conf1) return ch, nil } @@ -258,8 +272,14 @@ func (pwm *PWM) Set(channel uint8, value uint32) { // Set the duty value pwm.channelDuty(channel).Set(dutyValue) - // Trigger duty update by setting DUTY_START bit - pwm.channelConf1(channel).SetBits(chanDutyStartMask) + // Configure CONF1 for non-fading operation and trigger duty update + // duty_scale=0, duty_cycle=1, duty_num=1, duty_inc=1, duty_start=1 + var conf1 uint32 + conf1 |= 1 << chanDutyCyclePos // duty_cycle = 1 + conf1 |= 1 << chanDutyNumPos // duty_num = 1 + conf1 |= chanDutyIncMask // duty_inc = 1 + conf1 |= chanDutyStartMask // duty_start = 1 + pwm.channelConf1(channel).Set(conf1) } // SetPeriod updates the period of this PWM peripheral in nanoseconds. From 0cb1af7967e46f860774efb87645a999465b4801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 20 Jan 2026 15:43:47 +0000 Subject: [PATCH 3/9] machine/esp32: fix PWM clock divider calculation The LEDC clock divider register uses 8 fractional bits, meaning the register value is actual_divider * 256. The previous implementation was setting the divider directly without accounting for this, resulting in a divider that was 256x smaller than intended. Also added: - Set LEDC.CONF.APB_CLK_SEL = 1 to select APB clock source - Updated Period() calculation to account for fractional bits Co-authored-by: Ona --- src/machine/machine_esp32_pwm.go | 49 ++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index bfc4ccf7e8..f671f805b6 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -50,10 +50,15 @@ const ( // APB clock frequency (used by high-speed LEDC) apbClockFreq = 80000000 // 80 MHz + // Clock divider has 8 fractional bits + // So divider register value = actual_divider * 256 + dividerFractionalBits = 8 + // Maximum values - maxDivider = 0x3FFFF // 18-bit divider - maxResolution = 20 // Maximum duty resolution in bits - minResolution = 1 // Minimum duty resolution in bits + maxDivider = 0x3FFFF // 18-bit divider register value (actual max divider ~1024) + minDivider = 1 << dividerFractionalBits // Minimum divider = 256 (represents 1.0) + maxResolution = 20 // Maximum duty resolution in bits + minResolution = 1 // Minimum duty resolution in bits ) // LEDC register bit positions and masks for timer configuration @@ -117,6 +122,9 @@ func (pwm *PWM) Configure(config PWMConfig) error { // Clear reset bit esp.DPORT.PERIP_RST_EN.ClearBits(ledcClockEnable) + // Select APB clock (80MHz) for LEDC + esp.LEDC.CONF.Set(1) // APB_CLK_SEL = 1 + // Calculate timer configuration divider, resolution, err := pwm.calculateConfig(config.Period) if err != nil { @@ -147,7 +155,7 @@ func (pwm *PWM) Configure(config PWMConfig) error { } // calculateConfig determines the optimal divider and resolution for a given period. -// Returns divider, resolution, and any error. +// Returns divider (with 8 fractional bits), resolution, and any error. func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { if period == 0 { // Default: ~1kHz with 13-bit resolution (good for LEDs) @@ -156,25 +164,30 @@ func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { } // Formula: period_ns = (2^resolution * divider) / 80MHz * 1e9 - // period_ns = (2^resolution * divider) * 12.5 - // divider = period_ns / (2^resolution * 12.5) - // divider = period_ns * 80 / (2^resolution * 1000) + // Where divider is the actual divider (not the register value) + // Register value = actual_divider * 256 (8 fractional bits) + // + // period_ns = (2^resolution * divider_reg / 256) / 80MHz * 1e9 + // divider_reg = period_ns * 80MHz * 256 / (2^resolution * 1e9) + // divider_reg = period_ns * 80 * 256 / (2^resolution * 1000) + // divider_reg = period_ns * 20480 / (2^resolution * 1000) + // divider_reg = period_ns * 256 * 80 / (2^resolution * 1000) // Try to find the highest resolution that gives a valid divider for resolution := uint8(maxResolution); resolution >= minResolution; resolution-- { - // Calculate divider for this resolution - // divider = period * 80MHz / (2^resolution * 1e9) - // To avoid overflow: divider = period * 80 / (2^resolution * 1000) resolutionValue := uint64(1) << resolution - divider := (period * 80) / (resolutionValue * 1000) - if divider == 0 { + // Calculate divider register value (includes 8 fractional bits) + // divider_reg = period_ns * 80 * 256 / (2^resolution * 1000) + dividerReg := (period * 80 * 256) / (resolutionValue * 1000) + + if dividerReg < minDivider { // Period too short for this resolution, try lower resolution continue } - if divider <= maxDivider { - return uint32(divider), resolution, nil + if dividerReg <= maxDivider { + return uint32(dividerReg), resolution, nil } } @@ -353,13 +366,13 @@ func (pwm *PWM) Counter() uint32 { // Period returns the current period in nanoseconds. func (pwm *PWM) Period() uint64 { conf := pwm.timerConf().Get() - divider := (conf & timerDivNumMask) >> timerDivNumPos + dividerReg := (conf & timerDivNumMask) >> timerDivNumPos resolution := (conf & timerLimMask) + 1 - // period_ns = (2^resolution * divider) * 12.5 - // period_ns = (2^resolution * divider) * 1000 / 80 + // period_ns = (2^resolution * divider_reg / 256) / 80MHz * 1e9 + // period_ns = (2^resolution * divider_reg * 1000) / (80 * 256) resolutionValue := uint64(1) << resolution - return resolutionValue * uint64(divider) * 1000 / 80 + return resolutionValue * uint64(dividerReg) * 1000 / (80 * 256) } // SetInverting sets whether to invert the output of this channel. From 69fba70f59bf315bff85e362ff2146aae91e7a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 20 Jan 2026 15:48:17 +0000 Subject: [PATCH 4/9] machine/esp32: improve PWM duty update reliability - Always write full CONF1 register value when updating duty - Re-enable sig_out_en on each duty update (matching ESP-IDF behavior) This should improve the smoothness of duty cycle transitions. Co-authored-by: Ona --- src/machine/machine_esp32_pwm.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index f671f805b6..f48dd12992 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -285,14 +285,19 @@ func (pwm *PWM) Set(channel uint8, value uint32) { // Set the duty value pwm.channelDuty(channel).Set(dutyValue) - // Configure CONF1 for non-fading operation and trigger duty update - // duty_scale=0, duty_cycle=1, duty_num=1, duty_inc=1, duty_start=1 + // Configure CONF1 and trigger duty update + // For non-fading operation: duty_scale=0, duty_cycle=1, duty_num=1, duty_inc=1 + // Then set duty_start=1 to apply the new duty value + // We write the full register value each time to ensure consistent state var conf1 uint32 conf1 |= 1 << chanDutyCyclePos // duty_cycle = 1 conf1 |= 1 << chanDutyNumPos // duty_num = 1 conf1 |= chanDutyIncMask // duty_inc = 1 conf1 |= chanDutyStartMask // duty_start = 1 pwm.channelConf1(channel).Set(conf1) + + // Ensure signal output is enabled (as ESP-IDF does in _ledc_update_duty) + pwm.channelConf0(channel).SetBits(chanSigOutEnMask) } // SetPeriod updates the period of this PWM peripheral in nanoseconds. From 4f31a6a27e44bbd0a590d9d44074b71f96e61ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 20 Jan 2026 15:54:32 +0000 Subject: [PATCH 5/9] machine/esp32: fix PWM resolution register value The duty_resolution register stores the bit width directly, not bit width - 1. Setting resolution-1 caused the timer counter to wrap at half the expected value, resulting in duty cycle glitches when the duty value exceeded the actual counter range. For example, with 20-bit resolution intended: - Before: register = 19, counter wraps at 524287 - After: register = 20, counter wraps at 1048575 This fixes the 'reset in the middle' behavior during fades. Co-authored-by: Ona --- src/machine/machine_esp32_pwm.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index f48dd12992..301c80ad76 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -66,7 +66,7 @@ const ( // HSTIMER_CONF register timerDivNumPos = 5 // DIV_NUM position (bits 5-22) timerDivNumMask = 0x3FFFF << timerDivNumPos - timerLimPos = 0 // LIM position (bits 0-4), resolution = LIM + 1 + timerLimPos = 0 // duty_resolution position (bits 0-4), stores bit width directly timerLimMask = 0x1F timerPausePos = 23 // PAUSE bit timerPauseMask = 1 << timerPausePos @@ -141,11 +141,11 @@ func (pwm *PWM) Configure(config PWMConfig) error { // Configure timer: // - Use APB_CLK (80MHz) as clock source // - Set divider - // - Set resolution (LIM = resolution - 1) + // - Set resolution (duty_resolution field stores the bit width directly) var conf uint32 conf |= timerTickSelMask // APB_CLK conf |= (divider << timerDivNumPos) & timerDivNumMask // Divider - conf |= uint32(resolution-1) & timerLimMask // Resolution + conf |= uint32(resolution) & timerLimMask // Resolution (bit width) // Reset the timer (set rst=1, then rst=0) timerConf.Set(conf | timerRstMask) @@ -372,7 +372,7 @@ func (pwm *PWM) Counter() uint32 { func (pwm *PWM) Period() uint64 { conf := pwm.timerConf().Get() dividerReg := (conf & timerDivNumMask) >> timerDivNumPos - resolution := (conf & timerLimMask) + 1 + resolution := conf & timerLimMask // duty_resolution stores bit width directly // period_ns = (2^resolution * divider_reg / 256) / 80MHz * 1e9 // period_ns = (2^resolution * divider_reg * 1000) / (80 * 256) From 952dc77f327ac826130bef0086f1aaee3082854f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 20 Jan 2026 16:02:42 +0000 Subject: [PATCH 6/9] machine/esp32: improve PWM implementation Improvements: - Fix SetPeriod using resolution-1 instead of resolution - Refactor channel tracking to properly associate channels with timers - Add pwmChannelInfo struct to track pin, timer, and usage state - Prevent same pin from being used on different timers - Add ReleaseChannel() method to free channels - Add Get() method to read current duty value - Fix SetInverting to use GPIO matrix inversion (bit 9) - SetPeriod now only scales channels bound to this timer - Remove unused constants, add named constants for defaults - Add bounds checking in Period() for zero values Co-authored-by: Ona --- src/machine/machine_esp32_pwm.go | 175 ++++++++++++++++++++----------- 1 file changed, 112 insertions(+), 63 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index 301c80ad76..63b856c522 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -11,7 +11,7 @@ import ( // PWM is one PWM peripheral, which consists of a timer and the associated // channels. There are 4 high-speed timers available (PWM0-PWM3), each can -// drive up to 8 channels total (shared across all timers). +// use any of the 8 high-speed channels. // // The ESP32 LEDC peripheral is used for PWM generation. It provides flexible // frequency and duty cycle control with configurable resolution (1-20 bits). @@ -19,9 +19,15 @@ type PWM struct { num uint8 // Timer number (0-3 for high-speed) } -// pwmChannelPins tracks which pin is assigned to each channel (0-7) -// Initialized to NoPin (0xff) to indicate unused channels -var pwmChannelPins [8]Pin +// pwmChannelInfo tracks the state of each LEDC channel +type pwmChannelInfo struct { + pin Pin // The pin assigned to this channel (NoPin if unused) + timer uint8 // The timer this channel is bound to + inUse bool // Whether this channel is currently in use +} + +// pwmChannels tracks which pin and timer is assigned to each channel (0-7) +var pwmChannels [8]pwmChannelInfo // Hardware PWM peripherals available on ESP32. // These use the high-speed LEDC timers for glitch-free PWM updates. @@ -38,17 +44,7 @@ const ( ledcClockEnable = 1 << 11 // GPIO matrix output signal numbers for LEDC high-speed channels - ledcHSSignalOut0 = 71 - ledcHSSignalOut1 = 72 - ledcHSSignalOut2 = 73 - ledcHSSignalOut3 = 74 - ledcHSSignalOut4 = 75 - ledcHSSignalOut5 = 76 - ledcHSSignalOut6 = 77 - ledcHSSignalOut7 = 78 - - // APB clock frequency (used by high-speed LEDC) - apbClockFreq = 80000000 // 80 MHz + ledcHSSignalBase = 71 // LEDC_HS_SIG_OUT0, channels are consecutive (71-78) // Clock divider has 8 fractional bits // So divider register value = actual_divider * 256 @@ -59,6 +55,10 @@ const ( minDivider = 1 << dividerFractionalBits // Minimum divider = 256 (represents 1.0) maxResolution = 20 // Maximum duty resolution in bits minResolution = 1 // Minimum duty resolution in bits + + // Default values + defaultPeriodNs = 1_000_000 // 1ms = 1kHz, good for LEDs + defaultResolution = 13 // 13-bit resolution for default period ) // LEDC register bit positions and masks for timer configuration @@ -66,39 +66,25 @@ const ( // HSTIMER_CONF register timerDivNumPos = 5 // DIV_NUM position (bits 5-22) timerDivNumMask = 0x3FFFF << timerDivNumPos - timerLimPos = 0 // duty_resolution position (bits 0-4), stores bit width directly - timerLimMask = 0x1F - timerPausePos = 23 // PAUSE bit - timerPauseMask = 1 << timerPausePos - timerRstPos = 24 // RST bit - timerRstMask = 1 << timerRstPos - timerTickSelPos = 25 // TICK_SEL bit (0=REF_TICK, 1=APB_CLK) - timerTickSelMask = 1 << timerTickSelPos + timerLimMask = 0x1F // duty_resolution (bits 0-4), stores bit width directly + timerPauseMask = 1 << 23 + timerRstMask = 1 << 24 + timerTickSelMask = 1 << 25 // TICK_SEL bit (0=REF_TICK, 1=APB_CLK) ) // LEDC register bit positions and masks for channel configuration const ( // HSCH_CONF0 register - chanTimerSelPos = 0 // TIMER_SEL position (bits 0-1) - chanTimerSelMask = 0x3 - chanSigOutEnPos = 2 // SIG_OUT_EN bit - chanSigOutEnMask = 1 << chanSigOutEnPos - chanIdleLvPos = 3 // IDLE_LV bit - chanIdleLvMask = 1 << chanIdleLvPos - chanClkEnPos = 31 // CLK_EN bit - chanClkEnMask = 1 << chanClkEnPos + chanTimerSelMask = 0x3 // TIMER_SEL (bits 0-1) + chanSigOutEnMask = 1 << 2 // SIG_OUT_EN bit + chanIdleLvMask = 1 << 3 // IDLE_LV bit + chanClkEnMask = 1 << 31 // CLK_EN bit // HSCH_CONF1 register - chanDutyScalePos = 0 // DUTY_SCALE position (bits 0-9) - chanDutyScaleMask = 0x3FF chanDutyCyclePos = 10 // DUTY_CYCLE position (bits 10-19) - chanDutyCycleMask = 0x3FF << chanDutyCyclePos chanDutyNumPos = 20 // DUTY_NUM position (bits 20-29) - chanDutyNumMask = 0x3FF << chanDutyNumPos - chanDutyIncPos = 30 // DUTY_INC bit - chanDutyIncMask = 1 << chanDutyIncPos - chanDutyStartPos = 31 // DUTY_START bit - chanDutyStartMask = 1 << chanDutyStartPos + chanDutyIncMask = 1 << 30 + chanDutyStartMask = 1 << 31 // HSCH_DUTY register - duty value uses bits 0-24 (25 bits total) // The lower 4 bits are fractional, upper 20 bits are integer @@ -158,9 +144,7 @@ func (pwm *PWM) Configure(config PWMConfig) error { // Returns divider (with 8 fractional bits), resolution, and any error. func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { if period == 0 { - // Default: ~1kHz with 13-bit resolution (good for LEDs) - // period = 1e9 / 1000 = 1,000,000 ns - period = 1000000 + period = defaultPeriodNs } // Formula: period_ns = (2^resolution * divider) / 80MHz * 1e9 @@ -168,17 +152,13 @@ func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { // Register value = actual_divider * 256 (8 fractional bits) // // period_ns = (2^resolution * divider_reg / 256) / 80MHz * 1e9 - // divider_reg = period_ns * 80MHz * 256 / (2^resolution * 1e9) // divider_reg = period_ns * 80 * 256 / (2^resolution * 1000) - // divider_reg = period_ns * 20480 / (2^resolution * 1000) - // divider_reg = period_ns * 256 * 80 / (2^resolution * 1000) // Try to find the highest resolution that gives a valid divider for resolution := uint8(maxResolution); resolution >= minResolution; resolution-- { resolutionValue := uint64(1) << resolution // Calculate divider register value (includes 8 fractional bits) - // divider_reg = period_ns * 80 * 256 / (2^resolution * 1000) dividerReg := (period * 80 * 256) / (resolutionValue * 1000) if dividerReg < minDivider { @@ -205,21 +185,35 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { } } - // Check if this pin is already assigned to a channel + // Check if this pin is already assigned to a channel on THIS timer for ch := uint8(0); ch < 8; ch++ { - if pwmChannelPins[ch] == pin { + if pwmChannels[ch].inUse && + pwmChannels[ch].pin == pin && + pwmChannels[ch].timer == pwm.num { return ch, nil } } + // Check if pin is used by a different timer (error case) + for ch := uint8(0); ch < 8; ch++ { + if pwmChannels[ch].inUse && pwmChannels[ch].pin == pin { + // Pin is already used by a different timer + return 0, ErrInvalidOutputPin + } + } + // Find an available channel for ch := uint8(0); ch < 8; ch++ { - if pwmChannelPins[ch] == NoPin || pwmChannelPins[ch] == 0 { + if !pwmChannels[ch].inUse { // Found an available channel - pwmChannelPins[ch] = pin + pwmChannels[ch] = pwmChannelInfo{ + pin: pin, + timer: pwm.num, + inUse: true, + } // Configure the GPIO for PWM output through GPIO matrix - signal := uint32(ledcHSSignalOut0 + ch) + signal := uint32(ledcHSSignalBase + ch) pin.configure(PinConfig{Mode: PinOutput}, signal) // Configure the channel @@ -258,6 +252,27 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { return 0, ErrInvalidOutputPin } +// ReleaseChannel releases a PWM channel, making it available for other uses. +// The pin is not reconfigured; call pin.Configure() to change its function. +func (pwm *PWM) ReleaseChannel(channel uint8) error { + if channel >= 8 { + return ErrInvalidOutputPin + } + + if !pwmChannels[channel].inUse || pwmChannels[channel].timer != pwm.num { + // Channel not in use or belongs to different timer + return ErrInvalidOutputPin + } + + // Disable signal output + pwm.channelConf0(channel).ClearBits(chanSigOutEnMask) + + // Clear channel tracking + pwmChannels[channel] = pwmChannelInfo{} + + return nil +} + // Set updates the channel value. This is used to control the channel duty // cycle. For example, to set it to a 25% duty cycle, use: // @@ -288,7 +303,6 @@ func (pwm *PWM) Set(channel uint8, value uint32) { // Configure CONF1 and trigger duty update // For non-fading operation: duty_scale=0, duty_cycle=1, duty_num=1, duty_inc=1 // Then set duty_start=1 to apply the new duty value - // We write the full register value each time to ensure consistent state var conf1 uint32 conf1 |= 1 << chanDutyCyclePos // duty_cycle = 1 conf1 |= 1 << chanDutyNumPos // duty_num = 1 @@ -296,17 +310,27 @@ func (pwm *PWM) Set(channel uint8, value uint32) { conf1 |= chanDutyStartMask // duty_start = 1 pwm.channelConf1(channel).Set(conf1) - // Ensure signal output is enabled (as ESP-IDF does in _ledc_update_duty) + // Ensure signal output is enabled pwm.channelConf0(channel).SetBits(chanSigOutEnMask) } +// Get returns the current duty cycle value for the given channel. +func (pwm *PWM) Get(channel uint8) uint32 { + if channel >= 8 { + return 0 + } + + // Read from the duty read register and remove fractional bits + return pwm.channelDutyR(channel).Get() >> chanDutyFracBits +} + // SetPeriod updates the period of this PWM peripheral in nanoseconds. // To set a particular frequency, use the following formula: // // period = 1e9 / frequency // // SetPeriod will try to maintain the current duty cycle ratio when changing -// the period. +// the period for channels bound to this timer. func (pwm *PWM) SetPeriod(period uint64) error { // Calculate new configuration divider, resolution, err := pwm.calculateConfig(period) @@ -324,14 +348,13 @@ func (pwm *PWM) SetPeriod(period uint64) error { conf := timerConf.Get() conf &^= timerDivNumMask | timerLimMask conf |= (divider << timerDivNumPos) & timerDivNumMask - conf |= uint32(resolution-1) & timerLimMask + conf |= uint32(resolution) & timerLimMask timerConf.Set(conf) - // If resolution changed, we may need to scale duty values + // If resolution changed, scale duty values for channels bound to THIS timer if resolution != oldResolution { - // Scale all active channel duty values for ch := uint8(0); ch < 8; ch++ { - if pwmChannelPins[ch] != NoPin && pwmChannelPins[ch] != 0 { + if pwmChannels[ch].inUse && pwmChannels[ch].timer == pwm.num { // Read current duty (includes fractional bits) currentDuty := pwm.channelDuty(ch).Get() >> chanDutyFracBits @@ -357,7 +380,7 @@ func (pwm *PWM) SetPeriod(period uint64) error { func (pwm *PWM) Top() uint32 { resolution := pwmStates[pwm.num].resolution if resolution == 0 { - resolution = 13 // Default resolution + resolution = defaultResolution } return (1 << resolution) - 1 } @@ -372,7 +395,11 @@ func (pwm *PWM) Counter() uint32 { func (pwm *PWM) Period() uint64 { conf := pwm.timerConf().Get() dividerReg := (conf & timerDivNumMask) >> timerDivNumPos - resolution := conf & timerLimMask // duty_resolution stores bit width directly + resolution := conf & timerLimMask + + if resolution == 0 || dividerReg == 0 { + return 0 + } // period_ns = (2^resolution * divider_reg / 256) / 80MHz * 1e9 // period_ns = (2^resolution * divider_reg * 1000) / (80 * 256) @@ -390,11 +417,26 @@ func (pwm *PWM) SetInverting(channel uint8, inverting bool) { return } - chanConf0 := pwm.channelConf0(channel) + // Get the pin for this channel to configure GPIO matrix inversion + if !pwmChannels[channel].inUse || pwmChannels[channel].timer != pwm.num { + return + } + + pin := pwmChannels[channel].pin + + // Reconfigure the GPIO with inversion setting through GPIO matrix + // The GPIO matrix FUNC_OUT_SEL_CFG register has an inversion bit + signal := uint32(ledcHSSignalBase + channel) + + // Get the GPIO function output select register + outFunc := pin.outFunc() + if inverting { - chanConf0.SetBits(chanIdleLvMask) + // Set signal with inversion enabled (bit 9 is the invert bit) + outFunc.Set(signal | (1 << 9)) } else { - chanConf0.ClearBits(chanIdleLvMask) + // Set signal without inversion + outFunc.Set(signal) } } @@ -451,3 +493,10 @@ func (pwm *PWM) channelConf1(ch uint8) *volatile.Register32 { base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_CONF1)) return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) } + +// channelDutyR returns the DUTY_R (read) register for the given channel. +func (pwm *PWM) channelDutyR(ch uint8) *volatile.Register32 { + // HSCH0_DUTY_R is at offset 0x10, each channel is 0x14 bytes apart + base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_DUTY_R)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) +} From 971ef70e16f93aa4ed01198fedad7a8d22559c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 20 Jan 2026 16:07:06 +0000 Subject: [PATCH 7/9] machine/esp32: second pass improvements for PWM Additional improvements: - Add errPWMPeriodTooShort error for periods that are too short - Add pwmChannelCount constant and use it consistently - Add gpioMatrixInvertBit constant for clarity - Use dividerFracBits constant in calculations instead of magic 256 - Add isValidChannel() helper for centralized validation - Add IsConnected() method to check if channel is in use - Add SetCounter() method for timer synchronization (API compatibility) - Handle unconfigured timer in Set() by using default resolution - Improve Enable() documentation - Remove unused chanIdleLvMask constant Co-authored-by: Ona --- src/machine/machine_esp32_pwm.go | 88 ++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index 63b856c522..5284868adc 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -5,10 +5,16 @@ package machine import ( "device/esp" + "errors" "runtime/volatile" "unsafe" ) +// PWM peripheral errors +var ( + errPWMPeriodTooShort = errors.New("pwm: period too short") +) + // PWM is one PWM peripheral, which consists of a timer and the associated // channels. There are 4 high-speed timers available (PWM0-PWM3), each can // use any of the 8 high-speed channels. @@ -38,6 +44,9 @@ var ( PWM3 = &PWM{num: 3} ) +// Number of available PWM channels +const pwmChannelCount = 8 + // LEDC peripheral constants const ( // Clock enable bit in DPORT.PERIP_CLK_EN for LEDC @@ -46,15 +55,17 @@ const ( // GPIO matrix output signal numbers for LEDC high-speed channels ledcHSSignalBase = 71 // LEDC_HS_SIG_OUT0, channels are consecutive (71-78) - // Clock divider has 8 fractional bits - // So divider register value = actual_divider * 256 - dividerFractionalBits = 8 + // GPIO matrix output inversion bit + gpioMatrixInvertBit = 1 << 9 + + // Clock divider fractional bits (register value = actual_divider * 256) + dividerFracBits = 8 // Maximum values - maxDivider = 0x3FFFF // 18-bit divider register value (actual max divider ~1024) - minDivider = 1 << dividerFractionalBits // Minimum divider = 256 (represents 1.0) - maxResolution = 20 // Maximum duty resolution in bits - minResolution = 1 // Minimum duty resolution in bits + maxDivider = 0x3FFFF // 18-bit divider register value + minDivider = 1 << dividerFracBits // Minimum divider = 256 (represents 1.0) + maxResolution = 20 // Maximum duty resolution in bits + minResolution = 1 // Minimum duty resolution in bits // Default values defaultPeriodNs = 1_000_000 // 1ms = 1kHz, good for LEDs @@ -77,7 +88,6 @@ const ( // HSCH_CONF0 register chanTimerSelMask = 0x3 // TIMER_SEL (bits 0-1) chanSigOutEnMask = 1 << 2 // SIG_OUT_EN bit - chanIdleLvMask = 1 << 3 // IDLE_LV bit chanClkEnMask = 1 << 31 // CLK_EN bit // HSCH_CONF1 register @@ -154,12 +164,15 @@ func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { // period_ns = (2^resolution * divider_reg / 256) / 80MHz * 1e9 // divider_reg = period_ns * 80 * 256 / (2^resolution * 1000) + var lastDividerReg uint64 + // Try to find the highest resolution that gives a valid divider for resolution := uint8(maxResolution); resolution >= minResolution; resolution-- { resolutionValue := uint64(1) << resolution // Calculate divider register value (includes 8 fractional bits) - dividerReg := (period * 80 * 256) / (resolutionValue * 1000) + dividerReg := (period * 80 * (1 << dividerFracBits)) / (resolutionValue * 1000) + lastDividerReg = dividerReg if dividerReg < minDivider { // Period too short for this resolution, try lower resolution @@ -171,6 +184,10 @@ func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { } } + // Determine which error to return + if lastDividerReg < minDivider { + return 0, 0, errPWMPeriodTooShort + } return 0, 0, ErrPWMPeriodTooLong } @@ -186,7 +203,7 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { } // Check if this pin is already assigned to a channel on THIS timer - for ch := uint8(0); ch < 8; ch++ { + for ch := uint8(0); ch < pwmChannelCount; ch++ { if pwmChannels[ch].inUse && pwmChannels[ch].pin == pin && pwmChannels[ch].timer == pwm.num { @@ -195,7 +212,7 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { } // Check if pin is used by a different timer (error case) - for ch := uint8(0); ch < 8; ch++ { + for ch := uint8(0); ch < pwmChannelCount; ch++ { if pwmChannels[ch].inUse && pwmChannels[ch].pin == pin { // Pin is already used by a different timer return 0, ErrInvalidOutputPin @@ -203,7 +220,7 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { } // Find an available channel - for ch := uint8(0); ch < 8; ch++ { + for ch := uint8(0); ch < pwmChannelCount; ch++ { if !pwmChannels[ch].inUse { // Found an available channel pwmChannels[ch] = pwmChannelInfo{ @@ -255,7 +272,7 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { // ReleaseChannel releases a PWM channel, making it available for other uses. // The pin is not reconfigured; call pin.Configure() to change its function. func (pwm *PWM) ReleaseChannel(channel uint8) error { - if channel >= 8 { + if !pwm.isValidChannel(channel) { return ErrInvalidOutputPin } @@ -273,6 +290,19 @@ func (pwm *PWM) ReleaseChannel(channel uint8) error { return nil } +// IsConnected returns true if the given channel is in use by this PWM peripheral. +func (pwm *PWM) IsConnected(channel uint8) bool { + if !pwm.isValidChannel(channel) { + return false + } + return pwmChannels[channel].inUse && pwmChannels[channel].timer == pwm.num +} + +// isValidChannel returns true if the channel number is valid. +func (pwm *PWM) isValidChannel(channel uint8) bool { + return channel < pwmChannelCount +} + // Set updates the channel value. This is used to control the channel duty // cycle. For example, to set it to a 25% duty cycle, use: // @@ -281,11 +311,14 @@ func (pwm *PWM) ReleaseChannel(channel uint8) error { // pwm.Set(channel, 0) will set the output to low and pwm.Set(channel, // pwm.Top()) will set the output to high, assuming the output isn't inverted. func (pwm *PWM) Set(channel uint8, value uint32) { - if channel >= 8 { + if !pwm.isValidChannel(channel) { return } resolution := pwmStates[pwm.num].resolution + if resolution == 0 { + resolution = defaultResolution + } maxValue := uint32((1 << resolution) - 1) // Clamp value to valid range @@ -316,7 +349,7 @@ func (pwm *PWM) Set(channel uint8, value uint32) { // Get returns the current duty cycle value for the given channel. func (pwm *PWM) Get(channel uint8) uint32 { - if channel >= 8 { + if !pwm.isValidChannel(channel) { return 0 } @@ -353,7 +386,7 @@ func (pwm *PWM) SetPeriod(period uint64) error { // If resolution changed, scale duty values for channels bound to THIS timer if resolution != oldResolution { - for ch := uint8(0); ch < 8; ch++ { + for ch := uint8(0); ch < pwmChannelCount; ch++ { if pwmChannels[ch].inUse && pwmChannels[ch].timer == pwm.num { // Read current duty (includes fractional bits) currentDuty := pwm.channelDuty(ch).Get() >> chanDutyFracBits @@ -413,7 +446,7 @@ func (pwm *PWM) Period() uint64 { // was placed at the output, meaning that the output would be 25% low and 75% // high with a duty cycle of 25%. func (pwm *PWM) SetInverting(channel uint8, inverting bool) { - if channel >= 8 { + if !pwm.isValidChannel(channel) { return } @@ -425,22 +458,21 @@ func (pwm *PWM) SetInverting(channel uint8, inverting bool) { pin := pwmChannels[channel].pin // Reconfigure the GPIO with inversion setting through GPIO matrix - // The GPIO matrix FUNC_OUT_SEL_CFG register has an inversion bit + // The GPIO matrix FUNC_OUT_SEL_CFG register has an inversion bit (bit 9) signal := uint32(ledcHSSignalBase + channel) // Get the GPIO function output select register outFunc := pin.outFunc() if inverting { - // Set signal with inversion enabled (bit 9 is the invert bit) - outFunc.Set(signal | (1 << 9)) + outFunc.Set(signal | gpioMatrixInvertBit) } else { - // Set signal without inversion outFunc.Set(signal) } } -// Enable enables or disables the PWM output for all channels on this timer. +// Enable enables or disables this PWM timer. When disabled (paused), the timer +// stops counting and all channels using this timer will hold their current state. func (pwm *PWM) Enable(enable bool) { timerConf := pwm.timerConf() if enable { @@ -450,6 +482,18 @@ func (pwm *PWM) Enable(enable bool) { } } +// SetCounter sets the counter value of this PWM timer. This can be used to +// synchronize multiple PWM timers. +func (pwm *PWM) SetCounter(value uint32) { + // The counter is reset by setting the RST bit, then writing the value + // Note: ESP32 LEDC doesn't support directly writing counter value, + // so we reset to 0 instead. This method is provided for API compatibility. + timerConf := pwm.timerConf() + conf := timerConf.Get() + timerConf.Set(conf | timerRstMask) + timerConf.Set(conf) +} + // Register access helpers // timerConf returns the configuration register for this timer. From 2d3b1b22384ab7bded23f7ad3a6902ad6743ca70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 20 Jan 2026 16:16:08 +0000 Subject: [PATCH 8/9] machine/esp32: third pass improvements for PWM Additional improvements: - Add pwmTimerCount constant and use it for pwmStates array - Use pwmChannelCount constant for pwmChannels array declaration - Add IsEnabled() method to check if timer is running - Add Frequency() method to get current PWM frequency in Hz - Add Resolution() method to get current duty resolution in bits - Add GetPin() method to get pin assigned to a channel - Rename SetCounter() to ResetCounter() (ESP32 only supports reset to 0) - Fix Period() to use dividerFracBits constant instead of magic 256 - Remove misleading 'Idle level low' comment in Channel() Co-authored-by: Ona --- src/machine/machine_esp32_pwm.go | 60 +++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index 5284868adc..d038fbfe56 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -32,8 +32,14 @@ type pwmChannelInfo struct { inUse bool // Whether this channel is currently in use } +// Number of PWM timers and channels +const ( + pwmTimerCount = 4 // Number of high-speed timers (0-3) + pwmChannelCount = 8 // Number of high-speed channels (0-7) +) + // pwmChannels tracks which pin and timer is assigned to each channel (0-7) -var pwmChannels [8]pwmChannelInfo +var pwmChannels [pwmChannelCount]pwmChannelInfo // Hardware PWM peripherals available on ESP32. // These use the high-speed LEDC timers for glitch-free PWM updates. @@ -44,9 +50,6 @@ var ( PWM3 = &PWM{num: 3} ) -// Number of available PWM channels -const pwmChannelCount = 8 - // LEDC peripheral constants const ( // Clock enable bit in DPORT.PERIP_CLK_EN for LEDC @@ -107,7 +110,7 @@ type pwmState struct { configured bool // Whether the timer has been configured } -var pwmStates [4]pwmState +var pwmStates [pwmTimerCount]pwmState // Configure enables and configures this PWM peripheral. // The period is specified in nanoseconds. A period of 0 will select a default @@ -240,7 +243,6 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { // - Enable clock // - Select this timer // - Enable signal output - // - Idle level low var conf uint32 conf |= chanClkEnMask // Enable clock conf |= uint32(pwm.num) & chanTimerSelMask // Select timer @@ -435,9 +437,27 @@ func (pwm *PWM) Period() uint64 { } // period_ns = (2^resolution * divider_reg / 256) / 80MHz * 1e9 - // period_ns = (2^resolution * divider_reg * 1000) / (80 * 256) + // period_ns = (2^resolution * divider_reg * 1000) / (80 * (1 << dividerFracBits)) resolutionValue := uint64(1) << resolution - return resolutionValue * uint64(dividerReg) * 1000 / (80 * 256) + return resolutionValue * uint64(dividerReg) * 1000 / (80 << dividerFracBits) +} + +// Frequency returns the current PWM frequency in Hz. +func (pwm *PWM) Frequency() uint32 { + period := pwm.Period() + if period == 0 { + return 0 + } + return uint32(1_000_000_000 / period) +} + +// Resolution returns the current duty cycle resolution in bits. +func (pwm *PWM) Resolution() uint8 { + resolution := pwmStates[pwm.num].resolution + if resolution == 0 { + return defaultResolution + } + return resolution } // SetInverting sets whether to invert the output of this channel. @@ -482,18 +502,32 @@ func (pwm *PWM) Enable(enable bool) { } } -// SetCounter sets the counter value of this PWM timer. This can be used to +// IsEnabled returns true if this PWM timer is running (not paused). +func (pwm *PWM) IsEnabled() bool { + return (pwm.timerConf().Get() & timerPauseMask) == 0 +} + +// ResetCounter resets the timer counter to 0. This can be used to // synchronize multiple PWM timers. -func (pwm *PWM) SetCounter(value uint32) { - // The counter is reset by setting the RST bit, then writing the value - // Note: ESP32 LEDC doesn't support directly writing counter value, - // so we reset to 0 instead. This method is provided for API compatibility. +func (pwm *PWM) ResetCounter() { timerConf := pwm.timerConf() conf := timerConf.Get() timerConf.Set(conf | timerRstMask) timerConf.Set(conf) } +// GetPin returns the pin assigned to the given channel, or NoPin if the +// channel is not in use by this PWM peripheral. +func (pwm *PWM) GetPin(channel uint8) Pin { + if !pwm.isValidChannel(channel) { + return NoPin + } + if !pwmChannels[channel].inUse || pwmChannels[channel].timer != pwm.num { + return NoPin + } + return pwmChannels[channel].pin +} + // Register access helpers // timerConf returns the configuration register for this timer. From 39b6261379e9ff1dc9a2024f44bfbe7b568ba2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 20 Jan 2026 16:21:14 +0000 Subject: [PATCH 9/9] machine/esp32: fourth pass improvements for PWM Final improvements: - Add apbClockMHz constant (80) instead of hardcoded values - Add timerRegisterStride and channelRegisterStride constants - Add SetFrequency() method for convenience - Add ChannelCount() method to query available channels - Optimize Channel() to use single-pass loop instead of three loops - Add error documentation to Channel() function comment - Simplify register access helper comments Co-authored-by: Ona --- src/machine/machine_esp32_pwm.go | 163 +++++++++++++++++-------------- 1 file changed, 92 insertions(+), 71 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index d038fbfe56..9b43ea56fd 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -61,6 +61,9 @@ const ( // GPIO matrix output inversion bit gpioMatrixInvertBit = 1 << 9 + // APB clock frequency in MHz (used by high-speed LEDC) + apbClockMHz = 80 + // Clock divider fractional bits (register value = actual_divider * 256) dividerFracBits = 8 @@ -73,6 +76,10 @@ const ( // Default values defaultPeriodNs = 1_000_000 // 1ms = 1kHz, good for LEDs defaultResolution = 13 // 13-bit resolution for default period + + // Register offsets + timerRegisterStride = 0x8 // Bytes between timer registers + channelRegisterStride = 0x14 // Bytes between channel registers ) // LEDC register bit positions and masks for timer configuration @@ -174,7 +181,7 @@ func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { resolutionValue := uint64(1) << resolution // Calculate divider register value (includes 8 fractional bits) - dividerReg := (period * 80 * (1 << dividerFracBits)) / (resolutionValue * 1000) + dividerReg := (period * apbClockMHz * (1 << dividerFracBits)) / (resolutionValue * 1000) lastDividerReg = dividerReg if dividerReg < minDivider { @@ -197,6 +204,9 @@ func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { // Channel returns a PWM channel for the given pin. If the pin is already // configured for this PWM peripheral, the same channel is returned. // The pin is configured for PWM output. +// +// Returns ErrInvalidOutputPin if the pin is already used by a different timer +// or if no channels are available. func (pwm *PWM) Channel(pin Pin) (uint8, error) { if !pwmStates[pwm.num].configured { // Timer not configured, configure with default period @@ -205,70 +215,73 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { } } - // Check if this pin is already assigned to a channel on THIS timer + // Single pass: find existing assignment, check for conflicts, find free channel + var freeChannel int8 = -1 for ch := uint8(0); ch < pwmChannelCount; ch++ { - if pwmChannels[ch].inUse && - pwmChannels[ch].pin == pin && - pwmChannels[ch].timer == pwm.num { - return ch, nil + if !pwmChannels[ch].inUse { + if freeChannel < 0 { + freeChannel = int8(ch) + } + continue } - } - // Check if pin is used by a different timer (error case) - for ch := uint8(0); ch < pwmChannelCount; ch++ { - if pwmChannels[ch].inUse && pwmChannels[ch].pin == pin { - // Pin is already used by a different timer + if pwmChannels[ch].pin == pin { + if pwmChannels[ch].timer == pwm.num { + // Already assigned to this timer + return ch, nil + } + // Pin is used by a different timer return 0, ErrInvalidOutputPin } } - // Find an available channel - for ch := uint8(0); ch < pwmChannelCount; ch++ { - if !pwmChannels[ch].inUse { - // Found an available channel - pwmChannels[ch] = pwmChannelInfo{ - pin: pin, - timer: pwm.num, - inUse: true, - } + // No existing assignment found, use free channel if available + if freeChannel < 0 { + return 0, ErrInvalidOutputPin + } - // Configure the GPIO for PWM output through GPIO matrix - signal := uint32(ledcHSSignalBase + ch) - pin.configure(PinConfig{Mode: PinOutput}, signal) - - // Configure the channel - chanConf0 := pwm.channelConf0(ch) - - // Set channel configuration: - // - Enable clock - // - Select this timer - // - Enable signal output - var conf uint32 - conf |= chanClkEnMask // Enable clock - conf |= uint32(pwm.num) & chanTimerSelMask // Select timer - conf |= chanSigOutEnMask // Enable output - chanConf0.Set(conf) - - // Initialize duty to 0 - pwm.channelDuty(ch).Set(0) - - // Set HPOINT to 0 (start of cycle) - pwm.channelHpoint(ch).Set(0) - - // Configure CONF1 for non-fading operation and trigger duty update - // duty_scale=0, duty_cycle=1, duty_num=1, duty_inc=1, duty_start=1 - var conf1 uint32 - conf1 |= 1 << chanDutyCyclePos // duty_cycle = 1 - conf1 |= 1 << chanDutyNumPos // duty_num = 1 - conf1 |= chanDutyIncMask // duty_inc = 1 - conf1 |= chanDutyStartMask // duty_start = 1 - pwm.channelConf1(ch).Set(conf1) - - return ch, nil - } + ch := uint8(freeChannel) + + // Configure the new channel + pwmChannels[ch] = pwmChannelInfo{ + pin: pin, + timer: pwm.num, + inUse: true, } - return 0, ErrInvalidOutputPin + // Configure the GPIO for PWM output through GPIO matrix + signal := uint32(ledcHSSignalBase + ch) + pin.configure(PinConfig{Mode: PinOutput}, signal) + + // Configure the channel + chanConf0 := pwm.channelConf0(ch) + + // Set channel configuration: + // - Enable clock + // - Select this timer + // - Enable signal output + var conf uint32 + conf |= chanClkEnMask // Enable clock + conf |= uint32(pwm.num) & chanTimerSelMask // Select timer + conf |= chanSigOutEnMask // Enable output + chanConf0.Set(conf) + + // Initialize duty to 0 + pwm.channelDuty(ch).Set(0) + + // Set HPOINT to 0 (start of cycle) + pwm.channelHpoint(ch).Set(0) + + // Configure CONF1 for non-fading operation and trigger duty update + // duty_scale=0, duty_cycle=1, duty_num=1, duty_inc=1, duty_start=1 + var conf1 uint32 + conf1 |= 1 << chanDutyCyclePos // duty_cycle = 1 + conf1 |= 1 << chanDutyNumPos // duty_num = 1 + conf1 |= chanDutyIncMask // duty_inc = 1 + conf1 |= chanDutyStartMask // duty_start = 1 + pwm.channelConf1(ch).Set(conf1) + + return ch, nil } // ReleaseChannel releases a PWM channel, making it available for other uses. @@ -437,9 +450,9 @@ func (pwm *PWM) Period() uint64 { } // period_ns = (2^resolution * divider_reg / 256) / 80MHz * 1e9 - // period_ns = (2^resolution * divider_reg * 1000) / (80 * (1 << dividerFracBits)) + // period_ns = (2^resolution * divider_reg * 1000) / (apbClockMHz * (1 << dividerFracBits)) resolutionValue := uint64(1) << resolution - return resolutionValue * uint64(dividerReg) * 1000 / (80 << dividerFracBits) + return resolutionValue * uint64(dividerReg) * 1000 / (apbClockMHz << dividerFracBits) } // Frequency returns the current PWM frequency in Hz. @@ -451,6 +464,15 @@ func (pwm *PWM) Frequency() uint32 { return uint32(1_000_000_000 / period) } +// SetFrequency sets the PWM frequency in Hz. +// This is a convenience method equivalent to SetPeriod(1e9 / frequency). +func (pwm *PWM) SetFrequency(frequency uint32) error { + if frequency == 0 { + return ErrPWMPeriodTooLong + } + return pwm.SetPeriod(1_000_000_000 / uint64(frequency)) +} + // Resolution returns the current duty cycle resolution in bits. func (pwm *PWM) Resolution() uint8 { resolution := pwmStates[pwm.num].resolution @@ -460,6 +482,12 @@ func (pwm *PWM) Resolution() uint8 { return resolution } +// ChannelCount returns the number of channels available for this PWM peripheral. +// Note: Channels are shared across all PWM timers on ESP32. +func (pwm *PWM) ChannelCount() uint8 { + return pwmChannelCount +} + // SetInverting sets whether to invert the output of this channel. // Without inverting, a 25% duty cycle would mean the output is high for 25% of // the time and low for the rest. Inverting flips the output as if a NOT gate @@ -532,49 +560,42 @@ func (pwm *PWM) GetPin(channel uint8) Pin { // timerConf returns the configuration register for this timer. func (pwm *PWM) timerConf() *volatile.Register32 { - // HSTIMER0_CONF is at offset 0x140, each timer is 0x8 bytes apart base := uintptr(unsafe.Pointer(&esp.LEDC.HSTIMER0_CONF)) - return (*volatile.Register32)(unsafe.Pointer(base + uintptr(pwm.num)*0x8)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(pwm.num)*timerRegisterStride)) } // timerValue returns the value register for this timer. func (pwm *PWM) timerValue() *volatile.Register32 { - // HSTIMER0_VALUE is at offset 0x144, each timer is 0x8 bytes apart base := uintptr(unsafe.Pointer(&esp.LEDC.HSTIMER0_VALUE)) - return (*volatile.Register32)(unsafe.Pointer(base + uintptr(pwm.num)*0x8)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(pwm.num)*timerRegisterStride)) } // channelConf0 returns the CONF0 register for the given channel. func (pwm *PWM) channelConf0(ch uint8) *volatile.Register32 { - // HSCH0_CONF0 is at offset 0x0, each channel is 0x14 bytes apart base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_CONF0)) - return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*channelRegisterStride)) } // channelHpoint returns the HPOINT register for the given channel. func (pwm *PWM) channelHpoint(ch uint8) *volatile.Register32 { - // HSCH0_HPOINT is at offset 0x4, each channel is 0x14 bytes apart base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_HPOINT)) - return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*channelRegisterStride)) } // channelDuty returns the DUTY register for the given channel. func (pwm *PWM) channelDuty(ch uint8) *volatile.Register32 { - // HSCH0_DUTY is at offset 0x8, each channel is 0x14 bytes apart base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_DUTY)) - return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*channelRegisterStride)) } // channelConf1 returns the CONF1 register for the given channel. func (pwm *PWM) channelConf1(ch uint8) *volatile.Register32 { - // HSCH0_CONF1 is at offset 0xC, each channel is 0x14 bytes apart base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_CONF1)) - return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*channelRegisterStride)) } // channelDutyR returns the DUTY_R (read) register for the given channel. func (pwm *PWM) channelDutyR(ch uint8) *volatile.Register32 { - // HSCH0_DUTY_R is at offset 0x10, each channel is 0x14 bytes apart base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_DUTY_R)) - return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*channelRegisterStride)) }