4.8 KiB
4.8 KiB
Development Log
2026-04-25: Initial Setup
Project: nrfwarch
Smartwatch firmware for Seeed Xiao nRF54L15 Sense with GDEY0154D61LT 1.54" e-ink display (SSD1680 controller).
Board setup
- Ported SSD1680 e-ink driver from jcontrerasf/E-ink-display-Zephyr (nRF52840/PlatformIO) to Xiao nRF54L15 with modern Zephyr APIs
- Created custom devicetree binding (
nrfwarch,epd-ctrl) for EPD GPIO pins - VCOM command (0x2C, value 0xA5) added to init sequence
- Partial update uses
0xCFcontrol byte (clock + analog + temperature, no LUT reload)
Framebuffer renderer
- 200x200 monochrome framebuffer, 5000 bytes
- Y-flip in
wf_set_pixelcompensates for SSD1680 data entry mode 0x01 (Y decrement):ram_row = (199 - y) wf_draw_glyphdraws a glyph bitmap onto the framebuffer at (dst_x, dst_y). Font data uses bit=1 for foreground (draw black), bit=0 for background (skip). Background pixels are NOT written so the framebuffer underneath is preserved.- Stack overflow in
epd_clearfixed by using static buffer instead of stack allocation
Fonts
tools/gen_fonts.pygeneratessrc/fonts.handsrc/fonts.cprogrammatically- Digits are 24x40px (120 bytes each), colon is 8x40px (40 bytes)
- Digits are drawn as stroked outlines: horizontal/vertical lines + filled rectangles
- Each digit is a seven-segment-style rendering with 2px stroke thickness and 3px margins
- To regenerate:
python3 tools/gen_fonts.py
EPD wiring (Xiao nRF54L15 Sense)
| Signal | Pin | GPIO |
|---|---|---|
| MOSI | D10 | SPI MOSI |
| CLK | D8 | SPI SCK |
| CS | D1 | xiao_d 1 |
| DC | D3 | xiao_d 3 |
| RST | D0 | xiao_d 0 |
| BUSY | D2 | xiao_d 2 |
2026-04-25: Display Simulator
What was done
sim/directory with display simulator for watchface development without hardwaresim/src/epd_sim.cimplements the same API assrc/epd_drv.c, dumps framebuffer as hex over stdout with!F!/!E!framingsim/view.pyreads hex frames from stdin, inverts polarity (framebuffer bit=1 is white, PBM bit=1 is black), writes PBM to/tmp/nrfwarch.pbm- feh auto-launches with
--auto-reloadfor live display - Sim runs on Zephyr
native_simtarget (native Linux process, not QEMU). Time advances 1 min/sec.
Sim architecture
Zephyr native_sim binary (zephyr.exe)
-> watchface.c renders to framebuffer (same code as hardware)
-> epd_sim.c dumps hex frames to stdout
-> view.py reads stdin, converts to PBM
-> feh displays the image with auto-reload
Frame protocol
!F!= frame start, then 5000 bytes as hex (32 bytes/line),!E!= frame end- view.py ignores everything outside markers (handles Zephyr log output mixed in)
2026-04-25: Flake, Tests, and Font Fixes
Nix flake
- Converted from shell.nix to flake.nix with three apps:
nix run .#setup-sdk- downloads Zephyr v4.0 + SDK 0.17.0, patches all ELF binaries for NixOSnix run .#test- builds and runs ztest suite on QEMU x86nix run .#sim- builds and runs display simulator with feh viewer
- Dev shell includes: cmake, ninja, west, qemu_full, feh, dtc, patchelf
NixOS SDK patching
- Zephyr SDK binaries are dynamically linked with
/lib64/ld-linux-x86-64.so.2interpreter - Must patchelf every ELF binary (bin/, libexec/, lib/) to point at NixOS's ld-linux
- The setup-sdk app handles this automatically
Test suite (17 tests, all passing)
- Framebuffer: clear sets all 0xFF, pixel set/get roundtrip, bounds checking, Y-flip at corners
- Fonts: digit/colon array sizes match compile-time constants, all digits contain non-trivial data
- Rendering: draw_time modifies buffer, different times produce different buffers, battery indicator renders, 0% vs 100% battery differs
- Glyph: all-ones glyph sets pixel to black
Bugs fixed this session
- Inverted fonts:
wf_draw_glyphhadblack = !(val & bit)which inverted foreground/background. Fixed to only draw when bit=1 (foreground), skip bit=0 (background). - Leading bar artifact: Background pixels were being explicitly written as white on every glyph byte, overwriting adjacent content. Fixed by skipping background pixels entirely (
continueon bit=0). - Font data mismatch: Original hand-written font data had wrong byte counts (112 bytes declared as 64). Replaced with programmatic generator.
- epd_clear stack overflow: 5000-byte array on stack blew past 4KB main stack. Changed to static buffer.
- Devicetree binding:
compatible = "simple-bus"rejected custom GPIO properties. Created propernrfwarch,epd-ctrl.yamlbinding.
Next steps
- BLE CTS client for time sync from phone
- GRTC-based RTC for persistent timekeeping across system-off
- Battery monitoring via ADC channel 7 with VBAT regulator
- LSM6DSO IMU wrist-tilt wake detection
- System-off power management with regulator control
- Partial refresh for minute updates (avoid full refresh flicker)