CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-adafruit-blinka

CircuitPython APIs for non-CircuitPython versions of Python such as CPython on Linux and MicroPython.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

pwm-pulse.mddocs/

PWM and Pulse Control

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.

Capabilities

PWM Output

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.

class PWMOut(ContextManaged):
    def __init__(self, pin, *, duty_cycle: int = 0, frequency: int = 500, variable_frequency: bool = False):
        """
        Initialize PWM output pin.
        
        Args:
            pin: Board PWM-capable pin object (from board module)
            duty_cycle: Initial PWM duty cycle (0-65535, default 0)
            frequency: PWM frequency in Hz (default 500)
            variable_frequency: Enable variable frequency support (not implemented)
        
        Raises:
            RuntimeError: If no PWM channel found for the pin
            ValueError: If PWM channel does not exist or modules not loaded
        """
    
    @property
    def duty_cycle(self) -> int:
        """
        Get or set PWM duty cycle.
        
        Returns:
            int: Current duty cycle (0-65535, where 65535 = 100%)
        """
    
    @duty_cycle.setter
    def duty_cycle(self, value: int) -> None:
        """
        Set PWM duty cycle.
        
        Args:
            value: Duty cycle value (0-65535)
        
        Raises:
            TypeError: If value is not int or float
            ValueError: If value is outside 0.0-1.0 range (internally converted)
        """
    
    @property
    def frequency(self) -> float:
        """
        Get or set PWM frequency in Hz.
        
        Returns:
            float: Current PWM frequency
        """
    
    @frequency.setter
    def frequency(self, value: float) -> None:
        """
        Set PWM frequency.
        
        Args:
            value: Frequency in Hz
        
        Raises:
            TypeError: If value is not int or float
        """
    
    @property
    def period(self) -> float:
        """
        Get or set PWM period in seconds.
        
        Returns:
            float: Current PWM period (1/frequency)
        """
    
    @period.setter
    def period(self, value: float) -> None:
        """
        Set PWM period.
        
        Args:
            value: Period in seconds
        
        Raises:
            TypeError: If value is not int or float
        """
    
    def deinit(self) -> None:
        """Release PWM resources and disable output"""

Pulse Input Measurement

The PulseIn class provides pulse width measurement for reading PWM signals, IR remote controls, and other pulse-based communications. Limited platform support.

class PulseIn(ContextManaged):
    def __init__(self, pin, maxlen: int = 2, idle_state: bool = False):
        """
        Initialize pulse input measurement.
        
        Args:
            pin: Board pin object for pulse measurement
            maxlen: Maximum number of pulses to store (default 2)
            idle_state: Expected idle state - False for low, True for high
        
        Raises:
            RuntimeError: If platform not supported or setup failed
        """
    
    @property
    def maxlen(self) -> int:
        """
        Maximum number of pulses stored.
        
        Returns:
            int: Maximum pulse buffer length
        """
    
    @property
    def paused(self) -> bool:
        """
        True if pulse capture is paused.
        
        Returns:
            bool: Paused state
        """
    
    def __len__(self) -> int:
        """
        Number of pulses currently captured.
        
        Returns:
            int: Current number of stored pulses
        """
    
    def __getitem__(self, index: int) -> int:
        """
        Get pulse duration by index.
        
        Args:
            index: Pulse index (0 = oldest)
        
        Returns:
            int: Pulse duration in microseconds
        
        Raises:
            IndexError: If index is out of range
        """
    
    def clear(self) -> None:
        """Clear all captured pulses"""
    
    def popleft(self) -> int:
        """
        Remove and return oldest pulse.
        
        Returns:
            int: Duration of oldest pulse in microseconds
        
        Raises:
            IndexError: If no pulses available
        """
    
    def pause(self) -> None:
        """Pause pulse capture"""
    
    def resume(self, trigger_duration: int = 0) -> None:
        """
        Resume pulse capture.
        
        Args:
            trigger_duration: Optional trigger pulse duration in microseconds
        """
    
    def deinit(self) -> None:
        """Release pulse measurement resources"""

Usage Examples

Basic PWM LED Control

import board
import pwmio
import time

# Create PWM output
led = pwmio.PWMOut(board.D18, frequency=1000, duty_cycle=0)

# Fade in
for i in range(100):
    led.duty_cycle = int(i * 655.35)  # 0 to 65535
    time.sleep(0.01)

# Fade out
for i in range(100, 0, -1):
    led.duty_cycle = int(i * 655.35)
    time.sleep(0.01)

# Cleanup
led.deinit()

Servo Motor Control

import board
import pwmio
import time

# Standard servo control (50Hz, 1-2ms pulse width)
servo = pwmio.PWMOut(board.D18, frequency=50)

def set_servo_angle(servo, angle):
    """Set servo angle (0-180 degrees)"""
    # Convert angle to duty cycle
    # 1ms = 5% duty cycle, 2ms = 10% duty cycle at 50Hz
    min_duty = int(0.05 * 65535)  # 1ms pulse
    max_duty = int(0.10 * 65535)  # 2ms pulse
    duty_range = max_duty - min_duty
    
    duty_cycle = min_duty + int((angle / 180.0) * duty_range)
    servo.duty_cycle = duty_cycle

# Sweep servo back and forth
for _ in range(3):
    # Move to 0 degrees
    set_servo_angle(servo, 0)
    time.sleep(1)
    
    # Move to 90 degrees
    set_servo_angle(servo, 90)
    time.sleep(1)
    
    # Move to 180 degrees
    set_servo_angle(servo, 180)
    time.sleep(1)

servo.deinit()

PWM Motor Speed Control

import board
import pwmio
import digitalio
import time

# Motor control with PWM speed and direction
motor_pwm = pwmio.PWMOut(board.D18, frequency=1000)
motor_dir = digitalio.DigitalInOut(board.D19)
motor_dir.direction = digitalio.Direction.OUTPUT

def set_motor_speed(pwm, direction, speed_percent):
    """Set motor speed (-100 to +100 percent)"""
    if speed_percent < 0:
        direction.value = False  # Reverse
        speed_percent = -speed_percent
    else:
        direction.value = True   # Forward
    
    # Convert percentage to duty cycle
    duty_cycle = int((speed_percent / 100.0) * 65535)
    pwm.duty_cycle = duty_cycle

# Motor control demo
speeds = [25, 50, 75, 100, 0, -25, -50, -75, -100, 0]

for speed in speeds:
    set_motor_speed(motor_pwm, motor_dir, speed)
    print(f"Motor speed: {speed}%")
    time.sleep(2)

# Cleanup
motor_pwm.deinit()
motor_dir.deinit()

Variable Duty Cycle with Context Manager

import board
import pwmio
import time
import math

# Using context manager for automatic cleanup
with pwmio.PWMOut(board.D18, frequency=500) as pwm:
    # Generate sine wave pattern
    for i in range(360):
        # Convert angle to sine wave (0-1 range)
        sine_val = (math.sin(math.radians(i)) + 1) / 2
        
        # Convert to duty cycle
        pwm.duty_cycle = int(sine_val * 65535)
        
        time.sleep(0.01)  # ~10Hz update rate

Pulse Input Measurement

# Note: PulseIn has limited platform support (Raspberry Pi, some Odroid boards)
import board
import pulseio
import time

try:
    # Initialize pulse measurement
    pulse_in = pulseio.PulseIn(board.D2, maxlen=10, idle_state=False)
    
    print("Measuring pulses... Press Ctrl+C to stop")
    
    while True:
        # Wait for pulses
        while len(pulse_in) == 0:
            time.sleep(0.01)
        
        # Read and display pulses
        while len(pulse_in) > 0:
            pulse_duration = pulse_in.popleft()
            print(f"Pulse: {pulse_duration} microseconds")
        
        time.sleep(0.1)

except KeyboardInterrupt:
    print("Stopping pulse measurement")
except RuntimeError as e:
    print(f"PulseIn not supported on this platform: {e}")
finally:
    try:
        pulse_in.deinit()
    except:
        pass

PWM Frequency Sweep

import board
import pwmio
import time

# Create PWM with variable frequency
pwm = pwmio.PWMOut(board.D18, duty_cycle=32768)  # 50% duty cycle

# Sweep frequency from 100Hz to 2000Hz
frequencies = [100, 200, 500, 1000, 1500, 2000]

for freq in frequencies:
    pwm.frequency = freq
    print(f"PWM frequency: {freq} Hz")
    time.sleep(2)

# Reset to default
pwm.frequency = 500
pwm.duty_cycle = 0

pwm.deinit()

Platform Considerations

PWM Support

Widely Supported Platforms:

  • Raspberry Pi: Hardware PWM with lgpio (Pi 5) or RPi.GPIO (Pi 4 and earlier)
  • Linux SBCs: Generic sysfs PWM interface (BeagleBone, Banana Pi, Odroid, etc.)
  • Specialized Hardware: Binho Nova, GreatFET One, FTDI adapters
  • RP2040 Boards: Native PWM support via U2IF protocol

Platform-Specific Notes:

  • BeagleBone: Custom sysfs implementation for AM335x SoCs
  • Jetson Boards: Tegra-specific PWM drivers
  • Rock Pi: Rockchip SoC PWM support
  • Siemens IoT2000: AM65xx PWM implementation

Pulse Input Limitations

Supported Platforms:

  • Raspberry Pi: All models with libgpiod-based implementation
  • Odroid C4/N2: Amlogic G12 SoC support

Requirements:

  • libgpiod library must be installed
  • Requires compiled helper binaries (libgpiod_pulsein64/libgpiod_pulsein)
  • Uses inter-process communication via System V message queues

Performance Notes

  • PWM Duty Cycle: Hardware PWM provides precise timing
  • Software PWM: May have timing variations under CPU load
  • Frequency Limits: Platform-dependent, typically 1Hz to several kHz
  • Resolution: 16-bit duty cycle resolution (0-65535)

Error Handling

import board
import pwmio

try:
    pwm = pwmio.PWMOut(board.D18, frequency=1000)
    pwm.duty_cycle = 32768  # 50%
    # Use PWM...
    pwm.deinit()
except RuntimeError as e:
    if "PWM channel" in str(e):
        print(f"PWM not available on this pin: {e}")
    else:
        print(f"PWM error: {e}")
except ValueError as e:
    print(f"PWM configuration error: {e}")

Install with Tessl CLI

npx tessl i tessl/pypi-adafruit-blinka

docs

analog-io.md

bitbangio.md

board-pins.md

communication.md

core-framework.md

digital-io.md

index.md

peripherals.md

pwm-pulse.md

utilities.md

tile.json