Robot and Device Interfacing

The objective of this tutorial is to introduce some basic concepts for interfacing between robotics and external electromechical systems. This is an introductory tutorial rather than in depth or comprehensive introduction. Working with electronics and machinery can be dangerous! Be very always careful and tidy, shorting wires may cause havoc including shocks, injuries and damages to equipment. Do not follow along unless you have been through health and safety training.

Overview

There are two modes for interfacing between robotics and external devices and purpose built end-effectors: (a) Integrated: connect such devices to the robot controller such that they can be driven by the programming language of the machine. (b) External: use a laptop, mobile phone or micro-controller to trigger actions/reactions without connecting to the robot controller. The obvious benefits of the first approach is that everything works together without need to worry too much about synchronizing motion and signals. We will use this approach as it is the simplest in the long run.

At high level, external devices can take: (a) Simple discrete digital signals such as turning on/off switching an appliance, (b) Composite discrete signals such as use of 8-bits to control 256 levels of speed for a motor, (c) Continuous wave-forms such as analog 0-10V to achieve true proportional type of behavior, for instance set the temperature from 0 to 100 degrees, (d) Protocol based communications such as UART/RS232, I2C, SPI etc, where binary packets are exchanged between end-points.

On the other end, the robot has 16-bits for digital inputs and 16-bits for digital outputs. Thus at a simplistic level, it is all about assigning meaning and mapping those digital switches to inputs expected from external devices. Intermediate electronics, also known as programmable control logic circuits, just interface between those machines. We want to keep the complexity of this process to a minimum, when possible, because debugging electronics is much harder than software.

Industrial Programmable Logic Control

Due to the industrial heritage of the robotic machines used, we need to work with/around standards created for factory-type of operations. Those often include high-voltages and dangerous equipment. As a result, integrating external devices with robots is not as user-friendly as hobbyist-level electronic prototyping with Arduino’s and the like. For instance, signaling within the PLC paradigm is performed at 24VDC which will destroy the typical 5VDC or 3.3VDC popular micro-controller units. To bridge this gap we will use a product which combines the logic of industrial electrical operations with rapid prototyping, namely ArdBox PLC; a unit that allows communicating bidirectionally between the robot’s controller and external devices without having to worry, too much, about all these issues. We have two types available in the lab: (a) Relay type: for controlling mains devices such as power tools and appliances, and (b) Digital IO type: for applications that require only logic-level switching.

The devices can be powered directly from the robot controller as they don’t draw too much current. Internally, they contain an Arduino Leonardo which can be programmed using standard C/C++, unlike the highly complicated and often hideous programming paradigms used for industrial PLC applications. Most importantly there are numerous tutorials online and open source libraries that can be directly downloaded and used making life much simpler. Let’s start with the most basic example possible:

Controlling a Light Emitting Diode

We will use one bit of information to turn on/off an LED from the robot programming language by using the SetDO instruction. We need to connect a wire from DO00 on the controller to I09 on ardbox, connect the logic ground from ardbox to a 1K resistor, the resistor to the cathode of the LED and the anode of the LED to the SCL port on ardbox. The code below is almost matching the picture and wiring logic expressed with words above. We generally want to keep it that simple!

//-- Signal Names
//--
#define ROBOT A5
#define LIGHT  3

//-- Initialization
//--
void setup( ) 
{
  //-- Robot (out) -> (in) ArdBox (out) -> (in) LED
  //--
  pinMode( ROBOT, INPUT  );
  pinMode( LIGHT, OUTPUT );  
}

//-- Control Logic Loop
//--
void loop( ) 
{  
  //-- Read from Robot 
  //--
  int value = digitalRead( ROBOT );
       
  //-- Write to LED
  //--
  digitalWrite( LIGHT, value );

  //-- Slow Down a Bit
  //--
  delay( 50 );     
}

Connecting Mains Appliances

Now an LED draws very little current, something like 50mA at 5VDC is about 0.25W, which can be dealt with by connecting the 5VDC logic side of ardbox. However, for higher loads this would total the robot controller which provides the current/power. For running higher loads we must use external power supplies, either DC or AC. The relay on ardbox are electromechanical switches that can be inserted inline with the external power circuit and provide the desired enable/disable capability while keeping the logic level (low voltage) side isolated. Having said that there are also limits here: the relays are rated for maximum 5A at 250VAC equivalent to 1250VA or 3A at 30VDC equivalent to 90W. Anything above that may cause total meltdown. The general advice is to be extremely cautious with mains and high amperage/wattage power supplies and devices and always ask for recommendations before even trying anything! Having said that, the code logic for this type of application is exactly the same as above. We just need to change the port assignment from a logic level pin 3 to relay 8 at pin 13.

//-- Signal Names
//--
#define ROBOT A5
#define LIGHT 13

//-- Initialization
//--
void setup( ) 
{
  //-- Robot (out) -> (in) ArdBox (out) -> (in) LIGHT
  //--
  pinMode( ROBOT, INPUT  );
  pinMode( LIGHT, OUTPUT );  
}

//-- Control Logic Loop
//--
void loop( ) 
{  
  //-- Read from Robot 
  //--
  int value = digitalRead( ROBOT );
       
  //-- Write to Relay
  //--
  digitalWrite( LIGHT, value );

  //-- Slow Down a Bit
  //--
  delay( 50 );     
}

Basic Digital to Analog Logic

The objective of this code is to use 8-bits of output, set by the robot controller using the SetDO or SetGO instruction, and emit an analog signal from 0 to 10 volts DC from ardBox’s output port Q00. This is a common and useful technique to convert between digital and analog but we have to live with the limitation that we can only achieve 256 steps (2^8) between 0 and 10 volts (about 40mV increments). While the following code may seem enormous, without comments and debug mode information, it is actually two lines of code! The logic goes like this:

  1. Define the input and output channels: Robot (out) → (in) ArdBox (out) → (in) External Device
  2. Read all 8 incoming bits and pack them to a byte using LEFT SHIFT and bitwise OR operations.
  3. Set the analog output level using this byte value which maps between 0..255 to 0..10 volts.
  4. Add a tiny delay between cycles to reduce noise on the line.
//-- Remove the underscore for Debug Mode
//--
#define _DEBUG

//-- The Output bits from the Robot
//-- controller to the ArdBox inputs
//--
int I00 = 2;
int I01 = 3;
int I02 = 0;
int I03 = 1;
int I04 = A0;
int I05 = A1;
int I06 = A2;
int I07 = A3;
int I08 = A4;
int I09 = A5;

//-- The output bits from the ArdBox pins
//-- to some external analog 0..10V device
//--
int Q00 = 5;

//-- Initialization
//--
void setup( ) 
{
  //-- In debug mode we want to
  //-- view the values on the serial
  //--
  #ifdef DEBUG
  Serial.begin( 9600 );
  while( !Serial ) { }
  #endif
  
  //-- Robot Out -> ArdBox In
  //--
  pinMode( I00, INPUT );  //-- We skip I03 and I04
  pinMode( I01, INPUT );  //-- because they are used
  pinMode( I04, INPUT );  //-- for serial communications
  pinMode( I05, INPUT );  
  pinMode( I06, INPUT );  
  pinMode( I07, INPUT );
  pinMode( I08, INPUT );  
  pinMode( I09, INPUT );
   
  //-- ArdBox Out -> Device In
  //--
  pinMode( Q00, OUTPUT );  
}

//-- Control Logic Loop
//--
void loop( ) 
{
  //-- Debug Mode for Demonstration
  //-- Read all bits into variables
  //--
  #ifdef DEBUG 
  byte b0 = digitalRead( I00 );
  byte b1 = digitalRead( I01 );
  byte b2 = digitalRead( I04 );
  byte b3 = digitalRead( I05 );

  byte b4 = digitalRead( I06 );
  byte b5 = digitalRead( I07 );
  byte b6 = digitalRead( I08 );
  byte b7 = digitalRead( I09 );

  //-- Merge bits using shifts and ors
  //--
  byte v = b0 << 0 | b1 << 1 | b2 << 2 | b3 << 3 
         | b4 << 4 | b5 << 5 | b6 << 6 | b7 << 7;

  //-- Print out Bits and Bobs
  //--
  Serial.print( "D: " );
  
  Serial.print( b7 );
  Serial.print( b6 );
  Serial.print( b5 );
  Serial.print( b4 );
  
  Serial.print( b3 );
  Serial.print( b2 );
  Serial.print( b1 );
  Serial.print( b0 );

  Serial.print( " V: " );
  
  Serial.print( v );

  Serial.print( " A: " );
  Serial.print( v * 10.0f / 255.0f );
  Serial.print( "vdc" );

  Serial.print( "\r\n" );

  //-- Set Analog Output
  //--
  analogWrite( Q00, v );

  //-- Prevent Serial Flood
  //--
  delay( 1000 );     

  #else

  //-- In Production Mode
  //--
  int value  = digitalRead( I00 ) << 0;
      value |= digitalRead( I01 ) << 1;
      value |= digitalRead( I04 ) << 2;
      value |= digitalRead( I05 ) << 3;
      value |= digitalRead( I06 ) << 4;
      value |= digitalRead( I07 ) << 5;
      value |= digitalRead( I08 ) << 6;
      value |= digitalRead( I09 ) << 7;
       
  analogWrite( Q00, value );

  delay( 50 );     
  #endif
}

Smart Servo Pulsing Mode

The following example schematic shows how to connect the robot to a servo using pulse driving mode. The same logic can be used for steppers that require only one bit for motion and one bit for direction. This is the easiest mode of running motors so it is recommended. The only convoluted step that requires further explanation is the mapping of bits to speeds. To understand we need to consider that the real control over the motors rotation is based on the time between two pulses. Therefore, we need to convert from abstract bits to time in microseconds between pulses! The manufacturer’s specification reports that the 600μs (micro seconds) is full speed on one direction, 2400μs is full speed the other direction and at 1500μs the speed will be zero.

The formula used is: rate = 1500 + ( speed * 900 / 31 ) * ( 1 – 2 * direction ). To understand first lets use some specific values for speed and direction. Speed can be any number from 0 to 31, as this is the range of five bit unsigned integers (2^5).  Direction can be either 0 or 1. When the speed is zero, independent of what direction is, the rate is 1500. When direction is 0 and speed is at maximum value 31, rate is 2400, and finally when direction is 1 and speed is at maximum value 31, rate becomes 600. Thus we mapped from 5 + 1 bits to time domain (alternatively you can think of it as using a 6-bit signed integer).

Assembly

  1. Connect from the robot’s controller output bits DO00 to DO05 to ardbox’s inputs I04 to I09.
  2. Power the servo motor from the external 12VDC power supply. Carefully!
  3. Common the grounds between the ardbox and the servo power supply (black wires).
  4. Short the UART (yellow and orange) wires on the motor and let the SDA (red wire) float.
  5. Connect SLC (clock)/Pin3 from ardbox to SLC (brown wire) of the servo motor.
//-- Remove Underscore for Debug Mode
//--
#define _DEBUG

//-- Robot -> ArdBox -> Motor
//--
#define SPEED0 A0
#define SPEED1 A1
#define SPEED2 A2
#define SPEED3 A3
#define SPEED4 A4
#define DIRECT A5
#define SIGNAL 3

void setup( )
{
  //-- Set Signals
  //--
  pinMode( SIGNAL, OUTPUT );
  pinMode( SPEED0, INPUT  );
  pinMode( SPEED1, INPUT  );
  pinMode( SPEED2, INPUT  );
  pinMode( SPEED3, INPUT  );
  pinMode( SPEED4, INPUT  );
  pinMode( DIRECT, INPUT  );  

  //-- Use Serial on Debug Mode
  //--
  #ifdef DEBUG
  Serial.begin( 9600 );
  #endif
}

//-- Control Loop
//--
void loop( ) 
{
  //-- Load and Pack Speed
  //--
  int speed = 
    ( digitalRead( SPEED4 ) << 4 ) |
    ( digitalRead( SPEED3 ) << 3 ) |
    ( digitalRead( SPEED2 ) << 2 ) |
    ( digitalRead( SPEED1 ) << 1 ) |
    ( digitalRead( SPEED0 ) << 0 ) ;

  //-- Map Bits to Pulse & Direction
  //--
  int rate = 1500 + ( speed * 900 / 31 ) * ( 1 - 2 * digitalRead( DIRECT ) );

  //-- Pulse Motor
  //-- 
  digitalWrite( SIGNAL, LOW  );
  delayMicroseconds( rate );
  
  digitalWrite( SIGNAL, HIGH );
  delayMicroseconds( rate );

  //-- Debug Display
  //--
  #ifdef DEBUG
  Serial.print( "S: " );
  Serial.print( speed );  
  Serial.print( " D: " );
  Serial.print( direct );  
  Serial.print( " R: " );
  Serial.println( rate );  
  delay(1000);
  #endif
}

Stepper With Driver

The following schematic shows how to connect a large unipolar four-wire stepper motor using a dedicated driver module. The code is almost identical with the one above. Here we can use the SDA pin from ardbox to directly control the DIR+ input of the driver instead. The same 5-bit speed logic is used for setting the pulse rate. Note that the driver also offers some nice electrical isolation between the low and high-voltage side of the circuitry.

Hardcore Stepper

This is a bare-bone approach to driving external motors. It is not expected to approach motors from this perspective but only provided as indication that even at low-level things are not that difficult. The following schematic contains: (a) a low cost stepper motor running at logic level voltage of 5VDC, (b) the ardbox programmable control logic unit and (c) an integrated circuit chip, namely a high-voltage high-current darlington transistor array ULN2003A,to driver the motor windings, mounted on a solder-less breadboard.

The ULN chip’s job is to handle power sourcing from an external supply (typically 12 or 24VDC) to the logic controller (often using 5VDC). Think of it as a high-power electronic switch boards with some power safety built-in. It is not a good idea to connect motors to logic boards directly because inductive loads interfere (or may burn sensitive electronics due to power spikes) on the logic side. The schematic below is a bit on the sketchy side of life because of meh isolation between power sources and also it draws too much current for the robot supply. Anyway it is provided for illustration of how you can drive loads using transistors which are electronic rather than mechanical switches.

Coding Time

//-- Motor Windings
//--
#define B0 2
#define B1 3
#define B2 4
#define B3 7

#define EN A5

//-- Pulse Stepping Options
//--
#define FULL 2048
#define HALF 4096
#define WAIT 2

//-- Shortcut Digital Out
//--
#define DO( b, v ) digitalWrite( b, v );

//-- Motor State Variables
//--
int MotorPosition = 0;
int MotorRotation = 1;

//-- Initialization
//--
void setup( ) 
{
  //-- Start PC Communications
  //--
  Serial.begin( 9600 );
  
  //-- Define Output Wires
  //--
  pinMode( B0, OUTPUT ); 
  pinMode( B1, OUTPUT ); 
  pinMode( B2, OUTPUT ); 
  pinMode( B3, OUTPUT ); 

  //-- Enable Pin
  //--
  pinMode( EN, INPUT  ); 
}

//-- Control Loop
//--
void loop() 
{
  //-- Robot Controlled
  //--
  if( digitalRead( EN ) )
  {
    //-- Step 4000 forward,  wait 2sec 
    //-- Step 4000 backward, wait 2sec 
    //-- 
    MotorHalf(  4000 ); delay( 2000 ); 
    MotorHalf( -4000 ); delay( 2000 ); 
  }
}

//-- Rotate Motor by Count Steps
//--
void MotorFull( int count )
{
  if( count >= 0 )
  {
    MotorRotation =  1;
  }
  else
  {
    MotorRotation = -1;
    count = -count;
  }

  for( int index = 0; index < count; index++ )
  {
    MotorFullStep( );
  }
}

//-- Rotate Motor by Count Steps
//--
void MotorHalf( int count )
{
  if( count >= 0 )
  {
    MotorRotation =  1;
  }
  else
  {
    MotorRotation = -1;
    count = -count;
  }

  for( int index = 0; index < count; index++ )
  {
    MotorHalfStep( );
  }
}

//-- Full Step Pulse Sequence
//--
void MotorFullStep( )
{
  switch( MotorPosition )
  {
    case 0: DO( B0, 1 ); DO( B1, 1 ); DO( B2, 0 ); DO( B3, 0 ); break;    
    case 1: DO( B0, 0 ); DO( B1, 1 ); DO( B2, 1 ); DO( B3, 0 ); break; 
    case 2: DO( B0, 0 ); DO( B1, 0 ); DO( B2, 1 ); DO( B3, 1 ); break; 
    case 3: DO( B0, 1 ); DO( B1, 0 ); DO( B2, 0 ); DO( B3, 1 ); break; 
  }  
  MotorNext( 4 - 1 );
  delay( WAIT );
}

//-- Half Step Pulse Sequence
//--
void MotorHalfStep( )
{
  switch( MotorPosition )
  {
    case 0: DO( B0, 0 ); DO( B1, 0 ); DO( B2, 0 ); DO( B3, 1 ); break;    
    case 1: DO( B0, 0 ); DO( B1, 0 ); DO( B2, 1 ); DO( B3, 1 ); break; 
    case 2: DO( B0, 0 ); DO( B1, 0 ); DO( B2, 1 ); DO( B3, 0 ); break; 
    case 3: DO( B0, 0 ); DO( B1, 1 ); DO( B2, 1 ); DO( B3, 0 ); break; 
    case 4: DO( B0, 0 ); DO( B1, 1 ); DO( B2, 0 ); DO( B3, 0 ); break; 
    case 5: DO( B0, 1 ); DO( B1, 1 ); DO( B2, 0 ); DO( B3, 0 ); break; 
    case 6: DO( B0, 1 ); DO( B1, 0 ); DO( B2, 0 ); DO( B3, 0 ); break; 
    case 7: DO( B0, 1 ); DO( B1, 0 ); DO( B2, 0 ); DO( B3, 1 ); break; 
  }  
  MotorNext( 8 - 1 );
  delay( WAIT );
}

//-- Set All Outputs Bits
//--
void MotorNext( int bits )
{
  if( MotorRotation > 0 )
  {
   ++MotorPosition;
   if( MotorPosition > bits )
   {
     MotorPosition = 0;
   }
  }
  else
  {
   --MotorPosition;
   if( MotorPosition < 0 )
   {
     MotorPosition = bits;
   }
  }
}