nrfwarch
Smartwatch firmware for the Seeed Xiao nRF54L15 Sense with a GDEY0154D61LT 1.54" e-ink display.
Built on Zephyr RTOS v4.0. Targets the xiao_nrf54l15/nrf54l15/cpuapp board. Includes a display simulator that renders the watchface on your desktop using feh.
Hardware
- MCU: Nordic nRF54L15 (ARM Cortex-M33, 128KB RAM, 1MB Flash)
- Board: Seeed Xiao nRF54L15 Sense (includes LSM6DSO IMU, PDM mic)
- Display: Good Display GDEY0154D61LT 1.54" e-ink, 200x200px, SSD1680 controller, SPI
E-ink wiring
| Signal | Xiao Pin | Function |
|---|---|---|
| MOSI | D10 | SPI data out |
| CLK | D8 | SPI clock |
| CS | D1 | Chip select |
| DC | D3 | Data/command |
| RST | D0 | Hardware reset |
| BUSY | D2 | Busy status |
Quick start
1. Enter dev shell
nix develop
2. Set up Zephyr SDK (first time only)
Downloads Zephyr v4.0 workspace and SDK 0.17.0, patches binaries for NixOS:
nix run .#setup-sdk
3. Run the display simulator
Builds and runs the watchface simulator. Time advances 1 minute per second. A feh window opens showing the e-ink output in real time. The simulator uses the same rendering code that runs on the real hardware.
nix run .#sim
This pipes the native_sim binary output through sim/view.py, which converts the framebuffer to PBM and auto-launches feh with --auto-reload. The output file is /tmp/nrfwarch.pbm.
4. Run tests
17 ztest cases covering framebuffer operations, font data, and rendering:
nix run .#test
5. Build for hardware
Open the project in nRF Connect for VS Code and add the application with board target xiao_nrf54l15/nrf54l15/cpuapp. Or build from the command line:
west build -b xiao_nrf54l15/nrf54l15/cpuapp .
Display simulation
The simulator lets you develop the watchface visually without any hardware. It runs the actual Zephyr kernel and the real watchface.c / fonts.c code -- the only difference is the EPD driver writes to a file instead of SPI.
How it works:
sim/src/epd_sim.cimplements the same API assrc/epd_drv.c, but instead of sending SPI commands it dumps the raw framebuffer over stdout as hex, framed with!F!/!E!markerssim/view.pyreads the hex frames from stdin, inverts the polarity (framebuffer uses 1=white, PBM uses 1=black), and writes a PBM image to/tmp/nrfwarch.pbm- feh watches the file with
--auto-reloadand updates the display window
The simulated time starts at 12:00 and advances 1 minute every 0.8 seconds. Battery decreases by 2% per simulated hour. A full 24-hour cycle runs in about 19 minutes.
Manual usage
If you want to run the sim without feh or pipe the output yourself:
# Run the binary directly (outputs hex frames to stdout)
./build-sim/zephyr/zephyr.exe
# Convert a single frame to PBM manually
./build-sim/zephyr/zephyr.exe | python3 sim/view.py
# Just view the current frame
feh /tmp/nrfwarch.pbm
# Convert to PNG for sharing
pnmtopng /tmp/nrfwarch.pbm > watchface.png
Iterating on the watchface
- Edit
src/watchface.cor modify digit shapes intools/gen_fonts.py - If fonts changed:
python3 tools/gen_fonts.py - Rebuild sim:
cmake -B build-sim -GNinja -DBOARD=qemu_x86 -S sim && ninja -C build-sim - Run:
./build-sim/zephyr/zephyr.exe | python3 sim/view.py - feh auto-reloads when the PBM changes
Project layout
src/
main.c Entry point, minute-by-minute watch face loop
epd_drv.c/h SSD1680 e-ink driver (SPI, full/partial refresh, deep sleep)
watchface.c/h Framebuffer renderer (200x200 monochrome)
fonts.c/h Bitmap fonts (24x40 digits, 8x40 colon)
sim/
src/main.c Simulated main loop (accelerated time, no hardware)
src/epd_sim.c EPD mock - dumps framebuffer as hex over stdout
view.py Reads hex frames, writes PBM, launches feh
CMakeLists.txt Sim build config
prj.conf Sim Kconfig
boards/
xiao_nrf54l15_nrf54l15_cpuapp.overlay Devicetree overlay (SPI, GPIO, I2C, BT)
xiao_nrf54l15_nrf54l15_cpuapp.conf Board-specific Kconfig
dts/bindings/
nrfwarch,epd-ctrl.yaml Custom binding for EPD GPIO control node
tests/
src/main.c 17 ztest cases (framebuffer, pixels, fonts, rendering)
prj.conf Test build config
prj.conf Main application config
CMakeLists.txt Build definition
flake.nix Nix flake (dev shell, setup-sdk, test, sim)
tools/
gen_fonts.py Font generator (run to regenerate src/fonts.*)
Flake commands
| Command | Description |
|---|---|
nix develop |
Enter dev shell with all tools |
nix run .#setup-sdk |
Download and set up Zephyr SDK + workspace |
nix run .#test |
Build and run ztest suite on QEMU |
nix run .#sim |
Run display simulator with feh viewer |
What works
- SSD1680 e-ink driver with full refresh and partial refresh
- Framebuffer-based watch face with time rendering (HH:MM) and battery indicator
- Deep sleep / wake for the display
- Display simulator with real-time feh output
- 17 passing tests covering framebuffer operations, font data integrity, and rendering
In progress / planned
- BLE Current Time Service (CTS) client for phone time sync
- GRTC-based RTC for persistent timekeeping across deep sleep
- Battery monitoring via ADC (channel 7, VBAT regulator)
- LSM6DSO IMU wrist-tilt wake detection
- System-off power management with regulator control
- Partial refresh for minute updates (avoid full refresh flicker)
- BLE Battery Service (BAS) and notifications
References
- Seeed Xiao nRF54L15 Wiki
- Zephyr RTOS Documentation
- Zephyr QEMU x86 Board
- Zephyr ztest Framework
- SSD1680 datasheet (GDEY0154D61LT)
- Sample repo: Toastee0/Seeed-Xiao-nRF54L15
- E-ink driver reference: jcontrerasf/E-ink-display-Zephyr
License
Apache 2.0