diff --git a/drivers/hid/intel-thc-hid/intel-ipts/ipts.c b/drivers/hid/intel-thc-hid/intel-ipts/ipts.c
index 8529b91..5520620 100644
--- a/drivers/hid/intel-thc-hid/intel-ipts/ipts.c
+++ b/drivers/hid/intel-thc-hid/intel-ipts/ipts.c
@@ -4,7 +4,9 @@
* SPI and carries its HID reports in a private framing.
*/
+#include <linux/cleanup.h>
#include <linux/delay.h>
+#include <linux/hid.h>
#include <linux/interrupt.h>
#include <linux/minmax.h>
#include <linux/regmap.h>
@@ -135,8 +137,62 @@ static int thc_ipts_touch_set(struct thc_ipts *idev, bool enable)
&touch);
}
+/*
+ * One frame to the controller: code, length, then the payload padded to four
+ * bytes. The frame is built in @idev->tx_frame, so callers serialise with
+ * @idev->req_lock.
+ */
+static int thc_ipts_send(struct thc_ipts *idev, u32 code, const void *data,
+ size_t len)
+{
+ size_t padded = round_up(len, 4);
+ u8 *frame = idev->tx_frame;
+
+ if (!idev->dma_ready)
+ return -EAGAIN;
+ if (THC_IPTS_TX_HDR_SIZE + padded > idev->max_tx)
+ return -EMSGSIZE;
+
+ put_unaligned_le32(code, frame);
+ put_unaligned_le32((u32)len, frame + 4);
+ memset(frame + THC_IPTS_TX_HDR_SIZE, 0, padded);
+ if (len && data)
+ memcpy(frame + THC_IPTS_TX_HDR_SIZE, data, len);
+
+ return thc_dma_write(idev->thc, frame, THC_IPTS_TX_HDR_SIZE + padded);
+}
+
+static void thc_ipts_descriptor(struct thc_ipts *idev, const u8 *data,
+ size_t len)
+{
+ if (len < THC_IPTS_DESC_PREAMBLE)
+ return;
+
+ len = min(len - THC_IPTS_DESC_PREAMBLE, idev->max_rx);
+ memcpy(idev->desc, data + THC_IPTS_DESC_PREAMBLE, len);
+ idev->desc_len = len;
+ WRITE_ONCE(idev->have_desc, true);
+ wake_up(&idev->wq);
+}
+
+/* The report is collected in a private buffer; the caller copies it out. */
+static void thc_ipts_feature(struct thc_ipts *idev, const u8 *data,
+ size_t len)
+{
+ if (!READ_ONCE(idev->feature_pending))
+ return;
+
+ len = min(len, idev->max_rx);
+ memcpy(idev->feature, data, len);
+ idev->feature_len = len;
+ WRITE_ONCE(idev->feature_pending, false);
+ WRITE_ONCE(idev->feature_done, true);
+ wake_up(&idev->wq);
+}
+
static void thc_ipts_handle_frame(struct thc_ipts *idev, size_t len)
{
+ const u8 *payload = idev->rx_frame + THC_IPTS_RX_HDR_SIZE;
size_t capacity = idev->max_rx - THC_IPTS_RX_HDR_SIZE;
u32 code, size;
@@ -145,11 +201,28 @@ static void thc_ipts_handle_frame(struct thc_ipts *idev, size_t len)
code = get_unaligned_le32(idev->rx_frame);
/* The header may claim more than what actually arrived. */
- size = min_t(u32, get_unaligned_le32(idev->rx_frame + 4),
- capacity);
+ size = min_t(u32, get_unaligned_le32(idev->rx_frame + 4), capacity);
size = min_t(u32, size, len - THC_IPTS_RX_HDR_SIZE);
- dev_dbg(idev->dev, "frame code %u, %u bytes\n", code, size);
+ switch (code) {
+ case THC_IPTS_RX_INPUT:
+ if (idev->hid)
+ hid_safe_input_report(idev->hid, HID_INPUT_REPORT, (u8 *)payload,
+ capacity, size, 1);
+ break;
+ case THC_IPTS_RX_DESC:
+ thc_ipts_descriptor(idev, payload, size);
+ break;
+ case THC_IPTS_RX_FEATURE:
+ thc_ipts_feature(idev, payload, size);
+ break;
+ case THC_IPTS_RX_RESET:
+ dev_warn(idev->dev, "controller asked for a reset\n");
+ break;
+ default:
+ dev_dbg(idev->dev, "frame code %u, %u bytes\n", code, size);
+ break;
+ }
}
static void thc_ipts_dma_stop(struct thc_ipts *idev)@@ -178,11 +251,25 @@ static int thc_ipts_dma_start(struct thc_ipts *idev)
idev->rx_frame = devm_kzalloc(idev->dev,
round_up(idev->max_rx, SZ_4K),
GFP_KERNEL);
- if (!idev->rx_frame)
- return -ENOMEM;
}
+ if (!idev->tx_frame)
+ idev->tx_frame = devm_kzalloc(idev->dev,
+ round_up(idev->max_tx, 4),
+ GFP_KERNEL);
+ if (!idev->desc)
+ idev->desc = devm_kzalloc(idev->dev, idev->max_rx, GFP_KERNEL);
+ if (!idev->feature)
+ idev->feature = devm_kzalloc(idev->dev, idev->max_rx, GFP_KERNEL);
+ if (!idev->rx_frame || !idev->tx_frame || !idev->desc || !idev->feature)
+ return -ENOMEM;
- ret = thc_dma_set_max_packet_sizes(idev->thc, 0, idev->max_rx, idev->max_tx, 0);
+ /*
+ * The sizes the controller advertises are the largest frame it sends
+ * and the largest it accepts, which is what the engines need; the core
+ * rounds them up to a page and derives the PRD table sizes from them.
+ */
+ ret = thc_dma_set_max_packet_sizes(idev->thc, 0, idev->max_rx,
+ idev->max_tx, 0);
if (ret)
return ret;
@@ -291,6 +378,175 @@ irqreturn_t thc_ipts_thread(int irq, void *data)
return IRQ_HANDLED;
}
+static int thc_ipts_hid_parse(struct hid_device *hdev)
+{
+ struct thc_ipts *idev = hdev->driver_data;
+ u64 zero = 0;
+ int ret;
+ int try;
+
+ guard(mutex)(&idev->req_lock);
+
+ for (try = 0; try < THC_IPTS_REQ_TRIES; try++) {
+ WRITE_ONCE(idev->have_desc, false);
+
+ /* The controller ignores the eight byte argument. */
+ ret = thc_ipts_send(idev, THC_IPTS_TX_GET_DESC, &zero,
+ sizeof(zero));
+ if (ret)
+ return ret;
+
+ if (wait_event_timeout(idev->wq, READ_ONCE(idev->have_desc),
+ msecs_to_jiffies(THC_IPTS_REQ_TIMEOUT_MS)))
+ return hid_parse_report(hdev, idev->desc, idev->desc_len);
+ }
+
+ dev_err(idev->dev,
+ "controller did not return a report descriptor in %d tries\n",
+ try);
+
+ return -ETIMEDOUT;
+}
+
+/*
+ * A feature report is not pushed by the controller: it is asked for and comes
+ * back as a frame of its own, so the request waits for the handler.
+ */
+static int thc_ipts_get_feature(struct thc_ipts *idev, unsigned char reportnum,
+ u8 *buf, size_t len)
+{
+ int ret = -ETIMEDOUT;
+ int try;
+
+ guard(mutex)(&idev->req_lock);
+
+ WRITE_ONCE(idev->feature_done, false);
+ WRITE_ONCE(idev->feature_pending, true);
+
+ for (try = 0; try < THC_IPTS_REQ_TRIES; try++) {
+ ret = thc_ipts_send(idev, THC_IPTS_TX_GET_FEATURE, &reportnum, 1);
+ if (ret)
+ break;
+
+ if (wait_event_timeout(idev->wq, READ_ONCE(idev->feature_done),
+ msecs_to_jiffies(THC_IPTS_REQ_TIMEOUT_MS))) {
+ ret = 0;
+ break;
+ }
+
+ ret = -ETIMEDOUT;
+ }
+
+ WRITE_ONCE(idev->feature_pending, false);
+
+ if (ret) {
+ dev_err(idev->dev, "controller did not return feature %u in %d tries\n",
+ reportnum, try);
+ return ret;
+ }
+
+ /* The payload is the report as user space writes it, report ID first. */
+ len = min(len, idev->feature_len);
+ memcpy(buf, idev->feature, len);
+
+ return len;
+}
+
+static int thc_ipts_hid_raw_request(struct hid_device *hdev,
+ unsigned char reportnum, __u8 *buf,
+ size_t len, unsigned char rtype, int reqtype)
+{
+ struct thc_ipts *idev = hdev->driver_data;
+
+ if (!buf || !len)
+ return -EINVAL;
+
+ if (reqtype == HID_REQ_GET_REPORT && rtype == HID_FEATURE_REPORT)
+ return thc_ipts_get_feature(idev, reportnum, buf, len);
+
+ if (reqtype != HID_REQ_SET_REPORT ||
+ (rtype != HID_OUTPUT_REPORT && rtype != HID_FEATURE_REPORT))
+ return -EOPNOTSUPP;
+
+ guard(mutex)(&idev->req_lock);
+
+ if (rtype == HID_OUTPUT_REPORT)
+ return thc_ipts_send(idev, THC_IPTS_TX_OUTPUT, buf, len);
+
+ return thc_ipts_send(idev, THC_IPTS_TX_SET_FEATURE, buf, len);
+}
+
+/*
+ * The HID core calls all four of these unconditionally, and this transport
+ * has nothing to do when a user opens or closes the report stream.
+ */
+static int thc_ipts_hid_start(struct hid_device *hdev)
+{
+ return 0;
+}
+
+static void thc_ipts_hid_stop(struct hid_device *hdev)
+{
+}
+
+static int thc_ipts_hid_open(struct hid_device *hdev)
+{
+ return 0;
+}
+
+static void thc_ipts_hid_close(struct hid_device *hdev)
+{
+}
+
+static const struct hid_ll_driver thc_ipts_ll_driver = {
+ .start = thc_ipts_hid_start,
+ .stop = thc_ipts_hid_stop,
+ .open = thc_ipts_hid_open,
+ .close = thc_ipts_hid_close,
+ .parse = thc_ipts_hid_parse,
+ .raw_request = thc_ipts_hid_raw_request,
+};
+
+static void thc_ipts_hid_remove(struct thc_ipts *idev)
+{
+ if (!idev->hid)
+ return;
+
+ hid_destroy_device(idev->hid);
+ idev->hid = NULL;
+}
+
+static int thc_ipts_hid_add(struct thc_ipts *idev)
+{
+ struct hid_device *hdev;
+ int ret;
+
+ hdev = hid_allocate_device();
+ if (IS_ERR(hdev))
+ return PTR_ERR(hdev);
+
+ hdev->ll_driver = &thc_ipts_ll_driver;
+ hdev->bus = BUS_PCI;
+ hdev->vendor = idev->vendor;
+ hdev->product = idev->product;
+ hdev->version = le32_to_cpu(idev->cfg.revision) & 0xffff;
+ hdev->dev.parent = idev->dev;
+ hdev->driver_data = idev;
+ snprintf(hdev->name, sizeof(hdev->name), "intel-ipts %04X:%04X",
+ idev->vendor, idev->product);
+ strscpy(hdev->phys, dev_name(idev->dev), sizeof(hdev->phys));
+
+ ret = hid_add_device(hdev);
+ if (ret) {
+ hid_destroy_device(hdev);
+ return ret;
+ }
+
+ idev->hid = hdev;
+
+ return 0;
+}
+
/**
* thc_ipts_port_start - bring one port up
* @idev: port to start@@ -387,7 +643,7 @@ int thc_ipts_port_start(struct thc_ipts *idev)
thc_interrupt_enable(idev->thc, true);
- return 0;
+ return thc_ipts_hid_add(idev);
}
/*
@@ -413,6 +669,8 @@ void thc_ipts_port_stop(struct thc_ipts *idev)
cancel_work_sync(&idev->recover_work);
thc_ipts_dma_stop(idev);
+ thc_ipts_hid_remove(idev);
+
if (idev->running)
thc_ipts_touch_set(idev, false);
diff --git a/drivers/hid/intel-thc-hid/intel-ipts/ipts.h b/drivers/hid/intel-thc-hid/intel-ipts/ipts.h
index 3b5002c..be45f71 100644
--- a/drivers/hid/intel-thc-hid/intel-ipts/ipts.h
+++ b/drivers/hid/intel-thc-hid/intel-ipts/ipts.h
@@ -6,7 +6,9 @@
#include <linux/bits.h>
#include <linux/build_bug.h>
#include <linux/device.h>
+#include <linux/hid.h>
#include <linux/interrupt.h>
+#include <linux/mutex.h>
#include <linux/types.h>
#include <linux/wait.h>
@@ -43,6 +45,13 @@
#define THC_IPTS_CFG_TRIES 10
#define THC_IPTS_CFG_RETRY_MS 100
+/* A report descriptor arrives with eight bytes of preamble in front of it. */
+#define THC_IPTS_DESC_PREAMBLE 8
+
+/* Retry budget for the two handshakes user space can trigger. */
+#define THC_IPTS_REQ_TRIES 6
+#define THC_IPTS_REQ_TIMEOUT_MS 300
+
/* Bytes of the configuration block this driver interprets. */
#define THC_IPTS_CFG_KNOWN 40
@@ -105,6 +114,17 @@ static_assert(offsetof(struct thc_ipts_cfg, vendor) == 0x1c);
#define THC_IPTS_RX_HDR_SIZE 64
#define THC_IPTS_TX_HDR_SIZE 8
+/* Frame codes. The same numbers mean different things per direction. */
+#define THC_IPTS_RX_INPUT 3
+#define THC_IPTS_RX_FEATURE 4
+#define THC_IPTS_RX_DESC 5
+#define THC_IPTS_RX_RESET 7
+
+#define THC_IPTS_TX_SET_FEATURE 3
+#define THC_IPTS_TX_GET_FEATURE 4
+#define THC_IPTS_TX_OUTPUT 5
+#define THC_IPTS_TX_GET_DESC 7
+
/**
* struct thc_ipts - state of one THC port in IPTS mode
* @dev: device used for resources and logging
@@ -116,6 +136,17 @@ static_assert(offsetof(struct thc_ipts_cfg, vendor) == 0x1c);
* @max_rx: largest frame the controller may send, in bytes
* @max_tx: largest frame the driver may send, in bytes
* @rx_frame: buffer for one received frame, including its header
+ * @tx_frame: buffer for one frame to send, including its header
+ * @desc: copy of the report descriptor, past its preamble
+ * @desc_len: length of @desc
+ * @have_desc: a descriptor arrived and has not been consumed yet
+ * @feature: buffer a feature report is collected in
+ * @feature_len: length of the report in @feature
+ * @feature_pending: a feature report was asked for and has not arrived yet
+ * @feature_done: a feature report arrived and has not been consumed yet
+ * @wq: waited on for a report descriptor or a feature report
+ * @req_lock: serialises request frames and the feature handshake
+ * @hid: HID device the reports are handed to, once it has been registered
* @recover_work: resets the receive engine after a transfer error
* @running: the controller answered and is out of reset
* @dma_ready: DMA engines are configured and may be used
@@ -131,6 +162,17 @@ struct thc_ipts {
size_t max_rx;
size_t max_tx;
u8 *rx_frame;
+ u8 *tx_frame;
+ u8 *desc;
+ size_t desc_len;
+ bool have_desc;
+ u8 *feature;
+ size_t feature_len;
+ bool feature_pending;
+ bool feature_done;
+ wait_queue_head_t wq;
+ struct mutex req_lock; /* request frames are serialised */
+ struct hid_device *hid;
struct work_struct recover_work;
bool running;
bool dma_ready;diff --git a/drivers/hid/intel-thc-hid/intel-ipts/pci-ipts.c b/drivers/hid/intel-thc-hid/intel-ipts/pci-ipts.c
index 7e899df..5160ab5 100644
--- a/drivers/hid/intel-thc-hid/intel-ipts/pci-ipts.c
+++ b/drivers/hid/intel-thc-hid/intel-ipts/pci-ipts.c
@@ -45,13 +45,14 @@ static int thc_ipts_probe(struct pci_dev *pci, const struct pci_device_id *id)
return -ENOMEM;
idev->dev = &pci->dev;
+ mutex_init(&idev->req_lock);
+ init_waitqueue_head(&idev->wq);
+ INIT_WORK(&idev->recover_work, thc_ipts_recover);
pci_set_drvdata(pci, idev);
ret = pcim_enable_device(pci);
- if (ret) {
- dev_err(idev->dev, "cannot enable the PCI function, error %d\n", ret);
+ if (ret)
return ret;
- }
pci_set_master(pci);