Building a plugin
This page builds a small plugin from scratch, for the console and for the emulator, and runs it from a Lua game.
Contents
- What you need
- A minimal plugin
- Building for the console
- Building for the emulator
- Installing and running it
- Common mistakes
- Image layout
- Placing code in fast memory
What you need
- The Arm GNU toolchain (
arm-none-eabi-gcc,arm-none-eabi-g++,arm-none-eabi-objcopy) for console builds. - A host compiler (clang, gcc or MSVC) for emulator builds.
- From the plugin SDK (called
$C8B_PLUGIN_SDKbelow):include/: the headersc8b_plugin.h(helper macros) andcore8bit_plugin.h(theC8B_APItable and constants);stm32/plugin.ld: the console linker script;stm32/startup.cpp: the console startup code.
A minimal plugin
The plugin clears the screen, draws a green square that moves up while Up is held, and stops when Start is pressed. On the console the Menu key reports C8B_BTN_START.
// hello_plugin.cpp
#include "c8b_plugin.h"
C8B_PLUGIN_GLOBALS
static int box_y = 120;
C8B_PLUGIN_MAIN { // plugin_init
c8b.log("hello: init\n");
return true; // false makes plugin_init return -1
}
C8B_PLUGIN_UPDATE { // plugin_update, once per frame
C8B_API* api = c8b.raw; // full API table
if (api->get_buttons_pressed() & C8B_BTN_START)
return false; // tell Lua to call plugin.cleanup()
if (api->get_buttons() & C8B_BTN_UP)
box_y = (box_y > 0) ? box_y - 2 : api->screen_height - 32;
api->clear(0x0000); // black, RGB565
api->fill_rect(224, box_y, 32, 32, 0x07E0); // green
return true;
}
C8B_PLUGIN_CLEANUP { // plugin_cleanup
c8b.log("hello: cleanup\n");
}
On the console, C8B_PLUGIN_MAIN first zeroes .bss, sets up the heap, runs C++ constructors and copies the ITCM/DTCM sections, then runs your code and stores the api pointer. c8b_plugin.h also offers a small c8b helper object (c8b.log, c8b.getFramebuffer() and others) next to the full table.
Building for the console
The console runs a raw binary linked at the fixed address 0xC1620000. Do not use -fPIC.
SDK=$C8B_PLUGIN_SDK
CPU="-mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard"
CXXFLAGS="$CPU -O2 -ffunction-sections -fdata-sections \
-std=c++17 -fno-exceptions -fno-rtti -fno-threadsafe-statics \
-DPLUGIN_STM32 -I$SDK/include"
arm-none-eabi-g++ $CXXFLAGS -c hello_plugin.cpp -o hello_plugin.o
arm-none-eabi-g++ $CXXFLAGS -c $SDK/stm32/startup.cpp -o startup.o
arm-none-eabi-g++ $CPU -T $SDK/stm32/plugin.ld \
-Wl,--gc-sections -Wl,--print-memory-usage \
--specs=nano.specs --specs=nosys.specs \
hello_plugin.o startup.o -o hello_plugin.elf
arm-none-eabi-objcopy -O binary hello_plugin.elf hello_plugin.bin
| Flag | Purpose |
|---|---|
-mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard |
Required: the console ABI. |
-DPLUGIN_STM32 |
Selects the console startup path in c8b_plugin.h. Use it only for console builds. |
-ffunction-sections -fdata-sections, -Wl,--gc-sections |
Removes unused code and data. |
-fno-exceptions -fno-rtti -fno-threadsafe-statics |
No exception unwinder or RTTI is available in the plugin. |
--specs=nano.specs --specs=nosys.specs |
Small C library without an operating system. |
-T plugin.ld |
Places the header, code and data at the plugin slot addresses. |
-Wl,--print-memory-usage |
Prints how full ITCM, DTCM and the slot are. |
-O2 is a good default; -O3 and -ffast-math also work.
Building for the emulator
For the emulator, build the same sources as a shared library with the host compiler. Do not define PLUGIN_STM32 and do not link startup.cpp or plugin.ld.
macOS:
clang++ -std=c++17 -O2 -fPIC -I$C8B_PLUGIN_SDK/include \
-dynamiclib -undefined dynamic_lookup \
hello_plugin.cpp -o hello_plugin.dylib.new
codesign --force --sign - hello_plugin.dylib.new
mv hello_plugin.dylib.new hello_plugin.dylib
Re-sign the library after every build and replace the file (mv) instead of overwriting it in place; macOS refuses to load a signed library that was modified in place.
Linux:
g++ -std=c++17 -O2 -fPIC -shared -I$C8B_PLUGIN_SDK/include \
hello_plugin.cpp -o hello_plugin.so
Windows (MinGW or MSVC): build hello_plugin.dll from the same source. The plugin functions are exported by C8B_EXPORT (__declspec(dllexport) on Windows), so no .def file is needed:
x86_64-w64-mingw32-g++ -shared -O2 -I$C8B_PLUGIN_SDK/include -o hello_plugin.dll hello_plugin.cpp
Installing and running it
Put the plugin and a main.lua in a game folder:
/Games/Hello/
main.lua
plugins/
hello_plugin.bin console build
hello_plugin.dylib emulator build (.so on Linux, .dll on Windows)
-- Games/Hello/main.lua
local active = false
local function start()
if not plugin.load("plugins/hello_plugin.bin") then
print("plugin.load failed")
return
end
if plugin.init() ~= 0 then
print("plugin.init failed")
return
end
active = true
end
function c8b.update()
if not active then
start()
return
end
if not plugin.update() then
plugin.cleanup()
active = false
c8b.returnToLauncher()
end
end
Use the .bin path on both platforms: the emulator replaces .bin with the host library extension. Copy the folder to /Games/Hello/ on the SD card and start it from the launcher, or run it in the emulator with core8bit_emulator --game Hello.
Common mistakes
- Touching memory before startup. The console loader does not zero
.bssor copy ITCM/DTCM code.C8B_PLUGIN_MAIN(withstm32/startup.cpp) does it before your code runs. If you write your ownplugin_init, do these steps first and touch nothing in.bss, ITCM or DTCM before the matching step. - Position-independent code. Console plugins only run at
0xC1620000. Link withplugin.ld, never with-fPIC. - Missing header. The
.binmust start with the 48-byte header with magicC8P1(0x31503843) and non-NULLinit,updateandcleanupentries.plugin.ldkeeps the.plugin_headersection first; do not strip it and always produce the.binwithobjcopy -O binary. The header is defined in the SDK'sstartup.cpp; link it into every plugin. - Slot overflow. Image,
.bssand heap share the 5 632 KB slot that ends at0xC1BA0000. The template heap stops 4 KB below that address. - Too much DTCM. The plugin DTCM area is 40 KB. The emulator rejects a plugin whose DTCM sections are larger.
- ITCM below
0x800. The first 2 KB of ITCM belong to the system. Start plugin ITCM at0x800. - Stale macOS library. Re-sign with
codesign --force --sign -and replace the file rather than overwriting it. - Newer API functions. Check
api->versionand the pointer before calling anything added after v1:if (api->version >= 7u && api->present_set_async && api->present_wait).
Image layout
The .bin contains everything from 0xC1620000 to the end of .data. .bss and the heap are not in the file.
file offset VMA section
0x000 0xC1620000 SDRAM .plugin_header (48 B)
0x030 0x00000800 ITCM .itcm_hot (copied at startup)
0x0000C000 ITCM ITCM phase overlays (optional)
0x3003FF40 D2 .d2_hot (optional)
SDRAM, in place .text
0x20016000 DTCM .dtcm_hot (copied at startup)
SDRAM, in place .rodata, .ARM.exidx, init/fini arrays, .data
(not in file) SDRAM .bss (zeroed at startup)
(not in file) end of .bss to 0xC1BA0000 - 4 KB heap
.data needs no copy: it is linked at its load address. A .dtcm_bss section is not loaded and must be zeroed by the plugin.
Memory regions in stm32/plugin.ld:
| Region | Start | Size |
|---|---|---|
CARTRIDGE (plugin slot) |
0xC1620000 |
5 632 KB |
ITCM_PLUGIN |
0x00000800 |
62 KB |
DTCM_PLUGIN |
0x20016000 |
40 KB |
Plugin header:
| Offset | Field | Value |
|---|---|---|
0x00 |
magic |
0x31503843 ("C8P1") |
0x04 |
version |
0x00010000 |
0x08 |
init |
address of plugin_init (required) |
0x0C |
update |
address of plugin_update (required) |
0x10 |
cleanup |
address of plugin_cleanup (required) |
0x14 |
call |
address of plugin_call or 0 |
0x18 |
get_exports |
address of plugin_get_exports or 0 |
0x1C–0x2F |
reserved[5] |
0 |
Placing code in fast memory
Code in SDRAM runs in place. Hot code and data run faster from ITCM and DTCM; the linker script collects the input sections .itcm_hot* and .dtcm_hot*, and the startup code copies them at init.
- Mark functions with
__attribute__((section(".itcm_hot")))and data with__attribute__((section(".dtcm_hot"))). - Or list whole object files in the linker script, for example
*r_draw.o(.text .text.*). Compile these objects without LTO. - Or rename a whole object's code:
arm-none-eabi-objcopy --rename-section .text=.itcm_hot file.o.
The copy uses word stores and ends with dsb; isb, so no ITCM function (an ITCM memcpy included) runs before it.
ITCM phase overlays (API v4)
When the hot code is larger than 62 KB, split ITCM into 46 KB resident code and one 16 KB window at 0x0000C000 (C8B_PLUGIN_ITCM_OVERLAY_VMA, C8B_PLUGIN_ITCM_OVERLAY_SIZE). In the linker script, put the phases in an OVERLAY 0x0000C000 : NOCROSSREFS block. Swap a phase in with api->load_itcm_overlay(load_address, size) and keep every function that is called across phases in resident memory.
D2 code tail (FastBuffer DATA_PLUS_CODE)
A .d2_hot section can be linked at C8B_D2_CODE_VMA (0x3003FF40, 30 KB). At init:
- Call
api->set_fast_buffer_mode(C8B_FB_MODE_DATA_PLUS_CODE). - Check that
get_fast_buffer() + get_fast_buffer_size()is at most the start of.d2_hot, and that.d2_hotends within 30 KB of it. - Call
api->fast_buffer_wait(). - Copy the
.d2_hotload image to0x3003FF40. - Call
api->invalidate_icache(addr, size).
The FastBuffer then has 98 KB for data.