5

AIR32F103(十) 在无系统环境和FreeRTOS环境集成LVGL - Milton

 1 year ago
source link: https://www.cnblogs.com/milton/p/17204239.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

LVGL简介

嵌入式常用的图形显示库

对设备的要求是 "all you need is at least 32kB RAM and 128 kB Flash, a C compiler, a frame buffer, and at least an 1/10 screen sized buffer for rendering". 最低要求是128KB Flash, 但实际上这个大小基本上什么也做不了, 所以直接用256K Flash 的 AIR32F103CCT6 和 AIR32F103RPT6.

集成LVGL到AIR32F103

Principle 大佬写过一篇 Air32F103试玩-移植LVGL+FreeRTOS Keil5 用户可以参考. 基于STM32标准库, 用的屏幕是 GC9306X 320x240LCD.

我没有这个型号的屏幕, 手里能找到现成的串口屏只有一个128x160的ST7735, 就用这个做测试吧.

参考LVGL的文档, 这两片内容差不多的, 第二篇会更细节一点

需要做的步骤为

  1. 将 lvgl 库目录放到项目里
  2. 复制一份 lvgl/lv_conf_template.h , 改名为 lv_conf.h 并修改定制
  3. 在项目中需要使用lvgl的地方, 包含 lvgl/lvgl.h 头文件
  4. 建一个定时器, 每隔1到10毫秒调用一次 lv_tick_inc(x) 用于lvgl内部定时. 如果不用这个方法, 就要定义 LV_TICK_CUSTOM 让 LVGL 可以直接读取时间.
  5. 调用 lv_init() 执行初始化
  6. 创建一个图像缓冲, 最小为1/10个屏幕尺寸所需要的数据大小.
  7. 实现一个绘图函数, 用于LVGL调用后往设备的指定区域写入显示内容.
  8. 如果有输入设备, 还可以再实现一个输入读取函数
  9. 在主循环 main while(1) 中, 如果是RTOS环境则在一个循环任务中, 每隔几个毫秒调用一次 lv_timer_handler(), 用于LVGL绘制更新图像显示.

最小化实现

1.将LVGL添加到项目中

https://github.com/lvgl/lvgl/releases 下载LVGL, 当前版本是v8.3.5, 解压.

在项目 Libraries 下创建lvgl目录, 复制必须的文件到这个目录下

demos/examples/src/LICENCE.txtlv_conf_template.hlvgl.hlvgl.mk

复制后的 Libraries 目录结构为

Libraries├───AIR32F10xLib├───CMSIS├───Debug├───DeviceSupport├───EPaper├───FreeRTOS├───Helix├───LDScripts├───lvgl│ ├───demos│ ├───examples│ └───src│ ├───core│ ├───draw│ ├───extra│ ├───font│ ├───hal│ ├───misc│ └───widgets

在 Makefile 中添加 LVGL 选项

# Build with lvgl, y:yes, n:noUSE_LVGL ?= n

LVGL的编译列表和头文件路径都已经在 lvgl.mk 里定义好了, 这里只需要把它 include 进来, 再合并到项目的列表中.

ifeq ($(USE_LVGL),y)LVGL_DIR ?= LibrariesLVGL_DIR_NAME ?= lvgl include Libraries/lvgl/lvgl.mk CFILES += $(CSRCS)INCLUDES += Libraries/lvgl else CFLAGS ?= endif

将 USE_LVGL 设为 y 之后, make 就会带上 LVGL 一起编译. 因为 LVGL 文件很多, 编译时间较长, 可以根据自己电脑的CPU个数设置并发编译, 例如对于8个逻辑核的L480, 可以执行

make -j8

因为编译结果有200多KByte, 写入的速度也很慢, 暂时没有什么好办法.

2. 定制 lv_conf.h

将 lvgl/lv_conf_template.h, 复制到 user 目录下, 改名为 lv_conf.h, 编辑

#if 0改为#if 1

/* clang-format off */#if 1 /*Set it to "1" to enable content*/

因为ST7735支持的是2byte的像素, 色深设为 16

/*Color depth: 1 (1 byte per pixel), 8 (RGB332), 16 (RGB565), 32 (ARGB8888)*/#define LV_COLOR_DEPTH 16

再往下, 都是用0和1代表对应功能项的关和开, 可以保持默认. 因为ST7735屏幕分辨率较小, 所以再修改一下字体, 将 LV_FONT_MONTSERRAT_10改为1, 将LV_FONT_MONTSERRAT_14改为0 启用10像素字体

#define LV_FONT_MONTSERRAT_10 0#define LV_FONT_MONTSERRAT_12 0#define LV_FONT_MONTSERRAT_14 1

再设置一下LV_FONT_DEFAULT, 改为&lv_font_montserrat_10, 替换为刚才启用的 10像素字体

/*Always set a default font*/#define LV_FONT_DEFAULT &lv_font_montserrat_14

3. 创建 lv_tick_inc(x) 定时器

这里使用TIM3, 将定时间隔设为1毫秒, 开启 TIM_IT_Update 中断

void TIM3_Configuration(void){ TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // Set counter limit to 100 -- interval will be 1ms TIM_TimeBaseStructure.TIM_Period = 100 - 1; /** * Clock source of TIM2,3,4,5,6,7: if(APB1 prescaler =1) then PCLK1 x1, else PCLK1 x2 * */ if (clocks.HCLK_Frequency == clocks.PCLK1_Frequency) { // clock source is PCLK1 x1. // Note: TIM_Prescaler is 16bit, [0, 65535], given PCLK1 is 36MHz, divider should > 550 TIM_TimeBaseStructure.TIM_Prescaler = clocks.PCLK1_Frequency / 100000 - 1; } else { // clock source is PCLK1 x2 TIM_TimeBaseStructure.TIM_Prescaler = clocks.PCLK1_Frequency * 2 / 100000 - 1; } TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // TDTS = Tck_tim TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // Enable interrupt from 'TIM update' TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); // NVIC config NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM3, ENABLE);}

创建TIM3的中断回调函数, 因为定时器间隔为1毫秒, 因此使用lv_tick_inc(1)

void TIM3_IRQHandler(void){ if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { // Clear INT flag TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // Required for the internal timing of LVGL lv_tick_inc(1); }}

如果提示找不到 lv_tick_inc(), 需要加上对头文件 lvgl/lvgl.h 的 include

4. 创建绘图函数

这里涉及到三部分: 初始化 SPI 和对应的 GPIO, 初始化 ST7735, 最后才是 ST7735 的绘图函数.

初始化 GPIO, 这4个pin是需要声明为推挽输出的 PA2:BL, PA3:CS, PA4:DC(Data/Command), PA6:RESET

void APP_GPIO_Config(void){ GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_6);}

初始化 SPI, 这里还需要设置 PA5:SCK/SCL 和 PA7:SI/SDA

void APP_SPI_Config(void){ GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA,GPIO_Pin_5 | GPIO_Pin_7); SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial = 0; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE);}

初始化 ST7735, 这部分已经在 st7735.c 中封装, 直接在main()中调用即可

ST7735_Init();

创建 ST7735 的区域绘图函数

void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p){ ST7735_WriteAddrWindow(area->x1, area->y1, area->x2, area->y2, (uint16_t *)color_p); // Indicate you are ready with the flushing lv_disp_flush_ready(disp);}

对应的 ST7735_WriteAddrWindow() 函数实现, 因为来源是16bit, SPI接口是8bit, 每一次调用分别写入两次

void ST7735_WriteAddrWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t *data){ uint32_t tmp, i; if (x1 > x2) { tmp = x1; x1 = x2; x2 = tmp; } if (y1 > y2) { tmp = y1; y1 = y2; y2 = tmp; } tmp = (x2 - x1 + 1) * (y2 - y1 + 1); ST7735_CS_LOW; ST7735_SetAddrWindow(x1, y1, x2, y2); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); for(i = 0; i < tmp; i ++) { SPI_I2S_SendData(SPI1, (uint8_t)(*data >> 8)); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET); SPI_I2S_SendData(SPI1, (uint8_t)(*data & 0xFF)); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET); data++; } ST7735_CS_HIGH;}

5. 创建图像缓冲, 初始化LVGL

最小为1/10个屏幕尺寸所需数据大小

static lv_disp_draw_buf_t draw_buf;// Declare a buffer for 1/10 screen sizestatic lv_color_t buf1[ST7735_WIDTH * ST7735_HEIGHT / 10];// Descriptor of a display driverstatic lv_disp_drv_t disp_drv;

在 main() 中进行初始化, 注意这部分官网给代码里的类型不太对, 这部分的代码已经修改

lv_init();// Initialize the display buffer.lv_disp_draw_buf_init(&draw_buf, buf1, NULL, ST7735_WIDTH * ST7735_HEIGHT / 10); lv_disp_drv_init(&disp_drv); /*Basic initialization*/disp_drv.flush_cb = my_disp_flush; /*Set your driver function*/disp_drv.draw_buf = &draw_buf; /*Assign the buffer to the display*/disp_drv.hor_res = ST7735_WIDTH; /*Set the horizontal resolution of the display*/disp_drv.ver_res = ST7735_HEIGHT; /*Set the vertical resolution of the display*/lv_disp_drv_register(&disp_drv); /*Finally register the driver*/

6. 主循环添加 lv_timer_handler()

因为这个 ST7735 没有触屏功能, 所以输入读取函数就省了. 在 main() while(1)主循环中加上 lv_timer_handler()

while (1) { lv_timer_handler(); Delay_Ms(10); }

6. 执行示例

经过以上的设置, LVGL就已经集成到项目中了, 可以运行LVGL自带的一些例子查看控件的显示效果

文字标签, 居中和滚动的效果

lv_example_label_1();
lv_example_btn_1();

以上LVGL整合示例的完整源代码已经提交到 GitHub: https://github.com/IOsetting/air32f103-template/tree/master/Examples/NonFreeRTOS/SPI/ST7735_LVGL

修改为 DMA 输出

上面的例子是使用 SPI_I2S_SendData() 函数传输图像数据的, 可以修改为 DMA 传输, 因为传输方式的变化, 外设初始化和图像更新要做对应的调整, 这里没有使用中断.

1. 外设调整

GPIO 不变, 启用 SPI 的 DMA

APP_SPI_Config()中启用SPI1 DMA

/* Enable SPI1 DMA TX request */ SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);

开启 DMA 时钟

void APP_DMA_Configuration(void){ RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);}

因为DMA每次传输的数量在变化, 所以DMA的初始化在图像输出的方法里

2. 修改图像更新方法

图像更新方法的变化比较大, 这里需要根据输入的坐标, 计算实际的数据长度, 并对DMA进行初始化, 然后启动传输, 等待完成后关闭DMA

void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p){ uint16_t len; DMA_InitTypeDef initStructure; len = (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1) * 2; ST7735_CS_LOW; ST7735_SetAddrWindow(area->x1, area->y1, area->x2, area->y2); /* DMA1 Channel3 (triggered by SPI1 Tx event) Config */ DMA_DeInit(DMA1_Channel3); initStructure.DMA_BufferSize = len; initStructure.DMA_M2M = DMA_M2M_Disable; initStructure.DMA_DIR = DMA_DIR_PeripheralDST; initStructure.DMA_MemoryBaseAddr = (uint32_t)color_p; initStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; initStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; initStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR; initStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; initStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; initStructure.DMA_Priority = DMA_Priority_High; initStructure.DMA_Mode = DMA_Mode_Normal; DMA_Init(DMA1_Channel3, &initStructure); // Start transfer DMA_Cmd(DMA1_Channel3, ENABLE); while (!DMA_GetFlagStatus(DMA1_FLAG_TC3)); DMA_ClearFlag(DMA1_FLAG_TC3); DMA_Cmd(DMA1_Channel3, DISABLE); ST7735_CS_HIGH; // Indicate you are ready with the flushing lv_disp_flush_ready(disp);}

3. 修改色彩字节顺序

上面修改完成后, 再次运行LVGL示例, 会发现颜色不正确, 这是因为在DMA传输中, 将一个 16bit 强转为两个 8bit 了, ST7735收到的两个字节的顺序有变化, 需要编辑 lv_conf.h, 将LV_COLOR_16_SWAP设为1

/*Swap the 2 bytes of RGB565 color. Useful if the display has an 8-bit interface (e.g. SPI)*/#define LV_COLOR_16_SWAP 1

以上LVGL DMA 示例的完整源代码已经提交到 GitHub: https://github.com/IOsetting/air32f103-template/tree/master/Examples/NonFreeRTOS/SPI/ST7735_LVGL_DMA

集成到 FreeRTOS

进一步, 在 FreeRTOS 中运行 DMA 传输的 LVGL. 在 FreeRTOS 中 LVGL 的初始化是一样的, 有变化的是初始化的时间点, 还有延时函数的修改

1. 将初始化从 main() 移入任务handler

新建lvglTaskHandler()用于处理LVGL初始化, 缓存初始化和执行benchmark, 并用固定间隔调用lv_timer_handler()

static void lvglTaskHandler(void *pvParameters){ TickType_t xLastWakeTime = xTaskGetTickCount(); const TickType_t xPeriod = pdMS_TO_TICKS(10); (void)(pvParameters); // Suppress "unused parameter" warning ST7735_Init(); lv_init(); // Initialize the display buffer. lv_disp_draw_buf_init(&draw_buf, buf1, NULL, ST7735_WIDTH * ST7735_HEIGHT / 10); lv_disp_drv_init(&disp_drv); /*Basic initialization*/ disp_drv.flush_cb = my_disp_flush; /*Set your driver function*/ disp_drv.draw_buf = &draw_buf; /*Assign the buffer to the display*/ disp_drv.hor_res = ST7735_WIDTH; /*Set the horizontal resolution of the display*/ disp_drv.ver_res = ST7735_HEIGHT; /*Set the vertical resolution of the display*/ lv_disp_drv_register(&disp_drv); /*Finally register the driver*/ lv_demo_benchmark(); while (1) { lv_timer_handler(); vTaskDelayUntil(&xLastWakeTime, xPeriod); }}

在 main() 中创建任务, 栈深度1024, 需要 4KByte 内存

xTaskCreate( lvglTaskHandler, // Task function point "LVGL Task", // Task name 1024, // Stack size, each take 4 bytes(32bit) NULL, // Parameters LVGL_TASK_PRORITY, // Priority NULL); // Task handler

2. 修改延时函数

更新图像的方法不变, 但是需要修改 ST7735 的延时函数, 修改 st7735.c, 引入FreeRTOS.h, 将Delay_Ms(ms);替换为 vTaskDelay(ms);

3. 中断设置

在 FreeRTOSConfig.h 中, 设置系统最高的, 可以安全使用FreeRTOS方法的中断优先级为1

#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 0x01

因为 AIR32F103 的中断为 3 bit, 可用的优先级为 0 到 7, 这样对于优先级为 0 的中断是不受FreeRTOS控制的, 小于等于 1 的是受FreeRTOS控制的, 可以在中断处理中调用 FreeRTOS 的方法.

将TIM3的中断优先级设置为2

NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;NVIC_Init(&NVIC_InitStructure);

4. 裁剪 LVGL

因为集成FreeRTOS, 加上运行 Benchmark, 256KB 的 Flash 容量就捉襟见肘了, 默认配置下编译出来会有300多KB, 需要进行压缩

首先确认编译参数已经优化, rules.mk 中, 优化项改为-O3-Os

# c flagsOPT ?= -O3

编辑 lv_conf.h 关闭一切不必要的组件, 使用尽可能小的字体(可以用10像素字体), 具体的改动可以参考示例代码.

以上LVGL+FreeRTO 示例的源代码已经提交到 GitHub: https://github.com/IOsetting/air32f103-template/tree/master/Examples/FreeRTOS/LVGL/ST7735_128x160

以上就是在 AIR32F103 上集成 LVGL 的步骤和说明, ST7735 也是常见模块. 对于 DMA 的例子, 可以进一步修改为使用中断判断 DMA 传输结束. 留有空再改了.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK