triangula.arduino: Controlling the Arduino

The Raspberry Pi is great, but sometimes you need to work closer to the hardware than is comfortable on a computer with a full operating system. In Triangula’s case, we have three wheels, each of which has a quadrature encoder on the motor shaft to measure rotation. Quadrature encoders work by sending two streams of on / off signals, by monitoring these streams and detecting changes we can calculate exactly where Triangula’s wheels are. However, to do that we have to be able to monitor six rapidly changing inputs - if we tried to do this with the Python code we’d certainly end up missing some of them, and that would make our wheel positions inaccurate. No good.

To solve this, Triangula has an Arduino - in contrast to the Pi, the Arduino is extremely simple, it’s really just a single microcontroller (a much simpler processor than the ARM chip on the Pi) and the bare minimum needed to make that chip work. The lovely thing about the Arduino is that it doesn’t have an operating system, sd-card, display, network, or really anything that makes a modern computer useful. The reason this is a lovely thing, rather than a drawback, is that it means there is nothing else running on the chip; when we run code on the Pi there’s all sorts of other stuff happening behind the scenes that we can’t see, when we run code on the Arduino we know exactly what’s happening, so it’s a great environment to run code which has to handle fast data processing, read analogue values (the Arduino has built-in analogue input), and other low level stuff.

So, in the case of our wheel encoders, the Arduino is responsible for the low level monitoring of the encoder data, and for interpreting it and working out a wheel position, and the Pi then needs to be able to get that calculated position from the Arduino. To do this we use the I2C bus through the smbus module. This is a very low-level interface, in that all it allows you to do is send and receive bytes of data from devices connected to the I2C bus. To make this more friendly the triangula.arduino.Arduino class exists, wrapping up the low level calls to the smbus library, and thence to the Arduino, into things that look more like sensible Python function calls.

Python Code

This code runs on the Pi and sends messages to the Arduino over I2C

class triangula.arduino.Arduino(arduino_address=112, i2c_delay=0.01, max_retries=30, bus_id=1)[source]

Handles communication over I2C with the Arduino, exposing methods which can be used to read e.g. wheel encoder positions, and to set wheel speeds, light colours etc.

Client code based on documentation at http://www.raspberry-projects.com/pi/programming-in-python/i2c-programming-in-python/using-the-i2c-interface-2

The logic for programming the Arduino to respond to these messages is implemented elsewhere, and based on the code at http://dsscircuits.com/articles/arduino-i2c-slave-guide.

DEVICE_ENCODERS_READ = 34

Register used to copy the current encoder values into the read buffer on the Arduino

DEVICE_LIGHTS_SET = 33

Register used to set the lights to a constant colour

DEVICE_MOTORS_SET = 32

Register used to set motor power

__init__(arduino_address=112, i2c_delay=0.01, max_retries=30, bus_id=1)[source]

Create a new client, uses the I2C bus to communicate with the arduino.

Parameters:
  • arduino_address – 7-bit address of the arduino on the I2C bus
  • i2c_delay – Delay in seconds, used when we either retry transactions or inbetween immediately consecutive read and write operations. This attempts to mitigate problems we’ve had where transactions have failed, probably because the arduino is still handling the previous read / write mode and gets confused.
  • max_retries – The maximum number of retries to attempt when an IOError occurs during a transaction.
  • bus_id – The bus ID used when initialising the smbus library. For the Pi2 this is 1, others may be on bus 0.
get_encoder_values()[source]

Read data from the encoders, returning as a triple of what would be a uint16 if we had such things.

Returns:Triple of encoder values for each wheel.
set_lights(hue, saturation, value)[source]

Set all the neopixel lights to a constant value.

Parameters:
  • hue – 0-255 hue
  • saturation – 0-255 saturation
  • value – 0-255 value
set_motor_power(a, b, c)[source]

Set motor power, writing values directly to the Syren controllers. Power values range from -1.0 to 1.0, where positive values correspond to increasing encoder counts and clockwise rotation when viewed from the outside of the robot looking inwards.

Parameters:
  • a (float) – Wheel a power, -1.0 to 1.0. Pink pylon.
  • b (float) – Wheel a power, -1.0 to 1.0. Yellow pylon.
  • c (float) – Wheel a power, -1.0 to 1.0. Green pylon.
triangula.arduino.check_byte(b)[source]

Clamp the supplied value to an integer between 0 and 255 inclusive

Parameters:b – A number
Returns:Integer representation of the number, limited to be between 0 and 255
triangula.arduino.compute_checksum(register, data)[source]

Calculate a checksum for the specified data and register, simply by XORing each byte in turn with the current value of the checksum. The value is initialised to be the register, then each byte of the data array is used in turn. This is intended to inter-operate with the code on the Arduino, it’s not a particularly generic checksum routine and won’t work with arbitrary I2C devices.

Parameters:
  • register (int) – The register to which we’re sending the data. This is used because the first part of the data transaction is actually the register, we want to calculate the checksum including the register part.
  • data (int[]) – The data array to use
Returns:

A checksum, this should be appended onto the data sent to the Arduino

triangula.arduino.float_to_byte(f)[source]

Map a float from -1.0 to 1.0 onto the range 0-255 and return as a byte-compatible number. Used when sending bytes for e.g. the speed controllers.

Parameters:f – A float, between -1.0 and 1.0. Values outside this range will not behave sensibly.
Returns:An integer where 128 is equivalent to 0, 0 to -1.0 and 255 to +1.0

Arduino Code

This code runs on the Arduino and receives messages from the Pi over I2C

The main code running on the Arduino. This is responsible for monitoring the wheel encoders, sending power values to the Syren10 motor drivers, and for setting colours on the attached LEDs.

Triangula_Main.ino
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
// Comment this out to entirely disable the motor control functions. Handy when testing and you
// really don't want the robot to vanish off into the distance mid-test.
#define ENABLE_MOTOR_FUNCTIONS

/*
   Simple sketch to directly control the Syren drivers. Send an I2C message containing 3 bytes, one
   for each motor, these are converted to values in the range -128 to 127 and sent to the motor drivers.
   Also handles lighting and reporting of encoder values in response to a bulk data request.
*/
#include <Adafruit_NeoPixel.h>
#include "Triangula_NeoPixel.h"
#include <Wire.h>
#ifdef ENABLE_MOTOR_FUNCTIONS
#include <Sabertooth.h>
#endif

// Address to receive messages from the I2C master
#define SLAVE_ADDRESS  0x70

// Command codes
#define MOTOR_SPEED_SET 0x20
#define SET_SOLID_COLOUR 0x21
#define ENCODER_READ 0x22

// Register map array size in bytes
#define REG_MAP_SIZE   6
// Maximum length of a command
#define MAX_SENT_BYTES 5

// Track absolute encoder values, these are unsigned ints and can (and will) roll over. The difference()
// function handles these cases properly. Note that these are volatile as they're updated on pin change
// interrupts, when reading them you should disable interrupts or risk reading half-way through an update.
volatile unsigned int pos_a = 0;
volatile unsigned int pos_b = 0;
volatile unsigned int pos_c = 0;

byte registerMap[REG_MAP_SIZE];
byte receivedCommands[MAX_SENT_BYTES];
volatile boolean newDataAvailable = false;

#ifdef ENABLE_MOTOR_FUNCTIONS
// Motor drivers, must be configured in packet serial mode with addresses 128, 129 and 130
Sabertooth ST[3] = { Sabertooth(130), Sabertooth(129), Sabertooth(128) };
#endif

// Lights
Triangula_NeoPixel pixels = Triangula_NeoPixel();

void setup() {
  pixels.begin();
  Serial.begin(9600);
  Wire.begin(SLAVE_ADDRESS);
  Wire.onRequest(requestEvent);
  Wire.onReceive(receiveEvent);
  // Start up connection to Syren motor controllers, setting baud rate
  // then waiting two seconds to allow the drivers to power up and sending
  // the auto-calibration signal to all controllers on the same bus
#ifdef ENABLE_MOTOR_FUNCTIONS
  SabertoothTXPinSerial.begin(9600);
  Sabertooth::autobaud(SabertoothTXPinSerial);
#endif
  // Stop interrupts
  cli();
  // Configure port 0, pins 8-13, as inputs
  // Note - this requires a 10k pullup between 5V and pin 13
  for (int pin = 8; pin <= 13; pin++) {
    pinMode(pin, INPUT);
    digitalWrite(pin, HIGH);
  }
  // Enable interrupts on port 0
  PCICR |= (1 << PCIE0);
  // Un-mask all pins
  PCMSK0 = 63;
  // Enable interrupts
  sei();

#ifdef ENABLE_MOTOR_FUNCTIONS
  for (int i = 0; i < 3; i++) {
    ST[i].motor(0);
  }
#endif
  pixels.setSolidColour(170, 255, 60);
  pixels.show();
}


volatile int encoderData[6];
volatile int encoderIndex = 6;

void loop() {
  if (newDataAvailable) {
    newDataAvailable = false;
    uint8_t i2c_command = receivedCommands[0];
    switch (i2c_command) {
      case MOTOR_SPEED_SET:
#ifdef ENABLE_MOTOR_FUNCTIONS
        if (checkCommand(3)) {
          for (int i = 1; i < MAX_SENT_BYTES; i++) {
            registerMap[i - 1] = receivedCommands[i];
          }
          for (int i = 0; i < 3; i++) {
            ST[i].motor(((int)(receivedCommands[i + 1])) - 128);
          }
          setColours(registerMap, REG_MAP_SIZE, 8, 0);
        }
#endif
        break;
      case SET_SOLID_COLOUR:
        if (checkCommand(3)) {
          pixels.setSolidColour(receivedCommands[1], receivedCommands[2], receivedCommands[3]);
          pixels.show();
        }
        break;
      case ENCODER_READ:
        encoderData[0] =  (pos_c & 0xff00) >> 8;
        encoderData[1] =  pos_c & 0xff;
        encoderData[2] =  (pos_b & 0xff00) >> 8;
        encoderData[3] =  pos_b & 0xff;
        encoderData[4] =  (pos_a & 0xff00) >> 8;
        encoderData[5] =  pos_a & 0xff;
        encoderIndex = 0;
        break;
      default:
#ifdef ENABLE_MOTOR_FUNCTIONS
        // Unknown command, stop the motors.
        for (int i = 0; i < 3; i++) {
          ST[i].motor(0);
        }
#endif
        pixels.setSolidColour(0, 255, 50);
        pixels.show();
        break;
    }
  }
}


// Called on I2C data request
void requestEvent() {
  if (encoderIndex < 6) {
    Wire.write(encoderData[encoderIndex++]);
  }
  else {
    Wire.write(0);
  }
}

// Validate a command with x bytes plus a register, implying a checksum byte at recievedCommands[x+1]
boolean checkCommand(uint8_t command_length) {
  uint8_t checksum = 0;
  for (int a = 0; a <= command_length; a++) {
    checksum ^= receivedCommands[a];
  }
  return checksum == receivedCommands[command_length + 1];
}

// Called on I2C data reception
void receiveEvent(int bytesReceived) {
  for (int a = 0; a < bytesReceived; a++) {
    if (a < MAX_SENT_BYTES) {
      receivedCommands[a] = Wire.read();
    }
    else {
      Wire.read();  // if we receive more data then allowed just throw it away
    }
  }
  newDataAvailable = true;
}

// Calculate the difference between two encoder readings, allowing for values which wrap. Will be positive
// if the second reading is larger than the first.
int difference(unsigned int a, unsigned int b) {
  int d = b - a;
  if (abs(d) > 0x7FFFu) {
    d = 0xFFFFu - (b + a);
  }
  return d;
}

/*
  Sequence of pulses clockwise is 0,1,3,2
  Assuming that the first state is the two LSB of a four bit value
  So we have the following returning '1'
  0->1 : 4
  1->3 : 13
  3->2 : 11
  2->0 : 2
  In the opposite direction we have 2,3,1,0
  2->3 : 14
  3->1 : 7
  1->0 : 1
  0->2 : 8
  All other values should return 0 as they don't represent valid state transitions
*/
const int encoderValues[] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};

// Track previous encoder states
byte encoder_a;
byte encoder_b;
byte encoder_c;

// Handle pin change interrupts
ISR (PCINT0_vect) {
  byte last_read = PINB;
  byte a = (last_read & 48) >> 4;
  byte b = (last_read & 12) >> 2;
  // The wires are swapped over on encoder c (the green pylon) so we need to swap things!
  byte c = last_read & 3;
  pos_a += encoderValues[a + (encoder_a << 2)];
  pos_b += encoderValues[b + (encoder_b << 2)];
  pos_c -= encoderValues[c + (encoder_c << 2)];
  encoder_a = a;
  encoder_b = b;
  encoder_c = c;
}

// Set colours based on the signal received from the Pi
void setColours(byte hues[], int hueCount, int pixelsPerValue, int fromPixel) {
  int pixel = fromPixel;
  for (int i = 0; i < hueCount; i++) {
    uint32_t colour_a = pixels.hsvToColour(hues[i], 255, 150);
    uint32_t colour_b = pixels.hsvToColour(hues[(i + 1) % hueCount], 255, 150);
    for (int j = 0; j < pixelsPerValue; j++) {
      uint32_t colour = pixels.interpolate(colour_a, colour_b, ((float)j) / ((float)pixelsPerValue));
      pixels.setPixelColor(pixel++, colour);
    }
  }
  pixels.show();
}

Triangula_NeoPixel

A simple extension to the AdaFruit NeoPixel library to handle colours in HSV space rather than RGB, and to manage a slightly higher-level view on the neopixel strips.

Triangula_NeoPixel.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
  Triangula_NeoPixel.h - Library to control the NeoPixel LEDs on Triangula
  Created by Tom Oinn, December 2, 2015
  Apache Software License
*/
#ifndef Triangula_NeoPixel_h
#define Triangula_NeoPixel_h

#include <Adafruit_NeoPixel.h>

class Triangula_NeoPixel : public Adafruit_NeoPixel {
  public:
    Triangula_NeoPixel();
    uint32_t interpolate(uint32_t colour_a, uint32_t colour_b, float i);
    uint32_t hsvToColour(uint8_t h, uint8_t s, uint8_t v);
    void setCameraRing(uint8_t intensity);
    void setPylon(uint8_t pylonIndex, uint8_t saturation, uint8_t value, uint8_t hue_top, uint8_t hue_bottom, uint8_t mask);
    void setSolidColour(uint8_t hue, uint8_t saturation, uint8_t value);
  private:
    uint8_t Red(uint32_t colour);
    uint8_t Green(uint32_t colour);
    uint8_t Blue(uint32_t colour);
};

#endif
Triangula_NeoPixel.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include "Triangula_NeoPixel.h"

// Three 8 pixel rods and a 24 pixel ring around the camera
#define LED_COUNT 48
// Connected to arduino pin 6
#define LED_PIN   6

Triangula_NeoPixel::Triangula_NeoPixel():
  Adafruit_NeoPixel(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800) {
};

// Returns the Red component of a 32-bit colour
uint8_t Triangula_NeoPixel::Red(uint32_t colour) {
  return (colour >> 16) & 0xFF;
};

// Returns the Green component of a 32-bit colour
uint8_t Triangula_NeoPixel::Green(uint32_t colour) {
  return (colour >> 8) & 0xFF;
};

// Returns the Blue component of a 32-bit colour
uint8_t Triangula_NeoPixel::Blue(uint32_t colour) {
  return colour & 0xFF;
};

// Interpolate two 32-bit colours
uint32_t Triangula_NeoPixel::interpolate(uint32_t colour_a, uint32_t colour_b, float i) {
  if (i < 0.0 || i > 1.0) {
    i = 0.0;
  }
  float j = 1.0 - i;
  return Color(Red(colour_a) * j + Red(colour_b) * i,
               Green(colour_a) * j + Green(colour_b) * i,
               Blue(colour_a) * j + Blue(colour_b) * i);
};

// Get a 32-bit colour from a triple of hue, saturation and value
uint32_t Triangula_NeoPixel::hsvToColour(uint8_t hue, uint8_t sat, uint8_t val) {
  unsigned char region, remainder, p, q, t;
  uint16_t h = (hue + 256) % 256;
  uint16_t s = sat;
  uint16_t v = val;
  if (s > 255) s = 255;
  if (v > 255) v = 255;
  else v = (v * v) >> 8;
  if (s == 0) return Color(v, v, v);
  region = h / 43;
  remainder = (h - (region * 43)) * 6;
  p = (v * (255 - s)) >> 8;
  q = (v * (255 - ((s * remainder) >> 8))) >> 8;
  t = (v * (255 - ((s * (255 - remainder)) >> 8))) >> 8;
  switch (region) {
    case 0:
      return Color(v, p, t);
    case 1:
      return Color(q, p, v);
    case 2:
      return Color(p, t, v);
    case 3:
      return Color(p, v, q);
    case 4:
      return Color(t, v, p);
  }
  return Color(v, q, p);
};

void Triangula_NeoPixel::setCameraRing(uint8_t intensity) {
};

void Triangula_NeoPixel::setPylon(uint8_t pylonIndex, uint8_t saturation, uint8_t value, uint8_t hue_top, uint8_t hue_bottom, uint8_t mask) {
};

void Triangula_NeoPixel::setSolidColour(uint8_t hue, uint8_t saturation, uint8_t value) {
  uint32_t colour = this->hsvToColour(hue, saturation, value);
  for (int i = 0; i < LED_COUNT; i++) {
    this->setPixelColor(i, colour);
  }
};