โ† Back to Index
Week 10

Output Devices

Fab Academy Barcelona, April 16 โ€“ April 22, 2026

The PCB business card with the Grove SSD1306 OLED showing 'Hold Me' in blue, copper board engraved 'Shiv Creative Technologist', coin cell and ATSAMD11 visible

Overview

This week's assignment: add an output device to a board you've designed and program it to do something. The output device is a Grove 0.66" SSD1306 OLED โ€” a 20ร—20mm module that plugs into the JST header already routed on my PCB business card from Week 8. When the capacitive touch pads are held, it scrolls personal facts. When released, it says: Hold Me.

Getting there took eight distinct failures โ€” wrong bootloader offset, bad SERCOM routing, hardware IยฒC instability, a hidden framebuffer crop window, flash overflow, dead USB serial, and a flickering marquee. Each failure is documented here as a distinct lesson.

The touch sensing and calibration that drives this display is documented in Week 9 (Input Devices, linked in the sidebar). Group assignment work โ€” measuring power consumption of output devices โ€” is reflected on below and documented on the group work page, linked in the sidebar.

"Getting a 64ร—48 pixel window to say something true took eight separate failures. The OLED itself was the only working debugger."

Power Consumption: Group Assignment

As part of the group assignment, we measured power draw on the SSD1306 OLED under different conditions. Setup: bench power supply in series with a current meter, 3.3V supply, module connected directly (no microcontroller in the loop for this measurement).

  • Display off (sleep mode): ~5โ€“10ยตA โ€” essentially nothing
  • Display on, all pixels off: ~300ยตA โ€” controller and IยฒC bus active
  • Display on, full white: ~20โ€“25mA โ€” all OLED pixels at maximum brightness
  • Typical text rendering (partial fill): ~5โ€“8mA โ€” realistic operating condition

What this measurement taught me is how much content drives power on an OLED. Unlike an LCD with a constant backlight, an OLED only spends energy on the pixels it lights โ€” so "full white" pulls roughly 50ร— more than a blank screen, and the meter reading tracks directly with what's on the panel. At typical draw (~6mA at 3.3V โ‰ˆ 20mW), a CR2032 (220mAh nominal, realistically ~180mAh at 3V) gives approximately 30 hours of continuous display time โ€” more than enough for the business card interaction. The marquee only runs when both pads are held; idle draw with display on but showing "Hold Me" is at the lower end of that range.

The Output Device: Grove SSD1306 0.66"

The Grove SSD1306 0.66" is a 20ร—20mm OLED module driven by the same SSD1306 controller used in common 128ร—64 displays โ€” but the physical panel is only 64ร—48 pixels. That mismatch is the source of most of what went wrong below.

The Framebuffer Trap

The SSD1306 always operates a 128ร—64 framebuffer internally. The 64ร—48 panel shows only a crop of it. Drawing at (0,0) writes to memory the panel never renders.

Diagram showing 128x64 SSD1306 framebuffer in red with 64x48 visible crop in black, bounded by X:30-95 Y:15-63

The full 128ร—64 framebuffer (red) vs. the 64ร—48 physical panel (black). Origin (0,0) is completely off-screen. Via Seeed Studio wiki.

The visible region:

X: 31 โ†’ 95   (64px wide)
Y: 16 โ†’ 63   (48px tall)
Imagine drawing on a 128ร—64 canvas, then placing a 64ร—48 window over the centre. Everything outside the window exists in memory โ€” it just isn't shown.

Connection to the Board

The OLED plugs into a 4-pin JST header designed into the board in Week 6: VCC, GND, SDA (pin 8), SCL (pin 9). No soldering after fabrication โ€” the header was already there.

OLED module plugged into JST header on the PCB business card โ€” white connector carrying VCC GND SDA SCL

Plug-and-play. The header was designed in KiCad during Week 6 and milled in Week 8.

Arduino Setup

Before tackling the OLED, the ATSAMD11 needed to be correctly configured in the IDE. The Fab SAM core has more settings than a standard Arduino board โ€” most of them matter.

Arduino IDE showing Generic D11C14A selected under Fab SAM core for Arduino in the Tools menu

Board: Generic D11C14A under Fab SAM core for Arduino.

Arduino Tools Programmer submenu showing CMSIS-DAP highlighted in blue

Programmer: CMSIS-DAP via the Quentorres SWD programmer.

Arduino Sketch menu with Upload Using Programmer highlighted โ€” keyboard shortcut Shift Cmd U

Upload Using Programmer (โ‡งโŒ˜U). Not the regular Upload button, which assumes a bootloader exists.

The Debug Journey: Eight Failures

Every failure below is a distinct embedded systems lesson. They happened in roughly this order.

Failure 1 โ€” Firmware Wasn't Running At All

Upload appeared successful. Nothing happened. Root cause: wrong bootloader configuration. My SWD/CMSIS-DAP workflow does direct flash writes, but the board was set to 4KB_BOOTLOADER โ€” which places firmware at the wrong flash offset. The chip was executing garbage memory.

Arduino Tools menu showing Bootloader Size set to 4KB_BOOTLOADER

The wrong setting โ€” firmware placed at wrong flash offset.

Arduino Tools menu showing NO_BOOTLOADER selected in blue

The fix. Then run Burn Bootloader โ€” sets fuses and flash layout, not actual bootloader code.

"Burn Bootloader" with NO_BOOTLOADER is not a contradiction โ€” it's a fuse/clock/layout operation. After this, firmware executed.

Failure 2 โ€” No Way to Confirm Execution

No onboard LED, no USB serial. How do you prove the chip is running? Multimeter on a GPIO pad.

Arduino IDE showing voltage test sketch: pinMode OUTPUT, digitalWrite HIGH delay 5000, LOW delay 5000 on pin A4

Pin A4 toggling every 5 seconds. Red probe on the pad, black on GND โ€” watching for 3.3V โ†” 0V. No LED required.

Root Cause 1 โ€” Wrong IยฒC Mapping

OLED blank. IยฒC scanner found nothing. My board physically routes SDA to A8 (pin 8) and SCL to A9 (pin 9) โ€” but the Fab SAM core was mapping hardware IยฒC to different pins under the default Serial Config. Fix: switch to NO_UART_ONE_WIRE_ONE_SPI.

Arduino Tools Serial Config submenu showing NO_UART_ONE_WIRE_ONE_SPI highlighted in blue

Serial Config controls SERCOM-to-peripheral assignment. This one change routes IยฒC to the correct physical pins. IยฒC scanner then found the OLED at 0x3C.

Root Cause 2 โ€” Hardware IยฒC Instability

Scanner worked. OLED still blank. Hardware Wire on ATSAMD11 with Fab SAM core was unreliable with OLED libraries. Fix: switch to software IยฒC. Bit-bangs the protocol in firmware โ€” slower, but completely stable on this chip.

U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(
  U8G2_R0,
  /* clock=*/ 9,
  /* data=*/  8,
  /* reset=*/ U8X8_PIN_NONE
);

Root Cause 3 โ€” Hidden Framebuffer Crop

OLED finally lit up โ€” text appeared misaligned or invisible. This is the framebuffer crop described above. All drawing coordinates had to be offset into the visible window. Drawing at (0,0) writes to memory the panel never shows.

Root Cause 4 โ€” Flash Overflow

The ATSAMD11 has 16KB flash. Large libraries caused:

FLASH overflowed by 668 bytes

Fixes: smaller fonts, removed sprintf, stripped unused U8g2 features, enabled config.h code size reductions in Build Options, removed #include <Wire.h> (not needed for software IยฒC). Every byte counted.

Failure 7 โ€” USB Serial Never Worked

USB CDC didn't enumerate with NO_BOOTLOADER on Fab SAM core. Serial Monitor stayed blank the entire project. Fix: used the OLED itself for all debugging โ€” printing raw sensor values and state directly to screen. Faster than USB serial anyway: real hardware, real time, no driver indirection.

Failure 8 โ€” Marquee Flickered with U8X8

Early marquee used U8X8 (the lightweight non-graphics variant) for speed. U8X8 is character-grid based โ€” it scrolls whole character columns, not sub-pixels. Result: chunky, flickering movement. Smooth pixel-level scrolling requires the full U8g2 graphics mode with clearBuffer() + sendBuffer() each frame.

How the Code Works

Final Working Setup

Board:          Generic D11C14A (Fab SAM core)
Programmer:     CMSIS-DAP
Bootloader:     NO_BOOTLOADER
Clock:          INTERNAL_USB_CALIBRATED_OSCILLATOR
Build Options:  config.h enabled (code size reductions)
Serial Config:  NO_UART_ONE_WIRE_ONE_SPI
Driver:         U8G2_SSD1306_128X64_NONAME_F_SW_I2C
                clock=9, data=8

The Rendering Loop

Each frame: clear buffer โ†’ draw string at current xPos โ†’ send buffer โ†’ decrement xPos. When text exits the visible window's left edge, advance to the next fact and reset.

u8g2.clearBuffer();

if (touched) {
  const char* message = facts[currentFact];
  u8g2.drawStr(xPos, 40, message); // Y=40 within visible Y:16โ€“63

  xPos -= 14; // 14px per frame ร— 50fps โ‰ˆ 700px/sec

  int textWidth = strlen(message) * 6; // 6x12 font: 6px per char

  if (xPos < -textWidth + 31) { // 31 = left edge of visible window
    currentFact = (currentFact + 1) % factCount;
    xPos = 95;                   // 95 = right edge of visible window
  }
} else {
  u8g2.drawStr(40, 40, "Hold Me");
}

u8g2.sendBuffer();
delay(20); // ~50fps

The numbers 31 and 95 are the visible window edges from the framebuffer crop. Without that knowledge they look arbitrary. With it, they're the exact bounds of what the panel can show.

Final Working Output System

  • Display: Grove OLED 0.66" SSD1306, 20ร—20mm, 64ร—48 physical pixels
  • Driver: U8G2_SSD1306_128X64_NONAME_F_SW_I2C โ€” clock=9, data=8
  • Visible window: X: 31โ†’95 ยท Y: 16โ†’63
  • Working features: touch-triggered UI, smooth marquee animation, 9-fact cycle, centered "Hold Me" text using crop offsets

Design Files & Source Code

The board this OLED connects to was designed in Week 6 (Electronics Design) and produced in Week 8 (Electronics Production); the touch sensing is in Week 9 (Input Devices) โ€” all linked in the sidebar. No new board was made this week โ€” the OLED plugs into the existing header โ€” so the design files are the same board, linked below alongside the combined firmware.

Smooth marquee on the 64ร—48 physical panel. Each fact plays fully before the next loads.