SolarOS 4.17.7 manual · build
Boards and hardware targets
Run board to identify your target and its capabilities. Commands and apps also depend on the firmware flavor; use pkg, apps, and jobs to check it. For attaching hardware to an existing board, start with Expansion.
Built-In Targets
The current tree includes these board targets:
| Target | PlatformIO env | Hardware | Highlights |
|---|---|---|---|
solar_term | solar_term | SolarTerm, built from the Waveshare ESP32-S3-RLCD-4.2 | Primary ST7305 reflective display target with SDMMC, CDC, UART, RTC, SHTC3, battery ADC, ES8311/ES7210 audio, expansion I2C/SPI/UART/GPIO/ADC/PWM, and runtime-routable SPI3 on GPIO1/GPIO2/GPIO3/GPIO17. SolarOS refers to this hardware configuration as SolarTerm. |
solar_writer | solar_writer | SolarWriter, built from the Elecrow CrowPanel 5.79-inch | 792x272 dual-SSD1683 display, INMP441 microphone, shared PCM5102 line output and MAX98357 speaker output, LC709203F fuel gauge, DS3231 RTC, microSD, CH340/UART console, and GPIO21 reserved for jack detect. |
freenove_esp32_s3_display_4_0 | freenove_esp32_s3_display_4_0 | Freenove ESP32-S3 Display 4.0-inch (FNK0104S) | Integrated 480x320 ST7796 display, FT6336 capacitive pointer, ES8311 speaker and microphone, four-bit SDMMC, battery ADC, native USB CDC, and UART/I2C/GPIO expansion connectors. |
qdtech_es3c28p | qdtech_es3c28p | QDtech ES3C28P 2.8-inch ESP32-S3 Touch Display | Capacitive-touch ESP32-S3R8 target with 16 MB flash, 8 MB PSRAM, a 320x240 landscape ILI9341V display, FT6336 pointer, ES8311 speaker and microphone, four-bit SDMMC, battery ADC, one WS2812B, native USB CDC, and UART/I2C/GPIO expansion connectors. |
qdtech_es3n28p | qdtech_es3n28p | QDtech ES3N28P 2.8-inch ESP32-S3 Display | Non-touch ESP32-S3R8 target with 16 MB flash, 8 MB PSRAM, a 320x240 landscape ILI9341V display, ES8311 speaker and microphone, four-bit SDMMC, battery ADC, one WS2812B, native USB CDC, and UART/I2C/GPIO expansion connectors. |
elecrow_crowpanel_esp32_s3_4_2_epaper | elecrow_crowpanel_esp32_s3_4_2_epaper | Elecrow CrowPanel ESP32-S3 4.2-inch E-paper | ESP32-S3-WROOM-1-N8R8 target with a 400x300 SSD1683 e-paper display, microSD over SDSPI, CH340C/UART console, rotary/menu/exit controls, status LED, Wi-Fi, BLE, and expansion I2C/SPI/UART/1-Wire/GPIO/ADC/PWM. |
elecrow_crowpanel_esp32_s3_5_79_epaper | elecrow_crowpanel_esp32_s3_5_79_epaper | Elecrow CrowPanel ESP32-S3 5.79-inch E-paper | ESP32-S3-WROOM-1-N8R8 target with a 792x272 dual-SSD1683 e-paper display, microSD over SDSPI, CH340C/UART console, rotary/menu/exit controls, status LED, Wi-Fi, BLE, and expansion I2C/SPI/UART/1-Wire/GPIO/ADC/PWM. |
waveshare_esp32_s3_epaper_3_97 | waveshare_esp32_s3_epaper_3_97 | Waveshare ESP32-S3-ePaper-3.97 | ESP32-S3-WROOM-1-N16R8 target with an SSD1677 e-paper display that defaults to 800x480 landscape, AXP2101 battery/charger monitoring, QMI8658 six-axis IMU, four-bit SDMMC, native USB CDC, UART, PCF85063 RTC, SHTC3 temperature/humidity sensor, ES8311 speaker/microphone audio, rotary navigation, Wi-Fi, BLE, and expansion I2C/UART. |
seeed_reterminal_sticky | seeed_reterminal_sticky | Seeed Studio reTerminal Sticky | ESP32-S3R8 target with 32 MB flash, 8 MB PSRAM, 3.97-inch 800x480 monochrome e-paper with SSD1677/SSD2677 startup detection, GT911 capacitive touch, three physical buttons (AI/Power: sleep/wake on tap, shutdown on hold; Up: arrow on tap, BLE pairing on hold; Down: arrow on tap), Wi-Fi, and BLE. Console is UART0 (CH343). |
xteink_x4_pro | xteink_x4_pro | Xteink X4 Pro | ESP32-S3 target with 16 MB flash, 8 MB OPI PSRAM, an 800x480 landscape e-paper display with cached SSD1677/UC8179/UC8279 identification, fixed-mix frontlight brightness, GT911 touch and Home pad, one-bit SDMMC, BM8563-compatible RTC, CW2017 gauge, native USB CDC, UART0, navigation buttons, Wi-Fi, and BLE. |
cl_32 | cl_32 | Pi0cket CL-32 | ESP32-S3-WROOM-1-N16R8 target with an automatically detected 384x168 ST7305 reflective LCD or SSD1685 e-paper panel, an ATmega808-backed keyboard and battery monitor, native USB CDC, UART, microSD over SDSPI, PCF85063 RTC, onboard PWM buzzer, Wi-Fi, BLE, and expansion I2C/SPI/UART/GPIO/ADC/PWM/I2S. |
odroid_go | odroid_go | Hardkernel ODROID-GO | Classic ESP32 target with ILI9341 display, SD over VSPI/SDSPI, battery ADC, ESP32 DAC speaker, buttons, ADC D-pad, status LED, display brightness, expansion I2C/SPI/UART/GPIO/PWM, and runtime GPIO4/GPIO15. |
freenove_esp32_wrover_v3 | freenove_esp32_wrover_v3 | Freenove ESP32-WROVER v3.0 (FNK0060) | Classic ESP32 target with 8 MB PSRAM, CH340/UART console, one-bit SDMMC, Wi-Fi, BLE, a GPIO0 BOOT/KEY button, and a 384x288 monochrome PAL composite display on GPIO25. |
ttgo_vga32_v14 | ttgo_vga32_v14 | LilyGO TTGO VGA32 v1.4 | ESP32-PICO-D4 desktop target with 8 MB external PSRAM, build-selectable 320x200@70Hz, 320x240@60Hz, 640x400@70Hz, or 640x480@60Hz VGA output through the onboard RGB222 resistor DAC, GPIO25 mono DAC audio, a default-attached PS/2 keyboard, v1.4 microSD wiring over HSPI, USB-UART, Wi-Fi, BLE disabled by default, and two input-only expansion GPIOs. |
esp32_s3_devkitc1_n16r8 | esp32_s3_devkitc1_n16r8 | Espressif ESP32-S3-DevKitC-1-N16R8 | Headless ESP32-S3 target with CDC, UART, Wi-Fi, BLE, a GPIO0 BOOT/KEY button, expansion I2C/SPI/UART/GPIO/ADC/PWM, graphics through attachable display targets, and no primary display or onboard sensors. |
goouuu_esp32_s3cam | goouuu_esp32_s3cam | GOOUUU ESP32-S3CAM | Headless ESP32-S3 camera target with 16 MB flash, 8 MB PSRAM, native USB CDC, CH340/UART console, one-bit SDMMC, an onboard WS2812, Wi-Fi, BLE, and fixed DVP/SCCB camera wiring. Its required driver.camera-esp32 package exposes OV2640/OV5640 detection and bounded QVGA/VGA JPEG capture through service.camera; cam-webd adds a single-client MJPEG stream and JPEG snapshots with optional authentication over the shared HTTP server. |
esp32_devkitc_v4_wrover | esp32_devkitc_v4_wrover | Espressif ESP32-DevKitC V4 with ESP32-WROVER-E | Headless classic ESP32 target with PSRAM, UART, Wi-Fi, BLE, a GPIO0 BOOT/KEY button, expansion I2C/SPI/UART/GPIO/ADC/PWM/I2S, graphics through attachable display targets, and no built-in peripherals. |
devkitc1_epaper_workbench | esp32_s3_devkitc1_n16r8 with SOLAR_OS_BOARD=devkitc1_epaper_workbench | ESP32-S3 DevKitC-1 E-paper Workbench | Manifest-generated development target with fixed CardKB, 400x300 SSD1683 display, and SDSPI storage attachments. |
t_lora_pager | t_lora_pager | LilyGO T-LoRa-Pager (SX1262) | ESP32-S3-WROOM-1-N16R8 target with a 480x222 ST7796 display, a TCA8418 4x10 matrix keyboard, an SX1262 LoRa radio, PCF85063 RTC, microSD over SDSPI, ES8311 audio, a BQ27220 battery gauge, a BQ25896 charger, a rotary encoder, an XL9555 I2C GPIO controller for integrated power rails, a MIA-M10Q GNSS receiver, an ST25R3916 NFC-A reader, a BHI260AP IMU, a DRV2605 haptic controller, native USB CDC, Wi-Fi, BLE, and expansion I2C/SPI/UART/GPIO/ADC/PWM. SD power and its external pull-up start enabled; GNSS and NFC power start disabled and are enabled with gnss power on and nfc power on. Use haptic play EFFECT for DRV2605 ROM effects 1 through 117. The Pager fixes BQ25896 fast-charge current at 704 mA during attachment; inspect or explicitly change charger settings with charger. |
thinknode_m9 | thinknode_m9 | Elecrow ThinkNode M9 | ESP32-S3R8 target with 16 MB flash, 8 MB PSRAM, a 320x240 ST7789 display, a 37-key STC8H-scanned QWERTY keypad with backlight, microSD over SDSPI, PCF8563 RTC, ATGM336H or BM72A GNSS over NMEA, battery ADC, passive buzzer, CH340C-class (CH340K) USB/UART console, Wi-Fi, and BLE. The keypad I2C address identifies the board revision (0x6c is v1.0, 0x6d is v1.1). The GNSS enable rail is active-low on v1.0, which the manifest declares; a v1.1 board, active-high, sets active = 1 on gnss0. GNSS power starts disabled and is enabled with gnss power on. The keypad's Home key exits the foreground app, Back is escape, Messages/Maps/Pin are F1/F2/F3, long-press Pin is F4, and the Message key arms the one-shot compose layer that supplies tab, pipe, backslash, brackets, and the other characters the keypad cannot type directly. Composed up and down page through the scrollback and composed right is tab (see the stc8h-keyboard driver reference). The onboard LR1110 LoRa radio is driven by the lr11xx driver as radio0 on the shared SPI bus, declared at 915 MHz with a 3.3 V TCXO and the board's own DIO5/DIO6 antenna-switch table; an 868 MHz unit changes the freq binding. It has exchanged LoRa packets with an RNode on the bench; 2.4 GHz, GNSS scanning, and Wi-Fi scanning are out of scope (the board has a separate ATGM336H GNSS receiver). |
t_deck_plus | t_deck_plus | LilyGO T-Deck Plus | ESP32-S3FN16R8 target with a 320x240 ST7789 display, GT911 touch, I2C keyboard, five-way trackball, separate I2S speaker output and ES7210 microphone capture, SX1262 LoRa radio, microSD over SDSPI, battery ADC, internal GPS UART, native USB CDC, Wi-Fi, and BLE. |
waveshare_esp32_s3_sim7670g_4g | waveshare_esp32_s3_sim7670g_4g | Waveshare ESP32-S3-SIM7670G-4G V2.0 | Headless ESP32-S3R8 target with 16 MB flash, 8 MB PSRAM, CH343/UART console, SIM7670G AT, GNSS, and IPv4 PPP access over UART1, MAX17048 battery gauge, one-bit SDMMC, one WS2812B, Wi-Fi, BLE, and expansion GPIO/ADC/PWM/SPI/I2S. Set the DIP switches to CAM OFF, HUB ON, 4G OFF, USB OFF. With CAM off, the unused camera signal pins are available as expansion GPIOs and can form runtime SPI or I2S buses; GPIO15/GPIO16 remain the shared MAX17048 I2C bus and GPIO46 remains blocked as an input-only strapping pin. SolarOS drives GPIO21 active-high to control the modem VBAT rail; leave the 4G switch OFF to avoid contention and permit software power-off/reset. The modem UART belongs exclusively to PPP while connected; disconnect it before AT or GNSS use. Camera support is not included. |
waveshare_esp32_s3_sim7670g_4g_epaper | waveshare_esp32_s3_sim7670g_4g with SOLAR_OS_BOARD=waveshare_esp32_s3_sim7670g_4g_epaper | Waveshare ESP32-S3-SIM7670G-4G E-paper workbench | Expansion-export reference target with a fixed M5Stack TAB5 keyboard on the board-owned I2C bus at address 0x6d, INT on GPIO41, and a fixed primary 400x300 SSD1683 display on SPI2 using GPIO3/GPIO7 through GPIO12. GPIO40 remains available for expansion. |
Build pio run -e cl_32 for either CL-32 screen. The board-owned cl32-display adapter identifies ST7305 RLCD or SSD1685 e-paper at startup and attaches the matching concrete driver as display0. Both controllers remain available as manual expansion drivers; cl32-display is hidden from that path. The former cl32_epd target is replaced by cl_32. RLCD modules without register readback are identified through their reset/TE response on GPIO14; the SSD1685 software-reset BUSY pulse distinguishes e-paper.
T-Deck Plus registers its factory L76K or MIA-M10Q receiver as gnss0 through the nmea driver, listening at 9600 baud and trying 38400 if needed. Use gnss list and gnss fix gnss0 to inspect it. This NMEA path supplies position, UTC, and satellite information; position accuracy and PDOP are unavailable. The receiver has no independent power control. The standard T-Deck Plus has no onboard NFC reader.
The Grove connector is the T-Deck's only expansion connector. Its UART signals (ESP32 TX GPIO43, RX GPIO44) are shared with the onboard GNSS receiver. The I/O map shows this connector, not the internal display, radio, or audio wiring. gnss0 attaches automatically with origin=board, but can be detached to release uart0:
expansion detach gnss0
com uart0
Exit COM with the app-exit key before restoring the board receiver:
expansion attach gnss0
Restoring it uses the board's original bindings and retains origin=board. If an external device changed the bus rate, run uart baud uart0 9600 before restoring GNSS so detection starts at the factory rate and can try 38400. Detachment lasts until restoration or reboot, including while other board devices retry attachment. Detaching stops the GNSS reader and releases the software UART claim; the receiver remains powered and electrically connected. gnss disable gnss0 first silences NMEA output on recognized L76K/CASIC and MIA-M10Q/UBX receivers. gnss enable gnss0 restores output. These commands do not release uart0; detach afterward when another consumer needs it. Reattaching the GNSS driver restores muted NMEA output automatically. Settings are changed in receiver RAM only, without changing sentence rates or baud. Software silencing does not guarantee that the receiver's TX pin becomes high impedance. An external transmitter on the same signal still needs electrical isolation unless the receiver's hardware specifies a suitable disconnected state.
Define a custom board
New board profiles use one versioned TOML manifest in boards/manifests/<target>.toml. The manifest contains identity, MCU family, build capabilities, buses, pins, connector metadata, and automatic fixed expansion devices. CMake generates its private board configuration and C header from that file. Do not edit generated files in .pio/build/.../generated.
boards/expansion_drivers.toml is the desktop configuration catalog. It states which MCU families, packages, board capabilities, and wiring bindings each expansion driver supports. The same catalog is used by the board compiler and the configuration TUI.
All built-in boards use manifests. A compatibility loader remains for out-of-tree legacy boards/<target>.cmake profiles, but new and maintained profiles must use TOML. Runtime capability bits remain derived from the generated SOLAR_OS_BOARD_HAS_* defines.
scripts/validate_board_metadata.py checks every board during CMake configuration. It verifies board registration and identity, capability registry coverage and dependencies, pin masks against their named lists and free-pin slots, static bus protocol gates, and the GPIO/bus tables in expansion.reference.md. Run it directly after changing board metadata:
python3 scripts/validate_board_metadata.py
File Layout
The source and generated paths are:
boards/manifests/<target>.toml source board profile
boards/manifests/common/<base>.toml private shared hardware template
boards/expansion_drivers.toml selectable driver catalog
scripts/board_config.py desktop TUI
scripts/os_builder.py flavor build/flash TUI
scripts/generate_board_profile.py manifest validator/compiler
.pio/build/<env>/generated/solar_os/ generated CMake and C metadata
Public profiles can use extends to share a template from boards/manifests/common/. Templates are inherited by board ID and do not appear as build targets or selectable boards. CL-32 has one public manifest, cl_32, which selects its display driver at runtime.
A custom profile normally extends one of the neutral MCU-family base profiles:
esp32_s3_devkitc1_n16r8 ESP32-S3
esp32_devkitc_v4_wrover classic ESP32 with PSRAM
It does not need a new CMake file, C header, selector branch, or PlatformIO environment. Use the base profile's PlatformIO environment and select the custom manifest with SOLAR_OS_BOARD.
Reusable CMake fragments in boards/drivers/ remain the package adapters for concrete board services. The manifest compiler selects them from driver-catalog metadata. Driver implementation and resource ownership stay in the packaged expansion driver, so the profile does not introduce driver-specific #ifdef branches.
Creating A Custom Board With The TUI
Run the tool from the SolarOS source directory:
python3 scripts/board_config.py
The workflow is MCU-first:
- Select
ESP32-S3orClassic ESP32. - Select a compatible neutral base board.
- Enter the board identity.
- Select the permanently wired expansion drivers.
- For a display with panel profiles, select the panel and rotation. Each rotation shows the resulting display size.
- Select existing buses or create new buses, then enter addresses, pins, chip selects, ports, and optional driver parameters.
- Confirm the generated profile.
Display profiles select supported glass and its driver parameters together. For example, ST7305 offers SolarTerm's 300x400 panel and CL-32's 168x384 panel; their default rotations give 400x300 and 384x168 displays. SSD1683 offers its 400x300 panels and the 792x272 dual-controller panel. The tool writes the matching panel and rotation bindings and derives the board's logical width and height. SSD1685, SSD1677, UC8179, and UC8279 also derive geometry from their panel profile and rotation. Runtime expansion commands continue to use numeric panel and rotation bindings.
SSD1306 and SH1106 also offer named OLED panel profiles and all four rotations. These configure auxiliary displays; the primary display's geometry stays with its own driver and panel selection.
Catalog display profiles declare native width and height, a named label, the panel selector bindings, and the default rotation. Exactly one profile per driver has default = true. All profiles for a driver use the same selector keys, with distinct supported values. For example:
[[drivers.st7305.display_profiles]]
name = "168x384"
label = "CL-32 2.9-inch RLCD"
width = 168
height = 384
bindings = { panel = 1 }
rotation = 3
The tool rejects unsupported MCU/driver combinations, duplicate device names, invalid addresses and parameters, unavailable chip-select pins, and GPIO conflicts. Pins used by a fixed device or a new bus are removed from the runtime user-pin surface automatically. The output is one inherited TOML file under boards/manifests/.
Promoting A Tested Runtime Configuration
You can start from any supported board, create buses in io, attach catalog drivers in expansion, and test the real wiring before defining a custom target. Export the settled hardware configuration on the device:
expansion export /sdcard/my-hardware.toml
Then copy the file into the SolarOS source tree and pass it to the same board configurator:
python3 scripts/board_config.py --expansion-manifest my-hardware.toml
The snapshot names its original board manifest. The configurator uses that as the inherited base, promotes exported buses and devices to board-owned entries, fixes their GPIOs, and removes their peripheral controllers from the runtime pool. Display dimensions follow the exported panel and rotation bindings. If the base has no primary display, storage, audio, pointer, battery, or RTC provider and exactly one imported driver supplies that role, its board capabilities, driver fragment, and required defines are promoted too. Existing primary roles remain primary; ambiguous new primary roles are rejected instead of selected silently.
The export contains hardware topology only. It does not include startup jobs, network settings, credentials, readiness, or resource leases. Runtime manual attachments make export fail because a custom board must reference a validated driver in boards/expansion_drivers.toml. Edit or remove temporary startup commands separately after flashing the generated target.
For example, devkitc1_epaper_workbench.toml extends the S3 DevKitC profile and declares CardKB, SSD1683, and SDSPI as fixed devices. It also declares the second SPI bus used by the SD card. Build an inherited profile through its base environment:
SOLAR_OS_BOARD=devkitc1_epaper_workbench \
pio run -e esp32_s3_devkitc1_n16r8
The configurator creates a SolarOS board profile, not a new PlatformIO environment. Use the base environment shown by the configurator, and keep SOLAR_OS_BOARD set for every build or upload invocation. To build and flash the example profile in one command:
SOLAR_OS_BOARD=devkitc1_epaper_workbench \
pio run -e esp32_s3_devkitc1_n16r8 -t upload
The upload target builds first when necessary. Add --upload-port <serial-device> if PlatformIO does not select the correct port. If you select a firmware flavor explicitly, keep it on the upload invocation as well:
SOLAR_OS_BOARD=devkitc1_epaper_workbench SOLAR_OS_FLAVOR=writerdeck \
pio run -e esp32_s3_devkitc1_n16r8 -t upload
You can edit the generated TOML after leaving the TUI. Validate it before a build:
python3 scripts/generate_board_profile.py \
--manifest boards/manifests/devkitc1_epaper_workbench.toml \
--manifest-dir boards/manifests \
--drivers boards/expansion_drivers.toml \
--validate-only
Board devices attach automatically and report origin=board. By default they are fixed and cannot be detached at runtime. A manifest can set detachable = true on an individual [[devices]] entry to permit expansion detach <name> and restoration with expansion attach <name>. Restoration retains the board origin and manifest bindings. Use the established names when the device provides a primary board service:
| Hardware role | Fixed device name |
|---|---|
| Primary display | display0 |
| Removable storage | storage0 |
| Audio input/output | audio0 |
| Primary pointer | touch0 |
| Battery monitor | battery0 |
| Real-time clock | rtc0 |
| Environmental sensor | environment0 |
The profile declares hardware, not policy. Keep job autostart commands in .shell/startup. Do not keep expansion attach, expansion bus create, or session create commands there for hardware that is now fixed in the profile.
After flashing, verify the automatic devices and their services:
status
pkg
port list
expansion drivers
expansion devices
A successful manifest validation and build do not prove wiring, polarity, power control, or peripheral behavior. Test the generated profile on its physical target.
Generated Build Interface
The manifest compiler converts build.drivers into reusable CMake driver fragments and converts build.capabilities into SOLAR_OS_BOARD_HAS_* definitions. Fixed devices add their driver packages and board-service capabilities through boards/expansion_drivers.toml. Packages whose requirements are not satisfied are pruned.
The generated CMake and header files are private build artifacts. Inspect them when diagnosing a profile, but make source changes in the TOML manifest or the driver catalog.
Capability Flags
The current capability flags are:
| Flag | Meaning |
|---|---|
PSRAM | External PSRAM is present and configured. SOLAR_OS_BOARD_PSRAM_BYTES gives the expected capacity. |
SIMD | CPU vector/SIMD instructions are available for bulk data engines such as image, audio, DSP, or accelerated math paths. |
DISPLAY | A board-integrated primary display driver and boot-time display target are available. Requires GFX. |
GFX | The firmware can host drawable display targets, including targets registered later by expansion drivers. It does not imply that a display exists at boot. |
CDC | USB Serial/JTAG CDC byte-stream port cdc0. The dormant TinyUSB composite driver can add keyboard, mouse, and gamepad HID reports when explicitly enabled. |
UART | Hardware UART service is supported. Named UART buses may be board-defined or created at runtime. |
SD | SD/MMC storage and filesystem mounting. |
I2C | Hardware I2C service is supported. Named I2C buses may be board-defined or created at runtime. |
SPI | Hardware SPI service is supported. Named SPI buses may be board-defined or created at runtime. |
RTC | RTC attached to the board I2C bus. |
BATTERY | Battery voltage monitor is available. |
AUDIO | Speaker/audio-output path is available. |
AUDIO_INPUT | Microphone/audio-input path is available. Usually paired with AUDIO on codec boards. |
WIFI | Wi-Fi station/AP services. |
BLE | BLE keyboard and BLE/GATT services. |
PS2_KEYBOARD | A board-integrated PS/2 keyboard bus is available. Requires GPIO. |
GPIO | Runtime-safe GPIO service. |
ADC | Runtime-safe ADC service. |
PWM | Runtime-safe PWM service. |
EXPANSION_GPIO | Expansion connector has runtime-safe GPIO pins for external hardware. |
EXPANSION_I2C | Expansion hardware may use a static or runtime-created named I2C bus. Requires I2C. |
EXPANSION_SPI | Expansion hardware may use a static or runtime-created named SPI bus. Requires SPI. |
EXPANSION_UART | Expansion hardware may use a static or runtime-created named UART bus. Requires UART. |
EXPANSION_ADC | Expansion connector has ADC-capable runtime pins. |
EXPANSION_PWM | Expansion connector has PWM-capable runtime pins. |
EXPANSION_I2S | Board has a spare I2S controller and at least three runtime-safe output GPIOs for an external three-wire I2S device. |
KEY | Built-in board key for sleep/pairing control. |
BUTTONS | Built-in digital buttons are available for keyboard/app input. |
JOYSTICK | Built-in analog joystick axes are available as generic axis input. |
ADC_DPAD | Built-in ADC D-pad axes are available for keyboard/app input. |
STATUS_LED | Board status LED output is available. |
DISPLAY_BRIGHTNESS | Display backlight or brightness control is available. |
POINTER | Board-integrated absolute or relative pointing input. Events carry a source, pointer ID, display target, coordinates, deltas, buttons, and press/move/release action. |
STREAMING_DISPLAY | The board's primary display has a bounded-cadence raster-frame presenter suitable for animation or games. This is separate from ordinary terminal and GUI drawing. |
CAMERA | A board-integrated camera interface and fixed pin contract are present. This hardware fact does not by itself imply that a camera driver or capture service is included. |
TEMPERATURE | Temperature sensor service. |
HUMIDITY | Humidity sensor service. |
src/CMakeLists.txt validates that every enabled driver-backed capability has a matching selector, then consumes direct board sources and required packages. It does not know which concrete source files belong to ST7305, SDMMC, PCF85063, or any future driver.
Expansion capabilities are compile-time gates for external hardware packages. Use them when a package needs connector resources rather than an internal board peripheral. A driver that can use either a static expansion SPI descriptor or a runtime-routed bus may accept either expansion_spi or expansion_gpio; a driver that also requires independent control pins must still require expansion_gpio. Do not gate these packages on plain spi and gpio, because those capabilities can refer only to internal display or storage hardware. Use expansion_i2s for external I2S packages; it is intentionally stricter than expansion_gpio and prevents unusable drivers from entering a board build. The user-facing connector tables and attachment workflow live in Expansion Ports.
Generated Board Metadata
The manifest compiler emits the C-visible identity, pin, bus, connector, and fixed-device tables. The examples below describe that generated runtime contract. Do not create or edit include/boards/<target>.h for a manifest profile.
#pragma once
#define SOLAR_OS_BOARD_ID "my_board"
#define SOLAR_OS_BOARD_NAME "My SolarOS Board"
#define SOLAR_OS_BOARD_VENDOR "Vendor"
#define SOLAR_OS_BOARD_MODULE_NAME "ESP32-S3-WROOM-1-N16R8"
Add board-specific compatibility macros to [defines] only when existing code requires them. Normal identity, capability, bus, connector, pin, and device metadata comes from dedicated manifest fields.
UART example:
#include "driver/gpio.h"
#include "driver/uart.h"
#define SOLAR_OS_BOARD_UART_PORT UART_NUM_0
#define SOLAR_OS_BOARD_PIN_UART_TX GPIO_NUM_43
#define SOLAR_OS_BOARD_PIN_UART_RX GPIO_NUM_44
Key example:
#include "driver/gpio.h"
#define SOLAR_OS_BOARD_PIN_KEY GPIO_NUM_18
#define SOLAR_OS_BOARD_KEY_ACTIVE_LEVEL 0
#define SOLAR_OS_BOARD_KEY_PULL_UP 1
#define SOLAR_OS_BOARD_KEY_PULL_DOWN 0
Runtime GPIO example:
#define SOLAR_OS_BOARD_EXPANSION_GPIO_MASK ((1ULL << GPIO_NUM_1) | \
(1ULL << GPIO_NUM_2))
#define SOLAR_OS_BOARD_USER_GPIO_MASK ((1ULL << GPIO_NUM_1) | \
(1ULL << GPIO_NUM_2))
#define SOLAR_OS_BOARD_EXPANSION_GPIO_LIST "1 2"
#define SOLAR_OS_BOARD_USER_GPIO_LIST "1 2"
#define SOLAR_OS_BOARD_GPIO_SLOTS { \
{.pin = 1, .policy = SOLAR_OS_PIN_POLICY_FREE, .role = "expansion"}, \
{.pin = 2, .policy = SOLAR_OS_PIN_POLICY_FREE, .role = "expansion"}, \
}
Pin policy is separate from physical connector membership:
SOLAR_OS_PIN_POLICY_FREE: available for direct GPIO and future routed buses.SOLAR_OS_PIN_POLICY_RELEASABLE: has a default board role but may be routed after its current service releases it. The board bus descriptor remains registered; releasing the service only stops the hardware and frees its pins.SOLAR_OS_PIN_POLICY_FIXED: never available to runtime pin routing.
Keep the user GPIO list conservative. Do not mark boot strapping, flash/PSRAM, display, SD, system I2C, or key pins free. A releasable pin remains unavailable to direct GPIO until a resource-aware service explicitly takes ownership.
Describe the physical placement of every exposed connector contact separately. The io app and expansion layout [connector] render this metadata and combine GPIO contacts with the live pin policy and claim registry:
#define SOLAR_OS_BOARD_CONNECTOR_LAYOUT_TITLE "J1 / J3 pin headers"
#define SOLAR_OS_BOARD_CONNECTOR_LAYOUT_VIEW \
"component side; antenna at top, USB connectors at bottom"
#define SOLAR_OS_BOARD_CONNECTOR_LAYOUT_ROWS 22
#define SOLAR_OS_BOARD_CONNECTOR_LAYOUT_COLUMNS 2
#define SOLAR_OS_BOARD_CONNECTOR_PIN_COUNT 44
#define SOLAR_OS_BOARD_CONNECTOR_PINS { \
{.connector = "J1", .position = 1, .row = 0, .column = 0, \
.pin = -1, .kind = SOLAR_OS_CONNECTOR_PIN_POWER, .label = "3V3"}, \
{.connector = "J3", .position = 1, .row = 0, .column = 1, \
.pin = -1, .kind = SOLAR_OS_CONNECTOR_PIN_GROUND, .label = "GND"}, \
}
row and column are zero-based display coordinates. position is the connector manufacturer's pin number and need not increase in screen order. Use GPIO, power, ground, control, and NC kinds as appropriate; only GPIO entries participate in live resource lookup. Keep the view description explicit about which board side is shown and its orientation. A board without this metadata still builds, but reports that no physical connector map is available.
Static board bus example:
#include "solar_os_bus_types.h"
#define SOLAR_OS_BOARD_BUSES { \
{ \
.name = "i2c0", \
.protocol = SOLAR_OS_BUS_PROTOCOL_I2C, \
.origin = SOLAR_OS_BUS_ORIGIN_BOARD, \
.sharing = SOLAR_OS_BUS_SHARED, \
.config.i2c = { \
.port = I2C_NUM_0, \
.sda_pin = GPIO_NUM_8, \
.scl_pin = GPIO_NUM_9, \
.speed_hz = SOLAR_OS_BUS_I2C_DEFAULT_SPEED_HZ, \
}, \
}, \
{ \
.name = "spi0", \
.protocol = SOLAR_OS_BUS_PROTOCOL_SPI, \
.origin = SOLAR_OS_BUS_ORIGIN_BOARD, \
.sharing = SOLAR_OS_BUS_SHARED, \
.config.spi = { \
.host = SPI2_HOST, \
.sclk_pin = GPIO_NUM_12, \
.miso_pin = GPIO_NUM_13, \
.mosi_pin = GPIO_NUM_11, \
.max_transfer_size = 4096, \
.cs_count = 2, \
.cs = { \
{.name = "gpio10", .pin = GPIO_NUM_10}, \
{.name = "gpio5", .pin = GPIO_NUM_5}, \
}, \
}, \
}, \
}
#define SOLAR_OS_BOARD_RUNTIME_SPI_HOST_MASK (1U << SPI3_HOST)
#define SOLAR_OS_BOARD_EXPANSION_ADC_MASK ((1ULL << GPIO_NUM_1) | \
(1ULL << GPIO_NUM_2))
#define SOLAR_OS_BOARD_EXPANSION_PWM_MASK SOLAR_OS_BOARD_USER_GPIO_MASK
SOLAR_OS_BOARD_BUSES is the canonical static-bus table consumed directly by the protocol-neutral named bus registry. It includes board buses exposed to OS services and expansion management, such as the Waveshare i2c0. Bus names are unique across protocols. I2C and SPI buses accept shared logical leases; UART and 1-Wire bus instances are exclusive. Attaching an expansion device acquires a lease under the device name and detaching it releases that lease.
Protocol capabilities describe whether the service can exist; they do not imply a static bus. A board may therefore enable UART, SPI, or I2C with no matching entry in SOLAR_OS_BOARD_BUSES when it supports only runtime-created buses. Expansion capability flags authorize that runtime-facing path, while the runtime controller masks and pin policy constrain the instances that may be created. Configuration checks reject expansion capabilities without their base protocol and non-empty SPI/UART runtime masks without matching base and expansion capabilities. SOLAR_OS_BOARD_RUNTIME_I2S_PORT_MASK similarly names the controller reserved for attachable I2S hardware and must accompany EXPANSION_I2S.
The registry distinguishes immutable board descriptors from runtime-created buses. Board buses cannot be unregistered. Every protocol uses the same named bus attach/detach lifecycle. Runtime descriptors are detachable and removable; board descriptors are detachable only when all signal pins are marked releasable, and otherwise remain fixed. Detachment releases the hardware endpoint and signal pins while preserving the name and configuration. Runtime I2C uses an unregistered hardware controller plus approved SDA/SCL pins. Runtime 1-Wire uses one approved pin. Runtime SPI is supported on hosts explicitly allowed by SOLAR_OS_BOARD_RUNTIME_SPI_HOST_MASK; CS entries are bus-owned GPIO slots, while their logical chip-select use is claimed per device. Runtime bus signal pins and hardware endpoints are claimed atomically and released when an idle bus is detached or removed. Runtime UART controllers are limited by SOLAR_OS_BOARD_RUNTIME_UART_PORT_MASK. An attached UART reserves its controller and pins, while its driver starts on first lease and stops on final release.
PlatformIO Environment
Inherited custom profiles normally use their base profile's environment, as shown in the TUI procedure. Add an environment only when an in-tree standalone target needs its own PlatformIO hardware, partition, or SDK configuration:
[env:my_board]
board = esp32-s3-devkitc1-n16r8
board_build.cmake_extra_args = -DSOLAR_OS_BOARD=my_board
board is the PlatformIO hardware definition. SOLAR_OS_BOARD is the SolarOS manifest name under boards/manifests/<target>.toml.
When the PlatformIO environment name and SolarOS board profile name are the same, the CMake argument is still preferred because it removes ambiguity and makes alias environments possible.
For an inherited custom profile, build and upload through its base environment while selecting the profile explicitly:
SOLAR_OS_BOARD=my_board pio run -e esp32_s3_devkitc1_n16r8
SOLAR_OS_BOARD=my_board pio run -e esp32_s3_devkitc1_n16r8 -t upload
For a standalone target with its own [env:my_board] entry, the environment already selects the profile:
pio run -e my_board
pio run -e my_board -t upload
pio device monitor -b 115200
Classic ESP32 boards can use a board-specific SDK defaults file when the common defaults are not appropriate for the target:
[env:odroid_go]
board = odroid_esp32
custom_solaros_default_flavor = classic
board_build.cmake_extra_args = -DSOLAR_OS_BOARD=odroid_go -DSDKCONFIG_DEFAULTS=sdkconfig.defaults.odroid_go
Update Layouts
os_builder selects an update layout after board selection. This choice fixes the partition table and application-image limit; internal storage size is not a separate user setting.
| Flash | OTA updates | Serial updates |
|---|---|---|
| 16 MiB | Two 0x700000-byte (7 MiB) app slots and 0x1F0000 bytes of internal storage | One 0xE00000-byte (14 MiB) app slot and 0x1F0000 bytes of internal storage |
| 8 MiB | Two 0x3E0000-byte (3.875 MiB) app slots and 0x20000 bytes (128 KiB) of internal storage | One 0x700000-byte (7 MiB) app slot and 0xF0000 bytes (960 KiB) of internal storage |
| 4 MiB | Not available | One 0x3D0000-byte app slot and 0x20000 bytes (128 KiB) of internal storage |
The OTA layout also includes the internal OTA service and its dependencies. The service is not a flavor group. The serial layout does not add it, although another selected feature can still depend on the shared OTA implementation. Changing layouts changes the partition table, so install the first image for a new layout through serial flashing. Do not switch a deployed device's layout with an ordinary OTA update.
For a direct PlatformIO build outside os_builder, set SOLAR_OS_LAYOUT:
SOLAR_OS_LAYOUT=single pio run -e solar_term
If it is omitted, 8 MiB and 16 MiB environments use the OTA layout and 4 MiB environments use their fixed serial layout.
Freenove ESP32-WROVER v3.0
The freenove_esp32_wrover_v3 target covers the FNK0060 v3.0 board with an ESP32-WROVER-E-N4R8 module, 4 MB flash, 8 MB physical PSRAM, a CH340 USB-to-UART bridge, and the rear microSD slot. It uses uart0 on GPIO1/GPIO3 as the boot shell and a fixed storage0 one-bit SDMMC attachment on GPIO14 clock, GPIO15 command, and GPIO2 data. The active-low BOOT button on GPIO0 is also the SolarOS KEY. A short press uses the configured short-press action, which defaults to suspend. Any KEY press resumes from suspend. A long press defaults to forgetting the remembered BLE keyboard and starting pairing. Configure the two gestures independently with setterm powerkey short <action> and setterm powerkey long <action>. Do not hold the button during reset or power-up, because GPIO0 low selects the ESP32 serial download boot mode.
The target deliberately leaves the OV2640 camera unsupported. Remove or disconnect it before using this target. GPIO25 is reserved for PAL composite output because it conflicts with the camera's VSYNC signal. The other former camera signals are available as runtime expansion GPIOs; GPIO34, GPIO35, GPIO36, and GPIO39 are input-only and also support ADC. GPIO5 is a boot strapping pin, so external circuitry must not force it to the wrong level while the ESP32 resets.
The cvbs_pal backend produces monochrome PAL through the original ESP32's DAC1 and I2S0 DMA hardware. Its default SolarOS canvas is 384x288 with PAL 625/50 timing. The U8g2 draw buffer lives in PSRAM, while two scanout buffers in internal RAM swap only at PAL field boundaries so applications keep the normal display service and do not tear the active field. The timing and low-level peripheral setup are adapted from LovyanGFX Panel_CVBS; SolarOS keeps its own one-bit graphics stack instead of linking LovyanGFX's separate color framebuffer.
Small composite displays that do not handle the full PAL raster well can use a centered 320x200 safe-area mode. Select it when building; the main and virtual display dimensions both change to 320x200:
SOLAR_OS_FLAVOR=classic SOLAR_OS_CVBS_MODE=320x200 \
pio run -e freenove_esp32_wrover_v3
Omit SOLAR_OS_CVBS_MODE (or set it to 384x288) to build the default full PAL mode. Composite scanout requires the ESP32's full 240 MHz clock, so SolarOS clamps all power profiles to that board-specific CPU floor on this target.
Use os_builder with the classic profile to make a specialized Synth image. Add the required group, remove unrelated groups until the measured image fits, then build and flash from the same TUI:
python3 scripts/os_builder.py \
--input flavors/classic.toml \
--board freenove_esp32_wrover_v3 \
--layout single
For serial diagnostics, run:
job start log uart0 debug
It is silent because composite scanout owns I2S0. For a custom Synth image, retain BLE, MIDI, controls, storage, basic file tools, Synth, and the PWM audio driver while removing unrelated stacks. Attach an LEDC PWM audio output at runtime because PAL scanout owns I2S0:
expansion attach audio-pwm audio pwm=gpio26
synth
Connect GPIO25 to the composite input's center conductor and a board GND to its shield/ground. Keep both leads short and use a PAL-capable input with its normal 75-ohm termination. The backend continuously owns I2S0 and the APLL while the display is active, so this board cannot use an I2S0 audio backend at the same time.
Runtime GPIO is available on GPIO4, GPIO5, GPIO13, GPIO18, GPIO19, GPIO21-GPIO23, GPIO26, GPIO27, GPIO32-GPIO36, and GPIO39. GPIO34-GPIO36 and GPIO39 are input-only and support runtime ADC; the other runtime pins support PWM and can form runtime I2C, SPI, UART, or 1-Wire buses. UART0 remains registered on the CH340 pins and cannot be detached by the shell using it.
The board uses partitions_4mb.csv, with one 0x3D0000-byte factory application slot and a 0x20000-byte (128 KiB) flash filesystem. A dual-OTA layout is not available on 4 MB flash. Install firmware through the CH340 serial connection; this partition layout does not support on-device OTA updates.
LilyGO TTGO VGA32 v1.4
The ttgo_vga32_v14 target covers the ESP32-PICO-D4 VGA32 revision 1.4 with 8 MB external PSRAM. It uses the onboard RGB222 resistor DAC for VGA, the USB-UART bridge on UART0, mono ESP32 DAC audio on GPIO25, a PS/2 keyboard on GPIO32/GPIO33, and the revision 1.4 microSD wiring on HSPI: MOSI GPIO12, MISO GPIO2, clock GPIO14, and chip select GPIO13. These SD pins differ from the older revision 1.2 board.
VGA scanout continuously streams a short line ring through I2S1 DMA. A level-3 IRAM interrupt on CPU1 refills completed line groups. Frame submissions are coalesced, and a CPU1 presentation worker converts the newest monochrome snapshot into the scanout buffer at a bounded rate. The SolarOS canvas stays monochrome, while a lookup table expands the configured foreground and background colors to the board's two-bit-per-channel VGA output. I2S1 and the VGA GPIOs remain fixed resources while the display is active.
The onboard audio path takes the ESP32 DAC1 signal from GPIO25 and routes the same mono output to the 3.5 mm jack and the NS4150 speaker amplifier. SolarOS uses the shared ESP32-DAC backend at 16 kHz. This is output-only hardware; the board does not advertise microphone or audio-input support. Test it with audio tone 880 500.
The default mode is 640x480@60 Hz. Select another mode at build time with SOLAR_OS_VGA_MODE:
pio run -e ttgo_vga32_v14
SOLAR_OS_VGA_MODE=320x200 pio run -e ttgo_vga32_v14
SOLAR_OS_VGA_MODE=320x240 pio run -e ttgo_vga32_v14
SOLAR_OS_VGA_MODE=640x400 pio run -e ttgo_vga32_v14
The 320x200 and 320x240 modes use double scan and two internal monochrome scanout buffers. The 320x240 mode derives its 60 Hz timing from 640x480 VGA. The 640x400@70 Hz and 640x480@60 Hz modes use the standard 25.175 MHz VGA pixel clock and one internal monochrome scanout buffer to preserve heap for SolarOS. Updating a high-resolution frame can therefore produce a brief tear while the new image is copied. Changing SOLAR_OS_VGA_MODE causes PlatformIO to reconfigure CMake automatically.
The board has 4 MB flash, and its PlatformIO environment defaults to the classic profile:
pio run -e ttgo_vga32_v14
To create a specialized build, start from classic in os_builder, adjust the granular groups, and build against this board's single-image limit:
python3 scripts/os_builder.py \
--input flavors/classic.toml \
--board ttgo_vga32_v14 \
--layout single
The board profile declares ps2kbd0 and creates the default keyboard0 ps2-keyboard expansion attachment before the shell. Its topology and input state remain visible through the generic commands:
expansion devices
input keyboard
input test keyboard0
BLE remains available but defaults to off on this board to preserve internal heap. Use setterm ble on and reboot to enable it. setterm ble default clears the user override and restores the board default.
The onboard PS/2 mouse connector is registered as the fixed ps2mouse0 bus. Attach a mouse when one is connected with expansion attach ps2-mouse mouse0 ps2=ps2mouse0; it is not a default attachment because the connector can be empty. The microSD signals are fixed board resources; GPIO34 and GPIO39 are the only header pins available for runtime GPIO/ADC, and both are input-only.
ODROID-GO
The built-in odroid_go target covers the classic ESP32 Hardkernel ODROID-GO. It uses an ESP32-WROVER module with 4 MiB PSRAM, the ILI9341 display driver, SDSPI storage on the VSPI bus, battery ADC, ESP32 DAC speaker output, digital buttons, ADC D-pad input, status LED, PWM display brightness, Wi-Fi, and BLE.
All classic ESP32 targets default to the compact classic flavor, including Python, Playground, buttons, and ADC D-pad support on ODROID-GO. See Packages and flavors.
The board does not have CDC, a board-defined I2C bus, RTC, onboard temperature/humidity sensors, or audio input enabled. It boots into the display shell, and uart0 on GPIO1/GPIO3 is available as the serial byte-stream port.
The ILI9341 backend plans changed monochrome tile runs, INDEX8 GUI tiles, and INDEX2 game rasters separately, then sends all three through the same queued two-line DMA pump. Game Boy uses a centered 240x216 raster at 30 presentation frames per second so RGB565 transfer stays below the shared-SPI pixel budget.
ODROID-GO uses the shared VSPI bus for the TFT, SD card, and external chip selects:
- GPIO18: VSPI SCLK
- GPIO19: VSPI MISO
- GPIO23: VSPI MOSI
- GPIO5: TFT chip select
- GPIO22: SD card chip select
- GPIO4 and GPIO15: external IO and runtime-safe SPI chip-select slots
Runtime GPIO access is intentionally limited to GPIO4 and GPIO15. Other visible or board-significant pins are reserved: GPIO2 is the status LED, GPIO14 is the LCD backlight, GPIO25 is speaker amplifier enable, GPIO26 is the DAC sample output, GPIO34/GPIO35 are the ADC D-pad axes, GPIO36 is battery ADC, GPIO39 is the board key input, and GPIO32/GPIO33/GPIO13/GPIO27/GPIO0 are built-in buttons.
Create an I2C bus on the free P2 header pins with:
expansion bus create i2c i2c0 port=i2c0 sda=gpio4 scl=gpio15 speed=100000
i2c scan i2c0
Either I2C controller (i2c0 or i2c1) can be selected. GPIO4 and GPIO15 must be unused; while the bus exists, they cannot also serve as external SPI chip selects, GPIO, PWM, or another runtime bus. No I2C bus is created at boot.
GPIO25 is amplifier enable/shutdown wiring, not a second SolarOS DAC channel. Treat GPIO26 as the only DAC sample output for ODROID-GO audio.
Freenove ESP32-S3 Display 4.0-inch
The freenove_esp32_s3_display_4_0 target supports the capacitive-touch FNK0104S board. SolarOS runs its portrait 320x480 ST7796 panel as a 480x320 landscape primary display and controls the active-high GPIO45 backlight with PWM. The FT6336 touch controller shares i2c0 with the ES8311 codec and emits generic absolute pointer events targeted at display0. Native apps opt in with SOLAR_OS_APP_FLAG_POINTER_EVENTS; pointer events include press, move, and release state rather than exposing FT6336 registers to applications. Foreground Python and Lua apps receive the same routed events through solaros.input. The board profile creates this as the default ft6336 expansion attachment named touch0; inspect it with expansion devices, input touch, and input test touch0. Optional logical-range correction is stored with input calibrate touch0 ....
The ST7796 backend uses the same changed-region planner and queued two-line DMA pump for monochrome terminal, INDEX8 GUI, and INDEX2 game content. A full 320x288 Game Boy image would leave too little margin on the 40 MHz RGB565 SPI link, so the presenter selects a centered 240x216 raster at 25 frames per second.
The ES8311 is configured as one duplex codec: GPIO8 carries playback data to the codec, GPIO6 carries microphone data to the ESP32-S3, and GPIO1 controls the active-low speaker amplifier. The SD slot is the fixed four-bit SDMMC attachment storage0. Battery voltage is measured on GPIO9 through the board divider. The TP4054 circuit is an analog charger, not a digitally addressable fuel gauge or battery manager.
Expansion wiring is divided across three connectors. uart0 uses GPIO43 and GPIO44. The I2C connector exposes GPIO16 SDA and GPIO15 SCL. The GPIO connector exposes runtime-safe GPIO2, GPIO3, GPIO14, and GPIO21. Current Freenove FNK0104S documentation identifies the third GPIO signal as GPIO14; GPIO4 is fixed as the ES8311 MCLK signal. The BOOT button on GPIO0 becomes the SolarOS KEY after startup; do not hold it during reset because it selects download mode.
Build the target with:
pio run -e freenove_esp32_s3_display_4_0
QDtech ES3C28P and ES3N28P 2.8-inch ESP32-S3 Displays
The qdtech_es3c28p target supports the capacitive-touch ES3C28P SKU, while qdtech_es3n28p supports the non-touch ES3N28P SKU. SolarOS runs their native 240x320 ILI9341V panel as a 320x240 landscape primary display at 40 MHz, enables the panel's required color inversion, and controls the active-high GPIO45 backlight with PWM.
On ES3C28P, the FT6336 controller shares i2c0 with the ES8311 codec and registers as the default attachment touch0. GPIO17 is its interrupt and GPIO18 is its reset. It emits absolute pointer events rotated into the landscape display0 coordinates. Inspect it with expansion devices, input touch, or input test touch0, and store optional logical-range correction with input calibrate touch0 .... The ES3N28P target does not enable pointer support; GPIO17/GPIO18 remain reserved for its unpopulated touch footprint.
The PlatformIO hardware definition selects the published 80 MHz DIO flash mode for the 16 MB external flash and OPI mode for the integrated 8 MB PSRAM.
The fixed devices are the ILI9341V display, four-bit SDMMC card slot, ES8311 duplex speaker/microphone path, GPIO9 battery ADC, and the GPIO42 WS2812B named pixels0. The shared i2c0 bus is exposed on P4 and also controls the audio codec. P3 exposes runtime GPIO2, GPIO3, GPIO14, and GPIO21; GPIO2, GPIO3, and GPIO14 support ADC. P2 exposes the releasable uart0 signals on GPIO43 and GPIO44. The LCD uses the internal spi0 bus, including its wired GPIO13 MISO signal; it is not an expansion SPI connector.
Build the standalone target with:
pio run -e qdtech_es3c28p
pio run -e qdtech_es3n28p
Elecrow CrowPanel ESP32-S3 4.2-inch E-paper
The built-in elecrow_crowpanel_esp32_s3_4_2_epaper target covers Elecrow's V1.0 400x300 monochrome CrowPanel. It uses an ESP32-S3-WROOM-1-N8R8 module, the SSD1683 e-paper driver, microSD over a dedicated SDSPI bus, five digital controls, a status LED, Wi-Fi, BLE, and selectable OTA or serial-update layouts.
Elecrow has shipped both the original panel and a newer panel identified by a green circular sticker on the back. The newer revision keeps the same GPIO pinout but requires a different reset, initialization, refresh, and sleep sequence. SolarOS detects the revision from its post-reset BUSY behavior and selects the matching command and waveform path automatically.
The green-sticker revision starts in refresh=pulse with the Fastwave phase builder adapted to its UC8276 controller. display mode display0 reports waveform=fastwave-uc8276. Its preset is 8 drive cycles, 8 scan frames, raw PLL code 12, asynchronous scanning with a 67 ms new-image interval, supply cycling, fast ramp, and 20000 kHz pulse SPI. Fixed cleanup runs every 8 changed targets consumed by the worker. It shares per-pixel drive budgets and the scan worker with the other pulse-capable panels. The builder drives only changing pixels, with no reset kick and one neutral settle frame. The adapter keeps the UC8276's own voltage, PLL, RAM, and power commands. pulse-period accepts 67–1000 ms between new images. Settling drives run as BUSY permits, independently of this interval. New targets can interrupt settling between drives; identical targets retain their remaining budgets. The requested interval does not guarantee an achieved rate. Runtime tuning remains available through display mode display0; internal scan frequency is separate from image update rate. Use display mode display0 refresh=auto to restore the native waveform. The original Elecrow 4.2-inch panel currently rejects pulse mode.
The board's USB-C data lines terminate at a CH340C USB-to-UART bridge. SolarOS therefore uses uart0 on GPIO43/GPIO44 for the serial console and does not claim native USB Serial/JTAG CDC. The BAT connector supplies the board but the published schematic does not provide a battery-voltage ADC path, so the target does not advertise the BATTERY capability.
The display shell defaults to the board's landscape orientation, rotated 90 degrees clockwise from the controller's portrait orientation. The onboard controls are mapped as follows:
- GPIO1: EXIT, mapped to the foreground app-exit key.
- GPIO2: MENU, used as the SolarOS KEY for sleep/suspend control and BLE pairing.
- GPIO6: rotary counter-clockwise/previous, mapped to Down.
- GPIO4: rotary clockwise/next, mapped to Up.
- GPIO5: rotary press, mapped to Enter.
The 20-pin GPIO header exposes GPIO3, GPIO8, GPIO9, GPIO14-GPIO21, and GPIO38. GPIO3 is a strapping pin and is listed as a physical connector pin but blocked from runtime control. Runtime GPIO/PWM is allowed on GPIO8, GPIO9, GPIO14-GPIO21, and GPIO38. ADC is available on the ADC-capable subset GPIO8, GPIO9, and GPIO14-GPIO20.
The same runtime-safe pins can be routed to named I2C buses on i2c0 or i2c1, named UART buses on uart1 or uart2, and named 1-Wire buses. They can also be routed to a named SPI bus on spi3 after the SD card is unmounted. SPI3 is arbitrated as one resource: mounting the SD card while an expansion SPI bus is attached, or creating an expansion SPI bus while SD is mounted, is rejected.
The panel and storage wiring remains internal board wiring:
- SSD1683: GPIO12 SCK, GPIO11 MOSI, GPIO47 reset, GPIO46 D/C, GPIO45 chip select, GPIO48 BUSY, and GPIO7 display-power enable.
- microSD: GPIO39 SCK, GPIO40 MOSI, GPIO13 MISO, GPIO10 chip select, and GPIO42 SD-power enable.
- GPIO41: active-high status LED.
The default OTA layout uses partitions_8mb.csv, with two 0x3E0000-byte OTA application slots and a 0x20000-byte flash filesystem partition. The serial layout uses partitions_8mb_single.csv, with one 0x700000-byte factory slot and a 0xF0000-byte flash filesystem partition.
Elecrow CrowPanel ESP32-S3 5.79-inch E-paper
The built-in elecrow_crowpanel_esp32_s3_5_79_epaper target covers Elecrow's DIS08792E 792x272 monochrome CrowPanel. It uses the same N8R8 module, console, microSD, controls, status LED, power gates, and expansion-header wiring as the 4.2-inch board. The display is different: two cascaded SSD1683 controllers drive two 396x272 halves. Their 400-pixel RAM widths create an internal eight-pixel address gap, which the SolarOS driver keeps white while presenting one contiguous 792x272 graphics surface.
The panel uses GPIO12 SCK, GPIO11 MOSI, GPIO47 reset, GPIO46 D/C, GPIO45 chip select, GPIO48 BUSY, and GPIO7 display-power enable. The driver follows Elecrow's master/slave RAM addressing and temperature-load sequence. The board starts in asynchronous monochrome pulse mode with a 67 ms minimum new-image interval, held supplies, and automatic cleanup, as described under SolarWriter. To select the native waveform, use display mode display0 refresh=auto. Auto mode uses one full cleanup refresh after initialization, then transfers changed frames with Elecrow's non-flashing 0xDC partial-update waveform; unchanged frames are skipped. Use display mode display0 refresh=full when an explicit full cleanup is needed.
The wide panel can also run two independent side-by-side sessions:
display split display0 --horizontal left0 right0
session create shell right0
Session 0 moves to left0 automatically. Use Ctrl+Alt+Left/Right, or Ctrl+AltGr+Left/Right on keyboards without a left Alt key, to move local input focus between the two viewports. display unsplit display0 restores the single 792x272 target without discarding session 0 shell state.
Build and flash the target with:
pio run -e elecrow_crowpanel_esp32_s3_5_79_epaper
pio run -e elecrow_crowpanel_esp32_s3_5_79_epaper -t upload --upload-port /dev/ttyUSB0
The onboard CH340 normally enumerates as /dev/ttyUSB* on Linux; use the actual serial-device path reported by the host when it differs.
The same 8 MB OTA and serial partition layouts documented for the 4.2-inch CrowPanel apply to this board.
SolarWriter
The solar_writer target combines the Elecrow CrowPanel 5.79-inch N8R8 board with a GY-INMP441 microphone, Adafruit PCM5102 DAC, Adafruit I2S Amplifier BFF (MAX98357), Adafruit LC709203F battery gauge, and DS3231 real-time clock. It inherits the CrowPanel's 792x272 dual-SSD1683 display, microSD, controls, CH340/UART console, Wi-Fi, BLE, 8 MB flash, and 8 MB OPI PSRAM. The default firmware flavor is writerdeck.
The following connections are fixed in this target:
| Function | GPIO | U5 pin | Module connection |
|---|---|---|---|
| I2C SDA | 8 | 20 | LC709203F SDI/SDA and DS3231 SDA |
| I2C SCL | 9 | 2 | LC709203F SCL and DS3231 SCL |
| Shared I2S bit clock | 14 | 19 | INMP441 SCK, PCM5102 BCK, BFF A2 |
| Shared I2S word select | 15 | 3 | INMP441 WS, PCM5102 WSEL, BFF A1 |
| Playback data | 16 | 18 | PCM5102 DIN and BFF A0 |
| Microphone data | 17 | 4 | INMP441 SD |
| Jack detect, reserved | 21 | 6 | Future isolated switch to GND, active low |
U5 pins 11–14 supply regulated 3.3 V and pins 7–10 are GND, as connected in Elecrow's published schematic. Power the microphone VDD, PCM5102 VIN, fuel-gauge VIN, DS3231 VCC, and amplifier BFF's 5V input pad from 3.3 V for this configuration. The BFF's 3V pad does not supply the amplifier. Use short power wiring and allow for speaker current when sizing the supply. Higher-power speaker operation requires a suitable separate supply with common ground; do not inject 5 V into U5's 3.3 V rail.
The battery connects through the LC709203F's parallel battery sockets to the CrowPanel BAT connector. Use a polarity-correct adapter for the different connectors. The gauge chip is battery-powered; its VIN connection sets the I2C logic level. No thermistor or interrupt wire is required. Fuel-gauge compensation uses an assumed 25.0 °C, not a measured temperature.
audio0 attaches automatically through the reusable inmp441 driver. It appears as INMP441 mic + I2S DAC/amp in audio device information and publishes audio0.capture as 16 kHz mono and audio0.playback as 16 kHz stereo. The microphone uses received slot 1. The DAC and amplifier share one playback data stream; the DAC provides stereo line output and the amplifier plays its default mono mix. The PCM5102 jack requires a line-level load, such as powered speakers or a headphone amplifier.
The 5.79-inch dual-SSD1683 display starts in monochrome pulse mode on both SolarWriter and the base CrowPanel 5.79-inch target. It uses the shared EPD pixel-budget layer and the Fastwave SSD1683 waveform builder. Mode output identifies this adapter as waveform=fastwave. If the scan worker cannot start, the display retains native refresh=auto mode. Inspect the mode with display mode display0, or select and tune it with:
display mode display0 refresh=pulse
display mode display0 drive-cycles=6
display mode display0 pulse-frames=8
display mode display0 waveform-fr=4
display mode display0 cleanup-every=64
display mode display0 cleanup-policy=adaptive
display mode display0 pulse-scan=async
display mode display0 pulse-period=67
display mode display0 pulse-power=hold
display mode display0 pulse-ramp=fast
display mode display0 pulse-spi-khz=20000
display mode display0
Each changed pixel receives drive-cycles completed waveform activations (1–32). Settled pixels select the neutral waveform; submitting the same target does not refill its budget. pulse-frames is the scan-frame duration of each drive phase (1–32); waveform-fr is a raw SSD1683 frame-rate code (1–5 or 9–12), not a frequency in Hz. Defaults are 6 cycles, 8 scan frames, code 4, and adaptive cleanup with cleanup-every=64. The builder uses no opposite-polarity reset kick, the configured drive duration, and one VSS settle frame. Both controllers receive the same generated LUT and nominal source rails (+15 V / -15 V), with VCOM set to 0x08. EOPT is excluded from the voltage-register write mask. Grayscale rendering continues to use the existing dithering path.
Each successful drive accumulates in a per-pixel history since the last native cleanup. Adaptive cleanup runs when the most-driven pixel reaches cleanup-every * drive-cycles activations (384 at the defaults), or after 4 * cleanup-every changed images (256 at the defaults), whichever comes first. Sparse changes can therefore run longer than repeated transitions at the same pixels. The age bound also limits time spent applying neutral waveforms to unchanged pixels; it is an activity heuristic, not an optical ghost detector. Idle time and repeated identical targets do not age cleanup.
Use cleanup-policy=fixed to clean up every cleanup-every changed images. Synchronous mode counts completed changed presentations; asynchronous mode counts changed targets consumed by the worker. At either limit, the asynchronous worker schedules a native cleanup even without another presentation. Cleanup uses its newest consumed target and can interrupt unfinished budgets. display mode display0 reports cleanup-policy, cleanup-peak-cycles, cleanup-limit-cycles, and cleanup-age for tuning. The cycle limit applies only in adaptive mode. Drive history counts completed activations, including reversals; it supplies no measurement of ink settling. Settings are runtime-only. Pulse mode is experimental: its speed, polarity, ghosting, and optical settling require verification on the physical panel. It does not provide native grayscale. Lower budgets can complete without visibly moving the ink, so a successful BUSY cycle does not prove optical settling. Reduce the cycle budget and frame duration only while inspecting the screen in both transition directions.
With pulse-scan=sync, the driver completes all pending cycles before returning from a presentation. Increasing the cycle budget increases that presentation's blocking time. The default pulse-scan=async submits the newest image to a persistent scan worker. It delivers one pulse per scan and continues settling without further presentations. New targets can arrive between cycles; queued images coalesce to the latest target. Settling can span several scans. The SSD1683 adapter also accepts monochrome raster frames from native foreground apps. The shared presenter scales and dithers indexed emulator frames before submission. These frames use the same refresh mode and latest-image queue as ordinary UI updates; physical cadence and ink settling follow the panel waveform. Select asynchronous operation with:
display mode display0 pulse-scan=async
display mode display0 pulse-period=67
On this dual-controller panel, pulse-period requests a minimum new-image interval of 67–1000 ms; the default is 67 ms. Settling drives continue as BUSY permits without waiting for another image slot. When its slot is due, a queued target replaces the current target between drives, even with older pixels still settling. Pixels whose target is unchanged retain their remaining budgets. Hardware work and pixel settling can require longer than the requested interval; it does not promise 15 fully settled images per second. The six-cycle, eight-frame preset retains its settling budget. display mode display0 reports scan-last-us, scan-interval-us, completed scan-count, submitted/coalesced image counts, and pending pixel count. frame-count counts distinct target snapshots processed successfully, including native baselines; frame-interval-us measures their start-to-start interval. Repeated identical submissions do not count as new frames. Drive count, new-image count, and optical settling are separate quantities.
Additional panel-specific controls are pulse-power=<cycle,hold>, pulse-ramp=<normal,fast>, and pulse-spi-khz=<100..20000>. The default power policy is hold, which retains the supply stages during a settling sequence and powers down when all budgets finish. It enables the analog stages on the first pulse of a sequence and uses display-only activations for subsequent pulses. A new sequence after idle power-down or reset enables the stages again. The normal booster ramp uses approximately 40/40/10 ms phases; the default fast uses 10/10/10 ms with the same current settings and voltage targets. Both ramp settings require optical calibration. Pulse SPI speed defaults to 20000 kHz; its override applies only in pulse mode, with the normal clock restored for native operation. These controls do not guarantee a particular frame rate.
The first presentation establishes a native full-refresh baseline. Logs report changed pixel count, completed cycles or scans, SPI transfer time, and waveform BUSY time separately in log show. Asynchronous scan logs also report the interval between waveform activations. A failed transfer or waveform does not consume the failed cycle's budget and returns the driver to refresh=auto. Synchronous mode recovers on the next paint; asynchronous mode immediately performs a native recovery of the newest accepted target. Sleep suspends scans; waking reseeds from a native baseline. Use display mode display0 refresh=auto to restore the standard waveform; the next presentation resets the controller and performs a full cleanup. For repeated native black/white recovery cycles, use display relax display0 (60 seconds) or display relax display0 3600 (one hour). The command restores the saved image and pulse settings when it finishes or is interrupted. See Display commands for supported panels and cancellation. The pattern script tests/hil/epd_pulse_pattern.py exercises both transition directions and moves patches across both controller halves. Its printed messages mark queued presentations; gfx.present() returns before physical completion. Read the driver timings in log show and judge optical settling and ghosting on the actual screen. tests/hil/epd_pulse_scan_pattern.py queues targets at 67 ms intervals with moving black and dithered patches and a stationary shade ramp, then holds the final image so its pixels can settle. Use the driver statistics and physical screen to assess contrast, erased trails, static shades, and the panel seam.
battery0 attaches automatically at I2C address 0x0b. Battery capacity defaults to 3000 mAh and remains configurable:
expansion devices
audio devices
battery status
battery capacity 3000
rtc0 attaches automatically through the reusable ds3231 driver at 0x68 on the same 100 kHz i2c0 bus as battery0. Its driver is included in every SolarWriter flavor. The clock stores UTC and supports calendar dates from 2000–2099 and second-resolution alarms. Set the clock after first power-up or loss of backup power; oscillator-stop status leaves time unreliable until set.
rtc status
date
ntp
rtc alarm set 07:30:15
ntp needs a network connection and the NTP package. INT/SQW is not wired in this profile, so RTC alarms have no GPIO wake signal. The DS3231 has no countdown timer; use schedule for portable timed actions. See DS3231 usage.
GPIO21 is reserved in the pin metadata and is not available for general GPIO or PWM. Plug detection and automatic output switching are not enabled in this profile. The PCM5102 board ties its jack switch contacts to audio nets; a separate isolated detection circuit is required. Both outputs receive playback regardless of plug insertion. GPIO18, GPIO19, GPIO20, and GPIO38 remain free; the optional DAC mute and fuel-gauge interrupt signals are not claimed.
Build with pio run -e solar_writer. The target uses the same 8 MB OTA and serial partition layouts as the base CrowPanel.
Waveshare 4G E-paper
The waveshare_esp32_s3_sim7670g_4g_epaper target uses the monochrome Waveshare 4.2-inch V2/revision-2.2 SSD1683 profile at 400x300. Native refresh defaults to auto, with dirty-window partial updates and periodic full cleanup. The display remains on its existing board-owned SPI bus and pins.
The optional experimental pulse adapter uses row-major transfers on one controller, the shared per-pixel drive budget and history, asynchronous scans, and adaptive native cleanup. Select it with:
display mode display0 refresh=pulse
display mode display0
Initial pulse defaults are 3 drive cycles, 8 scan frames, FR code 4, asynchronous operation, a 100 ms requested minimum new-image interval, supply cycling, normal booster ramp, the configured board SPI clock, and adaptive cleanup with a baseline of 64. These values are a calibration starting point; optical contrast, erasure, ghosting, and achieved cadence require inspection on the physical panel. The requested interval is not a measured frame rate.
The adapter exposes the same pulse controls as SolarWriter, including drive-cycles, pulse-frames, waveform-fr, cleanup-every, cleanup-policy, pulse-scan, pulse-period, pulse-power, pulse-ramp, and pulse-spi-khz. Mode output reports scan timings, pending pixels, and cleanup counters. log show includes native transfer/drive timings and pulse scan timings for comparison. Faster ramp, held supplies, and higher SPI clocks require separate panel and wiring validation.
The waveform uses the power rails and VCOM setting published for Waveshare's 4.2-inch V2 custom-LUT path; it is a monochrome differential drive waveform, not native grayscale. Switching back with display mode display0 refresh=auto resets the controller and cleans up on the next presentation. A failed asynchronous pulse disables scanning and immediately restores the newest accepted target with a native full refresh.
Waveshare ESP32-S3-ePaper-3.97
The built-in waveshare_esp32_s3_epaper_3_97 target covers Waveshare's 3.97-inch board with an ESP32-S3-WROOM-1-N16R8 module. SolarOS presents the native 800x480 monochrome SSD1677 panel with a 480x800 portrait base orientation. The display defaults to 800x480 landscape, rotated 90 degrees counterclockwise from that portrait orientation (display orientation display0 270). A saved orientation preference takes precedence over this factory default. The fixed devices include four-bit SDMMC storage, a PCF85063 RTC, an SHTC3 temperature and humidity sensor, and ES8311 duplex speaker/microphone audio. Native USB provides the primary CDC console; GPIO43/GPIO44 remain available as uart0 on the breakout pads.
The SSD1677 runs at the vendor reference clock of 20 MHz. Automatic refresh starts with a full cleanup, skips unchanged frames, uses byte-aligned dirty partial windows, and performs another full cleanup after 19 partial updates. Refreshes use the differential waveform with separate current and previous frame data; full cleanups force every pixel to transition. The display attachment sets the AXP2101 ALDO3 rail to 3.3 V, enables it before controller initialization, and disables it when the panel sleeps. This profile follows Waveshare's current AXP2101 reference source and schematic; a board revision fitted with a different power-management IC needs a separate profile or power callback before it is safe to use. Override the refresh policy when testing or clearing ghosting:
display mode display0 refresh=auto
display mode display0 refresh=partial
display mode display0 refresh=full
The onboard controls are mapped as follows:
- GPIO0 BOOT: foreground app exit.
- GPIO4 rotary up: Up.
- GPIO6 rotary down: Down.
- GPIO5 rotary press: Enter.
The exposed SDA/SCL pads share the fixed i2c0 bus with the onboard RTC, environment sensor, audio codec, PMIC, and IMU. The RX/TX pads expose uart0. The fixed AXP2101 attachment publishes its fuel-gauge data through battery and its charging state and controls as charger0. The board profile applies the vendor reference defaults of a 1500 mA VBUS input limit, 200 mA constant charge current, and 4.2 V target voltage. Use battery status and charger status charger0 to inspect them. The fixed imu0 attachment polls the QMI8658 at the reference repository's low I2C address (0x6a) and exposes plus/minus 8 g acceleration and 512 degrees-per-second angular velocity ranges in SI units. Use imu sample imu0 to read both vectors. Orientation is not advertised because this profile enables the raw accelerometer and gyroscope rather than the chip's AttitudeEngine.
Build the target with:
pio run -e waveshare_esp32_s3_epaper_3_97
Seeed Studio reTerminal Sticky
The seeed_reterminal_sticky target uses an ESP32-S3R8 with 32 MB flash and 8 MB PSRAM. It defaults to the full flavor. Its 3.97-inch 800x480 e-paper panel defaults to native landscape orientation. The sticky-display device identifies SSD1677 and SSD2677 panels at startup, then registers the selected controller and driver under display0. Detection checks command 0x70 and stable post-reset BUSY polarity twice. BUSY polarity also supports panels without working SPI readback; floating or inconsistent BUSY signals fail attachment. The result is checked on every attachment rather than cached in NVS. Pins follow the Seeed hardware overview:
- E-paper: SCK GPIO13, MOSI GPIO14, CS GPIO15, DC GPIO16, RST GPIO17, BUSY GPIO18, EN GPIO47
- Touch (GT911): SCL GPIO2, SDA GPIO3, INT GPIO21, RST GPIO41, EN GPIO42; 180-degree panel alignment before display rotation.
- Buttons: AI/Power GPIO4 (tap: light sleep/wake, hold: shutdown), Up GPIO5 (tap: Up arrow, hold for two seconds: BLE pairing), Down GPIO6 (tap: Down arrow, hold for two seconds: app exit); all active-low.
- Power: PWR_HOLD GPIO45, PWR_LOCK GPIO46; BQ25616 charger enable GPIO39 is configured as an output and driven LOW at boot to enable charging.
- Console: UART0 GPIO43/44 (CH343 USB-C bridge), DIO flash
- Sensor I2C: SCL GPIO0, SDA GPIO1; PCF8563 RTC at
0x51, SHT40 at0x44, BQ27220 at0x55 - microSD: shared display SPI bus, MISO GPIO12, CS GPIO8, power GPIO10
- Buzzer: PWM GPIO48, attached as
audio0throughaudio-pwm.
The default devices are display0, touch0, rtc0, environment0, battery0, imu0, storage0, audio0, and mic0. The console uses the USB-UART bridge; GPIO19/20 carry the PDM microphone clock/data. The 32 MiB partition table keeps two 7 MiB OTA application slots and reserves nearly 18 MiB for FAT storage at /flash, mounted alongside /sdcard. The serial-only layout uses one 14 MiB application slot and the same storage region. Updating from the former 16 MiB layout requires flashing the partition table over USB; an OTA image cannot change it. Back up flash files before changing layouts or formatting storage.
The SD rail stays powered while the shared display SPI bus is active. Use expansion attach storage0 after insertion or reinsertion to attach the slot or recover its mounted card.
An SSD1677 display starts with asynchronous refresh=pulse / waveform=fastwave at 20 MHz. The Sticky panel profile (panel=1) uses matching black/white data in both RAM planes for its factory full-refresh waveform. Pulse mode uses short monochrome drives with per-pixel settling budgets. Defaults are 100 ms between new images, five drive cycles, eight scan frames, frame-rate code 8, retained supplies during settling, and adaptive cleanup with baseline 64. refresh=fast aliases native refresh=partial; waveform=otp restores auto. If pulse resources cannot be allocated at startup, native auto mode remains available. Pulse buffers are allocated in PSRAM when enabled (about 2.75 MiB including worker snapshots). Optical contrast and ghosting depend on the panel. Returning to a native mode releases the pulse buffers, snapshots, and worker. pulse-scan=sync releases the asynchronous worker and its snapshots. The native framebuffer prefers PSRAM; SPI DMA staging stays in internal SRAM. Touch swaps the GT911 axes before its 180-degree board rotation to match landscape. The direct MONO1 frame presenter supports scaled animation and games at a preferred 10 presentations per second on SSD1677. Pulse settling continues asynchronously.
SSD2677 uses its own Sticky 3D97 V2 monochrome profile at 20 MHz. Sticky's panel=1 starts in asynchronous refresh=pulse / waveform=fastwave. The driver uploads an SSD2677-specific 535-byte RAM waveform and uses the same pixel-budget and scan worker as SSD1677. Defaults are drive-cycles=8, pulse-frames=4, waveform-fr=5 (85 Hz), pulse-period=100, pulse-power=hold, and adaptive cleanup every 64 changed targets. Static pixels continue settling after the application stops painting; newer frames replace queued targets without waiting for every preceding frame to settle. drive-cycles and pulse-frames accept 1..32. SSD2677 waveform-fr accepts 1..5, corresponding to 25, 50, 65, 75, and 85 Hz. Other pulse tuning, cleanup, inversion, synchronous scan, and SPI-rate controls follow SSD1677 semantics. The panel's own OTP supplies analog voltage settings. Full cleanup, recovery, and final power-state images use factory waveforms. Pulse mode uses aligned RAM windows around active pixels, including the preceding mask so settled pixels are cleared to HOLD. The first mask initializes the full visible RAM; subsequent transfers cover only that window.
Both Sticky controller revisions default to display0 scale 2 (a 400x240 logical canvas on the 800x480 panel). A saved per-display scale takes precedence.
Native refresh=auto|fast|partial|full modes remain available. The panel=0 alias starts in auto mode. refresh=partial selects factory partial refresh; refresh=fast selects partial refresh with a fixed 85 Hz scan rate. refresh=full forces factory full refresh. Auto and fast modes perform a full cleanup after 19 changed partial updates; explicit partial mode bypasses periodic cleanup. Full refreshes always use factory scan timing. Unchanged frames are skipped. This profile transmits full-frame old/new two-bit transition pairs. The panel scans 680 gate lines while accepting the 480 visible image rows. Temperature readings select the vendor waveform bucket, with a 25 C fallback for unavailable readback. Partial updates reuse that selection and resample temperature every five seconds; full refreshes always resample temperature and reload the factory waveform. The MONO1 raster presenter supports all four rotations, palette inversion, scaled animation and games, with a preferred rate of ten presentations per second. Actual scan time depends on the waveform, transfer cost, and temperature. display mode display0 reports scan/submission/coalescing statistics, pending pixels, the configured SPI rate, and the last native refresh, transfer, and panel drive times in microseconds. The driver reverses the V2 gate-row order while preserving the source X direction so the default orientation and touch calibration match SSD1677 units. Recovery (display relax), final power-state presentation, and sleep/resume use native full refreshes. The driver restores its short RAM waveform after the next pulse baseline. The SSD1677 LUT/register format cannot be used directly on SSD2677. This profile does not support generic SSD2677 color panels.
The LSM6DS3TR-C at 0x6a supplies acceleration and angular velocity through imu0. Its GPIO7 interrupt also supplies hardware tap, double-tap, tilt, wake, and free-fall input gestures. With gestures enabled, acceleration runs at 416 Hz and angular velocity at 104 Hz. Single taps wait 650 ms for a possible double tap. Use input test imu0 to inspect events and gesture bind source=imu0 to assign actions. wake reports motion; it does not automatically wake the OS. Fused orientation is not provided. The microphone provides mic0.capture: 16 kHz, signed 16-bit mono PCM through I2S0's PDM receiver. GPIO38 and capture DMA are enabled only while its stream is open; capture can run alongside the PWM buzzer output. Use audio mic 1000 for a level check, or select mic0.capture in arecord or Recorder. Native four-level grayscale and full peripheral power optimization remain unimplemented for this target. Battery percentage comes from the BQ27220 state-of-charge register; charging state is inferred from its discharge flag.
Power-key shutdown runs after release and releases the hardware power latch after jobs, storage, and display shut down. With USB power connected, the shutdown path falls back to deep sleep. Light sleep retains the latch and resumes the current session. An Up hold starts pairing once and does not send an arrow on release.
Build and flash:
pio run -e seeed_reterminal_sticky pio run -e seeed_reterminal_sticky -t upload
Xteink X4 Pro
The xteink_x4_pro target uses an ESP32-S3, 16 MB QIO flash at 80 MHz, and 8 MB OPI PSRAM. Its 800x480 e-paper display defaults to native landscape orientation. One firmware image covers SSD1677, UC8179, and UC8279 units. When SolarOS has no cached controller ID, it reads the UltraChip version/status registers twice over the bidirectional MOSI line. Recognized UltraChip responses select UC8179 or UC8279; repeatable floating-high responses select the board's SSD1677 alternative. Ambiguous responses leave the display unavailable and are not saved. After controller initialization, SolarOS stores its own controller ID in NVS namespace x4pro, key controller, and reuses it on subsequent boots. These IDs are independent of the stock firmware's calibration values. To repeat identification, use nvs erase x4pro controller; a successful erase reboots the unit. An invalid cached ID is reported instead of silently selecting a different controller.
Use display list to inspect display readiness and brightness support. The display supports display mode display0 refresh=auto, refresh=partial, and refresh=full. Automatic mode skips unchanged frames; UltraChip controllers perform a full cleanup every 20 changed frames. Partial mode uses a full refresh to establish the first frame after initialization or resume.
The frontlight uses two active-high PWM outputs: cool on GPIO8 and warm on GPIO9, at 25 kHz with 10-bit resolution. setterm brightness 0..100 controls their combined brightness at a fixed 50/50 mix. Zero turns both off; display suspend also turns both off and resume restores the selected brightness. Brightness uses the existing SolarOS persistence setting. Color-temperature adjustment is not exposed.
The shared 400 kHz i2c0 bus uses SDA39/SCL38 and carries GT911 touch (touch0, 0x5d with 0x14 fallback), BM8563-compatible RTC (rtc0, 0x51), and CW2017 battery gauge (battery0, 0x63). Touch uses reset GPIO4, interrupt GPIO10, active-low power GPIO2, and a portrait-to-landscape coordinate transform. The capacitive Home pad sends app exit on release. The left/right buttons (GPIO0/GPIO7) send Up/Down. GPIO3 is the system key: a short press defaults to app exit. Its short and long actions are independently configurable with setterm powerkey short <action> and setterm powerkey long <action>. Holding GPIO0 during reset selects the ESP32 bootloader.
The one-bit SDMMC slot (storage0) uses CLK41/CMD42/DAT0=40. Its active-low GPIO5 rail is power-cycled before card initialization. GPIO1 holds the shared peripheral rail on. The charger STAT signal on GPIO21 is reserved; USB/VBUS presence is not advertised. Native USB CDC and UART0 provide console access; the profile does not declare an external expansion connector.
Hardware mapping follows the FreeInk X4 Pro hardware profile and its controller-identification implementation.
pio run -e xteink_x4_pro
Headless Boards
A headless board is a valid SolarOS target as long as it has a byte-stream port. For boards without DISPLAY, SolarOS starts the primary shell on uart0 when UART is enabled. If UART is not available, it falls back to cdc0 when CDC is enabled.
Recommended minimal capability set for a generic ESP32-S3 board:
set(SOLAR_OS_BOARD_HAS_PSRAM ON)
set(SOLAR_OS_BOARD_PSRAM_BYTES 8388608)
set(SOLAR_OS_BOARD_HAS_SIMD ON)
include("${CMAKE_CURRENT_LIST_DIR}/drivers/cdc_usb_serial_jtag.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/drivers/uart_esp_idf.cmake")
set(SOLAR_OS_BOARD_HAS_CDC ON)
set(SOLAR_OS_BOARD_HAS_UART ON)
set(SOLAR_OS_BOARD_HAS_WIFI ON)
set(SOLAR_OS_BOARD_HAS_BLE ON)
With uart0 as the primary shell, cdc0 remains clean for logs, a later shell job, bridge jobs, or host-side tooling.
The built-in esp32_s3_devkitc1_n16r8 target keeps this headless shell model and also enables expansion GPIO, ADC, PWM, I2C, and SPI. The default I2C bus is GPIO8 SDA and GPIO9 SCL. The default SPI bus is FSPI on GPIO12 SCK, GPIO13 MISO, and GPIO11 MOSI, with chip-select slots on GPIO4, GPIO10, GPIO5, GPIO6, and GPIO7. Its active-low BOOT button on GPIO0 is also the SolarOS KEY. The short press defaults to suspend and the long press to BLE keyboard replacement. Configure either with setterm powerkey short <action> or setterm powerkey long <action>. A press of any duration wakes suspend or sleep. GPIO0 remains reserved from runtime routing. Do not hold the button during reset or power-up, because that selects download boot mode. The N16R8 target defaults to the common 16 MiB OTA partitions.csv layout. Its serial alternative uses partitions_16mb_single.csv. Both retain the same 0x1F0000-byte internal volume for durable agent conversations and normal file workflows without an SD card. The board also permits runtime routing on the spare SPI3 host. Static spi0 remains the usual choice; the runtime host is useful for isolated experiments on another set of routable expansion pins. Auxiliary SPI displays can use that expansion SPI bus through expansion drivers. For example, a PCD8544 84x48 LCD module can attach as lcd0 with expansion attach pcd8544 lcd0 spi=spi0 cs=gpio10 dc=gpio4 reset=gpio5 and then be exercised with display test lcd0. An RFM95W multimode radio wired with NSS on GPIO4 and reset on GPIO5 attaches with expansion attach rfm95 radio0 spi=spi0 cs=gpio4 reset=gpio5. Auxiliary I2C displays can use i2c0 as well. A common 128x64 SSD1306 OLED at address 0x3c can attach with expansion attach ssd1306 oled0 i2c=i2c0 addr=0x3c; use display test oled0 or session create shell oled0 after attachment. Modules whose image is shifted two pixels left use the SH1106 profile instead: expansion attach sh1106 oled0 i2c=i2c0 addr=0x3c.
The Waveshare target has no static SPI bus on its expansion connector, but its spare SPI3 host may be routed over the four free header pins. A full-duplex bus using GPIO1/GPIO2/GPIO3 plus GPIO17 as its device-select slot is created with:
expansion bus create spi spi1 host=spi3 sclk=gpio1 mosi=gpio2 miso=gpio3 cs=gpio17
The bus remains idle until a device attaches. After detaching all devices, expansion bus remove spi1 releases the three data/clock pins and its configured chip-select pins. The board-defined I2C bus on GPIO13/GPIO14 is fixed and is never remapped by this operation.
The spare I2C/UART controllers and free pins can instead form runtime I2C, UART, or named 1-Wire buses:
expansion bus create i2c i2c1 port=i2c1 sda=gpio1 scl=gpio2
expansion bus create onewire onewire0 pin=gpio3
expansion bus create uart uart1 port=uart1 tx=gpio1 rx=gpio2
The board-defined uart0 on GPIO43/GPIO44 is non-removable but detachable. From a display or other non-uart0 shell, expansion bus detach uart0 releases those pins for a temporary runtime bus. Remove the temporary descriptor and run expansion bus attach uart0 to restore the board UART. A UART carrying an active port owner cannot be detached.
For the N16R8 module, GPIO35, GPIO36, and GPIO37 are reserved by Octal PSRAM and must not be exposed as runtime GPIO. The generic DevKitC target also reserves GPIO38 and GPIO48 because the onboard RGB LED moved between board revisions. Use a revision-specific board profile if one of those pins must be exposed.
GOOUUU ESP32-S3CAM GPIO
GPIO2, GPIO3, GPIO45 and GPIO46 are available for runtime GPIO and PWM. GPIO2 and GPIO3 also support ADC. GPIO46 supports both input and output on ESP32-S3. Configure them explicitly with gpio mode, pwm or an expansion attachment.
GPIO2 may carry the board's status LED load. GPIO3 and GPIO46 are also labeled as onboard switch/shutter signals. Check that circuitry before driving outputs. GPIO3, GPIO45 and GPIO46 remain strapping pins during reset. GPIO3 selects a JTAG source, GPIO45 can select the flash/PSRAM supply voltage, and GPIO46 participates in boot-mode and ROM-message selection. External wiring must preserve the levels required for boot, including when a peripheral is powered before the board. These reset functions are independent of runtime GPIO policy. See the ESP32-S3 datasheet for strapping and eFuse overrides.
The runtime GPIO/PWM set is GPIO1-GPIO3, GPIO14, GPIO21, GPIO41, GPIO42 and GPIO45-GPIO47. ADC is available on GPIO1-GPIO3 and GPIO14. Camera, USB, PSRAM, SDMMC, console UART, BOOT/KEY and WS2812 pin assignments remain reserved.
Display Boards
For a board-integrated primary display, enable both DISPLAY and GFX, include the display fragment, and declare the packaged driver as an immutable early display0 attachment in the board header:
include("${CMAKE_CURRENT_LIST_DIR}/drivers/display_st7305.cmake")
set(SOLAR_OS_BOARD_HAS_DISPLAY ON)
set(SOLAR_OS_BOARD_HAS_GFX ON)
A headless board that supports attachable graphical displays enables GFX without enabling DISPLAY. Its graphics applications are compiled, but they require a ready named display target at runtime.
The board header then provides metadata and pins. The built-in Waveshare target uses the ST7305 reflective LCD driver:
SOLAR_OS_BOARD_DISPLAY_WIDTH and SOLAR_OS_BOARD_DISPLAY_HEIGHT always describe the logical SolarOS drawing area after rotation. A controller driver that needs the unrotated panel address space uses SOLAR_OS_BOARD_DISPLAY_NATIVE_WIDTH and SOLAR_OS_BOARD_DISPLAY_NATIVE_HEIGHT separately. Display targets register the dimensions of their initialized U8g2 logical coordinate space; registration rejects a target whose advertised dimensions disagree with that space.
#define SOLAR_OS_BOARD_DISPLAY_CONTROLLER "ST7305"
#define SOLAR_OS_BOARD_DISPLAY_WIDTH 400
#define SOLAR_OS_BOARD_DISPLAY_HEIGHT 300
#define SOLAR_OS_BOARD_PIN_LCD_DC GPIO_NUM_5
#define SOLAR_OS_BOARD_PIN_LCD_CS GPIO_NUM_40
#define SOLAR_OS_BOARD_PIN_LCD_SCK GPIO_NUM_11
#define SOLAR_OS_BOARD_PIN_LCD_MOSI GPIO_NUM_12
#define SOLAR_OS_BOARD_PIN_LCD_RST GPIO_NUM_41
#define SOLAR_OS_BOARD_PIN_LCD_TE GPIO_NUM_6
The built-in ODROID-GO target uses the ILI9341 TFT driver on the board VSPI bus:
#define SOLAR_OS_BOARD_DISPLAY_CONTROLLER "ILI9341"
#define SOLAR_OS_BOARD_DISPLAY_WIDTH 320
#define SOLAR_OS_BOARD_DISPLAY_HEIGHT 240
#define SOLAR_OS_BOARD_DISPLAY_NATIVE_WIDTH 240
#define SOLAR_OS_BOARD_DISPLAY_NATIVE_HEIGHT 320
#define SOLAR_OS_BOARD_PIN_LCD_DC GPIO_NUM_21
#define SOLAR_OS_BOARD_PIN_LCD_CS GPIO_NUM_5
#define SOLAR_OS_BOARD_PIN_LCD_SCK GPIO_NUM_18
#define SOLAR_OS_BOARD_PIN_LCD_MOSI GPIO_NUM_23
#define SOLAR_OS_BOARD_PIN_LCD_MISO GPIO_NUM_19
#define SOLAR_OS_BOARD_PIN_LCD_BL GPIO_NUM_14
The Elecrow CrowPanel target exposes its SSD1683 e-paper controller as a fixed early display0 expansion attachment on the board-defined spi0 bus:
#define SOLAR_OS_BOARD_DEFAULT_EXPANSION_DEVICES { \
{ \
.driver = "ssd1683", \
.name = "display0", \
.binding_count = 8, \
.bindings = { \
{SOLAR_OS_EXPANSION_BINDING_SPI_BUS, .target = "spi0"}, \
{SOLAR_OS_EXPANSION_BINDING_SPI_CS, .target = "spi0", .value = 45}, \
{SOLAR_OS_EXPANSION_BINDING_GPIO, "dc", .value = 46}, \
{SOLAR_OS_EXPANSION_BINDING_GPIO, "reset", .value = 47}, \
{SOLAR_OS_EXPANSION_BINDING_GPIO, "busy", .value = 48}, \
{SOLAR_OS_EXPANSION_BINDING_GPIO, "power", .value = 7}, \
{SOLAR_OS_EXPANSION_BINDING_PARAMETER, "clock", .value = 10000}, \
{SOLAR_OS_EXPANSION_BINDING_PARAMETER, "panel", .value = 0}, \
}, \
}, \
}
The fixed bindings select GPIO12 SCK, GPIO11 MOSI, GPIO45 CS, GPIO46 D/C, GPIO47 reset, GPIO48 BUSY, GPIO7 display power, a 10 MHz SPI clock, and the automatic Elecrow panel profile. That profile detects the original or green-sticker revision from BUSY behavior and defaults to rotation 2. Runtime Waveshare V2 attachments use panel profile 3, rotation 0, and 2 MHz by default.
Its refresh=auto default performs fast updates, skips unchanged frames, and inserts a full waveform on the first update and after every 19 fast updates to limit ghosting. display mode display0 refresh=fast forces fast updates and display mode display0 refresh=full forces the full waveform.
Different display controllers should get a separate driver and board display binding instead of overloading the ST7305 or ILI9341 macros.
The runtime path is:
main.c
-> early fixed expansion attachments
-> services/solar_os_<controller>_display.c
-> drivers/<concrete_display_driver>.c
-> generic board-display backend for display0
-> solar_os_display target display0
main.c, terminal, and graphics services do not include concrete display driver headers. A board panel is listed as a fixed expansion device and is registered with the display service as display0, source=board, and role=primary. When that controller driver is not already attached, a runtime attachment uses another name and registers an auxiliary target with source=expansion. Each of these controller drivers supports one attached instance at a time.
Storage, I2C, Sensors, RTC, And Audio
Declare built-in peripherals as fixed devices using the same catalog entries as runtime expansions. Add the protocol backends that their static buses need and use storage_expansion for either SDMMC or SDSPI:
[build]
drivers = ["storage_expansion", "i2c_esp_idf", "spi_esp_idf"]
capabilities = ["sd", "i2c", "spi", "expansion_i2c", "expansion_spi"]
[[buses]]
name = "i2c0"
protocol = "i2c"
sharing = "shared"
port = "I2C_NUM_0"
sda = 13
scl = 14
[[devices]]
driver = "sdmmc"
name = "storage0"
bindings = { clk = 38, cmd = 21, d0 = 39 }
SDSPI uses a named SPI bus and bindings = { spi = "spi0", cs = 22 } instead. The package driver consumes the fixed-device bindings and is not selected through board-pin #ifdef branches. Boards with a switched card supply can additionally define SOLAR_OS_BOARD_PIN_SD_POWER and SOLAR_OS_BOARD_SD_POWER_ACTIVE_LEVEL; the shared storage adapter enables that rail before probing or mounting the card.
RTC, sensor, battery, and audio devices follow the same rule. See the Waveshare manifest for PCF85063, SHTC3, battery ADC, and ES8311/ES7210 examples. See the ODROID-GO manifest for ESP32 DAC output and amplifier-enable bindings. The Waveshare PCF85063 interrupt is a fixed optional device binding on GPIO15; other PCF85063 attachments can omit it when the interrupt output is not wired.
RTC chip adapters register with the generic RTC service. That service presents their clock/calendar operations to the shared time service and retains generic RTC topology such as an optional interrupt GPIO. Providers advertise optional alarm, countdown, and interrupt-status operations; an RTC that only keeps time remains a valid provider. The PCF85063 adapter implements all three optional operations. Countdown periods from 1 to 255 seconds are supported directly; longer exact-minute periods are supported through 4 hours 15 minutes. Power, alarm, shell, and script features must use the RTC service rather than including a concrete RTC driver.
The runtime path follows the same pattern as display:
services/solar_os_<service>.c
-> solar_os_board_<class>_*
-> board/solar_os_board_<class>_<driver>.c
-> drivers/<concrete_driver>.c
Services and applications should include the board abstraction headers, not concrete driver headers such as sd_card.h, rtc_pcf85063.h, audio_codec_board.h, shtc3.h, or battery_adc.h.
Validation Checklist
Before committing a new board target:
- Build the new environment:
``sh pio run -e my_board ``
- Rebuild the Waveshare environment to catch shared regressions:
``sh pio run -e solar_term ``
For changes touching ESP32 classic support, ILI9341 display, SD-SPI, ESP32-DAC audio, buttons, or ADC D-pad input, also build ODROID-GO:
``sh pio run -e odroid_go ``
- Check the compile log for low-level drivers. A headless board should not compile display, SD, audio, battery, sensor, or GPIO drivers unless those capabilities were explicitly enabled.
- Flash and verify boot:
``sh pio run -e my_board -t upload ``
- On the device, run:
``text status port list pkg ``
- Try unsupported hardware commands and confirm they fail cleanly, for example:
``text disk status audio status battery status ``
- If the board has no display, confirm the primary shell starts on
uart0and thatcdc0can still be claimed by a job when needed.
Quick reference
Select the PlatformIO environment matching the physical target. Board profiles declare capabilities and drivers; flavors select packages within those capabilities. The built-in target table, pin rules, display/storage/audio details, porting procedure, and validation checklist are maintained here.