0
# PWM and Pulse Control
1
2
Pulse Width Modulation output and pulse measurement capabilities for motor control, servo positioning, and signal generation. Provides CircuitPython-compatible PWM and pulse measurement operations with automatic platform detection and driver loading.
3
4
## Capabilities
5
6
### PWM Output
7
8
The PWMOut class provides pulse width modulation output for controlling motors, servos, LEDs, and other devices that respond to duty cycle control. Automatically detects and uses appropriate platform-specific drivers.
9
10
```python { .api }
11
class PWMOut(ContextManaged):
12
def __init__(self, pin, *, duty_cycle: int = 0, frequency: int = 500, variable_frequency: bool = False):
13
"""
14
Initialize PWM output pin.
15
16
Args:
17
pin: Board PWM-capable pin object (from board module)
18
duty_cycle: Initial PWM duty cycle (0-65535, default 0)
19
frequency: PWM frequency in Hz (default 500)
20
variable_frequency: Enable variable frequency support (not implemented)
21
22
Raises:
23
RuntimeError: If no PWM channel found for the pin
24
ValueError: If PWM channel does not exist or modules not loaded
25
"""
26
27
@property
28
def duty_cycle(self) -> int:
29
"""
30
Get or set PWM duty cycle.
31
32
Returns:
33
int: Current duty cycle (0-65535, where 65535 = 100%)
34
"""
35
36
@duty_cycle.setter
37
def duty_cycle(self, value: int) -> None:
38
"""
39
Set PWM duty cycle.
40
41
Args:
42
value: Duty cycle value (0-65535)
43
44
Raises:
45
TypeError: If value is not int or float
46
ValueError: If value is outside 0.0-1.0 range (internally converted)
47
"""
48
49
@property
50
def frequency(self) -> float:
51
"""
52
Get or set PWM frequency in Hz.
53
54
Returns:
55
float: Current PWM frequency
56
"""
57
58
@frequency.setter
59
def frequency(self, value: float) -> None:
60
"""
61
Set PWM frequency.
62
63
Args:
64
value: Frequency in Hz
65
66
Raises:
67
TypeError: If value is not int or float
68
"""
69
70
@property
71
def period(self) -> float:
72
"""
73
Get or set PWM period in seconds.
74
75
Returns:
76
float: Current PWM period (1/frequency)
77
"""
78
79
@period.setter
80
def period(self, value: float) -> None:
81
"""
82
Set PWM period.
83
84
Args:
85
value: Period in seconds
86
87
Raises:
88
TypeError: If value is not int or float
89
"""
90
91
def deinit(self) -> None:
92
"""Release PWM resources and disable output"""
93
```
94
95
### Pulse Input Measurement
96
97
The PulseIn class provides pulse width measurement for reading PWM signals, IR remote controls, and other pulse-based communications. Limited platform support.
98
99
```python { .api }
100
class PulseIn(ContextManaged):
101
def __init__(self, pin, maxlen: int = 2, idle_state: bool = False):
102
"""
103
Initialize pulse input measurement.
104
105
Args:
106
pin: Board pin object for pulse measurement
107
maxlen: Maximum number of pulses to store (default 2)
108
idle_state: Expected idle state - False for low, True for high
109
110
Raises:
111
RuntimeError: If platform not supported or setup failed
112
"""
113
114
@property
115
def maxlen(self) -> int:
116
"""
117
Maximum number of pulses stored.
118
119
Returns:
120
int: Maximum pulse buffer length
121
"""
122
123
@property
124
def paused(self) -> bool:
125
"""
126
True if pulse capture is paused.
127
128
Returns:
129
bool: Paused state
130
"""
131
132
def __len__(self) -> int:
133
"""
134
Number of pulses currently captured.
135
136
Returns:
137
int: Current number of stored pulses
138
"""
139
140
def __getitem__(self, index: int) -> int:
141
"""
142
Get pulse duration by index.
143
144
Args:
145
index: Pulse index (0 = oldest)
146
147
Returns:
148
int: Pulse duration in microseconds
149
150
Raises:
151
IndexError: If index is out of range
152
"""
153
154
def clear(self) -> None:
155
"""Clear all captured pulses"""
156
157
def popleft(self) -> int:
158
"""
159
Remove and return oldest pulse.
160
161
Returns:
162
int: Duration of oldest pulse in microseconds
163
164
Raises:
165
IndexError: If no pulses available
166
"""
167
168
def pause(self) -> None:
169
"""Pause pulse capture"""
170
171
def resume(self, trigger_duration: int = 0) -> None:
172
"""
173
Resume pulse capture.
174
175
Args:
176
trigger_duration: Optional trigger pulse duration in microseconds
177
"""
178
179
def deinit(self) -> None:
180
"""Release pulse measurement resources"""
181
```
182
183
## Usage Examples
184
185
### Basic PWM LED Control
186
187
```python
188
import board
189
import pwmio
190
import time
191
192
# Create PWM output
193
led = pwmio.PWMOut(board.D18, frequency=1000, duty_cycle=0)
194
195
# Fade in
196
for i in range(100):
197
led.duty_cycle = int(i * 655.35) # 0 to 65535
198
time.sleep(0.01)
199
200
# Fade out
201
for i in range(100, 0, -1):
202
led.duty_cycle = int(i * 655.35)
203
time.sleep(0.01)
204
205
# Cleanup
206
led.deinit()
207
```
208
209
### Servo Motor Control
210
211
```python
212
import board
213
import pwmio
214
import time
215
216
# Standard servo control (50Hz, 1-2ms pulse width)
217
servo = pwmio.PWMOut(board.D18, frequency=50)
218
219
def set_servo_angle(servo, angle):
220
"""Set servo angle (0-180 degrees)"""
221
# Convert angle to duty cycle
222
# 1ms = 5% duty cycle, 2ms = 10% duty cycle at 50Hz
223
min_duty = int(0.05 * 65535) # 1ms pulse
224
max_duty = int(0.10 * 65535) # 2ms pulse
225
duty_range = max_duty - min_duty
226
227
duty_cycle = min_duty + int((angle / 180.0) * duty_range)
228
servo.duty_cycle = duty_cycle
229
230
# Sweep servo back and forth
231
for _ in range(3):
232
# Move to 0 degrees
233
set_servo_angle(servo, 0)
234
time.sleep(1)
235
236
# Move to 90 degrees
237
set_servo_angle(servo, 90)
238
time.sleep(1)
239
240
# Move to 180 degrees
241
set_servo_angle(servo, 180)
242
time.sleep(1)
243
244
servo.deinit()
245
```
246
247
### PWM Motor Speed Control
248
249
```python
250
import board
251
import pwmio
252
import digitalio
253
import time
254
255
# Motor control with PWM speed and direction
256
motor_pwm = pwmio.PWMOut(board.D18, frequency=1000)
257
motor_dir = digitalio.DigitalInOut(board.D19)
258
motor_dir.direction = digitalio.Direction.OUTPUT
259
260
def set_motor_speed(pwm, direction, speed_percent):
261
"""Set motor speed (-100 to +100 percent)"""
262
if speed_percent < 0:
263
direction.value = False # Reverse
264
speed_percent = -speed_percent
265
else:
266
direction.value = True # Forward
267
268
# Convert percentage to duty cycle
269
duty_cycle = int((speed_percent / 100.0) * 65535)
270
pwm.duty_cycle = duty_cycle
271
272
# Motor control demo
273
speeds = [25, 50, 75, 100, 0, -25, -50, -75, -100, 0]
274
275
for speed in speeds:
276
set_motor_speed(motor_pwm, motor_dir, speed)
277
print(f"Motor speed: {speed}%")
278
time.sleep(2)
279
280
# Cleanup
281
motor_pwm.deinit()
282
motor_dir.deinit()
283
```
284
285
### Variable Duty Cycle with Context Manager
286
287
```python
288
import board
289
import pwmio
290
import time
291
import math
292
293
# Using context manager for automatic cleanup
294
with pwmio.PWMOut(board.D18, frequency=500) as pwm:
295
# Generate sine wave pattern
296
for i in range(360):
297
# Convert angle to sine wave (0-1 range)
298
sine_val = (math.sin(math.radians(i)) + 1) / 2
299
300
# Convert to duty cycle
301
pwm.duty_cycle = int(sine_val * 65535)
302
303
time.sleep(0.01) # ~10Hz update rate
304
```
305
306
### Pulse Input Measurement
307
308
```python
309
# Note: PulseIn has limited platform support (Raspberry Pi, some Odroid boards)
310
import board
311
import pulseio
312
import time
313
314
try:
315
# Initialize pulse measurement
316
pulse_in = pulseio.PulseIn(board.D2, maxlen=10, idle_state=False)
317
318
print("Measuring pulses... Press Ctrl+C to stop")
319
320
while True:
321
# Wait for pulses
322
while len(pulse_in) == 0:
323
time.sleep(0.01)
324
325
# Read and display pulses
326
while len(pulse_in) > 0:
327
pulse_duration = pulse_in.popleft()
328
print(f"Pulse: {pulse_duration} microseconds")
329
330
time.sleep(0.1)
331
332
except KeyboardInterrupt:
333
print("Stopping pulse measurement")
334
except RuntimeError as e:
335
print(f"PulseIn not supported on this platform: {e}")
336
finally:
337
try:
338
pulse_in.deinit()
339
except:
340
pass
341
```
342
343
### PWM Frequency Sweep
344
345
```python
346
import board
347
import pwmio
348
import time
349
350
# Create PWM with variable frequency
351
pwm = pwmio.PWMOut(board.D18, duty_cycle=32768) # 50% duty cycle
352
353
# Sweep frequency from 100Hz to 2000Hz
354
frequencies = [100, 200, 500, 1000, 1500, 2000]
355
356
for freq in frequencies:
357
pwm.frequency = freq
358
print(f"PWM frequency: {freq} Hz")
359
time.sleep(2)
360
361
# Reset to default
362
pwm.frequency = 500
363
pwm.duty_cycle = 0
364
365
pwm.deinit()
366
```
367
368
## Platform Considerations
369
370
### PWM Support
371
372
**Widely Supported Platforms:**
373
- **Raspberry Pi**: Hardware PWM with lgpio (Pi 5) or RPi.GPIO (Pi 4 and earlier)
374
- **Linux SBCs**: Generic sysfs PWM interface (BeagleBone, Banana Pi, Odroid, etc.)
375
- **Specialized Hardware**: Binho Nova, GreatFET One, FTDI adapters
376
- **RP2040 Boards**: Native PWM support via U2IF protocol
377
378
**Platform-Specific Notes:**
379
- **BeagleBone**: Custom sysfs implementation for AM335x SoCs
380
- **Jetson Boards**: Tegra-specific PWM drivers
381
- **Rock Pi**: Rockchip SoC PWM support
382
- **Siemens IoT2000**: AM65xx PWM implementation
383
384
### Pulse Input Limitations
385
386
**Supported Platforms:**
387
- **Raspberry Pi**: All models with libgpiod-based implementation
388
- **Odroid C4/N2**: Amlogic G12 SoC support
389
390
**Requirements:**
391
- libgpiod library must be installed
392
- Requires compiled helper binaries (libgpiod_pulsein64/libgpiod_pulsein)
393
- Uses inter-process communication via System V message queues
394
395
### Performance Notes
396
397
- **PWM Duty Cycle**: Hardware PWM provides precise timing
398
- **Software PWM**: May have timing variations under CPU load
399
- **Frequency Limits**: Platform-dependent, typically 1Hz to several kHz
400
- **Resolution**: 16-bit duty cycle resolution (0-65535)
401
402
### Error Handling
403
404
```python
405
import board
406
import pwmio
407
408
try:
409
pwm = pwmio.PWMOut(board.D18, frequency=1000)
410
pwm.duty_cycle = 32768 # 50%
411
# Use PWM...
412
pwm.deinit()
413
except RuntimeError as e:
414
if "PWM channel" in str(e):
415
print(f"PWM not available on this pin: {e}")
416
else:
417
print(f"PWM error: {e}")
418
except ValueError as e:
419
print(f"PWM configuration error: {e}")
420
```