ValveControl Arduino Sketch¶
This sketch controls four pairs of fill-empty pneumatic valves, eight individual channels in all. These can be used to drive four single-chamber pneumatic actuators or two push-pull cylinders.
The full code for the sketch spans several files; all files may be downloaded in a single archive file as ValveControl.zip, browsed in raw form in the source folder, or browsed below.
Contents
Main¶
The main entry points and event loop are in file ValveControl.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 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 | /// \file ValveControl.ino
///
/// \brief Hardware I/O driver for controlling pneumatic valves using a simple message protocol.
///
/// \copyright Copyright (c) 2014-2020, Garth Zeglin. All rights
/// reserved. Licensed under the terms of the BSD 3-clause license.
///
/// \details This example is intended as a starting point for creating custom
/// firmware for driving one or more pneumatic actuators. It includes a
/// simple ASCII protocol for sending motion commands to ease
/// connecting to dynamic code (e.g. Max or Python) running on a laptop
/// or Raspberry Pi.
#include "Pneumatic.h"
#include "Joint.h"
/****************************************************************/
/**** ASCII messaging scheme ************************************/
/****************************************************************/
// The message protocol is based on commands encoded as a sequence of string
// tokens and integers in a line of text. One line is one message. All the
// input message formats begin with a string naming a specific command or
// destination followed by one or two argument integers. The output formats are
// similar but include more general debugging output with a variable number of
// tokens.
// The following message formats are recognized by this program.
// Command Arguments Meaning
// ping query whether the controller is running
// stop set all valve channels to a neutral state
// led <value> controls the built-in LED, value is 0 or non-zero
// speed <axis> <value> sets the PWM rate and direction; axis is an integer from 1 to N, value ranges over -100 to 100
// pos <axis> <value> sets the joint position target; axis is an integer from 1 to N, value is in degrees
// This program generates the following messages:
// Command Arguments Meaning
// awake initialization has completed or ping was received
// led <value> reply with current LED state
/****************************************************************/
/**** Global variables and constants ****************************/
/****************************************************************/
// The baud rate is the number of bits per second transmitted over the serial port.
#define BAUD_RATE 115200
// Some versions of the Arduino IDE don't correctly define this symbol for an
// Arduino Uno. Note that this pin definition is potentially shared with
// SPINDLE_DIR_PIN.
#ifndef LED_BUILTIN
#define LED_BUILTIN 13
#endif
/****************************************************************/
/// Property specification for each pneumatic axis. This is used to define an
/// initialization table for the specific connected hardware.
struct axis_config_t {
int8_t fill, empty;
enum Pneumatic::pneumatic_config_t config;
};
#if 1
// Declare the hardware configuration for a valve card in the valve stack rack.
static struct axis_config_t config_table[] = {
{ 2, 3, Pneumatic::FILL_EMPTY },
{ 4, 5, Pneumatic::FILL_EMPTY },
{ 6, 7, Pneumatic::FILL_EMPTY },
{ 8, 9, Pneumatic::FILL_EMPTY }
};
#endif
#if 0
// Declare the hardware configuration for the 'instructor station'.
static struct axis_config_t config_table[] = {
// { 3, 5, Pneumatic::PROP_FILL_PROP_EMPTY },
// { 6, 9, Pneumatic::PROP_FILL_PROP_EMPTY },
{ 3, 5, Pneumatic::FILL_EMPTY },
{ 6, 9, Pneumatic::FILL_EMPTY },
{ 2, 4, Pneumatic::FILL_EMPTY },
{ 7, -1, Pneumatic::FILL_THREEWAY },
{ 8, -1, Pneumatic::FILL_THREEWAY }
};
#endif
/// Compute the number of entries in the axis table in use.
#define NUM_CHANNELS (sizeof(config_table) / sizeof(struct axis_config_t))
/// Control object for each pneumatic channel. The declaration
/// statically initializes the global state objects for the
/// channels. Note that this does not initialize the hardware;
/// that is performed in setup().
static Pneumatic channel[NUM_CHANNELS];
/// Define the number of physical axes to control.
#define NUM_JOINTS 2
/// Control object for each closed-loop joint controller. These objects manage
/// the sensor input for each joint and can generate control signals to one or
/// more Pneumatic objects. The declaration statically initializes the global
/// state objects for the axes. Note that this does not initialize the
/// hardware; that is performed in setup().
static Joint controller[NUM_JOINTS];
/****************************************************************/
/****************************************************************/
/// Process an input message received from the USB port.
/// Unrecognized commands are silently ignored.
/// \param argc number of argument tokens
/// \param argv array of pointers to strings, one per token
static void vc_parse_input_message(int argc, char *argv[])
{
// Interpret the first token as a command symbol.
char *command = argv[0];
/* -- process zero-argument commands --------------------------- */
if (argc == 1) {
if ( string_equal( command, "ping" )) {
send_message("awake");
} else if (string_equal(command, "stop")) {
// stop all feedback control and close off all manifolds
for (int i = 0; i < NUM_JOINTS; i++) controller[i].setMode(Joint::IDLE);
for (int i = 0; i < NUM_CHANNELS; i++) channel[i].setDutyCycle(0.0);
} else if (string_equal(command, "empty")) {
// stop all feedback control and empty all manifolds
for (int i = 0; i < NUM_JOINTS; i++) controller[i].setMode(Joint::IDLE);
for (int i = 0; i < NUM_CHANNELS; i++) channel[i].setDutyCycle(-1.0);
}
}
/* -- process one-argument commands --------------------------- */
else if (argc == 2) {
long value = atol(argv[1] );
// Process the 'led' command.
if ( string_equal( command, "led" )) {
#ifdef LED_BUILTIN
// turn on the LED if that value is true, then echo it back as a handshake
digitalWrite(LED_BUILTIN, (value != 0) ? HIGH : LOW);
#endif
send_message( "led", value );
}
}
/* -- process two-argument commands --------------------------- */
else if (argc == 3) {
if ( string_equal( command, "speed" )) {
int axis = atoi(argv[1]);
int value = atoi(argv[2]);
// takes an integer value between -100 and 100
if (axis > 0 && axis <= NUM_CHANNELS) channel[axis-1].setDutyCycle( 0.01 * value );
}
else if ( string_equal( command, "pos" )) {
int axis = atoi(argv[1]); // joint number starting with 1
int value = atoi(argv[2]); // in degrees
if (axis > 0 && axis <= NUM_JOINTS) {
controller[axis-1].setMode(Joint::POSITION);
controller[axis-1].setTargetPosition((float) value);
}
}
else if ( string_equal( command, "offset" )) {
int axis = atoi(argv[1]); // joint number starting with 1
int value = atoi(argv[2]); // in raw ADC units
if (axis > 0 && axis <= NUM_JOINTS) controller[axis-1].setOffset(value);
}
else if ( string_equal( command, "scale" )) {
int axis = atoi(argv[1]); // joint number starting with 1
float value = atof(argv[2]); // in degree/raw ADC units
if (axis > 0 && axis <= NUM_JOINTS) controller[axis-1].setScale(value);
}
}
}
/****************************************************************/
/// Polling function to update all background control tasks.
static void vc_hardware_poll(long interval)
{
for (int i = 0; i < NUM_JOINTS; i++) {
controller[i].update(interval);
}
for (int i = 0; i < NUM_CHANNELS; i++) {
channel[i].update(interval);
}
}
/****************************************************************/
/**** Standard entry points for Arduino system ******************/
/****************************************************************/
/// Standard Arduino initialization function to configure the system. This code
/// will need to be modified to initialize the channel[] array with Pneumatic
/// objects matching the specific valve and manifold configuration, and the
/// controller[] array with the physical axis specifications.
void setup(void)
{
// Initialize the array of axis control objects and valve I/O as soon as possible.
for (int i = 0; i < NUM_CHANNELS; i++) {
channel[i] = Pneumatic(config_table[i].fill, config_table[i].empty, config_table[i].config);
channel[i].begin();
}
// Initialize a closed loop controller. The scale and offset were calculated empirically and
// will need to be adjusted for every individual joint.
// single-ended test:
// controller[0] = Joint(Joint::SINGLE_FILL_EMPTY, &channel[0], NULL, A0, -0.338, 608);
// Our default joints are double-ended:
controller[0] = Joint(Joint::DUAL_FILL_EMPTY, &channel[0], &channel[1], A0, 0.338, 512);
controller[1] = Joint(Joint::DUAL_FILL_EMPTY, &channel[2], &channel[3], A1, 0.338, 512);
#ifdef LED_BUILTIN
pinMode( LED_BUILTIN, OUTPUT );
#endif
// initialize the Serial port
Serial.begin( BAUD_RATE );
// additional hardware configuration can go here
// send a wakeup message
send_message("awake");
}
/****************************************************************/
/// Standard Arduino polling function to handle all I/O and periodic processing.
/// This function is called repeatedly as fast as possible from within the
/// built-in library to poll program events. This loop should never be allowed
/// to stall or block so that all tasks can be constantly serviced.
void loop()
{
// The timestamp in microseconds for the last polling cycle, used to compute
// the exact interval between output updates.
static unsigned long last_update_clock = 0;
// Read the microsecond clock.
unsigned long now = micros();
// Compute the time elapsed since the last poll. This will correctly handle wrapround of
// the 32-bit long time value given the properties of twos-complement arithmetic.
unsigned long interval = now - last_update_clock;
last_update_clock = now;
// Begin the polling cycle.
vc_hardware_poll(interval);
vc_serial_input_poll(interval);
vc_status_output_poll(interval);
// other polled tasks can go here
}
/****************************************************************/
void vc_status_output_poll(long interval)
{
static long timer = 0;
const long period = 100000; // 10 Hz
// Wait for the next status output event time point.
timer -= interval;
if (timer <= 0) {
timer += period;
// turn off data stream
#if 0
controller[0].send_status();
Serial.print(" ");
controller[1].send_status();
Serial.println();
#endif
}
}
/****************************************************************/
|