Coding the Arduino for the N Gauge Layout (version 2)

By Andy Joel

This is a sub-page of Wiring the N Gauge Layout, and focuses on the software in the Arduino.

What is an Arduino?

An Arduino is basically a computer. It is rather more basic than your standard computer, and has no easy way to hook up a keyboard or monitor (use a Raspberry Pi for that), but it is still a computer in that it can be programmed to do complex operations.

Here is an image.

The Arduino is the board sat on the breadboard (the white block) to the left.

It has a line of pins down each side long side, and these all extend into the bread board. Each has a specific use, and is labelled as such. In the image, the red leads are connected to a 5 V supply, and the black to GND (ground), for example.

Arduinos can be programmed using software on a computer, and the cable coming out to the left is a USB connection for that purpose. You can get the software here.

Introduction To Arduino Programming

Arduinos are programming using a language called C++. I think it is actually a reduced version, but that is generally not an issue, because the reduced version has what you need.

C++ is a very common programming language that can create small and fast programs, which is probably why it was chosen.

Arduino programming is a little odd because the program is run numerous times every second. More specifically, it is the loop() method that does that, but that is the important part. You can stop the loop for a certain time using the delay() method,  but that stops everything, and the Arduino will not respond to inputs during a delay, so for our purpose it is to be avoided.

Instead, we have to move things on in increments. Imagine a point needs to change, so the angle on its servo needs adjusting. The Arduino tracks what angle the servo should be at at each moment in time. In the next loop it works out where it should have got to at that moment.

Before it does that, however, it also checks to see if the user has pressed a button, and decides if it has to also react to that.

A System for Controlling Servo

For our system, then, we need to store data about each servo, some of which can change – its current angle (current_angle)and the angle it should be (target_angle). On the other hand, some will be set up in configuration will never change – its “off” position (off_angle) and it “on” position (on_angle), as well as the controller it is connected to, (cluster), and its position on that controller (number).

In the loop()  method, we need to do three things:

  1. Determine the elapsed time.
  2. Check the state of every input, and react to that, modifying the target_angle of affected servos.
  3. Check the state of every servo (i.e., the values for the servo in the Arduino), and if the target_angle is different to the current_angle adjust the current_angle by a small increment, and then set the servo itself to that angle.

It would be great if we had a display on the layout that reports what the Arduino is doing (in addition to saying what the state of the points is). If the back of the layout does not end up looking like the flight deck of the enterprise, I will be very disappointed…

The Code

Here is an early version that so far only handles inputs on the Arduino, not an I2C module. It does handle servos on multiple I2C modules. I am breaking it up into parts to explain what is going on.

Preprocessor directives

The first part is the “preprocessor directives”; these are start with a hash. The first two lines say we want to include two libraries – one for I2C communication, one for servos. The other lines set up constants. This test system has two PCA controllers and they start at address 0x40.

In theory, up to 62 PCA controllers can be connected to the I2C bus, and this software is designed to handle that. You just need to set CONTROLLER_COUNT to the right number. Note that the addresses must be sequential.

Each controller can handle up to 16 servos. Servos do not have to be connected sequentially.  The means you could control up to nearly a thousand servos, but there may be an impact on performance. That said, I would guess that if you never have more than a dozen moving at the same time, it will not be noticeable. I found a cycles was around 5 ms with six going at one.

TIME_FACTOR is a scaling factor applied to all servos, so we can adjust them all in one go. this has to be a float.

#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>

#define CONTROLLER_COUNT  2
#define CONTROLLER_START  0x40
#define TIME_FACTOR 1.0

// These lines taken from example code
#define SERVOMIN  150 // This is the 'minimum' pulse length count
#define SERVOMAX  600 // This is the 'maximum' pulse length count
#define SERVO_FREQ 50 // Analog servos run at ~50 Hz updates

#define WIRE Wire

Global Variables

There are a number of global variables that we will use later.

Global variables are generally frowned upon in computer programs because they can be accessed – and changed – by any part of the program at any time, leading to bugs that are hard to track down. They are, however, necessary when programming Arduinos if you want data accessed both in setup() and loop(), or if you just want it to persist.

The first stores the references to the controllers, and the second is to store the time the previous loop started. Both will have values assigned to them later.

Adafruit_PWMServoDriver pwms[CONTROLLER_COUNT];
unsigned long previous_time;

They are the easy ones…

Switch Data

The next chunk configures the switches – or more accurately links from switches to servos.

Each switch is stored in a struct (short for “data structure”). This is a collection of related data. I am calling it SwitchData, and saying I want there to be three values stored in it; two integers and one Boolean (either true or false).

The next part then uses SwitchData. It sets up an array called switches, of the type SwitchData. The next eight lines then create eight switches with the given data – this is the actual configuration. Note that the order of the data is that defined in the struct, so the first is on pin 5, modifies servo 1 to be turned on, while the second does the same but turns it off.

Note that pins 5 and 6 have two entries – they operate two servos at once.

struct SwitchData {
  int pin;
  int servo;
  bool turnOn;
};

SwitchData switches[] = {
  {5, 1, true},
  {6, 1, false},
  {5, 2, true},
  {6, 2, false},
  {7, 3, true},
  {8, 3, false},
  {9, 4, true},
  {10, 4, false},
  {11, 5, true},
  {12, 5, false},
};

Servo data

I have done this a bit differently, using a Class, rather than a struct. A Class can be thought of as a struct that also has functions as part of it. Note that functions that are part of a Class are called “methods”.

The reason for doing it this way is that I can use a special method called a “constructor” to build the servo record. The current_angle and target_angle can be set from the off_angle, making configuration easier.

This starts in a similar manner to a struct; it has seven fields, all integers.

Classes are kind of secretive; they protect their data, keeping it private. The idea is that you do not want other parts of your program making changes to values in random ways, so those seven values can only be changed by methods inside the class.

When we get to the methods, we want them accessed outside the class, so they are flagged as public.

Then it gets complicated, as we define a number of methods: Servo(), set(), adjust(), update() and status(). And really, all the hard work is being done here.

class Servo {
  int cluster;  // 0 - 5 or so
  int number; // 0 - 15
  int speed;
  int target_angle;
  int current_angle;
  int off_angle;
  int on_angle;

  public:
  Servo(int _cluster, int _number, int _speed, int _off_angle, int _on_angle) {
    cluster = _cluster;
    number = _number;
    speed = _speed;
    off_angle = _off_angle * 100;
    target_angle = _off_angle * 100;
    current_angle = _off_angle * 100;
    on_angle = _on_angle * 100;
  }

  void set(bool turnOn) {
    target_angle = turnOn ? on_angle : off_angle;
  }

  void adjust(int elapsed) {
    int diff = current_angle - target_angle;
    if (diff == 0) return;

    int increment = elapsed * speed;
    // diff is then capped at that
    if (diff > 0) {
      if (diff > speed) diff = increment;  // cap at speed
      current_angle -= diff;
    }
    else {
      if (diff < speed) diff = increment; // cap at speed
      current_angle += diff;
    } 
    update(); 
  } 

  void update() {
    int pulselen = current_angle * (SERVOMAX - SERVOMIN) / 18000 + SERVOMIN;
    if (pulselen > SERVOMAX) {
      Serial.print("ERROR: Too high!!!");
      return;
    }
    if (pulselen < SERVOMIN) {
      Serial.print("ERROR: Too low!!!");
      return;
    }
    pwms[cluster].setPWM(number, 0, pulselen);
  }

  String status() {
    return String(current_angle) + "/" + String(target_angle);
  }

};

We better look at these in detail…

The first method has the same name as the class; this is the “constructor”, and is used when an instance of the class is created. The first line says we expect to be given five values. The internal variables are set from these.

Servo(int _cluster, int _number, int _speed, int _off_angle, int _on_angle) {
    cluster = _cluster;
    number = _number;
    speed = _speed;
    off_angle = _off_angle * 100;
    target_angle = _off_angle * 100;
    current_angle = _off_angle * 100;
    on_angle = _on_angle * 100;
  }

The set() method sets the servo in response to a button being pressed. More specifically, it sets the target_angle – the angle we want the servo to move to. It is passed a Boolean value indicating if we want to move to the “on” position or not.

  void set(bool turnOn) {
    target_angle = turnOn ? on_angle : off_angle;
  }

The adjust() method will be run every loop, and decides if the current_angle needs to be adjusted, and if so by how much. It is passed the time that has passed since the last loop. After changing current_angle it calls update().

  void adjust(int elapsed) {
    int diff = current_angle - target_angle;
    if (diff == 0) return;

    int increment = elapsed * speed;
    // diff is then capped at that
    if (diff > 0) {
      if (diff > speed) diff = increment;  // cap at speed
      current_angle -= diff;
    }
    else {
      if (diff < speed) diff = increment; // cap at speed
      current_angle += diff;
    } 
    update(); 
  }

The update() method is the only one that communicates with the actual servo. It updates the angle of the servo with current_angle.

void update() {
    int pulselen = current_angle * (SERVOMAX - SERVOMIN) / 18000 + SERVOMIN;
    if (pulselen > SERVOMAX) {
      Serial.print("ERROR: Too high!!!");
      return;
    }
    if (pulselen < SERVOMIN) {
      Serial.print("ERROR: Too low!!!");
      return;
    }
    pwms[cluster].setPWM(number, 0, pulselen);
  }

Finally, the status() method is used only for debugging – it returns a string with the current_angle and the target_angle.

  String status() {
    return String(current_angle) + "/" + String(target_angle);
  }

Now each servo is configured. The first one in on controller 0, in position 0. Its speed is 1. Its “off” angle is 0, and its “on” angle is 180. Angles are specified in degrees here, but converted to hundredths of a degree in the constructor. It does not matter if the “off” angle is greater; they can be either way round.

Servo servos[] = {
  Servo(0, 0, 1, 0, 180),
  Servo(0, 1, 3, 10, 120),
  Servo(0, 3, 1, 0, 90),
  Servo(1, 1, 1, 0, 180),
  Servo(1, 2, 4, 0, 180),
  Servo(1, 3, 4, 0, 90),
};

More Globals

A couple more globals are then set up. We have to do them last because they are the size of the arrays and it is best to let the program work out the size – if we add another servo to the list, the number will still be right.

const int servo_count = sizeof(servos)/sizeof(servos[0]);
const int switch_count = sizeof(switches)/sizeof(switches[0]);

Arduino Functions

There are two functions all Arduino code needs.

setup()

The setup() function is run once, when the Arduino is turned on.

The serial interface with the computer is set up – this is useful for diagnostics during development.

The next three lines set up the input pins. This is done in a loop, where i is a counter. Note that arrays count from zero in C++ (in common with most other languages). The mode for each pin is set to INPUT_PULLUP, which is a built-in constant.

The next six lines set up communication with the PCA controllers. Again, this is done in a loop, with i being the count. The actual initialisation is pretty much copied from an example program.

Then another loop, going through each servo, and setting it to the “off” position, so the servos are all in the same state as the software thinks it is.

Then we record the time.

void setup() {
  Wire.begin();
  Serial.begin(9600);

  for (int i = 0; i < switch_count; i++) {
    pinMode(switches[i].pin, INPUT_PULLUP);      // set pin to input
  }

  for (int i = 0; i < CONTROLLER_COUNT; i++) {
    pwms[i] = Adafruit_PWMServoDriver(CONTROLLER_START + i);
    pwms[i].begin();
    pwms[i].setOscillatorFrequency(27000000);
    pwms[i].setPWMFreq(SERVO_FREQ);
  }

  for (int i = 0; i < servo_count; i++) {
    servos[i].update();
  }
  
  previous_time = millis();
}

loop()

The loop() function is where the action happens. This is repeated hundreds of times every second.

loop: Handling time

This is complicated because of the types of numbers involved…

In the Arduino a standard integer can have a value from -32,768 to 32,767. A long integer, however, has twice the storage space, and can be from -2,147,483,648 to 2,147,483,647. The millis() function returns the number of milliseconds since the unit was turned on as an unsigned long integer, which can range from 0 to 4,294,967,295.

void loop() {
  // HANDLE TIME
  unsigned long now_time = millis();
  unsigned long elapsed = now_time - previous_time;
  previous_time = now_time;
  int increment = TIME_FACTOR * elapsed;

However, in practice the elapsed time is only going to be a few milliseconds, so we are safe pushing that into an int. The TIME_FACTOR constant allows us to slow down or speed up all servos in one change.

loop: Handling Inputs

We go though each switch, and see if it is in the LOW state. We set them earlier to use a pull-up resistor, so LOW indicates the switch is closed.

If it is, we use the set() method of the relevant servo to change its target angle.

  // HANDLE INPUTS
  for (int i = 0; i < switch_count; i++) {
    if (digitalRead(switches[i].pin) == LOW) {
      servos[switches[i].servo].set(switches[i].turnOn);
    }
  }

Note that holding down the button will do no harm; you are just repeatedly setting a target_angle. In fact, holding down both the left and the right buttons at the same time will not cause any issues; target_angle will flip back and forth, but we are not adjust the actual servo yet, so whichever switch is later in the list will “win” and the servo will move towards that.

loop: Handling Outputs

Again we have a loop, with i as the counter, but now we are going through every servo. All the hard work is done in the class method adjust().

  for (int i = 0; i < servo_count; i++) {
    servos[i].adjust(increment);
  }
}

 

All Together Now…

The full code is here:

// Servo control via I2C bus version 0.2
// Copyright Andy Joel and Preston&District Model Railway Club
// Documentation here:
// http://www.prestonanddistrictmrs.org.uk/articles/point-control-with-servos/coding-the-arduino-for-the-n-gauge-layout/

#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>

#define CONTROLLER_COUNT  2
#define CONTROLLER_START  0x40
#define TIME_FACTOR 1.0

#define SERVOMIN  150 // This is the 'minimum' pulse length count (out of 4096)
#define SERVOMAX  600 // This is the 'maximum' pulse length count (out of 4096)
#define SERVO_FREQ 50 // Analog servos run at ~50 Hz updates

// Set I2C bus to use: Wire, Wire1, etc.
#define WIRE Wire

Adafruit_PWMServoDriver pwms[CONTROLLER_COUNT];
unsigned long previous_time;
 

struct SwitchData {
  int pin;      // the pin the switch is on
  int servo;    // Number of the sefvo in the list servo_configs below
                // Note that they count from zero!
  bool turnOn;  // If true, will set the servo to the "on" position
};

SwitchData switches[] = {
  {5, 1, true},
  {6, 1, false},
  {5, 2, true},
  {6, 2, false},
  {7, 3, true},
  {8, 3, false},
  {9, 4, true},
  {10, 4, false},
  {11, 5, true},
  {12, 5, false},
};

 
class Servo {
  int cluster;  // 0 - 5 or so
  int number; // 0 - 15
  int speed;
  int target_angle;
  int current_angle;
  int off_angle;
  int on_angle;

  public:
  Servo(int _cluster, int _number, int _speed, int _off_angle, int _on_angle) {
    cluster = _cluster;
    number = _number;
    speed = _speed;
    off_angle = _off_angle * 100;
    target_angle = _off_angle * 100;
    current_angle = _off_angle * 100;
    on_angle = _on_angle * 100;
  }

  void set(bool turnOn) {
    target_angle = turnOn ? on_angle : off_angle;
  }

  void adjust(int elapsed) {
    int diff = current_angle - target_angle;
    if (diff == 0) return;

    int increment = elapsed * speed;
    // diff is then capped at that
    if (diff > 0) {
      if (diff > speed) diff = increment;  // cap at speed
      current_angle -= diff;
    }
    else {
      if (diff < speed) diff = increment;  // cap at speed
      current_angle += diff;
    }
    update();    
  }

  void update() {
    int pulselen = current_angle * (SERVOMAX - SERVOMIN) / 18000 + SERVOMIN;
    if (pulselen > SERVOMAX) {
      Serial.print("ERROR: Too high!!!");
      return;
    }
    if (pulselen < SERVOMIN) {
      Serial.print("ERROR: Too low!!!");
      return;
    }
    pwms[cluster].setPWM(number, 0, pulselen);
  }

  String status() {
    return String(current_angle) + "/" + String(target_angle);
  }

  private:
};

 

 

Servo servos[] = {
  Servo(0, 0, 1, 0, 180),
  Servo(0, 1, 3, 10, 120), // this one does not like to go beyond 120deg!
  Servo(0, 3, 1, 0, 90),
  Servo(1, 1, 1, 0, 180),
  Servo(1, 2, 4, 0, 180),
  Servo(1, 3, 4, 0, 90),
};

 

const int servo_count = sizeof(servos)/sizeof(servos[0]);
const int switch_count = sizeof(switches)/sizeof(switches[0]);

 

void setup() {

  Serial.begin(9600);

  for (int i = 0; i < switch_count; i++) {
    pinMode(switches[i].pin, INPUT_PULLUP);           // set pin to input
  }

  for (int i = 0; i < CONTROLLER_COUNT; i++) {
    pwms[i] = Adafruit_PWMServoDriver(CONTROLLER_START + i);
    pwms[i].begin();
    pwms[i].setOscillatorFrequency(27000000);
    pwms[i].setPWMFreq(SERVO_FREQ);  // Analog servos run at ~50 Hz updates
  }

  for (int i = 0; i < servo_count; i++) {
    servos[i].update();
  }
  
  //while (!Serial); // wait for output to fire up before sending. But what if it is not connected???
  //Serial.println("Found " + String(servo_count) + " servos!");  // does not appear
  previous_time = millis();
}

 

 

void loop() {
  // HANDLE TIME
  unsigned long now_time = millis();
  unsigned long elapsed = now_time - previous_time;
  previous_time = now_time;
  int increment = TIME_FACTOR * elapsed;

 

  // HANDLE INPUTS
  for (int i = 0; i < switch_count; i++) {
    if (digitalRead(switches[i].pin) == LOW) {
      servos[switches[i].servo].set(switches[i].turnOn);
      Serial.println("switch:" + String(i) + " (" + servos[5].status() + ")");
      //Serial.println("servo_count:" + String(servo_count) + " (" + String(i) + ")");
    }
  }

 

  // HANDLE SERVOS
  for (int i = 0; i < servo_count; i++) {
    servos[i].adjust(increment);
  }
 
}