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}