Programming IoT Devices

Controlling Actuators

Learn to control LEDs, servos, DC motors, relays, OLED displays, and buzzers from MicroPython and Arduino — the output side of IoT systems.

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 blinking

PWM 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)  # Purple

Servo 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)   # Center

DC 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 minute

OLED 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 call show() 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.