PicoCat Electronics

Learn about the parts that make up the PicoCat

This detailed guide describes the all the steps required to wire-up your own PicoCat.


SG90 Servos

PicoCat uses 11 x SG90 servos. An alternative design made for MG90s Servos will also be created to enable a more powerful, stronger version of the cat.

SG90 Servo

You can grab the SG90 servo’s from our shop here.


PCA9685

The Servos connect to a PCA9685 board, which is powered by the main battery. The PCA9685 board also connects to the Raspberry Pi Pico.

PicoCat wiring diagram

You can grab the PCA9685 board from our shop here.


Raspberry Pi Pico

The brain of the PicoCat is the Raspberry Pi Pico. It connects to the PCA9685 Board, and uses a MicroPython library to enable easy communication with the board. The source code for the MicroPython PCA9685 library can be found here: https://github.com/kevinmcaleer/pca9685_for_pico.

An outline of a Raspberry Pi Pico

Servo.py

Below is the servo.py code. This is used with the PCA9685.py library, which is below the servo code listing.

# servo.py
# Kevin McAleer
# March 2021

from pca import PCA9685
import math


class Servos:
    def __init__(self, i2c, address=0x40, freq=50, min_us=600, max_us=2400,
                 degrees=180):
        self.period = 1000000 / freq
        self.min_duty = self._us2duty(min_us)
        self.max_duty = self._us2duty(max_us)
        self.degrees = degrees
        self.freq = freq
        self.pca9685 = PCA9685(i2c, address)
        self.pca9685.freq(freq)

    def _us2duty(self, value):
        return int(4095 * value / self.period)

    def position(self, index, degrees=None, radians=None, us=None, duty=None):
        span = self.max_duty - self.min_duty
        if degrees is not None:
            duty = self.min_duty + span * degrees / self.degrees
        elif radians is not None:
            duty = self.min_duty + span * radians / math.radians(self.degrees)
        elif us is not None:
            duty = self._us2duty(us)
        elif duty is not None:
            pass
        else:
            return self.pca9685.duty(index)
        duty = min(self.max_duty, max(self.min_duty, int(duty)))
        self.pca9685.duty(index, duty)

    def release(self, index):
        self.pca9685.duty(index, 0)

PCA9685.py

# pca9685.py
# Kevin McAleer
# March 2021
'''
@author Kevin McAleer
'''

import ustruct
import time


class PCA9685:
    """
    This class models the PCA9685 board, used to control up to 16
    servos, using just 2 wires for control over the I2C interface
    """
    def __init__(self, i2c, address=0x40):
        """
        class constructor

        Args:
            i2c ([I2C Class, from the build in machine library]): This is used to 
            bring in the i2c object, which can be created by 
            > i2c = I2C(id=0, sda=Pin(0), scl=Pin(1))
            address (hexadecimal, optional): [description]. Defaults to 0x40.
        """
        self.i2c = i2c
        self.address = address
        self.reset()

    def _write(self, address, value):
        self.i2c.writeto_mem(self.address, address, bytearray([value]))

    def _read(self, address):
        return self.i2c.readfrom_mem(self.address, address, 1)[0]

    def reset(self):
        self._write(0x00, 0x00) # Mode1

    def freq(self, freq=None):
        if freq is None:
            return int(25000000.0 / 4096 / (self._read(0xfe) - 0.5))
        prescale = int(25000000.0 / 4096.0 / freq + 0.5)
        old_mode = self._read(0x00) # Mode 1
        self._write(0x00, (old_mode & 0x7F) | 0x10) # Mode 1, sleep
        self._write(0xfe, prescale) # Prescale
        self._write(0x00, old_mode) # Mode 1
        time.sleep_us(5)
        self._write(0x00, old_mode | 0xa1) # Mode 1, autoincrement on

    def pwm(self, index, on=None, off=None):
        if on is None or off is None:
            data = self.i2c.readfrom_mem(self.address, 0x06 + 4 * index, 4)
            return ustruct.unpack('<HH', data)
        data = ustruct.pack('<HH', on, off)
        self.i2c.writeto_mem(self.address, 0x06 + 4 * index,  data)

    def duty(self, index, value=None, invert=False):
        if value is None:
            pwm = self.pwm(index)
            if pwm == (0, 4096):
                value = 0
            elif pwm == (4096, 0):
                value = 4095
            value = pwm[1]
            if invert:
                value = 4095 - value
            return value
        if not 0 <= value <= 4095:
            raise ValueError("Out of range")
        if invert:
            value = 4095 - value
        if value == 0:
            self.pwm(index, 0, 4096)
        elif value == 4095:
            self.pwm(index, 4096, 0)
        else:
            self.pwm(index, 0, value)

Demo.py

This is a demo program that uses both the servo.py and pca9685.py library to bring it all together.

from pca9685 import PCA9685
from machine import I2C, Pin
from servo import Servos

sda = Pin(0)
scl = Pin(1)
id = 0
i2c = I2C(id=id, sda=sda, scl=scl)

pca = PCA9685(i2c=i2c)
servo = Servos(i2c=i2c)
servo.position(index=0, degrees=0)

Range Finder

The PicoCat uses the HC-SR04 Range Finder to detect distances. There is a 3.3v version of the Range Finder - HC-SR04P, which works without the need for a voltage divider.

Range Finder

For more information about how to setup the 5v version of the range finder for use with the 3.3v Pico, watch this section of the video.

You can grab the 5v version of the range finder here.