PinballLogic Arduino Sketch

This sketch provides a basic pinball machine controller as an extended example of a real-time logic controller.

All files may be downloaded in a single archive file as PinballLogic.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.

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

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

class PopBumper

Control a single pinball solenoid actuator.

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.

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

class ToneSpeaker

Play musical tones on a speaker.

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

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

Main Source Code

The main top-level code is in PinballLogic.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
/// \file PinballLogic/PinballLogic.ino

/// \brief Arduino program demonstrating basic real-time logic for a custom pinball machine.

/// \copyright No copyright, 2016, 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

/****************************************************************/
// Forward declarations.
#include "PinballSensor.h"
#include "PopBumper.h"
#include "ToneSpeaker.h"

// functions in console.ino
extern void send_debug_message( const char *str );

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

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

// Analog input assignments.
const int start_switch_pin  = A4; // 'SWITCH1' on schematic
const int bumper_sensor_pin = A0; // 'PHOTO1' on schematic
const int drain_sensor_pin  = A1; // 'PHOTO2' on schematic

// Actuator output assignments.
const int LED1_pin    = 2;   // inverse-logic LED, 'LED1' on schematic
const int LED2_pin    = 4;   // inverse-logic LED, 'LED2' on schematic
const int bumper_pin  = 10;  // positive-logic solenoid driver, 'SOLENOID1' on schematic
const int speaker_pin = 3;   // positive-logic speaker driver, 'SPEAKER' on schematic

/****************************************************************/
/**** 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(start_switch_pin);
PinballSensor bumper_sensor(bumper_sensor_pin);
PinballSensor drain_sensor(drain_sensor_pin);
PopBumper     bumper(bumper_pin);
ToneSpeaker   speaker(speaker_pin);

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

/// 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);
      send_debug_message("entering ATTRACT");
    }
    break;

    //-----------------------------------------------
  case STATE_ATTRACT:
    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_IDLE;
      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()) {
      bumper.trigger();
    }
    
    break;

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

    if (bumper_sensor.isTriggered()) {
      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()) {
      bumper.trigger();
    }

    break;

    //-----------------------------------------------
  case STATE_GAME_OVER:
    if (game_state_elapsed > FIVE_SECONDS) {
      game_state_elapsed = 0;
      game_state = STATE_IDLE;
      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);
}

/****************************************************************/
// Debugging functions which can be called from user console input.
void user_print_report()
{
  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;
  }
}

/****************************************************************/
/**** 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(LED2_pin, OUTPUT);
  pinMode(bumper_pin, OUTPUT);
  pinMode(speaker_pin, OUTPUT);
  
  digitalWrite(LED1_pin,  HIGH); // off
  digitalWrite(LED2_pin,  HIGH); // off
  digitalWrite(bumper_pin, LOW); // off
  noTone(speaker_pin);
  
  // 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);
}

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

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 PinballLogic/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 PinballLogic/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 PinballLogic/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 PinballLogic/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 PinballLogic/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 PinballLogic/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 PinballLogic/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
/// \file PinballLogic/console.ino

/// \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.

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

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

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, (char *) "report" )) {
      user_print_report();
    }
    else if ( string_equal( command, (char *) "reset" )) {
      user_reset_game();
    }
    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, (char *) "state" )) {
      user_set_game_state(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;  // silence warnings about unused parameter

  // 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++;
	}
      }
    }
  }
}