···11+A fork of [pulp-os](https://github.com/hansmrtn/pulp-os). The creator has VERY much so done the hard parts. This is just a fork playing around and adding custom apps to it
22+33+44+pulp-os -- e-reader firmware for the XTEink X4
55+66+bare-metal e-reader operating system for the XTEink X4 board
77+(ESP32-C3 + SSD1677 e-paper). written in Rust. no std, no
88+framebuffer, no dyn dispatch. async runtime via Embassy on
99+esp-rtos.
1010+1111+hardware
1212+ mcu ESP32-C3, single-core RISC-V RV32IMC, 160 MHz
1313+ ram 400 KB DRAM; ~172 KB heap (108 KB main + 64 KB reclaimed)
1414+ display 800x480 SSD1677 mono e-paper, DMA-backed SPI, portrait
1515+ storage microSD over shared SPI bus (400 kHz probe, 20 MHz run)
1616+ input 2 ADC ladders (GPIO1, GPIO2) + power button (GPIO3 IRQ)
1717+ battery li-ion via ADC, 100K/100K divider on GPIO0
1818+1919+ pin map:
2020+ GPIO0 battery ADC GPIO6 EPD BUSY
2121+ GPIO1 button row 1 ADC GPIO7 SPI MISO
2222+ GPIO2 button row 2 ADC GPIO8 SPI SCK
2323+ GPIO3 power button GPIO10 SPI MOSI
2424+ GPIO4 EPD DC GPIO12 SD CS (raw register GPIO)
2525+ GPIO5 EPD RST GPIO21 EPD CS
2626+2727+ EPD and SD share SPI2, arbitrated by CriticalSectionDevice.
2828+2929+building
3030+ requires stable Rust >= 1.88 and the riscv32imc-unknown-none-elf
3131+ target. rust-toolchain.toml handles both automatically.
3232+3333+ cargo build --release
3434+ espflash flash --monitor --chip esp32c3 target/...
3535+3636+ or:
3737+3838+ cargo run --release
3939+4040+ local path dependencies (sibling dirs):
4141+ embedded-sdmmc async FAT filesystem over SD/SPI (local fork)
4242+ smol-epub no_std epub/zip/html/image processing
4343+4444+features
4545+ txt reader lazy page-indexed, read-ahead prefetch,
4646+ proportional font wrapping
4747+ epub reader ZIP/OPF/HTML-strip pipeline, chapter cache on SD,
4848+ proportional fonts with bold/italic/heading styles,
4949+ inline PNG/JPEG (1-bit Floyd-Steinberg dithered),
5050+ TOC browser (NCX or inline), chapter navigation
5151+ file browser paginated SD listing, background EPUB title
5252+ scanner (resolves titles from OPF metadata)
5353+ bookmarks 16-slot LRU in RAM, flushed to SD every 30 s;
5454+ home screen bookmarks browser sorted by recency
5555+ wifi upload HTTP file upload + mDNS (pulp.local);
5656+ drag-and-drop web UI with delete support
5757+ fonts regular/bold/italic TTFs rasterised at build time
5858+ via fontdue; five sizes, book and UI independently
5959+ configurable
6060+ display partial DU refresh (~400 ms page turn), periodic
6161+ full GC refresh (configurable interval)
6262+ quick menu per-app actions + screen refresh + go home,
6363+ triggered by power button
6464+ settings sleep timeout, ghost clear interval,
6565+ book font size, UI font size, wifi credentials
6666+ sleep idle timeout + power long-press; EPD deep sleep
6767+ (~3 uA) + ESP32-C3 deep sleep (~5 uA); GPIO3 wake
6868+6969+controls
7070+ Prev / Next scroll or turn page
7171+ PrevJump / NextJump page skip (files: full page; reader: chapter)
7272+ Select open item
7373+ Back go back; long-press goes home
7474+ Power (short) open quick-action menu
7575+ Power (long) deep sleep
7676+7777+runtime
7878+ embassy async executor on esp-rtos. five concurrent tasks:
7979+8080+ main event loop: input dispatch, app work, rendering
8181+ input_task 10 ms ADC poll, debounce, battery read (30 s)
8282+ housekeeping status bar (5 s), SD check (30 s), bookmark flush (30 s)
8383+ idle_timeout configurable idle timer, signals deep sleep
8484+ worker_task background CPU-heavy work (HTML strip, image decode)
8585+8686+ CPU sleeps (WFI) whenever all tasks are waiting.
8787+8888+directory layout
8989+ kernel/ pulp-kernel workspace crate (zero app imports)
9090+ src/
9191+ lib.rs crate root, re-exports
9292+ kernel/
9393+ mod.rs Kernel struct, resource ownership
9494+ app.rs App trait, AppLayer trait, AppIdType,
9595+ Transition, Redraw, AppContext, Launcher,
9696+ QuickAction protocol types
9797+ console.rs boot console (FONT_6X13, no fontdue)
9898+ scheduler.rs main loop, render pipeline, sleep
9999+ handle.rs KernelHandle (app I/O API)
100100+ tasks.rs spawned embassy tasks
101101+ work_queue.rs background work with generation cancellation
102102+ bookmarks.rs LRU bookmark cache
103103+ config.rs settings parser/writer
104104+ dir_cache.rs sorted directory cache with title resolution
105105+ wake.rs uptime helper (embassy monotonic clock)
106106+ board/ board support (pin map, SPI wiring, button layout)
107107+ mod.rs Board::init, peripheral splitting
108108+ action.rs ActionEvent (semantic button actions)
109109+ battery.rs voltage-to-percentage mapping
110110+ button.rs physical button enum, ButtonMapper
111111+ layout.rs button-to-action table
112112+ raw_gpio.rs register-level GPIO for SD CS
113113+ drivers/ hardware drivers
114114+ mod.rs driver re-exports
115115+ ssd1677.rs EPD display driver, 3-phase partial refresh
116116+ strip.rs 4 KB strip buffer, rotation, glyph blitting
117117+ sdcard.rs SD card init and SPI wiring
118118+ storage.rs FAT filesystem ops, poll_once, with_fs! macros
119119+ input.rs ADC button polling, debounce, repeat
120120+ battery.rs ADC battery voltage sampling
121121+ ui/ font-independent primitives
122122+ mod.rs Region, Alignment, stack measurement
123123+ stack_fmt.rs no_alloc formatting (StackFmt)
124124+ statusbar.rs status bar rendering
125125+ widget.rs widget trait and helpers
126126+127127+ src/ distro / app layer
128128+ bin/main.rs entry point, hardware init, boot
129129+ lib.rs crate root
130130+ ui/
131131+ mod.rs app-side UI helpers
132132+ fonts/
133133+ mod.rs font size tiers, FontSet lookups
134134+ bitmap.rs build-time bitmap font data
135135+ apps/
136136+ mod.rs AppId enum, type aliases binding kernel generics
137137+ manager.rs AppLayer impl, with_app! dispatch, lifecycle
138138+ home.rs launcher menu + bookmarks browser
139139+ files.rs SD file browser + background title scanner
140140+ settings.rs settings UI
141141+ upload.rs wifi upload server
142142+ reader/
143143+ mod.rs state machine, lifecycle, draw, quick actions
144144+ paging.rs text wrapping, page navigation, load/prefetch
145145+ epub_pipeline.rs ZIP/OPF parsing, chapter caching, background strip
146146+ images.rs image detection, decode dispatch, dithering
147147+ widgets/
148148+ mod.rs widget re-exports
149149+ bitmap_label.rs proportional text label (uses fonts/)
150150+ quick_menu.rs power-button overlay menu
151151+ button_feedback.rs button press visual feedback
152152+153153+ build.rs fontdue TTF rasterisation at compile time
154154+ assets/fonts/ TTF files (regular, bold, italic)
155155+ assets/upload.html web UI for wifi upload mode
156156+157157+design notes
158158+ kernel / app split. the kernel crate (kernel/) has zero imports
159159+ from apps/ or fonts/. the scheduler is generic over AppLayer;
160160+ it never names a concrete app. AppId is defined by the distro,
161161+ not the kernel -- the kernel only knows AppIdType::HOME.
162162+163163+ no dyn dispatch. with_app!() macro matches AppId, expands to
164164+ concrete calls per app struct. all monomorphised; no vtable,
165165+ no Box.
166166+167167+ strip rendering. 12 x 40-row strips (4 KB each) instead of a
168168+ 48 KB framebuffer. draw callback fires per strip during SPI
169169+ transfer. blit_1bpp_270 fast path walks physical memory linearly
170170+ for the portrait rotation. windowed mode for partial refresh.
171171+172172+ 3-phase partial refresh. write BW RAM, kick DU waveform, collect
173173+ input during ~400 ms refresh, then sync RED RAM. phase3 skipped
174174+ during rapid navigation (RED marked stale; next partial uses
175175+ inv_red recovery). full GC promoted after configurable number
176176+ of partials to clear ghosting.
177177+178178+ SPI bus sharing. EPD and SD share one SPI2 bus. all SD I/O
179179+ completes before any EPD render pass. busy_wait_with_input()
180180+ collects only input events, no background work. violating the
181181+ ordering panics (RefCell double-borrow), never corrupts.
182182+183183+ poll_once. embedded-sdmmc's async API wraps blocking SPI+DMA
184184+ that never pends. poll_once drives every future to completion
185185+ in a single poll, avoiding task spawn overhead.
186186+187187+ KernelHandle. apps never touch hardware. KernelHandle borrows
188188+ the Kernel for one lifecycle method and exposes file I/O, dir
189189+ cache, bookmarks. every async method does sync work then
190190+ yield_now() for executor fairness.
191191+192192+ smol-epub sync bridge. smol-epub I/O uses closures, not async.
193193+ with_sync_reader() provides a scoped closure that completes
194194+ all storage access before returning -- no borrows across await.
195195+196196+ heavy statics. large structs (ReaderApp ~28 KB, DirCache ~10 KB,
197197+ StripBuffer ~4 KB) live in ConstStaticCell / StaticCell so the
198198+ async future stays ~200 B.
199199+200200+ nav stack. Launcher<Id> holds a 4-deep stack. transitions
201201+ (Push/Pop/Replace/Home) drive on_suspend / on_enter / on_resume
202202+ lifecycle. Push degrades to Replace when stack is full.
203203+204204+ dirty-region tracking. apps call ctx.mark_dirty(region); regions
205205+ are unioned per frame. partial DU or full GC issued accordingly.
206206+207207+ work queue. dedicated embassy task for CPU-heavy work (HTML strip,
208208+ image decode). generation-based cancellation: bump a counter and
209209+ drain channels; worker checks generation before and after
210210+ processing. channel capacity 1 for back-pressure.
211211+212212+ input. ADC ladders at 100 Hz, 4-sample oversampling, 15 ms
213213+ debounce, 1 s long-press, 150 ms repeat. ButtonMapper translates
214214+ physical buttons to semantic actions. apps never see hardware.
215215+216216+ fonts. build.rs rasterises TTFs via fontdue into 1-bit bitmaps
217217+ at five sizes (xsmall through xlarge), three styles (regular,
218218+ bold, italic). ASCII direct-indexed, extended unicode binary-
219219+ searched. book and UI sizes independently hot-swappable.
220220+221221+ boot console. kernel renders text during hardware init using
222222+ built-in FONT_6X13 mono font. works with zero fontdue, zero
223223+ TTFs. if the SD card is missing, user still sees boot progress.
224224+225225+ bookmarks. 16-slot LRU, RAM-resident, binary format on SD.
226226+ flushed every 30 s if dirty, plus on sleep. lookup by fnv1a
227227+ hash + case-insensitive name comparison.
228228+229229+ settings. key=value text in _PULP/SETTINGS.TXT. parsed at boot,
230230+ saved on change. font size changes propagate to all apps.
231231+232232+ wifi upload. bypasses normal dispatch. HTTP server on port 80,
233233+ mDNS on 5353 (pulp.local). multipart upload with 8.3 filename
234234+ sanitisation. radio torn down before returning to app loop.
235235+236236+ memory budget. ~172 KB heap for epub text and image decode
237237+ (alloc::vec). everything else is static or stack. ~56 KB stack,
238238+ painted 0xDEAD_BEEF at boot, high-water mark logged every 5 s.
239239+240240+ forkable kernel. designed to be extracted as a standalone crate.
241241+ a fork defines its own AppId, implements AppLayer, brings its
242242+ own fonts and apps, writes a main.rs. the kernel provides
243243+ drivers, scheduling, storage, bookmarks, config, and a working
244244+ EPD with mono boot console.
245245+246246+license
247247+ MIT