-
Notifications
You must be signed in to change notification settings - Fork 0
PWM
This document describes the hardware PWM (Pulse Width Modulation) implementation in Gopper, which provides compatibility with Klipper's pwm_out protocol for efficient analog-like control.
The PWM implementation allows Klipper to control analog outputs (fans, heaters, LEDs, etc.) using hardware PWM peripherals. This provides more efficient and precise control compared to software PWM (available through GPIO), especially for high-frequency applications.
Key Differences from GPIO Software PWM:
- Hardware PWM (this implementation): Uses dedicated PWM peripherals, higher frequency capability (up to 125 MHz), lower CPU overhead
- GPIO Software PWM (see gpio.md): Timer-based toggling, more flexible but limited to ~1kHz practical maximum
The PWM implementation follows Gopper's modular architecture:
┌─────────────────────────────────────────┐
│ Klipper Host (Python) │
│ - Sends PWM commands via protocol │
└───────────────┬─────────────────────────┘
│ USB/UART
┌───────────────▼─────────────────────────┐
│ Protocol Layer (protocol/) │
│ - VLQ encoding/decoding │
└───────────────┬─────────────────────────┘
│
┌───────────────▼─────────────────────────┐
│ Core PWM Logic (core/pwm.go) │
│ - Command handlers │
│ - HardwarePWM management │
│ - Timer-based scheduling │
│ - Max duration safety │
└───────────────┬─────────────────────────┘
│
┌───────────────▼─────────────────────────┐
│ PWM HAL (core/pwm_hal.go) │
│ - Abstract PWMDriver interface │
└───────────────┬─────────────────────────┘
│
┌───────────────▼─────────────────────────┐
│ Platform Driver (targets/rp2040/) │
│ - RP2040PWMDriver │
│ - Hardware PWM slice management │
└─────────────────────────────────────────┘
-
core/pwm_hal.go: HAL interface definition-
PWMDriverinterface for platform abstraction -
ConfigureHardwarePWM(),SetDutyCycle(),GetMaxValue(),DisablePWM()methods - Type definitions:
PWMPin,PWMValue
-
-
core/pwm.go: Main PWM implementation-
HardwarePWMstruct for managing PWM state - Command handlers:
config_pwm_out,queue_pwm_out,set_pwm_out - Timer handlers for scheduled updates and max_duration enforcement
- Safety features: max_duration monitoring, shutdown behavior
-
-
targets/rp2040/pwm.go: RP2040/RP2350 hardware PWM driver-
RP2040PWMDriverimplementation - 8 PWM slices with 2 channels each (16 total PWM outputs)
- Uses TinyGo's
machine.PWMAPI - Automatic pin-to-slice mapping
-
-
test/pwm/: Standalone PWM test program- Isolated driver testing without Klipper protocol
- Tests basic configuration, duty cycle sweeps, and continuous patterns
- Uses built-in LED for visual feedback
Configures a pin for hardware PWM output.
Format: config_pwm_out oid=%c pin=%u cycle_ticks=%u value=%hu default_value=%hu max_duration=%u
Parameters:
-
oid: Object ID (0-255) for this PWM output -
pin: Hardware pin number (must support PWM) -
cycle_ticks: PWM period in timer ticks (determines frequency) -
value: Initial duty cycle (0-255, where 0=0%, 255=100%) -
default_value: Default duty cycle for shutdown/emergency stop -
max_duration: Maximum time (in clock ticks) pin can remain in non-default state (0=unlimited)
Example: Configure GPIO25 (LED on Pico) for 1kHz PWM at 50% duty cycle
# At 12MHz timer: 1kHz = 12,000 ticks/cycle
config_pwm_out oid=1 pin=25 cycle_ticks=12000 value=127 default_value=0 max_duration=0
Notes:
- Frequency calculation:
frequency = timer_freq / cycle_ticks - RP2040 timer frequency: 12 MHz (83.3ns resolution)
- Pins sharing a PWM slice must use the same frequency
Schedules a PWM duty cycle change at a specific time.
Format: queue_pwm_out oid=%c clock=%u value=%hu
Parameters:
-
oid: Object ID of the PWM output -
clock: System clock time to execute the change -
value: New duty cycle (0-255)
Example: Set to 75% duty cycle at clock time 1000000
queue_pwm_out oid=1 clock=1000000 value=191
Notes:
- Allows precise timing control for synchronized operations
- If
max_durationis set, it's reset when value differs from default
Immediately updates the PWM duty cycle (not scheduled).
Format: set_pwm_out oid=%c value=%hu
Parameters:
-
oid: Object ID of the PWM output -
value: New duty cycle (0-255)
Example: Immediately set to 25% duty cycle
set_pwm_out oid=1 value=63
The RP2040 has 8 PWM slices (PWM0-PWM7), each with 2 channels (A and B):
PWM Slice 0: Channel A (even pins: 0, 16) Channel B (odd pins: 1, 17)
PWM Slice 1: Channel A (even pins: 2, 18) Channel B (odd pins: 3, 19)
PWM Slice 2: Channel A (even pins: 4, 20) Channel B (odd pins: 5, 21)
PWM Slice 3: Channel A (even pins: 6, 22) Channel B (odd pins: 7, 23)
PWM Slice 4: Channel A (even pins: 8, 24) Channel B (odd pins: 9, 25) ← LED
PWM Slice 5: Channel A (even pins: 10, 26) Channel B (odd pins: 11, 27)
PWM Slice 6: Channel A (even pins: 12, 28) Channel B (odd pins: 13, 29)
PWM Slice 7: Channel A (even pins: 14) Channel B (odd pins: 15)
Pin Mapping Formula:
- Slice number:
(pin >> 1) & 0x7(divide by 2, modulo 8) - Channel:
pin & 1(0=A for even pins, 1=B for odd pins)
Example: GPIO 25 (built-in LED)
- Slice:
(25 >> 1) & 0x7=12 & 0x7= 4 - Channel:
25 & 1= 1 (B) - Result: PWM Slice 4, Channel B
PWM frequency is determined by:
frequency = timer_freq / cycle_ticks
Where timer_freq = 12 MHz (configurable in core/timer.go)
Common Frequencies:
- 1 kHz (fans, LEDs):
cycle_ticks = 12000 - 10 kHz (heaters):
cycle_ticks = 1200 - 100 Hz (servos):
cycle_ticks = 120000
Hardware Limits:
- Minimum frequency: ~183 Hz (max cycle_ticks = 65535)
- Maximum frequency: 125 MHz (RP2040 system clock)
- Practical range: 100 Hz - 100 kHz
The duty cycle is specified as 0-255 (8-bit) for Klipper compatibility, but internally converted to the hardware's counter resolution:
// Convert 0-255 to hardware counter value
dutyCycle = (value * top) / 255Where top is the hardware counter maximum (varies with frequency).
- High Frequency: Up to 125 MHz (vs ~1kHz for software PWM)
- Low CPU Overhead: Hardware handles toggling
- Precise Duty Cycle: Hardware counter resolution
- Independent Operation: Runs without CPU intervention
PWM updates can be precisely scheduled:
-
queue_pwm_outschedules changes at specific clock times - Sub-millisecond precision (12MHz timer = 83.3ns resolution)
- Non-blocking operation
- Integrated with Gopper's timer system
The max_duration parameter prevents outputs from staying in non-default states indefinitely:
config_pwm_out oid=1 pin=25 cycle_ticks=12000 value=255 default_value=0 max_duration=120000
- After 120,000 ticks (~10ms @ 12MHz) at non-default value, automatically returns to default
- Must be explicitly renewed by scheduling new updates
- Provides safety for heaters, fans, and other outputs
- Zero means no timeout (runs indefinitely)
Workflow:
- Set PWM to non-default value → max_duration timer starts
- Timer expires → PWM returns to default_value
- Flag
PWM_CHECK_ENDcleared
On emergency stop or firmware shutdown:
- All PWM outputs return to their default duty cycles
- Timer scheduling is halted
- Called from
ShutdownAllHardwarePWM()
Each configured PWM output has a HardwarePWM instance:
type HardwarePWM struct {
OID uint8 // Object ID
Pin PWMPin // Hardware pin number
Flags uint8 // State flags
Timer Timer // For scheduled updates and max_duration
CycleTicks uint32 // PWM period in ticks
Value PWMValue // Current duty cycle (0-255)
DefaultValue PWMValue // Default for shutdown
MaxDuration uint32 // Max time in non-default state
EndTime uint32 // When max_duration expires
}-
PWM_CHECK_END(0x01): Monitor max_duration
Two timer handlers manage PWM operations:
-
pwmLoadEvent: Executes scheduled duty cycle updates- Applies new PWM value to hardware
- Starts max_duration monitoring if needed
- Schedules
pwmEndEventifPWM_CHECK_ENDis set
-
pwmEndEvent: Enforces max_duration- Returns PWM to default value
- Clears
PWM_CHECK_ENDflag
The RP2040 implementation manages hardware PWM slices:
type RP2040PWMDriver struct {
slices map[uint8]uint64 // Slice -> period (ns)
channels map[uint32]uint8 // Pin -> channel
peripherals map[uint8]pwmPeripheral // Slice -> PWM peripheral
}ConfigureHardwarePWM:
- Calculate slice and channel from pin number
- Convert cycle_ticks to nanoseconds
- Configure PWM peripheral with period
- Get and store channel mapping
- Return actual cycle_ticks (may be adjusted by hardware)
SetDutyCycle:
- Lookup channel for pin
- Get PWM peripheral for slice
- Scale 0-255 value to hardware counter range
- Set hardware duty cycle
getPWMPeripheral:
- Returns appropriate
machine.PWMxperipheral for slice number - RP2040 has PWM0-PWM7
[output_pin fan]
pin: gpio25
pwm: True
hardware_pwm: True
cycle_time: 0.001 # 1kHz
value: 0
shutdown_value: 0# 1. Configure PWM at 1kHz, initially off
config_pwm_out oid=1 pin=25 cycle_ticks=12000 value=0 default_value=0 max_duration=0
# 2. Set to 50% speed
set_pwm_out oid=1 value=127
# 3. Schedule ramp to 100% at future time
queue_pwm_out oid=1 clock=5000000 value=255
# 4. Turn off immediately
set_pwm_out oid=1 value=0
# Connect to MCU
~/klippy-env/bin/python ~/klipper/klippy/console.py -v /dev/ttyACM0
# Test PWM on GPIO25 (LED)
config_pwm_out oid=0 pin=25 cycle_ticks=12000 value=127 default_value=0 max_duration=0
# Observe LED at 50% brightness
# Change to 25%
set_pwm_out oid=0 value=63
# Full brightness
set_pwm_out oid=0 value=255
# Off
set_pwm_out oid=0 value=0A dedicated test program validates the PWM driver in isolation:
# Build test
make test-pwm
# Flash to Pico (hold BOOTSEL, plug USB)
cp build/pwm-test-rp2040.uf2 /media/[user]/RPI-RP2/Test Sequence:
- Basic Config (2 sec): LED at 50% brightness
- Sweep (2 sec): Gradual brighten then dim
- Continuous Patterns: Breathing, stepping, rapid blinking
See test/pwm/README.md for details.
- No external hardware needed: Uses built-in LED (GPIO25)
- Optional: Oscilloscope on GPIO25 to verify 1kHz signal
- Alternative test pin: Any GPIO with PWM capability
PWM-capable pins: GPIO 0-29 (all GPIO pins support PWM)
Common uses:
- GPIO 25: Built-in LED (PWM4B) - great for testing
- GPIO 15: Pin 20 on Pico (PWM7B)
- GPIO 16-17: Slice 0 - often used for fans
- GPIO 20-21: Slice 2 - heater control
Important: Pins sharing a PWM slice must use the same frequency:
- GPIO 0 and GPIO 1 share PWM Slice 0
- GPIO 2 and GPIO 3 share PWM Slice 1
- ... and so on
Example conflict:
# This won't work well - both on same slice!
config_pwm_out oid=0 pin=0 cycle_ticks=12000 # 1kHz
config_pwm_out oid=1 pin=1 cycle_ticks=1200 # 10kHz (reconfigures slice!)
Solution: Use pins on different slices:
config_pwm_out oid=0 pin=0 cycle_ticks=12000 # 1kHz on Slice 0
config_pwm_out oid=1 pin=2 cycle_ticks=1200 # 10kHz on Slice 1
Per configured PWM:
-
HardwarePWMstruct: ~48 bytes - Map entry overhead: ~16 bytes
- Total: ~64 bytes per PWM output
- Configuration: One-time setup cost
- Duty cycle updates: Minimal (hardware register write)
-
Scheduled updates: One timer event per
queue_pwm_out - Max duration: One timer event per timeout
- Hardware PWM: Extremely precise, no jitter
- Duty cycle resolution: Depends on frequency (higher freq = lower resolution)
- Scheduling precision: 83.3ns @ 12MHz timer
Higher PWM frequency = lower duty cycle resolution:
-
1 kHz:
top = 12000→ high resolution (256 effective levels) -
10 kHz:
top = 1200→ medium resolution (256 → ~256 levels) -
100 kHz:
top = 120→ lower resolution (256 → ~120 levels)
Check:
- Pin supports PWM (all RP2040 GPIO do)
- Correct pin number in command
- PWM driver initialized:
core.SetPWMDriver(pwmDriver) - Commands registered:
core.InitPWMCommands()
Causes:
- Multiple pins on same slice configured with different cycle_ticks
- Last configuration wins for the shared slice
Solution: Verify pin mapping and use different slices
Causes:
- PWM frequency too low (< 100 Hz for LEDs)
-
cycle_tickstoo large
Solution: Increase frequency (reduce cycle_ticks)
Check:
-
max_duration> 0 inconfig_pwm_out - Value differs from
default_value - Timer system operational
| Feature | Hardware PWM | GPIO Software PWM |
|---|---|---|
| Max Frequency | 125 MHz | ~1 kHz |
| CPU Overhead | Minimal | High at high frequencies |
| Precision | Hardware counter | Timer resolution |
| Pin Flexibility | All GPIO pins | All GPIO pins |
| Slice Sharing | Yes (same freq required) | N/A |
| Best For | High-freq, efficient | Legacy, simple control |
When to use Hardware PWM:
- Fans requiring > 1kHz frequency
- Efficient heater control
- Multiple PWM outputs
- Precise duty cycle control
When to use GPIO Software PWM:
- Frequency < 1kHz is sufficient
- Need different frequencies on adjacent pins
- Simpler configuration
Possible improvements:
- Automatic slice conflict detection
- Phase-aligned PWM for multiple outputs
- Dead-time insertion for H-bridge control
- PWM capture for frequency measurement
- Dynamic frequency adjustment