📝 21 Apr 2022
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)
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, …
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 */
How is CST816S wired to PineDio Stack?
According to the schematic above, CST816S is wired to PineDio Stack like so…
BL604 Pin | CST816S Pin |
---|---|
GPIO 1 | SDA |
GPIO 2 | SCL |
GPIO 9 | Interrupt |
GPIO 18 | Reset |
(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.
How do Touchscreen Drivers work on NuttX?
At NuttX Startup, Touchscreen Drivers register themselves as “/dev/input0”
(Pic above)
NuttX Apps will open “/dev/input0” and call read()
to fetch Touch Data Samples from the driver
(More about this in the next section)
NuttX Apps may call poll()
to wait for available data
(Which blocks on a NuttX Semaphore until the data is available)
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…
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 */
};
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 */
};
Our driver returns the first 4 fields…
id: Always 0, since we detect one Touch Point
flags: We return a combination of these flags…
TOUCH_ID_VALID: Touch Point ID is always valid
TOUCH_DOWN or TOUCH_UP: Touch Down or Up
TOUCH_POS_VALID: If Touch Coordinates are valid
(Touch Coordinates are valid for Touch Down, not Touch Up)
x: X Coordinate of the Touch Point (0 to 239)
y: Y Coordinate of the Touch Point (0 to 239)
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.
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));
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)
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…
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.
What happens when a GPIO Interrupt is triggered on touch?
Our GPIO Interrupt Handler does the following…
Set the Pending Flag to true
(We’ll see why in a while)
Notify all callers to poll()
that the Touch Data is ready
(So they will be unblocked and can proceed to read the data)
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.
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”…
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:
Yep our CST816S Driver correctly handles the GPIO Interrupt!
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…
Touch Points: Number of Touch Points (always 1)
(Bits 0-3 of Register 0x02
)
Touch Event: 0
= Touch Down, 1
= Touch Up, 2
= Contact
(Bits 6-7 of Register 0x03
)
X Coordinate: 0 to 239
(High Byte: Bits 0-3 of Register 0x03
)
(Low Byte: Bits 0-7 of Register 0x04
)
Y Coordinate: 0 to 239
(High Byte: Bits 0-3 of Register 0x05
)
(Low Byte: Bits 0-7 of Register 0x06
)
Touch ID: Identifies the Touch Point (always 0)
(Bits 4-7 of Register 0x05
)
(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…
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!
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…
Screen has just been touched
(Indicated by the Pending Flag int_pending)
Or if the last event was Touch Down
(And we’re waiting for Touch Up)
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!)
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…
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)
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…
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:
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)
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
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)
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
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)
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
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)
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!
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”…
Should we rotate the “chin” to the bottom?
If PineDio Stack works like a “Chonky Watch”, the button should be at the side. Right?
Is there anything peculiar about I2C on BL602 and BL604?
We need to handle two I2C Quirks on NuttX for BL602 / BL604…
I2C Register ID must be sent as I2C Sub Address
I2C Warnings must be turned on
Let’s go into the details…
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 = ®,
.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…
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
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)
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
Touch Panel Calibration for Pine64 PineDio Stack BL604 RISC-V Board