Skip to content

Conversation

@jespino
Copy link
Contributor

@jespino jespino commented Jan 19, 2026

Summary

Add PWM support for ESP32 using the LEDC (LED Control) peripheral. We are not including Motor control PWM because it would probably require a different API and the current existing LEDC should work well for leds and basic motors and servos.

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

Implementation Details

The implementation uses the ESP32's LEDC peripheral which is designed for LED dimming but works well for general PWM applications. Key characteristics:

Feature Value
Clock source APB_CLK (80MHz)
Resolution 1-20 bits (configurable)
Period range ~25ns to ~3.4 seconds
Default period 1ms (~1kHz) with 13-bit resolution

API

Follows the standard TinyGo PWM interface:

// Configure PWM with 1ms period
machine.PWM0.Configure(machine.PWMConfig{
    Period: 1000000, // 1ms in nanoseconds
})

// Get channel for GPIO2
ch, _ := machine.PWM0.Channel(machine.GPIO2)

// Set 50% duty cycle
machine.PWM0.Set(ch, machine.PWM0.Top()/2)

Files Changed

  • src/machine/machine_esp32_pwm.go - Main PWM implementation
  • src/examples/pwm/esp32.go - Example configuration for ESP32 boards

Testing

  • Syntax verified with gofmt
  • Needs testing on actual ESP32 hardware
  • Needs TinyGo compilation test

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 <no-reply@ona.com>
jespino and others added 8 commits January 20, 2026 15:38
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 <no-reply@ona.com>
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 <no-reply@ona.com>
- 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 <no-reply@ona.com>
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 <no-reply@ona.com>
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 <no-reply@ona.com>
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 <no-reply@ona.com>
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 <no-reply@ona.com>
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 <no-reply@ona.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant