Servo Shield

The Servo Shield drives up to eight hobby servos in parallel from the OpenMV Cam over I2C, using a PCA9685 servo / PWM controller.

Servo Shield

For full datasheet, photos, and ordering see the Servo Shield product page.

Highlights

  • PCA9685 servo / PWM controller

  • Eight independent servo channels over I2C

  • Stacks with the Motor Shield and Pan and Tilt Shield

Pinout

Servo Shield Pinout

Pin reference

Pin

Function

P4

I²C SCL — clock to the PCA9685

P5

I²C SDA — data to the PCA9685

VIN rail

Powers the servos (from the camera’s VIN pin)

3.3V rail

Powers the PCA9685 logic

GND rail

Servo and camera common ground

The default I²C address is 0x40. Connect the on-board solder bridge to move the address to 0x60.

Note

The shield draws servo power straight from the camera’s VIN pin. USB does not feed VIN on any OpenMV Cam, so VIN must be supplied externally (battery, bench supply, or similar) — pick a source rated for the combined stall current of every servo you plan to drive.

Usage

Drive the eight servo channels through the PCA9685 over I²C. The pulse-width range varies between servos, so tune MIN_US and MAX_US to match yours — typical values are around 1000–2000 µs:

import time
from machine import SoftI2C, Pin


class PCA9685:
    """Minimal PCA9685 driver — 12-bit PWM on any of 8 channels."""

    def __init__(self, bus, address=0x40, freq=50):
        self._bus = bus
        self._addr = address
        bus.writeto_mem(address, 0x00, b"\x00")            # reset Mode1
        prescale = round(25_000_000 / (4096 * freq)) - 1
        bus.writeto_mem(address, 0x00, b"\x10")            # sleep
        bus.writeto_mem(address, 0xFE, bytes([prescale]))  # prescale
        bus.writeto_mem(address, 0x00, b"\x00")            # wake
        time.sleep_us(5)
        bus.writeto_mem(address, 0x00, b"\xA1")            # restart + AI + allcall
        self._period_us = 1_000_000 // freq

    def set_duty(self, channel, duty):
        duty &= 0xFFF                                      # 12-bit
        if duty == 0:
            on, off = 0, 0x1000                            # FULL_OFF
        elif duty == 0xFFF:
            on, off = 0x1000, 0                            # FULL_ON
        else:
            on, off = 0, duty
        self._bus.writeto_mem(
            self._addr, 0x06 + 4 * channel,
            bytes([on & 0xFF, on >> 8, off & 0xFF, off >> 8]))

    def set_us(self, channel, pulse_us):
        self.set_duty(channel, (pulse_us * 4096) // self._period_us)


MIN_US = 1000  # full-left pulse width (microseconds)
MAX_US = 2000  # full-right pulse width

bus = SoftI2C(scl=Pin("P4"), sda=Pin("P5"))
pca = PCA9685(bus, address=0x40, freq=50)


def angle(channel, deg):
    pca.set_us(channel, MIN_US + (deg * (MAX_US - MIN_US)) // 180)


while True:
    for ch in range(8):
        angle(ch, 0)
    time.sleep_ms(2000)
    for ch in range(8):
        angle(ch, 180)
    time.sleep_ms(2000)

The PCA9685 also handles general 12-bit PWM at any frequency — reuse the same class with set_duty (0–4095) to, for example, fade an LED on channel 0 at 1 kHz. The helper below scales a 0.0–100.0% float onto the chip’s 0–4095 duty range:

import time
from machine import SoftI2C, Pin

bus = SoftI2C(scl=Pin("P4"), sda=Pin("P5"))
pca = PCA9685(bus, address=0x40, freq=1000)


def brightness(channel, pct):
    pca.set_duty(channel, int(pct * 4095 / 100))


while True:
    # Ramp up 0 → 100%.
    for pct in range(101):
        brightness(0, float(pct))
        time.sleep_ms(20)
    # Ramp down 100 → 0%.
    for pct in reversed(range(101)):
        brightness(0, float(pct))
        time.sleep_ms(20)