前两篇把 IMU 的采集、训练、端侧部署走通了,三个动作在板上推理一次 18 ms,但屏幕是黑的。这篇把LVGL加上
素材用github.com/ChaozhongLiu/DyberPet/blob/main/docs/collection.md,一个守岸人的桌宠动图,这个项目只要三组:default 待机、aggrieved 委屈、butterfly 化蝴蝶。手势对应动画,静止→待机,晃动→委屈,拿起→化蝴蝶。



素材是一堆 PNG,每组动作一个尺寸。写了个转换脚本,把每组帧量化成 8bpp 调色板索引、逐帧 RLE 压缩,导出成 .c/.h——.c 里是调色板、帧表和压缩后的大数组,设备端查表边解压边用。三组合计 926 KB。
然后需要在e2studio里勾上 LVGL

编译,发现很多错误,重点是
fatal error: 'FreeRTOSConfig.h' file not found
原因是 LVGL 的 LV_USE_OS 选项默认值就是 LV_OS_FREERTOS,它会去 include FreeRTOS。
我去Components 里勾了FreeRTOS,但是还是报同样的错,我去查了查,原来components里勾FreeRTOS,只是把内核源码(tasks.c queue.c 那些)拷进 ra/aws/,它不生成 FreeRTOSConfig.h 和端口层。工程到底是不是 FreeRTOS 工程,是建工程那一步是否选RTOS定死的,写死在 configuration.xml 里,配置器界面改不了:
而且拷进来的源码是残的,没有 portable/ 目录,也就没有 portmacro.h、port.c、heap_4.c。手写一个 FreeRTOSConfig.h 塞进去也没用,下一步照样卡在 portmacro.h。端口层和配置头都是 FSP 生成 RTOS 工程时才吐出来的东西。
那就不要多任务,直接走裸机,不过需要将LV_USE_OS 改成 LV_OS_NONE,让LVGL不依赖rtos
还有需要把字体关掉,不然占用太多
板上屏是 H0233S001,222×480 竖屏,ST7796U,不带触摸。222 这个宽度很别扭,手册 Table 56.2 的数据对齐要求里,Macro Line 的偏移量要 64 字节对齐:222 像素 × RGB565 = 444 字节,不是 64 的倍数。


正确的组合是显示缓冲宽度 224、LCD 输出宽度 222
hsize 填真实宽度 222,生成出来就是 .hsize=222、.hstride=224,正好
播放器就一个 lv_image 对象,每帧解出来改描述符的宽高、改对象坐标。
/* pet_anim.c — 调色板动画解码 + LVGL image 显示 */
#include "pet_anim.h"
#include "lvgl.h"
#include "misc/cache/instance/lv_image_cache.h" /* lvgl.h 没带,换帧 drop 缓存要用 */
#include "pet/sk_default.h"
#include "pet/sk_aggrieved.h"
#include "pet/sk_butterfly.h"
#define PET_Y_OFFSET ((480 - SK_DEFAULT_CANVAS_H) / 2)
#define PET_MAX_PIXELS (SK_DEFAULT_MAX_RAW_BYTES) /* 按最大一帧开 */
typedef struct {
const sk_frame_t *p_frames;
const uint16_t *p_palette;
const uint8_t *p_blob;
uint16_t frame_count;
uint16_t act_num;
uint16_t frame_ms;
} pet_action_t;
static const pet_action_t g_actions[PET_ACT_COUNT] = {
[PET_ACT_DEFAULT] = { sk_default_frames, sk_default_palette, sk_default_blob,
SK_DEFAULT_FRAME_COUNT, SK_DEFAULT_ACT_NUM, SK_DEFAULT_FRAME_MS },
[PET_ACT_AGGRIEVED] = { sk_aggrieved_frames, sk_aggrieved_palette, sk_aggrieved_blob,
SK_AGGRIEVED_FRAME_COUNT, SK_AGGRIEVED_ACT_NUM, SK_AGGRIEVED_FRAME_MS },
[PET_ACT_BUTTERFLY] = { sk_butterfly_frames, sk_butterfly_palette, sk_butterfly_blob,
SK_BUTTERFLY_FRAME_COUNT, SK_BUTTERFLY_ACT_NUM, SK_BUTTERFLY_FRAME_MS },
};
static uint16_t g_pixels[PET_MAX_PIXELS]; /* 片内 SRAM,CPU 写 GPU 读 */
static lv_image_dsc_t g_dsc;
static lv_obj_t *g_img;
static pet_act_t g_act = PET_ACT_DEFAULT;
static pet_act_t g_pending = PET_ACT_DEFAULT;
static uint16_t g_frame = 0U;
static uint16_t g_loops = 0U;
static uint32_t g_next_ms = 0U;
/* RLE:0x00..0x7F 跟 n+1 个字面量,0x80..0xFF 下个字节重复 (n&0x7F)+2 次,边解边查表 */
static void rle_expand(const uint8_t *src, uint16_t *dst, uint32_t count, const uint16_t *pal)
{
uint32_t i = 0U;
while (i < count) {
uint8_t ctrl = *src++;
if (ctrl & 0x80U) {
uint32_t run = (ctrl & 0x7FU) + 2U;
uint16_t val = pal[*src++];
while (run--) dst[i++] = val;
} else {
uint32_t n = ctrl + 1U;
while (n--) dst[i++] = pal[*src++];
}
}
}
/* 每帧尺寸位置都不同,描述符和坐标跟着改;改完 drop 缓存,否则 LVGL 拿旧帧 */
static void show_frame(void)
{
const pet_action_t *a = &g_actions[g_act];
const sk_frame_t *f = &a->p_frames[g_frame];
rle_expand(a->p_blob + f->offset, g_pixels, f->raw_len, a->p_palette);
g_dsc.header.w = f->w;
g_dsc.header.h = f->h;
g_dsc.header.stride = (uint32_t)f->w * 2U;
g_dsc.data_size = (uint32_t)f->raw_len * 2U;
lv_image_cache_drop(&g_dsc);
lv_image_set_src(g_img, &g_dsc);
lv_obj_set_pos(g_img, f->x, f->y + PET_Y_OFFSET);
}
static void start_action(pet_act_t act)
{
g_act = act;
g_frame = 0U;
g_loops = g_actions[act].act_num;
g_next_ms = lv_tick_get() + g_actions[act].frame_ms;
show_frame();
}
void pet_anim_init(void)
{
lv_obj_t *scr = lv_screen_active();
lv_obj_set_style_bg_color(scr, lv_color_black(), LV_PART_MAIN);
g_dsc.header.magic = LV_IMAGE_HEADER_MAGIC;
g_dsc.header.cf = LV_COLOR_FORMAT_RGB565;
g_dsc.data = (const uint8_t *)g_pixels;
g_img = lv_image_create(scr);
start_action(PET_ACT_DEFAULT);
}
void pet_anim_request(pet_act_t act)
{
if (act == g_act) return;
g_pending = act;
if (act != PET_ACT_DEFAULT) start_action(act); /* 待机兜底不抢,其他立刻上 */
}
void pet_anim_tick(void)
{
const pet_action_t *a = &g_actions[g_act];
if ((int32_t)(lv_tick_get() - g_next_ms) < 0) return; /* 带符号,回绕不卡 */
g_next_ms += a->frame_ms;
g_frame++;
if (g_frame < a->frame_count) { show_frame(); return; }
g_frame = 0U;
if (g_act == PET_ACT_DEFAULT) { show_frame(); return; } /* 待机无限循环 */
if (g_loops > 1U) { g_loops--; show_frame(); return; }
start_action(g_pending); /* 播够遍数,回分类器当下想要的状态 */
}动作切换定了条规则:分类每 0.5 秒出一次结果,每次都直接切的话结果一抖表情就乱跳。所以非待机动作立刻打断当前播放;请求回待机要等当前动作把 act_num 遍播完;播完回到分类器当下想要的状态——还在摇就接着委屈,停了就回待机。
上一篇那套IMU采集是死等:循环里 while 空转,采满 300 个样本再推理,堵 3 秒。只打 RTT 无所谓,现在动画 120 ms 一帧,被堵 3 秒就是 25 帧空白,直接卡死。
改成环形缓冲加滑动窗口,全放主循环里跑:主循环每过一遍问一次 poll,它自己看 DWT 计数器,到点采一个样本,没到立刻返回;攒够 50 个新样本(0.5 秒)才把环形缓冲摊平成 300×6 跑一次推理。摊平用两次 memcpy,g_head 既是下一个写入位置也是最老样本的位置。
/* imu_app.c — 100Hz 非阻塞采集,攒够窗口就推理 */
#include "common_utils.h"
#include "imu_app.h"
#include "mpu6050.h"
#include <string.h>
int8_t g_imu_input[IMU_INFER_INPUT_LEN]; /* 摊平后喂模型,channels-last */
static int8_t g_out[IMU_INFER_CLASSES];
static uint32_t g_cycles_per_smp = 0U;
/* DWT 周期计数器给采样定拍。固定延时会把 I2C 传输时间算进每个间隔,
* 3 秒窗口会被拉长约 15%,模型没在这种时间尺度上训过。 */
static uint32_t dwt_now(void) { return DWT->CYCCNT; }
static bool dwt_init(void)
{
*(volatile uint32_t *)0xE000EDFCUL |= (1UL << 24); /* TRCENA */
DWT->CYCCNT = 0U;
DWT->CTRL |= (1UL << 0); /* CYCCNTENA */
uint32_t before = dwt_now();
for (volatile uint32_t i = 0U; i < 64U; i++) {}
return dwt_now() != before;
}
/* 原始计数 -> 工程量。写死 2g=16384 LSB/g、250dps=131 LSB/dps(×10 保整数),
* 改量程这两个也得跟着改。 */
static int32_t accel_to_mg(int16_t r) { return ((int32_t)r * 1000) / 16384; }
static int32_t gyro_to_mdps(int16_t r) { return ((int32_t)r * 10000) / 1310; }
/* 环形缓冲:主循环每过一遍问一次 poll,到点采一个样本,攒够 HOP 个新样本才推一次 */
static int8_t g_ring[IMU_APP_SAMPLES][IMU_INFER_CHANNELS];
static uint32_t g_head = 0U; /* 下一个写入槽,也是最老样本位 */
static uint32_t g_fill = 0U; /* 已采样本数,封顶 IMU_APP_SAMPLES */
static uint32_t g_since = 0U; /* 距上次推理又采了几个 */
static uint32_t g_next_cyc = 0U;
bool imu_app_start(void)
{
g_cycles_per_smp = R_FSP_SystemClockHzGet(FSP_PRIV_CLOCK_ICLK) / IMU_APP_RATE_HZ;
if (!dwt_init()) return false;
if (FSP_SUCCESS != mpu6050_open(MPU6050_ACCEL_FS_2G, MPU6050_GYRO_FS_250DPS)) return false;
if (!imu_infer_init()) return false;
g_head = g_fill = g_since = 0U;
g_next_cyc = dwt_now();
return true;
}
int32_t imu_app_poll(void)
{
if ((int32_t)(dwt_now() - g_next_cyc) < 0) return -1; /* 没到采样点,立刻走 */
g_next_cyc += g_cycles_per_smp;
mpu6050_raw_t raw;
if (FSP_SUCCESS != mpu6050_read_raw(&raw)) return -1;
/* 通道必须 ax ay az gx gy gz,中间不要 temp —— raw 里 temp 夹在两组中间,
* 只能逐字段抄,不能整块拷。 */
int8_t *s = g_ring[g_head];
s[0] = imu_infer_quant_accel(accel_to_mg(raw.accel_x));
s[1] = imu_infer_quant_accel(accel_to_mg(raw.accel_y));
s[2] = imu_infer_quant_accel(accel_to_mg(raw.accel_z));
s[3] = imu_infer_quant_gyro(gyro_to_mdps(raw.gyro_x));
s[4] = imu_infer_quant_gyro(gyro_to_mdps(raw.gyro_y));
s[5] = imu_infer_quant_gyro(gyro_to_mdps(raw.gyro_z));
g_head = (g_head + 1U) % IMU_APP_SAMPLES;
if (g_fill < IMU_APP_SAMPLES) g_fill++;
g_since++;
if ((g_fill < IMU_APP_SAMPLES) || (g_since < IMU_APP_HOP)) return -1;
g_since = 0U;
/* 摊平成模型要的时间顺序:g_head 既是下一个写入位也是最老样本 */
uint32_t tail = (IMU_APP_SAMPLES - g_head) * IMU_INFER_CHANNELS;
memcpy(g_imu_input, g_ring[g_head], tail);
memcpy(&g_imu_input[tail], g_ring[0], (size_t)g_head * IMU_INFER_CHANNELS);
return imu_infer_run(g_imu_input, g_out);
}主循环于是干净成这样:
/* hal_entry.c — 点屏 → 起 LVGL → 主循环同时喂动画和跑 IMU 分类 */
#include "hal_data.h"
#include "board_sdram.h"
#include "mipi_dsi_ep.h"
#include "imu_app.h"
#include "pet_anim.h"
#include "lvgl.h"
#include "rm_lvgl_port.h"
extern void ST7796U_init_HW(void); /* mipi_dsi_ep.c 里的 LCD 复位时序,没给头文件 */
/* 模型输出顺序 still / shake / pickup,映射到三个动作 */
static const pet_act_t g_cls_to_act[IMU_INFER_CLASSES] = {
PET_ACT_DEFAULT, /* still -> 待机 */
PET_ACT_AGGRIEVED, /* shake -> 委屈 */
PET_ACT_BUTTERFLY, /* pickup -> 化蝴蝶 */
};
static bool display_init(void)
{
bsp_sdram_init(); /* 帧缓冲在 SDRAM,不先初始化 GLCDC 一启动就读到垃圾 */
ST7796U_init_HW(); /* LCD 硬复位 */
lv_init();
/* 这一步里 R_GLCDC_Open()+Start(),并把两块 fb 接成 LVGL 绘制缓冲,双缓冲白送 */
if (FSP_SUCCESS != RM_LVGL_PORT_Open(&g_lvgl_port_ctrl, &g_lvgl_port_cfg)) return false;
/* DCS 初始化表只能在这之后推:GLCDC 的 Open 会顺带打开 MIPI DSI,而 PORT_Open 是
* Open+Start 连着做的插不进去,所以命令等视频流跑起来后靠 LP 模式在消隐期发。
* 上电后屏一直黑,第一个怀疑这里。 */
mipi_dsi_push_table(g_lcd_init_focuslcd);
return true;
}
void hal_entry(void)
{
R_IOPORT_Open(&g_ioport_ctrl, g_ioport.p_cfg); /* 不开引脚配置不生效 */
if (!display_init()) for (;;) {}
pet_anim_init();
bool imu_ok = imu_app_start(); /* 没起来动画照播,只是永远停在待机 */
for (;;)
{
lv_timer_handler();
pet_anim_tick();
if (imu_ok)
{
int32_t cls = imu_app_poll();
if ((cls >= 0) && (cls < IMU_INFER_CLASSES))
pet_anim_request(g_cls_to_act[cls]);
}
}
}裸机 + Dave2D 一定要把 BSP 堆调大。默认配置下会在第一次绘制时炸:
BFSR 0x82 (PRECISERR + BFARVALID,精确总线故障)
默认的堆只有4k,改到 64 KB 就够用
效果暂时是这样的

新的板子还没到,等到了再完整演示。不过已经能看到轮廓了,依据手势换动画也没问题
我要赚赏金
