STM32G474 + ST7789 + LVGL 포팅

신규 프로젝트가 LCD 패널이 추가되면서, UI 출력이 필요한 상황이라 예전에 STM32F429에 LVGL 라이브러리를 포팅한 경험이 있어 이것을 사용하려고 한다. LVGL 라이브러리는 MIT 라이센서로 상용으로 이용해도 문제가 없기 때문에 신규 프로젝트에 사용하여도 문제가 없을 것으로 판단된다.

 

20년도 말에 접한 LVGL 라이브러리 버전은 7.x ~ 8.1.0 버전을 사용하였는데, 24년도 현재 v9.1.0 버전까지 릴리즈되어 최신 버전을 사용하려고 한다. 최신 버전을 사용하려는 이유는 LVGL 라이브러리가 ST7789 IC를 포팅 가이드를 제공하고 있어, 최신 버전을 사용하는 것이 정신 건강에 이로울 것이다. 포팅 가이드는 아래 사이트를 참고하면 된다.

 

Step-by-step Guide: How to use the LVGL v9 LCD drivers with STM32 devices — LVGL documentation

This guide is intended to be a step-by-step instruction of how to configure the STM32Cube HAL with the new TFT-LCD display drivers introduced in LVGL v9.0. The example code has been tested on the STM32F746-based Nucleo-F746ZG board with an ST7789-based LCD

docs.lvgl.io

STM32CubeIDE에서 신규 프로젝트를 생성하고 LVGL 라이브러리를 서브모듈로 등록하도록 한다.

$ git submodule add git@github.com:lvgl/lvgl.git Core/Src/extlib/lvgl
$ git submodule init
$ git submodule update

lv_conf.h 파일을 수정하여 STM32G474 Chipset 스펙에 맞게 수정하여야 한다. G474 Chipset은 F429 Chipset과 다르게 DMA2D 모듈을 포함하지 않고 있기 때문에 디코딩을 SW로 처리하며, LVGL 동작 여부를 확인하는 것이기 때문에 None OS로 설정하도록 한다. 그리고 LVGL 실행되기 위한 최소 메모리는 38kbyte가 필요하기 때문에 38kbyte로 하였으나 실행에 문제가 있어 40kbyte로 설정하였다. 마지막으로 LCD 모듈이 RGB565 방식으로 16비트 컬러를 사용하기 때문에 Color Depth를 16으로 설정하도록 한다.

 

추후 FreeRTOS를 올리고 불필요한 컴포넌트를 제거하여 최적화를 하여야 하는데, 과연 제대로 동작할지 걱정스럽고 또 얼마나 삽질을 할란지 벌써부터 식은 땀이 나지만, 그건 나중에 생각하기로 하고 LVGL 포팅 가이드에서 수정한 사항을 정리하도록 하겠다.

 

CMakeList는 따로 수정하지 않고, STM32CubeIDE에 LVGL 디렉토리를 포함하였기 때문에 CubeIDE에서 자동으로 생성된 빌드 스크립트를 그대로 사용하였다. 이 부분은 보드 브링업 과정에서 추후 다시 정리할 예정이다.

 

LVGL ST7789 IC 포팅가이드 역시 4-Wire 기준으로 설명되어 있기 때문에 3-Wire 방식으로 변경해야하며, DMA 없이 포팅하였다. 추후 느리다면 다시 DMA 방식을 적용할지 말지 고민해야겠다.

 

NUCLEO 개발보드의 STM32G474 클럭은 24MHz 외부 크리스탈을 사용하여 시스템 클럭을 168MHz로 설정하였다.

포팅가이드 문서에 따라 Basic Timer을 TIM7를 1ms 타이머 인터럽트를 생성하여 LVGL tick을 증가시키도록 하였으며, 생성방법은 아래와 같다.

static void MX_TIM7_Init(void)
{
  TIM_MasterConfigTypeDef sMasterConfig = {0};
  htim7.Instance = TIM7;
  htim7.Init.Prescaler = 168 - 1;
  htim7.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim7.Init.Period = 999;
  htim7.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim7) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim7, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM7)
  {
    lv_tick_inc(1);
  }
}

ST7789 IC Reset은 포팅가이드 문서를 따라 동일하게 아래와 같이 작성하였다.

static int32_t lcd_io_init(void)
{
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET);
  HAL_Delay(100);
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET);
  HAL_Delay(100);

  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);
  return HAL_OK;
}

ST7789 IC의 Command / Data를 위한 TX 데이터 랩핑 함수를 아래와 같이 코딩하였으며, LVGL 포팅가이드는 SPI 4-Wire 방식으로 구현되어 있어,  내가 갖고 있는 LCD 모듈의 SPI 3-Wire 방식으로 변경하였다.

#define BUS_SPI1_POLL_TIMEOUT 0x1000U
#define SPI_MAX_BUF 4096

static void lcd_send_cmd(lv_display_t *disp, const uint8_t *cmd, size_t cmd_size, const uint8_t *param, size_t param_size)
{
  uint16_t cmd_buf[cmd_size];

  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET);
  for (int i = 0; i < cmd_size; i++) cmd_buf[i] = (0x00 << 8) | cmd[i];
  if (HAL_SPI_Transmit(&hspi2, (uint8_t *)cmd_buf, cmd_size, BUS_SPI1_POLL_TIMEOUT) == HAL_OK)
  {
    for (int i = 0; i < param_size; i++) send_data[i] = (0x01 << 8) | param[i];
    HAL_SPI_Transmit(&hspi2, (uint8_t *)send_data, param_size, BUS_SPI1_POLL_TIMEOUT);
  }
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);
}

static void lcd_send_color(lv_display_t *disp, const uint8_t *cmd, size_t cmd_size, uint8_t *param, size_t param_size)
{
  uint16_t cmd_buf[cmd_size];

  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET);
  for (int i = 0; i < cmd_size; i++) cmd_buf[i] = (0x00 << 8) | cmd[i];
  if (HAL_SPI_Transmit(&hspi2, (uint8_t *)cmd_buf, cmd_size, BUS_SPI1_POLL_TIMEOUT) == HAL_OK)
  {
    while(param_size > 0) {
      uint16_t length = param_size > SPI_MAX_BUF ? SPI_MAX_BUF : param_size;
      for (int i = 0 ; i < length ; i++) send_data[i] = (0x01 << 8) | param[i];
      HAL_SPI_Transmit(&hspi2, (uint8_t *)send_data, length, BUS_SPI1_POLL_TIMEOUT);
      param += length;
      param_size -= length;
    }
    lv_display_flush_ready(lcd_disp);
  }
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);
}

SPI 3-Wire 방식은 DCX 입력핀이 없는 관계로, 모든 데이터에 Command / Data 판별할 수 있는 MSB에 1비트를 추가하였으며, 제한된 메모리로 인해 SPI TX 데이터를 한번에 전송하는 것이 아니라 일정 Chunk 단위(4K)로 전송하였다. 만약 한번에 데이터를 전송할 경우 스택에 깨져 시스템에 행업되는 현상이 발생한다.

 

아래 코드는 3개의 라벨을 생성하여 간단히 RGB 컬러로 문자열을 출력한 것이다. 참고로 내가 갖고 있는 LCD 모듈은 Invert ON 시켜야 정상적인 색상이 출력된다.

#define LCD_H_RES 240
#define LCD_V_RES 320

void rgb_color(void)
{
  lv_obj_t *scr = lv_screen_active();
  lv_obj_set_style_bg_color(scr, lv_color_white(), 0);
  lv_obj_set_style_bg_opa(scr, LV_OPA_100, 0);

  lv_obj_t* lbl_red = lv_label_create(scr);
  lv_obj_set_align(lbl_red, LV_ALIGN_TOP_MID);
  lv_obj_set_height(lbl_red, LV_SIZE_CONTENT);
  lv_obj_set_width(lbl_red, LV_SIZE_CONTENT);
  lv_obj_set_style_text_font(lbl_red, &lv_font_montserrat_24, 0);
  lv_obj_set_style_text_color(lbl_red, lv_color_make(0xff, 0, 0), 0);
  lv_label_set_text(lbl_red, "RED");

  lv_obj_t* lbl_green = lv_label_create(scr);
  lv_obj_set_align(lbl_green, LV_ALIGN_CENTER);
  lv_obj_set_height(lbl_green, LV_SIZE_CONTENT);
  lv_obj_set_width(lbl_green, LV_SIZE_CONTENT);
  lv_obj_set_style_text_font(lbl_green, &lv_font_montserrat_24, 0);
  lv_obj_set_style_text_color(lbl_green, lv_color_make(0, 0xff, 0), 0);
  lv_label_set_text(lbl_green, "GREEN");

  lv_obj_t* lbl_blue = lv_label_create(scr);
  lv_obj_set_align(lbl_blue, LV_ALIGN_BOTTOM_MID);
  lv_obj_set_height(lbl_blue, LV_SIZE_CONTENT);
  lv_obj_set_width(lbl_blue, LV_SIZE_CONTENT);
  lv_obj_set_style_text_font(lbl_blue, &lv_font_montserrat_24, 0);
  lv_obj_set_style_text_color(lbl_blue, lv_color_make(0, 0, 0xff), 0);
  lv_label_set_text(lbl_blue, "BLUE");
}

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  
  MX_GPIO_Init();
  MX_LPUART1_UART_Init();
  MX_SPI2_Init();
  MX_TIM7_Init();
  HAL_TIM_Base_Start_IT(&htim7);

  lv_init();
  lcd_io_init();
  lcd_disp = lv_st7789_create(LCD_H_RES, LCD_V_RES, LV_LCD_FLAG_NONE, lcd_send_cmd, lcd_send_color);
  lv_display_set_rotation(lcd_disp, LV_DISPLAY_ROTATION_270);
  lv_st7789_set_invert(lcd_disp, true);

  uint32_t buf_size = LCD_H_RES * LCD_V_RES / 10 * lv_color_format_get_size(lv_display_get_color_format(lcd_disp));
  lv_color_t *buf1 = lv_malloc(buf_size);
  lv_color_t *buf2 = lv_malloc(buf_size);

  if (buf1 == NULL || buf2 == NULL) {
    printf("display draw buffer malloc failed\r\n");
  }
  lv_display_set_buffers(lcd_disp, buf1, buf2, buf_size, LV_DISPLAY_RENDER_MODE_PARTIAL);
  // lv_demo_widgets();
  rgb_color();
  uint32_t count = 0;
  while (1)
  {
    printf("count = %ld\r\n", count++);
    lv_timer_handler();
    HAL_Delay(10);
  }
}

lv_demo_widgets() 실행하여 다양한 위젯 데모를 확인하려고 하였으나, STM32G474 메모리(128Kb) 부족으로 실행이 불가하여 간단히 라벨을 출력하여 정상적으로 실행되는지 확인하였다.

LVGL Label 컴포넌트가 정상적으로 동작되는 것을 확인하였으나, RGB가 매우 이상하다는 것을 확인하였다. 이 문제는 다음 포스트에서 정리하도록 하겠다. 전체 예제코드는 아래 깃허브에서 받을 수 있다.

 

STM32/G474/LVGL at master · highgon2/STM32

STM32 TestCode. Contribute to highgon2/STM32 development by creating an account on GitHub.

github.com