π¦ Part of dot-ll-collection
β οΈ This project is under active development. The documentation is growing along the project as it's a work-in-progress.
A lightweight, bare-metal GUI library for embedded systems. Draws visual elements directly on a GC9A01 round LCD (240Γ240) and renders PNG images streamed from an SD card β all with minimal memory footprint, no OS, no heap allocations in the critical path.
Currently supports RP2040 (Raspberry Pi Pico), ESP32, and a Simulator (native desktop via SDL2) from a single codebase.
- π¨ Direct LCD rendering β pixels, lines, circles, rectangles, and text written straight to the display without a framebuffer
- πΌοΈ PNG from SD card β PNG files decoded on-the-fly from an SD card and streamed directly to the LCD, row by row
- π£― Minimalist QR Code model 2 generation β generator written exclusively for displaying short ASCII URLs
- π Multi-platform β same application code compiles for RP2040, ESP32, and a native desktop Simulator; platform differences are fully encapsulated in the HAL layer
- π₯οΈ Desktop simulator β renders the LCD output in an SDL2 window for rapid iteration without hardware
- β‘ Bare-metal β no RTOS, no dynamic memory allocator required, runs on the metal
- π¦ Self-contained β zlib, libpng, and FatFS are bundled as submodules; no package manager needed
- π§ CMake-native β integrates cleanly as a CMake component into any parent project
| Platform | RP2040 | ESP32 | Simulator |
|---|---|---|---|
| Pinout | ![]() |
![]() |
N/A (desktop) |
| Device | RP2040-LCD-1.28 | ESP32-S3-LCD-1.28 | Any Linux/WSL desktop |
| Toolchain | arm-none-eabi-gcc + Pico SDK | xtensa-esp32-elf-gcc + ESP-IDF | Host gcc + SDL2 |
| Output | .uf2 |
.bin |
Native ELF executable |
- One of the target devices:
- MicroSD to SD card adapter (for easy soldering)
- MicroSD card
- Some nice wires (30 AWG recommended)
- 2 x 10K resistors (for pull-up)
- 1 x 10uF capacitor (for decoupling)
- (Optional) 2 x 1.27mm 2x20Pin header (for connecting SD to target device)
The below table shows connections in a way that you can create a header connector/shield board to swap between the target devices.
β οΈ Connect it to the RP2040 or to the ESP32. You're NOT supposed to wire RP2040 and ESP32 together.
| RP2040 | ESP32 | R1(10K) | R2(10K) | C1(10uF) | SDCARD | LCD* |
|---|---|---|---|---|---|---|
| GP0 (MISO) | GP46 (MISO) | MISO | pin7 (DAT0/MISO) | |||
| GP1 (CS) | GP45 (CS) | CS | pin1 (DAT3/CS) | |||
| GP2 (SCK) | GP42 (SCK) | pin5 (CLK/SCK) | ||||
| GP3 (MOSI) | GP41 (MOSI) | pin2 (CMD/MOSI) | ||||
| GP5 | GP39 | pin3 (VSS1/GND) | ||||
| GND | GND | GND | pin6 (VSS2/GND) | GND | ||
| 3V3 | 3V3 | 3V3 | 3V3 | 3V3 | pin4 (VDD/3V3) | VCC (3V3) |
| GP8 | GP8 | DC | ||||
| GP9 | GP9 | CS | ||||
| GP10 | GP10 | CLK | ||||
| GP11 | GP11 | DIN (MOSI) | ||||
| GP12 | GP12 | RST | ||||
| GP25 | GP40 | BL |
* The LCD connection is internally made on these devices, GP pins just for reference.
The GC9A01A LCD driver datasheet is versioned in Documentation/ in two complementary forms:
| File | Role |
|---|---|
Documentation/GC9A01A.md |
Primary / quick data source. Markdown conversion (mostly used for quick ai insights) β search here first for registers, command opcodes, parameter bit fields, defaults, memory layout and timings. Section 6 (commands) is a faithful reproduction with bit-layout tables and verbatim Description/Restriction. |
Documentation/GC9A01A.pdf |
Secondary source for disambiguation. The original 192-page PDF β consult it to resolve any doubt, and for graphical waveform/timing diagrams (not extracted into the markdown). |
Figures and diagrams are described in text in the markdown, not embedded as images. When precision matters, the PDF is the ground truth.
- Host: Linux or Windows 10 PC with WSL 2 (Windows Subsystem Linux) - Ubuntu is recommended β code edited on Windows, compiled inside WSL
- IDE: Kiro or VSCode
- RP2040: arm-none-eabi-gcc, Pico SDK (installed via
Setup: RP2040 toolchain) - ESP32: ESP-IDF β₯ 5.x, xtensa toolchain, espflash (installed via
Setup: ESP32 toolchain) - Simulator: libsdl2-dev, gdb, dosfstools, mtools (installed via
Setup: Simulator toolchain) - CMake β₯ 3.16
The preferred IDE is Kiro, which provides agent-assisted development with project-aware context via
AGENTS.md. The project also works fully with VS Code β all tasks and settings are configured in.vscode/.
gui.ll/
βββ src/
β βββ Sample.c # Entry point β app_entry()
β βββ lib/
β β βββ Helper/
β β β βββ Trigonometry.c # Integer Q16.16 cos/sin LUT (used by curved text)
β β βββ Platform/ESP32/ # ESP-IDF component registration only
β β βββ Driver/GC9A01/ # LCD driver
β β βββ LCD/1in28/ # Display layer: LCDSetup (init) + LCDRenderer (blit)
β β βββ LCD/Simulator/ # SDL2 display layer: same API, renders to window
β β βββ GUI/ # Canvas / drawing primitives (Canvas.c/.h)
β β βββ Fonts/ # Bitmap font data
β βββ Dependency/
β βββ hal.ll/ # GPIO, SPI, PWM, timing, board pinout (downloaded)
β βββ fs.ll/ # SD card file I/O + FatFS (downloaded)
β βββ libpng/ # libpng
β βββ zlib/ # zlib (libpng dependency)
βββ Documentation/
β βββ GC9A01A.pdf # LCD driver datasheet (original PDF)
β βββ GC9A01A.md # Datasheet as markdown (primary data source)
β βββ Image/ # Pinout reference images
βββ Toolchain/
βββ RP2040/Setup.sh
βββ ESP32/Setup.sh
βββ Simulator/Setup.sh
Hardware access goes through hal.ll, which owns GPIO, SPI, PWM, UART, timing, the RTC and the board pinout. The drawing code calls abstract functions β DigitalWrite, SPIWriteByte, Delay, etc. β declared in hal.ll's per-platform HAL.h; its contract puts the active platform folder on the include path at build time, so #include "HAL.h" resolves to the correct implementation with no #ifdef scattered through the code.
What stays platform-specific here is the display layer, selected by folder rather than by preprocessor: src/lib/LCD/Simulator on the desktop, src/lib/LCD/1in28 on both hardware targets.
void app_entry(void) { /* your application code */ }
#ifdef ESP_PLATFORM
void app_main(void) { app_entry(); } // ESP-IDF entry
#else
int main(void) { app_entry(); return 0; } // RP2040 / standard entry
#endifThe Canvas module writes directly to the LCD without requiring a full framebuffer in RAM.
// Canvas setup
CanvasNewImage(buffer, 240, 240, ROTATE_0);
CanvasClear(BLACK);
// Geometry
CanvasDrawPoint(120, 120, RED, PIXEL_SIZE_2X2, PIXEL_FILL_STYLE_AROUND);
CanvasDrawLine(0, 0, 240, 240, WHITE, PIXEL_SIZE_1X1, LINE_STYLE_SOLID);
CanvasDrawRectangle(10, 10, 100, 80, BLUE, PIXEL_SIZE_1X1, DRAW_FILL_STYLE_EMPTY);
CanvasDrawCircle(120, 120, 60, GREEN, PIXEL_SIZE_1X1, DRAW_FILL_STYLE_FULL);
// Text
CanvasDrawText(20, 20, "Hello!", &Font16, WHITE, BLACK);
CanvasDrawNum(20, 50, 3.14, &Font12, 2, YELLOW, BLACK);
CanvasDrawTime(10, 200, &dateTime, &Font12, WHITE, BLACK);
// Curved text β placed on a circle (radius, startAngle in whole degrees, 0Β° = 3 o'clock,
// increasing clockwise) with the glyph kept tangent to the border. A single character or a
// whole string (which advances the angle per glyph automatically):
CanvasDrawCurvedChar('A', 120, 120, 100, 270, TEXT_ORIENTATION_INWARDS, &Font20, WHITE, TRANSPARENT);
CanvasDrawCurvedText("Hello, World!", 120, 120, 100, 250, TEXT_ORIENTATION_OUTWARDS, &Font20, WHITE, TRANSPARENT);Available colors: WHITE, BLACK, RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA, GRAY, and more.
Pixel sizes: PIXEL_SIZE_1X1 through PIXEL_SIZE_8X8.
Line styles: LINE_STYLE_SOLID, LINE_STYLE_DOTTED.
Fill modes: DRAW_FILL_STYLE_EMPTY, DRAW_FILL_STYLE_FULL.
Curved-text orientation: TEXT_ORIENTATION_INWARDS (advances clockwise) or TEXT_ORIENTATION_OUTWARDS (glyph flipped, advances counter-clockwise) β ideal for labels following the round display's rim. Pass TRANSPARENT as the background to draw only the glyph pixels over an existing image.
PNG files are decoded with libpng into the canvas RAM buffer, then blitted to the display in a single transfer β so the image appears instantly without visible row-by-row scanning. Memory usage is bounded to a single decoded row at a time; the full image never needs to fit in RAM beyond the canvas texture.
UINT8 texture[240 * 240 * 2]; // RGB565 framebuffer
FIL file;
CanvasNewImage(texture, 240, 240, ROTATE_0);
CanvasSetScale(65);
if (MountSdCard()
&& SelectActiveDrive()
&& OpenFile(&file, "image.png")) {
CanvasDrawPng(&file);
CloseFile(&file);
}
UnMountSdCard();
// Optionally draw text/shapes over the decoded image before blitting:
CanvasDrawText(20, 20, "Hello!", &Font16, WHITE, TRANSPARENT);
LCDRenderTexture(texture);Minimalist QR Code Model 2 generator, designed exclusively for displaying short ASCII URLs. Supports versions 4 and 5, error correction level M, Byte mode, and automatic selection of the best of the 8 available masks. The output is a 1-bit-per-module bitmap, completely independent of the rendering layer.
- ASCII input only, with a maximum length of 80 bytes.
- QR Code versions 4 and 5 only.
- Byte mode only.
- Error correction level M only.
- QR mask is selected automatically; manual mask selection is not supported.
- Maximum QR matrix size: 37 Γ 37 modules.
- Maximum output bitmap size: 172 bytes.
- No dynamic memory allocation (
malloc). - No external dependencies.
- No large precomputed lookup tables.
| URL length | QR version | Matrix |
|---|---|---|
| 0β62 bytes | Version 4 | 33 Γ 33 |
| 63β80 bytes | Version 5 | 37 Γ 37 |
The output bitmap requires 172 bytes in the worst case (37 Γ 37 modules, 1 bit per module). QR generation uses approximately 800 bytes of temporary stack memory and does not use dynamic memory allocation.
The CMakeLists.txt at the root is a functional standalone example. To integrate gui.ll as a component in your own CMake project, include the platform-specific fragments directly:
set(PLATFORM_NAME "RP2040") # or "ESP32"
# RP2040 only β include before add_executable
include(path/to/gui.ll/src/lib/Platform/RP2040/PreExecutable.cmake)
add_executable(my_app src/main.c)
# RP2040 only β include after add_executable
include(path/to/gui.ll/src/lib/Platform/RP2040/PostExecutable.cmake)For ESP32, set EXTRA_COMPONENT_DIRS to point at src/lib/Platform/ESP32 before calling project():
set(EXTRA_COMPONENT_DIRS "path/to/gui.ll/src/lib/Platform/ESP32")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(my_app)Your application entry point mirrors Sample.c β implement app_entry() and let the platform macro route main or app_main automatically.
git clone --recurse-submodules --shallow-submodules https://github.com/juliannojungle/gui.ll.gitThis single command clones the repository and all bundled dependencies (FatFS, libpng, zlib) in one step. No separate dependency installation needed.
Before building for the first time, install the required toolchain for your target platform. The recommended way is via the built-in tasks.
In Kiro or VS Code β open the Command Palette (Ctrl+Shift+P) and select Tasks: Run Task, then choose:
Setup: RP2040 toolchain in WSLβ installs arm-none-eabi-gcc and Pico SDKSetup: ESP32 toolchain in WSLβ installs ESP-IDF, xtensa toolchain, Rust, and espflashSetup: Simulator toolchain in WSL2β installs libsdl2-dev and gdb
These scripts are idempotent β safe to run again if you need to repair an installation.
Once the toolchain is ready, build using the tasks:
In Kiro or VS Code β open the Command Palette (Ctrl+Shift+P) β Tasks: Run Task:
| Task | Description |
|---|---|
Build: Full RP2040 |
Clean CMake configure + make for RP2040 |
Build: Full ESP32 |
Clean idf.py build for ESP32 |
Build: Simulator Full |
Clean CMake configure + make for Simulator |
Build: Simulator Incremental |
Reuses build dir if Simulator; else clean full |
Build: Incremental |
Auto-detects platform, runs make or ninja |
Copy UF2 to Windows |
Copies .uf2 to C:\temp for drag-and-drop flashing |
Alternatively, trigger the default build task directly with Ctrl+Shift+B (runs Build: Incremental).
See LICENSE.
@juliannojungle, 2026

