PWM — pymcu.hal.pwm#
from pymcu.hal.pwm import PWM
Hardware pulse-width modulation. Wraps the Timer/Counter OC channels on AVR.
class PWM#
PWM(pin: str, duty: uint8, freq: uint16 = 0, invert: const[uint8] = 0)#
Configures hardware PWM on the given pin. duty is 8-bit (0 = 0%, 255 = 100%); duty_u16
is the 16-bit entry every compatibility layer speaks, and it wins over duty. freq is
optional; 0 leaves the timer at its default prescaler.
A Timer1 channel honours the frequency exactly. PB1 and PB2 asking for a frequency
that is not one of the eight-bit buckets reach mode 14, where the period lives in a register
rather than being fixed at 256 counts. 50 Hz then really is 50 Hz, and the duty has 40 000
steps of 0.5 µs instead of 256 of 64 µs. That is what makes the servo idiom work: a servo
asked for 50 Hz used to run at 61 and had about 16 steps of angle.
Timer0 and Timer2 still pick the nearest bucket, because their period is fixed at
256 counts and there is nothing else to give: asking PD6 for 1000 Hz gets 976.6 Hz.
frequency() reports the rate the pin actually emits, on either path. It is not the request:
every layer above used to report the request, so PWMOut(board.D6, frequency=5000) emitted
7812 Hz and said 5000.
A PWM at an exact frequency cannot be retuned at run time: its period is a register computed
from the frequency, and so is every duty measured against it, so set_freq() is refused
there with a message naming the five frequencies that can be retuned.
Supported pins (ATmega328P)#
Pin |
Arduino |
Timer channel |
Note |
|---|---|---|---|
|
D6 |
Timer0 OC0A |
Fast PWM, 8-bit |
|
D5 |
Timer0 OC0B |
Fast PWM, 8-bit |
|
D9 |
Timer1 OC1A |
8-bit at a bucket frequency; 16-bit at any other, period in ICR1 |
|
D10 |
Timer1 OC1B |
8-bit at a bucket frequency; 16-bit at any other, period in ICR1 |
|
D11 |
Timer2 OC2A |
Fast PWM, 8-bit |
|
D3 |
Timer2 OC2B |
Fast PWM, 8-bit |
Methods#
Method |
Description |
|---|---|
|
The rate the pin actually emits, which is not always the one asked for |
|
Put the compare output back on the pin (a duty of 0 stays off) |
|
Take the compare output off the pin and drive it low; the timer keeps running for its other channel and for the time base |
|
|
|
Update the 8-bit duty while running (0 off, 255 fully on, else high for duty + 1 of 256 counts) |
|
Update the 16-bit duty, 0..65535 = 0..100 %, exact to the channel’s resolution: 32768 is 50.0 %, 65535 fully on, below half a count is off |
|
Select the prescaler closest to |
Two duty entries, one exact#
PWM(pin, duty) and set_duty() take the 8-bit duty the HAL always had. PWM(pin, duty_u16=...) and set_duty_u16() take the 16-bit one every architecture’s HAL shares
(0..65535 = 0..100 %, what CircuitPython’s duty_cycle and MicroPython’s duty_u16 mean),
and each chip module resolves it to its own compare register. On the AVR 8-bit channels the
value becomes round(duty * 256 / 65535) counts high and the compare register holds one less,
because fast PWM is high for OCR + 1 counts. Measured before this existed: every 16-bit duty
came out 1/256 above what was asked, 50.4 % for 32768 on an Arduino Uno. The compat layers
use only the 16-bit entry and know nothing about the resolution behind it.
Timer0 is also the time base#
On the ATmega parts PD5 and PD6 (Arduino D5 and D6) are the two channels of Timer0, and
Timer0’s overflow is what millis(), ticks_ms(), time.monotonic() and asyncio count.
The time base runs it at prescaler 64, so while it is in the program (the build injects
millis_init() for those calls, or the sources call it) the only frequency available on
those two pins is the 976 Hz bucket, which is also the default. Any other request there
is refused where it is written:
error: CompileError: PWM: PD5/PD6 (Arduino D5/D6) share Timer0 with the millisecond time
base (millis, ticks_ms, monotonic, asyncio), which fixes its prescaler at 64 ...
Use PD3/PB3 (D3/D11, Timer2) or PB1/PB2 (D9/D10, Timer1) for that frequency. Without the
time base every bucket stays available on Timer0. Measured before this rule existed: a
5000 Hz PWM on D6 made monotonic() run 8.44 times too fast on an Arduino Uno.
Inverting output#
invert=1 selects the inverting compare output mode (AVR: COMxn1:COMxn0 = 11). The pin is
set on compare match and cleared at BOTTOM, so duty counts the LOW time: duty=64 is 75 %
high. set_duty(0) still disconnects the compare output and drives the pin low; a non-zero
duty reconnects it inverting. The argument is a compile-time constant and costs nothing when
it is 0; machine.PWM(pin, invert=1) lowers to it.
Reachable frequencies#
An AVR timer does not run at an arbitrary frequency — the prescaler picks from a handful of buckets. At 16 MHz, Timer0 and Timer1 reach 61, 244, 976, 7812 and 62500 Hz; Timer2 also has /32 and /128, so it adds 488 and 1953 Hz.
set_freq(freq) chooses the nearest reachable bucket, comparing against the geometric
midpoints between them. Asking for 440 Hz on Timer2 lands on 488 Hz rather than dropping to
244 Hz — which is what keeps tone() melodies recognisably in tune. Read back through the
compat layer (machine.PWM.freq()) and you get the value you asked for, not the bucket; if
you need the physical frequency, derive it from __FREQ__ and the prescaler.
Duty at the extremes#
duty = 255 is 100% because fast PWM with the compare register at MAX holds the output
constantly high. duty = 0 is not 0% for the same reason in reverse: the output is set at
BOTTOM and cleared on compare match, so a compare register of 0 would leave a
one-prescaled-clock pulse in every 256 – about 0.4%, enough to keep an LED visibly lit
(ATmega328P section 15.7.3, “Fast PWM Mode”).
So duty = 0 does not write 0 to the compare register. It clears the channel’s COM bits in
TCCRxA, which returns the pin to normal port operation, and drives it low; the next
non-zero duty writes the compare register and sets the COM bits back. The pin stays an
output throughout. A duty that is a compile-time constant folds to one path or the other
with no runtime branch; a duty computed at run time costs one compare.
Two channels on one timer#
The channels of a single timer (D5+D6 on Timer0, D9+D10 on Timer1, D11+D3 on Timer2) can
run at the same time: configuring the second OR-s its COM bits into TCCRxA instead of
overwriting the register, so the first channel keeps its output. They do share the
prescaler, so they cannot have different frequencies.
Example#
LED fade#
from pymcu.hal.pwm import PWM
from pymcu.time import delay_ms
from pymcu.types import uint8
def main():
pwm = PWM("PD6", duty=0) # Timer0 OC0A (D6)
pwm.start()
duty: uint8 = 0
while True:
pwm.set_duty(duty)
duty += 5
delay_ms(20)
Servo control (50 Hz)#
from pymcu.hal.pwm import PWM
from pymcu.types import uint8
# Timer1 OC1A gives 16-bit resolution; set OCR1A directly for precision
from pymcu.types import ptr, uint16
OCR1A: ptr[uint16] = ptr(0x88)
pwm = PWM("PB1", duty=0)
pwm.start()
# 50 Hz servo: 1–2 ms pulse out of 20 ms period
# Prescaler 8, ICR1=40000 (20 ms), OCR1A=2000 (1 ms) to 4000 (2 ms)
OCR1A.value = 3000 # center position