Wednesday, 19 August 2015

I2C Port Expander

For all of my projects I have used the standard digital output pins when connecting to the Armdroid's 8-bit parallel interface.  But, what if you want to free up some of these pins in order to use external sensors, controls, or other devices...

The easiest way of getting more inputs and outputs is to use an "i/o port expander".  This is a device that allows you to control a number of ports using data sent to the device.  A port expander takes the data and controls the appropriate I/O pins.  This allows lots of sensors and devices, including the ability to control multiple Armdroids using only a few pins on the Arduino board.

The device I chose was the PCF8574A which has eight general purpose input/output pins controlled using I2C (pronounced I-squared-C).  I2C is a serial communications protocol allowing ICs to swap data on the same two-wire bus - Serial Data Line (SDA) and Serial Clock Line (SCL).

There are other devices available supporting 16 ports (eg. MCP23017), or SPI buses (eg. MCP23S17) and are similar in usage.  Although SPI (10Mhz) is faster than I2C (1.7MHz), for our purpose interfacing the Armdroid, it doesn't really make much difference

Pinouts

PCF8574A pinouts
The expanded I/O port are Pins 4-7 and 9-12 to be connected to the Armdroid interface D1-D8

Three address pins A0, A1, A3 determine the chips ID and must be wired to either +5v or GND.

Pin 13 (INT) used for interrupt output - leave disconnected for now

SDA - SDA pin on Arduino
SCL - SCL pin on Arduino
Vcc - +5v on Arduino
GND - GND on Arduino plus Armdroid interface GND

The 8574A can be assigned an address in the range 0x38-0x3F, and you can change this address by changing the connections of the pins A0, A1, and A2 as shown in the following table:

A0
A1
A2
Address
GND GND GND 0x38
+5V GND GND 0x39
GND +5V GND 0x3A
+5V +5V GND 0x3B
GND GND +5V 0x3C
+5V GND +5V 0x3D
+5V +5V GND 0x3E
+5V +5V +5V 0x3F

You can use up to eight of these PCF8574A chips on the same IC2 bus.  Texas Instruments also manufacture the PCF8574 (without the A), essentially the same device with addresses ranging from 0x20-0x27.

You might find this i2c_scanner_sketch useful to verify your I2C communication is working if you have any problems.

Circuit Diagram


Circuit diagram for 8-bit Armdroid interface with PCF8574A

PCF8574A / Armdroid 1 connections:

P0 (pin 4) - D2 (J1/pin 4)
P1 (pin 5) - D1 (J1/pin 3)
P2 (pin 6) - D4 (J1/pin 6)
P3 (pin 7) - D3 (J1/pin 5)
P4 (pin 9) - D6 (J1/pin 8)
P5 (pin 10) - D5 (J1/pin 7)
P6 (pin 11) - D8 (J1/pin 10)
P7 (pin 12) - D7 (J1/pin 9)

J1/pin 1 (+5v) - not connected
J1/pin 2 (Gnd) - Common Gnd (Arduino & PCF8574A)

The address of the PCF8574A is hard-coded to 0x38 by grounding A0, A1, and A2.  I would recommend starting with these settings until you have something working.

Code

Surprisingly, the code changes to support the port expander in the Armdroid Library are minimal thanks to C++ inheritance, and the Arduino 'Wire' library.

When designing the Armdroid library, flexibility was foremost, and it was always my intention to allow the library to be easily adapted for use in different projects.

To make this possible, core functionality was implemented in an abstract base class which can be derived into specialized classes.  This base class doesn't actually have any knowledge how control the hardware, rather, its up to the derived classes to implement that.  Another potential use in future might include supporting optimized versions for different Arduino boards.

In our case, we're creating a new class ArmdroidPortExp inheriting from ArmBase to represent our port expander variant:

 class ArmdroidPortExp : public ArmBase {  
  public:  
  void begin(uint8_t address = 0x38);  
  protected:  
  void armdroid_write(uint8_t output);  
  private:  
  uint8_t i2c_addr;  
 };  

This implementation adds a new member function ArmdroidPortExp::begin(uint8_t address) responsible for setting up the I2C hardware, and stores the supplied address of the port expander.  We also take the opportunity to initialize the Armdroid interface at this point.

The virtual function ArmdroidPortEx::armdroid_write(uint8_t output) simply writes a byte to the port expander using the address supplied earlier.  This is all done using the 'Wire' library that conveniently abstracts us from low-level I2C protocol/transmission details:

 void ArmdroidPortExp::begin(uint8_t address)  
 {  
  i2c_addr = address;  
  Wire.begin();  
  Wire.beginTransmission(i2c_addr);  
  Wire.write(STROBE);  
  Wire.endTransmission();  
 }  
 void ArmdroidPortExp::armdroid_write(uint8_t output)  
 {  
  Wire.beginTransmission(i2c_addr);  
  Wire.write(output);  
  Wire.endTransmission();  
 }  

The implementation really is that simple !

Finally, here's a snippet of code that instantiates and uses the newly derived class...

 #include <Wire.h>  
 #include "Armdroid.h"  
 #include "ArmdroidPortExp.h"  
 // port expander address:  
 const int i2c_addr = 0x38;  
 // initialize Armdroid library:  
 ArmdroidPortExp myArm;  
 void setup()  
 {   
  myArm.begin(i2c_addr);  
  myArm.setSpeed(120);  
  myArm.torqueMotors(true);  
 }  
 void loop()  
 {  
      .  
      .  
      .  
 }  

I've added this code as an extension to the Armdroid Library as a separate package:
https://github.com/Armdroid/Armdroid-Arduino-PortExp

It wasn't possible to add this code to the library without introducing a dependency on the Wire library due to an 'oddity' with the Arduino IDE build mechanism.  If you intend to make use of these extensions, you will need to first install the Armdroid-Arduino-Library, then follow the instructions to install Armdroid-Arduino-PortExp

Included with this library is a modified version of the AsyncDemo previously presented with the Asynchronous library enhancements.

Photographs of my test circuit for reference:

See Also

PCF8574A data sheet: http://www.ti.com/lit/ds/symlink/pcf8574a.pdf
I2C tutorial: http://www.robot-electronics.co.uk/i2c-tutorial
Arduino wire library: https://www.arduino.cc/en/Reference/Wire

Standard Armdroid / Arduino wiring:
http://armdroid1.blogspot.co.uk/2014/02/interface-bench-test-part-1.html
http://armdroid1.blogspot.co.uk/2014/02/interface-bench-test-part-2_15.html
http://armdroid1.blogspot.co.uk/2014/02/interface-bench-test-part-3.html

Sunday, 9 August 2015

Asynchronous Enhancements

I've been making several enhancements to Armdroid Library to support Asynchronous operation.  This means its now possible to drive all motors, and still do other processing at the same time - for example listening to the serial port for stop requests etc.

You may recall in my last post, when driving motors for very long durations, its simply not possible to interrupt things until all motors reached their target positions.  This has been a limitation for sometime - and with hindsight, I really wish I had implemented the library slightly differently in the first place.

Advanced users will welcome these changes, although you can still use the existing drive methods which have been retained for compatibility with existing sketches/projects.

GitHub: https://github.com/Armdroid/Armdroid-Arduino-Library

The new drive method is:
 void driveMotorsAsynchronous()

and complimented by the following control methods:
ArmAsyncState getAsyncState()
bool isRunning()
bool Start(MTR_CHANNELS target)
bool Pause()
bool Resume()
bool Stop()

To asynchronously drive motors, you must call driveMotorsAscynchronous() in your sketch loop() function.  This handles timings and pulses stepper motors when ArmAsyncState = ASYNC_DRIVE_RUNNING.  To enter this state, you call Start() with arguments to specify the target position for each motor channel.
Multiple calls to Start() are possible, but the current implementation does not wait for the existing target positions to be reached before running to new targets.  This behavior was intentional as you can always implement a data structure to cache requests as necessary.

To cancel running motors at any time, call the Stop() method which cancels the asynchronous run request, and stops all motors dead.

Likewise, Pause()/Resume() allows you to pause and resume movements, but does not cancel the asynchronous run request.

The asynchronous extensions implements a simple state machine, so the ArmAsyncState enumeration has been provided to allow programmers to evaluate the state at any time.

To demonstrate the concept - the following example shows how to send commands over serial and allows users to cancel movements, at any time, no matter how lengthy the motor operations are.

You can send bytes to the Arduino from any software that can access the computer serial port.

Code

 /*  
  * Asynchronous Drive Demonstration  
  * Copyright Richard Morris 2015. All Rights Reserved  
  * http://armdroid1.blogspot.co.uk  
  *  
  */  
 #include "Armdroid.h"  
 // initialize Armdroid library:  
 Armdroid myArm( 2, 3, 4, 5, 6, 7, 8, 9 );  
 // variables for receiving and decoding commands:  
 unsigned int rxCmdPos;  
 int rxCmdVal;  
 int rxCmdArg[5];  
 // variable to store previous state:  
 ArmAsyncState previous;  
 void setup()  
 {  
  Serial.begin(9600);  
  myArm.setSpeed(120);  
  myArm.torqueMotors(true);  
 }  
 void loop()  
 {  
  if (!Serial) {  
   // while the serial stream is not open, do nothing:  
   while(!Serial);  
   Serial.println(F("Welcome, Armdroid!"));  
   // reset command variables  
   rxCmdPos = rxCmdVal = 0;  
   memset(rxCmdArg, 0, sizeof(rxCmdArg));  
  }  
  if (Serial.available()) {  
   const char ch = Serial.read();  
   if (isDigit(ch))  
    rxCmdVal = (rxCmdVal * 10) + (ch - '0'); // accumulate value  
   else if (ch == '-')  
    rxCmdVal = rxCmdVal * -1;  
   else if (ch == ',') {  
    rxCmdArg[rxCmdPos++] = rxCmdVal;      // shift received value into  
                          // arguments array  
    if (rxCmdPos == 6)  
     rxCmdPos = 0;              // wrap argument index  
    rxCmdVal = 0;               // reset accumulator  
   }  
   else if (ch == 'D') {  
    Serial.println( F("drive motors") );  
    MTR_CHANNELS channels = { rxCmdArg[0], rxCmdArg[1], rxCmdArg[2], rxCmdArg[3], rxCmdArg[4], rxCmdVal };  
    myArm.Start(channels);  
    // reset accumulator  
    rxCmdPos = rxCmdVal = 0;  
   }  
   else if (ch == 'P') {  
    Serial.println( F("pause running motors") );  
    myArm.Pause();  
   }  
   else if (ch == 'C') {  
    Serial.println( F("continue driving motors") );  
    myArm.Resume();  
   }  
   else if (ch == 'S') {  
    Serial.println( F("stop dead all motors!") );  
    myArm.Stop();  
   }  
  }  
  // method called every loop iteration:  
  myArm.driveMotorsAsynchronous();  
  // report state changes:  
  ArmAsyncState current = myArm.getAsyncState();  
  if (current != previous) {  
       Serial.print( F("DRIVE STATUS = ") );  
       switch (current)  
       {  
            case ASYNC_DRIVE_STOPPED:  
                 Serial.println( F("STOPPED") );  
                 break;  
            case ASYNC_DRIVE_RUNNING:  
                 Serial.println( F("RUNNING") );  
                 break;  
            case ASYNC_DRIVE_PAUSED:  
                 Serial.println( F("PAUSED") );  
                 break;  
       }  
       previous = current;  
  }  
 }  

for example - drive base motor 2,500 steps counter-clockwise
issue:   0,0,0,0,0,2500D
at any time, send (P)ause, (C)ontinue, or (S)top

I'm currently working on a revised specification for the Armdroid Serial Protocol to include these changes and streaming modes.   In the meantime, I've included this demonstration (AsyncDemo) in the examples directory.