Badge firmware

The badge runs Arduino-framework firmware built with PlatformIO. Nothing here is badge-specific magic — it is an ESP32-S3 board with an SPI e-paper panel.

Toolchain

PlatformIO with the espressif32 platform and the Arduino framework. The display is driven by GxEPD2, which supports the SSD1680 controller on the badge’s panel.

[env:onetap-badge]
platform   = espressif32
board      = esp32-s3-devkitc-1
framework  = arduino
monitor_speed = 115200

lib_deps   = zinggjm/GxEPD2

There is no board definition for the badge itself, so a devkit definition is used and the flash settings are overridden — which is exactly where the sharp edge is.

Flash and partition configuration

The module fitted to the current board is an ESP32-S3-WROOM-1-N16R2: 16 MB of flash and 2 MB of quad PSRAM. The esp32-s3-devkitc-1 definition assumes 8 MB, so both the size and the partition table have to be set explicitly.

board_upload.flash_size  = 16MB
board_build.flash_mode   = dio
board_build.partitions   = default_16MB.csv

Whenever you override flash_sizeaway from the base board’s value, override the partition table too. A partition table that describes a different flash size will place partitions where no flash exists, and the second-stage bootloader resets in a loop before your application ever prints a line. This has already cost this project a debugging session — the symptom looks nothing like a flash-size problem.

PSRAM is optional for this firmware. If you enable it, note that R2 is a quad-PSRAM part: use a qspi memory type, not the opi setting an octal R8 module would need. Getting this wrong is another silent boot failure.

Driving the display

The panel is a GDEY0213B74 — 250 × 122, SSD1680 — driven through GxEPD2’s GxEPD2_213_BNclass over 4-wire SPI. The panel’s BS1 pin must be tied to ground for 4-wire mode.

Pin numbers are deliberately not listed here; take them from the schematic or the board silkscreen for your revision, then construct the driver with them:

#include <GxEPD2_BW.h>

// CS, DC, RES, BUSY — read these off your own board.
GxEPD2_BW<GxEPD2_213_BN, GxEPD2_213_BN::HEIGHT> display(
    GxEPD2_213_BN(/*CS=*/ PIN_CS, /*DC=*/ PIN_DC,
                  /*RES=*/ PIN_RST, /*BUSY=*/ PIN_BUSY));

void setup() {
  display.init(115200);
  display.setRotation(1);
  display.setFullWindow();
  display.firstPage();
  do {
    display.fillScreen(GxEPD_WHITE);
    display.setCursor(10, 30);
    display.print("Onetap Labs");
  } while (display.nextPage());
  display.hibernate();   // hold the image, drop the power
}

Call hibernate() when you are done drawing. The image stays on the panel with the controller powered down, which is the whole point of e-paper on a badge that spends its day on a lanyard.

Flashing a board

Over USB-C, using the two strapping buttons:

  • Hold BOOT, tap EN, then release BOOT. The board is now in the ROM serial bootloader.
  • Run pio run -t upload. The port appears as a USB CDC device.
  • Tap EN to run the new firmware, then pio device monitor to watch it boot.

When it boot-loops

A reset loop that prints rst:0x3 (RTC_SW_SYS_RST) straight after entry 0x..., with no application output at all, is a bootloader-stage failure. Your code is not running and never did.

  • Check the partition table against the real flash size first. This is the most common cause by a wide margin.
  • Check the PSRAM mode matches the module variant — qspi for R2, opi for R8.
  • Disconnect anything attached to the Qwiic port or the breakout pads and reset. A peripheral holding a strapping pin at the wrong level at boot produces the same silence.

Flash mode (qio versus dio) is worth trying, but in this project it has never been the actual cause.