NuttX Touch Panel Driver for PineDio Stack BL604

đź“ť 21 Apr 2022

Touch Panel Calibration for Pine64 PineDio Stack BL604 RISC-V Board

Touch Panel Calibration for Pine64 PineDio Stack BL604 RISC-V Board

PineDio Stack BL604 is Pine64’s newest microcontroller board, based on Bouffalo Lab’s BL604 RISC-V + WiFi + Bluetooth LE SoC.

(Available any day now!)

PineDio Stack is super interesting for an IoT Gadget…

It comes with a Colour LCD Touchscreen! (240 x 240 pixels)

Today we’ll talk about PineDio Stack’s Hynitron CST816S I2C Touch Panel and the driver we created for Apache NuttX RTOS…

Which was inspired by JF’s CST816S Driver for PineDio Stack… (Thanks JF!)

Let’s go inside the driver…

Touch Panel is connected in the middle, between the connectors for the Heart Rate Sensor (bottom left) and ST7789 Display (top left)

Touch Panel is connected in the middle, between the connectors for the Heart Rate Sensor (bottom left) and ST7789 Display (top left)

1 CST816S Touch Panel

What is CST816S? Where is it used?

Inside PineDio Stack is CST816S, an I2C Capacitive Touch Panel by Hynitron…

We don’t have the detailed docs for CST816S, but we have a Reference Driver for the Touch Panel…

This is the same Touch Panel used in Pine64’s PineTime Smartwatch…

Which explains why we have so many drivers available for CST816S: Arduino, FreeRTOS, RIOT OS, Rust, Zephyr OS, …

CST816S Operating Modes

(From CST816S Datasheet)

So it works like any other I2C Device?

CST816S is a peculiar I2C Device… It won’t respond to I2C Commands until we tap the screen and wake it up!

That’s because it tries to conserve power: It powers off the I2C Interface when it’s not in use. (Pic above)

So be careful when scanning for CST816S at its I2C Address 0x15. It might seem elusive until we tap the screen.

The I2C Address of CST816S is defined in bl602_bringup.c

#ifdef CONFIG_INPUT_CST816S
/* I2C Address of CST816S Touch Controller */
#define CST816S_DEVICE_ADDRESS 0x15
#include <nuttx/input/cst816s.h>
#endif /* CONFIG_INPUT_CST816S */

PineDio Stack Touch Panel

(From PineDio Stack Schematic)

1.1 CST816S Pins

How is CST816S wired to PineDio Stack?

According to the schematic above, CST816S is wired to PineDio Stack like so…

BL604 PinCST816S Pin
GPIO 1SDA
GPIO 2SCL
GPIO 9Interrupt
GPIO 18Reset

(We won’t use the Reset pin in our driver)

The CST816S Pins are defined in board.h

/* I2C Configuration */
#define BOARD_I2C_SCL (GPIO_INPUT | GPIO_PULLUP | GPIO_FUNC_I2C | GPIO_PIN2)
#define BOARD_I2C_SDA (GPIO_INPUT | GPIO_PULLUP | GPIO_FUNC_I2C | GPIO_PIN1)
...
#ifdef CONFIG_INPUT_CST816S
/* CST816S Touch Controller for PineDio Stack: GPIO Interrupt */
#define BOARD_TOUCH_INT (GPIO_INPUT | GPIO_FLOAT | GPIO_FUNC_SWGPIO | GPIO_PIN9)
#endif  /* CONFIG_INPUT_CST816S */

What’s the Interrupt Pin?

When we touch the screen, CST816S triggers a GPIO Interrupt and activates the I2C Interface (for a short while).

Note that CST816S doesn’t trigger an interrupt when the screen is no longer touched.

We’ll handle this in our CST816S Driver.

NuttX Touchscreen Device

2 NuttX Touchscreen Drivers

How do Touchscreen Drivers work on NuttX?

Touchscreen Drivers are documented here…

We learnt more by inspecting these Touchscreen Drivers…

The MBR3108 Driver looks structurally similar to our CST816S Driver (since both are I2C). So we copied the code as we built our CST816S Driver.

(We copied the MAX11802 Driver for reading Touch Data Samples)

Let’s talk about the data format…

NuttX Touch Data

(Source)

2.1 Touch Data

How are Touch Data Samples represented in NuttX?

NuttX defines a standard data format for Touch Data Samples that are returned by Touchscreen Drivers…

/* The typical touchscreen driver is a read-only, input character device
 * driver.the driver write() method is not supported and any attempt to
 * open the driver in any mode other than read-only will fail.
 *
 * Data read from the touchscreen device consists only of touch events and
 * touch sample data.  This is reflected by struct touch_sample_s.  This
 * structure is returned by either the driver read method.
 *
 * On some devices, multiple touchpoints may be supported. So this top level
 * data structure is a struct touch_sample_s that "contains" a set of touch
 * points.  Each touch point is managed individually using an ID that
 * identifies a touch from first contact until the end of the contact.
 */
struct touch_sample_s
{
  int npoints;                   /* The number of touch points in point[] */
  struct touch_point_s point[1]; /* Actual dimension is npoints */
};

(Source)

For our driver, we’ll return only one Touch Point.

Here’s the NuttX Definition of a Touch Point…

/* This structure contains information about a single touch point.
 * Positional units are device specific.
 */
struct touch_point_s
{
  uint8_t  id;        /* Unique identifies contact; Same in all reports for the contact */
  uint8_t  flags;     /* See TOUCH_* definitions above */
  int16_t  x;         /* X coordinate of the touch point (uncalibrated) */
  int16_t  y;         /* Y coordinate of the touch point (uncalibrated) */
  int16_t  h;         /* Height of touch point (uncalibrated) */
  int16_t  w;         /* Width of touch point (uncalibrated) */
  uint16_t gesture;   /* Gesture of touchscreen contact */
  uint16_t pressure;  /* Touch pressure */
  uint64_t timestamp; /* Touch event time stamp, in microseconds */
};

(Source)

Our driver returns the first 4 fields…

And sets the remaining fields to 0.

What about Touch Gestures? Like swiping and scrolling?

Touch Gestures are supported in the CST816S Driver for PineTime InfiniTime. (See this)

Someday we might support Touch Gestures in our NuttX Driver.

2.2 Read Touch Data

NuttX Apps will open “/dev/input0” and call read() repeatedly to fetch Touch Data Samples from the driver…

//  Open "/dev/input0"
int fd = open("/dev/input0", O_RDONLY | O_NONBLOCK);

//  Read one sample
struct touch_sample_s sample;
int nbytes = read(fd, &sample, sizeof(struct touch_sample_s));

(Source)

This populates a touch_sample_s struct, which we’ve seen earlier.

The code above comes from the LVGL Test App, which we’ll run later to test our driver.

(Calling read() repeatedly might be bad for performance, instead we should call poll() to block until touch data is available)

3 Load The Driver

Before we cover the internals of our driver, let’s load the CST816S Driver at NuttX Startup: bl602_bringup.c

#ifdef CONFIG_INPUT_CST816S
//  I2C Address of CST816S Touch Controller
#define CST816S_DEVICE_ADDRESS 0x15
#include <nuttx/input/cst816s.h>
#endif  //  CONFIG_INPUT_CST816S
...
#ifdef CONFIG_INPUT_CST816S
int bl602_bringup(void) {
  ...
  //  Init I2C bus for CST816S
  struct i2c_master_s *cst816s_i2c_bus = bl602_i2cbus_initialize(0);
  if (!cst816s_i2c_bus) {
    _err("ERROR: Failed to get I2C%d interface\n", 0);
  }

  //  Register the CST816S driver
  ret = cst816s_register(
    "/dev/input0",          //  Device Path
    cst816s_i2c_bus,        //  I2C Bus
    CST816S_DEVICE_ADDRESS  //  I2C Address
  );
  if (ret < 0) {
    _err("ERROR: Failed to register CST816S\n");
  }
#endif  //  CONFIG_INPUT_CST816S

This initialises our CST816S Driver and registers it at “/dev/input0”.

cst816s_register comes from our CST816S Driver, let’s dive in…

4 Initialise Driver

At NuttX Startup, we call cst816s_register to initialise our CST816S Driver. The function is defined below: cst816s.c

//  Initialise the CST816S Driver
int cst816s_register(FAR const char *devpath, FAR struct i2c_master_s *i2c_dev, uint8_t i2c_devaddr) {

  //  Allocate the Device Struct
  struct cst816s_dev_s *priv = kmm_zalloc(
    sizeof(struct cst816s_dev_s)
  );
  if (!priv) {
    ierr("Memory allocation failed\n");
    return -ENOMEM;
  }

We begin by allocating the Device Struct that will remember the state of our driver.

(Device Struct cst816s_dev_s is defined here)

We populate the Device Struct and initialise the Poll Semaphore…

  //  Init the Device Struct
  priv->addr = i2c_devaddr;  //  I2C Address
  priv->i2c  = i2c_dev;      //  I2C Bus

  //  Init the Poll Semaphore
  nxsem_init(&priv->devsem, 0, 1);

(Which will be used for blocking callers to poll())

Next we register the driver with NuttX at “/dev/input0”…

  //  Register the driver at "/dev/input0"
  int ret = register_driver(
    devpath,             //  Device Path
    &g_cst816s_fileops,  //  File Operations
    0666,                //  Permissions
    priv                 //  Device Struct
  );
  if (ret < 0) {
    kmm_free(priv);
    ierr("Driver registration failed\n");
    return ret;
  }

(We’ll see g_cst816s_fileops later)

Remember that CST816S will trigger GPIO Interrupts when we touch the screen.

We attach our Interrupt Handler that will handle the GPIO Interrupts…

  //  Configure GPIO interrupt to be triggered on falling edge
  DEBUGASSERT(bl602_expander != NULL);
  IOEXP_SETOPTION(
    bl602_expander,  //  BL602 GPIO Expander
    gpio_pin,        //  GPIO Pin
    IOEXPANDER_OPTION_INTCFG,            //  Configure interrupt trigger
    (FAR void *) IOEXPANDER_VAL_FALLING  //  Trigger on falling edge
  );

  //  Attach GPIO interrupt handler
  handle = IOEP_ATTACH(
    bl602_expander,                //  BL602 GPIO Expander
    (ioe_pinset_t) 1 << gpio_pin,  //  GPIO Pin converted to Pinset
    cst816s_isr_handler,  //  GPIO Interrupt Handler
    priv                  //  Callback argument
  );
  if (handle == NULL) {
    kmm_free(priv);
    ierr("Attach interrupt failed\n");
    return -EIO;
  }

(IOEXP_SETOPTION and IOEP_ATTACH are from the GPIO Expander)

And that’s how we initialise our CST816S Driver at startup!

What’s g_cst816s_fileops?

g_cst816s_fileops defines the NuttX File Operations (open, close, read, poll) that will be supported by our driver: cst816s.c

//  File Operations exposed to NuttX Apps
static const struct file_operations g_cst816s_fileops = {
  cst816s_open,   //  open
  cst816s_close,  //  close
  cst816s_read,   //  read
  NULL,           //  write
  NULL,           //  seek
  NULL,           //  ioctl
  cst816s_poll    //  poll
#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
  , NULL          //  unlink
#endif
};

We’ll see the File Operations in a while.

Initialise the CST816S Driver at startup

5 GPIO Interrupt

What happens when a GPIO Interrupt is triggered on touch?

Our GPIO Interrupt Handler does the following…

Below is cst816s_isr_handler, our GPIO Interrupt Handler: cst816s.c

//  Handle GPIO Interrupt triggered by touch
static int cst816s_isr_handler(FAR struct ioexpander_dev_s *dev, ioe_pinset_t pinset, FAR void *arg) {
  //  Get the Device Struct from the handler argument
  FAR struct cst816s_dev_s *priv = (FAR struct cst816s_dev_s *) arg;

  //  Enter a Critical Section
  irqstate_t flags = enter_critical_section();

  //  Set the Pending Flag to true
  priv->int_pending = true;

  //  Leave the Critical Section
  leave_critical_section(flags);

  //  Notify all poll() callers that data is ready
  cst816s_poll_notify(priv);
  return 0;
}

(cst816s_poll_notify is defined here)

We use a Critical Section to protect the Pending Flag from being modified by multiple threads.

5.1 Test GPIO Interrupt

Our GPIO Interrupt Handler… Does it really work?

Let’s test it! Follow these steps to build, flash and run NuttX on PineDio Stack (with CST816S logging enabled)…

In the NuttX Shell, enter this command to list all devices…

ls /dev

We should see our CST816S Driver loaded at “/dev/input0”…

NuttX Touchscreen Device

Tap the screen on PineDio Stack. We should see the GPIO Interrupt handled by our driver…

bl602_expander_interrupt: Interrupt!
bl602_expander_interrupt: Call callback
cst816s_poll_notify:

(See the Complete Log)

Yep our CST816S Driver correctly handles the GPIO Interrupt!

GPIO Interrupt

6 Fetch Touch Data

We’ve handled the GPIO Interrupt, now comes the exciting part of our CST816S Driver… Fetching the Touch Data over I2C!

Why bother with GPIO Interrupts anyway? Can’t we read the data directly over I2C?

Ah but the Touch Panel won’t respond to I2C Commands until the screen is tapped! (Which triggers the GPIO Interrupt)

That’s why we need to monitor for GPIO Interrupts (via the Pending Flag) and determine whether the Touch Panel’s I2C Interface is active.

What can we read from CST816S over I2C?

Here’s the Touch Data that we can read from I2C Registers 0x02 to 0x06 on CST816S…

(Derived from Hynitron’s Reference Driver)

Touch Gestures (like swiping and scrolling) might also be supported, according to the CST816S Driver for PineTime InfiniTime. (See this)

Any gotchas for the Touch Data?

If the Touch Event is 0 (Touch Down), all Touch Data is hunky dory.

But if the Touch Event is 1 (Touch Up), all the other fields are invalid!

Our driver fixes this by remembering and returning the last valid Touch Data.

What about Touch Event 2 (Contact)?

We haven’t seen this during our testing. Thus our driver ignores the event.

UPDATE: Our driver now handles the Contact Event. (See this)

Let’s check out our driver code…

Getting I2C Touch Data

(Source)

6.1 Get I2C Touch Data

This is how we read the Touch Data over I2C in our driver: cst816s.c

//  Read I2C Register 0x00 onwards
#define CST816S_REG_TOUCHDATA 0x00

//  Read Touch Data over I2C
static int cst816s_get_touch_data(FAR struct cst816s_dev_s *dev, FAR void *buf) {

  //  Read the Raw Touch Data over I2C
  uint8_t readbuf[7];
  int ret = cst816s_i2c_read(
    dev,                    //  Device Struct
    CST816S_REG_TOUCHDATA,  //  Read I2C Register 0x00 onwards
    readbuf,                //  Buffer for Touch Data
    sizeof(readbuf)         //  Read 7 bytes
  );
  if (ret < 0) {
    iinfo("Read touch data failed\n");
    return ret;
  }

(cst816s_i2c_read is defined here)

The function begins by reading I2C Registers 0x00 to 0x06.

Then it decodes the Touch Data (as described in the last section)…

  //  Interpret the Raw Touch Data
  uint8_t id = readbuf[5] >> 4;
  uint8_t touchpoints = readbuf[2] & 0x0f;
  uint8_t xhigh = readbuf[3] & 0x0f;
  uint8_t xlow  = readbuf[4];
  uint8_t yhigh = readbuf[5] & 0x0f;
  uint8_t ylow  = readbuf[6];
  uint8_t event = readbuf[3] >> 6;  //  0 = Touch Down, 1 = Touch Up, 2 = Contact */
  uint16_t x  = (xhigh << 8) | xlow;
  uint16_t y  = (yhigh << 8) | ylow;

For Touch Up Events: The Touch Coordinates are invalid, so we substitute the data from the last Touch Down Event…

  //  If touch coordinates are invalid,
  //  return the last valid coordinates
  bool valid = true;
  if (x >= 240 || y >= 240) {
    //  Quit if we have no last valid coordinates
    if (last_event == 0xff) { return -EINVAL; }

    //  Otherwise substitute the last valid coordinates
    valid = false;
    id = last_id;
    x  = last_x;
    y  = last_y;
  }

We remember the Touch Event and the Touch Data…

  //  Remember the last valid touch data
  last_event = event;
  last_id    = id;
  last_x     = x;
  last_y     = y;

NuttX expects the Touch Data to be returned as a touch_sample_s struct. (See this)

We assign the Touch Data to the struct…

  //  Set the Touch Data fields
  struct touch_sample_s data;
  memset(&data, 0, sizeof(data));
  data.npoints     = 1;   //  Number of Touch Points
  data.point[0].id = id;  //  Touch ID
  data.point[0].x  = x;   //  X Coordinate
  data.point[0].y  = y;   //  Y Coordinate

Now we tell NuttX whether it’s a Touch Down Event (with valid or invalid coordinates)…

  //  Set the Touch Flags for...
  //  Touch Down Event
  if (event == 0) {
    if (valid) {
      //  Touch coordinates were valid
      data.point[0].flags  = TOUCH_DOWN | TOUCH_ID_VALID | TOUCH_POS_VALID;
    } else {
      //  Touch coordinates were invalid
      data.point[0].flags  = TOUCH_DOWN | TOUCH_ID_VALID;
    }

Or a Touch Up Event (with valid or invalid coordinates)…

  //  Touch Up Event
  } else if (event == 1) {
    if (valid) {
      //  Touch coordinates were valid
      data.point[0].flags  = TOUCH_UP | TOUCH_ID_VALID | TOUCH_POS_VALID;
    } else {
      //  Touch coordinates were invalid
      data.point[0].flags  = TOUCH_UP | TOUCH_ID_VALID;
    }

We ignore all Contact Events (because we’ve never seen one)…

  //  Reject Contact Event
  } else {
    return -EINVAL;
  }

Finally we return the struct to the caller…

  //  Return the touch data
  memcpy(buf, &data, sizeof(data));
  return sizeof(data);
}

That’s how we read and decode the Touch Data from CST816S over I2C!

Returning I2C Touch Data

(Source)

6.2 Is Data Ready?

Who calls cst816s_get_touch_data to fetch the Touch Data over I2C?

cst816s_get_touch_data is called by the read() File Operation of our driver: cst816s.c

//  Implements the read() File Operation for the driver
static ssize_t cst816s_read(FAR struct file *filep, FAR char *buffer, size_t buflen) {
  ...
  //  Wait for semaphore to prevent concurrent reads
  int ret = nxsem_wait(&priv->devsem);

  //  Read the touch data, only if 
  //  screen has been touched or if 
  //  we're waiting for touch up
  ret = -EINVAL;
  if ((priv->int_pending || last_event == 0) 
    && buflen >= outlen) {
    ret = cst816s_get_touch_data(priv, buffer);
  }

  //  Clear the Pending Flag with critical section
  flags = enter_critical_section();
  priv->int_pending = false;
  leave_critical_section(flags);

  //  Release semaphore and allow next read
  nxsem_post(&priv->devsem);

(Which means that this code will run when a NuttX App reads “/dev/input0”)

Note that we fetch the Touch Data over I2C only if…

Why check the Pending Flag?

Recall that the Pending Flag is set when the screen is touched. (Which triggers a GPIO Interrupt)

The Pending Flag tells us when the Touch Panel’s I2C Interface is active. And there’s valid Touch Data to be fetched.

Thus this check prevents unnecessary I2C Reads, until the Touch Data is available for reading.

Why check if the last event was Touch Down?

When we’re no longer touching the screen, the Touch Panel doesn’t trigger a GPIO Interrupt.

Thus to catch the Touch Up Event, we must allow the Touch Data to be fetched over I2C. And we stop fetching thereafter. (Until the screen is touched again)

This causes a few redundant I2C Reads, but it shouldn’t affect performance.

(Unless we touch the screen for a very long time!)

7 Run The Driver

For our final demo today, let’s run our CST816S Driver and test the Touch Panel!

Follow these steps to build, flash and run NuttX on PineDio Stack (with CST816S logging enabled)…

In the NuttX Shell, enter this command to run the LVGL Test App…

lvgltest

We should see the Touch Calibration screen…

Touch Panel Calibration for Pine64 PineDio Stack BL604 RISC-V Board

When prompted, tap the 4 corners of the screen…

Yep our CST816S Driver responds correctly to touch! 🎉

The touchscreen looks laggy?

The ST7789 Display feels laggy because of inefficient SPI Data Transfer. The SPI Driver polls the SPI Port when transferring data. (See this)

That’s why we need to implement SPI Direct Memory Access (DMA) so that PineDio Stack can do other tasks (like handling the Touch Panel) while painting the ST7789 Display.

We’ll port to NuttX this implementation of SPI DMA from BL MCU SDK…

More about SPI DMA on BL602 / BL604…

UPDATE: SPI DMA is now supported on BL602 NuttX…

Let’s inspect the log…

(TODO: We should add a button and a message box to the LVGL Test App to demo the touchscreen)

7.1 Read Touch Data

Nothing appears in the log until we touch the screen. Why so?

Recall that the LVGL Test App calls read() repeatedly on our CST816S Driver to get Touch Data. (See this)

But read() won’t fetch any Touch Data over I2C until the screen is touched. (See this)

Thus we have successfully eliminated most of the unnecessary I2C Reads!

Now watch what happens when we touch the screen…

LVGL Test App calls read() repeatedly

(Source)

7.2 Trigger GPIO Interrupt

During the calibration process, we touch the screen. This triggers a GPIO Interrupt…

bl602_expander_interrupt: Interrupt!
bl602_expander_interrupt: Call callback
cst816s_poll_notify:

(See the Complete Log)

The Interrupt Handler in our driver sets the Pending Flag to true. (See this)

Then it calls cst816s_poll_notify to notify all callers to poll() that Touch Data is now available.

(The LVGL Test App doesn’t poll() our driver, so this has no effect)

7.3 Touch Down Event

The LVGL Test App is still calling read() repeatedly to get Touch Data from our driver.

Now that the Pending Flag is true, our driver proceeds to call cst816s_get_touch_data and fetch the Touch Data over I2C…

cst816s_get_touch_data:
cst816s_i2c_read:
bl602_i2c_transfer: subflag=0, subaddr=0x0, sublen=0
bl602_i2c_transfer: i2c transfer success
bl602_i2c_transfer: subflag=0, subaddr=0x0, sublen=0
bl602_i2c_transfer: i2c transfer success
bl602_i2c_recvdata: count=7, temp=0x500
bl602_i2c_recvdata: count=3, temp=0x1700de

(See the Complete Log)

Our driver has fetched the Touch Data over I2C…

cst816s_get_touch_data: DOWN: id=0,touch=0, x=222, y=23

Which gets returned directly to the app as a Touch Down Event…

cst816s_get_touch_data:   id:      0
cst816s_get_touch_data:   flags:   19
cst816s_get_touch_data:   x:       222
cst816s_get_touch_data:   y:       23

Our driver clears the Pending Flag and remembers that we’re expecting a Touch Up Event. (See this)

Our driver returns a Touch Down Event

7.4 Touch Down Event Again

We’re not done with Touch Down Events yet!

Because our driver remembers that we’re expecting a Touch Up Event, all calls to read() will continue to fetch the Touch Data over I2C. (Here’s why)

cst816s_get_touch_data:
cst816s_i2c_read:
cst816s_get_touch_data: DOWN: id=0, touch=0, x=222, y=23
cst816s_get_touch_data:   id:      0
cst816s_get_touch_data:   flags:   19
cst816s_get_touch_data:   x:       222
cst816s_get_touch_data:   y:       23

cst816s_get_touch_data:
cst816s_i2c_read:
cst816s_get_touch_data: DOWN: id=0, touch=0, x=222, y=23
cst816s_get_touch_data:   id:      0
cst816s_get_touch_data:   flags:   19
cst816s_get_touch_data:   x:       222
cst816s_get_touch_data:   y:       23

(See the Complete Log)

Our driver returns the same data twice to the app. (Until it sees the Touch Up Event)

(TODO: Perhaps we should ignore duplicate Touch Down Events? Might reduce the screen lag)

7.5 Touch Up Event

When we’re no longer longer touching the screen, cst816s_get_touch_data receives a Touch Up Event over I2C…

cst816s_get_touch_data:
cst816s_i2c_read:
cst816s_get_touch_data: Invalid touch data: id=9, touch=2, x=639, y=1688

(See the Complete Log)

This doesn’t look right: x=639, y=1688. Our screen is only 240 x 240 pixels!

We said earlier that Touch Up Events have invalid Touch Coordinates. (Right here)

Hence we substitute the Touch Coordinates with the data from the last Touch Down Event…

cst816s_get_touch_data: UP: id=0, touch=2, x=222, y=23
cst816s_get_touch_data:   id:      0
cst816s_get_touch_data:   flags:   0c
cst816s_get_touch_data:   x:       222
cst816s_get_touch_data:   y:       23

And we return the valid coordinates to the app.

The Pending Flag is now clear, and we’re no longer expecting a Touch Up Event.

All calls to read() will no longer fetch the Touch Data over I2C. (Until we touch the screen again)

Patching the Touch Coordinates

7.6 Screen Calibration Result

After we have touched the 4 corners of the screen, the LVGL Test App displays the result of the Screen Calibration…

tp_cal result
offset x:23, y:24
range x:194, y:198
invert x/y:1, x:0, y:1

Which will be used to tweak the Touch Coordinates later in the app.

And we’re done with the app!

Touch Panel Calibration for Pine64 PineDio Stack BL604 RISC-V Board

8 Screen Is Sideways

If we look closely at the screen above, the Touch Coordinates seem odd…

Top Left
x=181, y=12
Top Right
x=230, y=212
Bottom Left
x=9, y=10
Bottom Right
x=19, y=202

But we expect the Touch Coordinates to run left to right, top to bottom…

Top Left
x=0, y=0
Top Right
x=239, y=0
Bottom Left
x=0, y=239
Bottom Right
x=239, y=239

Try this: Tilt your head to the left and stare at the pic. You’ll see the Expected Touch Coordinates!

That’s right… Our screen is rotated sideways!

So be careful when mapping the Touch Coordinates to the rendered screen.

Can we fix this?

We can rotate the display in the ST7789 Display Driver.

(Portrait Mode vs Landscape Mode)

But first we need to agree which way is “up”…

Which way is up?

9 I2C Quirks

Is there anything peculiar about I2C on BL602 and BL604?

We need to handle two I2C Quirks on NuttX for BL602 / BL604…

Let’s go into the details…

9.1 I2C Sub Address

When we read an I2C Register, we must send the I2C Register ID as an I2C Sub Address: cst816s.c

//  Read from I2C device
static int cst816s_i2c_read(FAR struct cst816s_dev_s *dev, uint8_t reg,uint8_t *buf, size_t buflen) {

  //  Compose I2C Request to read I2C Registers
  struct i2c_msg_s msgv[2] = { {

    //  First I2C Message: Send the Register ID
    .frequency = CONFIG_CST816S_I2C_FREQUENCY,
    .addr      = dev->addr,
#ifdef CONFIG_BL602_I2C0
    //  For BL602: We must send Register ID as I2C Sub Address
    .flags     = I2C_M_NOSTART,
#else
    //  Otherwise we send the Register ID normally
    .flags     = 0,
#endif  //  CONFIG_BL602_I2C0
    .buffer    = &reg,
    .length    = 1
  }, {

    //  Second I2C Message: Receive the Register Data
    .frequency = CONFIG_CST816S_I2C_FREQUENCY,
    .addr      = dev->addr,
    .flags     = I2C_M_READ,
    .buffer    = buf,
    .length    = buflen
  } };

We do this by specifying the I2C_M_NOSTART flag (shown above).

This article explains why…

9.2 I2C Logging

During development we discovered that cst816s_get_touch_data won’t return any valid Touch Data unless we enable these two I2C Warnings in the BL602 I2C Driver: bl602_i2c.c

static int bl602_i2c_transfer(struct i2c_master_s *dev, struct i2c_msg_s *msgs, int count) {
  ...
  priv->msgid = i;
#ifdef CONFIG_INPUT_CST816S
  //  I2C Workaround #1 of 2 for CST816S: https://github.com/lupyuen/cst816s-nuttx#i2c-logging
  i2cwarn("subflag=%d, subaddr=0x%lx, sublen=%d\n", priv->subflag, priv->subaddr, priv->sublen);
#endif /* CONFIG_INPUT_CST816S */
  bl602_i2c_start_transfer(priv);
  ...  
  if (priv->i2cstate == EV_I2C_END_INT) {

#ifdef CONFIG_INPUT_CST816S
    //  I2C Workaround #2 of 2 for CST816S: https://github.com/lupyuen/cst816s-nuttx#i2c-logging
    i2cwarn("i2c transfer success\n");
#endif  //  CONFIG_INPUT_CST816S

That’s why we must always enable I2C Warnings in our NuttX Build…

(I2C Warnings are already enabled for PineDio Stack)

What happens if we don’t enable I2C Warnings?

If we disable I2C Warnings, we’ll never receive the Touch Down Event over I2C…

nsh> lvgltest
tp_init: Opening /dev/input0
cst816s_open:

bl602_expander_interrupt: Interrupt!
bl602_expander_interrupt: Call callback
cst816s_poll_notify:

cst816s_get_touch_data:
cst816s_i2c_read:
cst816s_get_touch_data: Invalid touch data: id=9, touch=2, x=639, y=1688
cst816s_get_touch_data: Can't return touch data: id=9, touch=2, x=639, y=1688

(See the Complete Log)

We’ll only get the Touch Up Event (with invalid Touch Coordinates).

Why would I2C Logging affect the fetching of Touch Data over I2C?

We’re not sure. This could be due to an I2C Timing Issue or a Race Condition.

Or perhaps our I2C Read is done too soon after the Touch Interrupt, and we need to wait a while?

(We might probe the I2C Bus with a Logic Analyser and learn more)

Is it OK to enable logging for everything in NuttX?

Not really. If we enable “Informational Debug Output” (CONFIG_DEBUG_INFO) in NuttX, we’ll get so much Debug Output that the LoRaWAN Test App will fail.

(Because LoRaWAN Timers are time-critical)

Hence we should enable NuttX Info Logging only when needed for troubleshooting.

(TODO: LoRaWAN Test App, LoRaWAN Library, SX1262 Library, NimBLE Porting Layer and SPI Test Driver should have their own flags for logging)

10 What’s Next

I hope this article has provided everything you need to get started on creating your own Touchscreen Apps on PineDio Stack.

Lemme know what you’re building with PineDio Stack!

In the next article we shall tackle the (happy) problem of too many GPIOs on PineDio Stack…

Stay Tuned!

Many Thanks to my GitHub Sponsors for supporting my work! This article wouldn’t have been possible without your support.

Got a question, comment or suggestion? Create an Issue or submit a Pull Request here…

lupyuen.github.io/src/touch.md

11 Notes

  1. This article is the expanded version of this Twitter Thread

Touch Panel Calibration for Pine64 PineDio Stack BL604 RISC-V Board

Touch Panel Calibration for Pine64 PineDio Stack BL604 RISC-V Board