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

目录

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.txt
lv_conf_template.h
lvgl.h
lvgl.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:no
USE_LVGL		?= n

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

ifeq ($(USE_LVGL),y)
LVGL_DIR	?= Libraries
LVGL_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 size
static lv_color_t buf1[ST7735_WIDTH * ST7735_HEIGHT / 10];
// Descriptor of a display driver
static 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 flags
OPT			?= -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 传输结束. 留有空再改了.

热门相关:斗神战帝   天启预报   大神你人设崩了   快递员的艳遇   霸皇纪