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.
Contents
Key Top-Level Functions¶
-
void
poll_sensor_inputs
(unsigned long interval)¶ Update all input state, including periodic sensor sampling, debouncing, and filtering.
-
void
poll_game_logic
(unsigned long interval)¶ Update the game state machine, including advancing game state and score and applying modal input-output mappings.
-
void
poll_actuator_outputs
(unsigned long interval)¶ Update all actuator state, including output pulse timers.
C++ Classes¶
-
class
PinballSensor
¶ Process inputs from a single-channel analog pinball sensor such as a photoreflective pair.
-
class
PopBumper
¶ Control a single pinball solenoid actuator. The primary function is to control timing of the impulsive actuators, but could be expanded to support PWM control for reducing holding currents.
-
class
ToneSpeaker
¶ Play musical tones on a speaker, used for audio game effects.
Main Source Code¶
The main top-level code is in PinballGame.ino.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 | /// \file PinballGame.ino
/// \brief Arduino program demonstrating essential real-time logic for a custom pinball machine.
/// \copyright No copyright, 2016-2017, Garth Zeglin. This file is explicitly placed in the public domain.
/// \details This example is intended as a starting point for developing a
/// custom pinball controller, demonstrating the use of state machines
/// to implement both I/O and game logic.
///
/// This example is written using Arduino C++ conventions:
///
/// 1. state variables are global
/// 2. global C++ objects are statically declared and initialized
/// 3. the sketch is divided into multiple .ino files which form one compilation unit
/// 4. C++ classes in .cpp files are separately compiled
/// This example assumes that several open-source libraries have been installed in the Arduino IDE:
/// 1. Adafruit-GFX general graphics support
/// 2. Adafruit-HT1632 LED matrix display driver
/// 3. Adafruit-WS2801 LED strand driver
/****************************************************************/
// Library imports.
#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_HT1632.h"
#include "Adafruit_WS2801.h"
// Forward declarations.
#include "PinballSensor.h"
#include "PopBumper.h"
#include "ToneSpeaker.h"
#include "StrandGraphics.h"
#include "MatrixGraphics.h"
#include "console.h"
/****************************************************************/
/**** Hardware pin assignments **********************************/
/****************************************************************/
// The following pin assignments correspond to the hardware on the PinballShield
// Rev A board.
// Analog inputs.
const int PHOTO1_PIN = A0; /// photo-interrupter input, value decreases when object present
const int PHOTO2_PIN = A1; /// photo-interrupter input, value decreases when object present
const int PHOTO3_PIN = A2; /// photo-interrupter input, value decreases when object present
const int PHOTO4_PIN = A3; /// photo-interrupter input, value decreases when object present
const int SWITCH1_PIN = A4; /// active-low switch input
const int SWITCH2_PIN = A5; /// active-low switch input
// Digital outputs. D0 and D1 are reserved for use as serial port RX/TX
const int LED1_PIN = 2; /// active-low LED output
const int SPEAKER_PIN = 3; /// active-high MOSFET-driven speaker output
const int LED2_PIN = 4; /// active-low LED output
const int SOLENOID4_PIN = 5; /// active-high MOSFET-driven solenoid output
const int SOLENOID3_PIN = 6; /// active-high MOSFET-driven solenoid output
const int MATRIX_DATA_PIN = 7; /// HT1632 LED matrix display data output
const int MATRIX_WR_PIN = 8; /// HT1632 LED matrix display clock output
const int SOLENOID2_PIN = 9; /// active-high MOSFET-driven solenoid output
const int SOLENOID1_PIN = 10; /// active-high MOSFET-driven solenoid output
const int STRAND_DATA_PIN = 11; /// WS2801 LED strand data output (yellow wire on strand)
const int MATRIX_CS0_PIN = 12; /// HT1632 LED matrix display select output
const int STRAND_CLK_PIN = 13; /// WS2801 LED strand clock output (green wire on strand)
/****************************************************************/
/**** Global variables and constants ****************************/
/****************************************************************/
// The baud rate is the number of bits per second transmitted over the serial port.
const long BAUD_RATE = 115200;
PinballSensor start_switch(SWITCH1_PIN);
PinballSensor bumper_sensor(PHOTO1_PIN);
PinballSensor drain_sensor(PHOTO2_PIN);
PopBumper bumper(SOLENOID1_PIN);
ToneSpeaker speaker(SPEAKER_PIN);
// Initialize the LED hardware.
Adafruit_HT1632LEDMatrix matrix = Adafruit_HT1632LEDMatrix(MATRIX_DATA_PIN, MATRIX_WR_PIN, MATRIX_CS0_PIN);
Adafruit_WS2801 strand = Adafruit_WS2801((uint16_t) 25, (uint8_t) STRAND_DATA_PIN, (uint8_t) STRAND_CLK_PIN, WS2801_GRB);
// ==============================================================
// Define the game state variables.
/// Define symbolic values for the main game state machine.
enum state_t { STATE_IDLE, STATE_ATTRACT, STATE_BALL1, STATE_BALL2, STATE_BALL3, STATE_GAME_OVER, NUM_STATES } game_state;
/// A count of the number of microseconds elapsed in the current game state.
long game_state_elapsed;
/// Convenient time constants, in microseconds.
const long FIVE_SECONDS = 5000000;
/// Current score (single player only).
unsigned long score = 0;
/// Attract mode melody.
const unsigned char attract_melody[] = {
MIDI_C4, EIGHTH, MIDI_E4, EIGHTH, MIDI_G4, EIGHTH, MIDI_C5, EIGHTH, MIDI_B4, EIGHTH, MIDI_G4, EIGHTH,
MIDI_E4, QUARTER,
MIDI_END
};
/****************************************************************/
/// Update all input state, including periodic sensor sampling, debouncing, and filtering.
void poll_sensor_inputs(unsigned long interval)
{
start_switch.update(interval);
bumper_sensor.update(interval);
drain_sensor.update(interval);
}
/****************************************************************/
/// Update the game state machine, including advancing game state and score and applying modal input-output mappings.
void poll_game_logic(unsigned long interval)
{
game_state_elapsed += interval;
switch(game_state) {
//-----------------------------------------------
case STATE_IDLE:
if (start_switch.isTriggered()) {
game_state_elapsed = 0;
game_state = STATE_BALL1;
send_debug_message("entering BALL1");
}
if (game_state_elapsed > FIVE_SECONDS) {
game_state_elapsed = 0;
game_state = STATE_ATTRACT;
speaker.start_melody(attract_melody);
strand_set_fast_animation();
send_debug_message("entering ATTRACT");
}
break;
//-----------------------------------------------
case STATE_ATTRACT:
if (start_switch.isTriggered()) {
game_state_elapsed = 0;
game_state = STATE_BALL1;
score = 0; // reset previous score
send_debug_message("entering BALL1");
}
if (game_state_elapsed > FIVE_SECONDS) {
game_state_elapsed = 0;
game_state = STATE_IDLE;
strand_set_slow_animation();
send_debug_message("entering IDLE");
}
break;
//-----------------------------------------------
case STATE_BALL1:
if (drain_sensor.isTriggered()) {
game_state_elapsed = 0;
game_state = STATE_BALL2;
send_debug_message("entering BALL2");
}
if (bumper_sensor.isTriggered()) {
score++;
bumper.trigger();
}
break;
//-----------------------------------------------
case STATE_BALL2:
if (drain_sensor.isTriggered()) {
game_state_elapsed = 0;
game_state = STATE_BALL3;
strand_set_fast_animation();
send_debug_message("entering BALL3");
}
if (bumper_sensor.isTriggered()) {
score++;
bumper.trigger();
}
break;
//-----------------------------------------------
case STATE_BALL3:
if (drain_sensor.isTriggered()) {
game_state_elapsed = 0;
game_state = STATE_GAME_OVER;
send_debug_message("entering GAME_OVER");
}
if (bumper_sensor.isTriggered()) {
score++;
bumper.trigger();
}
break;
//-----------------------------------------------
case STATE_GAME_OVER:
if (game_state_elapsed > FIVE_SECONDS) {
game_state_elapsed = 0;
game_state = STATE_IDLE;
strand_set_slow_animation();
send_debug_message("entering IDLE");
}
break;
//-----------------------------------------------
default:
// Any other value of game_state is invalid, so re-enter the IDLE state.
send_debug_message("Unexpected game_state entered.");
game_state = STATE_IDLE;
game_state_elapsed = 0;
break;
}
/****************************************************************/
// some simple LED animation based on the game state
switch(game_state) {
case STATE_IDLE:
digitalWrite(LED1_PIN, HIGH);
digitalWrite(LED2_PIN, HIGH);
break;
case STATE_ATTRACT:
digitalWrite(LED1_PIN, (game_state_elapsed % 500000) < 250000);
digitalWrite(LED2_PIN, (game_state_elapsed % 300000) < 150000);
break;
case STATE_BALL1:
case STATE_BALL2:
case STATE_BALL3:
if (game_state_elapsed < 1000000) {
// if the state was freshly entered, flash a little
digitalWrite(LED1_PIN, (game_state_elapsed % 100000) < 50000);
digitalWrite(LED2_PIN, (game_state_elapsed % 100000) < 50000);
} else {
// otherwise flash when the bumper is hit
digitalWrite(LED1_PIN, !bumper.isActive());
digitalWrite(LED2_PIN, !bumper.isActive());
}
break;
case STATE_GAME_OVER:
digitalWrite(LED1_PIN, (game_state_elapsed % 200000) < 100000);
digitalWrite(LED2_PIN, (game_state_elapsed % 200000) < 100000);
break;
default:
// On any invalid value or game_state, do nothing.
break;
}
}
/****************************************************************/
/// Update all actuator state, including output pulse timers.
void poll_actuator_outputs(unsigned long interval)
{
bumper.update(interval);
speaker.update(interval);
strand_update(interval);
// For now, just always show the current score on the LED matrix. This will
// evolve into a separate LED matrix animation state machine.
matrix_show_score(score);
}
/****************************************************************/
// Debugging functions which can be called from user console input.
void user_print_report(void)
{
send_debug_message("start of debugging report.");
start_switch.send_debug();
bumper_sensor.send_debug();
drain_sensor.send_debug();
bumper.send_debug();
speaker.send_debug();
send_debug_message("end of debugging report.");
}
void user_reset_game(void)
{
send_debug_message("user game reset.");
game_state = STATE_IDLE;
game_state_elapsed = 0;
}
void user_set_game_state(int value)
{
send_debug_message("user forcing game state.");
// Cast the integer to state_t, which is really just an integer, but
// considered a different kind of integer by the compiler.
if (value >= 0 && value < NUM_STATES) {
game_state = (state_t) value;
game_state_elapsed = 0;
}
}
void user_set_game_score(unsigned long value)
{
send_debug_message("user forcing game score.");
score = value;
}
void user_play_melody(void)
{
send_debug_message("user forcing melody.");
speaker.start_melody(attract_melody);
}
/****************************************************************/
/**** Standard entry points for Arduino system ******************/
/****************************************************************/
/// Standard Arduino initialization function to configure the system. This
/// function is called once after reset to initialize the program.
void setup()
{
// configure the actuator pins as soon as possible.
pinMode(LED1_PIN , OUTPUT);
pinMode(SPEAKER_PIN , OUTPUT);
pinMode(LED2_PIN , OUTPUT);
pinMode(SOLENOID4_PIN, OUTPUT);
pinMode(SOLENOID3_PIN, OUTPUT);
pinMode(SOLENOID2_PIN, OUTPUT);
pinMode(SOLENOID1_PIN, OUTPUT);
digitalWrite(LED1_PIN , HIGH); // off
digitalWrite(LED2_PIN , HIGH); // off
digitalWrite(SOLENOID4_PIN, LOW ); // off
digitalWrite(SOLENOID3_PIN, LOW ); // off
digitalWrite(SOLENOID2_PIN, LOW ); // off
digitalWrite(SOLENOID1_PIN, LOW ); // off
// finish configuring the LED hardware
matrix.begin(ADA_HT1632_COMMON_16NMOS);
matrix.fillScreen();
matrix.clearScreen();
matrix.writeScreen();
strand.begin();
strand.show();
// initialize the Serial port for the user debugging console
Serial.begin( BAUD_RATE );
// send a message as a diagnostic
send_debug_message("wakeup");
}
/****************************************************************/
/// Standard Arduino polling function to handle all I/O and periodic processing.
/// This function is called repeatedly as fast as possible from within the
/// built-in library to poll program events. This loop should never be allowed
/// to stall or block so that all tasks can be constantly serviced.
void loop()
{
// The timestamp in microseconds for the last polling cycle, used to compute
// the exact interval between output updates.
static unsigned long last_update_clock = 0;
// Read the microsecond clock.
unsigned long now = micros();
// Compute the time elapsed since the last poll. This will correctly handle wrapround of
// the 32-bit long time value given the properties of twos-complement arithmetic.
unsigned long interval = now - last_update_clock;
last_update_clock = now;
// Begin the polling cycle.
poll_sensor_inputs(interval);
poll_game_logic(interval);
poll_actuator_outputs(interval);
poll_console_input(interval);
}
/****************************************************************/
/****************************************************************/
|
Graphics¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /// \file PinballGame/MatrixGraphics.cpp
/// \brief Real-time graphics functions for a HT1632 monochrome LED matrix.
/// \copyright No copyright, 2016-2017, Garth Zeglin. This file is explicitly placed in the public domain.
/// \details This is a collection of functions compiled with the main pinball
/// game code. Since only one LED matrix is supported, these are all global
/// functions with global state.
#include "Arduino.h"
#include "Adafruit_GFX.h"
#include "Adafruit_HT1632.h"
#include "MatrixGraphics.h"
// The 'matrix' object is declared in the main file.
extern Adafruit_HT1632LEDMatrix matrix;
void matrix_show_score(unsigned long points)
{
matrix.clearScreen();
// draw some text!
matrix.setTextSize(1 * .5); // size 1 == 8 pixels high
matrix.setTextColor(1); // 'lit' LEDs
matrix.setTextWrap(false);
matrix.setCursor(5, 4);// start at top left, with one pixel of spacing
matrix.print(points);
matrix.writeScreen();
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | /// \file PinballGame/StrandGraphics.cpp
/// \brief Real-time graphics functions for a WS2801 RGB LED strand.
/// \copyright No copyright, 2016-2017, Garth Zeglin. This file is explicitly placed in the public domain.
/// \details This is a collection of functions compiled with the main pinball
/// game code. Since only one LED strand is supported, these are all global
/// functions with global state.
#include "Arduino.h"
#include "Adafruit_WS2801.h"
#include "StrandGraphics.h"
// The 'strand' object is declared in the main file with type Adafruit_WS2801 strand.
extern Adafruit_WS2801 strand;
/// LED animation state.
static long strand_interval = 50000; /// microseconds between frames
static long strand_timer = 0; /// microseconds until the next frame
static int strand_frame = 0; /// current animation frame count
/// Utility function to create a 24 bit RGB color value encoded in a 32 bit integer.
static uint32_t strand_color(byte r, byte g, byte b)
{
uint32_t c;
c = r;
c <<= 8;
c |= g;
c <<= 8;
c |= b;
return c;
}
/// Utility function to compute a 32 bit RGB color value from an 8-bit phase.
/// The colors are a transition from r -> g -> b -> r ...
static uint32_t strand_color_wheel(byte color_phase)
{
if (color_phase < 85) {
return strand_color(color_phase * 3, 255 - color_phase * 3, 90);
} else if (color_phase < 170) {
color_phase -= 85;
return strand_color(255 - color_phase * 3, 90, color_phase * 3);
} else {
color_phase -= 170;
return strand_color(90, color_phase * 3, 255 - color_phase * 3);
}
}
/// Display one frame of an animated color spectrum. The frame value can
/// continuously increment for each frame update.
static void strand_show_rainbow(int frame)
{
int i;
for (i = 0; i < strand.numPixels(); i++) {
// tricky math! we use each pixel as a fraction of the full 96-color wheel
// (thats the i / strand.numPixels() part)
// Then add in frame which makes the colors go around per pixel
// the % 96 is to make the wheel cycle around
strand.setPixelColor(i, strand_color_wheel( ((i * 256 / strand.numPixels()) + frame) % 256) );
}
strand.show(); // write all the pixels out
}
/// Poll the LED strand timer and update the hardware with new animation as needed.
void strand_update(unsigned long interval)
{
// Subtract the elapsed time from the counter until the right amount of time
// has elapsed; this will keep the update rate more constant as the execution
// rate varies.
strand_timer -= interval;
if (strand_timer < 0){
strand_timer += strand_interval;
strand_show_rainbow(strand_frame++);
}
}
const int FAST_LEDS = 50000;
const int SLOW_LEDS = 100000;
void strand_set_fast_animation(void)
{
strand_interval = FAST_LEDS;
}
void strand_set_slow_animation(void)
{
strand_interval = SLOW_LEDS;
}
|
Sensor Processing¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | /// \file PinballGame/PinballSensor.h
/// \brief Process inputs from a single-channel analog pinball sensor such as a photoreflective pair.
/// \copyright No copyright, 2016, Garth Zeglin. This file is explicitly placed in the public domain.
/// \details This file contains support code for implementing input processing
/// for the Arduino pinball machine demo.
/****************************************************************/
class PinballSensor {
private:
/// Number of the analog pin to use for input. The hardware is assumed to be
/// active-low, i.e., idling at a high voltage, and pulled low during an
/// 'event'.
int input_pin;
/// Input sampling interval, in microseconds.
long sampling_interval;
/// Countdown to the next input measurement, in microseconds.
long sample_timer;
/// Most recent raw measurement. The units are the 10-bit (0-1023) integer ADC value.
int raw_input;
/// Analog input threshold defining the trigger level for an 'event', in ADC units.
int lower_threshold;
/// Analog input threshold defining the trigger level for resetting. The
/// difference between upper_threshold and lower_threshold defines the
/// 'deadband' of 'hysteresis' of the event detector. Specified in ADC units.
int upper_threshold;
/// The current Boolean state of the input detector.
bool active;
/// A Boolean flag which has a true value only during the polling cycle in
/// which an event begins. This supports event-driven programming in which
/// the sensor input causes another event to begin.
bool triggered;
/// Count of the total number of events observed.
long event_count;
/// Count of the total number of samples measured.
long sample_count;
public:
/// Constructor to initialize an instance of the class.
PinballSensor(int pin);
/// Update function to be called as frequently as possible to sample the pin
/// and process the data. It requires the number of microseconds elapsed
/// since the last update.
void update(unsigned long interval);
/// Debugging function to print a representation of the current state to the serial port.
void send_debug(void);
/// Access function to return the current state.
bool isTriggered(void) { return triggered; }
};
/****************************************************************/
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | /// \file PinballGame/PinballSensor.cpp
/// \copyright No copyright, 2016, Garth Zeglin. This file is explicitly placed in the public domain.
/****************************************************************/
#include "Arduino.h"
#include "PinballSensor.h"
/****************************************************************/
// Constructor for an instance of the class.
PinballSensor::PinballSensor(int pin)
{
// initialize the state variables
input_pin = pin;
sample_timer = 0;
raw_input = 0;
sampling_interval = 5000; // 5000 usec == 5 msec == 200 Hz
lower_threshold = 700;
upper_threshold = 750;
active = false;
triggered = false;
event_count = 0;
sample_count = 0;
}
/****************************************************************/
// Update polling function for an instance of the class.
void PinballSensor::update(unsigned long interval)
{
// always reset any event indication
triggered = false;
// test whether to sample the input
sample_timer -= interval;
if (sample_timer <= 0) {
// Reset the timer for the next sampling period. Adding in the value helps
// maintain precise timing in the presence of variation in the polling time,
// e.g. if this sampling point was a little late, the next one will occur a
// little sooner, maintaining the overall average.
sample_timer += sampling_interval;
// read the raw input
raw_input = analogRead(input_pin);
sample_count++;
if (!active) {
// if waiting for another input, use the lower threshold to detect an event
if (raw_input < lower_threshold) {
active = true;
triggered = true;
event_count++;
}
}
else {
// if waiting for an input to end, use the upper threshold to detect a reset
if (raw_input > upper_threshold) {
active = false;
}
}
}
}
/****************************************************************/
void PinballSensor::send_debug(void)
{
Serial.print("sensor pin:");
Serial.print(input_pin);
Serial.print(" raw: ");
Serial.print(raw_input);
Serial.print(" active: ");
Serial.print(active);
Serial.print(" samples: ");
Serial.print(sample_count);
Serial.print(" events: ");
Serial.print(event_count);
Serial.print(" thresholds: ");
Serial.print(lower_threshold);
Serial.print(" ");
Serial.print(upper_threshold);
Serial.println();
}
/****************************************************************/
|
Actuator Control¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | /// \file PinballGame/PopBumper.h
/// \brief Control a single pinball solenoid actuator.
/// \copyright No copyright, 2016, Garth Zeglin. This file is explicitly placed in the public domain.
/// \details This file contains support code for implementing impulsive actuator
/// control for the Arduino pinball machine demo. This is very simple for now,
/// but could be expanded to support PWM control for reducing holding currents.
/****************************************************************/
class PopBumper {
private:
/// Number of the digital pin to use for output. The hardware is assumed to be
/// active-high, i.e., idling at a low voltage, and driven high when firing the solenoid.
int output_pin;
/// Countdown for the actuator ON period, in microseconds.
long output_timer;
/// Duration for the actuator ON period, in microseconds.
long pulse_width;
/// The current Boolean state of the output.
bool active;
/// Count of the total number of output events.
long event_count;
public:
/// Constructor to initialize an instance of the class. Note that this only
/// initializes object state, it does not configure the hardware, which should
/// be performed directly by the user.
PopBumper(int pin);
/// Update function to be called as frequently as possible to operate the
/// output state machine. It requires the number of microseconds elapsed since
/// the last update.
void update(unsigned long interval);
/// Trigger function to start an actuation cycle.
void trigger(void);
/// Debugging function to print a representation of the current state to the serial port.
void send_debug(void);
/// Access function to return the current state.
bool isActive(void) { return active; }
};
/****************************************************************/
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | /// \file PinballGame/PopBumper.cpp
/// \copyright No copyright, 2016, Garth Zeglin. This file is explicitly placed in the public domain.
/****************************************************************/
#include "Arduino.h"
#include "PopBumper.h"
/****************************************************************/
PopBumper::PopBumper(int pin)
{
output_pin = pin;
output_timer = 0;
active = false;
event_count = 0;
pulse_width = 100000; // 100 msec = 0.1 seconds
}
void PopBumper::update(unsigned long interval)
{
// for now, this only needs to check when to turn off the solenoid
if (active) {
output_timer -= interval;
if (output_timer < 0) {
active = false;
digitalWrite(output_pin, LOW);
}
}
}
void PopBumper::trigger(void)
{
// only accept a trigger if not already active; if the solenoid is currently firing, the new trigger is ignored
if (!active) {
output_timer = pulse_width;
active = true;
digitalWrite(output_pin, HIGH);
}
}
void PopBumper::send_debug(void)
{
Serial.print("bumper pin:");
Serial.print(output_pin);
Serial.print(" active: ");
Serial.print(active);
Serial.print(" events: ");
Serial.print(event_count);
Serial.print(" pulse width: ");
Serial.print(pulse_width);
Serial.println();
}
/****************************************************************/
|
Speaker Melodies¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | /// \file PinballGame/ToneSpeaker.h
/// \brief Play musical tones on a speaker.
/// \copyright No copyright, 2016, Garth Zeglin. This file is explicitly placed in the public domain.
/// \details This file contains support code for implementing audio effects for
/// the Arduino pinball machine demo, playing tone sequences at constant volume.
/****************************************************************/
class ToneSpeaker {
private:
/// Number of the digital pin to use for output. The hardware is assumed to
/// be active-high, i.e., idling at a low voltage with no speaker current, and
/// driven high when driving the speaker.
int output_pin;
/// Countdown for the current note or silence period, in microseconds.
long note_timer;
/// Tempo multiplier: duration in microseconds of a single MIDI 'tick' which
/// is 1/24 of a quarter note.
long tick_duration;
/// True if currently playing a tone sequence.
bool playing;
/// Count of the total number of notes played.
long event_count;
/// Pointer to the next note to play. A melody is specified as a series of
/// pairs of bytes: note value, duration. Invalid notes will play as a rest
/// (silence). A zero note ends the sequence.
const unsigned char *playhead;
/// Private function to begin a new note.
void _start_note(unsigned char note, unsigned char value);
public:
/// Constructor to initialize an instance of the class. Note that this only
/// initializes object state, it does not configure the hardware, which should
/// be performed directly by the user.
ToneSpeaker(int pin);
/// Set the tempo in beats per minute. The desired units are microseconds per MIDI tick:
/// (microseconds / tick) = (microseconds / minute) / (ticks/minute)
/// (ticks / minute) = (ticks / beat) * (beat / minute)
// So:
/// (microseconds / tick) = (microseconds / minute) / ((ticks / beat) * (beat / minute))
/// (microseconds / tick) = 60000000 / (24 * (beat / minute))
/// (microseconds / tick) = 2500000 / (beat / minute)
void setTempo(int bpm) { tick_duration = 2500000L / bpm; }
/// Update function to be called as frequently as possible to operate the
/// output state machine. It requires the number of microseconds elapsed since
/// the last update.
void update(unsigned long interval);
/// Start the player on a new melody. A melody is specified as a series of pairs of
/// bytes: note value, duration. The melody ends with a zero note. Invalid notes are rests.
void start_melody(const unsigned char melody[]);
/// Debugging function to print a representation of the current state to the serial port.
void send_debug(void);
/// Access function to check whether a melody is currently playing.
bool isPlaying(void) { return playing; }
};
/****************************************************************/
// Convenient symbols for the MIDI pitch scale. Each value is a half-step. For details of the tone
// definitions, see pitch_table.h.
#define MIDI_MIDDLE_C 60
#define MIDI_END 0
#define MIDI_REST 1
#define MIDI_C1 24
#define MIDI_C2 36
#define MIDI_C3 48
#define MIDI_C4 60
#define MIDI_D4 62
#define MIDI_E4 64
#define MIDI_F4 65
#define MIDI_G4 67
#define MIDI_A4 69
#define MIDI_B4 71
#define MIDI_C5 72
#define MIDI_D5 74
#define MIDI_E5 76
#define MIDI_F5 77
#define MIDI_G5 79
#define MIDI_A5 81
#define MIDI_B5 83
// Define note durations in units of MIDI beat clock 'ticks', each 1/24 of a
// quarter note. This multiplier allows even triplets, e.g. three
// eighth-triplets equals one quarter note.
#define HALF 48
#define QUARTER 24
#define EIGHTH 12
#define SIXTEENTH 6
#define THIRTYSECOND 3
#define EIGHTH_TRIPLET 8
#define SIXTEENTH_TRIPLET 4
#define THIRTYSECOND_TRIPLET 2
/****************************************************************/
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | /// \file PinballGame/ToneSpeaker.cpp
/// \copyright No copyright, 2016, Garth Zeglin. This file is explicitly placed in the public domain.
/****************************************************************/
#include "Arduino.h"
#include "ToneSpeaker.h"
#include "pitch_table.h"
/****************************************************************/
ToneSpeaker::ToneSpeaker(int pin)
{
output_pin = pin;
note_timer = 0;
setTempo(120);
playing = false;
event_count = 0;
playhead = NULL;
}
void ToneSpeaker::_start_note(unsigned char note, unsigned char value)
{
if (note < FIRST_MIDI_NOTE || note > LAST_MIDI_NOTE) {
noTone(output_pin);
} else {
// Use the AVR pgmspace.h API to read a value from the table in FLASH.
// Reference: https://www.arduino.cc/en/Reference/PROGMEM
int pitch = pgm_read_word_near( midi_freq_table + (note - FIRST_MIDI_NOTE));
tone(output_pin, pitch);
}
note_timer += value * tick_duration;
event_count++;
}
void ToneSpeaker::update(unsigned long interval)
{
if (playing) {
note_timer -= interval;
if (note_timer < 0) {
// start the next note
if (*playhead == 0) {
// if end of sequence
noTone(output_pin);
playing = false;
} else {
_start_note(playhead[0], playhead[1]);
playhead += 2;
}
}
}
}
void ToneSpeaker::start_melody(const unsigned char melody[])
{
Serial.print("entering start_melody, first value is ");
Serial.println(melody[0]);
if (melody[0] != 0) {
// Reset any existing melody timing.
note_timer = 0;
// Kick off the sequence; the update() function will continue it.
playing = true;
_start_note( melody[0], melody[1] );
playhead = &melody[2];
}
}
/****************************************************************/
void ToneSpeaker::send_debug(void)
{
Serial.print("speaker pin:");
Serial.print(output_pin);
Serial.print(" playing: ");
Serial.print(playing);
Serial.print(" events: ");
Serial.print(event_count);
Serial.print(" tick duration: ");
Serial.print(tick_duration);
Serial.println();
}
/****************************************************************/
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | /// \file PinballGame/pitch_table.h
/// \brief Define the mapping from MIDI notes to integer tone frequencies.
/// Transcribed from https://www.arduino.cc/en/Tutorial/toneMelody
// Define the range of supported pitch values.
#define FIRST_MIDI_NOTE 23
#define LAST_MIDI_NOTE 111
// The special PROGMEM keyword places the table in FLASH program memory (saving
// RAM space), but requires the use of AVR pgmspace functions to access it.
const uint16_t midi_freq_table[] PROGMEM = {
31, // NOTE_B0, MIDI note 23
33, // NOTE_C1, MIDI note 24
35, // NOTE_CS1
37, // NOTE_D1
39, // NOTE_DS1
41, // NOTE_E1
44, // NOTE_F1
46, // NOTE_FS1
49, // NOTE_G1
52, // NOTE_GS1
55, // NOTE_A1
58, // NOTE_AS1
62, // NOTE_B1
65, // NOTE_C2
69, // NOTE_CS2
73, // NOTE_D2
78, // NOTE_DS2
82, // NOTE_E2
87, // NOTE_F2
93, // NOTE_FS2
98, // NOTE_G2
104, // NOTE_GS2
110, // NOTE_A2
117, // NOTE_AS2
123, // NOTE_B2
131, // NOTE_C3
139, // NOTE_CS3
147, // NOTE_D3
156, // NOTE_DS3
165, // NOTE_E3
175, // NOTE_F3
185, // NOTE_FS3
196, // NOTE_G3
208, // NOTE_GS3
220, // NOTE_A3
233, // NOTE_AS3
247, // NOTE_B3
262, // NOTE_C4, MIDI note 60
277, // NOTE_CS4
294, // NOTE_D4
311, // NOTE_DS4
330, // NOTE_E4
349, // NOTE_F4
370, // NOTE_FS4
392, // NOTE_G4
415, // NOTE_GS4
440, // NOTE_A4, MIDI note 69, the usual orchestral tuning pitch
466, // NOTE_AS4
494, // NOTE_B4
523, // NOTE_C5
554, // NOTE_CS5
587, // NOTE_D5
622, // NOTE_DS5
659, // NOTE_E5
698, // NOTE_F5
740, // NOTE_FS5
784, // NOTE_G5
831, // NOTE_GS5
880, // NOTE_A5
932, // NOTE_AS5
932, // NOTE_AS5
988, // NOTE_B5
1047, // NOTE_C6
1109, // NOTE_CS6
1175, // NOTE_D6
1245, // NOTE_DS6
1319, // NOTE_E6
1397, // NOTE_F6
1480, // NOTE_FS6
1568, // NOTE_G6
1661, // NOTE_GS6
1760, // NOTE_A6
1865, // NOTE_AS6
1976, // NOTE_B6
2093, // NOTE_C7
2217, // NOTE_CS7
2349, // NOTE_D7
2489, // NOTE_DS7
2637, // NOTE_E7
2794, // NOTE_F7
2960, // NOTE_FS7
3136, // NOTE_G7
3322, // NOTE_GS7
3520, // NOTE_A7
3729, // NOTE_AS7
3951, // NOTE_B7
4186, // NOTE_C8, MIDI note 108
4435, // NOTE_CS8
4699, // NOTE_D8
4978 // NOTE_DS8, MIDI note 111
};
|
User Debugging Interface¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | /// \file PinballGame/console.cpp
/// \brief User console interface for debugging using a host computer.
/// \copyright No copyright, 2016, Garth Zeglin. This file is explicitly placed
/// in the public domain.
/// \details This file contains support code for implementing a command line
/// user interface using the default serial port on an Arduino.
#include "Arduino.h"
#include "console.h"
/****************************************************************/
/**** Global variables and constants ****************************/
/****************************************************************/
// These are declared in the main file.
extern void user_print_report(void);
extern void user_reset_game(void);
extern void user_set_game_state(int value);
extern void user_set_game_score(unsigned long value);
extern void user_play_melody(void);
// The maximum message line length.
const int MAX_LINE_LENGTH = 80;
// The maximum number of tokens in a single message.
const int MAX_TOKENS = 10;
/****************************************************************/
/**** Utility functions *****************************************/
/****************************************************************/
/// Send a single debugging string to the console.
void send_debug_message( const char *str )
{
Serial.print("dbg ");
Serial.println( str );
}
/****************************************************************/
/// Send a single debugging integer to the console.
void send_debug_message( int i )
{
Serial.print("dbg ");
Serial.println( i );
}
/****************************************************************/
/// Send a single-argument message back to the host.
void send_message( const char *command, long value )
{
Serial.print( command );
Serial.print( " " );
Serial.println( value );
}
/****************************************************************/
/// Send a two-argument message back to the host.
void send_message( const char *command, long value1, long value2 )
{
Serial.print( command );
Serial.print( " " );
Serial.print( value1 );
Serial.print( " " );
Serial.println( value2 );
}
/****************************************************************/
// Wrapper on strcmp for clarity of code. Returns true if strings are
// identical.
static int string_equal( char *str1, const char str2[])
{
return !strcmp(str1, str2);
}
/****************************************************************/
/// Process an input message. Unrecognized commands are silently ignored.
/// \param argc number of argument tokens
/// \param argv array of pointers to strings, one per token
static void parse_user_input(int argc, char *argv[])
{
// Interpret the first token as a command symbol.
char *command = argv[0];
/* -- process zero-argument commands --------------------------- */
if (argc == 1) {
if ( string_equal( command, "report" )) {
user_print_report();
}
else if ( string_equal( command, "reset" )) {
user_reset_game();
}
else if ( string_equal( command, "melody" )) {
user_play_melody();
}
else {
send_debug_message("unrecognized command.");
}
}
/* -- process one-argument commands --------------------------- */
else if (argc == 2) {
int value = atoi(argv[1] );
// Set the game state to a particular mode.
if ( string_equal( command, "state" )) {
user_set_game_state(value);
}
else if ( string_equal( command, "score" )) {
user_set_game_score(value);
}
else {
send_debug_message("unrecognized single-argument command.");
}
}
else {
send_debug_message("unrecognized command format.");
}
}
/****************************************************************/
/// Polling function to process messages arriving over the serial port. Each
/// iteration through this polling function processes at most one character. It
/// records the input message line into a buffer while simultaneously dividing it
/// into 'tokens' delimited by whitespace. Each token is a string of
/// non-whitespace characters, and might represent either a symbol or an integer.
/// Once a message is complete, parse_input_message() is called.
void poll_console_input(unsigned long elapsed)
{
static char input_buffer[ MAX_LINE_LENGTH ]; // buffer for input characters
static char *argv[MAX_TOKENS]; // buffer for pointers to tokens
static int chars_in_buffer = 0; // counter for characters in buffer
static int chars_in_token = 0; // counter for characters in current partially-received token (the 'open' token)
static int argc = 0; // counter for tokens in argv
static int error = 0; // flag for any error condition in the current message
(void) elapsed; // no-op to suppress compiler warning
// Check if at least one byte is available on the serial input.
if (Serial.available()) {
int input = Serial.read();
// If the input is a whitespace character, end any currently open token.
if ( isspace(input) ) {
if ( !error && chars_in_token > 0) {
if (chars_in_buffer == MAX_LINE_LENGTH) error = 1;
else {
input_buffer[chars_in_buffer++] = 0; // end the current token
argc++; // increase the argument count
chars_in_token = 0; // reset the token state
}
}
// If the whitespace input is an end-of-line character, then pass the message buffer along for interpretation.
if (input == '\r' || input == '\n') {
// if the message included too many tokens or too many characters, report an error
if (error) send_debug_message("excessive input error");
// else process any complete message
else if (argc > 0) parse_user_input( argc, argv );
// reset the full input state
error = chars_in_token = chars_in_buffer = argc = 0;
}
}
// Else the input is a character to store in the buffer at the end of the current token.
else {
// if beginning a new token
if (chars_in_token == 0) {
// if the token array is full, set an error state
if (argc == MAX_TOKENS) error = 1;
// otherwise save a pointer to the start of the token
else argv[ argc ] = &input_buffer[chars_in_buffer];
}
// the save the input and update the counters
if (!error) {
if (chars_in_buffer == MAX_LINE_LENGTH) error = 1;
else {
input_buffer[chars_in_buffer++] = input;
chars_in_token++;
}
}
}
}
}
|