Showing posts with label library. Show all posts
Showing posts with label library. Show all posts

Tuesday, June 25, 2019

Mesh Security System (Argon Hub, OLED and MP3 Shields) - Part 2

OLED Display


Figure 5. Argon mounted on Tripler with OLED.


Having demonstrated that we can blink a LED on the Argon, we now want to move onto something a bit more useful. The Argon will form the hub of the Mesh Security System and will connect to an OLED and MP3 shield to indicate system status. In Part 2 we will get the OLED and MP3 shields working.

As shown in Figure 5, connection is simple using the Featherwing Tripler. By mounting the shields horizontally rather than stacking them you can still easily see all the indication LEDs. You will have to solder the headers on the tripler and shields. Do the tripler first. I solder one pin and then check that the header is correctly positioned before soldering the rest. It is a lot easier to rectify an issue with only one pin soldered in place. Once you have completed soldering the headers on the tripler you can use this as a jig to hold the pins in place when soldering them to the shields. This will ensure that the shield pins line up with the headers on the tripler.

Figure 6. OLED Operational


The display board is 128x32 monochrome OLED which has 3 user buttons plus reset. This screen is made of 128x32 individual white OLED pixels and because the display makes its own light, no backlight is required. This reduces the power required to run the OLED and is why the display has such high contrast. The board uses a SSD1306 and connects via I2C (pins D0 and D1), so it is very pin frugal. As I2C is a shared bus you can have other shields which utilise I2C connected at the same time (as long as they have different I2C addresses). The three buttons use:
ButtonPinNotes
AD4No pull-up. Can't be used with Ethernet.
BD3100K pull-up. Can't be used with Ethernet.
CD2No pull-up.
So all up this shield uses 5 pins (D0 - D4).

The library is available in the Web IDE as oled-wing-adafruit and using the display from the Argon is easy. The library takes care of setting the appropriate input modes and debouncing the buttons for you.

I've reproduced my test code stub below. I always like to get each element of a project working before adding the next. This makes debugging much easier.



MP3 Shield


The MP3 Shield is shown in Figure 5 above. This is before the through hole headers have been soldered onto the shield. The shield version that we are using is the Adafruit Music Maker FeatherWing. This shield uses the the VS1053, an encoding/decoding (codec) chip that can decode a wide variety of audio formats such as MP3, AAC, Ogg Vorbis, WMA, MIDI, FLAC, WAV (PCM and ADPCM). This chip also allows you to adjust bass, treble, and volume digitally.

Figure 7. Argon Block Diagram (showing I/O).


Communication is via a SPI interface which allows audio to be played from an SD card. There's also a special MIDI mode that you can boot the chip into that will read 'classic' 31250 Kbaud MIDI data from the UART TX pin. The hardware SPI pins are needed whenever you are transmitting data from the SD card to the decoder chip. If you are using the wing in the special MIDI mode, they're not used.

D11: SPI MISO - connected to MISO - used by both the SD card and VS1053
D12: SPI MOSI - connected to MOSI - used by both the SD card and VS1053
D13: SPI SCK - connected to SCK - used by both the SD card and VS1053

The Adafruit VS1053 Library does include a constructor to define the SPI pins you want to use, but this doesn't help us because:

  1. The hardware SPI pins are already connected by the tripler; and
  2. The alternative SPI pins on the Argon are D2, D3 and D4 - which seem to be very popular with shield designers!


Figure 8. Adafruit Music Maker FeatherWing Shield.


Next are the control pins required to play music. From left to right, in Figure 9 below, they are:

MP3_DCS - this is the VS1053 data select pin
DREQ        - this is the VS1053 data request interrupt pin
MP3_CS    - this is the VS1053 chip select pin
SD_CS       - this is the SD Card chip select pin

Figure 9. MP3 Control Pins.


Unfortunately the MP3 control pins connected (via the tripler) to the Argon conflict with the A, B and C buttons connected to D2, D3 and D4 from the OLED shield. Thankfully there is no conflict on pins D0 or D1, so we can still control the OLED with the MP3 shield in place. Obviously the designers of the two shields at Adafruit didn't talk to each other!

Figure 10. MP3 Shield Installed.


To summarise, the Argon pins used to control the MP3 shield are:

SD_CS                = D2;                 // SD Card chip select pin
MP3_CS             = D3;                 // VS1053 chip select pin (output)
DREQ                 = D4;                 // VS1053 Data request, ideally an Interrupt pin
MP3_DCS          = D5;                 // VS1053 Data/command select pin (output)
SPI MISO           = D11;               // used by both the SD card and VS1053
SPI MOSI           = D12;              // used by both the SD card and VS1053
SPI SCK             = D13;               // used by both the SD card and VS1053

Figure 10 shows the MP3 shield in place on the tripler adjacent to the OLED shield. To give myself a bit more room, I removed the OLED shield while soldering the header pins to the MP3 shield. I again inserted the header pins into the tripler before soldering to make sure that everything lined up.

There are two versions of the Adafruit Music Maker, one includes an amplifier and the other just has a 3.5mm connection for headphones or powered speakers. In retrospect I should have got the one with the amplifier built in. Nevertheless I happen to have a Duinotech 2 x 3W amplifier, so I might as well use that. This is the red PCB shown in Figure 10. Before dealing with this, you will want to make sure that the MP3 shield is working.

Thankfully ScruffR has done the hard work of porting the Adafruit VS1053 Arduino library to work with Particle mesh boards. You will need to import this library and the SDFat library in order to get the shield working. This is easy, just search for the libraries in the Web IDE and then add them. Plug in some headphones (assuming you have the same version shield as I do) and you can use the code below to test the operation of your shield. You will obviously need to copy some mp3 files to SD card before you can play them. Make sure that the names of the files are in the 8.3 format or they wont be able to be played.



Duinotech 2 x 3W Amplifier


Rather than use the 3.5mm jack on the MP3 shield, we will connect directly to the Ground, Right and Left pins next to the headphone jack (Figure 11). They are line level, AC coupled outputs which are suitable for connection to an amplifier.

Figure 11. MP3 Shield Audio Out Pins.


The Duinotech 2 x 3W Class D Amplifier (Figure 12) has greater than 90% efficiency and typically delivers 3W into 4 ohm speakers (or 1.5W into 8 ohms). Its operating voltage range is 2.5 to 5.5 VDC.

The amplifier board uses the PAM8403 chip and power output will be determined by a combination of the input voltage supplied and output impedance. As we are using the regulated 3.3V from the Argon and 8 ohm speakers our expected power output from the amplifier is around 0.5W.

Figure 12. Duinotech 2 x 3W Amplifier.

The amplifier pin out description is provided in the table below.

Amplifier Pinout
Module
Function
R+/R-
Right Speaker
L-/L+
Left Speaker
GND
Ground Connection
+5V
Power Supply
5W
Shutdown Control
GND
Ground Connection
LIN
Left Audio In
GND
Ground for Audio
RIN
Right Audio In

Connection between the MP3 shield and amplifier is straight forward.
  1. MP3 Shield L and G connect to LIN and Audio GND on the amplifier.
  2. MP3 Shield R and G connect to RIN and Audio GND on the amplifier.
  3. R+/R- on the amplifier connect to the right speaker.
  4. L+/L- on the amplifier connect to the left speaker.
  5. +5V and GND on the amplifier connect to the 3.3V and GND pins on the Argon.
In Part 3 we will complete construction of the Argon Hub and 3D print an enclosure for it. We will then move onto configuring the Xenon's.

Sunday, November 5, 2017

Duinotech 8 x 8 LED Dot Matrix Module Red

Introduction



Jaycar in Australia have a range of Arduino compatible kit, one such piece is the Duinotech 8 x 8 LED Dot Matrix Module Red. They provide the following specifications:

A 64 Red LED matrix, this module is easily controlled with the Led Control library. Display your own custom characters, or use multiple modules together to make a scrolling display.

•    Operating Voltage: 5VDC
•    Protocol: SPI (Shift-Register)
•    Chipset: MAX7219
•    LED Colour: RED
•    Dimensions: 62(W) x 32(H) x 14(D)mm

This is fine but doesn't really demonstrate how to use it. So I don't have figure this out again and to hopefully assist someone else who might want to use the module, I have summarised the key information below.

Hardware


Connection is very straight forward. From the dot matrix module:

VCC - connects to 5V on the Arduino
GND - connects to GND
CLK - connects to Arduino D10
CS - connects to Arduino D11
DIN - connects to Arduino D12

You can change the CLK, CS and DIN pin assignments in the software (see below).

Software


To drive this module you need the LedControl library. LedControl is an Arduino library for MAX7219 and MAX7221 Led display drivers. The code also works with the Teensy. Download this library and unzip it into your Arduino libraries folder. You can then try out the library using the sample code provided.

#include <LedControl.h>

int DIN = 12;
int CS =  11;
int CLK = 10;

LedControl lc=LedControl(DIN,CLK,CS,0);

void setup(){
 lc.shutdown(0,false); // The MAX72XX is in power-saving mode on startup
 lc.setIntensity(0,7); // Set the brightness, 15 = maximum value
 lc.clearDisplay(0);   // and clear the display
}

void loop(){ 

    byte smile[8]=   {0x3C,0x42,0xA5,0x81,0xA5,0x99,0x42,0x3C};
    byte neutral[8]= {0x3C,0x42,0xA5,0x81,0xBD,0x81,0x42,0x3C};
    byte frown[8]=   {0x3C,0x42,0xA5,0x81,0x99,0xA5,0x42,0x3C};
    
    printByte(smile);
    delay(1000);
    printByte(neutral);
    delay(1000);
    printByte(frown);    
    delay(1000);
    lc.clearDisplay(0);
    delay(1000);
}

void printByte(byte character [])
{
  int i = 0;
  for(i=0;i<8;i++)
  {
    lc.setRow(0,i,character[i]);
  }
}
The initialization code for the lc variable through which we talk to the MAX72XX devices takes 4 arguments. The first 3 arguments are the pin-numbers on the Arduino that are connected to the MAX72XX. These can be any of the digital IO-pins on an Arduino. In the example, pins 12,11 and 10 were chosen.

The fourth argument to LedControl(dataPin,clockPin,csPin,numDevices) declaration is the number of cascaded MAX72XX devices that you're using with this LedControl. The library can address up to 8 devices from a single LedControl variable. There is a small performance penalty with each device you add to the chain, but the amount of memory used by the library code will stay the same, no matter how many devices you set. Since one LedControl cannot address more than 8 devices, only values between 1-8 are allowed.

LED Byte Generator




If you want to design your own images to display on the Dot Matrix module, the easiest way is to download the LED Byte Generator written by Bernhard Hofmann. This Javascript app generates byte codes for sending to an LED matrix.

It works for for 8x8 LED matrices driven by the MAX7219/MAX7221 to use in code such as C/C++ or Energia and run on an Arduino, Raspberry Pi or micro controller such as the MSP430.

To run the app, just File - Open the index.html file in your browser.




Friday, March 17, 2017

HC-SR04 Ultrasonic Sensor Python Class for Raspberry Pi

The HC-SR04




The HC - SR04 ultrasonic ranging module provides 2cm - 400cm non-contact
measurement, with ranging accuracy up to 3mm. The module includes ultrasonic transmitters, receiver and control circuitry. The time difference between transmission and reception of ultrasonic signals is calculated. Using the speed of sound and ‘Speed = Distance/Time‘ equation, the distance between the source and target can be easily calculated.

Credit to Vivek and his article on the same subject for the diagrams.




Wiring the HC-SR04 to a Raspberry Pi


The module has 4 pins:

  • VCC - 5V Supply
  • TRIG - Trigger Pulse Input
  • ECHO - Echo Pulse Output
  • GND - 0V Ground 

Wiring is straight forward with one exception, note that the sensor operates at 5V not the 3.3V of the Raspberry Pi. Connecting the ECHO pulse pin directly to the Raspberry Pi would be a BAD idea and could damage the Pi. We need to use a voltage divider or a logic level converter module to drop the logic level from the HC-SR04 to a maximum of 3.3V. Current draw for the sensor is 15 mA.

As we have a spare logic level converter, we will use that. Connections for the logic converter are shown below.


For the voltage divider option: Vout = Vin x R2/(R1+R2) = 5 x 10000/(4700 + 10000) = 3.4V






Python Class for the HC-SR04 Ultrasonic Sensor



To utilise the HC-SR04:

  1. Provide a trigger signal to TRIG input, it requires a HIGH signal of at least 10μS duration.
  2. This enables the module to transmit eight 40KHz ultrasonic bursts.
  3. If there is an obstacle in-front of the module, it will reflect those ultrasonic waves
  4. If the signal comes back, the ECHO output of the module will be HIGH for a duration of time taken for sending and receiving ultrasonic signals. The pulse width ranges from 150μS to 25mS depending upon the distance of the obstacle from the sensor and it will be about 38ms if there is no obstacle.
  5. Obstacle distance = (high level time × velocity of sound (343.21 m/s at sea level and 20°C) / 2
  6. Allow at least 60 ms between measurements.





Time taken by the pulse is actually for return travel of the ultrasonic signals. Therefore Time is taken as Time/2.

Distance = Speed * Time/2

Speed of sound at sea level = 343.21 m/s or 34321 cm/s

Thus, Distance = 17160.5 * Time (unit cm).

As we are using the ultrasonic sensor with our Raspberry Pi robot, we have created a python class that can be easily imported and used. Note the calibration function which can be used to help correct for things like altitude and temperature.

We have included a simple low pass filter function which is equivalent to an exponentially weighted moving average. This is useful for smoothing the distance values returned from the sensor. The higher the value of beta, the greater the smoothing.

#!/usr/bin/python
# RS_UltraSonic.py - Ultrasonic Distance Sensor Class for the Raspberry Pi 
#
# 15 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, time

# Private Attributes
__CALIBRATE      = "1"
__TEST           = "2"
__FILTER         = "3"
__QUIT           = "q"

class UltraSonic():
    # Ultrasonic sensor class 
    
    def __init__(self, TRIG, ECHO, offset = 0.5):
        # Create a new sensor instance
        self.TRIG = TRIG
        self.ECHO = ECHO
        self.offset = offset                             # Sensor calibration factor
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(self.TRIG, GPIO.OUT)                  # Set pin as GPIO output
        GPIO.setup(self.ECHO, GPIO.IN)                   # Set pin as GPIO input

    def __str__(self):
        # Return string representation of sensor
        return "Ultrasonic Sensor: TRIG - {0}, ECHO - {1}, Offset: {2} cm".format(self.TRIG, self.ECHO, self.offset)

    def ping(self):
        # Get distance measurement
        GPIO.output(self.TRIG, GPIO.LOW)                 # Set TRIG LOW
        sleep(0.1)                                       # Min gap between measurements        
        # Create 10 us pulse on TRIG
        GPIO.output(self.TRIG, GPIO.HIGH)                # Set TRIG HIGH
        sleep(0.00001)                                   # Delay 10 us
        GPIO.output(self.TRIG, GPIO.LOW)                 # Set TRIG LOW
        # Measure return echo pulse duration
        while GPIO.input(self.ECHO) == GPIO.LOW:         # Wait until ECHO is LOW
            pulse_start = time()                         # Save pulse start time

        while GPIO.input(self.ECHO) == GPIO.HIGH:        # Wait until ECHO is HIGH
            pulse_end = time()                           # Save pulse end time

        pulse_duration = pulse_end - pulse_start 
        # Distance = 17160.5 * Time (unit cm) at sea level and 20C
        distance = pulse_duration * 17160.5              # Calculate distance
        distance = round(distance, 2)                    # Round to two decimal points

        if distance > 2 and distance < 400:              # Check distance is in sensor range
            distance = distance + self.offset
            print("Distance: ", distance," cm")
        else:
            distance = 0
            print("No obstacle")                         # Nothing detected by sensor
        return distance

    def calibrate(self):
        # Calibrate sensor distance measurement
        while True:
            self.ping()
            response = input("Enter Offset (q = quit): ")
            if response == __QUIT:
                break;
            sensor.offset = float(response)
            print(sensor)
            
    @staticmethod
    def low_pass_filter(value, previous_value, beta):
        # Simple infinite-impulse-response (IIR) single-pole low-pass filter.
        # ß = discrete-time smoothing parameter (determines smoothness). 0 < ß < 1
        # LPF: Y(n) = (1-ß)*Y(n-1) + (ß*X(n))) = Y(n-1) - (ß*(Y(n-1)-X(n)))
        smooth_value = previous_value - (beta * (previous_value - value))
        return smooth_value
        

def main():
    sensor = UltraSonic(8, 7)       # create a new sensor instance on GPIO pins 7 & 8
    print(sensor)

    def endProcess(signum = None, frame = None):
        # Called on process termination. 
        if signum is not None:
            SIGNAL_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(SIGNAL_NAMES_DICT[signum], os.getpid()))
        print("\n-- Terminating program --")
        print("Cleaning up GPIO...")
        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:
        action = input("\nSelect Action - (1) Calibrate, (2) Test, or (3) Filter: ")

        if action == __CALIBRATE:
            sensor.calibrate()
        elif action == __FILTER:
            beta = input("Enter Beta 0 < ß < 1 (q = quit): ")
            filtered_value = 0
            if beta == __QUIT:
                break;
            while True:
                filtered_value = sensor.low_pass_filter(sensor.ping(), filtered_value, float(beta))
                filtered_value = round(filtered_value, 2)
                print("Filtered: ", filtered_value, " cm")
        else:
            sensor.ping()

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.



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.