Touchscreen calibration and porting to version 8

Is there a touchscreen calibration sample for lvgl? I found some code, I’m not allowed to post links here, but you can find it when you search in Google for “TP_CAL_STATE_INIT”, tpcal.c. But it doesn’t compile with version 8. How can I change it to compile it for version 8? Is there a guide how to replace the old functions? For example looks like lv_btn_set_style is not available anymore, but I guess version 8 has button styles?

The original code was pretty horrible, with lots of code duplications. I wrote a new version:

#include "tpcal.h"
#include <stdio.h>
#include <stdlib.h>
#include "src/lvgl.h"

// calibration coefficients
float alphaX;
float betaX;
float deltaX;
float alphaY;
float betaY;
float deltaY;

Callback onCalibrationEnd;

static lv_point_t p[3];
static int step;
static lv_obj_t *label_main;
static lv_timer_t* timer;
static lv_obj_t* circle;

const char* helpText[] = { "Click the circle in upper left-hand corner\nand hold down until the next circle appears.",
                           "Click the circle in uppper right-hand corner\nand hold down until the next circle appears.",
                           "Click the circle in lower right-hand corner\nand hold down until the text disappears."
                         };
static lv_point_t circlePos[3];

static int currentCenterX;
static int currentCenterY;

void centeredGrowing(lv_obj_t* obj, int32_t size)
{
  lv_obj_set_size(obj, size, size);
  lv_obj_set_pos(obj, currentCenterX - size / 2, currentCenterY - size / 2);
}

void addCircle(int centerX, int centerY)
{
  if (circle) lv_obj_del(circle);

  // create style
  lv_style_t* style = malloc(sizeof(lv_style_t));
  lv_style_init(style);
  lv_style_set_radius(style, 10);
  lv_style_set_bg_opa(style, LV_OPA_COVER);
  lv_style_set_bg_color(style, lv_color_hex(0));

  // create object for drawing a circle, set style, and remove clickable flag
  circle = lv_obj_create(lv_scr_act());
  lv_obj_remove_style_all(circle);
  lv_obj_add_style(circle, style, 0);
  lv_obj_clear_flag(circle, LV_OBJ_FLAG_CLICKABLE);

  // initial size is 0
  lv_obj_set_size(circle, 0, 0);
  currentCenterX = centerX;
  currentCenterY = centerY;
  lv_obj_set_pos(circle, centerX, centerY);

  // animation for increasing the size of the circle
  static lv_anim_t a;
  lv_anim_init(&a);
  lv_anim_set_var(&a, circle);
  lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t) centeredGrowing);
  lv_anim_set_values(&a, 0, 20);
  lv_anim_set_time(&a, 300);
  a.act_time = 0;
  lv_anim_start(&a);
}

void nextStep(int x, int y)
{
  // save and show coordinates from last step
  if (step > 0) {
    p[step - 1].x = x;
    p[step - 1].y = y;
  }

  if (step == 3) {
    lv_obj_del(circle);
    circle = NULL;
    lv_obj_del(label_main);
    lv_timer_del(timer);

    // calculate coefficients based on the algorithms described here:
    // https://www.ti.com/lit/an/slyt277/slyt277.pdf
    float x1 = circlePos[0].x;
    float y1 = circlePos[0].y;
    float x2 = circlePos[1].x;
    float y2 = circlePos[1].y;
    float x3 = circlePos[2].x;
    float y3 = circlePos[2].y;
    float xs1 = p[0].x;
    float ys1 = p[0].y;
    float xs2 = p[1].x;
    float ys2 = p[1].y;
    float xs3 = p[2].x;
    float ys3 = p[2].y;

    float delta = (xs1 - xs3) * (ys2 - ys3) - (xs2 - xs3) * (ys1 - ys3);

    float deltaX1 = (x1 - x3) * (ys2 - ys3) - (x2 - x3) * (ys1 - ys3);
    float deltaX2 = (xs1 - xs3) * (x2 - x3) - (xs2 - xs3) * (x1 - x3);
    float deltaX3 = x1 * (xs2 * ys3 - xs3 * ys2) - x2 * (xs1 * ys3 - xs3 * ys1) + x3 * (xs1 * ys2 - xs2 * ys1);

    float deltaY1 = (y1 - y3) * (ys2 - ys3) - (y2 - y3) * (ys1 - ys3);
    float deltaY2 = (xs1 - xs3) * (y2 - y3) - (xs2 - xs3) * (y1 - y3);
    float deltaY3 = y1 * (xs2 * ys3 - xs3 * ys2) - y2 * (xs1 * ys3 - xs3 * ys1) + y3 * (xs1 * ys2 - xs2 * ys1);

    alphaX = deltaX1 / delta;
    betaX = deltaX2 / delta;
    deltaX = deltaX3 / delta;

    alphaY = deltaY1 / delta;
    betaY = deltaY2 / delta;
    deltaY = deltaY3 / delta;

    onCalibrationEnd();
  } else {
    // show help text for next step
    lv_label_set_text(label_main, helpText[step]);

    // show circle
    addCircle(circlePos[step].x, circlePos[step].y);
    step++;
  }
}

void inputPolling(lv_timer_t * timer)
{
  // get input device and read touch position
  lv_indev_t* indev = lv_indev_get_next(NULL);
  lv_indev_data_t data;
  indev->driver->read_cb(indev->driver, &data);

  // sample touch position after half a second delay after pressed, to get stable coordinates
  static int lastState = LV_INDEV_STATE_REL;
  static int counter = 0;
  if (lastState == LV_INDEV_STATE_REL && data.state == LV_INDEV_STATE_PR) {
    counter = 5;
  }
  if (counter) {
    counter--;
    if (counter == 0) nextStep(data.point.x, data.point.y);
  }
  lastState = data.state;
}

void tpcalCreate(Callback fun)
{
  onCalibrationEnd = fun;

  // set calibration coefficients to identy
  alphaX = 1;
  betaX = 0;
  deltaX = 0;
  alphaY = 0;
  betaY = 1;
  deltaY = 0;

  timer = lv_timer_create(inputPolling, 50,  NULL);

  // create help text label
  label_main = lv_label_create(lv_scr_act());
  lv_obj_set_width(label_main, LV_HOR_RES);
  lv_obj_set_pos(label_main, 0, LV_VER_RES - 50);
  lv_obj_set_style_text_align(label_main, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);

  // initialize reference positions
  circlePos[0].x = 50;
  circlePos[0].y = 50;
  circlePos[1].x = LV_HOR_RES - 50;
  circlePos[1].y = 50;
  circlePos[2].x = LV_HOR_RES - 50;
  circlePos[2].y = LV_VER_RES - 50;

  // show first circle and help text
  step = 0;
  nextStep(0, 0);
}

PS: there are some memory leaks, but I don’t care :slight_smile:

It assumes that you use the coefficients in your touchscreen driver as well, for example like this:

bool my_touchpad_read( lv_indev_drv_t * indev_driver, lv_indev_data_t * data )
{
  if (ts.touched()) {
    lastTouched = true;
    TS_Point p = ts.getPoint();

    float xs = p.x;
    float ys = p.y;
    int x = alphaX * xs + betaX * ys + deltaX;
    int y = alphaY * xs + betaY * ys + deltaY;
    
    data->state = LV_INDEV_STATE_PR;

    /*Set the coordinates*/
    data->point.x = x;
    data->point.y = y;
    lastX = x;
    lastY = y;
  } else {
    lastTouched = false;
    data->state = LV_INDEV_STATE_REL;
  }

  return false;
}


void lvgl_init()
{
  lv_init();
  lv_disp_draw_buf_init( &draw_buf, buf, NULL, screenWidth * 10 );

  /*Initialize the display*/
  static lv_disp_drv_t disp_drv;
  lv_disp_drv_init( &disp_drv );
  /*Change the following line to your display resolution*/
  disp_drv.hor_res = screenWidth;
  disp_drv.ver_res = screenHeight;
  disp_drv.flush_cb = my_disp_flush;
  disp_drv.draw_buf = &draw_buf;
  lv_disp_drv_register( &disp_drv );

  /*Initialize the (dummy) input device driver*/
  static lv_indev_drv_t indev_drv;
  lv_indev_drv_init( &indev_drv );
  indev_drv.type = LV_INDEV_TYPE_POINTER;
  indev_drv.read_cb = my_touchpad_read;
  lv_indev_drv_register( &indev_drv );
}

The calibration function also provides a callback to show the main GUI. Here is an example for the Arduino environment, which shows the calibration screen when started first, and then stores it in the EEPROM:

#define VALID_CALIBRATION_MARKER 42

// write a float value at the specified address into the EERPOM
void writeFloat(int address, float value) {
  char* ptr = (char*) &value;
  for (int i = 0; i < 4; i++) EEPROM.write(address + i, ptr[i]);
}

// read a float value from the specified addres from the EEPROM
float readFloat(int address) {
  float value;
  char* ptr = (char*) &value;
  for (int i = 0; i < 4; i++) ptr[i] = EEPROM.read(address + i);
  return value;
}

// save the touchscreen calibration values
void saveCalibration() {
    EEPROM.write(0, VALID_CALIBRATION_MARKER);
    writeFloat(1, alphaX);
    writeFloat(5, betaX);
    writeFloat(9, deltaX);
    writeFloat(13, alphaY);
    writeFloat(17, betaY);
    writeFloat(21, deltaY);
    startApplication();
}

void setup() {
  // init LCD
  SSD1963_Initial();

  // init LVGL library
  lvgl_init();

  // test if the touchscreen sensor is calibrated
  if (EEPROM.read(0) == VALID_CALIBRATION_MARKER) {
    // load calibration from EEPROM
    alphaX = readFloat(1);
    betaX = readFloat(5);
    deltaX = readFloat(9);
    alphaY = readFloat(13);
    betaY = readFloat(17);
    deltaY = readFloat(21);
    startApplication();
  } else {
    // run calibration procedure and save calibration data to EEPROM
    tpcalCreate(saveCalibration);
  }
}

startApplication is called after the calibration, or after reset, if already calibrated, and can show the main GUI.

It is my first LVGL program, comments for improvement are welcome. And feel free to add it to the github repository.

PS: I didn’t find an easy way to capture all events. This is why I used the timer for polling the input driver directly. Might be useful if it would be possible to add a global event hook, as for example Qt or GDI of win32 allows it, if it is not already implemented and I missed it.

The header file:

#ifndef TPCAL_H
#define TPCAL_H

// calibration coefficients
extern float alphaX;
extern float betaX;
extern float deltaX;
extern float alphaY;
extern float betaY;
extern float deltaY;

typedef void (*Callback)();

void tpcalCreate(Callback fun);

#endif

very nice new tpcal.
How did you compile and link and use it?

Thanks Pepito

Thanks. I used it with Arduino, with a Teensy 4.1. Here is how it looks like:

I integrated the latest LVGL version 8 library into the project in the src directory, and changed all include directives, always easier to have all code in one project.

@frankbuss
Would you be interested in adding it as an “extra” widget?

It is not a widget, but might be useful in examples, if someone wants to add it.

True, but it’s also more than an example. If it were a “widget” it would have its own page in the docs and probably got more visibility.

Maybe we could have an “apps” folder in src/extra?

@embeddedt what do you think?

I think it makes more sense for it to be an example, not a widget. It can still have its own page in the documentation.

Perhaps, once the screen is calibrated, there could be an extra utility function (void tpcal_adjust_coords(lv_indev_data_t *data)) that could be called at the end of someone’s read_cb function. That way, the calibration logic could be independent of the actual touchscreen.

Okay, I don’t insist to make it a widget.

Then we should have an examples/indev folder, right?
We could have group-related examples there too.
(BTW, could we add keyboard and mouse wheel support to the example iframes?)

I’ve added it to lv_sim_emscripten so it should be visible when the docs are rebuilt next.

There is a limitation. The way the SDL keyboard driver works, it seems that it doesn’t handle key rollover correctly. This causes keys to be dropped when typing rapidly.

I am a pretty fast typist, so if I type “test”, but I press the “e” before the “t” is fully released, LVGL only seems to register the “t”.

If you try it on a text box in lv_demo_widgets and then try it in the browser search bar, I think you will see what I mean.

Great, thanks! Now the LVGL examples are rebuilt too. It works well with the mouse wheel but unfortunately the keyboard events are lost. See Text area (lv_textarea) — LVGL documentation