Understanding Rotary Encoder (or physical) input to UI elements

Description

DISCLAMER: I am so sorry for asking, I looked high and low for information in last 3 days to understand, but came up empty handed.

I am a lowly mechanical engineer and i am struggling to understand how to go from my “count” variable to display it on the screen. I need help since i am not getting anywhere. My last UI i made before i switched to stm32 was hard coded and i had easy control of everything but its tedious to code. I need help to understand how to integrate my inputs to the UI (assume I am a coding noob). I know i am supposed to add events to UI elements, but there are a lot of options and i am overwhelmed.

What MCU/Processor/Board and compiler are you using?

i am using a stm32h723vt6 with a parallel interface ssd1963 with FMC. I am using cubeIDE and eez studio. Currently using LVGL version 9.xx.

What do you want to achieve?

Understand how to integrate physical inputs to display elements. For now just to get a box on the display to display the rotary encoder count.

What have you tried so far?

I have read the documentation (lv_indev) and i did the initialization of with the 3 lines of code and wrote a separate function for the encoder (encoder code was running inside a encoder timer call back). Currently stuck here.

Code to reproduce

Add the relevant code snippets here.

The code block(s) should be between ```c and ``` tags:

//basic testing code before using LVGL
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
  /* Prevent unused argument(s) compilation warning */

		  count=(TIM2->CNT)/2;

		  LCD_Rect_Fill(0, 0, 800, 480, BLACK);

		  LCD_Rect_Fill(0, 0, count, 480, 0xff900c3e);
		  char temp[5];
		  sprintf(temp,"%d",count);
		  LCD_Font(5, 100,temp , _16_Segment_16_Full, 2, YELLOW);

		  if(count>60)
		  	 		  {
		  	 			  LCD_Font(100, 120, "what now?", _9_Serif_Bold, 4, YELLOW);
		  	 		  }

}

// LVGL setup.

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{

		  count=(TIM2->CNT)/2;
}

void encoder_read(lv_indev_t * indev, lv_indev_data_t * data){
  data->enc_diff = count;

  if(enc_pressed()) data->state = LV_INDEV_STATE_PRESSED;
  else data->state = LV_INDEV_STATE_RELEASED;
}

void my_flush_cb(lv_display_t * display, const lv_area_t * area, uint8_t * px_map)
{
    /* The most simple case (also the slowest) to send all rendered pixels to the
     * screen one-by-one.  `put_px` is just an example.  It needs to be implemented by you. */

	LCD_Window(area->y1, area->x1, area->y2, area->x2);


    uint16_t * buf16 = (uint16_t *)px_map; /* Let's say it's a 16 bit (RGB565) display */
    int32_t x, y;
    for(y = area->y1; y <= area->y2; y++) {
        for(x = area->x1; x <= area->x2; x++) {
        	LCD_Send_Dat(*buf16);

            buf16++;
        }
    }

    /* IMPORTANT!!!
     * Inform LVGL that flushing is complete so buffer can be modified again. */
    lv_display_flush_ready(display);
}

int main(void)
{

  MPU_Config();
  SCB_EnableICache();
  SCB_EnableDCache();

  HAL_Init();
  SystemClock_Config();
  PeriphCommonClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_FMC_Init();
  MX_OCTOSPI1_Init();
  MX_SPI1_Init();
  MX_CORDIC_Init();
  MX_FMAC_Init();
  MX_DMA2D_Init();
  MX_TIM2_Init();

  /* Initialize interrupts */
  MX_NVIC_Init();


  LCD_Init();
  lv_init();
  lv_tick_set_cb(HAL_GetTick);
  lv_display_t * display1 = lv_display_create(800, 480);
  lv_display_set_buffers(display1, buf1, NULL, sizeof(buf1), 
  LV_DISPLAY_RENDER_MODE_PARTIAL);
  lv_display_set_flush_cb(display1, my_flush_cb);
  ui_init();

 
  lv_indev_t * indev = lv_indev_create();        
  lv_indev_set_type(indev, LV_INDEV_TYPE_ENCODER);  
  lv_indev_set_read_cb(indev, my_input_read);  


  HAL_TIM_Encoder_Start_IT(&htim2, TIM_CHANNEL_ALL);


  while (1)
  {
   
	  uint32_t time_till_next = lv_timer_handler();
	  HAL_Delay(time_till_next);


  }
 
}


Screenshot and/or video

If possible, add screenshots and/or videos about the current state.

Welcome,

For clarification, what is working right now? Do you get anything on your display? I cannot help you with the flush callback but displaying a variable on the screen should not be too hard.

Kind regards

I just have a basic text box and a generic spinner.
image

but for now i will be happy to just have a white screen that displays the current count from the encoder so i can see how to interface my button inputs to LVGL functions. If i can do that i can learn the rest.

Hello,

Good to see that LVGL itself is functioning fine.

I see you have a label setup already that currently displays “lvgl test”.
You could use this for displaying that rotary encoder data for the time being.

I find that displaying variables works best using a timer to update a label periodically.
https://docs.lvgl.io/master/details/main-components/timer.html#timer

See the example code in the documentation above for creating a timer and assigning a callback function. This function is called every x ms, depending on the parameters used when calling lv_timer_create().
In this callback function you can update the label on screen. LVGL has built-in text formatting for displaying variables, like sprintf in C.

I would do something like this. I wrote this from the top of my head so it may not compile correctly, should work though.


static int encoderValue = 0;
static lv_obj_t* labEncoder; // encoder label

static void timer_update(lv_timer_t* timer);

void init()
{
   lv_timer_create(timer_update, 500, NULL) // call timer_update every 500ms

   lv_obj_t* labEncoder = lv_label_create(lv_screen_active());
   if (labEncoder != NULL)
   {
      lv_obj_center(labEncoder);
      lv_label_set_text(labEncoder, "waiting for timer...");
   }
}

static void timer_update(lv_timer_t* timer)
{
   // TODO: set encoder value somewhere...

  if (labEncoder != NULL && lv_obj_is_valid(labEncoder))
  {
     lv_label_set_text_fmt(labEncoder, "Encoder value: %d", encoderValue);
  }
}

I will most likely reply tomorrow again. thanks for the help. If i have time today when i get back i will try further and see how far i get.

So a update. I got it working. the main issue is that the original guide i was following to get started used

uint32_t time_till_next = lv_timer_handler();
HAL_Delay(time_till_next);

in the while loop and it “broke” everything.

changed to this in the while loop.

lv_timer_handler();
ui_tick();

and I can update the label object now without issue with the below code.

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{

	      count=(TIM2->CNT);
		  update=true;
}

//while loop in main
while (1)
  {

	  lv_timer_handler();
	  ui_tick();

	  if(update)
	  {
	  	sprintf(countChar,"%3d%%",count);
	  	lv_label_set_text_fmt(objects.count_test, countChar);
	  	update=false;
	    }

}

tbh, i was about to start tearing my hair out. also had weird intermitted issues with the count register values because of the the original code in the while loop.

Hello,

Glad to hear that it works now, but I am surprised that it does. Updating your UI really should only be done through functions that are called by LVGLs timer handler, else issues with memory management and timing could occur. If you were to make your application more complicated I think this solution will stop working at some point.

See:

yes, i understand. Issue was that no matter what i tried, i could not update the label text outside of setup. Now that i have something to play with, i can add a timer like in the code sippet you posted.

Basically i am throwing as much shoddy code against the wall (to see what sticks) to get something going, then refining it as i go.

1 Like

Typically you are going to update the UI in response to something. If it’s user input then the updates would happen inside of a callback that you assign to a specific LVGL object. If it is from reading a pin on an MCU or an attached device then you would check for that input in the main loop and act accordingly in the main loop. You should be calling lv_task_handler from the main loop as well but only when the tick gets incremented. I would have the tick incremented inside of a callback for a hardware timer. This will allow for proper time keeping. calling the task handler in LVGL each loop of the main thread isn’t going to hurt anything. It will have some cost to it in terms of processor use but if there is nothing that needs to be done it is going to exit without really changing anything. You can use a flag that you can toggle the state of to let the main thread know if it should run the task handler or not.

i have already added a separate timer that calls the timer handler every 5ms.

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if(htim==&htim3)
  {
	  lv_timer_handler();
  }
	  
}

I am looking into the lvgl input devices (Input Device (lv_indev) — LVGL documentation) for the button and rotary encoder i want to use.

I want to keep the use of HAL_Delay to a minimum as well.

This is not very good choice, ISR context isnt safe for… read lvgl safety doc
Operating system and interrupts — LVGL documentation

thanks i will do this instead " flag or some value in the interrupt" and run the timer handler in loop.

like this

volatile bool Tupdate=1;


void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if(htim==&htim3)
  {  
         Tupdate=1;  
  }
	  
}



int main(void)
{
while (1)
  {

	  if (Tupdate)
	  {
		  lv_timer_handler();
		  Tupdate=0;
	  }
	  
  }

}




This is better, but handler dnt require this precise timing, only require valid tick elapsed info.

true, but i have access to 24 timers with this controller and it will make future additions of this project simpler.

This is my previous UI on the arduino mega.

old UI

i was about 1/8th done with it but i was hitting flash limitation and due to the very low screen refresh rate, my code was very complex (I2C data transfer rate). Moved over to stm32 for the increased speed, features, rom and ram. LVGL and eez studio will also help me make the UI on a separate design tool. but i will need to learn it.

lv_tick_inc is the ONLY thing that is thread safe and also ISR safe. There is no memory allocation as the result of calling that function. It can be called cleanly from an ISR without any problems. It is actually the best way of handling the time keeping aspects of LVGL because the hardware timers are typically called on time. Add a simple boolean flag to let your main loop know that the timer has been incremented so it can call lv_task_handler and when that happens reset the boolean flag

static uint8_t call_handler = 0;

static void ISR_timer_cb(...)
{
    lv_tick_inc(3);
    call_handler = 1;
}

// set up hardware timer here and set it to be called every 3 milliseconds or so.

static void main_loop(void) 
{

    while (1) {
        if (call_handler) {
            call_handler = 0;
            lv_task_handler();
        }
        // other user code to run in the main loop.
    }
}