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/// \file ValveControl.ino
  2///
  3/// \brief Hardware I/O driver for controlling pneumatic valves using a simple message protocol.
  4///
  5/// \copyright Copyright (c) 2014-2020, Garth Zeglin.  All rights
  6///            reserved. Licensed under the terms of the BSD 3-clause license.
  7///
  8/// \details This example is intended as a starting point for creating custom
  9///          firmware for driving one or more pneumatic actuators. It includes a
 10///          simple ASCII protocol for sending motion commands to ease
 11///          connecting to dynamic code (e.g. Max or Python) running on a laptop
 12///          or Raspberry Pi.
 13
 14#include "Pneumatic.h"
 15#include "Joint.h"
 16
 17/****************************************************************/
 18/**** ASCII messaging scheme ************************************/
 19/****************************************************************/
 20
 21// The message protocol is based on commands encoded as a sequence of string
 22// tokens and integers in a line of text.  One line is one message.  All the
 23// input message formats begin with a string naming a specific command or
 24// destination followed by one or two argument integers.  The output formats are
 25// similar but include more general debugging output with a variable number of
 26// tokens.
 27
 28// The following message formats are recognized by this program.
 29
 30// Command	Arguments		Meaning
 31// ping                                 query whether the controller is running
 32// stop					set all valve channels to a neutral state
 33// led		<value>			controls the built-in LED, value is 0 or non-zero
 34// speed	<axis> <value>		sets the PWM rate and direction; axis is an integer from 1 to N, value ranges over -100 to 100
 35// pos          <axis> <value>		sets the joint position target; axis is an integer from 1 to N, value is in degrees
 36
 37// This program generates the following messages:
 38
 39// Command	Arguments		Meaning
 40// awake                                initialization has completed or ping was received
 41// led		<value>			reply with current LED state
 42
 43/****************************************************************/
 44/**** Global variables and constants ****************************/
 45/****************************************************************/
 46
 47// The baud rate is the number of bits per second transmitted over the serial port.
 48#define BAUD_RATE 115200
 49
 50// Some versions of the Arduino IDE don't correctly define this symbol for an
 51// Arduino Uno.  Note that this pin definition is potentially shared with
 52// SPINDLE_DIR_PIN.
 53#ifndef LED_BUILTIN
 54#define LED_BUILTIN 13
 55#endif
 56
 57/****************************************************************/
 58/// Property specification for each pneumatic axis.  This is used to define an
 59/// initialization table for the specific connected hardware.
 60struct axis_config_t {
 61  int8_t fill, empty;
 62  enum Pneumatic::pneumatic_config_t config;
 63};
 64
 65#if 1
 66
 67// Declare the hardware configuration for a valve card in the valve stack rack.
 68static struct axis_config_t config_table[] = {
 69  { 2, 3, Pneumatic::FILL_EMPTY },
 70  { 4, 5, Pneumatic::FILL_EMPTY },
 71  { 6, 7, Pneumatic::FILL_EMPTY },
 72  { 8, 9, Pneumatic::FILL_EMPTY }
 73};
 74#endif
 75
 76#if 0
 77// Declare the hardware configuration for the 'instructor station'.
 78static struct axis_config_t config_table[] = {
 79  // { 3, 5, Pneumatic::PROP_FILL_PROP_EMPTY },
 80  // { 6, 9, Pneumatic::PROP_FILL_PROP_EMPTY },
 81
 82  { 3, 5, Pneumatic::FILL_EMPTY },
 83  { 6, 9, Pneumatic::FILL_EMPTY },
 84  
 85  { 2, 4, Pneumatic::FILL_EMPTY },
 86  { 7, -1, Pneumatic::FILL_THREEWAY },
 87  { 8, -1, Pneumatic::FILL_THREEWAY }
 88};
 89#endif
 90
 91/// Compute the number of entries in the axis table in use.
 92#define NUM_CHANNELS (sizeof(config_table) / sizeof(struct axis_config_t))
 93
 94/// Control object for each pneumatic channel. The declaration
 95/// statically initializes the global state objects for the
 96/// channels.  Note that this does not initialize the hardware;
 97/// that is performed in setup().
 98static Pneumatic channel[NUM_CHANNELS];
 99
100/// Define the number of physical axes to control.
101#define NUM_JOINTS 2
102
103/// Control object for each closed-loop joint controller.  These objects manage
104/// the sensor input for each joint and can generate control signals to one or
105/// more Pneumatic objects.  The declaration statically initializes the global
106/// state objects for the axes.  Note that this does not initialize the
107/// hardware; that is performed in setup().
108static Joint controller[NUM_JOINTS];
109
110/****************************************************************/
111/****************************************************************/
112/// Process an input message received from the USB port.
113/// Unrecognized commands are silently ignored.
114
115/// \param argc		number of argument tokens
116/// \param argv		array of pointers to strings, one per token
117
118static void vc_parse_input_message(int argc, char *argv[])
119{
120  // Interpret the first token as a command symbol.
121  char *command = argv[0];
122
123  /* -- process zero-argument commands --------------------------- */
124  if (argc == 1) {
125    if ( string_equal( command, "ping" )) {
126      send_message("awake");
127
128    } else if (string_equal(command, "stop")) {
129      // stop all feedback control and close off all manifolds
130      for (int i = 0; i < NUM_JOINTS; i++)   controller[i].setMode(Joint::IDLE);
131      for (int i = 0; i < NUM_CHANNELS; i++) channel[i].setDutyCycle(0.0);
132
133    } else if (string_equal(command, "empty")) {
134      // stop all feedback control and empty all manifolds
135      for (int i = 0; i < NUM_JOINTS; i++)   controller[i].setMode(Joint::IDLE);
136      for (int i = 0; i < NUM_CHANNELS; i++) channel[i].setDutyCycle(-1.0);
137    }
138  }
139
140  /* -- process one-argument commands --------------------------- */
141  else if (argc == 2) {
142    long value = atol(argv[1] );
143
144    // Process the 'led' command.
145    if ( string_equal( command, "led" )) {
146#ifdef LED_BUILTIN
147      // turn on the LED if that value is true, then echo it back as a handshake
148      digitalWrite(LED_BUILTIN, (value != 0) ? HIGH : LOW);
149#endif
150      send_message( "led", value );
151    }
152  }
153
154  /* -- process two-argument commands --------------------------- */
155  else if (argc == 3) {
156    
157    if ( string_equal( command, "speed" )) {
158      int axis = atoi(argv[1]);
159      int value = atoi(argv[2]);
160      // takes an integer value between -100 and 100
161      if (axis > 0 && axis <= NUM_CHANNELS) channel[axis-1].setDutyCycle( 0.01 * value );
162    }
163    else if ( string_equal( command, "pos" )) {
164      int axis = atoi(argv[1]); // joint number starting with 1
165      int value = atoi(argv[2]);  // in degrees
166      if (axis > 0 && axis <= NUM_JOINTS) {
167	controller[axis-1].setMode(Joint::POSITION);
168	controller[axis-1].setTargetPosition((float) value);
169      }
170    }
171    else if ( string_equal( command, "offset" )) {
172      int axis = atoi(argv[1]); // joint number starting with 1
173      int value = atoi(argv[2]);  // in raw ADC units
174      if (axis > 0 && axis <= NUM_JOINTS) controller[axis-1].setOffset(value);
175    }
176
177    else if ( string_equal( command, "scale" )) {
178      int axis = atoi(argv[1]); // joint number starting with 1
179      float value = atof(argv[2]);  // in degree/raw ADC units
180      if (axis > 0 && axis <= NUM_JOINTS) controller[axis-1].setScale(value);
181    }
182  }
183}
184
185/****************************************************************/
186/// Polling function to update all background control tasks.
187
188static void vc_hardware_poll(long interval)
189{
190  for (int i = 0; i < NUM_JOINTS; i++) {
191    controller[i].update(interval);
192  }
193  for (int i = 0; i < NUM_CHANNELS; i++) {
194    channel[i].update(interval);
195  }
196}
197
198/****************************************************************/
199/**** Standard entry points for Arduino system ******************/
200/****************************************************************/
201
202/// Standard Arduino initialization function to configure the system.  This code
203/// will need to be modified to initialize the channel[] array with Pneumatic
204/// objects matching the specific valve and manifold configuration, and the
205/// controller[] array with the physical axis specifications.
206void setup(void)
207{
208  // Initialize the array of axis control objects and valve I/O as soon as possible.
209  for (int i = 0; i < NUM_CHANNELS; i++) {
210    channel[i] = Pneumatic(config_table[i].fill, config_table[i].empty, config_table[i].config);
211    channel[i].begin();
212  }
213
214  // Initialize a closed loop controller. The scale and offset were calculated empirically and
215  // will need to be adjusted for every individual joint.
216
217  // single-ended test:
218  // controller[0] = Joint(Joint::SINGLE_FILL_EMPTY, &channel[0], NULL, A0, -0.338, 608);
219
220  // Our default joints are double-ended:
221  controller[0] = Joint(Joint::DUAL_FILL_EMPTY, &channel[0], &channel[1], A0, 0.338, 512);
222  controller[1] = Joint(Joint::DUAL_FILL_EMPTY, &channel[2], &channel[3], A1, 0.338, 512);
223  
224#ifdef LED_BUILTIN
225  pinMode( LED_BUILTIN, OUTPUT );
226#endif
227
228  // initialize the Serial port
229  Serial.begin( BAUD_RATE );
230
231  // additional hardware configuration can go here
232
233  // send a wakeup message
234  send_message("awake");
235}
236
237/****************************************************************/
238/// Standard Arduino polling function to handle all I/O and periodic processing.
239/// This function is called repeatedly as fast as possible from within the
240/// built-in library to poll program events.  This loop should never be allowed
241/// to stall or block so that all tasks can be constantly serviced.
242void loop()
243{
244  // The timestamp in microseconds for the last polling cycle, used to compute
245  // the exact interval between output updates.
246  static unsigned long last_update_clock = 0;
247  
248  // Read the microsecond clock.
249  unsigned long now = micros();
250
251  // Compute the time elapsed since the last poll.  This will correctly handle wrapround of
252  // the 32-bit long time value given the properties of twos-complement arithmetic.
253  unsigned long interval = now - last_update_clock;
254  last_update_clock = now;
255
256  // Begin the polling cycle.
257  vc_hardware_poll(interval);
258  vc_serial_input_poll(interval);
259  vc_status_output_poll(interval);
260  
261  // other polled tasks can go here
262}
263
264/****************************************************************/
265void vc_status_output_poll(long interval)
266{
267  static long timer = 0;
268  const long period = 100000;  // 10 Hz
269  
270  // Wait for the next status output event time point.
271  timer -= interval;
272  if (timer <= 0) {
273    timer += period;
274
275    // turn off data stream
276#if 0
277    controller[0].send_status();
278    Serial.print(" ");
279    controller[1].send_status();
280    Serial.println();
281#endif
282    
283  }
284}
285/****************************************************************/