CKS-Shield-1-Test Arduino Sketch

This Arduino sketch exercises the standard hardware of the CKS-1-Shield board. It provides a console interface for reading inputs and triggering outputs, and a set of asynchronous state machines for generating test motions.

Console Interface

static void parse_user_input(int argc, char *argv[])

Process an input message tokenized from a line of console input. New commands may be defined by adding additional clauses into the if-else structures. The message is passed in argv, an array of argc pointers to strings of, one per token.

void poll_console_input(unsigned long elapsed)

Polling function to process messages arriving over the serial port. Each iteration through this polling function processes at most one character. It records the input message line into a buffer while simultaneously dividing it into ‘tokens’ delimited by whitespace. Each token is a string of non-whitespace characters, and might represent either a symbol or an integer. Once a message is complete, parse_user_input() is called. The argument elapsed contains the number of microseconds elapsed since last update.

Motion Generators

bool running = true

Global flag to enable the autonomous demo motions.

void poll_servos(unsigned long interval)

Polling function to update the servo motion generator state machine. This should be called as frequently as possible. The argument interval provides the number of microseconds elapsed since the last update.

void poll_beeps(unsigned long interval)

Polling function to update the tone generator state machine. This should be called as frequently as possible. The argument interval provides the number of microseconds elapsed since the last update.

void poll_motors(unsigned long interval)

Polling function to update the DC motor motion generator state machine. This should be called as frequently as possible. The argument interval provides the number of microseconds elapsed since the last update.

Full Source Code

The full code is all in one file CKS-Shield-1-Test.ino.

  1/// @file CKS-Shield-1-Test.ino
  2/// @brief Arduino program for demonstrating and testing the CKS Shield 1 hardware.
  3
  4/// @copyright No copyright, 2019, Garth Zeglin.  This file is explicitly placed in the public domain.
  5
  6/****************************************************************/
  7// Library imports.
  8#include <Servo.h>
  9
 10/****************************************************************/
 11/**** Hardware pin assignments **********************************/
 12/****************************************************************/
 13
 14// The following pin assignments correspond to the hardware on the CKS Shield 1 Rev A board.
 15// More hardware details can be found at https://courses.ideate.cmu.edu/16-223/f2019/text/resources/CKS-1-Shield.html
 16
 17// Analog inputs.
 18const int PHOTO1_PIN  = A0; /// photo-interrupter input, value decreases when object present
 19const int PHOTO2_PIN  = A1; /// photo-interrupter input, value decreases when object present
 20const int SWITCH1_PIN = A2; /// active-low switch input
 21const int SWITCH2_PIN = A3; /// active-low switch input
 22const int SWITCH3_PIN = A4; /// active-low switch input
 23const int SWITCH4_PIN = A5; /// active-low switch input
 24
 25// If the optional ADXL335 accelerometer is used, it replaces switches 3,4,5.
 26// N.B. If the optional I2C interface is used, A4 and A5 are not available.
 27const int ACCELX_PIN  = A3;
 28const int ACCELY_PIN  = A4;
 29const int ACCELZ_PIN  = A5;
 30
 31// Digital outputs. D0 and D1 are reserved for use as serial port RX/TX.
 32
 33// Please note that many of these pins have alternate connections or may be used
 34// as GPIO; please refer to the documentation and specific configuration of your
 35// board.
 36
 37const int RX0_PIN         = 0;  /// Serial receive (normally from USB, optionally from TTL Serial connector)
 38const int TX0_PIN         = 1;  /// Serial transmit(normally to USB, optionally to TTL Serial connector)
 39const int SERVO4_PIN      = 2;  /// servo PWM output
 40const int MOSFET1_PIN     = 3;  /// active-high MOSFET driver output
 41const int SERVO3_PIN      = 4;  /// servo PWM output
 42const int MOTA_PWM1_PIN   = 5;  /// DRV8833 dual H-bridge AIN1 (motor A, pin 1)
 43const int MOTA_PWM2_PIN   = 6;  /// DRV8833 dual H-bridge AIN2 (motor A, pin 2)
 44const int SERVO2_PIN      = 7;  /// servo PWM output
 45const int SERVO1_PIN      = 8;  /// servo PWM output
 46const int MOTB_PWM1_PIN   = 9;  /// DRV8833 dual H-bridge BIN1 (motor B, pin 1)
 47const int MOTB_PWM2_PIN   = 10; /// DRV8833 dual H-bridge BIN2 (motor B, pin 2)
 48const int MOSFET2_PIN     = 11; /// active-high MOSFET driver output
 49const int LED1_PIN        = 12; /// active-low LED on "LED1" connector, or GPIO
 50const int ONBOARD_LED_PIN = 13; /// active-high LED on Arduino
 51
 52// If using a WS2801 digital LED strand on the SPI output, the following will displace other usage:
 53const int STRAND_DATA_PIN = 11; /// WS2801 LED strand data output (yellow wire on strand) (SPI MOSI)
 54const int STRAND_CLK_PIN  = 13; /// WS2801 LED strand clock output (green wire on strand) (SPI SCK)
 55
 56/****************************************************************/
 57/**** Global variables and constants ****************************/
 58/****************************************************************/
 59
 60// The baud rate is the number of bits per second transmitted over the serial port.
 61const long BAUD_RATE = 115200;
 62
 63Servo servo1;
 64Servo servo2;
 65Servo servo3;
 66Servo servo4;
 67
 68bool running = true;  ///< Global flag to enable the autonomous demo motions.
 69
 70/****************************************************************/
 71/**** Standard entry points for Arduino system ******************/
 72/****************************************************************/
 73
 74/// Standard Arduino initialization function to configure the system.  This
 75/// function is called once after reset to initialize the program.
 76void setup()
 77{
 78  // configure the actuator pins as soon as possible.
 79  pinMode(MOSFET1_PIN, OUTPUT);
 80  pinMode(MOSFET2_PIN, OUTPUT);
 81
 82  pinMode(MOTA_PWM1_PIN, OUTPUT);
 83  pinMode(MOTA_PWM2_PIN, OUTPUT);
 84
 85  pinMode(MOTB_PWM1_PIN, OUTPUT);
 86  pinMode(MOTB_PWM2_PIN, OUTPUT);
 87    
 88  pinMode(ONBOARD_LED_PIN, OUTPUT);
 89  pinMode(LED1_PIN,        OUTPUT);  
 90
 91  digitalWrite(MOSFET1_PIN, LOW);
 92  digitalWrite(MOSFET2_PIN, LOW);
 93
 94  digitalWrite(MOTA_PWM1_PIN, LOW);
 95  digitalWrite(MOTA_PWM2_PIN, LOW);
 96
 97  digitalWrite(MOTB_PWM1_PIN, LOW);
 98  digitalWrite(MOTB_PWM2_PIN, LOW);
 99    
100  digitalWrite(ONBOARD_LED_PIN, LOW);
101  digitalWrite(LED1_PIN,        HIGH);
102
103  servo1.attach(SERVO1_PIN);
104  servo2.attach(SERVO2_PIN);
105  servo3.attach(SERVO3_PIN);
106  servo4.attach(SERVO4_PIN);
107
108  // initialize the Serial port for the user debugging console
109  Serial.begin(BAUD_RATE);
110
111  // send a message as a diagnostic
112  Serial.println(__FILE__ " awake.");
113}
114
115/****************************************************************/
116/// Standard Arduino polling function to handle all I/O and periodic processing.
117/// This function is called repeatedly as fast as possible from within the
118/// built-in library to poll program events.  This loop should never be allowed
119/// to stall or block so that all tasks can be constantly serviced.
120void loop()
121{
122  // The timestamp in microseconds for the last polling cycle, used to compute
123  // the exact interval between output updates.
124  static unsigned long last_update_clock = 0;
125
126  // Read the microsecond clock.
127  unsigned long now = micros();
128
129  // Compute the time elapsed since the last poll.  This will correctly handle wrapround of
130  // the 32-bit long time value given the properties of twos-complement arithmetic.
131  unsigned long interval = now - last_update_clock;
132  last_update_clock = now;
133
134  // Begin the polling cycle.
135  poll_console_input(interval);
136  poll_servos(interval);
137  poll_beeps(interval);
138  poll_motors(interval);
139}
140
141/****************************************************************/
142/****************************************************************/
143// Configure the control lines for one half of a DRV8833 driver.
144// @param pwm  Motor PWM value ranging from -255 to 255, with zero representing stopped.
145// @param IN1_PIN  Integer pin value for either AIN1 or BIN1.
146// @param IN2_PIN  Integer pin value for either AIN2 or BIN2.
147static void set_motor_pwm(int pwm, int IN1_PIN, int IN2_PIN)
148{
149  if (pwm < 0) {  // reverse speeds
150    analogWrite(IN1_PIN, -pwm);
151    digitalWrite(IN2_PIN, LOW);
152
153  } else { // stop or forward
154    digitalWrite(IN1_PIN, LOW);
155    analogWrite(IN2_PIN, pwm);
156  }
157}
158/****************************************************************/
159// Wrapper on strcmp for clarity of code.  Returns true if strings are
160// identical.
161static int string_equal(char *str1, const char str2[])
162{
163  return !strcmp(str1, str2);
164}
165
166/****************************************************************/
167/// Process an input message tokenized from a line of console input.
168/// New commands may be defined by adding additional clauses into the if-else structures.
169///   @param argc   number of argument tokens
170///   @param argv   array of pointers to strings, one per token
171static void parse_user_input(int argc, char *argv[])
172{
173  // Interpret the first token as a command symbol.
174  char *command = argv[0];
175
176  /* -- process zero-argument commands --------------------------- */
177  if (argc == 1) {
178    if (string_equal(command, "run")) {
179      running = true;
180    }
181
182    else if (string_equal(command, "stop")) {
183      running = false;      
184    }
185
186    else if (string_equal(command, "inputs")) {
187      Serial.println("Raw sensor values:");
188      Serial.print("A0: "); Serial.println(analogRead(A0));
189      Serial.print("A1: "); Serial.println(analogRead(A1));
190      Serial.print("A2: "); Serial.println(analogRead(A2));
191      Serial.print("A3: "); Serial.println(analogRead(A3));
192      Serial.print("A4: "); Serial.println(analogRead(A4));
193      Serial.print("A5: "); Serial.println(analogRead(A5));
194    }
195
196    else {
197      Serial.println("unrecognized command.");
198    }
199  }
200
201  /* -- process one-argument commands --------------------------- */
202  else if (argc == 2) {
203    long value = atol(argv[1]);
204
205    // Set the onboard LED on or off.
206    if (string_equal(command, "led")) {
207      digitalWrite(ONBOARD_LED_PIN, value);
208    }
209    
210    else if (string_equal(command, "ma")) {
211      set_motor_pwm(value, MOTA_PWM1_PIN, MOTA_PWM2_PIN);
212    }
213
214    else if (string_equal(command, "mb")) {
215      set_motor_pwm(value, MOTB_PWM1_PIN, MOTB_PWM2_PIN);
216    }
217
218    else if (string_equal(command, "beep")) {
219      switch(value) {
220      case 1: tone(MOSFET1_PIN, 440, 50); break;
221      case 2: tone(MOSFET2_PIN, 660, 50); break;	
222      }
223    }
224
225    else {
226      Serial.println("unrecognized single-argument command.");
227    }
228  }
229  /* -- process two-argument commands --------------------------- */
230  else if (argc == 3) {
231    long value1 = atol(argv[1]);
232    long value2 = atol(argv[2]);
233
234    if (string_equal(command, "sv")) {
235      switch(value1) {
236      case 1: servo1.write(value2); break;
237      case 2: servo2.write(value2); break;
238      case 3: servo3.write(value2); break;
239      case 4: servo4.write(value2); break;
240      }
241    }
242
243    else {
244      Serial.println("unrecognized two-argument command.");
245    }
246  }
247
248  else {
249    Serial.println("unrecognized command format.");
250  }
251}
252
253/****************************************************************/
254/// Polling function to process messages arriving over the serial port.  Each
255/// iteration through this polling function processes at most one character.  It
256/// records the input message line into a buffer while simultaneously dividing it
257/// into 'tokens' delimited by whitespace.  Each token is a string of
258/// non-whitespace characters, and might represent either a symbol or an integer.
259/// Once a message is complete, parse_user_input() is called.
260///
261/// @param elapsed number of microseconds elapsed since last update
262void poll_console_input(unsigned long elapsed)
263{
264  const int MAX_LINE_LENGTH = 80;  // The maximum message line length.
265  const int MAX_TOKENS = 10;       // The maximum number of tokens in a single message.
266
267  static char input_buffer[MAX_LINE_LENGTH];   // buffer for input characters
268  static char *argv[MAX_TOKENS];                 // buffer for pointers to tokens
269  static int chars_in_buffer = 0;  // counter for characters in buffer
270  static int chars_in_token = 0;   // counter for characters in current partially-received token (the 'open' token)
271  static int argc = 0;             // counter for tokens in argv
272  static int error = 0;            // flag for any error condition in the current message
273
274  (void) elapsed;  // no-op to suppress compiler warning
275
276  // Check if at least one byte is available on the serial input.
277  if (Serial.available()) {
278    int input = Serial.read();
279
280    // If the input is a whitespace character, end any currently open token.
281    if (isspace(input)) {
282      if (!error && chars_in_token > 0) {
283	if (chars_in_buffer == MAX_LINE_LENGTH) error = 1;
284	else {
285	  input_buffer[chars_in_buffer++] = 0;  // end the current token
286	  argc++;                               // increase the argument count
287	  chars_in_token = 0;                   // reset the token state
288	}
289      }
290
291      // If the whitespace input is an end-of-line character, then pass the message buffer along for interpretation.
292      if (input == '\r' || input == '\n') {
293
294	// if the message included too many tokens or too many characters, report an error
295	if (error) Serial.println("excessive input error");
296
297	// else process any complete message
298	else if (argc > 0) parse_user_input(argc, argv);
299
300	// reset the full input state
301	error = chars_in_token = chars_in_buffer = argc = 0;
302      }
303    }
304
305    // Else the input is a character to store in the buffer at the end of the current token.
306    else {
307      // if beginning a new token
308      if (chars_in_token == 0) {
309
310	// if the token array is full, set an error state
311	if (argc == MAX_TOKENS) error = 1;
312
313	// otherwise save a pointer to the start of the token
314	else argv[ argc ] = &input_buffer[chars_in_buffer];
315      }
316
317      // the save the input and update the counters
318      if (!error) {
319	if (chars_in_buffer == MAX_LINE_LENGTH) error = 1;
320	else {
321	  input_buffer[chars_in_buffer++] = input;
322	  chars_in_token++;
323	}
324      }
325    }
326  }
327}
328/****************************************************************/
329// Demo state machine to create a pattern of motion on the servos.
330
331/// Countdown for the polling timer, in microseconds.
332long servo_timer = 0;
333
334/// Interval between updates, in microseconds.  Defaults to 20 Hz.
335long servo_interval = 50000;
336
337/// Servo demo motion frame counter.
338long servo_frame = 0;
339
340/// Polling function to update the servo motion generator state machine.  This should be called as frequently as possible.
341/// @param interval number of microseconds elapsed since last update
342void poll_servos(unsigned long interval)
343{
344  servo_timer -= interval;
345  if (servo_timer < 0) {
346    servo_timer += servo_interval;
347    if (running) {
348      servo_frame = servo_frame + 1;
349      
350      // create a series of wave-like movements across all four servos
351      int servo     = (servo_frame / 20) % 4;
352      int direction = (servo_frame / 80) % 2;
353      int angle     = (servo_frame % 20) * 9;
354
355      if (direction) angle = 180 - angle;
356      switch(servo) {
357      case 0: servo1.write(angle); break;
358      case 1: servo2.write(angle); break;
359      case 2: servo3.write(angle); break;
360      case 3: servo4.write(angle); break;
361      }
362    }
363  }
364}
365
366/****************************************************************/
367// Demo state machine to create a pattern of beeps.
368
369/// Countdown for the polling timer, in microseconds.
370long beep_timer = 0;
371
372/// Interval between updates, in microseconds.  Defaults to once every pi seconds.
373long beep_interval = 3141592;
374
375/// Beep melody position.
376int beep_note = 0;
377
378/// Beep melody table.  Pitches are in Hz.
379const int beep_freq[] = {262, 294, 330, 349, 392, 440, 494, 523, 0};
380
381/// Polling function to update the tone generator state machine.  This should be called as frequently as possible.
382/// @param interval number of microseconds elapsed since last update
383void poll_beeps(unsigned long interval)
384{
385  beep_timer -= interval;
386  if (beep_timer < 0) {
387    beep_timer += beep_interval;
388    
389    if (running) {
390      int freq = beep_freq[beep_note];
391      tone(MOSFET1_PIN, freq, 25);
392
393      beep_note = beep_note + 1;
394      if (beep_freq[beep_note] == 0) beep_note = 0;
395    }
396  }
397}
398
399/****************************************************************/
400// Demo state machine to create a pattern of motion on the motors.
401
402/// Countdown for the polling timer, in microseconds.
403long motor_timer = 0;
404
405/// Interval between updates, in microseconds.  Defaults to 20 Hz.
406long motor_interval = 50000;
407
408/// Motor demo motion frame counter.
409long motor_frame = 0;
410
411/// Polling function to update the DC motor motion generator state machine.  This should be called as frequently as possible.
412/// @param interval number of microseconds elapsed since last update
413void poll_motors(unsigned long interval)
414{
415  motor_timer -= interval;
416  if (motor_timer < 0) {
417    motor_timer += motor_interval;
418    if (running) {
419      motor_frame = motor_frame + 1;
420      
421      // create a series of wave-like movements across both motors
422      int phase_a   = (motor_frame % 50);
423      int phase_b   = (motor_frame % 70);
424      int pwm_a     = map(phase_a, 0, 50, -255, 255);
425      int pwm_b     = map(phase_b, 0, 70, -255, 255);
426      set_motor_pwm(constrain(pwm_a, -255, 255), MOTA_PWM1_PIN, MOTA_PWM2_PIN);
427      set_motor_pwm(constrain(pwm_b, -255, 255), MOTB_PWM1_PIN, MOTB_PWM2_PIN);
428    }
429  }
430}
431
432/****************************************************************/