Showing posts with label Seeed. Show all posts
Showing posts with label Seeed. Show all posts

Saturday, September 23, 2017

Raspberry Pi and Alexa Mobile Robot (Part 1)

The story so far...




In previous posts we have described building a Raspberry Pi based telepresence robot. At the moment, the robot can be remotely controlled via a web site to which it also streams video from the Pi camera. We have also added an ultrasonic sensor on a PTZ mount to allow it to roam autonomously. The next step in the robots evolution is to add voice recognition and speech using Amazon Alexa.

pi-top PULSE




The Raspberry Pi doesn't come with a microphone or speaker. There are lots of ways that you can add this capability but we decided to use the pi-top PULSE. The PULSE includes:

  • RGB LED's - 7x7 grid, illuminated speaker, underside ambient HAT and pi-top Accessory compatible;
  • SPEAKER - 2W with I2S amplifier; and a 
  • MICROPHONE - 200Hz to 11KHz response Automatic Gain Control (ACG).

Not only can we use the speaker and microphone for interfacing with Alexa but we can show some emotional/behaviour state changes via the LED's.

Wearing Multiple HATs - The 1st Problem


Even though HATs (Hardware Attached on Top) are not intended to be stacked, you can stack up to 62 HATs and not have an address collision. This assumes you don't have conflicting pin usage and you have compatible stackable headers.

The best way to check HAT / stackable board compatibility is to map out what every pin is being used for.


The image above illustrates the pin usage for the robot. The key is as follows:

  • Orange Pins - Are general I/O pins used for the PTZ servo's and ultrasonic sensor.
  • Blue Pins - Motor Driver Board pin usage.
  • Yellow Pins - Pi Top PULSE HAT pin usage.
Thus we don't have any electrical conflicts and can move on to the mechanical interfacing issues.




To call something a HAT it must meet the HAT requirements. We are using the Seeed Motor Driver Board (shown above) which can't be called a HAT because it doesn't have a full size 40W GPIO connector or an ID EEPROM. This presents us with two problems:

  1. We need access to 6 of the 14 pins which are not extended through the Motor Board.
  2. Even if all 40 pins were extended, placing the PULSE on top of the Motor Board wouldn't allow access to the power and GPIO pins used for the servo PTZ control and ultrasonic sensor. 
To solve this issue, we need a GPIO expansion shield which provides 3 x 40 pin connections in parallel. We can then use a couple of male to female cables to connect our "HAT's". We will conclude this build in the next post (once the expansion shield has arrived).




Saturday, March 4, 2017

Raspberry Pi Motor Board Python Class


Overview


We have made some additional changes to the Seeed Raspberry Pi Motor Board class. An obvious missing method is a way to change the speed of the motors. You can of course just change the duty attribute but this will only take effect the next time you change direction. So we have added a speed(duty) method. This will assign the new duty cycle and change the duty cycle of any motors which are already moving.

Note that in the Motor() class provided in the previous post:

def Stop():

should be:

def Stop(self):

Motor Control Class


Here is the updated Motor Control Class for the Seeed Motor Board. You many need to change the names of the direction methods as this will be determined by how you have wired your motors to the motor control board.

The MotorState enum class is used to record a history list of commands received. This may be useful when debugging the Robot in autonomous mode.

#!/usr/bin/python
# RS_MotorControl.py - Motor Control Class for the Seeed Raspberry Pi Motor Driver 
# Board v1.0 which uses the Freescale MC33932 dual H-Bridge Power IC.
#
# Based on Seeed Motor() Class 
# ref: http://wiki.seeed.cc/Raspberry_Pi_Motor_Driver_Board_v1.0/
#
# 1 March 2017 - 1.0 Original Issue
#
# Reefwing Software
# Simplified BSD Licence - see bottom of file.

import RPi.GPIO as GPIO
import os, signal

from time import sleep
from enum import Enum, unique
from PiSoftPwm import *

@unique
class MotorState(Enum):
    INIT         = 1
    STOPPED      = 2
    LEFT_FWD     = 3
    RIGHT_FWD    = 4
    BOTH_FWD     = 5
    LEFT_BACK    = 6
    RIGHT_BACK   = 7
    BOTH_BACK    = 8
    CHANGE_SPEED = 9

class MotorControl():
    def __init__(self, base_time=0.01, duty=50):
        # MC33932 pins connected to GPIO
        self.PWMA = 25  
        self.PWMB = 22
        self._IN1 = 23  
        self._IN2 = 24 
        self._IN3 = 17
        self._IN4 = 27

        self.base_time = base_time
        self.duty = duty
        self.history = [MotorState.INIT]

        # 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 Software PWM outputs
        # Left Motor  = OUT_1 and OUT_2
        # Right Motor = OUT_3 and OUT_4
        self.OUT_1  = PiSoftPwm(self.base_time, 100, self._IN1, GPIO.BCM)
        self.OUT_2  = PiSoftPwm(self.base_time, 100, self._IN2, GPIO.BCM)
        self.OUT_3  = PiSoftPwm(self.base_time, 100, self._IN3, GPIO.BCM)
        self.OUT_4  = PiSoftPwm(self.base_time, 100, self._IN4, GPIO.BCM)

        # Start PWM for outputs - nbSlicesOn = 0, i.e. duty cycle = 0
        self.OUT_1.start(0)
        self.OUT_2.start(0)
        self.OUT_3.start(0)
        self.OUT_4.start(0)

    def __str__(self):
        # Return string representation of motor control
        return "Motor Control: base time - {0} seconds, duty - {1}%".format(self.base_time, self.duty)

    def left_back(self):
        self.OUT_1.changeBaseTime(self.base_time)
        self.OUT_2.changeBaseTime(self.base_time)
        self.OUT_1.changeNbSlicesOn(self.duty)
        self.OUT_2.changeNbSlicesOn(0)
        self.history.append(MotorState.LEFT_BACK)

    def left_forward(self):
        self.OUT_1.changeBaseTime(self.base_time)
        self.OUT_2.changeBaseTime(self.base_time)
        self.OUT_1.changeNbSlicesOn(0)
        self.OUT_2.changeNbSlicesOn(self.duty)
        self.history.append(MotorState.LEFT_FWD)

    def right_back(self):
        self.OUT_3.changeBaseTime(self.base_time)
        self.OUT_4.changeBaseTime(self.base_time)
        self.OUT_3.changeNbSlicesOn(0)
        self.OUT_4.changeNbSlicesOn(self.duty)
        self.history.append(MotorState.RIGHT_BACK)

    def right_forward(self):
        self.OUT_3.changeBaseTime(self.base_time)
        self.OUT_4.changeBaseTime(self.base_time)
        self.OUT_3.changeNbSlicesOn(self.duty)
        self.OUT_4.changeNbSlicesOn(0)
        self.history.append(MotorState.RIGHT_FWD)

    def speed(self, duty):
        # Change motor speed to duty (0-100) if not stopped (0)
        self.duty = duty
        self.OUT_1.nbSlicesOn = duty if self.OUT_1.nbSlicesOn else 0
        self.OUT_2.nbSlicesOn = duty if self.OUT_2.nbSlicesOn else 0
        self.OUT_3.nbSlicesOn = duty if self.OUT_3.nbSlicesOn else 0
        self.OUT_4.nbSlicesOn = duty if self.OUT_4.nbSlicesOn else 0
        self.history.append(MotorState.CHANGE_SPEED)

    def stop(self):
        self.OUT_1.changeNbSlicesOn(0)
        self.OUT_2.changeNbSlicesOn(0)
        self.OUT_3.changeNbSlicesOn(0)
        self.OUT_4.changeNbSlicesOn(0)
        self.history.append(MotorState.STOPPED)
        
    def cleanup(self):
        # Stop PWM on all outputs
        self.OUT_1.stop()
        self.OUT_2.stop()
        self.OUT_3.stop()
        self.OUT_4.stop()

def main():
    motor_control = MotorControl()    # create a new motor control instance
    print(motor_control)

    def endProcess(signum = None, frame = None):
        # Called on process termination. Stop motor control PWM
        if signum is not None:
            SIGNALS_NAMES_DICT = dict((getattr(signal, n), n) for n in dir(signal) if n.startswith('SIG') and '_' not in n )
            print("signal {} received by process with PID {}".format(SIGNALS_NAMES_DICT[signum], os.getpid()))
        print("\n-- Terminating program --")
        print("Cleaning up motor control PWM and GPIO...")
        motor_control.cleanup()
        GPIO.cleanup()
        print("Done.")
        exit(0)

    # Assign handler for process exit
    signal.signal(signal.SIGTERM, endProcess)
    signal.signal(signal.SIGINT, endProcess)
    signal.signal(signal.SIGHUP, endProcess)
    signal.signal(signal.SIGQUIT, endProcess)

    while True:
        print('Testing motors...')
        motor_control.left_forward()
        sleep(1)
        motor_control.left_back()
        sleep(1)
        motor_control.right_forward()
        sleep(1)
        motor_control.right_back()
        sleep(1)
        # speed = int(input("Enter Speed (0-100, CTRL c to quit): "))
        # motor_control.speed(speed)
        
if __name__ == "__main__":
    # execute only if run as a script
    main()

## Copyright (c) 2017, Reefwing Software
## All rights reserved.
##
## Redistribution and use in source and binary forms, with or without
## modification, are permitted provided that the following conditions are met:
##
## 1. Redistributions of source code must retain the above copyright notice, this
##   list of conditions and the following disclaimer.
## 2. Redistributions in binary form must reproduce the above copyright notice,
##   this list of conditions and the following disclaimer in the documentation
##   and/or other materials provided with the distribution.
##
## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
## ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
## WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
## DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
## ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
## (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
## LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
## SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.



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.