Open Source · MCU Framework
ATmega2560 + ATmega328P · RS485 Bus

Build Your Machine.Your Rules.

Micro PinOS is a modular Micro PinOS — Nano switch nodes on the playfield, Mega 2560 game brain in the cabinet, connected over a shared RS485 bus.

5
Nano Nodes Max
32+
Switches Supported
4
Game Styles Built-In
NANO 0SLINGS + BUMPERSTYPE 2 · REACTION NANO 1ROLLOVER LANESTYPE 1 · SENSOR NANO 2TARGETS + RAMPSTYPE 1 · SENSOR NANO 3CABINET + TILTTYPE 1 · SENSOR NANO 4FLIPPERSTYPE 3 · DUAL WOUND RS485 BUS 115200 BAUD ATmega2560 GAME BRAIN readSwitchEvents() updateGameLogic() updateSolenoids() updateDisplays() updateLamps() Serial2 → SOUND Serial3 → LIGHTS/DISPLAY ATTRACT → GAME → DRAIN GAME OVER → ATTRACT EEPROM → Hi Score GameStyle Plugins ×4 N0:S3:1 EVENTS
// How it works

Four Layers, One Framework

1

Nano Reads Switches

Each Nano debounces its switches and fires local solenoids — pop bumpers and slings respond in microseconds, no Mega roundtrip.

2

Events Over RS485

Switch events travel as compact serial frames over one shared twisted pair. Up to five Nanos share the bus back to the Mega.

3

Mega Runs the Game

The Mega dispatches events to the active GameStyle plugin, manages scoring, ball tracking, and all high-power outputs.

4

Commands Out

SOUND, SCORE, and LIGHT commands go to dedicated secondary boards over separate serial channels.

// Real code

Infrastructure, Not Magic

The Mega's loop() does exactly four things, in order, every pass. No blocking delays anywhere. The Nano sketch is a single config table at the top — change the pins and IDs, reflash, done.

All game logic lives in GameStyle .h files. The OS knows nothing about scoring rules.

Full Architecture Docs →
// MainMega.ino — main loop, four calls only
void loop() {
  readRS485();         // drain bus → circular queue
  processNextEvent();  // one event → dispatcher
  updateGameLogic();   // state machine + timers
  updateSolenoids();   // non-blocking coil pulses
}

// RS485 frame parser — "S<id>:<state>\n"
void readRS485() {
  while (Serial1.available()) {
    char c = (char)Serial1.read();
    if (c == '\n') {
      rs485Buf[rs485Pos] = '\0';
      if (rs485Buf[0] == 'S') {
        char *col = strchr(rs485Buf+1, ':');
        if (col) enqueue(atoi(rs485Buf+1),
                         atoi(col+1));
      }
      rs485Pos = 0;
    } else if (rs485Pos < sizeof(rs485Buf)-1) {
      rs485Buf[rs485Pos++] = c;
    }
  }
}
// nano_universal.ino — configure at top, done
#define MY_ADDRESS  0      // unique per Nano (0–4)
#define DE_PIN      2      // MAX485 transmit enable

// pin | switchID | solPin | holdPin | pulseMs | debounceMs
// Type 1 Sensor:   solPin = -1, holdPin = -1
// Type 2 Reaction: solPin >= 0, holdPin = -1
// Type 3 Flipper:  solPin = power, holdPin = hold

static const SwEntry SW[] = {
  // Nano 0 — Slingshots + pop bumpers (Type 2)
  {  3, SW_SLINGSHOT_LEFT,   5, -1, 25, 5 },
  {  4, SW_SLINGSHOT_RIGHT,  6, -1, 25, 5 },
  {  7, SW_POP_BUMPER_TOP,   8, -1, 20, 5 },
  {  9, SW_POP_BUMPER_LEFT, 10, -1, 20, 5 },
  { 11, SW_POP_BUMPER_RIGHT,12, -1, 20, 5 },
};

// Nothing below this line needs editing
void loop() {
  readSerial();       // mode commands from Mega
  handleSwitches();   // debounce, fire, report
  handleSolenoids();  // kick/hold timing
}
// Event dispatcher — no game logic lives here
void dispatchEvent(int sw, int state) {

  // P1: Slam tilt — kills everything, any state
  if (sw == SW_SLAM_TILT && state == 1) {
    gameState = STATE_GAME_OVER;
    broadcastNanoMode(MODE_ATTRACT);
    sendSound(SND_SLAM_TILT);
    sendVideo(VID_SLAM_TILT);
    return;
  }

  // P2: Attract — wheel + coin handling
  if (gameState == STATE_ATTRACT) {
    handleAttractEvent(sw, state);
    return;
  }

  if (gameState != STATE_GAME_RUNNING) return;

  // P3: Tilt — disable flippers, broadcast
  if (sw == SW_TILT && state == 1) {
    if (!tiltActive) {
      tiltActive = true;
      sendSound(SND_TILT);
      sendLight(LGT_FLIPPERS_OFF);
      broadcastNanoMode(MODE_TILT);
    }
    return;
  }

  // P4: Drain → P5: Trough → P6: Game shot
  if (sw == SW_DRAIN && state == 1) { handleDrain(); return; }
  if (currentGame) currentGame->onShot(sw);
}
// Non-blocking solenoid manager — millis() only
struct Solenoid {
  uint8_t       pin;
  bool          active;
  unsigned long endMs;
};

Solenoid solenoids[NUM_SOLENOIDS] = {
  { PIN_TROUGH,      false, 0 },  // [0]
  { PIN_KNOCKER,     false, 0 },  // [1]
  { PIN_SLING_LEFT,  false, 0 },  // [2]
  { PIN_SLING_RIGHT, false, 0 },  // [3]
  { PIN_POP_LEFT,    false, 0 },  // [4]
  // ...
};

void fireSolenoid(int idx, unsigned long ms = SOL_PULSE_MS) {
  digitalWrite(solenoids[idx].pin, HIGH);
  solenoids[idx].active = true;
  solenoids[idx].endMs  = millis() + ms;
}

void updateSolenoids() {
  unsigned long now = millis();
  for (int i = 0; i < NUM_SOLENOIDS; i++) {
    if (solenoids[i].active && now >= solenoids[i].endMs) {
      digitalWrite(solenoids[i].pin, LOW);
      solenoids[i].active = false;
    }
  }
}
// Core features

Everything a Pinball Machine Needs

🔌
Universal Nano Sketch

One nano_universal.ino handles all three node types. Configure the switch table at the top and flash — no library forks, no edits below the table.

📡
RS485 Multi-Drop Bus

Up to five Nanos share one twisted pair at 115200 baud. Noise-immune across the full playfield — no ground loops, no long vulnerable UART runs.

🎮
GameStyle Plugin System

Define multiple game modes in separate .h files with a five-function interface. The attract wheel lets players choose before ball launch.

Non-Blocking Solenoids

All coil timing uses millis() — never delay(). The Solenoid struct tracks active pulses so the loop never stalls even at peak activity.

🔊
Command Vocabulary

A shared commands.h defines every SOUND, LIGHT, VIDEO, and SCORE string. No magic numbers anywhere — one file to rule all boards.

💾
EEPROM High Scores

High scores survive power cycles. Attract mode shows rankings; initials-entry lives in the OS layer, completely separate from each GameStyle.

// GameStyle plugin system

Write a Game, Not a Framework

Each game mode is a .h file that implements five functions. The OS handles everything else — state machine, ball tracking, solenoids, EEPROM. Your game style only thinks about scoring.

GameStyle Interface — register in gameStyles[]
// Five functions — that's the entire interface
struct GameStyle {
  const char *name;
  void (*onInit)();        // game start
  void (*onBallReset)();   // each new ball
  void (*onShot)(int sw);  // playfield events
  void (*onTimerTick)();   // mode timers
  void (*onBallDrain)();   // bonus + cleanup
};

// Register all four styles
const GameStyle gameStyles[NUM_STYLES] = {
  { "Game Style One",
    g1Init, g1BallReset,
    g1Shot, g1TimerTick, g1BallDrain },
  { "Game Style Two",
    g2Init, g2BallReset,
    g2Shot, g2TimerTick, g2BallDrain },
  // ... Three, Four
};
GameStyleOne.h — shot handler excerpt
// Scoring constants — top of file
#define G1_PTS_BUMPER_NORM    100L
#define G1_PTS_BUMPER_FRENZY  500L
#define G1_PTS_SLING           50L
#define G1_PTS_LANE           500L
#define G1_PTS_TARGET         300L
#define G1_PTS_RAMP          1000L
#define G1_BUMPERS_FOR_FRENZY  75
#define G1_FRENZY_MS       30000UL

void g1Shot(int sw) {
  if (tiltActive) return;  // tilt gate

  switch (sw) {
    case SW_LANE_F:
      g1LaneHit[0] = true;
      addScore(G1_PTS_LANE);
      sendSound(SND_ROLLOVER);
      sendLight(LGT_LANE_F);
      g1CheckLanes();   // advance mult if all lit
      break;

    case SW_POP_LEFT:
    case SW_POP_RIGHT:
    case SW_POP_CENTER:
      g1OnBumper(sw);   // frenzy counter inside
      break;

    case SW_RAMP_LEFT:
      g1OnRamp(true);   // extra ball counter inside
      break;
    // ...
  }
}
// Attract mode + command vocabulary

Two Files That Run Everything Else

attract.h owns the wheel selector — flippers cycle the game list, start button launches. commands.h is the single source of truth for every string that crosses a serial port.

Secondary boards only need to parse the TYPE field and look up PAYLOAD here. No magic numbers, no hardcoded strings scattered across files.

View Switch Reference →
// attract.h — wheel selector
#define WHEEL_AUTO_MS  4000UL  // auto-cycle idle timeout

void handleAttractEvent(int sw, int state) {
  if (state != 1) return;

  // Coin in — any slot
  if (sw == SW_COIN_LEFT   ||
      sw == SW_COIN_CENTER ||
      sw == SW_COIN_RIGHT) {
    if (credits < MAX_CREDITS) {
      credits++;
      sendSound(SND_COIN_IN);
      sendLight(LGT_CREDITS);
    }
    return;
  }

  // Flippers cycle the wheel
  if (sw == SW_FLIPPER_LEFT) {
    wheelPos = (wheelPos - 1 + NUM_STYLES) % NUM_STYLES;
    sendSound(SND_ROLLOVER);
    sendWheelUpdate();    // WHEEL:N → display board
    return;
  }
  if (sw == SW_FLIPPER_RIGHT) {
    wheelPos = (wheelPos + 1) % NUM_STYLES;
    sendSound(SND_ROLLOVER);
    sendWheelUpdate();
    return;
  }

  // Start button — launch selected style
  if (sw == SW_START) {
    if (credits > 0) startGame(wheelPos);
    else              sendSound(SND_NO_CREDITS);
  }
}
// commands.h — SOUND → Serial2
// TYPE = "SOUND", secondary board maps to DFPlayer track

// System / attract
#define SND_ATTRACT          "ATTRACT"
#define SND_COIN_IN          "COIN_IN"
#define SND_GAME_START       "GAME_START"
#define SND_BALL_READY       "BALL_READY"
#define SND_NEXT_BALL        "NEXT_BALL"
#define SND_GAME_OVER        "GAME_OVER"

// Playfield hits
#define SND_BUMPER           "BUMPER"
#define SND_SLING            "SLING"
#define SND_ROLLOVER         "ROLLOVER"
#define SND_ROLLOVER_COMPLETE "ROLLOVER_COMPLETE"
#define SND_TARGET           "TARGET"
#define SND_TARGET_BANK_COMPLETE "TARGET_BANK_COMPLETE"
#define SND_RAMP             "RAMP"
#define SND_JACKPOT          "JACKPOT"

// Modes
#define SND_FRENZY_START     "FRENZY_START"
#define SND_FRENZY_END       "FRENZY_END"
#define SND_BUMPER_FRENZY    "BUMPER_FRENZY"
#define SND_EXTRA_BALL_LIT   "EXTRA_BALL_LIT"
#define SND_EXTRA_BALL       "EXTRA_BALL"

// Danger
#define SND_TILT             "TILT"
#define SND_SLAM_TILT        "SLAM_TILT"
#define SND_DRAIN            "DRAIN"

// Usage:
void sendSound(const char *n) {
  sendCommand("SOUND", n, Serial2);
}
// commands.h — LIGHT → Serial3
// TYPE = "LIGHT", maps to lamp index / LED address

// Game state
#define LGT_ATTRACT          "ATTRACT"
#define LGT_GAME_START       "GAME_START"
#define LGT_BALL_RESET       "BALL_RESET"
#define LGT_TILT             "TILT"
#define LGT_FLIPPERS_ON      "FLIPPERS_ON"
#define LGT_FLIPPERS_OFF     "FLIPPERS_OFF"

// FIRE rollover lanes
#define LGT_LANE_F           "LANE_F"
#define LGT_LANE_I           "LANE_I"
#define LGT_LANE_R           "LANE_R"
#define LGT_LANE_E           "LANE_E"
#define LGT_LANES_RESET      "LANES_RESET"
#define LGT_LANES_COMPLETE   "LANES_COMPLETE"

// Bonus multiplier
#define LGT_MULT_ADVANCE     "MULT_ADVANCE"
#define LGT_MULT_2X          "MULT_2X"
#define LGT_MULT_3X          "MULT_3X"
#define LGT_MULT_4X          "MULT_4X"
#define LGT_MULT_5X          "MULT_5X"

// Modes
#define LGT_FRENZY_ON        "FRENZY_ON"
#define LGT_FRENZY_OFF       "FRENZY_OFF"
#define LGT_EXTRA_BALL       "EXTRA_BALL"
#define LGT_PLUNGE           "PLUNGE"

// Solenoid commands → Serial3
#define SOL_CMD_RESET_TARGETS "RESET_TARGETS"
#define SOL_CMD_KICKBACK      "KICKBACK"
#define SOL_CMD_EJECT_1       "EJECT_1"
// Ready to build?

Your first ball in minutes.

The Quick Start guide walks you from blank Mega to a working ball-in-play state, step by step.