Saturday 3 September 2011

The Power Supply

At some stage, I'm going to need to deliver more than 1A into the 12V rail for the motors so I'm contemplating using a PC ATX PSU to drive all my equipment.

I just found a nifty article on Instructibles.com about converting an ATX PSU into a general purpose lab bench PSU here.

Came with a very nice little circuit diagram that I'm pinching...
Of course, there is a great Wikipedia entry for ATX PSUs.

Arduino Servo Points Controller Software

First, a Disclaimer
  • I freely admit that this code is not exactly pretty. However, it does work and is sufficiently bug free.
  • It needs comments and should be refactored to use a class for the Points struct.
  • It's not been code reviewed and I'd be happy for your gracious comments.
Overview

There are only 2 files - Points.h and the main Arduino sketch. I was going to work it all into one file but there are some (known) bugs in the Arduino IDE that forced me to move some of my typedefs into the header file. The bug in the IDE si to do with the fact that the main sketch file is not parsed in strict line number order (somehow!).

The basic concept for this software is that I have an array of Points each of which is controlled by a well defined state machine. The system is essentially a table driven finite state machine with the context of where each point is "up to" stored in the Point data structure.

Feel free to pinch this code.

Points.h

#ifndef    POINTS_H
#define    POINTS_H    1

/*
 * Points.h - my arduino driven switch machine software….
 */

typedef enum  { cal_st, cal_sw, moving_sw, moving_st, 

                idle_sw, idle_st
} PointState ;

typedef enum  { cal_enter, cal_exit, mov_straight, 

                mov_switch, set_pt_changed, pt_tick 
} PointEvent ;



typedef struct  {
  int  ID;
  int  servo_pin;
  int  calib_pin;
  int  control_pin;
  int  set_pt_pin;
  int  eeprom_sw_addr;
  int  eeprom_st_addr;
 
  int  straight_angle;
  int  switch_angle;
 
  int  calib;
  int  control;
  int  set_point;

  int  current_angle;
  int  dest_angle;
 
  PointState  current_state;
 
  Servo point_servo;

} Point;

typedef    struct    {
    PointState    current;
    PointEvent    event;
    PointState    next;
    void          (*transition)( Point* p);
} PointStateElement;


#endif    POINTS_H

And the main Arduino Sketch

#include <Servo.h>
#include <EEPROM.h>
#include "<insert path>/Points.h"

#define  NUM_POINTS          4

#define  SVO_START_PIN       10  // The first pin to which a servo is connected
#define  CTL_START_PIN       6   // The first pin to be used for controlling the points
#define  CAL_START_PIN       2   // The first pin to be used for controlling calibration
#define  SET_PT_PIN          0   // The analogue pin to be used for reading the set point
#define  PT_EEPROM_ST        0   // The first address in the EEPROM for point settings to be kept

//#define  SER_OUT             1   // If defined, serial debug output is compiled in

#define  SERVO_DFT_ANG       90  // Where the servo will sit if it is uncalibrated
#define  SERVO_MIN_ANG       70
#define  SERVO_MAX_ANG       110

#define  IsAngleValid(a)     (((a) >= SERVO_MIN_ANG) && ((a) <= SERVO_MAX_ANG))

#define  PT_POS_STRAIGHT     1
#define  PT_POS_SWITCH       0
#define  PT_CALIBRATE        0
#define  PT_OPERATE          1

void
Points_error(Point* p)
{
#ifdef  SER_OUT
  Serial.print(p->ID);
  Serial.println(" error.");
#endif
}

void
Points_nop(Point* p)
{

}

void
Points_write_cal_st(Point* p)
{
#ifdef  SER_OUT
  Serial.print(p->ID);
  Serial.println(" writing calibration for straight.");
#endif
  EEPROM.write(p->eeprom_st_addr , p->current_angle);
  p->straight_angle = p->current_angle;
}

void
Points_match_set_pt(Point* p)
{
#ifdef  SER_OUT
  Serial.print(p->ID);
  Serial.println(" match set point.");
#endif
  p->current_angle = p->set_point;
  p->point_servo.write(p->set_point);
}

void
Points_write_cal_sw(Point* p)
{
#ifdef  SER_OUT
  Serial.print(p->ID);
  Serial.println(" writing calibration for switch.");
#endif
  EEPROM.write(p->eeprom_sw_addr , p->current_angle);
  p->switch_angle = p->current_angle;

}

void
Points_start_moving_st(Point* p)
{
#ifdef  SER_OUT
  Serial.print(p->ID);
  Serial.println("start moving straight.");
#endif
  if ( IsAngleValid(p->straight_angle) )
  {
    p->dest_angle = p->straight_angle;
  }
  else
  {
    p->point_servo.write(SERVO_DFT_ANG - 2);
    p->point_servo.write(SERVO_DFT_ANG + 2);
    p->point_servo.write(SERVO_DFT_ANG);
#ifdef  SER_OUT
  Serial.print(p->ID);
  Serial.println(" straight is uncalibrated");
#endif 
  }
 
}

void
Points_start_moving_sw(Point* p)
{
#ifdef  SER_OUT
  Serial.print(p->ID);
  Serial.println(" start moving switch.");
#endif
  if ( IsAngleValid(p->switch_angle) )
  {
    p->dest_angle = p->switch_angle;
  }
  else
  {
    p->point_servo.write(SERVO_DFT_ANG - 2);
    p->point_servo.write(SERVO_DFT_ANG + 2);
    p->point_servo.write(SERVO_DFT_ANG);
#ifdef  SER_OUT
  Serial.print(p->ID);
  Serial.println(" switch is uncalibrated");
#endif
  }
}

void
Points_move_step(Point* p)
{
#ifdef  SER_OUT
  Serial.print(p->ID);
  Serial.println(" move_step.");
#endif
   
    if(p->current_angle < p->dest_angle)
    {
      p->current_angle ++;
    }
    else if(p->current_angle > p->dest_angle)
    {
      p->current_angle --;
    }
   
    p->point_servo.write(p->current_angle);
    if(p->current_angle == p->dest_angle)
    {
#ifdef  SER_OUT
  Serial.print(p->ID);
  Serial.print(" reached destination ");
  Serial.print(p->dest_angle);
  Serial.print("( ");
  Serial.print(p->switch_angle);
  Serial.print(", ");
  Serial.print(p->straight_angle);
  Serial.println(")");
#endif

      if(p->dest_angle == p->switch_angle)
        p->current_state = idle_sw;
      else
        p->current_state = idle_st;
    }
}


PointStateElement PointStateTable[] = {
  {cal_st,    cal_enter,      cal_st,     Points_error        },
  {cal_st,    cal_exit,       idle_st,    Points_write_cal_st },
  {cal_st,    mov_straight,   cal_st,     Points_error        },
  {cal_st,    mov_switch,     cal_sw,     Points_nop          },
  {cal_st,    set_pt_changed, cal_st,     Points_match_set_pt },
  {cal_st,    pt_tick,        cal_st,     Points_nop          },
  {cal_sw,    cal_enter,      cal_sw,     Points_error        },
  {cal_sw,    cal_exit,       idle_sw,    Points_write_cal_sw },
  {cal_sw,    mov_straight,   cal_st,     Points_nop          },
  {cal_sw,    mov_switch,     cal_sw,     Points_error        },
  {cal_sw,    set_pt_changed, cal_sw,     Points_match_set_pt },
  {cal_sw,    pt_tick,        cal_sw,     Points_nop          },
  {moving_sw, cal_enter,      cal_sw,     Points_match_set_pt },
  {moving_sw, cal_exit,       moving_sw,  Points_error        },
  {moving_sw, mov_straight,   moving_st,  Points_start_moving_st},
  {moving_sw, mov_switch,     moving_sw,  Points_error        },
  {moving_sw, set_pt_changed, moving_sw,  Points_nop          },
  {moving_sw, pt_tick,        moving_sw,  Points_move_step    },
  {moving_st, cal_enter,      cal_st,     Points_match_set_pt },
  {moving_st, cal_exit,       moving_st,  Points_error        },
  {moving_st, mov_straight,   moving_st,  Points_error        },
  {moving_st, mov_switch,     moving_sw,  Points_start_moving_sw},
  {moving_st, set_pt_changed, moving_st,  Points_nop          },
  {moving_st, pt_tick,        moving_st,  Points_move_step    },
  {idle_sw,   cal_enter,      cal_sw,     Points_match_set_pt },
  {idle_sw,   cal_exit,       idle_sw,    Points_error        },
  {idle_sw,   mov_straight,   moving_st,  Points_start_moving_st},
  {idle_sw,   mov_switch,     idle_sw,    Points_error        },
  {idle_sw,   set_pt_changed, idle_sw,    Points_nop          },
  {idle_sw,   pt_tick,        idle_sw,    Points_nop          },
  {idle_st,   cal_enter,      cal_st,     Points_match_set_pt },
  {idle_st,   cal_exit,       idle_st,    Points_error        },
  {idle_st,   mov_straight,   idle_st,    Points_error        },
  {idle_st,   mov_switch,     moving_sw,  Points_start_moving_sw},
  {idle_st,   set_pt_changed, idle_st,    Points_nop          },
  {idle_st,   pt_tick,        idle_st,    Points_nop          }
};

#define  NUM_STATES  (sizeof(PointStateTable)/sizeof(PointStateElement))

int
PointInit(  Point* p, int ID,  int  ser_pin, int  cal_pin, int  con_pin, int  eeprom_addr, int set_pt_pin )
{
  int  val;
 
  p->ID = ID;
  p->servo_pin = ser_pin;
  p->calib_pin = cal_pin;
  p->control_pin = con_pin;
  p->eeprom_sw_addr = eeprom_addr;
  p->eeprom_st_addr = eeprom_addr + 1;
  p->set_pt_pin = set_pt_pin;
 
  p->point_servo.attach(ser_pin);
 
  p->straight_angle = EEPROM.read(p->eeprom_st_addr);
  p->switch_angle = EEPROM.read(p->eeprom_sw_addr);
 
  pinMode(p->calib_pin, INPUT);
  pinMode(p->control_pin, INPUT);
 
  val = analogRead(p->set_pt_pin);
  p->calib = PT_OPERATE;
  p->control = PT_POS_STRAIGHT;
  p->set_point = map(val, 0, 1023, SERVO_MIN_ANG, SERVO_MAX_ANG);
 
  p->current_state = moving_st;
  p->point_servo.write(SERVO_DFT_ANG);
 
  p->current_angle = SERVO_DFT_ANG;
  p->dest_angle = SERVO_DFT_ANG;
 
}


PointEvent
PointGetEvent(  Point* p)
{
  int  setval;
  int  set_pt;
 
  if(( p->calib == PT_CALIBRATE) && (digitalRead(p->calib_pin) == PT_OPERATE))
  {
    p->calib = PT_OPERATE;
    delay(20);        //debounce it
#ifdef  SER_OUT
  Serial.print(p->ID);
  Serial.println("cal_exit");
#endif
    return(cal_exit);
  }
  else if(( p->calib == PT_OPERATE) && (digitalRead(p->calib_pin) == PT_CALIBRATE))
  {
    p->calib = PT_CALIBRATE;
    delay(20);        //debounce it
#ifdef  SER_OUT
  Serial.print(p->ID);
  Serial.println("cal_enter");
#endif
    return(cal_enter);
  }
  if(( p->control == PT_POS_STRAIGHT) && (digitalRead(p->control_pin) == PT_POS_SWITCH))
  {
    p->control = PT_POS_SWITCH;
    delay(20);       //debounce it
#ifdef  SER_OUT
  Serial.print(p->ID);
  Serial.println("mov_switch");
#endif
    return(mov_switch);
  }
  else if(( p->control == PT_POS_SWITCH) && (digitalRead(p->control_pin) == PT_POS_STRAIGHT))
  {
    p->control = PT_POS_STRAIGHT;
    delay(20);       //debounce it
#ifdef  SER_OUT
  Serial.print(p->ID);
  Serial.println("mov_straight");
#endif
    return(mov_straight);
  }
 
  setval = analogRead(p->set_pt_pin);
  set_pt = map(setval, 0, 1023, SERVO_MIN_ANG, SERVO_MAX_ANG);
  if( p->set_point != set_pt)
  {
    p->set_point = set_pt;
    delay(20);
#ifdef  SER_OUT
  Serial.print(p->ID);
  Serial.print("set_pt_changed: ");
  Serial.println(set_pt);
#endif
    return(set_pt_changed);
  }
 
  delay(20);
  return ( pt_tick );
}

void
PointHandleEvent(  Point* p, PointEvent e)
{
    int         i,j;
    PointState  cs;
   
    cs = p->current_state;
    i = 0;
    while( i < NUM_STATES)
    {
      if ((PointStateTable[i].current == cs) && (PointStateTable[i].event == e))
      {
        p->current_state = PointStateTable[i].next;
        (*PointStateTable[i].transition)(p);
        i = NUM_STATES;     
      }
      else
        i++;
    } 
 
}

void
PointRun( Point* p)
{
  PointEvent  e;
 
  e = PointGetEvent(p);
  PointHandleEvent(p,e);
}

Point  PointArray[NUM_POINTS];


void setup()
{
  int  i;

#ifdef  SER_OUT
  Serial.begin(9600);
#endif
 
  for ( i = 0; i < NUM_POINTS; i++)
  {
    PointInit(&PointArray[i], i, SVO_START_PIN + i, CAL_START_PIN + i, CTL_START_PIN + i, PT_EEPROM_ST + i*2, SET_PT_PIN);
  }
 
}

void loop()
{
  int  i;
 
  for ( i = 0; i < NUM_POINTS; i++)
  {
    PointRun(&PointArray[i]);
  }
 
}

Thursday 1 September 2011

Finally, the electronics!

The electronics to control the test layout is actually pretty simple.

A this stage, I'm using a simple home brew power supply. You can see this below. My plan is to replace this with a standard PC power supply which has more than enough grunt to power several trains, all the points, signals, accessories etc.



The point controller is built using a single Arduino board seen below left. There's a lot of grunt here to run just 4 servos but I want to make this a nice independent unit that can be "sold separately". Below right is a prototyping shield that I have used to hook up some jumper pins that I use to plug the servos in to. These were then connected to D10-13.

D0 & D1 I have left for serial I/O. D2-9 are used to control the points. Notice that I have them pulled HIGH simply to ensure reliable operation. Pins D2-5 are used to indicate that the point is being calibrated. More on this later. Pins D6-9 are used to control position (straight or switched). Very simple! Analog Pin 0 is used to connect a potentiometer to set the angles for the servo in the straight or switched positions under calibration.

 

In order to make it easier to connect to this board, I'm using these nifty screw terminal breakouts from DFRobot which I bought at littlebirdelectronics.

Calibration
Below is my home brew calibration board. You must have a way of calibrating this setup and I could have used serial IO but that's not easy for most folks and having a set of buttons and knobs is very intuitive.

Why do I need it? well, mounting servos under the bench is a messy business and there is a lot of slop in the system! Also, what if I want to use this on HO rather than N-scale?

Calibrating is easy. Set the position of the point you want to calibrate, flick it's calibration switch up. Move the knob and the blades of the point will move. Drop the calibration switch and the position is remembered in the EEPROM in the Arduino. Very simple.

Of course, serial control of the points is an easy extension and is on it's way. After all, why wouldn't you?

The photos below show it all hooked together - very nice!



Train Speed and Position
You have seen this in a previous post but I'm including it here in a bit more detail. This is yet another stack of Arduino shields. At the bottom is an Arduino Diecimila (an original form Italy). Then the DFRobot motor shield. This can control 2 motors bi-directional. On top of that is another screw terminal shield and on top of that is a prototype shield which makes it easier to hook up sensors. I'm using these great little analog IR sensors from littlebird. Analog is far better as the triggers can be tuned nicely inside the Arduino.


You can see the motor shield exposed below. 

And here they are all hooked together nicely. The blue wires between the boards allow the Controller system to drive the points. A lot of the cabling was hand soldered but I got really cheap and nicely built extension cables for the sensors and servos of DealExtreme for bugger all cash!


What's next?
  • Source code for the points system - not brilliant but good enough.
  • Controlling 2 separate tracks off this system.
  • Some cute signals I got off eBay.
  • Expanding to 48 digital IO lines.
  • A relay control board for track sections.

Wednesday 31 August 2011

A correction!

Folks, just so you're minds are all set to rest, the cause of the servo motor's making too much noise and needing a lot of recalibration had nothing to do with the servo motors!!

Shock, horror - it was a software bug.

Now fixed and ready to roll.

Monday 29 August 2011

A working test layout controlled by Arduinos

So, finally, I got the software written on my Arduino Point Control system!

What I now have is a system controlled by 2 Arduinos. One for the points and one for the trains and sensors.

You can see the videos of the operations below:
And now for a detailed view of the points/switches/turnouts actually working:

One thing I have found, though, is that these el-cheapo motors (9g servos) from China are not well calibrated and quite weak so I may need to use larger motors that need less fine tuning and make less noise when under load.

Stay tuned. In the next episode I'll be showing you the electronics and how the two systems hook together.