platform/x86/lenovo: Add Yoga Book 9 keyboard dock detection driver

The Lenovo Yoga Book 9 14IAH10 ships with a detachable Bluetooth keyboard
that magnetically attaches to the bottom (secondary) screen in one of two
positions.  The Embedded Controller tracks the attachment state in a 2-bit
field called BKBD and signals changes via WMI event GUID
806BD2A2-177B-481D-BFB5-3BA0BB4A2285 (notify ID 0xEB on the WM10 ACPI
device, _UID "GMZN").

The device contains embedded BMOF data (WQDD, 20705 bytes) documenting
both WMI interfaces used by this driver:

  LENOVO_BTKBD_EVENT (event GUID): WmiDataId(1) uint32 Status.
  The ACPI _WED(0xEB) method returns EC.BKBD directly as an integer,
  so the notify callback receives BKBD without a separate query.

  LENOVO_FEATURE_STATUS_DATA (block GUID, WQAF method): returns an
  8-byte buffer {uint32 IDs=0x00060000, uint32 Status=BKBD}.
  Used for the initial state read on probe and after resume.

BKBD encoding:
  0 = keyboard detached
  1 = keyboard docked on top half of bottom screen
  2 = keyboard docked on bottom half of bottom screen
  3 = reserved (not observed in practice)

This driver registers two WMI drivers sharing a module-level
BLOCKING_NOTIFIER_HEAD:

  - The event driver (LENOVO_BTKBD_EVENT) uses .notify_new() to receive
    a pre-parsed wmi_buffer and fires the notifier chain with the BKBD
    value extracted from the buffer.

  - The block driver (LENOVO_FEATURE_STATUS_DATA) owns the input_dev in
    its per-device private struct.  At probe time it registers a
    notifier_block on the chain and reads the initial BKBD state via
    wmidev_query_block().  The WMI buffer is parsed as
    struct lenovo_feature_status { __le32 id; __le32 status; }, and the
    ID field is verified before the status is used.

  - SW_TABLET_MODE=1 is reported when the keyboard is detached;
    SW_TABLET_MODE=0 when docked in either position (keyboard present).

  - The raw BKBD value is exposed via read-only sysfs attribute
    "keyboard_position".

  - BKBD state is re-read via wmidev_query_block() on resume from
    suspend or hibernation.

Tested on: Lenovo Yoga Book 9 14IAH10 (model 83KJ), kernel 7.0.

Acked-by: Hans de Goede <johannes.goede@oss.qualcomm.com>
Reviewed-by: Armin Wolf <W_Armin@gmx.de>
Signed-off-by: Dave Carey <carvsdriver@gmail.com>
Link: https://patch.msgid.link/20260728225545.1333610-3-carvsdriver@gmail.com
Reviewed-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
Signed-off-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
This commit is contained in:
Dave Carey
2026-07-28 18:55:44 -04:00
committed by Ilpo Järvinen
parent bdd0f4e333
commit c9b5c8ff06
5 changed files with 364 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
What: /sys/bus/wmi/drivers/lenovo-yb9-kbdock/<guid>/keyboard_position
Date: June 2027
KernelVersion: 7.3
Contact: Dave Carey <carvsdriver@gmail.com>
Description:
Read-only attribute reporting the current keyboard dock position
as reported by the Embedded Controller on the Lenovo Yoga Book 9
14IAH10.
Possible values:
== =============================================================
0 keyboard is not docked to any screen (detached)
1 keyboard docked on the top half of the bottom screen
2 keyboard docked on the bottom half of the bottom screen
== =============================================================
SW_TABLET_MODE input events are also emitted: 0 when the keyboard
is docked (either position), 1 when detached.

View File

@@ -14710,6 +14710,13 @@ F: drivers/hid/hid-lenovo-go-s.c
F: drivers/hid/hid-lenovo-go.c
F: drivers/hid/hid-lenovo.c
LENOVO YOGA BOOK 9 KEYBOARD DOCK DRIVER
M: Dave Carey <carvsdriver@gmail.com>
L: platform-driver-x86@vger.kernel.org
S: Maintained
F: Documentation/ABI/testing/sysfs-driver-lenovo-yb9-kbdock
F: drivers/platform/x86/lenovo/yb9-kbdock.c
LETSKETCH HID TABLET DRIVER
M: Hans de Goede <hansg@kernel.org>
L: linux-input@vger.kernel.org

View File

@@ -43,6 +43,20 @@ config LENOVO_WMI_CAMERA
To compile this driver as a module, choose M here: the module
will be called lenovo-wmi-camera.
config LENOVO_YB9_KBDOCK
tristate "Lenovo Yoga Book 9 keyboard dock detection"
depends on ACPI_WMI
depends on DMI
depends on INPUT
help
Say Y here to enable keyboard dock detection on the Lenovo Yoga Book 9
14IAH10. The detachable Bluetooth keyboard magnetically attaches to
either screen; this driver reports SW_TABLET_MODE input events based
on the attachment state and exposes the raw position in sysfs.
To compile this driver as a module, choose M here: the module will be
called lenovo-yb9-kbdock.
config LENOVO_YMC
tristate "Lenovo Yoga Tablet Mode Control"
depends on ACPI_WMI

View File

@@ -8,6 +8,7 @@ obj-$(CONFIG_THINKPAD_LMI) += think-lmi.o
obj-$(CONFIG_THINKPAD_ACPI) += thinkpad_acpi.o
lenovo-target-$(CONFIG_LENOVO_WMI_HOTKEY_UTILITIES) += wmi-hotkey-utilities.o
lenovo-target-$(CONFIG_LENOVO_YB9_KBDOCK) += yb9-kbdock.o
lenovo-target-$(CONFIG_LENOVO_YMC) += ymc.o
lenovo-target-$(CONFIG_YOGABOOK) += yogabook.o
lenovo-target-$(CONFIG_YT2_1380) += yoga-tab2-pro-1380-fastcharger.o

View File

@@ -0,0 +1,323 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Lenovo Yoga Book 9 keyboard-dock detection
*
* The Yoga Book 9 ships with a detachable Bluetooth keyboard that magnetically
* attaches to the bottom screen in one of two positions. The EC tracks
* attachment state in a 2-bit field called BKBD and signals changes via WMI
* event 0xEB on the WM10 ACPI device (_UID "GMZN").
*
* BKBD values:
* 0 = keyboard detached
* 1 = keyboard docked on the top half of the bottom screen
* 2 = keyboard docked on the bottom half of the bottom screen
* 3 = reserved / not observed
*
* Two WMI interfaces are used (documented in embedded BMOF, WQDD, 20705 bytes):
*
* LENOVO_BTKBD_EVENT (event GUID, 806BD2A2-...)
* WmiDataId(1) uint32 Status _WED(0xEB) returns EC.BKBD directly.
* The notify callback receives BKBD as an integer; no separate query needed.
*
* LENOVO_FEATURE_STATUS_DATA (block GUID, E7F300FA-...)
* WmiDataId(1) uint32 IDs = 0x00060000 (feature selector)
* WmiDataId(2) uint32 Status = BKBD value
* Used on probe and resume to read initial state.
*
* The event driver (LENOVO_BTKBD_EVENT) fires a notifier chain on each WMI
* event. The block driver (LENOVO_FEATURE_STATUS_DATA) owns the input_dev
* and registers a notifier_block to receive those events, eliminating the
* need for shared global state or a mutex.
*
* SW_TABLET_MODE=1 is reported when the keyboard is detached;
* SW_TABLET_MODE=0 when docked in either position (keyboard present).
* The raw BKBD value is exposed via the sysfs attribute "keyboard_position".
*
* Copyright (C) 2026 Dave Carey <carvsdriver@gmail.com>
*/
#include <linux/acpi.h>
#include <linux/cleanup.h>
#include <linux/compiler_attributes.h>
#include <linux/dev_printk.h>
#include <linux/dmi.h>
#include <linux/input.h>
#include <linux/module.h>
#include <linux/notifier.h>
#include <linux/pm.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/sysfs.h>
#include <linux/types.h>
#include <linux/wmi.h>
#define YB9_KBDOCK_EVENT_GUID "806BD2A2-177B-481D-BFB5-3BA0BB4A2285"
#define YB9_KBDOCK_QUERY_GUID "E7F300FA-21CD-4003-ADAC-2696135982E6"
/* BKBD encoding */
#define BKBD_DETACHED 0
/* LENOVO_FEATURE_STATUS_DATA feature selector */
#define YB9_FEATURE_STATUS_ID 0x00060000u
/*
* LENOVO_FEATURE_STATUS_DATA: 8-byte buffer {uint32 IDs, uint32 Status}.
* IDs is always 0x00060000; Status holds the BKBD value (03).
*/
struct lenovo_feature_status {
__le32 id;
__le32 status;
} __packed;
/* ------------------------------------------------------------------
* Notifier chain event driver fires it, block driver listens
* ------------------------------------------------------------------ */
static BLOCKING_NOTIFIER_HEAD(yb9_kbdock_chain_head);
static void devm_yb9_kbdock_unregister_notifier(void *data)
{
struct notifier_block *nb = data;
blocking_notifier_chain_unregister(&yb9_kbdock_chain_head, nb);
}
static int devm_yb9_kbdock_register_notifier(struct device *dev,
struct notifier_block *nb)
{
int ret;
ret = blocking_notifier_chain_register(&yb9_kbdock_chain_head, nb);
if (ret < 0)
return ret;
return devm_add_action_or_reset(dev, devm_yb9_kbdock_unregister_notifier, nb);
}
/* ------------------------------------------------------------------
* Block WMI driver LENOVO_FEATURE_STATUS_DATA
* (owns input_dev, sysfs, PM resume)
* ------------------------------------------------------------------ */
struct yb9_kbdock_data {
struct wmi_device *wdev;
struct input_dev *input_dev;
struct notifier_block nb;
spinlock_t lock; /* protects input_report_switch + input_sync */
};
static int yb9_kbdock_query(struct yb9_kbdock_data *d, u32 *bkbd)
{
struct wmi_buffer out;
int ret;
ret = wmidev_query_block(d->wdev, 0, &out,
sizeof(struct lenovo_feature_status));
if (ret)
return ret;
struct lenovo_feature_status *fs __free(kfree) = out.data;
if (le32_to_cpu(fs->id) != YB9_FEATURE_STATUS_ID)
return -EIO;
*bkbd = le32_to_cpu(fs->status);
return 0;
}
static void yb9_kbdock_report(struct yb9_kbdock_data *d, u32 bkbd)
{
int tablet = (bkbd == BKBD_DETACHED) ? 1 : 0;
spin_lock(&d->lock);
input_report_switch(d->input_dev, SW_TABLET_MODE, tablet);
input_sync(d->input_dev);
spin_unlock(&d->lock);
dev_dbg(&d->wdev->dev, "BKBD=%u SW_TABLET_MODE=%d\n", bkbd, tablet);
}
static int yb9_kbdock_sync(struct yb9_kbdock_data *d)
{
u32 bkbd;
int ret;
ret = yb9_kbdock_query(d, &bkbd);
if (ret)
return ret;
yb9_kbdock_report(d, bkbd);
return 0;
}
static int yb9_kbdock_nb_call(struct notifier_block *nb,
unsigned long bkbd, void *unused)
{
struct yb9_kbdock_data *d =
container_of(nb, struct yb9_kbdock_data, nb);
yb9_kbdock_report(d, bkbd);
return NOTIFY_DONE;
}
static ssize_t keyboard_position_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct yb9_kbdock_data *d = dev_get_drvdata(dev);
u32 bkbd;
int ret;
ret = yb9_kbdock_query(d, &bkbd);
if (ret)
return ret;
return sysfs_emit(buf, "%u\n", bkbd);
}
static DEVICE_ATTR_RO(keyboard_position);
static const struct attribute * const yb9_kbdock_attrs[] = {
&dev_attr_keyboard_position.attr,
NULL,
};
ATTRIBUTE_GROUPS(yb9_kbdock);
static int yb9_kbdock_resume(struct device *dev)
{
struct yb9_kbdock_data *d = dev_get_drvdata(dev);
return yb9_kbdock_sync(d);
}
static DEFINE_SIMPLE_DEV_PM_OPS(yb9_kbdock_pm_ops, NULL, yb9_kbdock_resume);
static int yb9_kbdock_block_probe(struct wmi_device *wdev, const void *ctx)
{
struct yb9_kbdock_data *d;
struct input_dev *input_dev;
int ret;
d = devm_kzalloc(&wdev->dev, sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
d->wdev = wdev;
spin_lock_init(&d->lock);
input_dev = devm_input_allocate_device(&wdev->dev);
if (!input_dev)
return -ENOMEM;
input_dev->name = "Lenovo Yoga Book 9 keyboard dock switch";
input_dev->phys = YB9_KBDOCK_QUERY_GUID "/input0";
input_dev->id.bustype = BUS_HOST;
input_set_capability(input_dev, EV_SW, SW_TABLET_MODE);
ret = input_register_device(input_dev);
if (ret)
return ret;
d->input_dev = input_dev;
d->nb.notifier_call = yb9_kbdock_nb_call;
ret = devm_yb9_kbdock_register_notifier(&wdev->dev, &d->nb);
if (ret)
return ret;
dev_set_drvdata(&wdev->dev, d);
return yb9_kbdock_sync(d);
}
static const struct wmi_device_id yb9_kbdock_block_id_table[] = {
{ .guid_string = YB9_KBDOCK_QUERY_GUID },
{ }
};
static struct wmi_driver yb9_kbdock_block_driver = {
.driver = {
.name = "lenovo-yb9-kbdock",
.dev_groups = yb9_kbdock_groups,
.pm = pm_sleep_ptr(&yb9_kbdock_pm_ops),
},
.id_table = yb9_kbdock_block_id_table,
.no_singleton = true,
.probe = yb9_kbdock_block_probe,
};
/* ------------------------------------------------------------------
* Event WMI driver LENOVO_BTKBD_EVENT
* (fires the notifier chain on each WMI event)
* ------------------------------------------------------------------ */
static void yb9_kbdock_notify_new(struct wmi_device *wdev,
const struct wmi_buffer *data)
{
/*
* _WED(0xEB) returns EC.BKBD directly as a 32-bit integer
* (LENOVO_BTKBD_EVENT WmiDataId(1) uint32 Status).
* Short-buffer guard is handled by .min_event_size below.
*/
u32 bkbd = le32_to_cpu(*(const __le32 *)data->data);
blocking_notifier_call_chain(&yb9_kbdock_chain_head, bkbd, NULL);
}
static const struct wmi_device_id yb9_kbdock_event_id_table[] = {
{ .guid_string = YB9_KBDOCK_EVENT_GUID },
{ }
};
MODULE_DEVICE_TABLE(wmi, yb9_kbdock_event_id_table);
static struct wmi_driver yb9_kbdock_event_driver = {
.driver = {
.name = "lenovo-yb9-kbdock-event",
},
.id_table = yb9_kbdock_event_id_table,
.no_singleton = true,
.notify_new = yb9_kbdock_notify_new,
.min_event_size = sizeof(__le32),
};
/* ------------------------------------------------------------------
* Module init / exit
* ------------------------------------------------------------------ */
static const struct dmi_system_id yb9_kbdock_dmi_table[] __initconst = {
{
/* Lenovo Yoga Book 9 14IAH10 */
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
DMI_MATCH(DMI_PRODUCT_NAME, "83KJ"),
},
},
{ }
};
static int __init yb9_kbdock_init(void)
{
int ret;
if (!dmi_check_system(yb9_kbdock_dmi_table))
return -ENODEV;
ret = wmi_driver_register(&yb9_kbdock_event_driver);
if (ret)
return ret;
ret = wmi_driver_register(&yb9_kbdock_block_driver);
if (ret) {
wmi_driver_unregister(&yb9_kbdock_event_driver);
return ret;
}
return 0;
}
module_init(yb9_kbdock_init);
static void __exit yb9_kbdock_exit(void)
{
wmi_driver_unregister(&yb9_kbdock_block_driver);
wmi_driver_unregister(&yb9_kbdock_event_driver);
}
module_exit(yb9_kbdock_exit);
MODULE_AUTHOR("Dave Carey <carvsdriver@gmail.com>");
MODULE_DESCRIPTION("Lenovo Yoga Book 9 keyboard dock detection");
MODULE_LICENSE("GPL");