CKS-Shield-1-Test Arduino Sketch

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

Top-Level Functions

void setup()

Standard Arduino initialization function to configure the system. This function is called once after reset to initialize the program.

void loop()

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.

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.

Parameters
  • argc: number of argument tokens

  • argv: array of pointers to strings, 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.

Parameters
  • elapsed: 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.

Parameters
  • interval: number of microseconds elapsed since 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.

Parameters
  • interval: number of microseconds elapsed since 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.

Parameters
  • interval: number of microseconds elapsed since last update

Full Source Code

The full code is all in one file CKS-Shield-1-Test.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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
/// @file CKS-Shield-1-Test.ino
/// @brief Arduino program for demonstrating and testing the CKS Shield 1 hardware.

/// @copyright No copyright, 2019, Garth Zeglin.  This file is explicitly placed in the public domain.

/****************************************************************/
// Library imports.
#include <Servo.h>

/****************************************************************/
/**** Hardware pin assignments **********************************/
/****************************************************************/

// The following pin assignments correspond to the hardware on the CKS Shield 1 Rev A board.
// More hardware details can be found at https://courses.ideate.cmu.edu/16-223/f2019/text/resources/CKS-1-Shield.html

// Analog inputs.
const int PHOTO1_PIN  = A0; /// photo-interrupter input, value decreases when object present
const int PHOTO2_PIN  = A1; /// photo-interrupter input, value decreases when object present
const int SWITCH1_PIN = A2; /// active-low switch input
const int SWITCH2_PIN = A3; /// active-low switch input
const int SWITCH3_PIN = A4; /// active-low switch input
const int SWITCH4_PIN = A5; /// active-low switch input

// If the optional ADXL335 accelerometer is used, it replaces switches 3,4,5.
// N.B. If the optional I2C interface is used, A4 and A5 are not available.
const int ACCELX_PIN  = A3;
const int ACCELY_PIN  = A4;
const int ACCELZ_PIN  = A5;

// Digital outputs. D0 and D1 are reserved for use as serial port RX/TX.

// Please note that many of these pins have alternate connections or may be used
// as GPIO; please refer to the documentation and specific configuration of your
// board.

const int RX0_PIN         = 0;  /// Serial receive (normally from USB, optionally from TTL Serial connector)
const int TX0_PIN         = 1;  /// Serial transmit(normally to USB, optionally to TTL Serial connector)
const int SERVO4_PIN      = 2;  /// servo PWM output
const int MOSFET1_PIN     = 3;  /// active-high MOSFET driver output
const int SERVO3_PIN      = 4;  /// servo PWM output
const int MOTA_PWM1_PIN   = 5;  /// DRV8833 dual H-bridge AIN1 (motor A, pin 1)
const int MOTA_PWM2_PIN   = 6;  /// DRV8833 dual H-bridge AIN2 (motor A, pin 2)
const int SERVO2_PIN      = 7;  /// servo PWM output
const int SERVO1_PIN      = 8;  /// servo PWM output
const int MOTB_PWM1_PIN   = 9;  /// DRV8833 dual H-bridge BIN1 (motor B, pin 1)
const int MOTB_PWM2_PIN   = 10; /// DRV8833 dual H-bridge BIN2 (motor B, pin 2)
const int MOSFET2_PIN     = 11; /// active-high MOSFET driver output
const int LED1_PIN        = 12; /// active-low LED on "LED1" connector, or GPIO
const int ONBOARD_LED_PIN = 13; /// active-high LED on Arduino

// If using a WS2801 digital LED strand on the SPI output, the following will displace other usage:
const int STRAND_DATA_PIN = 11; /// WS2801 LED strand data output (yellow wire on strand) (SPI MOSI)
const int STRAND_CLK_PIN  = 13; /// WS2801 LED strand clock output (green wire on strand) (SPI SCK)

/****************************************************************/
/**** Global variables and constants ****************************/
/****************************************************************/

// The baud rate is the number of bits per second transmitted over the serial port.
const long BAUD_RATE = 115200;

Servo servo1;
Servo servo2;
Servo servo3;
Servo servo4;

bool running = true;  ///< Global flag to enable the autonomous demo motions.

/****************************************************************/
/**** Standard entry points for Arduino system ******************/
/****************************************************************/

/// Standard Arduino initialization function to configure the system.  This
/// function is called once after reset to initialize the program.
void setup()
{
  // configure the actuator pins as soon as possible.
  pinMode(MOSFET1_PIN, OUTPUT);
  pinMode(MOSFET2_PIN, OUTPUT);

  pinMode(MOTA_PWM1_PIN, OUTPUT);
  pinMode(MOTA_PWM2_PIN, OUTPUT);

  pinMode(MOTB_PWM1_PIN, OUTPUT);
  pinMode(MOTB_PWM2_PIN, OUTPUT);
    
  pinMode(ONBOARD_LED_PIN, OUTPUT);
  pinMode(LED1_PIN,        OUTPUT);  

  digitalWrite(MOSFET1_PIN, LOW);
  digitalWrite(MOSFET2_PIN, LOW);

  digitalWrite(MOTA_PWM1_PIN, LOW);
  digitalWrite(MOTA_PWM2_PIN, LOW);

  digitalWrite(MOTB_PWM1_PIN, LOW);
  digitalWrite(MOTB_PWM2_PIN, LOW);
    
  digitalWrite(ONBOARD_LED_PIN, LOW);
  digitalWrite(LED1_PIN,        HIGH);

  servo1.attach(SERVO1_PIN);
  servo2.attach(SERVO2_PIN);
  servo3.attach(SERVO3_PIN);
  servo4.attach(SERVO4_PIN);

  // initialize the Serial port for the user debugging console
  Serial.begin(BAUD_RATE);

  // send a message as a diagnostic
  Serial.println(__FILE__ " 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.
  poll_console_input(interval);
  poll_servos(interval);
  poll_beeps(interval);
  poll_motors(interval);
}

/****************************************************************/
/****************************************************************/
// Configure the control lines for one half of a DRV8833 driver.
// @param pwm  Motor PWM value ranging from -255 to 255, with zero representing stopped.
// @param IN1_PIN  Integer pin value for either AIN1 or BIN1.
// @param IN2_PIN  Integer pin value for either AIN2 or BIN2.
static void set_motor_pwm(int pwm, int IN1_PIN, int IN2_PIN)
{
  if (pwm < 0) {  // reverse speeds
    analogWrite(IN1_PIN, -pwm);
    digitalWrite(IN2_PIN, LOW);

  } else { // stop or forward
    digitalWrite(IN1_PIN, LOW);
    analogWrite(IN2_PIN, pwm);
  }
}
/****************************************************************/
// Wrapper on strcmp for clarity of code.  Returns true if strings are
// identical.
static int string_equal(char *str1, const char str2[])
{
  return !strcmp(str1, str2);
}

/****************************************************************/
/// 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.
///   @param argc   number of argument tokens
///   @param argv   array of pointers to strings, one per token
static void parse_user_input(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, "run")) {
      running = true;
    }

    else if (string_equal(command, "stop")) {
      running = false;      
    }

    else if (string_equal(command, "inputs")) {
      Serial.println("Raw sensor values:");
      Serial.print("A0: "); Serial.println(analogRead(A0));
      Serial.print("A1: "); Serial.println(analogRead(A1));
      Serial.print("A2: "); Serial.println(analogRead(A2));
      Serial.print("A3: "); Serial.println(analogRead(A3));
      Serial.print("A4: "); Serial.println(analogRead(A4));
      Serial.print("A5: "); Serial.println(analogRead(A5));
    }

    else {
      Serial.println("unrecognized command.");
    }
  }

  /* -- process one-argument commands --------------------------- */
  else if (argc == 2) {
    long value = atol(argv[1]);

    // Set the onboard LED on or off.
    if (string_equal(command, "led")) {
      digitalWrite(ONBOARD_LED_PIN, value);
    }
    
    else if (string_equal(command, "ma")) {
      set_motor_pwm(value, MOTA_PWM1_PIN, MOTA_PWM2_PIN);
    }

    else if (string_equal(command, "mb")) {
      set_motor_pwm(value, MOTB_PWM1_PIN, MOTB_PWM2_PIN);
    }

    else if (string_equal(command, "beep")) {
      switch(value) {
      case 1: tone(MOSFET1_PIN, 440, 50); break;
      case 2: tone(MOSFET2_PIN, 660, 50); break;	
      }
    }

    else {
      Serial.println("unrecognized single-argument command.");
    }
  }
  /* -- process two-argument commands --------------------------- */
  else if (argc == 3) {
    long value1 = atol(argv[1]);
    long value2 = atol(argv[2]);

    if (string_equal(command, "sv")) {
      switch(value1) {
      case 1: servo1.write(value2); break;
      case 2: servo2.write(value2); break;
      case 3: servo3.write(value2); break;
      case 4: servo4.write(value2); break;
      }
    }

    else {
      Serial.println("unrecognized two-argument command.");
    }
  }

  else {
    Serial.println("unrecognized command format.");
  }
}

/****************************************************************/
/// 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.
///
/// @param elapsed number of microseconds elapsed since last update
void poll_console_input(unsigned long elapsed)
{
  const int MAX_LINE_LENGTH = 80;  // The maximum message line length.
  const int MAX_TOKENS = 10;       // The maximum number of tokens in a single message.

  static char input_buffer[MAX_LINE_LENGTH];   // buffer for input characters
  static char *argv[MAX_TOKENS];                 // buffer for pointers to tokens
  static int chars_in_buffer = 0;  // counter for characters in buffer
  static int chars_in_token = 0;   // counter for characters in current partially-received token (the 'open' token)
  static int argc = 0;             // counter for tokens in argv
  static int error = 0;            // flag for any error condition in the current message

  (void) elapsed;  // no-op to suppress compiler warning

  // Check if at least one byte is available on the serial input.
  if (Serial.available()) {
    int input = Serial.read();

    // If the input is a whitespace character, end any currently open token.
    if (isspace(input)) {
      if (!error && chars_in_token > 0) {
	if (chars_in_buffer == MAX_LINE_LENGTH) error = 1;
	else {
	  input_buffer[chars_in_buffer++] = 0;  // end the current token
	  argc++;                               // increase the argument count
	  chars_in_token = 0;                   // reset the token state
	}
      }

      // If the whitespace input is an end-of-line character, then pass the message buffer along for interpretation.
      if (input == '\r' || input == '\n') {

	// if the message included too many tokens or too many characters, report an error
	if (error) Serial.println("excessive input error");

	// else process any complete message
	else if (argc > 0) parse_user_input(argc, argv);

	// reset the full input state
	error = chars_in_token = chars_in_buffer = argc = 0;
      }
    }

    // Else the input is a character to store in the buffer at the end of the current token.
    else {
      // if beginning a new token
      if (chars_in_token == 0) {

	// if the token array is full, set an error state
	if (argc == MAX_TOKENS) error = 1;

	// otherwise save a pointer to the start of the token
	else argv[ argc ] = &input_buffer[chars_in_buffer];
      }

      // the save the input and update the counters
      if (!error) {
	if (chars_in_buffer == MAX_LINE_LENGTH) error = 1;
	else {
	  input_buffer[chars_in_buffer++] = input;
	  chars_in_token++;
	}
      }
    }
  }
}
/****************************************************************/
// Demo state machine to create a pattern of motion on the servos.

/// Countdown for the polling timer, in microseconds.
long servo_timer = 0;

/// Interval between updates, in microseconds.  Defaults to 20 Hz.
long servo_interval = 50000;

/// Servo demo motion frame counter.
long servo_frame = 0;

/// Polling function to update the servo motion generator state machine.  This should be called as frequently as possible.
/// @param interval number of microseconds elapsed since last update
void poll_servos(unsigned long interval)
{
  servo_timer -= interval;
  if (servo_timer < 0) {
    servo_timer += servo_interval;
    if (running) {
      servo_frame = servo_frame + 1;
      
      // create a series of wave-like movements across all four servos
      int servo     = (servo_frame / 20) % 4;
      int direction = (servo_frame / 80) % 2;
      int angle     = (servo_frame % 20) * 9;

      if (direction) angle = 180 - angle;
      switch(servo) {
      case 0: servo1.write(angle); break;
      case 1: servo2.write(angle); break;
      case 2: servo3.write(angle); break;
      case 3: servo4.write(angle); break;
      }
    }
  }
}

/****************************************************************/
// Demo state machine to create a pattern of beeps.

/// Countdown for the polling timer, in microseconds.
long beep_timer = 0;

/// Interval between updates, in microseconds.  Defaults to once every pi seconds.
long beep_interval = 3141592;

/// Beep melody position.
int beep_note = 0;

/// Beep melody table.  Pitches are in Hz.
const int beep_freq[] = {262, 294, 330, 349, 392, 440, 494, 523, 0};

/// Polling function to update the tone generator state machine.  This should be called as frequently as possible.
/// @param interval number of microseconds elapsed since last update
void poll_beeps(unsigned long interval)
{
  beep_timer -= interval;
  if (beep_timer < 0) {
    beep_timer += beep_interval;
    
    if (running) {
      int freq = beep_freq[beep_note];
      tone(MOSFET1_PIN, freq, 25);

      beep_note = beep_note + 1;
      if (beep_freq[beep_note] == 0) beep_note = 0;
    }
  }
}

/****************************************************************/
// Demo state machine to create a pattern of motion on the motors.

/// Countdown for the polling timer, in microseconds.
long motor_timer = 0;

/// Interval between updates, in microseconds.  Defaults to 20 Hz.
long motor_interval = 50000;

/// Motor demo motion frame counter.
long motor_frame = 0;

/// Polling function to update the DC motor motion generator state machine.  This should be called as frequently as possible.
/// @param interval number of microseconds elapsed since last update
void poll_motors(unsigned long interval)
{
  motor_timer -= interval;
  if (motor_timer < 0) {
    motor_timer += motor_interval;
    if (running) {
      motor_frame = motor_frame + 1;
      
      // create a series of wave-like movements across both motors
      int phase_a   = (motor_frame % 50);
      int phase_b   = (motor_frame % 70);
      int pwm_a     = map(phase_a, 0, 50, -255, 255);
      int pwm_b     = map(phase_b, 0, 70, -255, 255);
      set_motor_pwm(constrain(pwm_a, -255, 255), MOTA_PWM1_PIN, MOTA_PWM2_PIN);
      set_motor_pwm(constrain(pwm_b, -255, 255), MOTB_PWM1_PIN, MOTB_PWM2_PIN);
    }
  }
}

/****************************************************************/