Files
nrfwatch/developmentlog.md
T
2026-05-01 11:53:56 +02:00

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 0xCF control byte (clock + analog + temperature, no LUT reload)

Framebuffer renderer

  • 200x200 monochrome framebuffer, 5000 bytes
  • Y-flip in wf_set_pixel compensates for SSD1680 data entry mode 0x01 (Y decrement): ram_row = (199 - y)
  • wf_draw_glyph draws 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_clear fixed by using static buffer instead of stack allocation

Fonts

  • tools/gen_fonts.py generates src/fonts.h and src/fonts.c programmatically
  • 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 hardware
  • sim/src/epd_sim.c implements the same API as src/epd_drv.c, dumps framebuffer as hex over stdout with !F!/!E! framing
  • sim/view.py reads 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-reload for live display
  • Sim runs on Zephyr native_sim target (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 NixOS
    • nix run .#test - builds and runs ztest suite on QEMU x86
    • nix 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.2 interpreter
  • 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_glyph had black = !(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 (continue on 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 proper nrfwarch,epd-ctrl.yaml binding.

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)