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