- Variant
- mtb-mpy and mtb-only
CM55 is variant-agnostic; everything in this chapter applies to both packages.
Learning goal
Wire a new page into the CM55 UI so that it is reachable, and know — from having reproduced each one — what every partial wiring state looks like. The worked example is Edge AI, the only small page in the shipped template with all three fragments present and a documented create/render/destroy contract. The README's suggested templates, motion and environ, are not used here, for a reason given in the traps.
Real firmware sequence
(1) The enum entry — page_manager.h:65, PAGE_ID_EDGE_AI = 13. The surrounding contract is ABI:
PM_MAX_PAGES is 24U (page_manager.h:21) and a _Static_assert at :79-80 refuses a page_id_t that exceeds it.
(2) The callback triple — page_def_t (page_manager.h:87-95):
typedef struct {
const char *name;
const char *subtitle;
uint32_t accent_color;
lv_obj_t *(*create_cb)(void);
void (*destroy_cb)(void);
bool cacheable;
} page_def_t;
Registered for Edge AI at sensorhub_ui.c:254-266, guarded by BENTO_HAS_EDGE_AI:
#if defined(BENTO_HAS_EDGE_AI) && (BENTO_HAS_EDGE_AI == 1)
{
page_def_t def = {
.name = "Edge AI",
.subtitle = "On-device inference",
.accent_color = UI_COLOR_ACCENT_PURPLE,
};
pm_register(&s_pm, PAGE_ID_EDGE_AI, &def);
}
page_edge_ai_create/render/destroy are never called directly — the page manager owns the lifecycle and calls them in GFX-task context. Their bodies are prebuilt (proj_cm55/Makefile:994 ignores page_edge_ai.c; the symbols come from libbento_cm55.a), so for the source of a triple read page_motion.c:106/:210/:267.
(3) The Home card — page_home.c:147-149, inside s_card_defs[]:
typedef struct {
page_id_t id;
const char *title;
const char *icon;
uint32_t color;
const lv_font_t *icon_font;
const lv_image_dsc_t *icon_img;
} home_card_def_t;
static const home_card_def_t s_card_defs[] = {
{ PAGE_ID_DASHBOARD, "Sensor Dashboard", NULL, UI_COLOR_SENSOR_IMU, NULL, &icon_dashboard },
#if ENABLE_PAGE_GPIO_RGB
{ PAGE_ID_GPIO_RGB, "GPIO & RGB Matrix", NULL, UI_COLOR_ACCENT_GREEN, NULL, &icon_touch_rgb },
#endif
#if ENABLE_PAGE_MOTOR_CTRL
{ PAGE_ID_MOTOR_CTRL, "Motor Controller", LV_SYMBOL_REFRESH, UI_COLOR_ACCENT_ORANGE, NULL, NULL },
#endif
#if defined(BENTO_HAS_EDGE_AI) && (BENTO_HAS_EDGE_AI == 1)
{ PAGE_ID_EDGE_AI, "Edge AI", NULL, UI_COLOR_ACCENT_PURPLE, NULL, &icon_ai_chip },
#endif
Where the three meet at runtime. Each card binds card_click_cb with its page id:
s_ctx.card_page_ids[i] = def->id;
lv_obj_add_flag(card, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(card, card_click_cb, LV_EVENT_CLICKED,
&s_ctx.card_page_ids[i]);
…which calls pm_navigate:
void pm_navigate(page_manager_t *pm, page_id_t target)
{
if (pm->animating) return;
if (target >= pm->page_count) return;
if (pm->pages[target].create_cb == NULL) return;
if (target == pm->current_page) return;
page_id_t leaving = pm->current_page;
bool cache_leaving = pm->pages[leaving].cacheable;
if (leaving == PAGE_ID_PLAYGROUND) {
pm_notify_sensor_resume();
}
if (cache_leaving) {
pm->cached_screens[leaving] = lv_screen_active();
} else {
if (pm->pages[leaving].destroy_cb) {
pm->pages[leaving].destroy_cb();
}
}
if (pm->nav_top < PM_NAV_STACK_DEPTH - 1) {
pm->nav_top++;
pm->nav_stack[pm->nav_top] = leaving;
}
lv_obj_t *new_scr;
if (pm->pages[target].cacheable && pm->cached_screens[target] != NULL) {
new_scr = pm->cached_screens[target];
pm->cached_screens[target] = NULL;
} else {
new_scr = pm->pages[target].create_cb();
}
if (new_scr == NULL) return;
lv_obj_t *leaving_scr = lv_screen_active();
lv_obj_remove_event_cb(leaving_scr, pm_unloaded_cb);
lv_obj_add_event_cb(leaving_scr, pm_unloaded_cb,
LV_EVENT_SCREEN_UNLOADED, pm);
pm->animating = true;
lv_screen_load_anim(new_scr, LV_SCR_LOAD_ANIM_MOVE_LEFT,
PM_ANIM_TIME_MS, 0, !cache_leaving);
create_cb runs, the screen slides left over PM_ANIM_TIME_MS (300, page_manager.h:29), render_cb is driven only by pm_render from the 33 ms timer (sensorhub_ui.c:166) with the tick's sensorhub_snapshot_t, and Back calls destroy_cb (page_manager.c:107-108).
The five Makefile touch points the README omits
template/README.md:95-97 says "three places". A new page directory needs these as well, all by hand (bento.sh:245-253 offers doctor|menus|enable|disable|remove|build|flash|verify|clean — there is no add):
| # | What | proj_cm55/Makefile |
| 1 | Flag default ENABLE_PAGE_X ?= 1 | :23-30 |
| 2 | Auto-guard: $(if $(filter 1,$(ENABLE_PAGE_X)),$(if $(wildcard $(_PD)/x/.),,$(eval ENABLE_PAGE_X:=0))) | :65-72 |
| 3 | CY_IGNORE line when the flag is 0 | :636-700 |
| 4 | INCLUDES+=modules/page-components/<dir> — no wildcard; only _core and listed dirs are on the include path | :737-798 |
| 5 | DEFINES+=ENABLE_PAGE_X=$(ENABLE_PAGE_X) | :897-898 |
Plus the #include "page_x.h" in sensorhub_ui.c:34-79.
Step by step — reproduce all four wiring states deliberately
Do these in order on a copy of the Edge AI fragments renamed to your page; rebuild CM55 between steps. There is no UART output on this path (proj_cm55/main.c:9-10) — every observable is screen state.
State 1 — enum only
Add PAGE_ID_X = 24, at the end of page_id_t (next free number) and raise PM_MAX_PAGES if the static assert fires.
What you should observe. Build succeeds. Home grid unchanged. Silent.
State 2 — enum + pm_register, no card
Add the pm_register block and the Makefile touch points, but no s_card_defs[] entry.
What you should observe. Build succeeds; the board boots; the Home grid is identical; the page is dead. This is the shipped state of Motion and Environ — registered at sensorhub_ui.c:235 and :250, enabled by default (proj_cm55/Makefile:23-24), absent from page_home.c:138-200. README.md:216 names the diagnostic: "Menu is missing from Home | its flag is 0, or s_card_defs[] has
no entry. ./bento.sh menus tells you which."
State 3 — card, no pm_register
Comment out the pm_register block; keep the card.
What you should observe. The card renders. Tapping it does nothing — pm_navigate returns on create_cb == NULL (page_manager.c:92). It is silently ignored, not a crash. README.md:100 and :217 say it "crashes the moment it is tapped"; that claim is stale.
State 4 — all three
Restore the pm_register block.
What you should observe. A new card at the position of its s_card_defs[] entry, with the accent colour from .color; a tap slides left over 300 ms; render_cb begins firing at ~33 Hz; Back slides right (page_manager.c:198) and destroy_cb runs.
Two compile-time failure signatures worth seeing once
- Omit touch point 4 → fatal error: page_x.h: No such file or directory from sensorhub_ui.c. A compile error, not a link error.
- Renumber an existing page → page_id_ordinal_assert.c:22-25 fires: "PAGE_ID_PLAYGROUND has moved.
lib/ipc_core/libbento_ipc.a was built for ordinal 7 and stores it as an immediate; a mismatch links
cleanly and compares the wrong page at runtime."
Traps
- Warning
- Do not copy motion or environ as your template. Both are the "registered, no card" state the README itself calls the most common mistake. A reader who mirrors all three sites from a model that only wires two will see nothing.
-
page_id_t ordinals are ABI. libbento_ipc.a bakes PAGE_ID_PLAYGROUND == 7 (lib/ipc_core/PROVENANCE.txt). Add at the end; never renumber, never re-guard. The rationale comment in page_id_ordinal_assert.c:11-13 still describes the old guarded enum — the assert is right, its comment is stale.
-
All LVGL work in the GFX task. Your create_cb/render_cb/destroy_cb already run there. If your page hosts IPC-driven widgets, bind the containers on create and NULL them on destroy (Chapter F2, and B3).
-
page_edge_ai.h:12-14 says "PM_MAX_PAGES is 20 … 18 pages". Actual: 24 and 25 registrations (id-indexed, so safe). Do not size anything from that comment.
Variant box
Identical on both variants. The only variant-specific line in proj_cm55/Makefile is the MPY gate at :237 and the version label; page wiring does not touch it.