Week 10: Output Devices

This week I added a Touch Screen display to the copper PCB that I designed and milled in weeks 4 and 8. This relates to my final project directly as I will be incorporating a touchscreen into the handheld console portion of my 2-part device. The touchscreen joins the textile button I created last week during inputs with the motor I tested previously and my UV index from inputs week. Through many trials and tribulations I finally got all four running together on my board. This week I showcase how once I added the touchscreen, a prompt appears, the textile button advances it, the UV index is displayed and if a user does not respond within a designated amount of time, the motor buzzes a reminder. This is my final project's core loop proving sucessful, end to end.

Group Assignment

As mentioned before, I am a remote student and am coordinating with my instructor on how the group measruement should be handled by my node.

For my own output device this measurement matters in a concrete way: the display backlight is the single biggest power draw in the whole console, so measuring it tells me what battery the console needs. My plan is to measure the board current with the backlight off versus on and idle versus actively redrawing using my multimeter in series. The backlight still being independent of the rest of the controller means I can isolate it's draw cleanly.

Condition Current Notes
Board only (backlight off) Baseline
Backlight on with static prompt Backlight dominates
Backlight on and redrawing SPI Traffic and energy draw
Motor pulse at it's peak Brief draw during the buzz reaction

Individual Assignment

This week's addition: A Touch Screen

Why choose an LCD?

My final project is a reflective journaling companion in two pieces.

  1. A console: The user holds the console which stores the users thought and information about the users' surroundings (UV conditions). Everything is stored on an SD card right in the users hand. There is no account, no cloud and this device is fully off grid.
  2. A wristband: The user wears a sustainable textile that incrporates technology to provide gentle haptic nudges and receives acknowledgment through a textile button.

Week 9 gave my devices a way to sense via the UV input. This week I gave my devices a way to speak. The display is how the console actually presents prompts and other infomration ot users. It is the single most important output of my entire final project. Without it, there is no prompt to answer and the whole journaling interaction has no surface.

Why the Display this week and not the keyboard?

I have two parts that I plan to incorporate into the console, a keybaord and the Touchscreen. The premier decision I had to make was which one speaks more to my final project and the goals of this week. The keyboard is an input device that sends keystrokes to the microcontroller. The display is the output tho it also can send information to the microcontroller, It is the only component that I have not yet tested that does both input and output.

This week also asked u to make the output device DO something. Confirming that I can make the screen receive power wasn't going to be enough so I took this opportunity to prove that I can code it to do it's intendend purpose for my final project as well. Instead of having it simply show a graphic or a few words, I wanted to practice having it to behave as I will need my final project to behave. I wanted to show that it can render a prompt, showcase the users real world settings through the UV sensor, and provide feedback to the user.

How the Display Works

Based on the assignment I added this output device to a board I designed and fabricated in a previous week. This was the hardest part for me because I had no idea when I drew up the schematics or actually milled the PCB that I'd be adding 4 extra components to it. Thankfully the board's hardware SPI bus and pins had room for the display's connections. Based on the layout of my PCB the biggest issue was that I'd left NO room for any extra components to be added without some blood, sweat and tears involved.

The module is a 2.8" panel driven by an ILI9341 controller over SPI. My microcontroller (RP2040) writes pixel and command data to the controller over the SPI bus. The controller holds its own frame and refreshes the panel so I only have to send what changes.

Two extra lines do the rest:

  1. DC tells the controller whether a byte is a command or pixel data.
  2. CS selects the chip so it shares the bus wit hthe on board micro SD slot and touch controller

On the backside of my board, I noticed the SD card slot is broken out on it's own header and separate from the main display row. This was a surprise convenience for later when I wire the SD storage for journal entries. Those four will share the same bus and just need their own chip-select.

Back of Touchscreen

Circuitry and Wiring

BOM

Component Part Interface
Display 2.8" ILI9341 controller SI + DC/CS/RST
Host Board Milled XIAO RP2040 board (week 4 and 8 of Heaven Whitby's Fabacademy 2025)

Connections

Display Pin RP 2040 Pin GPIO Note
VCC 3V3 - NOT 5v
GND GND - Common Ground for all components
SCK D8 GP2 Hardware SPI clock
SDI D10 GP3 Data to Display
CS D1 GP27 Chip Select
DC/RS D6 GP0 Data/Command
RESET D7 GP1 RST in code
LED 3V3 - Backlight On
SDO D9 - Display Only Does Not Read Back

Getting these pins connected was the most time consuming and stressful part of this week!!!!!!

It took me over 5 hours to get these pins connected without frying the board or distroying the touchscreen's components. I did a lot of work to ensure that the actual touchscreen remained "brand new".

I took the time to secure wires to the touchscreen's pins using a stripping and taping method that required extreme patience and labeling. I made sure to both color code each connection and label each wire to ensure that I did not accidently solder anything incorrectly. I also took the time to ensure that before I soldered anything, I did not have any wires or pins touching eachother.

Touchscreen Wires Labled

Pin conflict notes

I had to pick the control pins carefully because D3 is my UV sensor and D4 and D5 are the pins I'm saving for the keyboard. I chose D1, D6, and D7 to avoid any issues. The textile button is using D0 and the motor is using D2 so I had to work around those. As previously stated. I had no idea I'd be using this screen or that it would have these pins so I really had to be patient when adding the touchscreen to not disturb any of my previous successes.

I had to also make sure that my display's silkscreen reads reset and the code calls that TFT_RST and to avoid the RP2040s actual reset button.

Through the pain and many many many desolders I got everything connected without the use of a breadboard and ensured that the display is soldered directly to the board.

My beautiful wild connections

Setting up the IDE

Honestly, I didn't need to make many adjustments here since my RP2040 was already installed from weeks 8 and 9. In the Library Manager I added the Adafruit ILI9341 and GFX Library. When I did this it also added the Adafruit BusIO. Everything else is already in the core and the board remained the same on COM6, serial at 115200.

Coding

I have the same toolchain as in my earlier weeks: Arduino IDE with the RP2040 core, board set to the Seeed Xiao R2040. Rather than a display only demo I wrote a combined sketch that drives the display and keeps the textile button, UV sensor and motor from previous week. This way, the output is a real interaction.

Source Code

  // ─────────────────────────────────────────────────────────────
  // Fab Academy Week 10 — Output Devices
  // Heaven Whitby — handheld journaling console (test bench)
  // Board: Seeed Studio XIAO RP2040 (custom milled PCB, Week 8)
  //
  // OUTPUT device under test:
  //   - 2.8" ILI9341 SPI TFT (240x320) — displays the journaling prompt
  // Also driven (preserved): DC vibration motor via MOSFET on D2
  // Inputs preserved from Week 9:
  //   - Velostat fabric button on A0/D0   (press = advance prompt)
  //   - GUVA-S12SD analog UV sensor on A3  (shown on screen)
  // ─────────────────────────────────────────────────────────────
   
  #include <SPI.h>
  #include <Adafruit_GFX.h>
  #include <Adafruit_ILI9341.h>
   
  #define TFT_CS   D1   // GP27
  #define TFT_DC   D6   // GP0
  #define TFT_RST  D7   // GP1
  // SCK -> D8, MOSI -> D10, MISO -> D9 (default SPI); LED -> 3V3
   
  Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
   
  const int velostatPin = A0;
  const int uvPin       = A3;
  const int motorPin    = D2;
   
  // Velostat press = reading drops LOW. Auto-baselined at startup so the
  // threshold adapts to the ADC scale (fixed Week 9's 10-bit -> 12-bit bug).
  int velostatBaseline = 0;            // measured at power-on (untouched)
  const float PRESS_FRACTION = 0.5;    // press = below 50% of resting baseline
  int pressThreshold = 200;            // computed in setup() from the baseline
   
  const char* prompts[] = {
    "What pulled your attention today?",
    "Where did you feel most like yourself?",
    "Name one thing you're carrying that isn't yours.",
    "What would this morning's you want to hear right now?"
  };
  const int promptCount = sizeof(prompts) / sizeof(prompts[0]);
  int currentPrompt = 0;
   
  const unsigned long PROMPT_WINDOW_MS = 20000;
  unsigned long promptStart = 0;
  bool reminded   = false;
  bool wasPressed = false;
   
  void setup() {
    Serial.begin(115200);
    analogReadResolution(12);
    pinMode(motorPin, OUTPUT);
    digitalWrite(motorPin, LOW);
   
    // Auto-baseline the velostat — DO NOT press it at power-on.
    long sum = 0;
    for (int i = 0; i < 32; i++) { sum += analogRead(velostatPin); delay(5); }
    velostatBaseline = sum / 32;
    pressThreshold = (int)(velostatBaseline * PRESS_FRACTION);
    Serial.print("Velostat baseline="); Serial.print(velostatBaseline);
    Serial.print("  press threshold="); Serial.println(pressThreshold);
   
    tft.begin();
    tft.setRotation(0);
    showPrompt();
    Serial.println("Week 10 ready — LCD + velostat + UV + motor");
  }
   
  void loop() {
    int pressure  = analogRead(velostatPin);
    int uvRaw     = analogRead(uvPin);
    float uvIndex = (uvRaw * (3.3 / 4095.0)) / 0.1;
   
    Serial.print("pressure="); Serial.print(pressure);
    Serial.print("  uvIndex="); Serial.println(uvIndex, 1);
   
    bool pressed = (pressure < pressThreshold);
    if (pressed && !wasPressed) {
      currentPrompt = (currentPrompt + 1) % promptCount;
      showPrompt();
    }
    wasPressed = pressed;
   
    if (!reminded && (millis() - promptStart > PROMPT_WINDOW_MS)) {
      pulseMotor();
      drawReminder();
      reminded = true;
    }
   
    drawStatus(uvIndex);
    delay(200);
  }
   
  void showPrompt() {
    promptStart = millis();
    reminded = false;
    tft.fillScreen(ILI9341_BLACK);
    tft.setTextColor(ILI9341_CYAN); tft.setTextSize(2);
    tft.setCursor(10, 12); tft.println("MARGIN");
    tft.drawFastHLine(10, 38, 220, ILI9341_DARKGREY);
    tft.setTextColor(ILI9341_WHITE); tft.setTextSize(2);
    tft.setTextWrap(true);
    tft.setCursor(10, 60); tft.println(prompts[currentPrompt]);
  }
   
  void drawReminder() {
    tft.setTextColor(ILI9341_ORANGE); tft.setTextSize(2);
    tft.setCursor(10, 268); tft.println("take a moment...");
  }
   
  void drawStatus(float uvIndex) {
    tft.fillRect(0, 300, 240, 20, ILI9341_BLACK);
    tft.setTextColor(ILI9341_DARKGREY); tft.setTextSize(1);
    tft.setCursor(10, 305);
    tft.print("UV index: "); tft.print(uvIndex, 1);
  }
   
  void pulseMotor() {
    digitalWrite(motorPin, HIGH);
    delay(500);
    digitalWrite(motorPin, LOW);
  }
  
Code In IDE

Building: Soldering and Running Code

I connected the display the same way I did the UV sensor: loose wires soldered to the pins so I can move the board away from the PCB and reuse the breakout later, each joint wrapped in masking tape for insulation and strain relief. I also labeled every wire with tape so I'd know which was which. I ended up with 9 wires and enough masking tape to make you go blind lol. I did have a small moment of regret when realizing I'd taped them in a way that would require me to run them over the topside of the display which would inevitably make it difficult for me to even read what is on the display. I had to just trust my creative instinct to move beyond that issue becasue I was not going to undo all the work that the taping required. This was the slow unglamourous portion of this entire week.

The soldering question. There was a moment when I started soldering pins to the pads and legs of the RP2040 that I realized that not only am I normally not the most talented at soldering but I'd designed this PCB EXTREMELY tightly for the wearable. I'd made the pads extremely close together because based on my original schematic I wasn't goign to use the majority of them anyway. Well, now I'm using every single one and space between them matters, a WHOLE lot. I literally have JUST enough space between the copper to avoid touching and not nearly enough for sloppy blobs flux. I decided to see if there was any way for me to get a bit creative and go vertical and solder to the legs and not just the flat pads. This worked. I have no idea if i'll get points docked for this during review, but for now, I'm super proud of my ingenuity.

Bring Up Order

I soldered things in stages so that if something when wrong I'd know where to look instead of debugging 9 wires in one sitting which would've probably led to me taking a break for the day.

  1. Power and Ground first. I did these first so that I could actually plug the RP2040 into the computer VIA UsbC and observe if the screen misbehaved or did not power on. Before plugging it in i did a quick check with my multimeter as well to make sure I didn't instantly fry my board upon plug in.
  2. I confirmed that the backlight lights independently and showed a uniform white color. This proved that the backlight was alive. It was good that it stayed blank because no communication pins had been soldered yet tho they were attached to the board, so I could assume that they werent accidently touching either power or ground somehow. Note that before soldering the LED wire or ground wire I plugging in the board and uplaoded the final touchscreen code (from above) so that everything would be ready as soon as everything was connected properly.
  3. Finally I soldered all five data/control lines and checked each cold before plugging in the board via UsbC. Again, this took forever but I took my time to make sure I was not getting any beeps from my multimeter where I shouldnt have. This was super important because like I mentioned before, I wanted to use this touchscreen as if it is brand new in my final console so it's super important that I do not fry antyhign at this pre-prototype phase.
Fully Soldered Board

Testing the Loop, Live

With all nine wires down and short-checked, I plugged in. The screen showed my prompt, the UV reading appeared along the bottom, pressing the velostat advanced to the next prompt, and after the response window elapsed the motor vibrated and the "take a moment…" nudge appeared.

I was so excited.

I recorded a screen capture of the serial pressure values changing as I pressed, alongside video of the board itself.

Check Result Note
Display initates, prompt renders Perfect portrait, rotation 0
UV readout updates on footer Perfect Week 9 inputs preserved
Velostat press advances prompt Perfect Auto baseline threshold established and adjusts on press
Motor fires at window timeout Perfect =20s

What the Serial Monitor Showed

The first lines print once at boot, then the loop streams the live readings. Indoors the UV index sits near 0.2; with the sensor in daylight it climbed into the 5–6 range. The resting pressure drifted up between sessions. I saw 600-690 one session and 770-910 in another session.

The baseline = and week 10 ready lines only print once when I immedietly plug in the device. If the monitor is not connected at that instat or if the loop has been running and scrolled off I cant get them back by scrolling up, in order to capture them I have to be conected and live then rest the board so they print again on camera OR unplug and scroll to the top to capture what happened.

baseline capture

Problems

Textile Button threshold

The biggest code change I made this week wasn't about the display at all but related to fixing a velostat bug I carried in from Week 9. Last week, I swwitched the ADC to 12-bit for the UV sensor's full range but my textile threshold was still the value I calibrated back at 10 bit. At 10 bit the textile rested around 100-200 but my predd drove it below 20 so a threshold of 20 worked. At 12 bit every reading intensified so the resting jumped tp roughly 600-800 but the threshold was still 200 which is now below the hardest I could press. So, the comparisson pressure< 20 never became true and my presses never registered.

Initially I thought the problem was that the threshold was too high when it was actually too low and had slipped under the pressed value when the whole scale moved up. The threshold actually need to sit between the resting and pressed levels and a fixed nnumber cant stay there if the scale changes.

Instead of picking a new fixed number that would eventually fail again if something changes, I switched to an auto-baseline. Now, the power on averages 32 samples of the resting textile button then sets the threshold to a percentage of that. This way there is adaptation to whatever scale i'm running with one rule that stays the same: the textile button can NOT be pressed when the device powers on because thats when resting is measured. After that, presses register reliable and the prompt advances every time.

Lesson Learned:Changing the ADC resolution has an effect on every analog input on my board not just the one I intended. A balibration constant tied to a specific scale is fragile and deriving the threshild from a live baseline is more resilient over time.

The motor doesn't buzz when I press it

Lesson Learned:I initially thought this was a problem but actually wasn't. In the input week I designed the code to allow me to trigger the motors vibration upon pressing the textile button. In this week's code the motor only fires on the timeout path and does not provide a response in the window. I did discover that the presses do advance the prompt as designed tho so in exploring what I thought was a problem I discovered that not only did I not have a problem but my future design goals were working properly.

I couldn't find the startup lines in the serial monitor

Lesson Learned:My baseline and ready line only print at the initial plug in. Once it the monitor starts to scroll I have to unplug to scroll back to the top and capture that initial line.

Backlit but blank screen during bring up

Lesson Learned:With only power and the LED connected, the screen lit but showed nothing. This worried me for a second until I reasoned it through: the backlight is independent of the controller, so a lit-but-blank panel is exactly what "power good, no data yet" looks like. Once I soldered the five data/control lines, the prompt appeared.

Nine bare wires on a maskless board is a swap and short risk

Lesson Learned:On my unmasked board every bare joint is also a potential short. I managed it by labeling every wire, taping each joint, and buzzing every new joint both against its neighbors (for shorts) and back to its intended pin (for swaps) before powering up. With so many wires on a maskless board, the multimeter is my bestie.

Videos

From Vimeo

Sound Waves from George Gally (Radarboy) on Vimeo.


From Youtube

3D Models

Dita's Gown by Francis Bitonti Studio on Sketchfab

Files

My board's KiCad schemati and PCB layout are documented in and linked in weeks 4 and 8.