* [PATCH] HID: core: do not allow parsing 0-sized reports
From: Dmitry Torokhov @ 2026-04-01 6:04 UTC (permalink / raw)
To: Jiri Kosina
Cc: Benjamin Tissoires, Peter Hutterer, Kees Cook, Alan Stern,
Charles Keepax, linux-input, linux-kernel
Commit d7db259bd6df ("HID: core: factor out hid_parse_collections()")
reworked collection parsing code and inadvertently allowed returning
"success" when parsing 0-sized reports where old code returned -EINVAL.
Restore the original behavior by doing an explicit check.
Note that the error message now differs from the generic "item fetching
failed at offset %u/%u" that is now used only for non-empty descriptors.
Fixes: d7db259bd6df ("HID: core: factor out hid_parse_collections()")
Signed-off-by: Dmitry Torokhov <dmitry.torokhov@gmail.com>
---
drivers/hid/hid-core.c | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c
index b40953e0f52e..be9d2b3356c3 100644
--- a/drivers/hid/hid-core.c
+++ b/drivers/hid/hid-core.c
@@ -1272,6 +1272,11 @@ static int hid_parse_collections(struct hid_device *device)
device->collection[i].parent_idx = -1;
ret = -EINVAL;
+ if (start == end) {
+ hid_err(device, "rejecting 0-sized report descriptor\n");
+ goto out;
+ }
+
while ((next = fetch_item(start, end, &item)) != NULL) {
start = next;
--
2.53.0.1185.g05d4b7b318-goog
--
Dmitry
^ permalink raw reply related
* Re: [PATCH 0/4] Input: refactor USB endpoint lookups
From: Johan Hovold @ 2026-04-01 7:23 UTC (permalink / raw)
To: Dmitry Torokhov; +Cc: linux-input, linux-kernel
In-Reply-To: <acwskOMBhv9yWkD7@google.com>
On Tue, Mar 31, 2026 at 01:21:05PM -0700, Dmitry Torokhov wrote:
> On Mon, Mar 30, 2026 at 04:55:35PM -0700, Dmitry Torokhov wrote:
> > On Mon, Mar 30, 2026 at 11:59:44AM +0200, Johan Hovold wrote:
> > > Use the common USB helpers for looking up bulk and interrupt endpoints
> > > instead of open coding.
> >
> > Applied the lot, thank you.
>
> Actually, dropped patch #4:
>
> https://sashiko.dev/#/patchset/20260330095948.1663141-1-johan%40kernel.org
Thanks, I saw this one last night as well (after reading the LWN
article).
I'll send a v2.
Johan
^ permalink raw reply
* [PATCH v2] Input: usbtouchscreen - refactor endpoint lookup
From: Johan Hovold @ 2026-04-01 8:22 UTC (permalink / raw)
To: Dmitry Torokhov; +Cc: linux-input, linux-kernel, Johan Hovold
Use the common USB helpers for looking up bulk and interrupt endpoints
(and determining endpoint numbers) instead of open coding.
Note that the NEXIO data interface has two bulk endpoints (see commit
5197424cdccc ("Input: usbtouchscreen - add NEXIO (or iNexio) support")
for the descriptors).
The lookup in probe handles both bulk-in and interrupt-in endpoints and
was added to handle NEXIO devices. Replace the open coded lookup with a
lookup for the common interrupt endpoint and an explicit fallback
accepting a bulk endpoint.
This iterates over the (two) endpoints twice for NEXIO devices but makes
it more clear what is going on.
Signed-off-by: Johan Hovold <johan@kernel.org>
---
Changes in v2
- add the missing fallback to handle bulk endpoints to probe
- amend commit message
drivers/input/touchscreen/usbtouchscreen.c | 43 ++++++++--------------
1 file changed, 16 insertions(+), 27 deletions(-)
diff --git a/drivers/input/touchscreen/usbtouchscreen.c b/drivers/input/touchscreen/usbtouchscreen.c
index 657555c8796c..daa28135f887 100644
--- a/drivers/input/touchscreen/usbtouchscreen.c
+++ b/drivers/input/touchscreen/usbtouchscreen.c
@@ -969,24 +969,21 @@ static int nexio_init(struct usbtouch_usb *usbtouch)
{
struct usb_device *dev = interface_to_usbdev(usbtouch->interface);
struct usb_host_interface *interface = usbtouch->interface->cur_altsetting;
+ struct usb_endpoint_descriptor *ep_in, *ep_out;
struct nexio_priv *priv = usbtouch->priv;
- int ret = -ENOMEM;
int actual_len, i;
char *firmware_ver = NULL, *device_name = NULL;
- int input_ep = 0, output_ep = 0;
+ int input_ep, output_ep;
+ int ret;
/* find first input and output endpoint */
- for (i = 0; i < interface->desc.bNumEndpoints; i++) {
- if (!input_ep &&
- usb_endpoint_dir_in(&interface->endpoint[i].desc))
- input_ep = interface->endpoint[i].desc.bEndpointAddress;
- if (!output_ep &&
- usb_endpoint_dir_out(&interface->endpoint[i].desc))
- output_ep = interface->endpoint[i].desc.bEndpointAddress;
- }
- if (!input_ep || !output_ep)
+ ret = usb_find_common_endpoints(interface, &ep_in, &ep_out, NULL, NULL);
+ if (ret)
return -ENXIO;
+ input_ep = usb_endpoint_num(ep_in);
+ output_ep = usb_endpoint_num(ep_out);
+
u8 *buf __free(kfree) = kmalloc(NEXIO_BUFSIZE, GFP_NOIO);
if (!buf)
return -ENOMEM;
@@ -1427,18 +1424,6 @@ static void usbtouch_free_buffers(struct usb_device *udev,
kfree(usbtouch->buffer);
}
-static struct usb_endpoint_descriptor *
-usbtouch_get_input_endpoint(struct usb_host_interface *interface)
-{
- int i;
-
- for (i = 0; i < interface->desc.bNumEndpoints; i++)
- if (usb_endpoint_dir_in(&interface->endpoint[i].desc))
- return &interface->endpoint[i].desc;
-
- return NULL;
-}
-
static int usbtouch_probe(struct usb_interface *intf,
const struct usb_device_id *id)
{
@@ -1447,17 +1432,21 @@ static int usbtouch_probe(struct usb_interface *intf,
struct usb_endpoint_descriptor *endpoint;
struct usb_device *udev = interface_to_usbdev(intf);
const struct usbtouch_device_info *type;
- int err = -ENOMEM;
+ int err;
/* some devices are ignored */
type = (const struct usbtouch_device_info *)id->driver_info;
if (!type)
return -ENODEV;
- endpoint = usbtouch_get_input_endpoint(intf->cur_altsetting);
- if (!endpoint)
- return -ENXIO;
+ err = usb_find_int_in_endpoint(intf->cur_altsetting, &endpoint);
+ if (err) {
+ err = usb_find_bulk_in_endpoint(intf->cur_altsetting, &endpoint);
+ if (err)
+ return -ENXIO;
+ }
+ err = -ENOMEM;
usbtouch = kzalloc_obj(*usbtouch);
input_dev = input_allocate_device();
if (!usbtouch || !input_dev)
--
2.52.0
^ permalink raw reply related
* [syzbot] [input?] [usb?] KASAN: slab-use-after-free Read in hiddev_disconnect (4)
From: syzbot @ 2026-04-01 9:33 UTC (permalink / raw)
To: bentiss, jikos, linux-input, linux-kernel, linux-usb,
syzkaller-bugs
Hello,
syzbot found the following issue on:
HEAD commit: 46b513250491 Merge tag 'v7.0-rc5-smb3-client-fix' of git:/..
git tree: upstream
console output: https://syzkaller.appspot.com/x/log.txt?x=177c11da580000
kernel config: https://syzkaller.appspot.com/x/.config?x=5a3e5e8c17cc174e
dashboard link: https://syzkaller.appspot.com/bug?extid=563191a4939ddbfe73d4
compiler: gcc (Debian 14.2.0-19) 14.2.0, GNU ld (GNU Binutils for Debian) 2.44
Unfortunately, I don't have any reproducer for this issue yet.
Downloadable assets:
disk image (non-bootable): https://storage.googleapis.com/syzbot-assets/d900f083ada3/non_bootable_disk-46b51325.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/22fdb6baf246/vmlinux-46b51325.xz
kernel image: https://storage.googleapis.com/syzbot-assets/a3a69d9008ed/bzImage-46b51325.xz
IMPORTANT: if you fix the issue, please add the following tag to the commit:
Reported-by: syzbot+563191a4939ddbfe73d4@syzkaller.appspotmail.com
usb 6-1: USB disconnect, device number 25
==================================================================
BUG: KASAN: slab-use-after-free in debug_spin_lock_before kernel/locking/spinlock_debug.c:86 [inline]
BUG: KASAN: slab-use-after-free in do_raw_spin_lock+0x23b/0x260 kernel/locking/spinlock_debug.c:115
Read of size 4 at addr ffff8880289f981c by task kworker/1:3/6000
CPU: 1 UID: 0 PID: 6000 Comm: kworker/1:3 Tainted: G L syzkaller #0 PREEMPT(full)
Tainted: [L]=SOFTLOCKUP
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
Workqueue: usb_hub_wq hub_event
Call Trace:
<TASK>
__dump_stack lib/dump_stack.c:94 [inline]
dump_stack_lvl+0x100/0x190 lib/dump_stack.c:120
print_address_description mm/kasan/report.c:378 [inline]
print_report+0x156/0x4c9 mm/kasan/report.c:482
kasan_report+0xdf/0x1e0 mm/kasan/report.c:595
debug_spin_lock_before kernel/locking/spinlock_debug.c:86 [inline]
do_raw_spin_lock+0x23b/0x260 kernel/locking/spinlock_debug.c:115
__raw_spin_lock_irqsave include/linux/spinlock_api_smp.h:133 [inline]
_raw_spin_lock_irqsave+0x42/0x60 kernel/locking/spinlock.c:162
__mutex_unlock_slowpath+0x18b/0x790 kernel/locking/mutex.c:963
hiddev_disconnect+0x15b/0x1d0 drivers/hid/usbhid/hiddev.c:940
hid_disconnect+0xbe/0x1b0 drivers/hid/hid-core.c:2353
hid_hw_stop drivers/hid/hid-core.c:2400 [inline]
hid_device_remove+0x1b2/0x270 drivers/hid/hid-core.c:2834
device_remove+0xcb/0x180 drivers/base/dd.c:631
__device_release_driver drivers/base/dd.c:1344 [inline]
device_release_driver_internal+0x44e/0x620 drivers/base/dd.c:1367
bus_remove_device+0x2bc/0x560 drivers/base/bus.c:657
device_del+0x376/0x9b0 drivers/base/core.c:3880
hid_remove_device drivers/hid/hid-core.c:3009 [inline]
hid_destroy_device+0x19c/0x240 drivers/hid/hid-core.c:3031
usbhid_disconnect+0xa0/0xe0 drivers/hid/usbhid/hid-core.c:1477
usb_unbind_interface+0x1dd/0x9e0 drivers/usb/core/driver.c:458
device_remove drivers/base/dd.c:633 [inline]
device_remove+0x12a/0x180 drivers/base/dd.c:625
__device_release_driver drivers/base/dd.c:1344 [inline]
device_release_driver_internal+0x44e/0x620 drivers/base/dd.c:1367
bus_remove_device+0x2bc/0x560 drivers/base/bus.c:657
device_del+0x376/0x9b0 drivers/base/core.c:3880
usb_disable_device+0x367/0x810 drivers/usb/core/message.c:1476
usb_disconnect+0x2e2/0x9a0 drivers/usb/core/hub.c:2345
hub_port_connect drivers/usb/core/hub.c:5407 [inline]
hub_port_connect_change drivers/usb/core/hub.c:5707 [inline]
port_event drivers/usb/core/hub.c:5871 [inline]
hub_event+0x1d0c/0x4af0 drivers/usb/core/hub.c:5953
process_one_work+0xa23/0x19a0 kernel/workqueue.c:3276
process_scheduled_works kernel/workqueue.c:3359 [inline]
worker_thread+0x5ef/0xe50 kernel/workqueue.c:3440
kthread+0x370/0x450 kernel/kthread.c:436
ret_from_fork+0x754/0xd80 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
</TASK>
Allocated by task 24:
kasan_save_stack+0x30/0x50 mm/kasan/common.c:57
kasan_save_track+0x14/0x30 mm/kasan/common.c:78
poison_kmalloc_redzone mm/kasan/common.c:398 [inline]
__kasan_kmalloc+0xaa/0xb0 mm/kasan/common.c:415
kmalloc_noprof include/linux/slab.h:950 [inline]
kzalloc_noprof include/linux/slab.h:1188 [inline]
hiddev_connect+0x259/0x5d0 drivers/hid/usbhid/hiddev.c:893
hid_connect+0x241/0x1750 drivers/hid/hid-core.c:2267
hid_hw_start+0xaa/0x140 drivers/hid/hid-core.c:2380
plantronics_probe+0x2b9/0x3a0 drivers/hid/hid-plantronics.c:209
__hid_device_probe drivers/hid/hid-core.c:2776 [inline]
hid_device_probe+0x50e/0x800 drivers/hid/hid-core.c:2813
call_driver_probe drivers/base/dd.c:643 [inline]
really_probe+0x241/0xa60 drivers/base/dd.c:721
__driver_probe_device+0x1de/0x400 drivers/base/dd.c:863
driver_probe_device+0x4c/0x1b0 drivers/base/dd.c:893
__device_attach_driver+0x1df/0x340 drivers/base/dd.c:1021
bus_for_each_drv+0x159/0x1e0 drivers/base/bus.c:500
__device_attach+0x1e4/0x4d0 drivers/base/dd.c:1093
device_initial_probe+0xaf/0xd0 drivers/base/dd.c:1148
bus_probe_device+0x64/0x160 drivers/base/bus.c:613
device_add+0x11d9/0x1950 drivers/base/core.c:3691
hid_add_device+0x2bf/0x440 drivers/hid/hid-core.c:2952
usbhid_probe+0xd57/0x1350 drivers/hid/usbhid/hid-core.c:1450
usb_probe_interface+0x303/0x8f0 drivers/usb/core/driver.c:396
call_driver_probe drivers/base/dd.c:643 [inline]
really_probe+0x241/0xa60 drivers/base/dd.c:721
__driver_probe_device+0x1de/0x400 drivers/base/dd.c:863
driver_probe_device+0x4c/0x1b0 drivers/base/dd.c:893
__device_attach_driver+0x1df/0x340 drivers/base/dd.c:1021
bus_for_each_drv+0x159/0x1e0 drivers/base/bus.c:500
__device_attach+0x1e4/0x4d0 drivers/base/dd.c:1093
device_initial_probe+0xaf/0xd0 drivers/base/dd.c:1148
bus_probe_device+0x64/0x160 drivers/base/bus.c:613
device_add+0x11d9/0x1950 drivers/base/core.c:3691
usb_set_configuration+0xd97/0x1c60 drivers/usb/core/message.c:2266
usb_generic_driver_probe+0xa1/0xe0 drivers/usb/core/generic.c:250
usb_probe_device+0xef/0x400 drivers/usb/core/driver.c:291
call_driver_probe drivers/base/dd.c:643 [inline]
really_probe+0x241/0xa60 drivers/base/dd.c:721
__driver_probe_device+0x1de/0x400 drivers/base/dd.c:863
driver_probe_device+0x4c/0x1b0 drivers/base/dd.c:893
__device_attach_driver+0x1df/0x340 drivers/base/dd.c:1021
bus_for_each_drv+0x159/0x1e0 drivers/base/bus.c:500
__device_attach+0x1e4/0x4d0 drivers/base/dd.c:1093
device_initial_probe+0xaf/0xd0 drivers/base/dd.c:1148
bus_probe_device+0x64/0x160 drivers/base/bus.c:613
device_add+0x11d9/0x1950 drivers/base/core.c:3691
usb_new_device.cold+0x685/0x115c drivers/usb/core/hub.c:2695
hub_port_connect drivers/usb/core/hub.c:5567 [inline]
hub_port_connect_change drivers/usb/core/hub.c:5707 [inline]
port_event drivers/usb/core/hub.c:5871 [inline]
hub_event+0x314d/0x4af0 drivers/usb/core/hub.c:5953
process_one_work+0xa23/0x19a0 kernel/workqueue.c:3276
process_scheduled_works kernel/workqueue.c:3359 [inline]
worker_thread+0x5ef/0xe50 kernel/workqueue.c:3440
kthread+0x370/0x450 kernel/kthread.c:436
ret_from_fork+0x754/0xd80 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
Freed by task 11803:
kasan_save_stack+0x30/0x50 mm/kasan/common.c:57
kasan_save_track+0x14/0x30 mm/kasan/common.c:78
kasan_save_free_info+0x3b/0x70 mm/kasan/generic.c:584
poison_slab_object mm/kasan/common.c:253 [inline]
__kasan_slab_free+0x5f/0x80 mm/kasan/common.c:285
kasan_slab_free include/linux/kasan.h:235 [inline]
slab_free_hook mm/slub.c:2685 [inline]
slab_free mm/slub.c:6165 [inline]
kfree+0x1f6/0x6b0 mm/slub.c:6483
hiddev_release+0x40d/0x520 drivers/hid/usbhid/hiddev.c:232
__fput+0x3ff/0xb40 fs/file_table.c:469
task_work_run+0x150/0x240 kernel/task_work.c:233
resume_user_mode_work include/linux/resume_user_mode.h:50 [inline]
__exit_to_user_mode_loop kernel/entry/common.c:67 [inline]
exit_to_user_mode_loop+0x100/0x4a0 kernel/entry/common.c:98
__exit_to_user_mode_prepare include/linux/irq-entry-common.h:226 [inline]
syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:256 [inline]
syscall_exit_to_user_mode include/linux/entry-common.h:325 [inline]
do_syscall_64+0x67c/0xf80 arch/x86/entry/syscall_64.c:100
entry_SYSCALL_64_after_hwframe+0x77/0x7f
The buggy address belongs to the object at ffff8880289f9800
which belongs to the cache kmalloc-512 of size 512
The buggy address is located 28 bytes inside of
freed 512-byte region [ffff8880289f9800, ffff8880289f9a00)
The buggy address belongs to the physical page:
page: refcount:0 mapcount:0 mapping:0000000000000000 index:0xffff8880289f8c00 pfn:0x289f8
head: order:2 mapcount:0 entire_mapcount:0 nr_pages_mapped:0 pincount:0
flags: 0xfff00000000240(workingset|head|node=0|zone=1|lastcpupid=0x7ff)
page_type: f5(slab)
raw: 00fff00000000240 ffff88801b842c80 ffffea0000b3f710 ffffea00015f9410
raw: ffff8880289f8c00 0000000800100008 00000000f5000000 0000000000000000
head: 00fff00000000240 ffff88801b842c80 ffffea0000b3f710 ffffea00015f9410
head: ffff8880289f8c00 0000000800100008 00000000f5000000 0000000000000000
head: 00fff00000000002 ffffea0000a27e01 00000000ffffffff 00000000ffffffff
head: ffffffffffffffff 0000000000000000 00000000ffffffff 0000000000000004
page dumped because: kasan: bad access detected
page_owner tracks the page as allocated
page last allocated via order 2, migratetype Unmovable, gfp_mask 0x1d20c0(__GFP_IO|__GFP_FS|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC|__GFP_HARDWALL), pid 10245, tgid 10245 (udevd), ts 189371739713, free_ts 189294134261
set_page_owner include/linux/page_owner.h:32 [inline]
post_alloc_hook+0x153/0x170 mm/page_alloc.c:1889
prep_new_page mm/page_alloc.c:1897 [inline]
get_page_from_freelist+0x111d/0x3140 mm/page_alloc.c:3962
__alloc_frozen_pages_noprof+0x27c/0x2ba0 mm/page_alloc.c:5250
alloc_slab_page mm/slub.c:3292 [inline]
allocate_slab mm/slub.c:3481 [inline]
new_slab+0xa6/0x6b0 mm/slub.c:3539
refill_objects+0x26b/0x400 mm/slub.c:7175
refill_sheaf mm/slub.c:2812 [inline]
__pcs_replace_empty_main+0x1ab/0x660 mm/slub.c:4615
alloc_from_pcs mm/slub.c:4717 [inline]
slab_alloc_node mm/slub.c:4851 [inline]
__kmalloc_cache_noprof+0x493/0x6f0 mm/slub.c:5375
kmalloc_noprof include/linux/slab.h:950 [inline]
kzalloc_noprof include/linux/slab.h:1188 [inline]
kernfs_fop_open+0x23d/0xd50 fs/kernfs/file.c:641
do_dentry_open+0x6d8/0x1660 fs/open.c:949
vfs_open+0x82/0x3f0 fs/open.c:1081
do_open fs/namei.c:4671 [inline]
path_openat+0x208c/0x31a0 fs/namei.c:4830
do_file_open+0x20e/0x430 fs/namei.c:4859
do_sys_openat2+0x10d/0x1e0 fs/open.c:1366
do_sys_open fs/open.c:1372 [inline]
__do_sys_openat fs/open.c:1388 [inline]
__se_sys_openat fs/open.c:1383 [inline]
__x64_sys_openat+0x12d/0x210 fs/open.c:1383
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x106/0xf80 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
page last free pid 11125 tgid 11125 stack trace:
reset_page_owner include/linux/page_owner.h:25 [inline]
__free_pages_prepare mm/page_alloc.c:1433 [inline]
__free_frozen_pages+0x7e1/0x10d0 mm/page_alloc.c:2978
stack_depot_save_flags+0x435/0x9d0 lib/stackdepot.c:735
kasan_save_stack+0x3f/0x50 mm/kasan/common.c:58
kasan_save_track+0x14/0x30 mm/kasan/common.c:78
poison_kmalloc_redzone mm/kasan/common.c:398 [inline]
__kasan_kmalloc+0xaa/0xb0 mm/kasan/common.c:415
kmalloc_noprof include/linux/slab.h:950 [inline]
kzalloc_noprof include/linux/slab.h:1188 [inline]
ida_alloc_range+0x464/0x830 lib/idr.c:420
ida_alloc include/linux/idr.h:293 [inline]
create_worker+0x99/0x750 kernel/workqueue.c:2796
maybe_create_worker kernel/workqueue.c:3075 [inline]
manage_workers kernel/workqueue.c:3152 [inline]
worker_thread+0x8e2/0xe50 kernel/workqueue.c:3415
kthread+0x370/0x450 kernel/kthread.c:436
ret_from_fork+0x754/0xd80 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
Memory state around the buggy address:
ffff8880289f9700: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
ffff8880289f9780: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
>ffff8880289f9800: fa fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
^
ffff8880289f9880: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
ffff8880289f9900: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
==================================================================
---
This report is generated by a bot. It may contain errors.
See https://goo.gl/tpsmEJ for more information about syzbot.
syzbot engineers can be reached at syzkaller@googlegroups.com.
syzbot will keep track of this issue. See:
https://goo.gl/tpsmEJ#status for how to communicate with syzbot.
If the report is already addressed, let syzbot know by replying with:
#syz fix: exact-commit-title
If you want to overwrite report's subsystems, reply with:
#syz set subsystems: new-subsystem
(See the list of subsystem names on the web dashboard)
If the report is a duplicate of another one, reply with:
#syz dup: exact-subject-of-another-report
If you want to undo deduplication, reply with:
#syz undup
^ permalink raw reply
* Re: [PATCH v4 3/4] Input: aw86938 - add driver for Awinic AW86938
From: Luca Weiss @ 2026-04-01 14:44 UTC (permalink / raw)
To: Dmitry Torokhov, Griffin Kroah-Hartman
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Bjorn Andersson,
Konrad Dybcio, Luca Weiss, linux-input, devicetree, linux-kernel,
linux-arm-msm
In-Reply-To: <aae7fRYaoDHMptyu@google.com>
Hi Dmitry,
On Wed Mar 4, 2026 at 5:56 AM CET, Dmitry Torokhov wrote:
> On Mon, Mar 02, 2026 at 11:50:27AM +0100, Griffin Kroah-Hartman wrote:
>> Add support for the I2C-connected Awinic AW86938 LRA haptic driver.
>>
>> The AW86938 has a similar but slightly different register layout. In
>> particular, the boost mode register values.
>> The AW86938 also has some extra features that aren't implemented
>> in this driver yet.
>>
>> Signed-off-by: Griffin Kroah-Hartman <griffin.kroah@fairphone.com>
>
> Applied, thank you.
I'm curious, where did you apply these patches? linux-next doesn't have
it and I don't see it in your kernel.org repo either.
https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git/
Did this slip through the cracks or will these patches still appear
there?
Regards
Luca
^ permalink raw reply
* [PATCH] HID: logitech-hidpp: fix race condition when accessing stale stack pointer
From: Benoit Sevens @ 2026-04-01 14:48 UTC (permalink / raw)
To: Filipe Laíns, Bastien Nocera
Cc: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel,
Benoît Sevens
From: Benoît Sevens <bsevens@google.com>
The driver uses hidpp->send_receive_buf to point to a stack-allocated
buffer in the synchronous command path (__do_hidpp_send_message_sync).
However, this pointer is not cleared when the function returns.
If an event is processed (e.g. by a different thread) while the
send_mutex is held by a new command, but before that command has
updated send_receive_buf, the handler (hidpp_raw_hidpp_event) will
observe that the mutex is locked and dereference the stale pointer.
This results in an out-of-bounds access on a different thread's kernel
stack (or a NULL pointer dereference on the very first command).
Fix this by:
1. Clearing hidpp->send_receive_buf to NULL before releasing the mutex
in the synchronous command path.
2. Moving the assignment of the local 'question' and 'answer' pointers
inside the mutex_is_locked() block in the handler, and adding
a NULL check before dereferencing.
Signed-off-by: Benoît Sevens <bsevens@google.com>
---
drivers/hid/hid-logitech-hidpp.c | 24 +++++++++++++++++-------
1 file changed, 17 insertions(+), 7 deletions(-)
diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
index e871f1729d4b..42f7ea5b25dc 100644
--- a/drivers/hid/hid-logitech-hidpp.c
+++ b/drivers/hid/hid-logitech-hidpp.c
@@ -306,21 +306,22 @@ static int __do_hidpp_send_message_sync(struct hidpp_device *hidpp,
if (ret) {
dbg_hid("__hidpp_send_report returned err: %d\n", ret);
memset(response, 0, sizeof(struct hidpp_report));
- return ret;
+ goto out;
}
if (!wait_event_timeout(hidpp->wait, hidpp->answer_available,
5*HZ)) {
dbg_hid("%s:timeout waiting for response\n", __func__);
memset(response, 0, sizeof(struct hidpp_report));
- return -ETIMEDOUT;
+ ret = -ETIMEDOUT;
+ goto out;
}
if (response->report_id == REPORT_ID_HIDPP_SHORT &&
response->rap.sub_id == HIDPP_ERROR) {
ret = response->rap.params[1];
dbg_hid("%s:got hidpp error %02X\n", __func__, ret);
- return ret;
+ goto out;
}
if ((response->report_id == REPORT_ID_HIDPP_LONG ||
@@ -328,10 +329,14 @@ static int __do_hidpp_send_message_sync(struct hidpp_device *hidpp,
response->fap.feature_index == HIDPP20_ERROR) {
ret = response->fap.params[1];
dbg_hid("%s:got hidpp 2.0 error %02X\n", __func__, ret);
- return ret;
+ goto out;
}
- return 0;
+ ret = 0;
+
+out:
+ hidpp->send_receive_buf = NULL;
+ return ret;
}
/*
@@ -3840,8 +3845,7 @@ static int hidpp_input_configured(struct hid_device *hdev,
static int hidpp_raw_hidpp_event(struct hidpp_device *hidpp, u8 *data,
int size)
{
- struct hidpp_report *question = hidpp->send_receive_buf;
- struct hidpp_report *answer = hidpp->send_receive_buf;
+ struct hidpp_report *question, *answer;
struct hidpp_report *report = (struct hidpp_report *)data;
int ret;
int last_online;
@@ -3851,6 +3855,12 @@ static int hidpp_raw_hidpp_event(struct hidpp_device *hidpp, u8 *data,
* previously sent command.
*/
if (unlikely(mutex_is_locked(&hidpp->send_mutex))) {
+ question = hidpp->send_receive_buf;
+ answer = hidpp->send_receive_buf;
+
+ if (!question)
+ return 0;
+
/*
* Check for a correct hidpp20 answer or the corresponding
* error
--
2.53.0.1118.gaef5881109-goog
^ permalink raw reply related
* Re: [PATCH] Input: i8042 - add TUXEDO InfinityBook Max 16 Gen10 AMD to i8042 quirk table
From: Werner Sembach @ 2026-04-01 15:48 UTC (permalink / raw)
To: Dmitry Torokhov; +Cc: Christoffer Sandberg, linux-input, linux-kernel
In-Reply-To: <aZzMe2-4IqzIg3HS@google.com>
Hello Dmitry
Am 23.02.26 um 22:54 schrieb Dmitry Torokhov:
> On Mon, Feb 23, 2026 at 03:20:45PM +0100, Werner Sembach wrote:
>> From: Christoffer Sandberg <cs@tuxedo.de>
>>
>> The device occasionally wakes up from suspend with missing input on the
>> internal keyboard and the following suspend attempt results in an instant
>> wake-up. The quirks fix both issues for this device.
>>
>> Signed-off-by: Christoffer Sandberg <cs@tuxedo.de>
>> Signed-off-by: Werner Sembach <wse@tuxedocomputers.com>
> Applied, thank you.
>
May i ask you where it was applied?
I can only find the former patch
https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git/commit/?id=aed3716db7fff74919cc5775ca3a80c8bb246489
for the InfinityBook Max 15 and not this one for the InfinityBook Max 16 (both
patches have a nearly identical subject line).
Also I forgot to add cc stable, if the patch is not yet applied, can you add it
before?
Best regards,
Werner
^ permalink raw reply
* Re: [PATCH v4 3/4] Input: aw86938 - add driver for Awinic AW86938
From: Dmitry Torokhov @ 2026-04-01 16:11 UTC (permalink / raw)
To: Luca Weiss
Cc: Griffin Kroah-Hartman, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Bjorn Andersson, Konrad Dybcio, linux-input,
devicetree, linux-kernel, linux-arm-msm
In-Reply-To: <DHHWDE7QEOTO.1AQ85UBLO8IQG@fairphone.com>
On Wed, Apr 01, 2026 at 04:44:47PM +0200, Luca Weiss wrote:
> Hi Dmitry,
>
> On Wed Mar 4, 2026 at 5:56 AM CET, Dmitry Torokhov wrote:
> > On Mon, Mar 02, 2026 at 11:50:27AM +0100, Griffin Kroah-Hartman wrote:
> >> Add support for the I2C-connected Awinic AW86938 LRA haptic driver.
> >>
> >> The AW86938 has a similar but slightly different register layout. In
> >> particular, the boost mode register values.
> >> The AW86938 also has some extra features that aren't implemented
> >> in this driver yet.
> >>
> >> Signed-off-by: Griffin Kroah-Hartman <griffin.kroah@fairphone.com>
> >
> > Applied, thank you.
>
> I'm curious, where did you apply these patches? linux-next doesn't have
> it and I don't see it in your kernel.org repo either.
> https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git/
>
> Did this slip through the cracks or will these patches still appear
> there?
My bad, I think there was a conflict with Dan Carpenter's patch and as a
result the series got stuck in my internal queue. My apologies.
Should be out in 'next' branch now.
Thanks.
--
Dmitry
^ permalink raw reply
* Re: [PATCH] Input: i8042 - add TUXEDO InfinityBook Max 16 Gen10 AMD to i8042 quirk table
From: Dmitry Torokhov @ 2026-04-01 16:22 UTC (permalink / raw)
To: Werner Sembach; +Cc: Christoffer Sandberg, linux-input, linux-kernel
In-Reply-To: <11135d8c-ef30-410a-a94a-4e11e062c27b@tuxedocomputers.com>
Hi Werner,
On Wed, Apr 01, 2026 at 05:48:20PM +0200, Werner Sembach wrote:
> Hello Dmitry
>
> Am 23.02.26 um 22:54 schrieb Dmitry Torokhov:
> > On Mon, Feb 23, 2026 at 03:20:45PM +0100, Werner Sembach wrote:
> > > From: Christoffer Sandberg <cs@tuxedo.de>
> > >
> > > The device occasionally wakes up from suspend with missing input on the
> > > internal keyboard and the following suspend attempt results in an instant
> > > wake-up. The quirks fix both issues for this device.
> > >
> > > Signed-off-by: Christoffer Sandberg <cs@tuxedo.de>
> > > Signed-off-by: Werner Sembach <wse@tuxedocomputers.com>
> > Applied, thank you.
> >
> May i ask you where it was applied?
>
> I can only find the former patch https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git/commit/?id=aed3716db7fff74919cc5775ca3a80c8bb246489
> for the InfinityBook Max 15 and not this one for the InfinityBook Max 16
> (both patches have a nearly identical subject line).
Isn't it this:
https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git/commit/?h=for-linus&id=5839419cffc7788a356428d321e3ec18055c0286
?
>
> Also I forgot to add cc stable, if the patch is not yet applied, can you add
> it before?
I added the tags to the commit above (unless it is not the right one,
they all look very similar).
Thanks.
--
Dmitry
^ permalink raw reply
* Re: [PATCH] Input: i8042 - add TUXEDO InfinityBook Max 16 Gen10 AMD to i8042 quirk table
From: Werner Sembach @ 2026-04-01 17:19 UTC (permalink / raw)
To: Dmitry Torokhov; +Cc: Christoffer Sandberg, linux-input, linux-kernel
In-Reply-To: <ac1FzV9dmb_JDXxo@google.com>
Hello Dmitry,
Am 01.04.26 um 18:22 schrieb Dmitry Torokhov:
> Hi Werner,
>
> On Wed, Apr 01, 2026 at 05:48:20PM +0200, Werner Sembach wrote:
>> Hello Dmitry
>>
>> Am 23.02.26 um 22:54 schrieb Dmitry Torokhov:
>>> On Mon, Feb 23, 2026 at 03:20:45PM +0100, Werner Sembach wrote:
>>>> From: Christoffer Sandberg <cs@tuxedo.de>
>>>>
>>>> The device occasionally wakes up from suspend with missing input on the
>>>> internal keyboard and the following suspend attempt results in an instant
>>>> wake-up. The quirks fix both issues for this device.
>>>>
>>>> Signed-off-by: Christoffer Sandberg <cs@tuxedo.de>
>>>> Signed-off-by: Werner Sembach <wse@tuxedocomputers.com>
>>> Applied, thank you.
>>>
>> May i ask you where it was applied?
>>
>> I can only find the former patch https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git/commit/?id=aed3716db7fff74919cc5775ca3a80c8bb246489
>> for the InfinityBook Max 15 and not this one for the InfinityBook Max 16
>> (both patches have a nearly identical subject line).
> Isn't it this:
>
> https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git/commit/?h=for-linus&id=5839419cffc7788a356428d321e3ec18055c0286
>
> ?
Yes, thanks and sorry I somehow missed to search the for-linus branch.
>
>> Also I forgot to add cc stable, if the patch is not yet applied, can you add
>> it before?
> I added the tags to the commit above (unless it is not the right one,
> they all look very similar).
Yes, it's the one I was talking about. Maybe the next time I add the board name
to the subject line in the hope that it makes it a little bit more clear.
Thank you very much,
Werner
>
> Thanks.
>
^ permalink raw reply
* Re: [PATCH] HID: gpd: fix report descriptor on GPD Win handheld (2f24:0137)
From: Denis Benato @ 2026-04-01 17:40 UTC (permalink / raw)
To: Thorsten Leemhuis, honjow, Jiri Kosina, Benjamin Tissoires
Cc: linux-kernel, linux-input, Linux kernel regressions list
In-Reply-To: <6268c55d-2f1b-473c-b82d-1de1416d14b0@leemhuis.info>
On 3/31/26 08:25, Thorsten Leemhuis wrote:
> On 3/24/26 02:38, honjow wrote:
>> The OEM USB HID interface found on GPD Win handhelds (VID 2f24, registered
>> to ShenZhen HuiJiaZhi / GameSir, PID 0137) declares 63-byte Input and
>> Feature reports for Report ID 1, but the firmware only transfers 11 data
>> bytes per interrupt.
>>
>> Since commit 0a3fe972a7cb ("HID: core: Mitigate potential OOB by removing
>> bogus memset()"), the HID core rejects undersized reports instead of zero-
>> padding them. This breaks the device entirely on kernels >= v7.0-rc5.
>>
>> Fix it by patching the report descriptor in report_fixup(), reducing
>> Report Count from 63 to 11 for both Input and Feature.
>>
>> Closes: https://bugzilla.kernel.org/show_bug.cgi?id=221271
>> Signed-off-by: honjow <honjow311@gmail.com>
>> ---
>> drivers/hid/Kconfig | 10 +++++++++
>> drivers/hid/Makefile | 1 +
>> drivers/hid/hid-gpd.c | 52 +++++++++++++++++++++++++++++++++++++++++++
>> drivers/hid/hid-ids.h | 3 +++
>> 4 files changed, 66 insertions(+)
>> create mode 100644 drivers/hid/hid-gpd.c
>>
>> diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
>> index 10c12d8e65579..20c60f5aca4c5 100644
>> --- a/drivers/hid/Kconfig
>> +++ b/drivers/hid/Kconfig
>> @@ -419,6 +419,16 @@ config HID_GLORIOUS
>> Support for Glorious PC Gaming Race mice such as
>> the Glorious Model O, O- and D.
>>
>> +config HID_GPD
>> + tristate "GPD Win handheld OEM HID support"
> Hmmm, why does this need to be a config option? Can't this be enabled
> unconditionally? I ask in general, as it's just another point where
> things can go wrong. But I mainly ask because it's a regression fix –
> and from my understanding wrt to what Linus wants we don't expect users
> to turn some .config on to keep their hardware running (unless it can't
> be avoided at all costs).
>
> Ciao, Thorsten
>
Perhaps the place of this, just for affected kernel series that are out
(assuming stable has the bug, I haven't checked) is to do this on hid-generic,
otherwise it's a change of driver managing the device?
>> + depends on USB_HID
>> + help
>> + Report descriptor fix for the OEM USB HID interface (GameSir
>> + 2f24:0137) found on GPD Win handhelds. The firmware declares 63-byte
>> + reports but only sends 11 bytes, which the HID core rejects.
>> +
>> + Say Y or M here if you use a GPD Win handheld with this interface.
>> +
>> config HID_HOLTEK
>> tristate "Holtek HID devices"
>> depends on USB_HID
>> diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
>> index 07dfdb6a49c59..03ef72ec4499f 100644
>> --- a/drivers/hid/Makefile
>> +++ b/drivers/hid/Makefile
>> @@ -53,6 +53,7 @@ obj-$(CONFIG_HID_ELO) += hid-elo.o
>> obj-$(CONFIG_HID_EVISION) += hid-evision.o
>> obj-$(CONFIG_HID_EZKEY) += hid-ezkey.o
>> obj-$(CONFIG_HID_FT260) += hid-ft260.o
>> +obj-$(CONFIG_HID_GPD) += hid-gpd.o
>> obj-$(CONFIG_HID_GEMBIRD) += hid-gembird.o
>> obj-$(CONFIG_HID_GFRM) += hid-gfrm.o
>> obj-$(CONFIG_HID_GLORIOUS) += hid-glorious.o
>> diff --git a/drivers/hid/hid-gpd.c b/drivers/hid/hid-gpd.c
>> new file mode 100644
>> index 0000000000000..5b4d203e24995
>> --- /dev/null
>> +++ b/drivers/hid/hid-gpd.c
>> @@ -0,0 +1,52 @@
>> +// SPDX-License-Identifier: GPL-2.0-or-later
>> +/*
>> + * HID report descriptor fixup for GPD Win handhelds.
>> + *
>> + * The OEM HID interface (VID 2f24 / GameSir, PID 0137) declares Report ID 1
>> + * with Report Count 63 (8-bit fields) for both Input and Feature, but the
>> + * firmware only sends 11 bytes of payload after the report ID.
>> + */
>> +
>> +#include <linux/hid.h>
>> +#include <linux/module.h>
>> +
>> +#include "hid-ids.h"
>> +
>> +#define RDESC_LEN 38
>> +#define RPT_COUNT_INPUT_OFF 21
>> +#define RPT_COUNT_FEATURE_OFF 34
>> +
>> +static const __u8 *gpd_report_fixup(struct hid_device *hdev, __u8 *rdesc,
>> + unsigned int *rsize)
>> +{
>> + if (*rsize != RDESC_LEN)
>> + return rdesc;
>> +
>> + if (rdesc[RPT_COUNT_INPUT_OFF - 1] == 0x95 &&
>> + rdesc[RPT_COUNT_INPUT_OFF] == 0x3f &&
>> + rdesc[RPT_COUNT_FEATURE_OFF - 1] == 0x95 &&
>> + rdesc[RPT_COUNT_FEATURE_OFF] == 0x3f) {
>> + hid_info(hdev, "fixing report counts (63 -> 11 bytes)\n");
>> + rdesc[RPT_COUNT_INPUT_OFF] = 11;
>> + rdesc[RPT_COUNT_FEATURE_OFF] = 11;
>> + }
>> +
>> + return rdesc;
>> +}
>> +
>> +static const struct hid_device_id gpd_devices[] = {
>> + { HID_USB_DEVICE(USB_VENDOR_ID_GAMESIR, USB_DEVICE_ID_GAMESIR_0137) },
>> + { }
>> +};
>> +MODULE_DEVICE_TABLE(hid, gpd_devices);
>> +
>> +static struct hid_driver gpd_driver = {
>> + .name = "gpd",
>> + .id_table = gpd_devices,
>> + .report_fixup = gpd_report_fixup,
>> +};
>> +
>> +module_hid_driver(gpd_driver);
>> +
>> +MODULE_DESCRIPTION("HID report descriptor fix for GPD Win handheld (GameSir 2f24:0137)");
>> +MODULE_LICENSE("GPL");
>> diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
>> index 933b7943bdb50..d0a6c19baa660 100644
>> --- a/drivers/hid/hid-ids.h
>> +++ b/drivers/hid/hid-ids.h
>> @@ -533,6 +533,9 @@
>> #define USB_VENDOR_ID_FRUCTEL 0x25B6
>> #define USB_DEVICE_ID_GAMETEL_MT_MODE 0x0002
>>
>> +#define USB_VENDOR_ID_GAMESIR 0x2f24
>> +#define USB_DEVICE_ID_GAMESIR_0137 0x0137
>> +
>> #define USB_VENDOR_ID_GAMEVICE 0x27F8
>> #define USB_DEVICE_ID_GAMEVICE_GV186 0x0BBE
>> #define USB_DEVICE_ID_GAMEVICE_KISHI 0x0BBF
^ permalink raw reply
* [PATCH v2] HID: pulsar: add driver for Pulsar gaming mice
From: Nikolas Koesling @ 2026-04-01 18:57 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires; +Cc: linux-input, linux-kernel, Leo
Add a HID driver for Pulsar wireless gaming mice (X2 V2, X2H, X2A,
Xlite V3). The driver exposes battery level, voltage, and charging
status through the power supply framework. It supports wired, 1kHz,
and 4kHz wireless dongle connections.
The driver also supports Kysona M600 ATK, VXE R1 SE+ and
VXE Dragonfly R1 Pro, which use the same protocol for reading
battery status and availibility.
The protocol used by this driver is based on findings from
python-pulsar-mouse-tool by Andrew Rabert (MIT License):
https://github.com/andrewrabert/python-pulsar-mouse-tool
ATK vendor and device IDs were provided by Leo <leo@managarm.org>.
VXE and Kysona vendor and device IDS are from hid-kysona.c by
Lode Willems <me@lodewillems.com>
Tested-by: Leo <leo@managarm.org>
Signed-off-by: Nikolas Koesling <nikolas@koesling.info>
---
Changes in v2:
- Add support for Kysona M600, ATK VXE R1 SE+, and VXE Dragonfly R1 Pro
- Add device type enum to distinguish vendors and generate proper
battery names per vendor/model
- Add mutual exclusion with HID_KYSONA in Kconfig
- Add ATK and VXE vendor/device IDs to hid-ids.h
- Refactor model name generation: extract model_pulsar() and add
model_atk() for vendor-specific battery naming
- Fall back to hdev->name for battery model when device info read
fails on non-Pulsar devices (downgrade error to debug log)
- Remove POWER_SUPPLY_PROP_MANUFACTURER property
- Pass device type via driver_data in hid_device_id table
---
MAINTAINERS | 6 +
drivers/hid/Kconfig | 15 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 15 +
drivers/hid/hid-pulsar.c | 754 +++++++++++++++++++++++++++++++++++++++
5 files changed, 791 insertions(+)
create mode 100644 drivers/hid/hid-pulsar.c
diff --git a/MAINTAINERS b/MAINTAINERS
index c3fe46d7c4bc..207216632918 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11352,6 +11352,12 @@ L: linux-input@vger.kernel.org
S: Supported
F: drivers/hid/hid-playstation.c
+HID PULSAR DRIVER
+M: Nikolas Koesling <nikolas@koesling.info>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-pulsar.c
+
HID SENSOR HUB DRIVERS
M: Jiri Kosina <jikos@kernel.org>
M: Jonathan Cameron <jic23@kernel.org>
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index c1d9f7c6a5f2..333d165554ee 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -511,12 +511,15 @@ config HID_KYE
config HID_KYSONA
tristate "Kysona devices"
depends on USB_HID
+ depends on !HID_PULSAR
help
Support for Kysona mice.
Say Y here if you have a Kysona M600 mouse
and want to be able to read its battery capacity.
+ Note: The Kysona M600 is also supported by HID_PULSAR.
+
config HID_UCLOGIC
tristate "UC-Logic"
depends on USB_HID
@@ -1280,6 +1283,18 @@ config HID_UNIVERSAL_PIDFF
Supports Moza Racing, Cammus, VRS, FFBeast and more.
+config HID_PULSAR
+ tristate "Pulsar gaming mouse support"
+ depends on USB_HID
+ select POWER_SUPPLY
+ help
+ Support for Pulsar gaming mice (X2 V2, X2H, X2A, Xlite V3)
+ connected via 1kHz/4kHz USB dongle or wired.
+ Provides battery level, voltage, and charging status
+ monitoring via the power supply framework.
+
+ Additional supported devices: Kysona M600, ATK VXE R1 SE+
+
config HID_WACOM
tristate "Wacom Intuos/Graphire tablet support (USB)"
depends on USB_HID
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index e01838239ae6..67ad39b47df1 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -112,6 +112,7 @@ hid-picolcd-$(CONFIG_DEBUG_FS) += hid-picolcd_debugfs.o
obj-$(CONFIG_HID_PLANTRONICS) += hid-plantronics.o
obj-$(CONFIG_HID_PLAYSTATION) += hid-playstation.o
obj-$(CONFIG_HID_PRIMAX) += hid-primax.o
+obj-$(CONFIG_HID_PULSAR) += hid-pulsar.o
obj-$(CONFIG_HID_PXRC) += hid-pxrc.o
obj-$(CONFIG_HID_RAPOO) += hid-rapoo.o
obj-$(CONFIG_HID_RAZER) += hid-razer.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index afcee13bad61..5ce542150d61 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -248,6 +248,12 @@
#define USB_VENDOR_ID_ATMEL_V_USB 0x16c0
#define USB_DEVICE_ID_ATMEL_V_USB 0x05df
+#define USB_VENDOR_ID_ATK 0x373B
+#define USB_DEVICE_ID_ATK_VXE_R1_SE_WIRED 0xF58F
+
+#define USB_VENDOR_ID_ATK_ALT 0x3554
+#define USB_DEVICE_ID_ATK_VXE_R1_SE_DONGLE 0x1085
+
#define USB_VENDOR_ID_AUREAL 0x0755
#define USB_DEVICE_ID_AUREAL_W01RN 0x2626
@@ -1169,6 +1175,11 @@
#define USB_VENDOR_ID_PRODIGE 0x05af
#define USB_DEVICE_ID_PRODIGE_CORDLESS 0x3062
+#define USB_VENDOR_ID_PULSAR 0x3554
+#define USB_DEVICE_ID_PULSAR_WIRED 0xf507
+#define USB_DEVICE_ID_PULSAR_1KHZ 0xf508
+#define USB_DEVICE_ID_PULSAR_4KHZ 0xf509
+
#define I2C_VENDOR_ID_QTEC 0x6243
#define USB_VENDOR_ID_QUANTA 0x0408
@@ -1471,6 +1482,10 @@
#define USB_VENDOR_ID_VTL 0x0306
#define USB_DEVICE_ID_VTL_MULTITOUCH_FF3F 0xff3f
+#define USB_VENDOR_ID_VXE 0x3554
+#define USB_DEVICE_ID_VXE_DRAGONFLY_R1_PRO_DONGLE 0xf58a
+#define USB_DEVICE_ID_VXE_DRAGONFLY_R1_PRO_WIRED 0xf58c
+
#define USB_VENDOR_ID_WACOM 0x056a
#define USB_DEVICE_ID_WACOM_GRAPHIRE_BLUETOOTH 0x81
#define USB_DEVICE_ID_WACOM_INTUOS4_BLUETOOTH 0x00BD
diff --git a/drivers/hid/hid-pulsar.c b/drivers/hid/hid-pulsar.c
new file mode 100644
index 000000000000..1c6a4f485d76
--- /dev/null
+++ b/drivers/hid/hid-pulsar.c
@@ -0,0 +1,754 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID driver for pulsar mice
+ *
+ * Supported pulsar devices:
+ * - Pulsar
+ * - X2 V2
+ * - X2H
+ * - X2A
+ * - Xlite V3
+ * - Kysona
+ * -M600
+ * - ATK
+ * - VXE R1 SE+
+ * - VXE
+ * - Dragonfly R1 Pro
+ *
+ * Copyright (c) 2026 Nikolas Koesling
+ */
+
+#include <linux/hid.h>
+#include <linux/usb.h>
+#include <linux/power_supply.h>
+#include "hid-ids.h"
+
+/* ----- driver settings ----- */
+#define CMD_TIMEOUT_MSEC 100
+#define MAX_BATTERY_AGE_NS 60000000000ULL /* 60s */
+#define MAX_UNAVAIL_AGE_NS 5000000000ULL /* 5s */
+#define INIT_RETRIES 1
+#define INIT_DELAY_MSEC 1000
+
+/* ----- constants ----- */
+#define USB_INTERFACE 1
+#define USB_PAYLOAD_LEN 17
+#define CMD_HID_REPORT_ID 0x08
+#define CHECKSUM_MAGIC 0x55
+#define DEV_INFO_LEN 4
+#define CON_1K 0x00
+#define CON_4K 0x01
+#define CON_WIRED 0x02
+
+/* ----- device commands ----- */
+enum pulsar_cmd {
+ CMD_NONE = 0,
+ CMD_INFO = 0x01,
+ CMD_STATUS = 0x03,
+ CMD_POWER = 0x04,
+ CMD_EVENT = 0x0a, /* recv only */
+};
+
+#define EVENT_PWR 0x40 /* power status change */
+#define EVENT_PWR_CHK 0xf9
+
+/* ----- device types ----- */
+enum dev_type {
+ TYPE_UNKNOWN,
+ TYPE_PULSAR,
+ TYPE_KYSONA,
+ TYPE_ATK,
+ TYPE_VXE,
+};
+
+/* ----- structs ----- */
+struct pulsar_battery {
+ struct power_supply *ps;
+ struct power_supply_desc desc;
+ char name[48];
+ char model[32];
+ u8 level; /* percent */
+ u16 voltage; /* millivolts */
+ bool conn;
+ bool available;
+ u64 last_read;
+ u64 last_status;
+};
+
+struct pulsar_data {
+ struct hid_device *hdev;
+
+ enum dev_type type;
+
+ spinlock_t raw_event_lock; /* protects response_buf, pending_event */
+ struct mutex lock_cmd; /* serializes device command execution */
+ struct rw_semaphore lock_bat; /* protects battery state */
+
+ struct completion response_ready;
+ u8 response_buf[USB_PAYLOAD_LEN];
+ u8 pending_event;
+ struct work_struct power_uevent_work;
+ struct delayed_work init_work;
+ unsigned int init_retries;
+ atomic_t device_verified;
+ atomic_t stopping;
+
+ struct pulsar_battery battery;
+};
+
+static u8 calc_checksum(const u8 *data, size_t len)
+{
+ u8 sum = 0;
+
+ for (size_t i = 0; i < len - 1; i++)
+ sum += data[i];
+
+ return (u8)CHECKSUM_MAGIC - sum;
+}
+
+static int send_cmd(struct hid_device *hdev, const u8 *buf, size_t len)
+{
+ int ret;
+ u8 *dmabuf;
+
+ hid_dbg(hdev, "send command: %*ph\n", (int)len, buf);
+
+ dmabuf = kmemdup(buf, len, GFP_KERNEL);
+ if (!dmabuf)
+ return -ENOMEM;
+
+ /* device listens only to control transfers */
+ ret = hid_hw_raw_request(hdev, dmabuf[0], dmabuf, len,
+ HID_OUTPUT_REPORT, HID_REQ_SET_REPORT);
+
+ kfree(dmabuf);
+
+ if (ret < 0)
+ return ret;
+ if (ret != len)
+ return -EIO;
+
+ return 0;
+}
+
+static int exec_cmd(struct pulsar_data *drvdata, const u8 *payload,
+ u8 *response, unsigned int timeout_msec)
+{
+ struct hid_device *hdev = drvdata->hdev;
+ unsigned long flags;
+ int ret;
+ unsigned long timeout;
+ u8 checksum;
+
+ if (atomic_read(&drvdata->stopping))
+ return -ENODEV;
+
+ mutex_lock(&drvdata->lock_cmd);
+
+ if (atomic_read(&drvdata->stopping)) {
+ ret = -ENODEV;
+ goto out;
+ }
+
+ spin_lock_irqsave(&drvdata->raw_event_lock, flags);
+ reinit_completion(&drvdata->response_ready);
+ drvdata->pending_event = payload[1];
+ spin_unlock_irqrestore(&drvdata->raw_event_lock, flags);
+
+ ret = send_cmd(hdev, payload, USB_PAYLOAD_LEN);
+
+ if (ret < 0) {
+ spin_lock_irqsave(&drvdata->raw_event_lock, flags);
+ drvdata->pending_event = CMD_NONE;
+ spin_unlock_irqrestore(&drvdata->raw_event_lock, flags);
+ hid_err(hdev, "failed to send command 0x%02x: %d\n",
+ payload[1], ret);
+ goto out;
+ }
+
+ timeout = wait_for_completion_timeout(&drvdata->response_ready,
+ msecs_to_jiffies(timeout_msec));
+
+ if (timeout == 0) {
+ spin_lock_irqsave(&drvdata->raw_event_lock, flags);
+ drvdata->pending_event = CMD_NONE;
+ spin_unlock_irqrestore(&drvdata->raw_event_lock, flags);
+ ret = -ETIMEDOUT;
+ goto out;
+ }
+
+ spin_lock_irqsave(&drvdata->raw_event_lock, flags);
+ memcpy(response, drvdata->response_buf, USB_PAYLOAD_LEN);
+ spin_unlock_irqrestore(&drvdata->raw_event_lock, flags);
+
+ /* validate checksum */
+ checksum = calc_checksum(response, USB_PAYLOAD_LEN);
+
+ if (response[USB_PAYLOAD_LEN - 1] != checksum) {
+ hid_err(hdev,
+ "invalid checksum in response: 0x%02x (expected 0x%02x)\n",
+ response[USB_PAYLOAD_LEN - 1], checksum);
+ ret = -EIO;
+ goto out;
+ }
+
+ ret = 0;
+out:
+ mutex_unlock(&drvdata->lock_cmd);
+ return ret;
+}
+
+static inline void finalize_payload(u8 *payload, u8 cmd)
+{
+ payload[0] = CMD_HID_REPORT_ID;
+ payload[1] = cmd;
+ payload[USB_PAYLOAD_LEN - 1] = calc_checksum(payload, USB_PAYLOAD_LEN);
+}
+
+static int read_status(struct pulsar_data *drvdata)
+{
+ int ret;
+ u8 payload[USB_PAYLOAD_LEN] = { 0 };
+ u8 response[USB_PAYLOAD_LEN];
+
+ finalize_payload(payload, CMD_STATUS);
+
+ ret = exec_cmd(drvdata, payload, response, CMD_TIMEOUT_MSEC);
+ if (ret < 0)
+ return ret;
+ if (response[6] > 0x01)
+ return -EIO;
+
+ return (int)response[6]; /* 1: available, 0: not available */
+}
+
+static int read_device_info(struct pulsar_data *drvdata, u8 *data)
+{
+ int ret;
+ u8 payload[USB_PAYLOAD_LEN] = { 0 };
+ u8 response[USB_PAYLOAD_LEN];
+
+ payload[5] = DEV_INFO_LEN * 2;
+ get_random_bytes(payload + 6, DEV_INFO_LEN);
+ finalize_payload(payload, CMD_INFO);
+
+ ret = exec_cmd(drvdata, payload, response, CMD_TIMEOUT_MSEC);
+ if (ret < 0)
+ return ret;
+
+ if (data)
+ memcpy(data, response + 6 + DEV_INFO_LEN, DEV_INFO_LEN);
+
+ response[8 + DEV_INFO_LEN] = 0;
+ response[9 + DEV_INFO_LEN] = 0;
+
+ /*
+ * Verify challenge-response. Response layout from offset 6:
+ * [0..3] encoded response [4..7] device info (ID + conn type)
+ *
+ * resp[i] = challenge[i] * (i+1) + challenge[(i+1) % 4] + device_id[i]
+ *
+ * bytes 6..7 are zeroed for verification.
+ */
+ for (int i = 0; i < DEV_INFO_LEN; i++) {
+ u8 expect = response[6 + DEV_INFO_LEN + i];
+ u8 actual = response[6 + i] - (i + 1) * payload[6 + i] -
+ payload[6 + (i + 1) % DEV_INFO_LEN];
+
+ if (expect != actual) {
+ hid_warn(drvdata->hdev,
+ "device info[%d] mismatch: %02x != %02x\n",
+ i, expect, actual);
+ return -EIO;
+ }
+ }
+
+ return 0;
+}
+
+static int read_power(struct pulsar_data *drvdata)
+{
+ u64 now;
+ bool need_status, need_power;
+ int ret = 0;
+ u8 payload[USB_PAYLOAD_LEN] = { 0 };
+ u8 response[USB_PAYLOAD_LEN];
+ struct pulsar_battery *battery = &drvdata->battery;
+
+ now = ktime_get_ns();
+
+ down_write(&drvdata->lock_bat);
+
+ need_status = (now - battery->last_status >= MAX_UNAVAIL_AGE_NS);
+ need_power = battery->available &&
+ (now - battery->last_read >= MAX_BATTERY_AGE_NS);
+
+ if (!need_status && !need_power)
+ goto unlock;
+
+ if (need_status) {
+ ret = read_status(drvdata);
+ if (ret < 0) {
+ hid_err(drvdata->hdev,
+ "%s: failed to read status: %d\n",
+ __func__, ret);
+ goto unlock;
+ }
+
+ battery->last_status = now;
+
+ if (!ret) {
+ battery->available = false;
+ goto unlock;
+ }
+
+ /* device just became available, force power read */
+ if (!battery->available)
+ need_power = true;
+ }
+
+ if (!need_power)
+ goto unlock;
+
+ finalize_payload(payload, CMD_POWER);
+
+ ret = exec_cmd(drvdata, payload, response, CMD_TIMEOUT_MSEC);
+ if (ret < 0) {
+ hid_err(drvdata->hdev, "%s: failed to read power: %d\n",
+ __func__, ret);
+ goto unlock;
+ }
+
+ if (response[6] > 100 || response[7] > 0x01) {
+ ret = -EIO;
+ goto unlock;
+ }
+
+ battery->available = true;
+ battery->level = response[6];
+ battery->conn = response[7] == 1;
+ battery->voltage = (response[8] << 8) | response[9];
+ battery->last_read = now;
+
+ hid_dbg(drvdata->hdev, "%s: level=%d, conn=%d, voltage=%d\n",
+ __func__, battery->level, battery->conn, battery->voltage);
+
+unlock:
+ up_write(&drvdata->lock_bat);
+ return ret;
+}
+
+static int battery_get_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct pulsar_data *drvdata;
+ int ret;
+
+ drvdata = power_supply_get_drvdata(psy);
+
+ ret = read_power(drvdata);
+ if (ret)
+ return ret;
+
+ down_read(&drvdata->lock_bat);
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_STATUS:
+ if (!drvdata->battery.available)
+ val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
+ else if (drvdata->battery.conn && drvdata->battery.level < 100)
+ val->intval = POWER_SUPPLY_STATUS_CHARGING;
+ else if (drvdata->battery.conn && drvdata->battery.level >= 100)
+ val->intval = POWER_SUPPLY_STATUS_FULL;
+ else
+ val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
+ break;
+ case POWER_SUPPLY_PROP_CAPACITY:
+ val->intval = drvdata->battery.level;
+ break;
+ case POWER_SUPPLY_PROP_VOLTAGE_NOW:
+ val->intval = drvdata->battery.voltage * 1000;
+ break;
+ case POWER_SUPPLY_PROP_PRESENT:
+ case POWER_SUPPLY_PROP_ONLINE:
+ val->intval = drvdata->battery.available;
+ break;
+ case POWER_SUPPLY_PROP_SCOPE:
+ val->intval = POWER_SUPPLY_SCOPE_DEVICE;
+ break;
+ case POWER_SUPPLY_PROP_MODEL_NAME:
+ val->strval = drvdata->battery.model;
+ break;
+ default:
+ ret = -EINVAL;
+ }
+
+ up_read(&drvdata->lock_bat);
+ return ret;
+}
+
+static void power_uevent_work_handler(struct work_struct *work)
+{
+ struct pulsar_data *drvdata;
+ int ret;
+
+ drvdata = container_of(work, struct pulsar_data, power_uevent_work);
+
+ if (atomic_read(&drvdata->stopping))
+ return;
+
+ down_write(&drvdata->lock_bat);
+ drvdata->battery.last_read = 0;
+ drvdata->battery.last_status = 0;
+ up_write(&drvdata->lock_bat);
+
+ ret = read_power(drvdata);
+ if (ret < 0) {
+ hid_err(drvdata->hdev, "%s: failed to read power: %d\n",
+ __func__, ret);
+ return;
+ }
+
+ power_supply_changed(drvdata->battery.ps);
+}
+
+static int pulsar_raw_event(struct hid_device *hdev,
+ struct hid_report *report, u8 *data, int size)
+{
+ struct pulsar_data *drvdata;
+
+ drvdata = hid_get_drvdata(hdev);
+ if (!drvdata)
+ return 0;
+
+ hid_dbg(hdev, "received raw event: %*ph\n", size, data);
+
+ if (size != USB_PAYLOAD_LEN || data[0] != CMD_HID_REPORT_ID)
+ return 0;
+
+ if (data[1] != CMD_EVENT) {
+ spin_lock(&drvdata->raw_event_lock);
+ if (drvdata->pending_event != data[1]) {
+ spin_unlock(&drvdata->raw_event_lock);
+ return 0;
+ }
+ memcpy(drvdata->response_buf, data, size);
+ drvdata->pending_event = CMD_NONE;
+ complete(&drvdata->response_ready);
+ spin_unlock(&drvdata->raw_event_lock);
+ return 1;
+ }
+
+ if (!atomic_read(&drvdata->device_verified))
+ return 0;
+
+ if (data[6] == EVENT_PWR && data[USB_PAYLOAD_LEN - 1] == EVENT_PWR_CHK) {
+ schedule_work(&drvdata->power_uevent_work);
+ hid_dbg(hdev, "received power event\n");
+ return 1;
+ }
+
+ return 0;
+}
+
+static const enum power_supply_property pulsar_battery_props[] = {
+ POWER_SUPPLY_PROP_STATUS, POWER_SUPPLY_PROP_CAPACITY,
+ POWER_SUPPLY_PROP_VOLTAGE_NOW, POWER_SUPPLY_PROP_ONLINE,
+ POWER_SUPPLY_PROP_MODEL_NAME, POWER_SUPPLY_PROP_SCOPE,
+ POWER_SUPPLY_PROP_PRESENT
+};
+
+static void init_power_supply_desc(struct pulsar_data *drvdata)
+{
+ drvdata->battery.desc.name = drvdata->battery.name;
+ drvdata->battery.desc.type = POWER_SUPPLY_TYPE_BATTERY;
+ drvdata->battery.desc.properties = pulsar_battery_props;
+ drvdata->battery.desc.num_properties = ARRAY_SIZE(pulsar_battery_props);
+ drvdata->battery.desc.get_property = battery_get_property;
+}
+
+static void model_pulsar(u8 *device_id, struct pulsar_data *drvdata)
+{
+ u16 model_id;
+ const char *con_type = "unknown";
+
+ model_id = device_id[0] << 8 | device_id[1];
+
+ switch (device_id[2]) {
+ case CON_1K:
+ con_type = "1kHz";
+ break;
+ case CON_4K:
+ con_type = "4kHz";
+ break;
+ case CON_WIRED:
+ con_type = "wired";
+ break;
+ }
+
+ switch (model_id) {
+ case 0x060a:
+ case 0x060b:
+ case 0x0612:
+ case 0x0613:
+ case 0x0614:
+ case 0x0615:
+ snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
+ "Pulsar X2 V2 (%s)", con_type);
+ break;
+ case 0x060c:
+ case 0x060d:
+ snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
+ "Pulsar X2H (%s)", con_type);
+ break;
+ case 0x0607:
+ case 0x060e:
+ case 0x060f:
+ case 0x0610:
+ case 0x0611:
+ snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
+ "Pulsar Xlite V3 (%s)", con_type);
+ break;
+ case 0x0608:
+ case 0x0609:
+ snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
+ "Pulsar X2A (%s)", con_type);
+ break;
+ default:
+ snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
+ "Pulsar unknown (%s)", con_type);
+ }
+}
+
+static void model_atk(u8 *device_id, struct pulsar_data *drvdata)
+{
+ u16 model_id;
+ const char *con_type = "unknown";
+
+ model_id = device_id[0] << 8 | device_id[1];
+
+ switch (device_id[2]) {
+ case CON_1K:
+ con_type = "1kHz";
+ break;
+ case CON_4K:
+ con_type = "4kHz";
+ break;
+ case CON_WIRED:
+ con_type = "wired";
+ break;
+ }
+
+ switch (model_id) {
+ case 0x0220:
+ snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
+ "ATK VXE R1 SE+ (%s)", con_type);
+ break;
+ default:
+ snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
+ "Unknown ATK (%s)", con_type);
+ }
+}
+
+static void pulsar_init_work(struct work_struct *work)
+{
+ struct pulsar_data *drvdata;
+ struct hid_device *hdev;
+ struct power_supply_config psy_cfg;
+ int ret;
+ u8 data[DEV_INFO_LEN];
+
+ drvdata = container_of(work, struct pulsar_data, init_work.work);
+ hdev = drvdata->hdev;
+
+ ret = read_device_info(drvdata, data);
+ if (ret == -ETIMEDOUT) {
+ if (drvdata->init_retries--) {
+ hid_dbg(hdev,
+ "device info read timed out, retrying (%u left)\n",
+ drvdata->init_retries);
+ schedule_delayed_work(&drvdata->init_work,
+ msecs_to_jiffies
+ (INIT_DELAY_MSEC));
+ return;
+ }
+ hid_err(hdev, "device info read timed out, giving up\n");
+ return;
+ }
+ if (ret < 0) {
+ if (drvdata->type == TYPE_PULSAR) {
+ hid_err(hdev, "failed to read device info: %d\n", ret);
+ return;
+ }
+ hid_dbg(hdev, "failed to read device info: %d\n", ret);
+ snprintf(drvdata->battery.model,
+ sizeof(drvdata->battery.model), "%s", hdev->name);
+ goto register_battery;
+ }
+
+ hid_dbg(hdev, "device info: %*ph (%d)\n", DEV_INFO_LEN, data, ret);
+
+ switch (drvdata->type) {
+ case TYPE_PULSAR:
+ model_pulsar(data, drvdata);
+ break;
+ case TYPE_ATK:
+ model_atk(data, drvdata);
+ break;
+ default:
+ snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
+ "%s", hdev->name);
+ }
+
+register_battery:
+ init_power_supply_desc(drvdata);
+
+ psy_cfg = (struct power_supply_config) {.drv_data = drvdata };
+ drvdata->battery.ps =
+ devm_power_supply_register(&hdev->dev, &drvdata->battery.desc,
+ &psy_cfg);
+ if (IS_ERR(drvdata->battery.ps)) {
+ hid_err(hdev, "failed to register battery: %ld\n",
+ PTR_ERR(drvdata->battery.ps));
+ drvdata->battery.ps = NULL;
+ return;
+ }
+
+ atomic_set(&drvdata->device_verified, 1);
+ hid_info(hdev, "device verified, battery registered\n");
+}
+
+static int pulsar_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+ int ret;
+ struct usb_interface *intf;
+ struct usb_device *usbdev;
+ struct pulsar_data *drvdata;
+ struct hid_report *report_in;
+ struct hid_report *report_out;
+
+ if (!hid_is_usb(hdev))
+ return -ENODEV;
+
+ ret = hid_parse(hdev);
+ if (ret < 0) {
+ hid_err(hdev, "hid_parse failed: %d\n", ret);
+ return ret;
+ }
+
+ intf = to_usb_interface(hdev->dev.parent);
+ report_in =
+ hdev->report_enum[HID_INPUT_REPORT].report_id_hash[CMD_HID_REPORT_ID];
+ report_out =
+ hdev->report_enum[HID_OUTPUT_REPORT].report_id_hash[CMD_HID_REPORT_ID];
+
+ if (!report_in || !report_out ||
+ hid_report_len(report_in) != USB_PAYLOAD_LEN ||
+ hid_report_len(report_out) != USB_PAYLOAD_LEN ||
+ intf->cur_altsetting->desc.bInterfaceNumber != USB_INTERFACE)
+ return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+
+ drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL);
+ if (!drvdata)
+ return -ENOMEM;
+
+ drvdata->hdev = hdev;
+ drvdata->type = id->driver_data;
+
+ mutex_init(&drvdata->lock_cmd);
+ init_rwsem(&drvdata->lock_bat);
+
+ usbdev = interface_to_usbdev(intf);
+
+ spin_lock_init(&drvdata->raw_event_lock);
+ hid_set_drvdata(hdev, drvdata);
+ init_completion(&drvdata->response_ready);
+ INIT_WORK(&drvdata->power_uevent_work, power_uevent_work_handler);
+ INIT_DELAYED_WORK(&drvdata->init_work, pulsar_init_work);
+ drvdata->init_retries = INIT_RETRIES;
+
+ snprintf(drvdata->battery.name, sizeof(drvdata->battery.name),
+ "pulsar_%s_battery", usbdev->devpath);
+
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret < 0) {
+ hid_err(hdev, "hw start failed\n");
+ return ret;
+ }
+
+ ret = hid_hw_open(hdev);
+ if (ret < 0) {
+ hid_err(hdev, "hw open failed\n");
+ goto err_open;
+ }
+
+ schedule_delayed_work(&drvdata->init_work, 0);
+
+ return 0;
+
+err_open:
+ cancel_work_sync(&drvdata->power_uevent_work);
+ hid_hw_stop(hdev);
+ return ret;
+}
+
+static void pulsar_remove(struct hid_device *hdev)
+{
+ struct pulsar_data *drvdata;
+
+ drvdata = hid_get_drvdata(hdev);
+ if (!drvdata) {
+ hid_hw_stop(hdev);
+ return;
+ }
+
+ atomic_set(&drvdata->stopping, 1);
+ cancel_delayed_work_sync(&drvdata->init_work);
+ cancel_work_sync(&drvdata->power_uevent_work);
+
+ /* wait for active device i/o (exec_cmd) */
+ mutex_lock(&drvdata->lock_cmd);
+ hid_hw_close(hdev);
+ mutex_unlock(&drvdata->lock_cmd);
+
+ hid_hw_stop(hdev);
+ mutex_destroy(&drvdata->lock_cmd);
+}
+
+static const struct hid_device_id pulsar_table[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_PULSAR, USB_DEVICE_ID_PULSAR_WIRED),
+ .driver_data = TYPE_PULSAR },
+ { HID_USB_DEVICE(USB_VENDOR_ID_PULSAR, USB_DEVICE_ID_PULSAR_1KHZ),
+ .driver_data = TYPE_PULSAR },
+ { HID_USB_DEVICE(USB_VENDOR_ID_PULSAR, USB_DEVICE_ID_PULSAR_4KHZ),
+ .driver_data = TYPE_PULSAR },
+ { HID_USB_DEVICE(USB_VENDOR_ID_KYSONA, USB_DEVICE_ID_KYSONA_M600_DONGLE),
+ .driver_data = TYPE_KYSONA },
+ { HID_USB_DEVICE(USB_VENDOR_ID_KYSONA, USB_DEVICE_ID_KYSONA_M600_WIRED),
+ .driver_data = TYPE_KYSONA },
+ { HID_USB_DEVICE(USB_VENDOR_ID_ATK, USB_DEVICE_ID_ATK_VXE_R1_SE_DONGLE),
+ .driver_data = TYPE_ATK },
+ { HID_USB_DEVICE(USB_VENDOR_ID_ATK_ALT, USB_DEVICE_ID_ATK_VXE_R1_SE_WIRED),
+ .driver_data = TYPE_ATK },
+ { HID_USB_DEVICE(USB_VENDOR_ID_VXE, USB_DEVICE_ID_VXE_DRAGONFLY_R1_PRO_DONGLE),
+ .driver_data = TYPE_VXE },
+ { HID_USB_DEVICE(USB_VENDOR_ID_VXE, USB_DEVICE_ID_VXE_DRAGONFLY_R1_PRO_WIRED),
+ .driver_data = TYPE_VXE },
+ { }
+};
+
+static struct hid_driver pulsar_driver = {
+ .name = "pulsar",
+ .id_table = pulsar_table,
+ .probe = pulsar_probe,
+ .remove = pulsar_remove,
+ .raw_event = pulsar_raw_event,
+};
+
+module_hid_driver(pulsar_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("HID driver for pulsar mice");
+MODULE_AUTHOR("Nikolas Koesling");
+MODULE_DEVICE_TABLE(hid, pulsar_table);
--
2.53.0
^ permalink raw reply related
* Re: [PATCH v2] Input: usbtouchscreen - refactor endpoint lookup
From: Dmitry Torokhov @ 2026-04-01 19:29 UTC (permalink / raw)
To: Johan Hovold; +Cc: linux-input, linux-kernel
In-Reply-To: <20260401082212.2180434-1-johan@kernel.org>
On Wed, Apr 01, 2026 at 10:22:12AM +0200, Johan Hovold wrote:
> Use the common USB helpers for looking up bulk and interrupt endpoints
> (and determining endpoint numbers) instead of open coding.
>
> Note that the NEXIO data interface has two bulk endpoints (see commit
> 5197424cdccc ("Input: usbtouchscreen - add NEXIO (or iNexio) support")
> for the descriptors).
>
> The lookup in probe handles both bulk-in and interrupt-in endpoints and
> was added to handle NEXIO devices. Replace the open coded lookup with a
> lookup for the common interrupt endpoint and an explicit fallback
> accepting a bulk endpoint.
>
> This iterates over the (two) endpoints twice for NEXIO devices but makes
> it more clear what is going on.
>
> Signed-off-by: Johan Hovold <johan@kernel.org>
Applied, thank you.
--
Dmitry
^ permalink raw reply
* Re: [PATCH v2 00/11] Add spi-hid transport driver
From: Jingyuan Liang @ 2026-04-02 1:54 UTC (permalink / raw)
To: Krzysztof Kozlowski
Cc: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
linux-input, linux-doc, linux-kernel, linux-spi,
linux-trace-kernel, devicetree, hbarnor, tfiga, Jarrett Schultz,
Dmitry Antipov, Angela Czubak
In-Reply-To: <20260325-naughty-hungry-wapiti-658e83@quoll>
On Wed, Mar 25, 2026 at 1:49 AM Krzysztof Kozlowski <krzk@kernel.org> wrote:
>
> On Tue, Mar 24, 2026 at 06:39:33AM +0000, Jingyuan Liang wrote:
> > This series picks up the spi-hid driver work originally started by
> > Microsoft. The patch breakdown has been modified and the implementation
> > has been refactored to address upstream feedback and testing issues. We
> > are submitting this as a new series while keeping the original sign-off
> > chain to reflect the history.
> >
> > Same as the original series, there is a change to HID documentation, some
> > HID core changes to support a SPI device, the SPI HID transport driver,
> > and HID over SPI Device Tree binding. We have added the HID over SPI ACPI
> > support, power management, panel follower, and quirks for Ilitek touch
> > controllers.
> >
> > Original authors: Jarrett Schultz <jaschultz@microsoft.com>,
> > Dmitry Antipov <dmanti@microsoft.com>
> > Link: https://lore.kernel.org/r/86b63b7b-afda-d7f4-7bfa-175085d5a8ef@gmail.com
> >
> > Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
> > ---
> > Changes in v2:
> > - Fix style problems and remove unnecessary fields from the DT binding file
>
> Style and removal? So other comments were skipped?
>
> Please write detailed changelogs, otherwise it feels you just ignore
> parts of the feedback.
>
> Best regards,
> Krzysztof
>
Comments are either resolved or awaiting further confirmation. I will
add more details
to v2 changelog in v3.
^ permalink raw reply
* [PATCH v3 00/11] Add spi-hid transport driver
From: Jingyuan Liang @ 2026-04-02 1:59 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-input, linux-doc, linux-kernel, linux-spi,
linux-trace-kernel, devicetree, hbarnor, tfiga, Jingyuan Liang,
Jarrett Schultz, Dmitry Antipov, Angela Czubak
This series picks up the spi-hid driver work originally started by
Microsoft. The patch breakdown has been modified and the implementation
has been refactored to address upstream feedback and testing issues. We
are submitting this as a new series while keeping the original sign-off
chain to reflect the history.
Same as the original series, there is a change to HID documentation, some
HID core changes to support a SPI device, the SPI HID transport driver,
and HID over SPI Device Tree binding. We have added the HID over SPI ACPI
support, power management, panel follower, and quirks for Ilitek touch
controllers.
Original authors: Jarrett Schultz <jaschultz@microsoft.com>,
Dmitry Antipov <dmanti@microsoft.com>
Link: https://lore.kernel.org/r/86b63b7b-afda-d7f4-7bfa-175085d5a8ef@gmail.com
Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
---
Changes in v3:
- Add io_lock init
- Relocate tracepoints to drivers/hid/spi-hid/ and fix tracepoint macros
- Add tracepoints for sync, error handling, reset, and report processing
- Clean up internal includes and fix Makefile CFLAGS
- Add more details in v2 changelog
- Link to v2: https://lore.kernel.org/r/20260324-send-upstream-v2-0-521ce8afff86@chromium.org
Changes in v2:
- Clean up DT bindings: fix formatting and remove timing and flags properties
- Update DT binding example: use a device-specific compatible and drop
reset_assert
- Simplify ACPI/OF match tables by removing ACPI_PTR/of_match_ptr
- Refactor OF driver to use match data for timing parameters instead
of DT properties
- Switch to fsleep() for delays in ACPI and OF drivers
- Drop patch 12 as it is vendor specific
- Add a lock to fix input/output concurrency race
- Link to v1: https://lore.kernel.org/r/20260303-send-upstream-v1-0-1515ba218f3d@chromium.org
---
Angela Czubak (2):
HID: spi-hid: add transport driver skeleton for HID over SPI bus
HID: spi_hid: add ACPI support for SPI over HID
Jarrett Schultz (3):
Documentation: Correction in HID output_report callback description.
HID: Add BUS_SPI support and define HID_SPI_DEVICE macro
HID: spi_hid: add device tree support for SPI over HID
Jingyuan Liang (6):
HID: spi-hid: add spi-hid driver HID layer
HID: spi-hid: add HID SPI protocol implementation
HID: spi_hid: add spi_hid traces
dt-bindings: input: Document hid-over-spi DT schema
HID: spi-hid: add power management implementation
HID: spi-hid: add panel follower support
.../devicetree/bindings/input/hid-over-spi.yaml | 126 ++
Documentation/hid/hid-transport.rst | 4 +-
drivers/hid/Kconfig | 2 +
drivers/hid/Makefile | 2 +
drivers/hid/hid-core.c | 3 +
drivers/hid/spi-hid/Kconfig | 45 +
drivers/hid/spi-hid/Makefile | 12 +
drivers/hid/spi-hid/spi-hid-acpi.c | 254 ++++
drivers/hid/spi-hid/spi-hid-core.c | 1456 ++++++++++++++++++++
drivers/hid/spi-hid/spi-hid-core.h | 98 ++
drivers/hid/spi-hid/spi-hid-of.c | 244 ++++
drivers/hid/spi-hid/spi-hid-trace.h | 169 +++
drivers/hid/spi-hid/spi-hid.h | 46 +
include/linux/hid.h | 2 +
14 files changed, 2461 insertions(+), 2 deletions(-)
---
base-commit: 05f7e89ab9731565d8a62e3b5d1ec206485eeb0b
change-id: 20260212-send-upstream-75f6fd9ed92e
Best regards,
--
Jingyuan Liang <jingyliang@chromium.org>
^ permalink raw reply
* [PATCH v3 01/11] Documentation: Correction in HID output_report callback description.
From: Jingyuan Liang @ 2026-04-02 1:59 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-input, linux-doc, linux-kernel, linux-spi,
linux-trace-kernel, devicetree, hbarnor, tfiga, Jingyuan Liang,
Jarrett Schultz, Dmitry Antipov
In-Reply-To: <20260402-send-upstream-v3-0-6091c458d357@chromium.org>
From: Jarrett Schultz <jaschultz@microsoft.com>
Originally output_report callback was described as must-be asynchronous,
but that is not the case in some implementations, namely i2c-hid.
Correct the documentation to say that it may be asynchronous.
Signed-off-by: Dmitry Antipov <dmanti@microsoft.com>
Reviewed-by: Dmitry Torokhov <dmitry.torokhov@gmail.com>
Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
---
Documentation/hid/hid-transport.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Documentation/hid/hid-transport.rst b/Documentation/hid/hid-transport.rst
index 6f1692da296c..2008cf432af1 100644
--- a/Documentation/hid/hid-transport.rst
+++ b/Documentation/hid/hid-transport.rst
@@ -327,8 +327,8 @@ The available HID callbacks are:
Send raw output report via intr channel. Used by some HID device drivers
which require high throughput for outgoing requests on the intr channel. This
- must not cause SET_REPORT calls! This must be implemented as asynchronous
- output report on the intr channel!
+ must not cause SET_REPORT calls! This call might be asynchronous, so the
+ caller should not expect an immediate response!
::
--
2.53.0.1185.g05d4b7b318-goog
^ permalink raw reply related
* [PATCH v3 02/11] HID: Add BUS_SPI support and define HID_SPI_DEVICE macro
From: Jingyuan Liang @ 2026-04-02 1:59 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-input, linux-doc, linux-kernel, linux-spi,
linux-trace-kernel, devicetree, hbarnor, tfiga, Jingyuan Liang,
Jarrett Schultz, Dmitry Antipov
In-Reply-To: <20260402-send-upstream-v3-0-6091c458d357@chromium.org>
From: Jarrett Schultz <jaschultz@microsoft.com>
If connecting a hid_device with bus field indicating BUS_SPI print out
"SPI" in the debug print.
Macro sets the bus field to BUS_SPI and uses arguments to set vendor
product fields.
Signed-off-by: Dmitry Antipov <dmanti@microsoft.com>
Reviewed-by: Dmitry Torokhov <dmitry.torokhov@gmail.com>
Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
---
drivers/hid/hid-core.c | 3 +++
include/linux/hid.h | 2 ++
2 files changed, 5 insertions(+)
diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c
index a5b3a8ca2fcb..813c9c743ccd 100644
--- a/drivers/hid/hid-core.c
+++ b/drivers/hid/hid-core.c
@@ -2316,6 +2316,9 @@ int hid_connect(struct hid_device *hdev, unsigned int connect_mask)
case BUS_I2C:
bus = "I2C";
break;
+ case BUS_SPI:
+ bus = "SPI";
+ break;
case BUS_SDW:
bus = "SOUNDWIRE";
break;
diff --git a/include/linux/hid.h b/include/linux/hid.h
index dce862cafbbd..957f322a0ebd 100644
--- a/include/linux/hid.h
+++ b/include/linux/hid.h
@@ -786,6 +786,8 @@ struct hid_descriptor {
.bus = BUS_BLUETOOTH, .vendor = (ven), .product = (prod)
#define HID_I2C_DEVICE(ven, prod) \
.bus = BUS_I2C, .vendor = (ven), .product = (prod)
+#define HID_SPI_DEVICE(ven, prod) \
+ .bus = BUS_SPI, .vendor = (ven), .product = (prod)
#define HID_REPORT_ID(rep) \
.report_type = (rep)
--
2.53.0.1185.g05d4b7b318-goog
^ permalink raw reply related
* [PATCH v3 03/11] HID: spi-hid: add transport driver skeleton for HID over SPI bus
From: Jingyuan Liang @ 2026-04-02 1:59 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-input, linux-doc, linux-kernel, linux-spi,
linux-trace-kernel, devicetree, hbarnor, tfiga, Jingyuan Liang,
Angela Czubak, Dmitry Antipov
In-Reply-To: <20260402-send-upstream-v3-0-6091c458d357@chromium.org>
From: Angela Czubak <acz@semihalf.com>
Create spi-hid folder and add Kconfig and Makefile for spi-hid driver.
Add basic device structure, definitions, and probe/remove functions.
Signed-off-by: Dmitry Antipov <dmanti@microsoft.com>
Signed-off-by: Angela Czubak <acz@semihalf.com>
Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
---
drivers/hid/Kconfig | 2 +
drivers/hid/Makefile | 2 +
drivers/hid/spi-hid/Kconfig | 15 +++
drivers/hid/spi-hid/Makefile | 9 ++
drivers/hid/spi-hid/spi-hid-core.c | 213 +++++++++++++++++++++++++++++++++++++
5 files changed, 241 insertions(+)
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 920a64b66b25..c6ae23bfb75d 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1434,6 +1434,8 @@ source "drivers/hid/bpf/Kconfig"
source "drivers/hid/i2c-hid/Kconfig"
+source "drivers/hid/spi-hid/Kconfig"
+
source "drivers/hid/intel-ish-hid/Kconfig"
source "drivers/hid/amd-sfh-hid/Kconfig"
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 361a7daedeb8..6b43e789b39a 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -169,6 +169,8 @@ obj-$(CONFIG_USB_KBD) += usbhid/
obj-$(CONFIG_I2C_HID_CORE) += i2c-hid/
+obj-$(CONFIG_SPI_HID_CORE) += spi-hid/
+
obj-$(CONFIG_INTEL_ISH_HID) += intel-ish-hid/
obj-$(CONFIG_AMD_SFH_HID) += amd-sfh-hid/
diff --git a/drivers/hid/spi-hid/Kconfig b/drivers/hid/spi-hid/Kconfig
new file mode 100644
index 000000000000..836fdefe8345
--- /dev/null
+++ b/drivers/hid/spi-hid/Kconfig
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (c) 2021 Microsoft Corporation
+#
+
+menuconfig SPI_HID
+ tristate "SPI HID support"
+ default y
+ depends on SPI
+
+if SPI_HID
+
+config SPI_HID_CORE
+ tristate
+endif
diff --git a/drivers/hid/spi-hid/Makefile b/drivers/hid/spi-hid/Makefile
new file mode 100644
index 000000000000..92e24cddbfc2
--- /dev/null
+++ b/drivers/hid/spi-hid/Makefile
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Makefile for the SPI HID input drivers
+#
+# Copyright (c) 2021 Microsoft Corporation
+#
+
+obj-$(CONFIG_SPI_HID_CORE) += spi-hid.o
+spi-hid-objs = spi-hid-core.o
diff --git a/drivers/hid/spi-hid/spi-hid-core.c b/drivers/hid/spi-hid/spi-hid-core.c
new file mode 100644
index 000000000000..d7b4d4adad95
--- /dev/null
+++ b/drivers/hid/spi-hid/spi-hid-core.c
@@ -0,0 +1,213 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * HID over SPI protocol implementation
+ *
+ * Copyright (c) 2021 Microsoft Corporation
+ * Copyright (c) 2026 Google LLC
+ *
+ * This code is partly based on "HID over I2C protocol implementation:
+ *
+ * Copyright (c) 2012 Benjamin Tissoires <benjamin.tissoires@gmail.com>
+ * Copyright (c) 2012 Ecole Nationale de l'Aviation Civile, France
+ * Copyright (c) 2012 Red Hat, Inc
+ *
+ * which in turn is partly based on "USB HID support for Linux":
+ *
+ * Copyright (c) 1999 Andreas Gal
+ * Copyright (c) 2000-2005 Vojtech Pavlik <vojtech@suse.cz>
+ * Copyright (c) 2005 Michael Haboustak <mike-@cinci.rr.com> for Concept2, Inc
+ * Copyright (c) 2007-2008 Oliver Neukum
+ * Copyright (c) 2006-2010 Jiri Kosina
+ */
+
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/hid-over-spi.h>
+#include <linux/interrupt.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/spi/spi.h>
+
+/* struct spi_hid_conf - Conf provided to the core */
+struct spi_hid_conf {
+ u32 input_report_header_address;
+ u32 input_report_body_address;
+ u32 output_report_address;
+ u8 read_opcode;
+ u8 write_opcode;
+};
+
+/**
+ * struct spihid_ops - Ops provided to the core
+ * @power_up: do sequencing to power up the device
+ * @power_down: do sequencing to power down the device
+ * @assert_reset: do sequencing to assert the reset line
+ * @deassert_reset: do sequencing to deassert the reset line
+ * @sleep_minimal_reset_delay: minimal sleep delay during reset
+ */
+struct spihid_ops {
+ int (*power_up)(struct spihid_ops *ops);
+ int (*power_down)(struct spihid_ops *ops);
+ int (*assert_reset)(struct spihid_ops *ops);
+ int (*deassert_reset)(struct spihid_ops *ops);
+ void (*sleep_minimal_reset_delay)(struct spihid_ops *ops);
+};
+
+/* Driver context */
+struct spi_hid {
+ struct spi_device *spi; /* spi device. */
+ struct hid_device *hid; /* pointer to corresponding HID dev. */
+
+ struct spihid_ops *ops;
+ struct spi_hid_conf *conf;
+
+ enum hidspi_power_state power_state;
+
+ u32 regulator_error_count;
+ int regulator_last_error;
+ u32 bus_error_count;
+ int bus_last_error;
+ u32 dir_count; /* device initiated reset count. */
+};
+
+static const char *spi_hid_power_mode_string(enum hidspi_power_state power_state)
+{
+ switch (power_state) {
+ case HIDSPI_ON:
+ return "d0";
+ case HIDSPI_SLEEP:
+ return "d2";
+ case HIDSPI_OFF:
+ return "d3";
+ default:
+ return "unknown";
+ }
+}
+
+static irqreturn_t spi_hid_dev_irq(int irq, void *_shid)
+{
+ return IRQ_HANDLED;
+}
+
+static ssize_t bus_error_count_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct spi_hid *shid = dev_get_drvdata(dev);
+
+ return sysfs_emit(buf, "%d (%d)\n",
+ shid->bus_error_count, shid->bus_last_error);
+}
+static DEVICE_ATTR_RO(bus_error_count);
+
+static ssize_t regulator_error_count_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct spi_hid *shid = dev_get_drvdata(dev);
+
+ return sysfs_emit(buf, "%d (%d)\n",
+ shid->regulator_error_count,
+ shid->regulator_last_error);
+}
+static DEVICE_ATTR_RO(regulator_error_count);
+
+static ssize_t device_initiated_reset_count_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct spi_hid *shid = dev_get_drvdata(dev);
+
+ return sysfs_emit(buf, "%d\n", shid->dir_count);
+}
+static DEVICE_ATTR_RO(device_initiated_reset_count);
+
+static struct attribute *spi_hid_attrs[] = {
+ &dev_attr_bus_error_count.attr,
+ &dev_attr_regulator_error_count.attr,
+ &dev_attr_device_initiated_reset_count.attr,
+ NULL /* Terminator */
+};
+
+static const struct attribute_group spi_hid_group = {
+ .attrs = spi_hid_attrs,
+};
+
+const struct attribute_group *spi_hid_groups[] = {
+ &spi_hid_group,
+ NULL
+};
+EXPORT_SYMBOL_GPL(spi_hid_groups);
+
+int spi_hid_core_probe(struct spi_device *spi, struct spihid_ops *ops,
+ struct spi_hid_conf *conf)
+{
+ struct device *dev = &spi->dev;
+ struct spi_hid *shid;
+ int error;
+
+ if (spi->irq <= 0)
+ return dev_err_probe(dev, spi->irq ?: -EINVAL, "Missing IRQ\n");
+
+ shid = devm_kzalloc(dev, sizeof(*shid), GFP_KERNEL);
+ if (!shid)
+ return -ENOMEM;
+
+ shid->spi = spi;
+ shid->power_state = HIDSPI_ON;
+ shid->ops = ops;
+ shid->conf = conf;
+
+ spi_set_drvdata(spi, shid);
+
+ /*
+ * At the end of probe we initialize the device:
+ * 0) assert reset, bias the interrupt line
+ * 1) sleep minimal reset delay
+ * 2) request IRQ
+ * 3) power up the device
+ * 4) deassert reset (high)
+ * After this we expect an IRQ with a reset response.
+ */
+
+ shid->ops->assert_reset(shid->ops);
+
+ shid->ops->sleep_minimal_reset_delay(shid->ops);
+
+ error = devm_request_threaded_irq(dev, spi->irq, NULL, spi_hid_dev_irq,
+ IRQF_ONESHOT, dev_name(&spi->dev), shid);
+ if (error) {
+ dev_err(dev, "%s: unable to request threaded IRQ.", __func__);
+ return error;
+ }
+
+ error = shid->ops->power_up(shid->ops);
+ if (error) {
+ dev_err(dev, "%s: could not power up.", __func__);
+ return error;
+ }
+
+ shid->ops->deassert_reset(shid->ops);
+
+ dev_dbg(dev, "%s: d3 -> %s.", __func__,
+ spi_hid_power_mode_string(shid->power_state));
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(spi_hid_core_probe);
+
+void spi_hid_core_remove(struct spi_device *spi)
+{
+ struct spi_hid *shid = spi_get_drvdata(spi);
+ struct device *dev = &spi->dev;
+ int error;
+
+ shid->ops->assert_reset(shid->ops);
+ error = shid->ops->power_down(shid->ops);
+ if (error)
+ dev_err(dev, "failed to disable regulator.");
+}
+EXPORT_SYMBOL_GPL(spi_hid_core_remove);
+
+MODULE_DESCRIPTION("HID over SPI transport driver");
+MODULE_AUTHOR("Dmitry Antipov <dmanti@microsoft.com>");
+MODULE_LICENSE("GPL");
--
2.53.0.1185.g05d4b7b318-goog
^ permalink raw reply related
* [PATCH v3 04/11] HID: spi-hid: add spi-hid driver HID layer
From: Jingyuan Liang @ 2026-04-02 1:59 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-input, linux-doc, linux-kernel, linux-spi,
linux-trace-kernel, devicetree, hbarnor, tfiga, Jingyuan Liang,
Dmitry Antipov, Angela Czubak
In-Reply-To: <20260402-send-upstream-v3-0-6091c458d357@chromium.org>
Add HID low level driver callbacks to register SPI as a HID driver, and
an external touch device as a HID device.
Signed-off-by: Dmitry Antipov <dmanti@microsoft.com>
Signed-off-by: Angela Czubak <acz@semihalf.com>
Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
---
drivers/hid/spi-hid/spi-hid-core.c | 519 +++++++++++++++++++++++++++++++++++++
1 file changed, 519 insertions(+)
diff --git a/drivers/hid/spi-hid/spi-hid-core.c b/drivers/hid/spi-hid/spi-hid-core.c
index d7b4d4adad95..4723b87346d4 100644
--- a/drivers/hid/spi-hid/spi-hid-core.c
+++ b/drivers/hid/spi-hid/spi-hid-core.c
@@ -20,13 +20,69 @@
* Copyright (c) 2006-2010 Jiri Kosina
*/
+#include <linux/completion.h>
+#include <linux/crc32.h>
#include <linux/device.h>
+#include <linux/err.h>
#include <linux/hid.h>
#include <linux/hid-over-spi.h>
#include <linux/interrupt.h>
+#include <linux/jiffies.h>
#include <linux/module.h>
+#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/spi/spi.h>
+#include <linux/string.h>
+#include <linux/sysfs.h>
+#include <linux/unaligned.h>
+
+#define SPI_HID_OUTPUT_REPORT_CONTENT_ID_DESC_REQUEST 0x00
+
+#define SPI_HID_RESP_TIMEOUT 1000
+
+/* Protocol message size constants */
+#define SPI_HID_OUTPUT_HEADER_LEN 8
+
+/* flags */
+/*
+ * ready flag indicates that the FW is ready to accept commands and
+ * requests. The FW becomes ready after sending the report descriptor.
+ */
+#define SPI_HID_READY 0
+
+/* Raw input buffer with data from the bus */
+struct spi_hid_input_buf {
+ u8 header[HIDSPI_INPUT_HEADER_SIZE];
+ u8 body[HIDSPI_INPUT_BODY_HEADER_SIZE];
+ u8 content[];
+};
+
+/* Raw output report buffer to be put on the bus */
+struct spi_hid_output_buf {
+ u8 header[SPI_HID_OUTPUT_HEADER_LEN];
+ u8 content[];
+};
+
+/* Data necessary to send an output report */
+struct spi_hid_output_report {
+ u8 report_type;
+ u16 content_length;
+ u8 content_id;
+ u8 *content;
+};
+
+/* Processed data from a device descriptor */
+struct spi_hid_device_descriptor {
+ u16 hid_version;
+ u16 report_descriptor_length;
+ u16 max_input_length;
+ u16 max_output_length;
+ u16 max_fragment_length;
+ u16 vendor_id;
+ u16 product_id;
+ u16 version_id;
+ u8 no_output_report_ack;
+};
/* struct spi_hid_conf - Conf provided to the core */
struct spi_hid_conf {
@@ -61,8 +117,26 @@ struct spi_hid {
struct spihid_ops *ops;
struct spi_hid_conf *conf;
+ struct spi_hid_device_descriptor desc; /* HID device descriptor. */
+ struct spi_hid_output_buf *output; /* Output buffer. */
+ struct spi_hid_input_buf *input; /* Input buffer. */
+ struct spi_hid_input_buf *response; /* Response buffer. */
+
+ u16 response_length;
+ u16 bufsize;
+
enum hidspi_power_state power_state;
+ u8 reset_attempts; /* The number of reset attempts. */
+
+ unsigned long flags; /* device flags. */
+
+ /* Control lock to make sure one output transaction at a time. */
+ struct mutex output_lock;
+ struct completion output_done;
+
+ u32 report_descriptor_crc32; /* HID report descriptor crc32 checksum. */
+
u32 regulator_error_count;
int regulator_last_error;
u32 bus_error_count;
@@ -70,6 +144,33 @@ struct spi_hid {
u32 dir_count; /* device initiated reset count. */
};
+static struct hid_ll_driver spi_hid_ll_driver;
+
+static void spi_hid_populate_output_header(u8 *buf,
+ const struct spi_hid_conf *conf,
+ const struct spi_hid_output_report *report)
+{
+ buf[0] = conf->write_opcode;
+ put_unaligned_be24(conf->output_report_address, &buf[1]);
+ buf[4] = report->report_type;
+ put_unaligned_le16(report->content_length, &buf[5]);
+ buf[7] = report->content_id;
+}
+
+static int spi_hid_output(struct spi_hid *shid, const void *buf, u16 length)
+{
+ int error;
+
+ error = spi_write(shid->spi, buf, length);
+
+ if (error) {
+ shid->bus_error_count++;
+ shid->bus_last_error = error;
+ }
+
+ return error;
+}
+
static const char *spi_hid_power_mode_string(enum hidspi_power_state power_state)
{
switch (power_state) {
@@ -84,11 +185,416 @@ static const char *spi_hid_power_mode_string(enum hidspi_power_state power_state
}
}
+static void spi_hid_stop_hid(struct spi_hid *shid)
+{
+ struct hid_device *hid = shid->hid;
+
+ shid->hid = NULL;
+ clear_bit(SPI_HID_READY, &shid->flags);
+
+ if (hid)
+ hid_destroy_device(hid);
+}
+
+static int spi_hid_send_output_report(struct spi_hid *shid,
+ struct spi_hid_output_report *report)
+{
+ struct spi_hid_output_buf *buf = shid->output;
+ struct device *dev = &shid->spi->dev;
+ u16 report_length;
+ u16 padded_length;
+ u8 padding;
+ int error;
+
+ guard(mutex)(&shid->output_lock);
+ if (report->content_length > shid->desc.max_output_length) {
+ dev_err(dev, "Output report too big, content_length 0x%x.",
+ report->content_length);
+ return -E2BIG;
+ }
+
+ spi_hid_populate_output_header(buf->header, shid->conf, report);
+
+ if (report->content_length)
+ memcpy(&buf->content, report->content, report->content_length);
+
+ report_length = sizeof(buf->header) + report->content_length;
+ padded_length = round_up(report_length, 4);
+ padding = padded_length - report_length;
+ memset(&buf->content[report->content_length], 0, padding);
+
+ error = spi_hid_output(shid, buf, padded_length);
+ if (error)
+ dev_err(dev, "Failed output transfer: %d.", error);
+
+ return error;
+}
+
+static int spi_hid_sync_request(struct spi_hid *shid,
+ struct spi_hid_output_report *report)
+{
+ struct device *dev = &shid->spi->dev;
+ int error;
+
+ error = spi_hid_send_output_report(shid, report);
+ if (error)
+ return error;
+
+ error = wait_for_completion_interruptible_timeout(&shid->output_done,
+ msecs_to_jiffies(SPI_HID_RESP_TIMEOUT));
+ if (error == 0) {
+ dev_err(dev, "Response timed out.");
+ return -ETIMEDOUT;
+ }
+
+ return 0;
+}
+
+/*
+ * This function returns the length of the report descriptor, or a negative
+ * error code if something went wrong.
+ */
+static int spi_hid_report_descriptor_request(struct spi_hid *shid)
+{
+ struct device *dev = &shid->spi->dev;
+ struct spi_hid_output_report report = {
+ .report_type = REPORT_DESCRIPTOR,
+ .content_length = 0,
+ .content_id = SPI_HID_OUTPUT_REPORT_CONTENT_ID_DESC_REQUEST,
+ .content = NULL,
+ };
+ int ret;
+
+ ret = spi_hid_sync_request(shid, &report);
+ if (ret) {
+ dev_err(dev,
+ "Expected report descriptor not received: %d.", ret);
+ return ret;
+ }
+
+ ret = shid->response_length;
+ if (ret != shid->desc.report_descriptor_length) {
+ ret = min_t(unsigned int, ret, shid->desc.report_descriptor_length);
+ dev_err(dev, "Received report descriptor length doesn't match device descriptor field, using min of the two: %d.",
+ ret);
+ }
+
+ return ret;
+}
+
+static int spi_hid_create_device(struct spi_hid *shid)
+{
+ struct hid_device *hid;
+ struct device *dev = &shid->spi->dev;
+ int error;
+
+ hid = hid_allocate_device();
+ error = PTR_ERR_OR_ZERO(hid);
+ if (error) {
+ dev_err(dev, "Failed to allocate hid device: %d.", error);
+ return error;
+ }
+
+ hid->driver_data = shid->spi;
+ hid->ll_driver = &spi_hid_ll_driver;
+ hid->dev.parent = &shid->spi->dev;
+ hid->bus = BUS_SPI;
+ hid->version = shid->desc.hid_version;
+ hid->vendor = shid->desc.vendor_id;
+ hid->product = shid->desc.product_id;
+
+ snprintf(hid->name, sizeof(hid->name), "spi %04X:%04X",
+ hid->vendor, hid->product);
+ strscpy(hid->phys, dev_name(&shid->spi->dev), sizeof(hid->phys));
+
+ shid->hid = hid;
+
+ error = hid_add_device(hid);
+ if (error) {
+ dev_err(dev, "Failed to add hid device: %d.", error);
+ /*
+ * We likely got here because report descriptor request timed
+ * out. Let's disconnect and destroy the hid_device structure.
+ */
+ spi_hid_stop_hid(shid);
+ return error;
+ }
+
+ return 0;
+}
+
+static int spi_hid_get_request(struct spi_hid *shid, u8 content_id)
+{
+ struct device *dev = &shid->spi->dev;
+ struct spi_hid_output_report report = {
+ .report_type = GET_FEATURE,
+ .content_length = 0,
+ .content_id = content_id,
+ .content = NULL,
+ };
+ int error;
+
+ error = spi_hid_sync_request(shid, &report);
+ if (error) {
+ dev_err(dev,
+ "Expected get request response not received! Error %d.",
+ error);
+ return error;
+ }
+
+ return 0;
+}
+
+static int spi_hid_set_request(struct spi_hid *shid, u8 *arg_buf, u16 arg_len,
+ u8 content_id)
+{
+ struct spi_hid_output_report report = {
+ .report_type = SET_FEATURE,
+ .content_length = arg_len,
+ .content_id = content_id,
+ .content = arg_buf,
+ };
+
+ return spi_hid_sync_request(shid, &report);
+}
+
+/* This is a placeholder. Will be implemented in the next patch. */
static irqreturn_t spi_hid_dev_irq(int irq, void *_shid)
{
return IRQ_HANDLED;
}
+static int spi_hid_alloc_buffers(struct spi_hid *shid, size_t report_size)
+{
+ struct device *dev = &shid->spi->dev;
+ int inbufsize = sizeof(shid->input->header) + sizeof(shid->input->body) + report_size;
+ int outbufsize = sizeof(shid->output->header) + report_size;
+
+ // devm_krealloc with __GFP_ZERO ensures the new memory is initialized
+ shid->output = devm_krealloc(dev, shid->output, outbufsize, GFP_KERNEL | __GFP_ZERO);
+ shid->input = devm_krealloc(dev, shid->input, inbufsize, GFP_KERNEL | __GFP_ZERO);
+ shid->response = devm_krealloc(dev, shid->response, inbufsize, GFP_KERNEL | __GFP_ZERO);
+
+ if (!shid->output || !shid->input || !shid->response)
+ return -ENOMEM;
+
+ shid->bufsize = report_size;
+
+ return 0;
+}
+
+static int spi_hid_get_report_length(struct hid_report *report)
+{
+ return ((report->size - 1) >> 3) + 1 +
+ report->device->report_enum[report->type].numbered + 2;
+}
+
+/*
+ * Traverse the supplied list of reports and find the longest
+ */
+static void spi_hid_find_max_report(struct hid_device *hid, u32 type,
+ u16 *max)
+{
+ struct hid_report *report;
+ u16 size;
+
+ /*
+ * We should not rely on wMaxInputLength, as some devices may set it to
+ * a wrong length.
+ */
+ list_for_each_entry(report, &hid->report_enum[type].report_list, list) {
+ size = spi_hid_get_report_length(report);
+ if (*max < size)
+ *max = size;
+ }
+}
+
+/* hid_ll_driver interface functions */
+
+static int spi_hid_ll_start(struct hid_device *hid)
+{
+ struct spi_device *spi = hid->driver_data;
+ struct spi_hid *shid = spi_get_drvdata(spi);
+ int error = 0;
+ u16 bufsize = 0;
+
+ spi_hid_find_max_report(hid, HID_INPUT_REPORT, &bufsize);
+ spi_hid_find_max_report(hid, HID_OUTPUT_REPORT, &bufsize);
+ spi_hid_find_max_report(hid, HID_FEATURE_REPORT, &bufsize);
+
+ if (bufsize < HID_MIN_BUFFER_SIZE) {
+ dev_err(&spi->dev,
+ "HID_MIN_BUFFER_SIZE > max_input_length (%d).",
+ bufsize);
+ return -EINVAL;
+ }
+
+ if (bufsize > shid->bufsize) {
+ guard(disable_irq)(&shid->spi->irq);
+
+ error = spi_hid_alloc_buffers(shid, bufsize);
+ if (error)
+ return error;
+ }
+
+ return 0;
+}
+
+static void spi_hid_ll_stop(struct hid_device *hid)
+{
+ hid->claimed = 0;
+}
+
+static int spi_hid_ll_open(struct hid_device *hid)
+{
+ struct spi_device *spi = hid->driver_data;
+ struct spi_hid *shid = spi_get_drvdata(spi);
+
+ set_bit(SPI_HID_READY, &shid->flags);
+ return 0;
+}
+
+static void spi_hid_ll_close(struct hid_device *hid)
+{
+ struct spi_device *spi = hid->driver_data;
+ struct spi_hid *shid = spi_get_drvdata(spi);
+
+ clear_bit(SPI_HID_READY, &shid->flags);
+ shid->reset_attempts = 0;
+}
+
+static int spi_hid_ll_power(struct hid_device *hid, int level)
+{
+ struct spi_device *spi = hid->driver_data;
+ struct spi_hid *shid = spi_get_drvdata(spi);
+ int error = 0;
+
+ guard(mutex)(&shid->output_lock);
+ if (!shid->hid)
+ error = -ENODEV;
+
+ return error;
+}
+
+static int spi_hid_ll_parse(struct hid_device *hid)
+{
+ struct spi_device *spi = hid->driver_data;
+ struct spi_hid *shid = spi_get_drvdata(spi);
+ struct device *dev = &spi->dev;
+ int error, len;
+
+ len = spi_hid_report_descriptor_request(shid);
+ if (len < 0) {
+ dev_err(dev, "Report descriptor request failed, %d.", len);
+ return len;
+ }
+
+ /*
+ * FIXME: below call returning 0 doesn't mean that the report descriptor
+ * is good. We might be caching a crc32 of a corrupted r. d. or who
+ * knows what the FW sent. Need to have a feedback loop about r. d.
+ * being ok and only then cache it.
+ */
+ error = hid_parse_report(hid, (u8 *)shid->response->content, len);
+ if (error) {
+ dev_err(dev, "failed parsing report: %d.", error);
+ return error;
+ }
+ shid->report_descriptor_crc32 = crc32_le(0,
+ (unsigned char const *)shid->response->content,
+ len);
+
+ return 0;
+}
+
+static int spi_hid_ll_raw_request(struct hid_device *hid,
+ unsigned char reportnum, __u8 *buf,
+ size_t len, unsigned char rtype, int reqtype)
+{
+ struct spi_device *spi = hid->driver_data;
+ struct spi_hid *shid = spi_get_drvdata(spi);
+ struct device *dev = &spi->dev;
+ int ret;
+
+ switch (reqtype) {
+ case HID_REQ_SET_REPORT:
+ if (buf[0] != reportnum) {
+ dev_err(dev, "report id mismatch.");
+ return -EINVAL;
+ }
+
+ ret = spi_hid_set_request(shid, &buf[1], len - 1,
+ reportnum);
+ if (ret) {
+ dev_err(dev, "failed to set report.");
+ return ret;
+ }
+
+ ret = len;
+ break;
+ case HID_REQ_GET_REPORT:
+ ret = spi_hid_get_request(shid, reportnum);
+ if (ret) {
+ dev_err(dev, "failed to get report.");
+ return ret;
+ }
+
+ ret = min_t(size_t, len,
+ (shid->response->body[1] | (shid->response->body[2] << 8)) + 1);
+ buf[0] = shid->response->body[3];
+ memcpy(&buf[1], &shid->response->content, ret);
+ break;
+ default:
+ dev_err(dev, "invalid request type.");
+ return -EIO;
+ }
+
+ return ret;
+}
+
+static int spi_hid_ll_output_report(struct hid_device *hid, __u8 *buf,
+ size_t len)
+{
+ struct spi_device *spi = hid->driver_data;
+ struct spi_hid *shid = spi_get_drvdata(spi);
+ struct device *dev = &spi->dev;
+ struct spi_hid_output_report report = {
+ .report_type = OUTPUT_REPORT,
+ .content_length = len - 1,
+ .content_id = buf[0],
+ .content = &buf[1],
+ };
+ int error;
+
+ if (!test_bit(SPI_HID_READY, &shid->flags)) {
+ dev_err(dev, "%s called in unready state", __func__);
+ return -ENODEV;
+ }
+
+ if (shid->desc.no_output_report_ack)
+ error = spi_hid_send_output_report(shid, &report);
+ else
+ error = spi_hid_sync_request(shid, &report);
+
+ if (error) {
+ dev_err(dev, "failed to send output report.");
+ return error;
+ }
+
+ return len;
+}
+
+static struct hid_ll_driver spi_hid_ll_driver = {
+ .start = spi_hid_ll_start,
+ .stop = spi_hid_ll_stop,
+ .open = spi_hid_ll_open,
+ .close = spi_hid_ll_close,
+ .power = spi_hid_ll_power,
+ .parse = spi_hid_ll_parse,
+ .output_report = spi_hid_ll_output_report,
+ .raw_request = spi_hid_ll_raw_request,
+};
+
static ssize_t bus_error_count_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
@@ -159,6 +665,15 @@ int spi_hid_core_probe(struct spi_device *spi, struct spihid_ops *ops,
spi_set_drvdata(spi, shid);
+ /*
+ * we need to allocate the buffer without knowing the maximum
+ * size of the reports. Let's use SZ_2K, then we do the
+ * real computation later.
+ */
+ error = spi_hid_alloc_buffers(shid, SZ_2K);
+ if (error)
+ return error;
+
/*
* At the end of probe we initialize the device:
* 0) assert reset, bias the interrupt line
@@ -191,6 +706,8 @@ int spi_hid_core_probe(struct spi_device *spi, struct spihid_ops *ops,
dev_dbg(dev, "%s: d3 -> %s.", __func__,
spi_hid_power_mode_string(shid->power_state));
+ spi_hid_create_device(shid);
+
return 0;
}
EXPORT_SYMBOL_GPL(spi_hid_core_probe);
@@ -201,6 +718,8 @@ void spi_hid_core_remove(struct spi_device *spi)
struct device *dev = &spi->dev;
int error;
+ spi_hid_stop_hid(shid);
+
shid->ops->assert_reset(shid->ops);
error = shid->ops->power_down(shid->ops);
if (error)
--
2.53.0.1185.g05d4b7b318-goog
^ permalink raw reply related
* [PATCH v3 05/11] HID: spi-hid: add HID SPI protocol implementation
From: Jingyuan Liang @ 2026-04-02 1:59 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-input, linux-doc, linux-kernel, linux-spi,
linux-trace-kernel, devicetree, hbarnor, tfiga, Jingyuan Liang,
Dmitry Antipov, Angela Czubak
In-Reply-To: <20260402-send-upstream-v3-0-6091c458d357@chromium.org>
This driver follows HID Over SPI Protocol Specification 1.0 available at
https://www.microsoft.com/en-us/download/details.aspx?id=103325. The
initial version of the driver does not support: 1) multi-fragment input
reports, 2) sending GET_INPUT and COMMAND output report types and
processing their respective acknowledge input reports, and 3) device
sleep power state.
Signed-off-by: Dmitry Antipov <dmanti@microsoft.com>
Signed-off-by: Angela Czubak <acz@semihalf.com>
Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
---
drivers/hid/spi-hid/spi-hid-core.c | 582 ++++++++++++++++++++++++++++++++++++-
1 file changed, 572 insertions(+), 10 deletions(-)
diff --git a/drivers/hid/spi-hid/spi-hid-core.c b/drivers/hid/spi-hid/spi-hid-core.c
index 4723b87346d4..00b9718ba2c3 100644
--- a/drivers/hid/spi-hid/spi-hid-core.c
+++ b/drivers/hid/spi-hid/spi-hid-core.c
@@ -23,11 +23,16 @@
#include <linux/completion.h>
#include <linux/crc32.h>
#include <linux/device.h>
+#include <linux/dma-mapping.h>
#include <linux/err.h>
#include <linux/hid.h>
#include <linux/hid-over-spi.h>
+#include <linux/input.h>
#include <linux/interrupt.h>
+#include <linux/irq.h>
#include <linux/jiffies.h>
+#include <linux/kernel.h>
+#include <linux/list.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/slab.h>
@@ -35,12 +40,22 @@
#include <linux/string.h>
#include <linux/sysfs.h>
#include <linux/unaligned.h>
+#include <linux/wait.h>
+#include <linux/workqueue.h>
+
+/* Protocol constants */
+#define SPI_HID_READ_APPROVAL_CONSTANT 0xff
+#define SPI_HID_INPUT_HEADER_SYNC_BYTE 0x5a
+#define SPI_HID_INPUT_HEADER_VERSION 0x03
+#define SPI_HID_SUPPORTED_VERSION 0x0300
#define SPI_HID_OUTPUT_REPORT_CONTENT_ID_DESC_REQUEST 0x00
-#define SPI_HID_RESP_TIMEOUT 1000
+#define SPI_HID_MAX_RESET_ATTEMPTS 3
+#define SPI_HID_RESP_TIMEOUT 1000
/* Protocol message size constants */
+#define SPI_HID_READ_APPROVAL_LEN 5
#define SPI_HID_OUTPUT_HEADER_LEN 8
/* flags */
@@ -49,6 +64,22 @@
* requests. The FW becomes ready after sending the report descriptor.
*/
#define SPI_HID_READY 0
+/*
+ * refresh_in_progress is set to true while the refresh_device worker
+ * thread is destroying and recreating the hidraw device. When this flag
+ * is set to true, the ll_close and ll_open functions will not cause
+ * power state changes.
+ */
+#define SPI_HID_REFRESH_IN_PROGRESS 1
+/*
+ * reset_pending indicates that the device is being reset. When this flag
+ * is set to true, garbage interrupts triggered during reset will be
+ * dropped and will not cause error handling.
+ */
+#define SPI_HID_RESET_PENDING 2
+#define SPI_HID_RESET_RESPONSE 3
+#define SPI_HID_CREATE_DEVICE 4
+#define SPI_HID_ERROR 5
/* Raw input buffer with data from the bus */
struct spi_hid_input_buf {
@@ -57,6 +88,22 @@ struct spi_hid_input_buf {
u8 content[];
};
+/* Processed data from input report header */
+struct spi_hid_input_header {
+ u8 version;
+ u16 report_length;
+ u8 last_fragment_flag;
+ u8 sync_const;
+};
+
+/* Processed data from an input report */
+struct spi_hid_input_report {
+ u8 report_type;
+ u16 content_length;
+ u8 content_id;
+ u8 *content;
+};
+
/* Raw output report buffer to be put on the bus */
struct spi_hid_output_buf {
u8 header[SPI_HID_OUTPUT_HEADER_LEN];
@@ -114,6 +161,9 @@ struct spi_hid {
struct spi_device *spi; /* spi device. */
struct hid_device *hid; /* pointer to corresponding HID dev. */
+ struct spi_transfer input_transfer[2]; /* Transfer buffer for read and write. */
+ struct spi_message input_message; /* used to execute a sequence of spi transfers. */
+
struct spihid_ops *ops;
struct spi_hid_conf *conf;
@@ -131,10 +181,20 @@ struct spi_hid {
unsigned long flags; /* device flags. */
- /* Control lock to make sure one output transaction at a time. */
+ struct work_struct reset_work;
+
+ /* Control lock to ensure complete output transaction. */
struct mutex output_lock;
+ /* Power lock to make sure one power state change at a time. */
+ struct mutex power_lock;
+ /* I/O lock to prevent concurrent output writes during the input read. */
+ struct mutex io_lock;
+
struct completion output_done;
+ u8 read_approval_header[SPI_HID_READ_APPROVAL_LEN];
+ u8 read_approval_body[SPI_HID_READ_APPROVAL_LEN];
+
u32 report_descriptor_crc32; /* HID report descriptor crc32 checksum. */
u32 regulator_error_count;
@@ -146,6 +206,66 @@ struct spi_hid {
static struct hid_ll_driver spi_hid_ll_driver;
+static void spi_hid_populate_read_approvals(const struct spi_hid_conf *conf,
+ u8 *header_buf, u8 *body_buf)
+{
+ header_buf[0] = conf->read_opcode;
+ put_unaligned_be24(conf->input_report_header_address, &header_buf[1]);
+ header_buf[4] = SPI_HID_READ_APPROVAL_CONSTANT;
+
+ body_buf[0] = conf->read_opcode;
+ put_unaligned_be24(conf->input_report_body_address, &body_buf[1]);
+ body_buf[4] = SPI_HID_READ_APPROVAL_CONSTANT;
+}
+
+static void spi_hid_parse_dev_desc(const struct hidspi_dev_descriptor *raw,
+ struct spi_hid_device_descriptor *desc)
+{
+ desc->hid_version = le16_to_cpu(raw->bcd_ver);
+ desc->report_descriptor_length = le16_to_cpu(raw->rep_desc_len);
+ desc->max_input_length = le16_to_cpu(raw->max_input_len);
+ desc->max_output_length = le16_to_cpu(raw->max_output_len);
+
+ /* FIXME: multi-fragment not supported, field below not used */
+ desc->max_fragment_length = le16_to_cpu(raw->max_frag_len);
+
+ desc->vendor_id = le16_to_cpu(raw->vendor_id);
+ desc->product_id = le16_to_cpu(raw->product_id);
+ desc->version_id = le16_to_cpu(raw->version_id);
+ desc->no_output_report_ack = le16_to_cpu(raw->flags) & BIT(0);
+}
+
+static void spi_hid_populate_input_header(const u8 *buf,
+ struct spi_hid_input_header *header)
+{
+ header->version = buf[0] & 0xf;
+ header->report_length = (get_unaligned_le16(&buf[1]) & 0x3fff) * 4;
+ header->last_fragment_flag = (buf[2] & 0x40) >> 6;
+ header->sync_const = buf[3];
+}
+
+static void spi_hid_populate_input_body(const u8 *buf,
+ struct input_report_body_header *body)
+{
+ body->input_report_type = buf[0];
+ body->content_len = get_unaligned_le16(&buf[1]);
+ body->content_id = buf[3];
+}
+
+static void spi_hid_input_report_prepare(struct spi_hid_input_buf *buf,
+ struct spi_hid_input_report *report)
+{
+ struct spi_hid_input_header header;
+ struct input_report_body_header body;
+
+ spi_hid_populate_input_header(buf->header, &header);
+ spi_hid_populate_input_body(buf->body, &body);
+ report->report_type = body.input_report_type;
+ report->content_length = body.content_len;
+ report->content_id = body.content_id;
+ report->content = buf->content;
+}
+
static void spi_hid_populate_output_header(u8 *buf,
const struct spi_hid_conf *conf,
const struct spi_hid_output_report *report)
@@ -157,6 +277,33 @@ static void spi_hid_populate_output_header(u8 *buf,
buf[7] = report->content_id;
}
+static int spi_hid_input_sync(struct spi_hid *shid, void *buf, u16 length,
+ bool is_header)
+{
+ int error;
+
+ shid->input_transfer[0].tx_buf = is_header ?
+ shid->read_approval_header :
+ shid->read_approval_body;
+ shid->input_transfer[0].len = SPI_HID_READ_APPROVAL_LEN;
+
+ shid->input_transfer[1].rx_buf = buf;
+ shid->input_transfer[1].len = length;
+
+ spi_message_init_with_transfers(&shid->input_message,
+ shid->input_transfer, 2);
+
+ error = spi_sync(shid->spi, &shid->input_message);
+ if (error) {
+ dev_err(&shid->spi->dev, "Error starting sync transfer: %d.", error);
+ shid->bus_error_count++;
+ shid->bus_last_error = error;
+ return error;
+ }
+
+ return 0;
+}
+
static int spi_hid_output(struct spi_hid *shid, const void *buf, u16 length)
{
int error;
@@ -196,6 +343,50 @@ static void spi_hid_stop_hid(struct spi_hid *shid)
hid_destroy_device(hid);
}
+static void spi_hid_error(struct spi_hid *shid)
+{
+ struct device *dev = &shid->spi->dev;
+ int error;
+
+ guard(mutex)(&shid->power_lock);
+ if (shid->power_state == HIDSPI_OFF)
+ return;
+
+ if (shid->reset_attempts++ >= SPI_HID_MAX_RESET_ATTEMPTS) {
+ dev_err(dev, "unresponsive device, aborting.");
+ spi_hid_stop_hid(shid);
+ shid->ops->assert_reset(shid->ops);
+ error = shid->ops->power_down(shid->ops);
+ if (error) {
+ dev_err(dev, "failed to disable regulator.");
+ shid->regulator_error_count++;
+ shid->regulator_last_error = error;
+ }
+ return;
+ }
+
+ clear_bit(SPI_HID_READY, &shid->flags);
+ set_bit(SPI_HID_RESET_PENDING, &shid->flags);
+
+ shid->ops->assert_reset(shid->ops);
+
+ shid->power_state = HIDSPI_OFF;
+
+ /*
+ * We want to cancel pending reset work as the device is being reset
+ * to recover from an error. cancel_work_sync will put us in a deadlock
+ * because this function is scheduled in 'reset_work' and we should
+ * avoid waiting for itself.
+ */
+ cancel_work(&shid->reset_work);
+
+ shid->ops->sleep_minimal_reset_delay(shid->ops);
+
+ shid->power_state = HIDSPI_ON;
+
+ shid->ops->deassert_reset(shid->ops);
+}
+
static int spi_hid_send_output_report(struct spi_hid *shid,
struct spi_hid_output_report *report)
{
@@ -206,13 +397,13 @@ static int spi_hid_send_output_report(struct spi_hid *shid,
u8 padding;
int error;
- guard(mutex)(&shid->output_lock);
if (report->content_length > shid->desc.max_output_length) {
dev_err(dev, "Output report too big, content_length 0x%x.",
report->content_length);
return -E2BIG;
}
+ guard(mutex)(&shid->io_lock);
spi_hid_populate_output_header(buf->header, shid->conf, report);
if (report->content_length)
@@ -236,6 +427,7 @@ static int spi_hid_sync_request(struct spi_hid *shid,
struct device *dev = &shid->spi->dev;
int error;
+ guard(mutex)(&shid->output_lock);
error = spi_hid_send_output_report(shid, report);
if (error)
return error;
@@ -250,6 +442,86 @@ static int spi_hid_sync_request(struct spi_hid *shid,
return 0;
}
+/*
+ * Handle the reset response from the FW by sending a request for the device
+ * descriptor.
+ */
+static void spi_hid_reset_response(struct spi_hid *shid)
+{
+ struct device *dev = &shid->spi->dev;
+ struct spi_hid_output_report report = {
+ .report_type = DEVICE_DESCRIPTOR,
+ .content_length = 0x0,
+ .content_id = SPI_HID_OUTPUT_REPORT_CONTENT_ID_DESC_REQUEST,
+ .content = NULL,
+ };
+ int error;
+
+ if (test_bit(SPI_HID_READY, &shid->flags)) {
+ dev_err(dev, "Spontaneous FW reset!");
+ clear_bit(SPI_HID_READY, &shid->flags);
+ shid->dir_count++;
+ }
+
+ if (shid->power_state == HIDSPI_OFF)
+ return;
+
+ error = spi_hid_sync_request(shid, &report);
+ if (error) {
+ dev_WARN_ONCE(dev, true,
+ "Failed to send device descriptor request: %d.", error);
+ set_bit(SPI_HID_ERROR, &shid->flags);
+ schedule_work(&shid->reset_work);
+ }
+}
+
+static int spi_hid_input_report_handler(struct spi_hid *shid,
+ struct spi_hid_input_buf *buf)
+{
+ struct device *dev = &shid->spi->dev;
+ struct spi_hid_input_report r;
+ int error = 0;
+
+ if (!test_bit(SPI_HID_READY, &shid->flags) ||
+ test_bit(SPI_HID_REFRESH_IN_PROGRESS, &shid->flags) || !shid->hid) {
+ dev_err(dev, "HID not ready");
+ return 0;
+ }
+
+ spi_hid_input_report_prepare(buf, &r);
+
+ error = hid_input_report(shid->hid, HID_INPUT_REPORT,
+ r.content - 1, r.content_length + 1, 1);
+
+ if (error == -ENODEV || error == -EBUSY) {
+ dev_err(dev, "ignoring report --> %d.", error);
+ return 0;
+ } else if (error) {
+ dev_err(dev, "Bad input report: %d.", error);
+ }
+
+ return error;
+}
+
+static void spi_hid_response_handler(struct spi_hid *shid,
+ struct input_report_body_header *body)
+{
+ shid->response_length = body->content_len;
+ /* completion_done returns 0 if there are waiters, otherwise 1 */
+ if (completion_done(&shid->output_done)) {
+ dev_err(&shid->spi->dev, "Unexpected response report.");
+ } else {
+ if (body->input_report_type == REPORT_DESCRIPTOR_RESPONSE ||
+ body->input_report_type == GET_FEATURE_RESPONSE) {
+ memcpy(shid->response->body, shid->input->body,
+ sizeof(shid->input->body));
+ memcpy(shid->response->content, shid->input->content,
+ body->content_len);
+ }
+ complete(&shid->output_done);
+ }
+}
+
/*
* This function returns the length of the report descriptor, or a negative
* error code if something went wrong.
@@ -269,6 +541,8 @@ static int spi_hid_report_descriptor_request(struct spi_hid *shid)
if (ret) {
dev_err(dev,
"Expected report descriptor not received: %d.", ret);
+ set_bit(SPI_HID_ERROR, &shid->flags);
+ schedule_work(&shid->reset_work);
return ret;
}
@@ -323,6 +597,205 @@ static int spi_hid_create_device(struct spi_hid *shid)
return 0;
}
+static void spi_hid_refresh_device(struct spi_hid *shid)
+{
+ struct device *dev = &shid->spi->dev;
+ u32 new_crc32 = 0;
+ int error = 0;
+
+ error = spi_hid_report_descriptor_request(shid);
+ if (error < 0) {
+ dev_err(dev,
+ "%s: failed report descriptor request: %d",
+ __func__, error);
+ return;
+ }
+ new_crc32 = crc32_le(0, (unsigned char const *)shid->response->content,
+ (size_t)error);
+
+ /* Same report descriptor, so no need to create a new hid device. */
+ if (new_crc32 == shid->report_descriptor_crc32) {
+ set_bit(SPI_HID_READY, &shid->flags);
+ return;
+ }
+
+ shid->report_descriptor_crc32 = new_crc32;
+
+ set_bit(SPI_HID_REFRESH_IN_PROGRESS, &shid->flags);
+
+ spi_hid_stop_hid(shid);
+
+ error = spi_hid_create_device(shid);
+ if (error) {
+ dev_err(dev, "%s: Failed to create hid device: %d.", __func__, error);
+ return;
+ }
+
+ clear_bit(SPI_HID_REFRESH_IN_PROGRESS, &shid->flags);
+}
+
+static void spi_hid_reset_work(struct work_struct *work)
+{
+ struct spi_hid *shid =
+ container_of(work, struct spi_hid, reset_work);
+ struct device *dev = &shid->spi->dev;
+ int error = 0;
+
+ if (test_and_clear_bit(SPI_HID_RESET_RESPONSE, &shid->flags)) {
+ spi_hid_reset_response(shid);
+ return;
+ }
+
+ if (test_and_clear_bit(SPI_HID_CREATE_DEVICE, &shid->flags)) {
+ guard(mutex)(&shid->power_lock);
+ if (shid->power_state == HIDSPI_OFF) {
+ dev_err(dev, "%s: Powered off, returning", __func__);
+ return;
+ }
+
+ if (!shid->hid) {
+ error = spi_hid_create_device(shid);
+ if (error) {
+ dev_err(dev, "%s: Failed to create hid device: %d.",
+ __func__, error);
+ return;
+ }
+ } else {
+ spi_hid_refresh_device(shid);
+ }
+
+ return;
+ }
+
+ if (test_and_clear_bit(SPI_HID_ERROR, &shid->flags)) {
+ spi_hid_error(shid);
+ return;
+ }
+}
+
+static int spi_hid_process_input_report(struct spi_hid *shid,
+ struct spi_hid_input_buf *buf)
+{
+ struct spi_hid_input_header header;
+ struct input_report_body_header body;
+ struct device *dev = &shid->spi->dev;
+ struct hidspi_dev_descriptor *raw;
+
+ spi_hid_populate_input_header(buf->header, &header);
+ spi_hid_populate_input_body(buf->body, &body);
+
+ if (body.content_len > header.report_length) {
+ dev_err(dev, "Bad body length %d > %d.", body.content_len,
+ header.report_length);
+ return -EPROTO;
+ }
+
+ switch (body.input_report_type) {
+ case DATA:
+ return spi_hid_input_report_handler(shid, buf);
+ case RESET_RESPONSE:
+ clear_bit(SPI_HID_RESET_PENDING, &shid->flags);
+ set_bit(SPI_HID_RESET_RESPONSE, &shid->flags);
+ schedule_work(&shid->reset_work);
+ break;
+ case DEVICE_DESCRIPTOR_RESPONSE:
+ /* Mark the completion done to avoid timeout */
+ spi_hid_response_handler(shid, &body);
+
+ /* Reset attempts at every device descriptor fetch */
+ shid->reset_attempts = 0;
+ raw = (struct hidspi_dev_descriptor *)buf->content;
+
+ /* Validate device descriptor length before parsing */
+ if (body.content_len != HIDSPI_DEVICE_DESCRIPTOR_SIZE) {
+ dev_err(dev, "Invalid content length %d, expected %lu.",
+ body.content_len,
+ HIDSPI_DEVICE_DESCRIPTOR_SIZE);
+ return -EPROTO;
+ }
+
+ if (le16_to_cpu(raw->dev_desc_len) !=
+ HIDSPI_DEVICE_DESCRIPTOR_SIZE) {
+ dev_err(dev,
+ "Invalid wDeviceDescLength %d, expected %lu.",
+ raw->dev_desc_len,
+ HIDSPI_DEVICE_DESCRIPTOR_SIZE);
+ return -EPROTO;
+ }
+
+ spi_hid_parse_dev_desc(raw, &shid->desc);
+
+ if (shid->desc.hid_version != SPI_HID_SUPPORTED_VERSION) {
+ dev_err(dev,
+ "Unsupported device descriptor version %4x.",
+ shid->desc.hid_version);
+ return -EPROTONOSUPPORT;
+ }
+
+ set_bit(SPI_HID_CREATE_DEVICE, &shid->flags);
+ schedule_work(&shid->reset_work);
+
+ break;
+ case OUTPUT_REPORT_RESPONSE:
+ if (shid->desc.no_output_report_ack) {
+ dev_err(dev, "Unexpected output report response.");
+ break;
+ }
+ fallthrough;
+ case GET_FEATURE_RESPONSE:
+ case SET_FEATURE_RESPONSE:
+ case REPORT_DESCRIPTOR_RESPONSE:
+ spi_hid_response_handler(shid, &body);
+ break;
+ /*
+ * FIXME: sending GET_INPUT and COMMAND reports not supported, thus
+ * throw away responses to those, they should never come.
+ */
+ case GET_INPUT_REPORT_RESPONSE:
+ case COMMAND_RESPONSE:
+ dev_err(dev, "Not a supported report type: 0x%x.",
+ body.input_report_type);
+ break;
+ default:
+ dev_err(dev, "Unknown input report: 0x%x.", body.input_report_type);
+ return -EPROTO;
+ }
+
+ return 0;
+}
+
+static int spi_hid_bus_validate_header(struct spi_hid *shid,
+ struct spi_hid_input_header *header)
+{
+ struct device *dev = &shid->spi->dev;
+
+ if (header->version != SPI_HID_INPUT_HEADER_VERSION) {
+ dev_err(dev, "Unknown input report version (v 0x%x).",
+ header->version);
+ return -EINVAL;
+ }
+
+ if (shid->desc.max_input_length != 0 &&
+ header->report_length > shid->desc.max_input_length) {
+ dev_err(dev, "Input report body size %u > max expected of %u.",
+ header->report_length, shid->desc.max_input_length);
+ return -EMSGSIZE;
+ }
+
+ if (header->last_fragment_flag != 1) {
+ dev_err(dev, "Multi-fragment reports not supported.");
+ return -EOPNOTSUPP;
+ }
+
+ if (header->sync_const != SPI_HID_INPUT_HEADER_SYNC_BYTE) {
+ dev_err(dev, "Invalid input report sync constant (0x%x).",
+ header->sync_const);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
static int spi_hid_get_request(struct spi_hid *shid, u8 content_id)
{
struct device *dev = &shid->spi->dev;
@@ -339,6 +812,8 @@ static int spi_hid_get_request(struct spi_hid *shid, u8 content_id)
dev_err(dev,
"Expected get request response not received! Error %d.",
error);
+ set_bit(SPI_HID_ERROR, &shid->flags);
+ schedule_work(&shid->reset_work);
return error;
}
@@ -358,9 +833,83 @@ static int spi_hid_set_request(struct spi_hid *shid, u8 *arg_buf, u16 arg_len,
return spi_hid_sync_request(shid, &report);
}
-/* This is a placeholder. Will be implemented in the next patch. */
static irqreturn_t spi_hid_dev_irq(int irq, void *_shid)
{
+ struct spi_hid *shid = _shid;
+ struct device *dev = &shid->spi->dev;
+ struct spi_hid_input_header header;
+ int error = 0;
+
+ scoped_guard(mutex, &shid->io_lock) {
+ error = spi_hid_input_sync(shid, shid->input->header,
+ sizeof(shid->input->header), true);
+ if (error) {
+ dev_err(dev, "Failed to transfer header: %d.", error);
+ goto err;
+ }
+
+ if (shid->power_state == HIDSPI_OFF) {
+ dev_warn(dev, "Device is off after header was received.");
+ goto out;
+ }
+
+ if (shid->input_message.status < 0) {
+ dev_warn(dev, "Error reading header: %d.",
+ shid->input_message.status);
+ shid->bus_error_count++;
+ shid->bus_last_error = shid->input_message.status;
+ goto err;
+ }
+
+ spi_hid_populate_input_header(shid->input->header, &header);
+
+ error = spi_hid_bus_validate_header(shid, &header);
+ if (error) {
+ if (!test_bit(SPI_HID_RESET_PENDING, &shid->flags)) {
+ dev_err(dev, "Failed to validate header: %d.", error);
+ print_hex_dump(KERN_ERR, "spi_hid: header buffer: ",
+ DUMP_PREFIX_NONE, 16, 1, shid->input->header,
+ sizeof(shid->input->header), false);
+ shid->bus_error_count++;
+ shid->bus_last_error = error;
+ goto err;
+ }
+ goto out;
+ }
+
+ error = spi_hid_input_sync(shid, shid->input->body, header.report_length,
+ false);
+ if (error) {
+ dev_err(dev, "Failed to transfer body: %d.", error);
+ goto err;
+ }
+
+ if (shid->power_state == HIDSPI_OFF) {
+ dev_warn(dev, "Device is off after body was received.");
+ goto out;
+ }
+
+ if (shid->input_message.status < 0) {
+ dev_warn(dev, "Error reading body: %d.",
+ shid->input_message.status);
+ shid->bus_error_count++;
+ shid->bus_last_error = shid->input_message.status;
+ goto err;
+ }
+ }
+
+ error = spi_hid_process_input_report(shid, shid->input);
+ if (error) {
+ dev_err(dev, "Failed to process input report: %d.", error);
+ goto err;
+ }
+
+out:
+ return IRQ_HANDLED;
+
+err:
+ set_bit(SPI_HID_ERROR, &shid->flags);
+ schedule_work(&shid->reset_work);
return IRQ_HANDLED;
}
@@ -571,10 +1120,13 @@ static int spi_hid_ll_output_report(struct hid_device *hid, __u8 *buf,
return -ENODEV;
}
- if (shid->desc.no_output_report_ack)
- error = spi_hid_send_output_report(shid, &report);
- else
+ if (shid->desc.no_output_report_ack) {
+ scoped_guard(mutex, &shid->output_lock) {
+ error = spi_hid_send_output_report(shid, &report);
+ }
+ } else {
error = spi_hid_sync_request(shid, &report);
+ }
if (error) {
dev_err(dev, "failed to send output report.");
@@ -662,11 +1214,23 @@ int spi_hid_core_probe(struct spi_device *spi, struct spihid_ops *ops,
shid->power_state = HIDSPI_ON;
shid->ops = ops;
shid->conf = conf;
+ set_bit(SPI_HID_RESET_PENDING, &shid->flags);
spi_set_drvdata(spi, shid);
+ /* Using now populated conf let's pre-calculate the read approvals */
+ spi_hid_populate_read_approvals(shid->conf, shid->read_approval_header,
+ shid->read_approval_body);
+
+ mutex_init(&shid->output_lock);
+ mutex_init(&shid->power_lock);
+ mutex_init(&shid->io_lock);
+ init_completion(&shid->output_done);
+
+ INIT_WORK(&shid->reset_work, spi_hid_reset_work);
+
/*
- * we need to allocate the buffer without knowing the maximum
+ * We need to allocate the buffer without knowing the maximum
* size of the reports. Let's use SZ_2K, then we do the
* real computation later.
*/
@@ -706,8 +1270,6 @@ int spi_hid_core_probe(struct spi_device *spi, struct spihid_ops *ops,
dev_dbg(dev, "%s: d3 -> %s.", __func__,
spi_hid_power_mode_string(shid->power_state));
- spi_hid_create_device(shid);
-
return 0;
}
EXPORT_SYMBOL_GPL(spi_hid_core_probe);
--
2.53.0.1185.g05d4b7b318-goog
^ permalink raw reply related
* [PATCH v3 06/11] HID: spi_hid: add spi_hid traces
From: Jingyuan Liang @ 2026-04-02 1:59 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-input, linux-doc, linux-kernel, linux-spi,
linux-trace-kernel, devicetree, hbarnor, tfiga, Jingyuan Liang,
Dmitry Antipov, Angela Czubak
In-Reply-To: <20260402-send-upstream-v3-0-6091c458d357@chromium.org>
Add traces for purposed of debugging spi_hid driver.
Signed-off-by: Dmitry Antipov <dmanti@microsoft.com>
Signed-off-by: Angela Czubak <acz@semihalf.com>
Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
---
drivers/hid/spi-hid/Makefile | 1 +
drivers/hid/spi-hid/spi-hid-core.c | 118 +++++++++----------------
drivers/hid/spi-hid/spi-hid-core.h | 91 +++++++++++++++++++
drivers/hid/spi-hid/spi-hid-trace.h | 169 ++++++++++++++++++++++++++++++++++++
4 files changed, 303 insertions(+), 76 deletions(-)
diff --git a/drivers/hid/spi-hid/Makefile b/drivers/hid/spi-hid/Makefile
index 92e24cddbfc2..733e006df56e 100644
--- a/drivers/hid/spi-hid/Makefile
+++ b/drivers/hid/spi-hid/Makefile
@@ -7,3 +7,4 @@
obj-$(CONFIG_SPI_HID_CORE) += spi-hid.o
spi-hid-objs = spi-hid-core.o
+CFLAGS_spi-hid-core.o := -I$(src)
diff --git a/drivers/hid/spi-hid/spi-hid-core.c b/drivers/hid/spi-hid/spi-hid-core.c
index 00b9718ba2c3..802615565541 100644
--- a/drivers/hid/spi-hid/spi-hid-core.c
+++ b/drivers/hid/spi-hid/spi-hid-core.c
@@ -43,6 +43,11 @@
#include <linux/wait.h>
#include <linux/workqueue.h>
+#include "spi-hid-core.h"
+
+#define CREATE_TRACE_POINTS
+#include "spi-hid-trace.h"
+
/* Protocol constants */
#define SPI_HID_READ_APPROVAL_CONSTANT 0xff
#define SPI_HID_INPUT_HEADER_SYNC_BYTE 0x5a
@@ -81,13 +86,6 @@
#define SPI_HID_CREATE_DEVICE 4
#define SPI_HID_ERROR 5
-/* Raw input buffer with data from the bus */
-struct spi_hid_input_buf {
- u8 header[HIDSPI_INPUT_HEADER_SIZE];
- u8 body[HIDSPI_INPUT_BODY_HEADER_SIZE];
- u8 content[];
-};
-
/* Processed data from input report header */
struct spi_hid_input_header {
u8 version;
@@ -104,12 +102,6 @@ struct spi_hid_input_report {
u8 *content;
};
-/* Raw output report buffer to be put on the bus */
-struct spi_hid_output_buf {
- u8 header[SPI_HID_OUTPUT_HEADER_LEN];
- u8 content[];
-};
-
/* Data necessary to send an output report */
struct spi_hid_output_report {
u8 report_type;
@@ -118,19 +110,6 @@ struct spi_hid_output_report {
u8 *content;
};
-/* Processed data from a device descriptor */
-struct spi_hid_device_descriptor {
- u16 hid_version;
- u16 report_descriptor_length;
- u16 max_input_length;
- u16 max_output_length;
- u16 max_fragment_length;
- u16 vendor_id;
- u16 product_id;
- u16 version_id;
- u8 no_output_report_ack;
-};
-
/* struct spi_hid_conf - Conf provided to the core */
struct spi_hid_conf {
u32 input_report_header_address;
@@ -156,54 +135,6 @@ struct spihid_ops {
void (*sleep_minimal_reset_delay)(struct spihid_ops *ops);
};
-/* Driver context */
-struct spi_hid {
- struct spi_device *spi; /* spi device. */
- struct hid_device *hid; /* pointer to corresponding HID dev. */
-
- struct spi_transfer input_transfer[2]; /* Transfer buffer for read and write. */
- struct spi_message input_message; /* used to execute a sequence of spi transfers. */
-
- struct spihid_ops *ops;
- struct spi_hid_conf *conf;
-
- struct spi_hid_device_descriptor desc; /* HID device descriptor. */
- struct spi_hid_output_buf *output; /* Output buffer. */
- struct spi_hid_input_buf *input; /* Input buffer. */
- struct spi_hid_input_buf *response; /* Response buffer. */
-
- u16 response_length;
- u16 bufsize;
-
- enum hidspi_power_state power_state;
-
- u8 reset_attempts; /* The number of reset attempts. */
-
- unsigned long flags; /* device flags. */
-
- struct work_struct reset_work;
-
- /* Control lock to ensure complete output transaction. */
- struct mutex output_lock;
- /* Power lock to make sure one power state change at a time. */
- struct mutex power_lock;
- /* I/O lock to prevent concurrent output writes during the input read. */
- struct mutex io_lock;
-
- struct completion output_done;
-
- u8 read_approval_header[SPI_HID_READ_APPROVAL_LEN];
- u8 read_approval_body[SPI_HID_READ_APPROVAL_LEN];
-
- u32 report_descriptor_crc32; /* HID report descriptor crc32 checksum. */
-
- u32 regulator_error_count;
- int regulator_last_error;
- u32 bus_error_count;
- int bus_last_error;
- u32 dir_count; /* device initiated reset count. */
-};
-
static struct hid_ll_driver spi_hid_ll_driver;
static void spi_hid_populate_read_approvals(const struct spi_hid_conf *conf,
@@ -293,6 +224,11 @@ static int spi_hid_input_sync(struct spi_hid *shid, void *buf, u16 length,
spi_message_init_with_transfers(&shid->input_message,
shid->input_transfer, 2);
+ trace_spi_hid_input_sync(shid, shid->input_transfer[0].tx_buf,
+ shid->input_transfer[0].len,
+ shid->input_transfer[1].rx_buf,
+ shid->input_transfer[1].len, 0);
+
error = spi_sync(shid->spi, &shid->input_message);
if (error) {
dev_err(&shid->spi->dev, "Error starting sync transfer: %d.", error);
@@ -343,11 +279,13 @@ static void spi_hid_stop_hid(struct spi_hid *shid)
hid_destroy_device(hid);
}
-static void spi_hid_error(struct spi_hid *shid)
+static void spi_hid_error_handler(struct spi_hid *shid)
{
struct device *dev = &shid->spi->dev;
int error;
+ trace_spi_hid_error_handler(shid);
+
guard(mutex)(&shid->power_lock);
if (shid->power_state == HIDSPI_OFF)
return;
@@ -457,6 +395,8 @@ static void spi_hid_reset_response(struct spi_hid *shid)
};
int error;
+ trace_spi_hid_reset_response(shid);
+
if (test_bit(SPI_HID_READY, &shid->flags)) {
dev_err(dev, "Spontaneous FW reset!");
clear_bit(SPI_HID_READY, &shid->flags);
@@ -482,6 +422,8 @@ static int spi_hid_input_report_handler(struct spi_hid *shid,
struct spi_hid_input_report r;
int error = 0;
+ trace_spi_hid_input_report_handler(shid);
+
if (!test_bit(SPI_HID_READY, &shid->flags) ||
test_bit(SPI_HID_REFRESH_IN_PROGRESS, &shid->flags) || !shid->hid) {
dev_err(dev, "HID not ready");
@@ -506,6 +448,8 @@ static int spi_hid_input_report_handler(struct spi_hid *shid,
static void spi_hid_response_handler(struct spi_hid *shid,
struct input_report_body_header *body)
{
+ trace_spi_hid_response_handler(shid);
+
shid->response_length = body->content_len;
/* completion_done returns 0 if there are waiters, otherwise 1 */
if (completion_done(&shid->output_done)) {
@@ -562,6 +506,8 @@ static int spi_hid_create_device(struct spi_hid *shid)
struct device *dev = &shid->spi->dev;
int error;
+ trace_spi_hid_create_device(shid);
+
hid = hid_allocate_device();
error = PTR_ERR_OR_ZERO(hid);
if (error) {
@@ -603,6 +549,8 @@ static void spi_hid_refresh_device(struct spi_hid *shid)
u32 new_crc32 = 0;
int error = 0;
+ trace_spi_hid_refresh_device(shid);
+
error = spi_hid_report_descriptor_request(shid);
if (error < 0) {
dev_err(dev,
@@ -668,7 +616,7 @@ static void spi_hid_reset_work(struct work_struct *work)
}
if (test_and_clear_bit(SPI_HID_ERROR, &shid->flags)) {
- spi_hid_error(shid);
+ spi_hid_error_handler(shid);
return;
}
}
@@ -681,6 +629,8 @@ static int spi_hid_process_input_report(struct spi_hid *shid,
struct device *dev = &shid->spi->dev;
struct hidspi_dev_descriptor *raw;
+ trace_spi_hid_process_input_report(shid);
+
spi_hid_populate_input_header(buf->header, &header);
spi_hid_populate_input_body(buf->body, &body);
@@ -840,6 +790,9 @@ static irqreturn_t spi_hid_dev_irq(int irq, void *_shid)
struct spi_hid_input_header header;
int error = 0;
+ trace_spi_hid_dev_irq(shid, irq);
+ trace_spi_hid_header_transfer(shid);
+
scoped_guard(mutex, &shid->io_lock) {
error = spi_hid_input_sync(shid, shid->input->header,
sizeof(shid->input->header), true);
@@ -853,6 +806,13 @@ static irqreturn_t spi_hid_dev_irq(int irq, void *_shid)
goto out;
}
+ trace_spi_hid_input_header_complete(shid,
+ shid->input_transfer[0].tx_buf,
+ shid->input_transfer[0].len,
+ shid->input_transfer[1].rx_buf,
+ shid->input_transfer[1].len,
+ shid->input_message.status);
+
if (shid->input_message.status < 0) {
dev_warn(dev, "Error reading header: %d.",
shid->input_message.status);
@@ -889,6 +849,12 @@ static irqreturn_t spi_hid_dev_irq(int irq, void *_shid)
goto out;
}
+ trace_spi_hid_input_body_complete(shid, shid->input_transfer[0].tx_buf,
+ shid->input_transfer[0].len,
+ shid->input_transfer[1].rx_buf,
+ shid->input_transfer[1].len,
+ shid->input_message.status);
+
if (shid->input_message.status < 0) {
dev_warn(dev, "Error reading body: %d.",
shid->input_message.status);
diff --git a/drivers/hid/spi-hid/spi-hid-core.h b/drivers/hid/spi-hid/spi-hid-core.h
new file mode 100644
index 000000000000..293e2cfcfbf7
--- /dev/null
+++ b/drivers/hid/spi-hid/spi-hid-core.h
@@ -0,0 +1,91 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2021 Microsoft Corporation
+ * Copyright (c) 2026 Google LLC
+ */
+
+#ifndef SPI_HID_CORE_H
+#define SPI_HID_CORE_H
+
+#include <linux/hid-over-spi.h>
+#include <linux/spi/spi.h>
+
+/* Protocol message size constants */
+#define SPI_HID_READ_APPROVAL_LEN 5
+#define SPI_HID_OUTPUT_HEADER_LEN 8
+
+/* Raw input buffer with data from the bus */
+struct spi_hid_input_buf {
+ u8 header[HIDSPI_INPUT_HEADER_SIZE];
+ u8 body[HIDSPI_INPUT_BODY_HEADER_SIZE];
+ u8 content[];
+};
+
+/* Raw output report buffer to be put on the bus */
+struct spi_hid_output_buf {
+ u8 header[SPI_HID_OUTPUT_HEADER_LEN];
+ u8 content[];
+};
+
+/* Processed data from a device descriptor */
+struct spi_hid_device_descriptor {
+ u16 hid_version;
+ u16 report_descriptor_length;
+ u16 max_input_length;
+ u16 max_output_length;
+ u16 max_fragment_length;
+ u16 vendor_id;
+ u16 product_id;
+ u16 version_id;
+ u8 no_output_report_ack;
+};
+
+/* Driver context */
+struct spi_hid {
+ struct spi_device *spi; /* spi device. */
+ struct hid_device *hid; /* pointer to corresponding HID dev. */
+
+ struct spi_transfer input_transfer[2]; /* Transfer buffer for read and write. */
+ struct spi_message input_message; /* used to execute a sequence of spi transfers. */
+
+ struct spihid_ops *ops;
+ struct spi_hid_conf *conf;
+
+ struct spi_hid_device_descriptor desc; /* HID device descriptor. */
+ struct spi_hid_output_buf *output; /* Output buffer. */
+ struct spi_hid_input_buf *input; /* Input buffer. */
+ struct spi_hid_input_buf *response; /* Response buffer. */
+
+ u16 response_length;
+ u16 bufsize;
+
+ enum hidspi_power_state power_state;
+
+ u8 reset_attempts; /* The number of reset attempts. */
+
+ unsigned long flags; /* device flags. */
+
+ struct work_struct reset_work;
+
+ /* Control lock to ensure complete output transaction. */
+ struct mutex output_lock;
+ /* Power lock to make sure one power state change at a time. */
+ struct mutex power_lock;
+ /* I/O lock to prevent concurrent output writes during the input read. */
+ struct mutex io_lock;
+
+ struct completion output_done;
+
+ u8 read_approval_header[SPI_HID_READ_APPROVAL_LEN];
+ u8 read_approval_body[SPI_HID_READ_APPROVAL_LEN];
+
+ u32 report_descriptor_crc32; /* HID report descriptor crc32 checksum. */
+
+ u32 regulator_error_count;
+ int regulator_last_error;
+ u32 bus_error_count;
+ int bus_last_error;
+ u32 dir_count; /* device initiated reset count. */
+};
+
+#endif /* SPI_HID_CORE_H */
diff --git a/drivers/hid/spi-hid/spi-hid-trace.h b/drivers/hid/spi-hid/spi-hid-trace.h
new file mode 100644
index 000000000000..cc13d71a14de
--- /dev/null
+++ b/drivers/hid/spi-hid/spi-hid-trace.h
@@ -0,0 +1,169 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2021 Microsoft Corporation
+ */
+
+#undef TRACE_SYSTEM
+#define TRACE_SYSTEM spi_hid
+
+#if !defined(_SPI_HID_TRACE_H) || defined(TRACE_HEADER_MULTI_READ)
+#define _SPI_HID_TRACE_H
+
+#include <linux/types.h>
+#include <linux/tracepoint.h>
+#include "spi-hid-core.h"
+
+DECLARE_EVENT_CLASS(spi_hid_transfer,
+ TP_PROTO(struct spi_hid *shid, const void *tx_buf, int tx_len,
+ const void *rx_buf, u16 rx_len, int ret),
+
+ TP_ARGS(shid, tx_buf, tx_len, rx_buf, rx_len, ret),
+
+ TP_STRUCT__entry(
+ __field(int, bus_num)
+ __field(int, chip_select)
+ __field(int, ret)
+ __dynamic_array(u8, rx_buf, rx_len)
+ __dynamic_array(u8, tx_buf, tx_len)
+ ),
+
+ TP_fast_assign(
+ __entry->bus_num = shid->spi->controller->bus_num;
+ __entry->chip_select = shid->spi->chip_select;
+ __entry->ret = ret;
+
+ memcpy(__get_dynamic_array(tx_buf), tx_buf, tx_len);
+ memcpy(__get_dynamic_array(rx_buf), rx_buf, rx_len);
+ ),
+
+ TP_printk("spi%d.%d: len=%d tx=[%*phD] rx=[%*phD] --> %d",
+ __entry->bus_num, __entry->chip_select,
+ __get_dynamic_array_len(tx_buf) + __get_dynamic_array_len(rx_buf),
+ __get_dynamic_array_len(tx_buf), __get_dynamic_array(tx_buf),
+ __get_dynamic_array_len(rx_buf), __get_dynamic_array(rx_buf),
+ __entry->ret)
+);
+
+DEFINE_EVENT(spi_hid_transfer, spi_hid_input_sync,
+ TP_PROTO(struct spi_hid *shid, const void *tx_buf, int tx_len,
+ const void *rx_buf, u16 rx_len, int ret),
+ TP_ARGS(shid, tx_buf, tx_len, rx_buf, rx_len, ret));
+
+DEFINE_EVENT(spi_hid_transfer, spi_hid_input_header_complete,
+ TP_PROTO(struct spi_hid *shid, const void *tx_buf, int tx_len,
+ const void *rx_buf, u16 rx_len, int ret),
+ TP_ARGS(shid, tx_buf, tx_len, rx_buf, rx_len, ret));
+
+DEFINE_EVENT(spi_hid_transfer, spi_hid_input_body_complete,
+ TP_PROTO(struct spi_hid *shid, const void *tx_buf, int tx_len,
+ const void *rx_buf, u16 rx_len, int ret),
+ TP_ARGS(shid, tx_buf, tx_len, rx_buf, rx_len, ret));
+
+DECLARE_EVENT_CLASS(spi_hid_irq,
+ TP_PROTO(struct spi_hid *shid, int irq),
+
+ TP_ARGS(shid, irq),
+
+ TP_STRUCT__entry(
+ __field(int, bus_num)
+ __field(int, chip_select)
+ __field(int, irq)
+ ),
+
+ TP_fast_assign(
+ __entry->bus_num = shid->spi->controller->bus_num;
+ __entry->chip_select = shid->spi->chip_select;
+ __entry->irq = irq;
+ ),
+
+ TP_printk("spi%d.%d: IRQ %d",
+ __entry->bus_num, __entry->chip_select, __entry->irq)
+);
+
+DEFINE_EVENT(spi_hid_irq, spi_hid_dev_irq,
+ TP_PROTO(struct spi_hid *shid, int irq), TP_ARGS(shid, irq));
+
+DECLARE_EVENT_CLASS(spi_hid,
+ TP_PROTO(struct spi_hid *shid),
+
+ TP_ARGS(shid),
+
+ TP_STRUCT__entry(
+ __field(int, bus_num)
+ __field(int, chip_select)
+ __field(int, power_state)
+ __field(u32, flags)
+
+ __field(int, vendor_id)
+ __field(int, product_id)
+ __field(int, max_input_length)
+ __field(int, max_output_length)
+ __field(u16, hid_version)
+ __field(u16, report_descriptor_length)
+ __field(u16, version_id)
+ ),
+
+ TP_fast_assign(
+ __entry->bus_num = shid->spi->controller->bus_num;
+ __entry->chip_select = shid->spi->chip_select;
+ __entry->power_state = shid->power_state;
+ __entry->flags = shid->flags;
+
+ __entry->vendor_id = shid->desc.vendor_id;
+ __entry->product_id = shid->desc.product_id;
+ __entry->max_input_length = shid->desc.max_input_length;
+ __entry->max_output_length = shid->desc.max_output_length;
+ __entry->hid_version = shid->desc.hid_version;
+ __entry->report_descriptor_length =
+ shid->desc.report_descriptor_length;
+ __entry->version_id = shid->desc.version_id;
+ ),
+
+ TP_printk("spi%d.%d: (%04x:%04x v%d) HID v%d.%d state p:%d len i:%d o:%d r:%d flags 0x%08x",
+ __entry->bus_num, __entry->chip_select,
+ __entry->vendor_id, __entry->product_id, __entry->version_id,
+ __entry->hid_version >> 8, __entry->hid_version & 0xff,
+ __entry->power_state, __entry->max_input_length,
+ __entry->max_output_length, __entry->report_descriptor_length,
+ __entry->flags)
+);
+
+DEFINE_EVENT(spi_hid, spi_hid_header_transfer, TP_PROTO(struct spi_hid *shid),
+ TP_ARGS(shid));
+
+DEFINE_EVENT(spi_hid, spi_hid_process_input_report,
+ TP_PROTO(struct spi_hid *shid), TP_ARGS(shid));
+
+DEFINE_EVENT(spi_hid, spi_hid_input_report_handler,
+ TP_PROTO(struct spi_hid *shid), TP_ARGS(shid));
+
+DEFINE_EVENT(spi_hid, spi_hid_reset_response, TP_PROTO(struct spi_hid *shid),
+ TP_ARGS(shid));
+
+DEFINE_EVENT(spi_hid, spi_hid_create_device, TP_PROTO(struct spi_hid *shid),
+ TP_ARGS(shid));
+
+DEFINE_EVENT(spi_hid, spi_hid_refresh_device, TP_PROTO(struct spi_hid *shid),
+ TP_ARGS(shid));
+
+DEFINE_EVENT(spi_hid, spi_hid_response_handler, TP_PROTO(struct spi_hid *shid),
+ TP_ARGS(shid));
+
+DEFINE_EVENT(spi_hid, spi_hid_error_handler, TP_PROTO(struct spi_hid *shid),
+ TP_ARGS(shid));
+
+#endif /* _SPI_HID_TRACE_H */
+
+/*
+ * The following must be outside the protection of the above #if block.
+ */
+#undef TRACE_INCLUDE_PATH
+#undef TRACE_INCLUDE_FILE
+#define TRACE_INCLUDE_PATH .
+
+/*
+ * It is required that the TRACE_INCLUDE_FILE be the same
+ * as this file without the ".h".
+ */
+#define TRACE_INCLUDE_FILE spi-hid-trace
+#include <trace/define_trace.h>
--
2.53.0.1185.g05d4b7b318-goog
^ permalink raw reply related
* [PATCH v3 07/11] HID: spi_hid: add ACPI support for SPI over HID
From: Jingyuan Liang @ 2026-04-02 1:59 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-input, linux-doc, linux-kernel, linux-spi,
linux-trace-kernel, devicetree, hbarnor, tfiga, Jingyuan Liang,
Angela Czubak
In-Reply-To: <20260402-send-upstream-v3-0-6091c458d357@chromium.org>
From: Angela Czubak <acz@semihalf.com>
Detect SPI HID devices described in ACPI.
Signed-off-by: Angela Czubak <acz@semihalf.com>
Reviewed-by: Dmitry Torokhov <dmitry.torokhov@gmail.com>
Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
---
drivers/hid/spi-hid/Kconfig | 15 +++
drivers/hid/spi-hid/Makefile | 1 +
drivers/hid/spi-hid/spi-hid-acpi.c | 253 +++++++++++++++++++++++++++++++++++++
drivers/hid/spi-hid/spi-hid-core.c | 26 +---
drivers/hid/spi-hid/spi-hid.h | 45 +++++++
5 files changed, 315 insertions(+), 25 deletions(-)
diff --git a/drivers/hid/spi-hid/Kconfig b/drivers/hid/spi-hid/Kconfig
index 836fdefe8345..114b1e00da39 100644
--- a/drivers/hid/spi-hid/Kconfig
+++ b/drivers/hid/spi-hid/Kconfig
@@ -10,6 +10,21 @@ menuconfig SPI_HID
if SPI_HID
+config SPI_HID_ACPI
+ tristate "HID over SPI transport layer ACPI driver"
+ depends on ACPI
+ select SPI_HID_CORE
+ help
+ Say Y here if you use a keyboard, a touchpad, a touchscreen, or any
+ other HID based devices which are connected to your computer via SPI.
+ This driver supports ACPI-based systems.
+
+ If unsure, say N.
+
+ This support is also available as a module. If so, the module
+ will be called spi-hid-acpi. It will also build/depend on the
+ module spi-hid.
+
config SPI_HID_CORE
tristate
endif
diff --git a/drivers/hid/spi-hid/Makefile b/drivers/hid/spi-hid/Makefile
index 733e006df56e..3ca326602643 100644
--- a/drivers/hid/spi-hid/Makefile
+++ b/drivers/hid/spi-hid/Makefile
@@ -8,3 +8,4 @@
obj-$(CONFIG_SPI_HID_CORE) += spi-hid.o
spi-hid-objs = spi-hid-core.o
CFLAGS_spi-hid-core.o := -I$(src)
+obj-$(CONFIG_SPI_HID_ACPI) += spi-hid-acpi.o
diff --git a/drivers/hid/spi-hid/spi-hid-acpi.c b/drivers/hid/spi-hid/spi-hid-acpi.c
new file mode 100644
index 000000000000..298e3ba44d8a
--- /dev/null
+++ b/drivers/hid/spi-hid/spi-hid-acpi.c
@@ -0,0 +1,253 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * HID over SPI protocol, ACPI related code
+ *
+ * Copyright (c) 2021 Microsoft Corporation
+ * Copyright (c) 2026 Google LLC
+ *
+ * This code was forked out of the HID over SPI core code, which is partially
+ * based on "HID over I2C protocol implementation:
+ *
+ * Copyright (c) 2012 Benjamin Tissoires <benjamin.tissoires@gmail.com>
+ * Copyright (c) 2012 Ecole Nationale de l'Aviation Civile, France
+ * Copyright (c) 2012 Red Hat, Inc
+ *
+ * which in turn is partially based on "USB HID support for Linux":
+ *
+ * Copyright (c) 1999 Andreas Gal
+ * Copyright (c) 2000-2005 Vojtech Pavlik <vojtech@suse.cz>
+ * Copyright (c) 2005 Michael Haboustak <mike-@cinci.rr.com> for Concept2, Inc
+ * Copyright (c) 2007-2008 Oliver Neukum
+ * Copyright (c) 2006-2010 Jiri Kosina
+ */
+
+#include <linux/acpi.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/reset.h>
+#include <linux/uuid.h>
+
+#include "spi-hid.h"
+
+/* Config structure is filled with data from ACPI */
+struct spi_hid_acpi_config {
+ struct spihid_ops ops;
+
+ struct spi_hid_conf property_conf;
+ u32 post_power_on_delay_ms;
+ u32 minimal_reset_delay_ms;
+ struct acpi_device *adev;
+};
+
+/* HID SPI Device: 6e2ac436-0fcf41af-a265-b32a220dcfab */
+static guid_t spi_hid_guid =
+ GUID_INIT(0x6E2AC436, 0x0FCF, 0x41AF,
+ 0xA2, 0x65, 0xB3, 0x2A, 0x22, 0x0D, 0xCF, 0xAB);
+
+static int spi_hid_acpi_populate_config(struct spi_hid_acpi_config *conf,
+ struct acpi_device *adev)
+{
+ acpi_handle handle = acpi_device_handle(adev);
+ union acpi_object *obj;
+
+ conf->adev = adev;
+
+ /* Revision 3 for HID over SPI V1, see specification. */
+ obj = acpi_evaluate_dsm_typed(handle, &spi_hid_guid, 3, 1, NULL,
+ ACPI_TYPE_INTEGER);
+ if (!obj) {
+ acpi_handle_err(handle,
+ "Error _DSM call to get HID input report header address failed");
+ return -ENODEV;
+ }
+ conf->property_conf.input_report_header_address = obj->integer.value;
+ ACPI_FREE(obj);
+
+ obj = acpi_evaluate_dsm_typed(handle, &spi_hid_guid, 3, 2, NULL,
+ ACPI_TYPE_INTEGER);
+ if (!obj) {
+ acpi_handle_err(handle,
+ "Error _DSM call to get HID input report body address failed");
+ return -ENODEV;
+ }
+ conf->property_conf.input_report_body_address = obj->integer.value;
+ ACPI_FREE(obj);
+
+ obj = acpi_evaluate_dsm_typed(handle, &spi_hid_guid, 3, 3, NULL,
+ ACPI_TYPE_INTEGER);
+ if (!obj) {
+ acpi_handle_err(handle,
+ "Error _DSM call to get HID output report header address failed");
+ return -ENODEV;
+ }
+ conf->property_conf.output_report_address = obj->integer.value;
+ ACPI_FREE(obj);
+
+ obj = acpi_evaluate_dsm_typed(handle, &spi_hid_guid, 3, 4, NULL,
+ ACPI_TYPE_BUFFER);
+ if (!obj) {
+ acpi_handle_err(handle,
+ "Error _DSM call to get HID read opcode failed");
+ return -ENODEV;
+ }
+ if (obj->buffer.length == 1) {
+ conf->property_conf.read_opcode = obj->buffer.pointer[0];
+ } else {
+ acpi_handle_err(handle,
+ "Error _DSM call to get HID read opcode, too long buffer");
+ ACPI_FREE(obj);
+ return -ENODEV;
+ }
+ ACPI_FREE(obj);
+
+ obj = acpi_evaluate_dsm_typed(handle, &spi_hid_guid, 3, 5, NULL,
+ ACPI_TYPE_BUFFER);
+ if (!obj) {
+ acpi_handle_err(handle,
+ "Error _DSM call to get HID write opcode failed");
+ return -ENODEV;
+ }
+ if (obj->buffer.length == 1) {
+ conf->property_conf.write_opcode = obj->buffer.pointer[0];
+ } else {
+ acpi_handle_err(handle,
+ "Error _DSM call to get HID write opcode, too long buffer");
+ ACPI_FREE(obj);
+ return -ENODEV;
+ }
+ ACPI_FREE(obj);
+
+ /* Value not provided in ACPI,*/
+ conf->post_power_on_delay_ms = 5;
+ conf->minimal_reset_delay_ms = 150;
+
+ if (!acpi_has_method(handle, "_RST")) {
+ acpi_handle_err(handle, "No reset method for acpi handle");
+ return -EINVAL;
+ }
+
+ /* FIXME: not reading hid-over-spi-flags, multi-SPI not supported */
+
+ return 0;
+}
+
+static int spi_hid_acpi_power_none(struct spihid_ops *ops)
+{
+ return 0;
+}
+
+static int spi_hid_acpi_power_down(struct spihid_ops *ops)
+{
+ struct spi_hid_acpi_config *conf = container_of(ops,
+ struct spi_hid_acpi_config,
+ ops);
+
+ return acpi_device_set_power(conf->adev, ACPI_STATE_D3);
+}
+
+static int spi_hid_acpi_power_up(struct spihid_ops *ops)
+{
+ struct spi_hid_acpi_config *conf = container_of(ops,
+ struct spi_hid_acpi_config,
+ ops);
+ int error;
+
+ error = acpi_device_set_power(conf->adev, ACPI_STATE_D0);
+ if (error) {
+ dev_err(&conf->adev->dev, "Error could not power up ACPI device: %d.", error);
+ return error;
+ }
+
+ if (conf->post_power_on_delay_ms)
+ msleep(conf->post_power_on_delay_ms);
+
+ return 0;
+}
+
+static int spi_hid_acpi_assert_reset(struct spihid_ops *ops)
+{
+ return 0;
+}
+
+static int spi_hid_acpi_deassert_reset(struct spihid_ops *ops)
+{
+ struct spi_hid_acpi_config *conf = container_of(ops,
+ struct spi_hid_acpi_config,
+ ops);
+
+ return device_reset(&conf->adev->dev);
+}
+
+static void spi_hid_acpi_sleep_minimal_reset_delay(struct spihid_ops *ops)
+{
+ struct spi_hid_acpi_config *conf = container_of(ops,
+ struct spi_hid_acpi_config,
+ ops);
+ fsleep(1000 * conf->minimal_reset_delay_ms);
+}
+
+static int spi_hid_acpi_probe(struct spi_device *spi)
+{
+ struct device *dev = &spi->dev;
+ struct acpi_device *adev;
+ struct spi_hid_acpi_config *config;
+ int error;
+
+ adev = ACPI_COMPANION(dev);
+ if (!adev) {
+ dev_err(dev, "Error could not get ACPI device.");
+ return -ENODEV;
+ }
+
+ config = devm_kzalloc(dev, sizeof(struct spi_hid_acpi_config),
+ GFP_KERNEL);
+ if (!config)
+ return -ENOMEM;
+
+ if (acpi_device_power_manageable(adev)) {
+ config->ops.power_up = spi_hid_acpi_power_up;
+ config->ops.power_down = spi_hid_acpi_power_down;
+ } else {
+ config->ops.power_up = spi_hid_acpi_power_none;
+ config->ops.power_down = spi_hid_acpi_power_none;
+ }
+ config->ops.assert_reset = spi_hid_acpi_assert_reset;
+ config->ops.deassert_reset = spi_hid_acpi_deassert_reset;
+ config->ops.sleep_minimal_reset_delay =
+ spi_hid_acpi_sleep_minimal_reset_delay;
+
+ error = spi_hid_acpi_populate_config(config, adev);
+ if (error) {
+ dev_err(dev, "%s: unable to populate config data.", __func__);
+ return error;
+ }
+
+ return spi_hid_core_probe(spi, &config->ops, &config->property_conf);
+}
+
+static const struct acpi_device_id spi_hid_acpi_match[] = {
+ { "ACPI0C51", 0 },
+ { "PNP0C51", 0 },
+ { }
+};
+MODULE_DEVICE_TABLE(acpi, spi_hid_acpi_match);
+
+static struct spi_driver spi_hid_acpi_driver = {
+ .driver = {
+ .name = "spi_hid_acpi",
+ .owner = THIS_MODULE,
+ .acpi_match_table = spi_hid_acpi_match,
+ .probe_type = PROBE_PREFER_ASYNCHRONOUS,
+ .dev_groups = spi_hid_groups,
+ },
+ .probe = spi_hid_acpi_probe,
+ .remove = spi_hid_core_remove,
+};
+
+module_spi_driver(spi_hid_acpi_driver);
+
+MODULE_DESCRIPTION("HID over SPI ACPI transport driver");
+MODULE_AUTHOR("Angela Czubak <aczubak@google.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/hid/spi-hid/spi-hid-core.c b/drivers/hid/spi-hid/spi-hid-core.c
index 802615565541..d48175c764b9 100644
--- a/drivers/hid/spi-hid/spi-hid-core.c
+++ b/drivers/hid/spi-hid/spi-hid-core.c
@@ -43,6 +43,7 @@
#include <linux/wait.h>
#include <linux/workqueue.h>
+#include "spi-hid.h"
#include "spi-hid-core.h"
#define CREATE_TRACE_POINTS
@@ -110,31 +111,6 @@ struct spi_hid_output_report {
u8 *content;
};
-/* struct spi_hid_conf - Conf provided to the core */
-struct spi_hid_conf {
- u32 input_report_header_address;
- u32 input_report_body_address;
- u32 output_report_address;
- u8 read_opcode;
- u8 write_opcode;
-};
-
-/**
- * struct spihid_ops - Ops provided to the core
- * @power_up: do sequencing to power up the device
- * @power_down: do sequencing to power down the device
- * @assert_reset: do sequencing to assert the reset line
- * @deassert_reset: do sequencing to deassert the reset line
- * @sleep_minimal_reset_delay: minimal sleep delay during reset
- */
-struct spihid_ops {
- int (*power_up)(struct spihid_ops *ops);
- int (*power_down)(struct spihid_ops *ops);
- int (*assert_reset)(struct spihid_ops *ops);
- int (*deassert_reset)(struct spihid_ops *ops);
- void (*sleep_minimal_reset_delay)(struct spihid_ops *ops);
-};
-
static struct hid_ll_driver spi_hid_ll_driver;
static void spi_hid_populate_read_approvals(const struct spi_hid_conf *conf,
diff --git a/drivers/hid/spi-hid/spi-hid.h b/drivers/hid/spi-hid/spi-hid.h
new file mode 100644
index 000000000000..f5a5f4d54beb
--- /dev/null
+++ b/drivers/hid/spi-hid/spi-hid.h
@@ -0,0 +1,45 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2021 Microsoft Corporation
+ * Copyright (c) 2026 Google LLC
+ */
+
+#ifndef SPI_HID_H
+#define SPI_HID_H
+
+#include <linux/spi/spi.h>
+#include <linux/sysfs.h>
+
+/* struct spi_hid_conf - Conf provided to the core */
+struct spi_hid_conf {
+ u32 input_report_header_address;
+ u32 input_report_body_address;
+ u32 output_report_address;
+ u8 read_opcode;
+ u8 write_opcode;
+};
+
+/**
+ * struct spihid_ops - Ops provided to the core
+ * @power_up: do sequencing to power up the device
+ * @power_down: do sequencing to power down the device
+ * @assert_reset: do sequencing to assert the reset line
+ * @deassert_reset: do sequencing to deassert the reset line
+ * @sleep_minimal_reset_delay: minimal sleep delay during reset
+ */
+struct spihid_ops {
+ int (*power_up)(struct spihid_ops *ops);
+ int (*power_down)(struct spihid_ops *ops);
+ int (*assert_reset)(struct spihid_ops *ops);
+ int (*deassert_reset)(struct spihid_ops *ops);
+ void (*sleep_minimal_reset_delay)(struct spihid_ops *ops);
+};
+
+int spi_hid_core_probe(struct spi_device *spi, struct spihid_ops *ops,
+ struct spi_hid_conf *conf);
+
+void spi_hid_core_remove(struct spi_device *spi);
+
+extern const struct attribute_group *spi_hid_groups[];
+
+#endif /* SPI_HID_H */
--
2.53.0.1185.g05d4b7b318-goog
^ permalink raw reply related
* [PATCH v3 08/11] HID: spi_hid: add device tree support for SPI over HID
From: Jingyuan Liang @ 2026-04-02 1:59 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-input, linux-doc, linux-kernel, linux-spi,
linux-trace-kernel, devicetree, hbarnor, tfiga, Jingyuan Liang,
Jarrett Schultz, Dmitry Antipov
In-Reply-To: <20260402-send-upstream-v3-0-6091c458d357@chromium.org>
From: Jarrett Schultz <jaschultz@microsoft.com>
Signed-off-by: Dmitry Antipov <dmanti@microsoft.com>
Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
---
drivers/hid/spi-hid/Kconfig | 15 +++
drivers/hid/spi-hid/Makefile | 1 +
drivers/hid/spi-hid/spi-hid-of.c | 243 +++++++++++++++++++++++++++++++++++++++
3 files changed, 259 insertions(+)
diff --git a/drivers/hid/spi-hid/Kconfig b/drivers/hid/spi-hid/Kconfig
index 114b1e00da39..76a2cd587a3e 100644
--- a/drivers/hid/spi-hid/Kconfig
+++ b/drivers/hid/spi-hid/Kconfig
@@ -25,6 +25,21 @@ config SPI_HID_ACPI
will be called spi-hid-acpi. It will also build/depend on the
module spi-hid.
+config SPI_HID_OF
+ tristate "HID over SPI transport layer Open Firmware driver"
+ depends on OF
+ select SPI_HID_CORE
+ help
+ Say Y here if you use a keyboard, a touchpad, a touchscreen, or any
+ other HID based devices which are connected to your computer via SPI.
+ This driver supports Open Firmware (Device Tree)-based systems.
+
+ If unsure, say N.
+
+ This support is also available as a module. If so, the module
+ will be called spi-hid-of. It will also build/depend on the
+ module spi-hid.
+
config SPI_HID_CORE
tristate
endif
diff --git a/drivers/hid/spi-hid/Makefile b/drivers/hid/spi-hid/Makefile
index 3ca326602643..31192e71edae 100644
--- a/drivers/hid/spi-hid/Makefile
+++ b/drivers/hid/spi-hid/Makefile
@@ -9,3 +9,4 @@ obj-$(CONFIG_SPI_HID_CORE) += spi-hid.o
spi-hid-objs = spi-hid-core.o
CFLAGS_spi-hid-core.o := -I$(src)
obj-$(CONFIG_SPI_HID_ACPI) += spi-hid-acpi.o
+obj-$(CONFIG_SPI_HID_OF) += spi-hid-of.o
diff --git a/drivers/hid/spi-hid/spi-hid-of.c b/drivers/hid/spi-hid/spi-hid-of.c
new file mode 100644
index 000000000000..651456b6906d
--- /dev/null
+++ b/drivers/hid/spi-hid/spi-hid-of.c
@@ -0,0 +1,243 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * HID over SPI protocol, Open Firmware related code
+ *
+ * Copyright (c) 2021 Microsoft Corporation
+ *
+ * This code was forked out of the HID over SPI core code, which is partially
+ * based on "HID over I2C protocol implementation:
+ *
+ * Copyright (c) 2012 Benjamin Tissoires <benjamin.tissoires@gmail.com>
+ * Copyright (c) 2012 Ecole Nationale de l'Aviation Civile, France
+ * Copyright (c) 2012 Red Hat, Inc
+ *
+ * which in turn is partially based on "USB HID support for Linux":
+ *
+ * Copyright (c) 1999 Andreas Gal
+ * Copyright (c) 2000-2005 Vojtech Pavlik <vojtech@suse.cz>
+ * Copyright (c) 2005 Michael Haboustak <mike-@cinci.rr.com> for Concept2, Inc
+ * Copyright (c) 2007-2008 Oliver Neukum
+ * Copyright (c) 2006-2010 Jiri Kosina
+ */
+
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/regulator/consumer.h>
+#include <linux/gpio/consumer.h>
+#include <linux/delay.h>
+
+#include "spi-hid.h"
+
+struct spi_hid_timing_data {
+ u32 post_power_on_delay_ms;
+ u32 minimal_reset_delay_ms;
+};
+
+/* Config structure is filled with data from Device Tree */
+struct spi_hid_of_config {
+ struct spihid_ops ops;
+
+ struct spi_hid_conf property_conf;
+ const struct spi_hid_timing_data *timing_data;
+
+ struct gpio_desc *reset_gpio;
+ struct regulator *supply;
+ bool supply_enabled;
+ u16 hid_over_spi_flags;
+};
+
+static int spi_hid_of_populate_config(struct spi_hid_of_config *conf,
+ struct device *dev)
+{
+ int error;
+ u32 val;
+
+ error = device_property_read_u32(dev, "input-report-header-address",
+ &val);
+ if (error) {
+ dev_err(dev, "Input report header address not provided.");
+ return -ENODEV;
+ }
+ conf->property_conf.input_report_header_address = val;
+
+ error = device_property_read_u32(dev, "input-report-body-address", &val);
+ if (error) {
+ dev_err(dev, "Input report body address not provided.");
+ return -ENODEV;
+ }
+ conf->property_conf.input_report_body_address = val;
+
+ error = device_property_read_u32(dev, "output-report-address", &val);
+ if (error) {
+ dev_err(dev, "Output report address not provided.");
+ return -ENODEV;
+ }
+ conf->property_conf.output_report_address = val;
+
+ error = device_property_read_u32(dev, "read-opcode", &val);
+ if (error) {
+ dev_err(dev, "Read opcode not provided.");
+ return -ENODEV;
+ }
+ conf->property_conf.read_opcode = val;
+
+ error = device_property_read_u32(dev, "write-opcode", &val);
+ if (error) {
+ dev_err(dev, "Write opcode not provided.");
+ return -ENODEV;
+ }
+ conf->property_conf.write_opcode = val;
+
+ conf->supply = devm_regulator_get(dev, "vdd");
+ if (IS_ERR(conf->supply)) {
+ if (PTR_ERR(conf->supply) != -EPROBE_DEFER)
+ dev_err(dev, "Failed to get regulator: %ld.",
+ PTR_ERR(conf->supply));
+ return PTR_ERR(conf->supply);
+ }
+ conf->supply_enabled = false;
+
+ conf->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW);
+ if (IS_ERR(conf->reset_gpio)) {
+ dev_err(dev, "%s: error getting reset GPIO.", __func__);
+ return PTR_ERR(conf->reset_gpio);
+ }
+
+ return 0;
+}
+
+static int spi_hid_of_power_down(struct spihid_ops *ops)
+{
+ struct spi_hid_of_config *conf = container_of(ops,
+ struct spi_hid_of_config,
+ ops);
+ int error;
+
+ if (!conf->supply_enabled)
+ return 0;
+
+ error = regulator_disable(conf->supply);
+ if (error == 0)
+ conf->supply_enabled = false;
+
+ return error;
+}
+
+static int spi_hid_of_power_up(struct spihid_ops *ops)
+{
+ struct spi_hid_of_config *conf = container_of(ops,
+ struct spi_hid_of_config,
+ ops);
+ int error;
+
+ if (conf->supply_enabled)
+ return 0;
+
+ error = regulator_enable(conf->supply);
+
+ if (error == 0) {
+ conf->supply_enabled = true;
+ fsleep(1000 * conf->timing_data->post_power_on_delay_ms);
+ }
+
+ return error;
+}
+
+static int spi_hid_of_assert_reset(struct spihid_ops *ops)
+{
+ struct spi_hid_of_config *conf = container_of(ops,
+ struct spi_hid_of_config,
+ ops);
+
+ gpiod_set_value(conf->reset_gpio, 1);
+ return 0;
+}
+
+static int spi_hid_of_deassert_reset(struct spihid_ops *ops)
+{
+ struct spi_hid_of_config *conf = container_of(ops,
+ struct spi_hid_of_config,
+ ops);
+
+ gpiod_set_value(conf->reset_gpio, 0);
+ return 0;
+}
+
+static void spi_hid_of_sleep_minimal_reset_delay(struct spihid_ops *ops)
+{
+ struct spi_hid_of_config *conf = container_of(ops,
+ struct spi_hid_of_config,
+ ops);
+ fsleep(1000 * conf->timing_data->minimal_reset_delay_ms);
+}
+
+static int spi_hid_of_probe(struct spi_device *spi)
+{
+ struct device *dev = &spi->dev;
+ struct spi_hid_of_config *config;
+ int error;
+
+ config = devm_kzalloc(dev, sizeof(struct spi_hid_of_config),
+ GFP_KERNEL);
+ if (!config)
+ return -ENOMEM;
+
+ config->ops.power_up = spi_hid_of_power_up;
+ config->ops.power_down = spi_hid_of_power_down;
+ config->ops.assert_reset = spi_hid_of_assert_reset;
+ config->ops.deassert_reset = spi_hid_of_deassert_reset;
+ config->ops.sleep_minimal_reset_delay =
+ spi_hid_of_sleep_minimal_reset_delay;
+
+ config->timing_data = device_get_match_data(dev);
+ /*
+ * FIXME: hid_over_spi_flags could be retrieved from spi mode.
+ * It is always 0 because multi-SPI not supported.
+ */
+ config->hid_over_spi_flags = 0;
+
+ error = spi_hid_of_populate_config(config, dev);
+ if (error) {
+ dev_err(dev, "%s: unable to populate config data.", __func__);
+ return error;
+ }
+
+ return spi_hid_core_probe(spi, &config->ops, &config->property_conf);
+}
+
+const struct spi_hid_timing_data timing_data = {
+ .post_power_on_delay_ms = 10,
+ .minimal_reset_delay_ms = 100,
+};
+
+const struct of_device_id spi_hid_of_match[] = {
+ { .compatible = "hid-over-spi", .data = &timing_data },
+ {}
+};
+MODULE_DEVICE_TABLE(of, spi_hid_of_match);
+
+static const struct spi_device_id spi_hid_of_id_table[] = {
+ { "hid", 0 },
+ { "hid-over-spi", 0 },
+ { }
+};
+MODULE_DEVICE_TABLE(spi, spi_hid_of_id_table);
+
+static struct spi_driver spi_hid_of_driver = {
+ .driver = {
+ .name = "spi_hid_of",
+ .owner = THIS_MODULE,
+ .of_match_table = spi_hid_of_match,
+ .probe_type = PROBE_PREFER_ASYNCHRONOUS,
+ .dev_groups = spi_hid_groups,
+ },
+ .probe = spi_hid_of_probe,
+ .remove = spi_hid_core_remove,
+ .id_table = spi_hid_of_id_table,
+};
+
+module_spi_driver(spi_hid_of_driver);
+
+MODULE_DESCRIPTION("HID over SPI OF transport driver");
+MODULE_AUTHOR("Dmitry Antipov <dmanti@microsoft.com>");
+MODULE_LICENSE("GPL");
--
2.53.0.1185.g05d4b7b318-goog
^ permalink raw reply related
* [PATCH v3 10/11] HID: spi-hid: add power management implementation
From: Jingyuan Liang @ 2026-04-02 1:59 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-input, linux-doc, linux-kernel, linux-spi,
linux-trace-kernel, devicetree, hbarnor, tfiga, Jingyuan Liang
In-Reply-To: <20260402-send-upstream-v3-0-6091c458d357@chromium.org>
Implement HID over SPI driver power management callbacks.
Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
---
drivers/hid/spi-hid/spi-hid-acpi.c | 1 +
drivers/hid/spi-hid/spi-hid-core.c | 107 +++++++++++++++++++++++++++++++++++++
drivers/hid/spi-hid/spi-hid-of.c | 1 +
drivers/hid/spi-hid/spi-hid.h | 1 +
4 files changed, 110 insertions(+)
diff --git a/drivers/hid/spi-hid/spi-hid-acpi.c b/drivers/hid/spi-hid/spi-hid-acpi.c
index 298e3ba44d8a..15cfc4e6cc2f 100644
--- a/drivers/hid/spi-hid/spi-hid-acpi.c
+++ b/drivers/hid/spi-hid/spi-hid-acpi.c
@@ -238,6 +238,7 @@ static struct spi_driver spi_hid_acpi_driver = {
.driver = {
.name = "spi_hid_acpi",
.owner = THIS_MODULE,
+ .pm = &spi_hid_core_pm,
.acpi_match_table = spi_hid_acpi_match,
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
.dev_groups = spi_hid_groups,
diff --git a/drivers/hid/spi-hid/spi-hid-core.c b/drivers/hid/spi-hid/spi-hid-core.c
index d48175c764b9..5f7a5bb692d9 100644
--- a/drivers/hid/spi-hid/spi-hid-core.c
+++ b/drivers/hid/spi-hid/spi-hid-core.c
@@ -35,6 +35,8 @@
#include <linux/list.h>
#include <linux/module.h>
#include <linux/mutex.h>
+#include <linux/pm.h>
+#include <linux/pm_wakeirq.h>
#include <linux/slab.h>
#include <linux/spi/spi.h>
#include <linux/string.h>
@@ -244,6 +246,81 @@ static const char *spi_hid_power_mode_string(enum hidspi_power_state power_state
}
}
+static void spi_hid_suspend(struct spi_hid *shid)
+{
+ int error;
+ struct device *dev = &shid->spi->dev;
+
+ guard(mutex)(&shid->power_lock);
+ if (shid->power_state == HIDSPI_OFF)
+ return;
+
+ if (shid->hid) {
+ error = hid_driver_suspend(shid->hid, PMSG_SUSPEND);
+ if (error) {
+ dev_err(dev, "%s failed to suspend hid driver: %d",
+ __func__, error);
+ return;
+ }
+ }
+
+ disable_irq(shid->spi->irq);
+
+ clear_bit(SPI_HID_READY, &shid->flags);
+
+ if (!device_may_wakeup(dev)) {
+ set_bit(SPI_HID_RESET_PENDING, &shid->flags);
+
+ shid->ops->assert_reset(shid->ops);
+
+ error = shid->ops->power_down(shid->ops);
+ if (error) {
+ dev_err(dev, "%s: could not power down.", __func__);
+ shid->regulator_error_count++;
+ shid->regulator_last_error = error;
+ return;
+ }
+
+ shid->power_state = HIDSPI_OFF;
+ }
+}
+
+static void spi_hid_resume(struct spi_hid *shid)
+{
+ int error;
+ struct device *dev = &shid->spi->dev;
+
+ guard(mutex)(&shid->power_lock);
+ if (shid->power_state == HIDSPI_ON)
+ return;
+
+ enable_irq(shid->spi->irq);
+
+ if (!device_may_wakeup(dev)) {
+ shid->ops->assert_reset(shid->ops);
+
+ shid->ops->sleep_minimal_reset_delay(shid->ops);
+
+ error = shid->ops->power_up(shid->ops);
+ if (error) {
+ dev_err(dev, "%s: could not power up.", __func__);
+ shid->regulator_error_count++;
+ shid->regulator_last_error = error;
+ return;
+ }
+ shid->power_state = HIDSPI_ON;
+
+ shid->ops->deassert_reset(shid->ops);
+ }
+
+ if (shid->hid) {
+ error = hid_driver_reset_resume(shid->hid);
+ if (error)
+ dev_err(dev, "%s: failed to reset resume hid driver: %d.",
+ __func__, error);
+ }
+}
+
static void spi_hid_stop_hid(struct spi_hid *shid)
{
struct hid_device *hid = shid->hid;
@@ -1200,6 +1277,13 @@ int spi_hid_core_probe(struct spi_device *spi, struct spihid_ops *ops,
dev_err(dev, "%s: unable to request threaded IRQ.", __func__);
return error;
}
+ if (device_may_wakeup(dev)) {
+ error = dev_pm_set_wake_irq(dev, spi->irq);
+ if (error) {
+ dev_err(dev, "%s: failed to set wake IRQ.", __func__);
+ return error;
+ }
+ }
error = shid->ops->power_up(shid->ops);
if (error) {
@@ -1231,6 +1315,29 @@ void spi_hid_core_remove(struct spi_device *spi)
}
EXPORT_SYMBOL_GPL(spi_hid_core_remove);
+static int spi_hid_core_pm_suspend(struct device *dev)
+{
+ struct spi_hid *shid = dev_get_drvdata(dev);
+
+ spi_hid_suspend(shid);
+
+ return 0;
+}
+
+static int spi_hid_core_pm_resume(struct device *dev)
+{
+ struct spi_hid *shid = dev_get_drvdata(dev);
+
+ spi_hid_resume(shid);
+
+ return 0;
+}
+
+const struct dev_pm_ops spi_hid_core_pm = {
+ SYSTEM_SLEEP_PM_OPS(spi_hid_core_pm_suspend, spi_hid_core_pm_resume)
+};
+EXPORT_SYMBOL_GPL(spi_hid_core_pm);
+
MODULE_DESCRIPTION("HID over SPI transport driver");
MODULE_AUTHOR("Dmitry Antipov <dmanti@microsoft.com>");
MODULE_LICENSE("GPL");
diff --git a/drivers/hid/spi-hid/spi-hid-of.c b/drivers/hid/spi-hid/spi-hid-of.c
index 651456b6906d..80c481b77149 100644
--- a/drivers/hid/spi-hid/spi-hid-of.c
+++ b/drivers/hid/spi-hid/spi-hid-of.c
@@ -227,6 +227,7 @@ static struct spi_driver spi_hid_of_driver = {
.driver = {
.name = "spi_hid_of",
.owner = THIS_MODULE,
+ .pm = &spi_hid_core_pm,
.of_match_table = spi_hid_of_match,
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
.dev_groups = spi_hid_groups,
diff --git a/drivers/hid/spi-hid/spi-hid.h b/drivers/hid/spi-hid/spi-hid.h
index f5a5f4d54beb..17b2fdf192ed 100644
--- a/drivers/hid/spi-hid/spi-hid.h
+++ b/drivers/hid/spi-hid/spi-hid.h
@@ -41,5 +41,6 @@ int spi_hid_core_probe(struct spi_device *spi, struct spihid_ops *ops,
void spi_hid_core_remove(struct spi_device *spi);
extern const struct attribute_group *spi_hid_groups[];
+extern const struct dev_pm_ops spi_hid_core_pm;
#endif /* SPI_HID_H */
--
2.53.0.1185.g05d4b7b318-goog
^ permalink raw reply related
* [PATCH v3 09/11] dt-bindings: input: Document hid-over-spi DT schema
From: Jingyuan Liang @ 2026-04-02 1:59 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-input, linux-doc, linux-kernel, linux-spi,
linux-trace-kernel, devicetree, hbarnor, tfiga, Jingyuan Liang,
Dmitry Antipov, Jarrett Schultz
In-Reply-To: <20260402-send-upstream-v3-0-6091c458d357@chromium.org>
Documentation describes the required and optional properties for
implementing Device Tree for a Microsoft G6 Touch Digitizer that
supports HID over SPI Protocol 1.0 specification.
The properties are common to HID over SPI.
Signed-off-by: Dmitry Antipov <dmanti@microsoft.com>
Signed-off-by: Jarrett Schultz <jaschultz@microsoft.com>
Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
---
.../devicetree/bindings/input/hid-over-spi.yaml | 126 +++++++++++++++++++++
1 file changed, 126 insertions(+)
diff --git a/Documentation/devicetree/bindings/input/hid-over-spi.yaml b/Documentation/devicetree/bindings/input/hid-over-spi.yaml
new file mode 100644
index 000000000000..d1b0a2e26c32
--- /dev/null
+++ b/Documentation/devicetree/bindings/input/hid-over-spi.yaml
@@ -0,0 +1,126 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/input/hid-over-spi.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: HID over SPI Devices
+
+maintainers:
+ - Benjamin Tissoires <benjamin.tissoires@redhat.com>
+ - Jiri Kosina <jkosina@suse.cz>
+
+description: |+
+ HID over SPI provides support for various Human Interface Devices over the
+ SPI bus. These devices can be for example touchpads, keyboards, touch screens
+ or sensors.
+
+ The specification has been written by Microsoft and is currently available
+ here: https://www.microsoft.com/en-us/download/details.aspx?id=103325
+
+ If this binding is used, the kernel module spi-hid will handle the
+ communication with the device and the generic hid core layer will handle the
+ protocol.
+
+allOf:
+ - $ref: /schemas/input/touchscreen/touchscreen.yaml#
+
+properties:
+ compatible:
+ oneOf:
+ - items:
+ - enum:
+ - microsoft,g6-touch-digitizer
+ - const: hid-over-spi
+ - description: Just "hid-over-spi" alone is allowed, but not recommended.
+ const: hid-over-spi
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ maxItems: 1
+
+ reset-gpios:
+ maxItems: 1
+ description:
+ GPIO specifier for the digitizer's reset pin (active low). The line must
+ be flagged with GPIO_ACTIVE_LOW.
+
+ vdd-supply:
+ description:
+ Regulator for the VDD supply voltage.
+
+ input-report-header-address:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ minimum: 0
+ maximum: 0xffffff
+ description:
+ A value to be included in the Read Approval packet, listing an address of
+ the input report header to be put on the SPI bus. This address has 24
+ bits.
+
+ input-report-body-address:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ minimum: 0
+ maximum: 0xffffff
+ description:
+ A value to be included in the Read Approval packet, listing an address of
+ the input report body to be put on the SPI bus. This address has 24 bits.
+
+ output-report-address:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ minimum: 0
+ maximum: 0xffffff
+ description:
+ A value to be included in the Output Report sent by the host, listing an
+ address where the output report on the SPI bus is to be written to. This
+ address has 24 bits.
+
+ read-opcode:
+ $ref: /schemas/types.yaml#/definitions/uint8
+ description:
+ Value to be used in Read Approval packets. 1 byte.
+
+ write-opcode:
+ $ref: /schemas/types.yaml#/definitions/uint8
+ description:
+ Value to be used in Write Approval packets. 1 byte.
+
+required:
+ - compatible
+ - interrupts
+ - reset-gpios
+ - vdd-supply
+ - input-report-header-address
+ - input-report-body-address
+ - output-report-address
+ - read-opcode
+ - write-opcode
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/irq.h>
+ #include <dt-bindings/gpio/gpio.h>
+
+ spi {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ hid@0 {
+ compatible = "microsoft,g6-touch-digitizer", "hid-over-spi";
+ reg = <0x0>;
+ interrupts-extended = <&gpio 42 IRQ_TYPE_EDGE_FALLING>;
+ reset-gpios = <&gpio 27 GPIO_ACTIVE_LOW>;
+ vdd-supply = <&pm8350c_l3>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&ts_d6_int_bias>;
+ input-report-header-address = <0x1000>;
+ input-report-body-address = <0x1004>;
+ output-report-address = <0x2000>;
+ read-opcode = /bits/ 8 <0x0b>;
+ write-opcode = /bits/ 8 <0x02>;
+ };
+ };
--
2.53.0.1185.g05d4b7b318-goog
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox