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/// \file PinballGame.ino
  2/// \brief Arduino program demonstrating essential real-time logic for a custom pinball machine.
  3
  4/// \copyright No copyright, 2016-2017, Garth Zeglin.  This file is explicitly placed in the public domain.
  5
  6/// \details This example is intended as a starting point for developing a
  7///          custom pinball controller, demonstrating the use of state machines
  8///          to implement both I/O and game logic.
  9///
 10/// This example is written using Arduino C++ conventions:
 11///
 12///  1. state variables are global
 13///  2. global C++ objects are statically declared and initialized
 14///  3. the sketch is divided into multiple .ino files which form one compilation unit
 15///  4. C++ classes in .cpp files are separately compiled
 16
 17/// This example assumes that several open-source libraries have been installed in the Arduino IDE:
 18///  1. Adafruit-GFX	general graphics support
 19///  2. Adafruit-HT1632	LED matrix display driver
 20///  3. Adafruit-WS2801	LED strand driver
 21
 22/****************************************************************/
 23// Library imports.
 24#include "SPI.h"
 25#include "Adafruit_GFX.h"
 26#include "Adafruit_HT1632.h"
 27#include "Adafruit_WS2801.h"
 28
 29// Forward declarations.
 30#include "PinballSensor.h"
 31#include "PopBumper.h"
 32#include "ToneSpeaker.h"
 33#include "StrandGraphics.h"
 34#include "MatrixGraphics.h"
 35#include "console.h"
 36
 37/****************************************************************/
 38/**** Hardware pin assignments **********************************/
 39/****************************************************************/
 40
 41// The following pin assignments correspond to the hardware on the PinballShield
 42// Rev A board.
 43
 44// Analog inputs.
 45const int PHOTO1_PIN = A0;  /// photo-interrupter input, value decreases when object present
 46const int PHOTO2_PIN = A1;  /// photo-interrupter input, value decreases when object present
 47const int PHOTO3_PIN = A2;  /// photo-interrupter input, value decreases when object present
 48const int PHOTO4_PIN = A3;  /// photo-interrupter input, value decreases when object present
 49const int SWITCH1_PIN = A4; /// active-low switch input
 50const int SWITCH2_PIN = A5; /// active-low switch input
 51
 52// Digital outputs. D0 and D1 are reserved for use as serial port RX/TX
 53const int LED1_PIN        = 2;  /// active-low LED output
 54const int SPEAKER_PIN     = 3;  /// active-high MOSFET-driven speaker output
 55const int LED2_PIN        = 4;  /// active-low LED output
 56const int SOLENOID4_PIN   = 5;  /// active-high MOSFET-driven solenoid output
 57const int SOLENOID3_PIN   = 6;  /// active-high MOSFET-driven solenoid output
 58const int MATRIX_DATA_PIN = 7;  /// HT1632 LED matrix display data output
 59const int MATRIX_WR_PIN   = 8;  /// HT1632 LED matrix display clock output
 60const int SOLENOID2_PIN   = 9;  /// active-high MOSFET-driven solenoid output
 61const int SOLENOID1_PIN   = 10; /// active-high MOSFET-driven solenoid output
 62const int STRAND_DATA_PIN = 11; /// WS2801 LED strand data output (yellow wire on strand)
 63const int MATRIX_CS0_PIN  = 12; /// HT1632 LED matrix display select output
 64const int STRAND_CLK_PIN  = 13; /// WS2801 LED strand clock output (green wire on strand)
 65
 66/****************************************************************/
 67/**** Global variables and constants ****************************/
 68/****************************************************************/
 69
 70// The baud rate is the number of bits per second transmitted over the serial port.
 71const long BAUD_RATE = 115200;
 72
 73PinballSensor start_switch(SWITCH1_PIN);
 74PinballSensor bumper_sensor(PHOTO1_PIN);
 75PinballSensor drain_sensor(PHOTO2_PIN);
 76
 77PopBumper     bumper(SOLENOID1_PIN);
 78ToneSpeaker   speaker(SPEAKER_PIN);
 79
 80// Initialize the LED hardware.
 81Adafruit_HT1632LEDMatrix matrix = Adafruit_HT1632LEDMatrix(MATRIX_DATA_PIN, MATRIX_WR_PIN, MATRIX_CS0_PIN);
 82Adafruit_WS2801 strand = Adafruit_WS2801((uint16_t) 25, (uint8_t) STRAND_DATA_PIN, (uint8_t) STRAND_CLK_PIN, WS2801_GRB);
 83
 84// ==============================================================
 85// Define the game state variables.
 86
 87/// Define symbolic values for the main game state machine.
 88enum state_t { STATE_IDLE, STATE_ATTRACT, STATE_BALL1, STATE_BALL2, STATE_BALL3, STATE_GAME_OVER, NUM_STATES } game_state;
 89
 90/// A count of the number of microseconds elapsed in the current game state.
 91long game_state_elapsed;
 92
 93/// Convenient time constants, in microseconds.
 94const long FIVE_SECONDS = 5000000;
 95
 96/// Current score (single player only).
 97unsigned long score = 0;
 98
 99/// Attract mode melody.
100const unsigned char attract_melody[] = {
101  MIDI_C4, EIGHTH,   MIDI_E4, EIGHTH,   MIDI_G4, EIGHTH,   MIDI_C5, EIGHTH,   MIDI_B4, EIGHTH,   MIDI_G4, EIGHTH,
102  MIDI_E4, QUARTER,
103  MIDI_END
104};
105/****************************************************************/
106/// Update all input state, including periodic sensor sampling, debouncing, and filtering.
107void poll_sensor_inputs(unsigned long interval)
108{
109  start_switch.update(interval);
110  bumper_sensor.update(interval);
111  drain_sensor.update(interval);
112}
113
114/****************************************************************/
115/// Update the game state machine, including advancing game state and score and applying modal input-output mappings.
116void poll_game_logic(unsigned long interval)
117{
118  game_state_elapsed += interval;
119
120  switch(game_state) {
121    //-----------------------------------------------
122  case STATE_IDLE:
123
124    if (start_switch.isTriggered()) {
125      game_state_elapsed = 0;
126      game_state = STATE_BALL1;
127      send_debug_message("entering BALL1");
128    }
129
130    if (game_state_elapsed > FIVE_SECONDS) {
131      game_state_elapsed = 0;
132      game_state = STATE_ATTRACT;
133      speaker.start_melody(attract_melody);
134      strand_set_fast_animation();
135      send_debug_message("entering ATTRACT");
136    }
137    break;
138
139    //-----------------------------------------------
140  case STATE_ATTRACT:
141    if (start_switch.isTriggered()) {
142      game_state_elapsed = 0;
143      game_state = STATE_BALL1;
144      score = 0; // reset previous score
145      send_debug_message("entering BALL1");
146    }
147
148    if (game_state_elapsed > FIVE_SECONDS) {
149      game_state_elapsed = 0;
150      game_state = STATE_IDLE;
151      strand_set_slow_animation();
152      send_debug_message("entering IDLE");
153    }
154    break;
155
156    //-----------------------------------------------
157  case STATE_BALL1:
158    if (drain_sensor.isTriggered()) {
159      game_state_elapsed = 0;
160      game_state = STATE_BALL2;
161      send_debug_message("entering BALL2");
162    }
163
164    if (bumper_sensor.isTriggered()) {
165      score++;
166      bumper.trigger();
167    }
168
169    break;
170
171    //-----------------------------------------------
172  case STATE_BALL2:
173    if (drain_sensor.isTriggered()) {
174      game_state_elapsed = 0;
175      game_state = STATE_BALL3;
176      strand_set_fast_animation();
177      send_debug_message("entering BALL3");
178    }
179
180    if (bumper_sensor.isTriggered()) {
181      score++;
182      bumper.trigger();
183    }
184
185    break;
186
187    //-----------------------------------------------
188  case STATE_BALL3:
189    if (drain_sensor.isTriggered()) {
190      game_state_elapsed = 0;
191      game_state = STATE_GAME_OVER;
192      send_debug_message("entering GAME_OVER");
193    }
194
195    if (bumper_sensor.isTriggered()) {
196      score++;
197      bumper.trigger();
198    }
199
200    break;
201
202    //-----------------------------------------------
203  case STATE_GAME_OVER:
204    if (game_state_elapsed > FIVE_SECONDS) {
205      game_state_elapsed = 0;
206      game_state = STATE_IDLE;
207      strand_set_slow_animation();
208      send_debug_message("entering IDLE");
209    }
210    break;
211    //-----------------------------------------------
212  default:
213    // Any other value of game_state is invalid, so re-enter the IDLE state.
214    send_debug_message("Unexpected game_state entered.");
215    game_state = STATE_IDLE;
216    game_state_elapsed = 0;
217    break;
218  }
219
220  /****************************************************************/
221
222  // some simple LED animation based on the game state
223  switch(game_state) {
224  case STATE_IDLE:
225    digitalWrite(LED1_PIN, HIGH);
226    digitalWrite(LED2_PIN, HIGH);
227    break;
228
229  case STATE_ATTRACT:
230    digitalWrite(LED1_PIN, (game_state_elapsed % 500000) < 250000);
231    digitalWrite(LED2_PIN, (game_state_elapsed % 300000) < 150000);
232    break;
233
234  case STATE_BALL1:
235  case STATE_BALL2:
236  case STATE_BALL3:
237    if (game_state_elapsed < 1000000) {
238      // if the state was freshly entered, flash a little
239      digitalWrite(LED1_PIN, (game_state_elapsed % 100000) < 50000);
240      digitalWrite(LED2_PIN, (game_state_elapsed % 100000) < 50000);
241
242    } else {
243      // otherwise flash when the bumper is hit
244      digitalWrite(LED1_PIN, !bumper.isActive());
245      digitalWrite(LED2_PIN, !bumper.isActive());
246    }
247    break;
248
249  case STATE_GAME_OVER:
250    digitalWrite(LED1_PIN, (game_state_elapsed % 200000) < 100000);
251    digitalWrite(LED2_PIN, (game_state_elapsed % 200000) < 100000);
252    break;
253
254  default:
255    // On any invalid value or game_state, do nothing.
256    break;
257  }
258}
259
260/****************************************************************/
261/// Update all actuator state, including output pulse timers.
262void poll_actuator_outputs(unsigned long interval)
263{
264  bumper.update(interval);
265  speaker.update(interval);
266  strand_update(interval);
267
268  // For now, just always show the current score on the LED matrix.  This will
269  // evolve into a separate LED matrix animation state machine.
270  matrix_show_score(score);
271}
272
273/****************************************************************/
274// Debugging functions which can be called from user console input.
275void user_print_report(void)
276{
277  send_debug_message("start of debugging report.");
278  start_switch.send_debug();
279  bumper_sensor.send_debug();
280  drain_sensor.send_debug();
281  bumper.send_debug();
282  speaker.send_debug();
283  send_debug_message("end of debugging report.");
284}
285void user_reset_game(void)
286{
287  send_debug_message("user game reset.");
288  game_state = STATE_IDLE;
289  game_state_elapsed = 0;
290}
291
292void user_set_game_state(int value)
293{
294  send_debug_message("user forcing game state.");
295
296  // Cast the integer to state_t, which is really just an integer, but
297  // considered a different kind of integer by the compiler.
298  if (value >= 0 && value < NUM_STATES) {
299    game_state = (state_t) value;
300    game_state_elapsed = 0;
301  }
302}
303void user_set_game_score(unsigned long value)
304{
305  send_debug_message("user forcing game score.");
306  score = value;
307}
308void user_play_melody(void)
309{
310  send_debug_message("user forcing melody.");
311  speaker.start_melody(attract_melody);
312}
313
314/****************************************************************/
315/**** Standard entry points for Arduino system ******************/
316/****************************************************************/
317
318/// Standard Arduino initialization function to configure the system.  This
319/// function is called once after reset to initialize the program.
320void setup()
321{
322  // configure the actuator pins as soon as possible.
323  pinMode(LED1_PIN     , OUTPUT);
324  pinMode(SPEAKER_PIN  , OUTPUT);
325  pinMode(LED2_PIN     , OUTPUT);
326  pinMode(SOLENOID4_PIN, OUTPUT);
327  pinMode(SOLENOID3_PIN, OUTPUT);
328  pinMode(SOLENOID2_PIN, OUTPUT);
329  pinMode(SOLENOID1_PIN, OUTPUT);
330
331  digitalWrite(LED1_PIN     , HIGH); // off
332  digitalWrite(LED2_PIN     , HIGH); // off
333  digitalWrite(SOLENOID4_PIN, LOW ); // off
334  digitalWrite(SOLENOID3_PIN, LOW ); // off
335  digitalWrite(SOLENOID2_PIN, LOW ); // off
336  digitalWrite(SOLENOID1_PIN, LOW ); // off
337
338  // finish configuring the LED hardware
339  matrix.begin(ADA_HT1632_COMMON_16NMOS);
340  matrix.fillScreen();
341  matrix.clearScreen();
342  matrix.writeScreen();
343  strand.begin();
344  strand.show();
345
346  // initialize the Serial port for the user debugging console
347  Serial.begin( BAUD_RATE );
348
349  // send a message as a diagnostic
350  send_debug_message("wakeup");
351}
352
353/****************************************************************/
354/// Standard Arduino polling function to handle all I/O and periodic processing.
355/// This function is called repeatedly as fast as possible from within the
356/// built-in library to poll program events.  This loop should never be allowed
357/// to stall or block so that all tasks can be constantly serviced.
358void loop()
359{
360  // The timestamp in microseconds for the last polling cycle, used to compute
361  // the exact interval between output updates.
362  static unsigned long last_update_clock = 0;
363
364  // Read the microsecond clock.
365  unsigned long now = micros();
366
367  // Compute the time elapsed since the last poll.  This will correctly handle wrapround of
368  // the 32-bit long time value given the properties of twos-complement arithmetic.
369  unsigned long interval = now - last_update_clock;
370  last_update_clock = now;
371
372  // Begin the polling cycle.
373  poll_sensor_inputs(interval);
374  poll_game_logic(interval);
375  poll_actuator_outputs(interval);
376  poll_console_input(interval);
377}
378
379/****************************************************************/
380/****************************************************************/

Graphics

 1/// \file PinballGame/MatrixGraphics.cpp
 2
 3/// \brief Real-time graphics functions for a HT1632 monochrome LED matrix.
 4
 5/// \copyright No copyright, 2016-2017, Garth Zeglin.  This file is explicitly placed in the public domain.
 6
 7/// \details This is a collection of functions compiled with the main pinball
 8/// game code.  Since only one LED matrix is supported, these are all global
 9/// functions with global state.
10
11#include "Arduino.h"
12#include "Adafruit_GFX.h"
13#include "Adafruit_HT1632.h"
14#include "MatrixGraphics.h"
15
16// The 'matrix' object is declared in the main file.
17extern Adafruit_HT1632LEDMatrix matrix;
18  
19void matrix_show_score(unsigned long points)
20{
21  matrix.clearScreen();
22  // draw some text!
23  matrix.setTextSize(1 * .5);  // size 1 == 8 pixels high
24  matrix.setTextColor(1);   // 'lit' LEDs
25  matrix.setTextWrap(false);
26  matrix.setCursor(5, 4);// start at top left, with one pixel of spacing
27  matrix.print(points);
28  matrix.writeScreen();
29}
 1/// \file PinballGame/StrandGraphics.cpp
 2
 3/// \brief Real-time graphics functions for a WS2801 RGB LED strand.
 4
 5/// \copyright No copyright, 2016-2017, Garth Zeglin.  This file is explicitly placed in the public domain.
 6
 7/// \details This is a collection of functions compiled with the main pinball
 8/// game code.  Since only one LED strand is supported, these are all global
 9/// functions with global state.
10
11#include "Arduino.h"
12#include "Adafruit_WS2801.h"
13#include "StrandGraphics.h"
14  
15// The 'strand' object is declared in the main file with type Adafruit_WS2801 strand.
16extern Adafruit_WS2801 strand;
17
18/// LED animation state.
19static long strand_interval = 50000; /// microseconds between frames
20static long strand_timer = 0;        /// microseconds until the next frame
21static int strand_frame = 0;         /// current animation frame count
22
23/// Utility function to create a 24 bit RGB color value encoded in a 32 bit integer.
24static uint32_t strand_color(byte r, byte g, byte b)
25{
26  uint32_t c;
27  c = r;
28  c <<= 8;
29  c |= g;
30  c <<= 8;
31  c |= b;
32  return c;
33}
34
35/// Utility function to compute a 32 bit RGB color value from an 8-bit phase.
36/// The colors are a transition from r -> g -> b -> r ...
37static uint32_t strand_color_wheel(byte color_phase)
38{
39  if (color_phase < 85) {
40    return strand_color(color_phase * 3, 255 - color_phase * 3, 90);
41  } else if (color_phase < 170) {
42    color_phase -= 85;
43    return strand_color(255 - color_phase * 3, 90, color_phase * 3);
44  } else {
45    color_phase -= 170;
46    return strand_color(90, color_phase * 3, 255 - color_phase * 3);
47  }
48}
49
50/// Display one frame of an animated color spectrum.  The frame value can
51/// continuously increment for each frame update.
52static void strand_show_rainbow(int frame)
53{
54  int i;
55  for (i = 0; i < strand.numPixels(); i++) {
56    // tricky math! we use each pixel as a fraction of the full 96-color wheel
57    // (thats the i / strand.numPixels() part)
58    // Then add in frame which makes the colors go around per pixel
59    // the % 96 is to make the wheel cycle around
60    strand.setPixelColor(i, strand_color_wheel( ((i * 256 / strand.numPixels()) + frame) % 256) );
61  }
62  strand.show();   // write all the pixels out
63}
64
65/// Poll the LED strand timer and update the hardware with new animation as needed.
66void strand_update(unsigned long interval)
67{
68  // Subtract the elapsed time from the counter until the right amount of time
69  // has elapsed; this will keep the update rate more constant as the execution
70  // rate varies.
71  strand_timer -= interval;
72  if (strand_timer < 0){
73    strand_timer += strand_interval;
74    strand_show_rainbow(strand_frame++);
75  }
76}
77
78const int FAST_LEDS = 50000;
79const int SLOW_LEDS = 100000;
80
81void strand_set_fast_animation(void)
82{
83  strand_interval = FAST_LEDS;
84}
85
86void strand_set_slow_animation(void)
87{
88  strand_interval = SLOW_LEDS;
89}

Sensor Processing

 1/// \file PinballGame/PinballSensor.h
 2
 3/// \brief Process inputs from a single-channel analog pinball sensor such as a photoreflective pair.
 4
 5/// \copyright No copyright, 2016, Garth Zeglin.  This file is explicitly placed in the public domain.
 6
 7/// \details This file contains support code for implementing input processing
 8/// for the Arduino pinball machine demo.
 9
10/****************************************************************/
11
12class PinballSensor {
13
14private:
15  /// Number of the analog pin to use for input.  The hardware is assumed to be
16  /// active-low, i.e., idling at a high voltage, and pulled low during an
17  /// 'event'.
18  int input_pin;
19
20  /// Input sampling interval, in microseconds.
21  long sampling_interval;
22  
23  /// Countdown to the next input measurement, in microseconds.
24  long sample_timer;
25
26  /// Most recent raw measurement.  The units are the 10-bit (0-1023) integer ADC value.
27  int raw_input;
28
29  /// Analog input threshold defining the trigger level for an 'event', in ADC units.
30  int lower_threshold;
31
32  /// Analog input threshold defining the trigger level for resetting.  The
33  /// difference between upper_threshold and lower_threshold defines the
34  /// 'deadband' of 'hysteresis' of the event detector.  Specified in ADC units.
35  int upper_threshold;
36
37  /// The current Boolean state of the input detector.
38  bool active;
39
40  /// A Boolean flag which has a true value only during the polling cycle in
41  /// which an event begins.  This supports event-driven programming in which
42  /// the sensor input causes another event to begin.
43  bool triggered;
44
45  /// Count of the total number of events observed.
46  long event_count;
47
48  /// Count of the total number of samples measured.
49  long sample_count;
50  
51public:
52
53  /// Constructor to initialize an instance of the class.
54  PinballSensor(int pin);
55
56  /// Update function to be called as frequently as possible to sample the pin
57  /// and process the data.  It requires the number of microseconds elapsed
58  /// since the last update.
59  void update(unsigned long interval);
60
61  /// Debugging function to print a representation of the current state to the serial port.
62  void send_debug(void);
63
64  /// Access function to return the current state.
65  bool isTriggered(void) { return triggered; }
66};
67
68/****************************************************************/
 1/// \file PinballGame/PinballSensor.cpp
 2/// \copyright No copyright, 2016, Garth Zeglin.  This file is explicitly placed in the public domain.
 3
 4/****************************************************************/
 5
 6#include "Arduino.h"
 7#include "PinballSensor.h"
 8
 9/****************************************************************/
10// Constructor for an instance of the class.
11PinballSensor::PinballSensor(int pin)
12{
13  // initialize the state variables
14  input_pin   	    = pin;
15  sample_timer      = 0;
16  raw_input         = 0;
17  sampling_interval = 5000;  // 5000 usec == 5 msec == 200 Hz
18  lower_threshold   = 700;
19  upper_threshold   = 750;
20  active            = false;
21  triggered         = false;
22  event_count       = 0;
23  sample_count      = 0;
24}
25
26/****************************************************************/
27
28// Update polling function for an instance of the class.
29void PinballSensor::update(unsigned long interval)
30{
31  // always reset any event indication
32  triggered = false;
33
34  // test whether to sample the input
35  sample_timer -= interval;
36  
37  if (sample_timer <= 0) {
38
39    // Reset the timer for the next sampling period.  Adding in the value helps
40    // maintain precise timing in the presence of variation in the polling time,
41    // e.g. if this sampling point was a little late, the next one will occur a
42    // little sooner, maintaining the overall average.
43    sample_timer += sampling_interval;
44
45    // read the raw input
46    raw_input = analogRead(input_pin);
47    sample_count++;
48    
49    if (!active) {
50      // if waiting for another input, use the lower threshold to detect an event
51      if (raw_input < lower_threshold) {
52	active = true;
53	triggered = true;
54	event_count++;
55      }
56    }
57    else {
58      // if waiting for an input to end, use the upper threshold to detect a reset
59      if (raw_input > upper_threshold) {
60	active = false;
61      }
62    }
63  }
64}
65/****************************************************************/
66void PinballSensor::send_debug(void)
67{
68  Serial.print("sensor pin:");
69  Serial.print(input_pin);
70
71  Serial.print("  raw: ");
72  Serial.print(raw_input);
73
74  Serial.print("  active: ");
75  Serial.print(active);
76
77  Serial.print("  samples: ");
78  Serial.print(sample_count);
79
80  Serial.print("  events: ");
81  Serial.print(event_count);
82
83  Serial.print("  thresholds: ");
84  Serial.print(lower_threshold);
85  Serial.print(" ");
86  Serial.print(upper_threshold);
87  
88  Serial.println();
89}
90/****************************************************************/

Actuator Control

 1/// \file PinballGame/PopBumper.h
 2
 3/// \brief Control a single pinball solenoid actuator.
 4
 5/// \copyright No copyright, 2016, Garth Zeglin.  This file is explicitly placed in the public domain.
 6
 7/// \details This file contains support code for implementing impulsive actuator
 8/// control for the Arduino pinball machine demo.  This is very simple for now,
 9/// but could be expanded to support PWM control for reducing holding currents.
10
11/****************************************************************/
12class PopBumper {
13
14private:
15  /// Number of the digital pin to use for output.  The hardware is assumed to be
16  /// active-high, i.e., idling at a low voltage, and driven high when firing the solenoid.
17  int output_pin;
18
19  /// Countdown for the actuator ON period, in microseconds.
20  long output_timer;
21
22  /// Duration for the actuator ON period, in microseconds.
23  long pulse_width;
24
25  /// The current Boolean state of the output.
26  bool active;
27
28  /// Count of the total number of output events.
29  long event_count;
30  
31public:
32
33  /// Constructor to initialize an instance of the class.  Note that this only
34  /// initializes object state, it does not configure the hardware, which should
35  /// be performed directly by the user.
36  PopBumper(int pin);    
37
38  /// Update function to be called as frequently as possible to operate the
39  /// output state machine. It requires the number of microseconds elapsed since
40  /// the last update.
41  void update(unsigned long interval);
42
43  /// Trigger function to start an actuation cycle.
44  void trigger(void);
45  
46  /// Debugging function to print a representation of the current state to the serial port.
47  void send_debug(void);
48
49  /// Access function to return the current state.
50  bool isActive(void) { return active; }
51};
52
53/****************************************************************/
 1/// \file PinballGame/PopBumper.cpp
 2/// \copyright No copyright, 2016, Garth Zeglin.  This file is explicitly placed in the public domain.
 3
 4/****************************************************************/
 5
 6#include "Arduino.h"
 7#include "PopBumper.h"
 8
 9/****************************************************************/
10
11PopBumper::PopBumper(int pin)
12{
13  output_pin = pin;
14  output_timer = 0;
15  active = false;
16  event_count = 0;
17  pulse_width = 100000;  // 100 msec = 0.1 seconds
18}
19
20void PopBumper::update(unsigned long interval)
21{
22  // for now, this only needs to check when to turn off the solenoid
23  if (active) {
24    output_timer -= interval;
25    if (output_timer < 0) {
26      active = false;
27      digitalWrite(output_pin, LOW);
28    }
29  }
30}
31
32void PopBumper::trigger(void)
33{
34  // only accept a trigger if not already active; if the solenoid is currently firing, the new trigger is ignored
35  if (!active) {
36    output_timer = pulse_width;
37    active = true;
38    digitalWrite(output_pin, HIGH);
39  }
40}
41  
42void PopBumper::send_debug(void)
43{
44  Serial.print("bumper pin:");
45  Serial.print(output_pin);
46
47  Serial.print("  active: ");
48  Serial.print(active);
49
50  Serial.print("  events: ");
51  Serial.print(event_count);
52
53  Serial.print("  pulse width: ");
54  Serial.print(pulse_width);
55  
56  Serial.println();
57}
58
59/****************************************************************/

Speaker Melodies

  1/// \file PinballGame/ToneSpeaker.h
  2
  3/// \brief Play musical tones on a speaker.
  4
  5/// \copyright No copyright, 2016, Garth Zeglin.  This file is explicitly placed in the public domain.
  6
  7/// \details This file contains support code for implementing audio effects for
  8/// the Arduino pinball machine demo, playing tone sequences at constant volume.
  9
 10/****************************************************************/
 11class ToneSpeaker {
 12
 13private:
 14  /// Number of the digital pin to use for output.  The hardware is assumed to
 15  /// be active-high, i.e., idling at a low voltage with no speaker current, and
 16  /// driven high when driving the speaker.
 17  int output_pin;
 18
 19  /// Countdown for the current note or silence period, in microseconds.
 20  long note_timer;
 21
 22  /// Tempo multiplier: duration in microseconds of a single MIDI 'tick' which
 23  /// is 1/24 of a quarter note.
 24  long tick_duration;
 25  
 26  /// True if currently playing a tone sequence.
 27  bool playing;
 28
 29  /// Count of the total number of notes played.
 30  long event_count;
 31
 32  /// Pointer to the next note to play.  A melody is specified as a series of
 33  /// pairs of bytes: note value, duration.  Invalid notes will play as a rest
 34  /// (silence).  A zero note ends the sequence.
 35  const unsigned char *playhead;
 36
 37  /// Private function to begin a new note.
 38  void _start_note(unsigned char note, unsigned char value);
 39  
 40public:
 41
 42  /// Constructor to initialize an instance of the class.  Note that this only
 43  /// initializes object state, it does not configure the hardware, which should
 44  /// be performed directly by the user.
 45  ToneSpeaker(int pin);    
 46
 47  /// Set the tempo in beats per minute.  The desired units are microseconds per MIDI tick:
 48  ///   (microseconds / tick) = (microseconds / minute) / (ticks/minute)
 49  ///   (ticks / minute)      = (ticks / beat) * (beat / minute)
 50  //  So:
 51  ///   (microseconds / tick) = (microseconds / minute) / ((ticks / beat) * (beat / minute))
 52  ///   (microseconds / tick) = 60000000 / (24 * (beat / minute))
 53  ///   (microseconds / tick) =  2500000 / (beat / minute)
 54  void setTempo(int bpm)  { tick_duration = 2500000L / bpm; }
 55		
 56  /// Update function to be called as frequently as possible to operate the
 57  /// output state machine. It requires the number of microseconds elapsed since
 58  /// the last update.
 59  void update(unsigned long interval);
 60
 61  /// Start the player on a new melody.  A melody is specified as a series of pairs of
 62  /// bytes: note value, duration.  The melody ends with a zero note.  Invalid notes are rests.
 63  void start_melody(const unsigned char melody[]);
 64  
 65  /// Debugging function to print a representation of the current state to the serial port.
 66  void send_debug(void);
 67
 68  /// Access function to check whether a melody is currently playing.
 69  bool isPlaying(void) { return playing; }
 70};
 71
 72/****************************************************************/
 73// Convenient symbols for the MIDI pitch scale.  Each value is a half-step.  For details of the tone
 74// definitions, see pitch_table.h.
 75
 76#define MIDI_MIDDLE_C 60
 77
 78#define MIDI_END 0
 79#define MIDI_REST 1
 80
 81#define MIDI_C1 24
 82#define MIDI_C2 36
 83#define MIDI_C3 48
 84
 85#define MIDI_C4 60
 86#define MIDI_D4 62
 87#define MIDI_E4 64
 88#define MIDI_F4 65
 89#define MIDI_G4 67
 90#define MIDI_A4 69
 91#define MIDI_B4 71
 92
 93#define MIDI_C5 72
 94#define MIDI_D5 74
 95#define MIDI_E5 76
 96#define MIDI_F5 77
 97#define MIDI_G5 79
 98#define MIDI_A5 81
 99#define MIDI_B5 83
100
101// Define note durations in units of MIDI beat clock 'ticks', each 1/24 of a
102// quarter note.  This multiplier allows even triplets, e.g. three
103// eighth-triplets equals one quarter note.
104#define HALF                48
105#define QUARTER             24
106#define EIGHTH              12
107#define SIXTEENTH            6
108#define THIRTYSECOND         3
109
110#define EIGHTH_TRIPLET       8
111#define SIXTEENTH_TRIPLET    4
112#define THIRTYSECOND_TRIPLET 2
113
114/****************************************************************/
 1/// \file PinballGame/ToneSpeaker.cpp
 2/// \copyright No copyright, 2016, Garth Zeglin.  This file is explicitly placed in the public domain.
 3
 4/****************************************************************/
 5
 6#include "Arduino.h"
 7#include "ToneSpeaker.h"
 8#include "pitch_table.h"
 9
10/****************************************************************/
11
12ToneSpeaker::ToneSpeaker(int pin)
13{
14  output_pin = pin;
15  note_timer = 0;
16  setTempo(120);
17  playing = false;
18  event_count = 0;
19  playhead = NULL;
20}
21
22void ToneSpeaker::_start_note(unsigned char note, unsigned char value)
23{
24  if (note < FIRST_MIDI_NOTE || note > LAST_MIDI_NOTE) {
25    noTone(output_pin);
26  } else {
27    // Use the AVR pgmspace.h API to read a value from the table in FLASH.
28    // Reference: https://www.arduino.cc/en/Reference/PROGMEM
29    int pitch = pgm_read_word_near( midi_freq_table + (note - FIRST_MIDI_NOTE));
30    tone(output_pin, pitch);
31  }
32  note_timer += value * tick_duration;
33  event_count++;
34}
35
36void ToneSpeaker::update(unsigned long interval)
37{
38  if (playing) {
39    note_timer -= interval;
40    if (note_timer < 0) {
41      // start the next note
42      if (*playhead == 0) {
43	// if end of sequence
44	noTone(output_pin);
45	playing = false;
46      } else {
47	_start_note(playhead[0], playhead[1]);
48	playhead += 2;
49      }
50    }
51  }
52}
53
54void ToneSpeaker::start_melody(const unsigned char melody[])
55{
56  Serial.print("entering start_melody, first value is ");
57  Serial.println(melody[0]);
58  
59  if (melody[0] != 0) {
60    // Reset any existing melody timing.
61    note_timer = 0;
62
63    // Kick off the sequence; the update() function will continue it.
64    playing = true;
65    _start_note( melody[0], melody[1] );
66    playhead = &melody[2];
67  }
68}
69
70/****************************************************************/  
71void ToneSpeaker::send_debug(void)
72{
73  Serial.print("speaker pin:");
74  Serial.print(output_pin);
75
76  Serial.print("  playing: ");
77  Serial.print(playing);
78
79  Serial.print("  events: ");
80  Serial.print(event_count);
81
82  Serial.print("  tick duration: ");
83  Serial.print(tick_duration);
84  
85  Serial.println();
86}
87
88/****************************************************************/
  1/// \file PinballGame/pitch_table.h
  2/// \brief Define the mapping from MIDI notes to integer tone frequencies.
  3/// Transcribed from https://www.arduino.cc/en/Tutorial/toneMelody
  4
  5// Define the range of supported pitch values.
  6#define FIRST_MIDI_NOTE 23
  7#define LAST_MIDI_NOTE  111
  8
  9// The special PROGMEM keyword places the table in FLASH program memory (saving
 10// RAM space), but requires the use of AVR pgmspace functions to access it.
 11
 12const uint16_t midi_freq_table[] PROGMEM = {
 13  31,   // NOTE_B0, MIDI note 23
 14  33,   // NOTE_C1, MIDI note 24   
 15  35,   // NOTE_CS1 
 16  37,   // NOTE_D1  
 17  39,   // NOTE_DS1 
 18  41,   // NOTE_E1  
 19  44,   // NOTE_F1  
 20  46,   // NOTE_FS1 
 21  49,   // NOTE_G1  
 22  52,   // NOTE_GS1 
 23  55,   // NOTE_A1  
 24  58,   // NOTE_AS1 
 25  62,   // NOTE_B1  
 26  65,   // NOTE_C2  
 27  69,   // NOTE_CS2 
 28  73,   // NOTE_D2  
 29  78,   // NOTE_DS2 
 30  82,   // NOTE_E2  
 31  87,   // NOTE_F2  
 32  93,   // NOTE_FS2 
 33  98,   // NOTE_G2  
 34  104,   // NOTE_GS2 
 35  110,   // NOTE_A2  
 36  117,   // NOTE_AS2 
 37  123,   // NOTE_B2  
 38  131,   // NOTE_C3  
 39  139,   // NOTE_CS3 
 40  147,   // NOTE_D3  
 41  156,   // NOTE_DS3 
 42  165,   // NOTE_E3  
 43  175,   // NOTE_F3  
 44  185,   // NOTE_FS3 
 45  196,   // NOTE_G3  
 46  208,   // NOTE_GS3 
 47  220,   // NOTE_A3  
 48  233,   // NOTE_AS3 
 49  247,   // NOTE_B3  
 50  262,   // NOTE_C4, MIDI note 60  
 51  277,   // NOTE_CS4 
 52  294,   // NOTE_D4  
 53  311,   // NOTE_DS4 
 54  330,   // NOTE_E4  
 55  349,   // NOTE_F4  
 56  370,   // NOTE_FS4 
 57  392,   // NOTE_G4  
 58  415,   // NOTE_GS4 
 59  440,   // NOTE_A4, MIDI note 69, the usual orchestral tuning pitch  
 60  466,   // NOTE_AS4 
 61  494,   // NOTE_B4  
 62  523,   // NOTE_C5  
 63  554,   // NOTE_CS5 
 64  587,   // NOTE_D5  
 65  622,   // NOTE_DS5 
 66  659,   // NOTE_E5  
 67  698,   // NOTE_F5  
 68  740,   // NOTE_FS5 
 69  784,   // NOTE_G5  
 70  831,   // NOTE_GS5 
 71  880,   // NOTE_A5  
 72  932,   // NOTE_AS5 
 73  932,   // NOTE_AS5 
 74  988,   // NOTE_B5  
 75  1047,   // NOTE_C6  
 76  1109,   // NOTE_CS6 
 77  1175,   // NOTE_D6  
 78  1245,   // NOTE_DS6 
 79  1319,   // NOTE_E6  
 80  1397,   // NOTE_F6  
 81  1480,   // NOTE_FS6 
 82  1568,   // NOTE_G6  
 83  1661,   // NOTE_GS6 
 84  1760,   // NOTE_A6  
 85  1865,   // NOTE_AS6 
 86  1976,   // NOTE_B6  
 87  2093,   // NOTE_C7  
 88  2217,   // NOTE_CS7 
 89  2349,   // NOTE_D7  
 90  2489,   // NOTE_DS7 
 91  2637,   // NOTE_E7  
 92  2794,   // NOTE_F7  
 93  2960,   // NOTE_FS7 
 94  3136,   // NOTE_G7  
 95  3322,   // NOTE_GS7 
 96  3520,   // NOTE_A7  
 97  3729,   // NOTE_AS7 
 98  3951,   // NOTE_B7  
 99  4186,   // NOTE_C8, MIDI note 108  
100  4435,   // NOTE_CS8 
101  4699,   // NOTE_D8  
102  4978   // NOTE_DS8, MIDI note 111
103};

User Debugging Interface

  1/// \file PinballGame/console.cpp
  2
  3/// \brief User console interface for debugging using a host computer.
  4
  5/// \copyright No copyright, 2016, Garth Zeglin.  This file is explicitly placed
  6///            in the public domain.
  7
  8/// \details This file contains support code for implementing a command line
  9/// user interface using the default serial port on an Arduino.
 10
 11#include "Arduino.h"
 12#include "console.h"
 13
 14/****************************************************************/
 15/**** Global variables and constants ****************************/
 16/****************************************************************/
 17
 18// These are declared in the main file.
 19extern void user_print_report(void);
 20extern void user_reset_game(void);
 21extern void user_set_game_state(int value);
 22extern void user_set_game_score(unsigned long value);
 23extern void user_play_melody(void);
 24
 25// The maximum message line length.
 26const int MAX_LINE_LENGTH = 80;
 27 
 28// The maximum number of tokens in a single message.
 29const int MAX_TOKENS = 10;
 30
 31/****************************************************************/
 32/**** Utility functions *****************************************/
 33/****************************************************************/
 34
 35/// Send a single debugging string to the console.
 36void send_debug_message( const char *str )
 37{
 38  Serial.print("dbg ");
 39  Serial.println( str );
 40}
 41
 42/****************************************************************/
 43/// Send a single debugging integer to the console.
 44void send_debug_message( int i )
 45{
 46  Serial.print("dbg ");
 47  Serial.println( i );
 48}
 49
 50/****************************************************************/
 51/// Send a single-argument message back to the host.
 52void send_message( const char *command, long value )
 53{
 54  Serial.print( command );
 55  Serial.print( " " );
 56  Serial.println( value );
 57}
 58
 59/****************************************************************/
 60/// Send a two-argument message back to the host.
 61void send_message( const char *command, long value1, long value2 )
 62{
 63  Serial.print( command );
 64  Serial.print( " " );
 65  Serial.print( value1 );
 66  Serial.print( " " );
 67  Serial.println( value2 );
 68}
 69
 70/****************************************************************/
 71// Wrapper on strcmp for clarity of code.  Returns true if strings are
 72// identical.
 73static int string_equal( char *str1, const char str2[])
 74{
 75  return !strcmp(str1, str2);
 76}
 77
 78/****************************************************************/
 79/// Process an input message.  Unrecognized commands are silently ignored.
 80///   \param argc   number of argument tokens
 81///   \param argv   array of pointers to strings, one per token
 82static void parse_user_input(int argc, char *argv[])
 83{
 84  // Interpret the first token as a command symbol.
 85  char *command = argv[0];
 86
 87  /* -- process zero-argument commands --------------------------- */
 88  if (argc == 1) {
 89    if ( string_equal( command, "report" )) {
 90      user_print_report();
 91    }
 92    else if ( string_equal( command, "reset" )) {
 93      user_reset_game();
 94    }
 95    else if ( string_equal( command, "melody" )) {
 96      user_play_melody();
 97    }
 98    else {
 99      send_debug_message("unrecognized command.");
100    }    
101  }
102
103  /* -- process one-argument commands --------------------------- */
104  else if (argc == 2) {
105    int value = atoi(argv[1] );
106    
107    // Set the game state to a particular mode.
108    if ( string_equal( command, "state" )) {
109      user_set_game_state(value);
110    }
111    else if ( string_equal( command, "score" )) {
112      user_set_game_score(value);
113    }
114    else {
115      send_debug_message("unrecognized single-argument command.");
116    }    
117  }
118  else {
119    send_debug_message("unrecognized command format.");
120  }
121}
122
123/****************************************************************/
124/// Polling function to process messages arriving over the serial port.  Each
125/// iteration through this polling function processes at most one character.  It
126/// records the input message line into a buffer while simultaneously dividing it
127/// into 'tokens' delimited by whitespace.  Each token is a string of
128/// non-whitespace characters, and might represent either a symbol or an integer.
129/// Once a message is complete, parse_input_message() is called.
130
131void poll_console_input(unsigned long elapsed)
132{
133  static char input_buffer[ MAX_LINE_LENGTH ];   // buffer for input characters
134  static char *argv[MAX_TOKENS];                 // buffer for pointers to tokens
135  static int chars_in_buffer = 0;  // counter for characters in buffer
136  static int chars_in_token = 0;   // counter for characters in current partially-received token (the 'open' token)
137  static int argc = 0;             // counter for tokens in argv
138  static int error = 0;            // flag for any error condition in the current message
139
140  (void) elapsed;  // no-op to suppress compiler warning
141
142  // Check if at least one byte is available on the serial input.
143  if (Serial.available()) {
144    int input = Serial.read();
145
146    // If the input is a whitespace character, end any currently open token.
147    if ( isspace(input) ) {
148      if ( !error && chars_in_token > 0) {
149	if (chars_in_buffer == MAX_LINE_LENGTH) error = 1;
150	else {
151	  input_buffer[chars_in_buffer++] = 0;  // end the current token
152	  argc++;                               // increase the argument count
153	  chars_in_token = 0;                   // reset the token state
154	}
155      }
156
157      // If the whitespace input is an end-of-line character, then pass the message buffer along for interpretation.
158      if (input == '\r' || input == '\n') {
159
160	// if the message included too many tokens or too many characters, report an error
161	if (error) send_debug_message("excessive input error");
162
163	// else process any complete message
164	else if (argc > 0) parse_user_input( argc, argv ); 
165
166	// reset the full input state
167	error = chars_in_token = chars_in_buffer = argc = 0;                     
168      }
169    }
170
171    // Else the input is a character to store in the buffer at the end of the current token.
172    else {
173      // if beginning a new token
174      if (chars_in_token == 0) {
175
176	// if the token array is full, set an error state
177	if (argc == MAX_TOKENS) error = 1;
178
179	// otherwise save a pointer to the start of the token
180	else argv[ argc ] = &input_buffer[chars_in_buffer];
181      }
182
183      // the save the input and update the counters
184      if (!error) {
185	if (chars_in_buffer == MAX_LINE_LENGTH) error = 1;
186	else {
187	  input_buffer[chars_in_buffer++] = input;
188	  chars_in_token++;
189	}
190      }
191    }
192  }
193}