Chapter 27: The Pocket Flasher

The Distribution Problem

Part 1 gave us a survival workstation — a bare-metal UEFI application that boots on any computer, includes a C compiler, a text editor, a file browser, and a library of survival knowledge. All of it fits in under 4 MB. All of it can rebuild itself from source.

But we skipped a question. How does the workstation get onto an SD card in the first place?

In the chapters so far, we cross-compiled from a Linux machine, copied the binary to a FAT32 partition, and booted. That works fine when you have a laptop with a working OS. In a survival scenario, you might not. The whole point of the project is to work when infrastructure has collapsed. If you need infrastructure to deploy it, you’ve built a lifeboat that can only be launched from a yacht.

We need a device that:

  • Stores the complete workstation image
  • Writes it to a blank SD card without any other computer
  • Is cheap enough to stockpile
  • Is small enough to carry in a pocket
  • Runs on minimal power

The ESP32 CYD

The answer is the ESP32-2432S028R, known informally as the “Cheap Yellow Display” or CYD. It costs about $7, ships from dozens of vendors, and packs everything we need onto a board the size of a credit card:

┌──────────────────────────────────────────────────┐
│                                                  │
│   ┌──────────────────────────┐  ┌──────────┐    │
│   │                          │  │ micro-USB │    │
│   │   2.8" ILI9341 Display   │  └──────────┘    │
│   │      320 × 240 pixels    │                   │
│   │      RGB565 (16-bit)     │  ┌──────────┐    │
│   │                          │  │ SD card   │    │
│   │   XPT2046 Resistive      │  │ slot      │    │
│   │   Touchscreen (overlay)  │  └──────────┘    │
│   │                          │                   │
│   └──────────────────────────┘                   │
│                                                  │
│   ESP32 (Xtensa dual-core, 240 MHz)             │
│   4 MB SPI flash, 520 KB SRAM                   │
│   Wi-Fi + Bluetooth (unused)                     │
│                                                  │
└──────────────────────────────────────────────────┘

Four components matter:

The ESP32 MCU. A dual-core 240 MHz processor with 520 KB of SRAM and 4 MB of external SPI flash. It runs FreeRTOS (a real-time operating system for microcontrollers) and has an excellent SDK called ESP-IDF. The Wi-Fi and Bluetooth radios are present but we never turn them on — they are irrelevant to our task and would waste power.

The ILI9341 display. A 2.8-inch TFT LCD running at 320x240 pixels in RGB565 (16-bit color). It connects to the ESP32 via SPI and can be driven at 40 MHz — fast enough to update the screen in real time. We use it to show a menu, a progress bar, and status messages.

The XPT2046 touchscreen. A resistive touch controller overlaid on the display. It reads finger position as analog voltages and converts them to coordinates. We use it for one thing: detecting which button the user tapped. No gestures, no multitouch, no scrolling — just “where did the finger land?”

The SD card slot. A standard micro-SD socket wired to the ESP32’s SPI bus. We write the workstation image here. SD cards speak SPI natively (it’s their slow but simple interface), so no special hardware is needed.

The Constraints

The ESP32 is orders of magnitude less powerful than even a basic laptop. Understanding the constraints shapes every design decision:

4 MB flash. The ESP32’s external flash holds everything: the firmware (our flasher application), FreeRTOS, ESP-IDF runtime libraries, and the compressed workstation payload. The partition layout splits this into:

Address     Size      Purpose
──────────  ────────  ──────────────────────────────
0x009000    24 KB     NVS (non-volatile storage, ESP-IDF bookkeeping)
0x010000    1.4 MB    Application firmware
0x170000    2.6 MB    Workstation payload (compressed images)

The application gets 1.4 MB. The payload gets 2.6 MB. The workstation binaries — both aarch64 and x86_64, with all their supporting files — must fit in that 2.6 MB after compression.

520 KB SRAM. This is all the working memory we have. FreeRTOS and ESP-IDF claim about 340 KB of it for their stacks, heaps, and driver buffers. That leaves roughly 180 KB for our code’s data. We need to decompress files, build FAT32 structures, compute CRC32 checksums, and manage directory entries — all within that budget. Every buffer allocation is a conscious decision.

SPI for everything. The display is on SPI2. The SD card is on SPI3. The touchscreen would need a third SPI bus, but the ESP32 only has two hardware SPI controllers available (the third is reserved for the flash chip). So the touch controller uses bit-banged SPI — we toggle GPIO pins manually in software. This is slow (~50 Hz polling rate) but the touchscreen only needs to detect taps, not track smooth motion.

What We’re Building

Over the next seven chapters, we build the complete flasher:

  • Chapter 28: Set up the ESP-IDF toolchain, configure the project, and get the display and touch working
  • Chapter 29: Drive the ILI9341 display — SPI configuration, landscape mode, drawing text and rectangles, reading touch input
  • Chapter 30: Talk to the SD card — SPI3 configuration, sector-level read and write
  • Chapter 31: Create a GPT partition table — protective MBR, primary and backup headers, CRC32
  • Chapter 32: Build a FAT32 filesystem — BPB, FAT tables, directories, long filenames, streaming writes
  • Chapter 33: Pack the payload — compression, multi-architecture support, the binary format, the Python packing script
  • Chapter 34: Flash! — orchestrate the whole sequence, stream-decompress files, verify, and boot

The architecture mirrors Part 1’s approach: each chapter builds one layer, each layer depends only on the layers below it.

┌──────────────────────────────────────┐
│  Chapter 34: Flasher (orchestrator)  │
├──────────┬──────────┬────────────────┤
│ Ch.33    │ Ch.32    │ Ch.31          │
│ Payload  │ FAT32    │ GPT            │
├──────────┴──────────┴────────────────┤
│  Chapter 30: SD Card (sector I/O)    │
├──────────────────────────────────────┤
│  Chapter 29: Display + Touch (UI)    │
├──────────────────────────────────────┤
│  Chapter 28: ESP-IDF (toolchain)     │
└──────────────────────────────────────┘

By Chapter 34, the flow is: power on the ESP32 → splash screen → tap an architecture → insert SD card → GPT → FAT32 → decompress and write files → verify → done. Pull the card out, put it in any UEFI computer, boot, and you have a survival workstation that can rebuild itself.

The Source Files

The complete flasher is about 2,000 lines of C across ten source files, plus a 180-line Python script for packing the payload:

esp32/main/
  main.c        111 lines   Entry point and main loop
  display.c     157 lines   ILI9341 display driver
  display.h      45 lines   Display interface
  touch.c       168 lines   XPT2046 touch driver
  touch.h        21 lines   Touch interface
  sdcard.c      131 lines   SD card sector I/O
  sdcard.h       33 lines   SD card interface
  gpt.c         217 lines   GPT partition table creation
  gpt.h          22 lines   GPT interface
  fat32.c       893 lines   FAT32 filesystem (largest file)
  fat32.h        49 lines   FAT32 interface
  flasher.c     173 lines   Flash sequence orchestrator
  flasher.h      14 lines   Flasher interface
  payload.c     161 lines   Payload manifest reader
  payload.h      52 lines   Payload interface
  ui.c          166 lines   Touch UI (menus, progress, done/error)
  ui.h           32 lines   UI interface
  font.c        202 lines   8x16 VGA font data
  font.h         20 lines   Font interface

esp32/scripts/
  pack_payload.py  179 lines   Payload packer

FAT32 is the largest module by far — building a filesystem from scratch takes real work, as we learned in Part 1. But the ESP32 version benefits from having already solved it once. The code is a port of Part 1’s src/fat32.c, adapted to use standard C types instead of UEFI types and to write through the SD card SPI driver instead of UEFI’s block I/O protocol.

Why Not Just dd an Image?

You might wonder: why not create a complete disk image on the Linux build machine and have the ESP32 write it sector by sector? No GPT creation, no FAT32 formatting, no file-by-file decompression — just a raw byte-for-byte copy.

Two reasons.

Size. A raw disk image of even a small FAT32 partition is mostly zeros. A 64 MB partition with 2 MB of files has 62 MB of empty space. Even compressed, the image carries overhead from the filesystem metadata structure. Packing individual files with per-file deflate compression is more space-efficient because we only store the data that matters.

Flexibility. The flasher adapts to whatever SD card you insert. A 2 GB card gets a 2 GB partition. A 32 GB card gets a 32 GB partition. The GPT and FAT32 structures are computed at flash time based on the card’s actual size. With a raw image, you’d need a different image for every card size — or waste most of the card.

The file-by-file approach is more code (we have to implement GPT and FAT32 creation), but it produces a better result: the entire SD card is usable, the filesystem is clean, and the payload compresses better.

Let’s Begin

In the next chapter, we set up the ESP-IDF toolchain, create the project skeleton, and get our first signs of life from the display. The ESP32 is a different world from UEFI — we have an RTOS, a proper SDK, and a C standard library. But the constraint is tighter: 520 KB of RAM instead of gigabytes, and SPI buses instead of memory-mapped framebuffers.

The survival workstation is about to get a pocket-sized deployment device.


Next: Chapter 28: Setting Up the ESP32


This site uses Just the Docs, a documentation theme for Jekyll.