Quick Start

First connect PixelUI’s display, time, input, and rendering paths. Tickless mode is currently experimental and has not been tested.

1. Prepare dependencies

PixelUI depends on U8g2 and ETL. The main repository includes them as submodules:

1
2
3
git clone --branch v0.3.1-beta --recursive https://github.com/Lawrence-Link/PixelUI.git
cd PixelUI
git submodule update --init --recursive

The project uses C++20. PixelUI core disables exceptions and RTTI and defines ETL_NO_STL. Keep these compiler conditions consistent when integrating it into another build system.

Configure U8g2 and ETL first, and make sure your build environment can link the ETL library.

2. Test U8g2 by itself

PixelUI uses U8g2’s full-screen framebuffer. Before adding PixelUI, confirm that the underlying display works:

1
2
3
4
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(0, 12, "U8g2 works");
u8g2.sendBuffer();

3. Create PixelUI

1
2
3
4
#include "PixelUI.h"

U8G2 u8g2;
PixelUI ui(u8g2);

U8G2 must outlive PixelUI. This is normally straightforward in an embedded system. PixelUI contains the App arena and all managers inline; it is not a small handle. Prefer global or static storage instead of a small RTOS task stack.

In the current version, begin() is a reserved empty initialization point. It does not initialize U8g2 or launch an App for you.

4. Understand how PixelUI runs

PixelUI is a host-driven UI library. It does not create its own thread, timer, or infinite loop. This makes it usable on bare metal, RTOS, and desktop simulators, but the platform must actively provide three things:

  1. Elapsed time: Animations, Popup timeouts, Coroutine delays, and page fades depend on an accurate internal timestamp. The timestamp is accumulated through ticking, called a heartbeat in the API.
  2. User actions: Key or encoder events enter through handleInput() and are routed to Focus, Popup, and the current App.
  3. Rendering requests: Call markDirty() after state changes, then let renderer() draw and send the U8g2 framebuffer when needed.

Why heartbeat is required

PixelUI cannot assume that every platform exposes the same clock API. The host passes real elapsed milliseconds to heartbeat(elapsedMs), or to tickFromISR(elapsedMs) in a timer ISR. These functions only provide time. renderer() advances due state and submits the framebuffer only when needed. See Event-driven rendering for complete semantics.

5. Connect the main loop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include "PixelUI.h"
#include "core/app/MyApp.h"

U8G2 display;
PixelUI ui(display);

constexpr uint32_t UI_TIMER_INTERVAL_MS = 1U;

/*
* STM32 HAL timer interrupt callback.
*
* Assume TIM2 produces one update interrupt every 1 ms.
*/
extern "C" void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim)
{
if (htim->Instance == TIM2) {
/*
* Only provide elapsed time here.
*
* Do not call these APIs from an ISR:
* ui.process()
* ui.renderer()
* ui.handleInput()
* Widget/Popup/Animation APIs
*/
ui.tickFromISR(UI_TIMER_INTERVAL_MS);
}
}

int main()
{
HAL_Init();
SystemClock_Config();

MX_GPIO_Init();
MX_SPI1_Init(); // or MX_I2C1_Init()
MX_TIM2_Init(); // configure one update interrupt every 1 ms

display.begin(); // initialize the display first
display.setPowerSave(0);

ui.begin();

ViewManager& views = *ui.getViewManagerPtr();

// Start interrupts only after PixelUI, display, and App setup is complete.
HAL_TIM_Base_Start_IT(&htim2);

const ViewManager::LaunchResult result = AppLauncher::launch(ui, views);
if (result != ViewManager::LaunchResult::Ok) {
Error_Handler();
}

while (true) {
InputEvent event;
while (readInputEvent(event)) {
ui.handleInput(event); // consume input events
}

ui.renderer(); // render when needed
}
}

Simplest polling integration

On bare metal or a simple single-threaded system, dispatch input and attempt rendering in one loop while reading the system timestamp from an API similar to millis():

1
2
3
4
5
6
7
8
9
10
11
12
uint32_t previous = platform_millis();
for (;;) {
const uint32_t now = platform_millis();
ui.heartbeat(now - previous); // actual elapsed milliseconds
previous = now;

InputEvent event;
while (read_input_event(event)) ui.handleInput(event);

ui.renderer();
platform_delay_ms(5);
}

now - previous must be the real elapsed time. Do not always pass 16 just because the target is 60 FPS. If one loop actually takes 35 ms, pass 35 ms.

renderer() calls process() first and then decides whether a frame needs to be submitted.

6. RTOS and timer ISRs

In an RTOS or hardware-timer integration, the timer ISR and UI logic usually run in different contexts. Use tickFromISR() instead of heartbeat(): it only atomically accumulates time and may optionally wake the UI task. Widget, App, Popup, Animation, and navigation APIs still remain in one UI task:

1
2
3
4
5
6
7
8
9
10
11
12
void timer_isr(uint32_t elapsedMs) {
ui.tickFromISR(elapsedMs);
}

void ui_task() {
for (;;) {
wait_for_ui_notification();
dispatch_input_and_data_events();
const uint32_t nextDelay = ui.handler(16U);
program_next_ui_timer(nextDelay);
}
}

setTaskNotifyFromISR() installs a platform-specific ISR-safe wakeup function. Do not use both tickFromISR() and heartbeat() for the same elapsed interval, and do not call handleInput() or renderer() directly from an ISR.

The 16U passed to handler(16U) is the desired frame period, not elapsed time.

See Event-driven rendering for tickless deadlines and external event wakeups.

7. Refresh and wakeup

When does the display refresh?

Refresh behavior

  • setRefreshCallback() notifies the host after a frame has been sent to the U8g2 buffer.
  • setRenderRequestCallback() coalesces a wakeup when clean becomes dirty; the callback must not synchronously re-enter renderer().
  • markDirty() requests the next frame after application data changes.

8. Next step

A blank screen is normal when no App has been launched. Next, launch AppLauncher or build your first App. See Event-driven rendering for the complete scheduling model.