picocalc-c-module

This skill should be used when the user asks to "create a C module", "modify the display driver", "modify the terminal emulator", "add native code", "MicroPython C API", "compile firmware", "picocalcdisplay module", "vtterminal module", or wants to create or modify C extension modules compiled into the MicroPython firmware for PicoCalc (RP2350/Pico 2W).

Create or Modify PicoCalc C Modules

Build C extension modules compiled into the PicoCalc MicroPython firmware for RP2350.

Module File Structure

modulename/
├── modulename.c           # Implementation + MicroPython bindings
├── modulename.h           # Header with hardware defines
├── micropython.cmake      # CMake build integration (required)
└── micropython.mk         # Make build integration (legacy)

CMake Build Pattern

add_library(usermod_modulename INTERFACE)
target_sources(usermod_modulename INTERFACE
    ${CMAKE_CURRENT_LIST_DIR}/modulename.c
)
target_include_directories(usermod_modulename INTERFACE
    ${CMAKE_CURRENT_LIST_DIR}
)
target_link_libraries(usermod INTERFACE usermod_modulename)

MicroPython C API Pattern

1. Define C Functions

#include "py/runtime.h"

static mp_obj_t mymod_init(mp_obj_t fb_obj, mp_obj_t type_obj) {
    int color_type = mp_obj_get_int(type_obj);
    mp_buffer_info_t buf_info;
    mp_get_buffer_raise(fb_obj, &buf_info, MP_BUFFER_READ);
    uint8_t *frameBuff = buf_info.buf;
    // ... init code ...
    return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_2(mymod_init_obj, mymod_init);

2. Build Globals Table

static const mp_rom_map_elem_t mymod_globals_table[] = {
    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_mymodule) },
    { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&mymod_init_obj) },
};
static MP_DEFINE_CONST_DICT(mymod_globals, mymod_globals_table);

3. Register Module

const mp_obj_module_t mymod_module = {
    .base = { &mp_type_module },
    .globals = (mp_obj_dict_t *)&mymod_globals,
};
MP_REGISTER_MODULE(MP_QSTR_mymodule, mymod_module);

Existing Modules

picocalcdisplay - Python API: init(framebuf, color_type, auto_refresh), update(), startAutoUpdate(), stopAutoUpdate(), setLUT(buf), drawTxt6x8(text, x, y, color). Uses SPI1+DMA, Core 1 auto-refresh, 256-entry LUT.

vtterminal - Python API: init(framebuf), printChar(char_code), read(). Full VT100 emulator with cursor, scrolling, attributes, 8-color support, cursor blink timer.

Firmware Build

The Dockerfile at MicroPython/firmware/Dockerfile defines the build environment.

# Build Docker image (one-time)
docker build -t picocalc-build MicroPython/firmware/

# Compile firmware
docker run --rm \
  -v $(pwd)/MicroPython:/picocalc \
  -v $(pwd)/MicroPython/firmware:/out \
  picocalc-build \
  bash -c "make BOARD=RPI_PICO2_W USER_C_MODULES=/picocalc/micropython.cmake -j\$(nproc) && \
           cp build-RPI_PICO2_W/firmware.uf2 /out/picocalc_micropython_pico2w.uf2"

Critical build requirements:

  • MUST specify BOARD=RPI_PICO2_W (default is RPI_PICO for RP2040, wrong chip)
  • Dockerfile MUST pin MicroPython to a known-good commit (v1.25.0-preview, commit f187c77da). MicroPython master (v1.28.0+) has USB-CDC regression on RP2350 — device fails to enumerate over USB.
  • Dockerfile MUST init lib/cyw43-driver submodule inside pico-sdk (required for WiFi/USB on Pico 2W)
  • Run make BOARD=RPI_PICO2_W submodules before build to ensure all board deps are present
  • Known-good .uf2 is ~2.1MB. If build produces ~1.7MB, CYW43 stack is missing.

picocalcdisplay API (updated)

init(framebuf, color_type, auto_refresh), update(), beginDraw(), startAutoUpdate(), stopAutoUpdate(), setLUT(buf), drawTxt6x8(text, x, y, color).

  • beginDraw() — sets skipUpdate=true, blocks Core 1 from pushing framebuffer. Call before fill(0) to prevent flicker.
  • update() / show() — in auto mode, sets skipUpdate=false so Core 1 pushes the complete frame on next cycle.

Critical Constraints

  • multicore_lockout_victim_init() on Core 1 prevents flash write crashes
  • Cancel repeating timers before re-adding (timer slot leak on soft reset)
  • DMA for SPI transfers (double-buffered line approach)
  • Framebuffer shared between cores — use beginDraw()/show() to coordinate
  • skipUpdate volatile bool prevents Core 1 from pushing partial frames during fill(0)
  • CORE1_STACK_SIZE is 1024 uint32_t words (4KB allocated, but SDK param is byte count — currently passes 1024 meaning only 1KB used)
  • pico-sdk: hardware/spi.h, hardware/dma.h, hardware/gpio.h, pico/multicore.h
  • Function macros: OBJ_0 through OBJ_3 for fixed args, OBJ_VAR_BETWEEN for variable

Additional Resources