LVGL源码(7):渲染

LVGL源码(4):LVGL关于EVENT事件的响应逻辑_lvgl实现显示打车-CSDN博客这篇文章中,我们提到了LVGL的三大步骤:检测用户输入操作、调用我们编写的逻辑、在屏幕上显示对应的画面;而在学习完"样式"之后,我们或许可以将上述步骤说明的更详细一些,即检测用户输入操作->调用控件的系统逻辑和我们编写的逻辑->如果控件样式改变则重新渲染->在屏幕上显示对应的画面;

其实我们可以认为,UI的变化就是各个不同控件样式的变化,而样式变化就会涉及到如何渲染,在本篇文章中我们就来粗略的讲一下LVGL的渲染逻辑,如有错误,忘指正。

渲染图层:

LVGL 使用分层(Layering)渲染,主要分为以下几种不同类型的图层(Layer Types)

1、普通渲染:

大部分 静态 UI 组件(如 label, button, image)在主帧缓冲区中直接绘制,不需要额外的中间层。主帧缓冲区就是我们在lv_por_disp.c中定义的 static lv_disp_draw_buf_t draw_buf_dsc_x;

2、中间层(Intermediate Layers):

当某些特效(opacity, blend_mode, transform_angle, transform_zoom) 启用时,LVGL 不会直接绘制对象,而是创建一个中间渲染层(Intermediate Layer),这通常是一个 离屏缓冲区(offscreen buffer),先在这里绘制,之后再合成到主屏幕,也被称为 "临时快照"(Snapshot) 或 "混合层"(Blending Layer),触发中间层的情况如下:

(1)style_opa < 255(具有不透明度的控件)或者控件使用了blend_mode(使用混合模式):其中间层缓冲区分配方式在lv_conf.h中的

#define LV_LAYER_SIMPLE_BUF_SIZE (24 * 1024)(小块缓冲区,减少内存占用)

#define LV_LAYER_SIMPLE_FALLBACK_BUF_SIZE (3 * 1024)(备用缓冲区,防止分配失败)

这种方式也称之为软件ARGB 渲染,A、R、G、B(透明+红绿蓝)通常 为32bit,支持透明度(alpha),R、G、B(红绿蓝)通常 24bit 或 16bit,不支持透明度,这是由显示驱动决定的,属于硬件层面。当使用RGB想要有透明度效果时就会使用上述中间渲染层。

(2)控件使用了transform_angle(旋转)、transform_zoom(缩放),由lv_mem_alloc 动态分配;

(3)当显示驱动启用了软件旋转功能(比如 90°/180°/270° 屏幕旋转),LVGL 会在绘图时,把待绘制内容旋转后再传给屏幕(注意这里是整个屏幕旋转而不是单个控件旋转),而这个旋转过程需要一个临时缓冲区 ------ 由#define LV_DISP_ROT_MAX_BUF (10*1024)这个宏控制这个临时缓冲区的 最大大小(单位:字节);

上述所有的中间层缓冲区通过 lv_mem_alloc 动态申请,使用的内存来自于 lv_conf.h中定义的LV_MEM_SIZE

3、预合成层(Pre-Rendered Layers)

如果一个组件 不会频繁变动,LVGL 可能会缓存它的渲染结果,然后直接拷贝到屏幕,而不是每次重绘,这种预合成层可以显著 提升性能,减少不必要的重绘,在lv_conf.h中可以定义关于预合成层的一些参数:

静态图像(image):#define LV_IMG_CACHE_DEF_SIZE 0

渐变缓存:#define LV_GRAD_CACHE_DEF_SIZE 0

复杂阴影(shadow):#define LV_SHADOW_CACHE_SIZE 0

圆形抗锯齿:#define LV_CIRCLE_CACHE_SIZE 4

渲染屏幕结构体介绍:

lv_disp_t管理的是一个显示屏的"运行时上下文",它是渲染逻辑、屏幕状态管理、无效区域控制、双缓冲处理等的核心。

cpp 复制代码
/**
 * Display structure.
 * @note `lv_disp_drv_t` should be the first member of the structure.
 */
typedef struct _lv_disp_t {
    /**显示器使用的驱动配置结构体(lv_disp_drv_t),包含绘图函数、刷新策略、分辨率等*/
    struct _lv_disp_drv_t * driver;

    /**刷新定时器,周期性检查无效区域invalidate(也叫dirty区域)并调用驱动进行渲染*/
    lv_timer_t * refr_timer;

    /**当前显示器使用的主题指针(控制颜色、字体、样式等)*/
    struct _lv_theme_t * theme;

    /**主题与屏幕管理*/
    struct _lv_obj_t ** screens;    /**显示器上所有屏幕对象的数组(包括历史、当前、待加载的屏幕)*/
    struct _lv_obj_t * act_scr;     /**当前显示器的活跃屏幕(active screen)*/
    struct _lv_obj_t * prev_scr;    /**上一个屏幕,在执行屏幕切换动画时使用*/
    struct _lv_obj_t * scr_to_load; /**准备加载的新屏幕(通过 lv_scr_load_anim 设定)*/
    /**图层系统*/
    struct _lv_obj_t * top_layer;   /**顶层图层(如浮动窗口、对话框等)*/
    struct _lv_obj_t * sys_layer;   /**系统图层(如系统提示、输入法、光标等)*/
    uint32_t screen_cnt;            /**屏幕的数量(screens 数组的长度*/
    uint8_t draw_prev_over_act : 1; /**在渲染时是否把上一屏幕绘制在当前屏幕上(用于过渡动画),1为是*/
    uint8_t del_prev : 1;           /**在完成切换动画时是否自动删除上一屏幕,1为是*/
    uint8_t rendering_in_progress : 1; /**当前是否正在进行渲染(用于避免在渲染过程中修改无效区域等),1为是*/
    /**背景设置*/
    lv_opa_t bg_opa;                /**背景透明度(当屏幕透明时,显示此背景色)*/
    lv_color_t bg_color;            /**显示器默认背景颜色*/
    const void * bg_img;            /**背景图像(可以设置为壁纸)*/

    /** Dirty 区域管理(无效区域)*/
    lv_area_t inv_areas[LV_INV_BUF_SIZE];    /**当前显示器所有无效区域的数组*/

    uint8_t inv_area_joined[LV_INV_BUF_SIZE]; /**标记某个区域是否被合并(加快 dirty 区域更新效率),为1表示对应下标的inv_areas[i]已被合并*/    
    uint16_t inv_p;        /**当前有效的Dirty区域数量*/
    int32_t inv_en_cnt;    /**用于嵌套的"启用/禁用刷新"的计数器*/

    /** 双缓冲同步区域 */
    lv_ll_t sync_areas;   /**双缓冲时用于帧同步的数据结构(链表形式,记录需要同步的区域)*/

    /*活动时间记录*/
    uint32_t last_activity_time;        /**最近一次显示器被用户操作(如触摸)的时间戳,用于屏保或节能等功能*/
} lv_disp_t;

lv_disp_drv_t 这个结构体是 LVGL 显示驱动的核心配置结构体,用于硬件抽象层(HAL)注册显示设备。你可以把它理解为连接 LVGL 和底层显示硬件之间的"桥梁"。LVGL 的渲染器会通过这个结构体完成绘图、刷新、缓存、色彩处理等任务。

cpp 复制代码
/**
 * Display Driver structure to be registered by HAL.
 * Only its pointer will be saved in `lv_disp_t` so it should be declared as
 * `static lv_disp_drv_t my_drv` or allocated dynamically.
 */
typedef struct _lv_disp_drv_t {
//分辨率与显示区域设置
    //当前LVGL显示区域分辨率(例如设置为 480x320)
    lv_coord_t hor_res;         /**水平方向*/
    lv_coord_t ver_res;         /**垂直方向*/
    
    //实际物理显示屏的全分辨率(可用于全屏或窗口模式),-1为全屏大小
    lv_coord_t physical_hor_res;     /**水平方向*/
    lv_coord_t physical_ver_res;     /**垂直方向*/

    //显示区域在物理屏上的偏移,用于设置子区域显示(如画中画),0为全屏模式
    lv_coord_t offset_x;             /**水平方向*/
    lv_coord_t offset_y;             /**垂直方向*/

//缓冲与绘图配置
    /** 显示缓冲区配置结构体(由 lv_disp_draw_buf_init() 初始化),LVGL的主帧缓冲区*/
    lv_disp_draw_buf_t * draw_buf;

    uint32_t direct_mode : 1;        /**1:直写模式(优点:不再需要整个帧缓冲,边画边显;条件:需要提供一块"映射到显存"的 draw buffer以便直接操作显存、不支持复杂叠加(比如半透明、遮罩)、要求显示驱动快速响应;场景:常用于显示屏有自己的硬件 framebuffer(比如 Linux framebuffer、GPU))
    0:普通模式(就是正常需要在lv_disp_port.c中定义主帧缓冲区大小的模式)
   普通模式画得漂亮但慢,要缓存;直写模式快但画不出复杂图像,没缓存。*/
    uint32_t full_refresh : 1;       /**< 1: 每一帧都重新渲染整个屏幕(调试、渐变动画时可用)*/
    uint32_t sw_rotate : 1;          /**< 1: 启用软件旋转(通常较慢)*/
    uint32_t antialiasing : 1;       /**< 1: 启用抗锯齿*/
    uint32_t rotated : 2;            /**< 1: 屏幕旋转90度.   屏幕旋转标志(0、90、180、270,LV_DISP_ROT_NONE 等),@warning不会随着旋转自动更新控件坐标*/
    uint32_t screen_transp : 1;      /** 1:处理透明屏幕背景(比如半透明层,开启会稍慢)*/

    uint32_t dpi : 10;              /** 屏幕的DPI(dot per inch)信息,决定了字体、布局缩放比例,默认值为`LV_DPI_DEF`.*/

//核心回调函数
    /** 必须实现:将 draw_buf 中的数据写入屏幕(区域刷新的核心)*/
    void (*flush_cb)(struct _lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);

    /** 可选: 扩展 dirty 区域时调用,用于对齐边界(如对 monochrome 显示做8, 16 ..像素对齐)
某些 显示硬件(尤其是低阶 monochrome、DMA、GPU 显示),对刷新的区域边界有要求,因此需要在渲染前把这些区域扩大或对齐成"合法"的区域,避免 DMA 错位、闪屏、绘图混乱*/
    void (*rounder_cb)(struct _lv_disp_drv_t * disp_drv, lv_area_t * area);

    /** 可选: 像素设置回调(用于不被 LVGL 支持的像素格式)如 2 bit -> 4 gray scales
     * @note 比LVGL支持的颜色格式更慢*/
    void (*set_px_cb)(struct _lv_disp_drv_t * disp_drv, uint8_t * buf, lv_coord_t buf_w, lv_coord_t x, lv_coord_t y,
                      lv_color_t color, lv_opa_t opa);

    /** 可选: 自定义清除缓冲区的方法(可选)*/
    void (*clear_cb)(struct _lv_disp_drv_t * disp_drv, uint8_t * buf, uint32_t size);

//调试、性能、同步相关回调函数
    /** 可选: 刷新结束后调用,提供渲染耗时和刷新像素数(用于性能分析)*/
    void (*monitor_cb)(struct _lv_disp_drv_t * disp_drv, uint32_t time, uint32_t px);

    /** 可选: 刷新等待中周期调用(可进行低功耗操作、yield 等)*/
    void (*wait_cb)(struct _lv_disp_drv_t * disp_drv);

    /** 可选: 清理 CPU cache(用于 DMA/GPU 与 CPU 协作时)*/
    void (*clean_dcache_cb)(struct _lv_disp_drv_t * disp_drv);

    /** 可选: 当驱动器参数被更改时调用(热插拔等场景)*/
    void (*drv_update_cb)(struct _lv_disp_drv_t * disp_drv);

    /** 可选: 每帧渲染开始时调用(可用于打 log、控制背光等) */
    void (*render_start_cb)(struct _lv_disp_drv_t * disp_drv);

//色彩配置
    /** chroma-key 透明色(通常用于带透明背景的图像,默认LV_COLOR_CHROMA_KEY (lv_conf.h)*/
    lv_color_t color_chroma_key;

//自定义绘图上下文(高级),一般用于定制 GPU 渲染器、不同色彩格式支持等
    lv_draw_ctx_t * draw_ctx;   /**绘图上下文指针,用于自定义绘图实现*/
    /**初始化绘图上下文*/ 
    void (*draw_ctx_init)(struct _lv_disp_drv_t * disp_drv, lv_draw_ctx_t * draw_ctx);
    /**释放绘图上下文资源*/ 
    void (*draw_ctx_deinit)(struct _lv_disp_drv_t * disp_drv, lv_draw_ctx_t * draw_ctx);
    size_t draw_ctx_size;       /**绘图上下文结构体的大小*/ 

#if LV_USE_USER_DATA
    void * user_data; /**用户可自由使用的结构体指针*/
#endif

} lv_disp_drv_t;
复制代码

渲染步骤:

样式 -> 绘图 -> 渲染帧缓存

lv_obj_refresh_style(obj) │

│ └─ 标记样式刷新 │

│ └─ 判断是否需要布局重算 │

│ └─ 调用 lv_obj_invalidate,登记 dirty 区域 │

└────────────┬───────────────┘

┌────────────────────────────┐

│ LVGL 定时任务 │

│ lv_timer_handler() │

│ → 显示屏disp定时器回调函数_lv_disp_refr_timer()

│ 检测到 dirty 区域触发刷新

└────────────┬───────────────┘

┌────────────────────────────┐

│ _lv_disp_refr_timer() │

│ → lv_refr_obj_and_children│

│ → 检查 invalidated 对象,遍历 dirty 区域,开始重绘│

└────────────┬───────────────┘

┌────────────────────────────┐

refr_area_part()

│ └─ lv_obj_redraw() │

│ └─ 调用控件绘制事件lv_event_send(obj, LV_EVENT_DRAW_MAIN, draw_ctx); │

└────────────┬───────────────┘

┌────────────────────────────┐

│lv_obj_event_base(NULL, e);基础事件处理函数 │

│ └─ 调用当前所绘制控件的draw_xxxx()绘制函数 │

│ └─ 调用lv_draw_...() 绘图函数族 rect / label / image 等│

└────────────┬───────────────┘

┌────────────────────────────┐

│ 将像素写入 draw_buf主屏幕缓冲区 │

│ →通过自己在lv_disp_port.c中写的 flush_cb刷新画面

└────────────┬───────────────┘

cpp 复制代码
void lv_style_set_transition(lv_style_t * style, const lv_style_transition_dsc_t * value)
{
    lv_style_value_t v = {
        .ptr = value
    };
    lv_style_set_prop(style, LV_STYLE_TRANSITION, v);
}

void lv_style_set_prop(lv_style_t * style, lv_style_prop_t prop, lv_style_value_t value)
{
    lv_style_set_prop_internal(style, prop, value, lv_style_set_prop_helper);
}


void _lv_obj_style_init(void)

{

    _lv_ll_init(&LV_GC_ROOT(_lv_obj_style_trans_ll), sizeof(trans_t));

}

渲染通知:

如果已应用到对象的样式发生变化(如添加或修改了属性,或对象状态发生改变),我们就需要通过void lv_obj_refresh_style(lv_obj_t * obj, lv_style_selector_t selector, lv_style_prop_t prop)函数通知 LVGL 重新渲染对象,该函数就像一个风向标,本身并不负责渲染,只负责告知哪些地方需要渲染。

同时由于在LVGL中,渲染的目的是因为有控件的样式发生了改变(界面的背景(act_scr)实际上也是一个控件),而控件的样式刷新都需要通过 lv_obj_refresh_style()来告知渲染区域,因此我们可以认为使用lv_obj_refresh_style()是渲染的前置条件,这里称之为渲染通知,一般在 lv_obj_add_style()lv_obj_remove_style()lv_obj_report_style_change() 时调用,以更新对象的样式。

1、复杂渲染

渲染通知函数的大概工作逻辑如下:

1、使对象无效(invalidate),触发重绘。

2、根据查表法查找当前样式属性ID是否属于LV_STYLE_PROP_LAYOUT_REFR(刷新布局)、LV_STYLE_PROP_EXT_DRAW(扩展绘制区域)、LV_STYLE_PROP_INHERIT(继承传播)、LV_STYLE_PROP_LAYER_REFR(层类型)这四种渲染类型中的某一个或多个,并用对应的四个标志位来分别保存查表结果;

3、当前属性ID属于LV_STYLE_PROP_LAYOUT_REFR(刷新布局)渲染样式时,如果样式影响主部分(LV_PART_MAIN)或者 width/height 设为 LV_SIZE_CONTENT,则触发 LV_EVENT_STYLE_CHANGED 事件,通知对象样式发生变化,并标记对象布局为脏,意味着对象的大小、位置可能需要重新计算。

4、如果影响主部分(LV_PART_MAIN) 且样式属性ID属于LV_STYLE_PROP_LAYOUT_REFR(刷新布局)渲染样式或样式属性ID为LV_STYLE_PROP_ANY也需要标记父对象布局为脏以刷新父对象的布局,因为对象尺寸变了,可能影响父对象的排列。

5、如果样式属性ID属于LV_STYLE_PROP_LAYER_REFR(层类型)渲染样式则说明可能需要使用中间层进行渲染,这里将中间层分为两种图层类型,即LV_LAYER_TYPE_SIMPLE(控件使用OPA或者混合模式)、LV_LAYER_TYPE_TRANSFORM(控件旋转或者缩放),LV_LAYER_TYPE_NONE就是不用中间层进行渲染。我们首先使用calculate_layer_type(obj)计算图层类型,如果obj->spec_attr 不为空则存储当前图层类型,如果obj->spec_attr 为空且当前图层类型不为LV_LAYER_TYPE_NONE则分配 spec_attr 结构体再存储当前图层类型。

6、如果 样式属性ID属于LV_STYLE_PROP_EXT_DRAW(扩展绘制区域) 或 样式属性ID为LV_STYLE_PROP_ANY则需要重新计算对象的 扩展绘制区域(例如shadow, outline 可能需要更大绘制区域)。

7、如果:prop == LV_STYLE_PROP_ANY(所有样式属性变化)或is_inheritable == true 且 (影响扩展绘制或布局)则 递归刷新子对象的样式。特殊情况:如果是 LV_PART_SCROLLBAR,则 不需要 递归刷新子对象。

cpp 复制代码
void lv_obj_refresh_style(lv_obj_t * obj, lv_style_selector_t selector, lv_style_prop_t prop)
{
    LV_ASSERT_OBJ(obj, MY_CLASS);

    if(!style_refr) return;

    lv_obj_invalidate(obj);

    lv_part_t part = lv_obj_style_get_selector_part(selector);

    bool is_layout_refr = lv_style_prop_has_flag(prop, LV_STYLE_PROP_LAYOUT_REFR);
    bool is_ext_draw = lv_style_prop_has_flag(prop, LV_STYLE_PROP_EXT_DRAW);
    bool is_inheritable = lv_style_prop_has_flag(prop, LV_STYLE_PROP_INHERIT);
    bool is_layer_refr = lv_style_prop_has_flag(prop, LV_STYLE_PROP_LAYER_REFR);

    if(is_layout_refr) {
        if(part == LV_PART_ANY ||
           part == LV_PART_MAIN ||
           lv_obj_get_style_height(obj, 0) == LV_SIZE_CONTENT ||
           lv_obj_get_style_width(obj, 0) == LV_SIZE_CONTENT) {
            lv_event_send(obj, LV_EVENT_STYLE_CHANGED, NULL);
            lv_obj_mark_layout_as_dirty(obj);
        }
    }
    if((part == LV_PART_ANY || part == LV_PART_MAIN) && (prop == LV_STYLE_PROP_ANY || is_layout_refr)) {
        lv_obj_t * parent = lv_obj_get_parent(obj);
        if(parent) lv_obj_mark_layout_as_dirty(parent);
    }

    /*Cache the layer type*/
    if((part == LV_PART_ANY || part == LV_PART_MAIN) && is_layer_refr) {
        lv_layer_type_t layer_type = calculate_layer_type(obj);
        if(obj->spec_attr) obj->spec_attr->layer_type = layer_type;
        else if(layer_type != LV_LAYER_TYPE_NONE) {
            lv_obj_allocate_spec_attr(obj);
            obj->spec_attr->layer_type = layer_type;
        }
    }

    if(prop == LV_STYLE_PROP_ANY || is_ext_draw) {
        lv_obj_refresh_ext_draw_size(obj);
    }
    lv_obj_invalidate(obj);

    if(prop == LV_STYLE_PROP_ANY || (is_inheritable && (is_ext_draw || is_layout_refr))) {
        if(part != LV_PART_SCROLLBAR) {
            refresh_children_style(obj);
        }
    }
}

2、简单渲染

lv_obj_refresh_style()函数介绍中我们可以发现,如果只是修改了颜色、透明度等简单属性而不是一些复杂的属性如字体、边框,我们其实就只使用了lv_obj_invalidate()函数对界面进行重绘,如下:

lv_obj_invalidate(obj);

lv_obj_invalidate(lv_scr_act()); /* 重新绘制整个屏幕 */

该函数功能为标记一个对象的可视区域为"无效"区域(dirty area),即告诉 LVGL:"这个区域需要重绘",LVGL 是按区域(Area)进行局部重绘的,因此在样式变化、内容变化、动画等情况下,必须先标记区域为脏区(dirty),然后 LVGL 才会在下一帧中只重绘这个区域,大概工作逻辑如下:

1、初始化一个区域结构体 obj_coords,用于存储这个对象需要重绘的屏幕区域;

2、计算"扩展绘制区域大小",比如一个按钮设置了 shadowoutline,这些效果是"超出对象边框"的,所以需要扩大绘制区域;

3、lv_obj_invalidate_area(obj, &obj_coords):把这块区域注册为"需要刷新的区域",LVGL 会将其加入 dirty 区域列表,在下一轮刷新时进行重新绘制;

cpp 复制代码
void lv_obj_invalidate(const lv_obj_t * obj)
{
    LV_ASSERT_OBJ(obj, MY_CLASS);

    /*Truncate the area to the object*/
    lv_area_t obj_coords;
    lv_coord_t ext_size = _lv_obj_get_ext_draw_size(obj);
    lv_area_copy(&obj_coords, &obj->coords);
    obj_coords.x1 -= ext_size;
    obj_coords.y1 -= ext_size;
    obj_coords.x2 += ext_size;
    obj_coords.y2 += ext_size;

    lv_obj_invalidate_area(obj, &obj_coords);

}

lv_coord_t _lv_obj_get_ext_draw_size(const lv_obj_t * obj)
{
    if(obj->spec_attr) return obj->spec_attr->ext_draw_size;
    else return 0;
}

/**
 * Copy an area
 * @param dest pointer to the destination area
 * @param src pointer to the source area
 */
inline static void lv_area_copy(lv_area_t * dest, const lv_area_t * src)
{
    dest->x1 = src->x1;
    dest->y1 = src->y1;
    dest->x2 = src->x2;
    dest->y2 = src->y2;
}

lv_obj_invalidate_area(obj, &obj_coords)的大概工作逻辑如下:

1、获取该对象所属的显示器(display) ,LVGL 支持多个 display,每个 display 都有自己的"无效区域列表"和刷新逻辑;

2、如果此时不允许无效区域登记(比如正在刷新),则直接返回,避免冲突;

3、复制"需要刷新的区域area" 到本地变量area_tmp,避免原始数据被改动,判断区域是否在屏幕内(是否可见),不可见就直接返回不用刷新了,节省性能。

4、调用 _lv_inv_area()area_tmp 注册为 dirty 区域。LVGL 在下一帧会处理所有 dirty 区域并刷新重绘;

cpp 复制代码
void lv_obj_invalidate_area(const lv_obj_t * obj, const lv_area_t * area)
{
    LV_ASSERT_OBJ(obj, MY_CLASS);

    lv_disp_t * disp   = lv_obj_get_disp(obj);
    if(!lv_disp_is_invalidation_enabled(disp)) return;

    lv_area_t area_tmp;
    lv_area_copy(&area_tmp, area);
    if(!lv_obj_area_is_visible(obj, &area_tmp)) return;

    _lv_inv_area(lv_obj_get_disp(obj),  &area_tmp);
}

_lv_inv_area(lv_disp_t * disp, const lv_area_t * area_p)的大概工作逻辑如下:

1、如果输入的显示器disp句柄为NULL则将disp设置为默认显示器句柄,如果默认显示器句柄也为为NULL就直接返回,判断当前显示器 disp 是否允许进行 invalidate 操作(即脏区域标记),不允许也直接返回;

2、如果当前显示器处于刷新状态就直接返回,防止在绘制过程中修改 dirty 区域,这会导致数据错乱; 是否会导致快速输入时状态和显示不一致,丢状态的情况?看渲染触发条件

3、 如果刷新区域area_p为NULL表示清空所有 dirty 区域,同时将当前有效的Dirty区域数量disp->inv_p清零;

4、计算出当前屏幕的完整区域(也就是整个屏幕的坐标范围),将 area_p 和屏幕区域取交集(com_area),裁剪出「可见部分」。如果完全在屏幕外,则直接跳过;

5、如果显卡/驱动要求"每次都全屏刷新"(full_refresh=1),那就忽略局部无效区域,直接刷全屏,将当前有效的Dirty区域数量disp->inv_p置1;

6、执行显示驱动的 rounder 回调函数,用来对 com_area 进行对齐(比如按 8px 对齐,方便 DMA)

7、如果该区域已经包含在之前的 dirty 区域中,跳过,避免重复加入。

8、保存该区域到 dirty 区域数组中(最多 LV_INV_BUF_SIZE 个)。

如果超过最大值,就清空重来,直接标记整屏刷。

9、更新 dirty 区域数量disp->inv_p,确保下一轮 lv_refr_timer() 会触发屏幕刷新。

cpp 复制代码
/**
 * Invalidate an area on display to redraw it
 * @param area_p pointer to area which should be invalidated (NULL: delete the invalidated areas)
 * @param disp pointer to display where the area should be invalidated (NULL can be used if there is
 * only one display)
 */
void _lv_inv_area(lv_disp_t * disp, const lv_area_t * area_p)
{
    if(!disp) disp = lv_disp_get_default();
    if(!disp) return;
    if(!lv_disp_is_invalidation_enabled(disp)) return;

    if(disp->rendering_in_progress) {
        LV_LOG_ERROR("detected modifying dirty areas in render");
        return;
    }

    /*Clear the invalidate buffer if the parameter is NULL*/
    if(area_p == NULL) {
        disp->inv_p = 0;
        return;
    }

    lv_area_t scr_area;
    scr_area.x1 = 0;
    scr_area.y1 = 0;
    scr_area.x2 = lv_disp_get_hor_res(disp) - 1;
    scr_area.y2 = lv_disp_get_ver_res(disp) - 1;

    lv_area_t com_area;
    bool suc;

    suc = _lv_area_intersect(&com_area, area_p, &scr_area);
    if(suc == false)  return; /*Out of the screen*/

    /*If there were at least 1 invalid area in full refresh mode, redraw the whole screen*/
    if(disp->driver->full_refresh) {
        disp->inv_areas[0] = scr_area;
        disp->inv_p = 1;
        if(disp->refr_timer) lv_timer_resume(disp->refr_timer);
        return;
    }

    if(disp->driver->rounder_cb) disp->driver->rounder_cb(disp->driver, &com_area);

    /*Save only if this area is not in one of the saved areas*/
    uint16_t i;
    for(i = 0; i < disp->inv_p; i++) {
        if(_lv_area_is_in(&com_area, &disp->inv_areas[i], 0) != false) return;
    }

    /*Save the area*/
    if(disp->inv_p < LV_INV_BUF_SIZE) {
        lv_area_copy(&disp->inv_areas[disp->inv_p], &com_area);
    }
    else {   /*If no place for the area add the screen*/
        disp->inv_p = 0;
        lv_area_copy(&disp->inv_areas[disp->inv_p], &scr_area);
    }
    disp->inv_p++;
    if(disp->refr_timer) lv_timer_resume(disp->refr_timer);
}

开始渲染:

void _lv_disp_refr_timer(lv_timer_t * tmr)屏幕刷新定时器回调函数,通过lv_timer_handler()被周期性调用,调用逻辑可参考

LVGL源码(3):通过LVGL显示屏刷新进一步理解lv_task_handler()函数_lvgl 源码-CSDN博客

用于执行 屏幕刷新 + 样式过渡 + 动画执行 + 性能统计 + 缓存清理等 全流程逻辑;

大致工作逻辑如下:

1、获取当前显示器对象,如果定时器指针tmr存在就将显示屏句柄disp_refr设置为tmr的user_data参数,这里定时器tmr创建时将user_data设置为其绑定的显示屏句柄disp;

2、刷新布局lv_obj_update_layout(...):如果你有使用 lv_obj_set_flex_flow()grid 布局等,它们的计算都会在这里被更新;

3、如果当前显示屏disp中没有活动屏幕act_scr,直接退出

4、合并无效区域 & 准备绘制,这里是刷新的关键 :只有无效区域会被重绘。你在前面通过lv_obj_invalidate() 标记无效区域之后,先通过**lv_refr_join_area();**合并重叠的无效区域,减少刷新次数,提高性能,inv_areas[i]和inv_area_joined[i]一一对应,当inv_area_joined[i]为1时表明对应的inv_areas[i]已经被合并到另一个inv_areas[x]中,绘制的时候就会忽略inv_areas[i],只有当inv_areas[i]的inv_area_joined[i]为0时才会对inv_areas[i]进行绘制;

refr_sync_areas(); 在直写模式(direct_mode = true) 且启用双缓冲时记录上帧未被重绘的无效区域,并在切换缓冲时拷贝到新缓冲区。

最终在refr_invalid_areas(); 这里被绘制出来。

5、若刷新了内容,执行清理 & 性能监控,将本轮记录的无效区域信息清空,为下一帧做准备;统计刷新耗时,如果用户注册了 monitor_cb(性能监控回调函数),就会被调用,用于记录刷新时间和像素数,便于调试或 UI 性能统计。需要注意的是如果刷新方式为双缓冲且direct_mode 为真时,此模式下显示驱动不会等待刷新完成再换缓冲区,需要手动同步哪些区域是"脏的",以便下一次刷新前正确处理。

6、缓存清理 & 特效释放:

cpp 复制代码
    lv_mem_buf_free_all();    // 临时内存释放
    _lv_font_clean_up_fmt_txt();   // 文字格式缓存清理

#if LV_DRAW_COMPLEX
    _lv_draw_mask_cleanup();   // 蒙版栈清理
#endif

7、#if LV_USE_PERF_MONITOR && LV_USE_LABEL:如果你启用了性能监控标签绘制,这里将会为开发者在屏幕上输出实时 FPS / CPU 占用;

#if LV_USE_MEM_MONITOR && LV_MEM_CUSTOM == 0 && LV_USE_LABEL:如果你启用了内存监控标签绘制,这里将会为开发者在屏幕上输出实时内存占用;

cpp 复制代码
lv_refer.c:
/**
 * Called periodically to handle the refreshing
 * @param tmr pointer to the timer itself
 */
void _lv_disp_refr_timer(lv_timer_t * tmr)
{
    REFR_TRACE("begin");

    uint32_t start = lv_tick_get();
    volatile uint32_t elaps = 0;

    if(tmr) {
        disp_refr = tmr->user_data;
#if LV_USE_PERF_MONITOR == 0 && LV_USE_MEM_MONITOR == 0
        /**
         * Ensure the timer does not run again automatically.
         * This is done before refreshing in case refreshing invalidates something else.
         */
        lv_timer_pause(tmr);
#endif
    }
    else {
        disp_refr = lv_disp_get_default();
    }

    /*Refresh the screen's layout if required*/
    lv_obj_update_layout(disp_refr->act_scr);
    if(disp_refr->prev_scr) lv_obj_update_layout(disp_refr->prev_scr);

    lv_obj_update_layout(disp_refr->top_layer);
    lv_obj_update_layout(disp_refr->sys_layer);

    /*Do nothing if there is no active screen*/
    if(disp_refr->act_scr == NULL) {
        disp_refr->inv_p = 0;
        LV_LOG_WARN("there is no active screen");
        REFR_TRACE("finished");
        return;
    }

    lv_refr_join_area();   // 合并重叠的 invalid 区域
    refr_sync_areas();     // 在直写模式(direct_mode = true) 且启用双缓冲时记录上帧未更新的区域,并在切换缓冲时拷贝回来。
    refr_invalid_areas();  // 调用 draw_ctx 执行重绘

    /*If refresh happened ...*/
    if(disp_refr->inv_p != 0) {

        /*Copy invalid areas for sync next refresh in double buffered direct mode*/
        if(disp_refr->driver->direct_mode && disp_refr->driver->draw_buf->buf2) {

            uint16_t i;
            for(i = 0; i < disp_refr->inv_p; i++) {
                if(disp_refr->inv_area_joined[i])
                    continue;

                lv_area_t * sync_area = _lv_ll_ins_tail(&disp_refr->sync_areas);
                *sync_area = disp_refr->inv_areas[i];
            }
        }

        /*Clean up*/
        lv_memset_00(disp_refr->inv_areas, sizeof(disp_refr->inv_areas));
        lv_memset_00(disp_refr->inv_area_joined, sizeof(disp_refr->inv_area_joined));
        disp_refr->inv_p = 0;

        elaps = lv_tick_elaps(start);

        /*Call monitor cb if present*/
        if(disp_refr->driver->monitor_cb) {
            disp_refr->driver->monitor_cb(disp_refr->driver, elaps, px_num);
        }
    }

    lv_mem_buf_free_all();    // 临时内存释放
    _lv_font_clean_up_fmt_txt();   // 文字格式缓存清理

#if LV_DRAW_COMPLEX
    _lv_draw_mask_cleanup();   // 蒙版栈清理
#endif

#if LV_USE_PERF_MONITOR && LV_USE_LABEL
    lv_obj_t * perf_label = perf_monitor.perf_label;
    if(perf_label == NULL) {
        perf_label = lv_label_create(lv_layer_sys());
        lv_obj_set_style_bg_opa(perf_label, LV_OPA_50, 0);
        lv_obj_set_style_bg_color(perf_label, lv_color_black(), 0);
        lv_obj_set_style_text_color(perf_label, lv_color_white(), 0);
        lv_obj_set_style_pad_top(perf_label, 3, 0);
        lv_obj_set_style_pad_bottom(perf_label, 3, 0);
        lv_obj_set_style_pad_left(perf_label, 3, 0);
        lv_obj_set_style_pad_right(perf_label, 3, 0);
        lv_obj_set_style_text_align(perf_label, LV_TEXT_ALIGN_RIGHT, 0);
        lv_label_set_text(perf_label, "?");
        lv_obj_align(perf_label, LV_USE_PERF_MONITOR_POS, 0, 0);
        perf_monitor.perf_label = perf_label;
    }

    if(lv_tick_elaps(perf_monitor.perf_last_time) < 300) {
        if(px_num > 5000) {
            perf_monitor.elaps_sum += elaps;
            perf_monitor.frame_cnt ++;
        }
    }
    else {
        perf_monitor.perf_last_time = lv_tick_get();
        uint32_t fps_limit;
        uint32_t fps;

        if(disp_refr->refr_timer) {
            fps_limit = 1000 / disp_refr->refr_timer->period;
        }
        else {
            fps_limit = 1000 / LV_DISP_DEF_REFR_PERIOD;
        }

        if(perf_monitor.elaps_sum == 0) {
            perf_monitor.elaps_sum = 1;
        }
        if(perf_monitor.frame_cnt == 0) {
            fps = fps_limit;
        }
        else {
            fps = (1000 * perf_monitor.frame_cnt) / perf_monitor.elaps_sum;
        }
        perf_monitor.elaps_sum = 0;
        perf_monitor.frame_cnt = 0;
        if(fps > fps_limit) {
            fps = fps_limit;
        }

        perf_monitor.fps_sum_all += fps;
        perf_monitor.fps_sum_cnt ++;
        uint32_t cpu = 100 - lv_timer_get_idle();
        lv_label_set_text_fmt(perf_label, "%"LV_PRIu32" FPS\n%"LV_PRIu32"%% CPU", fps, cpu);
    }
#endif

#if LV_USE_MEM_MONITOR && LV_MEM_CUSTOM == 0 && LV_USE_LABEL
    lv_obj_t * mem_label = mem_monitor.mem_label;
    if(mem_label == NULL) {
        mem_label = lv_label_create(lv_layer_sys());
        lv_obj_set_style_bg_opa(mem_label, LV_OPA_50, 0);
        lv_obj_set_style_bg_color(mem_label, lv_color_black(), 0);
        lv_obj_set_style_text_color(mem_label, lv_color_white(), 0);
        lv_obj_set_style_pad_top(mem_label, 3, 0);
        lv_obj_set_style_pad_bottom(mem_label, 3, 0);
        lv_obj_set_style_pad_left(mem_label, 3, 0);
        lv_obj_set_style_pad_right(mem_label, 3, 0);
        lv_label_set_text(mem_label, "?");
        lv_obj_align(mem_label, LV_USE_MEM_MONITOR_POS, 0, 0);
        mem_monitor.mem_label = mem_label;
    }

    if(lv_tick_elaps(mem_monitor.mem_last_time) > 300) {
        mem_monitor.mem_last_time = lv_tick_get();
        lv_mem_monitor_t mon;
        lv_mem_monitor(&mon);
        uint32_t used_size = mon.total_size - mon.free_size;;
        uint32_t used_kb = used_size / 1024;
        uint32_t used_kb_tenth = (used_size - (used_kb * 1024)) / 102;
        lv_label_set_text_fmt(mem_label,
                              "%"LV_PRIu32 ".%"LV_PRIu32 " kB used (%d %%)\n"
                              "%d%% frag.",
                              used_kb, used_kb_tenth, mon.used_pct,
                              mon.frag_pct);
    }
#endif

    REFR_TRACE("finished");
}

渲染逻辑:

refr_invalid_areas()

1、没有需要刷新的无效区域,直接返回

2、找到所有需要刷新的"独立无效区域"中最后一个的位置last_i,inv_area_joined[i] == 0 代表该无效区域是"独立"的(没有被合并到其他无效区域中)

3、执行可选回调函数:disp_refr->driver->render_start_cb(disp_refr->driver)如果你自定义驱动时实现了它,这里可以通知驱动做一些准备工作(如打开 DMA 通道、锁定帧缓冲等)

4、设置状态标志:准备渲染,用于告诉底层 draw buffer

disp_refr->driver->draw_buf->last_area = 0; //我们刚开始刷;

disp_refr->driver->draw_buf->last_part = 0; //还没有结束;

disp_refr->rendering_in_progress = true; //当前帧正在绘制中

5、遍历所有未被合并的无效区域,调用 refr_area() 实际绘制,如果为last_i则修改标志位last_area = 1表示当前处理的是最后一个区域,供底层绘制做结束处理。px_num:统计本次刷新绘制的像素总数,后续可用于性能监测回调(monitor_cb),绘制结束更改标志位rendering_in_progress为false;

cpp 复制代码
/**
 * Refresh the joined areas
 */
static void refr_invalid_areas(void)
{
    px_num = 0;

    if(disp_refr->inv_p == 0) return;

    /*Find the last area which will be drawn*/
    int32_t i;
    int32_t last_i = 0;
    for(i = disp_refr->inv_p - 1; i >= 0; i--) {
        if(disp_refr->inv_area_joined[i] == 0) {
            last_i = i;
            break;
        }
    }

    /*Notify the display driven rendering has started*/
    if(disp_refr->driver->render_start_cb) {
        disp_refr->driver->render_start_cb(disp_refr->driver);
    }

    disp_refr->driver->draw_buf->last_area = 0;
    disp_refr->driver->draw_buf->last_part = 0;
    disp_refr->rendering_in_progress = true;

    for(i = 0; i < disp_refr->inv_p; i++) {
        /*Refresh the unjoined areas*/
        if(disp_refr->inv_area_joined[i] == 0) {

            if(i == last_i) disp_refr->driver->draw_buf->last_area = 1;
            disp_refr->driver->draw_buf->last_part = 0;
            refr_area(&disp_refr->inv_areas[i]);

            px_num += lv_area_get_size(&disp_refr->inv_areas[i]);
        }
    }

    disp_refr->rendering_in_progress = false;
}

refr_area():把给定的区域 area_p 按照是否是 full-refresh / direct-mode / normal mode 等方式,分块绘制(或一次性绘制),调用底层 refr_area_part() 完成实际的图像渲染。

full_refresh 模式和普通模式(full-refresh和direct-mode都为0就是normal mode)的区别就是使用前者时需要主帧缓冲区的大小和屏幕屏幕分辨率一致,因为它要整屏一次性重绘,不能分块绘制,这样做需要很大的RAM,而且由于每次都刷新一整块屏幕,因此还对屏幕刷新接口通讯速率有要求,优点就是逻辑简单。一般我们都默认使用普通模式;

直写模式(direct-mode)(优点:不再需要整个帧缓冲,边画边显;条件:需要提供一块"映射到显存"的 draw buffer以便直接操作显存、不支持复杂叠加(比如半透明、遮罩)、要求显示驱动快速响应;场景:常用于显示屏有自己的硬件 framebuffer(比如 Linux framebuffer、GPU))

主要逻辑如下:

1、将当前绘图上下文 draw_ctx 的目标缓冲区设置为当前使用的帧缓冲(buf_act

2、判断是否为 full_refresh 或direct-mode模式,前者就构建整屏区域 disp_area,作为绘制目标区域,调用**refr_area_part()** 绘制整个屏幕(整帧),并设置 last_part 标志标记为最后一部分;在直写模式下,仅绘制当前 area_p 区域,不做切块。并设置 last_part 标志是否为最后一块;

3、普通模式下就分块刷新,首先计算 area_p 的宽高,防止越界。然后计算当前区域最多可绘制多少行,主要依据 draw_buf 的大小来决定(比如只开了 1/10 的屏幕大小缓冲,就只能一次画最多 1/10 的区域),然后将其作为一个分块。

4、for(row = area_p->y1; row + max_row - 1 <= y2; row += max_row)分块遍历绘制每一个子区域,将大区域切成若干行块 sub_area,依次绘制,最后一块时,设置 last_part 标志。

5、收尾: if(y2 != row_last)若还有未处理的尾部区域,再绘制一次,这个是为了确保最后那一小段也能被绘制(比如分块无法整除总高时的余数);

cpp 复制代码
/**
 * Refresh an area if there is Virtual Display Buffer
 * @param area_p  pointer to an area to refresh
 */
static void refr_area(const lv_area_t * area_p)
{
    lv_draw_ctx_t * draw_ctx = disp_refr->driver->draw_ctx;
    draw_ctx->buf = disp_refr->driver->draw_buf->buf_act;

    /*With full refresh just redraw directly into the buffer*/
    /*In direct mode draw directly on the absolute coordinates of the buffer*/
    if(disp_refr->driver->full_refresh || disp_refr->driver->direct_mode) {
        lv_area_t disp_area;
        lv_area_set(&disp_area, 0, 0, lv_disp_get_hor_res(disp_refr) - 1, lv_disp_get_ver_res(disp_refr) - 1);
        draw_ctx->buf_area = &disp_area;

        if(disp_refr->driver->full_refresh) {
            disp_refr->driver->draw_buf->last_part = 1;
            draw_ctx->clip_area = &disp_area;
            refr_area_part(draw_ctx);
        }
        else {
            disp_refr->driver->draw_buf->last_part = disp_refr->driver->draw_buf->last_area;
            draw_ctx->clip_area = area_p;
            refr_area_part(draw_ctx);
        }
        return;
    }

    /*Normal refresh: draw the area in parts*/
    /*Calculate the max row num*/
    lv_coord_t w = lv_area_get_width(area_p);
    lv_coord_t h = lv_area_get_height(area_p);
    lv_coord_t y2 = area_p->y2 >= lv_disp_get_ver_res(disp_refr) ?
                    lv_disp_get_ver_res(disp_refr) - 1 : area_p->y2;

    int32_t max_row = get_max_row(disp_refr, w, h);

    lv_coord_t row;
    lv_coord_t row_last = 0;
    lv_area_t sub_area;
    for(row = area_p->y1; row + max_row - 1 <= y2; row += max_row) {
        /*Calc. the next y coordinates of draw_buf*/
        sub_area.x1 = area_p->x1;
        sub_area.x2 = area_p->x2;
        sub_area.y1 = row;
        sub_area.y2 = row + max_row - 1;
        draw_ctx->buf_area = &sub_area;
        draw_ctx->clip_area = &sub_area;
        draw_ctx->buf = disp_refr->driver->draw_buf->buf_act;
        if(sub_area.y2 > y2) sub_area.y2 = y2;
        row_last = sub_area.y2;
        if(y2 == row_last) disp_refr->driver->draw_buf->last_part = 1;
        refr_area_part(draw_ctx);
    }

    /*If the last y coordinates are not handled yet ...*/
    if(y2 != row_last) {
        /*Calc. the next y coordinates of draw_buf*/
        sub_area.x1 = area_p->x1;
        sub_area.x2 = area_p->x2;
        sub_area.y1 = row;
        sub_area.y2 = y2;
        draw_ctx->buf_area = &sub_area;
        draw_ctx->clip_area = &sub_area;
        draw_ctx->buf = disp_refr->driver->draw_buf->buf_act;
        disp_refr->driver->draw_buf->last_part = 1;
        refr_area_part(draw_ctx);
    }
}

渲染核心逻辑:

refr_area_part():负责"在当前缓冲区上把指定区域完整绘制出来"的具体函数,内部执行了对象树遍历、背景绘制、双缓冲同步、系统层叠加、flush 输出等完整的刷新流程。

大概工作逻辑如下:

1、初始化缓冲区(可选):如果 draw_ctx 里提供了初始化函数,则先初始化缓冲区,比如清除背景色。

2、如果是 单缓冲 或 全屏双缓冲(full_refresh 模式),必须等待上一次绘制完成再画。如果定义了wait_cb()就在等待绘制完成时调用该函数;关于缓冲区绘制完成的通知函数使用,下面给一个例子:

cpp 复制代码
void LV_ATTRIBUTE_FLUSH_READY lv_disp_flush_ready(lv_disp_drv_t * disp_drv)
{
    disp_drv->draw_buf->flushing = 0;
    disp_drv->draw_buf->flushing_last = 0;
}

// SPI + DMA 传输完成中断回调函数
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
	if (hspi->Instance == SPI1)
	{  // 根据实际 SPI 实例
		lv_disp_flush_ready(disp_drv_temp);       // 通知LVGL刷新完成
		transfer_complete = 1;     // 设置传输完成标志
		LCD_CS1(1); //传输完成拉高片选
	}
}

3、#if LV_COLOR_SCREEN_TRANSP if(disp_refr->driver->screen_transp) 如果设置了屏幕透明(alpha 渲染),在刷新前需要清理掉之前的缓冲内容。

4、使用lv_refr_get_top_obj()获取当前激活屏幕/前一个屏幕中,从子对象列表中按"从上到下"(即后添加的在前,因为先添加的在下层,后添加的在上层)顺序进行遍历,查找 在指定区域 buf_area 内可见、未被其他对象完全遮挡的最上层对象;

**5、**无对象时,绘制背景(填充背景色、背景图)

cpp 复制代码
if(top_act_scr == NULL && top_prev_scr == NULL) {
    // draw_ctx->draw_bg 或 bg_img / bg_color 绘制背景
}

6、使用refr_obj_and_children() 函数刷新对象树(当前屏幕 + 前一帧) 、刷新系统图层(Top Layer + Sys Layer),绘制控件的具体函数就是在**refr_obj_and_children()**函数中被调用;

7、draw_buf_flush(disp_refr);如果是普通模式(分块双缓冲):等待另一个缓冲区刷写完,标记是否是最后一个区域(只在分块渲染中使用),通知 LVGL 调用 flush_cb() 进行缓冲区输出(在 direct_mode 下不会执行 flush_cb,而是系统负责交换帧缓冲),最后进行进行缓冲区切换(双缓冲专用),将前面刷写完的缓冲区作为后面的渲染缓冲区;

直写模式不用等待是因为其直接对显存进行操作,这种方式一般都是靠显示屏驱动的硬件同步;

疑问点:为什么全屏大小的双缓冲需要先等待另一个缓冲区刷写完毕再对当前缓冲区进行渲染,而普通分块双缓冲则是先对当前缓冲区进行渲染,再等另一个缓冲区刷写完毕,明明后者的逻辑更合理一些?

cpp 复制代码
static void refr_area_part(lv_draw_ctx_t * draw_ctx)
{
    lv_disp_draw_buf_t * draw_buf = lv_disp_get_draw_buf(disp_refr);

    if(draw_ctx->init_buf)
        draw_ctx->init_buf(draw_ctx);

    /* Below the `area_p` area will be redrawn into the draw buffer.
     * In single buffered mode wait here until the buffer is freed.
     * In full double buffered mode wait here while the buffers are swapped and a buffer becomes available*/
    bool full_sized = draw_buf->size == (uint32_t)disp_refr->driver->hor_res * disp_refr->driver->ver_res;
    if((draw_buf->buf1 && !draw_buf->buf2) ||
       (draw_buf->buf1 && draw_buf->buf2 && full_sized)) {
        while(draw_buf->flushing) {
            if(disp_refr->driver->wait_cb) disp_refr->driver->wait_cb(disp_refr->driver);
        }

        /*If the screen is transparent initialize it when the flushing is ready*/
#if LV_COLOR_SCREEN_TRANSP
        if(disp_refr->driver->screen_transp) {
            if(disp_refr->driver->clear_cb) {
                disp_refr->driver->clear_cb(disp_refr->driver, disp_refr->driver->draw_buf->buf_act, disp_refr->driver->draw_buf->size);
            }
            else {
                lv_memset_00(disp_refr->driver->draw_buf->buf_act, disp_refr->driver->draw_buf->size * LV_IMG_PX_SIZE_ALPHA_BYTE);
            }
        }
#endif
    }

    lv_obj_t * top_act_scr = NULL;
    lv_obj_t * top_prev_scr = NULL;

    /*Get the most top object which is not covered by others*/
    top_act_scr = lv_refr_get_top_obj(draw_ctx->buf_area, lv_disp_get_scr_act(disp_refr));
    if(disp_refr->prev_scr) {
        top_prev_scr = lv_refr_get_top_obj(draw_ctx->buf_area, disp_refr->prev_scr);
    }

    /*Draw a display background if there is no top object*/
    if(top_act_scr == NULL && top_prev_scr == NULL) {
        lv_area_t a;
        lv_area_set(&a, 0, 0,
                    lv_disp_get_hor_res(disp_refr) - 1, lv_disp_get_ver_res(disp_refr) - 1);
        if(draw_ctx->draw_bg) {
            lv_draw_rect_dsc_t dsc;
            lv_draw_rect_dsc_init(&dsc);
            dsc.bg_img_src = disp_refr->bg_img;
            dsc.bg_img_opa = disp_refr->bg_opa;
            dsc.bg_color = disp_refr->bg_color;
            dsc.bg_opa = disp_refr->bg_opa;
            draw_ctx->draw_bg(draw_ctx, &dsc, &a);
        }
        else if(disp_refr->bg_img) {
            lv_img_header_t header;
            lv_res_t res = lv_img_decoder_get_info(disp_refr->bg_img, &header);
            if(res == LV_RES_OK) {
                lv_draw_img_dsc_t dsc;
                lv_draw_img_dsc_init(&dsc);
                dsc.opa = disp_refr->bg_opa;
                lv_draw_img(draw_ctx, &dsc, &a, disp_refr->bg_img);
            }
            else {
                LV_LOG_WARN("Can't draw the background image");
            }
        }
        else {
            lv_draw_rect_dsc_t dsc;
            lv_draw_rect_dsc_init(&dsc);
            dsc.bg_color = disp_refr->bg_color;
            dsc.bg_opa = disp_refr->bg_opa;
            lv_draw_rect(draw_ctx, &dsc, draw_ctx->buf_area);
        }
    }

    if(disp_refr->draw_prev_over_act) {
        if(top_act_scr == NULL) top_act_scr = disp_refr->act_scr;
        refr_obj_and_children(draw_ctx, top_act_scr);

        /*Refresh the previous screen if any*/
        if(disp_refr->prev_scr) {
            if(top_prev_scr == NULL) top_prev_scr = disp_refr->prev_scr;
            refr_obj_and_children(draw_ctx, top_prev_scr);
        }
    }
    else {
        /*Refresh the previous screen if any*/
        if(disp_refr->prev_scr) {
            if(top_prev_scr == NULL) top_prev_scr = disp_refr->prev_scr;
            refr_obj_and_children(draw_ctx, top_prev_scr);
        }

        if(top_act_scr == NULL) top_act_scr = disp_refr->act_scr;
        refr_obj_and_children(draw_ctx, top_act_scr);
    }

    /*Also refresh top and sys layer unconditionally*/
    refr_obj_and_children(draw_ctx, lv_disp_get_layer_top(disp_refr));
    refr_obj_and_children(draw_ctx, lv_disp_get_layer_sys(disp_refr));

    draw_buf_flush(disp_refr);
}

/**
 * Flush the content of the draw buffer
 */
static void draw_buf_flush(lv_disp_t * disp)
{
    lv_disp_draw_buf_t * draw_buf = lv_disp_get_draw_buf(disp_refr);

    /*Flush the rendered content to the display*/
    lv_draw_ctx_t * draw_ctx = disp->driver->draw_ctx;
    if(draw_ctx->wait_for_finish) draw_ctx->wait_for_finish(draw_ctx);

    /* In partial double buffered mode wait until the other buffer is freed
     * and driver is ready to receive the new buffer */
    bool full_sized = draw_buf->size == (uint32_t)disp_refr->driver->hor_res * disp_refr->driver->ver_res;
    if(draw_buf->buf1 && draw_buf->buf2 && !full_sized) {
        while(draw_buf->flushing) {
            if(disp_refr->driver->wait_cb) disp_refr->driver->wait_cb(disp_refr->driver);
        }
    }

    draw_buf->flushing = 1;

    if(disp_refr->driver->draw_buf->last_area && disp_refr->driver->draw_buf->last_part) draw_buf->flushing_last = 1;
    else draw_buf->flushing_last = 0;

    bool flushing_last = draw_buf->flushing_last;

    if(disp->driver->flush_cb) {
        /*Rotate the buffer to the display's native orientation if necessary*/
        if(disp->driver->rotated != LV_DISP_ROT_NONE && disp->driver->sw_rotate) {
            draw_buf_rotate(draw_ctx->buf_area, draw_ctx->buf);
        }
        else {
            call_flush_cb(disp->driver, draw_ctx->buf_area, draw_ctx->buf);
        }
    }

    /*If there are 2 buffers swap them. With direct mode swap only on the last area*/
    if(draw_buf->buf1 && draw_buf->buf2 && (!disp->driver->direct_mode || flushing_last)) {
        if(draw_buf->buf_act == draw_buf->buf1)
            draw_buf->buf_act = draw_buf->buf2;
        else
            draw_buf->buf_act = draw_buf->buf1;
    }
}

static void call_flush_cb(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p)
{
    REFR_TRACE("Calling flush_cb on (%d;%d)(%d;%d) area with %p image pointer", area->x1, area->y1, area->x2, area->y2,
               (void *)color_p);

    lv_area_t offset_area = {
        .x1 = area->x1 + drv->offset_x,
        .y1 = area->y1 + drv->offset_y,
        .x2 = area->x2 + drv->offset_x,
        .y2 = area->y2 + drv->offset_y
    };

    drv->flush_cb(drv, &offset_area, color_p);
}

refr_obj_and_children():从某个对象 top_obj 开始,递归绘制它及其子对象,同时向上追溯并绘制其兄弟对象(以及父对象的事件回调)。

工作逻辑如下:

1、确定起始对象,如果传入的 top_obj == NULL,默认取当前激活屏幕。

2、使用refr_obj(draw_ctx, top_obj);刷新对象及其所有子对象,这是对象递归绘制的核心函数:refr_obj() 会处理 top_objdraw() 回调,并递归其子对象。

3、从 top_obj 向父级逐层上溯,border_p 标记"当前处理对象"的位置,用来识别"其之后的兄弟节点"。

4、绘制当前父级对象中排在 top_obj 后的兄弟对象

5、触发父对象的 绘制后事件(LV_EVENT_DRAW_POST),用于通知父对象:其子项已绘制完,可以做额外的绘制(如遮罩、特效、边框等)。

6、把当前 parent 设为下一个 border 起点,再往上走一级,重复步骤。

总结:

  • 向下递归 :绘制 top_obj 及其所有子孙。

  • 向上回溯:对其父对象中比它靠后的兄弟节点也做刷新(因为可能遮挡你)。

  • 加事件:每一级父对象调用 Post Draw 事件,允许执行额外绘制。

cpp 复制代码
/**
 * Make the refreshing from an object. Draw all its children and the youngers too.
 * @param top_p pointer to an objects. Start the drawing from it.
 * @param mask_p pointer to an area, the objects will be drawn only here
 */
static void refr_obj_and_children(lv_draw_ctx_t * draw_ctx, lv_obj_t * top_obj)
{
    /*Normally always will be a top_obj (at least the screen)
     *but in special cases (e.g. if the screen has alpha) it won't.
     *In this case use the screen directly*/
    if(top_obj == NULL) top_obj = lv_disp_get_scr_act(disp_refr);
    if(top_obj == NULL) return;  /*Shouldn't happen*/

    /*Refresh the top object and its children*/
    refr_obj(draw_ctx, top_obj);

    /*Draw the 'younger' sibling objects because they can be on top_obj*/
    lv_obj_t * parent;
    lv_obj_t * border_p = top_obj;

    parent = lv_obj_get_parent(top_obj);

    /*Do until not reach the screen*/
    while(parent != NULL) {
        bool go = false;
        uint32_t i;
        uint32_t child_cnt = lv_obj_get_child_cnt(parent);
        for(i = 0; i < child_cnt; i++) {
            lv_obj_t * child = parent->spec_attr->children[i];
            if(!go) {
                if(child == border_p) go = true;
            }
            else {
                /*Refresh the objects*/
                refr_obj(draw_ctx, child);
            }
        }

        /*Call the post draw draw function of the parents of the to object*/
        lv_event_send(parent, LV_EVENT_DRAW_POST_BEGIN, (void *)draw_ctx);
        lv_event_send(parent, LV_EVENT_DRAW_POST, (void *)draw_ctx);
        lv_event_send(parent, LV_EVENT_DRAW_POST_END, (void *)draw_ctx);

        /*The new border will be the last parents,
         *so the 'younger' brothers of parent will be refreshed*/
        border_p = parent;
        /*Go a level deeper*/
        parent = lv_obj_get_parent(parent);
    }
}

refr_obj():LVGL 中绘制每个对象(lv_obj_t)的核心函数之一。它处理了对象的基本绘制逻辑,同时考虑了图层(layer)、不透明度(opa)、裁剪、旋转、缩放等高级特性。

工作逻辑如下:

1、对于隐藏对象(带有 LV_OBJ_FLAG_HIDDEN),直接跳过不渲染。

2、判断该对象的图层类型(NONE, SIMPLE, OPA, TRANSP, 等),决定渲染方式,非图层对象就使用**lv_obj_redraw(draw_ctx, obj);**直接绘制;如果需要使用到中间层渲染,就再做进一步处理;

3、如果使用中间层渲染,先处理旋转、缩放、混合等样式特性,然后将中间层存入临时缓冲区,再通过lv_obj_redraw(draw_ctx, obj); // 在 layer_ctx 上绘制该对象,lv_draw_layer_blend(draw_ctx, layer_ctx, &draw_dsc);// 将 layer_ctx 合成到原 draw_ctx 上

完成整个渲染流程;

cpp 复制代码
void refr_obj(lv_draw_ctx_t * draw_ctx, lv_obj_t * obj)
{
    /*Do not refresh hidden objects*/
    if(lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN)) return;
    lv_layer_type_t layer_type = _lv_obj_get_layer_type(obj);
    if(layer_type == LV_LAYER_TYPE_NONE) {
        lv_obj_redraw(draw_ctx, obj);
    }
    else {
        lv_opa_t opa = lv_obj_get_style_opa_layered(obj, 0);
        if(opa < LV_OPA_MIN) return;

        lv_area_t layer_area_full;
        lv_res_t res = layer_get_area(draw_ctx, obj, layer_type, &layer_area_full);
        if(res != LV_RES_OK) return;

        lv_draw_layer_flags_t flags = LV_DRAW_LAYER_FLAG_HAS_ALPHA;

        if(_lv_area_is_in(&layer_area_full, &obj->coords, 0)) {
            lv_cover_check_info_t info;
            info.res = LV_COVER_RES_COVER;
            info.area = &layer_area_full;
            lv_event_send(obj, LV_EVENT_COVER_CHECK, &info);
            if(info.res == LV_COVER_RES_COVER) flags &= ~LV_DRAW_LAYER_FLAG_HAS_ALPHA;
        }

        if(layer_type == LV_LAYER_TYPE_SIMPLE) flags |= LV_DRAW_LAYER_FLAG_CAN_SUBDIVIDE;

        lv_draw_layer_ctx_t * layer_ctx = lv_draw_layer_create(draw_ctx, &layer_area_full, flags);
        if(layer_ctx == NULL) {
            LV_LOG_WARN("Couldn't create a new layer context");
            return;
        }
        lv_point_t pivot = {
            .x = lv_obj_get_style_transform_pivot_x(obj, 0),
            .y = lv_obj_get_style_transform_pivot_y(obj, 0)
        };

        if(LV_COORD_IS_PCT(pivot.x)) {
            pivot.x = (LV_COORD_GET_PCT(pivot.x) * lv_area_get_width(&obj->coords)) / 100;
        }
        if(LV_COORD_IS_PCT(pivot.y)) {
            pivot.y = (LV_COORD_GET_PCT(pivot.y) * lv_area_get_height(&obj->coords)) / 100;
        }

        lv_draw_img_dsc_t draw_dsc;
        lv_draw_img_dsc_init(&draw_dsc);
        draw_dsc.opa = opa;
        draw_dsc.angle = lv_obj_get_style_transform_angle(obj, 0);
        if(draw_dsc.angle > 3600) draw_dsc.angle -= 3600;
        else if(draw_dsc.angle < 0) draw_dsc.angle += 3600;

        draw_dsc.zoom = lv_obj_get_style_transform_zoom(obj, 0);
        draw_dsc.blend_mode = lv_obj_get_style_blend_mode(obj, 0);
        draw_dsc.antialias = disp_refr->driver->antialiasing;

        if(flags & LV_DRAW_LAYER_FLAG_CAN_SUBDIVIDE) {
            layer_ctx->area_act = layer_ctx->area_full;
            layer_ctx->area_act.y2 = layer_ctx->area_act.y1 + layer_ctx->max_row_with_no_alpha - 1;
            if(layer_ctx->area_act.y2 > layer_ctx->area_full.y2) layer_ctx->area_act.y2 = layer_ctx->area_full.y2;
        }

        while(layer_ctx->area_act.y1 <= layer_area_full.y2) {
            if(flags & LV_DRAW_LAYER_FLAG_CAN_SUBDIVIDE) {
                layer_alpha_test(obj, draw_ctx, layer_ctx, flags);
            }

            lv_obj_redraw(draw_ctx, obj);

            draw_dsc.pivot.x = obj->coords.x1 + pivot.x - draw_ctx->buf_area->x1;
            draw_dsc.pivot.y = obj->coords.y1 + pivot.y - draw_ctx->buf_area->y1;

            /*With LV_DRAW_LAYER_FLAG_CAN_SUBDIVIDE it should also go the next chunk*/
            lv_draw_layer_blend(draw_ctx, layer_ctx, &draw_dsc);

            if((flags & LV_DRAW_LAYER_FLAG_CAN_SUBDIVIDE) == 0) break;

            layer_ctx->area_act.y1 = layer_ctx->area_act.y2 + 1;
            layer_ctx->area_act.y2 = layer_ctx->area_act.y1 + layer_ctx->max_row_with_no_alpha - 1;
        }

        lv_draw_layer_destroy(draw_ctx, layer_ctx);
    }
}

**lv_obj_redraw():**主要是通过调用lv_event_send(obj, LV_EVENT_DRAW_MAIN, draw_ctx);事件组绘制控件主图形,调用lv_event_send(obj, LV_EVENT_DRAW_POST, draw_ctx);事件组绘制后处理内容;

cpp 复制代码
void lv_obj_redraw(lv_draw_ctx_t * draw_ctx, lv_obj_t * obj)
{
    const lv_area_t * clip_area_ori = draw_ctx->clip_area;
    lv_area_t clip_coords_for_obj;

    /*Truncate the clip area to `obj size + ext size` area*/
    lv_area_t obj_coords_ext;
    lv_obj_get_coords(obj, &obj_coords_ext);
    lv_coord_t ext_draw_size = _lv_obj_get_ext_draw_size(obj);
    lv_area_increase(&obj_coords_ext, ext_draw_size, ext_draw_size);
    bool com_clip_res = _lv_area_intersect(&clip_coords_for_obj, clip_area_ori, &obj_coords_ext);
    /*If the object is visible on the current clip area OR has overflow visible draw it.
     *With overflow visible drawing should happen to apply the masks which might affect children */
    bool should_draw = com_clip_res || lv_obj_has_flag(obj, LV_OBJ_FLAG_OVERFLOW_VISIBLE);
    if(should_draw) {
        draw_ctx->clip_area = &clip_coords_for_obj;

        lv_event_send(obj, LV_EVENT_DRAW_MAIN_BEGIN, draw_ctx);
        lv_event_send(obj, LV_EVENT_DRAW_MAIN, draw_ctx);
        lv_event_send(obj, LV_EVENT_DRAW_MAIN_END, draw_ctx);
#if LV_USE_REFR_DEBUG
        lv_color_t debug_color = lv_color_make(lv_rand(0, 0xFF), lv_rand(0, 0xFF), lv_rand(0, 0xFF));
        lv_draw_rect_dsc_t draw_dsc;
        lv_draw_rect_dsc_init(&draw_dsc);
        draw_dsc.bg_color.full = debug_color.full;
        draw_dsc.bg_opa = LV_OPA_20;
        draw_dsc.border_width = 1;
        draw_dsc.border_opa = LV_OPA_30;
        draw_dsc.border_color = debug_color;
        lv_draw_rect(draw_ctx, &draw_dsc, &obj_coords_ext);
#endif
    }

    /*With overflow visible keep the previous clip area to let the children visible out of this object too
     *With not overflow visible limit the clip are to the object's coordinates to clip the children*/
    lv_area_t clip_coords_for_children;
    bool refr_children = true;
    if(lv_obj_has_flag(obj, LV_OBJ_FLAG_OVERFLOW_VISIBLE)) {
        clip_coords_for_children  = *clip_area_ori;
    }
    else {
        if(!_lv_area_intersect(&clip_coords_for_children, clip_area_ori, &obj->coords)) {
            refr_children = false;
        }
    }

    if(refr_children) {
        draw_ctx->clip_area = &clip_coords_for_children;
        uint32_t i;
        uint32_t child_cnt = lv_obj_get_child_cnt(obj);
        for(i = 0; i < child_cnt; i++) {
            lv_obj_t * child = obj->spec_attr->children[i];
            refr_obj(draw_ctx, child);
        }
    }

    /*If the object was visible on the clip area call the post draw events too*/
    if(should_draw) {
        draw_ctx->clip_area = &clip_coords_for_obj;

        /*If all the children are redrawn make 'post draw' draw*/
        lv_event_send(obj, LV_EVENT_DRAW_POST_BEGIN, draw_ctx);
        lv_event_send(obj, LV_EVENT_DRAW_POST, draw_ctx);
        lv_event_send(obj, LV_EVENT_DRAW_POST_END, draw_ctx);
    }

    draw_ctx->clip_area = clip_area_ori;
}

调用控件绘制事件

我们知道每一个控件都有其绘制函数draw_main(),有的复杂控件还有draw_xx()函数,这里拿dropdowm控件举例:下面代码中的draw_main(e);就是绘制下拉列表控件主体的函数,而draw_list(e);就是绘制下拉列表控件下拉列表的函数;

cpp 复制代码
static void lv_dropdown_event(const lv_obj_class_t * class_p, lv_event_t * e)
{
   ...
    else if(code == LV_EVENT_DRAW_MAIN) {
        draw_main(e);
    }
}

static void lv_dropdown_list_event(const lv_obj_class_t * class_p, lv_event_t * e)
{
   ...
    else if(code == LV_EVENT_DRAW_POST) {
        draw_list(e);
        res = lv_obj_event_base(MY_CLASS_LIST, e);
        if(res != LV_RES_OK) return;
    }
}

我们从LVGL源码(4):LVGL关于EVENT事件的响应逻辑_lvgl event-CSDN博客中可以知道,当我们调用lv_event_send(obj, LV_EVENT_DRAW_MAIN, draw_ctx);时,最终会执行一个lv_obj_event_base(NULL, e);基础事件处理函数(对象的基类的事件处理,控件面向LVGL系统的事件),该函数就会调用当前控件的事件回调函数,如果当前控件为lv_dropdown的话那么毫无疑问其事件回调函数lv_dropdown_event中的draw_main(e)主体绘制函数将会得以执行;lv_event_send(obj, LV_EVENT_DRAW_POST, draw_ctx);同理;

因此我们可以知道,在上面渲染过程中一层又一层的函数嵌套,最终执行控件绘制的行为还是通过调用lv_event_send(obj, LV_EVENT_DRAW_MAIN, draw_ctx);和lv_event_send(obj, LV_EVENT_DRAW_POST, draw_ctx);事件处理函数,然后调用最终的draw_main(e)、draw_xxxx(e)等来进行实现;

样式过渡渲染实现:

/***************************未完成*************************/

void _lv_obj_style_init(void){

_lv_ll_init(&LV_GC_ROOT(_lv_obj_style_trans_ll), sizeof(trans_t));

}

void _lv_obj_style_create_transition(lv_obj_t * obj, lv_part_t part, lv_state_t prev_state, lv_state_t new_state,const _lv_obj_style_transition_dsc_t * tr_dsc)

判断样式是否变化,如果开启了 LV_STYLE_TRANSITION,就会向 _lv_obj_style_trans_ll 添加 trans_t 节点,启动动画。通过调用lv_timer_handler() -> lv_anim_timer():在全局 timer tick 中推进每一帧。

cpp 复制代码
void _lv_obj_style_create_transition(lv_obj_t * obj, lv_part_t part, lv_state_t prev_state, lv_state_t new_state,
                                     const _lv_obj_style_transition_dsc_t * tr_dsc)
{
    trans_t * tr;

    /*Get the previous and current values*/
    obj->skip_trans = 1;
    obj->state = prev_state;
    lv_style_value_t v1 = lv_obj_get_style_prop(obj, part, tr_dsc->prop);
    obj->state = new_state;
    lv_style_value_t v2 = lv_obj_get_style_prop(obj, part, tr_dsc->prop);
    obj->skip_trans = 0;

    if(v1.ptr == v2.ptr && v1.num == v2.num && v1.color.full == v2.color.full)  return;
    obj->state = prev_state;
    v1 = lv_obj_get_style_prop(obj, part, tr_dsc->prop);
    obj->state = new_state;

    _lv_obj_style_t * style_trans = get_trans_style(obj, part);
    lv_style_set_prop(style_trans->style, tr_dsc->prop, v1);   /*Be sure `trans_style` has a valid value*/

    if(tr_dsc->prop == LV_STYLE_RADIUS) {
        if(v1.num == LV_RADIUS_CIRCLE || v2.num == LV_RADIUS_CIRCLE) {
            lv_coord_t whalf = lv_obj_get_width(obj) / 2;
            lv_coord_t hhalf = lv_obj_get_height(obj) / 2;
            if(v1.num == LV_RADIUS_CIRCLE) v1.num = LV_MIN(whalf + 1, hhalf + 1);
            if(v2.num == LV_RADIUS_CIRCLE) v2.num = LV_MIN(whalf + 1, hhalf + 1);
        }
    }

    tr = _lv_ll_ins_head(&LV_GC_ROOT(_lv_obj_style_trans_ll));
    LV_ASSERT_MALLOC(tr);
    if(tr == NULL) return;
    tr->start_value = v1;
    tr->end_value = v2;
    tr->obj = obj;
    tr->prop = tr_dsc->prop;
    tr->selector = part;

    lv_anim_t a;
    lv_anim_init(&a);
    lv_anim_set_var(&a, tr);
    lv_anim_set_exec_cb(&a, trans_anim_cb);
    lv_anim_set_start_cb(&a, trans_anim_start_cb);
    lv_anim_set_ready_cb(&a, trans_anim_ready_cb);
    lv_anim_set_values(&a, 0x00, 0xFF);
    lv_anim_set_time(&a, tr_dsc->time);
    lv_anim_set_delay(&a, tr_dsc->delay);
    lv_anim_set_path_cb(&a, tr_dsc->path_cb);
    lv_anim_set_early_apply(&a, false);
#if LV_USE_USER_DATA
    a.user_data = tr_dsc->user_data;
#endif
    lv_anim_start(&a);
}
相关推荐
长流小哥2 小时前
Linux 深入浅出信号量:从线程到进程的同步与互斥实战指南
linux·c语言·开发语言·bash
努力创造奇迹2 小时前
STM32 HAL库 实现485通信
stm32·单片机·嵌入式硬件
Tlog嵌入式2 小时前
STM32提高篇: 以太网通讯
网络·stm32·单片机·嵌入式硬件·mcu·iot
张立龙6663 小时前
有序二叉树各种操作实现(数据结构C语言多文件编写)
c语言·开发语言·数据结构
菜狗想要变强4 小时前
RVOS-7.实现抢占式多任务
linux·c语言·驱动开发·单片机·嵌入式硬件·risc-v
番茄老夫子4 小时前
适合stm32 前端adc使用的放大器芯片
stm32·单片机·嵌入式硬件
m0_疾风5 小时前
STM32
stm32·单片机·嵌入式硬件
硬匠的博客5 小时前
C/C++基础
stm32·单片机·嵌入式硬件
LaoZhangGong1235 小时前
MCU屏和RGB屏
经验分享·stm32·单片机·嵌入式硬件·fsmc