PinballGame Arduino Sketch

This sketch provides a pinball machine controller as an extended example of a real-time logic controller utilizing third-party hardware drivers. It is configured to use hardware in the PinballShield circuit board.

This sketch assumes you have already installed several third-party Arduino libraries in your IDE as described in the section Arduino Libraries.

All other sketch files may be downloaded in a single archive file as PinballGame.zip, or browsed in raw form in the source folder.

Key Top-Level Functions

void poll_sensor_inputs(unsigned long interval)

Update all input state, including periodic sensor sampling, debouncing, and filtering.

void poll_game_logic(unsigned long interval)

Update the game state machine, including advancing game state and score and applying modal input-output mappings.

void poll_actuator_outputs(unsigned long interval)

Update all actuator state, including output pulse timers.

C++ Classes

class PinballSensor

Process inputs from a single-channel analog pinball sensor such as a photoreflective pair.

class PopBumper

Control a single pinball solenoid actuator. The primary function is to control timing of the impulsive actuators, but could be expanded to support PWM control for reducing holding currents.

class ToneSpeaker

Play musical tones on a speaker, used for audio game effects.

Main Source Code

The main top-level code is in PinballGame.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
/// \file PinballGame.ino
/// \brief Arduino program demonstrating essential real-time logic for a custom pinball machine.

/// \copyright No copyright, 2016-2017, Garth Zeglin.  This file is explicitly placed in the public domain.

/// \details This example is intended as a starting point for developing a
///          custom pinball controller, demonstrating the use of state machines
///          to implement both I/O and game logic.
///
/// This example is written using Arduino C++ conventions:
///
///  1. state variables are global
///  2. global C++ objects are statically declared and initialized
///  3. the sketch is divided into multiple .ino files which form one compilation unit
///  4. C++ classes in .cpp files are separately compiled

/// This example assumes that several open-source libraries have been installed in the Arduino IDE:
///  1. Adafruit-GFX	general graphics support
///  2. Adafruit-HT1632	LED matrix display driver
///  3. Adafruit-WS2801	LED strand driver

/****************************************************************/
// Library imports.
#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_HT1632.h"
#include "Adafruit_WS2801.h"

// Forward declarations.
#include "PinballSensor.h"
#include "PopBumper.h"
#include "ToneSpeaker.h"
#include "StrandGraphics.h"
#include "MatrixGraphics.h"
#include "console.h"

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

// The following pin assignments correspond to the hardware on the PinballShield
// Rev A board.

// 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 PHOTO3_PIN = A2;  /// photo-interrupter input, value decreases when object present
const int PHOTO4_PIN = A3;  /// photo-interrupter input, value decreases when object present
const int SWITCH1_PIN = A4; /// active-low switch input
const int SWITCH2_PIN = A5; /// active-low switch input

// Digital outputs. D0 and D1 are reserved for use as serial port RX/TX
const int LED1_PIN        = 2;  /// active-low LED output
const int SPEAKER_PIN     = 3;  /// active-high MOSFET-driven speaker output
const int LED2_PIN        = 4;  /// active-low LED output
const int SOLENOID4_PIN   = 5;  /// active-high MOSFET-driven solenoid output
const int SOLENOID3_PIN   = 6;  /// active-high MOSFET-driven solenoid output
const int MATRIX_DATA_PIN = 7;  /// HT1632 LED matrix display data output
const int MATRIX_WR_PIN   = 8;  /// HT1632 LED matrix display clock output
const int SOLENOID2_PIN   = 9;  /// active-high MOSFET-driven solenoid output
const int SOLENOID1_PIN   = 10; /// active-high MOSFET-driven solenoid output
const int STRAND_DATA_PIN = 11; /// WS2801 LED strand data output (yellow wire on strand)
const int MATRIX_CS0_PIN  = 12; /// HT1632 LED matrix display select output
const int STRAND_CLK_PIN  = 13; /// WS2801 LED strand clock output (green wire on strand)

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

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

PinballSensor start_switch(SWITCH1_PIN);
PinballSensor bumper_sensor(PHOTO1_PIN);
PinballSensor drain_sensor(PHOTO2_PIN);

PopBumper     bumper(SOLENOID1_PIN);
ToneSpeaker   speaker(SPEAKER_PIN);

// Initialize the LED hardware.
Adafruit_HT1632LEDMatrix matrix = Adafruit_HT1632LEDMatrix(MATRIX_DATA_PIN, MATRIX_WR_PIN, MATRIX_CS0_PIN);
Adafruit_WS2801 strand = Adafruit_WS2801((uint16_t) 25, (uint8_t) STRAND_DATA_PIN, (uint8_t) STRAND_CLK_PIN, WS2801_GRB);

// ==============================================================
// Define the game state variables.

/// Define symbolic values for the main game state machine.
enum state_t { STATE_IDLE, STATE_ATTRACT, STATE_BALL1, STATE_BALL2, STATE_BALL3, STATE_GAME_OVER, NUM_STATES } game_state;

/// A count of the number of microseconds elapsed in the current game state.
long game_state_elapsed;

/// Convenient time constants, in microseconds.
const long FIVE_SECONDS = 5000000;

/// Current score (single player only).
unsigned long score = 0;

/// Attract mode melody.
const unsigned char attract_melody[] = {
  MIDI_C4, EIGHTH,   MIDI_E4, EIGHTH,   MIDI_G4, EIGHTH,   MIDI_C5, EIGHTH,   MIDI_B4, EIGHTH,   MIDI_G4, EIGHTH,
  MIDI_E4, QUARTER,
  MIDI_END
};
/****************************************************************/
/// Update all input state, including periodic sensor sampling, debouncing, and filtering.
void poll_sensor_inputs(unsigned long interval)
{
  start_switch.update(interval);
  bumper_sensor.update(interval);
  drain_sensor.update(interval);
}

/****************************************************************/
/// Update the game state machine, including advancing game state and score and applying modal input-output mappings.
void poll_game_logic(unsigned long interval)
{
  game_state_elapsed += interval;

  switch(game_state) {
    //-----------------------------------------------
  case STATE_IDLE:

    if (start_switch.isTriggered()) {
      game_state_elapsed = 0;
      game_state = STATE_BALL1;
      send_debug_message("entering BALL1");
    }

    if (game_state_elapsed > FIVE_SECONDS) {
      game_state_elapsed = 0;
      game_state = STATE_ATTRACT;
      speaker.start_melody(attract_melody);
      strand_set_fast_animation();
      send_debug_message("entering ATTRACT");
    }
    break;

    //-----------------------------------------------
  case STATE_ATTRACT:
    if (start_switch.isTriggered()) {
      game_state_elapsed = 0;
      game_state = STATE_BALL1;
      score = 0; // reset previous score
      send_debug_message("entering BALL1");
    }

    if (game_state_elapsed > FIVE_SECONDS) {
      game_state_elapsed = 0;
      game_state = STATE_IDLE;
      strand_set_slow_animation();
      send_debug_message("entering IDLE");
    }
    break;

    //-----------------------------------------------
  case STATE_BALL1:
    if (drain_sensor.isTriggered()) {
      game_state_elapsed = 0;
      game_state = STATE_BALL2;
      send_debug_message("entering BALL2");
    }

    if (bumper_sensor.isTriggered()) {
      score++;
      bumper.trigger();
    }

    break;

    //-----------------------------------------------
  case STATE_BALL2:
    if (drain_sensor.isTriggered()) {
      game_state_elapsed = 0;
      game_state = STATE_BALL3;
      strand_set_fast_animation();
      send_debug_message("entering BALL3");
    }

    if (bumper_sensor.isTriggered()) {
      score++;
      bumper.trigger();
    }

    break;

    //-----------------------------------------------
  case STATE_BALL3:
    if (drain_sensor.isTriggered()) {
      game_state_elapsed = 0;
      game_state = STATE_GAME_OVER;
      send_debug_message("entering GAME_OVER");
    }

    if (bumper_sensor.isTriggered()) {
      score++;
      bumper.trigger();
    }

    break;

    //-----------------------------------------------
  case STATE_GAME_OVER:
    if (game_state_elapsed > FIVE_SECONDS) {
      game_state_elapsed = 0;
      game_state = STATE_IDLE;
      strand_set_slow_animation();
      send_debug_message("entering IDLE");
    }
    break;
    //-----------------------------------------------
  default:
    // Any other value of game_state is invalid, so re-enter the IDLE state.
    send_debug_message("Unexpected game_state entered.");
    game_state = STATE_IDLE;
    game_state_elapsed = 0;
    break;
  }

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

  // some simple LED animation based on the game state
  switch(game_state) {
  case STATE_IDLE:
    digitalWrite(LED1_PIN, HIGH);
    digitalWrite(LED2_PIN, HIGH);
    break;

  case STATE_ATTRACT:
    digitalWrite(LED1_PIN, (game_state_elapsed % 500000) < 250000);
    digitalWrite(LED2_PIN, (game_state_elapsed % 300000) < 150000);
    break;

  case STATE_BALL1:
  case STATE_BALL2:
  case STATE_BALL3:
    if (game_state_elapsed < 1000000) {
      // if the state was freshly entered, flash a little
      digitalWrite(LED1_PIN, (game_state_elapsed % 100000) < 50000);
      digitalWrite(LED2_PIN, (game_state_elapsed % 100000) < 50000);

    } else {
      // otherwise flash when the bumper is hit
      digitalWrite(LED1_PIN, !bumper.isActive());
      digitalWrite(LED2_PIN, !bumper.isActive());
    }
    break;

  case STATE_GAME_OVER:
    digitalWrite(LED1_PIN, (game_state_elapsed % 200000) < 100000);
    digitalWrite(LED2_PIN, (game_state_elapsed % 200000) < 100000);
    break;

  default:
    // On any invalid value or game_state, do nothing.
    break;
  }
}

/****************************************************************/
/// Update all actuator state, including output pulse timers.
void poll_actuator_outputs(unsigned long interval)
{
  bumper.update(interval);
  speaker.update(interval);
  strand_update(interval);

  // For now, just always show the current score on the LED matrix.  This will
  // evolve into a separate LED matrix animation state machine.
  matrix_show_score(score);
}

/****************************************************************/
// Debugging functions which can be called from user console input.
void user_print_report(void)
{
  send_debug_message("start of debugging report.");
  start_switch.send_debug();
  bumper_sensor.send_debug();
  drain_sensor.send_debug();
  bumper.send_debug();
  speaker.send_debug();
  send_debug_message("end of debugging report.");
}
void user_reset_game(void)
{
  send_debug_message("user game reset.");
  game_state = STATE_IDLE;
  game_state_elapsed = 0;
}

void user_set_game_state(int value)
{
  send_debug_message("user forcing game state.");

  // Cast the integer to state_t, which is really just an integer, but
  // considered a different kind of integer by the compiler.
  if (value >= 0 && value < NUM_STATES) {
    game_state = (state_t) value;
    game_state_elapsed = 0;
  }
}
void user_set_game_score(unsigned long value)
{
  send_debug_message("user forcing game score.");
  score = value;
}
void user_play_melody(void)
{
  send_debug_message("user forcing melody.");
  speaker.start_melody(attract_melody);
}

/****************************************************************/
/**** 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(LED1_PIN     , OUTPUT);
  pinMode(SPEAKER_PIN  , OUTPUT);
  pinMode(LED2_PIN     , OUTPUT);
  pinMode(SOLENOID4_PIN, OUTPUT);
  pinMode(SOLENOID3_PIN, OUTPUT);
  pinMode(SOLENOID2_PIN, OUTPUT);
  pinMode(SOLENOID1_PIN, OUTPUT);

  digitalWrite(LED1_PIN     , HIGH); // off
  digitalWrite(LED2_PIN     , HIGH); // off
  digitalWrite(SOLENOID4_PIN, LOW ); // off
  digitalWrite(SOLENOID3_PIN, LOW ); // off
  digitalWrite(SOLENOID2_PIN, LOW ); // off
  digitalWrite(SOLENOID1_PIN, LOW ); // off

  // finish configuring the LED hardware
  matrix.begin(ADA_HT1632_COMMON_16NMOS);
  matrix.fillScreen();
  matrix.clearScreen();
  matrix.writeScreen();
  strand.begin();
  strand.show();

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

  // send a message as a diagnostic
  send_debug_message("wakeup");
}

/****************************************************************/
/// 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_sensor_inputs(interval);
  poll_game_logic(interval);
  poll_actuator_outputs(interval);
  poll_console_input(interval);
}

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

Graphics

 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
/// \file PinballGame/MatrixGraphics.cpp

/// \brief Real-time graphics functions for a HT1632 monochrome LED matrix.

/// \copyright No copyright, 2016-2017, Garth Zeglin.  This file is explicitly placed in the public domain.

/// \details This is a collection of functions compiled with the main pinball
/// game code.  Since only one LED matrix is supported, these are all global
/// functions with global state.

#include "Arduino.h"
#include "Adafruit_GFX.h"
#include "Adafruit_HT1632.h"
#include "MatrixGraphics.h"

// The 'matrix' object is declared in the main file.
extern Adafruit_HT1632LEDMatrix matrix;
  
void matrix_show_score(unsigned long points)
{
  matrix.clearScreen();
  // draw some text!
  matrix.setTextSize(1 * .5);  // size 1 == 8 pixels high
  matrix.setTextColor(1);   // 'lit' LEDs
  matrix.setTextWrap(false);
  matrix.setCursor(5, 4);// start at top left, with one pixel of spacing
  matrix.print(points);
  matrix.writeScreen();
}
 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
/// \file PinballGame/StrandGraphics.cpp

/// \brief Real-time graphics functions for a WS2801 RGB LED strand.

/// \copyright No copyright, 2016-2017, Garth Zeglin.  This file is explicitly placed in the public domain.

/// \details This is a collection of functions compiled with the main pinball
/// game code.  Since only one LED strand is supported, these are all global
/// functions with global state.

#include "Arduino.h"
#include "Adafruit_WS2801.h"
#include "StrandGraphics.h"
  
// The 'strand' object is declared in the main file with type Adafruit_WS2801 strand.
extern Adafruit_WS2801 strand;

/// LED animation state.
static long strand_interval = 50000; /// microseconds between frames
static long strand_timer = 0;        /// microseconds until the next frame
static int strand_frame = 0;         /// current animation frame count

/// Utility function to create a 24 bit RGB color value encoded in a 32 bit integer.
static uint32_t strand_color(byte r, byte g, byte b)
{
  uint32_t c;
  c = r;
  c <<= 8;
  c |= g;
  c <<= 8;
  c |= b;
  return c;
}

/// Utility function to compute a 32 bit RGB color value from an 8-bit phase.
/// The colors are a transition from r -> g -> b -> r ...
static uint32_t strand_color_wheel(byte color_phase)
{
  if (color_phase < 85) {
    return strand_color(color_phase * 3, 255 - color_phase * 3, 90);
  } else if (color_phase < 170) {
    color_phase -= 85;
    return strand_color(255 - color_phase * 3, 90, color_phase * 3);
  } else {
    color_phase -= 170;
    return strand_color(90, color_phase * 3, 255 - color_phase * 3);
  }
}

/// Display one frame of an animated color spectrum.  The frame value can
/// continuously increment for each frame update.
static void strand_show_rainbow(int frame)
{
  int i;
  for (i = 0; i < strand.numPixels(); i++) {
    // tricky math! we use each pixel as a fraction of the full 96-color wheel
    // (thats the i / strand.numPixels() part)
    // Then add in frame which makes the colors go around per pixel
    // the % 96 is to make the wheel cycle around
    strand.setPixelColor(i, strand_color_wheel( ((i * 256 / strand.numPixels()) + frame) % 256) );
  }
  strand.show();   // write all the pixels out
}

/// Poll the LED strand timer and update the hardware with new animation as needed.
void strand_update(unsigned long interval)
{
  // Subtract the elapsed time from the counter until the right amount of time
  // has elapsed; this will keep the update rate more constant as the execution
  // rate varies.
  strand_timer -= interval;
  if (strand_timer < 0){
    strand_timer += strand_interval;
    strand_show_rainbow(strand_frame++);
  }
}

const int FAST_LEDS = 50000;
const int SLOW_LEDS = 100000;

void strand_set_fast_animation(void)
{
  strand_interval = FAST_LEDS;
}

void strand_set_slow_animation(void)
{
  strand_interval = SLOW_LEDS;
}

Sensor Processing

 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
/// \file PinballGame/PinballSensor.h

/// \brief Process inputs from a single-channel analog pinball sensor such as a photoreflective pair.

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

/// \details This file contains support code for implementing input processing
/// for the Arduino pinball machine demo.

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

class PinballSensor {

private:
  /// Number of the analog pin to use for input.  The hardware is assumed to be
  /// active-low, i.e., idling at a high voltage, and pulled low during an
  /// 'event'.
  int input_pin;

  /// Input sampling interval, in microseconds.
  long sampling_interval;
  
  /// Countdown to the next input measurement, in microseconds.
  long sample_timer;

  /// Most recent raw measurement.  The units are the 10-bit (0-1023) integer ADC value.
  int raw_input;

  /// Analog input threshold defining the trigger level for an 'event', in ADC units.
  int lower_threshold;

  /// Analog input threshold defining the trigger level for resetting.  The
  /// difference between upper_threshold and lower_threshold defines the
  /// 'deadband' of 'hysteresis' of the event detector.  Specified in ADC units.
  int upper_threshold;

  /// The current Boolean state of the input detector.
  bool active;

  /// A Boolean flag which has a true value only during the polling cycle in
  /// which an event begins.  This supports event-driven programming in which
  /// the sensor input causes another event to begin.
  bool triggered;

  /// Count of the total number of events observed.
  long event_count;

  /// Count of the total number of samples measured.
  long sample_count;
  
public:

  /// Constructor to initialize an instance of the class.
  PinballSensor(int pin);

  /// Update function to be called as frequently as possible to sample the pin
  /// and process the data.  It requires the number of microseconds elapsed
  /// since the last update.
  void update(unsigned long interval);

  /// Debugging function to print a representation of the current state to the serial port.
  void send_debug(void);

  /// Access function to return the current state.
  bool isTriggered(void) { return triggered; }
};

/****************************************************************/
 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
/// \file PinballGame/PinballSensor.cpp
/// \copyright No copyright, 2016, Garth Zeglin.  This file is explicitly placed in the public domain.

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

#include "Arduino.h"
#include "PinballSensor.h"

/****************************************************************/
// Constructor for an instance of the class.
PinballSensor::PinballSensor(int pin)
{
  // initialize the state variables
  input_pin   	    = pin;
  sample_timer      = 0;
  raw_input         = 0;
  sampling_interval = 5000;  // 5000 usec == 5 msec == 200 Hz
  lower_threshold   = 700;
  upper_threshold   = 750;
  active            = false;
  triggered         = false;
  event_count       = 0;
  sample_count      = 0;
}

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

// Update polling function for an instance of the class.
void PinballSensor::update(unsigned long interval)
{
  // always reset any event indication
  triggered = false;

  // test whether to sample the input
  sample_timer -= interval;
  
  if (sample_timer <= 0) {

    // Reset the timer for the next sampling period.  Adding in the value helps
    // maintain precise timing in the presence of variation in the polling time,
    // e.g. if this sampling point was a little late, the next one will occur a
    // little sooner, maintaining the overall average.
    sample_timer += sampling_interval;

    // read the raw input
    raw_input = analogRead(input_pin);
    sample_count++;
    
    if (!active) {
      // if waiting for another input, use the lower threshold to detect an event
      if (raw_input < lower_threshold) {
	active = true;
	triggered = true;
	event_count++;
      }
    }
    else {
      // if waiting for an input to end, use the upper threshold to detect a reset
      if (raw_input > upper_threshold) {
	active = false;
      }
    }
  }
}
/****************************************************************/
void PinballSensor::send_debug(void)
{
  Serial.print("sensor pin:");
  Serial.print(input_pin);

  Serial.print("  raw: ");
  Serial.print(raw_input);

  Serial.print("  active: ");
  Serial.print(active);

  Serial.print("  samples: ");
  Serial.print(sample_count);

  Serial.print("  events: ");
  Serial.print(event_count);

  Serial.print("  thresholds: ");
  Serial.print(lower_threshold);
  Serial.print(" ");
  Serial.print(upper_threshold);
  
  Serial.println();
}
/****************************************************************/

Actuator Control

 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
/// \file PinballGame/PopBumper.h

/// \brief Control a single pinball solenoid actuator.

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

/// \details This file contains support code for implementing impulsive actuator
/// control for the Arduino pinball machine demo.  This is very simple for now,
/// but could be expanded to support PWM control for reducing holding currents.

/****************************************************************/
class PopBumper {

private:
  /// Number of the digital pin to use for output.  The hardware is assumed to be
  /// active-high, i.e., idling at a low voltage, and driven high when firing the solenoid.
  int output_pin;

  /// Countdown for the actuator ON period, in microseconds.
  long output_timer;

  /// Duration for the actuator ON period, in microseconds.
  long pulse_width;

  /// The current Boolean state of the output.
  bool active;

  /// Count of the total number of output events.
  long event_count;
  
public:

  /// Constructor to initialize an instance of the class.  Note that this only
  /// initializes object state, it does not configure the hardware, which should
  /// be performed directly by the user.
  PopBumper(int pin);    

  /// Update function to be called as frequently as possible to operate the
  /// output state machine. It requires the number of microseconds elapsed since
  /// the last update.
  void update(unsigned long interval);

  /// Trigger function to start an actuation cycle.
  void trigger(void);
  
  /// Debugging function to print a representation of the current state to the serial port.
  void send_debug(void);

  /// Access function to return the current state.
  bool isActive(void) { return active; }
};

/****************************************************************/
 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
/// \file PinballGame/PopBumper.cpp
/// \copyright No copyright, 2016, Garth Zeglin.  This file is explicitly placed in the public domain.

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

#include "Arduino.h"
#include "PopBumper.h"

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

PopBumper::PopBumper(int pin)
{
  output_pin = pin;
  output_timer = 0;
  active = false;
  event_count = 0;
  pulse_width = 100000;  // 100 msec = 0.1 seconds
}

void PopBumper::update(unsigned long interval)
{
  // for now, this only needs to check when to turn off the solenoid
  if (active) {
    output_timer -= interval;
    if (output_timer < 0) {
      active = false;
      digitalWrite(output_pin, LOW);
    }
  }
}

void PopBumper::trigger(void)
{
  // only accept a trigger if not already active; if the solenoid is currently firing, the new trigger is ignored
  if (!active) {
    output_timer = pulse_width;
    active = true;
    digitalWrite(output_pin, HIGH);
  }
}
  
void PopBumper::send_debug(void)
{
  Serial.print("bumper pin:");
  Serial.print(output_pin);

  Serial.print("  active: ");
  Serial.print(active);

  Serial.print("  events: ");
  Serial.print(event_count);

  Serial.print("  pulse width: ");
  Serial.print(pulse_width);
  
  Serial.println();
}

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

Speaker Melodies

  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
/// \file PinballGame/ToneSpeaker.h

/// \brief Play musical tones on a speaker.

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

/// \details This file contains support code for implementing audio effects for
/// the Arduino pinball machine demo, playing tone sequences at constant volume.

/****************************************************************/
class ToneSpeaker {

private:
  /// Number of the digital pin to use for output.  The hardware is assumed to
  /// be active-high, i.e., idling at a low voltage with no speaker current, and
  /// driven high when driving the speaker.
  int output_pin;

  /// Countdown for the current note or silence period, in microseconds.
  long note_timer;

  /// Tempo multiplier: duration in microseconds of a single MIDI 'tick' which
  /// is 1/24 of a quarter note.
  long tick_duration;
  
  /// True if currently playing a tone sequence.
  bool playing;

  /// Count of the total number of notes played.
  long event_count;

  /// Pointer to the next note to play.  A melody is specified as a series of
  /// pairs of bytes: note value, duration.  Invalid notes will play as a rest
  /// (silence).  A zero note ends the sequence.
  const unsigned char *playhead;

  /// Private function to begin a new note.
  void _start_note(unsigned char note, unsigned char value);
  
public:

  /// Constructor to initialize an instance of the class.  Note that this only
  /// initializes object state, it does not configure the hardware, which should
  /// be performed directly by the user.
  ToneSpeaker(int pin);    

  /// Set the tempo in beats per minute.  The desired units are microseconds per MIDI tick:
  ///   (microseconds / tick) = (microseconds / minute) / (ticks/minute)
  ///   (ticks / minute)      = (ticks / beat) * (beat / minute)
  //  So:
  ///   (microseconds / tick) = (microseconds / minute) / ((ticks / beat) * (beat / minute))
  ///   (microseconds / tick) = 60000000 / (24 * (beat / minute))
  ///   (microseconds / tick) =  2500000 / (beat / minute)
  void setTempo(int bpm)  { tick_duration = 2500000L / bpm; }
		
  /// Update function to be called as frequently as possible to operate the
  /// output state machine. It requires the number of microseconds elapsed since
  /// the last update.
  void update(unsigned long interval);

  /// Start the player on a new melody.  A melody is specified as a series of pairs of
  /// bytes: note value, duration.  The melody ends with a zero note.  Invalid notes are rests.
  void start_melody(const unsigned char melody[]);
  
  /// Debugging function to print a representation of the current state to the serial port.
  void send_debug(void);

  /// Access function to check whether a melody is currently playing.
  bool isPlaying(void) { return playing; }
};

/****************************************************************/
// Convenient symbols for the MIDI pitch scale.  Each value is a half-step.  For details of the tone
// definitions, see pitch_table.h.

#define MIDI_MIDDLE_C 60

#define MIDI_END 0
#define MIDI_REST 1

#define MIDI_C1 24
#define MIDI_C2 36
#define MIDI_C3 48

#define MIDI_C4 60
#define MIDI_D4 62
#define MIDI_E4 64
#define MIDI_F4 65
#define MIDI_G4 67
#define MIDI_A4 69
#define MIDI_B4 71

#define MIDI_C5 72
#define MIDI_D5 74
#define MIDI_E5 76
#define MIDI_F5 77
#define MIDI_G5 79
#define MIDI_A5 81
#define MIDI_B5 83

// Define note durations in units of MIDI beat clock 'ticks', each 1/24 of a
// quarter note.  This multiplier allows even triplets, e.g. three
// eighth-triplets equals one quarter note.
#define HALF                48
#define QUARTER             24
#define EIGHTH              12
#define SIXTEENTH            6
#define THIRTYSECOND         3

#define EIGHTH_TRIPLET       8
#define SIXTEENTH_TRIPLET    4
#define THIRTYSECOND_TRIPLET 2

/****************************************************************/
 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
/// \file PinballGame/ToneSpeaker.cpp
/// \copyright No copyright, 2016, Garth Zeglin.  This file is explicitly placed in the public domain.

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

#include "Arduino.h"
#include "ToneSpeaker.h"
#include "pitch_table.h"

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

ToneSpeaker::ToneSpeaker(int pin)
{
  output_pin = pin;
  note_timer = 0;
  setTempo(120);
  playing = false;
  event_count = 0;
  playhead = NULL;
}

void ToneSpeaker::_start_note(unsigned char note, unsigned char value)
{
  if (note < FIRST_MIDI_NOTE || note > LAST_MIDI_NOTE) {
    noTone(output_pin);
  } else {
    // Use the AVR pgmspace.h API to read a value from the table in FLASH.
    // Reference: https://www.arduino.cc/en/Reference/PROGMEM
    int pitch = pgm_read_word_near( midi_freq_table + (note - FIRST_MIDI_NOTE));
    tone(output_pin, pitch);
  }
  note_timer += value * tick_duration;
  event_count++;
}

void ToneSpeaker::update(unsigned long interval)
{
  if (playing) {
    note_timer -= interval;
    if (note_timer < 0) {
      // start the next note
      if (*playhead == 0) {
	// if end of sequence
	noTone(output_pin);
	playing = false;
      } else {
	_start_note(playhead[0], playhead[1]);
	playhead += 2;
      }
    }
  }
}

void ToneSpeaker::start_melody(const unsigned char melody[])
{
  Serial.print("entering start_melody, first value is ");
  Serial.println(melody[0]);
  
  if (melody[0] != 0) {
    // Reset any existing melody timing.
    note_timer = 0;

    // Kick off the sequence; the update() function will continue it.
    playing = true;
    _start_note( melody[0], melody[1] );
    playhead = &melody[2];
  }
}

/****************************************************************/  
void ToneSpeaker::send_debug(void)
{
  Serial.print("speaker pin:");
  Serial.print(output_pin);

  Serial.print("  playing: ");
  Serial.print(playing);

  Serial.print("  events: ");
  Serial.print(event_count);

  Serial.print("  tick duration: ");
  Serial.print(tick_duration);
  
  Serial.println();
}

/****************************************************************/
  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
/// \file PinballGame/pitch_table.h
/// \brief Define the mapping from MIDI notes to integer tone frequencies.
/// Transcribed from https://www.arduino.cc/en/Tutorial/toneMelody

// Define the range of supported pitch values.
#define FIRST_MIDI_NOTE 23
#define LAST_MIDI_NOTE  111

// The special PROGMEM keyword places the table in FLASH program memory (saving
// RAM space), but requires the use of AVR pgmspace functions to access it.

const uint16_t midi_freq_table[] PROGMEM = {
  31,   // NOTE_B0, MIDI note 23
  33,   // NOTE_C1, MIDI note 24   
  35,   // NOTE_CS1 
  37,   // NOTE_D1  
  39,   // NOTE_DS1 
  41,   // NOTE_E1  
  44,   // NOTE_F1  
  46,   // NOTE_FS1 
  49,   // NOTE_G1  
  52,   // NOTE_GS1 
  55,   // NOTE_A1  
  58,   // NOTE_AS1 
  62,   // NOTE_B1  
  65,   // NOTE_C2  
  69,   // NOTE_CS2 
  73,   // NOTE_D2  
  78,   // NOTE_DS2 
  82,   // NOTE_E2  
  87,   // NOTE_F2  
  93,   // NOTE_FS2 
  98,   // NOTE_G2  
  104,   // NOTE_GS2 
  110,   // NOTE_A2  
  117,   // NOTE_AS2 
  123,   // NOTE_B2  
  131,   // NOTE_C3  
  139,   // NOTE_CS3 
  147,   // NOTE_D3  
  156,   // NOTE_DS3 
  165,   // NOTE_E3  
  175,   // NOTE_F3  
  185,   // NOTE_FS3 
  196,   // NOTE_G3  
  208,   // NOTE_GS3 
  220,   // NOTE_A3  
  233,   // NOTE_AS3 
  247,   // NOTE_B3  
  262,   // NOTE_C4, MIDI note 60  
  277,   // NOTE_CS4 
  294,   // NOTE_D4  
  311,   // NOTE_DS4 
  330,   // NOTE_E4  
  349,   // NOTE_F4  
  370,   // NOTE_FS4 
  392,   // NOTE_G4  
  415,   // NOTE_GS4 
  440,   // NOTE_A4, MIDI note 69, the usual orchestral tuning pitch  
  466,   // NOTE_AS4 
  494,   // NOTE_B4  
  523,   // NOTE_C5  
  554,   // NOTE_CS5 
  587,   // NOTE_D5  
  622,   // NOTE_DS5 
  659,   // NOTE_E5  
  698,   // NOTE_F5  
  740,   // NOTE_FS5 
  784,   // NOTE_G5  
  831,   // NOTE_GS5 
  880,   // NOTE_A5  
  932,   // NOTE_AS5 
  932,   // NOTE_AS5 
  988,   // NOTE_B5  
  1047,   // NOTE_C6  
  1109,   // NOTE_CS6 
  1175,   // NOTE_D6  
  1245,   // NOTE_DS6 
  1319,   // NOTE_E6  
  1397,   // NOTE_F6  
  1480,   // NOTE_FS6 
  1568,   // NOTE_G6  
  1661,   // NOTE_GS6 
  1760,   // NOTE_A6  
  1865,   // NOTE_AS6 
  1976,   // NOTE_B6  
  2093,   // NOTE_C7  
  2217,   // NOTE_CS7 
  2349,   // NOTE_D7  
  2489,   // NOTE_DS7 
  2637,   // NOTE_E7  
  2794,   // NOTE_F7  
  2960,   // NOTE_FS7 
  3136,   // NOTE_G7  
  3322,   // NOTE_GS7 
  3520,   // NOTE_A7  
  3729,   // NOTE_AS7 
  3951,   // NOTE_B7  
  4186,   // NOTE_C8, MIDI note 108  
  4435,   // NOTE_CS8 
  4699,   // NOTE_D8  
  4978   // NOTE_DS8, MIDI note 111
};

User Debugging Interface

  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
/// \file PinballGame/console.cpp

/// \brief User console interface for debugging using a host computer.

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

/// \details This file contains support code for implementing a command line
/// user interface using the default serial port on an Arduino.

#include "Arduino.h"
#include "console.h"

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

// These are declared in the main file.
extern void user_print_report(void);
extern void user_reset_game(void);
extern void user_set_game_state(int value);
extern void user_set_game_score(unsigned long value);
extern void user_play_melody(void);

// The maximum message line length.
const int MAX_LINE_LENGTH = 80;
 
// The maximum number of tokens in a single message.
const int MAX_TOKENS = 10;

/****************************************************************/
/**** Utility functions *****************************************/
/****************************************************************/

/// Send a single debugging string to the console.
void send_debug_message( const char *str )
{
  Serial.print("dbg ");
  Serial.println( str );
}

/****************************************************************/
/// Send a single debugging integer to the console.
void send_debug_message( int i )
{
  Serial.print("dbg ");
  Serial.println( i );
}

/****************************************************************/
/// Send a single-argument message back to the host.
void send_message( const char *command, long value )
{
  Serial.print( command );
  Serial.print( " " );
  Serial.println( value );
}

/****************************************************************/
/// Send a two-argument message back to the host.
void send_message( const char *command, long value1, long value2 )
{
  Serial.print( command );
  Serial.print( " " );
  Serial.print( value1 );
  Serial.print( " " );
  Serial.println( value2 );
}

/****************************************************************/
// 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.  Unrecognized commands are silently ignored.
///   \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, "report" )) {
      user_print_report();
    }
    else if ( string_equal( command, "reset" )) {
      user_reset_game();
    }
    else if ( string_equal( command, "melody" )) {
      user_play_melody();
    }
    else {
      send_debug_message("unrecognized command.");
    }    
  }

  /* -- process one-argument commands --------------------------- */
  else if (argc == 2) {
    int value = atoi(argv[1] );
    
    // Set the game state to a particular mode.
    if ( string_equal( command, "state" )) {
      user_set_game_state(value);
    }
    else if ( string_equal( command, "score" )) {
      user_set_game_score(value);
    }
    else {
      send_debug_message("unrecognized single-argument command.");
    }    
  }
  else {
    send_debug_message("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_input_message() is called.

void poll_console_input(unsigned long elapsed)
{
  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) send_debug_message("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++;
	}
      }
    }
  }
}