先把 PixelUI 的显示、时间、输入和刷新链路跑通。
Tickless 模式当前处于 Experimental 阶段,并未经测试。
1. 准备依赖
PixelUI 依赖 U8g2 和 ETL,主仓库通过 submodule 引入它们:
1 | git clone --branch v0.3.1-beta --recursive https://github.com/Lawrence-Link/PixelUI.git |
工程使用 C++20。PixelUI 核心关闭 exceptions/RTTI,并定义 ETL_NO_STL。移植到自己的构建系统时,需要保持这些编译条件一致。
请首先配置好 U8G2 与 ETL ( 确保编译环境可以链接到 ETL Library )
2. 测试 U8g2 单独工作
PixelUI 使用 U8g2 的全屏 framebuffer。接入 PixelUI 前,先确认底层显示正常:
1 | u8g2.clearBuffer(); |
3. 创建 PixelUI
1 |
|
U8G2 理应比 PixelUI 活得更久。这在嵌入式系统里一般不是问题。PixelUI 内联了 App arena 和各个 manager,不是一个小句柄。建议放在全局/静态存储区,不要放进小容量 RTOS 任务栈。
begin() 在当前版本是保留的空初始化点;它不会替你初始化 U8g2 或启动 App。
4. 先理解 PixelUI 怎样运行
PixelUI 是一个由宿主驱动的 UI 库,它不会自己创建线程、定时器或无限循环。这样才能同时适应裸机、RTOS 和桌面模拟器,但也意味着平台需要主动把三类事情交给它:
- 时间经过了多久:动画、Popup 超时、Coroutine 延时和页面 fade 都要依靠内部精准的时间戳。时间戳通过心跳 (ticking, 也就是heartbeat) 在内部进行不断累加。
- 用户做了什么:各种按键或编码器事件通过
handleInput()路由进入 Focus、Popup 和当前 App。 - 画面是否需要更新:状态改变后用
markDirty()请求新帧,最后由renderer()在 U8g2 framebuffer 中绘制并发送。
为什么需要 heartbeat
PixelUI 不能假设每个平台都有相同的时钟 API。宿主需要把真实经过的毫秒数交给 heartbeat(elapsedMs),或在定时 ISR 中交给 tickFromISR(elapsedMs)。它们只输入时间;renderer() 会推进到期状态,并只在需要时提交 framebuffer。完整语义见事件驱动渲染。
5. 接入主循环
1 |
|
最简单的轮询接入
在裸机或简单单线程系统中,可以在同一个循环中派发输入、尝试绘制,并用类似 millis() 的 API 读取系统时间戳。
1 | uint32_t previous = platform_millis(); |
now - previous 必须是真实经过的时间。即使目标是 60 FPS,也不要固定传 16;某次循环实际间隔 35 ms,就传 35 ms。
renderer() 会先调用 process(),再判断是否需要提交一帧。
6. RTOS 和定时器 ISR
在 RTOS / 硬件定时器ISR 中,定时器 ISR 和 UI 逻辑往往不在同一上下文。这时使用 tickFromISR() 代替普通 heartbeat():它只原子累加时间并可选唤醒 UI task。Widget、App、Popup、动画和导航 API 仍全部留在同一个 UI task:
1 | void timer_isr(uint32_t elapsedMs) { |
setTaskNotifyFromISR() 可安装平台的 ISR-safe 唤醒函数。不要混用 tickFromISR() 和 heartbeat() 注入同一段时间,也不要从 ISR 直接调 handleInput() 或 renderer()。
这里 handler(16U) 的 16U 是期望的帧周期,不是已经过去的时间。
Tickless 的 deadline 和外部事件唤醒规则见事件驱动渲染。
7. 刷新与唤醒
什么时候刷新?
![]()
setRefreshCallback():一帧送入 U8g2 buffer 后通知宿主。setRenderRequestCallback():clean 变 dirty 时合并唤醒 UI task;不能在回调中同步重入renderer()。markDirty():业务数据改变后请求下一帧。
8. 下一步
没有启动 App 时空屏是正常的。接下来可以启动 AppLauncher,或构建第一个 App。更完整的调度说明见事件驱动渲染。
