效果预览


上图展示了RT-Thread 睿擎平台(Rockchip RK3506)上接入一块 4.3 寸 MIPI DSI LCD(物理分辨率为 480×800 竖屏),通过软件层面的旋转处理,以 800×480 横屏方向完成 UI 内容呈现。
一、LVGL 概述
LVGL,是一款专为嵌入式系统设计的轻量级开源 GUI 库。该库在资源极度受限的 MCU 及 MPU 平台上同样能够实现流畅的图形界面渲染,被广泛应用于智能家电、车载仪表、工业 HMI 等嵌入式人机交互场景。
二、LVGL 渲染流水线
在讨论旋转实现之前,需要介绍 LVGL 完整渲染过程——从应用层发起初始化,到像素数据最终输出至物理显示设备,数据经过了哪些组件、完成了哪些阶段的处理。下图以框图形式展示了这一流水线的整体架构。

上图展示了 LVGL 渲染流水线的七个核心阶段,自上而下依次为:应用层入口 → LVGL 线程创建与初始化 → 刷新定时器周期性触发渲染 → 绘制缓冲区承载一帧完整画面 → call_flush_cb 完成坐标修正与回调分发 → 用户 flush_cb 执行旋转处理及平台加速传输 → LCD 硬件显示。
2.1 应用层入口与线程模型
对应于框图中 ①→② 阶段,LVGL 在 RT-Thread 睿擎平台上运行于一个独立线程之中。main() 函数仅负责调用 lvgl_thread_init() 创建并启动该线程:
intmain(void){ rt_kprintf("Hello, RT-Thread app\n"); // 创建并启动 LVGL 线程 lvgl_thread_init(); return0;}
lvgl_thread_init 通过 rt_thread_init 创建一个名为 “LVGL” 的线程,其入口函数为 lvgl_thread_entry,随后通过 rt_thread_startup 启动。进入入口函数后,即可观察到 LVGL 的完整初始化序列与主循环结构:
staticvoidlvgl_thread_entry(void*parameter){ // LVGL 内核初始化 lv_init(); // 注册 RT-Thread tick 作为心跳时钟 lv_tick_set_cb(rt_tick_get_millisecond); // 显示驱动初始化 lv_port_disp_init(); // 输入设备初始化 lv_port_indev_init(); // 用户 UI 入口 lv_user_gui_init(); while(1) { lv_task_handler(); rt_thread_mdelay(1); }}
上述五个初始化步骤与末尾的死循环,构成了 LVGL 运行的全部生命周期。
2.2 显示驱动初始化与渲染模式配置
对应于框图中 ② 阶段内部 lv_port_disp_init 的执行,该函数是整个旋转机制的起点:
voidlv_port_disp_init(void){ // ... 变量声明 ...
// 步骤1: 查找 LCD 设备 device =rt_device_find("lcd");
// ... 错误检查 ...
rt_device_init(device); // 步骤2: 打开设备 rt_device_open(device, RT_DEVICE_FLAG_RDWR); // 步骤3: 获取物理分辨率 (480×800) rt_device_control(device, RTGRAPHIC_CTRL_GET_INFO, &info); // 步骤4: 分配绘制缓冲,大小 = 480×800×2 字节(RGB565) framebuffer =rt_malloc( info.width * info.height *sizeof(lv_color_t)); // ... 错误检查 ...
// 步骤5: 创建显示对象,传入物理分辨率 display =lv_display_create(info.width, info.height); // ... 错误检查 ...
// 步骤6: 单缓冲 + FULL 渲染模式 lv_display_set_buffers(display, framebuffer,NULL, info.width * info.height *sizeof(lv_color_t), LV_DISPLAY_RENDER_MODE_FULL); // 步骤7: 注册 flush_cb 与旋转角度 lv_display_set_flush_cb(display, lvgl_flush_cb); lv_display_set_rotation(display, LV_DISPLAY_ROTATION_270); return; // ... __fail 清理逻辑 ...}
该函数涵盖七个关键步骤:查找 LCD 设备 → 打开设备 → 获取物理分辨率(480×800) → 分配绘制缓冲区 → 创建 LVGL 显示对象 → 配置单缓冲 FULL 渲染模式(每帧刷新完整画面,便于旋转处理) → 注册 flush_cb 回调并声明 270° 旋转角度。其中 lv_display_set_rotation 的实现机制将在第四章详述。
2.3 帧刷新机制与回调分发
对应于框图中 ③→④→⑤ 阶段。LVGL 内部维护一个刷新定时器 _lv_display_refr_timer,周期性触发:合并脏区域 → 逐层渲染像素至缓冲区 → 调用 draw_buf_flush。call_flush_cb 作为内核与用户回调的分界点,将刷新区域坐标叠加 display 偏移量后调用用户注册的 flush_cb:
staticvoidcall_flush_cb(lv_display_t* disp,constlv_area_t* area,uint8_t* px_map){ lv_area_toffset_area = { .x1 = area->x1 + disp->offset_x, .y1 = area->y1 + disp->offset_y, .x2 = area->x2 + disp->offset_x, .y2 = area->y2 + disp->offset_y }; disp->flush_cb(disp, &offset_area, px_map);}
关键点:LVGL 使用的坐标空间是逻辑分辨率。物理 LCD 为 480×800,声明 270° 旋转后 LVGL 识别 800×480,所有 UI 均按此逻辑空间布局。flush_cb 接收到的 area 与 px_map 也处于同一逻辑空间,其核心职责即为完成逻辑空间到物理硬件的映射转换。
完整处理链路:LVGL 800×480 逻辑渲染 →call_flush_cb→ 用户flush_cb旋转变换 (480×800) → LCD。底层 MIPI DSI 驱动始终以物理 480×800 工作,对旋转完全无感知。
2.4 旋转模块在流水线中的定位
观察上述框图可以发现,旋转处理的所有逻辑全部集中在第 ⑥ 阶段——用户 flush_cb 内部。LVGL 内核(① 至 ⑤)完全不涉及旋转相关的逻辑,LCD 硬件驱动(⑦)也不感知旋转的存在。这种架构设计的优势在于:旋转处理与渲染引擎和硬件驱动均实现了解耦,修改旋转角度仅需调整 flush_cb 中的处理逻辑,不影响其他任何模块。
三、屏幕旋转实现原理
实现屏幕旋转,本质上需要完成三个关键操作:“声明旋转角度 → 坐标映射 → 像素重排”。
1.声明旋转角度— lv_display_set_rotation 让 LVGL 将水平分辨率识别为 800、垂直 480,UI 代码如同在原生 800×480 横屏上开发。
2.刷新区域坐标映射— lv_display_rotate_area 将 flush_cb 收到的 800×480 逻辑坐标映射为 480×800 物理坐标,仅改坐标不改像素。
3.像素数据重排— lv_draw_sw_rotate 将像素从 800×480 逻辑排布重组为 480×800 物理排布。
数据流向:LVGL 逻辑层 (800×480) → flush_cb → [rotate_area + sw_rotate + RGA] → MIPI LCD (480×800)
四、旋转机制核心函数源码分析
4.1 lv_display_set_rotation:旋转角度声明
voidlv_display_set_rotation(lv_display_t* disp,lv_display_rotation_trotation){ // ... NULL 检查 ...
// 仅将角度值写入字段 disp->rotation = rotation;
// 触发 screen/layer 尺寸重算 update_resolution(disp); }
该函数不修改disp->hor_res 与 disp->ver_res(始终为物理分辨率 480 和 800)。逻辑分辨率的切换实现在 getter 函数中:
int32_tlv_display_get_horizontal_resolution(constlv_display_t* disp){ // ... NULL 检查 ... switch(disp->rotation) { caseLV_DISPLAY_ROTATION_90: caseLV_DISPLAY_ROTATION_270: returndisp->ver_res; default: returndisp->hor_res; }}
90°/270° 时水平分辨率返回 ver_res(800),垂直分辨率同理反向映射。上层代码统一通过 getter 获取尺寸,所有布局自动适配。update_resolution 负责向对象树广播变更——更新所有 screen/layer 尺寸、清空无效区域强制重绘、触发 LV_EVENT_RESOLUTION_CHANGED 事件。

核心思想:存储与访问分离。
4.2 lv_display_rotate_area:刷新区域坐标映射
将 flush_cb 中逻辑空间的 area 坐标映射为物理空间坐标:
voidlv_display_rotate_area(lv_display_t* disp,lv_area_t* area){ lv_display_rotation_trotation =lv_display_get_rotation(disp); if(rotation == LV_DISPLAY_ROTATION_0)return; int32_tw =lv_area_get_width(area); int32_th =lv_area_get_height(area); switch(rotation) { caseLV_DISPLAY_ROTATION_90: area->y2 = disp->ver_res - area->x1 -1; area->x1 = area->y1; area->x2 = area->x1 + h -1; area->y1 = area->y2 - w +1; break; caseLV_DISPLAY_ROTATION_180: // ... px = hor_res - lx - 1, py = ver_res - ly - 1 ... break; caseLV_DISPLAY_ROTATION_270: area->x1 = disp->hor_res - area->y2 -1; area->y2 = area->x2; area->x2 = area->x1 + h -1; area->y1 = area->y2 - w +1; break; }}
注意函数直接访问字段 disp->hor_res(480)和 disp->ver_res(800),不经 getter,始终为物理分辨率。以 270° 为例,逻辑区域 {100,50,300,150} 变换后为 {329,100,429,300}。

4.3 lv_draw_sw_rotate:像素数据重排
分发函数,按颜色格式和角度派发至对应的旋转实现:
voidlv_draw_sw_rotate(...,lv_display_rotation_trotation,lv_color_format_tcolor_format){ uint32_tpx_bpp =lv_color_format_get_bpp(color_format); if(rotation == LV_DISPLAY_ROTATION_90) { if(px_bpp ==16)rotate90_rgb565(...); // ... } // ... 180°、270° 同理 ...}
本项目为 RGB565(BPP=16)+ 270°,最终执行 rotate270_rgb565:
staticvoidrotate270_rgb565(constuint16_t* src,uint16_t* dst, int32_tsrcW,int32_tsrcH,int32_tsrcStride,int32_tdstStride){// 硬件加速钩子 if(LV_RESULT_OK ==LV_DRAW_SW_ROTATE270_RGB565(...))return; srcStride /=sizeof(uint16_t); dstStride /=sizeof(uint16_t); for(int32_tx =0; x < srcW; ++x) { // 源第 x 列 → 目标倒数第 x 列 int32_t dstIndex = (srcW - x - 1); int32_t srcIndex = x; for(int32_t y = 0; y < srcH; ++y) { dst[dstIndex * dstStride + y] = src[srcIndex]; srcIndex += srcStride; } }}
核心算法:外层按列遍历,dstIndex = srcW - x - 1 将源第 0 列映射至目标最右列;内层沿列方向逐像素搬运。源 srcW×srcH 矩阵旋转变为目标 srcH×srcW 矩阵。
4.4 flush_cb 完整旋转流水线
staticvoidlvgl_flush_cb(lv_display_t*display,constlv_area_t*area,uint8_t*px_map){ // ... 取出 device、framebuffer ... staticuint8_trotated_buf[480*800*8]; lv_display_rotation_trotation =lv_display_get_rotation(display); if(rotation != LV_DISPLAY_ROTATION_0) { lv_area_trotated_area = *area; // A. 坐标映射 lv_display_rotate_area(display, &rotated_area); uint32_tsrc_stride =lv_draw_buf_width_to_stride(lv_area_get_width(area), cf); uint32_tdest_stride =lv_draw_buf_width_to_stride(lv_area_get_width(&rotated_area), cf); // C. 像素重排 lv_draw_sw_rotate(px_map, rotated_buf, lv_area_get_width(area),lv_area_get_height(area), src_stride, dest_stride, rotation, cf); // 重定向至物理空间 area = &rotated_area; px_map = rotated_buf; } // ... RGA 封装 → imcopy(RGB565→RGB888) → RTGRAPHIC_CTRL_RECT_UPDATE ... lv_display_flush_ready(display);}
整体流程:旋转判断 → 坐标映射 → 像素重排 → RGA 硬件传输 → 完成通知。旋转逻辑封闭于 if 块内,平台加速独立于旋转处理。
五、工程实践要点
四种旋转角度的区别。物理竖屏 LCD 需要以横屏方向显示时,可选用 90° 或 270°。两种角度的逻辑分辨率相同(均为 800×480),区别在于逻辑坐标原点分别对应物理屏幕的不同角落——90° 时逻辑 (0,0) 对应物理 (0,799),270° 时逻辑 (0,0) 对应物理 (479,0)。180° 适用于物理横屏的倒置显示场景,此时逻辑分辨率与物理分辨率保持一致。
触摸坐标的同步。显示方向发生旋转后,触摸坐标也必须执行同步旋转变换。若通过 lv_indev_set_display 将输入设备绑定至显示对象,LVGL v9 可自动完成触摸坐标的旋转变换。若采用手动方式处理触摸回调,则需在回调中自行实现坐标变换逻辑。
双缓冲模式的注意事项。本文所述项目采用单缓冲 + FULL 渲染模式。若切换为双缓冲 + DIRECT 模式以利用"一个缓冲区渲染、另一个缓冲区传输"的并行优势,旋转逻辑本身无需修改——rotated_buf 为静态分配,每次 flush_cb 调用时直接覆写即可。但若启用多线程架构(例如单独线程负责渲染、另一线程负责 flush),rotated_buf 可能面临并发访问风险,此时需引入互斥保护机制。
配套资料包
想在自己的项目里实现 LVGL 屏幕旋转?我们整理了完整资料包:
完整示例工程源码(RuiChing Studio 可直接导入,含 270° 旋转 + RGA 加速)
旋转配置代码与注释(坐标映射、像素重排关键函数解析)
本篇文章涉及的全部代码片段
-
屏幕
+关注
关注
7文章
1248浏览量
57253 -
RT-Thread
+关注
关注
32文章
1661浏览量
45489 -
LVGL
+关注
关注
3文章
128浏览量
4704
发布评论请先 登录
RT-Thread NUC97x 移植 LVGL
RT-Thread的C语言编码规范
在基于PC的RT-Thread模拟器上搭建LVGL图形库
基于树莓派pico移植LVGL软件包的设计如何去实现呢
树莓派PICO:使用rt-thread micropython软件包联网获取天气
RT-Thread编程指南
RT-Thread全球技术大会:RT-Thread开源重塑软件发展新生态
中新社:RT-Thread携“睿擎平台”亮相工博会 | 媒体视角
像STM32一样轻松玩转 MPU!RT-Thread 睿擎平台 Workshop 上海站开启硬核实战!下一城?你定!
RT-Thread 睿擎派 LVGL 屏幕旋转的软件实现
评论