Monday, February 27, 2017

Raspberry Pi and the Seeed Motor Drive Controller

Raspberry Pi Motor Board


To control the two drive motors for Alexa M we are using the Raspberry Pi Motor Board from Seeed. It is based on the Freescale MC33932 dual H-Bridge Power IC, which can control inductive loads with currents of up to 5.0A peak per single bridge. It lets you drive two DC motors with your Raspberry Pi B/B+/A+ and Pi 2/3 Model B, controlling the speed and direction of each one independently.

The Raspberry Pi Motor Driver Board v1.0 supports input voltage from 6V~28V, the on board DC/DC converter provides a 5V power supply for the Raspberry Pi with 1000mA maximum current.

Thus, you just need one power supply to drive the motors and power up the Raspberry Pi and Motor Board.


The board has the following features:

  • Operating Voltage: 6V ~ 28V
  • DC/DC output: 5V 1000mA @ "5V" pin
  • Output Current(For Each Channel ): 2A (continuous operation) / 5A(peak)
  • Output Duty Range: 0%~100%
  • Output short-circuit protection (short to VPWR or GND)
  • Over-current limiting (regulation) via internal constant-off-time PWM
  • Temperature dependant current limit threshold reduction





Wiring up the Motor Control Board


Wiring is pretty straight forward. The battery pack plugs into J1. We are using 6 x 1.5 AA batteries in the battery pack (i.e. 9V) which is sufficient to drive both motors without the Raspberry Pi browning out and rebooting.

The motors connect to J2. We connected:

  • OUT1 - left motor red
  • OUT2 - left motor black
  • OUT3 - right motor red
  • OUT4 - right motor black

The Raspberry Pi connects via the header. You need the pin details to control the motor control board and to ensure there are no conflicts with HAT's or other I/O.



The pins used by Alexa M are shown above. The blue highlighted ones are the Motor Control Board. In particular, the GPIO connected to the MC33932 pins are:

        PWMA        = GPIO 25
        PWMB        = GPIO 22
        IN1              = GPIO 23
        IN2              = GPIO 24
        IN3              = GPIO 17
        IN4              = GPIO 27

The block diagram of the MC33932 throttle control H bridge is shown below.




And the logic commands required to operate the chip are:


Luckily we don't have to write our own motor driver code as the folks at Seeed have already provided it. I have tweaked this a bit to make it Python 3 compliant and have also provided the required PiSoftPwm class as this is a bit difficult to track down.

To test your Motor Control Board, run the following:

#!/usr/bin/python
import RPi.GPIO as GPIO
import time
import signal   

from PiSoftPwm import *

#print 'Go_1...'
#frequency = 1.0 / self.sc_1.GetValue()
#speed = self.sc_2.GetValue()

class Motor():
    def __init__(self):
        # MC33932 pins
        self.PWMA = 25  
        self.PWMB = 22
        self._IN1 = 23  
        self._IN2 = 24 
        self._IN3 = 17
        self._IN4 = 27

        # Initialize PWMA PWMB 
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(self.PWMA, GPIO.OUT)
        GPIO.setup(self.PWMB, GPIO.OUT)
        GPIO.output(self.PWMA, True)
        GPIO.output(self.PWMB, True)

        # Initialize PWM outputs
        self.OUT_1  = PiSoftPwm(0.1, 100, self._IN1, GPIO.BCM)
        self.OUT_2  = PiSoftPwm(0.1, 100, self._IN2, GPIO.BCM)
        self.OUT_3  = PiSoftPwm(0.1, 100, self._IN3, GPIO.BCM)
        self.OUT_4  = PiSoftPwm(0.1, 100, self._IN4, GPIO.BCM)

            # Close pwm output
        self.OUT_1.start(0)
        self.OUT_2.start(0)
        self.OUT_3.start(0)
        self.OUT_4.start(0)

        self.frequency = 0.01
        self.duty = 60

    def Setting(self, frequency, duty):
        self.frequency = frequency
        self.duty = duty

    def Go_1(self):
        self.OUT_1.changeBaseTime(self.frequency)
        self.OUT_2.changeBaseTime(self.frequency)
        self.OUT_1.changeNbSlicesOn(self.duty)
        self.OUT_2.changeNbSlicesOn(0)

    def Back_1(self):
        self.OUT_1.changeBaseTime(self.frequency)
        self.OUT_2.changeBaseTime(self.frequency)
        self.OUT_1.changeNbSlicesOn(0)
        self.OUT_2.changeNbSlicesOn(self.duty)

    def Go_2(self):
        self.OUT_3.changeBaseTime(self.frequency)
        self.OUT_4.changeBaseTime(self.frequency)
        self.OUT_3.changeNbSlicesOn(0)
        self.OUT_4.changeNbSlicesOn(self.duty)

    def Back_2(self):
        self.OUT_3.changeBaseTime(self.frequency)
        self.OUT_4.changeBaseTime(self.frequency)
        self.OUT_3.changeNbSlicesOn(self.duty)
        self.OUT_4.changeNbSlicesOn(0)

    def Stop():
        self.OUT_1.changeNbSlicesOn(0)
        self.OUT_2.changeNbSlicesOn(0)
        self.OUT_3.changeNbSlicesOn(0)
        self.OUT_4.changeNbSlicesOn(0)

if __name__=="__main__":
    motor=Motor()
    # Called on process interruption. Set all pins to "Input" default mode.
    def endProcess(signalnum = None, handler = None):
        motor.OUT_1.stop()
        motor.OUT_2.stop()
        motor.OUT_3.stop()
        motor.OUT_4.stop()
        GPIO.cleanup()
        exit(0)

    # Prepare handlers for process exit
    signal.signal(signal.SIGTERM, endProcess)
    signal.signal(signal.SIGINT, endProcess)
    signal.signal(signal.SIGHUP, endProcess)
    signal.signal (signal.SIGQUIT, endProcess)

    motor.Setting(0.01, 60)
    print('motor start...')
    while True:
        print('turning direction...')
        motor.Go_1()
        time.sleep(1)
        motor.Back_1()
        time.sleep(1)
        motor.Go_2()
        time.sleep(1)
        motor.Back_2()
        time.sleep(1)

The PiSoftPwm class needs to be saved in the same directory as the motor test code above.

# The original is aboudou ,the Source code is here : https://goddess-gate.com/dc2/index.php/pages/raspiledmeter.en
# The modifier is ukonline2000

import RPi.GPIO as GPIO
import threading
import time
 
class PiSoftPwm(threading.Thread):

  def __init__(self, baseTime, nbSlices, gpioPin, gpioScheme):
     """ 
     Init the PiSoftPwm instance. Expected parameters are :
     - baseTime : the base time in seconds for the PWM pattern. You may choose a small value (i.e 0.01 s)
     - nbSlices : the number of divisions of the PWM pattern. A single pulse will have a min duration of baseTime * (1 / nbSlices)
     - gpioPin : the pin number which will act as PWM ouput
     - gpioScheme : the GPIO naming scheme (see RPi.GPIO documentation)
     """
     self.sliceTime = baseTime / nbSlices
     self.baseTime = baseTime
     self.nbSlices = nbSlices
     self.gpioPin = gpioPin
     self.terminated = False
     self.toTerminate = False
     GPIO.setmode(gpioScheme)

  def start(self, nbSlicesOn):
    """
    Start PWM output. Expected parameter is :
    - nbSlicesOn : number of divisions (on a total of nbSlices - see init() doc) to set HIGH output on the GPIO pin
    
    Exemple : with a total of 100 slices, a baseTime of 1 second, and an nbSlicesOn set to 25, the PWM pattern will
    have a duty cycle of 25%. With a duration of 1 second, will stay HIGH for 1*(25/100) seconds on HIGH output, and
    1*(75/100) seconds on LOW output.
    """
    self.nbSlicesOn = nbSlicesOn
    GPIO.setup(self.gpioPin, GPIO.OUT)
    self.thread = threading.Thread(None, self.run, None, (), {})
    self.thread.start()

  def run(self):
    """
    Run the PWM pattern into a background thread. This function should not be called outside of this class.
    """
    while self.toTerminate == False:
      if self.nbSlicesOn > 0:
        GPIO.output(self.gpioPin, GPIO.HIGH)
        time.sleep(self.nbSlicesOn * self.sliceTime)
      if self.nbSlicesOn < self.nbSlices:
        GPIO.output(self.gpioPin, GPIO.LOW)
        time.sleep((self.nbSlices - self.nbSlicesOn) * self.sliceTime)
    self.terminated = True

  def changeNbSlicesOn(self, nbSlicesOn):
    """
    Change the duration of HIGH output of the pattern. Expected parameter is :
    - nbSlicesOn : number of divisions (on a total of nbSlices - see init() doc) to set HIGH output on the GPIO pin
    
    Exemple : with a total of 100 slices, a baseTime of 1 second, and an nbSlicesOn set to 25, the PWM pattern will
    have a duty cycle of 25%. With a duration of 1 second, will stay HIGH for 1*(25/100) seconds on HIGH output, and
    1*(75/100) seconds on LOW output.
    """
    self.nbSlicesOn = nbSlicesOn

  def changeNbSlices(self, nbSlices):
    """
    Change the number of slices of the PWM pattern. Expected parameter is :
    - nbSlices : number of divisions of the PWM pattern.
    
    Exemple : with a total of 100 slices, a baseTime of 1 second, and an nbSlicesOn set to 25, the PWM pattern will
    have a duty cycle of 25%. With a duration of 1 second, will stay HIGH for 1*(25/100) seconds on HIGH output, and
    1*(75/100) seconds on LOW output.
    """
    if self.nbSlicesOn > nbSlices:
      self.nbSlicesOn = nbSlices

    self.nbSlices = nbSlices
    self.sliceTime = self.baseTime / self.nbSlices

  def changeBaseTime(self, baseTime):
    """
    Change the base time of the PWM pattern. Expected parameter is :
    - baseTime : the base time in seconds for the PWM pattern.
    
    Exemple : with a total of 100 slices, a baseTime of 1 second, and an nbSlicesOn set to 25, the PWM pattern will
    have a duty cycle of 25%. With a duration of 1 second, will stay HIGH for 1*(25/100) seconds on HIGH output, and
    1*(75/100) seconds on LOW output.
    """
    self.baseTime = baseTime
    self.sliceTime = self.baseTime / self.nbSlices


  def stop(self):
    """
    Stops PWM output.
    """
    self.toTerminate = True
    while self.terminated == False:
      # Just wait
      time.sleep(0.01)
  
    GPIO.output(self.gpioPin, GPIO.LOW)
    GPIO.setup(self.gpioPin, GPIO.IN)

Next up we will pull everything together in a Robot python class.

Wednesday, February 22, 2017

Installing the Raspberry Pi Camera


Installing the Camera


If you have any HATS on your Raspberry Pi, then remove them before you install the camera cable. You can put them back on afterwards. In our case we had to remove the motor driver board.

Be careful not to touch the camera PCB components, they are static sensitive. Ground yourself before you start if possible.

On the Raspberry Pi B+, 2 and 3, the camera port is between the audio port and the HDMI port. On the original Raspberry Pi B, it is between the Ethernet port and the HDMI port. To open the port, use two fingers and lift the ends up slightly. Note that there is another port on the Pi board that looks just the same, but that other one is not meant for the camera - use the one labelled camera!


The cable has to be inserted with the right orientation: the blue side needs to face the Ethernet port, and the silver side faces the HDMI port. Insert the cable so that almost no blue is showing. The photo below shows the beginning of the insertion.


To close the port and keep the cable in place, push the top of the port while holding the cable with the other hand. Give the cable a GENTLE tug to check that it is installed correctly. Note that the cable at the camera end has a similar connector but is a bit stiffer, so may require a bit more effort to open and close.

Enabling & Testing the Camera


You need to enable camera support using the raspi-config program you will have used when you first set up your Raspberry Pi. From the command line:

sudo raspi-config

Use the cursor keys to move to the camera option, and select 'enable'. On exiting  raspi-config, it will ask to reboot. The enable option will ensure that on reboot the correct GPU firmware will be running with the camera driver and tuning, and the GPU memory split is sufficient to allow the camera to acquire enough memory to run correctly.

To test that the system is installed and working, try the following command:

raspistill -v -o test.jpg
The display should show a five-second preview from the camera and then take a picture, saved to the file test.jpg, whilst displaying various log messages.

If you have SSH'ed in, then you wont see the preview. To view the jpg image you can use gpicview. Note that you will need X forwarding set up (i.e. ssh -XY on a Mac).

gpicview test.jpg
You will then see something like:


Your picture will probably be a bit more interesting! I hadn't mounted the camera at this stage. To mount the camera on Alexa M, I used one of these:


This shows the camera attached and half mounted.


Friday, February 17, 2017

Raspberry Pi and the Duinotech Servo (SG90) Pan and Tilt Bracket

Building the Duinotech Pan and Tilt Bracket (Part 2)



Back from our necessary detour to put together some python servo code, we will continue with the build of the Duinotech pan and tilt bracket. The first part of the build is available in part 1.



Assembling the Tilt Unit



  • Mount the servo so that the shaft aligns with the pivot hole and attach with the two screws as shown.



  • Fit the single-armed servo horn into the recess in the pan-base and attach it with one of the smallest screws. I had to trim the end of the servo horn to allow it to fit.



  1. Align the servo shaft from the tilt bracket with the hole in the servo horn.
  2. Snap the tilt bracket into place, using a small screwdriver as a lever to lift the pivot hole over the pivot pin on the tilt-base.



  • Make sure that the tilt bracket can move through the full 180 degrees of servo motion. If not, detach it from the servo horn, rotate and re-install.
  • Complete the assembly by securing the horn to the shaft with a short screw.
  • Use one of the shorter screws to attach the base to the servo shaft. The longer mounting screws packed with some servos will damage the servo if screwed in too far.



Testing both Servos


Using the python servo library we created earlier we can test both the pan and tilt functionality. Our pan control input is connected to GPIO 5 and tilt to GPIO 6. There is just enough 5V pins to connect two servos, moving forward we will need a 5V rail for additional sensors (like ultrasonic). At the same time we will install a ground rail to make wiring easier.

To start try testing the two servos independently, run RS_Servo and change the control pin to the relevant number. Note the min and max duty cycle for each servo, we will use these in the Robot class that we will develop next. The Robot class will be the software framework on which we will add additional functionality.

In the interim, once you have calibrated your servo's using RS_Servo, you can use the following test program to run both servo's through their paces. Note that I have set min and max duty cycles for both servo's when I initialise them.

# PT_Test.py - Test Pan and Tilt Servo's
#
# 18 February 2017

import RPi.GPIO as GPIO
from RS_Servo import Servo

# create new servo's to be controlled from GPIO pins 5 & 6
pan_servo = Servo(5, 3, 11)    
tilt_servo = Servo(6, 2, 11)

# start PWM for servo's
pan_servo.start()
tilt_servo.start()

print(pan_servo)
print(tilt_servo)
print("Servo instances: ", Servo.count())

try:
    while True:
        print("\nScanning (CTRL c to exit)...")
        pan_servo.scan()
        tilt_servo.scan()
except KeyboardInterrupt:
    print("\n-- CTRL-C: Terminating program --")
finally:
    print("Cleaning up PWM and GPIO...")
    pan_servo.cleanup()
    tilt_servo.cleanup()
    GPIO.cleanup()
    print("Done.")


Python Servo Module/Library for Raspberry Pi

Python Servo Library


We will now create a servo wrapper class which will add some convenience methods and attributes using software PWM. It will be designed so that it can be imported into other code or used by itself to calibrate or test a servo.

From here we will start putting together a Python Robot class similar to what we did for AVA in C++.

This library is based on the code we used to calibrate the sensor and still has some jitter. In due course we will look at improving the code using hardware DMA (perhaps using the pigpio library this time).

The code is well commented, so with no further ado, here it is.

# RS_Servo.py - Wrapper Servo Class for Raspberry Pi
#
# 15 February 2017 - 1.0 Original Issue
# 18 February 2017 - 1.1 Modified with @property
#
# Reefwing Software

import RPi.GPIO as GPIO
from time import sleep

# Private Attributes
__CALIBRATE      = "1"
__SET_DUTY_CYCLE = "2"
__SCAN           = "3"
__QUIT           = "q"

class Servo:
    # Servo class wrapper using RPi.GPIO PWM

    # Servo private class attribute - to count servo instances
    __number = 0

    def __init__(self, pin, min_dc=2, max_dc=10, freq=50):
        # Create a new servo instance with default pulse width limits if not provided

        # Configure the Pi to use pin names (i.e. BCM) and allocate I/O
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(pin, GPIO.OUT)

        # Create PWM channel on the servo pin with a frequency of freq - default 50Hz
        self.PWM = GPIO.PWM(pin, freq)

        # Increment Servo instances
        type(self).__number += 1     
        
        # Instance attributes
        self.pin = pin
        self.min_duty_cycle = min_dc
        self.max_duty_cycle = max_dc
        self.duty_cycle = self.get_centre(min_dc, max_dc)
        self.angle = 0

    def __str__(self):
        # Return string representation of servo
        return "Servo: pin - {0}, MIN_DC - {1}, MAX_DC - {2}, DC - {3}".format(self.pin, self.min_duty_cycle, self.max_duty_cycle, self.duty_cycle)

    def start(self):
        # Start PWM
        self.PWM.start(self.duty_cycle)

    def stop(self):
        # Stop PWM
        self.PWM.stop()

    def centre(self):
        # Move servo to the centre position
        centre = self.get_centre(self.min_duty_cycle, self.max_duty_cycle)
        self.duty_cycle = centre

    def min_dc(self):
        # Move servo to minimum duty cycle position
        self.duty_cycle = self.min_duty_cycle

    def max_dc(self):
        # Move servo to maximum duty cycle position
        self.duty_cycle = self.max_duty_cycle

    def scan(self, min_dc=None, max_dc=None):
        # Scans from min_dc to max_dc - defaults to max and min duty cycle
        min_dc = (min_dc or self.min_duty_cycle)
        max_dc = (max_dc or self.max_duty_cycle)
        centre = self.get_centre(min_dc, max_dc)
        self.duty_cycle = min_dc
        sleep(1)
        self.duty_cycle = centre
        sleep(1)
        self.duty_cycle = max_dc
        sleep(1)
        self.duty_cycle = centre
        sleep(1)

    def cal_duty_cycle(self, dc):
        # Set duty cycle for servo - not clamped
        self.PWM.ChangeDutyCycle(dc)

    def cleanup(self):
        # Stop PWM channel for servo and centre
        self.centre()
        sleep(1)
        self.stop()

    @property
    def duty_cycle(self):
        return self.__duty_cycle

    @duty_cycle.setter
    def duty_cycle(self, dc):
        # Set duty cycle for servo - clamped to max and min duty cycle
        dc = self.clamp(dc, self.min_duty_cycle, self.max_duty_cycle)
        self.PWM.ChangeDutyCycle(dc)
        self.__duty_cycle = dc

    @staticmethod
    def get_centre(min_dc, max_dc):
         return min_dc + (max_dc - min_dc)/2

    @staticmethod
    def clamp(dc, min_dc, max_dc):
        return max(min(dc, max_dc), min_dc)
    
    @classmethod
    def count(cls):
        # Returns the number of Servo instances
        return cls.__number

def main():
    try:
        servo = Servo(6)    # create a new servo to be controlled from GPIO pin 5
        servo.start()       # start PWM for servo

        print(servo)
        print("Servo instances: ", Servo.count())

        while True:
            test = input("\nSelect Action - (1) Calibrate, (2) Set max/min duty cycle, or (3) Scan: ")

            if test == __CALIBRATE:
                while True:
                    response = input("Enter Duty Cycle (q = quit): ")
                    if response == __QUIT:
                        break;
                    servo.cal_duty_cycle(float(response))
            elif test == __SET_DUTY_CYCLE:
                min_dc = float(input("Enter minimum duty cycle: "))
                max_dc = float(input("Enter maximum duty cycle: "))
                servo.min_duty_cycle = min_dc
                servo.max_duty_cycle = max_dc
                print(servo)
            else:
                while True:
                    # Scan Servo from min duty cycle to max duty cycle
                    print("\nScanning (CTRL c to exit)...")
                    servo.scan()
            
    except KeyboardInterrupt:
        print("\n-- CTRL-C: Terminating program --")
    finally:
        print("Cleaning up PWM and GPIO...")
        servo.cleanup()
        GPIO.cleanup()
        print("Done.")

if __name__ == "__main__":
    # execute only if run as a script
    main()
    

Wednesday, February 15, 2017

Raspberry Pi and "better" PWM for Servo Control?

RPIO and DMA PWM


Our current method of using software PWM to control servos is ok, but it does use CPU cycles and there is some jitter when the servos are in one position for any length of time. It turns out that there is an alternative (although it is currently in beta) in the RPIO module. From the documentation:

RPIO.PWM provides PWM via DMA for the Raspberry Pi, using the onboard PWM module for semi-hardware pulse width modulation, with a precision of up to 1µs.

With RPIO.PWM you can use any of the 15 DMA channels and any number of GPIOs per channel. Since the PWM is done via DMA, RPIO.PWM uses almost zero CPU resources and can generate stable pulses with a very high resolution. RPIO.PWM is implemented in C (source); you can use it in Python via the provided wrapper, as well as directly from your C source.

An important point to note is that you can use any number of GPIO's per channel, so you don't instantiate a new channel for each servo but use the one channel for as many servos as you have connected. An example of using RPIO.PWM is shown below.

from RPIO import PWM

servo = PWM.Servo()

# Set servo on GPIO17 to 1200µs (1.2ms)
servo.set_servo(17, 1200)

# Set servo on GPIO17 to 2000µs (2.0ms)
servo.set_servo(17, 2000)

# Clear servo on GPIO17
servo.stop_servo(17)

This example shows the high level application of RPIO.PWM. There are also a number of low level methods which you can review via the documentation link above. Two that we would like to briefly cover are:

  1. Subcycles - Each DMA channel is setup with a specific subcycle, within which pulses are added, and which will be repeated endlessly. Servos, for instance, typically use a subcycle of 20ms, which will be repeated 50 times a second. You can add pulses for multiple GPIOs, as well as multiple pulses for one GPIO. Subcycles cannot be lower than 2ms. This default works for us since the SG90 likes a frequency of 50 Hz (i.e. a period of 20 ms).
  2. Pulse-width increment granularity - (10µs by default) is used for all DMA channels (since its passed to the PWM timing hardware). Pulses are added to a subcycle by specifying a start and a width parameter, both in multiples of the granularity. For instance to set 500µs pulses with a granularity setting of 10µs, you’ll need to set the pulse-width as 50 (50 * 10µs = 500µs). The pulse-width granularity is a system-wide setting used by the PWM hardware, therefore you cannot use different granularities at the same time, even in different processes. So unless you change the default, increase your pulse width by 10µs increments.

Installing RPIO


RPIO is an advanced GPIO module for the Raspberry Pi. It includes the following:

  • PWM via DMA (up to 1µs resolution)
  • GPIO input and output (drop-in replacement for RPi.GPIO)
  • GPIO interrupts (callbacks when events occur on input gpios)
  • TCP socket interrupts (callbacks when tcp socket clients send data)
  • Command-line tools rpio and rpio-curses
  • Well documented, fast source code with minimal CPU usage
  • Open source (LGPLv3+)

RPIO is not installed by default on the Raspberry Pi. Depending on what model you are using you may come across some issues. As a first step try:
sudo apt-get update
sudo apt-get install python3-setuptools
sudo easy_install3 -U RPIO
Note that there are different versions for Python 3 and Python 2. This is for Python version 3. If you see this error:
fatal error: Python.h: No such file or directory
compilation terminated.
error: Setup script exited with error: command 'gcc' failed with exit status 1`
The fix is to install the python development headers. Again there are different versions for Python 2 and 3. To install for Python 3 use:
sudo apt-get install python3-dev
Lastly, if when you try and import the module you get the following:
SystemError: This module can only be run on a Raspberry Pi!
Try:
cd ~
git clone https://github.com/metachris/RPIO.git --branch v2 --single-branch
cd RPIO
sudo python setup.py install

Results


We got it working but the results seem a bit flaky. When it works it is good but every couple of runs we get the following error:
Traceback (most recent call last):
  File "/home/pi/python_code/RPIO_test.py", line 11, in <module>
    servo = PWM.Servo()
  File "/usr/local/lib/python3.4/dist-packages/RPIO-2.0.0_beta1-py3.4-linux-armv7l.egg/RPIO/PWM/__init__.py", line 188, in __init__
    setup(pulse_incr_us=pulse_incr_us)
  File "/usr/local/lib/python3.4/dist-packages/RPIO-2.0.0_beta1-py3.4-linux-armv7l.egg/RPIO/PWM/__init__.py", line 87, in setup
    return _PWM.setup(pulse_incr_us, delay_hw)
RuntimeError: Failed to create mailbox device

Started trying to track this error down but then decided life was too short! We will work with the RPi.GPIO PWM and see if we can't work around the jitter. At least it works consistently.

Sunday, February 12, 2017

Raspberry Pi and the TowerPro SG90 Micro Servo

TowerPro SG90 Micro Servo 9g




To control our pan tilt bracket on Alexa M we are using two SG90 Micro Servos. We had a look for a Python library to control these but was only able to locate one which controlled a servo driver board via I2C. Based on this we decided to roll our own.



The Jaycar SG90 Data Sheet only helps with the wiring details. We need the pulse width's required to drive the servo. We found a more useful Data Sheet at MicoPik. From this we can determine the following characteristics:
  • Weight: 9 g
  • Dimension: 22.2 x 11.8 x 31 mm approx.
  • Stall torque: 1.8 kgf·cm
  • Operating speed: 0.1 s/60 degree
  • Operating voltage: 4.8 V (~5V)
  • Dead band width: 10 µs
  • Temperature range: 0 ºC – 55 ºC 
  • Running current with 5V supply (no mechanical load) 220 ±50mA
  • Stall current with 5V supply (horn locked)                 650 ±80mA
  • Idle current with 5V supply                                         6 ±10mA

More importantly, it tells us that the servo will move to:

  • 0 degrees - the centre position with a 1.5 ms pulse;
  • + 90 degrees with a ~2 ms pulse; and
  • - 90 degrees with a  ~1 ms pulse.


Whether + or - 90 degrees is left or right will depend on how you mounted the servo.



Calibrating your Servo


Every servo is different, so you will need to calibrate it for the best performance. From the data sheet, we see that a SG90 expects a frequency of 50 Hz on the control line and the position it moves to depends on the pulse width of the signal.

For the Raspberry Pi we do not have a change pulse width method for PWM, but we can change the Duty Cycle. Note that:

Duty Cycle = Pulse Width * Frequency

Given a 50 Hz frequency we can calculate the required duty cycle for any pulse width. For example:

  • We need a 1.5 ms pulse to centre the servo, or a Duty Cycle = 0.0015 * 50 = 0.075 (i.e 7.5%).
  • Similarly, 1 ms pulse (- 90 degrees) requires a Duty Cycle = 0.001 * 50 = 5%; and
  • 2 ms pulse (+ 90 degrees), Duty Cycle = 0.002 * 50 = 10%

Thus the duty cycle range should be from 5 - 10% with the centre at 7.5%. We will determine the exact numbers for your servo shortly.



Wire up the Pi as shown above. You can use any GPIO pins you want, I used GPIO 5 because it doesn't conflict with the motor driver board and speaker pHat which will also be mounted on Alexa M. Although the servo runs off a nominal 5V, there is no problem controlling it with the 3.3V levels of the Pi since that is still above the logic high threshold.

You can power (Vcc) the servo from the Raspberry Pi 5V pin but do your current calculations first. Use the current draws from the data sheet above. The Pi draws approximately 700 mA from the +5 V supply. You may draw current from the +5 V pins provided the sum of that current and the board's 700 mA doesn't exceed the supply you provide to the board. I'm using a 2A supply so one servo is fine (even stalled).

Python Calibration Program


Load up the following servo_test.py program on your Pi and then run it.

# servo_test.py - Test functionality of SG90 Micro Servo
#
# Written By: David Such

import RPi.GPIO as GPIO
import time

servo_pin = 5
duty_cycle = 7.5     # Should be the centre for a SG90

# Configure the Pi to use pin names (i.e. BCM) and allocate I/O
GPIO.setmode(GPIO.BCM)
GPIO.setup(servo_pin, GPIO.OUT)

# Create PWM channel on the servo pin with a frequency of 50Hz
pwm_servo = GPIO.PWM(servo_pin, 50)
pwm_servo.start(duty_cycle)

try:
    while True:
        duty_cycle = float(input("Enter Duty Cycle (Left = 5 to Right = 10):"))
        pwm_servo.ChangeDutyCycle(duty_cycle)
            
except KeyboardInterrupt:
    print("CTRL-C: Terminating program.")
finally:
    print("Cleaning up GPIO...")
    GPIO.cleanup()

Start with the theoretical duty cycle values above and then gradually move them up and down. Note the values just before the limits, you might have to use 0.5% increments towards the end. For my pan servo, the minimum duty cycle was 3% and the maximum was 11%, with the centre at 7%. Use CTRL-C to exit the program.

You will need to be running idle3 as sudo since we are controlling the GPIO pins.

Once you have the maximum and minimum duty cycle's you can use the following code to create the scan effect shown in the introductory video.

# servo_scan.py - Test functionality of SG90 Micro Servo
#
# Written By: David Such

import RPi.GPIO as GPIO
import time

MIN_DUTY = 3
MAX_DUTY = 11
CENTRE = MIN_DUTY + (MAX_DUTY - MIN_DUTY) / 2

servo_pin = 5
duty_cycle = CENTRE     # Should be the centre for a SG90

# Configure the Pi to use pin names (i.e. BCM) and allocate I/O
GPIO.setmode(GPIO.BCM)
GPIO.setup(servo_pin, GPIO.OUT)

# Create PWM channel on the servo pin with a frequency of 50Hz
pwm_servo = GPIO.PWM(servo_pin, 50)
pwm_servo.start(duty_cycle)

try:
    while True:
        pwm_servo.ChangeDutyCycle(MIN_DUTY)
        time.sleep(0.5)
        pwm_servo.ChangeDutyCycle(CENTRE)
        time.sleep(0.5)
        pwm_servo.ChangeDutyCycle(MAX_DUTY)
        time.sleep(0.5)
        pwm_servo.ChangeDutyCycle(CENTRE)
        time.sleep(0.5)
            
except KeyboardInterrupt:
    print("CTRL-C: Terminating program.")
finally:
    print("Cleaning up GPIO...")
    pwm_servo.ChangeDutyCycle(CENTRE)
    time.sleep(0.5)
    GPIO.cleanup()

Next up we will create a generalised python Servo class that we can reuse.

Raspberry Pi and the Duinotech Servo (SG90) Pan and Tilt Bracket

Building the Duinotech Pan and Tilt Bracket (Part 1)


As part of our mission to build Alexa M (a Raspberry Pi based robot with voice recognition), we have decided to include an ultrasonic sensor and/or camera. To that end we will include a pan/tilt bracket with a couple of servos.

The kit we chose was the duinotech Pan and Tilt Action Camera Bracket Mount for 9G Servos. This is cheap and designed to operate with two of the TowerPro SG90 MicroServo's.

While they are cheap, construction details are best described as scant (or perhaps missing). You do get a before and after shot, but the after shot only shows one servo.



It looks like Adafruit supply a similar unit, so here are the construction details from them.

Assembling the Base (Pan Unit)

  • Insert the servo into one side of the pan bracket.
  • Make sure that the servo shaft is at the opposite side from the pivot point for the tilt bracket.
  • Assemble the second side of the pan bracket to enclose the servo.
  • Attach the two halves of the pan bracket using two of the small screws.



  • Attach the 4-armed servo horn to the servo shaft using one of the short screws as shown.
  • Use one of the shorter screws to attach the base to the servo shaft. The longer mounting screws packed with some servos will damage the servo if screwed in too far.
  • Align the servo horn so that the longer arms are vertical when the servo is at the midpoint of its rotation - don't worry too much about this you will need to move it when you calibrate the servo.
  • Attach the base to the servo horn, using 4 of the smallest screws as shown. I had to trim the horn so it would fit in the base.


Before assembling the tilt bracket in the next post we will test our pan servo with the Raspberry Pi and create a servo library that we can reuse.

Raspberry Pi Robot - Alexa M

Raspberry Pi Robot


As mentioned in an earlier post we recently acquired an Amazon Echo Dot. We have also been playing around with a 2WD robot chassis and the Raspberry Pi. Put all these together and you have the makings of an interesting robot. The folks over at Dexter Industries obviously had the same idea.


We are going to have a crack at making our own version - called Alexa M (to differentiate it from our Echo Dot). We will do this in stages to ensure each part works!

In part 1, we will construct a pan/tilt bracket which could hold an ultrasonic sensor or a camera.