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.

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
    
  }
}
/****************************************************************/