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;
}
}