Friday, December 16, 2022

LVGL with STM32F4 and STM32F7

LVGL stands for Light and Versatile Graphics Library. LVGL is an open-source graphics library providing everything we need to create an embedded GUI with easy-to-use graphical elements, beautiful visual effects, and a low-memory footprint.

In this post, I will show you how to use LVGL with STM32 micro-controller in two different environments, the first example is based on the STM32F429 Discovery Board, and the second example is based on the STM32F769I Discovery board and the third example is based on the Labcenter Electronics based STM32F4 and ILI9341 Simulation.

STM32F7 using LVGL to display Temperature Graph
I had a third plan also which is based on Labcenter Electronics Proteus simulation also, it is working, but it had a lot of issues, if someone wants to refer to that then they can use the project at the following location.

Proteus-Based LVGL and STM32 Project

Some of the issues with the Proteus are as follows:

  • UART with Virtual Terminal doesn't work when PLL is enabled in the clock configuration
  • SPI DMA mode crashing the Proteus Simulation
  • When using SPI 16-Bit data transfer mode, the ILI9341 display module crashes.
  • SPI debugger can't use when there is a lot of data, the system hangs completely.
  • The simulation runs really slow, making it almost unusable.

This LVGL-based program does the following things.

  • First VIBGYOR colors are displayed on display for 4 seconds.
  • Red-Green-Blue Color Mixer is shown on display.
  • The chart containing the temperature data from the LM35DZ sensor is displayed.

STM32F429 Discovery Board

The STM32F429ZI Discovery board kit leverages the capabilities of the STM32F429 high-performance microcontrollers, to allow users to develop rich applications easily with advanced graphical user interfaces. This board consists of a 2.4-inch QVGA TFT LCD based on the ILI9341 display controller.
In this board, the microcontroller uses the SPI module to send the commands to the display module based on the ILI9341 display controller.
STM32F4 and ILI9341 Programming Interface
STM32F4 and ILI9341 Programming Interface

This can be seen in the above programming interface image block diagram, the blue lines represent the SPI lines, where PF7 is the SPI clock and PF8 is the Data Line which can be used as MISO and MOSI also, as this is a bi-directional data line of the SPI communication (This is SPI5 and it can be used in Half-Duplex mode). In this board, the LTDC is used to send the RGB data to the ILI9341 display controller.
The goal of this post is not to tell how to configure LTDC, SPI, and External SDRAM, because the ports are easily available on GitHub.
The following is the link to the official LVGL port on GitHub for the STM32F429 Discovery Board.

I used this port, mainly the driver's files, and created a new project using CubeMX, the benefit of CubeMX I can update the other modules whenever needed.
The following is the demo of the project, I will explain the LVGL project at the bottom of this post.
This project is inside my STM32F4 repository, so it's better to clone the whole repository using the following commands.
git clone https://github.com/xpress-embedo/STM32F4.git
cd STM32F4
git submodule update --init --recursive

Then the project is available under the following path.
"STM32F4/Without_Cube/LVGL_Sample2"

STM32F769I Discovery Board

This discovery board is based on the STM32F769NIH6 microcontroller which has 
2 Mbytes of Flash memory and 512+16+4 Kbytes of RAM, in the BGA216 package. With the onboard ST-LINK/V2-1 supporting USB and can also be used as a virtual COM port.
This discovery kit also has a 4" capacitive touch LCD display with a MIPI® DSI connector (on STM32F769I-DISCO only).
The goal of this post is not to tell how to configure LTDC, MIPI DSI Internal, and External SDRAM, because the ports are easily available on GitHub.
The following is the link to the official LVGL port on GitHub for the STM32F769 Discovery Board.

I used this port, mainly the driver's files, and created a new project using CubeMX, the benefit of CubeMX I can update the other modules whenever needed.
The following is the demo of the project, I will explain the LVGL project at the bottom of this post.

This project is inside my STM32F7 repository, so it's better to clone the whole repository using the following commands.
git clone https://github.com/xpress-embedo/STM32F7.git
cd STM32F7
git submodule update --init --recursive

Then the project is available under the following path.
STM32F7/LVGL_Sample2

Source Code Understanding

As I mentioned previously also, I used references from the officially provided LVGL ports for both of these discovery boards and modified them to use the CubeMX tool which can be useful for future configurations if some other peripheral is needed.
The following are some of the activity diagrams which will help in understanding the overall flow of the project.
Project Overview

Project Interrupt Routines
Display Manager (LVGL)

In this project the main function is "Display_Mng", this function calls all sub-functions which uses different-different LVGL widgets to display different display screens on the TFT. The "Display_Mng" function calls three important functions, and they are as below.

Display VIBGYOR

This function displays the VIBGYOR color on the TFT screen and is like this.
NOTE: If you don't have STM32 Discovery Boards, then you can use LVGL Simulator also for learning and Development.
static void Display_Vibgyor( void )
{
  static lv_style_t style;
  lv_coord_t width = 0u;
  lv_coord_t length = 0u;

  lv_obj_t * V_rectangle;
  lv_obj_t * I_rectangle;
  lv_obj_t * B_rectangle;
  lv_obj_t * G_rectangle;
  lv_obj_t * Y_rectangle;
  lv_obj_t * O_rectangle;
  lv_obj_t * R_rectangle;

  lv_obj_t *act_scr = lv_scr_act();           // Get the active screen object

  R_rectangle = lv_obj_create( act_scr );     // Create Rectangle Object
  O_rectangle = lv_obj_create( act_scr );
  Y_rectangle = lv_obj_create( act_scr );
  G_rectangle = lv_obj_create( act_scr );
  B_rectangle = lv_obj_create( act_scr );
  I_rectangle = lv_obj_create( act_scr );
  V_rectangle = lv_obj_create( act_scr );

  lv_style_init(&style);
  // set the radius to zero
  lv_style_set_radius(&style, 0);
  // by default the object which we created for rectangle has some radius component
  // and it looks bad for this particular example, hence updating style for all
  // created objects
  lv_obj_add_style(R_rectangle, &style, 0);
  lv_obj_add_style(O_rectangle, &style, 0);
  lv_obj_add_style(Y_rectangle, &style, 0);
  lv_obj_add_style(G_rectangle, &style, 0);
  lv_obj_add_style(B_rectangle, &style, 0);
  lv_obj_add_style(I_rectangle, &style, 0);
  lv_obj_add_style(V_rectangle, &style, 0);


  length = lv_disp_get_hor_res(NULL);
  // VIBGYOR are seven colors
  width = lv_disp_get_physical_ver_res(NULL)/7;

  lv_obj_set_size(R_rectangle, length, width);
  lv_obj_align(R_rectangle, LV_ALIGN_TOP_LEFT, 0, 0 );
  lv_obj_set_style_border_color(R_rectangle, lv_palette_main(LV_PALETTE_RED), LV_PART_MAIN );
  lv_obj_set_style_bg_color( R_rectangle, lv_palette_main(LV_PALETTE_RED), LV_PART_MAIN );

  lv_obj_set_size(O_rectangle, length, width );
  // lv_obj_align_to(O_rectangle, R_rectangle, LV_ALIGN_BOTTOM_MID, 0, 0);
  lv_obj_align(O_rectangle, LV_ALIGN_TOP_LEFT, 0, width );
  lv_obj_set_style_border_color(O_rectangle, lv_palette_main(LV_PALETTE_ORANGE), LV_PART_MAIN );
  lv_obj_set_style_bg_color( O_rectangle, lv_palette_main(LV_PALETTE_ORANGE), LV_PART_MAIN );

  lv_obj_set_size(Y_rectangle, length, width );
  // lv_obj_align_to(Y_rectangle, O_rectangle, LV_ALIGN_TOP_LEFT, 0, 0);
  lv_obj_align(Y_rectangle, LV_ALIGN_TOP_LEFT, 0, width*2u );
  lv_obj_set_style_border_color(Y_rectangle, lv_palette_main(LV_PALETTE_YELLOW), LV_PART_MAIN );
  lv_obj_set_style_bg_color( Y_rectangle, lv_palette_main(LV_PALETTE_YELLOW), LV_PART_MAIN );

  lv_obj_set_size(G_rectangle, length, width );
  // lv_obj_align_to(G_rectangle, Y_rectangle, LV_ALIGN_TOP_LEFT, 0, 0);
  lv_obj_align(G_rectangle, LV_ALIGN_TOP_LEFT, 0, width*3u );
  lv_obj_set_style_border_color(G_rectangle, lv_palette_main(LV_PALETTE_GREEN), LV_PART_MAIN );
  lv_obj_set_style_bg_color( G_rectangle, lv_palette_main(LV_PALETTE_GREEN), LV_PART_MAIN );

  lv_obj_set_size(B_rectangle, length, width );
  // lv_obj_align_to(B_rectangle, G_rectangle, LV_ALIGN_TOP_LEFT, 0, 0);
  lv_obj_align(B_rectangle, LV_ALIGN_TOP_LEFT, 0, width*4u );
  lv_obj_set_style_border_color(B_rectangle, lv_palette_main(LV_PALETTE_BLUE), LV_PART_MAIN );
  lv_obj_set_style_bg_color( B_rectangle, lv_palette_main(LV_PALETTE_BLUE), LV_PART_MAIN );

  lv_obj_set_size(I_rectangle, length, width );
  // lv_obj_align_to(Y_rectangle, B_rectangle, LV_ALIGN_TOP_LEFT, 0, 0);
  lv_obj_align(I_rectangle, LV_ALIGN_TOP_LEFT, 0, width*5u );
  lv_obj_set_style_border_color(I_rectangle, lv_palette_main(LV_PALETTE_INDIGO), LV_PART_MAIN );
  lv_obj_set_style_bg_color( I_rectangle, lv_palette_main(LV_PALETTE_INDIGO), LV_PART_MAIN );

  lv_obj_set_size(V_rectangle, length, width );
  // lv_obj_align_to(V_rectangle, I_rectangle, LV_ALIGN_TOP_LEFT, 0, 0);
  lv_obj_align(V_rectangle, LV_ALIGN_TOP_LEFT, 0, width*6u );
  lv_obj_set_style_border_color(V_rectangle, lv_palette_main(LV_PALETTE_DEEP_PURPLE), LV_PART_MAIN );
  lv_obj_set_style_bg_color( V_rectangle, lv_palette_main(LV_PALETTE_DEEP_PURPLE), LV_PART_MAIN );
}

Display RGB Mixer

This function has two main parts one is the function which displays the RGB Mixer on the TFT screen and the other is the Slider Callback function, this function is called whenever their is some changes on the slider.
static void Display_RGBMixer( void )
{
  lv_obj_t *act_scr = lv_scr_act();                     // Get the active screen object

  lv_obj_clean( act_scr );                              // Clear the screen

  // intialize the styles
  lv_style_init(&style);
  // we have to enable other font sizes in menuconfig
  lv_style_set_text_font(&style, &lv_font_montserrat_32);

  // RED, Green and Blue Slider Configuration
  lv_obj_t *slider_r = lv_slider_create( act_scr );     // create a red slider base object
  lv_obj_t *slider_g = lv_slider_create( act_scr );     // create a green slider base object
  lv_obj_t *slider_b = lv_slider_create( act_scr );     // create a blue slider base object

  // Setting Sliders Width
  lv_obj_set_width( slider_r, LV_PCT(80) );
  lv_obj_set_width( slider_g, LV_PCT(80) );
  lv_obj_set_width( slider_b, LV_PCT(80) );

  // Setting Sliders Height
  lv_obj_set_height( slider_r, LV_PCT(4) );
  lv_obj_set_height( slider_g, LV_PCT(4) );
  lv_obj_set_height( slider_b, LV_PCT(4) );

  // Align Sliders with Each Other
  lv_obj_align( slider_r, LV_ALIGN_TOP_MID, 0u, LV_PCT(20) );
  lv_obj_align_to( slider_g, slider_r, LV_ALIGN_TOP_MID, 0u, 70u );
  lv_obj_align_to( slider_b, slider_g, LV_ALIGN_TOP_MID, 0u, 70u );

  // set slider range also (by default it is 0 to 100 but we want till 255)
  lv_slider_set_range( slider_r, 0, 255 );
  lv_slider_set_range( slider_g, 0, 255 );
  lv_slider_set_range( slider_b, 0, 255 );

  // Coloring Sliders, Slider has three parts Main, Indicator and Knob
  // apply red color to the indicator part
  lv_obj_set_style_bg_color( slider_r, lv_palette_main(LV_PALETTE_RED), LV_PART_INDICATOR );
  // apply red color to the knob part
  lv_obj_set_style_bg_color( slider_r, lv_palette_main(LV_PALETTE_RED), LV_PART_KNOB );

  // apply green color to the indicator part
  lv_obj_set_style_bg_color( slider_g, lv_palette_main(LV_PALETTE_GREEN), LV_PART_INDICATOR );
  // apply green color to the knob part
  lv_obj_set_style_bg_color( slider_g, lv_palette_main(LV_PALETTE_GREEN), LV_PART_KNOB );

  // apply blue color to the indicator part
  lv_obj_set_style_bg_color( slider_b, lv_palette_main(LV_PALETTE_BLUE), LV_PART_INDICATOR );
  // apply blue color to the knob part
  lv_obj_set_style_bg_color( slider_b, lv_palette_main(LV_PALETTE_BLUE), LV_PART_KNOB );

  rectangle = lv_obj_create(act_scr);                   // Creates a base object Rectangle to display color
  lv_obj_set_size( rectangle, LV_PCT(93), LV_PCT(33) );
  lv_obj_align_to( rectangle, slider_b, LV_ALIGN_TOP_MID, 0u, 50u );
  lv_obj_set_style_border_color( rectangle, lv_color_black(), LV_PART_MAIN );   // add black border to rectangle
  lv_obj_set_style_border_width( rectangle, 2, LV_PART_MAIN );                  // increase the width of the border by 2px
  lv_obj_set_style_bg_color( rectangle, lv_color_make( 0, 0, 0), LV_PART_MAIN); // all sliders are at zero, so background color should be black

  // Create Main Heading Label
  lv_obj_t *heading = lv_label_create(act_scr);
  lv_label_set_text( heading, "RGB Mixer");
  lv_obj_align( heading, LV_ALIGN_TOP_MID, 0u, LV_PCT(5) );
  lv_obj_add_style( heading, &style, 0 );

  // Creating labels for individual slider current values
  red.slider_type = SLIDER_TYPE_RED;
  red.label = lv_label_create(act_scr);
  lv_label_set_text( red.label, "0");
  // lv_obj_align_to( red.label, slider_r, LV_ALIGN_TOP_MID, 0u, 0u );         // this will display inside slider
  lv_obj_align_to( red.label, slider_r, LV_ALIGN_OUT_TOP_MID, 0u, 0u );    // this will display outside slider

  green.slider_type = SLIDER_TYPE_GREEN;
  green.label = lv_label_create(act_scr);
  lv_label_set_text( green.label, "0");
  lv_obj_align_to( green.label, slider_g, LV_ALIGN_OUT_TOP_MID, 0u, 0u );

  blue.slider_type = SLIDER_TYPE_BLUE;
  blue.label = lv_label_create(act_scr);
  lv_label_set_text( blue.label, "0");
  lv_obj_align_to( blue.label, slider_b, LV_ALIGN_OUT_TOP_MID, 0u, 0u );

  // add event callbacks for sliders
  lv_obj_add_event_cb( slider_r, Slider_Callback, LV_EVENT_VALUE_CHANGED, &red );
  lv_obj_add_event_cb( slider_g, Slider_Callback, LV_EVENT_VALUE_CHANGED, &green );
  lv_obj_add_event_cb( slider_b, Slider_Callback, LV_EVENT_VALUE_CHANGED, &blue );
}

And the below is the "Slider_Callback" function, this function updates the rectangle color based on the values of Slider.
static void Slider_Callback( lv_event_t *e )
{
  static int32_t red, green, blue;
  int32_t slider_value = 0;
  // get the object (slider) for which the we received the event
  lv_obj_t *slider = lv_event_get_target(e);
  // extract the object (slider) user data
  RGB_Mixer_s *user_data = lv_event_get_user_data(e);
  // get the current slider value
  slider_value = lv_slider_get_value(slider);

  // now we have to update the slider labels
  lv_label_set_text_fmt( user_data->label, "%ld", slider_value );

  // now we have to update the color of the rectangle object based on the current
  // selected values of the color
  if( user_data->slider_type == SLIDER_TYPE_RED )
  {
    red = slider_value;
  }
  else if( user_data->slider_type == SLIDER_TYPE_GREEN )
  {
    green = slider_value;
  }
  else if( user_data->slider_type == SLIDER_TYPE_BLUE )
  {
    blue = slider_value;
  }
  // now we have the color information, update the rectangle color
  // NOTE: rectangle object must be file global else we will not be able to
  // update it from here
  lv_obj_set_style_bg_color( rectangle, lv_color_make( red, green, blue), LV_PART_MAIN);
  // the function `lv_color_make` can form colors if red, green and blue color
  // are specified

  if( (red == 255) && (green == 255) && (blue == 255) )
  {
    disp_state = DISP_STATE_TEMP_SENSOR;
  }
}

And the last part is displaying the Temperature data as line chart on the graph, and it's content is as below.
This is main function, and there is an another function also, which is called periodically at the rate of 1 second to update the latest temperature data on the graph.

static void Display_TemperatureChart( void )
{
  uint16_t idx = 0u;
  // this should match with the temperature buffer length
  uint16_t chart_hor_res = 260;
  uint16_t chart_ver_res = lv_disp_get_ver_res(NULL) - 100;
  uint8_t *data = Display_GetTempData();

  lv_obj_clean( lv_scr_act() );                         // Clean the screen

  // Create a chart object
  chart = lv_chart_create( lv_scr_act() );

  // Create a label for Title text
  lv_obj_t * lbl_title = lv_label_create( lv_scr_act() );
  lv_label_set_text( lbl_title, "Temperature Graph");
  lv_obj_set_style_text_align( lbl_title, LV_TEXT_ALIGN_CENTER, 0);
  lv_obj_align( lbl_title, LV_ALIGN_TOP_MID, 0, 0 );
  lv_obj_add_style( lbl_title, &style, 0 );

  // Set the chart size (Size should be set properly because we wanted to display
  // chart title and some data on y-axis also)
  // display is 320x240
  lv_obj_set_size( chart, (lv_disp_get_hor_res(NULL) - 100), chart_ver_res );
  // TODO: XS I don't want to center it, will check later
  // lv_obj_center( chart );
  lv_obj_align( chart, LV_ALIGN_CENTER, LV_PCT(5), 0 );
  // lv_obj_align( chart, LV_ALIGN_BOTTOM_RIGHT, 0, 0 );

  // Set Chart Type to Line Chart
  lv_chart_set_type( chart, LV_CHART_TYPE_LINE );
  // By Default the number of points are 10, update it to chart width
  lv_chart_set_point_count( chart, chart_hor_res );
  // Update mode shift or circular, here shift is selected
  lv_chart_set_update_mode( chart, LV_CHART_UPDATE_MODE_SHIFT );
  // Specify Vertical Range
  lv_chart_set_range( chart, LV_CHART_AXIS_PRIMARY_Y, 10, 60);
  // Tick Marks and Labels
  // 2nd argument is axis, 3rd argument is major tick length, 4th is minor tick length
  // 5th is number of major ticks on the axis
  // 6th is number of minor ticks between two major ticks
  // 7th is enable label drawing on major ticks
  // 8th is extra size required to draw labels and ticks
  lv_chart_set_axis_tick(chart, LV_CHART_AXIS_PRIMARY_Y, 10, 5, 10, 2, true, 50);

  // Add Data Series
  temp_series = lv_chart_add_series(chart, lv_palette_main(LV_PALETTE_RED), LV_CHART_AXIS_PRIMARY_Y);

  for( idx=0; idx<chart_hor_res; idx++ )
  {
    temp_series->y_points[idx] = (lv_coord_t)*(data+idx);
  }

  lv_chart_refresh(chart); /*Required after direct set*/
}

Below is the chart refresh function.
static void Display_TemperatureChartRefresh( void )
{
  uint16_t idx = 0u;
  uint8_t *data = Display_GetTempData();
  // this should match with the temperature buffer length
  uint16_t chart_hor_res = 260;

  for( idx=0; idx<chart_hor_res; idx++ )
  {
    temp_series->y_points[idx] = (lv_coord_t)*(data+idx);
  }

  lv_chart_refresh(chart); /*Required after direct set*/
}

No comments:

Post a Comment