| /* |
| * thinkpad_acpi.c - ThinkPad ACPI Extras |
| * |
| * |
| * Copyright (C) 2004-2005 Borislav Deianov <borislav@users.sf.net> |
| * Copyright (C) 2006-2009 Henrique de Moraes Holschuh <hmh@hmh.eng.br> |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| * 02110-1301, USA. |
| */ |
| |
| #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
| |
| #define TPACPI_VERSION "0.25" |
| #define TPACPI_SYSFS_VERSION 0x030000 |
| |
| /* |
| * Changelog: |
| * 2007-10-20 changelog trimmed down |
| * |
| * 2007-03-27 0.14 renamed to thinkpad_acpi and moved to |
| * drivers/misc. |
| * |
| * 2006-11-22 0.13 new maintainer |
| * changelog now lives in git commit history, and will |
| * not be updated further in-file. |
| * |
| * 2005-03-17 0.11 support for 600e, 770x |
| * thanks to Jamie Lentin <lentinj@dial.pipex.com> |
| * |
| * 2005-01-16 0.9 use MODULE_VERSION |
| * thanks to Henrik Brix Andersen <brix@gentoo.org> |
| * fix parameter passing on module loading |
| * thanks to Rusty Russell <rusty@rustcorp.com.au> |
| * thanks to Jim Radford <radford@blackbean.org> |
| * 2004-11-08 0.8 fix init error case, don't return from a macro |
| * thanks to Chris Wright <chrisw@osdl.org> |
| */ |
| |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/init.h> |
| #include <linux/types.h> |
| #include <linux/string.h> |
| #include <linux/list.h> |
| #include <linux/mutex.h> |
| #include <linux/sched.h> |
| #include <linux/kthread.h> |
| #include <linux/freezer.h> |
| #include <linux/delay.h> |
| #include <linux/slab.h> |
| #include <linux/nvram.h> |
| #include <linux/proc_fs.h> |
| #include <linux/seq_file.h> |
| #include <linux/sysfs.h> |
| #include <linux/backlight.h> |
| #include <linux/fb.h> |
| #include <linux/platform_device.h> |
| #include <linux/hwmon.h> |
| #include <linux/hwmon-sysfs.h> |
| #include <linux/input.h> |
| #include <linux/leds.h> |
| #include <linux/rfkill.h> |
| #include <linux/dmi.h> |
| #include <linux/jiffies.h> |
| #include <linux/workqueue.h> |
| #include <linux/acpi.h> |
| #include <linux/pci_ids.h> |
| #include <linux/thinkpad_acpi.h> |
| #include <sound/core.h> |
| #include <sound/control.h> |
| #include <sound/initval.h> |
| #include <linux/uaccess.h> |
| #include <acpi/video.h> |
| |
| /* ThinkPad CMOS commands */ |
| #define TP_CMOS_VOLUME_DOWN 0 |
| #define TP_CMOS_VOLUME_UP 1 |
| #define TP_CMOS_VOLUME_MUTE 2 |
| #define TP_CMOS_BRIGHTNESS_UP 4 |
| #define TP_CMOS_BRIGHTNESS_DOWN 5 |
| #define TP_CMOS_THINKLIGHT_ON 12 |
| #define TP_CMOS_THINKLIGHT_OFF 13 |
| |
| /* NVRAM Addresses */ |
| enum tp_nvram_addr { |
| TP_NVRAM_ADDR_HK2 = 0x57, |
| TP_NVRAM_ADDR_THINKLIGHT = 0x58, |
| TP_NVRAM_ADDR_VIDEO = 0x59, |
| TP_NVRAM_ADDR_BRIGHTNESS = 0x5e, |
| TP_NVRAM_ADDR_MIXER = 0x60, |
| }; |
| |
| /* NVRAM bit masks */ |
| enum { |
| TP_NVRAM_MASK_HKT_THINKPAD = 0x08, |
| TP_NVRAM_MASK_HKT_ZOOM = 0x20, |
| TP_NVRAM_MASK_HKT_DISPLAY = 0x40, |
| TP_NVRAM_MASK_HKT_HIBERNATE = 0x80, |
| TP_NVRAM_MASK_THINKLIGHT = 0x10, |
| TP_NVRAM_MASK_HKT_DISPEXPND = 0x30, |
| TP_NVRAM_MASK_HKT_BRIGHTNESS = 0x20, |
| TP_NVRAM_MASK_LEVEL_BRIGHTNESS = 0x0f, |
| TP_NVRAM_POS_LEVEL_BRIGHTNESS = 0, |
| TP_NVRAM_MASK_MUTE = 0x40, |
| TP_NVRAM_MASK_HKT_VOLUME = 0x80, |
| TP_NVRAM_MASK_LEVEL_VOLUME = 0x0f, |
| TP_NVRAM_POS_LEVEL_VOLUME = 0, |
| }; |
| |
| /* Misc NVRAM-related */ |
| enum { |
| TP_NVRAM_LEVEL_VOLUME_MAX = 14, |
| }; |
| |
| /* ACPI HIDs */ |
| #define TPACPI_ACPI_IBM_HKEY_HID "IBM0068" |
| #define TPACPI_ACPI_LENOVO_HKEY_HID "LEN0068" |
| #define TPACPI_ACPI_LENOVO_HKEY_V2_HID "LEN0268" |
| #define TPACPI_ACPI_EC_HID "PNP0C09" |
| |
| /* Input IDs */ |
| #define TPACPI_HKEY_INPUT_PRODUCT 0x5054 /* "TP" */ |
| #define TPACPI_HKEY_INPUT_VERSION 0x4101 |
| |
| /* ACPI \WGSV commands */ |
| enum { |
| TP_ACPI_WGSV_GET_STATE = 0x01, /* Get state information */ |
| TP_ACPI_WGSV_PWR_ON_ON_RESUME = 0x02, /* Resume WWAN powered on */ |
| TP_ACPI_WGSV_PWR_OFF_ON_RESUME = 0x03, /* Resume WWAN powered off */ |
| TP_ACPI_WGSV_SAVE_STATE = 0x04, /* Save state for S4/S5 */ |
| }; |
| |
| /* TP_ACPI_WGSV_GET_STATE bits */ |
| enum { |
| TP_ACPI_WGSV_STATE_WWANEXIST = 0x0001, /* WWAN hw available */ |
| TP_ACPI_WGSV_STATE_WWANPWR = 0x0002, /* WWAN radio enabled */ |
| TP_ACPI_WGSV_STATE_WWANPWRRES = 0x0004, /* WWAN state at resume */ |
| TP_ACPI_WGSV_STATE_WWANBIOSOFF = 0x0008, /* WWAN disabled in BIOS */ |
| TP_ACPI_WGSV_STATE_BLTHEXIST = 0x0001, /* BLTH hw available */ |
| TP_ACPI_WGSV_STATE_BLTHPWR = 0x0002, /* BLTH radio enabled */ |
| TP_ACPI_WGSV_STATE_BLTHPWRRES = 0x0004, /* BLTH state at resume */ |
| TP_ACPI_WGSV_STATE_BLTHBIOSOFF = 0x0008, /* BLTH disabled in BIOS */ |
| TP_ACPI_WGSV_STATE_UWBEXIST = 0x0010, /* UWB hw available */ |
| TP_ACPI_WGSV_STATE_UWBPWR = 0x0020, /* UWB radio enabled */ |
| }; |
| |
| /* HKEY events */ |
| enum tpacpi_hkey_event_t { |
| /* Hotkey-related */ |
| TP_HKEY_EV_HOTKEY_BASE = 0x1001, /* first hotkey (FN+F1) */ |
| TP_HKEY_EV_BRGHT_UP = 0x1010, /* Brightness up */ |
| TP_HKEY_EV_BRGHT_DOWN = 0x1011, /* Brightness down */ |
| TP_HKEY_EV_KBD_LIGHT = 0x1012, /* Thinklight/kbd backlight */ |
| TP_HKEY_EV_VOL_UP = 0x1015, /* Volume up or unmute */ |
| TP_HKEY_EV_VOL_DOWN = 0x1016, /* Volume down or unmute */ |
| TP_HKEY_EV_VOL_MUTE = 0x1017, /* Mixer output mute */ |
| |
| /* Reasons for waking up from S3/S4 */ |
| TP_HKEY_EV_WKUP_S3_UNDOCK = 0x2304, /* undock requested, S3 */ |
| TP_HKEY_EV_WKUP_S4_UNDOCK = 0x2404, /* undock requested, S4 */ |
| TP_HKEY_EV_WKUP_S3_BAYEJ = 0x2305, /* bay ejection req, S3 */ |
| TP_HKEY_EV_WKUP_S4_BAYEJ = 0x2405, /* bay ejection req, S4 */ |
| TP_HKEY_EV_WKUP_S3_BATLOW = 0x2313, /* battery empty, S3 */ |
| TP_HKEY_EV_WKUP_S4_BATLOW = 0x2413, /* battery empty, S4 */ |
| |
| /* Auto-sleep after eject request */ |
| TP_HKEY_EV_BAYEJ_ACK = 0x3003, /* bay ejection complete */ |
| TP_HKEY_EV_UNDOCK_ACK = 0x4003, /* undock complete */ |
| |
| /* Misc bay events */ |
| TP_HKEY_EV_OPTDRV_EJ = 0x3006, /* opt. drive tray ejected */ |
| TP_HKEY_EV_HOTPLUG_DOCK = 0x4010, /* docked into hotplug dock |
| or port replicator */ |
| TP_HKEY_EV_HOTPLUG_UNDOCK = 0x4011, /* undocked from hotplug |
| dock or port replicator */ |
| |
| /* User-interface events */ |
| TP_HKEY_EV_LID_CLOSE = 0x5001, /* laptop lid closed */ |
| TP_HKEY_EV_LID_OPEN = 0x5002, /* laptop lid opened */ |
| TP_HKEY_EV_TABLET_TABLET = 0x5009, /* tablet swivel up */ |
| TP_HKEY_EV_TABLET_NOTEBOOK = 0x500a, /* tablet swivel down */ |
| TP_HKEY_EV_TABLET_CHANGED = 0x60c0, /* X1 Yoga (2016): |
| * enter/leave tablet mode |
| */ |
| TP_HKEY_EV_PEN_INSERTED = 0x500b, /* tablet pen inserted */ |
| TP_HKEY_EV_PEN_REMOVED = 0x500c, /* tablet pen removed */ |
| TP_HKEY_EV_BRGHT_CHANGED = 0x5010, /* backlight control event */ |
| |
| /* Key-related user-interface events */ |
| TP_HKEY_EV_KEY_NUMLOCK = 0x6000, /* NumLock key pressed */ |
| TP_HKEY_EV_KEY_FN = 0x6005, /* Fn key pressed? E420 */ |
| TP_HKEY_EV_KEY_FN_ESC = 0x6060, /* Fn+Esc key pressed X240 */ |
| |
| /* Thermal events */ |
| TP_HKEY_EV_ALARM_BAT_HOT = 0x6011, /* battery too hot */ |
| TP_HKEY_EV_ALARM_BAT_XHOT = 0x6012, /* battery critically hot */ |
| TP_HKEY_EV_ALARM_SENSOR_HOT = 0x6021, /* sensor too hot */ |
| TP_HKEY_EV_ALARM_SENSOR_XHOT = 0x6022, /* sensor critically hot */ |
| TP_HKEY_EV_THM_TABLE_CHANGED = 0x6030, /* thermal table changed */ |
| |
| /* AC-related events */ |
| TP_HKEY_EV_AC_CHANGED = 0x6040, /* AC status changed */ |
| |
| /* Misc */ |
| TP_HKEY_EV_RFKILL_CHANGED = 0x7000, /* rfkill switch changed */ |
| }; |
| |
| /**************************************************************************** |
| * Main driver |
| */ |
| |
| #define TPACPI_NAME "thinkpad" |
| #define TPACPI_DESC "ThinkPad ACPI Extras" |
| #define TPACPI_FILE TPACPI_NAME "_acpi" |
| #define TPACPI_URL "http://ibm-acpi.sf.net/" |
| #define TPACPI_MAIL "ibm-acpi-devel@lists.sourceforge.net" |
| |
| #define TPACPI_PROC_DIR "ibm" |
| #define TPACPI_ACPI_EVENT_PREFIX "ibm" |
| #define TPACPI_DRVR_NAME TPACPI_FILE |
| #define TPACPI_DRVR_SHORTNAME "tpacpi" |
| #define TPACPI_HWMON_DRVR_NAME TPACPI_NAME "_hwmon" |
| |
| #define TPACPI_NVRAM_KTHREAD_NAME "ktpacpi_nvramd" |
| #define TPACPI_WORKQUEUE_NAME "ktpacpid" |
| |
| #define TPACPI_MAX_ACPI_ARGS 3 |
| |
| /* Debugging printk groups */ |
| #define TPACPI_DBG_ALL 0xffff |
| #define TPACPI_DBG_DISCLOSETASK 0x8000 |
| #define TPACPI_DBG_INIT 0x0001 |
| #define TPACPI_DBG_EXIT 0x0002 |
| #define TPACPI_DBG_RFKILL 0x0004 |
| #define TPACPI_DBG_HKEY 0x0008 |
| #define TPACPI_DBG_FAN 0x0010 |
| #define TPACPI_DBG_BRGHT 0x0020 |
| #define TPACPI_DBG_MIXER 0x0040 |
| |
| #define onoff(status, bit) ((status) & (1 << (bit)) ? "on" : "off") |
| #define enabled(status, bit) ((status) & (1 << (bit)) ? "enabled" : "disabled") |
| #define strlencmp(a, b) (strncmp((a), (b), strlen(b))) |
| |
| |
| /**************************************************************************** |
| * Driver-wide structs and misc. variables |
| */ |
| |
| struct ibm_struct; |
| |
| struct tp_acpi_drv_struct { |
| const struct acpi_device_id *hid; |
| struct acpi_driver *driver; |
| |
| void (*notify) (struct ibm_struct *, u32); |
| acpi_handle *handle; |
| u32 type; |
| struct acpi_device *device; |
| }; |
| |
| struct ibm_struct { |
| char *name; |
| |
| int (*read) (struct seq_file *); |
| int (*write) (char *); |
| void (*exit) (void); |
| void (*resume) (void); |
| void (*suspend) (void); |
| void (*shutdown) (void); |
| |
| struct list_head all_drivers; |
| |
| struct tp_acpi_drv_struct *acpi; |
| |
| struct { |
| u8 acpi_driver_registered:1; |
| u8 acpi_notify_installed:1; |
| u8 proc_created:1; |
| u8 init_called:1; |
| u8 experimental:1; |
| } flags; |
| }; |
| |
| struct ibm_init_struct { |
| char param[32]; |
| |
| int (*init) (struct ibm_init_struct *); |
| umode_t base_procfs_mode; |
| struct ibm_struct *data; |
| }; |
| |
| static struct { |
| u32 bluetooth:1; |
| u32 hotkey:1; |
| u32 hotkey_mask:1; |
| u32 hotkey_wlsw:1; |
| enum { |
| TP_HOTKEY_TABLET_NONE = 0, |
| TP_HOTKEY_TABLET_USES_MHKG, |
| /* X1 Yoga 2016, seen on BIOS N1FET44W */ |
| TP_HOTKEY_TABLET_USES_CMMD, |
| } hotkey_tablet; |
| u32 kbdlight:1; |
| u32 light:1; |
| u32 light_status:1; |
| u32 bright_acpimode:1; |
| u32 bright_unkfw:1; |
| u32 wan:1; |
| u32 uwb:1; |
| u32 fan_ctrl_status_undef:1; |
| u32 second_fan:1; |
| u32 beep_needs_two_args:1; |
| u32 mixer_no_level_control:1; |
| u32 input_device_registered:1; |
| u32 platform_drv_registered:1; |
| u32 platform_drv_attrs_registered:1; |
| u32 sensors_pdrv_registered:1; |
| u32 sensors_pdrv_attrs_registered:1; |
| u32 sensors_pdev_attrs_registered:1; |
| u32 hotkey_poll_active:1; |
| u32 has_adaptive_kbd:1; |
| } tp_features; |
| |
| static struct { |
| u16 hotkey_mask_ff:1; |
| u16 volume_ctrl_forbidden:1; |
| } tp_warned; |
| |
| struct thinkpad_id_data { |
| unsigned int vendor; /* ThinkPad vendor: |
| * PCI_VENDOR_ID_IBM/PCI_VENDOR_ID_LENOVO */ |
| |
| char *bios_version_str; /* Something like 1ZET51WW (1.03z) */ |
| char *ec_version_str; /* Something like 1ZHT51WW-1.04a */ |
| |
| u16 bios_model; /* 1Y = 0x5931, 0 = unknown */ |
| u16 ec_model; |
| u16 bios_release; /* 1ZETK1WW = 0x314b, 0 = unknown */ |
| u16 ec_release; |
| |
| char *model_str; /* ThinkPad T43 */ |
| char *nummodel_str; /* 9384A9C for a 9384-A9C model */ |
| }; |
| static struct thinkpad_id_data thinkpad_id; |
| |
| static enum { |
| TPACPI_LIFE_INIT = 0, |
| TPACPI_LIFE_RUNNING, |
| TPACPI_LIFE_EXITING, |
| } tpacpi_lifecycle; |
| |
| static int experimental; |
| static u32 dbg_level; |
| |
| static struct workqueue_struct *tpacpi_wq; |
| |
| enum led_status_t { |
| TPACPI_LED_OFF = 0, |
| TPACPI_LED_ON, |
| TPACPI_LED_BLINK, |
| }; |
| |
| /* tpacpi LED class */ |
| struct tpacpi_led_classdev { |
| struct led_classdev led_classdev; |
| int led; |
| }; |
| |
| /* brightness level capabilities */ |
| static unsigned int bright_maxlvl; /* 0 = unknown */ |
| |
| #ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES |
| static int dbg_wlswemul; |
| static bool tpacpi_wlsw_emulstate; |
| static int dbg_bluetoothemul; |
| static bool tpacpi_bluetooth_emulstate; |
| static int dbg_wwanemul; |
| static bool tpacpi_wwan_emulstate; |
| static int dbg_uwbemul; |
| static bool tpacpi_uwb_emulstate; |
| #endif |
| |
| |
| /************************************************************************* |
| * Debugging helpers |
| */ |
| |
| #define dbg_printk(a_dbg_level, format, arg...) \ |
| do { \ |
| if (dbg_level & (a_dbg_level)) \ |
| printk(KERN_DEBUG pr_fmt("%s: " format), \ |
| __func__, ##arg); \ |
| } while (0) |
| |
| #ifdef CONFIG_THINKPAD_ACPI_DEBUG |
| #define vdbg_printk dbg_printk |
| static const char *str_supported(int is_supported); |
| #else |
| static inline const char *str_supported(int is_supported) { return ""; } |
| #define vdbg_printk(a_dbg_level, format, arg...) \ |
| do { if (0) no_printk(format, ##arg); } while (0) |
| #endif |
| |
| static void tpacpi_log_usertask(const char * const what) |
| { |
| printk(KERN_DEBUG pr_fmt("%s: access by process with PID %d\n"), |
| what, task_tgid_vnr(current)); |
| } |
| |
| #define tpacpi_disclose_usertask(what, format, arg...) \ |
| do { \ |
| if (unlikely((dbg_level & TPACPI_DBG_DISCLOSETASK) && \ |
| (tpacpi_lifecycle == TPACPI_LIFE_RUNNING))) { \ |
| printk(KERN_DEBUG pr_fmt("%s: PID %d: " format), \ |
| what, task_tgid_vnr(current), ## arg); \ |
| } \ |
| } while (0) |
| |
| /* |
| * Quirk handling helpers |
| * |
| * ThinkPad IDs and versions seen in the field so far |
| * are two-characters from the set [0-9A-Z], i.e. base 36. |
| * |
| * We use values well outside that range as specials. |
| */ |
| |
| #define TPACPI_MATCH_ANY 0xffffU |
| #define TPACPI_MATCH_UNKNOWN 0U |
| |
| /* TPID('1', 'Y') == 0x5931 */ |
| #define TPID(__c1, __c2) (((__c2) << 8) | (__c1)) |
| |
| #define TPACPI_Q_IBM(__id1, __id2, __quirk) \ |
| { .vendor = PCI_VENDOR_ID_IBM, \ |
| .bios = TPID(__id1, __id2), \ |
| .ec = TPACPI_MATCH_ANY, \ |
| .quirks = (__quirk) } |
| |
| #define TPACPI_Q_LNV(__id1, __id2, __quirk) \ |
| { .vendor = PCI_VENDOR_ID_LENOVO, \ |
| .bios = TPID(__id1, __id2), \ |
| .ec = TPACPI_MATCH_ANY, \ |
| .quirks = (__quirk) } |
| |
| #define TPACPI_QEC_LNV(__id1, __id2, __quirk) \ |
| { .vendor = PCI_VENDOR_ID_LENOVO, \ |
| .bios = TPACPI_MATCH_ANY, \ |
| .ec = TPID(__id1, __id2), \ |
| .quirks = (__quirk) } |
| |
| struct tpacpi_quirk { |
| unsigned int vendor; |
| u16 bios; |
| u16 ec; |
| unsigned long quirks; |
| }; |
| |
| /** |
| * tpacpi_check_quirks() - search BIOS/EC version on a list |
| * @qlist: array of &struct tpacpi_quirk |
| * @qlist_size: number of elements in @qlist |
| * |
| * Iterates over a quirks list until one is found that matches the |
| * ThinkPad's vendor, BIOS and EC model. |
| * |
| * Returns 0 if nothing matches, otherwise returns the quirks field of |
| * the matching &struct tpacpi_quirk entry. |
| * |
| * The match criteria is: vendor, ec and bios much match. |
| */ |
| static unsigned long __init tpacpi_check_quirks( |
| const struct tpacpi_quirk *qlist, |
| unsigned int qlist_size) |
| { |
| while (qlist_size) { |
| if ((qlist->vendor == thinkpad_id.vendor || |
| qlist->vendor == TPACPI_MATCH_ANY) && |
| (qlist->bios == thinkpad_id.bios_model || |
| qlist->bios == TPACPI_MATCH_ANY) && |
| (qlist->ec == thinkpad_id.ec_model || |
| qlist->ec == TPACPI_MATCH_ANY)) |
| return qlist->quirks; |
| |
| qlist_size--; |
| qlist++; |
| } |
| return 0; |
| } |
| |
| static inline bool __pure __init tpacpi_is_lenovo(void) |
| { |
| return thinkpad_id.vendor == PCI_VENDOR_ID_LENOVO; |
| } |
| |
| static inline bool __pure __init tpacpi_is_ibm(void) |
| { |
| return thinkpad_id.vendor == PCI_VENDOR_ID_IBM; |
| } |
| |
| /**************************************************************************** |
| **************************************************************************** |
| * |
| * ACPI Helpers and device model |
| * |
| **************************************************************************** |
| ****************************************************************************/ |
| |
| /************************************************************************* |
| * ACPI basic handles |
| */ |
| |
| static acpi_handle root_handle; |
| static acpi_handle ec_handle; |
| |
| #define TPACPI_HANDLE(object, parent, paths...) \ |
| static acpi_handle object##_handle; \ |
| static const acpi_handle * const object##_parent __initconst = \ |
| &parent##_handle; \ |
| static char *object##_paths[] __initdata = { paths } |
| |
| TPACPI_HANDLE(ecrd, ec, "ECRD"); /* 570 */ |
| TPACPI_HANDLE(ecwr, ec, "ECWR"); /* 570 */ |
| |
| TPACPI_HANDLE(cmos, root, "\\UCMS", /* R50, R50e, R50p, R51, */ |
| /* T4x, X31, X40 */ |
| "\\CMOS", /* A3x, G4x, R32, T23, T30, X22-24, X30 */ |
| "\\CMS", /* R40, R40e */ |
| ); /* all others */ |
| |
| TPACPI_HANDLE(hkey, ec, "\\_SB.HKEY", /* 600e/x, 770e, 770x */ |
| "^HKEY", /* R30, R31 */ |
| "HKEY", /* all others */ |
| ); /* 570 */ |
| |
| /************************************************************************* |
| * ACPI helpers |
| */ |
| |
| static int acpi_evalf(acpi_handle handle, |
| int *res, char *method, char *fmt, ...) |
| { |
| char *fmt0 = fmt; |
| struct acpi_object_list params; |
| union acpi_object in_objs[TPACPI_MAX_ACPI_ARGS]; |
| struct acpi_buffer result, *resultp; |
| union acpi_object out_obj; |
| acpi_status status; |
| va_list ap; |
| char res_type; |
| int success; |
| int quiet; |
| |
| if (!*fmt) { |
| pr_err("acpi_evalf() called with empty format\n"); |
| return 0; |
| } |
| |
| if (*fmt == 'q') { |
| quiet = 1; |
| fmt++; |
| } else |
| quiet = 0; |
| |
| res_type = *(fmt++); |
| |
| params.count = 0; |
| params.pointer = &in_objs[0]; |
| |
| va_start(ap, fmt); |
| while (*fmt) { |
| char c = *(fmt++); |
| switch (c) { |
| case 'd': /* int */ |
| in_objs[params.count].integer.value = va_arg(ap, int); |
| in_objs[params.count++].type = ACPI_TYPE_INTEGER; |
| break; |
| /* add more types as needed */ |
| default: |
| pr_err("acpi_evalf() called with invalid format character '%c'\n", |
| c); |
| va_end(ap); |
| return 0; |
| } |
| } |
| va_end(ap); |
| |
| if (res_type != 'v') { |
| result.length = sizeof(out_obj); |
| result.pointer = &out_obj; |
| resultp = &result; |
| } else |
| resultp = NULL; |
| |
| status = acpi_evaluate_object(handle, method, ¶ms, resultp); |
| |
| switch (res_type) { |
| case 'd': /* int */ |
| success = (status == AE_OK && |
| out_obj.type == ACPI_TYPE_INTEGER); |
| if (success && res) |
| *res = out_obj.integer.value; |
| break; |
| case 'v': /* void */ |
| success = status == AE_OK; |
| break; |
| /* add more types as needed */ |
| default: |
| pr_err("acpi_evalf() called with invalid format character '%c'\n", |
| res_type); |
| return 0; |
| } |
| |
| if (!success && !quiet) |
| pr_err("acpi_evalf(%s, %s, ...) failed: %s\n", |
| method, fmt0, acpi_format_exception(status)); |
| |
| return success; |
| } |
| |
| static int acpi_ec_read(int i, u8 *p) |
| { |
| int v; |
| |
| if (ecrd_handle) { |
| if (!acpi_evalf(ecrd_handle, &v, NULL, "dd", i)) |
| return 0; |
| *p = v; |
| } else { |
| if (ec_read(i, p) < 0) |
| return 0; |
| } |
| |
| return 1; |
| } |
| |
| static int acpi_ec_write(int i, u8 v) |
| { |
| if (ecwr_handle) { |
| if (!acpi_evalf(ecwr_handle, NULL, NULL, "vdd", i, v)) |
| return 0; |
| } else { |
| if (ec_write(i, v) < 0) |
| return 0; |
| } |
| |
| return 1; |
| } |
| |
| static int issue_thinkpad_cmos_command(int cmos_cmd) |
| { |
| if (!cmos_handle) |
| return -ENXIO; |
| |
| if (!acpi_evalf(cmos_handle, NULL, NULL, "vd", cmos_cmd)) |
| return -EIO; |
| |
| return 0; |
| } |
| |
| /************************************************************************* |
| * ACPI device model |
| */ |
| |
| #define TPACPI_ACPIHANDLE_INIT(object) \ |
| drv_acpi_handle_init(#object, &object##_handle, *object##_parent, \ |
| object##_paths, ARRAY_SIZE(object##_paths)) |
| |
| static void __init drv_acpi_handle_init(const char *name, |
| acpi_handle *handle, const acpi_handle parent, |
| char **paths, const int num_paths) |
| { |
| int i; |
| acpi_status status; |
| |
| vdbg_printk(TPACPI_DBG_INIT, "trying to locate ACPI handle for %s\n", |
| name); |
| |
| for (i = 0; i < num_paths; i++) { |
| status = acpi_get_handle(parent, paths[i], handle); |
| if (ACPI_SUCCESS(status)) { |
| dbg_printk(TPACPI_DBG_INIT, |
| "Found ACPI handle %s for %s\n", |
| paths[i], name); |
| return; |
| } |
| } |
| |
| vdbg_printk(TPACPI_DBG_INIT, "ACPI handle for %s not found\n", |
| name); |
| *handle = NULL; |
| } |
| |
| static acpi_status __init tpacpi_acpi_handle_locate_callback(acpi_handle handle, |
| u32 level, void *context, void **return_value) |
| { |
| struct acpi_device *dev; |
| if (!strcmp(context, "video")) { |
| if (acpi_bus_get_device(handle, &dev)) |
| return AE_OK; |
| if (strcmp(ACPI_VIDEO_HID, acpi_device_hid(dev))) |
| return AE_OK; |
| } |
| |
| *(acpi_handle *)return_value = handle; |
| |
| return AE_CTRL_TERMINATE; |
| } |
| |
| static void __init tpacpi_acpi_handle_locate(const char *name, |
| const char *hid, |
| acpi_handle *handle) |
| { |
| acpi_status status; |
| acpi_handle device_found; |
| |
| BUG_ON(!name || !handle); |
| vdbg_printk(TPACPI_DBG_INIT, |
| "trying to locate ACPI handle for %s, using HID %s\n", |
| name, hid ? hid : "NULL"); |
| |
| memset(&device_found, 0, sizeof(device_found)); |
| status = acpi_get_devices(hid, tpacpi_acpi_handle_locate_callback, |
| (void *)name, &device_found); |
| |
| *handle = NULL; |
| |
| if (ACPI_SUCCESS(status)) { |
| *handle = device_found; |
| dbg_printk(TPACPI_DBG_INIT, |
| "Found ACPI handle for %s\n", name); |
| } else { |
| vdbg_printk(TPACPI_DBG_INIT, |
| "Could not locate an ACPI handle for %s: %s\n", |
| name, acpi_format_exception(status)); |
| } |
| } |
| |
| static void dispatch_acpi_notify(acpi_handle handle, u32 event, void *data) |
| { |
| struct ibm_struct *ibm = data; |
| |
| if (tpacpi_lifecycle != TPACPI_LIFE_RUNNING) |
| return; |
| |
| if (!ibm || !ibm->acpi || !ibm->acpi->notify) |
| return; |
| |
| ibm->acpi->notify(ibm, event); |
| } |
| |
| static int __init setup_acpi_notify(struct ibm_struct *ibm) |
| { |
| acpi_status status; |
| int rc; |
| |
| BUG_ON(!ibm->acpi); |
| |
| if (!*ibm->acpi->handle) |
| return 0; |
| |
| vdbg_printk(TPACPI_DBG_INIT, |
| "setting up ACPI notify for %s\n", ibm->name); |
| |
| rc = acpi_bus_get_device(*ibm->acpi->handle, &ibm->acpi->device); |
| if (rc < 0) { |
| pr_err("acpi_bus_get_device(%s) failed: %d\n", ibm->name, rc); |
| return -ENODEV; |
| } |
| |
| ibm->acpi->device->driver_data = ibm; |
| sprintf(acpi_device_class(ibm->acpi->device), "%s/%s", |
| TPACPI_ACPI_EVENT_PREFIX, |
| ibm->name); |
| |
| status = acpi_install_notify_handler(*ibm->acpi->handle, |
| ibm->acpi->type, dispatch_acpi_notify, ibm); |
| if (ACPI_FAILURE(status)) { |
| if (status == AE_ALREADY_EXISTS) { |
| pr_notice("another device driver is already handling %s events\n", |
| ibm->name); |
| } else { |
| pr_err("acpi_install_notify_handler(%s) failed: %s\n", |
| ibm->name, acpi_format_exception(status)); |
| } |
| return -ENODEV; |
| } |
| ibm->flags.acpi_notify_installed = 1; |
| return 0; |
| } |
| |
| static int __init tpacpi_device_add(struct acpi_device *device) |
| { |
| return 0; |
| } |
| |
| static int __init register_tpacpi_subdriver(struct ibm_struct *ibm) |
| { |
| int rc; |
| |
| dbg_printk(TPACPI_DBG_INIT, |
| "registering %s as an ACPI driver\n", ibm->name); |
| |
| BUG_ON(!ibm->acpi); |
| |
| ibm->acpi->driver = kzalloc(sizeof(struct acpi_driver), GFP_KERNEL); |
| if (!ibm->acpi->driver) { |
| pr_err("failed to allocate memory for ibm->acpi->driver\n"); |
| return -ENOMEM; |
| } |
| |
| sprintf(ibm->acpi->driver->name, "%s_%s", TPACPI_NAME, ibm->name); |
| ibm->acpi->driver->ids = ibm->acpi->hid; |
| |
| ibm->acpi->driver->ops.add = &tpacpi_device_add; |
| |
| rc = acpi_bus_register_driver(ibm->acpi->driver); |
| if (rc < 0) { |
| pr_err("acpi_bus_register_driver(%s) failed: %d\n", |
| ibm->name, rc); |
| kfree(ibm->acpi->driver); |
| ibm->acpi->driver = NULL; |
| } else if (!rc) |
| ibm->flags.acpi_driver_registered = 1; |
| |
| return rc; |
| } |
| |
| |
| /**************************************************************************** |
| **************************************************************************** |
| * |
| * Procfs Helpers |
| * |
| **************************************************************************** |
| ****************************************************************************/ |
| |
| static int dispatch_proc_show(struct seq_file *m, void *v) |
| { |
| struct ibm_struct *ibm = m->private; |
| |
| if (!ibm || !ibm->read) |
| return -EINVAL; |
| return ibm->read(m); |
| } |
| |
| static int dispatch_proc_open(struct inode *inode, struct file *file) |
| { |
| return single_open(file, dispatch_proc_show, PDE_DATA(inode)); |
| } |
| |
| static ssize_t dispatch_proc_write(struct file *file, |
| const char __user *userbuf, |
| size_t count, loff_t *pos) |
| { |
| struct ibm_struct *ibm = PDE_DATA(file_inode(file)); |
| char *kernbuf; |
| int ret; |
| |
| if (!ibm || !ibm->write) |
| return -EINVAL; |
| if (count > PAGE_SIZE - 2) |
| return -EINVAL; |
| |
| kernbuf = kmalloc(count + 2, GFP_KERNEL); |
| if (!kernbuf) |
| return -ENOMEM; |
| |
| if (copy_from_user(kernbuf, userbuf, count)) { |
| kfree(kernbuf); |
| return -EFAULT; |
| } |
| |
| kernbuf[count] = 0; |
| strcat(kernbuf, ","); |
| ret = ibm->write(kernbuf); |
| if (ret == 0) |
| ret = count; |
| |
| kfree(kernbuf); |
| |
| return ret; |
| } |
| |
| static const struct file_operations dispatch_proc_fops = { |
| .owner = THIS_MODULE, |
| .open = dispatch_proc_open, |
| .read = seq_read, |
| .llseek = seq_lseek, |
| .release = single_release, |
| .write = dispatch_proc_write, |
| }; |
| |
| static char *next_cmd(char **cmds) |
| { |
| char *start = *cmds; |
| char *end; |
| |
| while ((end = strchr(start, ',')) && end == start) |
| start = end + 1; |
| |
| if (!end) |
| return NULL; |
| |
| *end = 0; |
| *cmds = end + 1; |
| return start; |
| } |
| |
| |
| /**************************************************************************** |
| **************************************************************************** |
| * |
| * Device model: input, hwmon and platform |
| * |
| **************************************************************************** |
| ****************************************************************************/ |
| |
| static struct platform_device *tpacpi_pdev; |
| static struct platform_device *tpacpi_sensors_pdev; |
| static struct device *tpacpi_hwmon; |
| static struct input_dev *tpacpi_inputdev; |
| static struct mutex tpacpi_inputdev_send_mutex; |
| static LIST_HEAD(tpacpi_all_drivers); |
| |
| #ifdef CONFIG_PM_SLEEP |
| static int tpacpi_suspend_handler(struct device *dev) |
| { |
| struct ibm_struct *ibm, *itmp; |
| |
| list_for_each_entry_safe(ibm, itmp, |
| &tpacpi_all_drivers, |
| all_drivers) { |
| if (ibm->suspend) |
| (ibm->suspend)(); |
| } |
| |
| return 0; |
| } |
| |
| static int tpacpi_resume_handler(struct device *dev) |
| { |
| struct ibm_struct *ibm, *itmp; |
| |
| list_for_each_entry_safe(ibm, itmp, |
| &tpacpi_all_drivers, |
| all_drivers) { |
| if (ibm->resume) |
| (ibm->resume)(); |
| } |
| |
| return 0; |
| } |
| #endif |
| |
| static SIMPLE_DEV_PM_OPS(tpacpi_pm, |
| tpacpi_suspend_handler, tpacpi_resume_handler); |
| |
| static void tpacpi_shutdown_handler(struct platform_device *pdev) |
| { |
| struct ibm_struct *ibm, *itmp; |
| |
| list_for_each_entry_safe(ibm, itmp, |
| &tpacpi_all_drivers, |
| all_drivers) { |
| if (ibm->shutdown) |
| (ibm->shutdown)(); |
| } |
| } |
| |
| static struct platform_driver tpacpi_pdriver = { |
| .driver = { |
| .name = TPACPI_DRVR_NAME, |
| .pm = &tpacpi_pm, |
| }, |
| .shutdown = tpacpi_shutdown_handler, |
| }; |
| |
| static struct platform_driver tpacpi_hwmon_pdriver = { |
| .driver = { |
| .name = TPACPI_HWMON_DRVR_NAME, |
| }, |
| }; |
| |
| /************************************************************************* |
| * sysfs support helpers |
| */ |
| |
| struct attribute_set { |
| unsigned int members, max_members; |
| struct attribute_group group; |
| }; |
| |
| struct attribute_set_obj { |
| struct attribute_set s; |
| struct attribute *a; |
| } __attribute__((packed)); |
| |
| static struct attribute_set *create_attr_set(unsigned int max_members, |
| const char *name) |
| { |
| struct attribute_set_obj *sobj; |
| |
| if (max_members == 0) |
| return NULL; |
| |
| /* Allocates space for implicit NULL at the end too */ |
| sobj = kzalloc(sizeof(struct attribute_set_obj) + |
| max_members * sizeof(struct attribute *), |
| GFP_KERNEL); |
| if (!sobj) |
| return NULL; |
| sobj->s.max_members = max_members; |
| sobj->s.group.attrs = &sobj->a; |
| sobj->s.group.name = name; |
| |
| return &sobj->s; |
| } |
| |
| #define destroy_attr_set(_set) \ |
| kfree(_set); |
| |
| /* not multi-threaded safe, use it in a single thread per set */ |
| static int add_to_attr_set(struct attribute_set *s, struct attribute *attr) |
| { |
| if (!s || !attr) |
| return -EINVAL; |
| |
| if (s->members >= s->max_members) |
| return -ENOMEM; |
| |
| s->group.attrs[s->members] = attr; |
| s->members++; |
| |
| return 0; |
| } |
| |
| static int add_many_to_attr_set(struct attribute_set *s, |
| struct attribute **attr, |
| unsigned int count) |
| { |
| int i, res; |
| |
| for (i = 0; i < count; i++) { |
| res = add_to_attr_set(s, attr[i]); |
| if (res) |
| return res; |
| } |
| |
| return 0; |
| } |
| |
| static void delete_attr_set(struct attribute_set *s, struct kobject *kobj) |
| { |
| sysfs_remove_group(kobj, &s->group); |
| destroy_attr_set(s); |
| } |
| |
| #define register_attr_set_with_sysfs(_attr_set, _kobj) \ |
| sysfs_create_group(_kobj, &_attr_set->group) |
| |
| static int parse_strtoul(const char *buf, |
| unsigned long max, unsigned long *value) |
| { |
| char *endp; |
| |
| *value = simple_strtoul(skip_spaces(buf), &endp, 0); |
| endp = skip_spaces(endp); |
| if (*endp || *value > max) |
| return -EINVAL; |
| |
| return 0; |
| } |
| |
| static void tpacpi_disable_brightness_delay(void) |
| { |
| if (acpi_evalf(hkey_handle, NULL, "PWMS", "qvd", 0)) |
| pr_notice("ACPI backlight control delay disabled\n"); |
| } |
| |
| static void printk_deprecated_attribute(const char * const what, |
| const char * const details) |
| { |
| tpacpi_log_usertask("deprecated sysfs attribute"); |
| pr_warn("WARNING: sysfs attribute %s is deprecated and will be removed. %s\n", |
| what, details); |
| } |
| |
| /************************************************************************* |
| * rfkill and radio control support helpers |
| */ |
| |
| /* |
| * ThinkPad-ACPI firmware handling model: |
| * |
| * WLSW (master wireless switch) is event-driven, and is common to all |
| * firmware-controlled radios. It cannot be controlled, just monitored, |
| * as expected. It overrides all radio state in firmware |
| * |
| * The kernel, a masked-off hotkey, and WLSW can change the radio state |
| * (TODO: verify how WLSW interacts with the returned radio state). |
| * |
| * The only time there are shadow radio state changes, is when |
| * masked-off hotkeys are used. |
| */ |
| |
| /* |
| * Internal driver API for radio state: |
| * |
| * int: < 0 = error, otherwise enum tpacpi_rfkill_state |
| * bool: true means radio blocked (off) |
| */ |
| enum tpacpi_rfkill_state { |
| TPACPI_RFK_RADIO_OFF = 0, |
| TPACPI_RFK_RADIO_ON |
| }; |
| |
| /* rfkill switches */ |
| enum tpacpi_rfk_id { |
| TPACPI_RFK_BLUETOOTH_SW_ID = 0, |
| TPACPI_RFK_WWAN_SW_ID, |
| TPACPI_RFK_UWB_SW_ID, |
| TPACPI_RFK_SW_MAX |
| }; |
| |
| static const char *tpacpi_rfkill_names[] = { |
| [TPACPI_RFK_BLUETOOTH_SW_ID] = "bluetooth", |
| [TPACPI_RFK_WWAN_SW_ID] = "wwan", |
| [TPACPI_RFK_UWB_SW_ID] = "uwb", |
| [TPACPI_RFK_SW_MAX] = NULL |
| }; |
| |
| /* ThinkPad-ACPI rfkill subdriver */ |
| struct tpacpi_rfk { |
| struct rfkill *rfkill; |
| enum tpacpi_rfk_id id; |
| const struct tpacpi_rfk_ops *ops; |
| }; |
| |
| struct tpacpi_rfk_ops { |
| /* firmware interface */ |
| int (*get_status)(void); |
| int (*set_status)(const enum tpacpi_rfkill_state); |
| }; |
| |
| static struct tpacpi_rfk *tpacpi_rfkill_switches[TPACPI_RFK_SW_MAX]; |
| |
| /* Query FW and update rfkill sw state for a given rfkill switch */ |
| static int tpacpi_rfk_update_swstate(const struct tpacpi_rfk *tp_rfk) |
| { |
| int status; |
| |
| if (!tp_rfk) |
| return -ENODEV; |
| |
| status = (tp_rfk->ops->get_status)(); |
| if (status < 0) |
| return status; |
| |
| rfkill_set_sw_state(tp_rfk->rfkill, |
| (status == TPACPI_RFK_RADIO_OFF)); |
| |
| return status; |
| } |
| |
| /* Query FW and update rfkill sw state for all rfkill switches */ |
| static void tpacpi_rfk_update_swstate_all(void) |
| { |
| unsigned int i; |
| |
| for (i = 0; i < TPACPI_RFK_SW_MAX; i++) |
| tpacpi_rfk_update_swstate(tpacpi_rfkill_switches[i]); |
| } |
| |
| /* |
| * Sync the HW-blocking state of all rfkill switches, |
| * do notice it causes the rfkill core to schedule uevents |
| */ |
| static void tpacpi_rfk_update_hwblock_state(bool blocked) |
| { |
| unsigned int i; |
| struct tpacpi_rfk *tp_rfk; |
| |
| for (i = 0; i < TPACPI_RFK_SW_MAX; i++) { |
| tp_rfk = tpacpi_rfkill_switches[i]; |
| if (tp_rfk) { |
| if (rfkill_set_hw_state(tp_rfk->rfkill, |
| blocked)) { |
| /* ignore -- we track sw block */ |
| } |
| } |
| } |
| } |
| |
| /* Call to get the WLSW state from the firmware */ |
| static int hotkey_get_wlsw(void); |
| |
| /* Call to query WLSW state and update all rfkill switches */ |
| static bool tpacpi_rfk_check_hwblock_state(void) |
| { |
| int res = hotkey_get_wlsw(); |
| int hw_blocked; |
| |
| /* When unknown or unsupported, we have to assume it is unblocked */ |
| if (res < 0) |
| return false; |
| |
| hw_blocked = (res == TPACPI_RFK_RADIO_OFF); |
| tpacpi_rfk_update_hwblock_state(hw_blocked); |
| |
| return hw_blocked; |
| } |
| |
| static int tpacpi_rfk_hook_set_block(void *data, bool blocked) |
| { |
| struct tpacpi_rfk *tp_rfk = data; |
| int res; |
| |
| dbg_printk(TPACPI_DBG_RFKILL, |
| "request to change radio state to %s\n", |
| blocked ? "blocked" : "unblocked"); |
| |
| /* try to set radio state */ |
| res = (tp_rfk->ops->set_status)(blocked ? |
| TPACPI_RFK_RADIO_OFF : TPACPI_RFK_RADIO_ON); |
| |
| /* and update the rfkill core with whatever the FW really did */ |
| tpacpi_rfk_update_swstate(tp_rfk); |
| |
| return (res < 0) ? res : 0; |
| } |
| |
| static const struct rfkill_ops tpacpi_rfk_rfkill_ops = { |
| .set_block = tpacpi_rfk_hook_set_block, |
| }; |
| |
| static int __init tpacpi_new_rfkill(const enum tpacpi_rfk_id id, |
| const struct tpacpi_rfk_ops *tp_rfkops, |
| const enum rfkill_type rfktype, |
| const char *name, |
| const bool set_default) |
| { |
| struct tpacpi_rfk *atp_rfk; |
| int res; |
| bool sw_state = false; |
| bool hw_state; |
| int sw_status; |
| |
| BUG_ON(id >= TPACPI_RFK_SW_MAX || tpacpi_rfkill_switches[id]); |
| |
| atp_rfk = kzalloc(sizeof(struct tpacpi_rfk), GFP_KERNEL); |
| if (atp_rfk) |
| atp_rfk->rfkill = rfkill_alloc(name, |
| &tpacpi_pdev->dev, |
| rfktype, |
| &tpacpi_rfk_rfkill_ops, |
| atp_rfk); |
| if (!atp_rfk || !atp_rfk->rfkill) { |
| pr_err("failed to allocate memory for rfkill class\n"); |
| kfree(atp_rfk); |
| return -ENOMEM; |
| } |
| |
| atp_rfk->id = id; |
| atp_rfk->ops = tp_rfkops; |
| |
| sw_status = (tp_rfkops->get_status)(); |
| if (sw_status < 0) { |
| pr_err("failed to read initial state for %s, error %d\n", |
| name, sw_status); |
| } else { |
| sw_state = (sw_status == TPACPI_RFK_RADIO_OFF); |
| if (set_default) { |
| /* try to keep the initial state, since we ask the |
| * firmware to preserve it across S5 in NVRAM */ |
| rfkill_init_sw_state(atp_rfk->rfkill, sw_state); |
| } |
| } |
| hw_state = tpacpi_rfk_check_hwblock_state(); |
| rfkill_set_hw_state(atp_rfk->rfkill, hw_state); |
| |
| res = rfkill_register(atp_rfk->rfkill); |
| if (res < 0) { |
| pr_err("failed to register %s rfkill switch: %d\n", name, res); |
| rfkill_destroy(atp_rfk->rfkill); |
| kfree(atp_rfk); |
| return res; |
| } |
| |
| tpacpi_rfkill_switches[id] = atp_rfk; |
| |
| pr_info("rfkill switch %s: radio is %sblocked\n", |
| name, (sw_state || hw_state) ? "" : "un"); |
| return 0; |
| } |
| |
| static void tpacpi_destroy_rfkill(const enum tpacpi_rfk_id id) |
| { |
| struct tpacpi_rfk *tp_rfk; |
| |
| BUG_ON(id >= TPACPI_RFK_SW_MAX); |
| |
| tp_rfk = tpacpi_rfkill_switches[id]; |
| if (tp_rfk) { |
| rfkill_unregister(tp_rfk->rfkill); |
| rfkill_destroy(tp_rfk->rfkill); |
| tpacpi_rfkill_switches[id] = NULL; |
| kfree(tp_rfk); |
| } |
| } |
| |
| static void printk_deprecated_rfkill_attribute(const char * const what) |
| { |
| printk_deprecated_attribute(what, |
| "Please switch to generic rfkill before year 2010"); |
| } |
| |
| /* sysfs <radio> enable ------------------------------------------------ */ |
| static ssize_t tpacpi_rfk_sysfs_enable_show(const enum tpacpi_rfk_id id, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| int status; |
| |
| printk_deprecated_rfkill_attribute(attr->attr.name); |
| |
| /* This is in the ABI... */ |
| if (tpacpi_rfk_check_hwblock_state()) { |
| status = TPACPI_RFK_RADIO_OFF; |
| } else { |
| status = tpacpi_rfk_update_swstate(tpacpi_rfkill_switches[id]); |
| if (status < 0) |
| return status; |
| } |
| |
| return snprintf(buf, PAGE_SIZE, "%d\n", |
| (status == TPACPI_RFK_RADIO_ON) ? 1 : 0); |
| } |
| |
| static ssize_t tpacpi_rfk_sysfs_enable_store(const enum tpacpi_rfk_id id, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| unsigned long t; |
| int res; |
| |
| printk_deprecated_rfkill_attribute(attr->attr.name); |
| |
| if (parse_strtoul(buf, 1, &t)) |
| return -EINVAL; |
| |
| tpacpi_disclose_usertask(attr->attr.name, "set to %ld\n", t); |
| |
| /* This is in the ABI... */ |
| if (tpacpi_rfk_check_hwblock_state() && !!t) |
| return -EPERM; |
| |
| res = tpacpi_rfkill_switches[id]->ops->set_status((!!t) ? |
| TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF); |
| tpacpi_rfk_update_swstate(tpacpi_rfkill_switches[id]); |
| |
| return (res < 0) ? res : count; |
| } |
| |
| /* procfs -------------------------------------------------------------- */ |
| static int tpacpi_rfk_procfs_read(const enum tpacpi_rfk_id id, struct seq_file *m) |
| { |
| if (id >= TPACPI_RFK_SW_MAX) |
| seq_printf(m, "status:\t\tnot supported\n"); |
| else { |
| int status; |
| |
| /* This is in the ABI... */ |
| if (tpacpi_rfk_check_hwblock_state()) { |
| status = TPACPI_RFK_RADIO_OFF; |
| } else { |
| status = tpacpi_rfk_update_swstate( |
| tpacpi_rfkill_switches[id]); |
| if (status < 0) |
| return status; |
| } |
| |
| seq_printf(m, "status:\t\t%s\n", |
| (status == TPACPI_RFK_RADIO_ON) ? |
| "enabled" : "disabled"); |
| seq_printf(m, "commands:\tenable, disable\n"); |
| } |
| |
| return 0; |
| } |
| |
| static int tpacpi_rfk_procfs_write(const enum tpacpi_rfk_id id, char *buf) |
| { |
| char *cmd; |
| int status = -1; |
| int res = 0; |
| |
| if (id >= TPACPI_RFK_SW_MAX) |
| return -ENODEV; |
| |
| while ((cmd = next_cmd(&buf))) { |
| if (strlencmp(cmd, "enable") == 0) |
| status = TPACPI_RFK_RADIO_ON; |
| else if (strlencmp(cmd, "disable") == 0) |
| status = TPACPI_RFK_RADIO_OFF; |
| else |
| return -EINVAL; |
| } |
| |
| if (status != -1) { |
| tpacpi_disclose_usertask("procfs", "attempt to %s %s\n", |
| (status == TPACPI_RFK_RADIO_ON) ? |
| "enable" : "disable", |
| tpacpi_rfkill_names[id]); |
| res = (tpacpi_rfkill_switches[id]->ops->set_status)(status); |
| tpacpi_rfk_update_swstate(tpacpi_rfkill_switches[id]); |
| } |
| |
| return res; |
| } |
| |
| /************************************************************************* |
| * thinkpad-acpi driver attributes |
| */ |
| |
| /* interface_version --------------------------------------------------- */ |
| static ssize_t interface_version_show(struct device_driver *drv, char *buf) |
| { |
| return snprintf(buf, PAGE_SIZE, "0x%08x\n", TPACPI_SYSFS_VERSION); |
| } |
| static DRIVER_ATTR_RO(interface_version); |
| |
| /* debug_level --------------------------------------------------------- */ |
| static ssize_t debug_level_show(struct device_driver *drv, char *buf) |
| { |
| return snprintf(buf, PAGE_SIZE, "0x%04x\n", dbg_level); |
| } |
| |
| static ssize_t debug_level_store(struct device_driver *drv, const char *buf, |
| size_t count) |
| { |
| unsigned long t; |
| |
| if (parse_strtoul(buf, 0xffff, &t)) |
| return -EINVAL; |
| |
| dbg_level = t; |
| |
| return count; |
| } |
| static DRIVER_ATTR_RW(debug_level); |
| |
| /* version ------------------------------------------------------------- */ |
| static ssize_t version_show(struct device_driver *drv, char *buf) |
| { |
| return snprintf(buf, PAGE_SIZE, "%s v%s\n", |
| TPACPI_DESC, TPACPI_VERSION); |
| } |
| static DRIVER_ATTR_RO(version); |
| |
| /* --------------------------------------------------------------------- */ |
| |
| #ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES |
| |
| /* wlsw_emulstate ------------------------------------------------------ */ |
| static ssize_t wlsw_emulstate_show(struct device_driver *drv, char *buf) |
| { |
| return snprintf(buf, PAGE_SIZE, "%d\n", !!tpacpi_wlsw_emulstate); |
| } |
| |
| static ssize_t wlsw_emulstate_store(struct device_driver *drv, const char *buf, |
| size_t count) |
| { |
| unsigned long t; |
| |
| if (parse_strtoul(buf, 1, &t)) |
| return -EINVAL; |
| |
| if (tpacpi_wlsw_emulstate != !!t) { |
| tpacpi_wlsw_emulstate = !!t; |
| tpacpi_rfk_update_hwblock_state(!t); /* negative logic */ |
| } |
| |
| return count; |
| } |
| static DRIVER_ATTR_RW(wlsw_emulstate); |
| |
| /* bluetooth_emulstate ------------------------------------------------- */ |
| static ssize_t bluetooth_emulstate_show(struct device_driver *drv, char *buf) |
| { |
| return snprintf(buf, PAGE_SIZE, "%d\n", !!tpacpi_bluetooth_emulstate); |
| } |
| |
| static ssize_t bluetooth_emulstate_store(struct device_driver *drv, |
| const char *buf, size_t count) |
| { |
| unsigned long t; |
| |
| if (parse_strtoul(buf, 1, &t)) |
| return -EINVAL; |
| |
| tpacpi_bluetooth_emulstate = !!t; |
| |
| return count; |
| } |
| static DRIVER_ATTR_RW(bluetooth_emulstate); |
| |
| /* wwan_emulstate ------------------------------------------------- */ |
| static ssize_t wwan_emulstate_show(struct device_driver *drv, char *buf) |
| { |
| return snprintf(buf, PAGE_SIZE, "%d\n", !!tpacpi_wwan_emulstate); |
| } |
| |
| static ssize_t wwan_emulstate_store(struct device_driver *drv, const char *buf, |
| size_t count) |
| { |
| unsigned long t; |
| |
| if (parse_strtoul(buf, 1, &t)) |
| return -EINVAL; |
| |
| tpacpi_wwan_emulstate = !!t; |
| |
| return count; |
| } |
| static DRIVER_ATTR_RW(wwan_emulstate); |
| |
| /* uwb_emulstate ------------------------------------------------- */ |
| static ssize_t uwb_emulstate_show(struct device_driver *drv, char *buf) |
| { |
| return snprintf(buf, PAGE_SIZE, "%d\n", !!tpacpi_uwb_emulstate); |
| } |
| |
| static ssize_t uwb_emulstate_store(struct device_driver *drv, const char *buf, |
| size_t count) |
| { |
| unsigned long t; |
| |
| if (parse_strtoul(buf, 1, &t)) |
| return -EINVAL; |
| |
| tpacpi_uwb_emulstate = !!t; |
| |
| return count; |
| } |
| static DRIVER_ATTR_RW(uwb_emulstate); |
| #endif |
| |
| /* --------------------------------------------------------------------- */ |
| |
| static struct driver_attribute *tpacpi_driver_attributes[] = { |
| &driver_attr_debug_level, &driver_attr_version, |
| &driver_attr_interface_version, |
| }; |
| |
| static int __init tpacpi_create_driver_attributes(struct device_driver *drv) |
| { |
| int i, res; |
| |
| i = 0; |
| res = 0; |
| while (!res && i < ARRAY_SIZE(tpacpi_driver_attributes)) { |
| res = driver_create_file(drv, tpacpi_driver_attributes[i]); |
| i++; |
| } |
| |
| #ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES |
| if (!res && dbg_wlswemul) |
| res = driver_create_file(drv, &driver_attr_wlsw_emulstate); |
| if (!res && dbg_bluetoothemul) |
| res = driver_create_file(drv, &driver_attr_bluetooth_emulstate); |
| if (!res && dbg_wwanemul) |
| res = driver_create_file(drv, &driver_attr_wwan_emulstate); |
| if (!res && dbg_uwbemul) |
| res = driver_create_file(drv, &driver_attr_uwb_emulstate); |
| #endif |
| |
| return res; |
| } |
| |
| static void tpacpi_remove_driver_attributes(struct device_driver *drv) |
| { |
| int i; |
| |
| for (i = 0; i < ARRAY_SIZE(tpacpi_driver_attributes); i++) |
| driver_remove_file(drv, tpacpi_driver_attributes[i]); |
| |
| #ifdef THINKPAD_ACPI_DEBUGFACILITIES |
| driver_remove_file(drv, &driver_attr_wlsw_emulstate); |
| driver_remove_file(drv, &driver_attr_bluetooth_emulstate); |
| driver_remove_file(drv, &driver_attr_wwan_emulstate); |
| driver_remove_file(drv, &driver_attr_uwb_emulstate); |
| #endif |
| } |
| |
| /************************************************************************* |
| * Firmware Data |
| */ |
| |
| /* |
| * Table of recommended minimum BIOS versions |
| * |
| * Reasons for listing: |
| * 1. Stable BIOS, listed because the unknown amount of |
| * bugs and bad ACPI behaviour on older versions |
| * |
| * 2. BIOS or EC fw with known bugs that trigger on Linux |
| * |
| * 3. BIOS with known reduced functionality in older versions |
| * |
| * We recommend the latest BIOS and EC version. |
| * We only support the latest BIOS and EC fw version as a rule. |
| * |
| * Sources: IBM ThinkPad Public Web Documents (update changelogs), |
| * Information from users in ThinkWiki |
| * |
| * WARNING: we use this table also to detect that the machine is |
| * a ThinkPad in some cases, so don't remove entries lightly. |
| */ |
| |
| #define TPV_Q(__v, __id1, __id2, __bv1, __bv2) \ |
| { .vendor = (__v), \ |
| .bios = TPID(__id1, __id2), \ |
| .ec = TPACPI_MATCH_ANY, \ |
| .quirks = TPACPI_MATCH_ANY << 16 \ |
| | (__bv1) << 8 | (__bv2) } |
| |
| #define TPV_Q_X(__v, __bid1, __bid2, __bv1, __bv2, \ |
| __eid, __ev1, __ev2) \ |
| { .vendor = (__v), \ |
| .bios = TPID(__bid1, __bid2), \ |
| .ec = __eid, \ |
| .quirks = (__ev1) << 24 | (__ev2) << 16 \ |
| | (__bv1) << 8 | (__bv2) } |
| |
| #define TPV_QI0(__id1, __id2, __bv1, __bv2) \ |
| TPV_Q(PCI_VENDOR_ID_IBM, __id1, __id2, __bv1, __bv2) |
| |
| /* Outdated IBM BIOSes often lack the EC id string */ |
| #define TPV_QI1(__id1, __id2, __bv1, __bv2, __ev1, __ev2) \ |
| TPV_Q_X(PCI_VENDOR_ID_IBM, __id1, __id2, \ |
| __bv1, __bv2, TPID(__id1, __id2), \ |
| __ev1, __ev2), \ |
| TPV_Q_X(PCI_VENDOR_ID_IBM, __id1, __id2, \ |
| __bv1, __bv2, TPACPI_MATCH_UNKNOWN, \ |
| __ev1, __ev2) |
| |
| /* Outdated IBM BIOSes often lack the EC id string */ |
| #define TPV_QI2(__bid1, __bid2, __bv1, __bv2, \ |
| __eid1, __eid2, __ev1, __ev2) \ |
| TPV_Q_X(PCI_VENDOR_ID_IBM, __bid1, __bid2, \ |
| __bv1, __bv2, TPID(__eid1, __eid2), \ |
| __ev1, __ev2), \ |
| TPV_Q_X(PCI_VENDOR_ID_IBM, __bid1, __bid2, \ |
| __bv1, __bv2, TPACPI_MATCH_UNKNOWN, \ |
| __ev1, __ev2) |
| |
| #define TPV_QL0(__id1, __id2, __bv1, __bv2) \ |
| TPV_Q(PCI_VENDOR_ID_LENOVO, __id1, __id2, __bv1, __bv2) |
| |
| #define TPV_QL1(__id1, __id2, __bv1, __bv2, __ev1, __ev2) \ |
| TPV_Q_X(PCI_VENDOR_ID_LENOVO, __id1, __id2, \ |
| __bv1, __bv2, TPID(__id1, __id2), \ |
| __ev1, __ev2) |
| |
| #define TPV_QL2(__bid1, __bid2, __bv1, __bv2, \ |
| __eid1, __eid2, __ev1, __ev2) \ |
| TPV_Q_X(PCI_VENDOR_ID_LENOVO, __bid1, __bid2, \ |
| __bv1, __bv2, TPID(__eid1, __eid2), \ |
| __ev1, __ev2) |
| |
| static const struct tpacpi_quirk tpacpi_bios_version_qtable[] __initconst = { |
| /* Numeric models ------------------ */ |
| /* FW MODEL BIOS VERS */ |
| TPV_QI0('I', 'M', '6', '5'), /* 570 */ |
| TPV_QI0('I', 'U', '2', '6'), /* 570E */ |
| TPV_QI0('I', 'B', '5', '4'), /* 600 */ |
| TPV_QI0('I', 'H', '4', '7'), /* 600E */ |
| TPV_QI0('I', 'N', '3', '6'), /* 600E */ |
| TPV_QI0('I', 'T', '5', '5'), /* 600X */ |
| TPV_QI0('I', 'D', '4', '8'), /* 770, 770E, 770ED */ |
| TPV_QI0('I', 'I', '4', '2'), /* 770X */ |
| TPV_QI0('I', 'O', '2', '3'), /* 770Z */ |
| |
| /* A-series ------------------------- */ |
| /* FW MODEL BIOS VERS EC VERS */ |
| TPV_QI0('I', 'W', '5', '9'), /* A20m */ |
| TPV_QI0('I', 'V', '6', '9'), /* A20p */ |
| TPV_QI0('1', '0', '2', '6'), /* A21e, A22e */ |
| TPV_QI0('K', 'U', '3', '6'), /* A21e */ |
| TPV_QI0('K', 'X', '3', '6'), /* A21m, A22m */ |
| TPV_QI0('K', 'Y', '3', '8'), /* A21p, A22p */ |
| TPV_QI0('1', 'B', '1', '7'), /* A22e */ |
| TPV_QI0('1', '3', '2', '0'), /* A22m */ |
| TPV_QI0('1', 'E', '7', '3'), /* A30/p (0) */ |
| TPV_QI1('1', 'G', '4', '1', '1', '7'), /* A31/p (0) */ |
| TPV_QI1('1', 'N', '1', '6', '0', '7'), /* A31/p (0) */ |
| |
| /* G-series ------------------------- */ |
| /* FW MODEL BIOS VERS */ |
| TPV_QI0('1', 'T', 'A', '6'), /* G40 */ |
| TPV_QI0('1', 'X', '5', '7'), /* G41 */ |
| |
| /* R-series, T-series --------------- */ |
| /* FW MODEL BIOS VERS EC VERS */ |
| TPV_QI0('1', 'C', 'F', '0'), /* R30 */ |
| TPV_QI0('1', 'F', 'F', '1'), /* R31 */ |
| TPV_QI0('1', 'M', '9', '7'), /* R32 */ |
| TPV_QI0('1', 'O', '6', '1'), /* R40 */ |
| TPV_QI0('1', 'P', '6', '5'), /* R40 */ |
| TPV_QI0('1', 'S', '7', '0'), /* R40e */ |
| TPV_QI1('1', 'R', 'D', 'R', '7', '1'), /* R50/p, R51, |
| T40/p, T41/p, T42/p (1) */ |
| TPV_QI1('1', 'V', '7', '1', '2', '8'), /* R50e, R51 (1) */ |
| TPV_QI1('7', '8', '7', '1', '0', '6'), /* R51e (1) */ |
| TPV_QI1('7', '6', '6', '9', '1', '6'), /* R52 (1) */ |
| TPV_QI1('7', '0', '6', '9', '2', '8'), /* R52, T43 (1) */ |
| |
| TPV_QI0('I', 'Y', '6', '1'), /* T20 */ |
| TPV_QI0('K', 'Z', '3', '4'), /* T21 */ |
| TPV_QI0('1', '6', '3', '2'), /* T22 */ |
| TPV_QI1('1', 'A', '6', '4', '2', '3'), /* T23 (0) */ |
| TPV_QI1('1', 'I', '7', '1', '2', '0'), /* T30 (0) */ |
| TPV_QI1('1', 'Y', '6', '5', '2', '9'), /* T43/p (1) */ |
| |
| TPV_QL1('7', '9', 'E', '3', '5', '0'), /* T60/p */ |
| TPV_QL1('7', 'C', 'D', '2', '2', '2'), /* R60, R60i */ |
| TPV_QL1('7', 'E', 'D', '0', '1', '5'), /* R60e, R60i */ |
| |
| /* BIOS FW BIOS VERS EC FW EC VERS */ |
| TPV_QI2('1', 'W', '9', '0', '1', 'V', '2', '8'), /* R50e (1) */ |
| TPV_QL2('7', 'I', '3', '4', '7', '9', '5', '0'), /* T60/p wide */ |
| |
| /* X-series ------------------------- */ |
| /* FW MODEL BIOS VERS EC VERS */ |
| TPV_QI0('I', 'Z', '9', 'D'), /* X20, X21 */ |
| TPV_QI0('1', 'D', '7', '0'), /* X22, X23, X24 */ |
| TPV_QI1('1', 'K', '4', '8', '1', '8'), /* X30 (0) */ |
| TPV_QI1('1', 'Q', '9', '7', '2', '3'), /* X31, X32 (0) */ |
| TPV_QI1('1', 'U', 'D', '3', 'B', '2'), /* X40 (0) */ |
| TPV_QI1('7', '4', '6', '4', '2', '7'), /* X41 (0) */ |
| TPV_QI1('7', '5', '6', '0', '2', '0'), /* X41t (0) */ |
| |
| TPV_QL1('7', 'B', 'D', '7', '4', '0'), /* X60/s */ |
| TPV_QL1('7', 'J', '3', '0', '1', '3'), /* X60t */ |
| |
| /* (0) - older versions lack DMI EC fw string and functionality */ |
| /* (1) - older versions known to lack functionality */ |
| }; |
| |
| #undef TPV_QL1 |
| #undef TPV_QL0 |
| #undef TPV_QI2 |
| #undef TPV_QI1 |
| #undef TPV_QI0 |
| #undef TPV_Q_X |
| #undef TPV_Q |
| |
| static void __init tpacpi_check_outdated_fw(void) |
| { |
| unsigned long fwvers; |
| u16 ec_version, bios_version; |
| |
| fwvers = tpacpi_check_quirks(tpacpi_bios_version_qtable, |
| ARRAY_SIZE(tpacpi_bios_version_qtable)); |
| |
| if (!fwvers) |
| return; |
| |
| bios_version = fwvers & 0xffffU; |
| ec_version = (fwvers >> 16) & 0xffffU; |
| |
| /* note that unknown versions are set to 0x0000 and we use that */ |
| if ((bios_version > thinkpad_id.bios_release) || |
| (ec_version > thinkpad_id.ec_release && |
| ec_version != TPACPI_MATCH_ANY)) { |
| /* |
| * The changelogs would let us track down the exact |
| * reason, but it is just too much of a pain to track |
| * it. We only list BIOSes that are either really |
| * broken, or really stable to begin with, so it is |
| * best if the user upgrades the firmware anyway. |
| */ |
| pr_warn("WARNING: Outdated ThinkPad BIOS/EC firmware\n"); |
| pr_warn("WARNING: This firmware may be missing critical bug fixes and/or important features\n"); |
| } |
| } |
| |
| static bool __init tpacpi_is_fw_known(void) |
| { |
| return tpacpi_check_quirks(tpacpi_bios_version_qtable, |
| ARRAY_SIZE(tpacpi_bios_version_qtable)) != 0; |
| } |
| |
| /**************************************************************************** |
| **************************************************************************** |
| * |
| * Subdrivers |
| * |
| **************************************************************************** |
| ****************************************************************************/ |
| |
| /************************************************************************* |
| * thinkpad-acpi metadata subdriver |
| */ |
| |
| static int thinkpad_acpi_driver_read(struct seq_file *m) |
| { |
| seq_printf(m, "driver:\t\t%s\n", TPACPI_DESC); |
| seq_printf(m, "version:\t%s\n", TPACPI_VERSION); |
| return 0; |
| } |
| |
| static struct ibm_struct thinkpad_acpi_driver_data = { |
| .name = "driver", |
| .read = thinkpad_acpi_driver_read, |
| }; |
| |
| /************************************************************************* |
| * Hotkey subdriver |
| */ |
| |
| /* |
| * ThinkPad firmware event model |
| * |
| * The ThinkPad firmware has two main event interfaces: normal ACPI |
| * notifications (which follow the ACPI standard), and a private event |
| * interface. |
| * |
| * The private event interface also issues events for the hotkeys. As |
| * the driver gained features, the event handling code ended up being |
| * built around the hotkey subdriver. This will need to be refactored |
| * to a more formal event API eventually. |
| * |
| * Some "hotkeys" are actually supposed to be used as event reports, |
| * such as "brightness has changed", "volume has changed", depending on |
| * the ThinkPad model and how the firmware is operating. |
| * |
| * Unlike other classes, hotkey-class events have mask/unmask control on |
| * non-ancient firmware. However, how it behaves changes a lot with the |
| * firmware model and version. |
| */ |
| |
| enum { /* hot key scan codes (derived from ACPI DSDT) */ |
| TP_ACPI_HOTKEYSCAN_FNF1 = 0, |
| TP_ACPI_HOTKEYSCAN_FNF2, |
| TP_ACPI_HOTKEYSCAN_FNF3, |
| TP_ACPI_HOTKEYSCAN_FNF4, |
| TP_ACPI_HOTKEYSCAN_FNF5, |
| TP_ACPI_HOTKEYSCAN_FNF6, |
| TP_ACPI_HOTKEYSCAN_FNF7, |
| TP_ACPI_HOTKEYSCAN_FNF8, |
| TP_ACPI_HOTKEYSCAN_FNF9, |
| TP_ACPI_HOTKEYSCAN_FNF10, |
| TP_ACPI_HOTKEYSCAN_FNF11, |
| TP_ACPI_HOTKEYSCAN_FNF12, |
| TP_ACPI_HOTKEYSCAN_FNBACKSPACE, |
| TP_ACPI_HOTKEYSCAN_FNINSERT, |
| TP_ACPI_HOTKEYSCAN_FNDELETE, |
| TP_ACPI_HOTKEYSCAN_FNHOME, |
| TP_ACPI_HOTKEYSCAN_FNEND, |
| TP_ACPI_HOTKEYSCAN_FNPAGEUP, |
| TP_ACPI_HOTKEYSCAN_FNPAGEDOWN, |
| TP_ACPI_HOTKEYSCAN_FNSPACE, |
| TP_ACPI_HOTKEYSCAN_VOLUMEUP, |
| TP_ACPI_HOTKEYSCAN_VOLUMEDOWN, |
| TP_ACPI_HOTKEYSCAN_MUTE, |
| TP_ACPI_HOTKEYSCAN_THINKPAD, |
| TP_ACPI_HOTKEYSCAN_UNK1, |
| TP_ACPI_HOTKEYSCAN_UNK2, |
| TP_ACPI_HOTKEYSCAN_UNK3, |
| TP_ACPI_HOTKEYSCAN_UNK4, |
| TP_ACPI_HOTKEYSCAN_UNK5, |
| TP_ACPI_HOTKEYSCAN_UNK6, |
| TP_ACPI_HOTKEYSCAN_UNK7, |
| TP_ACPI_HOTKEYSCAN_UNK8, |
| |
| /* Adaptive keyboard keycodes */ |
| TP_ACPI_HOTKEYSCAN_ADAPTIVE_START, |
| TP_ACPI_HOTKEYSCAN_MUTE2 = TP_ACPI_HOTKEYSCAN_ADAPTIVE_START, |
| TP_ACPI_HOTKEYSCAN_BRIGHTNESS_ZERO, |
| TP_ACPI_HOTKEYSCAN_CLIPPING_TOOL, |
| TP_ACPI_HOTKEYSCAN_CLOUD, |
| TP_ACPI_HOTKEYSCAN_UNK9, |
| TP_ACPI_HOTKEYSCAN_VOICE, |
| TP_ACPI_HOTKEYSCAN_UNK10, |
| TP_ACPI_HOTKEYSCAN_GESTURES, |
| TP_ACPI_HOTKEYSCAN_UNK11, |
| TP_ACPI_HOTKEYSCAN_UNK12, |
| TP_ACPI_HOTKEYSCAN_UNK13, |
| TP_ACPI_HOTKEYSCAN_CONFIG, |
| TP_ACPI_HOTKEYSCAN_NEW_TAB, |
| TP_ACPI_HOTKEYSCAN_RELOAD, |
| TP_ACPI_HOTKEYSCAN_BACK, |
| TP_ACPI_HOTKEYSCAN_MIC_DOWN, |
| TP_ACPI_HOTKEYSCAN_MIC_UP, |
| TP_ACPI_HOTKEYSCAN_MIC_CANCELLATION, |
| TP_ACPI_HOTKEYSCAN_CAMERA_MODE, |
| TP_ACPI_HOTKEYSCAN_ROTATE_DISPLAY, |
| |
| /* Lenovo extended keymap, starting at 0x1300 */ |
| TP_ACPI_HOTKEYSCAN_EXTENDED_START, |
| /* first new observed key (star, favorites) is 0x1311 */ |
| TP_ACPI_HOTKEYSCAN_STAR = 69, |
| TP_ACPI_HOTKEYSCAN_CLIPPING_TOOL2, |
| TP_ACPI_HOTKEYSCAN_UNK25, |
| TP_ACPI_HOTKEYSCAN_BLUETOOTH, |
| TP_ACPI_HOTKEYSCAN_KEYBOARD, |
| |
| /* Hotkey keymap size */ |
| TPACPI_HOTKEY_MAP_LEN |
| }; |
| |
| enum { /* Keys/events available through NVRAM polling */ |
| TPACPI_HKEY_NVRAM_KNOWN_MASK = 0x00fb88c0U, |
| TPACPI_HKEY_NVRAM_GOOD_MASK = 0x00fb8000U, |
| }; |
| |
| enum { /* Positions of some of the keys in hotkey masks */ |
| TP_ACPI_HKEY_DISPSWTCH_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNF7, |
| TP_ACPI_HKEY_DISPXPAND_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNF8, |
| TP_ACPI_HKEY_HIBERNATE_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNF12, |
| TP_ACPI_HKEY_BRGHTUP_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNHOME, |
| TP_ACPI_HKEY_BRGHTDWN_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNEND, |
| TP_ACPI_HKEY_KBD_LIGHT_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNPAGEUP, |
| TP_ACPI_HKEY_ZOOM_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNSPACE, |
| TP_ACPI_HKEY_VOLUP_MASK = 1 << TP_ACPI_HOTKEYSCAN_VOLUMEUP, |
| TP_ACPI_HKEY_VOLDWN_MASK = 1 << TP_ACPI_HOTKEYSCAN_VOLUMEDOWN, |
| TP_ACPI_HKEY_MUTE_MASK = 1 << TP_ACPI_HOTKEYSCAN_MUTE, |
| TP_ACPI_HKEY_THINKPAD_MASK = 1 << TP_ACPI_HOTKEYSCAN_THINKPAD, |
| }; |
| |
| enum { /* NVRAM to ACPI HKEY group map */ |
| TP_NVRAM_HKEY_GROUP_HK2 = TP_ACPI_HKEY_THINKPAD_MASK | |
| TP_ACPI_HKEY_ZOOM_MASK | |
| TP_ACPI_HKEY_DISPSWTCH_MASK | |
| TP_ACPI_HKEY_HIBERNATE_MASK, |
| TP_NVRAM_HKEY_GROUP_BRIGHTNESS = TP_ACPI_HKEY_BRGHTUP_MASK | |
| TP_ACPI_HKEY_BRGHTDWN_MASK, |
| TP_NVRAM_HKEY_GROUP_VOLUME = TP_ACPI_HKEY_VOLUP_MASK | |
| TP_ACPI_HKEY_VOLDWN_MASK | |
| TP_ACPI_HKEY_MUTE_MASK, |
| }; |
| |
| #ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL |
| struct tp_nvram_state { |
| u16 thinkpad_toggle:1; |
| u16 zoom_toggle:1; |
| u16 display_toggle:1; |
| u16 thinklight_toggle:1; |
| u16 hibernate_toggle:1; |
| u16 displayexp_toggle:1; |
| u16 display_state:1; |
| u16 brightness_toggle:1; |
| u16 volume_toggle:1; |
| u16 mute:1; |
| |
| u8 brightness_level; |
| u8 volume_level; |
| }; |
| |
| /* kthread for the hotkey poller */ |
| static struct task_struct *tpacpi_hotkey_task; |
| |
| /* |
| * Acquire mutex to write poller control variables as an |
| * atomic block. |
| * |
| * Increment hotkey_config_change when changing them if you |
| * want the kthread to forget old state. |
| * |
| * See HOTKEY_CONFIG_CRITICAL_START/HOTKEY_CONFIG_CRITICAL_END |
| */ |
| static struct mutex hotkey_thread_data_mutex; |
| static unsigned int hotkey_config_change; |
| |
| /* |
| * hotkey poller control variables |
| * |
| * Must be atomic or readers will also need to acquire mutex |
| * |
| * HOTKEY_CONFIG_CRITICAL_START/HOTKEY_CONFIG_CRITICAL_END |
| * should be used only when the changes need to be taken as |
| * a block, OR when one needs to force the kthread to forget |
| * old state. |
| */ |
| static u32 hotkey_source_mask; /* bit mask 0=ACPI,1=NVRAM */ |
| static unsigned int hotkey_poll_freq = 10; /* Hz */ |
| |
| #define HOTKEY_CONFIG_CRITICAL_START \ |
| do { \ |
| mutex_lock(&hotkey_thread_data_mutex); \ |
| hotkey_config_change++; \ |
| } while (0); |
| #define HOTKEY_CONFIG_CRITICAL_END \ |
| mutex_unlock(&hotkey_thread_data_mutex); |
| |
| #else /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ |
| |
| #define hotkey_source_mask 0U |
| #define HOTKEY_CONFIG_CRITICAL_START |
| #define HOTKEY_CONFIG_CRITICAL_END |
| |
| #endif /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ |
| |
| static struct mutex hotkey_mutex; |
| |
| static enum { /* Reasons for waking up */ |
| TP_ACPI_WAKEUP_NONE = 0, /* None or unknown */ |
| TP_ACPI_WAKEUP_BAYEJ, /* Bay ejection request */ |
| TP_ACPI_WAKEUP_UNDOCK, /* Undock request */ |
| } hotkey_wakeup_reason; |
| |
| static int hotkey_autosleep_ack; |
| |
| static u32 hotkey_orig_mask; /* events the BIOS had enabled */ |
| static u32 hotkey_all_mask; /* all events supported in fw */ |
| static u32 hotkey_adaptive_all_mask; /* all adaptive events supported in fw */ |
| static u32 hotkey_reserved_mask; /* events better left disabled */ |
| static u32 hotkey_driver_mask; /* events needed by the driver */ |
| static u32 hotkey_user_mask; /* events visible to userspace */ |
| static u32 hotkey_acpi_mask; /* events enabled in firmware */ |
| |
| static u16 *hotkey_keycode_map; |
| |
| static struct attribute_set *hotkey_dev_attributes; |
| |
| static void tpacpi_driver_event(const unsigned int hkey_event); |
| static void hotkey_driver_event(const unsigned int scancode); |
| static void hotkey_poll_setup(const bool may_warn); |
| |
| /* HKEY.MHKG() return bits */ |
| #define TP_HOTKEY_TABLET_MASK (1 << 3) |
| /* ThinkPad X1 Yoga (2016) */ |
| #define TP_EC_CMMD_TABLET_MODE 0x6 |
| |
| static int hotkey_get_wlsw(void) |
| { |
| int status; |
| |
| if (!tp_features.hotkey_wlsw) |
| return -ENODEV; |
| |
| #ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES |
| if (dbg_wlswemul) |
| return (tpacpi_wlsw_emulstate) ? |
| TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; |
| #endif |
| |
| if (!acpi_evalf(hkey_handle, &status, "WLSW", "d")) |
| return -EIO; |
| |
| return (status) ? TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; |
| } |
| |
| static int hotkey_get_tablet_mode(int *status) |
| { |
| int s; |
| |
| switch (tp_features.hotkey_tablet) { |
| case TP_HOTKEY_TABLET_USES_MHKG: |
| if (!acpi_evalf(hkey_handle, &s, "MHKG", "d")) |
| return -EIO; |
| |
| *status = ((s & TP_HOTKEY_TABLET_MASK) != 0); |
| break; |
| case TP_HOTKEY_TABLET_USES_CMMD: |
| if (!acpi_evalf(ec_handle, &s, "CMMD", "d")) |
| return -EIO; |
| |
| *status = (s == TP_EC_CMMD_TABLET_MODE); |
| break; |
| default: |
| break; |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * Reads current event mask from firmware, and updates |
| * hotkey_acpi_mask accordingly. Also resets any bits |
| * from hotkey_user_mask that are unavailable to be |
| * delivered (shadow requirement of the userspace ABI). |
| * |
| * Call with hotkey_mutex held |
| */ |
| static int hotkey_mask_get(void) |
| { |
| if (tp_features.hotkey_mask) { |
| u32 m = 0; |
| |
| if (!acpi_evalf(hkey_handle, &m, "DHKN", "d")) |
| return -EIO; |
| |
| hotkey_acpi_mask = m; |
| } else { |
| /* no mask support doesn't mean no event support... */ |
| hotkey_acpi_mask = hotkey_all_mask; |
| } |
| |
| /* sync userspace-visible mask */ |
| hotkey_user_mask &= (hotkey_acpi_mask | hotkey_source_mask); |
| |
| return 0; |
| } |
| |
| static void hotkey_mask_warn_incomplete_mask(void) |
| { |
| /* log only what the user can fix... */ |
| const u32 wantedmask = hotkey_driver_mask & |
| ~(hotkey_acpi_mask | hotkey_source_mask) & |
| (hotkey_all_mask | TPACPI_HKEY_NVRAM_KNOWN_MASK); |
| |
| if (wantedmask) |
| pr_notice("required events 0x%08x not enabled!\n", wantedmask); |
| } |
| |
| /* |
| * Set the firmware mask when supported |
| * |
| * Also calls hotkey_mask_get to update hotkey_acpi_mask. |
| * |
| * NOTE: does not set bits in hotkey_user_mask, but may reset them. |
| * |
| * Call with hotkey_mutex held |
| */ |
| static int hotkey_mask_set(u32 mask) |
| { |
| int i; |
| int rc = 0; |
| |
| const u32 fwmask = mask & ~hotkey_source_mask; |
| |
| if (tp_features.hotkey_mask) { |
| for (i = 0; i < 32; i++) { |
| if (!acpi_evalf(hkey_handle, |
| NULL, "MHKM", "vdd", i + 1, |
| !!(mask & (1 << i)))) { |
| rc = -EIO; |
| break; |
| } |
| } |
| } |
| |
| /* |
| * We *must* make an inconditional call to hotkey_mask_get to |
| * refresh hotkey_acpi_mask and update hotkey_user_mask |
| * |
| * Take the opportunity to also log when we cannot _enable_ |
| * a given event. |
| */ |
| if (!hotkey_mask_get() && !rc && (fwmask & ~hotkey_acpi_mask)) { |
| pr_notice("asked for hotkey mask 0x%08x, but firmware forced it to 0x%08x\n", |
| fwmask, hotkey_acpi_mask); |
| } |
| |
| if (tpacpi_lifecycle != TPACPI_LIFE_EXITING) |
| hotkey_mask_warn_incomplete_mask(); |
| |
| return rc; |
| } |
| |
| /* |
| * Sets hotkey_user_mask and tries to set the firmware mask |
| * |
| * Call with hotkey_mutex held |
| */ |
| static int hotkey_user_mask_set(const u32 mask) |
| { |
| int rc; |
| |
| /* Give people a chance to notice they are doing something that |
| * is bound to go boom on their users sooner or later */ |
| if (!tp_warned.hotkey_mask_ff && |
| (mask == 0xffff || mask == 0xffffff || |
| mask == 0xffffffff)) { |
| tp_warned.hotkey_mask_ff = 1; |
| pr_notice("setting the hotkey mask to 0x%08x is likely not the best way to go about it\n", |
| mask); |
| pr_notice("please consider using the driver defaults, and refer to up-to-date thinkpad-acpi documentation\n"); |
| } |
| |
| /* Try to enable what the user asked for, plus whatever we need. |
| * this syncs everything but won't enable bits in hotkey_user_mask */ |
| rc = hotkey_mask_set((mask | hotkey_driver_mask) & ~hotkey_source_mask); |
| |
| /* Enable the available bits in hotkey_user_mask */ |
| hotkey_user_mask = mask & (hotkey_acpi_mask | hotkey_source_mask); |
| |
| return rc; |
| } |
| |
| /* |
| * Sets the driver hotkey mask. |
| * |
| * Can be called even if the hotkey subdriver is inactive |
| */ |
| static int tpacpi_hotkey_driver_mask_set(const u32 mask) |
| { |
| int rc; |
| |
| /* Do the right thing if hotkey_init has not been called yet */ |
| if (!tp_features.hotkey) { |
| hotkey_driver_mask = mask; |
| return 0; |
| } |
| |
| mutex_lock(&hotkey_mutex); |
| |
| HOTKEY_CONFIG_CRITICAL_START |
| hotkey_driver_mask = mask; |
| #ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL |
| hotkey_source_mask |= (mask & ~hotkey_all_mask); |
| #endif |
| HOTKEY_CONFIG_CRITICAL_END |
| |
| rc = hotkey_mask_set((hotkey_acpi_mask | hotkey_driver_mask) & |
| ~hotkey_source_mask); |
| hotkey_poll_setup(true); |
| |
| mutex_unlock(&hotkey_mutex); |
| |
| return rc; |
| } |
| |
| static int hotkey_status_get(int *status) |
| { |
| if (!acpi_evalf(hkey_handle, status, "DHKC", "d")) |
| return -EIO; |
| |
| return 0; |
| } |
| |
| static int hotkey_status_set(bool enable) |
| { |
| if (!acpi_evalf(hkey_handle, NULL, "MHKC", "vd", enable ? 1 : 0)) |
| return -EIO; |
| |
| return 0; |
| } |
| |
| static void tpacpi_input_send_tabletsw(void) |
| { |
| int state; |
| |
| if (tp_features.hotkey_tablet && |
| !hotkey_get_tablet_mode(&state)) { |
| mutex_lock(&tpacpi_inputdev_send_mutex); |
| |
| input_report_switch(tpacpi_inputdev, |
| SW_TABLET_MODE, !!state); |
| input_sync(tpacpi_inputdev); |
| |
| mutex_unlock(&tpacpi_inputdev_send_mutex); |
| } |
| } |
| |
| /* Do NOT call without validating scancode first */ |
| static void tpacpi_input_send_key(const unsigned int scancode) |
| { |
| const unsigned int keycode = hotkey_keycode_map[scancode]; |
| |
| if (keycode != KEY_RESERVED) { |
| mutex_lock(&tpacpi_inputdev_send_mutex); |
| |
| input_event(tpacpi_inputdev, EV_MSC, MSC_SCAN, scancode); |
| input_report_key(tpacpi_inputdev, keycode, 1); |
| input_sync(tpacpi_inputdev); |
| |
| input_event(tpacpi_inputdev, EV_MSC, MSC_SCAN, scancode); |
| input_report_key(tpacpi_inputdev, keycode, 0); |
| input_sync(tpacpi_inputdev); |
| |
| mutex_unlock(&tpacpi_inputdev_send_mutex); |
| } |
| } |
| |
| /* Do NOT call without validating scancode first */ |
| static void tpacpi_input_send_key_masked(const unsigned int scancode) |
| { |
| hotkey_driver_event(scancode); |
| if (hotkey_user_mask & (1 << scancode)) |
| tpacpi_input_send_key(scancode); |
| } |
| |
| #ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL |
| static struct tp_acpi_drv_struct ibm_hotkey_acpidriver; |
| |
| /* Do NOT call without validating scancode first */ |
| static void tpacpi_hotkey_send_key(unsigned int scancode) |
| { |
| tpacpi_input_send_key_masked(scancode); |
| } |
| |
| static void hotkey_read_nvram(struct tp_nvram_state *n, const u32 m) |
| { |
| u8 d; |
| |
| if (m & TP_NVRAM_HKEY_GROUP_HK2) { |
| d = nvram_read_byte(TP_NVRAM_ADDR_HK2); |
| n->thinkpad_toggle = !!(d & TP_NVRAM_MASK_HKT_THINKPAD); |
| n->zoom_toggle = !!(d & TP_NVRAM_MASK_HKT_ZOOM); |
| n->display_toggle = !!(d & TP_NVRAM_MASK_HKT_DISPLAY); |
| n->hibernate_toggle = !!(d & TP_NVRAM_MASK_HKT_HIBERNATE); |
| } |
| if (m & TP_ACPI_HKEY_KBD_LIGHT_MASK) { |
| d = nvram_read_byte(TP_NVRAM_ADDR_THINKLIGHT); |
| n->thinklight_toggle = !!(d & TP_NVRAM_MASK_THINKLIGHT); |
| } |
| if (m & TP_ACPI_HKEY_DISPXPAND_MASK) { |
| d = nvram_read_byte(TP_NVRAM_ADDR_VIDEO); |
| n->displayexp_toggle = |
| !!(d & TP_NVRAM_MASK_HKT_DISPEXPND); |
| } |
| if (m & TP_NVRAM_HKEY_GROUP_BRIGHTNESS) { |
| d = nvram_read_byte(TP_NVRAM_ADDR_BRIGHTNESS); |
| n->brightness_level = (d & TP_NVRAM_MASK_LEVEL_BRIGHTNESS) |
| >> TP_NVRAM_POS_LEVEL_BRIGHTNESS; |
| n->brightness_toggle = |
| !!(d & TP_NVRAM_MASK_HKT_BRIGHTNESS); |
| } |
| if (m & TP_NVRAM_HKEY_GROUP_VOLUME) { |
| d = nvram_read_byte(TP_NVRAM_ADDR_MIXER); |
| n->volume_level = (d & TP_NVRAM_MASK_LEVEL_VOLUME) |
| >> TP_NVRAM_POS_LEVEL_VOLUME; |
| n->mute = !!(d & TP_NVRAM_MASK_MUTE); |
| n->volume_toggle = !!(d & TP_NVRAM_MASK_HKT_VOLUME); |
| } |
| } |
| |
| #define TPACPI_COMPARE_KEY(__scancode, __member) \ |
| do { \ |
| if ((event_mask & (1 << __scancode)) && \ |
| oldn->__member != newn->__member) \ |
| tpacpi_hotkey_send_key(__scancode); \ |
| } while (0) |
| |
| #define TPACPI_MAY_SEND_KEY(__scancode) \ |
| do { \ |
| if (event_mask & (1 << __scancode)) \ |
| tpacpi_hotkey_send_key(__scancode); \ |
| } while (0) |
| |
| static void issue_volchange(const unsigned int oldvol, |
| const unsigned int newvol, |
| const u32 event_mask) |
| { |
| unsigned int i = oldvol; |
| |
| while (i > newvol) { |
| TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEDOWN); |
| i--; |
| } |
| while (i < newvol) { |
| TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEUP); |
| i++; |
| } |
| } |
| |
| static void issue_brightnesschange(const unsigned int oldbrt, |
| const unsigned int newbrt, |
| const u32 event_mask) |
| { |
| unsigned int i = oldbrt; |
| |
| while (i > newbrt) { |
| TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNEND); |
| i--; |
| } |
| while (i < newbrt) { |
| TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNHOME); |
| i++; |
| } |
| } |
| |
| static void hotkey_compare_and_issue_event(struct tp_nvram_state *oldn, |
| struct tp_nvram_state *newn, |
| const u32 event_mask) |
| { |
| |
| TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_THINKPAD, thinkpad_toggle); |
| TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNSPACE, zoom_toggle); |
| TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNF7, display_toggle); |
| TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNF12, hibernate_toggle); |
| |
| TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNPAGEUP, thinklight_toggle); |
| |
| TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNF8, displayexp_toggle); |
| |
| /* |
| * Handle volume |
| * |
| * This code is supposed to duplicate the IBM firmware behaviour: |
| * - Pressing MUTE issues mute hotkey message, even when already mute |
| * - Pressing Volume up/down issues volume up/down hotkey messages, |
| * even when already at maximum or minimum volume |
| * - The act of unmuting issues volume up/down notification, |
| * depending which key was used to unmute |
| * |
| * We are constrained to what the NVRAM can tell us, which is not much |
| * and certainly not enough if more than one volume hotkey was pressed |
| * since the last poll cycle. |
| * |
| * Just to make our life interesting, some newer Lenovo ThinkPads have |
| * bugs in the BIOS and may fail to update volume_toggle properly. |
| */ |
| if (newn->mute) { |
| /* muted */ |
| if (!oldn->mute || |
| oldn->volume_toggle != newn->volume_toggle || |
| oldn->volume_level != newn->volume_level) { |
| /* recently muted, or repeated mute keypress, or |
| * multiple presses ending in mute */ |
| issue_volchange(oldn->volume_level, newn->volume_level, |
| event_mask); |
| TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_MUTE); |
| } |
| } else { |
| /* unmute */ |
| if (oldn->mute) { |
| /* recently unmuted, issue 'unmute' keypress */ |
| TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEUP); |
| } |
| if (oldn->volume_level != newn->volume_level) { |
| issue_volchange(oldn->volume_level, newn->volume_level, |
| event_mask); |
| } else if (oldn->volume_toggle != newn->volume_toggle) { |
| /* repeated vol up/down keypress at end of scale ? */ |
| if (newn->volume_level == 0) |
| TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEDOWN); |
| else if (newn->volume_level >= TP_NVRAM_LEVEL_VOLUME_MAX) |
| TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEUP); |
| } |
| } |
| |
| /* handle brightness */ |
| if (oldn->brightness_level != newn->brightness_level) { |
| issue_brightnesschange(oldn->brightness_level, |
| newn->brightness_level, event_mask); |
| } else if (oldn->brightness_toggle != newn->brightness_toggle) { |
| /* repeated key presses that didn't change state */ |
| if (newn->brightness_level == 0) |
| TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNEND); |
| else if (newn->brightness_level >= bright_maxlvl |
| && !tp_features.bright_unkfw) |
| TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNHOME); |
| } |
| |
| #undef TPACPI_COMPARE_KEY |
| #undef TPACPI_MAY_SEND_KEY |
| } |
| |
| /* |
| * Polling driver |
| * |
| * We track all events in hotkey_source_mask all the time, since |
| * most of them are edge-based. We only issue those requested by |
| * hotkey_user_mask or hotkey_driver_mask, though. |
| */ |
| static int hotkey_kthread(void *data) |
| { |
| struct tp_nvram_state s[2]; |
| u32 poll_mask, event_mask; |
| unsigned int si, so; |
| unsigned long t; |
| unsigned int change_detector; |
| unsigned int poll_freq; |
| bool was_frozen; |
| |
| if (tpacpi_lifecycle == TPACPI_LIFE_EXITING) |
| goto exit; |
| |
| set_freezable(); |
| |
| so = 0; |
| si = 1; |
| t = 0; |
| |
| /* Initial state for compares */ |
| mutex_lock(&hotkey_thread_data_mutex); |
| change_detector = hotkey_config_change; |
| poll_mask = hotkey_source_mask; |
| event_mask = hotkey_source_mask & |
| (hotkey_driver_mask | hotkey_user_mask); |
| poll_freq = hotkey_poll_freq; |
| mutex_unlock(&hotkey_thread_data_mutex); |
| hotkey_read_nvram(&s[so], poll_mask); |
| |
| while (!kthread_should_stop()) { |
| if (t == 0) { |
| if (likely(poll_freq)) |
| t = 1000/poll_freq; |
| else |
| t = 100; /* should never happen... */ |
| } |
| t = msleep_interruptible(t); |
| if (unlikely(kthread_freezable_should_stop(&was_frozen))) |
| break; |
| |
| if (t > 0 && !was_frozen) |
| continue; |
| |
| mutex_lock(&hotkey_thread_data_mutex); |
| if (was_frozen || hotkey_config_change != change_detector) { |
| /* forget old state on thaw or config change */ |
| si = so; |
| t = 0; |
| change_detector = hotkey_config_change; |
| } |
| poll_mask = hotkey_source_mask; |
| event_mask = hotkey_source_mask & |
| (hotkey_driver_mask | hotkey_user_mask); |
| poll_freq = hotkey_poll_freq; |
| mutex_unlock(&hotkey_thread_data_mutex); |
| |
| if (likely(poll_mask)) { |
| hotkey_read_nvram(&s[si], poll_mask); |
| if (likely(si != so)) { |
| hotkey_compare_and_issue_event(&s[so], &s[si], |
| event_mask); |
| } |
| } |
| |
| so = si; |
| si ^= 1; |
| } |
| |
| exit: |
| return 0; |
| } |
| |
| /* call with hotkey_mutex held */ |
| static void hotkey_poll_stop_sync(void) |
| { |
| if (tpacpi_hotkey_task) { |
| kthread_stop(tpacpi_hotkey_task); |
| tpacpi_hotkey_task = NULL; |
| } |
| } |
| |
| /* call with hotkey_mutex held */ |
| static void hotkey_poll_setup(const bool may_warn) |
| { |
| const u32 poll_driver_mask = hotkey_driver_mask & hotkey_source_mask; |
| const u32 poll_user_mask = hotkey_user_mask & hotkey_source_mask; |
| |
| if (hotkey_poll_freq > 0 && |
| (poll_driver_mask || |
| (poll_user_mask && tpacpi_inputdev->users > 0))) { |
| if (!tpacpi_hotkey_task) { |
| tpacpi_hotkey_task = kthread_run(hotkey_kthread, |
| NULL, TPACPI_NVRAM_KTHREAD_NAME); |
| if (IS_ERR(tpacpi_hotkey_task)) { |
| tpacpi_hotkey_task = NULL; |
| pr_err("could not create kernel thread for hotkey polling\n"); |
| } |
| } |
| } else { |
| hotkey_poll_stop_sync(); |
| if (may_warn && (poll_driver_mask || poll_user_mask) && |
| hotkey_poll_freq == 0) { |
| pr_notice("hot keys 0x%08x and/or events 0x%08x require polling, which is currently disabled\n", |
| poll_user_mask, poll_driver_mask); |
| } |
| } |
| } |
| |
| static void hotkey_poll_setup_safe(const bool may_warn) |
| { |
| mutex_lock(&hotkey_mutex); |
| hotkey_poll_setup(may_warn); |
| mutex_unlock(&hotkey_mutex); |
| } |
| |
| /* call with hotkey_mutex held */ |
| static void hotkey_poll_set_freq(unsigned int freq) |
| { |
| if (!freq) |
| hotkey_poll_stop_sync(); |
| |
| hotkey_poll_freq = freq; |
| } |
| |
| #else /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ |
| |
| static void hotkey_poll_setup(const bool __unused) |
| { |
| } |
| |
| static void hotkey_poll_setup_safe(const bool __unused) |
| { |
| } |
| |
| #endif /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ |
| |
| static int hotkey_inputdev_open(struct input_dev *dev) |
| { |
| switch (tpacpi_lifecycle) { |
| case TPACPI_LIFE_INIT: |
| case TPACPI_LIFE_RUNNING: |
| hotkey_poll_setup_safe(false); |
| return 0; |
| case TPACPI_LIFE_EXITING: |
| return -EBUSY; |
| } |
| |
| /* Should only happen if tpacpi_lifecycle is corrupt */ |
| BUG(); |
| return -EBUSY; |
| } |
| |
| static void hotkey_inputdev_close(struct input_dev *dev) |
| { |
| /* disable hotkey polling when possible */ |
| if (tpacpi_lifecycle != TPACPI_LIFE_EXITING && |
| !(hotkey_source_mask & hotkey_driver_mask)) |
| hotkey_poll_setup_safe(false); |
| } |
| |
| /* sysfs hotkey enable ------------------------------------------------- */ |
| static ssize_t hotkey_enable_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| int res, status; |
| |
| printk_deprecated_attribute("hotkey_enable", |
| "Hotkey reporting is always enabled"); |
| |
| res = hotkey_status_get(&status); |
| if (res) |
| return res; |
| |
| return snprintf(buf, PAGE_SIZE, "%d\n", status); |
| } |
| |
| static ssize_t hotkey_enable_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| unsigned long t; |
| |
| printk_deprecated_attribute("hotkey_enable", |
| "Hotkeys can be disabled through hotkey_mask"); |
| |
| if (parse_strtoul(buf, 1, &t)) |
| return -EINVAL; |
| |
| if (t == 0) |
| return -EPERM; |
| |
| return count; |
| } |
| |
| static DEVICE_ATTR_RW(hotkey_enable); |
| |
| /* sysfs hotkey mask --------------------------------------------------- */ |
| static ssize_t hotkey_mask_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| return snprintf(buf, PAGE_SIZE, "0x%08x\n", hotkey_user_mask); |
| } |
| |
| static ssize_t hotkey_mask_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| unsigned long t; |
| int res; |
| |
| if (parse_strtoul(buf, 0xffffffffUL, &t)) |
| return -EINVAL; |
| |
| if (mutex_lock_killable(&hotkey_mutex)) |
| return -ERESTARTSYS; |
| |
| res = hotkey_user_mask_set(t); |
| |
| #ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL |
| hotkey_poll_setup(true); |
| #endif |
| |
| mutex_unlock(&hotkey_mutex); |
| |
| tpacpi_disclose_usertask("hotkey_mask", "set to 0x%08lx\n", t); |
| |
| return (res) ? res : count; |
| } |
| |
| static DEVICE_ATTR_RW(hotkey_mask); |
| |
| /* sysfs hotkey bios_enabled ------------------------------------------- */ |
| static ssize_t hotkey_bios_enabled_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| return sprintf(buf, "0\n"); |
| } |
| |
| static DEVICE_ATTR_RO(hotkey_bios_enabled); |
| |
| /* sysfs hotkey bios_mask ---------------------------------------------- */ |
| static ssize_t hotkey_bios_mask_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| printk_deprecated_attribute("hotkey_bios_mask", |
| "This attribute is useless."); |
| return snprintf(buf, PAGE_SIZE, "0x%08x\n", hotkey_orig_mask); |
| } |
| |
| static DEVICE_ATTR_RO(hotkey_bios_mask); |
| |
| /* sysfs hotkey all_mask ----------------------------------------------- */ |
| static ssize_t hotkey_all_mask_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| return snprintf(buf, PAGE_SIZE, "0x%08x\n", |
| hotkey_all_mask | hotkey_source_mask); |
| } |
| |
| static DEVICE_ATTR_RO(hotkey_all_mask); |
| |
| /* sysfs hotkey all_mask ----------------------------------------------- */ |
| static ssize_t hotkey_adaptive_all_mask_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| return snprintf(buf, PAGE_SIZE, "0x%08x\n", |
| hotkey_adaptive_all_mask | hotkey_source_mask); |
| } |
| |
| static DEVICE_ATTR_RO(hotkey_adaptive_all_mask); |
| |
| /* sysfs hotkey recommended_mask --------------------------------------- */ |
| static ssize_t hotkey_recommended_mask_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| return snprintf(buf, PAGE_SIZE, "0x%08x\n", |
| (hotkey_all_mask | hotkey_source_mask) |
| & ~hotkey_reserved_mask); |
| } |
| |
| static DEVICE_ATTR_RO(hotkey_recommended_mask); |
| |
| #ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL |
| |
| /* sysfs hotkey hotkey_source_mask ------------------------------------- */ |
| static ssize_t hotkey_source_mask_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| return snprintf(buf, PAGE_SIZE, "0x%08x\n", hotkey_source_mask); |
| } |
| |
| static ssize_t hotkey_source_mask_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| unsigned long t; |
| u32 r_ev; |
| int rc; |
| |
| if (parse_strtoul(buf, 0xffffffffUL, &t) || |
| ((t & ~TPACPI_HKEY_NVRAM_KNOWN_MASK) != 0)) |
| return -EINVAL; |
| |
| if (mutex_lock_killable(&hotkey_mutex)) |
| return -ERESTARTSYS; |
| |
| HOTKEY_CONFIG_CRITICAL_START |
| hotkey_source_mask = t; |
| HOTKEY_CONFIG_CRITICAL_END |
| |
| rc = hotkey_mask_set((hotkey_user_mask | hotkey_driver_mask) & |
| ~hotkey_source_mask); |
| hotkey_poll_setup(true); |
| |
| /* check if events needed by the driver got disabled */ |
| r_ev = hotkey_driver_mask & ~(hotkey_acpi_mask & hotkey_all_mask) |
| & ~hotkey_source_mask & TPACPI_HKEY_NVRAM_KNOWN_MASK; |
| |
| mutex_unlock(&hotkey_mutex); |
| |
| if (rc < 0) |
| pr_err("hotkey_source_mask: failed to update the firmware event mask!\n"); |
| |
| if (r_ev) |
| pr_notice("hotkey_source_mask: some important events were disabled: 0x%04x\n", |
| r_ev); |
| |
| tpacpi_disclose_usertask("hotkey_source_mask", "set to 0x%08lx\n", t); |
| |
| return (rc < 0) ? rc : count; |
| } |
| |
| static DEVICE_ATTR_RW(hotkey_source_mask); |
| |
| /* sysfs hotkey hotkey_poll_freq --------------------------------------- */ |
| static ssize_t hotkey_poll_freq_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| return snprintf(buf, PAGE_SIZE, "%d\n", hotkey_poll_freq); |
| } |
| |
| static ssize_t hotkey_poll_freq_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| unsigned long t; |
| |
| if (parse_strtoul(buf, 25, &t)) |
| return -EINVAL; |
| |
| if (mutex_lock_killable(&hotkey_mutex)) |
| return -ERESTARTSYS; |
| |
| hotkey_poll_set_freq(t); |
| hotkey_poll_setup(true); |
| |
| mutex_unlock(&hotkey_mutex); |
| |
| tpacpi_disclose_usertask("hotkey_poll_freq", "set to %lu\n", t); |
| |
| return count; |
| } |
| |
| static DEVICE_ATTR_RW(hotkey_poll_freq); |
| |
| #endif /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ |
| |
| /* sysfs hotkey radio_sw (pollable) ------------------------------------ */ |
| static ssize_t hotkey_radio_sw_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| int res; |
| res = hotkey_get_wlsw(); |
| if (res < 0) |
| return res; |
| |
| /* Opportunistic update */ |
| tpacpi_rfk_update_hwblock_state((res == TPACPI_RFK_RADIO_OFF)); |
| |
| return snprintf(buf, PAGE_SIZE, "%d\n", |
| (res == TPACPI_RFK_RADIO_OFF) ? 0 : 1); |
| } |
| |
| static DEVICE_ATTR_RO(hotkey_radio_sw); |
| |
| static void hotkey_radio_sw_notify_change(void) |
| { |
| if (tp_features.hotkey_wlsw) |
| sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, |
| "hotkey_radio_sw"); |
| } |
| |
| /* sysfs hotkey tablet mode (pollable) --------------------------------- */ |
| static ssize_t hotkey_tablet_mode_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| int res, s; |
| res = hotkey_get_tablet_mode(&s); |
| if (res < 0) |
| return res; |
| |
| return snprintf(buf, PAGE_SIZE, "%d\n", !!s); |
| } |
| |
| static DEVICE_ATTR_RO(hotkey_tablet_mode); |
| |
| static void hotkey_tablet_mode_notify_change(void) |
| { |
| if (tp_features.hotkey_tablet) |
| sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, |
| "hotkey_tablet_mode"); |
| } |
| |
| /* sysfs wakeup reason (pollable) -------------------------------------- */ |
| static ssize_t hotkey_wakeup_reason_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| return snprintf(buf, PAGE_SIZE, "%d\n", hotkey_wakeup_reason); |
| } |
| |
| static DEVICE_ATTR(wakeup_reason, S_IRUGO, hotkey_wakeup_reason_show, NULL); |
| |
| static void hotkey_wakeup_reason_notify_change(void) |
| { |
| sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, |
| "wakeup_reason"); |
| } |
| |
| /* sysfs wakeup hotunplug_complete (pollable) -------------------------- */ |
| static ssize_t hotkey_wakeup_hotunplug_complete_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| return snprintf(buf, PAGE_SIZE, "%d\n", hotkey_autosleep_ack); |
| } |
| |
| static DEVICE_ATTR(wakeup_hotunplug_complete, S_IRUGO, |
| hotkey_wakeup_hotunplug_complete_show, NULL); |
| |
| static void hotkey_wakeup_hotunplug_complete_notify_change(void) |
| { |
| sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, |
| "wakeup_hotunplug_complete"); |
| } |
| |
| /* sysfs adaptive kbd mode --------------------------------------------- */ |
| |
| static int adaptive_keyboard_get_mode(void); |
| static int adaptive_keyboard_set_mode(int new_mode); |
| |
| enum ADAPTIVE_KEY_MODE { |
| HOME_MODE, |
| WEB_BROWSER_MODE, |
| WEB_CONFERENCE_MODE, |
| FUNCTION_MODE, |
| LAYFLAT_MODE |
| }; |
| |
| static ssize_t adaptive_kbd_mode_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| int current_mode; |
| |
| current_mode = adaptive_keyboard_get_mode(); |
| if (current_mode < 0) |
| return current_mode; |
| |
| return snprintf(buf, PAGE_SIZE, "%d\n", current_mode); |
| } |
| |
| static ssize_t adaptive_kbd_mode_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| unsigned long t; |
| int res; |
| |
| if (parse_strtoul(buf, LAYFLAT_MODE, &t)) |
| return -EINVAL; |
| |
| res = adaptive_keyboard_set_mode(t); |
| return (res < 0) ? res : count; |
| } |
| |
| static DEVICE_ATTR_RW(adaptive_kbd_mode); |
| |
| static struct attribute *adaptive_kbd_attributes[] = { |
| &dev_attr_adaptive_kbd_mode.attr, |
| NULL |
| }; |
| |
| static const struct attribute_group adaptive_kbd_attr_group = { |
| .attrs = adaptive_kbd_attributes, |
| }; |
| |
| /* --------------------------------------------------------------------- */ |
| |
| static struct attribute *hotkey_attributes[] __initdata = { |
| &dev_attr_hotkey_enable.attr, |
| &dev_attr_hotkey_bios_enabled.attr, |
| &dev_attr_hotkey_bios_mask.attr, |
| &dev_attr_wakeup_reason.attr, |
| &dev_attr_wakeup_hotunplug_complete.attr, |
| &dev_attr_hotkey_mask.attr, |
| &dev_attr_hotkey_all_mask.attr, |
| &dev_attr_hotkey_adaptive_all_mask.attr, |
| &dev_attr_hotkey_recommended_mask.attr, |
| #ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL |
| &dev_attr_hotkey_source_mask.attr, |
| &dev_attr_hotkey_poll_freq.attr, |
| #endif |
| }; |
| |
| /* |
| * Sync both the hw and sw blocking state of all switches |
| */ |
| static void tpacpi_send_radiosw_update(void) |
| { |
| int wlsw; |
| |
| /* |
| * We must sync all rfkill controllers *before* issuing any |
| * rfkill input events, or we will race the rfkill core input |
| * handler. |
| * |
| * tpacpi_inputdev_send_mutex works as a synchronization point |
| * for the above. |
| * |
| * We optimize to avoid numerous calls to hotkey_get_wlsw. |
| */ |
| |
| wlsw = hotkey_get_wlsw(); |
| |
| /* Sync hw blocking state first if it is hw-blocked */ |
| if (wlsw == TPACPI_RFK_RADIO_OFF) |
| tpacpi_rfk_update_hwblock_state(true); |
| |
| /* Sync sw blocking state */ |
| tpacpi_rfk_update_swstate_all(); |
| |
| /* Sync hw blocking state last if it is hw-unblocked */ |
| if (wlsw == TPACPI_RFK_RADIO_ON) |
| tpacpi_rfk_update_hwblock_state(false); |
| |
| /* Issue rfkill input event for WLSW switch */ |
| if (!(wlsw < 0)) { |
| mutex_lock(&tpacpi_inputdev_send_mutex); |
| |
| input_report_switch(tpacpi_inputdev, |
| SW_RFKILL_ALL, (wlsw > 0)); |
| input_sync(tpacpi_inputdev); |
| |
| mutex_unlock(&tpacpi_inputdev_send_mutex); |
| } |
| |
| /* |
| * this can be unconditional, as we will poll state again |
| * if userspace uses the notify to read data |
| */ |
| hotkey_radio_sw_notify_change(); |
| } |
| |
| static void hotkey_exit(void) |
| { |
| #ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL |
| mutex_lock(&hotkey_mutex); |
| hotkey_poll_stop_sync(); |
| mutex_unlock(&hotkey_mutex); |
| #endif |
| |
| if (hotkey_dev_attributes) |
| delete_attr_set(hotkey_dev_attributes, &tpacpi_pdev->dev.kobj); |
| |
| dbg_printk(TPACPI_DBG_EXIT | TPACPI_DBG_HKEY, |
| "restoring original HKEY status and mask\n"); |
| /* yes, there is a bitwise or below, we want the |
| * functions to be called even if one of them fail */ |
| if (((tp_features.hotkey_mask && |
| hotkey_mask_set(hotkey_orig_mask)) | |
| hotkey_status_set(false)) != 0) |
| pr_err("failed to restore hot key mask to BIOS defaults\n"); |
| } |
| |
| static void __init hotkey_unmap(const unsigned int scancode) |
| { |
| if (hotkey_keycode_map[scancode] != KEY_RESERVED) { |
| clear_bit(hotkey_keycode_map[scancode], |
| tpacpi_inputdev->keybit); |
| hotkey_keycode_map[scancode] = KEY_RESERVED; |
| } |
| } |
| |
| /* |
| * HKEY quirks: |
| * TPACPI_HK_Q_INIMASK: Supports FN+F3,FN+F4,FN+F12 |
| */ |
| |
| #define TPACPI_HK_Q_INIMASK 0x0001 |
| |
| static const struct tpacpi_quirk tpacpi_hotkey_qtable[] __initconst = { |
| TPACPI_Q_IBM('I', 'H', TPACPI_HK_Q_INIMASK), /* 600E */ |
| TPACPI_Q_IBM('I', 'N', TPACPI_HK_Q_INIMASK), /* 600E */ |
| TPACPI_Q_IBM('I', 'D', TPACPI_HK_Q_INIMASK), /* 770, 770E, 770ED */ |
| TPACPI_Q_IBM('I', 'W', TPACPI_HK_Q_INIMASK), /* A20m */ |
| TPACPI_Q_IBM('I', 'V', TPACPI_HK_Q_INIMASK), /* A20p */ |
| TPACPI_Q_IBM('1', '0', TPACPI_HK_Q_INIMASK), /* A21e, A22e */ |
| TPACPI_Q_IBM('K', 'U', TPACPI_HK_Q_INIMASK), /* A21e */ |
| TPACPI_Q_IBM('K', 'X', TPACPI_HK_Q_INIMASK), /* A21m, A22m */ |
| TPACPI_Q_IBM('K', 'Y', TPACPI_HK_Q_INIMASK), /* A21p, A22p */ |
| TPACPI_Q_IBM('1', 'B', TPACPI_HK_Q_INIMASK), /* A22e */ |
| TPACPI_Q_IBM('1', '3', TPACPI_HK_Q_INIMASK), /* A22m */ |
| TPACPI_Q_IBM('1', 'E', TPACPI_HK_Q_INIMASK), /* A30/p (0) */ |
| TPACPI_Q_IBM('1', 'C', TPACPI_HK_Q_INIMASK), /* R30 */ |
| TPACPI_Q_IBM('1', 'F', TPACPI_HK_Q_INIMASK), /* R31 */ |
| TPACPI_Q_IBM('I', 'Y', TPACPI_HK_Q_INIMASK), /* T20 */ |
| TPACPI_Q_IBM('K', 'Z', TPACPI_HK_Q_INIMASK), /* T21 */ |
| TPACPI_Q_IBM('1', '6', TPACPI_HK_Q_INIMASK), /* T22 */ |
| TPACPI_Q_IBM('I', 'Z', TPACPI_HK_Q_INIMASK), /* X20, X21 */ |
| TPACPI_Q_IBM('1', 'D', TPACPI_HK_Q_INIMASK), /* X22, X23, X24 */ |
| }; |
| |
| typedef u16 tpacpi_keymap_entry_t; |
| typedef tpacpi_keymap_entry_t tpacpi_keymap_t[TPACPI_HOTKEY_MAP_LEN]; |
| |
| static int hotkey_init_tablet_mode(void) |
| { |
| int in_tablet_mode = 0, res; |
| char *type = NULL; |
| |
| if (acpi_evalf(hkey_handle, &res, "MHKG", "qd")) { |
| /* For X41t, X60t, X61t Tablets... */ |
| tp_features.hotkey_tablet = TP_HOTKEY_TABLET_USES_MHKG; |
| in_tablet_mode = !!(res & TP_HOTKEY_TABLET_MASK); |
| type = "MHKG"; |
| } else if (acpi_evalf(ec_handle, &res, "CMMD", "qd")) { |
| /* For X1 Yoga (2016) */ |
| tp_features.hotkey_tablet = TP_HOTKEY_TABLET_USES_CMMD; |
| in_tablet_mode = res == TP_EC_CMMD_TABLET_MODE; |
| type = "CMMD"; |
| } |
| |
| if (!tp_features.hotkey_tablet) |
| return 0; |
| |
| pr_info("Tablet mode switch found (type: %s), currently in %s mode\n", |
| type, in_tablet_mode ? "tablet" : "laptop"); |
| |
| res = add_to_attr_set(hotkey_dev_attributes, |
| &dev_attr_hotkey_tablet_mode.attr); |
| if (res) |
| return -1; |
| |
| return in_tablet_mode; |
| } |
| |
| static int __init hotkey_init(struct ibm_init_struct *iibm) |
| { |
| /* Requirements for changing the default keymaps: |
| * |
| * 1. Many of the keys are mapped to KEY_RESERVED for very |
| * good reasons. Do not change them unless you have deep |
| * knowledge on the IBM and Lenovo ThinkPad firmware for |
| * the various ThinkPad models. The driver behaves |
| * differently for KEY_RESERVED: such keys have their |
| * hot key mask *unset* in mask_recommended, and also |
| * in the initial hot key mask programmed into the |
| * firmware at driver load time, which means the firm- |
| * ware may react very differently if you change them to |
| * something else; |
| * |
| * 2. You must be subscribed to the linux-thinkpad and |
| * ibm-acpi-devel mailing lists, and you should read the |
| * list archives since 2007 if you want to change the |
| * keymaps. This requirement exists so that you will |
| * know the past history of problems with the thinkpad- |
| * acpi driver keymaps, and also that you will be |
| * listening to any bug reports; |
| * |
| * 3. Do not send thinkpad-acpi specific patches directly to |
| * for merging, *ever*. Send them to the linux-acpi |
| * mailinglist for comments. Merging is to be done only |
| * through acpi-test and the ACPI maintainer. |
| * |
| * If the above is too much to ask, don't change the keymap. |
| * Ask the thinkpad-acpi maintainer to do it, instead. |
| */ |
| |
| enum keymap_index { |
| TPACPI_KEYMAP_IBM_GENERIC = 0, |
| TPACPI_KEYMAP_LENOVO_GENERIC, |
| }; |
| |
| static const tpacpi_keymap_t tpacpi_keymaps[] __initconst = { |
| /* Generic keymap for IBM ThinkPads */ |
| [TPACPI_KEYMAP_IBM_GENERIC] = { |
| /* Scan Codes 0x00 to 0x0B: ACPI HKEY FN+F1..F12 */ |
| KEY_FN_F1, KEY_BATTERY, KEY_COFFEE, KEY_SLEEP, |
| KEY_WLAN, KEY_FN_F6, KEY_SWITCHVIDEOMODE, KEY_FN_F8, |
| KEY_FN_F9, KEY_FN_F10, KEY_FN_F11, KEY_SUSPEND, |
| |
| /* Scan codes 0x0C to 0x1F: Other ACPI HKEY hot keys */ |
| KEY_UNKNOWN, /* 0x0C: FN+BACKSPACE */ |
| KEY_UNKNOWN, /* 0x0D: FN+INSERT */ |
| KEY_UNKNOWN, /* 0x0E: FN+DELETE */ |
| |
| /* brightness: firmware always reacts to them */ |
| KEY_RESERVED, /* 0x0F: FN+HOME (brightness up) */ |
| KEY_RESERVED, /* 0x10: FN+END (brightness down) */ |
| |
| /* Thinklight: firmware always react to it */ |
| KEY_RESERVED, /* 0x11: FN+PGUP (thinklight toggle) */ |
| |
| KEY_UNKNOWN, /* 0x12: FN+PGDOWN */ |
| KEY_ZOOM, /* 0x13: FN+SPACE (zoom) */ |
| |
| /* Volume: firmware always react to it and reprograms |
| * the built-in *extra* mixer. Never map it to control |
| * another mixer by default. */ |
| KEY_RESERVED, /* 0x14: VOLUME UP */ |
| KEY_RESERVED, /* 0x15: VOLUME DOWN */ |
| KEY_RESERVED, /* 0x16: MUTE */ |
| |
| KEY_VENDOR, /* 0x17: Thinkpad/AccessIBM/Lenovo */ |
| |
| /* (assignments unknown, please report if found) */ |
| KEY_UNKNOWN, KEY_UNKNOWN, KEY_UNKNOWN, KEY_UNK
|