đź“ť 12 Jan 2023
We’re porting Apache NuttX RTOS (Real-Time Operating System) to Pine64 PinePhone…
Now we can render LVGL Graphical User Interfaces… But it won’t work yet with Touch Input!
Let’s talk about the Capacitive Touch Panel inside PinePhone…
How it’s connected to PinePhone
(Over I2C)
How we read Touch Points
(Polling vs Interrupts)
How we created the Touch Panel Driver for NuttX
(Despite the missing docs)
And how we call the driver from LVGL Apps
We begin with the internals of the Touch Panel…
Capacitive Touch Panel in PinePhone Schematic (Pages 9 and 11)
Inside PinePhone is the Goodix GT917S Capacitive Touch Panel (CTP) that talks over I2C.
According to the PinePhone Schematic Pages 9 and 11 (pic above)…
Touch Panel Interrupt (CTP-INT) is at PH4
(Touch Panel fires an interrupt at PH4 when it’s touched)
Touch Panel Reset (CTP-RST) is at PH11
(We toggle PH11 to reset the Touch Panel)
Touch Panel I2C (SCK / SDA) is at TWI0
(That’s the port for Two Wire Interface, compatible with I2C)
What are PH4 and PH11?
Just think of them as GPIOs on the Allwinner A64 SoC.
(Allwinner calls them PIOs)
Does it need special power?
Please remember to power up LDO (3.3V) through the Power Management Integrated Circuit…
PinePhone’s Touch Panel doesn’t seem to be the Power-Saving type like PineTime’s CST816S.
How do we program the Touch Panel?
The datasheet doesn’t say much about programming the Touch Panel…
So we’ll create the driver by replicating the I2C Read / Write Operations from the official Android Driver gt9xx.c.
(Or the unofficial simpler driver GT911.c)
So PinePhone’s Touch Panel is actually undocumented?
Yeah it’s strangely common for Touch Panels to be undocumented.
(Just like PineTime’s CST816S Touch Panel)
Let’s experiment with PinePhone’s Touch Panel to understand how it works…
(I think Touch Panels are poorly documented because of Apple’s patent on Multitouch)
What’s the simplest thing we can do with PinePhone’s Touch Panel?
Let’s read the Product ID from the Touch Panel.
We experimented with the Touch Panel (Bare Metal with NuttX) and discovered these I2C Settings…
I2C Address is 0x5D
I2C Frequency is 400 kHz
(What’s the max?)
I2C Register Addresses are 16-bit
(Send MSB before LSB, so we should swap the bytes)
Reading I2C Register 0x8140
(Product ID) will return the bytes…
39 31 37 53
Which is ASCII for “917S
”
(Goodix GT917S Touch Panel)
Based on the above settings, we wrote this Test Code that runs in the NuttX Kernel: pinephone_bringup.c
// Read Product ID from Touch Panel over I2C
static void touch_panel_read(
struct i2c_master_s *i2c // NuttX I2C Bus (Port TWI0)
) {
uint32_t freq = 400000; // I2C Frequency: 400 kHz
uint16_t addr = 0x5d; // Default I2C Address for Goodix GT917S
uint16_t reg = 0x8140; // Register Address: Read Product ID
// Swap the Register Address, MSB first
uint8_t regbuf[2] = {
reg >> 8, // First Byte: MSB
reg & 0xff // Second Byte: LSB
};
// Erase the Receive Buffer (4 bytes)
uint8_t buf[4];
memset(buf, 0xff, sizeof(buf));
// Compose the I2C Messages
struct i2c_msg_s msgv[2] = {
// Send the 16-bit Register Address (MSB first)
{
.frequency = freq,
.addr = addr,
.flags = 0,
.buffer = regbuf,
.length = sizeof(regbuf)
},
// Receive the Register Data (4 bytes)
{
.frequency = freq,
.addr = addr,
.flags = I2C_M_READ,
.buffer = buf,
.length = sizeof(buf)
}
};
// Execute the I2C Transfer
int ret = I2C_TRANSFER(i2c, msgv, 2);
DEBUGASSERT(ret == OK);
// Dump the Receive Buffer
infodumpbuffer("buf", buf, buflen);
// Shows "39 31 37 53" or "917S"
}
This is what we see (with TWI0 Logging Enabled)…
Yep the I2C Response is correct…
39 31 37 53
Which is ASCII for “917S
”!
(Goodix GT917S Touch Panel)
How’s the code above called by NuttX Kernel?
Read on to find out how we poll the Touch Panel and read the Product ID…
PinePhone’s Touch Panel will trigger interrupts right?
To detect Touch Events, we’ll need to handle the interrupts triggered by Touch Panel.
Based on our research, PinePhone’s Touch Panel Interrupt (CTP-INT) is connected at PH4.
But to simplify our first experiment, let’s poll PH4. (Instead of handling interrupts)
How do we poll PH4?
We read PH4 as a GPIO Input. When we touch the Touch Panel, PH4 goes from Low to High.
This is how we poll PH4: pinephone_bringup.c
// Test Touch Panel Interrupt by Polling as GPIO Input.
// Touch Panel Interrupt (CTP-INT) is at PH4.
// We configure it for GPIO Input.
#define CTP_INT (PIO_INPUT | PIO_PORT_PIOH | PIO_PIN4)
// Poll for Touch Panel Interrupt (PH4) by reading as GPIO Input
void touch_panel_initialize(
struct i2c_master_s *i2c // NuttX I2C Bus (Port TWI0)
) {
// Configure the Touch Panel Interrupt for GPIO Input
int ret = a64_pio_config(CTP_INT);
DEBUGASSERT(ret == OK);
// Poll the Touch Panel Interrupt as GPIO Input
bool prev_val = false;
for (int i = 0; i < 6000; i++) { // Poll for 60 seconds
// Read the GPIO Input
bool val = a64_pio_read(CTP_INT);
// If value has changed...
if (val != prev_val) {
// Print the transition
if (val) { up_putc('+'); } // PH4 goes Low to High
else { up_putc('-'); } // PH4 goes High to Low
prev_val = val;
// If PH4 has just transitioned from Low to High...
if (val) {
// Read the Touch Panel over I2C
touch_panel_read(i2c);
}
}
// Wait a while
up_mdelay(10);
}
}
(a64_pio_config configures PH4 as an Input Pin)
(a64_pio_read reads PH4 as an Input Pin)
The loop above watches for PH4 shifting from Low to High…
When PH4 shifts from Low to High, we print “+
”
When PH4 shifts from High to Low, we print “-
”
After shifting from Low to High, we call touch_panel_read to read the Touch Panel
Thus our simple loop simulates an Interrupt Handler!
How do we open the I2C Port?
On NuttX, this is how we open the I2C Port and pass it to the above loop: pinephone_bringup.c
// Open Allwinner A64 Port TWI0 for I2C
struct i2c_master_s *i2c =
a64_i2cbus_initialize(0); // 0 for TWI0
// Pass the I2C Port to the above loop
touch_panel_initialize(i2c);
We insert this code at the end of the PinePhone Bringup Function, so that NuttX Kernel will run it at the end of startup.
(Yes it sounds hacky, but it’s a simple way to do Kernel Experiments)
Now that we can poll our Touch Panel, let’s read a Touch Point!
When the Touch Panel is touched, how do we read the Touch Coordinates?
Based on the GT911 Reference Code, here are the steps to read a Touch Point…
Read the Touch Panel Status (1 byte) at I2C Register 0x814E
Status Code is Bit 7 of Touch Panel Status
Touched Points is Bits 0 to 3 of Touch Panel Status
If Status Code is non-zero and Touched Points is 1 or more…
Read the Touch Coordinates (6 bytes) at I2C Register 0x8150
First 2 Bytes (LSB First) are the X Coordinate (0 to 720)
Next 2 Bytes (LSB First) are the Y Coordinate (0 to 1440)
(What’s in the 2 remaining bytes? Doesn’t seem to indicate Touch Up / Touch Down)
To acknowledge the Touch Point, set the Touch Panel Status to 0…
Write 0 to I2C Register 0x814E
(This won’t support Multitouch, more about this later)
Here is our code: pinephone_bringup.c
// I2C Registers for Touch Panel
#define GTP_READ_COORD_ADDR 0x814E // Touch Panel Status
#define GTP_POINT1 0x8150 // First Touch Point
// Read Touch Panel over I2C
static void touch_panel_read(
struct i2c_master_s *i2c // NuttX I2C Bus (Port TWI0)
) {
// Read the Touch Panel Status
uint8_t status[1];
touch_panel_i2c_read( // Read from I2C Touch Panel...
i2c, // NuttX I2C Bus (Port TWI0)
GTP_READ_COORD_ADDR, // I2C Register: 0x814E
status, // Receive Buffer
sizeof(status) // Buffer Size
);
// Receives "81"
// Decode the Status Code and the Touched Points
const uint8_t status_code = status[0] & 0x80; // Set to 0x80
const uint8_t touched_points = status[0] & 0x0f; // Set to 0x01
if (status_code != 0 && // If Status Code is OK and...
touched_points >= 1) { // Touched Points is 1 or more
// Read the First Touch Coordinates
uint8_t touch[6];
touch_panel_i2c_read( // Read from I2C Touch Panel...
i2c, // NuttX I2C Bus (Port TWI0)
GTP_POINT1, // I2C Register: 0x8150
touch, // Receive Buffer
sizeof(touch) // Buffer Size
);
// Receives "92 02 59 05 1b 00"
// Decode the Touch Coordinates
const uint16_t x = touch[0] + (touch[1] << 8);
const uint16_t y = touch[2] + (touch[3] << 8);
_info("touch x=%d, y=%d\n", x, y);
// Shows "touch x=658, y=1369"
}
// Set the Touch Panel Status to 0
touch_panel_set_status(i2c, 0);
}
(touch_panel_i2c_read reads from the I2C Touch Panel)
(touch_panel_set_status sets the I2C Touch Panel Status)
Let’s run the code…
When we tap the screen, we see “-+
” which means that PH4 has shifted from Low to High.
Followed by the reading of the Touch Panel Status…
-+
twi_transfer: TWI0 count: 2
twi_wait: TWI0 Waiting...
twi_put_addr: TWI address 7bits+r/w = 0xba
twi_put_addr: TWI address 7bits+r/w = 0xbb
twi_wait: TWI0 Awakened with result: 0
0000 81 .
Touch Panel Status is 0x81
. Which means the status is OK and there’s One Touch Point detected.
Our code reads the Touch Coordinates…
twi_transfer: TWI0 count: 2
twi_wait: TWI0 Waiting...
twi_put_addr: TWI address 7bits+r/w = 0xba
twi_put_addr: TWI address 7bits+r/w = 0xbb
twi_wait: TWI0 Awakened with result: 0
0000 92 02 59 05 1b 00 ..Y...
touch_panel_read: touch x=658, y=1369
This says that the Touch Point is at…
x=658, y=1369
Which is quite close to the Lower Right Corner. (Screen size is 720 x 1440)
Yep we can read the Touch Coordinates correctly, through polling! (But not so efficiently)
Let’s handle interrupts from the Touch Panel…
We’ve done polling with the Touch Panel…
Can we handle interrupts from the Touch Panel?
Earlier we’ve read the Touch Panel by polling… Which is easier but inefficient.
So we tried reading the Touch Panel with an Interrupt Handler…
But there’s a problem: The Touch Panel only fires an interrupt once.
It won’t trigger interrupts correctly when we touch the screen.
Is this a showstopper for our Touch Panel Driver?
Not really, polling will work fine for now.
In a while we’ll run the LVGL Demo App, which uses polling. (Instead of interrupts)
Now we dive inside our NuttX Touch Panel Driver that will be called by NuttX Apps..
What’s inside our NuttX Touch Panel Driver for PinePhone?
We took the code from above and wrapped it inside our NuttX Touch Panel Driver for PinePhone…
NuttX Apps will access our driver at /dev/input0, which exposes the following File Operations: gt9xx.c
// File Operations supported by the Touch Panel
struct file_operations g_gt9xx_fileops = {
gt9xx_open, // Open the Touch Panel
gt9xx_close, // Close the Touch Panel
gt9xx_read, // Read a Touch Sample
gt9xx_poll // Setup Poll for Touch Sample
NuttX Apps will call these Touch Panel Operations through the POSIX Standard Functions open()
, close()
, read()
and poll()
.
(Later we’ll see how LVGL Apps do this)
How do we start the Touch Panel Driver?
This is how we start the Touch Panel Driver when NuttX boots: pinephone_touch.c
// Default I2C Address for Goodix GT917S
#define CTP_I2C_ADDR 0x5d
// Register the Touch Panel Driver
ret = gt9xx_register(
"/dev/input0", // Device Path
i2c, // I2C Bus
CTP_I2C_ADDR, // I2C Address of Touch Panel
&g_pinephone_gt9xx // Callbacks for PinePhone Operations
);
DEBUGASSERT(ret == OK);
(gt9xx_register comes from our Touch Panel Driver)
(g_pinephone_gt9xx defines the Interrupt Callbacks)
The Touch Panel operations are explained in the Appendix…
The driver code looks familiar?
We borrowed the logic from the NuttX Driver for Cypress MBR3108.
(Which is also an I2C Input Device)
Let’s test our Touch Panel Driver with a NuttX App…
Have we tested our driver with NuttX Apps?
Our NuttX Touch Panel Driver works great with the LVGL Demo App! (Pic above)
Here are the LVGL Settings for NuttX…
Enable “Application Configuration > Graphics Support > Light and Versatile Graphics Library (LVGL)”
Enable “LVGL > Enable Framebuffer Port”
Enable “LVGL > Enable Touchpad Port”
Browse into “LVGL > LVGL Configuration”
In “Color Settings”
Set Color Depth to “32: ARGB8888”
In “Memory settings”
Set Size of Memory to 64
In “HAL Settings”
Set Default Dots Per Inch to 300
In “Demos”
Enable “Show Some Widgets”
Enable “Application Configuration > Examples > LVGL Demo”
Also we need to set in .config
…
CONFIG_LV_TICK_CUSTOM=y
CONFIG_LV_TICK_CUSTOM_INCLUDE="port/lv_port_tick.h"
Which is advised by FASTSHIFT…
“The tick of LVGL should not be placed in the same thread as the rendering, because the execution time of
lv_timer_handler
is not deterministic, which will cause a large error in LVGL tick.”
“We should let LVGL use the system timestamp provided by
lv_port_tick
, just need to set two options (above)”
(Thank you so much FASTSHIFT!)
How does LVGL call our Touch Panel Driver?
The LVGL App begins by opening our Touch Panel Driver at /dev/input0: lv_port_touchpad.c
// From lv_port_touchpad_init()...
// Open the Touch Panel Device
int fd = open(
"/dev/input0", // Path of Touch Panel Device
O_RDONLY | O_NONBLOCK // Read-Only Access
);
The app runs an Event Loop that periodically reads a Touch Sample from our driver: lv_port_touchpad.c
// From touchpad_read()...
// Struct for Touch Sample
struct touch_sample_s sample;
// Read a Touch Sample from Touch Panel
read(
fd, // File Descriptor from `open("/dev/input0")`
&sample, // Touch Sample
sizeof(struct touch_sample_s) // Size of Touch Sample
);
(More about the Event Loop in a while)
We’ll receive a Touch Sample that contains 0 or 1 Touch Points. (More about Touch Samples)
(The Read Operation above is Non-Blocking. It returns 0 Touch Points if the screen hasn’t been touched)
We extract the First Touch Point (inside the Touch Sample) and return it to LVGL: lv_port_touchpad.c
// From touchpad_read()...
// Get the First Touch Event from the Touch Sample
uint8_t touch_flags = sample.point[0].flags;
// If the Touch Event is Touch Down or Touch Move...
if (touch_flags & TOUCH_DOWN || touch_flags & TOUCH_MOVE) {
// Report it as LVGL Press
// with the Touch Coordinates
touchpad_obj->last_state = LV_INDEV_STATE_PR;
touchpad_obj->last_x = sample.point[0].x;
touchpad_obj->last_y = sample.point[0].y;
...
} else if (touch_flags & TOUCH_UP) {
// If the Touch Event is Touch Up,
// report it as LVGL Release with
// the previous Touch Coordinates
touchpad_obj->last_state = LV_INDEV_STATE_REL;
}
And that’s how LVGL polls our driver to handle Touch Events!
(LVGL polling our driver is not so efficient, but it works!)
How to create our own LVGL Touchscreen App?
Inside our NuttX Project, look for the LVGL Demo Source Code…
Modify the function lv_demo_widgets to create our own LVGL Widgets…
// Create a Button, set the Width and Height
void lv_demo_widgets(void) {
lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_set_height(btn, LV_SIZE_CONTENT);
lv_obj_set_width(btn, 120);
}
For details, check out the LVGL Widget Docs.
Can we improve the rendering speed?
Yep we need to flush the CPU Cache to fix a rendering issue with the Allwinner A64 Display Engine.
This ought to improve the rendering speed and make LVGL more responsive.
Where’s this LVGL Event Loop that periodically reads a Touch Sample from our driver?
The LVGL Event Loop comes from the Main Function of our LVGL Demo App: lvgldemo.c
// Loop forever handling LVGL Event...
while (1) {
// Execute LVGL Background Tasks
uint32_t idle = lv_timer_handler();
// Minimum sleep of 1ms
idle = idle ? idle : 1;
usleep(idle * 1000);
}
lv_timer_handler will periodically execute LVGL Background Tasks to…
Read Touch Samples (from our Touch Input Driver)
Update the LVGL Display
Is there anything missing in our NuttX Touch Panel Driver for PinePhone?
Yep our driver has limitations, since the Touch Panel Hardware is poorly documented…
Our driver doesn’t support Multitouch and Swiping.
Someday we might fix this when we decipher the (undocumented) Official Android Driver.
(2,000 lines of code!)
But the LVGL Demo doesn’t support Multitouch either.
(So we might put on hold for now)
PinePhone’s Touch Panel seems to trigger too few interrupts. (See this)
Again we’ll have to decipher the (still undocumented) Official Android Driver to fix this.
Every read()
forces an I2C Read AND Write.
This feels expensive. We should fix this with our Interrupt Handler.
Note to Future Self: poll()
won’t work correctly for awaiting Touch Points!
That’s because the Touch Panel won’t generate interrupts for every Touch Point. (See this)
The LVGL Demo doesn’t call poll()
, it only calls non-blocking read()
.
So we’re good for now.
As we add more features to our Touch Panel Driver, we should reuse the Touchscreen Upper Half Driver: touchscreen_upper.c
PinePhone on NuttX will soon support LVGL Touchscreen Apps!
Now we need to tidy up the LVGL Demo App so that it’s more Touch-Friendly for a Phone Form Factor. We’ll talk more about this…
(Pardon me while I learn to shoot a decent video of a Touchscreen App with a Mirrorless Camera)
Please check out the other articles on NuttX for PinePhone…
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/touch2.md
What’s inside our NuttX Touch Panel Driver for PinePhone?
We took the code from above and wrapped it inside our NuttX Touch Panel Driver for PinePhone…
NuttX Apps will access our driver at /dev/input0, which exposes the following File Operations: gt9xx.c
// File Operations supported by the Touch Panel
struct file_operations g_gt9xx_fileops = {
gt9xx_open, // Open the Touch Panel
gt9xx_close, // Close the Touch Panel
gt9xx_read, // Read a Touch Sample
gt9xx_poll // Setup Poll for Touch Sample
NuttX Apps will call these Touch Panel Operations through the POSIX Standard Functions open()
, close()
, read()
and poll()
.
How do we start the Touch Panel Driver?
This is how we start the Touch Panel Driver when NuttX boots: pinephone_bringup.c
// Default I2C Address for Goodix GT917S
#define CTP_I2C_ADDR 0x5d
// Register the Touch Panel Driver
ret = gt9xx_register(
"/dev/input0", // Device Path
i2c, // I2C Bus
CTP_I2C_ADDR, // I2C Address of Touch Panel
&g_pinephone_gt9xx // Callbacks for PinePhone Operations
);
DEBUGASSERT(ret == OK);
(gt9xx_register comes from our Touch Panel Driver)
(g_pinephone_gt9xx defines the Interrupt Callbacks)
The driver code looks familiar?
We borrowed the logic from the NuttX Driver for Cypress MBR3108.
(Which is also an I2C Input Device)
UPDATE: We should reuse the Touchscreen Upper Half Driver: touchscreen_upper.c
Let’s talk about the Touch Panel operations…
At startup, pinephone_bringup registers our Touch Panel Driver at /dev/input0 by calling…
Which will…
Initialise the Struct for Touch Panel
Register the Touch Panel Driver with NuttX
(At /dev/input0)
Attach the Interrupt Handler with NuttX
Disable Interrupts from the Touch Panel
(We’ll enable interrupts when we open the Touch Panel)
Now watch what happens when a NuttX App opens the Touch Panel…
When a NuttX App calls open()
on /dev/input0, NuttX Kernel invokes this operation on our driver…
Inside the Open Operation we…
Power On the Touch Panel
Probe the Touch Panel on the I2C Bus, to verify that it exists
Enable Interrupts from the Touch Panel
The Actual Flow looks more complicated because we do Reference Counting.
(We do the above steps only on the first call to open()
)
Let’s read some touch data…
What’s a Touch Sample?
When a NuttX App reads data from our Touch Panel, the app passes a Touch Sample Struct…
// Struct for Touch Sample
struct touch_sample_s sample;
// Read a Touch Sample from Touch Panel
read(
fd, // File Descriptor from `open("/dev/input0")`
&sample, // Touch Sample
sizeof(struct touch_sample_s) // Size of Touch Sample
);
A Touch Sample contains One Touch Point (by default): touchscreen.h
// Touch Sample Struct
struct touch_sample_s {
int npoints; // Number of Touch Points in point[]
struct touch_point_s point[1]; // Touch Points of length npoints
};
A Touch Point contains the X and Y Coordinates, also indicates whether it’s Touch Up or Touch Down: touchscreen.h
// Touch Point Struct
struct touch_point_s {
uint8_t id; // Identifies the finger touched (Multitouch)
uint8_t flags; // Touch Up or Touch Down
int16_t x; // X Coordinate of the Touch Point
int16_t y; // Y Coordinate of the Touch Point
...
When the app calls read()
, NuttX Kernel calls our driver at…
Which does this…
If the Last Result was Touch Down…
We return the Last Touch Point, now changed to Touch Up.
If the Last Result was NOT Touch Down…
We clear the Interrupt Pending Flag, read the Touch Point from the Touch Panel and return it.
(Implemented as gt9xx_read_touch_data)
(Which calls gt9xx_set_status to set the status)
(Which calls gt9xx_i2c_write to write over I2C)
We ignore Duplicate Touch Points.
(Otherwise we’ll see duplicates like this)
(Also fixes the duplicate keypresses at the end of this video)
Since our driver doesn’t support Multitouch, the Read Operation will return either 0 or 1 Touch Points.
Why the Duplicate Touch Points?
Right now we ignore Duplicate Touch Points, because we saw the Touch Panel generating duplicate points. (See this)
The Touch Panel seems to be producing Touch Up Events… Even though the 6-byte Touch Data looks identical for the Touch Down and Touch Up Events. (See this)
Eventually we’ll have to decode the Touch Up Events. And then remove our Simulated Touch Up Event.
Let’s talk about the Interrupt Pending Flag…
This is our Interrupt Handler for Touch Panel Interrupts…
Inside the Interrupt Handler we…
Set the Interrupt Pending Flag
(Which is protected by a NuttX Critical Section)
Notify the Poll Waiters (Background Threads)
Now we talk about the Poll Waiters…
A NuttX App calls poll()
to set up (or tear down) a Poll for Touch Sample.
This enables the app to suspend itself and block until a Touch Panel Interrupt has been triggered. (And there’s a Touch Point available)
When an app calls poll()
, the NuttX Kernel calls our driver at…
For Poll Setup:
We find an Available Slot for the Poll Waiter
(Poll Waiter Slots are defined in gt9xx_dev_s)
(INPUT_GT9XX_NPOLLWAITERS is the max number of slots, set to 1)
We bind the Poll Struct and this Slot
If Interrupt Pending is set, we notify the Poll Waiters
For Poll Teardown: We unbind the Poll Setup
When a NuttX App calls close()
on /dev/input0, NuttX Kernel invokes this operation on our driver…
Inside the Close Operation we…
Disable Interrupts from the Touch Panel
Power Off the Touch Panel
We do this only if the Reference Count decrements to 0.
(Which indicates the final close()
for our driver)
Earlier we’ve read the Touch Panel by polling… Which is easier but inefficient.
So we tried reading the Touch Panel with an Interrupt Handler… But there’s a problem: The Touch Panel only fires an interrupt once!
It won’t trigger interrupts correctly when we touch the screen.
Is this a showstopper for our Touch Panel Driver?
Not really, polling will work fine for now.
Earlier we saw that the LVGL Demo App runs OK because it uses polling. (Instead of interrupts)
This section talks about our experiments with Touch Panel Interrupts…
Earlier we said that PinePhone’s Touch Panel fires an interrupt at PH4 when it’s touched…
This is how we attach our Interrupt Handler to PH4 in NuttX: pinephone_bringup.c
// Touch Panel Interrupt (CTP-INT) is at PH4
#define CTP_INT ( \
PIO_EINT | \ /* PIO External Interrupt */
PIO_PORT_PIOH | \ /* PIO Port H */
PIO_PIN4 \ /* PIO Pin 4 */
)
// Register the Interrupt Handler for Touch Panel
void touch_panel_initialize(void) {
// Attach the PIO Interrupt Handler for Port PH
int ret = irq_attach( // Attach a NuttX Interrupt Handler...
A64_IRQ_PH_EINT, // Interrupt Number for Port PH: 53
touch_panel_interrupt, // Interrupt Handler
NULL // Argument for Interrupt Handler
);
DEBUGASSERT(ret == OK);
// Set Interrupt Priority in Generic Interrupt Controller v2
arm64_gic_irq_set_priority(
A64_IRQ_PH_EINT, // Interrupt Number for Port PH: 53
2, // Interrupt Priority
IRQ_TYPE_EDGE // Trigger on Low-High Transition
);
// Enable the PIO Interrupt for Port PH.
// A64_IRQ_PH_EINT is 53.
up_enable_irq(A64_IRQ_PH_EINT);
// Configure the Touch Panel Interrupt for Pin PH4
ret = a64_pio_config(CTP_INT);
DEBUGASSERT(ret == OK);
// Enable the Touch Panel Interrupt for Pin PH4
ret = a64_pio_irqenable(CTP_INT);
DEBUGASSERT(ret == OK);
}
(arm64_gic_irq_set_priority configures the Generic Interrupt Controller)
(a64_pio_config configures PH4 as an Interrupt Pin)
(a64_pio_irqenable enables interrupts on Pin PH4)
Why call both up_enable_irq and a64_pio_irqenable?
Allwinner A64 does Two-Tier Interrupts, by Port and Pin…
First we enable interrupts for Port PH
(By calling up_enable_irq)
Then we enable interrupts for Pin PH4
Which means that our Interrupt Handler will be shared by all Pins on Port PH.
(When we enable them in future)
What’s touch_panel_interrupt?
touch_panel_interrupt is our Interrupt Handler. Let’s do a simple one…
// Interrupt Handler for Touch Panel
static int touch_panel_interrupt(int irq, void *context, void *arg) {
// Print something when interrupt is triggered
up_putc('.');
return OK;
}
This Interrupt Handler simply prints “.
” whenever the Touch Panel triggers an interrupt.
But our Interrupt Handler won’t actually read the Touch Coordinates.
(Because Interrupt Handlers can’t make I2C calls)
We’ll fix this in the next section.
It’s OK to call up_putc in an Interrupt Handler?
Yep it’s perfectly OK, because up_putc simply writes to the UART Output Register.
(It won’t trigger another interrupt)
Here’s the actual Interrupt Handler in our Touch Panel Driver: gt9xx.c
// Interrupt Handler for Touch Panel
static int gt9xx_isr_handler(int irq, FAR void *context, FAR void *arg) {
// For Testing: Print something when interrupt is triggered
up_putc('.');
// Get the Touch Panel Device
FAR struct gt9xx_dev_s *priv = (FAR struct gt9xx_dev_s *)arg;
// Begin Critical Section
flags = enter_critical_section();
// Set the Interrupt Pending Flag
priv->int_pending = true;
// End Critical Section
leave_critical_section(flags);
// Notify the Poll Waiters
poll_notify( // Notify these File Descriptors...
priv->fds, // File Descriptors to notify
1, // Max 1 File Descriptor supported
POLLIN // Poll Event to be notified
);
return 0;
}
(gt9xx_dev_s is the Touch Panel Device)
Our Interrupt Handler won’t actually read the Touch Coordinates. (Because Interrupt Handlers can’t make I2C calls)
Instead our Interrupt Handler sets the Interrupt Pending Flag and notifies the Background Thread (via Poll Waiters) that there’s a Touch Event waiting to be processed.
The Background Thread calls poll()
, suspends itself and waits for the notification before processing the Touch Event over I2C.
Let’s test our new and improved Interrupt Handler…
How do we test our Interrupt Handler?
We could start a Background Thread that will be notified when the screen is touched…
Or we can run a simple loop that checks whether the Interrupt Pending Flag is set by our Interrupt Handler.
Let’s test the simple way: pinephone_bringup.c
// Poll for Touch Panel Interrupt
for (int i = 0; i < 6000; i++) { // Poll for 60 seconds
// If Touch Panel Interrupt has been triggered...
if (priv->int_pending) {
// Read the Touch Panel over I2C
touch_panel_read(i2c_dev);
// Reset the Interrupt Pending Flag
priv->int_pending = false;
}
// Wait a while
up_mdelay(10); // 10 milliseconds
}
(This loop works only when Interrupt Trigger is set to IRQ_TYPE_LEVEL. See the next section)
Note that we call touch_panel_read to read the Touch Coordinates. (After the Touch Interrupt has been triggered)
And it works! (Pic above)
0000 81 .
0000 19 01 e6 02 2a 00 ....*.
touch_panel_read: touch x=281, y=742
0000 81 .
0000 81 02 33 00 25 00 ..3.%.
touch_panel_read: touch x=641, y=51
0000 81 .
0000 0f 00 72 05 14 00 ..r...
touch_panel_read: touch x=15, y=1394
The log shows that we’ve read the Touch Panel Status 0x81
, followed by the Touch Coordinates. Yep we’ve tested our Interrupt Handler successfully!
But there’s a problem…
So Touch Panel Interrupts are working OK?
There’s a problem: The Touch Panel only fires an interrupt once!
It won’t trigger interrupts correctly when we touch the screen.
How do we know this?
The Debug Log shows that “.
” is printed only once… Which means our Interrupt Handler is only triggered once!
gt9xx_probe_device (0x40b1ba18):
0000 39 31 37 53 917S
pinephone_gt9xx_irq_enable: enable=1
gt9xx_set_status: status=0
gt9xx_i2c_write: reg=0x814e, val=0
touchpad /dev/input0 open success
touchpad_init
.
Before: disp_size=2
After: disp_size=1
gt9xx_read: buflen=32
gt9xx_read_touch_data:
gt9xx_i2c_read: reg=0x814e, buflen=1
gt9xx_i2c_read (0x40b1bab0):
0000 80
We might need to study the Generic Interrupt Controller to learn how it handles such interrupts.
Is this a showstopper for our Touch Panel Driver?
Not really, polling will work fine for now.
Earlier we saw that the LVGL Demo App runs OK because it uses polling. (Instead of interrupts)
But let’s consider an alternative setup (with too many interrupts)…
Why did we set the Interrupt Trigger to IRQ_TYPE_EDGE?
// Set Interrupt Priority in Generic Interrupt Controller v2
arm64_gic_irq_set_priority(
A64_IRQ_PH_EINT, // Interrupt Number for Port PH: 53
0, // Interrupt Priority
IRQ_TYPE_EDGE // Trigger on Low-High Transition
);
IRQ_TYPE_EDGE means that the interrupt is triggered on Low-High Transitions.
When we tested this, the Touch Panel triggers an interrupt only once.
(We’re not sure why it doesn’t trigger further interrupts)
Let’s try out IRQ_TYPE_LEVEL, which triggers an interrupt by Low-High Level…
// Set Interrupt Priority in Generic Interrupt Controller v2
arm64_gic_irq_set_priority(
A64_IRQ_PH_EINT, // Interrupt Number for Port PH: 53
2, // Interrupt Priority
IRQ_TYPE_LEVEL // Trigger by Level
);
What happens when we run it?
When we run the code, it generates a never-ending stream of “.
” characters…
Without us touching the screen! (Pic above)
Is this a bad thing?
Yes it’s terrible! This means that the Touch Panel fires Touch Input Interrupts continuously…
NuttX will be overwhelmed handling Touch Input Interrupts 100% of the time. No time for other tasks!
What if we limit the interrupts?
Previously we tried throttling the interrupts from the Touch Panel. We disable the Touch Panel Interrupt if we’re still waiting for it to be processed: gt9xx.c
// Interrupt Handler for Touch Panel, with Throttling and Forwarding
static int gt9xx_isr_handler(int irq, FAR void *context, FAR void *arg) {
// Print "." when Interrupt Handler is triggered
up_putc('.');
// Get the Touch Panel Device
FAR struct gt9xx_dev_s *priv = (FAR struct gt9xx_dev_s *)arg;
// If the Touch Panel Interrupt has not been processed...
if (priv->int_pending) {
// Disable the Touch Panel Interrupt
priv->board->irq_enable(priv->board, false);
}
// Omitted: Set the Interrupt Pending Flag
// and notify the Poll Waiters
It seems to work… But it doesn’t look right to throttle interrupts in an Interrupt Handler.
Is it OK to throttle interrupts?
Between calls to read()
, our driver might fail to detect some Touch Input Events.
This happens because we throttle the Touch Panel Interrupts, and we re-enable them only when read()
is called. (Like this)
Interrupts that fire before read()
will likely get ignored.
Maybe we didn’t set the Touch Panel Status correctly? Causing the Excessive Interrupts?
We checked that the Touch Panel Status was correctly set to 0 after every interrupt. Yet we’re still receiving Excessive Interrupts. (See this)
(Why does Status 0x81
change to 0x80
, instead of 0?)
Thus we need to stick with IRQ_TYPE_EDGE and figure out how to trigger interrupts correctly on Touch Input.
(Perhaps by studying the Generic Interrupt Controller)
Why did we set Interrupt Priority to 2?
// Set Interrupt Priority in Generic Interrupt Controller v2
arm64_gic_irq_set_priority(
A64_IRQ_PH_EINT, // Interrupt Number for Port PH: 53
2, // Interrupt Priority
IRQ_TYPE_EDGE // Trigger on Low-High Transition
);
We set the Interrupt Priority to 2 for Legacy Reasons…
The code below comes from the Early Days of NuttX Arm64: arch/arm64/src/qemu/qemu_serial.c
// Attach Interrupt Handler for Arm64 QEMU UART
static int qemu_pl011_attach(struct uart_dev_s *dev) {
...
// Set Interrupt Priority for Arm64 QEMU UART
arm64_gic_irq_set_priority(
sport->irq_num, // Interrupt Number for UART
IRQ_TYPE_LEVEL, // Interrupt Priority is 2 ???
0 // Interrupt Trigger by Level ???
);
There seems to be a mix-up in the arguments: Interrupt Priority vs Interrupt Trigger.
IRQ_TYPE_LEVEL is 2, so the code above sets the Interrupt Priority to 2, with the Default Interrupt Trigger (by Level).
That’s why we configured our Touch Panel Interrupt for Priority 2, to be consistent with the existing calls.
Someday we should fix all existing calls to arm64_gic_irq_set_priority…
To this…
// Set Interrupt Priority with Correct Arguments
arm64_gic_irq_set_priority(
irq_num, // Interrupt Number
0, // Interrupt Priority
IRQ_TYPE_LEVEL // Interrupt Trigger by Level
);