Native App Development
PicoDeck supports running native ARM Cortex-M33 (RP2350) applications in addition to Lua scripts. Native apps are Position-Independent ELF32 (PIE) binaries loaded from the SD card into PSRAM at runtime.
Choosing Lua vs native
Section titled “Choosing Lua vs native”Both runtimes share the same PicoCalcAPI, but the convenience layers differ:
- Lua has the full game framework: sprites, spritesheets, tilemaps, camera, scene manager, animation, particles, save files. New games should start here.
- Native (C) gets the raw primitives: display (incl. clip rect, mode-7
drawPlane, raycasting columns), images, audio, input, fs. It suits ports and emulators (Doom, C-Dogs, GBC) and CPU-bound renderers. There is no native sprite/tilemap engine — replicate what you need in app code, or drive the game logic in Lua.
Application Structure
Section titled “Application Structure”A native application typically consists of:
main.elf: The compiled binary.app.json: Metadata for the launcher (name, description, requirements).
Place these files in /apps/<your_app_name>/ on the SD card.
app.json
Section titled “app.json”{ "id": "com.example.myapp", "name": "My App", "description": "A native PicoDeck app", "version": "1.0", "author": "Your Name", "requirements": ["audio", "http"]}See Global Variables and Permissions for available requirements (filesystem, root-filesystem, http, audio).
Development Environment
Section titled “Development Environment”Prerequisites
Section titled “Prerequisites”- GNU Arm Embedded Toolchain:
arm-none-eabi-gcc - PicoDeck SDK Headers:
app_abi.handos.h(found insdk/native/) - Linker Script:
linker.ld(found insdk/native/)
Creating a Native App
Section titled “Creating a Native App”Entry Point
Section titled “Entry Point”Your application must define an entry point named picodeck_main. The OS passes a pointer to the PicoCalcAPI struct, which provides access to all OS services.
#include "app_abi.h"#include "os.h"
void picodeck_main(const PicoCalcAPI *api, const char *app_dir, const char *app_id, const char *app_name){ const picocalc_display_t *d = api->display; const picocalc_input_t *i = api->input; const picocalc_sys_t *s = api->sys;
while (true) { s->poll(); // REQUIRED: polls keyboard, fires pending callbacks, // handles system menu (Sym key)
if (s->shouldExit()) // User selected "Exit App" from system menu return;
if (i->getButtonsPressed() & BTN_ESC) return;
d->clear(0x0000); d->drawText(10, 10, "Hello from C!", 0xFFFF, 0x0000); d->flush(); }}Important: You must call s->poll() each frame and check s->shouldExit() to properly handle the system menu exit. Returning from picodeck_main returns control to the launcher.
The API Surface
Section titled “The API Surface”The PicoCalcAPI struct contains pointers to all OS subsystems. The full type definitions are in sdk/native/os.h.
Core subsystems (always available)
Section titled “Core subsystems (always available)”| Pointer | Type | Description |
|---|---|---|
api->input | picocalc_input_t | Button state, edge detection, character input |
api->display | picocalc_display_t | Drawing primitives, framebuffer flush, effects |
api->fs | picocalc_fs_t | SD card file I/O (open, read, write, list) |
api->sys | picocalc_sys_t | Time, battery, reboot, menu items, logging, poll(), shouldExit() |
api->audio | picocalc_audio_t | Tone generation, PCM streaming |
api->wifi | picocalc_wifi_t | WiFi connect/disconnect/status (Pico 2W only) |
api->tcp | picocalc_tcp_t | Raw TCP/TLS sockets (non-blocking) |
api->ui | picocalc_ui_t | Modal dialogs: text input, confirmation |
api->psram | picocalc_psram_t | PIO PSRAM and QMI PSRAM allocation |
api->perf | picocalc_perf_t | FPS counting, frame timing |
api->terminal | picocalc_terminal_t | Terminal emulator widget |
Phase 1 additions (api->version >= 1)
Section titled “Phase 1 additions (api->version >= 1)”| Pointer | Type | Description |
|---|---|---|
api->http | picocalc_http_t | HTTP/HTTPS client (pool of 8 connections) |
api->soundplayer | picocalc_soundplayer_t | Sample playback, file player, MP3 player |
api->appconfig | picocalc_appconfig_t | Per-app key/value config (/data/<APP_ID>/config.json) |
api->crypto | picocalc_crypto_t | SHA-256, SHA-1, HMAC, AES-CTR, ECDH, signature verification |
Phase 2 additions (api->version >= 2)
Section titled “Phase 2 additions (api->version >= 2)”| Pointer | Type | Description |
|---|---|---|
api->graphics | picocalc_graphics_t | Image loading (BMP/JPEG/PNG/GIF), drawing, scaling |
api->video | picocalc_video_t | MJPEG video playback with audio |
api->modplayer | picocalc_modplayer_t | MOD tracker music playback |
api->zip | picocalc_zip_t | ZIP archive extraction |
Version detection
Section titled “Version detection”The api->version field indicates which additions are present:
1— Phase 1 additions2— Phase 2 additions3—api->fs->browse(modal file-browser overlay for native apps)4— display clip rect (setClipRect/getClipRect/clearClipRect), mode-7drawPlane, and native parity forfillHLine/fillTriangle/setScrollArea/setScrollOffset5— zip read-in-place handles6— fonts (setFont/getFont/getFontWidth/getFontHeight/textWidth/loadFont/unloadFont/drawTextTransparent)7— video time seek/position, progress OSD,hasEnded8— TLS verification:http->setInsecure,tcp->connectEx(PCTCP_TLS,PCTCP_TLS_INSECURE)
if (api->version >= 2) { // Phase 2 APIs are available pcimage_t img = api->graphics->load("/apps/myapp/logo.bmp");}sdk/native/os.h is the source of truth for the complete type definitions, the exact struct layout, and the latest version values. The C API maps directly to the picocalc.* Lua modules documented in the Lua SDK Reference.
Compilation
Section titled “Compilation”Native apps MUST be compiled as Position-Independent Executables (PIE). This allows the OS to load them anywhere in memory.
Required CFLAGS
Section titled “Required CFLAGS”-fpie: Generate position-independent code.-fno-plt: Avoid Procedure Linkage Table.-mcpu=cortex-m33 -mthumb: Target the RP2350 processor.
Required LDFLAGS
Section titled “Required LDFLAGS”-T linker.ld: Use the provided linker script.-Wl,--entry=picodeck_main: Set the entry point.-Wl,-pie: Final link as PIE.-Wl,--no-warn-rwx-segments: Suppress linker warnings about RWX segments.-nostartfiles -nodefaultlibs: Native apps do not use standard C runtime startup (CRT0).
Example Makefile
Section titled “Example Makefile”A complete working example can be found in sdk/native/Makefile.
CC = arm-none-eabi-gccCFLAGS = -mcpu=cortex-m33 -mthumb -fpie -fno-plt -Os -I.LDFLAGS = -T linker.ld -Wl,--entry=picodeck_main -Wl,-pie \ -Wl,--no-warn-rwx-segments -nostartfiles -nodefaultlibs
all: main.elf
main.elf: main.c $(CC) $(CFLAGS) main.c $(LDFLAGS) -o $@Binary Loading Process
Section titled “Binary Loading Process”When the PicoDeck launcher starts a native app:
- Core 1 is paused to prevent PSRAM heap contention during loading.
- The
main.elffile is read and the ELF header and program headers are validated. A malformed ELF file is refused with a reason (on screen, in/system/error.log). - The virtual address range of all
PT_LOADsegments is computed. - Split-mode loading: If the code segment (
PF_X) fits in SRAM, it is placed there for faster execution. Data/BSS segments go into PSRAM viaumm_malloc. If SRAM is insufficient, everything goes into PSRAM. - Code written to PSRAM uses the uncached alias (
0x15xxxxxx) to bypass write-back cache, then XIP cache is invalidated. Execution uses the cached alias (0x11xxxxxx) so the 16KB XIP cache serves most instruction fetches. R_ARM_RELATIVErelocations are applied with dual bias (code bias for SRAM, data bias for PSRAM).- The app runs on the Process Stack Pointer (PSP) on its own 64 KB stack in PSRAM (16 KB SRAM if it were ever available), guarded by
PSPLIM: an overflow faults (crash recordPSP (native app)) instead of corrupting memory. Interrupts keep using the main stack (MSP). - Core 1 is resumed after the app exits and resources are freed.
The application runs in the same privilege level as the OS but is expected to return control to the OS by returning from picodeck_main.
Memory
Section titled “Memory”- Stack: 64 KB in PSRAM (PSPLIM-guarded, runs on PSP).
- SRAM heap: effectively none (~2.6 KB). Use
api->psram->qmiAlloc(). - PSRAM heap: 8MB available via
api->psram->qmiAlloc()/api->psram->qmiFree(). Use for large allocations. - PIO PSRAM: 8MB secondary PSRAM available via
api->psram->pioRead()/api->psram->pioWrite()for bulk data. Only addresses0x48000and up are yours;pioRead/pioWrite/pioBulkRead/pioBulkWriterefuse anything overlapping the OS region (the call does nothing and logs[NATIVE] psram->… refused).
Important: Do not mix malloc/free (SRAM) with PSRAM allocation functions. They use separate heaps.
Resources are freed at exit
Section titled “Resources are freed at exit”Anything the app got through the API and did not free — files, images, samples and players, video/MOD players, terminals, AES/ECDH contexts, qmiAlloc blocks, HTTP/TCP connections, fonts, menu items, zip handles — is released when picodeck_main returns. Do not rely on double frees being harmless: a stale pointer whose address was reused by a newer handle frees that newer object.
Per-app config, randomness
Section titled “Per-app config, randomness”appconfig: nothing loads it for you. Call api->appconfig->load(your_app_id) first; another app’s id is refused. crypto->randomBytes returns bool (false = buffer zeroed, no healthy generator). There is no system-config table in the native API.
Debugging
Section titled “Debugging”- Native apps can log via
api->sys->log("message: %d", value). Output appears on USB serial at 115200 baud as[APP] message. - If the app crashes (HardFault), the fault handler displays register state, CFSR flags, and stack pointer info on the LCD. It detects whether the crash was in the app (PSP) or OS (MSP).
- A stack overflow raises a STKOF UsageFault (CFSR bit 20, 0x00100000) recorded in /system/crashlog.txt with the stack owner (PSP (native app) / PSP (Lua VM) / PSP (OS command) / MSP).
See also
Section titled “See also”- Lua SDK Reference — Lua API documentation (C API mirrors these modules)
- Global Variables and Permissions — App requirements
- Crash Logging and Watchdog — Fault recovery and reboot behavior