这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 【瑞萨RA8D1测评】LVGL显示动画

共1条 1/1 1 跳转至

【瑞萨RA8D1测评】LVGL显示动画

菜鸟
2026-09-19 16:43:32     打赏

前两篇把 IMU 的采集、训练、端侧部署走通了,三个动作在板上推理一次 18 ms,但屏幕是黑的。这篇把LVGL加上

素材用github.com/ChaozhongLiu/DyberPet/blob/main/docs/collection.md,一个守岸人的桌宠动图,这个项目只要三组:default 待机、aggrieved 委屈、butterfly 化蝴蝶。手势对应动画,静止→待机,晃动→委屈,拿起→化蝴蝶。

compare_aggrieved.png

compare_butterfly.png

compare_default.png

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

然后需要在e2studio里勾上 LVGL 

屏幕截图 2026-09-19 161818.png

编译,发现很多错误,重点是

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 的倍数。

image.png

image.png

正确的组合是显示缓冲宽度 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 就够用

效果暂时是这样的

image.png

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


共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]