Skip to content

PWM subsystem (subsys = 4)

PWM generation on the user header. The RP2350 has 12 PWM slices × 2 channels; this subsystem exposes six of those channels on the 2×10 breakout (GP30, GP31, GP37, GP38, GP39, GP40). The status-LED channels (GP22/23/24) are PWM-driven too but reserved for the firmware — they are not addressable via this subsystem.

Duty is a Q0.16 fraction (duty_q16 ∈ [0, 65535]): 0 = 0 %, 0xFFFF = 100 %, 0x8000 ≈ 50 %.

Frequency is a positive integer in Hz. The firmware picks the smallest achievable clock divider so the counter wrap has the highest possible resolution; hosts can read back the actually applied frequency via GET_STATE to cope with quantisation.

Opcode Name Description
0x00 CONFIGURE pin + freq + duty + flags; starts the slice.
0x01 SET_DUTY Update duty on a pin already configured.
0x02 START Resume the slice that owns pin.
0x03 STOP Freeze the slice (output goes to idle).
0x04 GET_STATE Query applied freq/duty/running/slice info.

0x00 — CONFIGURE

Args (8 B): [pin][flags][duty_q16 LE u16][freq_hz LE u32].

Flags:

Bit Name Meaning
0 INVERT Invert output polarity at the GPIO override

Returns ENOENT if pin is not on the PWM whitelist, EINVAL on zero / unreachable frequency, EBUSY if another pin on the same slice is already configured with a different frequency (slices share their wrap register).

0x01 — SET_DUTY

Args (3 B): [pin][duty_q16 LE u16].

Returns ENOENT if the pin has not been configured.

0x02 / 0x03 — START / STOP

Args (1 B): [pin]. Enables or disables the PWM slice that owns pin. STOP preserves the configured parameters; START resumes with the same freq and duty. When two pins share a slice, both are affected.

0x04 — GET_STATE

Args (1 B): [pin].

Reply body (10 B):

[0..3]   freq_hz_actual   LE u32
[4..5]   duty_q16         LE u16
[6]      running          (0 | 1)
[7]      invert           (0 | 1)
[8]      slice            (0..11)
[9]      channel          (0 = A, 1 = B)

Capability advertising

buses.pwm[0] = { idx: 0, max_freq: 62500000, pins: [30, 31, 37, 38, 39, 40] }
features[] += "pwm.invert"

Linux userland

Each user PWM pin maps to a /sys/class/pwm/pwmchip<N>/pwmX node via the standard pwm-chip driver abstraction. The Rust driver registers one chip per SPI-style bank so pwm_get(&dev, 0) resolves to GP30, pwm_get(&dev, 1) to GP31, etc.