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
-
-
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.
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.
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
|
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);
}
};
|