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.
/// \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
/// \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(); }
/// \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
/// \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; } }; /****************************************************************/
/// \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
/// \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; } }; /****************************************************************/
/// \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
/// \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 /****************************************************************/
/// \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(); } /****************************************************************/
/// \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
/// \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++; } } } } }
Source: Arduino Sketch PinballGame