需求说明

由于设备位置问题,屏幕需要翻转180°显示才好安装,如果没有旋转功能,则需要重新做代码布局,找到 LVGL8的文档,发现显示部分已经有这个的配置,只需要做以下简单配置就搞定了。

硬件部分

芯片:采用华芯微特的SWM341S芯片

屏幕:4.3寸屏幕 分辨率:800x480 触摸:GT9x

技术实现原理

LVGL 的旋转是"软件旋转"

LVGL 8 的显示驱动自带一个方向选项:sw_rotate = 1 打开软件旋转,rotated 指定 90/180/270。它的工作位置在渲染管线和 flush_cb 之间:控件照常按逻辑坐标画进 draw_buf,LVGL 在把缓冲送给你注册的 flush_cb 时按设定角度对坐标/数据做变换,flush_cb 拿到的已经是适配好旋转的结果。

带来的分工是:应用代码、布局、坐标全部不用动(逻辑上仍是 800x480 正常方向),只有最终输出被"转"了 180°——这就是"不用重新做代码布局"的原理。

为什么不用硬件旋转

RGB 接口屏的扫描方向可以由屏驱动 IC 的初始化命令(如 MADCTL)或 LCD 控制器时序来改,那是零开销的"硬件旋转"。但不是所有平台都留了这个口子——本文 SWM341 的 RGB LCD 外设加 4.3 寸 800x480 屏就走不了这条路,于是用软件旋转兜底。代价是每次 flush 多一遍像素搬运,帧率会降,界面不复杂的项目完全可接受。

180° 分辨率不变,触摸却要自己转

  • 180° 旋转横竖分辨率不变,hor_res / ver_res 保持原值即可(90/270 则要对调横竖,以官方文档为准);
  • sw_rotate 只管显示,不管输入:GT9x 报上来的触摸坐标仍是物理方向的,需要在触摸读取回调里自己做镜像变换(x’ = 800-1-x、y’ = 480-1-y),否则就是"显示转过来了、触摸全是错位"——这个功能最经典的坑。

关键代码

关键代码在void lv_port_disp_init(void)这个函数,以下是关键代码部分,缺一不可。

1
2
        disp_drv.sw_rotate = 1;
        disp_drv.rotated = LV_DISP_ROT_180;

完整配置

适配代码所在文件 porting/lv_port_disp.c 这个文件具体根据你项目来,以下是文件具体内容:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/**
 * @file lv_port_disp_templ.c
 *
 */

 /*Copy this file as "lv_port_disp.c" and set this value to "1" to enable content*/
#if 1

/*********************
 *      INCLUDES
 *********************/
#include "lv_port_disp.h"
#include "lv_conf.h"
#include "../lvgl/lvgl.h"
#include "main.h"
/*********************
 *      DEFINES
 *********************/

/**********************
 *      TYPEDEFS
 **********************/

/**********************
 *  STATIC PROTOTYPES
 **********************/
static void disp_init(void);

static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);
#if LV_USE_GPU
static void gpu_blend(lv_disp_drv_t * disp_drv, lv_color_t * dest, const lv_color_t * src, uint32_t length, lv_opa_t opa);
static void gpu_fill(lv_disp_drv_t * disp_drv, lv_color_t * dest_buf, lv_coord_t dest_width,
        const lv_area_t * fill_area, lv_color_t color);
#endif

/**********************
 *  STATIC VARIABLES
 **********************/

/**********************
 *      MACROS
 **********************/

/**********************
 *   GLOBAL FUNCTIONS
 **********************/

void lv_port_disp_init(void)
{
    /*-------------------------
     * Initialize your display
     * -----------------------*/
    disp_init();

    /*-----------------------------
     * Create a buffer for drawing
     *----------------------------*/

    /**
     * LVGL requires a buffer where it internally draws the widgets.
     * Later this buffer will passed to your display driver's `flush_cb` to copy its content to your display.
     * The buffer has to be greater than 1 display row
     *
     * There are 3 buffering configurations:
     * 1. Create ONE buffer:
     *      LVGL will draw the display's content here and writes it to your display
     *
     * 2. Create TWO buffer:
     *      LVGL will draw the display's content to a buffer and writes it your display.
     *      You should use DMA to write the buffer's content to the display.
     *      It will enable LVGL to draw the next part of the screen to the other buffer while
     *      the data is being sent form the first buffer. It makes rendering and flushing parallel.
     *
     * 3. Double buffering
     *      Set 2 screens sized buffers and set disp_drv.full_refresh = 1.
     *      This way LVGL will always provide the whole rendered screen in `flush_cb`
     *      and you only need to change the frame buffer's address.
     */

    /* Example for 1) */
    // static lv_disp_buf_t draw_buf_dsc_1;
    // static lv_color_t draw_buf_1[LV_HOR_RES_MAX * 10];                          /*A buffer for 10 rows*/
    // lv_disp_buf_init(&draw_buf_dsc_1, draw_buf_1, NULL, LV_HOR_RES_MAX * 10);   /*Initialize the display buffer*/

    /* Example for 2) */
    // static lv_disp_buf_t draw_buf_dsc_2;
    // static lv_color_t draw_buf_2_1[LV_HOR_RES_MAX * 10];                        /*A buffer for 10 rows*/
    // static lv_color_t draw_buf_2_2[LV_HOR_RES_MAX * 10];                        /*An other buffer for 10 rows*/
    // lv_disp_buf_init(&draw_buf_dsc_2, draw_buf_2_1, draw_buf_2_2, LV_HOR_RES_MAX * 10);   /*Initialize the display buffer*/

    /* Example for 3) */
    static lv_disp_draw_buf_t draw_buf_dsc_3;
    static lv_color_t draw_buf_3_1[LV_HOR_RES_MAX * LV_VER_RES_MAX]__attribute__((section(".SDRAM"))); /*A screen sized buffer*/
    static lv_color_t draw_buf_3_2[LV_HOR_RES_MAX * LV_VER_RES_MAX]__attribute__((section(".SDRAM"))); /*An other screen sized buffer*/
    lv_disp_draw_buf_init(&draw_buf_dsc_3, draw_buf_3_1, draw_buf_3_2, LV_HOR_RES_MAX * LV_VER_RES_MAX);   /*Initialize the display buffer*/


    /*-----------------------------------
     * Register the display in LVGL
     *----------------------------------*/

    static lv_disp_drv_t disp_drv;                         /*Descriptor of a display driver*/
    lv_disp_drv_init(&disp_drv);                    /*Basic initialization*/

    /*Set up the functions to access to your display*/

    /*Set the resolution of the display*/
    disp_drv.hor_res = LV_HOR_RES_MAX;
    disp_drv.ver_res = LV_VER_RES_MAX;

    /*Used to copy the buffer's content to the display*/
    disp_drv.flush_cb = disp_flush;
		//屏幕翻转180°
		disp_drv.sw_rotate = 1;
		disp_drv.rotated = LV_DISP_ROT_180;
    /*Set a display buffer*/
    disp_drv.draw_buf = &draw_buf_dsc_3;
		
#if LV_USE_GPU
    /*Optionally add functions to access the GPU. (Only in buffered mode, LV_VDB_SIZE != 0)*/

    /*Blend two color array using opacity*/
    disp_drv.gpu_blend_cb = gpu_blend;

    /*Fill a memory array with a color*/
    disp_drv.gpu_fill_cb = gpu_fill;
#endif

    /*Finally register the driver*/
    lv_disp_drv_register(&disp_drv);
}

/**********************
 *   STATIC FUNCTIONS
 **********************/

/*Initialize your display and the required peripherals.*/
static void disp_init(void)
{
    /*LCD显示屏移植*/
    rgb_init();
    LCD_Start(LCD);
}

/*Flush the content of the internal buffer the specific area on the display
 *You can use DMA or any hardware acceleration to do this operation in the background but
 *'lv_disp_flush_ready()' has to be called when finished.*/
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/

//    int32_t x;
//    int32_t y;
//    for(y = area->y1; y <= area->y2; y++) {
//        for(x = area->x1; x <= area->x2; x++) {
//            /*Put a pixel to the display. For example:*/
//            /*put_px(x, y, *color_p)*/
//            color_p++;
//        }
//    }

/*LCD显示屏移植*/
    LCD->L[0].ADDR = (uint32_t)color_p;
    LCD->CR |= (1 << LCD_CR_VBPRELOAD_Pos);
    /*IMPORTANT!!!
     *Inform the graphics library that you are ready with the flushing*/
    lv_disp_flush_ready(disp_drv);
}

/*OPTIONAL: GPU INTERFACE*/
#if LV_USE_GPU

/* If your MCU has hardware accelerator (GPU) then you can use it to blend to memories using opacity
 * It can be used only in buffered mode (LV_VDB_SIZE != 0 in lv_conf.h)*/
static void gpu_blend(lv_disp_drv_t * disp_drv, lv_color_t * dest, const lv_color_t * src, uint32_t length, lv_opa_t opa)
{
    /*It's an example code which should be done by your GPU*/
    uint32_t i;
    for(i = 0; i < length; i++) {
        dest[i] = lv_color_mix(dest[i], src[i], opa);
    }
}

/* If your MCU has hardware accelerator (GPU) then you can use it to fill a memory with a color
 * It can be used only in buffered mode (LV_VDB_SIZE != 0 in lv_conf.h)*/
static void gpu_fill(lv_disp_drv_t * disp_drv, lv_color_t * dest_buf, lv_coord_t dest_width,
                    const lv_area_t * fill_area, lv_color_t color)
{
    /*It's an example code which should be done by your GPU*/
    int32_t x, y;
    dest_buf += dest_width * fill_area->y1; /*Go to the first line*/

    for(y = fill_area->y1; y <= fill_area->y2; y++) {
        for(x = fill_area->x1; x <= fill_area->x2; x++) {
            dest_buf[x] = color;
        }
        dest_buf+=dest_width;    /*Go to the next line*/
    }
}

#endif  /*LV_USE_GPU*/

#else /* Enable this file at the top */

/*This dummy typedef exists purely to silence -Wpedantic.*/
typedef int keep_pedantic_happy;
#endif

代码说明

  • 两行配置加在 lv_port_disp_init() 里 disp_drv 的填充段、lv_disp_drv_register() 之前,注册时即生效;
  • 本文用了官方模板的方式 3:两个全屏缓冲放 SDRAM(__attribute__((section(".SDRAM")))),flush 时直接把缓冲地址交给 LCD 外设扫描输出(LCD->L[0].ADDR = color_p),像素搬运走硬件,CPU 不参与;
  • 模板注释里方式 3 的完整姿势还包括 disp_drv.full_refresh = 1(整帧刷新、只换地址)。本文没开也能正常工作,追求更高帧率时可以加上对比。

踩坑记录与注意事项

  • 触摸错位:sw_rotate 不转输入坐标,GT9x 的 x/y 要在 indev 读取回调里自己镜像(800-1-x / 480-1-y);
  • 两行缺一不可sw_rotate = 1 是开关、rotated = LV_DISP_ROT_180 是角度,只设其中一个不生效;
  • 软件旋转有性能开销:帧率下降明显时,优先找屏驱动 IC 的扫描方向寄存器做硬件旋转(零开销),找不到再回来用 sw_rotate;
  • 90/270 记得对调横竖分辨率,逻辑布局坐标不受影响。