-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
hid: Add support for Intel Touch Host Controller
Based on quo/ithc-linux@e1c3978 Signed-off-by: Dorian Stoll <[email protected]> Patchset: ithc
- Loading branch information
Showing
11 changed files
with
1,286 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
obj-$(CONFIG_HID_ITHC) := ithc.o | ||
|
||
ithc-objs := ithc-main.o ithc-regs.o ithc-dma.o ithc-debug.o | ||
|
||
ccflags-y := -std=gnu11 -Wno-declaration-after-statement | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
config HID_ITHC | ||
tristate "Intel Touch Host Controller" | ||
depends on PCI | ||
depends on HID | ||
help | ||
Say Y here if your system has a touchscreen using Intels | ||
Touch Host Controller (ITHC / IPTS) technology. | ||
|
||
If unsure say N. | ||
|
||
To compile this driver as a module, choose M here: the | ||
module will be called ithc. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
#include "ithc.h" | ||
|
||
void ithc_log_regs(struct ithc *ithc) { | ||
if (!ithc->prev_regs) return; | ||
u32 __iomem *cur = (__iomem void*)ithc->regs; | ||
u32 *prev = (void*)ithc->prev_regs; | ||
for (int i = 1024; i < sizeof *ithc->regs / 4; i++) { | ||
u32 x = readl(cur + i); | ||
if (x != prev[i]) { | ||
pci_info(ithc->pci, "reg %04x: %08x -> %08x\n", i * 4, prev[i], x); | ||
prev[i] = x; | ||
} | ||
} | ||
} | ||
|
||
static ssize_t ithc_debugfs_cmd_write(struct file *f, const char __user *buf, size_t len, loff_t *offset) { | ||
struct ithc *ithc = file_inode(f)->i_private; | ||
char cmd[256]; | ||
if (!ithc || !ithc->pci) return -ENODEV; | ||
if (!len) return -EINVAL; | ||
if (len >= sizeof cmd) return -EINVAL; | ||
if (copy_from_user(cmd, buf, len)) return -EFAULT; | ||
cmd[len] = 0; | ||
if (cmd[len-1] == '\n') cmd[len-1] = 0; | ||
pci_info(ithc->pci, "debug command: %s\n", cmd); | ||
u32 n = 0; | ||
const char *s = cmd + 1; | ||
u32 a[32]; | ||
while (*s && *s != '\n') { | ||
if (n >= ARRAY_SIZE(a)) return -EINVAL; | ||
if (*s++ != ' ') return -EINVAL; | ||
char *e; | ||
a[n++] = simple_strtoul(s, &e, 0); | ||
if (e == s) return -EINVAL; | ||
s = e; | ||
} | ||
ithc_log_regs(ithc); | ||
switch(cmd[0]) { | ||
case 'x': // reset | ||
ithc_reset(ithc); | ||
break; | ||
case 'w': // write register: offset mask value | ||
if (n != 3 || (a[0] & 3)) return -EINVAL; | ||
pci_info(ithc->pci, "debug write 0x%04x = 0x%08x (mask 0x%08x)\n", a[0], a[2], a[1]); | ||
bitsl(((__iomem u32 *)ithc->regs) + a[0] / 4, a[1], a[2]); | ||
break; | ||
case 'r': // read register: offset | ||
if (n != 1 || (a[0] & 3)) return -EINVAL; | ||
pci_info(ithc->pci, "debug read 0x%04x = 0x%08x\n", a[0], readl(((__iomem u32 *)ithc->regs) + a[0] / 4)); | ||
break; | ||
case 's': // spi command: cmd offset len data... | ||
// read config: s 4 0 64 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 | ||
// set touch cfg: s 6 12 4 XX | ||
if (n < 3 || a[2] > (n - 3) * 4) return -EINVAL; | ||
pci_info(ithc->pci, "debug spi command %u with %u bytes of data\n", a[0], a[2]); | ||
if (!CHECK(ithc_spi_command, ithc, a[0], a[1], a[2], a + 3)) | ||
for (u32 i = 0; i < (a[2] + 3) / 4; i++) pci_info(ithc->pci, "resp %u = 0x%08x\n", i, a[3+i]); | ||
break; | ||
case 'd': // dma command: cmd len data... | ||
// get report descriptor: d 7 8 0 0 | ||
// enable multitouch: d 3 2 0x0105 | ||
if (n < 2 || a[1] > (n - 2) * 4) return -EINVAL; | ||
pci_info(ithc->pci, "debug dma command %u with %u bytes of data\n", a[0], a[1]); | ||
if (ithc_dma_tx(ithc, a[0], a[1], a + 2)) pci_err(ithc->pci, "dma tx failed\n"); | ||
break; | ||
default: | ||
return -EINVAL; | ||
} | ||
ithc_log_regs(ithc); | ||
return len; | ||
} | ||
|
||
static const struct file_operations ithc_debugfops_cmd = { | ||
.owner = THIS_MODULE, | ||
.write = ithc_debugfs_cmd_write, | ||
}; | ||
|
||
static void ithc_debugfs_devres_release(struct device *dev, void *res) { | ||
struct dentry **dbgm = res; | ||
if (*dbgm) debugfs_remove_recursive(*dbgm); | ||
} | ||
|
||
int ithc_debug_init(struct ithc *ithc) { | ||
struct dentry **dbgm = devres_alloc(ithc_debugfs_devres_release, sizeof *dbgm, GFP_KERNEL); | ||
if (!dbgm) return -ENOMEM; | ||
devres_add(&ithc->pci->dev, dbgm); | ||
struct dentry *dbg = debugfs_create_dir(DEVNAME, NULL); | ||
if (IS_ERR(dbg)) return PTR_ERR(dbg); | ||
*dbgm = dbg; | ||
|
||
struct dentry *cmd = debugfs_create_file("cmd", 0220, dbg, ithc, &ithc_debugfops_cmd); | ||
if (IS_ERR(cmd)) return PTR_ERR(cmd); | ||
|
||
return 0; | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
#include "ithc.h" | ||
|
||
static int ithc_dma_prd_alloc(struct ithc *ithc, struct ithc_dma_prd_buffer *p, unsigned num_buffers, unsigned num_pages, enum dma_data_direction dir) { | ||
p->num_pages = num_pages; | ||
p->dir = dir; | ||
p->size = round_up(num_buffers * num_pages * sizeof(struct ithc_phys_region_desc), PAGE_SIZE); | ||
p->addr = dmam_alloc_coherent(&ithc->pci->dev, p->size, &p->dma_addr, GFP_KERNEL); | ||
if (!p->addr) return -ENOMEM; | ||
if (p->dma_addr & (PAGE_SIZE - 1)) return -EFAULT; | ||
return 0; | ||
} | ||
|
||
struct ithc_sg_table { | ||
void *addr; | ||
struct sg_table sgt; | ||
enum dma_data_direction dir; | ||
}; | ||
static void ithc_dma_sgtable_free(struct sg_table *sgt) { | ||
struct scatterlist *sg; | ||
int i; | ||
for_each_sgtable_sg(sgt, sg, i) { | ||
struct page *p = sg_page(sg); | ||
if (p) __free_page(p); | ||
} | ||
sg_free_table(sgt); | ||
} | ||
static void ithc_dma_data_devres_release(struct device *dev, void *res) { | ||
struct ithc_sg_table *sgt = res; | ||
if (sgt->addr) vunmap(sgt->addr); | ||
dma_unmap_sgtable(dev, &sgt->sgt, sgt->dir, 0); | ||
ithc_dma_sgtable_free(&sgt->sgt); | ||
} | ||
|
||
static int ithc_dma_data_alloc(struct ithc* ithc, struct ithc_dma_prd_buffer *prds, struct ithc_dma_data_buffer *b) { | ||
// We don't use dma_alloc_coherent for data buffers, because they don't have to be contiguous (we can use one PRD per page) or coherent (they are unidirectional). | ||
// Instead we use an sg_table of individually allocated pages (5.13 has dma_alloc_noncontiguous for this, but we'd like to support 5.10 for now). | ||
struct page *pages[16]; | ||
if (prds->num_pages == 0 || prds->num_pages > ARRAY_SIZE(pages)) return -EINVAL; | ||
b->active_idx = -1; | ||
struct ithc_sg_table *sgt = devres_alloc(ithc_dma_data_devres_release, sizeof *sgt, GFP_KERNEL); | ||
if (!sgt) return -ENOMEM; | ||
sgt->dir = prds->dir; | ||
if (!sg_alloc_table(&sgt->sgt, prds->num_pages, GFP_KERNEL)) { | ||
struct scatterlist *sg; | ||
int i; | ||
bool ok = true; | ||
for_each_sgtable_sg(&sgt->sgt, sg, i) { | ||
struct page *p = pages[i] = alloc_page(GFP_KERNEL | __GFP_ZERO); // don't need __GFP_DMA for PCI DMA | ||
if (!p) { ok = false; break; } | ||
sg_set_page(sg, p, PAGE_SIZE, 0); | ||
} | ||
if (ok && !dma_map_sgtable(&ithc->pci->dev, &sgt->sgt, prds->dir, 0)) { | ||
devres_add(&ithc->pci->dev, sgt); | ||
b->sgt = &sgt->sgt; | ||
b->addr = sgt->addr = vmap(pages, prds->num_pages, 0, PAGE_KERNEL); | ||
if (!b->addr) return -ENOMEM; | ||
return 0; | ||
} | ||
ithc_dma_sgtable_free(&sgt->sgt); | ||
} | ||
devres_free(sgt); | ||
return -ENOMEM; | ||
} | ||
|
||
static int ithc_dma_data_buffer_put(struct ithc *ithc, struct ithc_dma_prd_buffer *prds, struct ithc_dma_data_buffer *b, unsigned idx) { | ||
struct ithc_phys_region_desc *prd = prds->addr; | ||
prd += idx * prds->num_pages; | ||
if (b->active_idx >= 0) { pci_err(ithc->pci, "buffer already active\n"); return -EINVAL; } | ||
b->active_idx = idx; | ||
if (prds->dir == DMA_TO_DEVICE) { | ||
if (b->data_size > PAGE_SIZE) return -EINVAL; | ||
prd->addr = sg_dma_address(b->sgt->sgl) >> 10; | ||
prd->size = b->data_size | PRD_FLAG_END; | ||
flush_kernel_vmap_range(b->addr, b->data_size); | ||
} else if (prds->dir == DMA_FROM_DEVICE) { | ||
struct scatterlist *sg; | ||
int i; | ||
for_each_sgtable_dma_sg(b->sgt, sg, i) { | ||
prd->addr = sg_dma_address(sg) >> 10; | ||
prd->size = sg_dma_len(sg); | ||
prd++; | ||
} | ||
prd[-1].size |= PRD_FLAG_END; | ||
} | ||
dma_wmb(); // for the prds | ||
dma_sync_sgtable_for_device(&ithc->pci->dev, b->sgt, prds->dir); | ||
return 0; | ||
} | ||
|
||
static int ithc_dma_data_buffer_get(struct ithc *ithc, struct ithc_dma_prd_buffer *prds, struct ithc_dma_data_buffer *b, unsigned idx) { | ||
struct ithc_phys_region_desc *prd = prds->addr; | ||
prd += idx * prds->num_pages; | ||
if (b->active_idx != idx) { pci_err(ithc->pci, "wrong buffer index\n"); return -EINVAL; } | ||
b->active_idx = -1; | ||
if (prds->dir == DMA_FROM_DEVICE) { | ||
dma_rmb(); // for the prds | ||
b->data_size = 0; | ||
struct scatterlist *sg; | ||
int i; | ||
for_each_sgtable_dma_sg(b->sgt, sg, i) { | ||
unsigned size = prd->size; | ||
b->data_size += size & PRD_SIZE_MASK; | ||
if (size & PRD_FLAG_END) break; | ||
if ((size & PRD_SIZE_MASK) != sg_dma_len(sg)) { pci_err(ithc->pci, "truncated prd\n"); break; } | ||
prd++; | ||
} | ||
invalidate_kernel_vmap_range(b->addr, b->data_size); | ||
} | ||
dma_sync_sgtable_for_cpu(&ithc->pci->dev, b->sgt, prds->dir); | ||
return 0; | ||
} | ||
|
||
int ithc_dma_rx_init(struct ithc *ithc, u8 channel, const char *devname) { | ||
struct ithc_dma_rx *rx = &ithc->dma_rx[channel]; | ||
mutex_init(&rx->mutex); | ||
u32 buf_size = DEVCFG_DMA_RX_SIZE(ithc->config.dma_buf_sizes); | ||
unsigned num_pages = (buf_size + PAGE_SIZE - 1) / PAGE_SIZE; | ||
pci_dbg(ithc->pci, "allocating rx buffers: num = %u, size = %u, pages = %u\n", NUM_RX_BUF, buf_size, num_pages); | ||
CHECK_RET(ithc_dma_prd_alloc, ithc, &rx->prds, NUM_RX_BUF, num_pages, DMA_FROM_DEVICE); | ||
for (unsigned i = 0; i < NUM_RX_BUF; i++) | ||
CHECK_RET(ithc_dma_data_alloc, ithc, &rx->prds, &rx->bufs[i]); | ||
writeb(DMA_RX_CONTROL2_RESET, &ithc->regs->dma_rx[channel].control2); | ||
lo_hi_writeq(rx->prds.dma_addr, &ithc->regs->dma_rx[channel].addr); | ||
writeb(NUM_RX_BUF - 1, &ithc->regs->dma_rx[channel].num_bufs); | ||
writeb(num_pages - 1, &ithc->regs->dma_rx[channel].num_prds); | ||
u8 head = readb(&ithc->regs->dma_rx[channel].head); | ||
if (head) { pci_err(ithc->pci, "head is nonzero (%u)\n", head); return -EIO; } | ||
for (unsigned i = 0; i < NUM_RX_BUF; i++) | ||
CHECK_RET(ithc_dma_data_buffer_put, ithc, &rx->prds, &rx->bufs[i], i); | ||
writeb(head ^ DMA_RX_WRAP_FLAG, &ithc->regs->dma_rx[channel].tail); | ||
return 0; | ||
} | ||
void ithc_dma_rx_enable(struct ithc *ithc, u8 channel) { | ||
bitsb_set(&ithc->regs->dma_rx[channel].control, DMA_RX_CONTROL_ENABLE | DMA_RX_CONTROL_IRQ_ERROR | DMA_RX_CONTROL_IRQ_DATA); | ||
CHECK(waitl, ithc, &ithc->regs->dma_rx[1].status, DMA_RX_STATUS_ENABLED, DMA_RX_STATUS_ENABLED); | ||
} | ||
|
||
int ithc_dma_tx_init(struct ithc *ithc) { | ||
struct ithc_dma_tx *tx = &ithc->dma_tx; | ||
mutex_init(&tx->mutex); | ||
tx->max_size = DEVCFG_DMA_TX_SIZE(ithc->config.dma_buf_sizes); | ||
unsigned num_pages = (tx->max_size + PAGE_SIZE - 1) / PAGE_SIZE; | ||
pci_dbg(ithc->pci, "allocating tx buffers: size = %u, pages = %u\n", tx->max_size, num_pages); | ||
CHECK_RET(ithc_dma_prd_alloc, ithc, &tx->prds, 1, num_pages, DMA_TO_DEVICE); | ||
CHECK_RET(ithc_dma_data_alloc, ithc, &tx->prds, &tx->buf); | ||
lo_hi_writeq(tx->prds.dma_addr, &ithc->regs->dma_tx.addr); | ||
writeb(num_pages - 1, &ithc->regs->dma_tx.num_prds); | ||
CHECK_RET(ithc_dma_data_buffer_put, ithc, &ithc->dma_tx.prds, &ithc->dma_tx.buf, 0); | ||
return 0; | ||
} | ||
|
||
static int ithc_dma_rx_process_buf(struct ithc *ithc, struct ithc_dma_data_buffer *data, u8 channel, u8 buf) { | ||
if (buf >= NUM_RX_BUF) { | ||
pci_err(ithc->pci, "invalid dma ringbuffer index\n"); | ||
return -EINVAL; | ||
} | ||
ithc_set_active(ithc); | ||
u32 len = data->data_size; | ||
struct ithc_dma_rx_header *hdr = data->addr; | ||
u8 *hiddata = (void *)(hdr + 1); | ||
if (len >= sizeof *hdr && hdr->code == DMA_RX_CODE_RESET) { | ||
CHECK(ithc_reset, ithc); | ||
} else if (len < sizeof *hdr || len != sizeof *hdr + hdr->data_size) { | ||
if (hdr->code == DMA_RX_CODE_INPUT_REPORT) { | ||
// When the CPU enters a low power state during DMA, we can get truncated messages. | ||
// Typically this will be a single touch HID report that is only 1 byte, or a multitouch report that is 257 bytes. | ||
// See also ithc_set_active(). | ||
} else { | ||
pci_err(ithc->pci, "invalid dma rx data! channel %u, buffer %u, size %u, code %u, data size %u\n", channel, buf, len, hdr->code, hdr->data_size); | ||
print_hex_dump_debug(DEVNAME " data: ", DUMP_PREFIX_OFFSET, 32, 1, hdr, min(len, 0x400u), 0); | ||
} | ||
} else if (hdr->code == DMA_RX_CODE_REPORT_DESCRIPTOR && hdr->data_size > 8) { | ||
CHECK(hid_parse_report, ithc->hid, hiddata + 8, hdr->data_size - 8); | ||
WRITE_ONCE(ithc->hid_parse_done, true); | ||
wake_up(&ithc->wait_hid_parse); | ||
} else if (hdr->code == DMA_RX_CODE_INPUT_REPORT) { | ||
CHECK(hid_input_report, ithc->hid, HID_INPUT_REPORT, hiddata, hdr->data_size, 1); | ||
} else if (hdr->code == DMA_RX_CODE_FEATURE_REPORT) { | ||
bool done = false; | ||
mutex_lock(&ithc->hid_get_feature_mutex); | ||
if (ithc->hid_get_feature_buf) { | ||
if (hdr->data_size < ithc->hid_get_feature_size) ithc->hid_get_feature_size = hdr->data_size; | ||
memcpy(ithc->hid_get_feature_buf, hiddata, ithc->hid_get_feature_size); | ||
ithc->hid_get_feature_buf = NULL; | ||
done = true; | ||
} | ||
mutex_unlock(&ithc->hid_get_feature_mutex); | ||
if (done) wake_up(&ithc->wait_hid_get_feature); | ||
else CHECK(hid_input_report, ithc->hid, HID_FEATURE_REPORT, hiddata, hdr->data_size, 1); | ||
} else { | ||
pci_dbg(ithc->pci, "unhandled dma rx data! channel %u, buffer %u, size %u, code %u\n", channel, buf, len, hdr->code); | ||
print_hex_dump_debug(DEVNAME " data: ", DUMP_PREFIX_OFFSET, 32, 1, hdr, min(len, 0x400u), 0); | ||
} | ||
return 0; | ||
} | ||
|
||
static int ithc_dma_rx_unlocked(struct ithc *ithc, u8 channel) { | ||
struct ithc_dma_rx *rx = &ithc->dma_rx[channel]; | ||
unsigned n = rx->num_received; | ||
u8 head_wrap = readb(&ithc->regs->dma_rx[channel].head); | ||
while (1) { | ||
u8 tail = n % NUM_RX_BUF; | ||
u8 tail_wrap = tail | ((n / NUM_RX_BUF) & 1 ? 0 : DMA_RX_WRAP_FLAG); | ||
writeb(tail_wrap, &ithc->regs->dma_rx[channel].tail); | ||
// ringbuffer is full if tail_wrap == head_wrap | ||
// ringbuffer is empty if tail_wrap == head_wrap ^ WRAP_FLAG | ||
if (tail_wrap == (head_wrap ^ DMA_RX_WRAP_FLAG)) return 0; | ||
|
||
// take the buffer that the device just filled | ||
struct ithc_dma_data_buffer *b = &rx->bufs[n % NUM_RX_BUF]; | ||
CHECK_RET(ithc_dma_data_buffer_get, ithc, &rx->prds, b, tail); | ||
rx->num_received = ++n; | ||
|
||
// process data | ||
CHECK(ithc_dma_rx_process_buf, ithc, b, channel, tail); | ||
|
||
// give the buffer back to the device | ||
CHECK_RET(ithc_dma_data_buffer_put, ithc, &rx->prds, b, tail); | ||
} | ||
} | ||
int ithc_dma_rx(struct ithc *ithc, u8 channel) { | ||
struct ithc_dma_rx *rx = &ithc->dma_rx[channel]; | ||
mutex_lock(&rx->mutex); | ||
int ret = ithc_dma_rx_unlocked(ithc, channel); | ||
mutex_unlock(&rx->mutex); | ||
return ret; | ||
} | ||
|
||
static int ithc_dma_tx_unlocked(struct ithc *ithc, u32 cmdcode, u32 datasize, void *data) { | ||
pci_dbg(ithc->pci, "dma tx command %u, size %u\n", cmdcode, datasize); | ||
struct ithc_dma_tx_header *hdr; | ||
u8 padding = datasize & 3 ? 4 - (datasize & 3) : 0; | ||
unsigned fullsize = sizeof *hdr + datasize + padding; | ||
if (fullsize > ithc->dma_tx.max_size || fullsize > PAGE_SIZE) return -EINVAL; | ||
CHECK_RET(ithc_dma_data_buffer_get, ithc, &ithc->dma_tx.prds, &ithc->dma_tx.buf, 0); | ||
|
||
ithc->dma_tx.buf.data_size = fullsize; | ||
hdr = ithc->dma_tx.buf.addr; | ||
hdr->code = cmdcode; | ||
hdr->data_size = datasize; | ||
u8 *dest = (void *)(hdr + 1); | ||
memcpy(dest, data, datasize); | ||
dest += datasize; | ||
for (u8 p = 0; p < padding; p++) *dest++ = 0; | ||
CHECK_RET(ithc_dma_data_buffer_put, ithc, &ithc->dma_tx.prds, &ithc->dma_tx.buf, 0); | ||
|
||
bitsb_set(&ithc->regs->dma_tx.control, DMA_TX_CONTROL_SEND); | ||
CHECK_RET(waitb, ithc, &ithc->regs->dma_tx.control, DMA_TX_CONTROL_SEND, 0); | ||
writel(DMA_TX_STATUS_DONE, &ithc->regs->dma_tx.status); | ||
return 0; | ||
} | ||
int ithc_dma_tx(struct ithc *ithc, u32 cmdcode, u32 datasize, void *data) { | ||
mutex_lock(&ithc->dma_tx.mutex); | ||
int ret = ithc_dma_tx_unlocked(ithc, cmdcode, datasize, data); | ||
mutex_unlock(&ithc->dma_tx.mutex); | ||
return ret; | ||
} | ||
|
Oops, something went wrong.