dell-wmi: Support new hotkeys on the XPS 13 9350 (Skylake)
[deliverable/linux.git] / drivers / platform / x86 / dell-wmi.c
index cb8a9c2a3a1f6df4c3d7fdb1306676088e8bcbfc..e38258a82be532e8820143c8b85908571d1744cb 100644 (file)
@@ -2,6 +2,7 @@
  * Dell WMI hotkeys
  *
  * Copyright (C) 2008 Red Hat <mjg@redhat.com>
+ * Copyright (C) 2014-2015 Pali Rohár <pali.rohar@gmail.com>
  *
  * Portions based on wistron_btns.c:
  * Copyright (C) 2005 Miloslav Trmac <mitr@volny.cz>
 #include <acpi/video.h>
 
 MODULE_AUTHOR("Matthew Garrett <mjg@redhat.com>");
+MODULE_AUTHOR("Pali Rohár <pali.rohar@gmail.com>");
 MODULE_DESCRIPTION("Dell laptop WMI hotkeys driver");
 MODULE_LICENSE("GPL");
 
 #define DELL_EVENT_GUID "9DBB5994-A997-11DA-B012-B622A1EF5492"
+#define DELL_DESCRIPTOR_GUID "8D9DDCBC-A997-11DA-B012-B622A1EF5492"
+
+static u32 dell_wmi_interface_version;
 
 MODULE_ALIAS("wmi:"DELL_EVENT_GUID);
+MODULE_ALIAS("wmi:"DELL_DESCRIPTOR_GUID);
 
 /*
  * Certain keys are flagged as KE_IGNORE. All of these are either
@@ -114,30 +120,77 @@ struct dell_bios_hotkey_table {
 
 };
 
-static const struct dell_bios_hotkey_table *dell_bios_hotkey_table;
+struct dell_dmi_results {
+       int err;
+       struct key_entry *keymap;
+};
 
+/* Uninitialized entries here are KEY_RESERVED == 0. */
 static const u16 bios_to_linux_keycode[256] __initconst = {
+       [0]     = KEY_MEDIA,
+       [1]     = KEY_NEXTSONG,
+       [2]     = KEY_PLAYPAUSE,
+       [3]     = KEY_PREVIOUSSONG,
+       [4]     = KEY_STOPCD,
+       [5]     = KEY_UNKNOWN,
+       [6]     = KEY_UNKNOWN,
+       [7]     = KEY_UNKNOWN,
+       [8]     = KEY_WWW,
+       [9]     = KEY_UNKNOWN,
+       [10]    = KEY_VOLUMEDOWN,
+       [11]    = KEY_MUTE,
+       [12]    = KEY_VOLUMEUP,
+       [13]    = KEY_UNKNOWN,
+       [14]    = KEY_BATTERY,
+       [15]    = KEY_EJECTCD,
+       [16]    = KEY_UNKNOWN,
+       [17]    = KEY_SLEEP,
+       [18]    = KEY_PROG1,
+       [19]    = KEY_BRIGHTNESSDOWN,
+       [20]    = KEY_BRIGHTNESSUP,
+       [21]    = KEY_UNKNOWN,
+       [22]    = KEY_KBDILLUMTOGGLE,
+       [23]    = KEY_UNKNOWN,
+       [24]    = KEY_SWITCHVIDEOMODE,
+       [25]    = KEY_UNKNOWN,
+       [26]    = KEY_UNKNOWN,
+       [27]    = KEY_SWITCHVIDEOMODE,
+       [28]    = KEY_UNKNOWN,
+       [29]    = KEY_UNKNOWN,
+       [30]    = KEY_PROG2,
+       [31]    = KEY_UNKNOWN,
+       [32]    = KEY_UNKNOWN,
+       [33]    = KEY_UNKNOWN,
+       [34]    = KEY_UNKNOWN,
+       [35]    = KEY_UNKNOWN,
+       [36]    = KEY_UNKNOWN,
+       [37]    = KEY_UNKNOWN,
+       [38]    = KEY_MICMUTE,
+       [255]   = KEY_PROG3,
+};
+
+/*
+ * These are applied if the 0xB2 DMI hotkey table is present and doesn't
+ * override them.
+ */
+static const struct key_entry dell_wmi_extra_keymap[] __initconst = {
+       /* Fn-lock */
+       { KE_IGNORE, 0x151, { KEY_RESERVED } },
+
+       /* Change keyboard illumination */
+       { KE_IGNORE, 0x152, { KEY_KBDILLUMTOGGLE } },
+
+       /*
+        * Radio disable (notify only -- there is no model for which the
+        * WMI event is supposed to trigger an action).
+        */
+       { KE_IGNORE, 0x153, { KEY_RFKILL } },
+
+       /* RGB keyboard backlight control */
+       { KE_IGNORE, 0x154, { KEY_RESERVED } },
 
-       KEY_MEDIA,      KEY_NEXTSONG,   KEY_PLAYPAUSE, KEY_PREVIOUSSONG,
-       KEY_STOPCD,     KEY_UNKNOWN,    KEY_UNKNOWN,    KEY_UNKNOWN,
-       KEY_WWW,        KEY_UNKNOWN,    KEY_VOLUMEDOWN, KEY_MUTE,
-       KEY_VOLUMEUP,   KEY_UNKNOWN,    KEY_BATTERY,    KEY_EJECTCD,
-       KEY_UNKNOWN,    KEY_SLEEP,      KEY_PROG1, KEY_BRIGHTNESSDOWN,
-       KEY_BRIGHTNESSUP,       KEY_UNKNOWN,    KEY_KBDILLUMTOGGLE,
-       KEY_UNKNOWN,    KEY_SWITCHVIDEOMODE,    KEY_UNKNOWN, KEY_UNKNOWN,
-       KEY_SWITCHVIDEOMODE,    KEY_UNKNOWN,    KEY_UNKNOWN, KEY_PROG2,
-       KEY_UNKNOWN,    KEY_UNKNOWN,    KEY_UNKNOWN,    KEY_UNKNOWN,
-       KEY_UNKNOWN,    KEY_UNKNOWN,    KEY_UNKNOWN,    KEY_MICMUTE,
-       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-       0, 0, 0, 0, 0, 0, 0, 0, 0, KEY_PROG3
+       /* Stealth mode toggle */
+       { KE_IGNORE, 0x155, { KEY_RESERVED } },
 };
 
 static struct input_dev *dell_wmi_input_dev;
@@ -149,7 +202,8 @@ static void dell_wmi_process_key(int reported_key)
        key = sparse_keymap_entry_from_scancode(dell_wmi_input_dev,
                                                reported_key);
        if (!key) {
-               pr_info("Unknown key %x pressed\n", reported_key);
+               pr_info("Unknown key with scancode 0x%x pressed\n",
+                       reported_key);
                return;
        }
 
@@ -210,6 +264,22 @@ static void dell_wmi_notify(u32 value, void *context)
 
        buffer_end = buffer_entry + buffer_size;
 
+       /*
+        * BIOS/ACPI on devices with WMI interface version 0 does not clear
+        * buffer before filling it. So next time when BIOS/ACPI send WMI event
+        * which is smaller as previous then it contains garbage in buffer from
+        * previous event.
+        *
+        * BIOS/ACPI on devices with WMI interface version 1 clears buffer and
+        * sometimes send more events in buffer at one call.
+        *
+        * So to prevent reading garbage from buffer we will process only first
+        * one event on devices with WMI interface version 0.
+        */
+       if (dell_wmi_interface_version == 0 && buffer_entry < buffer_end)
+               if (buffer_end > buffer_entry + buffer_entry[0] + 1)
+                       buffer_end = buffer_entry + buffer_entry[0] + 1;
+
        while (buffer_entry < buffer_end) {
 
                len = buffer_entry[0];
@@ -294,39 +364,112 @@ static void dell_wmi_notify(u32 value, void *context)
        kfree(obj);
 }
 
-static const struct key_entry * __init dell_wmi_prepare_new_keymap(void)
+static bool have_scancode(u32 scancode, const struct key_entry *keymap, int len)
 {
-       int hotkey_num = (dell_bios_hotkey_table->header.length - 4) /
-                               sizeof(struct dell_bios_keymap_entry);
-       struct key_entry *keymap;
        int i;
 
-       keymap = kcalloc(hotkey_num + 1, sizeof(struct key_entry), GFP_KERNEL);
-       if (!keymap)
-               return NULL;
+       for (i = 0; i < len; i++)
+               if (keymap[i].code == scancode)
+                       return true;
+
+       return false;
+}
+
+static void __init handle_dmi_entry(const struct dmi_header *dm,
+
+                                   void *opaque)
+
+{
+       struct dell_dmi_results *results = opaque;
+       struct dell_bios_hotkey_table *table;
+       int hotkey_num, i, pos = 0;
+       struct key_entry *keymap;
+       int num_bios_keys;
+
+       if (results->err || results->keymap)
+               return;         /* We already found the hotkey table. */
+
+       if (dm->type != 0xb2)
+               return;
+
+       table = container_of(dm, struct dell_bios_hotkey_table, header);
+
+       hotkey_num = (table->header.length -
+                     sizeof(struct dell_bios_hotkey_table)) /
+                               sizeof(struct dell_bios_keymap_entry);
+       if (hotkey_num < 1) {
+               /*
+                * Historically, dell-wmi would ignore a DMI entry of
+                * fewer than 7 bytes.  Sizes between 4 and 8 bytes are
+                * nonsensical (both the header and all entries are 4
+                * bytes), so we approximate the old behavior by
+                * ignoring tables with fewer than one entry.
+                */
+               return;
+       }
+
+       keymap = kcalloc(hotkey_num + ARRAY_SIZE(dell_wmi_extra_keymap) + 1,
+                        sizeof(struct key_entry), GFP_KERNEL);
+       if (!keymap) {
+               results->err = -ENOMEM;
+               return;
+       }
 
        for (i = 0; i < hotkey_num; i++) {
                const struct dell_bios_keymap_entry *bios_entry =
-                                       &dell_bios_hotkey_table->keymap[i];
-               u16 keycode = bios_entry->keycode < 256 ?
-                                   bios_to_linux_keycode[bios_entry->keycode] :
-                                   KEY_RESERVED;
+                                       &table->keymap[i];
+
+               /* Uninitialized entries are 0 aka KEY_RESERVED. */
+               u16 keycode = (bios_entry->keycode <
+                              ARRAY_SIZE(bios_to_linux_keycode)) ?
+                       bios_to_linux_keycode[bios_entry->keycode] :
+                       KEY_RESERVED;
+
+               /*
+                * Log if we find an entry in the DMI table that we don't
+                * understand.  If this happens, we should figure out what
+                * the entry means and add it to bios_to_linux_keycode.
+                */
+               if (keycode == KEY_RESERVED) {
+                       pr_info("firmware scancode 0x%x maps to unrecognized keycode 0x%x\n",
+                               bios_entry->scancode, bios_entry->keycode);
+                       continue;
+               }
 
                if (keycode == KEY_KBDILLUMTOGGLE)
-                       keymap[i].type = KE_IGNORE;
+                       keymap[pos].type = KE_IGNORE;
                else
-                       keymap[i].type = KE_KEY;
-               keymap[i].code = bios_entry->scancode;
-               keymap[i].keycode = keycode;
+                       keymap[pos].type = KE_KEY;
+               keymap[pos].code = bios_entry->scancode;
+               keymap[pos].keycode = keycode;
+
+               pos++;
+       }
+
+       num_bios_keys = pos;
+
+       for (i = 0; i < ARRAY_SIZE(dell_wmi_extra_keymap); i++) {
+               const struct key_entry *entry = &dell_wmi_extra_keymap[i];
+
+               /*
+                * Check if we've already found this scancode.  This takes
+                * quadratic time, but it doesn't matter unless the list
+                * of extra keys gets very long.
+                */
+               if (!have_scancode(entry->code, keymap, num_bios_keys)) {
+                       keymap[pos] = *entry;
+                       pos++;
+               }
        }
 
-       keymap[hotkey_num].type = KE_END;
+       keymap[pos].type = KE_END;
 
-       return keymap;
+       results->keymap = keymap;
 }
 
 static int __init dell_wmi_input_setup(void)
 {
+       struct dell_dmi_results dmi_results = {};
        int err;
 
        dell_wmi_input_dev = input_allocate_device();
@@ -337,20 +480,31 @@ static int __init dell_wmi_input_setup(void)
        dell_wmi_input_dev->phys = "wmi/input0";
        dell_wmi_input_dev->id.bustype = BUS_HOST;
 
-       if (dell_new_hk_type) {
-               const struct key_entry *keymap = dell_wmi_prepare_new_keymap();
-               if (!keymap) {
-                       err = -ENOMEM;
-                       goto err_free_dev;
-               }
+       if (dmi_walk(handle_dmi_entry, &dmi_results)) {
+               /*
+                * Historically, dell-wmi ignored dmi_walk errors.  A failure
+                * is certainly surprising, but it probably just indicates
+                * a very old laptop.
+                */
+               pr_warn("no DMI; using the old-style hotkey interface\n");
+       }
+
+       if (dmi_results.err) {
+               err = dmi_results.err;
+               goto err_free_dev;
+       }
 
-               err = sparse_keymap_setup(dell_wmi_input_dev, keymap, NULL);
+       if (dmi_results.keymap) {
+               dell_new_hk_type = true;
+
+               err = sparse_keymap_setup(dell_wmi_input_dev,
+                                         dmi_results.keymap, NULL);
 
                /*
                 * Sparse keymap library makes a copy of keymap so we
                 * don't need the original one that was allocated.
                 */
-               kfree(keymap);
+               kfree(dmi_results.keymap);
        } else {
                err = sparse_keymap_setup(dell_wmi_input_dev,
                                          dell_wmi_legacy_keymap, NULL);
@@ -377,13 +531,70 @@ static void dell_wmi_input_destroy(void)
        input_unregister_device(dell_wmi_input_dev);
 }
 
-static void __init find_hk_type(const struct dmi_header *dm, void *dummy)
+/*
+ * Descriptor buffer is 128 byte long and contains:
+ *
+ *       Name             Offset  Length  Value
+ * Vendor Signature          0       4    "DELL"
+ * Object Signature          4       4    " WMI"
+ * WMI Interface Version     8       4    <version>
+ * WMI buffer length        12       4    4096
+ */
+static int __init dell_wmi_check_descriptor_buffer(void)
 {
-       if (dm->type == 0xb2 && dm->length > 6) {
-               dell_new_hk_type = true;
-               dell_bios_hotkey_table =
-                       container_of(dm, struct dell_bios_hotkey_table, header);
+       struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL };
+       union acpi_object *obj;
+       acpi_status status;
+       u32 *buffer;
+
+       status = wmi_query_block(DELL_DESCRIPTOR_GUID, 0, &out);
+       if (ACPI_FAILURE(status)) {
+               pr_err("Cannot read Dell descriptor buffer - %d\n", status);
+               return status;
        }
+
+       obj = (union acpi_object *)out.pointer;
+       if (!obj) {
+               pr_err("Dell descriptor buffer is empty\n");
+               return -EINVAL;
+       }
+
+       if (obj->type != ACPI_TYPE_BUFFER) {
+               pr_err("Cannot read Dell descriptor buffer\n");
+               kfree(obj);
+               return -EINVAL;
+       }
+
+       if (obj->buffer.length != 128) {
+               pr_err("Dell descriptor buffer has invalid length (%d)\n",
+                       obj->buffer.length);
+               if (obj->buffer.length < 16) {
+                       kfree(obj);
+                       return -EINVAL;
+               }
+       }
+
+       buffer = (u32 *)obj->buffer.pointer;
+
+       if (buffer[0] != 0x4C4C4544 && buffer[1] != 0x494D5720)
+               pr_warn("Dell descriptor buffer has invalid signature (%*ph)\n",
+                       8, buffer);
+
+       if (buffer[2] != 0 && buffer[2] != 1)
+               pr_warn("Dell descriptor buffer has unknown version (%d)\n",
+                       buffer[2]);
+
+       if (buffer[3] != 4096)
+               pr_warn("Dell descriptor buffer has invalid buffer length (%d)\n",
+                       buffer[3]);
+
+       dell_wmi_interface_version = buffer[2];
+
+       pr_info("Detected Dell WMI interface version %u\n",
+               dell_wmi_interface_version);
+
+       kfree(obj);
+       return 0;
 }
 
 static int __init dell_wmi_init(void)
@@ -391,12 +602,15 @@ static int __init dell_wmi_init(void)
        int err;
        acpi_status status;
 
-       if (!wmi_has_guid(DELL_EVENT_GUID)) {
-               pr_warn("No known WMI GUID found\n");
+       if (!wmi_has_guid(DELL_EVENT_GUID) ||
+           !wmi_has_guid(DELL_DESCRIPTOR_GUID)) {
+               pr_warn("Dell WMI GUID were not found\n");
                return -ENODEV;
        }
 
-       dmi_walk(find_hk_type, NULL);
+       err = dell_wmi_check_descriptor_buffer();
+       if (err)
+               return err;
 
        err = dell_wmi_input_setup();
        if (err)
This page took 0.050717 seconds and 5 git commands to generate.