0
# Specialized Peripherals
1
2
Support for advanced peripherals including NeoPixel LED strips, rotary encoders, keypads, and USB HID devices. Provides CircuitPython-compatible interfaces for complex input/output devices with platform-specific optimizations.
3
4
## Capabilities
5
6
### NeoPixel LED Strip Control
7
8
Precision-timed writing support for WS2812-style addressable RGB LED strips. Provides bit-banged timing control for accurate color data transmission.
9
10
```python { .api }
11
def neopixel_write(gpio, buf: bytes) -> None:
12
"""
13
Write RGB color buffer to NeoPixel LED strip.
14
15
Args:
16
gpio: DigitalInOut pin object configured as output
17
buf: Color data buffer (3 bytes per LED: G, R, B order)
18
19
Note:
20
Each LED requires 3 bytes in GRB (Green, Red, Blue) order.
21
Buffer length must be multiple of 3.
22
Timing is critical - interrupts may cause color corruption.
23
"""
24
```
25
26
### Rotary Encoder Position Tracking
27
28
Incremental encoder support for reading rotary position changes. Tracks quadrature encoder signals to provide accurate position feedback.
29
30
```python { .api }
31
class IncrementalEncoder(ContextManaged):
32
def __init__(self, pin_a, pin_b):
33
"""
34
Initialize rotary encoder tracking.
35
36
Args:
37
pin_a: First encoder signal pin (A channel)
38
pin_b: Second encoder signal pin (B channel, 90° phase shifted)
39
40
Note:
41
Uses interrupt-based tracking for accurate position counting.
42
Encoder should have pull-up resistors on both signal lines.
43
"""
44
45
@property
46
def position(self) -> int:
47
"""
48
Current encoder position.
49
50
Returns:
51
int: Position count (positive for clockwise, negative for counter-clockwise)
52
"""
53
54
def deinit(self) -> None:
55
"""Release encoder resources and disable tracking"""
56
```
57
58
### Keypad Scanning
59
60
Comprehensive key scanning support for individual keys, key matrices, and shift register-based keypads. Provides debounced input with event queuing.
61
62
```python { .api }
63
class Event:
64
def __init__(self, key_number: int = 0, pressed: bool = True):
65
"""
66
Key transition event.
67
68
Args:
69
key_number: Identifier for the key (0-based index)
70
pressed: True for key press, False for key release
71
"""
72
73
@property
74
def key_number(self) -> int:
75
"""Key number that generated this event"""
76
77
@property
78
def pressed(self) -> bool:
79
"""True if key was pressed, False if released"""
80
81
@property
82
def released(self) -> bool:
83
"""True if key was released, False if pressed"""
84
85
class EventQueue:
86
def get(self) -> Event:
87
"""
88
Get next key event from queue.
89
90
Returns:
91
Event: Next key transition event, or None if queue empty
92
"""
93
94
def get_into(self, event: Event) -> bool:
95
"""
96
Store next event in provided Event object.
97
98
Args:
99
event: Event object to populate
100
101
Returns:
102
bool: True if event was available and stored
103
"""
104
105
def clear(self) -> None:
106
"""Clear all queued events"""
107
108
def __len__(self) -> int:
109
"""Number of events in queue"""
110
111
def __bool__(self) -> bool:
112
"""True if queue has events"""
113
114
@property
115
def overflowed(self) -> bool:
116
"""True if events were dropped due to full queue"""
117
118
class Keys(ContextManaged):
119
def __init__(
120
self,
121
pins,
122
*,
123
value_when_pressed: bool,
124
pull: bool = True,
125
interval: float = 0.02,
126
max_events: int = 64
127
):
128
"""
129
Individual key scanner.
130
131
Args:
132
pins: Sequence of pin objects for keys
133
value_when_pressed: True if pin reads high when pressed
134
pull: Enable internal pull resistors
135
interval: Scan interval in seconds (default 0.02 = 20ms)
136
max_events: Maximum events to queue
137
"""
138
139
@property
140
def events(self) -> EventQueue:
141
"""Event queue for key transitions (read-only)"""
142
143
@property
144
def key_count(self) -> int:
145
"""Number of keys being scanned (read-only)"""
146
147
def reset(self) -> None:
148
"""Reset scanner state - treats all keys as released"""
149
150
def deinit(self) -> None:
151
"""Stop scanning and release pins"""
152
153
class KeyMatrix(ContextManaged):
154
def __init__(
155
self,
156
row_pins,
157
column_pins,
158
columns_to_anodes: bool = True,
159
interval: float = 0.02,
160
max_events: int = 64
161
):
162
"""
163
Key matrix scanner.
164
165
Args:
166
row_pins: Sequence of row pin objects
167
column_pins: Sequence of column pin objects
168
columns_to_anodes: True if diode anodes connect to columns
169
interval: Scan interval in seconds
170
max_events: Maximum events to queue
171
172
Note:
173
Key number = row * len(column_pins) + column
174
"""
175
176
@property
177
def events(self) -> EventQueue:
178
"""Event queue for key transitions (read-only)"""
179
180
@property
181
def key_count(self) -> int:
182
"""Total number of keys in matrix (rows × columns)"""
183
184
def reset(self) -> None:
185
"""Reset scanner state"""
186
187
def deinit(self) -> None:
188
"""Stop scanning and release pins"""
189
190
class ShiftRegisterKeys(ContextManaged):
191
def __init__(
192
self,
193
*,
194
clock,
195
data,
196
latch,
197
value_to_latch: bool = True,
198
key_count: int,
199
value_when_pressed: bool,
200
interval: float = 0.02,
201
max_events: int = 64
202
):
203
"""
204
Shift register key scanner (74HC165, CD4021).
205
206
Args:
207
clock: Clock pin for shift register
208
data: Serial data input pin
209
latch: Latch pin for parallel data capture
210
value_to_latch: True if data latched on high, False on low
211
key_count: Number of keys to read
212
value_when_pressed: True if key reads high when pressed
213
interval: Scan interval in seconds
214
max_events: Maximum events to queue
215
"""
216
217
@property
218
def events(self) -> EventQueue:
219
"""Event queue for key transitions (read-only)"""
220
221
@property
222
def key_count(self) -> int:
223
"""Number of keys being scanned"""
224
225
def reset(self) -> None:
226
"""Reset scanner state"""
227
228
def deinit(self) -> None:
229
"""Stop scanning and release pins"""
230
```
231
232
### USB HID Device Emulation
233
234
USB Human Interface Device emulation for creating keyboards, mice, and custom HID devices. Uses Linux USB gadget framework for device presentation.
235
236
```python { .api }
237
class Device:
238
# Pre-defined device types
239
KEYBOARD: Device # Standard keyboard
240
MOUSE: Device # Standard mouse
241
CONSUMER_CONTROL: Device # Media control device
242
BOOT_KEYBOARD: Device # BIOS-compatible keyboard
243
BOOT_MOUSE: Device # BIOS-compatible mouse
244
245
def __init__(
246
self,
247
*,
248
descriptor: bytes,
249
usage_page: int,
250
usage: int,
251
report_ids: tuple[int, ...],
252
in_report_lengths: tuple[int, ...],
253
out_report_lengths: tuple[int, ...]
254
):
255
"""
256
Create custom HID device.
257
258
Args:
259
descriptor: HID report descriptor bytes
260
usage_page: HID usage page
261
usage: HID usage within page
262
report_ids: Tuple of report ID numbers
263
in_report_lengths: Input report sizes for each report ID
264
out_report_lengths: Output report sizes for each report ID
265
"""
266
267
def send_report(self, report: bytes, report_id: int = None) -> None:
268
"""
269
Send HID input report to host.
270
271
Args:
272
report: Report data to send
273
report_id: Report ID (optional if device has single report ID)
274
"""
275
276
def get_last_received_report(self, report_id: int = None) -> bytes:
277
"""
278
Get last received HID output report.
279
280
Args:
281
report_id: Report ID to check (optional)
282
283
Returns:
284
bytes: Last received report data, or None if none received
285
"""
286
287
def enable(devices: tuple[Device, ...], boot_device: int = 0) -> None:
288
"""
289
Enable USB HID with specified devices.
290
291
Args:
292
devices: Tuple of Device objects to enable
293
boot_device: Boot device type (0=none, 1=keyboard, 2=mouse)
294
295
Note:
296
Must be called before USB connection. Requires dwc2 and libcomposite
297
kernel modules. Creates /dev/hidg* device files for communication.
298
"""
299
300
def disable() -> None:
301
"""Disable all USB HID devices"""
302
```
303
304
## Usage Examples
305
306
### NeoPixel LED Strip
307
308
```python
309
import board
310
import digitalio
311
import neopixel_write
312
import time
313
314
# Setup NeoPixel data pin
315
pixel_pin = digitalio.DigitalInOut(board.D18)
316
pixel_pin.direction = digitalio.Direction.OUTPUT
317
318
# Number of LEDs
319
num_pixels = 10
320
321
def set_pixel_color(index, red, green, blue, buf):
322
"""Set color for single pixel in buffer (GRB order)"""
323
offset = index * 3
324
buf[offset] = green # Green first
325
buf[offset + 1] = red # Red second
326
buf[offset + 2] = blue # Blue third
327
328
# Create color buffer (3 bytes per pixel: GRB)
329
pixel_buffer = bytearray(num_pixels * 3)
330
331
# Rainbow effect
332
for cycle in range(100):
333
for i in range(num_pixels):
334
# Calculate rainbow colors
335
hue = (i * 256 // num_pixels + cycle * 5) % 256
336
337
# Simple HSV to RGB conversion
338
if hue < 85:
339
red, green, blue = hue * 3, 255 - hue * 3, 0
340
elif hue < 170:
341
hue -= 85
342
red, green, blue = 255 - hue * 3, 0, hue * 3
343
else:
344
hue -= 170
345
red, green, blue = 0, hue * 3, 255 - hue * 3
346
347
set_pixel_color(i, red, green, blue, pixel_buffer)
348
349
# Write to NeoPixel strip
350
neopixel_write.neopixel_write(pixel_pin, pixel_buffer)
351
time.sleep(0.05)
352
353
# Turn off all pixels
354
pixel_buffer = bytearray(num_pixels * 3) # All zeros
355
neopixel_write.neopixel_write(pixel_pin, pixel_buffer)
356
pixel_pin.deinit()
357
```
358
359
### Rotary Encoder Position Reading
360
361
```python
362
import board
363
import rotaryio
364
import time
365
366
# Initialize rotary encoder
367
encoder = rotaryio.IncrementalEncoder(board.D2, board.D3)
368
369
# Track position
370
last_position = 0
371
372
print("Rotate encoder... Press Ctrl+C to stop")
373
374
try:
375
while True:
376
position = encoder.position
377
378
if position != last_position:
379
direction = "clockwise" if position > last_position else "counter-clockwise"
380
print(f"Position: {position} ({direction})")
381
last_position = position
382
383
time.sleep(0.01)
384
385
except KeyboardInterrupt:
386
print("Stopping encoder reading")
387
388
encoder.deinit()
389
```
390
391
### Individual Key Scanning
392
393
```python
394
import board
395
import keypad
396
import time
397
398
# Setup keys on pins D2, D3, D4
399
keys = keypad.Keys([board.D2, board.D3, board.D4],
400
value_when_pressed=False, # Grounded when pressed
401
pull=True) # Enable pull-ups
402
403
print("Press keys... Press Ctrl+C to stop")
404
405
try:
406
while True:
407
event = keys.events.get()
408
409
if event:
410
if event.pressed:
411
print(f"Key {event.key_number} pressed")
412
else:
413
print(f"Key {event.key_number} released")
414
415
time.sleep(0.01)
416
417
except KeyboardInterrupt:
418
print("Stopping key scanning")
419
420
keys.deinit()
421
```
422
423
### Key Matrix Scanning
424
425
```python
426
import board
427
import keypad
428
import time
429
430
# 3x3 key matrix
431
row_pins = [board.D18, board.D19, board.D20]
432
col_pins = [board.D21, board.D22, board.D23]
433
434
matrix = keypad.KeyMatrix(row_pins, col_pins)
435
436
print(f"Scanning {matrix.key_count} keys in 3x3 matrix")
437
print("Press keys... Press Ctrl+C to stop")
438
439
try:
440
while True:
441
event = matrix.events.get()
442
443
if event:
444
row = event.key_number // len(col_pins)
445
col = event.key_number % len(col_pins)
446
action = "pressed" if event.pressed else "released"
447
print(f"Key at row {row}, col {col} (#{event.key_number}) {action}")
448
449
time.sleep(0.01)
450
451
except KeyboardInterrupt:
452
print("Stopping matrix scanning")
453
454
matrix.deinit()
455
```
456
457
### Shift Register Key Scanning
458
459
```python
460
import board
461
import keypad
462
import time
463
464
# 74HC165 shift register setup
465
shift_keys = keypad.ShiftRegisterKeys(
466
clock=board.D18, # Clock pin
467
data=board.D19, # Serial data in
468
latch=board.D20, # Parallel load
469
key_count=8, # 8 keys
470
value_when_pressed=False, # Active low
471
value_to_latch=False # Latch on low (74HC165 style)
472
)
473
474
print("Scanning 8 keys via shift register")
475
print("Press keys... Press Ctrl+C to stop")
476
477
try:
478
while True:
479
event = shift_keys.events.get()
480
481
if event:
482
action = "pressed" if event.pressed else "released"
483
print(f"Shift register key {event.key_number} {action}")
484
485
time.sleep(0.01)
486
487
except KeyboardInterrupt:
488
print("Stopping shift register scanning")
489
490
shift_keys.deinit()
491
```
492
493
### USB HID Device Support
494
495
Linux-based USB Human Interface Device (HID) support for creating keyboard, mouse, and custom HID devices using the USB Gadget framework.
496
497
```python { .api }
498
class Device:
499
"""USB HID device specification"""
500
501
def __init__(
502
self,
503
*,
504
descriptor: bytes,
505
usage_page: int,
506
usage: int,
507
report_ids: Sequence[int],
508
in_report_lengths: Sequence[int],
509
out_report_lengths: Sequence[int]
510
):
511
"""
512
Create a custom HID device.
513
514
Args:
515
descriptor: HID report descriptor bytes
516
usage_page: HID usage page (e.g., 0x01 for Generic Desktop)
517
usage: HID usage ID (e.g., 0x06 for Keyboard)
518
report_ids: List of report IDs this device uses
519
in_report_lengths: List of input report lengths (device to host)
520
out_report_lengths: List of output report lengths (host to device)
521
"""
522
523
def send_report(self, report: bytearray, report_id: int = None) -> None:
524
"""
525
Send HID report to host.
526
527
Args:
528
report: Report data to send
529
report_id: Report ID (optional if device has only one report ID)
530
"""
531
532
def get_last_received_report(self, report_id: int = None) -> bytes:
533
"""
534
Get last received HID OUT or feature report.
535
536
Args:
537
report_id: Report ID to get (optional if device has only one)
538
539
Returns:
540
Last received report data, or None if nothing received
541
"""
542
543
@property
544
def last_received_report(self) -> bytes:
545
"""
546
Last received HID OUT report (deprecated, use get_last_received_report()).
547
548
Returns:
549
Last received report data, or None if nothing received
550
"""
551
552
# Pre-defined device constants
553
Device.KEYBOARD: Device # Standard USB keyboard
554
Device.MOUSE: Device # Standard USB mouse
555
556
def enable(devices: Sequence[Device]) -> None:
557
"""
558
Enable USB HID with specified devices.
559
560
Args:
561
devices: Tuple of Device objects to enable
562
563
Raises:
564
Exception: If required kernel modules not loaded or setup fails
565
566
Note:
567
Must be called before USB enumeration.
568
Requires root privileges and dwc2/libcomposite kernel modules.
569
"""
570
571
def disable() -> None:
572
"""Disable USB HID gadget"""
573
```
574
575
### USB HID Keyboard Emulation
576
577
```python
578
# Note: Requires Linux with dwc2 and libcomposite kernel modules
579
# Must be run as root and called before USB enumeration
580
581
import usb_hid
582
import time
583
584
# Enable USB HID keyboard
585
usb_hid.enable((usb_hid.Device.KEYBOARD,))
586
587
# Get keyboard device
588
keyboard = usb_hid.Device.KEYBOARD
589
590
# Keyboard usage codes (simplified)
591
KEY_A = 0x04
592
KEY_ENTER = 0x28
593
KEY_SPACE = 0x2C
594
595
def send_key(key_code, modifier=0):
596
"""Send a key press and release"""
597
# HID keyboard report: [modifier, reserved, key1, key2, key3, key4, key5, key6]
598
report = bytearray([modifier, 0, key_code, 0, 0, 0, 0, 0])
599
600
# Send key press
601
keyboard.send_report(report)
602
time.sleep(0.01)
603
604
# Send key release
605
report = bytearray([0, 0, 0, 0, 0, 0, 0, 0])
606
keyboard.send_report(report)
607
time.sleep(0.01)
608
609
# Type "Hello World"
610
print("Typing 'Hello World' via USB HID...")
611
612
# Type characters (this is simplified - real implementation needs full keycode mapping)
613
send_key(KEY_A) # This would need proper character-to-keycode mapping
614
time.sleep(0.5)
615
616
# Send Enter
617
send_key(KEY_ENTER)
618
619
print("USB HID keyboard demo complete")
620
```
621
622
### USB HID Mouse Emulation
623
624
```python
625
import usb_hid
626
import time
627
628
# Enable USB HID mouse
629
usb_hid.enable((usb_hid.Device.MOUSE,))
630
631
# Get mouse device
632
mouse = usb_hid.Device.MOUSE
633
634
def move_mouse(x, y, buttons=0, wheel=0):
635
"""Move mouse and/or click buttons"""
636
# HID mouse report: [buttons, x, y, wheel]
637
# Limit movement to signed 8-bit range (-127 to 127)
638
x = max(-127, min(127, x))
639
y = max(-127, min(127, y))
640
wheel = max(-127, min(127, wheel))
641
642
report = bytearray([buttons, x, y, wheel])
643
mouse.send_report(report)
644
645
# Mouse button constants
646
LEFT_BUTTON = 0x01
647
RIGHT_BUTTON = 0x02
648
MIDDLE_BUTTON = 0x04
649
650
print("USB HID mouse demo...")
651
652
# Move in square pattern
653
movements = [
654
(50, 0), # Right
655
(0, 50), # Down
656
(-50, 0), # Left
657
(0, -50), # Up
658
]
659
660
for dx, dy in movements:
661
move_mouse(dx, dy)
662
time.sleep(0.5)
663
664
# Click left button
665
move_mouse(0, 0, LEFT_BUTTON)
666
time.sleep(0.1)
667
move_mouse(0, 0, 0) # Release
668
669
print("USB HID mouse demo complete")
670
```
671
672
## Platform Considerations
673
674
### NeoPixel Support
675
676
**Supported Platforms:**
677
- **Raspberry Pi**: All models with optimized bit-banging
678
- **RP2040 via U2IF**: Pico and compatible boards
679
- **OS Agnostic**: Generic implementation
680
681
**Timing Requirements:**
682
- Critical timing for WS2812 protocol (800ns/bit)
683
- Interrupts can cause color corruption
684
- May require disabling interrupts during transmission
685
686
### Rotary Encoder Support
687
688
**Supported Platforms:**
689
- **Raspberry Pi 5**: Hardware-optimized implementation
690
- **Generic Linux**: Software-based quadrature decoding
691
692
**Implementation Notes:**
693
- Uses interrupt-based tracking for accuracy
694
- Requires pull-up resistors on encoder signals
695
- Thread-safe position tracking
696
697
### Keypad Limitations
698
699
**Debouncing:**
700
- Hardware debouncing: 20ms default scan interval
701
- Software debouncing: Built into scanning loop
702
- Configurable scan intervals (minimum ~5ms practical)
703
704
**Threading:**
705
- Background scanning thread for real-time response
706
- Thread-safe event queue access
707
- Automatic cleanup on context manager exit
708
709
### USB HID Requirements
710
711
**Linux Requirements:**
712
- dwc2 kernel module (USB Device Controller)
713
- libcomposite kernel module (USB Gadget framework)
714
- Root privileges for /sys/kernel/config access
715
- Available USB Device Controller in /sys/class/udc/
716
717
**Device Limitations:**
718
- Maximum endpoints limited by hardware
719
- Boot devices must be first USB interface
720
- Report descriptors define device capabilities
721
722
### Error Handling
723
724
```python
725
# NeoPixel error handling
726
try:
727
import neopixel_write
728
# Use NeoPixel functionality
729
except ImportError:
730
print("NeoPixel not supported on this platform")
731
732
# Rotary encoder error handling
733
try:
734
import rotaryio
735
encoder = rotaryio.IncrementalEncoder(board.D2, board.D3)
736
except RuntimeError as e:
737
print(f"Encoder not supported: {e}")
738
739
# USB HID error handling
740
try:
741
import usb_hid
742
usb_hid.enable((usb_hid.Device.KEYBOARD,))
743
except Exception as e:
744
if "dwc2" in str(e):
745
print("USB HID requires dwc2 kernel module")
746
elif "libcomposite" in str(e):
747
print("USB HID requires libcomposite kernel module")
748
else:
749
print(f"USB HID error: {e}")
750
```