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.
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
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.
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
- Time & Score arduino code (timer-score.ino) INO · 7 Kb
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.
Files for download
- HandFlipper (04FreeCAD_hand.FCStd) FCStd · 165 Kb
- HandFlipper (04FreeCAD_hand2.stl) STL· 441 Kb
- HandFlipper (04FreeCAD_hand.dxf) DXF · 476 Kb
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
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.
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.
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
- RCWL test code (05RCWL2.ino) INO · 0.5 Kb
- IR test code (04IR2.ino) INO · 0.5 Kb
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:
- Green color (associated with a goal)
- Fast blinking effect
- Duration of around 2 seconds
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 |
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:
- The score increases by one
- The value is displayed on the screen
- The LED animation is triggered
- The display briefly shows "GOAL" before returning to the score
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:
- Share GND between all components
- Integrate the LED resistor more cleanly
- Keep the wiring organized and flexible
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 |
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.
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.
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.
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.
Files for download
- IR Goal sensor + LED (03LEDgoal.ino) INO · 2 Kb
- IR Goal sensor + LED + Display (03LEDgoalDisplay.ino) INO · 3 Kb
- Double handflipper (03_2servos_2buttons.ino) INO · 1 Kb
- Double handflipper v2 (03_2servos_2buttons_v2.ino) INO · 3 Kb
