drivers: kscan: Add driver for XPT2046
Add driver for Xptek XPT2046 resistive touch controller on SPI. Only interrupt driven mode supported, does not do polling. Signed-off-by: Seppo Takalo <seppo.takalo@iki.fi>
This commit is contained in:
parent
0f601b8c00
commit
edae1bed3d
6 changed files with 363 additions and 0 deletions
|
|
@ -11,5 +11,6 @@ zephyr_library_sources_ifdef(CONFIG_KSCAN_HT16K33 kscan_ht16k33.c)
|
|||
zephyr_library_sources_ifdef(CONFIG_KSCAN_CST816S kscan_cst816s.c)
|
||||
zephyr_library_sources_ifdef(CONFIG_KSCAN_CAP1203 kscan_cap1203.c)
|
||||
zephyr_library_sources_ifdef(CONFIG_KSCAN_NPCX kscan_npcx.c)
|
||||
zephyr_library_sources_ifdef(CONFIG_KSCAN_XPT2046 kscan_xpt2046.c)
|
||||
|
||||
zephyr_library_sources_ifdef(CONFIG_USERSPACE kscan_handlers.c)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ source "drivers/kscan/Kconfig.ht16k33"
|
|||
source "drivers/kscan/Kconfig.cst816s"
|
||||
source "drivers/kscan/Kconfig.cap1203"
|
||||
source "drivers/kscan/Kconfig.npcx"
|
||||
source "drivers/kscan/Kconfig.xpt2046"
|
||||
|
||||
module = KSCAN
|
||||
module-str = kscan
|
||||
|
|
|
|||
11
drivers/kscan/Kconfig.xpt2046
Normal file
11
drivers/kscan/Kconfig.xpt2046
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Copyright (c) 2023 Seppo Takalo
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
config KSCAN_XPT2046
|
||||
bool "XPT2046 resistive touch panel driver"
|
||||
default y
|
||||
depends on DT_HAS_XPTEK_XPT2046_ENABLED
|
||||
select SPI
|
||||
help
|
||||
Enable driver for Xptek XPT2046 resistive touch panel.
|
||||
This driver is very similar to ADS7843, but differs on channel numbering.
|
||||
296
drivers/kscan/kscan_xpt2046.c
Normal file
296
drivers/kscan/kscan_xpt2046.c
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Seppo Takalo
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#define DT_DRV_COMPAT xptek_xpt2046
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/drivers/spi.h>
|
||||
#include <zephyr/drivers/kscan.h>
|
||||
|
||||
#include <zephyr/logging/log.h>
|
||||
LOG_MODULE_REGISTER(xpt2046, CONFIG_KSCAN_LOG_LEVEL);
|
||||
|
||||
struct xpt2046_config {
|
||||
const struct spi_dt_spec bus;
|
||||
const struct gpio_dt_spec int_gpio;
|
||||
uint16_t min_x;
|
||||
uint16_t min_y;
|
||||
uint16_t max_x;
|
||||
uint16_t max_y;
|
||||
uint16_t threshold;
|
||||
uint16_t screen_size_x;
|
||||
uint16_t screen_size_y;
|
||||
uint16_t reads;
|
||||
};
|
||||
struct xpt2046_data {
|
||||
const struct device *dev;
|
||||
kscan_callback_t callback;
|
||||
struct gpio_callback int_gpio_cb;
|
||||
bool enabled;
|
||||
struct k_work work;
|
||||
struct k_work_delayable dwork;
|
||||
uint8_t rbuf[9];
|
||||
uint32_t last_x;
|
||||
uint32_t last_y;
|
||||
bool pressed;
|
||||
};
|
||||
|
||||
enum xpt2046_channel {
|
||||
CH_TEMP0 = 0,
|
||||
CH_Y,
|
||||
CH_VBAT,
|
||||
CH_Z1,
|
||||
CH_Z2,
|
||||
CH_X,
|
||||
CH_AUXIN,
|
||||
CH_TEMP1
|
||||
};
|
||||
|
||||
struct measurement {
|
||||
uint32_t x;
|
||||
uint32_t y;
|
||||
uint32_t z;
|
||||
};
|
||||
|
||||
#define START BIT(7)
|
||||
#define CHANNEL(ch) ((ch & 0x7) << 4)
|
||||
#define MODE_8_BIT BIT(3)
|
||||
#define SINGLE_ENDED BIT(2)
|
||||
#define POWER_OFF 0
|
||||
#define POWER_ON 0x03
|
||||
#define CONVERT_U16(buf, idx) ((uint16_t)((buf[idx] & 0x7f) << 5) | (buf[idx + 1] >> 3))
|
||||
|
||||
/* Read all Z1, X, Y, Z2 channels using 16 Clocks-per-Conversion mode.
|
||||
* See the manual https://www.waveshare.com/w/upload/9/98/XPT2046-EN.pdf for details.
|
||||
* Each follow-up command interleaves with previous conversion.
|
||||
* So first command starts at byte 0. Second command starts at byte 2.
|
||||
*/
|
||||
static uint8_t tbuf[9] = {
|
||||
[0] = START | CHANNEL(CH_Z1) | POWER_ON,
|
||||
[2] = START | CHANNEL(CH_Z2) | POWER_ON,
|
||||
[4] = START | CHANNEL(CH_X) | POWER_ON,
|
||||
[6] = START | CHANNEL(CH_Y) | POWER_OFF,
|
||||
};
|
||||
|
||||
static void xpt2046_isr_handler(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
|
||||
{
|
||||
struct xpt2046_data *data = CONTAINER_OF(cb, struct xpt2046_data, int_gpio_cb);
|
||||
const struct xpt2046_config *config = data->dev->config;
|
||||
|
||||
gpio_remove_callback(config->int_gpio.port, &data->int_gpio_cb);
|
||||
k_work_submit(&data->work);
|
||||
}
|
||||
|
||||
static int xpt2046_read_and_cumulate(const struct spi_dt_spec *bus, const struct spi_buf_set *tx,
|
||||
const struct spi_buf_set *rx, struct measurement *meas)
|
||||
{
|
||||
int ret = spi_transceive_dt(bus, tx, rx);
|
||||
|
||||
if (ret < 0) {
|
||||
LOG_ERR("spi_transceive() %d\n", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint8_t *buf = rx->buffers->buf;
|
||||
|
||||
meas->z += CONVERT_U16(buf, 1) + 4096 - CONVERT_U16(buf, 3);
|
||||
meas->x += CONVERT_U16(buf, 5);
|
||||
meas->y += CONVERT_U16(buf, 7);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void xpt2046_release_handler(struct k_work *kw)
|
||||
{
|
||||
struct k_work_delayable *dw = k_work_delayable_from_work(kw);
|
||||
struct xpt2046_data *data = CONTAINER_OF(dw, struct xpt2046_data, dwork);
|
||||
struct xpt2046_config *config = (struct xpt2046_config *)data->dev->config;
|
||||
|
||||
if (!data->pressed || !data->enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check if touch is still pressed */
|
||||
if (gpio_pin_get_dt(&config->int_gpio) == 0) {
|
||||
data->pressed = false;
|
||||
data->callback(data->dev, data->last_y, data->last_y, false);
|
||||
} else {
|
||||
/* Re-check later */
|
||||
k_work_reschedule(&data->dwork, K_MSEC(10));
|
||||
}
|
||||
}
|
||||
|
||||
static void xpt2046_work_handler(struct k_work *kw)
|
||||
{
|
||||
struct xpt2046_data *data = CONTAINER_OF(kw, struct xpt2046_data, work);
|
||||
struct xpt2046_config *config = (struct xpt2046_config *)data->dev->config;
|
||||
|
||||
const struct spi_buf txb = {.buf = tbuf, .len = sizeof(tbuf)};
|
||||
const struct spi_buf rxb = {.buf = data->rbuf, .len = sizeof(data->rbuf)};
|
||||
const struct spi_buf_set tx_bufs = {.buffers = &txb, .count = 1};
|
||||
const struct spi_buf_set rx_bufs = {.buffers = &rxb, .count = 1};
|
||||
|
||||
/* Run number of reads and calculate average */
|
||||
int rounds = config->reads;
|
||||
struct measurement meas = {0};
|
||||
|
||||
for (int i = 0; i < rounds; i++) {
|
||||
if (xpt2046_read_and_cumulate(&config->bus, &tx_bufs, &rx_bufs, &meas) != 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
meas.x /= rounds;
|
||||
meas.y /= rounds;
|
||||
meas.z /= rounds;
|
||||
|
||||
/* Calculate Xp = M * Xt + C using fixed point aritchmetics, where
|
||||
* Xp is the point in screen coordinates, Xt is the touch coordinates.
|
||||
* Use signed int32_t for calculation to ensure that we cover the roll-over to negative
|
||||
* values and return zero instead.
|
||||
*/
|
||||
int32_t mx = (config->screen_size_x << 16) / (config->max_x - config->min_x);
|
||||
int32_t cx = (config->screen_size_x << 16) - mx * config->max_x;
|
||||
int32_t x = mx * meas.x + cx;
|
||||
|
||||
x = (x < 0 ? 0 : x) >> 16;
|
||||
|
||||
int32_t my = (config->screen_size_y << 16) / (config->max_y - config->min_y);
|
||||
int32_t cy = (config->screen_size_y << 16) - my * config->max_y;
|
||||
int32_t y = my * meas.y + cy;
|
||||
|
||||
y = (y < 0 ? 0 : y) >> 16;
|
||||
|
||||
bool pressed = meas.z > config->threshold;
|
||||
|
||||
/* Don't send any other than "pressed" events.
|
||||
* releasing seem to cause just random noise
|
||||
*/
|
||||
if (data->enabled && pressed) {
|
||||
LOG_DBG("raw: x=%4u y=%4u ==> x=%4d y=%4d", meas.x, meas.y, x, y);
|
||||
data->last_x = x;
|
||||
data->last_y = y;
|
||||
data->pressed = pressed;
|
||||
data->callback(data->dev, (uint32_t)y, (uint32_t)x, pressed);
|
||||
/* Ensure that we send released event */
|
||||
k_work_reschedule(&data->dwork, K_MSEC(100));
|
||||
}
|
||||
gpio_add_callback(config->int_gpio.port, &data->int_gpio_cb);
|
||||
}
|
||||
|
||||
static int xpt2046_configure(const struct device *dev, kscan_callback_t callback)
|
||||
{
|
||||
struct xpt2046_data *data = dev->data;
|
||||
|
||||
if (!callback) {
|
||||
LOG_ERR("Callback is null");
|
||||
return -EINVAL;
|
||||
}
|
||||
LOG_DBG("%s: set callback", dev->name);
|
||||
|
||||
data->callback = callback;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int xpt2046_enable_callback(const struct device *dev)
|
||||
{
|
||||
struct xpt2046_data *data = dev->data;
|
||||
const struct xpt2046_config *config = dev->config;
|
||||
|
||||
LOG_DBG("%s: enable cb", dev->name);
|
||||
data->enabled = true;
|
||||
gpio_add_callback(config->int_gpio.port, &data->int_gpio_cb);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int xpt2046_disable_callback(const struct device *dev)
|
||||
{
|
||||
struct xpt2046_data *data = dev->data;
|
||||
const struct xpt2046_config *config = dev->config;
|
||||
|
||||
gpio_remove_callback(config->int_gpio.port, &data->int_gpio_cb);
|
||||
data->enabled = false;
|
||||
|
||||
LOG_DBG("%s: disable cb", dev->name);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int xpt2046_init(const struct device *dev)
|
||||
{
|
||||
int r;
|
||||
const struct xpt2046_config *config = dev->config;
|
||||
struct xpt2046_data *data = dev->data;
|
||||
|
||||
if (!spi_is_ready_dt(&config->bus)) {
|
||||
LOG_ERR("SPI controller device not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
data->dev = dev;
|
||||
k_work_init(&data->work, xpt2046_work_handler);
|
||||
k_work_init_delayable(&data->dwork, xpt2046_release_handler);
|
||||
|
||||
if (!gpio_is_ready_dt(&config->int_gpio)) {
|
||||
LOG_ERR("Interrupt GPIO controller device not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
r = gpio_pin_configure_dt(&config->int_gpio, GPIO_INPUT);
|
||||
if (r < 0) {
|
||||
LOG_ERR("Could not configure interrupt GPIO pin");
|
||||
return r;
|
||||
}
|
||||
|
||||
r = gpio_pin_interrupt_configure_dt(&config->int_gpio, GPIO_INT_EDGE_TO_ACTIVE);
|
||||
if (r < 0) {
|
||||
LOG_ERR("Could not configure interrupt GPIO interrupt.");
|
||||
return r;
|
||||
}
|
||||
|
||||
gpio_init_callback(&data->int_gpio_cb, xpt2046_isr_handler, BIT(config->int_gpio.pin));
|
||||
|
||||
LOG_INF("Init '%s' device", dev->name);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct kscan_driver_api xpt2046_driver_api = {
|
||||
.config = xpt2046_configure,
|
||||
.enable_callback = xpt2046_enable_callback,
|
||||
.disable_callback = xpt2046_disable_callback,
|
||||
};
|
||||
|
||||
#define XPT2046_INIT(index) \
|
||||
static const struct xpt2046_config xpt2046_config_##index = { \
|
||||
.bus = SPI_DT_SPEC_INST_GET( \
|
||||
index, SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB | SPI_WORD_SET(8), 0), \
|
||||
.int_gpio = GPIO_DT_SPEC_INST_GET(index, int_gpios), \
|
||||
.min_x = DT_INST_PROP(index, min_x), \
|
||||
.min_y = DT_INST_PROP(index, min_y), \
|
||||
.max_x = DT_INST_PROP(index, max_x), \
|
||||
.max_y = DT_INST_PROP(index, max_y), \
|
||||
.threshold = DT_INST_PROP(index, z_threshold), \
|
||||
.screen_size_x = DT_INST_PROP(index, touchscreen_size_x), \
|
||||
.screen_size_y = DT_INST_PROP(index, touchscreen_size_y), \
|
||||
.reads = DT_INST_PROP(index, reads), \
|
||||
}; \
|
||||
static struct xpt2046_data xpt2046_data_##index; \
|
||||
DEVICE_DT_INST_DEFINE(index, xpt2046_init, NULL, &xpt2046_data_##index, \
|
||||
&xpt2046_config_##index, POST_KERNEL, CONFIG_KSCAN_INIT_PRIORITY, \
|
||||
&xpt2046_driver_api); \
|
||||
BUILD_ASSERT(DT_INST_PROP(index, min_x) < DT_INST_PROP(index, max_x), \
|
||||
"min_x must be less than max_x"); \
|
||||
BUILD_ASSERT(DT_INST_PROP(index, min_y) < DT_INST_PROP(index, max_y), \
|
||||
"min_y must be less than max_y"); \
|
||||
BUILD_ASSERT(DT_INST_PROP(index, z_threshold) > 10, "Too small threshold"); \
|
||||
BUILD_ASSERT(DT_INST_PROP(index, touchscreen_size_x) > 1 && \
|
||||
DT_INST_PROP(index, touchscreen_size_y) > 1, \
|
||||
"Screen size undefined"); \
|
||||
BUILD_ASSERT(DT_INST_PROP(index, reads) > 0, "Number of reads must be at least one");
|
||||
|
||||
DT_INST_FOREACH_STATUS_OKAY(XPT2046_INIT)
|
||||
53
dts/bindings/kscan/xptek,xpt2046.yaml
Normal file
53
dts/bindings/kscan/xptek,xpt2046.yaml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Copyright (c) 2023 Seppo Takalo
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
description: Driver for XPT2046 touch IC
|
||||
compatible: "xptek,xpt2046"
|
||||
|
||||
include: [kscan.yaml, spi-device.yaml]
|
||||
|
||||
properties:
|
||||
int-gpios:
|
||||
type: phandle-array
|
||||
required: true
|
||||
description: Interrupt GPIO.
|
||||
|
||||
touchscreen-size-x:
|
||||
type: int
|
||||
required: true
|
||||
description: horizontal resolution of screen
|
||||
|
||||
touchscreen-size-y:
|
||||
type: int
|
||||
required: true
|
||||
description: vertical resolution of screen
|
||||
|
||||
min-x:
|
||||
type: int
|
||||
required: true
|
||||
description: minimum raw X value reported.
|
||||
|
||||
min-y:
|
||||
type: int
|
||||
required: true
|
||||
description: minimum raw Y value reported.
|
||||
|
||||
max-x:
|
||||
type: int
|
||||
required: true
|
||||
description: maximum raw X value reported.
|
||||
|
||||
max-y:
|
||||
type: int
|
||||
required: true
|
||||
description: maximum raw Y value reported.
|
||||
|
||||
z-threshold:
|
||||
type: int
|
||||
description: Z value threshold to trigger a touch
|
||||
default: 100
|
||||
|
||||
reads:
|
||||
type: int
|
||||
description: How many reads per touch to average the value
|
||||
default: 1
|
||||
|
|
@ -680,6 +680,7 @@ xinpeng Shenzhen Xinpeng Technology Co., Ltd
|
|||
xiphera Xiphera Ltd.
|
||||
xlnx Xilinx
|
||||
xnano Xnano
|
||||
xptek Shenzhen Xptek Technology Co., Ltd
|
||||
xunlong Shenzhen Xunlong Software CO.,Limited
|
||||
xylon Xylon
|
||||
yamaha Yamaha Corporation
|
||||
|
|
|
|||
Loading…
Reference in a new issue