Final project

Development by weeks

Week 01: baby sketches

Dates: 21/01/2026 - 27/01/2026

During this week, I created several hand sketches to explore the layout, proportions, and main elements of the game. This work helped me clarify the concept and set a clear starting point for the future development of the project.

Photo 1
Sketch 1 (size 45Kb)
Photo 2
Sketch 2 (size 46Kb)
Photo 3
Sketch 3 (size: 23 Kb)
Photo 4
Sketch 4 (size: 22 Kb)

Week 02: 3D sketch

Dates: 28/01/2026 - 3/02/2026

I started shaping my final project by defining its main idea and visual structure. I created several 2D sketches to explore the layout and key components, and from there I began translating those ideas into 3D for the first time. I modeled the overall concept and focused on designing one of the main elements of the project, a hand-shaped flipper, first in 2D and then as a functional 3D part. This process helped me understand how the project could actually be built and how its parts might move and interact, turning an initial idea into something much more concrete

3D Sketch
3D Sketch (size 55Kb)

Week 04: Embedded Programming

Dates: 11/02/2026 - 17/02/2026

PinSocc Ball match display

The main objective was to develop a functional scoreboard system for my final project PinSocc Ball, simulating the behavior of a real football match display.

I started from the example sketches provided in the QPAD-XIAO repository: the RGB LED control example and the six capacitive touch buttons example. First, I made sure we fully understood how they worked. Using the Serial Monitor, I verified which touch pad corresponded to each GPIO pin, and confirmed that the RGB LED logic was active LOW.

Once inputs and outputs were clear, I began extending the code. The first step was implementing a 3-minute countdown timer. The timer starts when touching one control pad, pauses if touched again, and can be reset using another pad — but only when it is stopped.

After the timer was stable, I added the scoreboard logic. Each time the corresponding touch control is pressed, the score increases by one goal for that team — as long as the timer is running. In my project, this simulates that the goal sensor inside the football goal has detected a goal. This prevents goals from being counted while the game is paused.

Touch controls

Button Function
0 Start / Pause the timer
1 Reset the timer (only if paused)
3 Add one goal to the Home team (only if timer is running)
4 Add one goal to the Away team (only if timer is running)

The development process was incremental. I began with simple hardware examples and gradually added logic and state control until we achieved a complete and stable scoreboard system for PinSocc Ball.

PinSocc Ball Match Display (video size: 1.8Mb).

Full time & High quality video available on my YouTube channel ↗️.

Arduino code · timer-score.ino Show code

/*
  1-Minute Timer + Scoreboard (Clean rendering, fixed)
  Temporizador 1 minuto + Marcador (render limpio, corregido)

  Board / Placa: Seeed XIAO RP2040
  Display / Pantalla: SSD1306 128x64 I2C @ 0x3C
  Libraries / Librerías: Wire, Adafruit_GFX, Adafruit_SSD1306
  Serial: 115200
*/

#include 
#include 
#include 

// ------------------------- Display / Pantalla -------------------------
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
#define OLED_ADDR     0x3C

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// ------------------------- Fast IO fallback -------------------------
#ifndef digitalReadFast
  #define digitalReadFast digitalRead
#endif
#ifndef digitalWriteFast
  #define digitalWriteFast digitalWrite
#endif
#ifndef pinModeFast
  #define pinModeFast pinMode
#endif

// ------------------------- RGB LED (active LOW) / LED RGB (activo LOW) -------------------------
const uint8_t LED_R = 17;
const uint8_t LED_G = 16;
const uint8_t LED_B = 25;

inline void ledOn(uint8_t pin)  { digitalWriteFast(pin, LOW);  } // active LOW
inline void ledOff(uint8_t pin) { digitalWriteFast(pin, HIGH); }

void ledsAllOff() {
  ledOff(LED_R);
  ledOff(LED_G);
  ledOff(LED_B);
}

// ------------------------- Touch pins / Pines táctiles -------------------------
const uint8_t TOUCH_PINS[6] = { 3, 4, 2, 27, 1, 26 };

const uint16_t THRESHOLD = 6;
const uint16_t TOUCH_MAX_COUNT = 2000;
const uint32_t TOUCH_LOCKOUT_MS = 180;
uint32_t lastTouchTriggerMs[6] = {0,0,0,0,0,0};

uint16_t readTouchCount(uint8_t pin) {
  // Discharge / Descargar
  pinModeFast(pin, OUTPUT);
  digitalWriteFast(pin, LOW);
  delayMicroseconds(5);

  // Measure rise / Medir subida
  pinModeFast(pin, INPUT_PULLUP);

  uint16_t count = 0;
  while (digitalReadFast(pin) == LOW && count < TOUCH_MAX_COUNT) {
    count++;
  }
  return count;
}

bool touchPressed(uint8_t idx) {
  uint16_t v = readTouchCount(TOUCH_PINS[idx]);
  if (v <= THRESHOLD) return false;

  uint32_t now = millis();
  if (now - lastTouchTriggerMs[idx] < TOUCH_LOCKOUT_MS) return false;

  lastTouchTriggerMs[idx] = now;
  return true;
}

// ------------------------- Timer logic / Lógica temporizador -------------------------
const uint32_t START_TIME_MS = 10UL * 1000UL; // 10 seconds / 10 segundos
// const uint32_t START_TIME_MS = 1UL * 60UL * 1000UL; // 1 minute / 1 minuto

uint32_t remainingMs = START_TIME_MS;
bool isRunning = false;
bool isGameOver = false;
uint32_t lastUpdateMs = 0;

// ------------------------- Scoreboard / Marcador -------------------------
uint16_t goalsHome = 0; // Local
uint16_t goalsAway = 0; // Visitante

// Button mapping (indices in TOUCH_PINS)
// Button 3 -> index 3 (pin 27)  -> Home
// Button 4 -> index 4 (pin 1)   -> Away
const uint8_t BTN_HOME_SCORE = 3;
const uint8_t BTN_AWAY_SCORE = 4;

// ------------------------- Game Over strobe / Estrobo -------------------------
uint32_t strobeLastMs = 0;
uint8_t strobeStep = 0;
const uint32_t STROBE_STEP_MS = 90;

// ------------------------- Render -------------------------
void renderMainScreen() {
  display.clearDisplay();

  // IMPORTANT: force white text every frame / Forzar texto blanco cada frame
  display.setTextColor(SSD1306_WHITE);
  display.setTextWrap(false);

  // Title
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print("Timer + Score");

  // Timer (fixed position)
  uint32_t totalSeconds = remainingMs / 1000UL;
  uint32_t minutes = totalSeconds / 60UL;
  uint32_t seconds = totalSeconds % 60UL;

  char timeBuf[8];
  snprintf(timeBuf, sizeof(timeBuf), "%lu:%02lu",
           (unsigned long)minutes, (unsigned long)seconds);

  display.setTextSize(2);
  display.setCursor(28, 16);   // fixed
  display.print(timeBuf);

  // Score (fixed position)
  char scoreBuf[12];
  snprintf(scoreBuf, sizeof(scoreBuf), "%u-%u", goalsHome, goalsAway);

  display.setTextSize(2);
  display.setCursor(36, 36);   // fixed
  display.print(scoreBuf);

  // Footer
  display.setTextSize(1);
  display.setCursor(0, 56);
  if (isRunning) display.print("B0:Pause B3:+L B4:+V");
  else          display.print("B0:Start  B1:Reset");

  display.display();
}

void renderGameOverScreen() {
  display.clearDisplay();

  display.setTextColor(SSD1306_WHITE);
  display.setTextWrap(false);

  display.setTextSize(2);
  display.setCursor(10, 8);
  display.print("GAME OVER");

  char scoreBuf[12];
  snprintf(scoreBuf, sizeof(scoreBuf), "%u-%u", goalsHome, goalsAway);

  display.setTextSize(2);
  display.setCursor(36, 30);
  display.print(scoreBuf);

  display.setTextSize(1);
  display.setCursor(12, 56);
  display.print("Press BTN1 to reset");

  display.display();
}

// ------------------------- Setup -------------------------
void setup() {
  Serial.begin(115200);
  delay(80);

  // LEDs
  pinModeFast(LED_R, OUTPUT);
  pinModeFast(LED_G, OUTPUT);
  pinModeFast(LED_B, OUTPUT);
  ledsAllOff();

  // Touch pins
  for (uint8_t i = 0; i < 6; i++) {
    pinModeFast(TOUCH_PINS[i], INPUT_PULLUP);
  }

  // OLED
  Wire.begin();
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    Serial.println("SSD1306 init failed (addr 0x3C?) / Fallo init SSD1306");
    while (1) { delay(10); }
  }

  // Optional: set contrast / opcional
  display.ssd1306_command(SSD1306_SETCONTRAST);
  display.ssd1306_command(0xFF);

  remainingMs = START_TIME_MS;
  isRunning = false;
  isGameOver = false;
  goalsHome = 0;
  goalsAway = 0;
  lastUpdateMs = millis();

  Serial.println("Ready / Listo");
  renderMainScreen();
}

// ------------------------- Loop -------------------------
void loop() {
  uint32_t now = millis();

  bool btn0 = touchPressed(0);
  bool btn1 = touchPressed(1);
  bool btnHome = touchPressed(BTN_HOME_SCORE);
  bool btnAway = touchPressed(BTN_AWAY_SCORE);

  // GAME OVER state
  if (isGameOver) {
    if (btn1) {
      isGameOver = false;
      isRunning = false;
      remainingMs = START_TIME_MS;
      goalsHome = 0;
      goalsAway = 0;
      ledsAllOff();
      renderMainScreen();
    } else {
      if (now - strobeLastMs >= STROBE_STEP_MS) {
        strobeLastMs = now;
        ledsAllOff();
        if (strobeStep == 0) ledOn(LED_R);
        if (strobeStep == 1) ledOn(LED_G);
        if (strobeStep == 2) ledOn(LED_B);
        strobeStep = (strobeStep + 1) % 3;
      }
    }
    return;
  }

  // BTN0 toggle
  if (btn0) {
    isRunning = !isRunning;
    lastUpdateMs = now;
    renderMainScreen();
  }

  // BTN1 reset only if paused
  if (btn1 && !isRunning) {
    remainingMs = START_TIME_MS;
    goalsHome = 0;
    goalsAway = 0;
    renderMainScreen();
  }

  // Scoring only while running
  if (isRunning) {
    if (btnHome) { goalsHome++; renderMainScreen(); }
    if (btnAway) { goalsAway++; renderMainScreen(); }
  }

  // Timer update
  if (isRunning) {
    uint32_t dt = now - lastUpdateMs;
    lastUpdateMs = now;
    if (dt > 1000UL) dt = 1000UL;

    if (remainingMs > dt) remainingMs -= dt;
    else remainingMs = 0;

    static uint32_t lastRefresh = 0;
    if (now - lastRefresh >= 150) {
      lastRefresh = now;
      renderMainScreen();
    }

    if (remainingMs == 0) {
      isRunning = false;
      isGameOver = true;
      strobeLastMs = now;
      strobeStep = 0;
      ledsAllOff();
      renderGameOverScreen();
    }
  } else {
    lastUpdateMs = now;
  }
}
				

Files for download

Week 05: 3D Scanning and Printing

Dates: 18/02/2026 - 24/02/2026

Taking advantage of 3D printing, this week I decided to print a part for my final project. In this case, it's the handflipper that will push the ball when activated by a button.

Since this part, besides moving quickly, will have direct impact with the ball, 3D printing has been very useful for starting to consider the dimensions, hardness, and elasticity needed for proper functioning.

The design was the one I created in week 2. After printing it, I realized that it might need more thickness and rigidity, but until I know what material the ball will be made of, no further testing will be necessary.

HandFlipper
HandFlipper

Files for download

Week 06

Dates: 25/02/2026 - 3/03/2026

Although it has nothing to do with this week's theme (electronic design), this week I made a first cardboard model in order to start analyzing possible measurements, problems and solutions when carrying out my final project.

One of the things I did was divide it into two parts: the top part will be the playing area, and the bottom part will house the ball routing system so it can return to the starting position. There's also plenty of space to accommodate all the electronics.

Size: 70x40 cm

Cardboard first model (video size: 1,3 Mb).

High quality video available on my YouTube channel ↗️.

Week 07

Dates: 4/03/2026 - 19/03/2026

This week's Computer-Controlled Machining class, specifically the use of a CNC machine, is perfect for designing and milling wooden pieces, like the cardboard model I made last week. Although I haven't had time to actually do it, it's given me time to think about how to make both the top and bottom parts, as well as the playing surface.

I hope to be able to do it soon, although I know it won't be finalized until I know what input and output devices I'll need. It's essential to consider these when assembling everything, ensuring each component is in its correct place and properly secured.

Week 08

Dates: 11/03/2026 - 17/03/2026

This week, Electronics Production is going the same way as last week. I can't make any progress in manufacturing the PCBs I need because I still don't know which input and output devices I'll require. It has, however, been useful for manufacturing a basic PCB to run tests for the final project in the coming weeks.

Week 09

Dates: 18/03/2026 - 24/03/2026

RCWL-0516 microwave sensor

Taking advantage of the input devices tested during Week 09, specifically the RCWL-0516 microwave sensor and the IR road tracking sensor, I decided to run some experiments related to my final project. The goal was to simulate a goalpost and test whether the sensor could detect when a ball enters the goal.

To do this, I built a simple setup using a small cardboard box to represent the goal. I placed the sensor inside the box and started testing how it reacted when introducing a ball.

I began by testing the RCWL-0516 microwave sensor. I made a small modification to the Arduino code so that instead of printing “1” in the Serial Monitor when motion is detected, it displays the message “GOAL!”.

After running the code, I noticed that even with the sensor placed inside the box, it was still sensitive to movements outside. For example, every time I moved my hand to place the ball into the goal, the sensor detected that motion. Once I stayed still and confirmed that the sensor output was zero, I dropped the ball into the goal and the sensor correctly triggered the “GOAL!” message. However, when I tried to pick up the ball to repeat the test, the sensor detected movement again.

Although the sensor is able to detect the ball successfully, it turns out to be too sensitive for this application. In my “PinSocc Ball” project, the sensor would be placed inside the goal, below the playing field. Even if it is not directly visible or exposed, it would still detect any nearby movement, since that is exactly what it is designed to do.

GOAL test RCWL (video size: 1,5 Mb).

High quality video available on my YouTube channel ↗️.

Arduino code · 05RCWL2.ino Show code


const int sensorPin = D1;

void setup() {
  Serial.begin(115200);
  pinMode(sensorPin, INPUT);
  delay(1000);
  Serial.println("RCWL test start");
}

void loop() {
  int state = digitalRead(sensorPin);

  if (state == HIGH) {
    Serial.println("GOAL!");
  } else {
    Serial.println("0");
  }

  delay(500);
}
				

For this reason, I concluded that this sensor is not suitable for use in my Final Project.

IR road tracking sensor

Next, I moved on to testing the IR road tracking sensor, repeating the same setup as in the previous experiment.

I modified the Arduino code I had used before. In this case, I only read the digital output of the sensor so that it displays the message “GOAL!! ⚽” whenever a detection is triggered.

After several tests, I realized that due to the speed at which the ball falls, I needed to significantly reduce the delay value. I finally set it to 50 ms, which provided a reliable response.

GOAL test IR road tracking sensor (video size: 1,8 Mb).

High quality video available on my YouTube channel ↗️.

Arduino code · 04IR2.ino Show code


// Pin definition
const int digitalPin = D1;  // Sensor D0 -> XIAO D1 (P27)

int lastState = 1; // Initial state (no detection)

void setup() {
  Serial.begin(115200);
  pinMode(digitalPin, INPUT);

  Serial.println("IR Goal Detection System");
}

void loop() {
  int currentState = digitalRead(digitalPin);

  // Detect transition (object passing)
  if (lastState == 1 && currentState == 0) {
    Serial.println("GOAL!! ⚽");
  } else {
    // No detection
    Serial.println("0");
  }

  // Update previous state
  lastState = currentState;

  delay(50);
}
				

Based on these tests, I confirmed that an IR-based sensor is the right approach to detect goals in each goalpost. In this case, the sensor will be placed under the playing field, at the point where the ball drops into the rails that return it back into play.

This specific module may not be the optimal final solution, but it clearly shows that I need an IR barrier-type sensor for my Final Project.

Files for download

Week 10

Dates: 25/03/2026 - 31/03/2026

Goal detection + output devices integration

After validating in Week 09 that the IR sensor was a reliable solution to detect when the ball enters the goal, the next step during Week 10 was to start working with output devices and transform that detection into a visible and meaningful response.

Taking advantage of the tests I carried out this week with different output devices, especially the WS2812B RGB LED strip and the TM1637 4-digit display, I began integrating them into my final project.

Version 1 – Sensor + LED strip

In the first iteration, the goal was simple: create an immediate visual feedback when a goal is detected.

Using the same IR sensor logic developed in Week 09, I connected a WS2812B RGB LED strip and programmed a basic animation:

To detect the goal, I used a state change (edge detection), which prevents multiple triggers while the ball remains inside the goal area.

At this point, I also started replacing delay() with millis(), allowing me to control timing without blocking the system. This is important because the final project will need to manage multiple events at the same time.

With this setup, the system already behaves as a reactive goal indicator, providing immediate feedback to the player.

Pin Connection

Component Pin Connected to XIAO RP2040 Function
IR Sensor OUT D2 Goal detection
IR Sensor VCC 3.3V Power
IR Sensor GND GND Ground
WS2812B LED strip DIN D1 LED control
LED strip 5V 5V Power
LED strip GND GND Ground
GOAL detect and LED flash (video size: 1,6 Mb).

High quality video available on my YouTube channel ↗️.

Arduino code · 03LEDgoal.ino Show code

Version 2 – Sensor + LED strip + display

Once the visual feedback was working properly, I moved to the next step: adding information to the system.

Using the TM1637 display tested during Week 10, I implemented a simple scoring system. Now, every time a goal is detected:

I also added a reset button to bring the score back to zero, introducing a basic level of user interaction.

With this update, the system evolves from a simple reactive output to a state-based interactive system, capable of remembering and displaying what has happened during the game.

During this integration, I encountered a practical limitation: my PCB only provides two GND pins, which is not enough for the sensor, LED strip, and display at the same time.

To solve this, I used a breadboard as a common ground distribution point, which allowed me to:

This kind of workaround reflects a real prototyping situation, where the initial PCB design still needs adjustments before reaching the final version.

Pin Connection

Component Pin Connected to XIAO RP2040 Function
IR Sensor OUT D2 Goal detection
IR Sensor VCC 3.3V Power
IR Sensor GND GND Ground
WS2812B LED strip DIN D1 LED control
WS2812B LED strip 5V 5V Power
WS2812B LED strip GND GND Ground
TM1637 Display CLK D0 Clock signal
TM1637 Display DIO D7 Data signal
TM1637 Display VCC 5V Power
TM1637 Display GND GND Ground
Reset button Pin D4 Reset score
GOAL detect and LED flash with display (video size: 2 Mb).

High quality video available on my YouTube channel ↗️.

Arduino code · 03LEDgoalDisplay.ino Show code

With these two iterations, I moved from simply detecting a goal to building a complete interactive system that reacts, displays information, and keeps track of the game state — a key step towards the final integration of my project.

Hand-Flippers. Dual servo control

After the initial tests carried out during Week 10, where I explored the behavior of the SG90 servo both in automatic mode and controlled by a button, the next step in my final project development was to move towards a more realistic interaction.

In my project PinSocc Ball, the flippers are one of the most important elements, as they are responsible for directly interacting with the ball. For this reason, I needed to evolve from a simple single-servo test to a system capable of controlling two independent servos, simulating the behavior of the left and right flippers of a real pinball machine.

To achieve this, I designed a simple system based on two push buttons, where each one controls its own servo. Every time a button is pressed, the servo performs a fast movement to the hit position and then returns automatically to its resting position, recreating the typical flipper action.

To ensure reliable interaction, I implemented a edge detection logic (HIGH → LOW) instead of continuously reading the button state. This allowed me to detect each press more precisely and avoid repeated triggers while the button remains pressed, significantly improving the stability of the system.

During testing, one of the most interesting findings was that the servo did not always behave as expected when using intermediate values such as 90°. After several experiments, I realized that the actual physical range of the servo in my setup did not perfectly match the theoretical values, so I decided to work with the full range (0–180) to achieve a wider and more suitable movement for the flipper effect.

Another key aspect was the need to invert the movement of one of the servos. In a pinball system, the flippers move in opposite directions, so I configured the second servo in an inverted way by swapping its rest and hit positions. This results in a symmetrical and much more realistic behavior within the game.

Pin Connection

Component / Pin Connected to XIAO RP2040 Function
Servo 1 (Signal) D2 (GPIO28) Controls left flipper movement
Servo 1 VCC 5V Power supply
Servo 1 GND GND Ground reference
Servo 2 (Signal) D1 (GPIO27) Controls right flipper movement (inverted)
Servo 2 VCC 5V Power supply
Servo 2 GND GND Ground reference
Button 1 D4 (GPIO6) Trigger Servo 1 (left flipper)
Button 2 D6 (GPIO0) Trigger Servo 2 (right flipper)

With this iteration, the system evolves from a basic servo control test into a more complete interactive system, where the user can independently trigger each flipper. This represents an important step towards the full integration of the mechanical and electronic components in the final project.

Double servos HandFlipper v1 (video size: 1,6 Mb).

High quality video available on my YouTube channel ↗️.

Arduino code · 03_2servos_2buttons.ino Show code

After validating the basic behavior of both flippers, I identified an important limitation in the initial implementation: the use of delay() was blocking the system. This meant that when both buttons were pressed at the same time, one of the servos responded later than the other, breaking the feeling of a real pinball interaction.

Both buttons pressed at the same time v1 (video size: 0,6 Mb).

High quality video available on my YouTube channel ↗️.

To solve this, I replaced the blocking logic with a non-blocking approach using millis(). Instead of stopping the program while the servo was moving, each flipper now manages its own timing independently. This allows both servos to react simultaneously and smoothly, even when the two buttons are pressed at the same time.

With this improvement, the system becomes much more responsive and closer to a real interactive device, laying a stronger foundation for future features where multiple inputs and outputs need to work in parallel.

Both buttons pressed at the same time v2 (video size: 0,8 Mb).

High quality video available on my YouTube channel ↗️.

Arduino code · 03_2servos_2buttons_v2.ino Show code

These servos might not be the best for my final project, but they've been useful for testing and giving me a clear starting point for what I might need. I'll probably need faster servos.