How to use Encoder with widgets?

Description:

Want to use Encoder as the input device

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

STM32L on a custom board

What LVGL version are you using?

V7.2.0

What do you want to achieve?

Want to use a thumb-sized LCD with touch screen that outputs only gestures including click, swipe left / right, and long press for page widget lv_page & tile view lv_tileview. There is no x,y finger position returned. The touch screen is like an encoder. I need to change indev_drv from LV_INDEV_TYPE_POINTER to LV_INDEV_TYPE_ENCODER.

What have you tried so far?

Display driver & touch interface implemented. Starting from a simple button with indev_drv registered as a LV_INDEV_TYPE_POINTER and it is working. The button is responding with clicks. Event hook up also working. The problem is that when I changed indev_drv to LV_INDEV_TYPE_ENCODER the button gave no response. Why?

Code to reproduce

//Below is the hal_init() extract for touch registration
lv_indev_drv_t touch_drv;
lv_indev_drv_init(&touch_drv);
touch_drv.type = LV_INDEV_TYPE_ENCODER;
touch_drv.read_cb = my_touch_cb;
lv_indev_drv_register(&touch_drv);

Fcn my_touch_cb is listed below:

bool my_touch_cb(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
	(void) indev_drv;

	finger_t finger = get_touch_gesture();

	if(finger.gesture==IDLE)
	{
		data->state = LV_INDEV_STATE_REL;
	}
	else
	{
		if(finger.gesture==SLIDE_LR)
		{
			data->enc_diff--;
			data->btn_id = LV_KEY_LEFT;
			printf("Btn id = LV_KEY_LEFT @: %d\r\n", (int)SLIDE_LR);
		}
		else if (finger.gesture==SLIDE_RL)
		{
			data->enc_diff++;
			data->btn_id = LV_KEY_RIGHT;
			printf("Btn id = LV_KEY_RIGHT @: %d\r\n", (int)SLIDE_RL);
		}
		else if (finger.gesture==SINGLE_TAP_ANYKEY)
		{
			data->btn_id = LV_KEY_ENTER;
			printf("Btn id = LV_KEY_ENTER @: %d\r\n", (int)SINGLE_TAP_ANYKEY);
		}
		else
		{
			data->enc_diff = 0;
		}
		data->state = LV_INDEV_STATE_PR;
	}
	return false;
}

I think the widgets need to be part of a group.

Thank you, embeddedt. Button obj is responding with the object grouped to the encoder.
Now I am going a step forward to page with the following snippet:

//this is the standard demo released in LVGL repository
void lv_ex_page_1(void)
{
    /*Create a page*/
    lv_obj_t * page = lv_page_create(lv_scr_act(), NULL);
    lv_obj_set_size(page, lv_disp_get_hor_res(NULL), lv_disp_get_ver_res(NULL));
    lv_obj_align(page, NULL, LV_ALIGN_CENTER, 0, 0);

    /*Create a label on the page*/
    lv_obj_t * label = lv_label_create(page, NULL);
    lv_label_set_long_mode(label, LV_LABEL_LONG_BREAK);            /*Automatically break long lines*/
    lv_obj_set_width(label, lv_page_get_width_fit(page));          /*Set the label width to max value to not show hor. scroll bars*/
    lv_label_set_text(label, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, ....\n");

    lv_group_t *g = lv_group_create();
    lv_group_add_obj(g, page);
    lv_indev_set_group(touch_indev, g);
}

I do realize that a page responds to keys including LV_KEY_RIGHT/LEFT/UP/DOWN so I changed my_touch_cb() with data->key registered:

if(finger.gesture==SLIDE_LR)
		{
			data->enc_diff--;
			data->key = LV_KEY_LEFT;
			printf("Btn id = LV_KEY_LEFT @: %d\r\n", (int)SLIDE_LR);
		}
		else if (finger.gesture==SLIDE_RL)
		{
			data->enc_diff++;
			data->key = LV_KEY_RIGHT;
			printf("Btn id = LV_KEY_RIGHT @: %d\r\n", (int)SLIDE_RL);
		}
		else if (finger.gesture==SINGLE_TAP_ANYKEY)
		{
			data->key = LV_KEY_ENTER;
			printf("Btn id = LV_KEY_ENTER @: %d\r\n", (int)SINGLE_TAP_ANYKEY);
		}
		else
		{
			data->enc_diff = 0;
		}
		data->state = LV_INDEV_STATE_PR;

The page moved but the content is distorted. It is very close to success, I think.

@techtoys Maybe this example can help you with something: lv_demo_keypad_encoder

Thank you for the pointer. By grouping the page object with touch_indev has solved the encoder issue. But I find the distortion problem is now more difficult to solve than the indev issue. That is not relevant to indev demonstrated by a manual navigation with lv_page_scroll_ver(page, dist), the screen did scroll but distortion was still there in the absence of user interaction. The issue is related to display driver. I don’t understand why static images and text displaying OK but not with page scroll.

Anyway, using lv_indev_set_group(touch_indev, g) as embedded suggested has enabled Encoder interacting with page widget.

There are a lot of refreshes in quick succession with scrolling; maybe your driver has an issue with that.

Besides the callback function to draw an area on the framebuffer, is there any other interface function for succession update? I tried both DMA and non-DMA SPI transfer to avoid a mistake with screen redraw synchronization but the distortion remains.

Below is a link to video for the result. As you can see the first screen looks normal, with label on the page for long texts. As the page scroll the screen is completely distorted, but it still scrolls somehow.

Video

The only other issue that I can think of is an issue with refreshing areas smaller than the screen. You could try creating a small button and centering it on the screen. If it gets corrupted when you click on it, that is probably the issue.

Looks to me like it is trying to draw sections of the lcd at a time and lv is starting from the beginning instead of moving on to the next section. Kinda like when you print an array and your key isn’t incrementing so you get the first value over and over.

Look at how your driver is drawing the screen. My guess is it is trying to draw lines of text and expecting the next line. That would explain the line height as well.

Yes, it is the issue. Restarting with a simple button with snippet below:

	lv_obj_t * btn = lv_btn_create(lv_scr_act(), NULL);
	lv_obj_align(btn, NULL, LV_ALIGN_CENTER, 0, 0);
	lv_btn_set_fit2(btn, LV_FIT_TIGHT, LV_FIT_NONE);
	lv_obj_t * label = lv_label_create(btn, NULL);
	lv_label_set_text(label, "OK?");

	lv_group_t *g = lv_group_create();
	lv_group_add_obj(g, btn);
	lv_indev_set_group(touch_indev, g);

Video shows corruption

Now, the same demo changed to a full size button with:
lv_cont_set_fit2(btn, LV_FIT_PARENT, LV_FIT_PARENT);

Video shows full screen updating the button

There is a bug with my driver for a small segment update! Still thinking why.

Can you show the contents of flush_cb? Perhaps you’re accessing the color_p buffer incorrectly.

Really thank you for your help. Below is an extract of the code which I hope to show flush_cb and the logic in a clear manner. The listing is shown below:

color_t lv_buf_array[OLED_VER_RES*(OLED_HOR_RES>>3)];
static int16_t seg_y1, seg_y2;

//SPI transfer is invoked only when there is data pending to send (flush_pending()==true) AND FR rising edge to avoid tearing
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	//means FR rising edge 
	if(GPIO_Pin==OLED_FR_Pin)
	{
		if(flush_pending())
		{
			uint8_t cmd[3] = {0x21, (uint8_t)seg_y1, (uint8_t)seg_y2};
			SPI_Write_Command((const uint8_t *)&cmd, 3);//instruct the OLED to update from segment y1 to y2
			
			uint16_t length = (seg_y2-seg_y1+1)*(OLED_HOR_RES>>3);
            //BUFIDX(x, y) =  ((x >> 3) + (y*(OLED_HOR_RES >> 3)))
			spi_transfer_dma((const uint8_t *)&lv_buf_array[BUFIDX(0,seg_y1)], length);
		}
	}
}

//DMA callback after spi_transfer_dma()
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
	flush_pending_flag_clear(); //reset the flush pending flag
	lv_disp_flush_ready(&lv_disp_drv);
}

void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
	//this function draw on lv_buf_array[] in pixel precision
	write_lv_buf_array(area->x1,area->y1,area->x2,area->y2,color_p);
	//now save y1,y2 for global variables seg_y1 and seg_y2 waiting for spi dma transfer on FR
	seg_y1 = area->y1;
	seg_y2 = area->y2;
	
	wait_until_no_dma_operation();
	flush_pending_flag_set();
}

void my_hal_init(void)
{
	display_init();

	static lv_disp_buf_t lv_disp_buf;

	lv_disp_buf_init(&lv_disp_buf, lv_buf_array, NULL, LV_HOR_RES_MAX*LV_VER_RES_MAX);

	lv_disp_drv_init(&lv_disp_drv);

	lv_disp_drv.buffer = &lv_disp_buf;
	lv_disp_drv.flush_cb = my_flush_cb;
	lv_disp_drv.set_px_cb = my_set_px_cb;

	lv_disp = lv_disp_drv_register(&lv_disp_drv);

	/**
	 * Now add the touch screen as an input device
	 */
	lv_indev_drv_t touch_drv;
	lv_indev_drv_init(&touch_drv);
	touch_drv.type = LV_INDEV_TYPE_ENCODER; // LV_INDEV_TYPE_POINTER;
	touch_drv.read_cb = my_touch_cb;
	touch_indev = lv_indev_drv_register(&touch_drv);
}

Explanation

  • Everything starts from a buffer lv_buf_array[] that holds the content of the monochrome OLED. That OLED is similar to a Sharp Memory LCD with the difference that a touch screen is ready, so-called an In-cell OLED display.

  • I am using STM32 with CubeMX-dialect HAL functions. SPI transfer by DMA is used with synchronization with a FR pin (like TE) to avoid screen tearing.

  • Data pending to flush is invoked by my_flush_cb() in which the function write_lv_buf_array(area->x1,area->y1,area->x2,area->y2,color_p) is called. This function draws on lv_buf_array[] in pixel precision by bit mask and shifting. After running write_lv_buf_array(), seg_y1 and seg_y2 are saved with area->y1 and area->y2 waiting for spi_transfer_dma() in HAL_GPIO_EXTI_Callback. I admit that it is not a clean way. After having saved seg_y1 & seg_y2, the function wait_until_no_dma_operation() is called to make sure there is no current DMA operation in progress, and then the flag for flush pending is set by flush_pending_flag_set().

  • Because SPI DMA transfer is used, another callback function HAL_SPI_TxCpltCallback() is required, in which the flag for data flush pending is reset (by flush_pending_flag_clear()) and the LVGL specific function lv_disp_flush_ready(&lv_disp_drv) to let the framework know that SPI transfer is finished.

  • Callback function HAL_GPIO_EXTI_Callback() is called whenever there is a rising edge on FR pin (OLED_FR_Pin) and there is data pending to flush. If both conditions are met, SPI command is sent with command 0x21 to define the screen area to update with seg_y1 and seg_y2. There is no area->x1 and area->x2 because I am sending the whole horizontal line spanning seg_y1 to seg_y2 to avoid using double for-loop. Then the length of data to send is calculated with the key function to call spi_transfer_dma((const uint8_t *)&lv_buf_array[BUFIDX(0, seg_y1)], length). This is the problem.

The current issue with small button corruption on touch event happens with BUFIDX(0, seg_y1) as shown in the first video clip in last post.

When I change seg_y1 to 0 (spi_transfer_dma((const uint8_t *)&lv_buf_array[BUFIDX(0, 0)], length)), the small button now behaves better but there is a very subtle position shift to the left when I click on it. The video below shows the button align in the CENTRE on POR but it shifted to the left with a click. Afterward, the button is no longer aligned to the CENTRE.

Video

Question

  • Should I use the same lv_buf_array[] as the buffer that LVGL draws?
  • When the button is clicked there is a series of screen refresh going on. What happens to the LVGL buffer? In my case lv_buf_array[] is the LVGL buffer, is the content of lv_buf_array[] modified on a touch event?