Pass the Bomb: A Dual-Core Reflex Game on the Seeed XIAO RP2040

Pass the Bomb: A Dual-Core Reflex Game on the Seeed XIAO RP2040

01 Overview

"Pass the Bomb" is a handheld party game built around the Seeed XIAO RP2040, a thumb-sized board with a dual-core Cortex-M0+ chip. A piezoelectric sensor detects when the "bomb" is tapped, tossed, or passed between players, while an OLED screen displays the game visuals, and a buzzer escalates as time runs out. Whoever is holding it or is closest to the bomb when the timer hits zero is out.

The build is also a demonstration of the RP2040's dual-core architecture: one core is dedicated to sampling the piezo sensor fast enough to catch a real impact, while the other core runs the countdown, the display, and the audio, splitting a task that a single-core board would normally struggle to do smoothly.

Project Use Case

This project is aimed at makers and students who want a hands-on introduction to multicore embedded programming. It doubles as a physical party game for classrooms, game nights, or maker fair demos, and as a reference build for anyone who needs a board to react to a fast, momentary analog event while simultaneously driving a display and audio without stutter.

02 Hardware and Software Components

Gather everything below before you start. This is your checklist — names, models, and versions only.

Hardware Components

Component Description
Seeed XIAO RP2040 Dual-core Cortex-M0+ @ 133 MHz main board; runs the game logic and reads the sensor
Piezoelectric ceramic disc transducer Detects impact/vibration when the "bomb" is tapped or passed
OLED Display module 128x64 1.3-inch Displays game visuals and reflects player inputs
Passive buzzer PWM-driven tone that escalates as the timer counts down
Resistors: 1 MΩ (4), 33 kΩ (4), 10 kΩ (4), 330 Ω (1) formed into voltage dividers to control input voltage from piezo sensors
Male-to-male jumper wires/Solid wires used for applying connections while prototyping
Breadboard allow you to quickly build temporary circuits without soldering, used for prototyping
Stranded Wires 22AWG allow you to build flexible wire connections while prototyping
IN5819 Schottky diodes (4) clamp the piezo's voltage spikes to protect the ADC input pin
Dupont Jumper Wire Connectors (optional) attached to ends of stranded wires so it can firmly latch into to the breadboard
Soldering Iron Contains a heating element that allows you to solder components together
Soldering Lead Material used to bond components together while soldering

Software Tools

Software Version / Details
Arduino IDE 1.8.19 and above | official software used to write, compile, and upload code to Arduino boards and other microcontrollers

Project Files

All required files are available in the project repository. Download the repository before proceeding to the Software Setup section.

Repository: https://github.com/Eidess321/Pass-the-Bomb-Game/tree/main

File Description
bomb_game.ino Main sketch — dual-core piezo sampling, calibration, and the full game state machine on the Seeed XIAO RP2040
README.md Project overview, setup notes, and expected hardware behavior
Safety Note: The piezo disc can generate voltage spikes well above 3.3 V when struck hard. Do not connect it directly to the RP2040's ADC pin. Use the voltage divider, bleed resistor, and diode clamp circuit described in Section 04 before powering the board.

03 Application Discussion

Here is what each component does and why it is part of this project.

Seeed XIAO RP2040 (dual-core)

The XIAO RP2040 is one of the two boards under the Seed XIAO Rp2040 series. This board runs on the Raspberry Pi RP2040 microcontroller, which features dual-core Arm Cortex-M0+ processor running at up to 133 MHz and 264 KB of SRAM. In this project, the RP2040's two Cortex-M0+ cores are split by job: Core 0 runs a tight loop that samples the piezo disc fast enough to catch the peak of an impact, while Core 1 runs the countdown state machine, drives the OLED, and drives the buzzer. Splitting the work this way means the fast sensor loop is never delayed by a slow screen redraw, and vice versa.

Piezoelectric ceramic disc

Acts as the "pass" sensor. A piezo disc generates a voltage spike when it is struck or flexed, which the RP2040 reads on an analog pin. Because a real strike lasts under a millisecond, this is the component that makes Core 0's high-speed sampling loop necessary.

If ones you purchased did not have stranded wires pre-soldered, you will have to solder these yourself. Additionally, while you can simply insert the ends of the wires directly to the breadboard, this will not be secure enough, and will be easily dislodged with little force needed.

In this project, we soldered stranded wires into the disc and attached Dupont jumper wire connectors on its end.

OLED Display (SSD1306)

The OLED display shows the game animation and player state (armed, passed, exploded) so players can get a visual experience of reflected player input while playing the game. OLEDs are highly suitable for game-related projects due to its high contrast and fast response time. Quick-movement animations can be made to make the game more intense and engaging to participants.

Passive Buzzer

In this game, the speaker is used in combination with the OLED screen for added dramatic effect, providing sound effects for the timer increasing in beep frequency as the explosion nears. A passive buzzer requires an external signal in order to produce sounds, as opposed to active buzzer. This requirement enables you to produce sounds at specific frequencies. This is done using Pulse Width Modulation, which is supported by all of the RP2040's digital I/O pins.

04 Hardware Setup

Wire the components to the board using the tables below.

Voltage Divider Circuit

Signal Connect to Description
Piezo Sensor VCC Pin 33k resistor voltage divider top/series resistor
Piezo Sensor GND Pin Ground connect directly to ground
33k resistor 10k resistor voltage divider bottom/shunt resistor
10k Resistor Ground voltage divider connection to ground
1 MΩ resistor Ground (in parallel with 10k resistor) discharge path for residual voltage after tap
Schottky diodes Ground (in parallel with 10k resistor) ensures produced voltage does not go beyond 3.3V
ADC Pin (connected to divider junction) A0-A3 (P1-P4) one analog pin per player

The circuit should look like this:

Buzzer to XIAO RP2040

Pin / Signal Connects To
Buzzer (+) D6
Buzzer (−) GND

OLED Display to XIAO RP2040

OLED Pin Board Pin Description
SDA D4 I2C data
SCL D5 I2C clock
VCC 3.3V Power
GND GND Ground

Overall, the entire circuit should look like this:

Assembly Instructions

  1. Build the piezo front-end circuit (bleed resistor, voltage divider, diode clamp) on the breadboard before connecting it to the RP2040.
  2. Wire the OLED display to the I2C pins as shown above.
  3. Wire the buzzer through its current-limiting resistor to a PWM-capable pin.
Note: Always add the diode clamp before powering the ADC pin — a hard piezo strike can spike well past 3.3 V and damage the input.

05 Software Setup

Follow these steps in order. Do not skip any step.

Project Repository: https://github.com/Eidess321/Pass-the-Bomb-Game/tree/main
Download this first before proceeding.

Step 1 — Install the arduino-pico Board Core

1. In the Arduino IDE, open Preferences and add the arduino-pico board manager URL, which is https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json.

2. Open Boards Manager, search for "Raspberry Pi Pico/RP2040" (Earle Philhower core), and install it.

3. Once installed, go back to Board selection and choose "Seeed XIAO RP2040".

Step 2 — Board Settings

Use exactly these settings in your IDE. Wrong settings will cause upload failures.

Setting Value
Board Seeed XIAO RP2040
Flash Size 2MB (No FS)
CPU Speed 200 MHz
Upload Method Default (UF2 / Picotool)

Step 3 — Install Libraries

Install the following libraries via the Library Manager:

  • Wire.h (Pre-installed)
  • U8g2lib.h (Latest version)
  • Adafruit_NeoPixel (Latest version)
Note: In some cases, you will not be able to find the U8g2lib.h library through the Library Manager. When this happens, you can download the library externally through its Github repository linked at https://github.com/olikraus/U8g2_Arduino. This should be a ZIP file. In your Downloads folder, extract the ZIP file and copy the extracted folder into your Arduino IDE's Libraries folder. This is usually located at C:\Users\rapha\Documents\Arduino\libraries.

Step 4 — Upload the Code

  1. Connect the XIAO RP2040 via USB-C. Hold BOOT while plugging in if it is not detected.
  2. Select the correct board and COM port in the Arduino IDE.
  3. Click Upload.

06 Code

Copy each file below into the correct location as described in the Software Setup section. Read the Code Breakdown section to understand what each part does.

passTheBomb_game.ino

The final firmware is shown below exactly as written for the dual-core XIAO RP2040 build. This is the complete game control program, including calibration, tap detection, pass logic, elimination flow, and OLED/NeoPixel/audio output.

Arduino / C++
#include <Wire.h>
#include <U8g2lib.h>
#include <Adafruit_NeoPixel.h>

// XIAO RP2040 hardware I2C:
// D4 = GPIO6 = SDA
// D5 = GPIO7 = SCL
U8G2_SH1106_128X64_NONAME_F_HW_I2C oled(U8G2_R0, U8X8_PIN_NONE);

const uint8_t PIEZO[4] = {26,27,28,29};   // D0-D3
const uint8_t BUZZER = 0;   // XIAO RP2040 D6 = GPIO0; do NOT use GPIO6 (OLED SDA)

#ifndef PIN_NEOPIXEL
#define PIN_NEOPIXEL 12
#endif
#ifndef PIN_NEOPIXEL_POWER
#define PIN_NEOPIXEL_POWER 11
#endif

Adafruit_NeoPixel pixel(1, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);

const uint8_t PIXEL_DIM = 25;
const uint8_t PIXEL_BRIGHT = 65;

const uint8_t TAPS_REQUIRED = 3;
const uint16_t MIN_TAP_LEVEL = 75;
const uint16_t MAX_TAP_THRESHOLD = 450;
const uint16_t BASELINE_MARGIN = 35;
const uint16_t TAP_COOLDOWN = 120;

const unsigned long MIN_BOMB = 300;
const unsigned long MAX_BOMB = 5000;
const unsigned long PASS_TIME = 420;
const unsigned long STUN_TIME = 3000;
const unsigned long EXP_FRAME = 90;
const uint8_t EXP_FRAMES = 7;
const unsigned long TITLE_TIME = 2000;
const unsigned long ELIM_TIME = 1500;
const unsigned long WIN_TIME = 3500;
const unsigned long CAL_SWITCH_DELAY = 900;

// Calibration isolation: the assigned player's channel must
// clearly dominate any other channel that fired around the same
// time (real cross-talk from a shared mounting surface).
const uint16_t CAL_CROSS_MARGIN = 50;
const uint8_t CAL_DOM_NUM = 5;       // target >= 1.25x strongest other sensor
const uint8_t CAL_DOM_DEN = 4;
const unsigned long CAL_TAP_LOCKOUT = 300;
const unsigned long CROSS_TALK_WINDOW_MS = 80; // "around the same time" window

uint32_t packEvent(uint8_t ch, uint16_t strength) {
  return ((uint32_t)ch << 24) | ((uint32_t)strength << 8);
}
void unpackEvent(uint32_t packed, uint8_t &ch, uint16_t &strength) {
  ch = (packed >> 24) & 0xFF;
  strength = (packed >> 8) & 0xFFFF;
}

const int           CORE0_DETECT_FLOOR = 55;
const unsigned long STRIKE_WINDOW_MS   = 5;
const unsigned long CORE0_REFRACTORY_MS = 60;

int           peak0[4]        = {0,0,0,0};
bool          windowOpen0[4]  = {false,false,false,false};
unsigned long windowStart0[4] = {0,0,0,0};
unsigned long refractory0[4]  = {0,0,0,0};
volatile uint16_t currentReading[4] = {0,0,0,0};

void setup() {
  for (uint8_t i=0;i<4;i++) pinMode(PIEZO[i], INPUT);
  analogReadResolution(10);
}

void loop() {
  unsigned long now = millis();

  for (uint8_t ch=0; ch<4; ch++) {
    int v = analogRead(PIEZO[ch]);
    currentReading[ch] = v;

    if (now < refractory0[ch]) continue;

    if (!windowOpen0[ch]) {
      if (v > CORE0_DETECT_FLOOR) {
        windowOpen0[ch] = true;
        windowStart0[ch] = now;
        peak0[ch] = v;
      }
    } else {
      if (v > peak0[ch]) peak0[ch] = v;
      if (now - windowStart0[ch] >= STRIKE_WINDOW_MS) {
        rp2040.fifo.push(packEvent(ch, peak0[ch]));
        refractory0[ch] = now + CORE0_REFRACTORY_MS;
        windowOpen0[ch] = false;
        peak0[ch] = 0;
      }
    }
  }
}

enum State { TITLE, CAL_BASE, CAL_TAPS, CAL_WAIT, READY, PLAYING, PASSING,
             EXPLOSION, ELIMINATED, WIN };
State state = TITLE;

bool displayOK = false;
bool alive[4] = {true,true,true,true};
bool stunned[4] = {false,false,false,false};
unsigned long stunUntil[4] = {0,0,0,0};

uint16_t baseline[4] = {0,0,0,0};
uint16_t threshold[4] = {MIN_TAP_LEVEL,MIN_TAP_LEVEL,MIN_TAP_LEVEL,MIN_TAP_LEVEL};
uint16_t tapPeaks[4][TAPS_REQUIRED];

uint8_t calPlayer = 0;
uint8_t calTap = 0;

uint8_t currentPlayer = 0;
uint8_t previousPlayer = 0;
uint8_t winnerPlayer = 0;

unsigned long stateStart = 0;
unsigned long bombStart = 0;
unsigned long bombDuration = 0;
unsigned long lastTap = 0;
unsigned long explosionStart = 0;
unsigned long nextBeep = 0;
unsigned long calSwitchAt = 0;
unsigned long pixelOff = 0;
bool pixelFlash = false;
uint32_t pixelRestore = 0;

const int PX[4] = {2, 98, 98, 2};
const int PY[4] = {12, 12, 44, 44};

uint16_t lastEventPeak[4] = {0,0,0,0};
unsigned long lastEventTime[4] = {0,0,0,0};

struct TapEvent { uint8_t ch; uint16_t strength; };
TapEvent frameEvents[8];
uint8_t frameEventCount = 0;

void showTitle();
void startCalibration();
void processCalibration();
void drawCalibration();
void showCalibrationSummary();
void drainFifo();
uint16_t baselineRead(uint8_t p);
bool evaluateCalibrationTap(uint8_t target, uint16_t targetPeak);
int detectAnyTapFromEvents(uint16_t *outPeak);
void startReady();
void startGame(uint8_t starter);
void processGame();
void enterPlayingEffects();
void startPassing(uint8_t next);
void processPassing();
void startExplosion();
void processExplosion();
void startEliminated();
void processEliminated();
void startWin();
void processWin();
void drawPlayerSlot(uint8_t p, bool holder, bool elim);
void drawPlayerIcon(int cx, int cy, bool elim, bool active);
void drawBomb(int x, int y, bool burst);
void drawGameplay();
void drawCentered(const char *s, int y);
void drawX(int cx, int cy, int s);
void drawBurst(int cx, int cy, uint8_t frame);
void ledSet(bool on);
void setPixel(uint32_t c);
void flashPixel(uint32_t c, unsigned long ms, uint32_t restore);
void updatePixel();
uint32_t playerColor(uint8_t p, uint8_t b);
void beepTap();
void beepPass();
void beepStun();
void beepExplosion();
void beepNext();
void beepWin();
bool onlyOneAlive();
uint8_t lastAlive();
uint8_t randomOtherAlive(uint8_t from);
void seedRandom();

void setup1() {
  Serial.begin(115200);
  unsigned long t0 = millis();
  while (!Serial && millis() - t0 < 2000) { /* wait briefly for USB serial */ }

  Serial.println();
  Serial.println("=== PASS THE BOMB - DUAL CORE ===");
  Serial.println("Core 0: senses all 4 piezos continuously");
  Serial.println("Core 1: display, buzzer, LEDs, game logic");
  Serial.println("OLED I2C: D4/GPIO6 SDA, D5/GPIO7 SCL");
  Serial.println("BUZZER:   D6/GPIO0");

  pinMode(BUZZER, OUTPUT);
  digitalWrite(BUZZER, LOW);

  pinMode(LED_BUILTIN, OUTPUT);
  ledSet(false);

  pinMode(PIN_NEOPIXEL_POWER, OUTPUT);
  digitalWrite(PIN_NEOPIXEL_POWER, HIGH);
  pixel.begin();
  pixel.clear();
  pixel.show();

  displayOK = oled.begin();
  Serial.println(displayOK ? "OLED init OK." : "OLED init FAILED.");
  if (displayOK) {
    oled.setPowerSave(0);
    oled.clearBuffer();
    oled.setFont(u8g2_font_6x10_tf);
    drawCentered("Starting...", 35);
    oled.sendBuffer();
  }

  seedRandom();
  delay(400);
  showTitle();
}

void loop1() {
  drainFifo();
  updatePixel();

  switch (state) {
    case TITLE:
      if (millis()-stateStart >= TITLE_TIME) startCalibration();
      break;

    case CAL_BASE:
    case CAL_TAPS:
    case CAL_WAIT:
      processCalibration();
      break;

    case READY: {
      uint16_t peak;
      int t = detectAnyTapFromEvents(&peak);
      if (t >= 0) startGame((uint8_t)t);
      break;
    }

    case PLAYING:
      processGame();
      break;

    case PASSING:
      processPassing();
      break;

    case EXPLOSION:
      processExplosion();
      break;

    case ELIMINATED:
      processEliminated();
      break;

    case WIN:
      processWin();
      break;
  }
}

void drainFifo() {
  frameEventCount = 0;
  while (rp2040.fifo.available() && frameEventCount < 8) {
    uint32_t packed = rp2040.fifo.pop();
    uint8_t ch; uint16_t strength;
    unpackEvent(packed, ch, strength);

    lastEventPeak[ch] = strength;
    lastEventTime[ch] = millis();

    frameEvents[frameEventCount].ch = ch;
    frameEvents[frameEventCount].strength = strength;
    frameEventCount++;
  }
}

void showTitle() {
  state = TITLE;
  stateStart = millis();
  ledSet(false);
  setPixel(pixel.Color(0,0,0));

  if (!displayOK) return;
  oled.clearBuffer();
  oled.setFont(u8g2_font_7x14B_tf);
  drawCentered("PASS THE BOMB", 22);
  oled.setFont(u8g2_font_6x10_tf);
  drawCentered("4 PLAYER GAME", 39);
  drawCentered("GET READY", 55);
  oled.sendBuffer();
}

void startCalibration() {
  state = CAL_BASE;
  calPlayer = 0;
  calTap = 0;
  lastTap = 0;

  for (uint8_t p=0;p<4;p++) {
    baseline[p]=0;
    threshold[p]=MIN_TAP_LEVEL;
    for (uint8_t t=0;t<TAPS_REQUIRED;t++) tapPeaks[p][t]=0;
  }

  if (displayOK) {
    oled.clearBuffer();
    oled.setFont(u8g2_font_7x14B_tf);
    drawCentered("CALIBRATION", 14);
    oled.setFont(u8g2_font_6x10_tf);
    drawCentered("Measuring sensors...", 32);
    drawCentered("Don't tap yet", 48);
    drawCentered("Please wait", 61);
    oled.sendBuffer();
  }
  delay(350);
}

uint16_t baselineRead(uint8_t p) {
  unsigned long st=millis();
  uint32_t total=0;
  uint16_t n=0;
  while (millis()-st < 220) {
    total += currentReading[p];
    n++;
    delayMicroseconds(500);
  }
  return n ? total/n : 0;
}

bool evaluateCalibrationTap(uint8_t target, uint16_t targetPeak) {
  unsigned long now = millis();
  uint16_t strongestOther = 0;
  uint8_t strongestOtherPlayer = 255;

  for (uint8_t p = 0; p < 4; p++) {
    if (p == target) continue;
    if (now - lastEventTime[p] <= CROSS_TALK_WINDOW_MS && lastEventPeak[p] > strongestOther) {
      strongestOther = lastEventPeak[p];
      strongestOtherPlayer = p;
    }
  }

  uint16_t required = baseline[target] + BASELINE_MARGIN;
  if (required < MIN_TAP_LEVEL) required = MIN_TAP_LEVEL;

  bool strongEnough = targetPeak >= required;
  bool hasMargin = targetPeak >= (uint16_t)(strongestOther + CAL_CROSS_MARGIN);
  bool hasDominance = ((uint32_t)targetPeak * CAL_DOM_DEN >=
                        (uint32_t)strongestOther * CAL_DOM_NUM);

  if (targetPeak >= MIN_TAP_LEVEL || strongestOther >= MIN_TAP_LEVEL) {
    if (strongestOtherPlayer != 255 && strongestOther > targetPeak) {
      return false;
    }
    if (strongEnough && (!hasMargin || !hasDominance)) {
      return false;
    }
  }

  return strongEnough && hasMargin && hasDominance;
}

void drawCalibration() {
  if (!displayOK) return;
  oled.clearBuffer();

  oled.setFont(u8g2_font_7x14B_tf);
  drawCentered("CALIBRATION", 11);

  drawPlayerIcon(20,25,false,false);

  oled.setFont(u8g2_font_6x10_tf);
  char name[12];
  snprintf(name,sizeof(name),"PLAYER %d",calPlayer+1);
  oled.drawStr(39,25,name);

  char count[8];
  snprintf(count,sizeof(count),"%d / %d",calTap,TAPS_REQUIRED);
  oled.drawStr(39,38,count);

  for (uint8_t i=0;i<TAPS_REQUIRED;i++) {
    int x=39+i*13;
    oled.drawFrame(x,43,9,9);
    if (i<calTap) oled.drawBox(x+2,45,5,5);
  }

  oled.setFont(u8g2_font_5x7_tf);
  char instruction[22];
  snprintf(instruction, sizeof(instruction), "P%d ONLY - TAP SENSOR", calPlayer + 1);
  drawCentered(instruction,62);
  oled.sendBuffer();
}

void showCalibrationSummary() {
  if (!displayOK) return;
  oled.clearBuffer();
  oled.setFont(u8g2_font_6x10_tf);
  drawCentered("CALIBRATION OK",10);

  for (uint8_t p=0;p<4;p++) {
    int x=16+p*32;
    drawPlayerIcon(x,25,false,false);
    oled.setFont(u8g2_font_5x7_tf);
    char s[4];
    snprintf(s,sizeof(s),"P%d",p+1);
    int w=oled.getStrWidth(s);
    oled.drawStr(x-w/2,39,s);
    oled.drawFrame(x-4,44,8,8);
    oled.drawLine(x-2,48,x,50);
    oled.drawLine(x,50,x+3,46);
  }
  oled.setFont(u8g2_font_6x10_tf);
  drawCentered("ALL SET!",63);
  oled.sendBuffer();
  delay(900);
}

void processCalibration() {
  if (state == CAL_WAIT) {
    if (millis() < calSwitchAt) return;

    if (calPlayer >= 4) {
      showCalibrationSummary();
      startReady();
      return;
    }

    state = CAL_TAPS;
    calTap = 0;
    lastTap = millis();
    drawCalibration();
    return;
  }

  if (state == CAL_BASE) {
    for (uint8_t p = 0; p < 4; p++) {
      baseline[p] = baselineRead(p);
    }

    state = CAL_TAPS;
    calPlayer = 0;
    calTap = 0;
    lastTap = millis();
    drawCalibration();
    return;
  }

  if (calPlayer >= 4) {
    showCalibrationSummary();
    startReady();
    return;
  }

  if (millis() - lastTap < CAL_TAP_LOCKOUT) return;

  const uint8_t target = calPlayer;
  bool found = false;
  uint16_t targetPeak = 0;

  for (uint8_t i = 0; i < frameEventCount; i++) {
    if (frameEvents[i].ch == target) {
      targetPeak = frameEvents[i].strength;
      found = true;
      break;
    }
  }

  if (!found) return;

  if (!evaluateCalibrationTap(target, targetPeak)) return;

  lastTap = millis();
  tapPeaks[target][calTap] = targetPeak;
  calTap++;

  flashPixel(pixel.Color(PIXEL_BRIGHT, PIXEL_BRIGHT, PIXEL_BRIGHT), 70, pixel.Color(0,0,0));

  if (calTap >= TAPS_REQUIRED) {
    uint32_t sum = 0;
    for (uint8_t i = 0; i < TAPS_REQUIRED; i++) sum += tapPeaks[target][i];
    uint16_t avg = sum / TAPS_REQUIRED;

    uint16_t th = (uint16_t)(avg * 0.30f);
    if (th < MIN_TAP_LEVEL) th = MIN_TAP_LEVEL;

    uint16_t baselineRequired = baseline[target] + BASELINE_MARGIN;
    if (th < baselineRequired) th = baselineRequired;
    if (th > MAX_TAP_THRESHOLD) th = MAX_TAP_THRESHOLD;

    threshold[target] = th;

    calPlayer++;
    calTap = 0;

    if (calPlayer < 4) {
      state = CAL_WAIT;
      calSwitchAt = millis() + CAL_SWITCH_DELAY;
    } else {
      state = CAL_WAIT;
      calSwitchAt = millis() + 250;
    }
  } else {
    drawCalibration();
  }
}

void startReady() {
  state=READY;
  stateStart=millis();
  lastTap=0;
  ledSet(false);
  setPixel(pixel.Color(0,0,PIXEL_DIM));

  if (!displayOK) return;
  oled.clearBuffer();
  oled.setFont(u8g2_font_6x10_tf);
  drawCentered("READY",9);
  for (uint8_t p=0;p<4;p++) drawPlayerSlot(p,false,false);
  drawBomb(64,34,true);
  drawCentered("TAP ANY SENSOR",62);
  oled.sendBuffer();
}

int detectAnyTapFromEvents(uint16_t *outPeak) {
  if (millis()-lastTap < TAP_COOLDOWN) return -1;

  int best=-1;
  uint16_t bestMargin=0;
  uint16_t bestPeak=0;

  for (uint8_t i=0;i<frameEventCount;i++) {
    uint8_t ch = frameEvents[i].ch;
    uint16_t v = frameEvents[i].strength;
    if (v >= threshold[ch]) {
      uint16_t margin = v - threshold[ch];
      if (best<0 || margin>bestMargin) {
        best = ch;
        bestMargin = margin;
        bestPeak = v;
      }
    }
  }

  if (best>=0) {
    lastTap=millis();
    if (outPeak) *outPeak = bestPeak;
  }
  return best;
}

void startGame(uint8_t starter) {
  for (uint8_t i=0;i<4;i++) {
    alive[i]=true;
    stunned[i]=false;
    stunUntil[i]=0;
  }

  currentPlayer=starter;
  previousPlayer=starter;
  bombDuration=random(MIN_BOMB,MAX_BOMB+1);
  bombStart=millis();
  lastTap=millis();

  state=PLAYING;
  stateStart=millis();
  nextBeep=0;

  enterPlayingEffects();
  beepTap();

  if (displayOK) {
    oled.clearBuffer();
    for (uint8_t p=0;p<4;p++) drawPlayerSlot(p,p==currentPlayer,false);
    drawBomb(64,34,true);
    oled.sendBuffer();
  }
}

void enterPlayingEffects() {
  ledSet(true);
  setPixel(playerColor(currentPlayer,PIXEL_DIM));
}

void processGame() {
  unsigned long now=millis();

  if (now-bombStart>=bombDuration) {
    startExplosion();
    return;
  }

  for (uint8_t p=0;p<4;p++) {
    if (stunned[p] && now>=stunUntil[p]) stunned[p]=false;
  }

  uint16_t tapPeak;
  int tap = detectAnyTapFromEvents(&tapPeak);

  if (tap>=0 && alive[tap]) {
    if (tap==currentPlayer) {
      uint8_t next=randomOtherAlive(currentPlayer);
      flashPixel(pixel.Color(PIXEL_BRIGHT,PIXEL_BRIGHT,PIXEL_BRIGHT),
                 70,playerColor(next,PIXEL_DIM));
      startPassing(next);
      return;
    } else {
      stunned[tap]=true;
      stunUntil[tap]=now+STUN_TIME;
      beepStun();
      flashPixel(pixel.Color(PIXEL_BRIGHT,0,0),90,playerColor(currentPlayer,PIXEL_DIM));
    }
  }

  unsigned long elapsed=now-bombStart;
  unsigned long remaining=(elapsed>=bombDuration)?0:(bombDuration-elapsed);
  if (remaining<=3000 && now>=nextBeep) {
    tone(BUZZER,1700,35);
    flashPixel(pixel.Color(PIXEL_BRIGHT,0,0),45,playerColor(currentPlayer,PIXEL_DIM));
    unsigned long interval=map(remaining,0,3000,75,260);
    nextBeep=now+interval;
  }

  drawGameplay();
}

void drawGameplay() {
  if (!displayOK) return;
  oled.clearBuffer();

  for (uint8_t p=0;p<4;p++) {
    drawPlayerSlot(p,p==currentPlayer,!alive[p]);
    if (stunned[p] && alive[p]) {
      oled.setFont(u8g2_font_5x7_tf);
      oled.drawStr(PX[p]+20,PY[p]+7,"Z");
    }
  }

  drawBomb(64,34,true);
  oled.sendBuffer();
}

void startPassing(uint8_t next) {
  previousPlayer=currentPlayer;
  currentPlayer=next;
  state=PASSING;
  stateStart=millis();
  beepPass();
}

int arcY(int y1,int y2,float t) {
  int base=y1+(int)((y2-y1)*t);
  return base-(int)(13.0f*4.0f*t*(1.0f-t));
}

void processPassing() {
  unsigned long e=millis()-stateStart;
  float t=(float)e/(float)PASS_TIME;
  if (t>1.0f) t=1.0f;

  int x1=PX[previousPlayer]+14;
  int y1=PY[previousPlayer]+9;
  int x2=PX[currentPlayer]+14;
  int y2=PY[currentPlayer]+9;

  int x=x1+(int)((x2-x1)*t);
  int y=arcY(y1,y2,t);

  if (displayOK) {
    oled.clearBuffer();
    for (uint8_t p=0;p<4;p++) drawPlayerSlot(p,false,!alive[p]);

    for (uint8_t i=0;i<7;i++) {
      float q=t*i/7.0f;
      if (q>=t) break;
      oled.drawPixel(x1+(int)((x2-x1)*q),arcY(y1,y2,q));
    }

    drawBomb(x,y,false);
    oled.sendBuffer();
  }

  if (e>=PASS_TIME) {
    bombStart=millis();
    bombDuration=random(MIN_BOMB,MAX_BOMB+1);
    lastTap=millis();
    nextBeep=0;
    state=PLAYING;
    enterPlayingEffects();
  }
}

void startExplosion() {
  state=EXPLOSION;
  explosionStart=millis();
  beepExplosion();
}

void processExplosion() {
  unsigned long e=millis()-explosionStart;
  uint8_t frame=e/EXP_FRAME;

  if (frame>=EXP_FRAMES) {
    alive[currentPlayer]=false;
    stunned[currentPlayer]=false;

    ledSet(false);
    setPixel(pixel.Color(0,0,0));

    if (onlyOneAlive()) {
      winnerPlayer=lastAlive();
      startWin();
    } else {
      startEliminated();
    }
    return;
  }

  ledSet((frame&1)==0);
  setPixel((frame&1)==0 ? pixel.Color(PIXEL_BRIGHT,0,0)
                         : pixel.Color(PIXEL_BRIGHT,PIXEL_BRIGHT,PIXEL_BRIGHT));

  if (displayOK) {
    oled.clearBuffer();
    for (uint8_t p=0;p<4;p++)
      if (p!=currentPlayer) drawPlayerSlot(p,false,!alive[p]);

    drawBurst(64,34,frame);

    if (frame>=4) {
      oled.setFont(u8g2_font_7x14B_tf);
      drawCentered("BOOM!",57);
    }
    oled.sendBuffer();
  }
}

void startEliminated() {
  state=ELIMINATED;
  stateStart=millis();

  if (!displayOK) return;
  oled.clearBuffer();

  for (uint8_t p=0;p<4;p++)
    if (p!=currentPlayer) drawPlayerSlot(p,false,!alive[p]);

  oled.drawCircle(64,24,11);
  drawX(60,21,2);
  drawX(68,21,2);
  oled.drawLine(60,29,68,29);

  oled.setFont(u8g2_font_6x10_tf);
  char s[18];
  snprintf(s,sizeof(s),"P%d OUT",currentPlayer+1);
  drawCentered(s,46);

  oled.setFont(u8g2_font_5x7_tf);
  drawCentered("PASS CONTINUES",61);
  oled.sendBuffer();
}

void processEliminated() {
  if (millis()-stateStart>=ELIM_TIME) {
    currentPlayer=randomOtherAlive(255);
    bombStart=millis();
    bombDuration=random(MIN_BOMB,MAX_BOMB+1);
    lastTap=millis();
    nextBeep=0;
    state=PLAYING;
    enterPlayingEffects();
    beepNext();
  }
}

void startWin() {
  state=WIN;
  stateStart=millis();
  ledSet(true);
  beepWin();

  if (!displayOK) return;
  oled.clearBuffer();

  for (uint8_t p=0;p<4;p++)
    if (p!=winnerPlayer) drawPlayerSlot(p,false,!alive[p]);

  drawPlayerIcon(64,23,false,true);

  oled.setFont(u8g2_font_7x14B_tf);
  char s[14];
  snprintf(s,sizeof(s),"P%d WINS!",winnerPlayer+1);
  drawCentered(s,46);

  oled.setFont(u8g2_font_5x7_tf);
  drawCentered("* * *",60);
  oled.sendBuffer();
}

void processWin() {
  static unsigned long pulse=0;
  static bool bright=false;
  if (millis()>=pulse) {
    bright=!bright;
    setPixel(playerColor(winnerPlayer,bright?PIXEL_BRIGHT:PIXEL_DIM));
    pulse=millis()+250;
  }

  if (millis()-stateStart>=WIN_TIME) {
    showTitle();
  }
}

void drawPlayerIcon(int cx,int cy,bool elim,bool active) {
  if (active) {
    oled.drawFrame(cx-11,cy-9,22,19);
  }

  if (elim) {
    oled.drawCircle(cx,cy,5);
    drawX(cx,cy,3);
    return;
  }

  oled.drawBox(cx-3,cy-7,7,6);
  oled.drawBox(cx-5,cy,11,8);
  oled.drawBox(cx-7,cy+2,2,5);
  oled.drawBox(cx+5,cy+2,2,5);
  oled.drawBox(cx-5,cy+8,4,5);
  oled.drawBox(cx+1,cy+8,4,5);

  if (!active) {
    oled.drawPixel(cx-1,cy-5);
    oled.drawPixel(cx+2,cy-5);
  }
}

void drawPlayerSlot(uint8_t p,bool holder,bool elim) {
  int cx=PX[p]+14;
  int cy=PY[p]+8;

  oled.setFont(u8g2_font_5x7_tf);
  char label[4];
  snprintf(label,sizeof(label),"P%d",p+1);

  if (p==0 || p==3) oled.drawStr(PX[p],PY[p]+6,label);
  else {
    int w=oled.getStrWidth(label);
    oled.drawStr(PX[p]+28-w,PY[p]+6,label);
  }

  drawPlayerIcon(cx,cy,elim,holder);
}

void drawBomb(int x,int y,bool burst) {
  oled.drawDisc(x,y,5);
  oled.drawLine(x+3,y-5,x+6,y-9);
  oled.drawPixel(x+7,y-10);

  if (burst) {
    oled.drawLine(x-10,y,x-7,y);
    oled.drawLine(x+7,y,x+10,y);
    oled.drawLine(x,y-10,x,y-7);
    oled.drawLine(x,y+7,x,y+10);
    oled.drawPixel(x-7,y-7);
    oled.drawPixel(x+7,y-7);
    oled.drawPixel(x-7,y+7);
    oled.drawPixel(x+7,y+7);
  }
}

void drawBurst(int cx,int cy,uint8_t frame) {
  if (frame<2) {
    int r=6+frame*7;
    oled.drawCircle(cx,cy,r);
    oled.drawCircle(cx,cy,r/2);
  } else {
    uint8_t rays=12;
    int len=8+(frame-2)*4;
    for (uint8_t i=0;i<rays;i++) {
      int dx=0,dy=0;
      switch(i) {
        case 0: dx=len; break;
        case 1: dx=len*3/4; dy=len*3/4; break;
        case 2: dy=len; break;
        case 3: dx=-len*3/4; dy=len*3/4; break;
        case 4: dx=-len; break;
        case 5: dx=-len*3/4; dy=-len*3/4; break;
        case 6: dy=-len; break;
        case 7: dx=len*3/4; dy=-len*3/4; break;
        case 8: dx=len*2/3; dy=len/3; break;
        case 9: dx=-len*2/3; dy=len/3; break;
        case 10: dx=-len*2/3; dy=-len/3; break;
        default: dx=len*2/3; dy=-len/3; break;
      }
      oled.drawLine(cx,cy,cx+dx,cy+dy);
    }
  }
}

void drawX(int cx,int cy,int s) {
  oled.drawLine(cx-s,cy-s,cx+s,cy+s);
  oled.drawLine(cx-s,cy+s,cx+s,cy-s);
}

void drawCentered(const char *s,int y) {
  int w=oled.getStrWidth(s);
  oled.drawStr((128-w)/2,y,s);
}

void ledSet(bool on) {
  digitalWrite(LED_BUILTIN,on?LOW:HIGH);
}

uint32_t playerColor(uint8_t p,uint8_t b) {
  switch(p) {
    case 0: return pixel.Color(b,0,0);
    case 1: return pixel.Color(0,b,0);
    case 2: return pixel.Color(0,0,b);
    default:return pixel.Color(b,b,0);
  }
}

void setPixel(uint32_t c) {
  pixel.setPixelColor(0,c);
  pixel.show();
}

void flashPixel(uint32_t c,unsigned long ms,uint32_t restore) {
  setPixel(c);
  pixelFlash=true;
  pixelOff=millis()+ms;
  pixelRestore=restore;
}

void updatePixel() {
  if (pixelFlash && millis()>=pixelOff) {
    setPixel(pixelRestore);
    pixelFlash=false;
  }
}

void beepTap()       { tone(BUZZER,1200,55); }
void beepPass()      { tone(BUZZER,900,70); }
void beepStun()      { tone(BUZZER,350,90); }
void beepExplosion() {
  tone(BUZZER,1800,70); delay(80);
  tone(BUZZER,1000,80); delay(90);
  tone(BUZZER,400,160);
}
void beepNext() {
  tone(BUZZER,1000,50); delay(70);
  tone(BUZZER,1300,70);
}
void beepWin() {
  tone(BUZZER,1000,80); delay(90);
  tone(BUZZER,1400,90); delay(100);
  tone(BUZZER,1800,160);
}

bool onlyOneAlive() {
  uint8_t n=0;
  for (uint8_t i=0;i<4;i++) if (alive[i]) n++;
  return n==1;
}

uint8_t lastAlive() {
  for (uint8_t i=0;i<4;i++) if (alive[i]) return i;
  return 0;
}

uint8_t randomOtherAlive(uint8_t from) {
  uint8_t list[4];
  uint8_t n=0;
  for (uint8_t i=0;i<4;i++)
    if (alive[i] && i!=from) list[n++]=i;

  if (n==0) return from;
  return list[random(0,n)];
}

void seedRandom() {
  uint32_t seed=micros();
  for (uint8_t i=0;i<4;i++) {
    seed ^= ((uint32_t)currentReading[i] << (i*8));
  }
  randomSeed(seed);
}

07 Code Breakdown

Here is what each part of the code does. The program is divided between the two cores of the XIAO RP2040: Core 0 continuously detects taps from the four piezo sensors, while Core 1 handles the OLED display, buzzer, NeoPixel, calibration, and game logic.

Libraries

Library Purpose
Wire.h Provides I2C communication used by the OLED display.
U8g2lib.h Controls the 1.3-inch SH1106 128x64 OLED display, including text, player icons, the bomb, and explosion animations.
Adafruit_NeoPixel.h Controls the XIAO RP2040's NeoPixel indicator for player colors, flashes, warnings, explosions, and the winner indication.

Key Functions

packEvent()

Combines a piezo channel number and its detected tap strength into one value so the tap event can be sent from Core 0 to Core 1 through the RP2040 hardware FIFO.

unpackEvent()

Separates the channel number and tap strength from an event received from the hardware FIFO.

setup()

Runs on Core 0. It prepares all four piezo pins as inputs and sets the analog reading resolution to 10 bits.

loop()

Runs continuously on Core 0. It reads all four piezo sensors, detects possible taps, measures their peak strength over a short strike window, and sends the resulting tap event to Core 1.

setup1()

Runs on Core 1. It initializes Serial communication, the buzzer, onboard LED, NeoPixel, and OLED. It also seeds the random number generator and displays the game title.

loop1()

Runs continuously on Core 1. It receives tap events from Core 0, updates the NeoPixel, and executes the correct part of the program according to the current game state.

drainFifo()

Retrieves available tap events from the RP2040 hardware FIFO and stores them temporarily for calibration and gameplay processing.

showTitle()

Displays the opening screen containing the game title, player count, and a “GET READY” message. It also resets the main LED and NeoPixel.

startCalibration()

Starts the sensor calibration process. It resets the calibration values for all four players and displays the calibration instructions on the OLED.

baselineRead()

Measures the normal resting signal of a selected piezo sensor for a short period. This baseline is later used to determine how strong a signal must be before it is considered a tap.

evaluateCalibrationTap()

Checks whether a detected calibration tap belongs clearly to the assigned player. It compares the target sensor's strength with signals detected by the other sensors and rejects taps affected by significant cross-talk.

drawCalibration()

Updates the calibration screen to show which player is currently being calibrated, how many taps have been completed, and which player is expected to tap.

showCalibrationSummary()

Displays a confirmation screen after all four players have completed calibration and indicates that the system is ready.

processCalibration()

Controls the complete calibration sequence. It measures baselines, accepts only valid taps from the currently assigned player, records the required number of taps, calculates each player's tap threshold, and advances through Players 1 to 4 before entering the READY state.

startReady()

Changes the system to the READY state. The OLED displays all four players and the bomb, while the system waits for any player to tap to determine who starts the game.

detectAnyTapFromEvents()

Searches the received tap events for a signal that exceeds the calibrated threshold of a sensor. If multiple valid taps are detected, it selects the event with the greatest margin above its threshold.

startGame()

Initializes a new game by marking all four players as alive, clearing their stunned status, selecting the starting player, and assigning a random bomb duration.

enterPlayingEffects()

Activates the visual indicators used during gameplay. The onboard LED is turned on and the NeoPixel displays the color associated with the current bomb holder.

processGame()

Handles the main gameplay. It checks whether the bomb has reached its random explosion time, removes expired stun states, processes player taps, allows the current player to pass the bomb, and temporarily stuns players who tap when they are not holding the bomb.

drawGameplay()

Refreshes the OLED during gameplay. It displays the four player positions, identifies the current bomb holder, shows eliminated players, and displays a stunned indicator when applicable.

startPassing()

Begins the bomb-passing animation. It records the previous holder, changes the current player to the randomly selected next player, and activates the pass sound.

arcY()

Calculates the vertical position of the bomb while it travels between two players, creating an arcing movement instead of a straight-line movement.

processPassing()

Animates the bomb from the previous player to the next player. Once the animation finishes, a new random bomb duration is selected and normal gameplay resumes.

startExplosion()

Changes the game state to EXPLOSION, records the start of the explosion animation, and plays the explosion sound sequence.

processExplosion()

Runs the explosion animation on the OLED and flashes the LED and NeoPixel. When the animation ends, the current bomb holder is marked as eliminated. If only one player remains, the program starts the winning sequence; otherwise, the game continues.

startEliminated()

Displays the elimination screen showing which player was eliminated and informs the players that the pass continues.

processEliminated()

Waits for the elimination display period to finish, selects another living player to receive the bomb, assigns a new random bomb duration, and returns the game to PLAYING.

startWin()

Starts the winning state, activates the winner's indicators, plays the winning sound sequence, and displays the winning player's number on the OLED.

processWin()

Creates a pulsing NeoPixel effect for the winner and returns the system to the title screen after the winning display period.

drawPlayerIcon()

Draws an individual player icon on the OLED. It can display a normal player, an active player holding the bomb, or an eliminated player.

drawPlayerSlot()

Draws a player's position on the OLED, including the player number and player icon. It also identifies the current bomb holder or an eliminated player.

drawBomb()

Draws the bomb symbol on the OLED. It can also add surrounding marks to make the bomb appear active.

drawBurst()

Draws the different frames of the bomb explosion animation. The animation begins with expanding circles and progresses into outward explosion rays.

drawX()

Draws an X shape used to indicate an eliminated player.

drawCentered()

Centers a text string horizontally on the 128-pixel-wide OLED display.

ledSet()

Turns the XIAO RP2040 onboard LED on or off. The code accounts for the board's active-low LED behavior.

playerColor()

Assigns a NeoPixel color to each player. Each of the four players has a different color used to identify the current bomb holder and related effects.

setPixel()

Sets the NeoPixel to a specified color and immediately updates the LED.

flashPixel()

Temporarily changes the NeoPixel to a specified color, such as white or red, and records the color that should be restored afterward.

updatePixel()

Checks whether a temporary NeoPixel flash has finished and restores the previous color when the flash duration expires.

beepTap()

Produces a short, high-pitched sound when a valid tap is recognized.

beepPass()

Produces a short sound indicating that the bomb has been passed to another player.

beepStun()

Produces a lower-pitched sound when a player taps while not holding the bomb and becomes temporarily stunned.

beepExplosion()

Produces a sequence of tones representing the bomb explosion.

beepNext()

Produces a short sound when the bomb is assigned to the next player after an elimination.

beepWin()

Produces a three-tone sequence to indicate that a player has won the game.

onlyOneAlive()

Counts the remaining living players and returns true when only one player remains.

lastAlive()

Finds and returns the player number of the last remaining living player.

randomOtherAlive()

Builds a list of living players and randomly selects one that is different from the specified player. This is used when passing the bomb or selecting the next player after an elimination.

seedRandom()

Creates a starting value for the random-number generator using the current microsecond timing and the current piezo readings so that bomb durations and player selections are not always identical.

General Program Workflow

  1. The XIAO RP2040 starts both processing cores. Core 0 is dedicated to continuously reading the four piezo sensors, while Core 1 handles the display, sound, indicators, calibration, and game logic.
  2. Core 0 continuously reads the four piezo sensors and detects short increases in signal that may represent a tap.
  3. When a tap is detected, Core 0 measures its peak strength and sends the sensor channel and peak value to Core 1 through the RP2040 hardware FIFO.
  4. Core 1 initializes the OLED, buzzer, onboard LED, and NeoPixel, then displays the game title.
  5. After the title screen, the system enters calibration and measures the normal baseline signal of all four piezo sensors.
  6. The system calibrates Player 1 first. Only the assigned Player 1 sensor is accepted as the target input, while signals from the other sensors are checked for cross-talk.
  7. Player 1 performs three accepted taps. The recorded tap strengths are used to calculate the player's detection threshold.
  8. The same process is repeated for Players 2, 3, and 4. A calibration tap is accepted only when the assigned sensor produces a sufficiently strong signal compared with the other sensors.
  9. After all four players are calibrated, the OLED displays the calibration completion screen and the system enters the READY state.
  10. In READY, any calibrated player sensor can be tapped to select the starting player.
  11. The game starts with all four players marked as alive. The selected player becomes the initial bomb holder and a random bomb duration is generated.
  12. During PLAYING, the system continuously checks the bomb's elapsed time and processes incoming tap events.
  13. If the current bomb holder taps, the bomb is randomly assigned to another living player and the passing animation is displayed.
  14. If a player who does not hold the bomb taps, that player is temporarily stunned for the configured stun duration.
  15. As the bomb approaches its randomly selected explosion time, the buzzer and NeoPixel provide increasingly frequent warning effects.
  16. If the bomb reaches its randomly selected time, the explosion animation and sound sequence begin.
  17. The player holding the bomb when it explodes is eliminated and marked as no longer alive.
  18. If more than one player remains, the system selects another living player and resumes the game with a new random bomb duration.
  19. If only one player remains, the system displays the winner and plays the winning sound sequence.
  20. After the winning sequence finishes, the program returns to the title screen and the cycle can begin again.

08 Testing and Calibration

After uploading, verify each of the following to confirm the system is working correctly.

OLED Test

After upload, the screen should boot into the title state, then transition into calibration with a message such as "Measuring sensors..." and "Don't tap yet." If the OLED remains blank or shows corrupted text, verify the I2C wiring on D4/D5, ensure the SH1106 driver is selected, and confirm the board is set to the Seeed XIAO RP2040 profile.

Piezo Sensitivity Test

During calibration, each player must tap only their own sensor. The firmware evaluates the target sensor and compares it against recent values on the other channels. A tap is accepted only if it is above the player's baseline threshold and clearly dominates nearby cross-talk. Once complete, the OLED should display a calibration summary and the board will move to the READY state.

Common Issue: False triggers usually come from vibration or cross-talk between sensors mounted on the same surface. Keep the piezo discs mechanically isolated and verify that the dominant channel is the correct one before accepting the calibration pass.

Single-Core vs Dual-Core Comparison

The key advantage of this project is the split architecture: Core 0 handles high-speed piezo sampling while Core 1 handles the display, buzzer, and game state. This avoids missing short taps or causing OLED and audio stutter while the ADC loop is running. In a single-core version, the sensor loop and display logic compete for CPU time; the dual-core version resolves that by giving each task a dedicated processor core.

09 System Demonstration

Video Demonstration

10 Conclusion

This project demonstrates a Pass-the-Bomb game that highlight's the Seeed XIAO RP2040's dual-core capabilities. It uses piezoelectric sensors for detecting player inputs which are coded into the board's first core while output devices such as buzzers and OLED displays used for audio and visual interactions with the players are coded into the board's second core. Players take turns randomly pressing the piezo sensor in order to "pass" the bomb to another player. The player holding the bomb as it explodes gets eliminated one by one until only one remains.

Possible Improvements and Future Enhancements

  • Selectable number of players, allowing users to choose between 2, 3, or 4 players before starting instead of automatically starting with all four players. The system can then activate and calibrate only the selected player inputs.
  • Score-based gameplay instead of elimination, where piezo tap strength can be used as an additional scoring factor
  • Adjustable difficulty modes that modify the bomb's time limit or introduce different timing patterns for more varied gameplay.
  • Vibration motor feedback alongside the buzzer to provide players with another form of feedback.
  • Battery monitoring displayed on the OLED when the system is adapted for rechargeable battery operation.
  • Improved piezo signal filtering and calibration to better distinguish intentional taps from electrical noise and unintended triggers.
  • Improved enclosure and sensor mounting to reduce mechanical vibrations and accidental inputs between players.
  • A more stable circuit assembly, such as a soldered prototype board or custom PCB, to replace the breadboard. During testing, the breadboard's temporary and sometimes loose connections may have contributed to occasional undesirable inputs and noise from the piezo sensors.
  • Gameplay statistics, such as reaction time, successful passes, incorrect taps, and player performance, which can be processed and displayed by the XIAO RP2040.
  • Wireless synchronization between multiple units could be explored for larger groups, although this would require an additional wireless communication solution because the standard XIAO RP2040 does not have built-in wireless connectivity.
  • Expansion beyond four players could be explored through additional external input hardware, allowing the system to support more players without requiring the XIAO to directly accommodate additional analog inputs.

11 References

  • Seeed Studio XIAO RP2040 Wiki (official documentation) - https://wiki.seeedstudio.com/XIAO-RP2040/
  • Earle Philhower arduino-pico core documentation (GitHub)- https://github.com/earlephilhower/arduino-pico
  • Arduino Buzzer Tutorial - https://controllerstech.com/arduino-passive-buzzer-tutorial/

12 Project Authors

  • Kyla Jade Entorum
  • Lemuel Jess Azaria
Pass the Bomb (XIAO RP2040 Dual-Core Game) – CreateLabz
#stem, Arduino ide, Calibration, Piezo, Pwm, Resistors, Rpi

Leave a comment

All comments are moderated before being published