embedded programming

Ah yes, the trials and tribulations of embedded programming. How fared octoabby this week...?

Hardware

Well! Starting on a fun and awesome note, I loveeee soldering. So much fun. So satisfying.



I came to CBA to solder on friday, and obtained a QPAD-xiao board, a xiao RP2040, 2 resistors, and an OLED screen with pins.
Everything went very well! It was my first time soldering and I had a very lovely time.



But my screen was feeling a little mischevious.......



NOOOOOOO
It was plotting too many evil deeds and got decapitated. :(

Yes, unfortunately due to the precariousness of the screen being held by only one side, it snapped off, ripping the copper with it. After inspection, I realized I could not just simply re-solder it back in place.

But all hope was not lost!



While I attempted to fix it (to no avail) in the CBA shop, a friendly Dan and Olek appeared! They offered to help me DIY it back together so that I could work on my programming before I'd go back and make a new board.



So tada, it was fixed! Olek helped me rewire the pieces that had been severed back to their origins using silicon coated wire.

This worked for a bit, but then something happened to my microcontroller and it just stopped connecting to my computer... It started showing me "no drive to deploy". Allen ended up helping me
figure out that I needed to hold the bootloader button down while I plugged it in, and it started working again later!
But I did also return to CBA on Monday to craft a fresh one.



And this time, Quentin made us stabilizer pieces so that the screen would not break off again. Yippee!!


Programming a Game

So, for my little device, I first had the idea of making a very simple game because I have 0 programming experience.
The idea was to have one button be the winner button, and the rest would say "nope!". I began attempting this,
but then realized it would be very boring and predictable.

And we simply must not be boring!
So to be slightly less boring, I wanted to randomize where the winner button would be.

Here is a video of the game!

And here is my glorious Arduino code! I did use ChatGPT for much of this, but I have been learning a lot through doing!
I asked it to help me in small stages so I'd have a sense of what was going on.
I also have a general explanation of everything here (mostly for myself):




        
#include  < Wire.h >
#include < Adafruit_GFX.h >
#include < Adafruit_SSD1306.h >
#include < avr/pgmspace.h >

// ---------------- OLED ----------------
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1, 1000000UL, 1000000UL);

// ---------------- RGB LED ----------------
#define PIN_RED   17
#define PIN_GREEN 16
#define PIN_BLUE  25

// ---------------- Touch config ----------------
#define N_TOUCH 6
#define THRESHOLD 30  // base threshold; we add hysteresis around this
int  touch_pins[N_TOUCH]       = {3, 4, 2, 27, 1, 26};
int  touch_values[N_TOUCH]     = {0, 0, 0, 0, 0, 0};  // smoothed ints (for Serial debug)
bool pin_touched_now[N_TOUCH]  = {false, false, false, false, false, false};
bool pin_touched_past[N_TOUCH] = {false, false, false, false, false, false};

// --------- Stability  ---------
// Hysteresis (two thresholds)
const int THRESH_ON  = THRESHOLD + 6;  // must exceed this to turn ON
const int THRESH_OFF = THRESHOLD - 6;  // must go below this to turn OFF
// Debounce (consecutive loops to confirm change)
const int ON_COUNT_REQUIRED  = 3;
const int OFF_COUNT_REQUIRED = 3;
// Smoothing (exponential moving average)
const float FILTER_ALPHA = 0.40f;      // 0..1; higher = snappier, lower = smoother

int   touch_raw[N_TOUCH]       = {0,0,0,0,0,0};
float touch_filtered[N_TOUCH]  = {0,0,0,0,0,0};
int   on_count[N_TOUCH]        = {0,0,0,0,0,0};
int   off_count[N_TOUCH]       = {0,0,0,0,0,0};

// ---------------- UI Mode (optional, prevents redraw spam) ----------------
enum UiMode { UI_IDLE, UI_SHOW_PAD };
UiMode uiMode = UI_IDLE;
int    uiPadIndex = -1;

// ---------------- Game state ----------------
enum GameState { GAME_WAITING, GAME_SHOW_RESULT };
GameState game = GAME_WAITING;

// --- You Win ---
bool lastResultWasWin = false;          // remember if result was a win
unsigned long resultShownAt = 0;        // when we drew the result screen
const unsigned long WIN_DISPLAY_MS = 4000;  // how long to hold "You win!"

// --- hearts ---
#define HEART_W 8
#define HEART_H 7
const unsigned char PROGMEM HEART_BMP[] = {
  0b01100110, //
  0b11111111, //
  0b11111111, //
  0b11111111, //
  0b01111110, //
  0b00111100, //
  0b00011000  //
};


// heart positions
const int HEART_COUNT = 4;
int heartX[HEART_COUNT] = {20, 92, 40, 70};
int heartY[HEART_COUNT] = {10, 10, 42, 42};


// Randomized "Nope" messages
const char* NOPE_MESSAGES[] = {
  "Nope!",
  "Try again.",
  "Not quite...",
  "No...",
  "Not me :P",
  "Mwahaha NO."
};
const int N_NOPE_MSG = sizeof(NOPE_MESSAGES)/sizeof(NOPE_MESSAGES[0]);

// ---------------- Helpers ----------------
void centerText(const char* msg, int y) {
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds((char*)msg, 0, y, &x1, &y1, &w, &h);
  int x = (SCREEN_WIDTH - (int)w) / 2;
  display.setCursor(x, y);
  display.print(msg);
}

void print_touch_serial() {
  for (int i = 0; i < N_TOUCH; i++) {
    char buf[8];
    snprintf(buf, sizeof(buf), "%4d", touch_values[i]);
    Serial.print(buf);
    Serial.print(i == N_TOUCH - 1 ? '\n' : ' ');
  }
}

void pickNewTarget() {
  int old = targetPad;
  if (N_TOUCH <= 1) { targetPad = 0; return; }
  do {
    targetPad = random(0, N_TOUCH);
  } while (targetPad == old);  // avoid same pad twice in a row
}

void drawBitmapCentered(const unsigned char* bmp, int bw, int bh, int yTop) {
  int x = (SCREEN_WIDTH - bw) / 2;
  display.drawBitmap(x, yTop, bmp, bw, bh, SSD1306_WHITE);
}




// Stability-upgraded touch read: discharge, measure rise time, smooth, hysteresis+debounce
void update_touch() {
  const int t_max = 200;

  for (int i = 0; i < N_TOUCH; i++) {
    int p = touch_pins[i];

    // 1) Discharge pad
    pinMode(p, OUTPUT);
    digitalWrite(p, LOW);
    delayMicroseconds(40);  // slightly longer discharge for stability

    // 2) Time the rise via internal pull-up (capacitance slows the rise)
    noInterrupts();
    pinMode(p, INPUT_PULLUP);
    int t = 0;
    while (!digitalRead(p) && t < t_max) {
      t++;
    }
    interrupts();

    // 3) Smoothing
    touch_raw[i] = t;
    touch_filtered[i] = (FILTER_ALPHA * touch_raw[i]) +
                        ((1.0f - FILTER_ALPHA) * touch_filtered[i]);

    // 4) Hysteresis + debounce to produce stable ON/OFF
    bool was = pin_touched_now[i];
    bool now = was;

    if (!was) {  // currently OFF -> look for ON
      if (touch_filtered[i] > THRESH_ON) {
        on_count[i]++;
        off_count[i] = 0;
        if (on_count[i] >= ON_COUNT_REQUIRED) {
          now = true;
          on_count[i] = 0;
        }
      } else {
        on_count[i] = 0;
        off_count[i] = 0;
      }
    } else {     // currently ON -> look for OFF
      if (touch_filtered[i] < THRESH_OFF) {
        off_count[i]++;
        on_count[i] = 0;
        if (off_count[i] >= OFF_COUNT_REQUIRED) {
          now = false;
          off_count[i] = 0;
        }
      } else {
        on_count[i] = 0;
        off_count[i] = 0;
      }
    }

    pin_touched_past[i] = was;
    pin_touched_now[i]  = now;

    // For Serial debug: store smoothed integer
    touch_values[i] = (int)(touch_filtered[i] + 0.5f);
  }
}

// Any pad currently pressed?
bool anyPadPressed() {
  for (int i = 0; i < N_TOUCH; i++) if (pin_touched_now[i]) return true;
  return false;
}

// Return first pad that was JUST pressed this frame, or -1
int firstJustPressed() {
  for (int i = 0; i < N_TOUCH; i++) {
    if (pin_touched_now[i] && !pin_touched_past[i]) return i;
  }
  return -1;
}

// ---------------- OLED screens ----------------
void showIdlePrompt() {
  display.clearDisplay();
  display.setTextSize(1);
  // Leave vertical room for future icons (top and bottom)
  centerText("Touch the", 18);
  centerText("correct pad!", 30);
  display.display();
}

void showWinScreen() {
  display.clearDisplay();

  // Centered text
  display.setTextSize(1);
  centerText("You win!", 28);

  // Animate hearts: bounce gently up and down
  unsigned long t = millis() / 200; // controls speed
  for (int i = 0; i < HEART_COUNT; i++) {
    int wobble = ((t + i) % 2 == 0) ? 0 : 1;  // bob between y and y+1
    display.drawBitmap(heartX[i], heartY[i] + wobble,
                       HEART_BMP, HEART_W, HEART_H, SSD1306_WHITE);
  }

  display.display();
}




void showNopeScreen(const char* msg) {
  display.clearDisplay();
  display.setTextSize(1);
  centerText(msg, 20);

  display.display();
}

// ---------------- SETUP ----------------
void setup() {
  Serial.begin(115200);

  // Optional LED pins
  pinMode(PIN_RED, OUTPUT);
  pinMode(PIN_GREEN, OUTPUT);
  pinMode(PIN_BLUE, OUTPUT);
  digitalWrite(PIN_RED, HIGH);
  digitalWrite(PIN_GREEN, HIGH);
  digitalWrite(PIN_BLUE, HIGH);

  // OLED
  display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.display();

  // Touch pins safe state when idle
  for (int i = 0; i < N_TOUCH; i++) {
    pinMode(touch_pins[i], INPUT_PULLUP);

randomSeed(analogRead(A0) ^ micros());

  }

  // Start state
  uiMode = UI_IDLE;
  uiPadIndex = -1;
  game = GAME_WAITING;
  targetPad = 0;  // Phase 3: fixed target is pad 0
  showIdlePrompt();
}



// ---------------- LOOP ----------------
void loop() {
  update_touch();
  print_touch_serial();  // debug values

  int pressed = firstJustPressed();  // edge: just pressed this frame

  if (game == GAME_WAITING) {
    if (pressed != -1) {
      uiPadIndex = pressed;  // for "Not pad X" hint on miss

      if (pressed == targetPad) {
        // Correct
        showWinScreen();              // draw once immediately
        digitalWrite(PIN_GREEN, LOW); // green on
        digitalWrite(PIN_RED, HIGH);

        lastResultWasWin = true;
        resultShownAt = millis();
        game = GAME_SHOW_RESULT;

      } else {
        // Wrong
        int idx = random(N_NOPE_MSG);
        showNopeScreen(NOPE_MESSAGES[idx]);
        digitalWrite(PIN_GREEN, HIGH);
        digitalWrite(PIN_RED, LOW);   // red on

        lastResultWasWin = false;
        game = GAME_SHOW_RESULT;
      }
    }

  } else if (game == GAME_SHOW_RESULT) {
    if (lastResultWasWin) {
      // Re-render animated win screen every frame (for twinkle/hearts)
      showWinScreen();

      if (millis() - resultShownAt >= WIN_DISPLAY_MS) {
        pickNewTarget();              // new correct pad next round
        digitalWrite(PIN_GREEN, HIGH);
        digitalWrite(PIN_RED, HIGH);
        showIdlePrompt();
        game = GAME_WAITING;
      }
    } else {
      // On loss, wait for release, then return
      if (!anyPadPressed()) {
        digitalWrite(PIN_GREEN, HIGH);
        digitalWrite(PIN_RED, HIGH);
        showIdlePrompt();
        game = GAME_WAITING;
      }
    }
  }

  delay(50);  // keep this INSIDE loop(), at the very end
}

Explanation

My sketch starts with libraries that I took from Quentin's examples.
Then the OLED, RBG, and touch config parts have the OLED screen settings, LED pins, and touch pads. 6 pins are in an array so the code can loop and check for touches.





The touch pads are read by charging/discharging the pad and timing how long it takes to change the state, so to make it more reliable, I added filtering (smooths noisy readings), hysteresis (requires it to go clearly above/below thresholds to flip state), and debouncing (needs several consistent readings before it confirms a press/release). I added these because it was super flicker-y at first.


The game then is structured around two states: Waiting ("choose the correct pad!"") and showing result ("you win!" or "nope"). It uses timers (millis()) to hold the win screen before resetting and choosing a new winner button.


I also wanted to add some little hearts for fun so here they are.








I also did not want every button to say the same "nope" message every time, so this is a list of my randomized "nope" messages.



Here are the display helpers that handle drawing: centerText() prints the text centered,



print_touch_serial shows values in the serial monitor,



pickNewTarget() picks the next random winner button,



drawBitmapCentered is for the heart placement,





This is for touch input and stability,





































showIdlePrompt() is the start screen,



showWinScreen() shows "You win!" with the little hearts,



This is to animate the hearts.






showNopeScreen() prints random "nope" message.



Setup runs once when the board is powered on. It prepares the hardware and sets the starting state.>
The LEDs are configured as outputs and the OLED screen is set up.





The touch pins are set to default and the random number generator is seeded so the winner pad changes between runs.


The game is then in waiting mode and shows the prompt "Touch the correct pad!"

The loop runs forever after setup() and this is where the game happens.





If waiting for a press: GAME_WAITING. If you press the correct one, "You win!"" screen shows and LED turns green - setting state to show result. If you press the wrong pad, a random "Nope!" message appears, LED turns red, setting state to show result.






If showing result: GAME_SHOW_RESULT. If it was a win, keep redrawing the win screen so the hearts animate. After 4s, pick a new random correct pad, reset LED, and return to idle prompt. If it was a loss, keep showing nope screen until pad is released. Once released, reset LED and show idle prompt.


Programming 8-Ball

Following a similar logic of the randomized phrases in my previous game, I thought it would be fun to make an 8 Ball of sorts.



I also got the old board to work again!

Using a list of 8-Ball responses:
"It is certain.",
"It is decidedly so.",
"Without a doubt.",
"Yes, definitely.",
"You may rely on it.",
"As I see it, yes.",
"Most likely.",
"Outlook good.",
"Yes.",
"Signs point to yes.",

"Reply hazy, try again.",
"Ask again later.",
"Better not tell you now.",
"Cannot predict now.",
"Concentrate and ask again.",

"Don't count on it.",
"My reply is no.",
"My sources say no.",
"Outlook not so good.",
"Very doubtful."


I randomized the response for each button.

Here is the code:


#include  < Wire.h >
#include < Adafruit_GFX.h >
#include < Adafruit_SSD1306.h >

// ========================= OLED =========================
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1, 1000000UL, 1000000UL);

// ========================= Optional RGB LED =========================
#define PIN_RED   17
#define PIN_GREEN 16
#define PIN_BLUE  25

// ========================= Touch Config =========================
#define N_TOUCH 6
#define THRESHOLD 30  // base threshold; hysteresis will widen around this

int  touch_pins[N_TOUCH]       = {3, 4, 2, 27, 1, 26};
int  touch_values[N_TOUCH]     = {0, 0, 0, 0, 0, 0};  // smoothed values for Serial debug
bool pin_touched_now[N_TOUCH]  = {false, false, false, false, false, false};
bool pin_touched_past[N_TOUCH] = {false, false, false, false, false, false};

// ---- Stability (Phase 2.1) ----
// Hysteresis
const int THRESH_ON  = THRESHOLD + 6;  // exceed to turn ON
const int THRESH_OFF = THRESHOLD - 6;  // go below to turn OFF
// Debounce (consecutive loops)
const int ON_COUNT_REQUIRED  = 3;
const int OFF_COUNT_REQUIRED = 3;
// Smoothing
const float FILTER_ALPHA = 0.40f;      // 0..1, higher = snappier

int   touch_raw[N_TOUCH]       = {0,0,0,0,0,0};
float touch_filtered[N_TOUCH]  = {0,0,0,0,0,0};
int   on_count[N_TOUCH]        = {0,0,0,0,0,0};
int   off_count[N_TOUCH]       = {0,0,0,0,0,0};

// --- Magic 8-Ball phrases (grouped) ---
const char* PHRASES[] = {
  // Affirmative (10)
  "It is certain.",
  "It is decidedly so.",
  "Without a doubt.",
  "Yes, definitely.",
  "You may rely on it.",
  "As I see it, yes.",
  "Most likely.",
  "Outlook good.",
  "Yes.",
  "Signs point to yes.",

  // Non-committal (5)
  "Reply hazy, try again.",
  "Ask again later.",
  "Better not tell you now.",
  "Cannot predict now.",
  "Concentrate and ask again.",

  // Negative (5)
  "Don't count on it.",
  "My reply is no.",
  "My sources say no.",
  "Outlook not so good.",
  "Very doubtful."
};
const int N_PHRASES = sizeof(PHRASES) / sizeof(PHRASES[0]);


// ========================= Mode / Timing =========================
enum Mode { MODE_WAITING, MODE_SHOWING };
Mode mode = MODE_WAITING;

unsigned long phraseShownAt = 0;
const unsigned long PHRASE_DISPLAY_MS = 5000;  // how long to keep phrase visible

// Store which phrase we’re showing (for non-blocking redraw if needed)
int currentPhraseIdx = -1;

// ========================= Helpers =========================

// Center one line of text at y
void centerText(const char* msg, int y) {
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds((char*)msg, 0, y, &x1, &y1, &w, &h);
  int x = (SCREEN_WIDTH - (int)w) / 2;
  display.setCursor(x, y);
  display.print(msg);
}

// Very simple word-wrap + centered lines.
// Draws multiple lines starting at yStart, with 'lineGap' pixels between lines.
void drawWrappedCentered(const char* msg, int yStart, int lineGap = 10) {
  // We’ll build lines greedily. Using Arduino String for simplicity.
  String text(msg);
  String line = "";
  int y = yStart;

  // Break into words
  int start = 0;
  while (start < (int)text.length()) {
    // find next space
    int spacePos = text.indexOf(' ', start);
    String word;
    if (spacePos == -1) {
      word = text.substring(start);
      start = text.length();
    } else {
      word = text.substring(start, spacePos);
      start = spacePos + 1;
    }

    // Try adding this word to current line
    String trial = line.length() ? (line + " " + word) : word;

    int16_t x1, y1;
    uint16_t w, h;
    display.getTextBounds((char*)trial.c_str(), 0, y, &x1, &y1, &w, &h);

    if (w <= SCREEN_WIDTH - 2) {
      line = trial; // fits
    } else {
      // draw current line centered
      centerText(line.c_str(), y);
      y += lineGap;
      line = word; // start new line with this word
    }
  }

  // draw the last line if any
  if (line.length()) {
    centerText(line.c_str(), y);
  }
}

void print_touch_serial() {
  for (int i = 0; i < N_TOUCH; i++) {
    char buf[8];
    snprintf(buf, sizeof(buf), "%4d", touch_values[i]);
    Serial.print(buf);
    Serial.print(i == N_TOUCH - 1 ? '\n' : ' ');
  }
}

// Stable touch update: discharge, time rise, smooth, hysteresis+debounce
void update_touch() {
  const int t_max = 200;

  for (int i = 0; i < N_TOUCH; i++) {
    int p = touch_pins[i];

    // 1) Discharge pad
    pinMode(p, OUTPUT);
    digitalWrite(p, LOW);
    delayMicroseconds(40);

    // 2) Time the rise via pull-up (capacitance slows the rise)
    noInterrupts();
    pinMode(p, INPUT_PULLUP);
    int t = 0;
    while (!digitalRead(p) && t < t_max) {
      t++;
    }
    interrupts();

    // 3) Smoothing
    touch_raw[i] = t;
    touch_filtered[i] = (FILTER_ALPHA * touch_raw[i]) +
                        ((1.0f - FILTER_ALPHA) * touch_filtered[i]);

    // 4) Hysteresis + debounce
    bool was = pin_touched_now[i];
    bool now = was;

    if (!was) { // OFF -> look for ON
      if (touch_filtered[i] > THRESH_ON) {
        on_count[i]++; off_count[i] = 0;
        if (on_count[i] >= ON_COUNT_REQUIRED) { now = true; on_count[i] = 0; }
      } else { on_count[i] = 0; off_count[i] = 0; }
    } else {    // ON -> look for OFF
      if (touch_filtered[i] < THRESH_OFF) {
        off_count[i]++; on_count[i] = 0;
        if (off_count[i] >= OFF_COUNT_REQUIRED) { now = false; off_count[i] = 0; }
      } else { on_count[i] = 0; off_count[i] = 0; }
    }

    pin_touched_past[i] = was;
    pin_touched_now[i]  = now;

    touch_values[i] = (int)(touch_filtered[i] + 0.5f); // for Serial debug
  }
}

// Edge: returns first pad that was just pressed (now true, past false), or -1
int firstJustPressed() {
  for (int i = 0; i < N_TOUCH; i++) {
    if (pin_touched_now[i] && !pin_touched_past[i]) return i;
  }
  return -1;
}

// Screens
void showPrompt() {
  display.clearDisplay();
  display.setTextSize(1);
  centerText("Ask a question...", 10);
  centerText("Touch any pad", 28);
  centerText("for an answer...", 46);
  display.display();
}

void showPhrase(const char* msg) {
  display.clearDisplay();
  display.setTextSize(1);
  // Start a bit higher so 2–3 lines fit nicely
  drawWrappedCentered(msg, 20, 10);
  display.display();
}

// ========================= Setup / Loop =========================
void setup() {
  Serial.begin(115200);

  // Optional LED pins (ignore if not connected)
  pinMode(PIN_RED, OUTPUT);
  pinMode(PIN_GREEN, OUTPUT);
  pinMode(PIN_BLUE, OUTPUT);
  digitalWrite(PIN_RED, HIGH);
  digitalWrite(PIN_GREEN, HIGH);
  digitalWrite(PIN_BLUE, HIGH);

  // OLED
  display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.display();

  // Touch pins default to INPUT_PULLUP
  for (int i = 0; i < N_TOUCH; i++) {
    pinMode(touch_pins[i], INPUT_PULLUP);
  }

  // Seed randomness (use any floating analog or micros() as entropy)
  randomSeed(analogRead(A0) ^ micros());

  mode = MODE_WAITING;
  currentPhraseIdx = -1;
  showPrompt();
}

void loop() {
  update_touch();
  print_touch_serial();

  if (mode == MODE_WAITING) {
    int pressed = firstJustPressed();
    if (pressed != -1) {
      // Pick a random phrase and show it
      currentPhraseIdx = random(0, N_PHRASES);
      showPhrase(PHRASES[currentPhraseIdx]);

      // Optional LED blink: green for a beat
      digitalWrite(PIN_GREEN, LOW);
      digitalWrite(PIN_RED, HIGH);

      phraseShownAt = millis();
      mode = MODE_SHOWING;
    }
  }
  else if (mode == MODE_SHOWING) {
    // Keep phrase up until timer expires, then return to prompt
    if (millis() - phraseShownAt >= PHRASE_DISPLAY_MS) {
      digitalWrite(PIN_GREEN, HIGH);
      digitalWrite(PIN_RED, HIGH);
      showPrompt();
      mode = MODE_WAITING;
    }
  }

  delay(50); // ~20 FPS pacing; pairs nicely with debounce settings
}