Controlling Actuators
Actuators are the output side of IoT — they turn your device's decisions into physical actions. This topic covers the most common actuators you'll encounter: LEDs, servo motors, DC motors, relays, displays, and buzzers.
LED Control
Simple Digital On/Off
python
# MicroPython — LED on/off and blink patterns
from machine import Pin, PWM, Timer
import utime
# Digital control
led = Pin(2, Pin.OUT)
led.on()
utime.sleep(1)
led.off()
# Non-blocking blink using Timer (doesn't freeze main loop)
blink_timer = Timer(0)
blink_state = False
def blink_callback(t):
global blink_state
blink_state = not blink_state
led.value(blink_state)
blink_timer.init(period=500, mode=Timer.PERIODIC, callback=blink_callback)
# Main loop continues unaffected
for i in range(20):
print(f"Main loop iteration {i} (LED blinking independently)")
utime.sleep(0.3)
blink_timer.deinit() # Stop blinkingPWM Brightness Control
python
from machine import Pin, PWM
import utime, math
led_pwm = PWM(Pin(2), freq=1000)
def set_brightness(pct):
"""0–100% brightness with gamma correction for natural look."""
# Linear PWM looks non-linear to the human eye
# Gamma 2.2 correction makes it appear linear
gamma = 2.2
duty = int((pct / 100) ** gamma * 1023)
led_pwm.duty(duty)
# Breathing effect (sine wave)
t = 0
while True:
brightness = (math.sin(t) + 1) / 2 * 100 # 0–100%
set_brightness(brightness)
t += 0.05
utime.sleep_ms(20)RGB LED
python
# Common-cathode RGB LED: 3 PWM channels
from machine import Pin, PWM
class RGBLED:
def __init__(self, r_pin, g_pin, b_pin, freq=1000):
self.r = PWM(Pin(r_pin), freq=freq)
self.g = PWM(Pin(g_pin), freq=freq)
self.b = PWM(Pin(b_pin), freq=freq)
def set_color(self, r, g, b):
"""r, g, b: 0–255 each."""
self.r.duty(r * 4) # 255 * 4 = 1020 ≈ 1023 max
self.g.duty(g * 4)
self.b.duty(b * 4)
def off(self):
self.set_color(0, 0, 0)
# Predefined colors
def red(self): self.set_color(255, 0, 0)
def green(self): self.set_color(0, 255, 0)
def blue(self): self.set_color(0, 0, 255)
def white(self): self.set_color(255, 255, 255)
def yellow(self): self.set_color(255, 200, 0)
rgb = RGBLED(r_pin=25, g_pin=26, b_pin=27)
rgb.red()
utime.sleep(1)
rgb.set_color(100, 0, 200) # PurpleServo Motor
Servos are position-controlled — send them an angle and they hold it.
python
from machine import Pin, PWM
import utime
class Servo:
"""
Standard servo: 50 Hz PWM
Pulse width: 500 µs (0°) to 2500 µs (180°)
Period: 20,000 µs (50 Hz)
Duty range: 500/20000 * 1023 ≈ 26 to 2500/20000 * 1023 ≈ 128
"""
def __init__(self, pin_num, freq=50, min_us=500, max_us=2500):
self._pwm = PWM(Pin(pin_num), freq=freq)
self._min_us = min_us
self._max_us = max_us
self._period_us = 1_000_000 // freq
self.set_angle(90) # Center on init
def set_angle(self, degrees):
"""Set position: 0 to 180 degrees."""
degrees = max(0, min(180, degrees))
pulse_us = self._min_us + (self._max_us - self._min_us) * degrees / 180
duty = int(pulse_us / self._period_us * 1023)
self._pwm.duty(duty)
def sweep(self, start=0, end=180, step=1, delay_ms=15):
"""Sweep from start to end degrees."""
for angle in range(start, end + 1, step):
self.set_angle(angle)
utime.sleep_ms(delay_ms)
for angle in range(end, start - 1, -step):
self.set_angle(angle)
utime.sleep_ms(delay_ms)
def detach(self):
"""Stop PWM signal — servo holds last position."""
self._pwm.deinit()
servo = Servo(pin_num=15)
servo.sweep(0, 180) # Full sweep
servo.set_angle(90) # CenterDC Motor with L298N H-Bridge
DC motors need an H-bridge driver (like L298N) to control direction and speed.
python
from machine import Pin, PWM
import utime
class DCMotor:
"""
L298N H-bridge motor driver.
IN1/IN2 control direction; ENA controls speed via PWM.
"""
def __init__(self, in1_pin, in2_pin, ena_pin, freq=1000):
self._in1 = Pin(in1_pin, Pin.OUT)
self._in2 = Pin(in2_pin, Pin.OUT)
self._ena = PWM(Pin(ena_pin), freq=freq)
self.stop()
def forward(self, speed=100):
"""Drive forward at speed 0–100%."""
self._in1.on()
self._in2.off()
self._set_speed(speed)
def backward(self, speed=100):
"""Drive backward at speed 0–100%."""
self._in1.off()
self._in2.on()
self._set_speed(speed)
def stop(self):
"""Coast to stop (no braking)."""
self._in1.off()
self._in2.off()
self._ena.duty(0)
def brake(self):
"""Active braking (short circuit both terminals)."""
self._in1.on()
self._in2.on()
self._ena.duty(1023)
def _set_speed(self, pct):
duty = int(max(0, min(100, pct)) / 100 * 1023)
self._ena.duty(duty)
# Two-motor robot
left = DCMotor(in1_pin=26, in2_pin=25, ena_pin=33)
right = DCMotor(in1_pin=27, in2_pin=14, ena_pin=32)
def drive_forward(speed=80):
left.forward(speed)
right.forward(speed)
def turn_left(speed=60):
left.backward(speed // 2)
right.forward(speed)
def turn_right(speed=60):
left.forward(speed)
right.backward(speed // 2)
drive_forward(80)
utime.sleep(2)
turn_left()
utime.sleep(0.5)
left.stop()
right.stop()Relay Module
Relays switch high-voltage AC loads (lights, fans, heaters) using a small DC control signal.
python
from machine import Pin
import utime
class Relay:
"""
Relay module abstraction.
Most modules are ACTIVE LOW: LOW signal = relay ON.
Set active_low=False for active-high modules.
"""
def __init__(self, pin_num, active_low=True):
self._pin = Pin(pin_num, Pin.OUT)
self._active_low = active_low
self.off() # Ensure relay is off at startup
def on(self):
self._pin.value(0 if self._active_low else 1)
def off(self):
self._pin.value(1 if self._active_low else 0)
def toggle(self):
if self._active_low:
self._pin.value(1 - self._pin.value())
else:
self._pin.value(1 - self._pin.value())
def pulse(self, duration_ms=100):
"""Turn on briefly then off — useful for momentary switches."""
self.on()
utime.sleep_ms(duration_ms)
self.off()
# 4-channel relay module
relays = [Relay(pin) for pin in [26, 27, 14, 12]]
# Smart scheduling
import utime
def is_time(hour_start, hour_end):
"""Simple hour-based schedule."""
now = utime.localtime()
current_hour = now[3]
return hour_start <= current_hour < hour_end
# Turn on a lamp from 6 PM to 11 PM
while True:
if is_time(18, 23):
relays[0].on()
print("Lamp ON (evening schedule)")
else:
relays[0].off()
utime.sleep(60) # Check every minuteOLED Display (SSD1306 via I2C)
python
from machine import I2C, Pin
import ssd1306
import utime
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400_000)
screen = ssd1306.SSD1306_I2C(128, 64, i2c, addr=0x3C)
def display_sensor_data(temp, hum, pres, uptime_s):
screen.fill(0) # Clear screen (black)
# Title bar
screen.fill_rect(0, 0, 128, 12, 1) # White rectangle
screen.text(" IoT Monitor", 0, 2, 0) # Black text on white
# Sensor readings
screen.text(f"Temp: {temp:.1f} C", 0, 16, 1)
screen.text(f"Hum: {hum:.1f} %", 0, 28, 1)
screen.text(f"Pres: {pres:.0f}hPa", 0, 40, 1)
# Status bar
mins = uptime_s // 60
screen.text(f"Up: {mins}min", 0, 54, 1)
screen.show()
start = utime.time()
while True:
uptime = utime.time() - start
display_sensor_data(24.5, 61.2, 1013, uptime)
utime.sleep(2)Buzzer — Tones and Melodies
python
from machine import Pin, PWM
import utime
# Active buzzer (just needs HIGH/LOW)
buzzer_active = Pin(18, Pin.OUT)
buzzer_active.on()
utime.sleep_ms(100)
buzzer_active.off()
# Passive buzzer (needs PWM for frequency control)
buzzer = PWM(Pin(19), freq=440) # 440 Hz = A4 note
buzzer.duty(512) # 50% duty cycle
utime.sleep_ms(300)
buzzer.deinit()
# Musical notes (Hz)
NOTES = {
'C4': 262, 'D4': 294, 'E4': 330, 'F4': 349,
'G4': 392, 'A4': 440, 'B4': 494, 'C5': 523,
'R': 0 # Rest
}
def play_melody(melody, bpm=120):
"""melody = list of (note, beats)"""
beat_ms = int(60000 / bpm)
pwm = PWM(Pin(19))
for note, beats in melody:
duration = beat_ms * beats
freq = NOTES.get(note, 0)
if freq > 0:
pwm.freq(freq)
pwm.duty(512)
else:
pwm.duty(0) # Rest
utime.sleep_ms(int(duration * 0.9)) # Note duration
pwm.duty(0)
utime.sleep_ms(int(duration * 0.1)) # Gap between notes
pwm.deinit()
# Mary Had a Little Lamb
play_melody([
('E4', 1), ('D4', 1), ('C4', 1), ('D4', 1),
('E4', 1), ('E4', 1), ('E4', 2),
('D4', 1), ('D4', 1), ('D4', 2),
('E4', 1), ('G4', 1), ('G4', 2),
], bpm=120)Summary
- LEDs: Use PWM for brightness control; use gamma correction for perceptually linear dimming
- Servos: 50 Hz PWM; pulse width 500–2500 µs maps to 0–180°; always center on init
- DC motors: Need an H-bridge driver (L298N); control direction with IN1/IN2 and speed with ENA
- Relays: Most modules are active-low; always turn off at startup; respect contact ratings for AC loads
- OLED (SSD1306): I2C on address 0x3C;
fill(0)to clear, always callshow()to update display - Passive buzzers: Require PWM for frequency control; active buzzers need only a digital HIGH signal
Next, we'll explore real-time systems — timers, interrupts, task scheduling, and async IoT patterns.