diff options
Diffstat (limited to 'drivers/platform/x86')
-rw-r--r-- | drivers/platform/x86/Kconfig | 375 | ||||
-rw-r--r-- | drivers/platform/x86/Makefile | 19 | ||||
-rw-r--r-- | drivers/platform/x86/acer-wmi.c | 1345 | ||||
-rw-r--r-- | drivers/platform/x86/asus-laptop.c | 1266 | ||||
-rw-r--r-- | drivers/platform/x86/asus_acpi.c | 1460 | ||||
-rw-r--r-- | drivers/platform/x86/compal-laptop.c | 406 | ||||
-rw-r--r-- | drivers/platform/x86/eeepc-laptop.c | 872 | ||||
-rw-r--r-- | drivers/platform/x86/fujitsu-laptop.c | 1293 | ||||
-rw-r--r-- | drivers/platform/x86/hp-wmi.c | 512 | ||||
-rw-r--r-- | drivers/platform/x86/intel_menlow.c | 536 | ||||
-rw-r--r-- | drivers/platform/x86/msi-laptop.c | 437 | ||||
-rw-r--r-- | drivers/platform/x86/panasonic-laptop.c | 744 | ||||
-rw-r--r-- | drivers/platform/x86/sony-laptop.c | 2784 | ||||
-rw-r--r-- | drivers/platform/x86/tc1100-wmi.c | 289 | ||||
-rw-r--r-- | drivers/platform/x86/thinkpad_acpi.c | 6948 | ||||
-rw-r--r-- | drivers/platform/x86/toshiba_acpi.c | 863 | ||||
-rw-r--r-- | drivers/platform/x86/wmi.c | 747 |
17 files changed, 20896 insertions, 0 deletions
diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig new file mode 100644 index 000000000000..e65448e99b48 --- /dev/null +++ b/drivers/platform/x86/Kconfig @@ -0,0 +1,375 @@ +# +# X86 Platform Specific Drivers +# + +menuconfig X86_PLATFORM_DEVICES + bool "X86 Platform Specific Device Drivers" + default y + ---help--- + Say Y here to get to see options for device drivers for various + x86 platforms, including vendor-specific laptop extension drivers. + This option alone does not add any kernel code. + + If you say N, all options in this submenu will be skipped and disabled. + +if X86_PLATFORM_DEVICES + +config ACER_WMI + tristate "Acer WMI Laptop Extras (EXPERIMENTAL)" + depends on EXPERIMENTAL + depends on ACPI + depends on LEDS_CLASS + depends on NEW_LEDS + depends on BACKLIGHT_CLASS_DEVICE + depends on SERIO_I8042 + depends on RFKILL + select ACPI_WMI + ---help--- + This is a driver for newer Acer (and Wistron) laptops. It adds + wireless radio and bluetooth control, and on some laptops, + exposes the mail LED and LCD backlight. + + For more information about this driver see + <file:Documentation/laptops/acer-wmi.txt> + + If you have an ACPI-WMI compatible Acer/ Wistron laptop, say Y or M + here. + +config ASUS_LAPTOP + tristate "Asus Laptop Extras (EXPERIMENTAL)" + depends on ACPI + depends on EXPERIMENTAL && !ACPI_ASUS + depends on LEDS_CLASS + depends on NEW_LEDS + depends on BACKLIGHT_CLASS_DEVICE + ---help--- + This is the new Linux driver for Asus laptops. It may also support some + MEDION, JVC or VICTOR laptops. It makes all the extra buttons generate + standard ACPI events that go through /proc/acpi/events. It also adds + support for video output switching, LCD backlight control, Bluetooth and + Wlan control, and most importantly, allows you to blink those fancy LEDs. + + For more information and a userspace daemon for handling the extra + buttons see <http://acpi4asus.sf.net/>. + + If you have an ACPI-compatible ASUS laptop, say Y or M here. + +config FUJITSU_LAPTOP + tristate "Fujitsu Laptop Extras" + depends on ACPI + depends on INPUT + depends on BACKLIGHT_CLASS_DEVICE + ---help--- + This is a driver for laptops built by Fujitsu: + + * P2xxx/P5xxx/S6xxx/S7xxx series Lifebooks + * Possibly other Fujitsu laptop models + * Tested with S6410 and S7020 + + It adds support for LCD brightness control and some hotkeys. + + If you have a Fujitsu laptop, say Y or M here. + +config FUJITSU_LAPTOP_DEBUG + bool "Verbose debug mode for Fujitsu Laptop Extras" + depends on FUJITSU_LAPTOP + default n + ---help--- + Enables extra debug output from the fujitsu extras driver, at the + expense of a slight increase in driver size. + + If you are not sure, say N here. + +config TC1100_WMI + tristate "HP Compaq TC1100 Tablet WMI Extras (EXPERIMENTAL)" + depends on !X86_64 + depends on EXPERIMENTAL + depends on ACPI + select ACPI_WMI + ---help--- + This is a driver for the WMI extensions (wireless and bluetooth power + control) of the HP Compaq TC1100 tablet. + +config HP_WMI + tristate "HP WMI extras" + depends on ACPI_WMI + depends on INPUT + depends on RFKILL + help + Say Y here if you want to support WMI-based hotkeys on HP laptops and + to read data from WMI such as docking or ambient light sensor state. + + To compile this driver as a module, choose M here: the module will + be called hp-wmi. + +config MSI_LAPTOP + tristate "MSI Laptop Extras" + depends on ACPI + depends on BACKLIGHT_CLASS_DEVICE + ---help--- + This is a driver for laptops built by MSI (MICRO-STAR + INTERNATIONAL): + + MSI MegaBook S270 (MS-1013) + Cytron/TCM/Medion/Tchibo MD96100/SAM2000 + + It adds support for Bluetooth, WLAN and LCD brightness control. + + More information about this driver is available at + <http://0pointer.de/lennart/tchibo.html>. + + If you have an MSI S270 laptop, say Y or M here. + +config PANASONIC_LAPTOP + tristate "Panasonic Laptop Extras" + depends on INPUT && ACPI + depends on BACKLIGHT_CLASS_DEVICE + ---help--- + This driver adds support for access to backlight control and hotkeys + on Panasonic Let's Note laptops. + + If you have a Panasonic Let's note laptop (such as the R1(N variant), + R2, R3, R5, T2, W2 and Y2 series), say Y. + +config COMPAL_LAPTOP + tristate "Compal Laptop Extras" + depends on ACPI + depends on BACKLIGHT_CLASS_DEVICE + ---help--- + This is a driver for laptops built by Compal: + + Compal FL90/IFL90 + Compal FL91/IFL91 + Compal FL92/JFL92 + Compal FT00/IFT00 + + It adds support for Bluetooth, WLAN and LCD brightness control. + + If you have an Compal FL9x/IFL9x/FT00 laptop, say Y or M here. + +config SONY_LAPTOP + tristate "Sony Laptop Extras" + depends on ACPI + select BACKLIGHT_CLASS_DEVICE + depends on INPUT + ---help--- + This mini-driver drives the SNC and SPIC devices present in the ACPI + BIOS of the Sony Vaio laptops. + + It gives access to some extra laptop functionalities like Bluetooth, + screen brightness control, Fn keys and allows powering on/off some + devices. + + Read <file:Documentation/laptops/sony-laptop.txt> for more information. + +config SONYPI_COMPAT + bool "Sonypi compatibility" + depends on SONY_LAPTOP + ---help--- + Build the sonypi driver compatibility code into the sony-laptop driver. + +config THINKPAD_ACPI + tristate "ThinkPad ACPI Laptop Extras" + depends on ACPI + select BACKLIGHT_LCD_SUPPORT + select BACKLIGHT_CLASS_DEVICE + select HWMON + select NVRAM + select INPUT + select NEW_LEDS + select LEDS_CLASS + select NET + select RFKILL + ---help--- + This is a driver for the IBM and Lenovo ThinkPad laptops. It adds + support for Fn-Fx key combinations, Bluetooth control, video + output switching, ThinkLight control, UltraBay eject and more. + For more information about this driver see + <file:Documentation/laptops/thinkpad-acpi.txt> and + <http://ibm-acpi.sf.net/> . + + This driver was formerly known as ibm-acpi. + + If you have an IBM or Lenovo ThinkPad laptop, say Y or M here. + +config THINKPAD_ACPI_DEBUG + bool "Verbose debug mode" + depends on THINKPAD_ACPI + default n + ---help--- + Enables extra debugging information, at the expense of a slightly + increase in driver size. + + If you are not sure, say N here. + +config THINKPAD_ACPI_DOCK + bool "Legacy Docking Station Support" + depends on THINKPAD_ACPI + depends on ACPI_DOCK=n + default n + ---help--- + Allows the thinkpad_acpi driver to handle docking station events. + This support was made obsolete by the generic ACPI docking station + support (CONFIG_ACPI_DOCK). It will allow locking and removing the + laptop from the docking station, but will not properly connect PCI + devices. + + If you are not sure, say N here. + +config THINKPAD_ACPI_BAY + bool "Legacy Removable Bay Support" + depends on THINKPAD_ACPI + default y + ---help--- + Allows the thinkpad_acpi driver to handle removable bays. It will + electrically disable the device in the bay, and also generate + notifications when the bay lever is ejected or inserted. + + If you are not sure, say Y here. + +config THINKPAD_ACPI_VIDEO + bool "Video output control support" + depends on THINKPAD_ACPI + default y + ---help--- + Allows the thinkpad_acpi driver to provide an interface to control + the various video output ports. + + This feature often won't work well, depending on ThinkPad model, + display state, video output devices in use, whether there is a X + server running, phase of the moon, and the current mood of + Schroedinger's cat. If you can use X.org's RandR to control + your ThinkPad's video output ports instead of this feature, + don't think twice: do it and say N here to save some memory. + + If you are not sure, say Y here. + +config THINKPAD_ACPI_HOTKEY_POLL + bool "Support NVRAM polling for hot keys" + depends on THINKPAD_ACPI + default y + ---help--- + Some thinkpad models benefit from NVRAM polling to detect a few of + the hot key press events. If you know your ThinkPad model does not + need to do NVRAM polling to support any of the hot keys you use, + unselecting this option will save about 1kB of memory. + + ThinkPads T40 and newer, R52 and newer, and X31 and newer are + unlikely to need NVRAM polling in their latest BIOS versions. + + NVRAM polling can detect at most the following keys: ThinkPad/Access + IBM, Zoom, Switch Display (fn+F7), ThinkLight, Volume up/down/mute, + Brightness up/down, Display Expand (fn+F8), Hibernate (fn+F12). + + If you are not sure, say Y here. The driver enables polling only if + it is strictly necessary to do so. + +config INTEL_MENLOW + tristate "Thermal Management driver for Intel menlow platform" + depends on ACPI_THERMAL + select THERMAL + ---help--- + ACPI thermal management enhancement driver on + Intel Menlow platform. + + If unsure, say N. + +config EEEPC_LAPTOP + tristate "Eee PC Hotkey Driver (EXPERIMENTAL)" + depends on ACPI + depends on EXPERIMENTAL + select BACKLIGHT_CLASS_DEVICE + select HWMON + select RFKILL + ---help--- + This driver supports the Fn-Fx keys on Eee PC laptops. + It also adds the ability to switch camera/wlan on/off. + + If you have an Eee PC laptop, say Y or M here. + + +config ACPI_WMI + tristate "WMI (EXPERIMENTAL)" + depends on ACPI + depends on EXPERIMENTAL + help + This driver adds support for the ACPI-WMI (Windows Management + Instrumentation) mapper device (PNP0C14) found on some systems. + + ACPI-WMI is a proprietary extension to ACPI to expose parts of the + ACPI firmware to userspace - this is done through various vendor + defined methods and data blocks in a PNP0C14 device, which are then + made available for userspace to call. + + The implementation of this in Linux currently only exposes this to + other kernel space drivers. + + This driver is a required dependency to build the firmware specific + drivers needed on many machines, including Acer and HP laptops. + + It is safe to enable this driver even if your DSDT doesn't define + any ACPI-WMI devices. + +config ACPI_ASUS + tristate "ASUS/Medion Laptop Extras" + depends on ACPI + select BACKLIGHT_CLASS_DEVICE + ---help--- + This driver provides support for extra features of ACPI-compatible + ASUS laptops. As some of Medion laptops are made by ASUS, it may also + support some Medion laptops (such as 9675 for example). It makes all + the extra buttons generate standard ACPI events that go through + /proc/acpi/events, and (on some models) adds support for changing the + display brightness and output, switching the LCD backlight on and off, + and most importantly, allows you to blink those fancy LEDs intended + for reporting mail and wireless status. + + Note: display switching code is currently considered EXPERIMENTAL, + toying with these values may even lock your machine. + + All settings are changed via /proc/acpi/asus directory entries. Owner + and group for these entries can be set with asus_uid and asus_gid + parameters. + + More information and a userspace daemon for handling the extra buttons + at <http://sourceforge.net/projects/acpi4asus/>. + + If you have an ACPI-compatible ASUS laptop, say Y or M here. This + driver is still under development, so if your laptop is unsupported or + something works not quite as expected, please use the mailing list + available on the above page (acpi4asus-user@lists.sourceforge.net). + + NOTE: This driver is deprecated and will probably be removed soon, + use asus-laptop instead. + +config ACPI_TOSHIBA + tristate "Toshiba Laptop Extras" + depends on ACPI + depends on INPUT + select INPUT_POLLDEV + select NET + select RFKILL + select BACKLIGHT_CLASS_DEVICE + ---help--- + This driver adds support for access to certain system settings + on "legacy free" Toshiba laptops. These laptops can be recognized by + their lack of a BIOS setup menu and APM support. + + On these machines, all system configuration is handled through the + ACPI. This driver is required for access to controls not covered + by the general ACPI drivers, such as LCD brightness, video output, + etc. + + This driver differs from the non-ACPI Toshiba laptop driver (located + under "Processor type and features") in several aspects. + Configuration is accessed by reading and writing text files in the + /proc tree instead of by program interface to /dev. Furthermore, no + power management functions are exposed, as those are handled by the + general ACPI drivers. + + More information about this driver is available at + <http://memebeam.org/toys/ToshibaAcpiDriver>. + + If you have a legacy free Toshiba laptop (such as the Libretto L1 + series), say Y. +endif # X86_PLATFORM_DEVICES diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile new file mode 100644 index 000000000000..1e9de2ae0de5 --- /dev/null +++ b/drivers/platform/x86/Makefile @@ -0,0 +1,19 @@ +# +# Makefile for linux/drivers/platform/x86 +# x86 Platform-Specific Drivers +# +obj-$(CONFIG_ASUS_LAPTOP) += asus-laptop.o +obj-$(CONFIG_EEEPC_LAPTOP) += eeepc-laptop.o +obj-$(CONFIG_MSI_LAPTOP) += msi-laptop.o +obj-$(CONFIG_COMPAL_LAPTOP) += compal-laptop.o +obj-$(CONFIG_ACER_WMI) += acer-wmi.o +obj-$(CONFIG_HP_WMI) += hp-wmi.o +obj-$(CONFIG_TC1100_WMI) += tc1100-wmi.o +obj-$(CONFIG_SONY_LAPTOP) += sony-laptop.o +obj-$(CONFIG_THINKPAD_ACPI) += thinkpad_acpi.o +obj-$(CONFIG_FUJITSU_LAPTOP) += fujitsu-laptop.o +obj-$(CONFIG_PANASONIC_LAPTOP) += panasonic-laptop.o +obj-$(CONFIG_INTEL_MENLOW) += intel_menlow.o +obj-$(CONFIG_ACPI_WMI) += wmi.o +obj-$(CONFIG_ACPI_ASUS) += asus_acpi.o +obj-$(CONFIG_ACPI_TOSHIBA) += toshiba_acpi.o diff --git a/drivers/platform/x86/acer-wmi.c b/drivers/platform/x86/acer-wmi.c new file mode 100644 index 000000000000..94c9f911824e --- /dev/null +++ b/drivers/platform/x86/acer-wmi.c @@ -0,0 +1,1345 @@ +/* + * Acer WMI Laptop Extras + * + * Copyright (C) 2007-2008 Carlos Corbacho <carlos@strangeworlds.co.uk> + * + * Based on acer_acpi: + * Copyright (C) 2005-2007 E.M. Smith + * Copyright (C) 2007-2008 Carlos Corbacho <cathectic@gmail.com> + * + * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/types.h> +#include <linux/dmi.h> +#include <linux/fb.h> +#include <linux/backlight.h> +#include <linux/leds.h> +#include <linux/platform_device.h> +#include <linux/acpi.h> +#include <linux/i8042.h> +#include <linux/rfkill.h> +#include <linux/workqueue.h> +#include <linux/debugfs.h> + +#include <acpi/acpi_drivers.h> + +MODULE_AUTHOR("Carlos Corbacho"); +MODULE_DESCRIPTION("Acer Laptop WMI Extras Driver"); +MODULE_LICENSE("GPL"); + +#define ACER_LOGPREFIX "acer-wmi: " +#define ACER_ERR KERN_ERR ACER_LOGPREFIX +#define ACER_NOTICE KERN_NOTICE ACER_LOGPREFIX +#define ACER_INFO KERN_INFO ACER_LOGPREFIX + +/* + * The following defines quirks to get some specific functions to work + * which are known to not be supported over ACPI-WMI (such as the mail LED + * on WMID based Acer's) + */ +struct acer_quirks { + const char *vendor; + const char *model; + u16 quirks; +}; + +/* + * Magic Number + * Meaning is unknown - this number is required for writing to ACPI for AMW0 + * (it's also used in acerhk when directly accessing the BIOS) + */ +#define ACER_AMW0_WRITE 0x9610 + +/* + * Bit masks for the AMW0 interface + */ +#define ACER_AMW0_WIRELESS_MASK 0x35 +#define ACER_AMW0_BLUETOOTH_MASK 0x34 +#define ACER_AMW0_MAILLED_MASK 0x31 + +/* + * Method IDs for WMID interface + */ +#define ACER_WMID_GET_WIRELESS_METHODID 1 +#define ACER_WMID_GET_BLUETOOTH_METHODID 2 +#define ACER_WMID_GET_BRIGHTNESS_METHODID 3 +#define ACER_WMID_SET_WIRELESS_METHODID 4 +#define ACER_WMID_SET_BLUETOOTH_METHODID 5 +#define ACER_WMID_SET_BRIGHTNESS_METHODID 6 +#define ACER_WMID_GET_THREEG_METHODID 10 +#define ACER_WMID_SET_THREEG_METHODID 11 + +/* + * Acer ACPI method GUIDs + */ +#define AMW0_GUID1 "67C3371D-95A3-4C37-BB61-DD47B491DAAB" +#define AMW0_GUID2 "431F16ED-0C2B-444C-B267-27DEB140CF9C" +#define WMID_GUID1 "6AF4F258-B401-42fd-BE91-3D4AC2D7C0D3" +#define WMID_GUID2 "95764E09-FB56-4e83-B31A-37761F60994A" + +MODULE_ALIAS("wmi:67C3371D-95A3-4C37-BB61-DD47B491DAAB"); +MODULE_ALIAS("wmi:6AF4F258-B401-42fd-BE91-3D4AC2D7C0D3"); + +/* Temporary workaround until the WMI sysfs interface goes in */ +MODULE_ALIAS("dmi:*:*Acer*:*:"); + +/* + * Interface capability flags + */ +#define ACER_CAP_MAILLED (1<<0) +#define ACER_CAP_WIRELESS (1<<1) +#define ACER_CAP_BLUETOOTH (1<<2) +#define ACER_CAP_BRIGHTNESS (1<<3) +#define ACER_CAP_THREEG (1<<4) +#define ACER_CAP_ANY (0xFFFFFFFF) + +/* + * Interface type flags + */ +enum interface_flags { + ACER_AMW0, + ACER_AMW0_V2, + ACER_WMID, +}; + +#define ACER_DEFAULT_WIRELESS 0 +#define ACER_DEFAULT_BLUETOOTH 0 +#define ACER_DEFAULT_MAILLED 0 +#define ACER_DEFAULT_THREEG 0 + +static int max_brightness = 0xF; + +static int mailled = -1; +static int brightness = -1; +static int threeg = -1; +static int force_series; + +module_param(mailled, int, 0444); +module_param(brightness, int, 0444); +module_param(threeg, int, 0444); +module_param(force_series, int, 0444); +MODULE_PARM_DESC(mailled, "Set initial state of Mail LED"); +MODULE_PARM_DESC(brightness, "Set initial LCD backlight brightness"); +MODULE_PARM_DESC(threeg, "Set initial state of 3G hardware"); +MODULE_PARM_DESC(force_series, "Force a different laptop series"); + +struct acer_data { + int mailled; + int threeg; + int brightness; +}; + +struct acer_debug { + struct dentry *root; + struct dentry *devices; + u32 wmid_devices; +}; + +static struct rfkill *wireless_rfkill; +static struct rfkill *bluetooth_rfkill; + +/* Each low-level interface must define at least some of the following */ +struct wmi_interface { + /* The WMI device type */ + u32 type; + + /* The capabilities this interface provides */ + u32 capability; + + /* Private data for the current interface */ + struct acer_data data; + + /* debugfs entries associated with this interface */ + struct acer_debug debug; +}; + +/* The static interface pointer, points to the currently detected interface */ +static struct wmi_interface *interface; + +/* + * Embedded Controller quirks + * Some laptops require us to directly access the EC to either enable or query + * features that are not available through WMI. + */ + +struct quirk_entry { + u8 wireless; + u8 mailled; + s8 brightness; + u8 bluetooth; +}; + +static struct quirk_entry *quirks; + +static void set_quirks(void) +{ + if (!interface) + return; + + if (quirks->mailled) + interface->capability |= ACER_CAP_MAILLED; + + if (quirks->brightness) + interface->capability |= ACER_CAP_BRIGHTNESS; +} + +static int dmi_matched(const struct dmi_system_id *dmi) +{ + quirks = dmi->driver_data; + return 0; +} + +static struct quirk_entry quirk_unknown = { +}; + +static struct quirk_entry quirk_acer_aspire_1520 = { + .brightness = -1, +}; + +static struct quirk_entry quirk_acer_travelmate_2490 = { + .mailled = 1, +}; + +/* This AMW0 laptop has no bluetooth */ +static struct quirk_entry quirk_medion_md_98300 = { + .wireless = 1, +}; + +static struct quirk_entry quirk_fujitsu_amilo_li_1718 = { + .wireless = 2, +}; + +static struct dmi_system_id acer_quirks[] = { + { + .callback = dmi_matched, + .ident = "Acer Aspire 1360", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 1360"), + }, + .driver_data = &quirk_acer_aspire_1520, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 1520", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 1520"), + }, + .driver_data = &quirk_acer_aspire_1520, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 3100", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 3100"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 3610", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 3610"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 5100", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 5100"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 5610", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 5610"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 5630", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 5630"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 5650", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 5650"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 5680", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 5680"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 9110", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 9110"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer TravelMate 2490", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "TravelMate 2490"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer TravelMate 4200", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "TravelMate 4200"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Fujitsu Siemens Amilo Li 1718", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU SIEMENS"), + DMI_MATCH(DMI_PRODUCT_NAME, "AMILO Li 1718"), + }, + .driver_data = &quirk_fujitsu_amilo_li_1718, + }, + { + .callback = dmi_matched, + .ident = "Medion MD 98300", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "MEDION"), + DMI_MATCH(DMI_PRODUCT_NAME, "WAM2030"), + }, + .driver_data = &quirk_medion_md_98300, + }, + {} +}; + +/* Find which quirks are needed for a particular vendor/ model pair */ +static void find_quirks(void) +{ + if (!force_series) { + dmi_check_system(acer_quirks); + } else if (force_series == 2490) { + quirks = &quirk_acer_travelmate_2490; + } + + if (quirks == NULL) + quirks = &quirk_unknown; + + set_quirks(); +} + +/* + * General interface convenience methods + */ + +static bool has_cap(u32 cap) +{ + if ((interface->capability & cap) != 0) + return 1; + + return 0; +} + +/* + * AMW0 (V1) interface + */ +struct wmab_args { + u32 eax; + u32 ebx; + u32 ecx; + u32 edx; +}; + +struct wmab_ret { + u32 eax; + u32 ebx; + u32 ecx; + u32 edx; + u32 eex; +}; + +static acpi_status wmab_execute(struct wmab_args *regbuf, +struct acpi_buffer *result) +{ + struct acpi_buffer input; + acpi_status status; + input.length = sizeof(struct wmab_args); + input.pointer = (u8 *)regbuf; + + status = wmi_evaluate_method(AMW0_GUID1, 1, 1, &input, result); + + return status; +} + +static acpi_status AMW0_get_u32(u32 *value, u32 cap, +struct wmi_interface *iface) +{ + int err; + u8 result; + + switch (cap) { + case ACER_CAP_MAILLED: + switch (quirks->mailled) { + default: + err = ec_read(0xA, &result); + if (err) + return AE_ERROR; + *value = (result >> 7) & 0x1; + return AE_OK; + } + break; + case ACER_CAP_WIRELESS: + switch (quirks->wireless) { + case 1: + err = ec_read(0x7B, &result); + if (err) + return AE_ERROR; + *value = result & 0x1; + return AE_OK; + case 2: + err = ec_read(0x71, &result); + if (err) + return AE_ERROR; + *value = result & 0x1; + return AE_OK; + default: + err = ec_read(0xA, &result); + if (err) + return AE_ERROR; + *value = (result >> 2) & 0x1; + return AE_OK; + } + break; + case ACER_CAP_BLUETOOTH: + switch (quirks->bluetooth) { + default: + err = ec_read(0xA, &result); + if (err) + return AE_ERROR; + *value = (result >> 4) & 0x1; + return AE_OK; + } + break; + case ACER_CAP_BRIGHTNESS: + switch (quirks->brightness) { + default: + err = ec_read(0x83, &result); + if (err) + return AE_ERROR; + *value = result; + return AE_OK; + } + break; + default: + return AE_ERROR; + } + return AE_OK; +} + +static acpi_status AMW0_set_u32(u32 value, u32 cap, struct wmi_interface *iface) +{ + struct wmab_args args; + + args.eax = ACER_AMW0_WRITE; + args.ebx = value ? (1<<8) : 0; + args.ecx = args.edx = 0; + + switch (cap) { + case ACER_CAP_MAILLED: + if (value > 1) + return AE_BAD_PARAMETER; + args.ebx |= ACER_AMW0_MAILLED_MASK; + break; + case ACER_CAP_WIRELESS: + if (value > 1) + return AE_BAD_PARAMETER; + args.ebx |= ACER_AMW0_WIRELESS_MASK; + break; + case ACER_CAP_BLUETOOTH: + if (value > 1) + return AE_BAD_PARAMETER; + args.ebx |= ACER_AMW0_BLUETOOTH_MASK; + break; + case ACER_CAP_BRIGHTNESS: + if (value > max_brightness) + return AE_BAD_PARAMETER; + switch (quirks->brightness) { + default: + return ec_write(0x83, value); + break; + } + default: + return AE_ERROR; + } + + /* Actually do the set */ + return wmab_execute(&args, NULL); +} + +static acpi_status AMW0_find_mailled(void) +{ + struct wmab_args args; + struct wmab_ret ret; + acpi_status status = AE_OK; + struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + + args.eax = 0x86; + args.ebx = args.ecx = args.edx = 0; + + status = wmab_execute(&args, &out); + if (ACPI_FAILURE(status)) + return status; + + obj = (union acpi_object *) out.pointer; + if (obj && obj->type == ACPI_TYPE_BUFFER && + obj->buffer.length == sizeof(struct wmab_ret)) { + ret = *((struct wmab_ret *) obj->buffer.pointer); + } else { + return AE_ERROR; + } + + if (ret.eex & 0x1) + interface->capability |= ACER_CAP_MAILLED; + + kfree(out.pointer); + + return AE_OK; +} + +static acpi_status AMW0_set_capabilities(void) +{ + struct wmab_args args; + struct wmab_ret ret; + acpi_status status = AE_OK; + struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + + /* + * On laptops with this strange GUID (non Acer), normal probing doesn't + * work. + */ + if (wmi_has_guid(AMW0_GUID2)) { + interface->capability |= ACER_CAP_WIRELESS; + return AE_OK; + } + + args.eax = ACER_AMW0_WRITE; + args.ecx = args.edx = 0; + + args.ebx = 0xa2 << 8; + args.ebx |= ACER_AMW0_WIRELESS_MASK; + + status = wmab_execute(&args, &out); + if (ACPI_FAILURE(status)) + return status; + + obj = (union acpi_object *) out.pointer; + if (obj && obj->type == ACPI_TYPE_BUFFER && + obj->buffer.length == sizeof(struct wmab_ret)) { + ret = *((struct wmab_ret *) obj->buffer.pointer); + } else { + return AE_ERROR; + } + + if (ret.eax & 0x1) + interface->capability |= ACER_CAP_WIRELESS; + + args.ebx = 2 << 8; + args.ebx |= ACER_AMW0_BLUETOOTH_MASK; + + status = wmab_execute(&args, &out); + if (ACPI_FAILURE(status)) + return status; + + obj = (union acpi_object *) out.pointer; + if (obj && obj->type == ACPI_TYPE_BUFFER + && obj->buffer.length == sizeof(struct wmab_ret)) { + ret = *((struct wmab_ret *) obj->buffer.pointer); + } else { + return AE_ERROR; + } + + if (ret.eax & 0x1) + interface->capability |= ACER_CAP_BLUETOOTH; + + kfree(out.pointer); + + /* + * This appears to be safe to enable, since all Wistron based laptops + * appear to use the same EC register for brightness, even if they + * differ for wireless, etc + */ + if (quirks->brightness >= 0) + interface->capability |= ACER_CAP_BRIGHTNESS; + + return AE_OK; +} + +static struct wmi_interface AMW0_interface = { + .type = ACER_AMW0, +}; + +static struct wmi_interface AMW0_V2_interface = { + .type = ACER_AMW0_V2, +}; + +/* + * New interface (The WMID interface) + */ +static acpi_status +WMI_execute_u32(u32 method_id, u32 in, u32 *out) +{ + struct acpi_buffer input = { (acpi_size) sizeof(u32), (void *)(&in) }; + struct acpi_buffer result = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + u32 tmp; + acpi_status status; + + status = wmi_evaluate_method(WMID_GUID1, 1, method_id, &input, &result); + + if (ACPI_FAILURE(status)) + return status; + + obj = (union acpi_object *) result.pointer; + if (obj && obj->type == ACPI_TYPE_BUFFER && + obj->buffer.length == sizeof(u32)) { + tmp = *((u32 *) obj->buffer.pointer); + } else { + tmp = 0; + } + + if (out) + *out = tmp; + + kfree(result.pointer); + + return status; +} + +static acpi_status WMID_get_u32(u32 *value, u32 cap, +struct wmi_interface *iface) +{ + acpi_status status; + u8 tmp; + u32 result, method_id = 0; + + switch (cap) { + case ACER_CAP_WIRELESS: + method_id = ACER_WMID_GET_WIRELESS_METHODID; + break; + case ACER_CAP_BLUETOOTH: + method_id = ACER_WMID_GET_BLUETOOTH_METHODID; + break; + case ACER_CAP_BRIGHTNESS: + method_id = ACER_WMID_GET_BRIGHTNESS_METHODID; + break; + case ACER_CAP_THREEG: + method_id = ACER_WMID_GET_THREEG_METHODID; + break; + case ACER_CAP_MAILLED: + if (quirks->mailled == 1) { + ec_read(0x9f, &tmp); + *value = tmp & 0x1; + return 0; + } + default: + return AE_ERROR; + } + status = WMI_execute_u32(method_id, 0, &result); + + if (ACPI_SUCCESS(status)) + *value = (u8)result; + + return status; +} + +static acpi_status WMID_set_u32(u32 value, u32 cap, struct wmi_interface *iface) +{ + u32 method_id = 0; + char param; + + switch (cap) { + case ACER_CAP_BRIGHTNESS: + if (value > max_brightness) + return AE_BAD_PARAMETER; + method_id = ACER_WMID_SET_BRIGHTNESS_METHODID; + break; + case ACER_CAP_WIRELESS: + if (value > 1) + return AE_BAD_PARAMETER; + method_id = ACER_WMID_SET_WIRELESS_METHODID; + break; + case ACER_CAP_BLUETOOTH: + if (value > 1) + return AE_BAD_PARAMETER; + method_id = ACER_WMID_SET_BLUETOOTH_METHODID; + break; + case ACER_CAP_THREEG: + if (value > 1) + return AE_BAD_PARAMETER; + method_id = ACER_WMID_SET_THREEG_METHODID; + break; + case ACER_CAP_MAILLED: + if (value > 1) + return AE_BAD_PARAMETER; + if (quirks->mailled == 1) { + param = value ? 0x92 : 0x93; + i8042_command(¶m, 0x1059); + return 0; + } + break; + default: + return AE_ERROR; + } + return WMI_execute_u32(method_id, (u32)value, NULL); +} + +static acpi_status WMID_set_capabilities(void) +{ + struct acpi_buffer out = {ACPI_ALLOCATE_BUFFER, NULL}; + union acpi_object *obj; + acpi_status status; + u32 devices; + + status = wmi_query_block(WMID_GUID2, 1, &out); + if (ACPI_FAILURE(status)) + return status; + + obj = (union acpi_object *) out.pointer; + if (obj && obj->type == ACPI_TYPE_BUFFER && + obj->buffer.length == sizeof(u32)) { + devices = *((u32 *) obj->buffer.pointer); + } else { + return AE_ERROR; + } + + /* Not sure on the meaning of the relevant bits yet to detect these */ + interface->capability |= ACER_CAP_WIRELESS; + interface->capability |= ACER_CAP_THREEG; + + /* WMID always provides brightness methods */ + interface->capability |= ACER_CAP_BRIGHTNESS; + + if (devices & 0x10) + interface->capability |= ACER_CAP_BLUETOOTH; + + if (!(devices & 0x20)) + max_brightness = 0x9; + + return status; +} + +static struct wmi_interface wmid_interface = { + .type = ACER_WMID, +}; + +/* + * Generic Device (interface-independent) + */ + +static acpi_status get_u32(u32 *value, u32 cap) +{ + acpi_status status = AE_ERROR; + + switch (interface->type) { + case ACER_AMW0: + status = AMW0_get_u32(value, cap, interface); + break; + case ACER_AMW0_V2: + if (cap == ACER_CAP_MAILLED) { + status = AMW0_get_u32(value, cap, interface); + break; + } + case ACER_WMID: + status = WMID_get_u32(value, cap, interface); + break; + } + + return status; +} + +static acpi_status set_u32(u32 value, u32 cap) +{ + acpi_status status; + + if (interface->capability & cap) { + switch (interface->type) { + case ACER_AMW0: + return AMW0_set_u32(value, cap, interface); + case ACER_AMW0_V2: + if (cap == ACER_CAP_MAILLED) + return AMW0_set_u32(value, cap, interface); + + /* + * On some models, some WMID methods don't toggle + * properly. For those cases, we want to run the AMW0 + * method afterwards to be certain we've really toggled + * the device state. + */ + if (cap == ACER_CAP_WIRELESS || + cap == ACER_CAP_BLUETOOTH) { + status = WMID_set_u32(value, cap, interface); + if (ACPI_FAILURE(status)) + return status; + + return AMW0_set_u32(value, cap, interface); + } + case ACER_WMID: + return WMID_set_u32(value, cap, interface); + default: + return AE_BAD_PARAMETER; + } + } + return AE_BAD_PARAMETER; +} + +static void __init acer_commandline_init(void) +{ + /* + * These will all fail silently if the value given is invalid, or the + * capability isn't available on the given interface + */ + set_u32(mailled, ACER_CAP_MAILLED); + set_u32(threeg, ACER_CAP_THREEG); + set_u32(brightness, ACER_CAP_BRIGHTNESS); +} + +/* + * LED device (Mail LED only, no other LEDs known yet) + */ +static void mail_led_set(struct led_classdev *led_cdev, +enum led_brightness value) +{ + set_u32(value, ACER_CAP_MAILLED); +} + +static struct led_classdev mail_led = { + .name = "acer-wmi::mail", + .brightness_set = mail_led_set, +}; + +static int __devinit acer_led_init(struct device *dev) +{ + return led_classdev_register(dev, &mail_led); +} + +static void acer_led_exit(void) +{ + led_classdev_unregister(&mail_led); +} + +/* + * Backlight device + */ +static struct backlight_device *acer_backlight_device; + +static int read_brightness(struct backlight_device *bd) +{ + u32 value; + get_u32(&value, ACER_CAP_BRIGHTNESS); + return value; +} + +static int update_bl_status(struct backlight_device *bd) +{ + int intensity = bd->props.brightness; + + if (bd->props.power != FB_BLANK_UNBLANK) + intensity = 0; + if (bd->props.fb_blank != FB_BLANK_UNBLANK) + intensity = 0; + + set_u32(intensity, ACER_CAP_BRIGHTNESS); + + return 0; +} + +static struct backlight_ops acer_bl_ops = { + .get_brightness = read_brightness, + .update_status = update_bl_status, +}; + +static int __devinit acer_backlight_init(struct device *dev) +{ + struct backlight_device *bd; + + bd = backlight_device_register("acer-wmi", dev, NULL, &acer_bl_ops); + if (IS_ERR(bd)) { + printk(ACER_ERR "Could not register Acer backlight device\n"); + acer_backlight_device = NULL; + return PTR_ERR(bd); + } + + acer_backlight_device = bd; + + bd->props.power = FB_BLANK_UNBLANK; + bd->props.brightness = max_brightness; + bd->props.max_brightness = max_brightness; + backlight_update_status(bd); + return 0; +} + +static void acer_backlight_exit(void) +{ + backlight_device_unregister(acer_backlight_device); +} + +/* + * Rfkill devices + */ +static void acer_rfkill_update(struct work_struct *ignored); +static DECLARE_DELAYED_WORK(acer_rfkill_work, acer_rfkill_update); +static void acer_rfkill_update(struct work_struct *ignored) +{ + u32 state; + acpi_status status; + + status = get_u32(&state, ACER_CAP_WIRELESS); + if (ACPI_SUCCESS(status)) + rfkill_force_state(wireless_rfkill, state ? + RFKILL_STATE_UNBLOCKED : RFKILL_STATE_SOFT_BLOCKED); + + if (has_cap(ACER_CAP_BLUETOOTH)) { + status = get_u32(&state, ACER_CAP_BLUETOOTH); + if (ACPI_SUCCESS(status)) + rfkill_force_state(bluetooth_rfkill, state ? + RFKILL_STATE_UNBLOCKED : + RFKILL_STATE_SOFT_BLOCKED); + } + + schedule_delayed_work(&acer_rfkill_work, round_jiffies_relative(HZ)); +} + +static int acer_rfkill_set(void *data, enum rfkill_state state) +{ + acpi_status status; + u32 *cap = data; + status = set_u32((u32) (state == RFKILL_STATE_UNBLOCKED), *cap); + if (ACPI_FAILURE(status)) + return -ENODEV; + return 0; +} + +static struct rfkill * acer_rfkill_register(struct device *dev, +enum rfkill_type type, char *name, u32 cap) +{ + int err; + u32 state; + u32 *data; + struct rfkill *rfkill_dev; + + rfkill_dev = rfkill_allocate(dev, type); + if (!rfkill_dev) + return ERR_PTR(-ENOMEM); + rfkill_dev->name = name; + get_u32(&state, cap); + rfkill_dev->state = state ? RFKILL_STATE_UNBLOCKED : + RFKILL_STATE_SOFT_BLOCKED; + data = kzalloc(sizeof(u32), GFP_KERNEL); + if (!data) { + rfkill_free(rfkill_dev); + return ERR_PTR(-ENOMEM); + } + *data = cap; + rfkill_dev->data = data; + rfkill_dev->toggle_radio = acer_rfkill_set; + rfkill_dev->user_claim_unsupported = 1; + + err = rfkill_register(rfkill_dev); + if (err) { + kfree(rfkill_dev->data); + rfkill_free(rfkill_dev); + return ERR_PTR(err); + } + return rfkill_dev; +} + +static int acer_rfkill_init(struct device *dev) +{ + wireless_rfkill = acer_rfkill_register(dev, RFKILL_TYPE_WLAN, + "acer-wireless", ACER_CAP_WIRELESS); + if (IS_ERR(wireless_rfkill)) + return PTR_ERR(wireless_rfkill); + + if (has_cap(ACER_CAP_BLUETOOTH)) { + bluetooth_rfkill = acer_rfkill_register(dev, + RFKILL_TYPE_BLUETOOTH, "acer-bluetooth", + ACER_CAP_BLUETOOTH); + if (IS_ERR(bluetooth_rfkill)) { + kfree(wireless_rfkill->data); + rfkill_unregister(wireless_rfkill); + return PTR_ERR(bluetooth_rfkill); + } + } + + schedule_delayed_work(&acer_rfkill_work, round_jiffies_relative(HZ)); + + return 0; +} + +static void acer_rfkill_exit(void) +{ + cancel_delayed_work_sync(&acer_rfkill_work); + kfree(wireless_rfkill->data); + rfkill_unregister(wireless_rfkill); + if (has_cap(ACER_CAP_BLUETOOTH)) { + kfree(wireless_rfkill->data); + rfkill_unregister(bluetooth_rfkill); + } + return; +} + +/* + * sysfs interface + */ +static ssize_t show_bool_threeg(struct device *dev, + struct device_attribute *attr, char *buf) +{ + u32 result; \ + acpi_status status = get_u32(&result, ACER_CAP_THREEG); + if (ACPI_SUCCESS(status)) + return sprintf(buf, "%u\n", result); + return sprintf(buf, "Read error\n"); +} + +static ssize_t set_bool_threeg(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + u32 tmp = simple_strtoul(buf, NULL, 10); + acpi_status status = set_u32(tmp, ACER_CAP_THREEG); + if (ACPI_FAILURE(status)) + return -EINVAL; + return count; +} +static DEVICE_ATTR(threeg, S_IWUGO | S_IRUGO | S_IWUSR, show_bool_threeg, + set_bool_threeg); + +static ssize_t show_interface(struct device *dev, struct device_attribute *attr, + char *buf) +{ + switch (interface->type) { + case ACER_AMW0: + return sprintf(buf, "AMW0\n"); + case ACER_AMW0_V2: + return sprintf(buf, "AMW0 v2\n"); + case ACER_WMID: + return sprintf(buf, "WMID\n"); + default: + return sprintf(buf, "Error!\n"); + } +} + +static DEVICE_ATTR(interface, S_IWUGO | S_IRUGO | S_IWUSR, + show_interface, NULL); + +/* + * debugfs functions + */ +static u32 get_wmid_devices(void) +{ + struct acpi_buffer out = {ACPI_ALLOCATE_BUFFER, NULL}; + union acpi_object *obj; + acpi_status status; + + status = wmi_query_block(WMID_GUID2, 1, &out); + if (ACPI_FAILURE(status)) + return 0; + + obj = (union acpi_object *) out.pointer; + if (obj && obj->type == ACPI_TYPE_BUFFER && + obj->buffer.length == sizeof(u32)) { + return *((u32 *) obj->buffer.pointer); + } else { + return 0; + } +} + +/* + * Platform device + */ +static int __devinit acer_platform_probe(struct platform_device *device) +{ + int err; + + if (has_cap(ACER_CAP_MAILLED)) { + err = acer_led_init(&device->dev); + if (err) + goto error_mailled; + } + + if (has_cap(ACER_CAP_BRIGHTNESS)) { + err = acer_backlight_init(&device->dev); + if (err) + goto error_brightness; + } + + err = acer_rfkill_init(&device->dev); + + return err; + +error_brightness: + acer_led_exit(); +error_mailled: + return err; +} + +static int acer_platform_remove(struct platform_device *device) +{ + if (has_cap(ACER_CAP_MAILLED)) + acer_led_exit(); + if (has_cap(ACER_CAP_BRIGHTNESS)) + acer_backlight_exit(); + + acer_rfkill_exit(); + return 0; +} + +static int acer_platform_suspend(struct platform_device *dev, +pm_message_t state) +{ + u32 value; + struct acer_data *data = &interface->data; + + if (!data) + return -ENOMEM; + + if (has_cap(ACER_CAP_MAILLED)) { + get_u32(&value, ACER_CAP_MAILLED); + data->mailled = value; + } + + if (has_cap(ACER_CAP_BRIGHTNESS)) { + get_u32(&value, ACER_CAP_BRIGHTNESS); + data->brightness = value; + } + + return 0; +} + +static int acer_platform_resume(struct platform_device *device) +{ + struct acer_data *data = &interface->data; + + if (!data) + return -ENOMEM; + + if (has_cap(ACER_CAP_MAILLED)) + set_u32(data->mailled, ACER_CAP_MAILLED); + + if (has_cap(ACER_CAP_BRIGHTNESS)) + set_u32(data->brightness, ACER_CAP_BRIGHTNESS); + + return 0; +} + +static struct platform_driver acer_platform_driver = { + .driver = { + .name = "acer-wmi", + .owner = THIS_MODULE, + }, + .probe = acer_platform_probe, + .remove = acer_platform_remove, + .suspend = acer_platform_suspend, + .resume = acer_platform_resume, +}; + +static struct platform_device *acer_platform_device; + +static int remove_sysfs(struct platform_device *device) +{ + if (has_cap(ACER_CAP_THREEG)) + device_remove_file(&device->dev, &dev_attr_threeg); + + device_remove_file(&device->dev, &dev_attr_interface); + + return 0; +} + +static int create_sysfs(void) +{ + int retval = -ENOMEM; + + if (has_cap(ACER_CAP_THREEG)) { + retval = device_create_file(&acer_platform_device->dev, + &dev_attr_threeg); + if (retval) + goto error_sysfs; + } + + retval = device_create_file(&acer_platform_device->dev, + &dev_attr_interface); + if (retval) + goto error_sysfs; + + return 0; + +error_sysfs: + remove_sysfs(acer_platform_device); + return retval; +} + +static void remove_debugfs(void) +{ + debugfs_remove(interface->debug.devices); + debugfs_remove(interface->debug.root); +} + +static int create_debugfs(void) +{ + interface->debug.root = debugfs_create_dir("acer-wmi", NULL); + if (!interface->debug.root) { + printk(ACER_ERR "Failed to create debugfs directory"); + return -ENOMEM; + } + + interface->debug.devices = debugfs_create_u32("devices", S_IRUGO, + interface->debug.root, + &interface->debug.wmid_devices); + if (!interface->debug.devices) + goto error_debugfs; + + return 0; + +error_debugfs: + remove_debugfs(); + return -ENOMEM; +} + +static int __init acer_wmi_init(void) +{ + int err; + + printk(ACER_INFO "Acer Laptop ACPI-WMI Extras\n"); + + find_quirks(); + + /* + * Detect which ACPI-WMI interface we're using. + */ + if (wmi_has_guid(AMW0_GUID1) && wmi_has_guid(WMID_GUID1)) + interface = &AMW0_V2_interface; + + if (!wmi_has_guid(AMW0_GUID1) && wmi_has_guid(WMID_GUID1)) + interface = &wmid_interface; + + if (wmi_has_guid(WMID_GUID2) && interface) { + if (ACPI_FAILURE(WMID_set_capabilities())) { + printk(ACER_ERR "Unable to detect available WMID " + "devices\n"); + return -ENODEV; + } + } else if (!wmi_has_guid(WMID_GUID2) && interface) { + printk(ACER_ERR "No WMID device detection method found\n"); + return -ENODEV; + } + + if (wmi_has_guid(AMW0_GUID1) && !wmi_has_guid(WMID_GUID1)) { + interface = &AMW0_interface; + + if (ACPI_FAILURE(AMW0_set_capabilities())) { + printk(ACER_ERR "Unable to detect available AMW0 " + "devices\n"); + return -ENODEV; + } + } + + if (wmi_has_guid(AMW0_GUID1)) + AMW0_find_mailled(); + + if (!interface) { + printk(ACER_ERR "No or unsupported WMI interface, unable to " + "load\n"); + return -ENODEV; + } + + set_quirks(); + + if (!acpi_video_backlight_support() && has_cap(ACER_CAP_BRIGHTNESS)) { + interface->capability &= ~ACER_CAP_BRIGHTNESS; + printk(ACER_INFO "Brightness must be controlled by " + "generic video driver\n"); + } + + if (platform_driver_register(&acer_platform_driver)) { + printk(ACER_ERR "Unable to register platform driver.\n"); + goto error_platform_register; + } + acer_platform_device = platform_device_alloc("acer-wmi", -1); + platform_device_add(acer_platform_device); + + err = create_sysfs(); + if (err) + return err; + + if (wmi_has_guid(WMID_GUID2)) { + interface->debug.wmid_devices = get_wmid_devices(); + err = create_debugfs(); + if (err) + return err; + } + + /* Override any initial settings with values from the commandline */ + acer_commandline_init(); + + return 0; + +error_platform_register: + return -ENODEV; +} + +static void __exit acer_wmi_exit(void) +{ + remove_sysfs(acer_platform_device); + remove_debugfs(); + platform_device_del(acer_platform_device); + platform_driver_unregister(&acer_platform_driver); + + printk(ACER_INFO "Acer Laptop WMI Extras unloaded\n"); + return; +} + +module_init(acer_wmi_init); +module_exit(acer_wmi_exit); diff --git a/drivers/platform/x86/asus-laptop.c b/drivers/platform/x86/asus-laptop.c new file mode 100644 index 000000000000..8fb8b3591048 --- /dev/null +++ b/drivers/platform/x86/asus-laptop.c @@ -0,0 +1,1266 @@ +/* + * asus-laptop.c - Asus Laptop Support + * + * + * Copyright (C) 2002-2005 Julien Lerouge, 2003-2006 Karol Kozimor + * Copyright (C) 2006-2007 Corentin Chary + * + * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * + * The development page for this driver is located at + * http://sourceforge.net/projects/acpi4asus/ + * + * Credits: + * Pontus Fuchs - Helper functions, cleanup + * Johann Wiesner - Small compile fixes + * John Belmonte - ACPI code for Toshiba laptop was a good starting point. + * Eric Burghard - LED display support for W1N + * Josh Green - Light Sens support + * Thomas Tuttle - His first patch for led support was very helpfull + * Sam Lin - GPS support + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/types.h> +#include <linux/err.h> +#include <linux/proc_fs.h> +#include <linux/backlight.h> +#include <linux/fb.h> +#include <linux/leds.h> +#include <linux/platform_device.h> +#include <acpi/acpi_drivers.h> +#include <acpi/acpi_bus.h> +#include <asm/uaccess.h> + +#define ASUS_LAPTOP_VERSION "0.42" + +#define ASUS_HOTK_NAME "Asus Laptop Support" +#define ASUS_HOTK_CLASS "hotkey" +#define ASUS_HOTK_DEVICE_NAME "Hotkey" +#define ASUS_HOTK_FILE "asus-laptop" +#define ASUS_HOTK_PREFIX "\\_SB.ATKD." + +/* + * Some events we use, same for all Asus + */ +#define ATKD_BR_UP 0x10 +#define ATKD_BR_DOWN 0x20 +#define ATKD_LCD_ON 0x33 +#define ATKD_LCD_OFF 0x34 + +/* + * Known bits returned by \_SB.ATKD.HWRS + */ +#define WL_HWRS 0x80 +#define BT_HWRS 0x100 + +/* + * Flags for hotk status + * WL_ON and BT_ON are also used for wireless_status() + */ +#define WL_ON 0x01 //internal Wifi +#define BT_ON 0x02 //internal Bluetooth +#define MLED_ON 0x04 //mail LED +#define TLED_ON 0x08 //touchpad LED +#define RLED_ON 0x10 //Record LED +#define PLED_ON 0x20 //Phone LED +#define GLED_ON 0x40 //Gaming LED +#define LCD_ON 0x80 //LCD backlight +#define GPS_ON 0x100 //GPS + +#define ASUS_LOG ASUS_HOTK_FILE ": " +#define ASUS_ERR KERN_ERR ASUS_LOG +#define ASUS_WARNING KERN_WARNING ASUS_LOG +#define ASUS_NOTICE KERN_NOTICE ASUS_LOG +#define ASUS_INFO KERN_INFO ASUS_LOG +#define ASUS_DEBUG KERN_DEBUG ASUS_LOG + +MODULE_AUTHOR("Julien Lerouge, Karol Kozimor, Corentin Chary"); +MODULE_DESCRIPTION(ASUS_HOTK_NAME); +MODULE_LICENSE("GPL"); + +/* WAPF defines the behavior of the Fn+Fx wlan key + * The significance of values is yet to be found, but + * most of the time: + * 0x0 will do nothing + * 0x1 will allow to control the device with Fn+Fx key. + * 0x4 will send an ACPI event (0x88) while pressing the Fn+Fx key + * 0x5 like 0x1 or 0x4 + * So, if something doesn't work as you want, just try other values =) + */ +static uint wapf = 1; +module_param(wapf, uint, 0644); +MODULE_PARM_DESC(wapf, "WAPF value"); + +#define ASUS_HANDLE(object, paths...) \ + static acpi_handle object##_handle = NULL; \ + static char *object##_paths[] = { paths } + +/* LED */ +ASUS_HANDLE(mled_set, ASUS_HOTK_PREFIX "MLED"); +ASUS_HANDLE(tled_set, ASUS_HOTK_PREFIX "TLED"); +ASUS_HANDLE(rled_set, ASUS_HOTK_PREFIX "RLED"); /* W1JC */ +ASUS_HANDLE(pled_set, ASUS_HOTK_PREFIX "PLED"); /* A7J */ +ASUS_HANDLE(gled_set, ASUS_HOTK_PREFIX "GLED"); /* G1, G2 (probably) */ + +/* LEDD */ +ASUS_HANDLE(ledd_set, ASUS_HOTK_PREFIX "SLCM"); + +/* Bluetooth and WLAN + * WLED and BLED are not handled like other XLED, because in some dsdt + * they also control the WLAN/Bluetooth device. + */ +ASUS_HANDLE(wl_switch, ASUS_HOTK_PREFIX "WLED"); +ASUS_HANDLE(bt_switch, ASUS_HOTK_PREFIX "BLED"); +ASUS_HANDLE(wireless_status, ASUS_HOTK_PREFIX "RSTS"); /* All new models */ + +/* Brightness */ +ASUS_HANDLE(brightness_set, ASUS_HOTK_PREFIX "SPLV"); +ASUS_HANDLE(brightness_get, ASUS_HOTK_PREFIX "GPLV"); + +/* Backlight */ +ASUS_HANDLE(lcd_switch, "\\_SB.PCI0.SBRG.EC0._Q10", /* All new models */ + "\\_SB.PCI0.ISA.EC0._Q10", /* A1x */ + "\\_SB.PCI0.PX40.ECD0._Q10", /* L3C */ + "\\_SB.PCI0.PX40.EC0.Q10", /* M1A */ + "\\_SB.PCI0.LPCB.EC0._Q10", /* P30 */ + "\\_SB.PCI0.LPCB.EC0._Q0E", /* P30/P35 */ + "\\_SB.PCI0.PX40.Q10", /* S1x */ + "\\Q10"); /* A2x, L2D, L3D, M2E */ + +/* Display */ +ASUS_HANDLE(display_set, ASUS_HOTK_PREFIX "SDSP"); +ASUS_HANDLE(display_get, "\\_SB.PCI0.P0P1.VGA.GETD", /* A6B, A6K A6R A7D F3JM L4R M6R A3G + M6A M6V VX-1 V6J V6V W3Z */ + "\\_SB.PCI0.P0P2.VGA.GETD", /* A3E A4K, A4D A4L A6J A7J A8J Z71V M9V + S5A M5A z33A W1Jc W2V G1 */ + "\\_SB.PCI0.P0P3.VGA.GETD", /* A6V A6Q */ + "\\_SB.PCI0.P0PA.VGA.GETD", /* A6T, A6M */ + "\\_SB.PCI0.PCI1.VGAC.NMAP", /* L3C */ + "\\_SB.PCI0.VGA.GETD", /* Z96F */ + "\\ACTD", /* A2D */ + "\\ADVG", /* A4G Z71A W1N W5A W5F M2N M3N M5N M6N S1N S5N */ + "\\DNXT", /* P30 */ + "\\INFB", /* A2H D1 L2D L3D L3H L2E L5D L5C M1A M2E L4L W3V */ + "\\SSTE"); /* A3F A6F A3N A3L M6N W3N W6A */ + +ASUS_HANDLE(ls_switch, ASUS_HOTK_PREFIX "ALSC"); /* Z71A Z71V */ +ASUS_HANDLE(ls_level, ASUS_HOTK_PREFIX "ALSL"); /* Z71A Z71V */ + +/* GPS */ +/* R2H use different handle for GPS on/off */ +ASUS_HANDLE(gps_on, ASUS_HOTK_PREFIX "SDON"); /* R2H */ +ASUS_HANDLE(gps_off, ASUS_HOTK_PREFIX "SDOF"); /* R2H */ +ASUS_HANDLE(gps_status, ASUS_HOTK_PREFIX "GPST"); + +/* + * This is the main structure, we can use it to store anything interesting + * about the hotk device + */ +struct asus_hotk { + char *name; //laptop name + struct acpi_device *device; //the device we are in + acpi_handle handle; //the handle of the hotk device + char status; //status of the hotk, for LEDs, ... + u32 ledd_status; //status of the LED display + u8 light_level; //light sensor level + u8 light_switch; //light sensor switch value + u16 event_count[128]; //count for each event TODO make this better +}; + +/* + * This header is made available to allow proper configuration given model, + * revision number , ... this info cannot go in struct asus_hotk because it is + * available before the hotk + */ +static struct acpi_table_header *asus_info; + +/* The actual device the driver binds to */ +static struct asus_hotk *hotk; + +/* + * The hotkey driver declaration + */ +static const struct acpi_device_id asus_device_ids[] = { + {"ATK0100", 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, asus_device_ids); + +static int asus_hotk_add(struct acpi_device *device); +static int asus_hotk_remove(struct acpi_device *device, int type); +static struct acpi_driver asus_hotk_driver = { + .name = ASUS_HOTK_NAME, + .class = ASUS_HOTK_CLASS, + .ids = asus_device_ids, + .ops = { + .add = asus_hotk_add, + .remove = asus_hotk_remove, + }, +}; + +/* The backlight device /sys/class/backlight */ +static struct backlight_device *asus_backlight_device; + +/* + * The backlight class declaration + */ +static int read_brightness(struct backlight_device *bd); +static int update_bl_status(struct backlight_device *bd); +static struct backlight_ops asusbl_ops = { + .get_brightness = read_brightness, + .update_status = update_bl_status, +}; + +/* These functions actually update the LED's, and are called from a + * workqueue. By doing this as separate work rather than when the LED + * subsystem asks, we avoid messing with the Asus ACPI stuff during a + * potentially bad time, such as a timer interrupt. */ +static struct workqueue_struct *led_workqueue; + +#define ASUS_LED(object, ledname) \ + static void object##_led_set(struct led_classdev *led_cdev, \ + enum led_brightness value); \ + static void object##_led_update(struct work_struct *ignored); \ + static int object##_led_wk; \ + static DECLARE_WORK(object##_led_work, object##_led_update); \ + static struct led_classdev object##_led = { \ + .name = "asus::" ledname, \ + .brightness_set = object##_led_set, \ + } + +ASUS_LED(mled, "mail"); +ASUS_LED(tled, "touchpad"); +ASUS_LED(rled, "record"); +ASUS_LED(pled, "phone"); +ASUS_LED(gled, "gaming"); + +/* + * This function evaluates an ACPI method, given an int as parameter, the + * method is searched within the scope of the handle, can be NULL. The output + * of the method is written is output, which can also be NULL + * + * returns 0 if write is successful, -1 else. + */ +static int write_acpi_int(acpi_handle handle, const char *method, int val, + struct acpi_buffer *output) +{ + struct acpi_object_list params; //list of input parameters (an int here) + union acpi_object in_obj; //the only param we use + acpi_status status; + + if (!handle) + return 0; + + params.count = 1; + params.pointer = &in_obj; + in_obj.type = ACPI_TYPE_INTEGER; + in_obj.integer.value = val; + + status = acpi_evaluate_object(handle, (char *)method, ¶ms, output); + if (status == AE_OK) + return 0; + else + return -1; +} + +static int read_wireless_status(int mask) +{ + unsigned long long status; + acpi_status rv = AE_OK; + + if (!wireless_status_handle) + return (hotk->status & mask) ? 1 : 0; + + rv = acpi_evaluate_integer(wireless_status_handle, NULL, NULL, &status); + if (ACPI_FAILURE(rv)) + printk(ASUS_WARNING "Error reading Wireless status\n"); + else + return (status & mask) ? 1 : 0; + + return (hotk->status & mask) ? 1 : 0; +} + +static int read_gps_status(void) +{ + unsigned long long status; + acpi_status rv = AE_OK; + + rv = acpi_evaluate_integer(gps_status_handle, NULL, NULL, &status); + if (ACPI_FAILURE(rv)) + printk(ASUS_WARNING "Error reading GPS status\n"); + else + return status ? 1 : 0; + + return (hotk->status & GPS_ON) ? 1 : 0; +} + +/* Generic LED functions */ +static int read_status(int mask) +{ + /* There is a special method for both wireless devices */ + if (mask == BT_ON || mask == WL_ON) + return read_wireless_status(mask); + else if (mask == GPS_ON) + return read_gps_status(); + + return (hotk->status & mask) ? 1 : 0; +} + +static void write_status(acpi_handle handle, int out, int mask) +{ + hotk->status = (out) ? (hotk->status | mask) : (hotk->status & ~mask); + + switch (mask) { + case MLED_ON: + out = !(out & 0x1); + break; + case GLED_ON: + out = (out & 0x1) + 1; + break; + case GPS_ON: + handle = (out) ? gps_on_handle : gps_off_handle; + out = 0x02; + break; + default: + out &= 0x1; + break; + } + + if (write_acpi_int(handle, NULL, out, NULL)) + printk(ASUS_WARNING " write failed %x\n", mask); +} + +/* /sys/class/led handlers */ +#define ASUS_LED_HANDLER(object, mask) \ + static void object##_led_set(struct led_classdev *led_cdev, \ + enum led_brightness value) \ + { \ + object##_led_wk = (value > 0) ? 1 : 0; \ + queue_work(led_workqueue, &object##_led_work); \ + } \ + static void object##_led_update(struct work_struct *ignored) \ + { \ + int value = object##_led_wk; \ + write_status(object##_set_handle, value, (mask)); \ + } + +ASUS_LED_HANDLER(mled, MLED_ON); +ASUS_LED_HANDLER(pled, PLED_ON); +ASUS_LED_HANDLER(rled, RLED_ON); +ASUS_LED_HANDLER(tled, TLED_ON); +ASUS_LED_HANDLER(gled, GLED_ON); + +static int get_lcd_state(void) +{ + return read_status(LCD_ON); +} + +static int set_lcd_state(int value) +{ + int lcd = 0; + acpi_status status = 0; + + lcd = value ? 1 : 0; + + if (lcd == get_lcd_state()) + return 0; + + if (lcd_switch_handle) { + status = acpi_evaluate_object(lcd_switch_handle, + NULL, NULL, NULL); + + if (ACPI_FAILURE(status)) + printk(ASUS_WARNING "Error switching LCD\n"); + } + + write_status(NULL, lcd, LCD_ON); + return 0; +} + +static void lcd_blank(int blank) +{ + struct backlight_device *bd = asus_backlight_device; + + if (bd) { + bd->props.power = blank; + backlight_update_status(bd); + } +} + +static int read_brightness(struct backlight_device *bd) +{ + unsigned long long value; + acpi_status rv = AE_OK; + + rv = acpi_evaluate_integer(brightness_get_handle, NULL, NULL, &value); + if (ACPI_FAILURE(rv)) + printk(ASUS_WARNING "Error reading brightness\n"); + + return value; +} + +static int set_brightness(struct backlight_device *bd, int value) +{ + int ret = 0; + + value = (0 < value) ? ((15 < value) ? 15 : value) : 0; + /* 0 <= value <= 15 */ + + if (write_acpi_int(brightness_set_handle, NULL, value, NULL)) { + printk(ASUS_WARNING "Error changing brightness\n"); + ret = -EIO; + } + + return ret; +} + +static int update_bl_status(struct backlight_device *bd) +{ + int rv; + int value = bd->props.brightness; + + rv = set_brightness(bd, value); + if (rv) + return rv; + + value = (bd->props.power == FB_BLANK_UNBLANK) ? 1 : 0; + return set_lcd_state(value); +} + +/* + * Platform device handlers + */ + +/* + * We write our info in page, we begin at offset off and cannot write more + * than count bytes. We set eof to 1 if we handle those 2 values. We return the + * number of bytes written in page + */ +static ssize_t show_infos(struct device *dev, + struct device_attribute *attr, char *page) +{ + int len = 0; + unsigned long long temp; + char buf[16]; //enough for all info + acpi_status rv = AE_OK; + + /* + * We use the easy way, we don't care of off and count, so we don't set eof + * to 1 + */ + + len += sprintf(page, ASUS_HOTK_NAME " " ASUS_LAPTOP_VERSION "\n"); + len += sprintf(page + len, "Model reference : %s\n", hotk->name); + /* + * The SFUN method probably allows the original driver to get the list + * of features supported by a given model. For now, 0x0100 or 0x0800 + * bit signifies that the laptop is equipped with a Wi-Fi MiniPCI card. + * The significance of others is yet to be found. + */ + rv = acpi_evaluate_integer(hotk->handle, "SFUN", NULL, &temp); + if (!ACPI_FAILURE(rv)) + len += sprintf(page + len, "SFUN value : 0x%04x\n", + (uint) temp); + /* + * Another value for userspace: the ASYM method returns 0x02 for + * battery low and 0x04 for battery critical, its readings tend to be + * more accurate than those provided by _BST. + * Note: since not all the laptops provide this method, errors are + * silently ignored. + */ + rv = acpi_evaluate_integer(hotk->handle, "ASYM", NULL, &temp); + if (!ACPI_FAILURE(rv)) + len += sprintf(page + len, "ASYM value : 0x%04x\n", + (uint) temp); + if (asus_info) { + snprintf(buf, 16, "%d", asus_info->length); + len += sprintf(page + len, "DSDT length : %s\n", buf); + snprintf(buf, 16, "%d", asus_info->checksum); + len += sprintf(page + len, "DSDT checksum : %s\n", buf); + snprintf(buf, 16, "%d", asus_info->revision); + len += sprintf(page + len, "DSDT revision : %s\n", buf); + snprintf(buf, 7, "%s", asus_info->oem_id); + len += sprintf(page + len, "OEM id : %s\n", buf); + snprintf(buf, 9, "%s", asus_info->oem_table_id); + len += sprintf(page + len, "OEM table id : %s\n", buf); + snprintf(buf, 16, "%x", asus_info->oem_revision); + len += sprintf(page + len, "OEM revision : 0x%s\n", buf); + snprintf(buf, 5, "%s", asus_info->asl_compiler_id); + len += sprintf(page + len, "ASL comp vendor id : %s\n", buf); + snprintf(buf, 16, "%x", asus_info->asl_compiler_revision); + len += sprintf(page + len, "ASL comp revision : 0x%s\n", buf); + } + + return len; +} + +static int parse_arg(const char *buf, unsigned long count, int *val) +{ + if (!count) + return 0; + if (count > 31) + return -EINVAL; + if (sscanf(buf, "%i", val) != 1) + return -EINVAL; + return count; +} + +static ssize_t store_status(const char *buf, size_t count, + acpi_handle handle, int mask) +{ + int rv, value; + int out = 0; + + rv = parse_arg(buf, count, &value); + if (rv > 0) + out = value ? 1 : 0; + + write_status(handle, out, mask); + + return rv; +} + +/* + * LEDD display + */ +static ssize_t show_ledd(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "0x%08x\n", hotk->ledd_status); +} + +static ssize_t store_ledd(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int rv, value; + + rv = parse_arg(buf, count, &value); + if (rv > 0) { + if (write_acpi_int(ledd_set_handle, NULL, value, NULL)) + printk(ASUS_WARNING "LED display write failed\n"); + else + hotk->ledd_status = (u32) value; + } + return rv; +} + +/* + * WLAN + */ +static ssize_t show_wlan(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "%d\n", read_status(WL_ON)); +} + +static ssize_t store_wlan(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + return store_status(buf, count, wl_switch_handle, WL_ON); +} + +/* + * Bluetooth + */ +static ssize_t show_bluetooth(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "%d\n", read_status(BT_ON)); +} + +static ssize_t store_bluetooth(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + return store_status(buf, count, bt_switch_handle, BT_ON); +} + +/* + * Display + */ +static void set_display(int value) +{ + /* no sanity check needed for now */ + if (write_acpi_int(display_set_handle, NULL, value, NULL)) + printk(ASUS_WARNING "Error setting display\n"); + return; +} + +static int read_display(void) +{ + unsigned long long value = 0; + acpi_status rv = AE_OK; + + /* In most of the case, we know how to set the display, but sometime + we can't read it */ + if (display_get_handle) { + rv = acpi_evaluate_integer(display_get_handle, NULL, + NULL, &value); + if (ACPI_FAILURE(rv)) + printk(ASUS_WARNING "Error reading display status\n"); + } + + value &= 0x0F; /* needed for some models, shouldn't hurt others */ + + return value; +} + +/* + * Now, *this* one could be more user-friendly, but so far, no-one has + * complained. The significance of bits is the same as in store_disp() + */ +static ssize_t show_disp(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "%d\n", read_display()); +} + +/* + * Experimental support for display switching. As of now: 1 should activate + * the LCD output, 2 should do for CRT, 4 for TV-Out and 8 for DVI. + * Any combination (bitwise) of these will suffice. I never actually tested 4 + * displays hooked up simultaneously, so be warned. See the acpi4asus README + * for more info. + */ +static ssize_t store_disp(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int rv, value; + + rv = parse_arg(buf, count, &value); + if (rv > 0) + set_display(value); + return rv; +} + +/* + * Light Sens + */ +static void set_light_sens_switch(int value) +{ + if (write_acpi_int(ls_switch_handle, NULL, value, NULL)) + printk(ASUS_WARNING "Error setting light sensor switch\n"); + hotk->light_switch = value; +} + +static ssize_t show_lssw(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "%d\n", hotk->light_switch); +} + +static ssize_t store_lssw(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int rv, value; + + rv = parse_arg(buf, count, &value); + if (rv > 0) + set_light_sens_switch(value ? 1 : 0); + + return rv; +} + +static void set_light_sens_level(int value) +{ + if (write_acpi_int(ls_level_handle, NULL, value, NULL)) + printk(ASUS_WARNING "Error setting light sensor level\n"); + hotk->light_level = value; +} + +static ssize_t show_lslvl(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "%d\n", hotk->light_level); +} + +static ssize_t store_lslvl(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int rv, value; + + rv = parse_arg(buf, count, &value); + if (rv > 0) { + value = (0 < value) ? ((15 < value) ? 15 : value) : 0; + /* 0 <= value <= 15 */ + set_light_sens_level(value); + } + + return rv; +} + +/* + * GPS + */ +static ssize_t show_gps(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "%d\n", read_status(GPS_ON)); +} + +static ssize_t store_gps(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + return store_status(buf, count, NULL, GPS_ON); +} + +static void asus_hotk_notify(acpi_handle handle, u32 event, void *data) +{ + /* TODO Find a better way to handle events count. */ + if (!hotk) + return; + + /* + * We need to tell the backlight device when the backlight power is + * switched + */ + if (event == ATKD_LCD_ON) { + write_status(NULL, 1, LCD_ON); + lcd_blank(FB_BLANK_UNBLANK); + } else if (event == ATKD_LCD_OFF) { + write_status(NULL, 0, LCD_ON); + lcd_blank(FB_BLANK_POWERDOWN); + } + + acpi_bus_generate_proc_event(hotk->device, event, + hotk->event_count[event % 128]++); + + return; +} + +#define ASUS_CREATE_DEVICE_ATTR(_name) \ + struct device_attribute dev_attr_##_name = { \ + .attr = { \ + .name = __stringify(_name), \ + .mode = 0 }, \ + .show = NULL, \ + .store = NULL, \ + } + +#define ASUS_SET_DEVICE_ATTR(_name, _mode, _show, _store) \ + do { \ + dev_attr_##_name.attr.mode = _mode; \ + dev_attr_##_name.show = _show; \ + dev_attr_##_name.store = _store; \ + } while(0) + +static ASUS_CREATE_DEVICE_ATTR(infos); +static ASUS_CREATE_DEVICE_ATTR(wlan); +static ASUS_CREATE_DEVICE_ATTR(bluetooth); +static ASUS_CREATE_DEVICE_ATTR(display); +static ASUS_CREATE_DEVICE_ATTR(ledd); +static ASUS_CREATE_DEVICE_ATTR(ls_switch); +static ASUS_CREATE_DEVICE_ATTR(ls_level); +static ASUS_CREATE_DEVICE_ATTR(gps); + +static struct attribute *asuspf_attributes[] = { + &dev_attr_infos.attr, + &dev_attr_wlan.attr, + &dev_attr_bluetooth.attr, + &dev_attr_display.attr, + &dev_attr_ledd.attr, + &dev_attr_ls_switch.attr, + &dev_attr_ls_level.attr, + &dev_attr_gps.attr, + NULL +}; + +static struct attribute_group asuspf_attribute_group = { + .attrs = asuspf_attributes +}; + +static struct platform_driver asuspf_driver = { + .driver = { + .name = ASUS_HOTK_FILE, + .owner = THIS_MODULE, + } +}; + +static struct platform_device *asuspf_device; + +static void asus_hotk_add_fs(void) +{ + ASUS_SET_DEVICE_ATTR(infos, 0444, show_infos, NULL); + + if (wl_switch_handle) + ASUS_SET_DEVICE_ATTR(wlan, 0644, show_wlan, store_wlan); + + if (bt_switch_handle) + ASUS_SET_DEVICE_ATTR(bluetooth, 0644, + show_bluetooth, store_bluetooth); + + if (display_set_handle && display_get_handle) + ASUS_SET_DEVICE_ATTR(display, 0644, show_disp, store_disp); + else if (display_set_handle) + ASUS_SET_DEVICE_ATTR(display, 0200, NULL, store_disp); + + if (ledd_set_handle) + ASUS_SET_DEVICE_ATTR(ledd, 0644, show_ledd, store_ledd); + + if (ls_switch_handle && ls_level_handle) { + ASUS_SET_DEVICE_ATTR(ls_level, 0644, show_lslvl, store_lslvl); + ASUS_SET_DEVICE_ATTR(ls_switch, 0644, show_lssw, store_lssw); + } + + if (gps_status_handle && gps_on_handle && gps_off_handle) + ASUS_SET_DEVICE_ATTR(gps, 0644, show_gps, store_gps); +} + +static int asus_handle_init(char *name, acpi_handle * handle, + char **paths, int num_paths) +{ + int i; + acpi_status status; + + for (i = 0; i < num_paths; i++) { + status = acpi_get_handle(NULL, paths[i], handle); + if (ACPI_SUCCESS(status)) + return 0; + } + + *handle = NULL; + return -ENODEV; +} + +#define ASUS_HANDLE_INIT(object) \ + asus_handle_init(#object, &object##_handle, object##_paths, \ + ARRAY_SIZE(object##_paths)) + +/* + * This function is used to initialize the hotk with right values. In this + * method, we can make all the detection we want, and modify the hotk struct + */ +static int asus_hotk_get_info(void) +{ + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *model = NULL; + unsigned long long bsts_result, hwrs_result; + char *string = NULL; + acpi_status status; + + /* + * Get DSDT headers early enough to allow for differentiating between + * models, but late enough to allow acpi_bus_register_driver() to fail + * before doing anything ACPI-specific. Should we encounter a machine, + * which needs special handling (i.e. its hotkey device has a different + * HID), this bit will be moved. A global variable asus_info contains + * the DSDT header. + */ + status = acpi_get_table(ACPI_SIG_DSDT, 1, &asus_info); + if (ACPI_FAILURE(status)) + printk(ASUS_WARNING "Couldn't get the DSDT table header\n"); + + /* We have to write 0 on init this far for all ASUS models */ + if (write_acpi_int(hotk->handle, "INIT", 0, &buffer)) { + printk(ASUS_ERR "Hotkey initialization failed\n"); + return -ENODEV; + } + + /* This needs to be called for some laptops to init properly */ + status = + acpi_evaluate_integer(hotk->handle, "BSTS", NULL, &bsts_result); + if (ACPI_FAILURE(status)) + printk(ASUS_WARNING "Error calling BSTS\n"); + else if (bsts_result) + printk(ASUS_NOTICE "BSTS called, 0x%02x returned\n", + (uint) bsts_result); + + /* This too ... */ + write_acpi_int(hotk->handle, "CWAP", wapf, NULL); + + /* + * Try to match the object returned by INIT to the specific model. + * Handle every possible object (or the lack of thereof) the DSDT + * writers might throw at us. When in trouble, we pass NULL to + * asus_model_match() and try something completely different. + */ + if (buffer.pointer) { + model = buffer.pointer; + switch (model->type) { + case ACPI_TYPE_STRING: + string = model->string.pointer; + break; + case ACPI_TYPE_BUFFER: + string = model->buffer.pointer; + break; + default: + string = ""; + break; + } + } + hotk->name = kstrdup(string, GFP_KERNEL); + if (!hotk->name) + return -ENOMEM; + + if (*string) + printk(ASUS_NOTICE " %s model detected\n", string); + + ASUS_HANDLE_INIT(mled_set); + ASUS_HANDLE_INIT(tled_set); + ASUS_HANDLE_INIT(rled_set); + ASUS_HANDLE_INIT(pled_set); + ASUS_HANDLE_INIT(gled_set); + + ASUS_HANDLE_INIT(ledd_set); + + /* + * The HWRS method return informations about the hardware. + * 0x80 bit is for WLAN, 0x100 for Bluetooth. + * The significance of others is yet to be found. + * If we don't find the method, we assume the device are present. + */ + status = + acpi_evaluate_integer(hotk->handle, "HRWS", NULL, &hwrs_result); + if (ACPI_FAILURE(status)) + hwrs_result = WL_HWRS | BT_HWRS; + + if (hwrs_result & WL_HWRS) + ASUS_HANDLE_INIT(wl_switch); + if (hwrs_result & BT_HWRS) + ASUS_HANDLE_INIT(bt_switch); + + ASUS_HANDLE_INIT(wireless_status); + + ASUS_HANDLE_INIT(brightness_set); + ASUS_HANDLE_INIT(brightness_get); + + ASUS_HANDLE_INIT(lcd_switch); + + ASUS_HANDLE_INIT(display_set); + ASUS_HANDLE_INIT(display_get); + + /* There is a lot of models with "ALSL", but a few get + a real light sens, so we need to check it. */ + if (!ASUS_HANDLE_INIT(ls_switch)) + ASUS_HANDLE_INIT(ls_level); + + ASUS_HANDLE_INIT(gps_on); + ASUS_HANDLE_INIT(gps_off); + ASUS_HANDLE_INIT(gps_status); + + kfree(model); + + return AE_OK; +} + +static int asus_hotk_check(void) +{ + int result = 0; + + result = acpi_bus_get_status(hotk->device); + if (result) + return result; + + if (hotk->device->status.present) { + result = asus_hotk_get_info(); + } else { + printk(ASUS_ERR "Hotkey device not present, aborting\n"); + return -EINVAL; + } + + return result; +} + +static int asus_hotk_found; + +static int asus_hotk_add(struct acpi_device *device) +{ + acpi_status status = AE_OK; + int result; + + if (!device) + return -EINVAL; + + printk(ASUS_NOTICE "Asus Laptop Support version %s\n", + ASUS_LAPTOP_VERSION); + + hotk = kzalloc(sizeof(struct asus_hotk), GFP_KERNEL); + if (!hotk) + return -ENOMEM; + + hotk->handle = device->handle; + strcpy(acpi_device_name(device), ASUS_HOTK_DEVICE_NAME); + strcpy(acpi_device_class(device), ASUS_HOTK_CLASS); + device->driver_data = hotk; + hotk->device = device; + + result = asus_hotk_check(); + if (result) + goto end; + + asus_hotk_add_fs(); + + /* + * We install the handler, it will receive the hotk in parameter, so, we + * could add other data to the hotk struct + */ + status = acpi_install_notify_handler(hotk->handle, ACPI_ALL_NOTIFY, + asus_hotk_notify, hotk); + if (ACPI_FAILURE(status)) + printk(ASUS_ERR "Error installing notify handler\n"); + + asus_hotk_found = 1; + + /* WLED and BLED are on by default */ + write_status(bt_switch_handle, 1, BT_ON); + write_status(wl_switch_handle, 1, WL_ON); + + /* If the h/w switch is off, we need to check the real status */ + write_status(NULL, read_status(BT_ON), BT_ON); + write_status(NULL, read_status(WL_ON), WL_ON); + + /* LCD Backlight is on by default */ + write_status(NULL, 1, LCD_ON); + + /* LED display is off by default */ + hotk->ledd_status = 0xFFF; + + /* Set initial values of light sensor and level */ + hotk->light_switch = 1; /* Default to light sensor disabled */ + hotk->light_level = 0; /* level 5 for sensor sensitivity */ + + if (ls_switch_handle) + set_light_sens_switch(hotk->light_switch); + + if (ls_level_handle) + set_light_sens_level(hotk->light_level); + + /* GPS is on by default */ + write_status(NULL, 1, GPS_ON); + + end: + if (result) { + kfree(hotk->name); + kfree(hotk); + } + + return result; +} + +static int asus_hotk_remove(struct acpi_device *device, int type) +{ + acpi_status status = 0; + + if (!device || !acpi_driver_data(device)) + return -EINVAL; + + status = acpi_remove_notify_handler(hotk->handle, ACPI_ALL_NOTIFY, + asus_hotk_notify); + if (ACPI_FAILURE(status)) + printk(ASUS_ERR "Error removing notify handler\n"); + + kfree(hotk->name); + kfree(hotk); + + return 0; +} + +static void asus_backlight_exit(void) +{ + if (asus_backlight_device) + backlight_device_unregister(asus_backlight_device); +} + +#define ASUS_LED_UNREGISTER(object) \ + if (object##_led.dev) \ + led_classdev_unregister(&object##_led) + +static void asus_led_exit(void) +{ + destroy_workqueue(led_workqueue); + ASUS_LED_UNREGISTER(mled); + ASUS_LED_UNREGISTER(tled); + ASUS_LED_UNREGISTER(pled); + ASUS_LED_UNREGISTER(rled); + ASUS_LED_UNREGISTER(gled); +} + +static void __exit asus_laptop_exit(void) +{ + asus_backlight_exit(); + asus_led_exit(); + + acpi_bus_unregister_driver(&asus_hotk_driver); + sysfs_remove_group(&asuspf_device->dev.kobj, &asuspf_attribute_group); + platform_device_unregister(asuspf_device); + platform_driver_unregister(&asuspf_driver); +} + +static int asus_backlight_init(struct device *dev) +{ + struct backlight_device *bd; + + if (brightness_set_handle && lcd_switch_handle) { + bd = backlight_device_register(ASUS_HOTK_FILE, dev, + NULL, &asusbl_ops); + if (IS_ERR(bd)) { + printk(ASUS_ERR + "Could not register asus backlight device\n"); + asus_backlight_device = NULL; + return PTR_ERR(bd); + } + + asus_backlight_device = bd; + + bd->props.max_brightness = 15; + bd->props.brightness = read_brightness(NULL); + bd->props.power = FB_BLANK_UNBLANK; + backlight_update_status(bd); + } + return 0; +} + +static int asus_led_register(acpi_handle handle, + struct led_classdev *ldev, struct device *dev) +{ + if (!handle) + return 0; + + return led_classdev_register(dev, ldev); +} + +#define ASUS_LED_REGISTER(object, device) \ + asus_led_register(object##_set_handle, &object##_led, device) + +static int asus_led_init(struct device *dev) +{ + int rv; + + rv = ASUS_LED_REGISTER(mled, dev); + if (rv) + goto out; + + rv = ASUS_LED_REGISTER(tled, dev); + if (rv) + goto out1; + + rv = ASUS_LED_REGISTER(rled, dev); + if (rv) + goto out2; + + rv = ASUS_LED_REGISTER(pled, dev); + if (rv) + goto out3; + + rv = ASUS_LED_REGISTER(gled, dev); + if (rv) + goto out4; + + led_workqueue = create_singlethread_workqueue("led_workqueue"); + if (!led_workqueue) + goto out5; + + return 0; +out5: + rv = -ENOMEM; + ASUS_LED_UNREGISTER(gled); +out4: + ASUS_LED_UNREGISTER(pled); +out3: + ASUS_LED_UNREGISTER(rled); +out2: + ASUS_LED_UNREGISTER(tled); +out1: + ASUS_LED_UNREGISTER(mled); +out: + return rv; +} + +static int __init asus_laptop_init(void) +{ + struct device *dev; + int result; + + if (acpi_disabled) + return -ENODEV; + + result = acpi_bus_register_driver(&asus_hotk_driver); + if (result < 0) + return result; + + /* + * This is a bit of a kludge. We only want this module loaded + * for ASUS systems, but there's currently no way to probe the + * ACPI namespace for ASUS HIDs. So we just return failure if + * we didn't find one, which will cause the module to be + * unloaded. + */ + if (!asus_hotk_found) { + acpi_bus_unregister_driver(&asus_hotk_driver); + return -ENODEV; + } + + dev = acpi_get_physical_device(hotk->device->handle); + + if (!acpi_video_backlight_support()) { + result = asus_backlight_init(dev); + if (result) + goto fail_backlight; + } else + printk(ASUS_INFO "Brightness ignored, must be controlled by " + "ACPI video driver\n"); + + result = asus_led_init(dev); + if (result) + goto fail_led; + + /* Register platform stuff */ + result = platform_driver_register(&asuspf_driver); + if (result) + goto fail_platform_driver; + + asuspf_device = platform_device_alloc(ASUS_HOTK_FILE, -1); + if (!asuspf_device) { + result = -ENOMEM; + goto fail_platform_device1; + } + + result = platform_device_add(asuspf_device); + if (result) + goto fail_platform_device2; + + result = sysfs_create_group(&asuspf_device->dev.kobj, + &asuspf_attribute_group); + if (result) + goto fail_sysfs; + + return 0; + + fail_sysfs: + platform_device_del(asuspf_device); + + fail_platform_device2: + platform_device_put(asuspf_device); + + fail_platform_device1: + platform_driver_unregister(&asuspf_driver); + + fail_platform_driver: + asus_led_exit(); + + fail_led: + asus_backlight_exit(); + + fail_backlight: + + return result; +} + +module_init(asus_laptop_init); +module_exit(asus_laptop_exit); diff --git a/drivers/platform/x86/asus_acpi.c b/drivers/platform/x86/asus_acpi.c new file mode 100644 index 000000000000..1e74988c7b2d --- /dev/null +++ b/drivers/platform/x86/asus_acpi.c @@ -0,0 +1,1460 @@ +/* + * asus_acpi.c - Asus Laptop ACPI Extras + * + * + * Copyright (C) 2002-2005 Julien Lerouge, 2003-2006 Karol Kozimor + * + * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * + * The development page for this driver is located at + * http://sourceforge.net/projects/acpi4asus/ + * + * Credits: + * Pontus Fuchs - Helper functions, cleanup + * Johann Wiesner - Small compile fixes + * John Belmonte - ACPI code for Toshiba laptop was a good starting point. + * �ic Burghard - LED display support for W1N + * + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/types.h> +#include <linux/proc_fs.h> +#include <linux/backlight.h> +#include <acpi/acpi_drivers.h> +#include <acpi/acpi_bus.h> +#include <asm/uaccess.h> + +#define ASUS_ACPI_VERSION "0.30" + +#define PROC_ASUS "asus" /* The directory */ +#define PROC_MLED "mled" +#define PROC_WLED "wled" +#define PROC_TLED "tled" +#define PROC_BT "bluetooth" +#define PROC_LEDD "ledd" +#define PROC_INFO "info" +#define PROC_LCD "lcd" +#define PROC_BRN "brn" +#define PROC_DISP "disp" + +#define ACPI_HOTK_NAME "Asus Laptop ACPI Extras Driver" +#define ACPI_HOTK_CLASS "hotkey" +#define ACPI_HOTK_DEVICE_NAME "Hotkey" + +/* + * Some events we use, same for all Asus + */ +#define BR_UP 0x10 +#define BR_DOWN 0x20 + +/* + * Flags for hotk status + */ +#define MLED_ON 0x01 /* Mail LED */ +#define WLED_ON 0x02 /* Wireless LED */ +#define TLED_ON 0x04 /* Touchpad LED */ +#define BT_ON 0x08 /* Internal Bluetooth */ + +MODULE_AUTHOR("Julien Lerouge, Karol Kozimor"); +MODULE_DESCRIPTION(ACPI_HOTK_NAME); +MODULE_LICENSE("GPL"); + +static uid_t asus_uid; +static gid_t asus_gid; +module_param(asus_uid, uint, 0); +MODULE_PARM_DESC(asus_uid, "UID for entries in /proc/acpi/asus"); +module_param(asus_gid, uint, 0); +MODULE_PARM_DESC(asus_gid, "GID for entries in /proc/acpi/asus"); + +/* For each model, all features implemented, + * those marked with R are relative to HOTK, A for absolute */ +struct model_data { + char *name; /* name of the laptop________________A */ + char *mt_mled; /* method to handle mled_____________R */ + char *mled_status; /* node to handle mled reading_______A */ + char *mt_wled; /* method to handle wled_____________R */ + char *wled_status; /* node to handle wled reading_______A */ + char *mt_tled; /* method to handle tled_____________R */ + char *tled_status; /* node to handle tled reading_______A */ + char *mt_ledd; /* method to handle LED display______R */ + char *mt_bt_switch; /* method to switch Bluetooth on/off_R */ + char *bt_status; /* no model currently supports this__? */ + char *mt_lcd_switch; /* method to turn LCD on/off_________A */ + char *lcd_status; /* node to read LCD panel state______A */ + char *brightness_up; /* method to set brightness up_______A */ + char *brightness_down; /* method to set brightness down ____A */ + char *brightness_set; /* method to set absolute brightness_R */ + char *brightness_get; /* method to get absolute brightness_R */ + char *brightness_status;/* node to get brightness____________A */ + char *display_set; /* method to set video output________R */ + char *display_get; /* method to get video output________R */ +}; + +/* + * This is the main structure, we can use it to store anything interesting + * about the hotk device + */ +struct asus_hotk { + struct acpi_device *device; /* the device we are in */ + acpi_handle handle; /* the handle of the hotk device */ + char status; /* status of the hotk, for LEDs */ + u32 ledd_status; /* status of the LED display */ + struct model_data *methods; /* methods available on the laptop */ + u8 brightness; /* brightness level */ + enum { + A1x = 0, /* A1340D, A1300F */ + A2x, /* A2500H */ + A4G, /* A4700G */ + D1x, /* D1 */ + L2D, /* L2000D */ + L3C, /* L3800C */ + L3D, /* L3400D */ + L3H, /* L3H, L2000E, L5D */ + L4R, /* L4500R */ + L5x, /* L5800C */ + L8L, /* L8400L */ + M1A, /* M1300A */ + M2E, /* M2400E, L4400L */ + M6N, /* M6800N, W3400N */ + M6R, /* M6700R, A3000G */ + P30, /* Samsung P30 */ + S1x, /* S1300A, but also L1400B and M2400A (L84F) */ + S2x, /* S200 (J1 reported), Victor MP-XP7210 */ + W1N, /* W1000N */ + W5A, /* W5A */ + W3V, /* W3030V */ + xxN, /* M2400N, M3700N, M5200N, M6800N, + S1300N, S5200N*/ + A4S, /* Z81sp */ + F3Sa, /* (Centrino) */ + END_MODEL + } model; /* Models currently supported */ + u16 event_count[128]; /* Count for each event TODO make this better */ +}; + +/* Here we go */ +#define A1x_PREFIX "\\_SB.PCI0.ISA.EC0." +#define L3C_PREFIX "\\_SB.PCI0.PX40.ECD0." +#define M1A_PREFIX "\\_SB.PCI0.PX40.EC0." +#define P30_PREFIX "\\_SB.PCI0.LPCB.EC0." +#define S1x_PREFIX "\\_SB.PCI0.PX40." +#define S2x_PREFIX A1x_PREFIX +#define xxN_PREFIX "\\_SB.PCI0.SBRG.EC0." + +static struct model_data model_conf[END_MODEL] = { + /* + * TODO I have seen a SWBX and AIBX method on some models, like L1400B, + * it seems to be a kind of switch, but what for ? + */ + + { + .name = "A1x", + .mt_mled = "MLED", + .mled_status = "\\MAIL", + .mt_lcd_switch = A1x_PREFIX "_Q10", + .lcd_status = "\\BKLI", + .brightness_up = A1x_PREFIX "_Q0E", + .brightness_down = A1x_PREFIX "_Q0F"}, + + { + .name = "A2x", + .mt_mled = "MLED", + .mt_wled = "WLED", + .wled_status = "\\SG66", + .mt_lcd_switch = "\\Q10", + .lcd_status = "\\BAOF", + .brightness_set = "SPLV", + .brightness_get = "GPLV", + .display_set = "SDSP", + .display_get = "\\INFB"}, + + { + .name = "A4G", + .mt_mled = "MLED", +/* WLED present, but not controlled by ACPI */ + .mt_lcd_switch = xxN_PREFIX "_Q10", + .brightness_set = "SPLV", + .brightness_get = "GPLV", + .display_set = "SDSP", + .display_get = "\\ADVG"}, + + { + .name = "D1x", + .mt_mled = "MLED", + .mt_lcd_switch = "\\Q0D", + .lcd_status = "\\GP11", + .brightness_up = "\\Q0C", + .brightness_down = "\\Q0B", + .brightness_status = "\\BLVL", + .display_set = "SDSP", + .display_get = "\\INFB"}, + + { + .name = "L2D", + .mt_mled = "MLED", + .mled_status = "\\SGP6", + .mt_wled = "WLED", + .wled_status = "\\RCP3", + .mt_lcd_switch = "\\Q10", + .lcd_status = "\\SGP0", + .brightness_up = "\\Q0E", + .brightness_down = "\\Q0F", + .display_set = "SDSP", + .display_get = "\\INFB"}, + + { + .name = "L3C", + .mt_mled = "MLED", + .mt_wled = "WLED", + .mt_lcd_switch = L3C_PREFIX "_Q10", + .lcd_status = "\\GL32", + .brightness_set = "SPLV", + .brightness_get = "GPLV", + .display_set = "SDSP", + .display_get = "\\_SB.PCI0.PCI1.VGAC.NMAP"}, + + { + .name = "L3D", + .mt_mled = "MLED", + .mled_status = "\\MALD", + .mt_wled = "WLED", + .mt_lcd_switch = "\\Q10", + .lcd_status = "\\BKLG", + .brightness_set = "SPLV", + .brightness_get = "GPLV", + .display_set = "SDSP", + .display_get = "\\INFB"}, + + { + .name = "L3H", + .mt_mled = "MLED", + .mt_wled = "WLED", + .mt_lcd_switch = "EHK", + .lcd_status = "\\_SB.PCI0.PM.PBC", + .brightness_set = "SPLV", + .brightness_get = "GPLV", + .display_set = "SDSP", + .display_get = "\\INFB"}, + + { + .name = "L4R", + .mt_mled = "MLED", + .mt_wled = "WLED", + .wled_status = "\\_SB.PCI0.SBRG.SG13", + .mt_lcd_switch = xxN_PREFIX "_Q10", + .lcd_status = "\\_SB.PCI0.SBSM.SEO4", + .brightness_set = "SPLV", + .brightness_get = "GPLV", + .display_set = "SDSP", + .display_get = "\\_SB.PCI0.P0P1.VGA.GETD"}, + + { + .name = "L5x", + .mt_mled = "MLED", +/* WLED present, but not controlled by ACPI */ + .mt_tled = "TLED", + .mt_lcd_switch = "\\Q0D", + .lcd_status = "\\BAOF", + .brightness_set = "SPLV", + .brightness_get = "GPLV", + .display_set = "SDSP", + .display_get = "\\INFB"}, + + { + .name = "L8L" +/* No features, but at least support the hotkeys */ + }, + + { + .name = "M1A", + .mt_mled = "MLED", + .mt_lcd_switch = M1A_PREFIX "Q10", + .lcd_status = "\\PNOF", + .brightness_up = M1A_PREFIX "Q0E", + .brightness_down = M1A_PREFIX "Q0F", + .brightness_status = "\\BRIT", + .display_set = "SDSP", + .display_get = "\\INFB"}, + + { + .name = "M2E", + .mt_mled = "MLED", + .mt_wled = "WLED", + .mt_lcd_switch = "\\Q10", + .lcd_status = "\\GP06", + .brightness_set = "SPLV", + .brightness_get = "GPLV", + .display_set = "SDSP", + .display_get = "\\INFB"}, + + { + .name = "M6N", + .mt_mled = "MLED", + .mt_wled = "WLED", + .wled_status = "\\_SB.PCI0.SBRG.SG13", + .mt_lcd_switch = xxN_PREFIX "_Q10", + .lcd_status = "\\_SB.BKLT", + .brightness_set = "SPLV", + .brightness_get = "GPLV", + .display_set = "SDSP", + .display_get = "\\SSTE"}, + + { + .name = "M6R", + .mt_mled = "MLED", + .mt_wled = "WLED", + .mt_lcd_switch = xxN_PREFIX "_Q10", + .lcd_status = "\\_SB.PCI0.SBSM.SEO4", + .brightness_set = "SPLV", + .brightness_get = "GPLV", + .display_set = "SDSP", + .display_get = "\\_SB.PCI0.P0P1.VGA.GETD"}, + + { + .name = "P30", + .mt_wled = "WLED", + .mt_lcd_switch = P30_PREFIX "_Q0E", + .lcd_status = "\\BKLT", + .brightness_up = P30_PREFIX "_Q68", + .brightness_down = P30_PREFIX "_Q69", + .brightness_get = "GPLV", + .display_set = "SDSP", + .display_get = "\\DNXT"}, + + { + .name = "S1x", + .mt_mled = "MLED", + .mled_status = "\\EMLE", + .mt_wled = "WLED", + .mt_lcd_switch = S1x_PREFIX "Q10", + .lcd_status = "\\PNOF", + .brightness_set = "SPLV", + .brightness_get = "GPLV"}, + + { + .name = "S2x", + .mt_mled = "MLED", + .mled_status = "\\MAIL", + .mt_lcd_switch = S2x_PREFIX "_Q10", + .lcd_status = "\\BKLI", + .brightness_up = S2x_PREFIX "_Q0B", + .brightness_down = S2x_PREFIX "_Q0A"}, + + { + .name = "W1N", + .mt_mled = "MLED", + .mt_wled = "WLED", + .mt_ledd = "SLCM", + .mt_lcd_switch = xxN_PREFIX "_Q10", + .lcd_status = "\\BKLT", + .brightness_set = "SPLV", + .brightness_get = "GPLV", + .display_set = "SDSP", + .display_get = "\\ADVG"}, + + { + .name = "W5A", + .mt_bt_switch = "BLED", + .mt_wled = "WLED", + .mt_lcd_switch = xxN_PREFIX "_Q10", + .brightness_set = "SPLV", + .brightness_get = "GPLV", + .display_set = "SDSP", + .display_get = "\\ADVG"}, + + { + .name = "W3V", + .mt_mled = "MLED", + .mt_wled = "WLED", + .mt_lcd_switch = xxN_PREFIX "_Q10", + .lcd_status = "\\BKLT", + .brightness_set = "SPLV", + .brightness_get = "GPLV", + .display_set = "SDSP", + .display_get = "\\INFB"}, + + { + .name = "xxN", + .mt_mled = "MLED", +/* WLED present, but not controlled by ACPI */ + .mt_lcd_switch = xxN_PREFIX "_Q10", + .lcd_status = "\\BKLT", + .brightness_set = "SPLV", + .brightness_get = "GPLV", + .display_set = "SDSP", + .display_get = "\\ADVG"}, + + { + .name = "A4S", + .brightness_set = "SPLV", + .brightness_get = "GPLV", + .mt_bt_switch = "BLED", + .mt_wled = "WLED" + }, + + { + .name = "F3Sa", + .mt_bt_switch = "BLED", + .mt_wled = "WLED", + .mt_mled = "MLED", + .brightness_get = "GPLV", + .brightness_set = "SPLV", + .mt_lcd_switch = "\\_SB.PCI0.SBRG.EC0._Q10", + .lcd_status = "\\_SB.PCI0.SBRG.EC0.RPIN", + .display_get = "\\ADVG", + .display_set = "SDSP", + }, + +}; + +/* procdir we use */ +static struct proc_dir_entry *asus_proc_dir; + +static struct backlight_device *asus_backlight_device; + +/* + * This header is made available to allow proper configuration given model, + * revision number , ... this info cannot go in struct asus_hotk because it is + * available before the hotk + */ +static struct acpi_table_header *asus_info; + +/* The actual device the driver binds to */ +static struct asus_hotk *hotk; + +/* + * The hotkey driver and autoloading declaration + */ +static int asus_hotk_add(struct acpi_device *device); +static int asus_hotk_remove(struct acpi_device *device, int type); +static const struct acpi_device_id asus_device_ids[] = { + {"ATK0100", 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, asus_device_ids); + +static struct acpi_driver asus_hotk_driver = { + .name = "asus_acpi", + .class = ACPI_HOTK_CLASS, + .ids = asus_device_ids, + .ops = { + .add = asus_hotk_add, + .remove = asus_hotk_remove, + }, +}; + +/* + * This function evaluates an ACPI method, given an int as parameter, the + * method is searched within the scope of the handle, can be NULL. The output + * of the method is written is output, which can also be NULL + * + * returns 1 if write is successful, 0 else. + */ +static int write_acpi_int(acpi_handle handle, const char *method, int val, + struct acpi_buffer *output) +{ + struct acpi_object_list params; /* list of input parameters (int) */ + union acpi_object in_obj; /* the only param we use */ + acpi_status status; + + params.count = 1; + params.pointer = &in_obj; + in_obj.type = ACPI_TYPE_INTEGER; + in_obj.integer.value = val; + + status = acpi_evaluate_object(handle, (char *)method, ¶ms, output); + return (status == AE_OK); +} + +static int read_acpi_int(acpi_handle handle, const char *method, int *val) +{ + struct acpi_buffer output; + union acpi_object out_obj; + acpi_status status; + + output.length = sizeof(out_obj); + output.pointer = &out_obj; + + status = acpi_evaluate_object(handle, (char *)method, NULL, &output); + *val = out_obj.integer.value; + return (status == AE_OK) && (out_obj.type == ACPI_TYPE_INTEGER); +} + +/* + * We write our info in page, we begin at offset off and cannot write more + * than count bytes. We set eof to 1 if we handle those 2 values. We return the + * number of bytes written in page + */ +static int +proc_read_info(char *page, char **start, off_t off, int count, int *eof, + void *data) +{ + int len = 0; + int temp; + char buf[16]; /* enough for all info */ + /* + * We use the easy way, we don't care of off and count, + * so we don't set eof to 1 + */ + + len += sprintf(page, ACPI_HOTK_NAME " " ASUS_ACPI_VERSION "\n"); + len += sprintf(page + len, "Model reference : %s\n", + hotk->methods->name); + /* + * The SFUN method probably allows the original driver to get the list + * of features supported by a given model. For now, 0x0100 or 0x0800 + * bit signifies that the laptop is equipped with a Wi-Fi MiniPCI card. + * The significance of others is yet to be found. + */ + if (read_acpi_int(hotk->handle, "SFUN", &temp)) + len += + sprintf(page + len, "SFUN value : 0x%04x\n", temp); + /* + * Another value for userspace: the ASYM method returns 0x02 for + * battery low and 0x04 for battery critical, its readings tend to be + * more accurate than those provided by _BST. + * Note: since not all the laptops provide this method, errors are + * silently ignored. + */ + if (read_acpi_int(hotk->handle, "ASYM", &temp)) + len += + sprintf(page + len, "ASYM value : 0x%04x\n", temp); + if (asus_info) { + snprintf(buf, 16, "%d", asus_info->length); + len += sprintf(page + len, "DSDT length : %s\n", buf); + snprintf(buf, 16, "%d", asus_info->checksum); + len += sprintf(page + len, "DSDT checksum : %s\n", buf); + snprintf(buf, 16, "%d", asus_info->revision); + len += sprintf(page + len, "DSDT revision : %s\n", buf); + snprintf(buf, 7, "%s", asus_info->oem_id); + len += sprintf(page + len, "OEM id : %s\n", buf); + snprintf(buf, 9, "%s", asus_info->oem_table_id); + len += sprintf(page + len, "OEM table id : %s\n", buf); + snprintf(buf, 16, "%x", asus_info->oem_revision); + len += sprintf(page + len, "OEM revision : 0x%s\n", buf); + snprintf(buf, 5, "%s", asus_info->asl_compiler_id); + len += sprintf(page + len, "ASL comp vendor id : %s\n", buf); + snprintf(buf, 16, "%x", asus_info->asl_compiler_revision); + len += sprintf(page + len, "ASL comp revision : 0x%s\n", buf); + } + + return len; +} + +/* + * /proc handlers + * We write our info in page, we begin at offset off and cannot write more + * than count bytes. We set eof to 1 if we handle those 2 values. We return the + * number of bytes written in page + */ + +/* Generic LED functions */ +static int read_led(const char *ledname, int ledmask) +{ + if (ledname) { + int led_status; + + if (read_acpi_int(NULL, ledname, &led_status)) + return led_status; + else + printk(KERN_WARNING "Asus ACPI: Error reading LED " + "status\n"); + } + return (hotk->status & ledmask) ? 1 : 0; +} + +static int parse_arg(const char __user *buf, unsigned long count, int *val) +{ + char s[32]; + if (!count) + return 0; + if (count > 31) + return -EINVAL; + if (copy_from_user(s, buf, count)) + return -EFAULT; + s[count] = 0; + if (sscanf(s, "%i", val) != 1) + return -EINVAL; + return count; +} + +/* FIXME: kill extraneous args so it can be called independently */ +static int +write_led(const char __user *buffer, unsigned long count, + char *ledname, int ledmask, int invert) +{ + int rv, value; + int led_out = 0; + + rv = parse_arg(buffer, count, &value); + if (rv > 0) + led_out = value ? 1 : 0; + + hotk->status = + (led_out) ? (hotk->status | ledmask) : (hotk->status & ~ledmask); + + if (invert) /* invert target value */ + led_out = !led_out; + + if (!write_acpi_int(hotk->handle, ledname, led_out, NULL)) + printk(KERN_WARNING "Asus ACPI: LED (%s) write failed\n", + ledname); + + return rv; +} + +/* + * Proc handlers for MLED + */ +static int +proc_read_mled(char *page, char **start, off_t off, int count, int *eof, + void *data) +{ + return sprintf(page, "%d\n", + read_led(hotk->methods->mled_status, MLED_ON)); +} + +static int +proc_write_mled(struct file *file, const char __user *buffer, + unsigned long count, void *data) +{ + return write_led(buffer, count, hotk->methods->mt_mled, MLED_ON, 1); +} + +/* + * Proc handlers for LED display + */ +static int +proc_read_ledd(char *page, char **start, off_t off, int count, int *eof, + void *data) +{ + return sprintf(page, "0x%08x\n", hotk->ledd_status); +} + +static int +proc_write_ledd(struct file *file, const char __user *buffer, + unsigned long count, void *data) +{ + int rv, value; + + rv = parse_arg(buffer, count, &value); + if (rv > 0) { + if (!write_acpi_int + (hotk->handle, hotk->methods->mt_ledd, value, NULL)) + printk(KERN_WARNING + "Asus ACPI: LED display write failed\n"); + else + hotk->ledd_status = (u32) value; + } + return rv; +} + +/* + * Proc handlers for WLED + */ +static int +proc_read_wled(char *page, char **start, off_t off, int count, int *eof, + void *data) +{ + return sprintf(page, "%d\n", + read_led(hotk->methods->wled_status, WLED_ON)); +} + +static int +proc_write_wled(struct file *file, const char __user *buffer, + unsigned long count, void *data) +{ + return write_led(buffer, count, hotk->methods->mt_wled, WLED_ON, 0); +} + +/* + * Proc handlers for Bluetooth + */ +static int +proc_read_bluetooth(char *page, char **start, off_t off, int count, int *eof, + void *data) +{ + return sprintf(page, "%d\n", read_led(hotk->methods->bt_status, BT_ON)); +} + +static int +proc_write_bluetooth(struct file *file, const char __user *buffer, + unsigned long count, void *data) +{ + /* Note: mt_bt_switch controls both internal Bluetooth adapter's + presence and its LED */ + return write_led(buffer, count, hotk->methods->mt_bt_switch, BT_ON, 0); +} + +/* + * Proc handlers for TLED + */ +static int +proc_read_tled(char *page, char **start, off_t off, int count, int *eof, + void *data) +{ + return sprintf(page, "%d\n", + read_led(hotk->methods->tled_status, TLED_ON)); +} + +static int +proc_write_tled(struct file *file, const char __user *buffer, + unsigned long count, void *data) +{ + return write_led(buffer, count, hotk->methods->mt_tled, TLED_ON, 0); +} + +static int get_lcd_state(void) +{ + int lcd = 0; + + if (hotk->model == L3H) { + /* L3H and the like have to be handled differently */ + acpi_status status = 0; + struct acpi_object_list input; + union acpi_object mt_params[2]; + struct acpi_buffer output; + union acpi_object out_obj; + + input.count = 2; + input.pointer = mt_params; + /* Note: the following values are partly guessed up, but + otherwise they seem to work */ + mt_params[0].type = ACPI_TYPE_INTEGER; + mt_params[0].integer.value = 0x02; + mt_params[1].type = ACPI_TYPE_INTEGER; + mt_params[1].integer.value = 0x02; + + output.length = sizeof(out_obj); + output.pointer = &out_obj; + + status = + acpi_evaluate_object(NULL, hotk->methods->lcd_status, + &input, &output); + if (status != AE_OK) + return -1; + if (out_obj.type == ACPI_TYPE_INTEGER) + /* That's what the AML code does */ + lcd = out_obj.integer.value >> 8; + } else if (hotk->model == F3Sa) { + unsigned long long tmp; + union acpi_object param; + struct acpi_object_list input; + acpi_status status; + + /* Read pin 11 */ + param.type = ACPI_TYPE_INTEGER; + param.integer.value = 0x11; + input.count = 1; + input.pointer = ¶m; + + status = acpi_evaluate_integer(NULL, hotk->methods->lcd_status, + &input, &tmp); + if (status != AE_OK) + return -1; + + lcd = tmp; + } else { + /* We don't have to check anything if we are here */ + if (!read_acpi_int(NULL, hotk->methods->lcd_status, &lcd)) + printk(KERN_WARNING + "Asus ACPI: Error reading LCD status\n"); + + if (hotk->model == L2D) + lcd = ~lcd; + } + + return (lcd & 1); +} + +static int set_lcd_state(int value) +{ + int lcd = 0; + acpi_status status = 0; + + lcd = value ? 1 : 0; + if (lcd != get_lcd_state()) { + /* switch */ + if (hotk->model != L3H) { + status = + acpi_evaluate_object(NULL, + hotk->methods->mt_lcd_switch, + NULL, NULL); + } else { + /* L3H and the like must be handled differently */ + if (!write_acpi_int + (hotk->handle, hotk->methods->mt_lcd_switch, 0x07, + NULL)) + status = AE_ERROR; + /* L3H's AML executes EHK (0x07) upon Fn+F7 keypress, + the exact behaviour is simulated here */ + } + if (ACPI_FAILURE(status)) + printk(KERN_WARNING "Asus ACPI: Error switching LCD\n"); + } + return 0; + +} + +static int +proc_read_lcd(char *page, char **start, off_t off, int count, int *eof, + void *data) +{ + return sprintf(page, "%d\n", get_lcd_state()); +} + +static int +proc_write_lcd(struct file *file, const char __user *buffer, + unsigned long count, void *data) +{ + int rv, value; + + rv = parse_arg(buffer, count, &value); + if (rv > 0) + set_lcd_state(value); + return rv; +} + +static int read_brightness(struct backlight_device *bd) +{ + int value; + + if (hotk->methods->brightness_get) { /* SPLV/GPLV laptop */ + if (!read_acpi_int(hotk->handle, hotk->methods->brightness_get, + &value)) + printk(KERN_WARNING + "Asus ACPI: Error reading brightness\n"); + } else if (hotk->methods->brightness_status) { /* For D1 for example */ + if (!read_acpi_int(NULL, hotk->methods->brightness_status, + &value)) + printk(KERN_WARNING + "Asus ACPI: Error reading brightness\n"); + } else /* No GPLV method */ + value = hotk->brightness; + return value; +} + +/* + * Change the brightness level + */ +static int set_brightness(int value) +{ + acpi_status status = 0; + int ret = 0; + + /* SPLV laptop */ + if (hotk->methods->brightness_set) { + if (!write_acpi_int(hotk->handle, hotk->methods->brightness_set, + value, NULL)) + printk(KERN_WARNING + "Asus ACPI: Error changing brightness\n"); + ret = -EIO; + goto out; + } + + /* No SPLV method if we are here, act as appropriate */ + value -= read_brightness(NULL); + while (value != 0) { + status = acpi_evaluate_object(NULL, (value > 0) ? + hotk->methods->brightness_up : + hotk->methods->brightness_down, + NULL, NULL); + (value > 0) ? value-- : value++; + if (ACPI_FAILURE(status)) + printk(KERN_WARNING + "Asus ACPI: Error changing brightness\n"); + ret = -EIO; + } +out: + return ret; +} + +static int set_brightness_status(struct backlight_device *bd) +{ + return set_brightness(bd->props.brightness); +} + +static int +proc_read_brn(char *page, char **start, off_t off, int count, int *eof, + void *data) +{ + return sprintf(page, "%d\n", read_brightness(NULL)); +} + +static int +proc_write_brn(struct file *file, const char __user *buffer, + unsigned long count, void *data) +{ + int rv, value; + + rv = parse_arg(buffer, count, &value); + if (rv > 0) { + value = (0 < value) ? ((15 < value) ? 15 : value) : 0; + /* 0 <= value <= 15 */ + set_brightness(value); + } + return rv; +} + +static void set_display(int value) +{ + /* no sanity check needed for now */ + if (!write_acpi_int(hotk->handle, hotk->methods->display_set, + value, NULL)) + printk(KERN_WARNING "Asus ACPI: Error setting display\n"); + return; +} + +/* + * Now, *this* one could be more user-friendly, but so far, no-one has + * complained. The significance of bits is the same as in proc_write_disp() + */ +static int +proc_read_disp(char *page, char **start, off_t off, int count, int *eof, + void *data) +{ + int value = 0; + + if (!read_acpi_int(hotk->handle, hotk->methods->display_get, &value)) + printk(KERN_WARNING + "Asus ACPI: Error reading display status\n"); + value &= 0x07; /* needed for some models, shouldn't hurt others */ + return sprintf(page, "%d\n", value); +} + +/* + * Experimental support for display switching. As of now: 1 should activate + * the LCD output, 2 should do for CRT, and 4 for TV-Out. Any combination + * (bitwise) of these will suffice. I never actually tested 3 displays hooked + * up simultaneously, so be warned. See the acpi4asus README for more info. + */ +static int +proc_write_disp(struct file *file, const char __user *buffer, + unsigned long count, void *data) +{ + int rv, value; + + rv = parse_arg(buffer, count, &value); + if (rv > 0) + set_display(value); + return rv; +} + +typedef int (proc_readfunc) (char *page, char **start, off_t off, int count, + int *eof, void *data); +typedef int (proc_writefunc) (struct file *file, const char __user *buffer, + unsigned long count, void *data); + +static int +asus_proc_add(char *name, proc_writefunc *writefunc, + proc_readfunc *readfunc, mode_t mode, + struct acpi_device *device) +{ + struct proc_dir_entry *proc = + create_proc_entry(name, mode, acpi_device_dir(device)); + if (!proc) { + printk(KERN_WARNING " Unable to create %s fs entry\n", name); + return -1; + } + proc->write_proc = writefunc; + proc->read_proc = readfunc; + proc->data = acpi_driver_data(device); + proc->owner = THIS_MODULE; + proc->uid = asus_uid; + proc->gid = asus_gid; + return 0; +} + +static int asus_hotk_add_fs(struct acpi_device *device) +{ + struct proc_dir_entry *proc; + mode_t mode; + + /* + * If parameter uid or gid is not changed, keep the default setting for + * our proc entries (-rw-rw-rw-) else, it means we care about security, + * and then set to -rw-rw---- + */ + + if ((asus_uid == 0) && (asus_gid == 0)) { + mode = S_IFREG | S_IRUGO | S_IWUGO; + } else { + mode = S_IFREG | S_IRUSR | S_IRGRP | S_IWUSR | S_IWGRP; + printk(KERN_WARNING " asus_uid and asus_gid parameters are " + "deprecated, use chown and chmod instead!\n"); + } + + acpi_device_dir(device) = asus_proc_dir; + if (!acpi_device_dir(device)) + return -ENODEV; + + proc = create_proc_entry(PROC_INFO, mode, acpi_device_dir(device)); + if (proc) { + proc->read_proc = proc_read_info; + proc->data = acpi_driver_data(device); + proc->owner = THIS_MODULE; + proc->uid = asus_uid; + proc->gid = asus_gid; + } else { + printk(KERN_WARNING " Unable to create " PROC_INFO + " fs entry\n"); + } + + if (hotk->methods->mt_wled) { + asus_proc_add(PROC_WLED, &proc_write_wled, &proc_read_wled, + mode, device); + } + + if (hotk->methods->mt_ledd) { + asus_proc_add(PROC_LEDD, &proc_write_ledd, &proc_read_ledd, + mode, device); + } + + if (hotk->methods->mt_mled) { + asus_proc_add(PROC_MLED, &proc_write_mled, &proc_read_mled, + mode, device); + } + + if (hotk->methods->mt_tled) { + asus_proc_add(PROC_TLED, &proc_write_tled, &proc_read_tled, + mode, device); + } + + if (hotk->methods->mt_bt_switch) { + asus_proc_add(PROC_BT, &proc_write_bluetooth, + &proc_read_bluetooth, mode, device); + } + + /* + * We need both read node and write method as LCD switch is also + * accessible from the keyboard + */ + if (hotk->methods->mt_lcd_switch && hotk->methods->lcd_status) { + asus_proc_add(PROC_LCD, &proc_write_lcd, &proc_read_lcd, mode, + device); + } + + if ((hotk->methods->brightness_up && hotk->methods->brightness_down) || + (hotk->methods->brightness_get && hotk->methods->brightness_set)) { + asus_proc_add(PROC_BRN, &proc_write_brn, &proc_read_brn, mode, + device); + } + + if (hotk->methods->display_set) { + asus_proc_add(PROC_DISP, &proc_write_disp, &proc_read_disp, + mode, device); + } + + return 0; +} + +static int asus_hotk_remove_fs(struct acpi_device *device) +{ + if (acpi_device_dir(device)) { + remove_proc_entry(PROC_INFO, acpi_device_dir(device)); + if (hotk->methods->mt_wled) + remove_proc_entry(PROC_WLED, acpi_device_dir(device)); + if (hotk->methods->mt_mled) + remove_proc_entry(PROC_MLED, acpi_device_dir(device)); + if (hotk->methods->mt_tled) + remove_proc_entry(PROC_TLED, acpi_device_dir(device)); + if (hotk->methods->mt_ledd) + remove_proc_entry(PROC_LEDD, acpi_device_dir(device)); + if (hotk->methods->mt_bt_switch) + remove_proc_entry(PROC_BT, acpi_device_dir(device)); + if (hotk->methods->mt_lcd_switch && hotk->methods->lcd_status) + remove_proc_entry(PROC_LCD, acpi_device_dir(device)); + if ((hotk->methods->brightness_up + && hotk->methods->brightness_down) + || (hotk->methods->brightness_get + && hotk->methods->brightness_set)) + remove_proc_entry(PROC_BRN, acpi_device_dir(device)); + if (hotk->methods->display_set) + remove_proc_entry(PROC_DISP, acpi_device_dir(device)); + } + return 0; +} + +static void asus_hotk_notify(acpi_handle handle, u32 event, void *data) +{ + /* TODO Find a better way to handle events count. */ + if (!hotk) + return; + + if ((event & ~((u32) BR_UP)) < 16) + hotk->brightness = (event & ~((u32) BR_UP)); + else if ((event & ~((u32) BR_DOWN)) < 16) + hotk->brightness = (event & ~((u32) BR_DOWN)); + + acpi_bus_generate_proc_event(hotk->device, event, + hotk->event_count[event % 128]++); + + return; +} + +/* + * Match the model string to the list of supported models. Return END_MODEL if + * no match or model is NULL. + */ +static int asus_model_match(char *model) +{ + if (model == NULL) + return END_MODEL; + + if (strncmp(model, "L3D", 3) == 0) + return L3D; + else if (strncmp(model, "L2E", 3) == 0 || + strncmp(model, "L3H", 3) == 0 || strncmp(model, "L5D", 3) == 0) + return L3H; + else if (strncmp(model, "L3", 2) == 0 || strncmp(model, "L2B", 3) == 0) + return L3C; + else if (strncmp(model, "L8L", 3) == 0) + return L8L; + else if (strncmp(model, "L4R", 3) == 0) + return L4R; + else if (strncmp(model, "M6N", 3) == 0 || strncmp(model, "W3N", 3) == 0) + return M6N; + else if (strncmp(model, "M6R", 3) == 0 || strncmp(model, "A3G", 3) == 0) + return M6R; + else if (strncmp(model, "M2N", 3) == 0 || + strncmp(model, "M3N", 3) == 0 || + strncmp(model, "M5N", 3) == 0 || + strncmp(model, "M6N", 3) == 0 || + strncmp(model, "S1N", 3) == 0 || + strncmp(model, "S5N", 3) == 0 || strncmp(model, "W1N", 3) == 0) + return xxN; + else if (strncmp(model, "M1", 2) == 0) + return M1A; + else if (strncmp(model, "M2", 2) == 0 || strncmp(model, "L4E", 3) == 0) + return M2E; + else if (strncmp(model, "L2", 2) == 0) + return L2D; + else if (strncmp(model, "L8", 2) == 0) + return S1x; + else if (strncmp(model, "D1", 2) == 0) + return D1x; + else if (strncmp(model, "A1", 2) == 0) + return A1x; + else if (strncmp(model, "A2", 2) == 0) + return A2x; + else if (strncmp(model, "J1", 2) == 0) + return S2x; + else if (strncmp(model, "L5", 2) == 0) + return L5x; + else if (strncmp(model, "A4G", 3) == 0) + return A4G; + else if (strncmp(model, "W1N", 3) == 0) + return W1N; + else if (strncmp(model, "W3V", 3) == 0) + return W3V; + else if (strncmp(model, "W5A", 3) == 0) + return W5A; + else if (strncmp(model, "A4S", 3) == 0) + return A4S; + else if (strncmp(model, "F3Sa", 4) == 0) + return F3Sa; + else + return END_MODEL; +} + +/* + * This function is used to initialize the hotk with right values. In this + * method, we can make all the detection we want, and modify the hotk struct + */ +static int asus_hotk_get_info(void) +{ + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *model = NULL; + int bsts_result; + char *string = NULL; + acpi_status status; + + /* + * Get DSDT headers early enough to allow for differentiating between + * models, but late enough to allow acpi_bus_register_driver() to fail + * before doing anything ACPI-specific. Should we encounter a machine, + * which needs special handling (i.e. its hotkey device has a different + * HID), this bit will be moved. A global variable asus_info contains + * the DSDT header. + */ + status = acpi_get_table(ACPI_SIG_DSDT, 1, &asus_info); + if (ACPI_FAILURE(status)) + printk(KERN_WARNING " Couldn't get the DSDT table header\n"); + + /* We have to write 0 on init this far for all ASUS models */ + if (!write_acpi_int(hotk->handle, "INIT", 0, &buffer)) { + printk(KERN_ERR " Hotkey initialization failed\n"); + return -ENODEV; + } + + /* This needs to be called for some laptops to init properly */ + if (!read_acpi_int(hotk->handle, "BSTS", &bsts_result)) + printk(KERN_WARNING " Error calling BSTS\n"); + else if (bsts_result) + printk(KERN_NOTICE " BSTS called, 0x%02x returned\n", + bsts_result); + + /* + * Try to match the object returned by INIT to the specific model. + * Handle every possible object (or the lack of thereof) the DSDT + * writers might throw at us. When in trouble, we pass NULL to + * asus_model_match() and try something completely different. + */ + if (buffer.pointer) { + model = buffer.pointer; + switch (model->type) { + case ACPI_TYPE_STRING: + string = model->string.pointer; + break; + case ACPI_TYPE_BUFFER: + string = model->buffer.pointer; + break; + default: + kfree(model); + model = NULL; + break; + } + } + hotk->model = asus_model_match(string); + if (hotk->model == END_MODEL) { /* match failed */ + if (asus_info && + strncmp(asus_info->oem_table_id, "ODEM", 4) == 0) { + hotk->model = P30; + printk(KERN_NOTICE + " Samsung P30 detected, supported\n"); + } else { + hotk->model = M2E; + printk(KERN_NOTICE " unsupported model %s, trying " + "default values\n", string); + printk(KERN_NOTICE + " send /proc/acpi/dsdt to the developers\n"); + kfree(model); + return -ENODEV; + } + hotk->methods = &model_conf[hotk->model]; + return AE_OK; + } + hotk->methods = &model_conf[hotk->model]; + printk(KERN_NOTICE " %s model detected, supported\n", string); + + /* Sort of per-model blacklist */ + if (strncmp(string, "L2B", 3) == 0) + hotk->methods->lcd_status = NULL; + /* L2B is similar enough to L3C to use its settings, with this only + exception */ + else if (strncmp(string, "A3G", 3) == 0) + hotk->methods->lcd_status = "\\BLFG"; + /* A3G is like M6R */ + else if (strncmp(string, "S5N", 3) == 0 || + strncmp(string, "M5N", 3) == 0 || + strncmp(string, "W3N", 3) == 0) + hotk->methods->mt_mled = NULL; + /* S5N, M5N and W3N have no MLED */ + else if (strncmp(string, "L5D", 3) == 0) + hotk->methods->mt_wled = NULL; + /* L5D's WLED is not controlled by ACPI */ + else if (strncmp(string, "M2N", 3) == 0 || + strncmp(string, "W3V", 3) == 0 || + strncmp(string, "S1N", 3) == 0) + hotk->methods->mt_wled = "WLED"; + /* M2N, S1N and W3V have a usable WLED */ + else if (asus_info) { + if (strncmp(asus_info->oem_table_id, "L1", 2) == 0) + hotk->methods->mled_status = NULL; + /* S1300A reports L84F, but L1400B too, account for that */ + } + + kfree(model); + + return AE_OK; +} + +static int asus_hotk_check(void) +{ + int result = 0; + + result = acpi_bus_get_status(hotk->device); + if (result) + return result; + + if (hotk->device->status.present) { + result = asus_hotk_get_info(); + } else { + printk(KERN_ERR " Hotkey device not present, aborting\n"); + return -EINVAL; + } + + return result; +} + +static int asus_hotk_found; + +static int asus_hotk_add(struct acpi_device *device) +{ + acpi_status status = AE_OK; + int result; + + if (!device) + return -EINVAL; + + printk(KERN_NOTICE "Asus Laptop ACPI Extras version %s\n", + ASUS_ACPI_VERSION); + + hotk = kzalloc(sizeof(struct asus_hotk), GFP_KERNEL); + if (!hotk) + return -ENOMEM; + + hotk->handle = device->handle; + strcpy(acpi_device_name(device), ACPI_HOTK_DEVICE_NAME); + strcpy(acpi_device_class(device), ACPI_HOTK_CLASS); + device->driver_data = hotk; + hotk->device = device; + + result = asus_hotk_check(); + if (result) + goto end; + + result = asus_hotk_add_fs(device); + if (result) + goto end; + + /* + * We install the handler, it will receive the hotk in parameter, so, we + * could add other data to the hotk struct + */ + status = acpi_install_notify_handler(hotk->handle, ACPI_SYSTEM_NOTIFY, + asus_hotk_notify, hotk); + if (ACPI_FAILURE(status)) + printk(KERN_ERR " Error installing notify handler\n"); + + /* For laptops without GPLV: init the hotk->brightness value */ + if ((!hotk->methods->brightness_get) + && (!hotk->methods->brightness_status) + && (hotk->methods->brightness_up && hotk->methods->brightness_down)) { + status = + acpi_evaluate_object(NULL, hotk->methods->brightness_down, + NULL, NULL); + if (ACPI_FAILURE(status)) + printk(KERN_WARNING " Error changing brightness\n"); + else { + status = + acpi_evaluate_object(NULL, + hotk->methods->brightness_up, + NULL, NULL); + if (ACPI_FAILURE(status)) + printk(KERN_WARNING " Strange, error changing" + " brightness\n"); + } + } + + asus_hotk_found = 1; + + /* LED display is off by default */ + hotk->ledd_status = 0xFFF; + +end: + if (result) + kfree(hotk); + + return result; +} + +static int asus_hotk_remove(struct acpi_device *device, int type) +{ + acpi_status status = 0; + + if (!device || !acpi_driver_data(device)) + return -EINVAL; + + status = acpi_remove_notify_handler(hotk->handle, ACPI_SYSTEM_NOTIFY, + asus_hotk_notify); + if (ACPI_FAILURE(status)) + printk(KERN_ERR "Asus ACPI: Error removing notify handler\n"); + + asus_hotk_remove_fs(device); + + kfree(hotk); + + return 0; +} + +static struct backlight_ops asus_backlight_data = { + .get_brightness = read_brightness, + .update_status = set_brightness_status, +}; + +static void asus_acpi_exit(void) +{ + if (asus_backlight_device) + backlight_device_unregister(asus_backlight_device); + + acpi_bus_unregister_driver(&asus_hotk_driver); + remove_proc_entry(PROC_ASUS, acpi_root_dir); + + return; +} + +static int __init asus_acpi_init(void) +{ + int result; + + if (acpi_disabled) + return -ENODEV; + + asus_proc_dir = proc_mkdir(PROC_ASUS, acpi_root_dir); + if (!asus_proc_dir) { + printk(KERN_ERR "Asus ACPI: Unable to create /proc entry\n"); + return -ENODEV; + } + asus_proc_dir->owner = THIS_MODULE; + + result = acpi_bus_register_driver(&asus_hotk_driver); + if (result < 0) { + remove_proc_entry(PROC_ASUS, acpi_root_dir); + return result; + } + + /* + * This is a bit of a kludge. We only want this module loaded + * for ASUS systems, but there's currently no way to probe the + * ACPI namespace for ASUS HIDs. So we just return failure if + * we didn't find one, which will cause the module to be + * unloaded. + */ + if (!asus_hotk_found) { + acpi_bus_unregister_driver(&asus_hotk_driver); + remove_proc_entry(PROC_ASUS, acpi_root_dir); + return -ENODEV; + } + + asus_backlight_device = backlight_device_register("asus", NULL, NULL, + &asus_backlight_data); + if (IS_ERR(asus_backlight_device)) { + printk(KERN_ERR "Could not register asus backlight device\n"); + asus_backlight_device = NULL; + asus_acpi_exit(); + return -ENODEV; + } + asus_backlight_device->props.max_brightness = 15; + + return 0; +} + +module_init(asus_acpi_init); +module_exit(asus_acpi_exit); diff --git a/drivers/platform/x86/compal-laptop.c b/drivers/platform/x86/compal-laptop.c new file mode 100644 index 000000000000..11003bba10d3 --- /dev/null +++ b/drivers/platform/x86/compal-laptop.c @@ -0,0 +1,406 @@ +/*-*-linux-c-*-*/ + +/* + Copyright (C) 2008 Cezary Jackiewicz <cezary.jackiewicz (at) gmail.com> + + based on MSI driver + + Copyright (C) 2006 Lennart Poettering <mzxreary (at) 0pointer (dot) de> + + 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. + */ + +/* + * comapl-laptop.c - Compal laptop support. + * + * This driver exports a few files in /sys/devices/platform/compal-laptop/: + * + * wlan - wlan subsystem state: contains 0 or 1 (rw) + * + * bluetooth - Bluetooth subsystem state: contains 0 or 1 (rw) + * + * raw - raw value taken from embedded controller register (ro) + * + * In addition to these platform device attributes the driver + * registers itself in the Linux backlight control subsystem and is + * available to userspace under /sys/class/backlight/compal-laptop/. + * + * This driver might work on other laptops produced by Compal. If you + * want to try it you can pass force=1 as argument to the module which + * will force it to load even when the DMI data doesn't identify the + * laptop as FL9x. + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/acpi.h> +#include <linux/dmi.h> +#include <linux/backlight.h> +#include <linux/platform_device.h> +#include <linux/autoconf.h> + +#define COMPAL_DRIVER_VERSION "0.2.6" + +#define COMPAL_LCD_LEVEL_MAX 8 + +#define COMPAL_EC_COMMAND_WIRELESS 0xBB +#define COMPAL_EC_COMMAND_LCD_LEVEL 0xB9 + +#define KILLSWITCH_MASK 0x10 +#define WLAN_MASK 0x01 +#define BT_MASK 0x02 + +static int force; +module_param(force, bool, 0); +MODULE_PARM_DESC(force, "Force driver load, ignore DMI data"); + +/* Hardware access */ + +static int set_lcd_level(int level) +{ + if (level < 0 || level >= COMPAL_LCD_LEVEL_MAX) + return -EINVAL; + + ec_write(COMPAL_EC_COMMAND_LCD_LEVEL, level); + + return 0; +} + +static int get_lcd_level(void) +{ + u8 result; + + ec_read(COMPAL_EC_COMMAND_LCD_LEVEL, &result); + + return (int) result; +} + +static int set_wlan_state(int state) +{ + u8 result, value; + + ec_read(COMPAL_EC_COMMAND_WIRELESS, &result); + + if ((result & KILLSWITCH_MASK) == 0) + return -EINVAL; + else { + if (state) + value = (u8) (result | WLAN_MASK); + else + value = (u8) (result & ~WLAN_MASK); + ec_write(COMPAL_EC_COMMAND_WIRELESS, value); + } + + return 0; +} + +static int set_bluetooth_state(int state) +{ + u8 result, value; + + ec_read(COMPAL_EC_COMMAND_WIRELESS, &result); + + if ((result & KILLSWITCH_MASK) == 0) + return -EINVAL; + else { + if (state) + value = (u8) (result | BT_MASK); + else + value = (u8) (result & ~BT_MASK); + ec_write(COMPAL_EC_COMMAND_WIRELESS, value); + } + + return 0; +} + +static int get_wireless_state(int *wlan, int *bluetooth) +{ + u8 result; + + ec_read(COMPAL_EC_COMMAND_WIRELESS, &result); + + if (wlan) { + if ((result & KILLSWITCH_MASK) == 0) + *wlan = 0; + else + *wlan = result & WLAN_MASK; + } + + if (bluetooth) { + if ((result & KILLSWITCH_MASK) == 0) + *bluetooth = 0; + else + *bluetooth = (result & BT_MASK) >> 1; + } + + return 0; +} + +/* Backlight device stuff */ + +static int bl_get_brightness(struct backlight_device *b) +{ + return get_lcd_level(); +} + + +static int bl_update_status(struct backlight_device *b) +{ + return set_lcd_level(b->props.brightness); +} + +static struct backlight_ops compalbl_ops = { + .get_brightness = bl_get_brightness, + .update_status = bl_update_status, +}; + +static struct backlight_device *compalbl_device; + +/* Platform device */ + +static ssize_t show_wlan(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int ret, enabled; + + ret = get_wireless_state(&enabled, NULL); + if (ret < 0) + return ret; + + return sprintf(buf, "%i\n", enabled); +} + +static ssize_t show_raw(struct device *dev, + struct device_attribute *attr, char *buf) +{ + u8 result; + + ec_read(COMPAL_EC_COMMAND_WIRELESS, &result); + + return sprintf(buf, "%i\n", result); +} + +static ssize_t show_bluetooth(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int ret, enabled; + + ret = get_wireless_state(NULL, &enabled); + if (ret < 0) + return ret; + + return sprintf(buf, "%i\n", enabled); +} + +static ssize_t store_wlan_state(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + int state, ret; + + if (sscanf(buf, "%i", &state) != 1 || (state < 0 || state > 1)) + return -EINVAL; + + ret = set_wlan_state(state); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t store_bluetooth_state(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + int state, ret; + + if (sscanf(buf, "%i", &state) != 1 || (state < 0 || state > 1)) + return -EINVAL; + + ret = set_bluetooth_state(state); + if (ret < 0) + return ret; + + return count; +} + +static DEVICE_ATTR(bluetooth, 0644, show_bluetooth, store_bluetooth_state); +static DEVICE_ATTR(wlan, 0644, show_wlan, store_wlan_state); +static DEVICE_ATTR(raw, 0444, show_raw, NULL); + +static struct attribute *compal_attributes[] = { + &dev_attr_bluetooth.attr, + &dev_attr_wlan.attr, + &dev_attr_raw.attr, + NULL +}; + +static struct attribute_group compal_attribute_group = { + .attrs = compal_attributes +}; + +static struct platform_driver compal_driver = { + .driver = { + .name = "compal-laptop", + .owner = THIS_MODULE, + } +}; + +static struct platform_device *compal_device; + +/* Initialization */ + +static int dmi_check_cb(const struct dmi_system_id *id) +{ + printk(KERN_INFO "compal-laptop: Identified laptop model '%s'.\n", + id->ident); + + return 0; +} + +static struct dmi_system_id __initdata compal_dmi_table[] = { + { + .ident = "FL90/IFL90", + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "IFL90"), + DMI_MATCH(DMI_BOARD_VERSION, "IFT00"), + }, + .callback = dmi_check_cb + }, + { + .ident = "FL90/IFL90", + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "IFL90"), + DMI_MATCH(DMI_BOARD_VERSION, "REFERENCE"), + }, + .callback = dmi_check_cb + }, + { + .ident = "FL91/IFL91", + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "IFL91"), + DMI_MATCH(DMI_BOARD_VERSION, "IFT00"), + }, + .callback = dmi_check_cb + }, + { + .ident = "FL92/JFL92", + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "JFL92"), + DMI_MATCH(DMI_BOARD_VERSION, "IFT00"), + }, + .callback = dmi_check_cb + }, + { + .ident = "FT00/IFT00", + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "IFT00"), + DMI_MATCH(DMI_BOARD_VERSION, "IFT00"), + }, + .callback = dmi_check_cb + }, + { } +}; + +static int __init compal_init(void) +{ + int ret; + + if (acpi_disabled) + return -ENODEV; + + if (!force && !dmi_check_system(compal_dmi_table)) + return -ENODEV; + + /* Register backlight stuff */ + + if (!acpi_video_backlight_support()) { + compalbl_device = backlight_device_register("compal-laptop", NULL, NULL, + &compalbl_ops); + if (IS_ERR(compalbl_device)) + return PTR_ERR(compalbl_device); + + compalbl_device->props.max_brightness = COMPAL_LCD_LEVEL_MAX-1; + } + + ret = platform_driver_register(&compal_driver); + if (ret) + goto fail_backlight; + + /* Register platform stuff */ + + compal_device = platform_device_alloc("compal-laptop", -1); + if (!compal_device) { + ret = -ENOMEM; + goto fail_platform_driver; + } + + ret = platform_device_add(compal_device); + if (ret) + goto fail_platform_device1; + + ret = sysfs_create_group(&compal_device->dev.kobj, + &compal_attribute_group); + if (ret) + goto fail_platform_device2; + + printk(KERN_INFO "compal-laptop: driver "COMPAL_DRIVER_VERSION + " successfully loaded.\n"); + + return 0; + +fail_platform_device2: + + platform_device_del(compal_device); + +fail_platform_device1: + + platform_device_put(compal_device); + +fail_platform_driver: + + platform_driver_unregister(&compal_driver); + +fail_backlight: + + backlight_device_unregister(compalbl_device); + + return ret; +} + +static void __exit compal_cleanup(void) +{ + + sysfs_remove_group(&compal_device->dev.kobj, &compal_attribute_group); + platform_device_unregister(compal_device); + platform_driver_unregister(&compal_driver); + backlight_device_unregister(compalbl_device); + + printk(KERN_INFO "compal-laptop: driver unloaded.\n"); +} + +module_init(compal_init); +module_exit(compal_cleanup); + +MODULE_AUTHOR("Cezary Jackiewicz"); +MODULE_DESCRIPTION("Compal Laptop Support"); +MODULE_VERSION(COMPAL_DRIVER_VERSION); +MODULE_LICENSE("GPL"); + +MODULE_ALIAS("dmi:*:rnIFL90:rvrIFT00:*"); +MODULE_ALIAS("dmi:*:rnIFL90:rvrREFERENCE:*"); +MODULE_ALIAS("dmi:*:rnIFL91:rvrIFT00:*"); +MODULE_ALIAS("dmi:*:rnJFL92:rvrIFT00:*"); +MODULE_ALIAS("dmi:*:rnIFT00:rvrIFT00:*"); diff --git a/drivers/platform/x86/eeepc-laptop.c b/drivers/platform/x86/eeepc-laptop.c new file mode 100644 index 000000000000..02fe2b8b8939 --- /dev/null +++ b/drivers/platform/x86/eeepc-laptop.c @@ -0,0 +1,872 @@ +/* + * eepc-laptop.c - Asus Eee PC extras + * + * Based on asus_acpi.c as patched for the Eee PC by Asus: + * ftp://ftp.asus.com/pub/ASUS/EeePC/701/ASUS_ACPI_071126.rar + * Based on eee.c from eeepc-linux + * + * 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. + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/types.h> +#include <linux/platform_device.h> +#include <linux/backlight.h> +#include <linux/fb.h> +#include <linux/hwmon.h> +#include <linux/hwmon-sysfs.h> +#include <acpi/acpi_drivers.h> +#include <acpi/acpi_bus.h> +#include <linux/uaccess.h> +#include <linux/input.h> +#include <linux/rfkill.h> + +#define EEEPC_LAPTOP_VERSION "0.1" + +#define EEEPC_HOTK_NAME "Eee PC Hotkey Driver" +#define EEEPC_HOTK_FILE "eeepc" +#define EEEPC_HOTK_CLASS "hotkey" +#define EEEPC_HOTK_DEVICE_NAME "Hotkey" +#define EEEPC_HOTK_HID "ASUS010" + +#define EEEPC_LOG EEEPC_HOTK_FILE ": " +#define EEEPC_ERR KERN_ERR EEEPC_LOG +#define EEEPC_WARNING KERN_WARNING EEEPC_LOG +#define EEEPC_NOTICE KERN_NOTICE EEEPC_LOG +#define EEEPC_INFO KERN_INFO EEEPC_LOG + +/* + * Definitions for Asus EeePC + */ +#define NOTIFY_WLAN_ON 0x10 +#define NOTIFY_BRN_MIN 0x20 +#define NOTIFY_BRN_MAX 0x2f + +enum { + DISABLE_ASL_WLAN = 0x0001, + DISABLE_ASL_BLUETOOTH = 0x0002, + DISABLE_ASL_IRDA = 0x0004, + DISABLE_ASL_CAMERA = 0x0008, + DISABLE_ASL_TV = 0x0010, + DISABLE_ASL_GPS = 0x0020, + DISABLE_ASL_DISPLAYSWITCH = 0x0040, + DISABLE_ASL_MODEM = 0x0080, + DISABLE_ASL_CARDREADER = 0x0100 +}; + +enum { + CM_ASL_WLAN = 0, + CM_ASL_BLUETOOTH, + CM_ASL_IRDA, + CM_ASL_1394, + CM_ASL_CAMERA, + CM_ASL_TV, + CM_ASL_GPS, + CM_ASL_DVDROM, + CM_ASL_DISPLAYSWITCH, + CM_ASL_PANELBRIGHT, + CM_ASL_BIOSFLASH, + CM_ASL_ACPIFLASH, + CM_ASL_CPUFV, + CM_ASL_CPUTEMPERATURE, + CM_ASL_FANCPU, + CM_ASL_FANCHASSIS, + CM_ASL_USBPORT1, + CM_ASL_USBPORT2, + CM_ASL_USBPORT3, + CM_ASL_MODEM, + CM_ASL_CARDREADER, + CM_ASL_LID +}; + +static const char *cm_getv[] = { + "WLDG", NULL, NULL, NULL, + "CAMG", NULL, NULL, NULL, + NULL, "PBLG", NULL, NULL, + "CFVG", NULL, NULL, NULL, + "USBG", NULL, NULL, "MODG", + "CRDG", "LIDG" +}; + +static const char *cm_setv[] = { + "WLDS", NULL, NULL, NULL, + "CAMS", NULL, NULL, NULL, + "SDSP", "PBLS", "HDPS", NULL, + "CFVS", NULL, NULL, NULL, + "USBG", NULL, NULL, "MODS", + "CRDS", NULL +}; + +#define EEEPC_EC "\\_SB.PCI0.SBRG.EC0." + +#define EEEPC_EC_FAN_PWM EEEPC_EC "SC02" /* Fan PWM duty cycle (%) */ +#define EEEPC_EC_SC02 0x63 +#define EEEPC_EC_FAN_HRPM EEEPC_EC "SC05" /* High byte, fan speed (RPM) */ +#define EEEPC_EC_FAN_LRPM EEEPC_EC "SC06" /* Low byte, fan speed (RPM) */ +#define EEEPC_EC_FAN_CTRL EEEPC_EC "SFB3" /* Byte containing SF25 */ +#define EEEPC_EC_SFB3 0xD3 + +/* + * This is the main structure, we can use it to store useful information + * about the hotk device + */ +struct eeepc_hotk { + struct acpi_device *device; /* the device we are in */ + acpi_handle handle; /* the handle of the hotk device */ + u32 cm_supported; /* the control methods supported + by this BIOS */ + uint init_flag; /* Init flags */ + u16 event_count[128]; /* count for each event */ + struct input_dev *inputdev; + u16 *keycode_map; + struct rfkill *eeepc_wlan_rfkill; + struct rfkill *eeepc_bluetooth_rfkill; +}; + +/* The actual device the driver binds to */ +static struct eeepc_hotk *ehotk; + +/* Platform device/driver */ +static struct platform_driver platform_driver = { + .driver = { + .name = EEEPC_HOTK_FILE, + .owner = THIS_MODULE, + } +}; + +static struct platform_device *platform_device; + +struct key_entry { + char type; + u8 code; + u16 keycode; +}; + +enum { KE_KEY, KE_END }; + +static struct key_entry eeepc_keymap[] = { + /* Sleep already handled via generic ACPI code */ + {KE_KEY, 0x10, KEY_WLAN }, + {KE_KEY, 0x12, KEY_PROG1 }, + {KE_KEY, 0x13, KEY_MUTE }, + {KE_KEY, 0x14, KEY_VOLUMEDOWN }, + {KE_KEY, 0x15, KEY_VOLUMEUP }, + {KE_KEY, 0x30, KEY_SWITCHVIDEOMODE }, + {KE_KEY, 0x31, KEY_SWITCHVIDEOMODE }, + {KE_KEY, 0x32, KEY_SWITCHVIDEOMODE }, + {KE_END, 0}, +}; + +/* + * The hotkey driver declaration + */ +static int eeepc_hotk_add(struct acpi_device *device); +static int eeepc_hotk_remove(struct acpi_device *device, int type); + +static const struct acpi_device_id eeepc_device_ids[] = { + {EEEPC_HOTK_HID, 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, eeepc_device_ids); + +static struct acpi_driver eeepc_hotk_driver = { + .name = EEEPC_HOTK_NAME, + .class = EEEPC_HOTK_CLASS, + .ids = eeepc_device_ids, + .ops = { + .add = eeepc_hotk_add, + .remove = eeepc_hotk_remove, + }, +}; + +/* The backlight device /sys/class/backlight */ +static struct backlight_device *eeepc_backlight_device; + +/* The hwmon device */ +static struct device *eeepc_hwmon_device; + +/* + * The backlight class declaration + */ +static int read_brightness(struct backlight_device *bd); +static int update_bl_status(struct backlight_device *bd); +static struct backlight_ops eeepcbl_ops = { + .get_brightness = read_brightness, + .update_status = update_bl_status, +}; + +MODULE_AUTHOR("Corentin Chary, Eric Cooper"); +MODULE_DESCRIPTION(EEEPC_HOTK_NAME); +MODULE_LICENSE("GPL"); + +/* + * ACPI Helpers + */ +static int write_acpi_int(acpi_handle handle, const char *method, int val, + struct acpi_buffer *output) +{ + struct acpi_object_list params; + union acpi_object in_obj; + acpi_status status; + + params.count = 1; + params.pointer = &in_obj; + in_obj.type = ACPI_TYPE_INTEGER; + in_obj.integer.value = val; + + status = acpi_evaluate_object(handle, (char *)method, ¶ms, output); + return (status == AE_OK ? 0 : -1); +} + +static int read_acpi_int(acpi_handle handle, const char *method, int *val) +{ + acpi_status status; + unsigned long long result; + + status = acpi_evaluate_integer(handle, (char *)method, NULL, &result); + if (ACPI_FAILURE(status)) { + *val = -1; + return -1; + } else { + *val = result; + return 0; + } +} + +static int set_acpi(int cm, int value) +{ + if (ehotk->cm_supported & (0x1 << cm)) { + const char *method = cm_setv[cm]; + if (method == NULL) + return -ENODEV; + if (write_acpi_int(ehotk->handle, method, value, NULL)) + printk(EEEPC_WARNING "Error writing %s\n", method); + } + return 0; +} + +static int get_acpi(int cm) +{ + int value = -1; + if ((ehotk->cm_supported & (0x1 << cm))) { + const char *method = cm_getv[cm]; + if (method == NULL) + return -ENODEV; + if (read_acpi_int(ehotk->handle, method, &value)) + printk(EEEPC_WARNING "Error reading %s\n", method); + } + return value; +} + +/* + * Backlight + */ +static int read_brightness(struct backlight_device *bd) +{ + return get_acpi(CM_ASL_PANELBRIGHT); +} + +static int set_brightness(struct backlight_device *bd, int value) +{ + value = max(0, min(15, value)); + return set_acpi(CM_ASL_PANELBRIGHT, value); +} + +static int update_bl_status(struct backlight_device *bd) +{ + return set_brightness(bd, bd->props.brightness); +} + +/* + * Rfkill helpers + */ + +static int eeepc_wlan_rfkill_set(void *data, enum rfkill_state state) +{ + if (state == RFKILL_STATE_SOFT_BLOCKED) + return set_acpi(CM_ASL_WLAN, 0); + else + return set_acpi(CM_ASL_WLAN, 1); +} + +static int eeepc_wlan_rfkill_state(void *data, enum rfkill_state *state) +{ + if (get_acpi(CM_ASL_WLAN) == 1) + *state = RFKILL_STATE_UNBLOCKED; + else + *state = RFKILL_STATE_SOFT_BLOCKED; + return 0; +} + +static int eeepc_bluetooth_rfkill_set(void *data, enum rfkill_state state) +{ + if (state == RFKILL_STATE_SOFT_BLOCKED) + return set_acpi(CM_ASL_BLUETOOTH, 0); + else + return set_acpi(CM_ASL_BLUETOOTH, 1); +} + +static int eeepc_bluetooth_rfkill_state(void *data, enum rfkill_state *state) +{ + if (get_acpi(CM_ASL_BLUETOOTH) == 1) + *state = RFKILL_STATE_UNBLOCKED; + else + *state = RFKILL_STATE_SOFT_BLOCKED; + return 0; +} + +/* + * Sys helpers + */ +static int parse_arg(const char *buf, unsigned long count, int *val) +{ + if (!count) + return 0; + if (sscanf(buf, "%i", val) != 1) + return -EINVAL; + return count; +} + +static ssize_t store_sys_acpi(int cm, const char *buf, size_t count) +{ + int rv, value; + + rv = parse_arg(buf, count, &value); + if (rv > 0) + set_acpi(cm, value); + return rv; +} + +static ssize_t show_sys_acpi(int cm, char *buf) +{ + return sprintf(buf, "%d\n", get_acpi(cm)); +} + +#define EEEPC_CREATE_DEVICE_ATTR(_name, _cm) \ + static ssize_t show_##_name(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ + { \ + return show_sys_acpi(_cm, buf); \ + } \ + static ssize_t store_##_name(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return store_sys_acpi(_cm, buf, count); \ + } \ + static struct device_attribute dev_attr_##_name = { \ + .attr = { \ + .name = __stringify(_name), \ + .mode = 0644 }, \ + .show = show_##_name, \ + .store = store_##_name, \ + } + +EEEPC_CREATE_DEVICE_ATTR(camera, CM_ASL_CAMERA); +EEEPC_CREATE_DEVICE_ATTR(cardr, CM_ASL_CARDREADER); +EEEPC_CREATE_DEVICE_ATTR(disp, CM_ASL_DISPLAYSWITCH); + +static struct attribute *platform_attributes[] = { + &dev_attr_camera.attr, + &dev_attr_cardr.attr, + &dev_attr_disp.attr, + NULL +}; + +static struct attribute_group platform_attribute_group = { + .attrs = platform_attributes +}; + +/* + * Hotkey functions + */ +static struct key_entry *eepc_get_entry_by_scancode(int code) +{ + struct key_entry *key; + + for (key = eeepc_keymap; key->type != KE_END; key++) + if (code == key->code) + return key; + + return NULL; +} + +static struct key_entry *eepc_get_entry_by_keycode(int code) +{ + struct key_entry *key; + + for (key = eeepc_keymap; key->type != KE_END; key++) + if (code == key->keycode && key->type == KE_KEY) + return key; + + return NULL; +} + +static int eeepc_getkeycode(struct input_dev *dev, int scancode, int *keycode) +{ + struct key_entry *key = eepc_get_entry_by_scancode(scancode); + + if (key && key->type == KE_KEY) { + *keycode = key->keycode; + return 0; + } + + return -EINVAL; +} + +static int eeepc_setkeycode(struct input_dev *dev, int scancode, int keycode) +{ + struct key_entry *key; + int old_keycode; + + if (keycode < 0 || keycode > KEY_MAX) + return -EINVAL; + + key = eepc_get_entry_by_scancode(scancode); + if (key && key->type == KE_KEY) { + old_keycode = key->keycode; + key->keycode = keycode; + set_bit(keycode, dev->keybit); + if (!eepc_get_entry_by_keycode(old_keycode)) + clear_bit(old_keycode, dev->keybit); + return 0; + } + + return -EINVAL; +} + +static int eeepc_hotk_check(void) +{ + const struct key_entry *key; + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + int result; + + result = acpi_bus_get_status(ehotk->device); + if (result) + return result; + if (ehotk->device->status.present) { + if (write_acpi_int(ehotk->handle, "INIT", ehotk->init_flag, + &buffer)) { + printk(EEEPC_ERR "Hotkey initialization failed\n"); + return -ENODEV; + } else { + printk(EEEPC_NOTICE "Hotkey init flags 0x%x\n", + ehotk->init_flag); + } + /* get control methods supported */ + if (read_acpi_int(ehotk->handle, "CMSG" + , &ehotk->cm_supported)) { + printk(EEEPC_ERR + "Get control methods supported failed\n"); + return -ENODEV; + } else { + printk(EEEPC_INFO + "Get control methods supported: 0x%x\n", + ehotk->cm_supported); + } + ehotk->inputdev = input_allocate_device(); + if (!ehotk->inputdev) { + printk(EEEPC_INFO "Unable to allocate input device\n"); + return 0; + } + ehotk->inputdev->name = "Asus EeePC extra buttons"; + ehotk->inputdev->phys = EEEPC_HOTK_FILE "/input0"; + ehotk->inputdev->id.bustype = BUS_HOST; + ehotk->inputdev->getkeycode = eeepc_getkeycode; + ehotk->inputdev->setkeycode = eeepc_setkeycode; + + for (key = eeepc_keymap; key->type != KE_END; key++) { + switch (key->type) { + case KE_KEY: + set_bit(EV_KEY, ehotk->inputdev->evbit); + set_bit(key->keycode, ehotk->inputdev->keybit); + break; + } + } + result = input_register_device(ehotk->inputdev); + if (result) { + printk(EEEPC_INFO "Unable to register input device\n"); + input_free_device(ehotk->inputdev); + return 0; + } + } else { + printk(EEEPC_ERR "Hotkey device not present, aborting\n"); + return -EINVAL; + } + return 0; +} + +static void notify_brn(void) +{ + struct backlight_device *bd = eeepc_backlight_device; + bd->props.brightness = read_brightness(bd); +} + +static void eeepc_hotk_notify(acpi_handle handle, u32 event, void *data) +{ + static struct key_entry *key; + if (!ehotk) + return; + if (event >= NOTIFY_BRN_MIN && event <= NOTIFY_BRN_MAX) + notify_brn(); + acpi_bus_generate_proc_event(ehotk->device, event, + ehotk->event_count[event % 128]++); + if (ehotk->inputdev) { + key = eepc_get_entry_by_scancode(event); + if (key) { + switch (key->type) { + case KE_KEY: + input_report_key(ehotk->inputdev, key->keycode, + 1); + input_sync(ehotk->inputdev); + input_report_key(ehotk->inputdev, key->keycode, + 0); + input_sync(ehotk->inputdev); + break; + } + } + } +} + +static int eeepc_hotk_add(struct acpi_device *device) +{ + acpi_status status = AE_OK; + int result; + + if (!device) + return -EINVAL; + printk(EEEPC_NOTICE EEEPC_HOTK_NAME "\n"); + ehotk = kzalloc(sizeof(struct eeepc_hotk), GFP_KERNEL); + if (!ehotk) + return -ENOMEM; + ehotk->init_flag = DISABLE_ASL_WLAN | DISABLE_ASL_DISPLAYSWITCH; + ehotk->handle = device->handle; + strcpy(acpi_device_name(device), EEEPC_HOTK_DEVICE_NAME); + strcpy(acpi_device_class(device), EEEPC_HOTK_CLASS); + device->driver_data = ehotk; + ehotk->device = device; + result = eeepc_hotk_check(); + if (result) + goto end; + status = acpi_install_notify_handler(ehotk->handle, ACPI_SYSTEM_NOTIFY, + eeepc_hotk_notify, ehotk); + if (ACPI_FAILURE(status)) + printk(EEEPC_ERR "Error installing notify handler\n"); + + if (get_acpi(CM_ASL_WLAN) != -1) { + ehotk->eeepc_wlan_rfkill = rfkill_allocate(&device->dev, + RFKILL_TYPE_WLAN); + + if (!ehotk->eeepc_wlan_rfkill) + goto end; + + ehotk->eeepc_wlan_rfkill->name = "eeepc-wlan"; + ehotk->eeepc_wlan_rfkill->toggle_radio = eeepc_wlan_rfkill_set; + ehotk->eeepc_wlan_rfkill->get_state = eeepc_wlan_rfkill_state; + if (get_acpi(CM_ASL_WLAN) == 1) + ehotk->eeepc_wlan_rfkill->state = + RFKILL_STATE_UNBLOCKED; + else + ehotk->eeepc_wlan_rfkill->state = + RFKILL_STATE_SOFT_BLOCKED; + rfkill_register(ehotk->eeepc_wlan_rfkill); + } + + if (get_acpi(CM_ASL_BLUETOOTH) != -1) { + ehotk->eeepc_bluetooth_rfkill = + rfkill_allocate(&device->dev, RFKILL_TYPE_BLUETOOTH); + + if (!ehotk->eeepc_bluetooth_rfkill) + goto end; + + ehotk->eeepc_bluetooth_rfkill->name = "eeepc-bluetooth"; + ehotk->eeepc_bluetooth_rfkill->toggle_radio = + eeepc_bluetooth_rfkill_set; + ehotk->eeepc_bluetooth_rfkill->get_state = + eeepc_bluetooth_rfkill_state; + if (get_acpi(CM_ASL_BLUETOOTH) == 1) + ehotk->eeepc_bluetooth_rfkill->state = + RFKILL_STATE_UNBLOCKED; + else + ehotk->eeepc_bluetooth_rfkill->state = + RFKILL_STATE_SOFT_BLOCKED; + rfkill_register(ehotk->eeepc_bluetooth_rfkill); + } + + end: + if (result) { + kfree(ehotk); + ehotk = NULL; + } + return result; +} + +static int eeepc_hotk_remove(struct acpi_device *device, int type) +{ + acpi_status status = 0; + + if (!device || !acpi_driver_data(device)) + return -EINVAL; + status = acpi_remove_notify_handler(ehotk->handle, ACPI_SYSTEM_NOTIFY, + eeepc_hotk_notify); + if (ACPI_FAILURE(status)) + printk(EEEPC_ERR "Error removing notify handler\n"); + kfree(ehotk); + return 0; +} + +/* + * Hwmon + */ +static int eeepc_get_fan_pwm(void) +{ + int value = 0; + + read_acpi_int(NULL, EEEPC_EC_FAN_PWM, &value); + value = value * 255 / 100; + return (value); +} + +static void eeepc_set_fan_pwm(int value) +{ + value = SENSORS_LIMIT(value, 0, 255); + value = value * 100 / 255; + ec_write(EEEPC_EC_SC02, value); +} + +static int eeepc_get_fan_rpm(void) +{ + int high = 0; + int low = 0; + + read_acpi_int(NULL, EEEPC_EC_FAN_HRPM, &high); + read_acpi_int(NULL, EEEPC_EC_FAN_LRPM, &low); + return (high << 8 | low); +} + +static int eeepc_get_fan_ctrl(void) +{ + int value = 0; + + read_acpi_int(NULL, EEEPC_EC_FAN_CTRL, &value); + return ((value & 0x02 ? 1 : 0)); +} + +static void eeepc_set_fan_ctrl(int manual) +{ + int value = 0; + + read_acpi_int(NULL, EEEPC_EC_FAN_CTRL, &value); + if (manual) + value |= 0x02; + else + value &= ~0x02; + ec_write(EEEPC_EC_SFB3, value); +} + +static ssize_t store_sys_hwmon(void (*set)(int), const char *buf, size_t count) +{ + int rv, value; + + rv = parse_arg(buf, count, &value); + if (rv > 0) + set(value); + return rv; +} + +static ssize_t show_sys_hwmon(int (*get)(void), char *buf) +{ + return sprintf(buf, "%d\n", get()); +} + +#define EEEPC_CREATE_SENSOR_ATTR(_name, _mode, _set, _get) \ + static ssize_t show_##_name(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ + { \ + return show_sys_hwmon(_set, buf); \ + } \ + static ssize_t store_##_name(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return store_sys_hwmon(_get, buf, count); \ + } \ + static SENSOR_DEVICE_ATTR(_name, _mode, show_##_name, store_##_name, 0); + +EEEPC_CREATE_SENSOR_ATTR(fan1_input, S_IRUGO, eeepc_get_fan_rpm, NULL); +EEEPC_CREATE_SENSOR_ATTR(pwm1, S_IRUGO | S_IWUSR, + eeepc_get_fan_pwm, eeepc_set_fan_pwm); +EEEPC_CREATE_SENSOR_ATTR(pwm1_enable, S_IRUGO | S_IWUSR, + eeepc_get_fan_ctrl, eeepc_set_fan_ctrl); + +static ssize_t +show_name(struct device *dev, struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "eeepc\n"); +} +static SENSOR_DEVICE_ATTR(name, S_IRUGO, show_name, NULL, 0); + +static struct attribute *hwmon_attributes[] = { + &sensor_dev_attr_pwm1.dev_attr.attr, + &sensor_dev_attr_fan1_input.dev_attr.attr, + &sensor_dev_attr_pwm1_enable.dev_attr.attr, + &sensor_dev_attr_name.dev_attr.attr, + NULL +}; + +static struct attribute_group hwmon_attribute_group = { + .attrs = hwmon_attributes +}; + +/* + * exit/init + */ +static void eeepc_backlight_exit(void) +{ + if (eeepc_backlight_device) + backlight_device_unregister(eeepc_backlight_device); + if (ehotk->inputdev) + input_unregister_device(ehotk->inputdev); + if (ehotk->eeepc_wlan_rfkill) + rfkill_unregister(ehotk->eeepc_wlan_rfkill); + if (ehotk->eeepc_bluetooth_rfkill) + rfkill_unregister(ehotk->eeepc_bluetooth_rfkill); + eeepc_backlight_device = NULL; +} + +static void eeepc_hwmon_exit(void) +{ + struct device *hwmon; + + hwmon = eeepc_hwmon_device; + if (!hwmon) + return ; + sysfs_remove_group(&hwmon->kobj, + &hwmon_attribute_group); + hwmon_device_unregister(hwmon); + eeepc_hwmon_device = NULL; +} + +static void __exit eeepc_laptop_exit(void) +{ + eeepc_backlight_exit(); + eeepc_hwmon_exit(); + acpi_bus_unregister_driver(&eeepc_hotk_driver); + sysfs_remove_group(&platform_device->dev.kobj, + &platform_attribute_group); + platform_device_unregister(platform_device); + platform_driver_unregister(&platform_driver); +} + +static int eeepc_backlight_init(struct device *dev) +{ + struct backlight_device *bd; + + bd = backlight_device_register(EEEPC_HOTK_FILE, dev, + NULL, &eeepcbl_ops); + if (IS_ERR(bd)) { + printk(EEEPC_ERR + "Could not register eeepc backlight device\n"); + eeepc_backlight_device = NULL; + return PTR_ERR(bd); + } + eeepc_backlight_device = bd; + bd->props.max_brightness = 15; + bd->props.brightness = read_brightness(NULL); + bd->props.power = FB_BLANK_UNBLANK; + backlight_update_status(bd); + return 0; +} + +static int eeepc_hwmon_init(struct device *dev) +{ + struct device *hwmon; + int result; + + hwmon = hwmon_device_register(dev); + if (IS_ERR(hwmon)) { + printk(EEEPC_ERR + "Could not register eeepc hwmon device\n"); + eeepc_hwmon_device = NULL; + return PTR_ERR(hwmon); + } + eeepc_hwmon_device = hwmon; + result = sysfs_create_group(&hwmon->kobj, + &hwmon_attribute_group); + if (result) + eeepc_hwmon_exit(); + return result; +} + +static int __init eeepc_laptop_init(void) +{ + struct device *dev; + int result; + + if (acpi_disabled) + return -ENODEV; + result = acpi_bus_register_driver(&eeepc_hotk_driver); + if (result < 0) + return result; + if (!ehotk) { + acpi_bus_unregister_driver(&eeepc_hotk_driver); + return -ENODEV; + } + dev = acpi_get_physical_device(ehotk->device->handle); + + if (!acpi_video_backlight_support()) { + result = eeepc_backlight_init(dev); + if (result) + goto fail_backlight; + } else + printk(EEEPC_INFO "Backlight controlled by ACPI video " + "driver\n"); + + result = eeepc_hwmon_init(dev); + if (result) + goto fail_hwmon; + /* Register platform stuff */ + result = platform_driver_register(&platform_driver); + if (result) + goto fail_platform_driver; + platform_device = platform_device_alloc(EEEPC_HOTK_FILE, -1); + if (!platform_device) { + result = -ENOMEM; + goto fail_platform_device1; + } + result = platform_device_add(platform_device); + if (result) + goto fail_platform_device2; + result = sysfs_create_group(&platform_device->dev.kobj, + &platform_attribute_group); + if (result) + goto fail_sysfs; + return 0; +fail_sysfs: + platform_device_del(platform_device); +fail_platform_device2: + platform_device_put(platform_device); +fail_platform_device1: + platform_driver_unregister(&platform_driver); +fail_platform_driver: + eeepc_hwmon_exit(); +fail_hwmon: + eeepc_backlight_exit(); +fail_backlight: + return result; +} + +module_init(eeepc_laptop_init); +module_exit(eeepc_laptop_exit); diff --git a/drivers/platform/x86/fujitsu-laptop.c b/drivers/platform/x86/fujitsu-laptop.c new file mode 100644 index 000000000000..65dc41540c62 --- /dev/null +++ b/drivers/platform/x86/fujitsu-laptop.c @@ -0,0 +1,1293 @@ +/*-*-linux-c-*-*/ + +/* + Copyright (C) 2007,2008 Jonathan Woithe <jwoithe@physics.adelaide.edu.au> + Copyright (C) 2008 Peter Gruber <nokos@gmx.net> + Copyright (C) 2008 Tony Vroon <tony@linx.net> + Based on earlier work: + Copyright (C) 2003 Shane Spencer <shane@bogomip.com> + Adrian Yee <brewt-fujitsu@brewt.org> + + Templated from msi-laptop.c and thinkpad_acpi.c which is copyright + by its respective authors. + + 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. + */ + +/* + * fujitsu-laptop.c - Fujitsu laptop support, providing access to additional + * features made available on a range of Fujitsu laptops including the + * P2xxx/P5xxx/S6xxx/S7xxx series. + * + * This driver exports a few files in /sys/devices/platform/fujitsu-laptop/; + * others may be added at a later date. + * + * lcd_level - Screen brightness: contains a single integer in the + * range 0..7. (rw) + * + * In addition to these platform device attributes the driver + * registers itself in the Linux backlight control subsystem and is + * available to userspace under /sys/class/backlight/fujitsu-laptop/. + * + * Hotkeys present on certain Fujitsu laptops (eg: the S6xxx series) are + * also supported by this driver. + * + * This driver has been tested on a Fujitsu Lifebook S6410, S7020 and + * P8010. It should work on most P-series and S-series Lifebooks, but + * YMMV. + * + * The module parameter use_alt_lcd_levels switches between different ACPI + * brightness controls which are used by different Fujitsu laptops. In most + * cases the correct method is automatically detected. "use_alt_lcd_levels=1" + * is applicable for a Fujitsu Lifebook S6410 if autodetection fails. + * + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/acpi.h> +#include <linux/dmi.h> +#include <linux/backlight.h> +#include <linux/input.h> +#include <linux/kfifo.h> +#include <linux/video_output.h> +#include <linux/platform_device.h> +#ifdef CONFIG_LEDS_CLASS +#include <linux/leds.h> +#endif + +#define FUJITSU_DRIVER_VERSION "0.5.0" + +#define FUJITSU_LCD_N_LEVELS 8 + +#define ACPI_FUJITSU_CLASS "fujitsu" +#define ACPI_FUJITSU_HID "FUJ02B1" +#define ACPI_FUJITSU_DRIVER_NAME "Fujitsu laptop FUJ02B1 ACPI brightness driver" +#define ACPI_FUJITSU_DEVICE_NAME "Fujitsu FUJ02B1" +#define ACPI_FUJITSU_HOTKEY_HID "FUJ02E3" +#define ACPI_FUJITSU_HOTKEY_DRIVER_NAME "Fujitsu laptop FUJ02E3 ACPI hotkeys driver" +#define ACPI_FUJITSU_HOTKEY_DEVICE_NAME "Fujitsu FUJ02E3" + +#define ACPI_FUJITSU_NOTIFY_CODE1 0x80 + +#define ACPI_VIDEO_NOTIFY_INC_BRIGHTNESS 0x86 +#define ACPI_VIDEO_NOTIFY_DEC_BRIGHTNESS 0x87 + +/* FUNC interface - command values */ +#define FUNC_RFKILL 0x1000 +#define FUNC_LEDS 0x1001 +#define FUNC_BUTTONS 0x1002 +#define FUNC_BACKLIGHT 0x1004 + +/* FUNC interface - responses */ +#define UNSUPPORTED_CMD 0x80000000 + +#ifdef CONFIG_LEDS_CLASS +/* FUNC interface - LED control */ +#define FUNC_LED_OFF 0x1 +#define FUNC_LED_ON 0x30001 +#define KEYBOARD_LAMPS 0x100 +#define LOGOLAMP_POWERON 0x2000 +#define LOGOLAMP_ALWAYS 0x4000 +#endif + +/* Hotkey details */ +#define KEY1_CODE 0x410 /* codes for the keys in the GIRB register */ +#define KEY2_CODE 0x411 +#define KEY3_CODE 0x412 +#define KEY4_CODE 0x413 + +#define MAX_HOTKEY_RINGBUFFER_SIZE 100 +#define RINGBUFFERSIZE 40 + +/* Debugging */ +#define FUJLAPTOP_LOG ACPI_FUJITSU_HID ": " +#define FUJLAPTOP_ERR KERN_ERR FUJLAPTOP_LOG +#define FUJLAPTOP_NOTICE KERN_NOTICE FUJLAPTOP_LOG +#define FUJLAPTOP_INFO KERN_INFO FUJLAPTOP_LOG +#define FUJLAPTOP_DEBUG KERN_DEBUG FUJLAPTOP_LOG + +#define FUJLAPTOP_DBG_ALL 0xffff +#define FUJLAPTOP_DBG_ERROR 0x0001 +#define FUJLAPTOP_DBG_WARN 0x0002 +#define FUJLAPTOP_DBG_INFO 0x0004 +#define FUJLAPTOP_DBG_TRACE 0x0008 + +#define dbg_printk(a_dbg_level, format, arg...) \ + do { if (dbg_level & a_dbg_level) \ + printk(FUJLAPTOP_DEBUG "%s: " format, __func__ , ## arg); \ + } while (0) +#ifdef CONFIG_FUJITSU_LAPTOP_DEBUG +#define vdbg_printk(a_dbg_level, format, arg...) \ + dbg_printk(a_dbg_level, format, ## arg) +#else +#define vdbg_printk(a_dbg_level, format, arg...) +#endif + +/* Device controlling the backlight and associated keys */ +struct fujitsu_t { + acpi_handle acpi_handle; + struct acpi_device *dev; + struct input_dev *input; + char phys[32]; + struct backlight_device *bl_device; + struct platform_device *pf_device; + int keycode1, keycode2, keycode3, keycode4; + + unsigned int max_brightness; + unsigned int brightness_changed; + unsigned int brightness_level; +}; + +static struct fujitsu_t *fujitsu; +static int use_alt_lcd_levels = -1; +static int disable_brightness_adjust = -1; + +/* Device used to access other hotkeys on the laptop */ +struct fujitsu_hotkey_t { + acpi_handle acpi_handle; + struct acpi_device *dev; + struct input_dev *input; + char phys[32]; + struct platform_device *pf_device; + struct kfifo *fifo; + spinlock_t fifo_lock; + int rfkill_state; + int logolamp_registered; + int kblamps_registered; +}; + +static struct fujitsu_hotkey_t *fujitsu_hotkey; + +static void acpi_fujitsu_hotkey_notify(acpi_handle handle, u32 event, + void *data); + +#ifdef CONFIG_LEDS_CLASS +static enum led_brightness logolamp_get(struct led_classdev *cdev); +static void logolamp_set(struct led_classdev *cdev, + enum led_brightness brightness); + +struct led_classdev logolamp_led = { + .name = "fujitsu::logolamp", + .brightness_get = logolamp_get, + .brightness_set = logolamp_set +}; + +static enum led_brightness kblamps_get(struct led_classdev *cdev); +static void kblamps_set(struct led_classdev *cdev, + enum led_brightness brightness); + +struct led_classdev kblamps_led = { + .name = "fujitsu::kblamps", + .brightness_get = kblamps_get, + .brightness_set = kblamps_set +}; +#endif + +#ifdef CONFIG_FUJITSU_LAPTOP_DEBUG +static u32 dbg_level = 0x03; +#endif + +static void acpi_fujitsu_notify(acpi_handle handle, u32 event, void *data); + +/* Fujitsu ACPI interface function */ + +static int call_fext_func(int cmd, int arg0, int arg1, int arg2) +{ + acpi_status status = AE_OK; + union acpi_object params[4] = { + { .type = ACPI_TYPE_INTEGER }, + { .type = ACPI_TYPE_INTEGER }, + { .type = ACPI_TYPE_INTEGER }, + { .type = ACPI_TYPE_INTEGER } + }; + struct acpi_object_list arg_list = { 4, ¶ms[0] }; + struct acpi_buffer output; + union acpi_object out_obj; + acpi_handle handle = NULL; + + status = acpi_get_handle(fujitsu_hotkey->acpi_handle, "FUNC", &handle); + if (ACPI_FAILURE(status)) { + vdbg_printk(FUJLAPTOP_DBG_ERROR, + "FUNC interface is not present\n"); + return -ENODEV; + } + + params[0].integer.value = cmd; + params[1].integer.value = arg0; + params[2].integer.value = arg1; + params[3].integer.value = arg2; + + output.length = sizeof(out_obj); + output.pointer = &out_obj; + + status = acpi_evaluate_object(handle, NULL, &arg_list, &output); + if (ACPI_FAILURE(status)) { + vdbg_printk(FUJLAPTOP_DBG_WARN, + "FUNC 0x%x (args 0x%x, 0x%x, 0x%x) call failed\n", + cmd, arg0, arg1, arg2); + return -ENODEV; + } + + if (out_obj.type != ACPI_TYPE_INTEGER) { + vdbg_printk(FUJLAPTOP_DBG_WARN, + "FUNC 0x%x (args 0x%x, 0x%x, 0x%x) did not " + "return an integer\n", + cmd, arg0, arg1, arg2); + return -ENODEV; + } + + vdbg_printk(FUJLAPTOP_DBG_TRACE, + "FUNC 0x%x (args 0x%x, 0x%x, 0x%x) returned 0x%x\n", + cmd, arg0, arg1, arg2, (int)out_obj.integer.value); + return out_obj.integer.value; +} + +#ifdef CONFIG_LEDS_CLASS +/* LED class callbacks */ + +static void logolamp_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + if (brightness >= LED_FULL) { + call_fext_func(FUNC_LEDS, 0x1, LOGOLAMP_POWERON, FUNC_LED_ON); + call_fext_func(FUNC_LEDS, 0x1, LOGOLAMP_ALWAYS, FUNC_LED_ON); + } else if (brightness >= LED_HALF) { + call_fext_func(FUNC_LEDS, 0x1, LOGOLAMP_POWERON, FUNC_LED_ON); + call_fext_func(FUNC_LEDS, 0x1, LOGOLAMP_ALWAYS, FUNC_LED_OFF); + } else { + call_fext_func(FUNC_LEDS, 0x1, LOGOLAMP_POWERON, FUNC_LED_OFF); + } +} + +static void kblamps_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + if (brightness >= LED_FULL) + call_fext_func(FUNC_LEDS, 0x1, KEYBOARD_LAMPS, FUNC_LED_ON); + else + call_fext_func(FUNC_LEDS, 0x1, KEYBOARD_LAMPS, FUNC_LED_OFF); +} + +static enum led_brightness logolamp_get(struct led_classdev *cdev) +{ + enum led_brightness brightness = LED_OFF; + int poweron, always; + + poweron = call_fext_func(FUNC_LEDS, 0x2, LOGOLAMP_POWERON, 0x0); + if (poweron == FUNC_LED_ON) { + brightness = LED_HALF; + always = call_fext_func(FUNC_LEDS, 0x2, LOGOLAMP_ALWAYS, 0x0); + if (always == FUNC_LED_ON) + brightness = LED_FULL; + } + return brightness; +} + +static enum led_brightness kblamps_get(struct led_classdev *cdev) +{ + enum led_brightness brightness = LED_OFF; + + if (call_fext_func(FUNC_LEDS, 0x2, KEYBOARD_LAMPS, 0x0) == FUNC_LED_ON) + brightness = LED_FULL; + + return brightness; +} +#endif + +/* Hardware access for LCD brightness control */ + +static int set_lcd_level(int level) +{ + acpi_status status = AE_OK; + union acpi_object arg0 = { ACPI_TYPE_INTEGER }; + struct acpi_object_list arg_list = { 1, &arg0 }; + acpi_handle handle = NULL; + + vdbg_printk(FUJLAPTOP_DBG_TRACE, "set lcd level via SBLL [%d]\n", + level); + + if (level < 0 || level >= fujitsu->max_brightness) + return -EINVAL; + + if (!fujitsu) + return -EINVAL; + + status = acpi_get_handle(fujitsu->acpi_handle, "SBLL", &handle); + if (ACPI_FAILURE(status)) { + vdbg_printk(FUJLAPTOP_DBG_ERROR, "SBLL not present\n"); + return -ENODEV; + } + + arg0.integer.value = level; + + status = acpi_evaluate_object(handle, NULL, &arg_list, NULL); + if (ACPI_FAILURE(status)) + return -ENODEV; + + return 0; +} + +static int set_lcd_level_alt(int level) +{ + acpi_status status = AE_OK; + union acpi_object arg0 = { ACPI_TYPE_INTEGER }; + struct acpi_object_list arg_list = { 1, &arg0 }; + acpi_handle handle = NULL; + + vdbg_printk(FUJLAPTOP_DBG_TRACE, "set lcd level via SBL2 [%d]\n", + level); + + if (level < 0 || level >= fujitsu->max_brightness) + return -EINVAL; + + if (!fujitsu) + return -EINVAL; + + status = acpi_get_handle(fujitsu->acpi_handle, "SBL2", &handle); + if (ACPI_FAILURE(status)) { + vdbg_printk(FUJLAPTOP_DBG_ERROR, "SBL2 not present\n"); + return -ENODEV; + } + + arg0.integer.value = level; + + status = acpi_evaluate_object(handle, NULL, &arg_list, NULL); + if (ACPI_FAILURE(status)) + return -ENODEV; + + return 0; +} + +static int get_lcd_level(void) +{ + unsigned long long state = 0; + acpi_status status = AE_OK; + + vdbg_printk(FUJLAPTOP_DBG_TRACE, "get lcd level via GBLL\n"); + + status = + acpi_evaluate_integer(fujitsu->acpi_handle, "GBLL", NULL, &state); + if (status < 0) + return status; + + fujitsu->brightness_level = state & 0x0fffffff; + + if (state & 0x80000000) + fujitsu->brightness_changed = 1; + else + fujitsu->brightness_changed = 0; + + return fujitsu->brightness_level; +} + +static int get_max_brightness(void) +{ + unsigned long long state = 0; + acpi_status status = AE_OK; + + vdbg_printk(FUJLAPTOP_DBG_TRACE, "get max lcd level via RBLL\n"); + + status = + acpi_evaluate_integer(fujitsu->acpi_handle, "RBLL", NULL, &state); + if (status < 0) + return status; + + fujitsu->max_brightness = state; + + return fujitsu->max_brightness; +} + +/* Backlight device stuff */ + +static int bl_get_brightness(struct backlight_device *b) +{ + return get_lcd_level(); +} + +static int bl_update_status(struct backlight_device *b) +{ + int ret; + if (b->props.power == 4) + ret = call_fext_func(FUNC_BACKLIGHT, 0x1, 0x4, 0x3); + else + ret = call_fext_func(FUNC_BACKLIGHT, 0x1, 0x4, 0x0); + if (ret != 0) + vdbg_printk(FUJLAPTOP_DBG_ERROR, + "Unable to adjust backlight power, error code %i\n", + ret); + + if (use_alt_lcd_levels) + ret = set_lcd_level_alt(b->props.brightness); + else + ret = set_lcd_level(b->props.brightness); + if (ret != 0) + vdbg_printk(FUJLAPTOP_DBG_ERROR, + "Unable to adjust LCD brightness, error code %i\n", + ret); + return ret; +} + +static struct backlight_ops fujitsubl_ops = { + .get_brightness = bl_get_brightness, + .update_status = bl_update_status, +}; + +/* Platform LCD brightness device */ + +static ssize_t +show_max_brightness(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + int ret; + + ret = get_max_brightness(); + if (ret < 0) + return ret; + + return sprintf(buf, "%i\n", ret); +} + +static ssize_t +show_brightness_changed(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + int ret; + + ret = fujitsu->brightness_changed; + if (ret < 0) + return ret; + + return sprintf(buf, "%i\n", ret); +} + +static ssize_t show_lcd_level(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + int ret; + + ret = get_lcd_level(); + if (ret < 0) + return ret; + + return sprintf(buf, "%i\n", ret); +} + +static ssize_t store_lcd_level(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + + int level, ret; + + if (sscanf(buf, "%i", &level) != 1 + || (level < 0 || level >= fujitsu->max_brightness)) + return -EINVAL; + + if (use_alt_lcd_levels) + ret = set_lcd_level_alt(level); + else + ret = set_lcd_level(level); + if (ret < 0) + return ret; + + ret = get_lcd_level(); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t +ignore_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + return count; +} + +static ssize_t +show_lid_state(struct device *dev, + struct device_attribute *attr, char *buf) +{ + if (fujitsu_hotkey->rfkill_state == UNSUPPORTED_CMD) + return sprintf(buf, "unknown\n"); + if (fujitsu_hotkey->rfkill_state & 0x100) + return sprintf(buf, "open\n"); + else + return sprintf(buf, "closed\n"); +} + +static ssize_t +show_dock_state(struct device *dev, + struct device_attribute *attr, char *buf) +{ + if (fujitsu_hotkey->rfkill_state == UNSUPPORTED_CMD) + return sprintf(buf, "unknown\n"); + if (fujitsu_hotkey->rfkill_state & 0x200) + return sprintf(buf, "docked\n"); + else + return sprintf(buf, "undocked\n"); +} + +static ssize_t +show_radios_state(struct device *dev, + struct device_attribute *attr, char *buf) +{ + if (fujitsu_hotkey->rfkill_state == UNSUPPORTED_CMD) + return sprintf(buf, "unknown\n"); + if (fujitsu_hotkey->rfkill_state & 0x20) + return sprintf(buf, "on\n"); + else + return sprintf(buf, "killed\n"); +} + +static DEVICE_ATTR(max_brightness, 0444, show_max_brightness, ignore_store); +static DEVICE_ATTR(brightness_changed, 0444, show_brightness_changed, + ignore_store); +static DEVICE_ATTR(lcd_level, 0644, show_lcd_level, store_lcd_level); +static DEVICE_ATTR(lid, 0444, show_lid_state, ignore_store); +static DEVICE_ATTR(dock, 0444, show_dock_state, ignore_store); +static DEVICE_ATTR(radios, 0444, show_radios_state, ignore_store); + +static struct attribute *fujitsupf_attributes[] = { + &dev_attr_brightness_changed.attr, + &dev_attr_max_brightness.attr, + &dev_attr_lcd_level.attr, + &dev_attr_lid.attr, + &dev_attr_dock.attr, + &dev_attr_radios.attr, + NULL +}; + +static struct attribute_group fujitsupf_attribute_group = { + .attrs = fujitsupf_attributes +}; + +static struct platform_driver fujitsupf_driver = { + .driver = { + .name = "fujitsu-laptop", + .owner = THIS_MODULE, + } +}; + +static void dmi_check_cb_common(const struct dmi_system_id *id) +{ + acpi_handle handle; + printk(KERN_INFO "fujitsu-laptop: Identified laptop model '%s'.\n", + id->ident); + if (use_alt_lcd_levels == -1) { + if (ACPI_SUCCESS(acpi_get_handle(NULL, + "\\_SB.PCI0.LPCB.FJEX.SBL2", &handle))) + use_alt_lcd_levels = 1; + else + use_alt_lcd_levels = 0; + vdbg_printk(FUJLAPTOP_DBG_TRACE, "auto-detected usealt as " + "%i\n", use_alt_lcd_levels); + } +} + +static int dmi_check_cb_s6410(const struct dmi_system_id *id) +{ + dmi_check_cb_common(id); + fujitsu->keycode1 = KEY_SCREENLOCK; /* "Lock" */ + fujitsu->keycode2 = KEY_HELP; /* "Mobility Center" */ + return 0; +} + +static int dmi_check_cb_s6420(const struct dmi_system_id *id) +{ + dmi_check_cb_common(id); + fujitsu->keycode1 = KEY_SCREENLOCK; /* "Lock" */ + fujitsu->keycode2 = KEY_HELP; /* "Mobility Center" */ + return 0; +} + +static int dmi_check_cb_p8010(const struct dmi_system_id *id) +{ + dmi_check_cb_common(id); + fujitsu->keycode1 = KEY_HELP; /* "Support" */ + fujitsu->keycode3 = KEY_SWITCHVIDEOMODE; /* "Presentation" */ + fujitsu->keycode4 = KEY_WWW; /* "Internet" */ + return 0; +} + +static struct dmi_system_id fujitsu_dmi_table[] = { + { + .ident = "Fujitsu Siemens S6410", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU SIEMENS"), + DMI_MATCH(DMI_PRODUCT_NAME, "LIFEBOOK S6410"), + }, + .callback = dmi_check_cb_s6410}, + { + .ident = "Fujitsu Siemens S6420", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU SIEMENS"), + DMI_MATCH(DMI_PRODUCT_NAME, "LIFEBOOK S6420"), + }, + .callback = dmi_check_cb_s6420}, + { + .ident = "Fujitsu LifeBook P8010", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"), + DMI_MATCH(DMI_PRODUCT_NAME, "LifeBook P8010"), + }, + .callback = dmi_check_cb_p8010}, + {} +}; + +/* ACPI device for LCD brightness control */ + +static int acpi_fujitsu_add(struct acpi_device *device) +{ + acpi_status status; + acpi_handle handle; + int result = 0; + int state = 0; + struct input_dev *input; + int error; + + if (!device) + return -EINVAL; + + fujitsu->acpi_handle = device->handle; + sprintf(acpi_device_name(device), "%s", ACPI_FUJITSU_DEVICE_NAME); + sprintf(acpi_device_class(device), "%s", ACPI_FUJITSU_CLASS); + device->driver_data = fujitsu; + + status = acpi_install_notify_handler(device->handle, + ACPI_DEVICE_NOTIFY, + acpi_fujitsu_notify, fujitsu); + + if (ACPI_FAILURE(status)) { + printk(KERN_ERR "Error installing notify handler\n"); + error = -ENODEV; + goto err_stop; + } + + fujitsu->input = input = input_allocate_device(); + if (!input) { + error = -ENOMEM; + goto err_uninstall_notify; + } + + snprintf(fujitsu->phys, sizeof(fujitsu->phys), + "%s/video/input0", acpi_device_hid(device)); + + input->name = acpi_device_name(device); + input->phys = fujitsu->phys; + input->id.bustype = BUS_HOST; + input->id.product = 0x06; + input->dev.parent = &device->dev; + input->evbit[0] = BIT(EV_KEY); + set_bit(KEY_BRIGHTNESSUP, input->keybit); + set_bit(KEY_BRIGHTNESSDOWN, input->keybit); + set_bit(KEY_UNKNOWN, input->keybit); + + error = input_register_device(input); + if (error) + goto err_free_input_dev; + + result = acpi_bus_get_power(fujitsu->acpi_handle, &state); + if (result) { + printk(KERN_ERR "Error reading power state\n"); + goto end; + } + + printk(KERN_INFO PREFIX "%s [%s] (%s)\n", + acpi_device_name(device), acpi_device_bid(device), + !device->power.state ? "on" : "off"); + + fujitsu->dev = device; + + if (ACPI_SUCCESS + (acpi_get_handle(device->handle, METHOD_NAME__INI, &handle))) { + vdbg_printk(FUJLAPTOP_DBG_INFO, "Invoking _INI\n"); + if (ACPI_FAILURE + (acpi_evaluate_object + (device->handle, METHOD_NAME__INI, NULL, NULL))) + printk(KERN_ERR "_INI Method failed\n"); + } + + /* do config (detect defaults) */ + use_alt_lcd_levels = use_alt_lcd_levels == 1 ? 1 : 0; + disable_brightness_adjust = disable_brightness_adjust == 1 ? 1 : 0; + vdbg_printk(FUJLAPTOP_DBG_INFO, + "config: [alt interface: %d], [adjust disable: %d]\n", + use_alt_lcd_levels, disable_brightness_adjust); + + if (get_max_brightness() <= 0) + fujitsu->max_brightness = FUJITSU_LCD_N_LEVELS; + get_lcd_level(); + + return result; + +end: +err_free_input_dev: + input_free_device(input); +err_uninstall_notify: + acpi_remove_notify_handler(device->handle, ACPI_DEVICE_NOTIFY, + acpi_fujitsu_notify); +err_stop: + + return result; +} + +static int acpi_fujitsu_remove(struct acpi_device *device, int type) +{ + acpi_status status; + struct fujitsu_t *fujitsu = NULL; + + if (!device || !acpi_driver_data(device)) + return -EINVAL; + + fujitsu = acpi_driver_data(device); + + status = acpi_remove_notify_handler(fujitsu->acpi_handle, + ACPI_DEVICE_NOTIFY, + acpi_fujitsu_notify); + + if (!device || !acpi_driver_data(device)) + return -EINVAL; + + fujitsu->acpi_handle = NULL; + + return 0; +} + +/* Brightness notify */ + +static void acpi_fujitsu_notify(acpi_handle handle, u32 event, void *data) +{ + struct input_dev *input; + int keycode; + int oldb, newb; + + input = fujitsu->input; + + switch (event) { + case ACPI_FUJITSU_NOTIFY_CODE1: + keycode = 0; + oldb = fujitsu->brightness_level; + get_lcd_level(); + newb = fujitsu->brightness_level; + + vdbg_printk(FUJLAPTOP_DBG_TRACE, + "brightness button event [%i -> %i (%i)]\n", + oldb, newb, fujitsu->brightness_changed); + + if (oldb < newb) { + if (disable_brightness_adjust != 1) { + if (use_alt_lcd_levels) + set_lcd_level_alt(newb); + else + set_lcd_level(newb); + } + acpi_bus_generate_proc_event(fujitsu->dev, + ACPI_VIDEO_NOTIFY_INC_BRIGHTNESS, 0); + keycode = KEY_BRIGHTNESSUP; + } else if (oldb > newb) { + if (disable_brightness_adjust != 1) { + if (use_alt_lcd_levels) + set_lcd_level_alt(newb); + else + set_lcd_level(newb); + } + acpi_bus_generate_proc_event(fujitsu->dev, + ACPI_VIDEO_NOTIFY_DEC_BRIGHTNESS, 0); + keycode = KEY_BRIGHTNESSDOWN; + } + break; + default: + keycode = KEY_UNKNOWN; + vdbg_printk(FUJLAPTOP_DBG_WARN, + "unsupported event [0x%x]\n", event); + break; + } + + if (keycode != 0) { + input_report_key(input, keycode, 1); + input_sync(input); + input_report_key(input, keycode, 0); + input_sync(input); + } + + return; +} + +/* ACPI device for hotkey handling */ + +static int acpi_fujitsu_hotkey_add(struct acpi_device *device) +{ + acpi_status status; + acpi_handle handle; + int result = 0; + int state = 0; + struct input_dev *input; + int error; + int i; + + if (!device) + return -EINVAL; + + fujitsu_hotkey->acpi_handle = device->handle; + sprintf(acpi_device_name(device), "%s", + ACPI_FUJITSU_HOTKEY_DEVICE_NAME); + sprintf(acpi_device_class(device), "%s", ACPI_FUJITSU_CLASS); + device->driver_data = fujitsu_hotkey; + + status = acpi_install_notify_handler(device->handle, + ACPI_DEVICE_NOTIFY, + acpi_fujitsu_hotkey_notify, + fujitsu_hotkey); + + if (ACPI_FAILURE(status)) { + printk(KERN_ERR "Error installing notify handler\n"); + error = -ENODEV; + goto err_stop; + } + + /* kfifo */ + spin_lock_init(&fujitsu_hotkey->fifo_lock); + fujitsu_hotkey->fifo = + kfifo_alloc(RINGBUFFERSIZE * sizeof(int), GFP_KERNEL, + &fujitsu_hotkey->fifo_lock); + if (IS_ERR(fujitsu_hotkey->fifo)) { + printk(KERN_ERR "kfifo_alloc failed\n"); + error = PTR_ERR(fujitsu_hotkey->fifo); + goto err_stop; + } + + fujitsu_hotkey->input = input = input_allocate_device(); + if (!input) { + error = -ENOMEM; + goto err_uninstall_notify; + } + + snprintf(fujitsu_hotkey->phys, sizeof(fujitsu_hotkey->phys), + "%s/video/input0", acpi_device_hid(device)); + + input->name = acpi_device_name(device); + input->phys = fujitsu_hotkey->phys; + input->id.bustype = BUS_HOST; + input->id.product = 0x06; + input->dev.parent = &device->dev; + + set_bit(EV_KEY, input->evbit); + set_bit(fujitsu->keycode1, input->keybit); + set_bit(fujitsu->keycode2, input->keybit); + set_bit(fujitsu->keycode3, input->keybit); + set_bit(fujitsu->keycode4, input->keybit); + set_bit(KEY_UNKNOWN, input->keybit); + + error = input_register_device(input); + if (error) + goto err_free_input_dev; + + result = acpi_bus_get_power(fujitsu_hotkey->acpi_handle, &state); + if (result) { + printk(KERN_ERR "Error reading power state\n"); + goto end; + } + + printk(KERN_INFO PREFIX "%s [%s] (%s)\n", + acpi_device_name(device), acpi_device_bid(device), + !device->power.state ? "on" : "off"); + + fujitsu_hotkey->dev = device; + + if (ACPI_SUCCESS + (acpi_get_handle(device->handle, METHOD_NAME__INI, &handle))) { + vdbg_printk(FUJLAPTOP_DBG_INFO, "Invoking _INI\n"); + if (ACPI_FAILURE + (acpi_evaluate_object + (device->handle, METHOD_NAME__INI, NULL, NULL))) + printk(KERN_ERR "_INI Method failed\n"); + } + + i = 0; + while (call_fext_func(FUNC_BUTTONS, 0x1, 0x0, 0x0) != 0 + && (i++) < MAX_HOTKEY_RINGBUFFER_SIZE) + ; /* No action, result is discarded */ + vdbg_printk(FUJLAPTOP_DBG_INFO, "Discarded %i ringbuffer entries\n", i); + + fujitsu_hotkey->rfkill_state = + call_fext_func(FUNC_RFKILL, 0x4, 0x0, 0x0); + + /* Suspect this is a keymap of the application panel, print it */ + printk(KERN_INFO "fujitsu-laptop: BTNI: [0x%x]\n", + call_fext_func(FUNC_BUTTONS, 0x0, 0x0, 0x0)); + + #ifdef CONFIG_LEDS_CLASS + if (call_fext_func(FUNC_LEDS, 0x0, 0x0, 0x0) & LOGOLAMP_POWERON) { + result = led_classdev_register(&fujitsu->pf_device->dev, + &logolamp_led); + if (result == 0) { + fujitsu_hotkey->logolamp_registered = 1; + } else { + printk(KERN_ERR "fujitsu-laptop: Could not register " + "LED handler for logo lamp, error %i\n", result); + } + } + + if ((call_fext_func(FUNC_LEDS, 0x0, 0x0, 0x0) & KEYBOARD_LAMPS) && + (call_fext_func(FUNC_BUTTONS, 0x0, 0x0, 0x0) == 0x0)) { + result = led_classdev_register(&fujitsu->pf_device->dev, + &kblamps_led); + if (result == 0) { + fujitsu_hotkey->kblamps_registered = 1; + } else { + printk(KERN_ERR "fujitsu-laptop: Could not register " + "LED handler for keyboard lamps, error %i\n", result); + } + } + #endif + + return result; + +end: +err_free_input_dev: + input_free_device(input); +err_uninstall_notify: + acpi_remove_notify_handler(device->handle, ACPI_DEVICE_NOTIFY, + acpi_fujitsu_hotkey_notify); + kfifo_free(fujitsu_hotkey->fifo); +err_stop: + + return result; +} + +static int acpi_fujitsu_hotkey_remove(struct acpi_device *device, int type) +{ + acpi_status status; + struct fujitsu_hotkey_t *fujitsu_hotkey = NULL; + + if (!device || !acpi_driver_data(device)) + return -EINVAL; + + fujitsu_hotkey = acpi_driver_data(device); + + status = acpi_remove_notify_handler(fujitsu_hotkey->acpi_handle, + ACPI_DEVICE_NOTIFY, + acpi_fujitsu_hotkey_notify); + + fujitsu_hotkey->acpi_handle = NULL; + + kfifo_free(fujitsu_hotkey->fifo); + + return 0; +} + +static void acpi_fujitsu_hotkey_notify(acpi_handle handle, u32 event, + void *data) +{ + struct input_dev *input; + int keycode, keycode_r; + unsigned int irb = 1; + int i, status; + + input = fujitsu_hotkey->input; + + fujitsu_hotkey->rfkill_state = + call_fext_func(FUNC_RFKILL, 0x4, 0x0, 0x0); + + switch (event) { + case ACPI_FUJITSU_NOTIFY_CODE1: + i = 0; + while ((irb = + call_fext_func(FUNC_BUTTONS, 0x1, 0x0, 0x0)) != 0 + && (i++) < MAX_HOTKEY_RINGBUFFER_SIZE) { + switch (irb & 0x4ff) { + case KEY1_CODE: + keycode = fujitsu->keycode1; + break; + case KEY2_CODE: + keycode = fujitsu->keycode2; + break; + case KEY3_CODE: + keycode = fujitsu->keycode3; + break; + case KEY4_CODE: + keycode = fujitsu->keycode4; + break; + case 0: + keycode = 0; + break; + default: + vdbg_printk(FUJLAPTOP_DBG_WARN, + "Unknown GIRB result [%x]\n", irb); + keycode = -1; + break; + } + if (keycode > 0) { + vdbg_printk(FUJLAPTOP_DBG_TRACE, + "Push keycode into ringbuffer [%d]\n", + keycode); + status = kfifo_put(fujitsu_hotkey->fifo, + (unsigned char *)&keycode, + sizeof(keycode)); + if (status != sizeof(keycode)) { + vdbg_printk(FUJLAPTOP_DBG_WARN, + "Could not push keycode [0x%x]\n", + keycode); + } else { + input_report_key(input, keycode, 1); + input_sync(input); + } + } else if (keycode == 0) { + while ((status = + kfifo_get + (fujitsu_hotkey->fifo, (unsigned char *) + &keycode_r, + sizeof + (keycode_r))) == sizeof(keycode_r)) { + input_report_key(input, keycode_r, 0); + input_sync(input); + vdbg_printk(FUJLAPTOP_DBG_TRACE, + "Pop keycode from ringbuffer [%d]\n", + keycode_r); + } + } + } + + break; + default: + keycode = KEY_UNKNOWN; + vdbg_printk(FUJLAPTOP_DBG_WARN, + "Unsupported event [0x%x]\n", event); + input_report_key(input, keycode, 1); + input_sync(input); + input_report_key(input, keycode, 0); + input_sync(input); + break; + } + + return; +} + +/* Initialization */ + +static const struct acpi_device_id fujitsu_device_ids[] = { + {ACPI_FUJITSU_HID, 0}, + {"", 0}, +}; + +static struct acpi_driver acpi_fujitsu_driver = { + .name = ACPI_FUJITSU_DRIVER_NAME, + .class = ACPI_FUJITSU_CLASS, + .ids = fujitsu_device_ids, + .ops = { + .add = acpi_fujitsu_add, + .remove = acpi_fujitsu_remove, + }, +}; + +static const struct acpi_device_id fujitsu_hotkey_device_ids[] = { + {ACPI_FUJITSU_HOTKEY_HID, 0}, + {"", 0}, +}; + +static struct acpi_driver acpi_fujitsu_hotkey_driver = { + .name = ACPI_FUJITSU_HOTKEY_DRIVER_NAME, + .class = ACPI_FUJITSU_CLASS, + .ids = fujitsu_hotkey_device_ids, + .ops = { + .add = acpi_fujitsu_hotkey_add, + .remove = acpi_fujitsu_hotkey_remove, + }, +}; + +static int __init fujitsu_init(void) +{ + int ret, result, max_brightness; + + if (acpi_disabled) + return -ENODEV; + + fujitsu = kmalloc(sizeof(struct fujitsu_t), GFP_KERNEL); + if (!fujitsu) + return -ENOMEM; + memset(fujitsu, 0, sizeof(struct fujitsu_t)); + fujitsu->keycode1 = KEY_PROG1; + fujitsu->keycode2 = KEY_PROG2; + fujitsu->keycode3 = KEY_PROG3; + fujitsu->keycode4 = KEY_PROG4; + dmi_check_system(fujitsu_dmi_table); + + result = acpi_bus_register_driver(&acpi_fujitsu_driver); + if (result < 0) { + ret = -ENODEV; + goto fail_acpi; + } + + /* Register platform stuff */ + + fujitsu->pf_device = platform_device_alloc("fujitsu-laptop", -1); + if (!fujitsu->pf_device) { + ret = -ENOMEM; + goto fail_platform_driver; + } + + ret = platform_device_add(fujitsu->pf_device); + if (ret) + goto fail_platform_device1; + + ret = + sysfs_create_group(&fujitsu->pf_device->dev.kobj, + &fujitsupf_attribute_group); + if (ret) + goto fail_platform_device2; + + /* Register backlight stuff */ + + if (!acpi_video_backlight_support()) { + fujitsu->bl_device = + backlight_device_register("fujitsu-laptop", NULL, NULL, + &fujitsubl_ops); + if (IS_ERR(fujitsu->bl_device)) + return PTR_ERR(fujitsu->bl_device); + max_brightness = fujitsu->max_brightness; + fujitsu->bl_device->props.max_brightness = max_brightness - 1; + fujitsu->bl_device->props.brightness = fujitsu->brightness_level; + } + + ret = platform_driver_register(&fujitsupf_driver); + if (ret) + goto fail_backlight; + + /* Register hotkey driver */ + + fujitsu_hotkey = kmalloc(sizeof(struct fujitsu_hotkey_t), GFP_KERNEL); + if (!fujitsu_hotkey) { + ret = -ENOMEM; + goto fail_hotkey; + } + memset(fujitsu_hotkey, 0, sizeof(struct fujitsu_hotkey_t)); + + result = acpi_bus_register_driver(&acpi_fujitsu_hotkey_driver); + if (result < 0) { + ret = -ENODEV; + goto fail_hotkey1; + } + + /* Sync backlight power status (needs FUJ02E3 device, hence deferred) */ + + if (!acpi_video_backlight_support()) { + if (call_fext_func(FUNC_BACKLIGHT, 0x2, 0x4, 0x0) == 3) + fujitsu->bl_device->props.power = 4; + else + fujitsu->bl_device->props.power = 0; + } + + printk(KERN_INFO "fujitsu-laptop: driver " FUJITSU_DRIVER_VERSION + " successfully loaded.\n"); + + return 0; + +fail_hotkey1: + + kfree(fujitsu_hotkey); + +fail_hotkey: + + platform_driver_unregister(&fujitsupf_driver); + +fail_backlight: + + if (fujitsu->bl_device) + backlight_device_unregister(fujitsu->bl_device); + +fail_platform_device2: + + platform_device_del(fujitsu->pf_device); + +fail_platform_device1: + + platform_device_put(fujitsu->pf_device); + +fail_platform_driver: + + acpi_bus_unregister_driver(&acpi_fujitsu_driver); + +fail_acpi: + + kfree(fujitsu); + + return ret; +} + +static void __exit fujitsu_cleanup(void) +{ + #ifdef CONFIG_LEDS_CLASS + if (fujitsu_hotkey->logolamp_registered != 0) + led_classdev_unregister(&logolamp_led); + + if (fujitsu_hotkey->kblamps_registered != 0) + led_classdev_unregister(&kblamps_led); + #endif + + sysfs_remove_group(&fujitsu->pf_device->dev.kobj, + &fujitsupf_attribute_group); + platform_device_unregister(fujitsu->pf_device); + platform_driver_unregister(&fujitsupf_driver); + if (fujitsu->bl_device) + backlight_device_unregister(fujitsu->bl_device); + + acpi_bus_unregister_driver(&acpi_fujitsu_driver); + + kfree(fujitsu); + + acpi_bus_unregister_driver(&acpi_fujitsu_hotkey_driver); + + kfree(fujitsu_hotkey); + + printk(KERN_INFO "fujitsu-laptop: driver unloaded.\n"); +} + +module_init(fujitsu_init); +module_exit(fujitsu_cleanup); + +module_param(use_alt_lcd_levels, uint, 0644); +MODULE_PARM_DESC(use_alt_lcd_levels, + "Use alternative interface for lcd_levels (needed for Lifebook s6410)."); +module_param(disable_brightness_adjust, uint, 0644); +MODULE_PARM_DESC(disable_brightness_adjust, "Disable brightness adjustment ."); +#ifdef CONFIG_FUJITSU_LAPTOP_DEBUG +module_param_named(debug, dbg_level, uint, 0644); +MODULE_PARM_DESC(debug, "Sets debug level bit-mask"); +#endif + +MODULE_AUTHOR("Jonathan Woithe, Peter Gruber, Tony Vroon"); +MODULE_DESCRIPTION("Fujitsu laptop extras support"); +MODULE_VERSION(FUJITSU_DRIVER_VERSION); +MODULE_LICENSE("GPL"); + +MODULE_ALIAS("dmi:*:svnFUJITSUSIEMENS:*:pvr:rvnFUJITSU:rnFJNB1D3:*:cvrS6410:*"); +MODULE_ALIAS("dmi:*:svnFUJITSUSIEMENS:*:pvr:rvnFUJITSU:rnFJNB1E6:*:cvrS6420:*"); +MODULE_ALIAS("dmi:*:svnFUJITSU:*:pvr:rvnFUJITSU:rnFJNB19C:*:cvrS7020:*"); + +static struct pnp_device_id pnp_ids[] = { + {.id = "FUJ02bf"}, + {.id = "FUJ02B1"}, + {.id = "FUJ02E3"}, + {.id = ""} +}; + +MODULE_DEVICE_TABLE(pnp, pnp_ids); diff --git a/drivers/platform/x86/hp-wmi.c b/drivers/platform/x86/hp-wmi.c new file mode 100644 index 000000000000..4b7c24c519c3 --- /dev/null +++ b/drivers/platform/x86/hp-wmi.c @@ -0,0 +1,512 @@ +/* + * HP WMI hotkeys + * + * Copyright (C) 2008 Red Hat <mjg@redhat.com> + * + * Portions based on wistron_btns.c: + * Copyright (C) 2005 Miloslav Trmac <mitr@volny.cz> + * Copyright (C) 2005 Bernhard Rosenkraenzer <bero@arklinux.org> + * Copyright (C) 2005 Dmitry Torokhov <dtor@mail.ru> + * + * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/types.h> +#include <linux/input.h> +#include <acpi/acpi_drivers.h> +#include <linux/platform_device.h> +#include <linux/acpi.h> +#include <linux/rfkill.h> +#include <linux/string.h> + +MODULE_AUTHOR("Matthew Garrett <mjg59@srcf.ucam.org>"); +MODULE_DESCRIPTION("HP laptop WMI hotkeys driver"); +MODULE_LICENSE("GPL"); + +MODULE_ALIAS("wmi:95F24279-4D7B-4334-9387-ACCDC67EF61C"); +MODULE_ALIAS("wmi:5FB7F034-2C63-45e9-BE91-3D44E2C707E4"); + +#define HPWMI_EVENT_GUID "95F24279-4D7B-4334-9387-ACCDC67EF61C" +#define HPWMI_BIOS_GUID "5FB7F034-2C63-45e9-BE91-3D44E2C707E4" + +#define HPWMI_DISPLAY_QUERY 0x1 +#define HPWMI_HDDTEMP_QUERY 0x2 +#define HPWMI_ALS_QUERY 0x3 +#define HPWMI_DOCK_QUERY 0x4 +#define HPWMI_WIRELESS_QUERY 0x5 +#define HPWMI_HOTKEY_QUERY 0xc + +static int __init hp_wmi_bios_setup(struct platform_device *device); +static int __exit hp_wmi_bios_remove(struct platform_device *device); + +struct bios_args { + u32 signature; + u32 command; + u32 commandtype; + u32 datasize; + u32 data; +}; + +struct bios_return { + u32 sigpass; + u32 return_code; + u32 value; +}; + +struct key_entry { + char type; /* See KE_* below */ + u16 code; + u16 keycode; +}; + +enum { KE_KEY, KE_SW, KE_END }; + +static struct key_entry hp_wmi_keymap[] = { + {KE_SW, 0x01, SW_DOCK}, + {KE_KEY, 0x02, KEY_BRIGHTNESSUP}, + {KE_KEY, 0x03, KEY_BRIGHTNESSDOWN}, + {KE_KEY, 0x20e6, KEY_PROG1}, + {KE_KEY, 0x2142, KEY_MEDIA}, + {KE_KEY, 0x213b, KEY_INFO}, + {KE_KEY, 0x231b, KEY_HELP}, + {KE_END, 0} +}; + +static struct input_dev *hp_wmi_input_dev; +static struct platform_device *hp_wmi_platform_dev; + +static struct rfkill *wifi_rfkill; +static struct rfkill *bluetooth_rfkill; +static struct rfkill *wwan_rfkill; + +static struct platform_driver hp_wmi_driver = { + .driver = { + .name = "hp-wmi", + .owner = THIS_MODULE, + }, + .probe = hp_wmi_bios_setup, + .remove = hp_wmi_bios_remove, +}; + +static int hp_wmi_perform_query(int query, int write, int value) +{ + struct bios_return bios_return; + acpi_status status; + union acpi_object *obj; + struct bios_args args = { + .signature = 0x55434553, + .command = write ? 0x2 : 0x1, + .commandtype = query, + .datasize = write ? 0x4 : 0, + .data = value, + }; + struct acpi_buffer input = { sizeof(struct bios_args), &args }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + + status = wmi_evaluate_method(HPWMI_BIOS_GUID, 0, 0x3, &input, &output); + + obj = output.pointer; + + if (!obj || obj->type != ACPI_TYPE_BUFFER) + return -EINVAL; + + bios_return = *((struct bios_return *)obj->buffer.pointer); + if (bios_return.return_code > 0) + return bios_return.return_code * -1; + else + return bios_return.value; +} + +static int hp_wmi_display_state(void) +{ + return hp_wmi_perform_query(HPWMI_DISPLAY_QUERY, 0, 0); +} + +static int hp_wmi_hddtemp_state(void) +{ + return hp_wmi_perform_query(HPWMI_HDDTEMP_QUERY, 0, 0); +} + +static int hp_wmi_als_state(void) +{ + return hp_wmi_perform_query(HPWMI_ALS_QUERY, 0, 0); +} + +static int hp_wmi_dock_state(void) +{ + return hp_wmi_perform_query(HPWMI_DOCK_QUERY, 0, 0); +} + +static int hp_wmi_wifi_set(void *data, enum rfkill_state state) +{ + if (state) + return hp_wmi_perform_query(HPWMI_WIRELESS_QUERY, 1, 0x101); + else + return hp_wmi_perform_query(HPWMI_WIRELESS_QUERY, 1, 0x100); +} + +static int hp_wmi_bluetooth_set(void *data, enum rfkill_state state) +{ + if (state) + return hp_wmi_perform_query(HPWMI_WIRELESS_QUERY, 1, 0x202); + else + return hp_wmi_perform_query(HPWMI_WIRELESS_QUERY, 1, 0x200); +} + +static int hp_wmi_wwan_set(void *data, enum rfkill_state state) +{ + if (state) + return hp_wmi_perform_query(HPWMI_WIRELESS_QUERY, 1, 0x404); + else + return hp_wmi_perform_query(HPWMI_WIRELESS_QUERY, 1, 0x400); +} + +static int hp_wmi_wifi_state(void) +{ + int wireless = hp_wmi_perform_query(HPWMI_WIRELESS_QUERY, 0, 0); + + if (wireless & 0x100) + return RFKILL_STATE_UNBLOCKED; + else + return RFKILL_STATE_SOFT_BLOCKED; +} + +static int hp_wmi_bluetooth_state(void) +{ + int wireless = hp_wmi_perform_query(HPWMI_WIRELESS_QUERY, 0, 0); + + if (wireless & 0x10000) + return RFKILL_STATE_UNBLOCKED; + else + return RFKILL_STATE_SOFT_BLOCKED; +} + +static int hp_wmi_wwan_state(void) +{ + int wireless = hp_wmi_perform_query(HPWMI_WIRELESS_QUERY, 0, 0); + + if (wireless & 0x1000000) + return RFKILL_STATE_UNBLOCKED; + else + return RFKILL_STATE_SOFT_BLOCKED; +} + +static ssize_t show_display(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int value = hp_wmi_display_state(); + if (value < 0) + return -EINVAL; + return sprintf(buf, "%d\n", value); +} + +static ssize_t show_hddtemp(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int value = hp_wmi_hddtemp_state(); + if (value < 0) + return -EINVAL; + return sprintf(buf, "%d\n", value); +} + +static ssize_t show_als(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int value = hp_wmi_als_state(); + if (value < 0) + return -EINVAL; + return sprintf(buf, "%d\n", value); +} + +static ssize_t show_dock(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int value = hp_wmi_dock_state(); + if (value < 0) + return -EINVAL; + return sprintf(buf, "%d\n", value); +} + +static ssize_t set_als(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + u32 tmp = simple_strtoul(buf, NULL, 10); + hp_wmi_perform_query(HPWMI_ALS_QUERY, 1, tmp); + return count; +} + +static DEVICE_ATTR(display, S_IRUGO, show_display, NULL); +static DEVICE_ATTR(hddtemp, S_IRUGO, show_hddtemp, NULL); +static DEVICE_ATTR(als, S_IRUGO | S_IWUSR, show_als, set_als); +static DEVICE_ATTR(dock, S_IRUGO, show_dock, NULL); + +static struct key_entry *hp_wmi_get_entry_by_scancode(int code) +{ + struct key_entry *key; + + for (key = hp_wmi_keymap; key->type != KE_END; key++) + if (code == key->code) + return key; + + return NULL; +} + +static struct key_entry *hp_wmi_get_entry_by_keycode(int keycode) +{ + struct key_entry *key; + + for (key = hp_wmi_keymap; key->type != KE_END; key++) + if (key->type == KE_KEY && keycode == key->keycode) + return key; + + return NULL; +} + +static int hp_wmi_getkeycode(struct input_dev *dev, int scancode, int *keycode) +{ + struct key_entry *key = hp_wmi_get_entry_by_scancode(scancode); + + if (key && key->type == KE_KEY) { + *keycode = key->keycode; + return 0; + } + + return -EINVAL; +} + +static int hp_wmi_setkeycode(struct input_dev *dev, int scancode, int keycode) +{ + struct key_entry *key; + int old_keycode; + + if (keycode < 0 || keycode > KEY_MAX) + return -EINVAL; + + key = hp_wmi_get_entry_by_scancode(scancode); + if (key && key->type == KE_KEY) { + old_keycode = key->keycode; + key->keycode = keycode; + set_bit(keycode, dev->keybit); + if (!hp_wmi_get_entry_by_keycode(old_keycode)) + clear_bit(old_keycode, dev->keybit); + return 0; + } + + return -EINVAL; +} + +static void hp_wmi_notify(u32 value, void *context) +{ + struct acpi_buffer response = { ACPI_ALLOCATE_BUFFER, NULL }; + static struct key_entry *key; + union acpi_object *obj; + + wmi_get_event_data(value, &response); + + obj = (union acpi_object *)response.pointer; + + if (obj && obj->type == ACPI_TYPE_BUFFER && obj->buffer.length == 8) { + int eventcode = *((u8 *) obj->buffer.pointer); + if (eventcode == 0x4) + eventcode = hp_wmi_perform_query(HPWMI_HOTKEY_QUERY, 0, + 0); + key = hp_wmi_get_entry_by_scancode(eventcode); + if (key) { + switch (key->type) { + case KE_KEY: + input_report_key(hp_wmi_input_dev, + key->keycode, 1); + input_sync(hp_wmi_input_dev); + input_report_key(hp_wmi_input_dev, + key->keycode, 0); + input_sync(hp_wmi_input_dev); + break; + case KE_SW: + input_report_switch(hp_wmi_input_dev, + key->keycode, + hp_wmi_dock_state()); + input_sync(hp_wmi_input_dev); + break; + } + } else if (eventcode == 0x5) { + if (wifi_rfkill) + rfkill_force_state(wifi_rfkill, + hp_wmi_wifi_state()); + if (bluetooth_rfkill) + rfkill_force_state(bluetooth_rfkill, + hp_wmi_bluetooth_state()); + if (wwan_rfkill) + rfkill_force_state(wwan_rfkill, + hp_wmi_wwan_state()); + } else + printk(KERN_INFO "HP WMI: Unknown key pressed - %x\n", + eventcode); + } else + printk(KERN_INFO "HP WMI: Unknown response received\n"); +} + +static int __init hp_wmi_input_setup(void) +{ + struct key_entry *key; + int err; + + hp_wmi_input_dev = input_allocate_device(); + + hp_wmi_input_dev->name = "HP WMI hotkeys"; + hp_wmi_input_dev->phys = "wmi/input0"; + hp_wmi_input_dev->id.bustype = BUS_HOST; + hp_wmi_input_dev->getkeycode = hp_wmi_getkeycode; + hp_wmi_input_dev->setkeycode = hp_wmi_setkeycode; + + for (key = hp_wmi_keymap; key->type != KE_END; key++) { + switch (key->type) { + case KE_KEY: + set_bit(EV_KEY, hp_wmi_input_dev->evbit); + set_bit(key->keycode, hp_wmi_input_dev->keybit); + break; + case KE_SW: + set_bit(EV_SW, hp_wmi_input_dev->evbit); + set_bit(key->keycode, hp_wmi_input_dev->swbit); + break; + } + } + + err = input_register_device(hp_wmi_input_dev); + + if (err) { + input_free_device(hp_wmi_input_dev); + return err; + } + + return 0; +} + +static void cleanup_sysfs(struct platform_device *device) +{ + device_remove_file(&device->dev, &dev_attr_display); + device_remove_file(&device->dev, &dev_attr_hddtemp); + device_remove_file(&device->dev, &dev_attr_als); + device_remove_file(&device->dev, &dev_attr_dock); +} + +static int __init hp_wmi_bios_setup(struct platform_device *device) +{ + int err; + int wireless = hp_wmi_perform_query(HPWMI_WIRELESS_QUERY, 0, 0); + + err = device_create_file(&device->dev, &dev_attr_display); + if (err) + goto add_sysfs_error; + err = device_create_file(&device->dev, &dev_attr_hddtemp); + if (err) + goto add_sysfs_error; + err = device_create_file(&device->dev, &dev_attr_als); + if (err) + goto add_sysfs_error; + err = device_create_file(&device->dev, &dev_attr_dock); + if (err) + goto add_sysfs_error; + + if (wireless & 0x1) { + wifi_rfkill = rfkill_allocate(&device->dev, RFKILL_TYPE_WLAN); + wifi_rfkill->name = "hp-wifi"; + wifi_rfkill->state = hp_wmi_wifi_state(); + wifi_rfkill->toggle_radio = hp_wmi_wifi_set; + wifi_rfkill->user_claim_unsupported = 1; + rfkill_register(wifi_rfkill); + } + + if (wireless & 0x2) { + bluetooth_rfkill = rfkill_allocate(&device->dev, + RFKILL_TYPE_BLUETOOTH); + bluetooth_rfkill->name = "hp-bluetooth"; + bluetooth_rfkill->state = hp_wmi_bluetooth_state(); + bluetooth_rfkill->toggle_radio = hp_wmi_bluetooth_set; + bluetooth_rfkill->user_claim_unsupported = 1; + rfkill_register(bluetooth_rfkill); + } + + if (wireless & 0x4) { + wwan_rfkill = rfkill_allocate(&device->dev, RFKILL_TYPE_WWAN); + wwan_rfkill->name = "hp-wwan"; + wwan_rfkill->state = hp_wmi_wwan_state(); + wwan_rfkill->toggle_radio = hp_wmi_wwan_set; + wwan_rfkill->user_claim_unsupported = 1; + rfkill_register(wwan_rfkill); + } + + return 0; +add_sysfs_error: + cleanup_sysfs(device); + return err; +} + +static int __exit hp_wmi_bios_remove(struct platform_device *device) +{ + cleanup_sysfs(device); + + if (wifi_rfkill) + rfkill_unregister(wifi_rfkill); + if (bluetooth_rfkill) + rfkill_unregister(bluetooth_rfkill); + if (wwan_rfkill) + rfkill_unregister(wwan_rfkill); + + return 0; +} + +static int __init hp_wmi_init(void) +{ + int err; + + if (wmi_has_guid(HPWMI_EVENT_GUID)) { + err = wmi_install_notify_handler(HPWMI_EVENT_GUID, + hp_wmi_notify, NULL); + if (!err) + hp_wmi_input_setup(); + } + + if (wmi_has_guid(HPWMI_BIOS_GUID)) { + err = platform_driver_register(&hp_wmi_driver); + if (err) + return 0; + hp_wmi_platform_dev = platform_device_alloc("hp-wmi", -1); + if (!hp_wmi_platform_dev) { + platform_driver_unregister(&hp_wmi_driver); + return 0; + } + platform_device_add(hp_wmi_platform_dev); + } + + return 0; +} + +static void __exit hp_wmi_exit(void) +{ + if (wmi_has_guid(HPWMI_EVENT_GUID)) { + wmi_remove_notify_handler(HPWMI_EVENT_GUID); + input_unregister_device(hp_wmi_input_dev); + } + if (hp_wmi_platform_dev) { + platform_device_del(hp_wmi_platform_dev); + platform_driver_unregister(&hp_wmi_driver); + } +} + +module_init(hp_wmi_init); +module_exit(hp_wmi_exit); diff --git a/drivers/platform/x86/intel_menlow.c b/drivers/platform/x86/intel_menlow.c new file mode 100644 index 000000000000..27b7662955bb --- /dev/null +++ b/drivers/platform/x86/intel_menlow.c @@ -0,0 +1,536 @@ +/* + * intel_menlow.c - Intel menlow Driver for thermal management extension + * + * Copyright (C) 2008 Intel Corp + * Copyright (C) 2008 Sujith Thomas <sujith.thomas@intel.com> + * Copyright (C) 2008 Zhang Rui <rui.zhang@intel.com> + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * 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; version 2 of the License. + * + * 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., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * This driver creates the sys I/F for programming the sensors. + * It also implements the driver for intel menlow memory controller (hardware + * id is INT0002) which makes use of the platform specific ACPI methods + * to get/set bandwidth. + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/types.h> +#include <linux/pci.h> +#include <linux/pm.h> + +#include <linux/thermal.h> +#include <acpi/acpi_bus.h> +#include <acpi/acpi_drivers.h> + +MODULE_AUTHOR("Thomas Sujith"); +MODULE_AUTHOR("Zhang Rui"); +MODULE_DESCRIPTION("Intel Menlow platform specific driver"); +MODULE_LICENSE("GPL"); + +/* + * Memory controller device control + */ + +#define MEMORY_GET_BANDWIDTH "GTHS" +#define MEMORY_SET_BANDWIDTH "STHS" +#define MEMORY_ARG_CUR_BANDWIDTH 1 +#define MEMORY_ARG_MAX_BANDWIDTH 0 + +/* + * GTHS returning 'n' would mean that [0,n-1] states are supported + * In that case max_cstate would be n-1 + * GTHS returning '0' would mean that no bandwidth control states are supported + */ +static int memory_get_int_max_bandwidth(struct thermal_cooling_device *cdev, + unsigned long *max_state) +{ + struct acpi_device *device = cdev->devdata; + acpi_handle handle = device->handle; + unsigned long long value; + struct acpi_object_list arg_list; + union acpi_object arg; + acpi_status status = AE_OK; + + arg_list.count = 1; + arg_list.pointer = &arg; + arg.type = ACPI_TYPE_INTEGER; + arg.integer.value = MEMORY_ARG_MAX_BANDWIDTH; + status = acpi_evaluate_integer(handle, MEMORY_GET_BANDWIDTH, + &arg_list, &value); + if (ACPI_FAILURE(status)) + return -EFAULT; + + if (!value) + return -EINVAL; + + *max_state = value - 1; + return 0; +} + +static int memory_get_max_bandwidth(struct thermal_cooling_device *cdev, + char *buf) +{ + unsigned long value; + if (memory_get_int_max_bandwidth(cdev, &value)) + return -EINVAL; + + return sprintf(buf, "%ld\n", value); +} + +static int memory_get_cur_bandwidth(struct thermal_cooling_device *cdev, + char *buf) +{ + struct acpi_device *device = cdev->devdata; + acpi_handle handle = device->handle; + unsigned long long value; + struct acpi_object_list arg_list; + union acpi_object arg; + acpi_status status = AE_OK; + + arg_list.count = 1; + arg_list.pointer = &arg; + arg.type = ACPI_TYPE_INTEGER; + arg.integer.value = MEMORY_ARG_CUR_BANDWIDTH; + status = acpi_evaluate_integer(handle, MEMORY_GET_BANDWIDTH, + &arg_list, &value); + if (ACPI_FAILURE(status)) + return -EFAULT; + + return sprintf(buf, "%llu\n", value); +} + +static int memory_set_cur_bandwidth(struct thermal_cooling_device *cdev, + unsigned int state) +{ + struct acpi_device *device = cdev->devdata; + acpi_handle handle = device->handle; + struct acpi_object_list arg_list; + union acpi_object arg; + acpi_status status; + unsigned long long temp; + unsigned long max_state; + + if (memory_get_int_max_bandwidth(cdev, &max_state)) + return -EFAULT; + + if (state > max_state) + return -EINVAL; + + arg_list.count = 1; + arg_list.pointer = &arg; + arg.type = ACPI_TYPE_INTEGER; + arg.integer.value = state; + + status = + acpi_evaluate_integer(handle, MEMORY_SET_BANDWIDTH, &arg_list, + &temp); + + printk(KERN_INFO + "Bandwidth value was %d: status is %d\n", state, status); + if (ACPI_FAILURE(status)) + return -EFAULT; + + return 0; +} + +static struct thermal_cooling_device_ops memory_cooling_ops = { + .get_max_state = memory_get_max_bandwidth, + .get_cur_state = memory_get_cur_bandwidth, + .set_cur_state = memory_set_cur_bandwidth, +}; + +/* + * Memory Device Management + */ +static int intel_menlow_memory_add(struct acpi_device *device) +{ + int result = -ENODEV; + acpi_status status = AE_OK; + acpi_handle dummy; + struct thermal_cooling_device *cdev; + + if (!device) + return -EINVAL; + + status = acpi_get_handle(device->handle, MEMORY_GET_BANDWIDTH, &dummy); + if (ACPI_FAILURE(status)) + goto end; + + status = acpi_get_handle(device->handle, MEMORY_SET_BANDWIDTH, &dummy); + if (ACPI_FAILURE(status)) + goto end; + + cdev = thermal_cooling_device_register("Memory controller", device, + &memory_cooling_ops); + if (IS_ERR(cdev)) { + result = PTR_ERR(cdev); + goto end; + } + + device->driver_data = cdev; + result = sysfs_create_link(&device->dev.kobj, + &cdev->device.kobj, "thermal_cooling"); + if (result) + goto unregister; + + result = sysfs_create_link(&cdev->device.kobj, + &device->dev.kobj, "device"); + if (result) { + sysfs_remove_link(&device->dev.kobj, "thermal_cooling"); + goto unregister; + } + + end: + return result; + + unregister: + thermal_cooling_device_unregister(cdev); + return result; + +} + +static int intel_menlow_memory_remove(struct acpi_device *device, int type) +{ + struct thermal_cooling_device *cdev = acpi_driver_data(device); + + if (!device || !cdev) + return -EINVAL; + + sysfs_remove_link(&device->dev.kobj, "thermal_cooling"); + sysfs_remove_link(&cdev->device.kobj, "device"); + thermal_cooling_device_unregister(cdev); + + return 0; +} + +static const struct acpi_device_id intel_menlow_memory_ids[] = { + {"INT0002", 0}, + {"", 0}, +}; + +static struct acpi_driver intel_menlow_memory_driver = { + .name = "intel_menlow_thermal_control", + .ids = intel_menlow_memory_ids, + .ops = { + .add = intel_menlow_memory_add, + .remove = intel_menlow_memory_remove, + }, +}; + +/* + * Sensor control on menlow platform + */ + +#define THERMAL_AUX0 0 +#define THERMAL_AUX1 1 +#define GET_AUX0 "GAX0" +#define GET_AUX1 "GAX1" +#define SET_AUX0 "SAX0" +#define SET_AUX1 "SAX1" + +struct intel_menlow_attribute { + struct device_attribute attr; + struct device *device; + acpi_handle handle; + struct list_head node; +}; + +static LIST_HEAD(intel_menlow_attr_list); +static DEFINE_MUTEX(intel_menlow_attr_lock); + +/* + * sensor_get_auxtrip - get the current auxtrip value from sensor + * @name: Thermalzone name + * @auxtype : AUX0/AUX1 + * @buf: syfs buffer + */ +static int sensor_get_auxtrip(acpi_handle handle, int index, + unsigned long long *value) +{ + acpi_status status; + + if ((index != 0 && index != 1) || !value) + return -EINVAL; + + status = acpi_evaluate_integer(handle, index ? GET_AUX1 : GET_AUX0, + NULL, value); + if (ACPI_FAILURE(status)) + return -EIO; + + return 0; +} + +/* + * sensor_set_auxtrip - set the new auxtrip value to sensor + * @name: Thermalzone name + * @auxtype : AUX0/AUX1 + * @buf: syfs buffer + */ +static int sensor_set_auxtrip(acpi_handle handle, int index, int value) +{ + acpi_status status; + union acpi_object arg = { + ACPI_TYPE_INTEGER + }; + struct acpi_object_list args = { + 1, &arg + }; + unsigned long long temp; + + if (index != 0 && index != 1) + return -EINVAL; + + status = acpi_evaluate_integer(handle, index ? GET_AUX0 : GET_AUX1, + NULL, &temp); + if (ACPI_FAILURE(status)) + return -EIO; + if ((index && value < temp) || (!index && value > temp)) + return -EINVAL; + + arg.integer.value = value; + status = acpi_evaluate_integer(handle, index ? SET_AUX1 : SET_AUX0, + &args, &temp); + if (ACPI_FAILURE(status)) + return -EIO; + + /* do we need to check the return value of SAX0/SAX1 ? */ + + return 0; +} + +#define to_intel_menlow_attr(_attr) \ + container_of(_attr, struct intel_menlow_attribute, attr) + +static ssize_t aux0_show(struct device *dev, + struct device_attribute *dev_attr, char *buf) +{ + struct intel_menlow_attribute *attr = to_intel_menlow_attr(dev_attr); + unsigned long long value; + int result; + + result = sensor_get_auxtrip(attr->handle, 0, &value); + + return result ? result : sprintf(buf, "%lu", KELVIN_TO_CELSIUS(value)); +} + +static ssize_t aux1_show(struct device *dev, + struct device_attribute *dev_attr, char *buf) +{ + struct intel_menlow_attribute *attr = to_intel_menlow_attr(dev_attr); + unsigned long long value; + int result; + + result = sensor_get_auxtrip(attr->handle, 1, &value); + + return result ? result : sprintf(buf, "%lu", KELVIN_TO_CELSIUS(value)); +} + +static ssize_t aux0_store(struct device *dev, + struct device_attribute *dev_attr, + const char *buf, size_t count) +{ + struct intel_menlow_attribute *attr = to_intel_menlow_attr(dev_attr); + int value; + int result; + + /*Sanity check; should be a positive integer */ + if (!sscanf(buf, "%d", &value)) + return -EINVAL; + + if (value < 0) + return -EINVAL; + + result = sensor_set_auxtrip(attr->handle, 0, CELSIUS_TO_KELVIN(value)); + return result ? result : count; +} + +static ssize_t aux1_store(struct device *dev, + struct device_attribute *dev_attr, + const char *buf, size_t count) +{ + struct intel_menlow_attribute *attr = to_intel_menlow_attr(dev_attr); + int value; + int result; + + /*Sanity check; should be a positive integer */ + if (!sscanf(buf, "%d", &value)) + return -EINVAL; + + if (value < 0) + return -EINVAL; + + result = sensor_set_auxtrip(attr->handle, 1, CELSIUS_TO_KELVIN(value)); + return result ? result : count; +} + +/* BIOS can enable/disable the thermal user application in dabney platform */ +#define BIOS_ENABLED "\\_TZ.GSTS" +static ssize_t bios_enabled_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + acpi_status status; + unsigned long long bios_enabled; + + status = acpi_evaluate_integer(NULL, BIOS_ENABLED, NULL, &bios_enabled); + if (ACPI_FAILURE(status)) + return -ENODEV; + + return sprintf(buf, "%s\n", bios_enabled ? "enabled" : "disabled"); +} + +static int intel_menlow_add_one_attribute(char *name, int mode, void *show, + void *store, struct device *dev, + acpi_handle handle) +{ + struct intel_menlow_attribute *attr; + int result; + + attr = kzalloc(sizeof(struct intel_menlow_attribute), GFP_KERNEL); + if (!attr) + return -ENOMEM; + + attr->attr.attr.name = name; + attr->attr.attr.mode = mode; + attr->attr.show = show; + attr->attr.store = store; + attr->device = dev; + attr->handle = handle; + + result = device_create_file(dev, &attr->attr); + if (result) + return result; + + mutex_lock(&intel_menlow_attr_lock); + list_add_tail(&attr->node, &intel_menlow_attr_list); + mutex_unlock(&intel_menlow_attr_lock); + + return 0; +} + +static acpi_status intel_menlow_register_sensor(acpi_handle handle, u32 lvl, + void *context, void **rv) +{ + acpi_status status; + acpi_handle dummy; + struct thermal_zone_device *thermal; + int result; + + result = acpi_bus_get_private_data(handle, (void **)&thermal); + if (result) + return 0; + + /* _TZ must have the AUX0/1 methods */ + status = acpi_get_handle(handle, GET_AUX0, &dummy); + if (ACPI_FAILURE(status)) + goto not_found; + + status = acpi_get_handle(handle, SET_AUX0, &dummy); + if (ACPI_FAILURE(status)) + goto not_found; + + result = intel_menlow_add_one_attribute("aux0", 0644, + aux0_show, aux0_store, + &thermal->device, handle); + if (result) + return AE_ERROR; + + status = acpi_get_handle(handle, GET_AUX1, &dummy); + if (ACPI_FAILURE(status)) + goto not_found; + + status = acpi_get_handle(handle, SET_AUX1, &dummy); + if (ACPI_FAILURE(status)) + goto not_found; + + result = intel_menlow_add_one_attribute("aux1", 0644, + aux1_show, aux1_store, + &thermal->device, handle); + if (result) + return AE_ERROR; + + /* + * create the "dabney_enabled" attribute which means the user app + * should be loaded or not + */ + + result = intel_menlow_add_one_attribute("bios_enabled", 0444, + bios_enabled_show, NULL, + &thermal->device, handle); + if (result) + return AE_ERROR; + + not_found: + if (status == AE_NOT_FOUND) + return AE_OK; + else + return status; +} + +static void intel_menlow_unregister_sensor(void) +{ + struct intel_menlow_attribute *pos, *next; + + mutex_lock(&intel_menlow_attr_lock); + list_for_each_entry_safe(pos, next, &intel_menlow_attr_list, node) { + list_del(&pos->node); + device_remove_file(pos->device, &pos->attr); + kfree(pos); + } + mutex_unlock(&intel_menlow_attr_lock); + + return; +} + +static int __init intel_menlow_module_init(void) +{ + int result = -ENODEV; + acpi_status status; + unsigned long long enable; + + if (acpi_disabled) + return result; + + /* Looking for the \_TZ.GSTS method */ + status = acpi_evaluate_integer(NULL, BIOS_ENABLED, NULL, &enable); + if (ACPI_FAILURE(status) || !enable) + return -ENODEV; + + /* Looking for ACPI device MEM0 with hardware id INT0002 */ + result = acpi_bus_register_driver(&intel_menlow_memory_driver); + if (result) + return result; + + /* Looking for sensors in each ACPI thermal zone */ + status = acpi_walk_namespace(ACPI_TYPE_THERMAL, ACPI_ROOT_OBJECT, + ACPI_UINT32_MAX, + intel_menlow_register_sensor, NULL, NULL); + if (ACPI_FAILURE(status)) + return -ENODEV; + + return 0; +} + +static void __exit intel_menlow_module_exit(void) +{ + acpi_bus_unregister_driver(&intel_menlow_memory_driver); + intel_menlow_unregister_sensor(); +} + +module_init(intel_menlow_module_init); +module_exit(intel_menlow_module_exit); diff --git a/drivers/platform/x86/msi-laptop.c b/drivers/platform/x86/msi-laptop.c new file mode 100644 index 000000000000..759763d18e4c --- /dev/null +++ b/drivers/platform/x86/msi-laptop.c @@ -0,0 +1,437 @@ +/*-*-linux-c-*-*/ + +/* + Copyright (C) 2006 Lennart Poettering <mzxreary (at) 0pointer (dot) de> + + 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. + */ + +/* + * msi-laptop.c - MSI S270 laptop support. This laptop is sold under + * various brands, including "Cytron/TCM/Medion/Tchibo MD96100". + * + * Driver also supports S271, S420 models. + * + * This driver exports a few files in /sys/devices/platform/msi-laptop-pf/: + * + * lcd_level - Screen brightness: contains a single integer in the + * range 0..8. (rw) + * + * auto_brightness - Enable automatic brightness control: contains + * either 0 or 1. If set to 1 the hardware adjusts the screen + * brightness automatically when the power cord is + * plugged/unplugged. (rw) + * + * wlan - WLAN subsystem enabled: contains either 0 or 1. (ro) + * + * bluetooth - Bluetooth subsystem enabled: contains either 0 or 1 + * Please note that this file is constantly 0 if no Bluetooth + * hardware is available. (ro) + * + * In addition to these platform device attributes the driver + * registers itself in the Linux backlight control subsystem and is + * available to userspace under /sys/class/backlight/msi-laptop-bl/. + * + * This driver might work on other laptops produced by MSI. If you + * want to try it you can pass force=1 as argument to the module which + * will force it to load even when the DMI data doesn't identify the + * laptop as MSI S270. YMMV. + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/acpi.h> +#include <linux/dmi.h> +#include <linux/backlight.h> +#include <linux/platform_device.h> + +#define MSI_DRIVER_VERSION "0.5" + +#define MSI_LCD_LEVEL_MAX 9 + +#define MSI_EC_COMMAND_WIRELESS 0x10 +#define MSI_EC_COMMAND_LCD_LEVEL 0x11 + +static int force; +module_param(force, bool, 0); +MODULE_PARM_DESC(force, "Force driver load, ignore DMI data"); + +static int auto_brightness; +module_param(auto_brightness, int, 0); +MODULE_PARM_DESC(auto_brightness, "Enable automatic brightness control (0: disabled; 1: enabled; 2: don't touch)"); + +/* Hardware access */ + +static int set_lcd_level(int level) +{ + u8 buf[2]; + + if (level < 0 || level >= MSI_LCD_LEVEL_MAX) + return -EINVAL; + + buf[0] = 0x80; + buf[1] = (u8) (level*31); + + return ec_transaction(MSI_EC_COMMAND_LCD_LEVEL, buf, sizeof(buf), NULL, 0, 1); +} + +static int get_lcd_level(void) +{ + u8 wdata = 0, rdata; + int result; + + result = ec_transaction(MSI_EC_COMMAND_LCD_LEVEL, &wdata, 1, &rdata, 1, 1); + if (result < 0) + return result; + + return (int) rdata / 31; +} + +static int get_auto_brightness(void) +{ + u8 wdata = 4, rdata; + int result; + + result = ec_transaction(MSI_EC_COMMAND_LCD_LEVEL, &wdata, 1, &rdata, 1, 1); + if (result < 0) + return result; + + return !!(rdata & 8); +} + +static int set_auto_brightness(int enable) +{ + u8 wdata[2], rdata; + int result; + + wdata[0] = 4; + + result = ec_transaction(MSI_EC_COMMAND_LCD_LEVEL, wdata, 1, &rdata, 1, 1); + if (result < 0) + return result; + + wdata[0] = 0x84; + wdata[1] = (rdata & 0xF7) | (enable ? 8 : 0); + + return ec_transaction(MSI_EC_COMMAND_LCD_LEVEL, wdata, 2, NULL, 0, 1); +} + +static int get_wireless_state(int *wlan, int *bluetooth) +{ + u8 wdata = 0, rdata; + int result; + + result = ec_transaction(MSI_EC_COMMAND_WIRELESS, &wdata, 1, &rdata, 1, 1); + if (result < 0) + return -1; + + if (wlan) + *wlan = !!(rdata & 8); + + if (bluetooth) + *bluetooth = !!(rdata & 128); + + return 0; +} + +/* Backlight device stuff */ + +static int bl_get_brightness(struct backlight_device *b) +{ + return get_lcd_level(); +} + + +static int bl_update_status(struct backlight_device *b) +{ + return set_lcd_level(b->props.brightness); +} + +static struct backlight_ops msibl_ops = { + .get_brightness = bl_get_brightness, + .update_status = bl_update_status, +}; + +static struct backlight_device *msibl_device; + +/* Platform device */ + +static ssize_t show_wlan(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + int ret, enabled; + + ret = get_wireless_state(&enabled, NULL); + if (ret < 0) + return ret; + + return sprintf(buf, "%i\n", enabled); +} + +static ssize_t show_bluetooth(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + int ret, enabled; + + ret = get_wireless_state(NULL, &enabled); + if (ret < 0) + return ret; + + return sprintf(buf, "%i\n", enabled); +} + +static ssize_t show_lcd_level(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + int ret; + + ret = get_lcd_level(); + if (ret < 0) + return ret; + + return sprintf(buf, "%i\n", ret); +} + +static ssize_t store_lcd_level(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + + int level, ret; + + if (sscanf(buf, "%i", &level) != 1 || (level < 0 || level >= MSI_LCD_LEVEL_MAX)) + return -EINVAL; + + ret = set_lcd_level(level); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t show_auto_brightness(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + int ret; + + ret = get_auto_brightness(); + if (ret < 0) + return ret; + + return sprintf(buf, "%i\n", ret); +} + +static ssize_t store_auto_brightness(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + + int enable, ret; + + if (sscanf(buf, "%i", &enable) != 1 || (enable != (enable & 1))) + return -EINVAL; + + ret = set_auto_brightness(enable); + if (ret < 0) + return ret; + + return count; +} + +static DEVICE_ATTR(lcd_level, 0644, show_lcd_level, store_lcd_level); +static DEVICE_ATTR(auto_brightness, 0644, show_auto_brightness, store_auto_brightness); +static DEVICE_ATTR(bluetooth, 0444, show_bluetooth, NULL); +static DEVICE_ATTR(wlan, 0444, show_wlan, NULL); + +static struct attribute *msipf_attributes[] = { + &dev_attr_lcd_level.attr, + &dev_attr_auto_brightness.attr, + &dev_attr_bluetooth.attr, + &dev_attr_wlan.attr, + NULL +}; + +static struct attribute_group msipf_attribute_group = { + .attrs = msipf_attributes +}; + +static struct platform_driver msipf_driver = { + .driver = { + .name = "msi-laptop-pf", + .owner = THIS_MODULE, + } +}; + +static struct platform_device *msipf_device; + +/* Initialization */ + +static int dmi_check_cb(const struct dmi_system_id *id) +{ + printk("msi-laptop: Identified laptop model '%s'.\n", id->ident); + return 0; +} + +static struct dmi_system_id __initdata msi_dmi_table[] = { + { + .ident = "MSI S270", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "MICRO-STAR INT'L CO.,LTD"), + DMI_MATCH(DMI_PRODUCT_NAME, "MS-1013"), + DMI_MATCH(DMI_PRODUCT_VERSION, "0131"), + DMI_MATCH(DMI_CHASSIS_VENDOR, "MICRO-STAR INT'L CO.,LTD") + }, + .callback = dmi_check_cb + }, + { + .ident = "MSI S271", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International"), + DMI_MATCH(DMI_PRODUCT_NAME, "MS-1058"), + DMI_MATCH(DMI_PRODUCT_VERSION, "0581"), + DMI_MATCH(DMI_BOARD_NAME, "MS-1058") + }, + .callback = dmi_check_cb + }, + { + .ident = "MSI S420", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International"), + DMI_MATCH(DMI_PRODUCT_NAME, "MS-1412"), + DMI_MATCH(DMI_BOARD_VENDOR, "MSI"), + DMI_MATCH(DMI_BOARD_NAME, "MS-1412") + }, + .callback = dmi_check_cb + }, + { + .ident = "Medion MD96100", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "NOTEBOOK"), + DMI_MATCH(DMI_PRODUCT_NAME, "SAM2000"), + DMI_MATCH(DMI_PRODUCT_VERSION, "0131"), + DMI_MATCH(DMI_CHASSIS_VENDOR, "MICRO-STAR INT'L CO.,LTD") + }, + .callback = dmi_check_cb + }, + { } +}; + +static int __init msi_init(void) +{ + int ret; + + if (acpi_disabled) + return -ENODEV; + + if (!force && !dmi_check_system(msi_dmi_table)) + return -ENODEV; + + if (auto_brightness < 0 || auto_brightness > 2) + return -EINVAL; + + /* Register backlight stuff */ + + if (acpi_video_backlight_support()) { + printk(KERN_INFO "MSI: Brightness ignored, must be controlled " + "by ACPI video driver\n"); + } else { + msibl_device = backlight_device_register("msi-laptop-bl", NULL, + NULL, &msibl_ops); + if (IS_ERR(msibl_device)) + return PTR_ERR(msibl_device); + msibl_device->props.max_brightness = MSI_LCD_LEVEL_MAX-1; + } + + ret = platform_driver_register(&msipf_driver); + if (ret) + goto fail_backlight; + + /* Register platform stuff */ + + msipf_device = platform_device_alloc("msi-laptop-pf", -1); + if (!msipf_device) { + ret = -ENOMEM; + goto fail_platform_driver; + } + + ret = platform_device_add(msipf_device); + if (ret) + goto fail_platform_device1; + + ret = sysfs_create_group(&msipf_device->dev.kobj, &msipf_attribute_group); + if (ret) + goto fail_platform_device2; + + /* Disable automatic brightness control by default because + * this module was probably loaded to do brightness control in + * software. */ + + if (auto_brightness != 2) + set_auto_brightness(auto_brightness); + + printk(KERN_INFO "msi-laptop: driver "MSI_DRIVER_VERSION" successfully loaded.\n"); + + return 0; + +fail_platform_device2: + + platform_device_del(msipf_device); + +fail_platform_device1: + + platform_device_put(msipf_device); + +fail_platform_driver: + + platform_driver_unregister(&msipf_driver); + +fail_backlight: + + backlight_device_unregister(msibl_device); + + return ret; +} + +static void __exit msi_cleanup(void) +{ + + sysfs_remove_group(&msipf_device->dev.kobj, &msipf_attribute_group); + platform_device_unregister(msipf_device); + platform_driver_unregister(&msipf_driver); + backlight_device_unregister(msibl_device); + + /* Enable automatic brightness control again */ + if (auto_brightness != 2) + set_auto_brightness(1); + + printk(KERN_INFO "msi-laptop: driver unloaded.\n"); +} + +module_init(msi_init); +module_exit(msi_cleanup); + +MODULE_AUTHOR("Lennart Poettering"); +MODULE_DESCRIPTION("MSI Laptop Support"); +MODULE_VERSION(MSI_DRIVER_VERSION); +MODULE_LICENSE("GPL"); + +MODULE_ALIAS("dmi:*:svnMICRO-STARINT'LCO.,LTD:pnMS-1013:pvr0131*:cvnMICRO-STARINT'LCO.,LTD:ct10:*"); +MODULE_ALIAS("dmi:*:svnMicro-StarInternational:pnMS-1058:pvr0581:rvnMSI:rnMS-1058:*:ct10:*"); +MODULE_ALIAS("dmi:*:svnMicro-StarInternational:pnMS-1412:*:rvnMSI:rnMS-1412:*:cvnMICRO-STARINT'LCO.,LTD:ct10:*"); +MODULE_ALIAS("dmi:*:svnNOTEBOOK:pnSAM2000:pvr0131*:cvnMICRO-STARINT'LCO.,LTD:ct10:*"); diff --git a/drivers/platform/x86/panasonic-laptop.c b/drivers/platform/x86/panasonic-laptop.c new file mode 100644 index 000000000000..f30db367c82e --- /dev/null +++ b/drivers/platform/x86/panasonic-laptop.c @@ -0,0 +1,744 @@ +/* + * Panasonic HotKey and LCD brightness control driver + * (C) 2004 Hiroshi Miura <miura@da-cha.org> + * (C) 2004 NTT DATA Intellilink Co. http://www.intellilink.co.jp/ + * (C) YOKOTA Hiroshi <yokota (at) netlab. is. tsukuba. ac. jp> + * (C) 2004 David Bronaugh <dbronaugh> + * (C) 2006-2008 Harald Welte <laforge@gnumonks.org> + * + * derived from toshiba_acpi.c, Copyright (C) 2002-2004 John Belmonte + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * publicshed by the Free Software Foundation. + * + * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + *--------------------------------------------------------------------------- + * + * ChangeLog: + * Sep.23, 2008 Harald Welte <laforge@gnumonks.org> + * -v0.95 rename driver from drivers/acpi/pcc_acpi.c to + * drivers/misc/panasonic-laptop.c + * + * Jul.04, 2008 Harald Welte <laforge@gnumonks.org> + * -v0.94 replace /proc interface with device attributes + * support {set,get}keycode on th input device + * + * Jun.27, 2008 Harald Welte <laforge@gnumonks.org> + * -v0.92 merge with 2.6.26-rc6 input API changes + * remove broken <= 2.6.15 kernel support + * resolve all compiler warnings + * various coding style fixes (checkpatch.pl) + * add support for backlight api + * major code restructuring + * + * Dac.28, 2007 Harald Welte <laforge@gnumonks.org> + * -v0.91 merge with 2.6.24-rc6 ACPI changes + * + * Nov.04, 2006 Hiroshi Miura <miura@da-cha.org> + * -v0.9 remove warning about section reference. + * remove acpi_os_free + * add /proc/acpi/pcc/brightness interface for HAL access + * merge dbronaugh's enhancement + * Aug.17, 2004 David Bronaugh (dbronaugh) + * - Added screen brightness setting interface + * Thanks to FreeBSD crew (acpi_panasonic.c) + * for the ideas I needed to accomplish it + * + * May.29, 2006 Hiroshi Miura <miura@da-cha.org> + * -v0.8.4 follow to change keyinput structure + * thanks Fabian Yamaguchi <fabs@cs.tu-berlin.de>, + * Jacob Bower <jacob.bower@ic.ac.uk> and + * Hiroshi Yokota for providing solutions. + * + * Oct.02, 2004 Hiroshi Miura <miura@da-cha.org> + * -v0.8.2 merge code of YOKOTA Hiroshi + * <yokota@netlab.is.tsukuba.ac.jp>. + * Add sticky key mode interface. + * Refactoring acpi_pcc_generate_keyinput(). + * + * Sep.15, 2004 Hiroshi Miura <miura@da-cha.org> + * -v0.8 Generate key input event on input subsystem. + * This is based on yet another driver written by + * Ryuta Nakanishi. + * + * Sep.10, 2004 Hiroshi Miura <miura@da-cha.org> + * -v0.7 Change proc interface functions using seq_file + * facility as same as other ACPI drivers. + * + * Aug.28, 2004 Hiroshi Miura <miura@da-cha.org> + * -v0.6.4 Fix a silly error with status checking + * + * Aug.25, 2004 Hiroshi Miura <miura@da-cha.org> + * -v0.6.3 replace read_acpi_int by standard function + * acpi_evaluate_integer + * some clean up and make smart copyright notice. + * fix return value of pcc_acpi_get_key() + * fix checking return value of acpi_bus_register_driver() + * + * Aug.22, 2004 David Bronaugh <dbronaugh@linuxboxen.org> + * -v0.6.2 Add check on ACPI data (num_sifr) + * Coding style cleanups, better error messages/handling + * Fixed an off-by-one error in memory allocation + * + * Aug.21, 2004 David Bronaugh <dbronaugh@linuxboxen.org> + * -v0.6.1 Fix a silly error with status checking + * + * Aug.20, 2004 David Bronaugh <dbronaugh@linuxboxen.org> + * - v0.6 Correct brightness controls to reflect reality + * based on information gleaned by Hiroshi Miura + * and discussions with Hiroshi Miura + * + * Aug.10, 2004 Hiroshi Miura <miura@da-cha.org> + * - v0.5 support LCD brightness control + * based on the disclosed information by MEI. + * + * Jul.25, 2004 Hiroshi Miura <miura@da-cha.org> + * - v0.4 first post version + * add function to retrive SIFR + * + * Jul.24, 2004 Hiroshi Miura <miura@da-cha.org> + * - v0.3 get proper status of hotkey + * + * Jul.22, 2004 Hiroshi Miura <miura@da-cha.org> + * - v0.2 add HotKey handler + * + * Jul.17, 2004 Hiroshi Miura <miura@da-cha.org> + * - v0.1 start from toshiba_acpi driver written by John Belmonte + * + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/types.h> +#include <linux/backlight.h> +#include <linux/ctype.h> +#include <linux/seq_file.h> +#include <linux/uaccess.h> +#include <acpi/acpi_bus.h> +#include <acpi/acpi_drivers.h> +#include <linux/input.h> + + +#ifndef ACPI_HOTKEY_COMPONENT +#define ACPI_HOTKEY_COMPONENT 0x10000000 +#endif + +#define _COMPONENT ACPI_HOTKEY_COMPONENT + +MODULE_AUTHOR("Hiroshi Miura, David Bronaugh and Harald Welte"); +MODULE_DESCRIPTION("ACPI HotKey driver for Panasonic Let's Note laptops"); +MODULE_LICENSE("GPL"); + +#define LOGPREFIX "pcc_acpi: " + +/* Define ACPI PATHs */ +/* Lets note hotkeys */ +#define METHOD_HKEY_QUERY "HINF" +#define METHOD_HKEY_SQTY "SQTY" +#define METHOD_HKEY_SINF "SINF" +#define METHOD_HKEY_SSET "SSET" +#define HKEY_NOTIFY 0x80 + +#define ACPI_PCC_DRIVER_NAME "Panasonic Laptop Support" +#define ACPI_PCC_DEVICE_NAME "Hotkey" +#define ACPI_PCC_CLASS "pcc" + +#define ACPI_PCC_INPUT_PHYS "panasonic/hkey0" + +/* LCD_TYPEs: 0 = Normal, 1 = Semi-transparent + ENV_STATEs: Normal temp=0x01, High temp=0x81, N/A=0x00 +*/ +enum SINF_BITS { SINF_NUM_BATTERIES = 0, + SINF_LCD_TYPE, + SINF_AC_MAX_BRIGHT, + SINF_AC_MIN_BRIGHT, + SINF_AC_CUR_BRIGHT, + SINF_DC_MAX_BRIGHT, + SINF_DC_MIN_BRIGHT, + SINF_DC_CUR_BRIGHT, + SINF_MUTE, + SINF_RESERVED, + SINF_ENV_STATE, + SINF_STICKY_KEY = 0x80, + }; +/* R1 handles SINF_AC_CUR_BRIGHT as SINF_CUR_BRIGHT, doesn't know AC state */ + +static int acpi_pcc_hotkey_add(struct acpi_device *device); +static int acpi_pcc_hotkey_remove(struct acpi_device *device, int type); +static int acpi_pcc_hotkey_resume(struct acpi_device *device); + +static const struct acpi_device_id pcc_device_ids[] = { + { "MAT0012", 0}, + { "MAT0013", 0}, + { "MAT0018", 0}, + { "MAT0019", 0}, + { "", 0}, +}; + +static struct acpi_driver acpi_pcc_driver = { + .name = ACPI_PCC_DRIVER_NAME, + .class = ACPI_PCC_CLASS, + .ids = pcc_device_ids, + .ops = { + .add = acpi_pcc_hotkey_add, + .remove = acpi_pcc_hotkey_remove, + .resume = acpi_pcc_hotkey_resume, + }, +}; + +#define KEYMAP_SIZE 11 +static const int initial_keymap[KEYMAP_SIZE] = { + /* 0 */ KEY_RESERVED, + /* 1 */ KEY_BRIGHTNESSDOWN, + /* 2 */ KEY_BRIGHTNESSUP, + /* 3 */ KEY_DISPLAYTOGGLE, + /* 4 */ KEY_MUTE, + /* 5 */ KEY_VOLUMEDOWN, + /* 6 */ KEY_VOLUMEUP, + /* 7 */ KEY_SLEEP, + /* 8 */ KEY_PROG1, /* Change CPU boost */ + /* 9 */ KEY_BATTERY, + /* 10 */ KEY_SUSPEND, +}; + +struct pcc_acpi { + acpi_handle handle; + unsigned long num_sifr; + int sticky_mode; + u32 *sinf; + struct acpi_device *device; + struct input_dev *input_dev; + struct backlight_device *backlight; + int keymap[KEYMAP_SIZE]; +}; + +struct pcc_keyinput { + struct acpi_hotkey *hotkey; +}; + +/* method access functions */ +static int acpi_pcc_write_sset(struct pcc_acpi *pcc, int func, int val) +{ + union acpi_object in_objs[] = { + { .integer.type = ACPI_TYPE_INTEGER, + .integer.value = func, }, + { .integer.type = ACPI_TYPE_INTEGER, + .integer.value = val, }, + }; + struct acpi_object_list params = { + .count = ARRAY_SIZE(in_objs), + .pointer = in_objs, + }; + acpi_status status = AE_OK; + + status = acpi_evaluate_object(pcc->handle, METHOD_HKEY_SSET, + ¶ms, NULL); + + return status == AE_OK; +} + +static inline int acpi_pcc_get_sqty(struct acpi_device *device) +{ + unsigned long long s; + acpi_status status; + + status = acpi_evaluate_integer(device->handle, METHOD_HKEY_SQTY, + NULL, &s); + if (ACPI_SUCCESS(status)) + return s; + else { + ACPI_DEBUG_PRINT((ACPI_DB_ERROR, + "evaluation error HKEY.SQTY\n")); + return -EINVAL; + } +} + +static int acpi_pcc_retrieve_biosdata(struct pcc_acpi *pcc, u32 *sinf) +{ + acpi_status status; + struct acpi_buffer buffer = {ACPI_ALLOCATE_BUFFER, NULL}; + union acpi_object *hkey = NULL; + int i; + + status = acpi_evaluate_object(pcc->handle, METHOD_HKEY_SINF, 0, + &buffer); + if (ACPI_FAILURE(status)) { + ACPI_DEBUG_PRINT((ACPI_DB_ERROR, + "evaluation error HKEY.SINF\n")); + return 0; + } + + hkey = buffer.pointer; + if (!hkey || (hkey->type != ACPI_TYPE_PACKAGE)) { + ACPI_DEBUG_PRINT((ACPI_DB_ERROR, "Invalid HKEY.SINF\n")); + goto end; + } + + if (pcc->num_sifr < hkey->package.count) { + ACPI_DEBUG_PRINT((ACPI_DB_ERROR, + "SQTY reports bad SINF length\n")); + status = AE_ERROR; + goto end; + } + + for (i = 0; i < hkey->package.count; i++) { + union acpi_object *element = &(hkey->package.elements[i]); + if (likely(element->type == ACPI_TYPE_INTEGER)) { + sinf[i] = element->integer.value; + } else + ACPI_DEBUG_PRINT((ACPI_DB_ERROR, + "Invalid HKEY.SINF data\n")); + } + sinf[hkey->package.count] = -1; + +end: + kfree(buffer.pointer); + return status == AE_OK; +} + +/* backlight API interface functions */ + +/* This driver currently treats AC and DC brightness identical, + * since we don't need to invent an interface to the core ACPI + * logic to receive events in case a power supply is plugged in + * or removed */ + +static int bl_get(struct backlight_device *bd) +{ + struct pcc_acpi *pcc = bl_get_data(bd); + + if (!acpi_pcc_retrieve_biosdata(pcc, pcc->sinf)) + return -EIO; + + return pcc->sinf[SINF_AC_CUR_BRIGHT]; +} + +static int bl_set_status(struct backlight_device *bd) +{ + struct pcc_acpi *pcc = bl_get_data(bd); + int bright = bd->props.brightness; + int rc; + + if (!acpi_pcc_retrieve_biosdata(pcc, pcc->sinf)) + return -EIO; + + if (bright < pcc->sinf[SINF_AC_MIN_BRIGHT]) + bright = pcc->sinf[SINF_AC_MIN_BRIGHT]; + + if (bright < pcc->sinf[SINF_DC_MIN_BRIGHT]) + bright = pcc->sinf[SINF_DC_MIN_BRIGHT]; + + if (bright < pcc->sinf[SINF_AC_MIN_BRIGHT] || + bright > pcc->sinf[SINF_AC_MAX_BRIGHT]) + return -EINVAL; + + rc = acpi_pcc_write_sset(pcc, SINF_AC_CUR_BRIGHT, bright); + if (rc < 0) + return rc; + + return acpi_pcc_write_sset(pcc, SINF_DC_CUR_BRIGHT, bright); +} + +static struct backlight_ops pcc_backlight_ops = { + .get_brightness = bl_get, + .update_status = bl_set_status, +}; + + +/* sysfs user interface functions */ + +static ssize_t show_numbatt(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + + if (!acpi_pcc_retrieve_biosdata(pcc, pcc->sinf)) + return -EIO; + + return sprintf(buf, "%u\n", pcc->sinf[SINF_NUM_BATTERIES]); +} + +static ssize_t show_lcdtype(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + + if (!acpi_pcc_retrieve_biosdata(pcc, pcc->sinf)) + return -EIO; + + return sprintf(buf, "%u\n", pcc->sinf[SINF_LCD_TYPE]); +} + +static ssize_t show_mute(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + + if (!acpi_pcc_retrieve_biosdata(pcc, pcc->sinf)) + return -EIO; + + return sprintf(buf, "%u\n", pcc->sinf[SINF_MUTE]); +} + +static ssize_t show_sticky(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + + if (!acpi_pcc_retrieve_biosdata(pcc, pcc->sinf)) + return -EIO; + + return sprintf(buf, "%u\n", pcc->sinf[SINF_STICKY_KEY]); +} + +static ssize_t set_sticky(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + int val; + + if (count && sscanf(buf, "%i", &val) == 1 && + (val == 0 || val == 1)) { + acpi_pcc_write_sset(pcc, SINF_STICKY_KEY, val); + pcc->sticky_mode = val; + } + + return count; +} + +static DEVICE_ATTR(numbatt, S_IRUGO, show_numbatt, NULL); +static DEVICE_ATTR(lcdtype, S_IRUGO, show_lcdtype, NULL); +static DEVICE_ATTR(mute, S_IRUGO, show_mute, NULL); +static DEVICE_ATTR(sticky_key, S_IRUGO | S_IWUSR, show_sticky, set_sticky); + +static struct attribute *pcc_sysfs_entries[] = { + &dev_attr_numbatt.attr, + &dev_attr_lcdtype.attr, + &dev_attr_mute.attr, + &dev_attr_sticky_key.attr, + NULL, +}; + +static struct attribute_group pcc_attr_group = { + .name = NULL, /* put in device directory */ + .attrs = pcc_sysfs_entries, +}; + + +/* hotkey input device driver */ + +static int pcc_getkeycode(struct input_dev *dev, int scancode, int *keycode) +{ + struct pcc_acpi *pcc = input_get_drvdata(dev); + + if (scancode >= ARRAY_SIZE(pcc->keymap)) + return -EINVAL; + + *keycode = pcc->keymap[scancode]; + + return 0; +} + +static int keymap_get_by_keycode(struct pcc_acpi *pcc, int keycode) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(pcc->keymap); i++) { + if (pcc->keymap[i] == keycode) + return i+1; + } + + return 0; +} + +static int pcc_setkeycode(struct input_dev *dev, int scancode, int keycode) +{ + struct pcc_acpi *pcc = input_get_drvdata(dev); + int oldkeycode; + + if (scancode >= ARRAY_SIZE(pcc->keymap)) + return -EINVAL; + + if (keycode < 0 || keycode > KEY_MAX) + return -EINVAL; + + oldkeycode = pcc->keymap[scancode]; + pcc->keymap[scancode] = keycode; + + set_bit(keycode, dev->keybit); + + if (!keymap_get_by_keycode(pcc, oldkeycode)) + clear_bit(oldkeycode, dev->keybit); + + return 0; +} + +static void acpi_pcc_generate_keyinput(struct pcc_acpi *pcc) +{ + struct input_dev *hotk_input_dev = pcc->input_dev; + int rc; + int key_code, hkey_num; + unsigned long long result; + + rc = acpi_evaluate_integer(pcc->handle, METHOD_HKEY_QUERY, + NULL, &result); + if (!ACPI_SUCCESS(rc)) { + ACPI_DEBUG_PRINT((ACPI_DB_ERROR, + "error getting hotkey status\n")); + return; + } + + acpi_bus_generate_proc_event(pcc->device, HKEY_NOTIFY, result); + + hkey_num = result & 0xf; + + if (hkey_num < 0 || hkey_num > ARRAY_SIZE(pcc->keymap)) { + ACPI_DEBUG_PRINT((ACPI_DB_ERROR, + "hotkey number out of range: %d\n", + hkey_num)); + return; + } + + key_code = pcc->keymap[hkey_num]; + + if (key_code != KEY_RESERVED) { + int pushed = (result & 0x80) ? TRUE : FALSE; + + input_report_key(hotk_input_dev, key_code, pushed); + input_sync(hotk_input_dev); + } + + return; +} + +static void acpi_pcc_hotkey_notify(acpi_handle handle, u32 event, void *data) +{ + struct pcc_acpi *pcc = (struct pcc_acpi *) data; + + switch (event) { + case HKEY_NOTIFY: + acpi_pcc_generate_keyinput(pcc); + break; + default: + /* nothing to do */ + break; + } +} + +static int acpi_pcc_init_input(struct pcc_acpi *pcc) +{ + int i, rc; + + pcc->input_dev = input_allocate_device(); + if (!pcc->input_dev) { + ACPI_DEBUG_PRINT((ACPI_DB_ERROR, + "Couldn't allocate input device for hotkey")); + return -ENOMEM; + } + + pcc->input_dev->evbit[0] = BIT(EV_KEY); + + pcc->input_dev->name = ACPI_PCC_DRIVER_NAME; + pcc->input_dev->phys = ACPI_PCC_INPUT_PHYS; + pcc->input_dev->id.bustype = BUS_HOST; + pcc->input_dev->id.vendor = 0x0001; + pcc->input_dev->id.product = 0x0001; + pcc->input_dev->id.version = 0x0100; + pcc->input_dev->getkeycode = pcc_getkeycode; + pcc->input_dev->setkeycode = pcc_setkeycode; + + /* load initial keymap */ + memcpy(pcc->keymap, initial_keymap, sizeof(pcc->keymap)); + + for (i = 0; i < ARRAY_SIZE(pcc->keymap); i++) + __set_bit(pcc->keymap[i], pcc->input_dev->keybit); + __clear_bit(KEY_RESERVED, pcc->input_dev->keybit); + + input_set_drvdata(pcc->input_dev, pcc); + + rc = input_register_device(pcc->input_dev); + if (rc < 0) + input_free_device(pcc->input_dev); + + return rc; +} + +/* kernel module interface */ + +static int acpi_pcc_hotkey_resume(struct acpi_device *device) +{ + struct pcc_acpi *pcc = acpi_driver_data(device); + acpi_status status = AE_OK; + + if (device == NULL || pcc == NULL) + return -EINVAL; + + ACPI_DEBUG_PRINT((ACPI_DB_ERROR, "Sticky mode restore: %d\n", + pcc->sticky_mode)); + + status = acpi_pcc_write_sset(pcc, SINF_STICKY_KEY, pcc->sticky_mode); + + return status == AE_OK ? 0 : -EINVAL; +} + +static int acpi_pcc_hotkey_add(struct acpi_device *device) +{ + acpi_status status; + struct pcc_acpi *pcc; + int num_sifr, result; + + if (!device) + return -EINVAL; + + num_sifr = acpi_pcc_get_sqty(device); + + if (num_sifr > 255) { + ACPI_DEBUG_PRINT((ACPI_DB_ERROR, "num_sifr too large")); + return -ENODEV; + } + + pcc = kzalloc(sizeof(struct pcc_acpi), GFP_KERNEL); + if (!pcc) { + ACPI_DEBUG_PRINT((ACPI_DB_ERROR, + "Couldn't allocate mem for pcc")); + return -ENOMEM; + } + + pcc->sinf = kzalloc(sizeof(u32) * (num_sifr + 1), GFP_KERNEL); + if (!pcc->sinf) { + result = -ENOMEM; + goto out_hotkey; + } + + pcc->device = device; + pcc->handle = device->handle; + pcc->num_sifr = num_sifr; + device->driver_data = pcc; + strcpy(acpi_device_name(device), ACPI_PCC_DEVICE_NAME); + strcpy(acpi_device_class(device), ACPI_PCC_CLASS); + + result = acpi_pcc_init_input(pcc); + if (result) { + ACPI_DEBUG_PRINT((ACPI_DB_ERROR, + "Error installing keyinput handler\n")); + goto out_sinf; + } + + /* initialize hotkey input device */ + status = acpi_install_notify_handler(pcc->handle, ACPI_DEVICE_NOTIFY, + acpi_pcc_hotkey_notify, pcc); + + if (ACPI_FAILURE(status)) { + ACPI_DEBUG_PRINT((ACPI_DB_ERROR, + "Error installing notify handler\n")); + result = -ENODEV; + goto out_input; + } + + /* initialize backlight */ + pcc->backlight = backlight_device_register("panasonic", NULL, pcc, + &pcc_backlight_ops); + if (IS_ERR(pcc->backlight)) + goto out_notify; + + if (!acpi_pcc_retrieve_biosdata(pcc, pcc->sinf)) { + ACPI_DEBUG_PRINT((ACPI_DB_ERROR, + "Couldn't retrieve BIOS data\n")); + goto out_backlight; + } + + /* read the initial brightness setting from the hardware */ + pcc->backlight->props.max_brightness = + pcc->sinf[SINF_AC_MAX_BRIGHT]; + pcc->backlight->props.brightness = pcc->sinf[SINF_AC_CUR_BRIGHT]; + + /* read the initial sticky key mode from the hardware */ + pcc->sticky_mode = pcc->sinf[SINF_STICKY_KEY]; + + /* add sysfs attributes */ + result = sysfs_create_group(&device->dev.kobj, &pcc_attr_group); + if (result) + goto out_backlight; + + return 0; + +out_backlight: + backlight_device_unregister(pcc->backlight); +out_notify: + acpi_remove_notify_handler(pcc->handle, ACPI_DEVICE_NOTIFY, + acpi_pcc_hotkey_notify); +out_input: + input_unregister_device(pcc->input_dev); + /* no need to input_free_device() since core input API refcount and + * free()s the device */ +out_sinf: + kfree(pcc->sinf); +out_hotkey: + kfree(pcc); + + return result; +} + +static int __init acpi_pcc_init(void) +{ + int result = 0; + + if (acpi_disabled) + return -ENODEV; + + result = acpi_bus_register_driver(&acpi_pcc_driver); + if (result < 0) { + ACPI_DEBUG_PRINT((ACPI_DB_ERROR, + "Error registering hotkey driver\n")); + return -ENODEV; + } + + return 0; +} + +static int acpi_pcc_hotkey_remove(struct acpi_device *device, int type) +{ + struct pcc_acpi *pcc = acpi_driver_data(device); + + if (!device || !pcc) + return -EINVAL; + + sysfs_remove_group(&device->dev.kobj, &pcc_attr_group); + + backlight_device_unregister(pcc->backlight); + + acpi_remove_notify_handler(pcc->handle, ACPI_DEVICE_NOTIFY, + acpi_pcc_hotkey_notify); + + input_unregister_device(pcc->input_dev); + /* no need to input_free_device() since core input API refcount and + * free()s the device */ + + kfree(pcc->sinf); + kfree(pcc); + + return 0; +} + +static void __exit acpi_pcc_exit(void) +{ + acpi_bus_unregister_driver(&acpi_pcc_driver); +} + +module_init(acpi_pcc_init); +module_exit(acpi_pcc_exit); diff --git a/drivers/platform/x86/sony-laptop.c b/drivers/platform/x86/sony-laptop.c new file mode 100644 index 000000000000..537959d07148 --- /dev/null +++ b/drivers/platform/x86/sony-laptop.c @@ -0,0 +1,2784 @@ +/* + * ACPI Sony Notebook Control Driver (SNC and SPIC) + * + * Copyright (C) 2004-2005 Stelian Pop <stelian@popies.net> + * Copyright (C) 2007 Mattia Dongili <malattia@linux.it> + * + * Parts of this driver inspired from asus_acpi.c and ibm_acpi.c + * which are copyrighted by their respective authors. + * + * The SNY6001 driver part is based on the sonypi driver which includes + * material from: + * + * Copyright (C) 2001-2005 Stelian Pop <stelian@popies.net> + * + * Copyright (C) 2005 Narayanan R S <nars@kadamba.org> + * + * Copyright (C) 2001-2002 Alcôve <www.alcove.com> + * + * Copyright (C) 2001 Michael Ashley <m.ashley@unsw.edu.au> + * + * Copyright (C) 2001 Junichi Morita <jun1m@mars.dti.ne.jp> + * + * Copyright (C) 2000 Takaya Kinjo <t-kinjo@tc4.so-net.ne.jp> + * + * Copyright (C) 2000 Andrew Tridgell <tridge@valinux.com> + * + * Earlier work by Werner Almesberger, Paul `Rusty' Russell and Paul Mackerras. + * + * 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., 675 Mass Ave, Cambridge, MA 02139, USA. + * + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/moduleparam.h> +#include <linux/init.h> +#include <linux/smp_lock.h> +#include <linux/types.h> +#include <linux/backlight.h> +#include <linux/platform_device.h> +#include <linux/err.h> +#include <linux/dmi.h> +#include <linux/pci.h> +#include <linux/interrupt.h> +#include <linux/delay.h> +#include <linux/input.h> +#include <linux/kfifo.h> +#include <linux/workqueue.h> +#include <linux/acpi.h> +#include <acpi/acpi_drivers.h> +#include <acpi/acpi_bus.h> +#include <asm/uaccess.h> +#include <linux/sonypi.h> +#include <linux/sony-laptop.h> +#ifdef CONFIG_SONYPI_COMPAT +#include <linux/poll.h> +#include <linux/miscdevice.h> +#endif + +#define DRV_PFX "sony-laptop: " +#define dprintk(msg...) do { \ + if (debug) printk(KERN_WARNING DRV_PFX msg); \ +} while (0) + +#define SONY_LAPTOP_DRIVER_VERSION "0.6" + +#define SONY_NC_CLASS "sony-nc" +#define SONY_NC_HID "SNY5001" +#define SONY_NC_DRIVER_NAME "Sony Notebook Control Driver" + +#define SONY_PIC_CLASS "sony-pic" +#define SONY_PIC_HID "SNY6001" +#define SONY_PIC_DRIVER_NAME "Sony Programmable IO Control Driver" + +MODULE_AUTHOR("Stelian Pop, Mattia Dongili"); +MODULE_DESCRIPTION("Sony laptop extras driver (SPIC and SNC ACPI device)"); +MODULE_LICENSE("GPL"); +MODULE_VERSION(SONY_LAPTOP_DRIVER_VERSION); + +static int debug; +module_param(debug, int, 0); +MODULE_PARM_DESC(debug, "set this to 1 (and RTFM) if you want to help " + "the development of this driver"); + +static int no_spic; /* = 0 */ +module_param(no_spic, int, 0444); +MODULE_PARM_DESC(no_spic, + "set this if you don't want to enable the SPIC device"); + +static int compat; /* = 0 */ +module_param(compat, int, 0444); +MODULE_PARM_DESC(compat, + "set this if you want to enable backward compatibility mode"); + +static unsigned long mask = 0xffffffff; +module_param(mask, ulong, 0644); +MODULE_PARM_DESC(mask, + "set this to the mask of event you want to enable (see doc)"); + +static int camera; /* = 0 */ +module_param(camera, int, 0444); +MODULE_PARM_DESC(camera, + "set this to 1 to enable Motion Eye camera controls " + "(only use it if you have a C1VE or C1VN model)"); + +#ifdef CONFIG_SONYPI_COMPAT +static int minor = -1; +module_param(minor, int, 0); +MODULE_PARM_DESC(minor, + "minor number of the misc device for the SPIC compatibility code, " + "default is -1 (automatic)"); +#endif + +/*********** Input Devices ***********/ + +#define SONY_LAPTOP_BUF_SIZE 128 +struct sony_laptop_input_s { + atomic_t users; + struct input_dev *jog_dev; + struct input_dev *key_dev; + struct kfifo *fifo; + spinlock_t fifo_lock; + struct workqueue_struct *wq; +}; +static struct sony_laptop_input_s sony_laptop_input = { + .users = ATOMIC_INIT(0), +}; + +struct sony_laptop_keypress { + struct input_dev *dev; + int key; +}; + +/* Correspondance table between sonypi events + * and input layer indexes in the keymap + */ +static int sony_laptop_input_index[] = { + -1, /* 0 no event */ + -1, /* 1 SONYPI_EVENT_JOGDIAL_DOWN */ + -1, /* 2 SONYPI_EVENT_JOGDIAL_UP */ + -1, /* 3 SONYPI_EVENT_JOGDIAL_DOWN_PRESSED */ + -1, /* 4 SONYPI_EVENT_JOGDIAL_UP_PRESSED */ + -1, /* 5 SONYPI_EVENT_JOGDIAL_PRESSED */ + -1, /* 6 SONYPI_EVENT_JOGDIAL_RELEASED */ + 0, /* 7 SONYPI_EVENT_CAPTURE_PRESSED */ + 1, /* 8 SONYPI_EVENT_CAPTURE_RELEASED */ + 2, /* 9 SONYPI_EVENT_CAPTURE_PARTIALPRESSED */ + 3, /* 10 SONYPI_EVENT_CAPTURE_PARTIALRELEASED */ + 4, /* 11 SONYPI_EVENT_FNKEY_ESC */ + 5, /* 12 SONYPI_EVENT_FNKEY_F1 */ + 6, /* 13 SONYPI_EVENT_FNKEY_F2 */ + 7, /* 14 SONYPI_EVENT_FNKEY_F3 */ + 8, /* 15 SONYPI_EVENT_FNKEY_F4 */ + 9, /* 16 SONYPI_EVENT_FNKEY_F5 */ + 10, /* 17 SONYPI_EVENT_FNKEY_F6 */ + 11, /* 18 SONYPI_EVENT_FNKEY_F7 */ + 12, /* 19 SONYPI_EVENT_FNKEY_F8 */ + 13, /* 20 SONYPI_EVENT_FNKEY_F9 */ + 14, /* 21 SONYPI_EVENT_FNKEY_F10 */ + 15, /* 22 SONYPI_EVENT_FNKEY_F11 */ + 16, /* 23 SONYPI_EVENT_FNKEY_F12 */ + 17, /* 24 SONYPI_EVENT_FNKEY_1 */ + 18, /* 25 SONYPI_EVENT_FNKEY_2 */ + 19, /* 26 SONYPI_EVENT_FNKEY_D */ + 20, /* 27 SONYPI_EVENT_FNKEY_E */ + 21, /* 28 SONYPI_EVENT_FNKEY_F */ + 22, /* 29 SONYPI_EVENT_FNKEY_S */ + 23, /* 30 SONYPI_EVENT_FNKEY_B */ + 24, /* 31 SONYPI_EVENT_BLUETOOTH_PRESSED */ + 25, /* 32 SONYPI_EVENT_PKEY_P1 */ + 26, /* 33 SONYPI_EVENT_PKEY_P2 */ + 27, /* 34 SONYPI_EVENT_PKEY_P3 */ + 28, /* 35 SONYPI_EVENT_BACK_PRESSED */ + -1, /* 36 SONYPI_EVENT_LID_CLOSED */ + -1, /* 37 SONYPI_EVENT_LID_OPENED */ + 29, /* 38 SONYPI_EVENT_BLUETOOTH_ON */ + 30, /* 39 SONYPI_EVENT_BLUETOOTH_OFF */ + 31, /* 40 SONYPI_EVENT_HELP_PRESSED */ + 32, /* 41 SONYPI_EVENT_FNKEY_ONLY */ + 33, /* 42 SONYPI_EVENT_JOGDIAL_FAST_DOWN */ + 34, /* 43 SONYPI_EVENT_JOGDIAL_FAST_UP */ + 35, /* 44 SONYPI_EVENT_JOGDIAL_FAST_DOWN_PRESSED */ + 36, /* 45 SONYPI_EVENT_JOGDIAL_FAST_UP_PRESSED */ + 37, /* 46 SONYPI_EVENT_JOGDIAL_VFAST_DOWN */ + 38, /* 47 SONYPI_EVENT_JOGDIAL_VFAST_UP */ + 39, /* 48 SONYPI_EVENT_JOGDIAL_VFAST_DOWN_PRESSED */ + 40, /* 49 SONYPI_EVENT_JOGDIAL_VFAST_UP_PRESSED */ + 41, /* 50 SONYPI_EVENT_ZOOM_PRESSED */ + 42, /* 51 SONYPI_EVENT_THUMBPHRASE_PRESSED */ + 43, /* 52 SONYPI_EVENT_MEYE_FACE */ + 44, /* 53 SONYPI_EVENT_MEYE_OPPOSITE */ + 45, /* 54 SONYPI_EVENT_MEMORYSTICK_INSERT */ + 46, /* 55 SONYPI_EVENT_MEMORYSTICK_EJECT */ + -1, /* 56 SONYPI_EVENT_ANYBUTTON_RELEASED */ + -1, /* 57 SONYPI_EVENT_BATTERY_INSERT */ + -1, /* 58 SONYPI_EVENT_BATTERY_REMOVE */ + -1, /* 59 SONYPI_EVENT_FNKEY_RELEASED */ + 47, /* 60 SONYPI_EVENT_WIRELESS_ON */ + 48, /* 61 SONYPI_EVENT_WIRELESS_OFF */ + 49, /* 62 SONYPI_EVENT_ZOOM_IN_PRESSED */ + 50, /* 63 SONYPI_EVENT_ZOOM_OUT_PRESSED */ +}; + +static int sony_laptop_input_keycode_map[] = { + KEY_CAMERA, /* 0 SONYPI_EVENT_CAPTURE_PRESSED */ + KEY_RESERVED, /* 1 SONYPI_EVENT_CAPTURE_RELEASED */ + KEY_RESERVED, /* 2 SONYPI_EVENT_CAPTURE_PARTIALPRESSED */ + KEY_RESERVED, /* 3 SONYPI_EVENT_CAPTURE_PARTIALRELEASED */ + KEY_FN_ESC, /* 4 SONYPI_EVENT_FNKEY_ESC */ + KEY_FN_F1, /* 5 SONYPI_EVENT_FNKEY_F1 */ + KEY_FN_F2, /* 6 SONYPI_EVENT_FNKEY_F2 */ + KEY_FN_F3, /* 7 SONYPI_EVENT_FNKEY_F3 */ + KEY_FN_F4, /* 8 SONYPI_EVENT_FNKEY_F4 */ + KEY_FN_F5, /* 9 SONYPI_EVENT_FNKEY_F5 */ + KEY_FN_F6, /* 10 SONYPI_EVENT_FNKEY_F6 */ + KEY_FN_F7, /* 11 SONYPI_EVENT_FNKEY_F7 */ + KEY_FN_F8, /* 12 SONYPI_EVENT_FNKEY_F8 */ + KEY_FN_F9, /* 13 SONYPI_EVENT_FNKEY_F9 */ + KEY_FN_F10, /* 14 SONYPI_EVENT_FNKEY_F10 */ + KEY_FN_F11, /* 15 SONYPI_EVENT_FNKEY_F11 */ + KEY_FN_F12, /* 16 SONYPI_EVENT_FNKEY_F12 */ + KEY_FN_F1, /* 17 SONYPI_EVENT_FNKEY_1 */ + KEY_FN_F2, /* 18 SONYPI_EVENT_FNKEY_2 */ + KEY_FN_D, /* 19 SONYPI_EVENT_FNKEY_D */ + KEY_FN_E, /* 20 SONYPI_EVENT_FNKEY_E */ + KEY_FN_F, /* 21 SONYPI_EVENT_FNKEY_F */ + KEY_FN_S, /* 22 SONYPI_EVENT_FNKEY_S */ + KEY_FN_B, /* 23 SONYPI_EVENT_FNKEY_B */ + KEY_BLUETOOTH, /* 24 SONYPI_EVENT_BLUETOOTH_PRESSED */ + KEY_PROG1, /* 25 SONYPI_EVENT_PKEY_P1 */ + KEY_PROG2, /* 26 SONYPI_EVENT_PKEY_P2 */ + KEY_PROG3, /* 27 SONYPI_EVENT_PKEY_P3 */ + KEY_BACK, /* 28 SONYPI_EVENT_BACK_PRESSED */ + KEY_BLUETOOTH, /* 29 SONYPI_EVENT_BLUETOOTH_ON */ + KEY_BLUETOOTH, /* 30 SONYPI_EVENT_BLUETOOTH_OFF */ + KEY_HELP, /* 31 SONYPI_EVENT_HELP_PRESSED */ + KEY_FN, /* 32 SONYPI_EVENT_FNKEY_ONLY */ + KEY_RESERVED, /* 33 SONYPI_EVENT_JOGDIAL_FAST_DOWN */ + KEY_RESERVED, /* 34 SONYPI_EVENT_JOGDIAL_FAST_UP */ + KEY_RESERVED, /* 35 SONYPI_EVENT_JOGDIAL_FAST_DOWN_PRESSED */ + KEY_RESERVED, /* 36 SONYPI_EVENT_JOGDIAL_FAST_UP_PRESSED */ + KEY_RESERVED, /* 37 SONYPI_EVENT_JOGDIAL_VFAST_DOWN */ + KEY_RESERVED, /* 38 SONYPI_EVENT_JOGDIAL_VFAST_UP */ + KEY_RESERVED, /* 39 SONYPI_EVENT_JOGDIAL_VFAST_DOWN_PRESSED */ + KEY_RESERVED, /* 40 SONYPI_EVENT_JOGDIAL_VFAST_UP_PRESSED */ + KEY_ZOOM, /* 41 SONYPI_EVENT_ZOOM_PRESSED */ + BTN_THUMB, /* 42 SONYPI_EVENT_THUMBPHRASE_PRESSED */ + KEY_RESERVED, /* 43 SONYPI_EVENT_MEYE_FACE */ + KEY_RESERVED, /* 44 SONYPI_EVENT_MEYE_OPPOSITE */ + KEY_RESERVED, /* 45 SONYPI_EVENT_MEMORYSTICK_INSERT */ + KEY_RESERVED, /* 46 SONYPI_EVENT_MEMORYSTICK_EJECT */ + KEY_WLAN, /* 47 SONYPI_EVENT_WIRELESS_ON */ + KEY_WLAN, /* 48 SONYPI_EVENT_WIRELESS_OFF */ + KEY_ZOOMIN, /* 49 SONYPI_EVENT_ZOOM_IN_PRESSED */ + KEY_ZOOMOUT /* 50 SONYPI_EVENT_ZOOM_OUT_PRESSED */ +}; + +/* release buttons after a short delay if pressed */ +static void do_sony_laptop_release_key(struct work_struct *work) +{ + struct sony_laptop_keypress kp; + + while (kfifo_get(sony_laptop_input.fifo, (unsigned char *)&kp, + sizeof(kp)) == sizeof(kp)) { + msleep(10); + input_report_key(kp.dev, kp.key, 0); + input_sync(kp.dev); + } +} +static DECLARE_WORK(sony_laptop_release_key_work, + do_sony_laptop_release_key); + +/* forward event to the input subsystem */ +static void sony_laptop_report_input_event(u8 event) +{ + struct input_dev *jog_dev = sony_laptop_input.jog_dev; + struct input_dev *key_dev = sony_laptop_input.key_dev; + struct sony_laptop_keypress kp = { NULL }; + + if (event == SONYPI_EVENT_FNKEY_RELEASED) { + /* Nothing, not all VAIOs generate this event */ + return; + } + + /* report events */ + switch (event) { + /* jog_dev events */ + case SONYPI_EVENT_JOGDIAL_UP: + case SONYPI_EVENT_JOGDIAL_UP_PRESSED: + input_report_rel(jog_dev, REL_WHEEL, 1); + input_sync(jog_dev); + return; + + case SONYPI_EVENT_JOGDIAL_DOWN: + case SONYPI_EVENT_JOGDIAL_DOWN_PRESSED: + input_report_rel(jog_dev, REL_WHEEL, -1); + input_sync(jog_dev); + return; + + /* key_dev events */ + case SONYPI_EVENT_JOGDIAL_PRESSED: + kp.key = BTN_MIDDLE; + kp.dev = jog_dev; + break; + + default: + if (event >= ARRAY_SIZE(sony_laptop_input_index)) { + dprintk("sony_laptop_report_input_event, event not known: %d\n", event); + break; + } + if (sony_laptop_input_index[event] != -1) { + kp.key = sony_laptop_input_keycode_map[sony_laptop_input_index[event]]; + if (kp.key != KEY_UNKNOWN) + kp.dev = key_dev; + } + break; + } + + if (kp.dev) { + input_report_key(kp.dev, kp.key, 1); + /* we emit the scancode so we can always remap the key */ + input_event(kp.dev, EV_MSC, MSC_SCAN, event); + input_sync(kp.dev); + kfifo_put(sony_laptop_input.fifo, + (unsigned char *)&kp, sizeof(kp)); + + if (!work_pending(&sony_laptop_release_key_work)) + queue_work(sony_laptop_input.wq, + &sony_laptop_release_key_work); + } else + dprintk("unknown input event %.2x\n", event); +} + +static int sony_laptop_setup_input(struct acpi_device *acpi_device) +{ + struct input_dev *jog_dev; + struct input_dev *key_dev; + int i; + int error; + + /* don't run again if already initialized */ + if (atomic_add_return(1, &sony_laptop_input.users) > 1) + return 0; + + /* kfifo */ + spin_lock_init(&sony_laptop_input.fifo_lock); + sony_laptop_input.fifo = + kfifo_alloc(SONY_LAPTOP_BUF_SIZE, GFP_KERNEL, + &sony_laptop_input.fifo_lock); + if (IS_ERR(sony_laptop_input.fifo)) { + printk(KERN_ERR DRV_PFX "kfifo_alloc failed\n"); + error = PTR_ERR(sony_laptop_input.fifo); + goto err_dec_users; + } + + /* init workqueue */ + sony_laptop_input.wq = create_singlethread_workqueue("sony-laptop"); + if (!sony_laptop_input.wq) { + printk(KERN_ERR DRV_PFX + "Unabe to create workqueue.\n"); + error = -ENXIO; + goto err_free_kfifo; + } + + /* input keys */ + key_dev = input_allocate_device(); + if (!key_dev) { + error = -ENOMEM; + goto err_destroy_wq; + } + + key_dev->name = "Sony Vaio Keys"; + key_dev->id.bustype = BUS_ISA; + key_dev->id.vendor = PCI_VENDOR_ID_SONY; + key_dev->dev.parent = &acpi_device->dev; + + /* Initialize the Input Drivers: special keys */ + set_bit(EV_KEY, key_dev->evbit); + set_bit(EV_MSC, key_dev->evbit); + set_bit(MSC_SCAN, key_dev->mscbit); + key_dev->keycodesize = sizeof(sony_laptop_input_keycode_map[0]); + key_dev->keycodemax = ARRAY_SIZE(sony_laptop_input_keycode_map); + key_dev->keycode = &sony_laptop_input_keycode_map; + for (i = 0; i < ARRAY_SIZE(sony_laptop_input_keycode_map); i++) { + if (sony_laptop_input_keycode_map[i] != KEY_RESERVED) { + set_bit(sony_laptop_input_keycode_map[i], + key_dev->keybit); + } + } + + error = input_register_device(key_dev); + if (error) + goto err_free_keydev; + + sony_laptop_input.key_dev = key_dev; + + /* jogdial */ + jog_dev = input_allocate_device(); + if (!jog_dev) { + error = -ENOMEM; + goto err_unregister_keydev; + } + + jog_dev->name = "Sony Vaio Jogdial"; + jog_dev->id.bustype = BUS_ISA; + jog_dev->id.vendor = PCI_VENDOR_ID_SONY; + key_dev->dev.parent = &acpi_device->dev; + + jog_dev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REL); + jog_dev->keybit[BIT_WORD(BTN_MOUSE)] = BIT_MASK(BTN_MIDDLE); + jog_dev->relbit[0] = BIT_MASK(REL_WHEEL); + + error = input_register_device(jog_dev); + if (error) + goto err_free_jogdev; + + sony_laptop_input.jog_dev = jog_dev; + + return 0; + +err_free_jogdev: + input_free_device(jog_dev); + +err_unregister_keydev: + input_unregister_device(key_dev); + /* to avoid kref underflow below at input_free_device */ + key_dev = NULL; + +err_free_keydev: + input_free_device(key_dev); + +err_destroy_wq: + destroy_workqueue(sony_laptop_input.wq); + +err_free_kfifo: + kfifo_free(sony_laptop_input.fifo); + +err_dec_users: + atomic_dec(&sony_laptop_input.users); + return error; +} + +static void sony_laptop_remove_input(void) +{ + /* cleanup only after the last user has gone */ + if (!atomic_dec_and_test(&sony_laptop_input.users)) + return; + + /* flush workqueue first */ + flush_workqueue(sony_laptop_input.wq); + + /* destroy input devs */ + input_unregister_device(sony_laptop_input.key_dev); + sony_laptop_input.key_dev = NULL; + + if (sony_laptop_input.jog_dev) { + input_unregister_device(sony_laptop_input.jog_dev); + sony_laptop_input.jog_dev = NULL; + } + + destroy_workqueue(sony_laptop_input.wq); + kfifo_free(sony_laptop_input.fifo); +} + +/*********** Platform Device ***********/ + +static atomic_t sony_pf_users = ATOMIC_INIT(0); +static struct platform_driver sony_pf_driver = { + .driver = { + .name = "sony-laptop", + .owner = THIS_MODULE, + } +}; +static struct platform_device *sony_pf_device; + +static int sony_pf_add(void) +{ + int ret = 0; + + /* don't run again if already initialized */ + if (atomic_add_return(1, &sony_pf_users) > 1) + return 0; + + ret = platform_driver_register(&sony_pf_driver); + if (ret) + goto out; + + sony_pf_device = platform_device_alloc("sony-laptop", -1); + if (!sony_pf_device) { + ret = -ENOMEM; + goto out_platform_registered; + } + + ret = platform_device_add(sony_pf_device); + if (ret) + goto out_platform_alloced; + + return 0; + + out_platform_alloced: + platform_device_put(sony_pf_device); + sony_pf_device = NULL; + out_platform_registered: + platform_driver_unregister(&sony_pf_driver); + out: + atomic_dec(&sony_pf_users); + return ret; +} + +static void sony_pf_remove(void) +{ + /* deregister only after the last user has gone */ + if (!atomic_dec_and_test(&sony_pf_users)) + return; + + platform_device_del(sony_pf_device); + platform_device_put(sony_pf_device); + platform_driver_unregister(&sony_pf_driver); +} + +/*********** SNC (SNY5001) Device ***********/ + +/* the device uses 1-based values, while the backlight subsystem uses + 0-based values */ +#define SONY_MAX_BRIGHTNESS 8 + +#define SNC_VALIDATE_IN 0 +#define SNC_VALIDATE_OUT 1 + +static ssize_t sony_nc_sysfs_show(struct device *, struct device_attribute *, + char *); +static ssize_t sony_nc_sysfs_store(struct device *, struct device_attribute *, + const char *, size_t); +static int boolean_validate(const int, const int); +static int brightness_default_validate(const int, const int); + +struct sony_nc_value { + char *name; /* name of the entry */ + char **acpiget; /* names of the ACPI get function */ + char **acpiset; /* names of the ACPI set function */ + int (*validate)(const int, const int); /* input/output validation */ + int value; /* current setting */ + int valid; /* Has ever been set */ + int debug; /* active only in debug mode ? */ + struct device_attribute devattr; /* sysfs atribute */ +}; + +#define SNC_HANDLE_NAMES(_name, _values...) \ + static char *snc_##_name[] = { _values, NULL } + +#define SNC_HANDLE(_name, _getters, _setters, _validate, _debug) \ + { \ + .name = __stringify(_name), \ + .acpiget = _getters, \ + .acpiset = _setters, \ + .validate = _validate, \ + .debug = _debug, \ + .devattr = __ATTR(_name, 0, sony_nc_sysfs_show, sony_nc_sysfs_store), \ + } + +#define SNC_HANDLE_NULL { .name = NULL } + +SNC_HANDLE_NAMES(fnkey_get, "GHKE"); + +SNC_HANDLE_NAMES(brightness_def_get, "GPBR"); +SNC_HANDLE_NAMES(brightness_def_set, "SPBR"); + +SNC_HANDLE_NAMES(cdpower_get, "GCDP"); +SNC_HANDLE_NAMES(cdpower_set, "SCDP", "CDPW"); + +SNC_HANDLE_NAMES(audiopower_get, "GAZP"); +SNC_HANDLE_NAMES(audiopower_set, "AZPW"); + +SNC_HANDLE_NAMES(lanpower_get, "GLNP"); +SNC_HANDLE_NAMES(lanpower_set, "LNPW"); + +SNC_HANDLE_NAMES(lidstate_get, "GLID"); + +SNC_HANDLE_NAMES(indicatorlamp_get, "GILS"); +SNC_HANDLE_NAMES(indicatorlamp_set, "SILS"); + +SNC_HANDLE_NAMES(gainbass_get, "GMGB"); +SNC_HANDLE_NAMES(gainbass_set, "CMGB"); + +SNC_HANDLE_NAMES(PID_get, "GPID"); + +SNC_HANDLE_NAMES(CTR_get, "GCTR"); +SNC_HANDLE_NAMES(CTR_set, "SCTR"); + +SNC_HANDLE_NAMES(PCR_get, "GPCR"); +SNC_HANDLE_NAMES(PCR_set, "SPCR"); + +SNC_HANDLE_NAMES(CMI_get, "GCMI"); +SNC_HANDLE_NAMES(CMI_set, "SCMI"); + +static struct sony_nc_value sony_nc_values[] = { + SNC_HANDLE(brightness_default, snc_brightness_def_get, + snc_brightness_def_set, brightness_default_validate, 0), + SNC_HANDLE(fnkey, snc_fnkey_get, NULL, NULL, 0), + SNC_HANDLE(cdpower, snc_cdpower_get, snc_cdpower_set, boolean_validate, 0), + SNC_HANDLE(audiopower, snc_audiopower_get, snc_audiopower_set, + boolean_validate, 0), + SNC_HANDLE(lanpower, snc_lanpower_get, snc_lanpower_set, + boolean_validate, 1), + SNC_HANDLE(lidstate, snc_lidstate_get, NULL, + boolean_validate, 0), + SNC_HANDLE(indicatorlamp, snc_indicatorlamp_get, snc_indicatorlamp_set, + boolean_validate, 0), + SNC_HANDLE(gainbass, snc_gainbass_get, snc_gainbass_set, + boolean_validate, 0), + /* unknown methods */ + SNC_HANDLE(PID, snc_PID_get, NULL, NULL, 1), + SNC_HANDLE(CTR, snc_CTR_get, snc_CTR_set, NULL, 1), + SNC_HANDLE(PCR, snc_PCR_get, snc_PCR_set, NULL, 1), + SNC_HANDLE(CMI, snc_CMI_get, snc_CMI_set, NULL, 1), + SNC_HANDLE_NULL +}; + +static acpi_handle sony_nc_acpi_handle; +static struct acpi_device *sony_nc_acpi_device = NULL; + +/* + * acpi_evaluate_object wrappers + */ +static int acpi_callgetfunc(acpi_handle handle, char *name, int *result) +{ + struct acpi_buffer output; + union acpi_object out_obj; + acpi_status status; + + output.length = sizeof(out_obj); + output.pointer = &out_obj; + + status = acpi_evaluate_object(handle, name, NULL, &output); + if ((status == AE_OK) && (out_obj.type == ACPI_TYPE_INTEGER)) { + *result = out_obj.integer.value; + return 0; + } + + printk(KERN_WARNING DRV_PFX "acpi_callreadfunc failed\n"); + + return -1; +} + +static int acpi_callsetfunc(acpi_handle handle, char *name, int value, + int *result) +{ + struct acpi_object_list params; + union acpi_object in_obj; + struct acpi_buffer output; + union acpi_object out_obj; + acpi_status status; + + params.count = 1; + params.pointer = &in_obj; + in_obj.type = ACPI_TYPE_INTEGER; + in_obj.integer.value = value; + + output.length = sizeof(out_obj); + output.pointer = &out_obj; + + status = acpi_evaluate_object(handle, name, ¶ms, &output); + if (status == AE_OK) { + if (result != NULL) { + if (out_obj.type != ACPI_TYPE_INTEGER) { + printk(KERN_WARNING DRV_PFX "acpi_evaluate_object bad " + "return type\n"); + return -1; + } + *result = out_obj.integer.value; + } + return 0; + } + + printk(KERN_WARNING DRV_PFX "acpi_evaluate_object failed\n"); + + return -1; +} + +/* + * sony_nc_values input/output validate functions + */ + +/* brightness_default_validate: + * + * manipulate input output values to keep consistency with the + * backlight framework for which brightness values are 0-based. + */ +static int brightness_default_validate(const int direction, const int value) +{ + switch (direction) { + case SNC_VALIDATE_OUT: + return value - 1; + case SNC_VALIDATE_IN: + if (value >= 0 && value < SONY_MAX_BRIGHTNESS) + return value + 1; + } + return -EINVAL; +} + +/* boolean_validate: + * + * on input validate boolean values 0/1, on output just pass the + * received value. + */ +static int boolean_validate(const int direction, const int value) +{ + if (direction == SNC_VALIDATE_IN) { + if (value != 0 && value != 1) + return -EINVAL; + } + return value; +} + +/* + * Sysfs show/store common to all sony_nc_values + */ +static ssize_t sony_nc_sysfs_show(struct device *dev, struct device_attribute *attr, + char *buffer) +{ + int value; + struct sony_nc_value *item = + container_of(attr, struct sony_nc_value, devattr); + + if (!*item->acpiget) + return -EIO; + + if (acpi_callgetfunc(sony_nc_acpi_handle, *item->acpiget, &value) < 0) + return -EIO; + + if (item->validate) + value = item->validate(SNC_VALIDATE_OUT, value); + + return snprintf(buffer, PAGE_SIZE, "%d\n", value); +} + +static ssize_t sony_nc_sysfs_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + int value; + struct sony_nc_value *item = + container_of(attr, struct sony_nc_value, devattr); + + if (!item->acpiset) + return -EIO; + + if (count > 31) + return -EINVAL; + + value = simple_strtoul(buffer, NULL, 10); + + if (item->validate) + value = item->validate(SNC_VALIDATE_IN, value); + + if (value < 0) + return value; + + if (acpi_callsetfunc(sony_nc_acpi_handle, *item->acpiset, value, NULL) < 0) + return -EIO; + item->value = value; + item->valid = 1; + return count; +} + + +/* + * Backlight device + */ +static int sony_backlight_update_status(struct backlight_device *bd) +{ + return acpi_callsetfunc(sony_nc_acpi_handle, "SBRT", + bd->props.brightness + 1, NULL); +} + +static int sony_backlight_get_brightness(struct backlight_device *bd) +{ + int value; + + if (acpi_callgetfunc(sony_nc_acpi_handle, "GBRT", &value)) + return 0; + /* brightness levels are 1-based, while backlight ones are 0-based */ + return value - 1; +} + +static struct backlight_device *sony_backlight_device; +static struct backlight_ops sony_backlight_ops = { + .update_status = sony_backlight_update_status, + .get_brightness = sony_backlight_get_brightness, +}; + +/* + * New SNC-only Vaios event mapping to driver known keys + */ +struct sony_nc_event { + u8 data; + u8 event; +}; + +static struct sony_nc_event *sony_nc_events; + +/* Vaio C* --maybe also FE*, N* and AR* ?-- special init sequence + * for Fn keys + */ +static int sony_nc_C_enable(const struct dmi_system_id *id) +{ + int result = 0; + + printk(KERN_NOTICE DRV_PFX "detected %s\n", id->ident); + + sony_nc_events = id->driver_data; + + if (acpi_callsetfunc(sony_nc_acpi_handle, "SN02", 0x4, &result) < 0 + || acpi_callsetfunc(sony_nc_acpi_handle, "SN07", 0x2, &result) < 0 + || acpi_callsetfunc(sony_nc_acpi_handle, "SN02", 0x10, &result) < 0 + || acpi_callsetfunc(sony_nc_acpi_handle, "SN07", 0x0, &result) < 0 + || acpi_callsetfunc(sony_nc_acpi_handle, "SN03", 0x2, &result) < 0 + || acpi_callsetfunc(sony_nc_acpi_handle, "SN07", 0x101, &result) < 0) { + printk(KERN_WARNING DRV_PFX "failed to initialize SNC, some " + "functionalities may be missing\n"); + return 1; + } + return 0; +} + +static struct sony_nc_event sony_C_events[] = { + { 0x81, SONYPI_EVENT_FNKEY_F1 }, + { 0x01, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x85, SONYPI_EVENT_FNKEY_F5 }, + { 0x05, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x86, SONYPI_EVENT_FNKEY_F6 }, + { 0x06, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x87, SONYPI_EVENT_FNKEY_F7 }, + { 0x07, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x8A, SONYPI_EVENT_FNKEY_F10 }, + { 0x0A, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x8C, SONYPI_EVENT_FNKEY_F12 }, + { 0x0C, SONYPI_EVENT_FNKEY_RELEASED }, + { 0, 0 }, +}; + +/* SNC-only model map */ +static const struct dmi_system_id sony_nc_ids[] = { + { + .ident = "Sony Vaio FE Series", + .callback = sony_nc_C_enable, + .driver_data = sony_C_events, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Sony Corporation"), + DMI_MATCH(DMI_PRODUCT_NAME, "VGN-FE"), + }, + }, + { + .ident = "Sony Vaio FZ Series", + .callback = sony_nc_C_enable, + .driver_data = sony_C_events, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Sony Corporation"), + DMI_MATCH(DMI_PRODUCT_NAME, "VGN-FZ"), + }, + }, + { + .ident = "Sony Vaio C Series", + .callback = sony_nc_C_enable, + .driver_data = sony_C_events, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Sony Corporation"), + DMI_MATCH(DMI_PRODUCT_NAME, "VGN-C"), + }, + }, + { + .ident = "Sony Vaio N Series", + .callback = sony_nc_C_enable, + .driver_data = sony_C_events, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Sony Corporation"), + DMI_MATCH(DMI_PRODUCT_NAME, "VGN-N"), + }, + }, + { } +}; + +/* + * ACPI callbacks + */ +static void sony_acpi_notify(acpi_handle handle, u32 event, void *data) +{ + struct sony_nc_event *evmap; + u32 ev = event; + int result; + + if (ev == 0x92) { + /* read the key pressed from EC.GECR + * A call to SN07 with 0x0202 will do it as well respecting + * the current protocol on different OSes + * + * Note: the path for GECR may be + * \_SB.PCI0.LPCB.EC (C, FE, AR, N and friends) + * \_SB.PCI0.PIB.EC0 (VGN-FR notifications are sent directly, no GECR) + * + * TODO: we may want to do the same for the older GHKE -need + * dmi list- so this snippet may become one more callback. + */ + if (acpi_callsetfunc(handle, "SN07", 0x0202, &result) < 0) + dprintk("sony_acpi_notify, unable to decode event 0x%.2x\n", ev); + else + ev = result & 0xFF; + } + + if (sony_nc_events) + for (evmap = sony_nc_events; evmap->event; evmap++) { + if (evmap->data == ev) { + ev = evmap->event; + break; + } + } + + dprintk("sony_acpi_notify, event: 0x%.2x\n", ev); + sony_laptop_report_input_event(ev); + acpi_bus_generate_proc_event(sony_nc_acpi_device, 1, ev); +} + +static acpi_status sony_walk_callback(acpi_handle handle, u32 level, + void *context, void **return_value) +{ + struct acpi_device_info *info; + struct acpi_buffer buffer = {ACPI_ALLOCATE_BUFFER, NULL}; + + if (ACPI_SUCCESS(acpi_get_object_info(handle, &buffer))) { + info = buffer.pointer; + + printk(KERN_WARNING DRV_PFX "method: name: %4.4s, args %X\n", + (char *)&info->name, info->param_count); + + kfree(buffer.pointer); + } + + return AE_OK; +} + +/* + * ACPI device + */ +static int sony_nc_resume(struct acpi_device *device) +{ + struct sony_nc_value *item; + + for (item = sony_nc_values; item->name; item++) { + int ret; + + if (!item->valid) + continue; + ret = acpi_callsetfunc(sony_nc_acpi_handle, *item->acpiset, + item->value, NULL); + if (ret < 0) { + printk("%s: %d\n", __func__, ret); + break; + } + } + + /* set the last requested brightness level */ + if (sony_backlight_device && + !sony_backlight_update_status(sony_backlight_device)) + printk(KERN_WARNING DRV_PFX "unable to restore brightness level\n"); + + /* re-initialize models with specific requirements */ + dmi_check_system(sony_nc_ids); + + return 0; +} + +static int sony_nc_add(struct acpi_device *device) +{ + acpi_status status; + int result = 0; + acpi_handle handle; + struct sony_nc_value *item; + + printk(KERN_INFO DRV_PFX "%s v%s.\n", + SONY_NC_DRIVER_NAME, SONY_LAPTOP_DRIVER_VERSION); + + sony_nc_acpi_device = device; + strcpy(acpi_device_class(device), "sony/hotkey"); + + sony_nc_acpi_handle = device->handle; + + /* read device status */ + result = acpi_bus_get_status(device); + /* bail IFF the above call was successful and the device is not present */ + if (!result && !device->status.present) { + dprintk("Device not present\n"); + result = -ENODEV; + goto outwalk; + } + + if (debug) { + status = acpi_walk_namespace(ACPI_TYPE_METHOD, sony_nc_acpi_handle, + 1, sony_walk_callback, NULL, NULL); + if (ACPI_FAILURE(status)) { + printk(KERN_WARNING DRV_PFX "unable to walk acpi resources\n"); + result = -ENODEV; + goto outwalk; + } + } + + /* try to _INI the device if such method exists (ACPI spec 3.0-6.5.1 + * should be respected as we already checked for the device presence above */ + if (ACPI_SUCCESS(acpi_get_handle(sony_nc_acpi_handle, METHOD_NAME__INI, &handle))) { + dprintk("Invoking _INI\n"); + if (ACPI_FAILURE(acpi_evaluate_object(sony_nc_acpi_handle, METHOD_NAME__INI, + NULL, NULL))) + dprintk("_INI Method failed\n"); + } + + /* setup input devices and helper fifo */ + result = sony_laptop_setup_input(device); + if (result) { + printk(KERN_ERR DRV_PFX + "Unabe to create input devices.\n"); + goto outwalk; + } + + status = acpi_install_notify_handler(sony_nc_acpi_handle, + ACPI_DEVICE_NOTIFY, + sony_acpi_notify, NULL); + if (ACPI_FAILURE(status)) { + printk(KERN_WARNING DRV_PFX "unable to install notify handler (%u)\n", status); + result = -ENODEV; + goto outinput; + } + + if (acpi_video_backlight_support()) { + printk(KERN_INFO DRV_PFX "brightness ignored, must be " + "controlled by ACPI video driver\n"); + } else if (ACPI_SUCCESS(acpi_get_handle(sony_nc_acpi_handle, "GBRT", + &handle))) { + sony_backlight_device = backlight_device_register("sony", NULL, + NULL, + &sony_backlight_ops); + + if (IS_ERR(sony_backlight_device)) { + printk(KERN_WARNING DRV_PFX "unable to register backlight device\n"); + sony_backlight_device = NULL; + } else { + sony_backlight_device->props.brightness = + sony_backlight_get_brightness + (sony_backlight_device); + sony_backlight_device->props.max_brightness = + SONY_MAX_BRIGHTNESS - 1; + } + + } + + /* initialize models with specific requirements */ + dmi_check_system(sony_nc_ids); + + result = sony_pf_add(); + if (result) + goto outbacklight; + + /* create sony_pf sysfs attributes related to the SNC device */ + for (item = sony_nc_values; item->name; ++item) { + + if (!debug && item->debug) + continue; + + /* find the available acpiget as described in the DSDT */ + for (; item->acpiget && *item->acpiget; ++item->acpiget) { + if (ACPI_SUCCESS(acpi_get_handle(sony_nc_acpi_handle, + *item->acpiget, + &handle))) { + dprintk("Found %s getter: %s\n", + item->name, *item->acpiget); + item->devattr.attr.mode |= S_IRUGO; + break; + } + } + + /* find the available acpiset as described in the DSDT */ + for (; item->acpiset && *item->acpiset; ++item->acpiset) { + if (ACPI_SUCCESS(acpi_get_handle(sony_nc_acpi_handle, + *item->acpiset, + &handle))) { + dprintk("Found %s setter: %s\n", + item->name, *item->acpiset); + item->devattr.attr.mode |= S_IWUSR; + break; + } + } + + if (item->devattr.attr.mode != 0) { + result = + device_create_file(&sony_pf_device->dev, + &item->devattr); + if (result) + goto out_sysfs; + } + } + + return 0; + + out_sysfs: + for (item = sony_nc_values; item->name; ++item) { + device_remove_file(&sony_pf_device->dev, &item->devattr); + } + sony_pf_remove(); + + outbacklight: + if (sony_backlight_device) + backlight_device_unregister(sony_backlight_device); + + status = acpi_remove_notify_handler(sony_nc_acpi_handle, + ACPI_DEVICE_NOTIFY, + sony_acpi_notify); + if (ACPI_FAILURE(status)) + printk(KERN_WARNING DRV_PFX "unable to remove notify handler\n"); + + outinput: + sony_laptop_remove_input(); + + outwalk: + return result; +} + +static int sony_nc_remove(struct acpi_device *device, int type) +{ + acpi_status status; + struct sony_nc_value *item; + + if (sony_backlight_device) + backlight_device_unregister(sony_backlight_device); + + sony_nc_acpi_device = NULL; + + status = acpi_remove_notify_handler(sony_nc_acpi_handle, + ACPI_DEVICE_NOTIFY, + sony_acpi_notify); + if (ACPI_FAILURE(status)) + printk(KERN_WARNING DRV_PFX "unable to remove notify handler\n"); + + for (item = sony_nc_values; item->name; ++item) { + device_remove_file(&sony_pf_device->dev, &item->devattr); + } + + sony_pf_remove(); + sony_laptop_remove_input(); + dprintk(SONY_NC_DRIVER_NAME " removed.\n"); + + return 0; +} + +static const struct acpi_device_id sony_device_ids[] = { + {SONY_NC_HID, 0}, + {SONY_PIC_HID, 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, sony_device_ids); + +static const struct acpi_device_id sony_nc_device_ids[] = { + {SONY_NC_HID, 0}, + {"", 0}, +}; + +static struct acpi_driver sony_nc_driver = { + .name = SONY_NC_DRIVER_NAME, + .class = SONY_NC_CLASS, + .ids = sony_nc_device_ids, + .owner = THIS_MODULE, + .ops = { + .add = sony_nc_add, + .remove = sony_nc_remove, + .resume = sony_nc_resume, + }, +}; + +/*********** SPIC (SNY6001) Device ***********/ + +#define SONYPI_DEVICE_TYPE1 0x00000001 +#define SONYPI_DEVICE_TYPE2 0x00000002 +#define SONYPI_DEVICE_TYPE3 0x00000004 +#define SONYPI_DEVICE_TYPE4 0x00000008 + +#define SONYPI_TYPE1_OFFSET 0x04 +#define SONYPI_TYPE2_OFFSET 0x12 +#define SONYPI_TYPE3_OFFSET 0x12 +#define SONYPI_TYPE4_OFFSET 0x12 + +struct sony_pic_ioport { + struct acpi_resource_io io1; + struct acpi_resource_io io2; + struct list_head list; +}; + +struct sony_pic_irq { + struct acpi_resource_irq irq; + struct list_head list; +}; + +struct sonypi_eventtypes { + u8 data; + unsigned long mask; + struct sonypi_event *events; +}; + +struct device_ctrl { + int model; + int (*handle_irq)(const u8, const u8); + u16 evport_offset; + u8 has_camera; + u8 has_bluetooth; + u8 has_wwan; + struct sonypi_eventtypes *event_types; +}; + +struct sony_pic_dev { + struct device_ctrl *control; + struct acpi_device *acpi_dev; + struct sony_pic_irq *cur_irq; + struct sony_pic_ioport *cur_ioport; + struct list_head interrupts; + struct list_head ioports; + struct mutex lock; + u8 camera_power; + u8 bluetooth_power; + u8 wwan_power; +}; + +static struct sony_pic_dev spic_dev = { + .interrupts = LIST_HEAD_INIT(spic_dev.interrupts), + .ioports = LIST_HEAD_INIT(spic_dev.ioports), +}; + +/* Event masks */ +#define SONYPI_JOGGER_MASK 0x00000001 +#define SONYPI_CAPTURE_MASK 0x00000002 +#define SONYPI_FNKEY_MASK 0x00000004 +#define SONYPI_BLUETOOTH_MASK 0x00000008 +#define SONYPI_PKEY_MASK 0x00000010 +#define SONYPI_BACK_MASK 0x00000020 +#define SONYPI_HELP_MASK 0x00000040 +#define SONYPI_LID_MASK 0x00000080 +#define SONYPI_ZOOM_MASK 0x00000100 +#define SONYPI_THUMBPHRASE_MASK 0x00000200 +#define SONYPI_MEYE_MASK 0x00000400 +#define SONYPI_MEMORYSTICK_MASK 0x00000800 +#define SONYPI_BATTERY_MASK 0x00001000 +#define SONYPI_WIRELESS_MASK 0x00002000 + +struct sonypi_event { + u8 data; + u8 event; +}; + +/* The set of possible button release events */ +static struct sonypi_event sonypi_releaseev[] = { + { 0x00, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0, 0 } +}; + +/* The set of possible jogger events */ +static struct sonypi_event sonypi_joggerev[] = { + { 0x1f, SONYPI_EVENT_JOGDIAL_UP }, + { 0x01, SONYPI_EVENT_JOGDIAL_DOWN }, + { 0x5f, SONYPI_EVENT_JOGDIAL_UP_PRESSED }, + { 0x41, SONYPI_EVENT_JOGDIAL_DOWN_PRESSED }, + { 0x1e, SONYPI_EVENT_JOGDIAL_FAST_UP }, + { 0x02, SONYPI_EVENT_JOGDIAL_FAST_DOWN }, + { 0x5e, SONYPI_EVENT_JOGDIAL_FAST_UP_PRESSED }, + { 0x42, SONYPI_EVENT_JOGDIAL_FAST_DOWN_PRESSED }, + { 0x1d, SONYPI_EVENT_JOGDIAL_VFAST_UP }, + { 0x03, SONYPI_EVENT_JOGDIAL_VFAST_DOWN }, + { 0x5d, SONYPI_EVENT_JOGDIAL_VFAST_UP_PRESSED }, + { 0x43, SONYPI_EVENT_JOGDIAL_VFAST_DOWN_PRESSED }, + { 0x40, SONYPI_EVENT_JOGDIAL_PRESSED }, + { 0, 0 } +}; + +/* The set of possible capture button events */ +static struct sonypi_event sonypi_captureev[] = { + { 0x05, SONYPI_EVENT_CAPTURE_PARTIALPRESSED }, + { 0x07, SONYPI_EVENT_CAPTURE_PRESSED }, + { 0x40, SONYPI_EVENT_CAPTURE_PRESSED }, + { 0x01, SONYPI_EVENT_CAPTURE_PARTIALRELEASED }, + { 0, 0 } +}; + +/* The set of possible fnkeys events */ +static struct sonypi_event sonypi_fnkeyev[] = { + { 0x10, SONYPI_EVENT_FNKEY_ESC }, + { 0x11, SONYPI_EVENT_FNKEY_F1 }, + { 0x12, SONYPI_EVENT_FNKEY_F2 }, + { 0x13, SONYPI_EVENT_FNKEY_F3 }, + { 0x14, SONYPI_EVENT_FNKEY_F4 }, + { 0x15, SONYPI_EVENT_FNKEY_F5 }, + { 0x16, SONYPI_EVENT_FNKEY_F6 }, + { 0x17, SONYPI_EVENT_FNKEY_F7 }, + { 0x18, SONYPI_EVENT_FNKEY_F8 }, + { 0x19, SONYPI_EVENT_FNKEY_F9 }, + { 0x1a, SONYPI_EVENT_FNKEY_F10 }, + { 0x1b, SONYPI_EVENT_FNKEY_F11 }, + { 0x1c, SONYPI_EVENT_FNKEY_F12 }, + { 0x1f, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x21, SONYPI_EVENT_FNKEY_1 }, + { 0x22, SONYPI_EVENT_FNKEY_2 }, + { 0x31, SONYPI_EVENT_FNKEY_D }, + { 0x32, SONYPI_EVENT_FNKEY_E }, + { 0x33, SONYPI_EVENT_FNKEY_F }, + { 0x34, SONYPI_EVENT_FNKEY_S }, + { 0x35, SONYPI_EVENT_FNKEY_B }, + { 0x36, SONYPI_EVENT_FNKEY_ONLY }, + { 0, 0 } +}; + +/* The set of possible program key events */ +static struct sonypi_event sonypi_pkeyev[] = { + { 0x01, SONYPI_EVENT_PKEY_P1 }, + { 0x02, SONYPI_EVENT_PKEY_P2 }, + { 0x04, SONYPI_EVENT_PKEY_P3 }, + { 0, 0 } +}; + +/* The set of possible bluetooth events */ +static struct sonypi_event sonypi_blueev[] = { + { 0x55, SONYPI_EVENT_BLUETOOTH_PRESSED }, + { 0x59, SONYPI_EVENT_BLUETOOTH_ON }, + { 0x5a, SONYPI_EVENT_BLUETOOTH_OFF }, + { 0, 0 } +}; + +/* The set of possible wireless events */ +static struct sonypi_event sonypi_wlessev[] = { + { 0x59, SONYPI_EVENT_WIRELESS_ON }, + { 0x5a, SONYPI_EVENT_WIRELESS_OFF }, + { 0, 0 } +}; + +/* The set of possible back button events */ +static struct sonypi_event sonypi_backev[] = { + { 0x20, SONYPI_EVENT_BACK_PRESSED }, + { 0, 0 } +}; + +/* The set of possible help button events */ +static struct sonypi_event sonypi_helpev[] = { + { 0x3b, SONYPI_EVENT_HELP_PRESSED }, + { 0, 0 } +}; + + +/* The set of possible lid events */ +static struct sonypi_event sonypi_lidev[] = { + { 0x51, SONYPI_EVENT_LID_CLOSED }, + { 0x50, SONYPI_EVENT_LID_OPENED }, + { 0, 0 } +}; + +/* The set of possible zoom events */ +static struct sonypi_event sonypi_zoomev[] = { + { 0x39, SONYPI_EVENT_ZOOM_PRESSED }, + { 0x10, SONYPI_EVENT_ZOOM_IN_PRESSED }, + { 0x20, SONYPI_EVENT_ZOOM_OUT_PRESSED }, + { 0, 0 } +}; + +/* The set of possible thumbphrase events */ +static struct sonypi_event sonypi_thumbphraseev[] = { + { 0x3a, SONYPI_EVENT_THUMBPHRASE_PRESSED }, + { 0, 0 } +}; + +/* The set of possible motioneye camera events */ +static struct sonypi_event sonypi_meyeev[] = { + { 0x00, SONYPI_EVENT_MEYE_FACE }, + { 0x01, SONYPI_EVENT_MEYE_OPPOSITE }, + { 0, 0 } +}; + +/* The set of possible memorystick events */ +static struct sonypi_event sonypi_memorystickev[] = { + { 0x53, SONYPI_EVENT_MEMORYSTICK_INSERT }, + { 0x54, SONYPI_EVENT_MEMORYSTICK_EJECT }, + { 0, 0 } +}; + +/* The set of possible battery events */ +static struct sonypi_event sonypi_batteryev[] = { + { 0x20, SONYPI_EVENT_BATTERY_INSERT }, + { 0x30, SONYPI_EVENT_BATTERY_REMOVE }, + { 0, 0 } +}; + +static struct sonypi_eventtypes type1_events[] = { + { 0, 0xffffffff, sonypi_releaseev }, + { 0x70, SONYPI_MEYE_MASK, sonypi_meyeev }, + { 0x30, SONYPI_LID_MASK, sonypi_lidev }, + { 0x60, SONYPI_CAPTURE_MASK, sonypi_captureev }, + { 0x10, SONYPI_JOGGER_MASK, sonypi_joggerev }, + { 0x20, SONYPI_FNKEY_MASK, sonypi_fnkeyev }, + { 0x30, SONYPI_BLUETOOTH_MASK, sonypi_blueev }, + { 0x40, SONYPI_PKEY_MASK, sonypi_pkeyev }, + { 0x30, SONYPI_MEMORYSTICK_MASK, sonypi_memorystickev }, + { 0x40, SONYPI_BATTERY_MASK, sonypi_batteryev }, + { 0 }, +}; +static struct sonypi_eventtypes type2_events[] = { + { 0, 0xffffffff, sonypi_releaseev }, + { 0x38, SONYPI_LID_MASK, sonypi_lidev }, + { 0x11, SONYPI_JOGGER_MASK, sonypi_joggerev }, + { 0x61, SONYPI_CAPTURE_MASK, sonypi_captureev }, + { 0x21, SONYPI_FNKEY_MASK, sonypi_fnkeyev }, + { 0x31, SONYPI_BLUETOOTH_MASK, sonypi_blueev }, + { 0x08, SONYPI_PKEY_MASK, sonypi_pkeyev }, + { 0x11, SONYPI_BACK_MASK, sonypi_backev }, + { 0x21, SONYPI_HELP_MASK, sonypi_helpev }, + { 0x21, SONYPI_ZOOM_MASK, sonypi_zoomev }, + { 0x20, SONYPI_THUMBPHRASE_MASK, sonypi_thumbphraseev }, + { 0x31, SONYPI_MEMORYSTICK_MASK, sonypi_memorystickev }, + { 0x41, SONYPI_BATTERY_MASK, sonypi_batteryev }, + { 0x31, SONYPI_PKEY_MASK, sonypi_pkeyev }, + { 0 }, +}; +static struct sonypi_eventtypes type3_events[] = { + { 0, 0xffffffff, sonypi_releaseev }, + { 0x21, SONYPI_FNKEY_MASK, sonypi_fnkeyev }, + { 0x31, SONYPI_WIRELESS_MASK, sonypi_wlessev }, + { 0x31, SONYPI_MEMORYSTICK_MASK, sonypi_memorystickev }, + { 0x41, SONYPI_BATTERY_MASK, sonypi_batteryev }, + { 0x31, SONYPI_PKEY_MASK, sonypi_pkeyev }, + { 0 }, +}; +static struct sonypi_eventtypes type4_events[] = { + { 0, 0xffffffff, sonypi_releaseev }, + { 0x21, SONYPI_FNKEY_MASK, sonypi_fnkeyev }, + { 0x31, SONYPI_WIRELESS_MASK, sonypi_wlessev }, + { 0x31, SONYPI_MEMORYSTICK_MASK, sonypi_memorystickev }, + { 0x41, SONYPI_BATTERY_MASK, sonypi_batteryev }, + { 0x05, SONYPI_PKEY_MASK, sonypi_pkeyev }, + { 0x05, SONYPI_ZOOM_MASK, sonypi_zoomev }, + { 0x05, SONYPI_CAPTURE_MASK, sonypi_captureev }, + { 0 }, +}; + +/* low level spic calls */ +#define ITERATIONS_LONG 10000 +#define ITERATIONS_SHORT 10 +#define wait_on_command(command, iterations) { \ + unsigned int n = iterations; \ + while (--n && (command)) \ + udelay(1); \ + if (!n) \ + dprintk("command failed at %s : %s (line %d)\n", \ + __FILE__, __func__, __LINE__); \ +} + +static u8 sony_pic_call1(u8 dev) +{ + u8 v1, v2; + + wait_on_command(inb_p(spic_dev.cur_ioport->io1.minimum + 4) & 2, + ITERATIONS_LONG); + outb(dev, spic_dev.cur_ioport->io1.minimum + 4); + v1 = inb_p(spic_dev.cur_ioport->io1.minimum + 4); + v2 = inb_p(spic_dev.cur_ioport->io1.minimum); + dprintk("sony_pic_call1(0x%.2x): 0x%.4x\n", dev, (v2 << 8) | v1); + return v2; +} + +static u8 sony_pic_call2(u8 dev, u8 fn) +{ + u8 v1; + + wait_on_command(inb_p(spic_dev.cur_ioport->io1.minimum + 4) & 2, + ITERATIONS_LONG); + outb(dev, spic_dev.cur_ioport->io1.minimum + 4); + wait_on_command(inb_p(spic_dev.cur_ioport->io1.minimum + 4) & 2, + ITERATIONS_LONG); + outb(fn, spic_dev.cur_ioport->io1.minimum); + v1 = inb_p(spic_dev.cur_ioport->io1.minimum); + dprintk("sony_pic_call2(0x%.2x - 0x%.2x): 0x%.4x\n", dev, fn, v1); + return v1; +} + +static u8 sony_pic_call3(u8 dev, u8 fn, u8 v) +{ + u8 v1; + + wait_on_command(inb_p(spic_dev.cur_ioport->io1.minimum + 4) & 2, ITERATIONS_LONG); + outb(dev, spic_dev.cur_ioport->io1.minimum + 4); + wait_on_command(inb_p(spic_dev.cur_ioport->io1.minimum + 4) & 2, ITERATIONS_LONG); + outb(fn, spic_dev.cur_ioport->io1.minimum); + wait_on_command(inb_p(spic_dev.cur_ioport->io1.minimum + 4) & 2, ITERATIONS_LONG); + outb(v, spic_dev.cur_ioport->io1.minimum); + v1 = inb_p(spic_dev.cur_ioport->io1.minimum); + dprintk("sony_pic_call3(0x%.2x - 0x%.2x - 0x%.2x): 0x%.4x\n", + dev, fn, v, v1); + return v1; +} + +/* + * minidrivers for SPIC models + */ +static int type4_handle_irq(const u8 data_mask, const u8 ev) +{ + /* + * 0x31 could mean we have to take some extra action and wait for + * the next irq for some Type4 models, it will generate a new + * irq and we can read new data from the device: + * - 0x5c and 0x5f requires 0xA0 + * - 0x61 requires 0xB3 + */ + if (data_mask == 0x31) { + if (ev == 0x5c || ev == 0x5f) + sony_pic_call1(0xA0); + else if (ev == 0x61) + sony_pic_call1(0xB3); + return 0; + } + return 1; +} + +static struct device_ctrl spic_types[] = { + { + .model = SONYPI_DEVICE_TYPE1, + .handle_irq = NULL, + .evport_offset = SONYPI_TYPE1_OFFSET, + .event_types = type1_events, + }, + { + .model = SONYPI_DEVICE_TYPE2, + .handle_irq = NULL, + .evport_offset = SONYPI_TYPE2_OFFSET, + .event_types = type2_events, + }, + { + .model = SONYPI_DEVICE_TYPE3, + .handle_irq = NULL, + .evport_offset = SONYPI_TYPE3_OFFSET, + .event_types = type3_events, + }, + { + .model = SONYPI_DEVICE_TYPE4, + .handle_irq = type4_handle_irq, + .evport_offset = SONYPI_TYPE4_OFFSET, + .event_types = type4_events, + }, +}; + +static void sony_pic_detect_device_type(struct sony_pic_dev *dev) +{ + struct pci_dev *pcidev; + + pcidev = pci_get_device(PCI_VENDOR_ID_INTEL, + PCI_DEVICE_ID_INTEL_82371AB_3, NULL); + if (pcidev) { + dev->control = &spic_types[0]; + goto out; + } + + pcidev = pci_get_device(PCI_VENDOR_ID_INTEL, + PCI_DEVICE_ID_INTEL_ICH6_1, NULL); + if (pcidev) { + dev->control = &spic_types[2]; + goto out; + } + + pcidev = pci_get_device(PCI_VENDOR_ID_INTEL, + PCI_DEVICE_ID_INTEL_ICH7_1, NULL); + if (pcidev) { + dev->control = &spic_types[3]; + goto out; + } + + pcidev = pci_get_device(PCI_VENDOR_ID_INTEL, + PCI_DEVICE_ID_INTEL_ICH8_4, NULL); + if (pcidev) { + dev->control = &spic_types[3]; + goto out; + } + + /* default */ + dev->control = &spic_types[1]; + +out: + if (pcidev) + pci_dev_put(pcidev); + + printk(KERN_INFO DRV_PFX "detected Type%d model\n", + dev->control->model == SONYPI_DEVICE_TYPE1 ? 1 : + dev->control->model == SONYPI_DEVICE_TYPE2 ? 2 : + dev->control->model == SONYPI_DEVICE_TYPE3 ? 3 : 4); +} + +/* camera tests and poweron/poweroff */ +#define SONYPI_CAMERA_PICTURE 5 +#define SONYPI_CAMERA_CONTROL 0x10 + +#define SONYPI_CAMERA_BRIGHTNESS 0 +#define SONYPI_CAMERA_CONTRAST 1 +#define SONYPI_CAMERA_HUE 2 +#define SONYPI_CAMERA_COLOR 3 +#define SONYPI_CAMERA_SHARPNESS 4 + +#define SONYPI_CAMERA_EXPOSURE_MASK 0xC +#define SONYPI_CAMERA_WHITE_BALANCE_MASK 0x3 +#define SONYPI_CAMERA_PICTURE_MODE_MASK 0x30 +#define SONYPI_CAMERA_MUTE_MASK 0x40 + +/* the rest don't need a loop until not 0xff */ +#define SONYPI_CAMERA_AGC 6 +#define SONYPI_CAMERA_AGC_MASK 0x30 +#define SONYPI_CAMERA_SHUTTER_MASK 0x7 + +#define SONYPI_CAMERA_SHUTDOWN_REQUEST 7 +#define SONYPI_CAMERA_CONTROL 0x10 + +#define SONYPI_CAMERA_STATUS 7 +#define SONYPI_CAMERA_STATUS_READY 0x2 +#define SONYPI_CAMERA_STATUS_POSITION 0x4 + +#define SONYPI_DIRECTION_BACKWARDS 0x4 + +#define SONYPI_CAMERA_REVISION 8 +#define SONYPI_CAMERA_ROMVERSION 9 + +static int __sony_pic_camera_ready(void) +{ + u8 v; + + v = sony_pic_call2(0x8f, SONYPI_CAMERA_STATUS); + return (v != 0xff && (v & SONYPI_CAMERA_STATUS_READY)); +} + +static int __sony_pic_camera_off(void) +{ + if (!camera) { + printk(KERN_WARNING DRV_PFX "camera control not enabled\n"); + return -ENODEV; + } + + wait_on_command(sony_pic_call3(0x90, SONYPI_CAMERA_PICTURE, + SONYPI_CAMERA_MUTE_MASK), + ITERATIONS_SHORT); + + if (spic_dev.camera_power) { + sony_pic_call2(0x91, 0); + spic_dev.camera_power = 0; + } + return 0; +} + +static int __sony_pic_camera_on(void) +{ + int i, j, x; + + if (!camera) { + printk(KERN_WARNING DRV_PFX "camera control not enabled\n"); + return -ENODEV; + } + + if (spic_dev.camera_power) + return 0; + + for (j = 5; j > 0; j--) { + + for (x = 0; x < 100 && sony_pic_call2(0x91, 0x1); x++) + msleep(10); + sony_pic_call1(0x93); + + for (i = 400; i > 0; i--) { + if (__sony_pic_camera_ready()) + break; + msleep(10); + } + if (i) + break; + } + + if (j == 0) { + printk(KERN_WARNING DRV_PFX "failed to power on camera\n"); + return -ENODEV; + } + + wait_on_command(sony_pic_call3(0x90, SONYPI_CAMERA_CONTROL, + 0x5a), + ITERATIONS_SHORT); + + spic_dev.camera_power = 1; + return 0; +} + +/* External camera command (exported to the motion eye v4l driver) */ +int sony_pic_camera_command(int command, u8 value) +{ + if (!camera) + return -EIO; + + mutex_lock(&spic_dev.lock); + + switch (command) { + case SONY_PIC_COMMAND_SETCAMERA: + if (value) + __sony_pic_camera_on(); + else + __sony_pic_camera_off(); + break; + case SONY_PIC_COMMAND_SETCAMERABRIGHTNESS: + wait_on_command(sony_pic_call3(0x90, SONYPI_CAMERA_BRIGHTNESS, value), + ITERATIONS_SHORT); + break; + case SONY_PIC_COMMAND_SETCAMERACONTRAST: + wait_on_command(sony_pic_call3(0x90, SONYPI_CAMERA_CONTRAST, value), + ITERATIONS_SHORT); + break; + case SONY_PIC_COMMAND_SETCAMERAHUE: + wait_on_command(sony_pic_call3(0x90, SONYPI_CAMERA_HUE, value), + ITERATIONS_SHORT); + break; + case SONY_PIC_COMMAND_SETCAMERACOLOR: + wait_on_command(sony_pic_call3(0x90, SONYPI_CAMERA_COLOR, value), + ITERATIONS_SHORT); + break; + case SONY_PIC_COMMAND_SETCAMERASHARPNESS: + wait_on_command(sony_pic_call3(0x90, SONYPI_CAMERA_SHARPNESS, value), + ITERATIONS_SHORT); + break; + case SONY_PIC_COMMAND_SETCAMERAPICTURE: + wait_on_command(sony_pic_call3(0x90, SONYPI_CAMERA_PICTURE, value), + ITERATIONS_SHORT); + break; + case SONY_PIC_COMMAND_SETCAMERAAGC: + wait_on_command(sony_pic_call3(0x90, SONYPI_CAMERA_AGC, value), + ITERATIONS_SHORT); + break; + default: + printk(KERN_ERR DRV_PFX "sony_pic_camera_command invalid: %d\n", + command); + break; + } + mutex_unlock(&spic_dev.lock); + return 0; +} +EXPORT_SYMBOL(sony_pic_camera_command); + +/* gprs/edge modem (SZ460N and SZ210P), thanks to Joshua Wise */ +static void sony_pic_set_wwanpower(u8 state) +{ + state = !!state; + mutex_lock(&spic_dev.lock); + if (spic_dev.wwan_power == state) { + mutex_unlock(&spic_dev.lock); + return; + } + sony_pic_call2(0xB0, state); + spic_dev.wwan_power = state; + mutex_unlock(&spic_dev.lock); +} + +static ssize_t sony_pic_wwanpower_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + unsigned long value; + if (count > 31) + return -EINVAL; + + value = simple_strtoul(buffer, NULL, 10); + sony_pic_set_wwanpower(value); + + return count; +} + +static ssize_t sony_pic_wwanpower_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + ssize_t count; + mutex_lock(&spic_dev.lock); + count = snprintf(buffer, PAGE_SIZE, "%d\n", spic_dev.wwan_power); + mutex_unlock(&spic_dev.lock); + return count; +} + +/* bluetooth subsystem power state */ +static void __sony_pic_set_bluetoothpower(u8 state) +{ + state = !!state; + if (spic_dev.bluetooth_power == state) + return; + sony_pic_call2(0x96, state); + sony_pic_call1(0x82); + spic_dev.bluetooth_power = state; +} + +static ssize_t sony_pic_bluetoothpower_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + unsigned long value; + if (count > 31) + return -EINVAL; + + value = simple_strtoul(buffer, NULL, 10); + mutex_lock(&spic_dev.lock); + __sony_pic_set_bluetoothpower(value); + mutex_unlock(&spic_dev.lock); + + return count; +} + +static ssize_t sony_pic_bluetoothpower_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + ssize_t count = 0; + mutex_lock(&spic_dev.lock); + count = snprintf(buffer, PAGE_SIZE, "%d\n", spic_dev.bluetooth_power); + mutex_unlock(&spic_dev.lock); + return count; +} + +/* fan speed */ +/* FAN0 information (reverse engineered from ACPI tables) */ +#define SONY_PIC_FAN0_STATUS 0x93 +static int sony_pic_set_fanspeed(unsigned long value) +{ + return ec_write(SONY_PIC_FAN0_STATUS, value); +} + +static int sony_pic_get_fanspeed(u8 *value) +{ + return ec_read(SONY_PIC_FAN0_STATUS, value); +} + +static ssize_t sony_pic_fanspeed_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + unsigned long value; + if (count > 31) + return -EINVAL; + + value = simple_strtoul(buffer, NULL, 10); + if (sony_pic_set_fanspeed(value)) + return -EIO; + + return count; +} + +static ssize_t sony_pic_fanspeed_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + u8 value = 0; + if (sony_pic_get_fanspeed(&value)) + return -EIO; + + return snprintf(buffer, PAGE_SIZE, "%d\n", value); +} + +#define SPIC_ATTR(_name, _mode) \ +struct device_attribute spic_attr_##_name = __ATTR(_name, \ + _mode, sony_pic_## _name ##_show, \ + sony_pic_## _name ##_store) + +static SPIC_ATTR(bluetoothpower, 0644); +static SPIC_ATTR(wwanpower, 0644); +static SPIC_ATTR(fanspeed, 0644); + +static struct attribute *spic_attributes[] = { + &spic_attr_bluetoothpower.attr, + &spic_attr_wwanpower.attr, + &spic_attr_fanspeed.attr, + NULL +}; + +static struct attribute_group spic_attribute_group = { + .attrs = spic_attributes +}; + +/******** SONYPI compatibility **********/ +#ifdef CONFIG_SONYPI_COMPAT + +/* battery / brightness / temperature addresses */ +#define SONYPI_BAT_FLAGS 0x81 +#define SONYPI_LCD_LIGHT 0x96 +#define SONYPI_BAT1_PCTRM 0xa0 +#define SONYPI_BAT1_LEFT 0xa2 +#define SONYPI_BAT1_MAXRT 0xa4 +#define SONYPI_BAT2_PCTRM 0xa8 +#define SONYPI_BAT2_LEFT 0xaa +#define SONYPI_BAT2_MAXRT 0xac +#define SONYPI_BAT1_MAXTK 0xb0 +#define SONYPI_BAT1_FULL 0xb2 +#define SONYPI_BAT2_MAXTK 0xb8 +#define SONYPI_BAT2_FULL 0xba +#define SONYPI_TEMP_STATUS 0xC1 + +struct sonypi_compat_s { + struct fasync_struct *fifo_async; + struct kfifo *fifo; + spinlock_t fifo_lock; + wait_queue_head_t fifo_proc_list; + atomic_t open_count; +}; +static struct sonypi_compat_s sonypi_compat = { + .open_count = ATOMIC_INIT(0), +}; + +static int sonypi_misc_fasync(int fd, struct file *filp, int on) +{ + int retval; + + retval = fasync_helper(fd, filp, on, &sonypi_compat.fifo_async); + if (retval < 0) + return retval; + return 0; +} + +static int sonypi_misc_release(struct inode *inode, struct file *file) +{ + atomic_dec(&sonypi_compat.open_count); + return 0; +} + +static int sonypi_misc_open(struct inode *inode, struct file *file) +{ + /* Flush input queue on first open */ + lock_kernel(); + if (atomic_inc_return(&sonypi_compat.open_count) == 1) + kfifo_reset(sonypi_compat.fifo); + unlock_kernel(); + return 0; +} + +static ssize_t sonypi_misc_read(struct file *file, char __user *buf, + size_t count, loff_t *pos) +{ + ssize_t ret; + unsigned char c; + + if ((kfifo_len(sonypi_compat.fifo) == 0) && + (file->f_flags & O_NONBLOCK)) + return -EAGAIN; + + ret = wait_event_interruptible(sonypi_compat.fifo_proc_list, + kfifo_len(sonypi_compat.fifo) != 0); + if (ret) + return ret; + + while (ret < count && + (kfifo_get(sonypi_compat.fifo, &c, sizeof(c)) == sizeof(c))) { + if (put_user(c, buf++)) + return -EFAULT; + ret++; + } + + if (ret > 0) { + struct inode *inode = file->f_path.dentry->d_inode; + inode->i_atime = current_fs_time(inode->i_sb); + } + + return ret; +} + +static unsigned int sonypi_misc_poll(struct file *file, poll_table *wait) +{ + poll_wait(file, &sonypi_compat.fifo_proc_list, wait); + if (kfifo_len(sonypi_compat.fifo)) + return POLLIN | POLLRDNORM; + return 0; +} + +static int ec_read16(u8 addr, u16 *value) +{ + u8 val_lb, val_hb; + if (ec_read(addr, &val_lb)) + return -1; + if (ec_read(addr + 1, &val_hb)) + return -1; + *value = val_lb | (val_hb << 8); + return 0; +} + +static int sonypi_misc_ioctl(struct inode *ip, struct file *fp, + unsigned int cmd, unsigned long arg) +{ + int ret = 0; + void __user *argp = (void __user *)arg; + u8 val8; + u16 val16; + int value; + + mutex_lock(&spic_dev.lock); + switch (cmd) { + case SONYPI_IOCGBRT: + if (sony_backlight_device == NULL) { + ret = -EIO; + break; + } + if (acpi_callgetfunc(sony_nc_acpi_handle, "GBRT", &value)) { + ret = -EIO; + break; + } + val8 = ((value & 0xff) - 1) << 5; + if (copy_to_user(argp, &val8, sizeof(val8))) + ret = -EFAULT; + break; + case SONYPI_IOCSBRT: + if (sony_backlight_device == NULL) { + ret = -EIO; + break; + } + if (copy_from_user(&val8, argp, sizeof(val8))) { + ret = -EFAULT; + break; + } + if (acpi_callsetfunc(sony_nc_acpi_handle, "SBRT", + (val8 >> 5) + 1, NULL)) { + ret = -EIO; + break; + } + /* sync the backlight device status */ + sony_backlight_device->props.brightness = + sony_backlight_get_brightness(sony_backlight_device); + break; + case SONYPI_IOCGBAT1CAP: + if (ec_read16(SONYPI_BAT1_FULL, &val16)) { + ret = -EIO; + break; + } + if (copy_to_user(argp, &val16, sizeof(val16))) + ret = -EFAULT; + break; + case SONYPI_IOCGBAT1REM: + if (ec_read16(SONYPI_BAT1_LEFT, &val16)) { + ret = -EIO; + break; + } + if (copy_to_user(argp, &val16, sizeof(val16))) + ret = -EFAULT; + break; + case SONYPI_IOCGBAT2CAP: + if (ec_read16(SONYPI_BAT2_FULL, &val16)) { + ret = -EIO; + break; + } + if (copy_to_user(argp, &val16, sizeof(val16))) + ret = -EFAULT; + break; + case SONYPI_IOCGBAT2REM: + if (ec_read16(SONYPI_BAT2_LEFT, &val16)) { + ret = -EIO; + break; + } + if (copy_to_user(argp, &val16, sizeof(val16))) + ret = -EFAULT; + break; + case SONYPI_IOCGBATFLAGS: + if (ec_read(SONYPI_BAT_FLAGS, &val8)) { + ret = -EIO; + break; + } + val8 &= 0x07; + if (copy_to_user(argp, &val8, sizeof(val8))) + ret = -EFAULT; + break; + case SONYPI_IOCGBLUE: + val8 = spic_dev.bluetooth_power; + if (copy_to_user(argp, &val8, sizeof(val8))) + ret = -EFAULT; + break; + case SONYPI_IOCSBLUE: + if (copy_from_user(&val8, argp, sizeof(val8))) { + ret = -EFAULT; + break; + } + __sony_pic_set_bluetoothpower(val8); + break; + /* FAN Controls */ + case SONYPI_IOCGFAN: + if (sony_pic_get_fanspeed(&val8)) { + ret = -EIO; + break; + } + if (copy_to_user(argp, &val8, sizeof(val8))) + ret = -EFAULT; + break; + case SONYPI_IOCSFAN: + if (copy_from_user(&val8, argp, sizeof(val8))) { + ret = -EFAULT; + break; + } + if (sony_pic_set_fanspeed(val8)) + ret = -EIO; + break; + /* GET Temperature (useful under APM) */ + case SONYPI_IOCGTEMP: + if (ec_read(SONYPI_TEMP_STATUS, &val8)) { + ret = -EIO; + break; + } + if (copy_to_user(argp, &val8, sizeof(val8))) + ret = -EFAULT; + break; + default: + ret = -EINVAL; + } + mutex_unlock(&spic_dev.lock); + return ret; +} + +static const struct file_operations sonypi_misc_fops = { + .owner = THIS_MODULE, + .read = sonypi_misc_read, + .poll = sonypi_misc_poll, + .open = sonypi_misc_open, + .release = sonypi_misc_release, + .fasync = sonypi_misc_fasync, + .ioctl = sonypi_misc_ioctl, +}; + +static struct miscdevice sonypi_misc_device = { + .minor = MISC_DYNAMIC_MINOR, + .name = "sonypi", + .fops = &sonypi_misc_fops, +}; + +static void sonypi_compat_report_event(u8 event) +{ + kfifo_put(sonypi_compat.fifo, (unsigned char *)&event, sizeof(event)); + kill_fasync(&sonypi_compat.fifo_async, SIGIO, POLL_IN); + wake_up_interruptible(&sonypi_compat.fifo_proc_list); +} + +static int sonypi_compat_init(void) +{ + int error; + + spin_lock_init(&sonypi_compat.fifo_lock); + sonypi_compat.fifo = kfifo_alloc(SONY_LAPTOP_BUF_SIZE, GFP_KERNEL, + &sonypi_compat.fifo_lock); + if (IS_ERR(sonypi_compat.fifo)) { + printk(KERN_ERR DRV_PFX "kfifo_alloc failed\n"); + return PTR_ERR(sonypi_compat.fifo); + } + + init_waitqueue_head(&sonypi_compat.fifo_proc_list); + + if (minor != -1) + sonypi_misc_device.minor = minor; + error = misc_register(&sonypi_misc_device); + if (error) { + printk(KERN_ERR DRV_PFX "misc_register failed\n"); + goto err_free_kfifo; + } + if (minor == -1) + printk(KERN_INFO DRV_PFX "device allocated minor is %d\n", + sonypi_misc_device.minor); + + return 0; + +err_free_kfifo: + kfifo_free(sonypi_compat.fifo); + return error; +} + +static void sonypi_compat_exit(void) +{ + misc_deregister(&sonypi_misc_device); + kfifo_free(sonypi_compat.fifo); +} +#else +static int sonypi_compat_init(void) { return 0; } +static void sonypi_compat_exit(void) { } +static void sonypi_compat_report_event(u8 event) { } +#endif /* CONFIG_SONYPI_COMPAT */ + +/* + * ACPI callbacks + */ +static acpi_status +sony_pic_read_possible_resource(struct acpi_resource *resource, void *context) +{ + u32 i; + struct sony_pic_dev *dev = (struct sony_pic_dev *)context; + + switch (resource->type) { + case ACPI_RESOURCE_TYPE_START_DEPENDENT: + { + /* start IO enumeration */ + struct sony_pic_ioport *ioport = kzalloc(sizeof(*ioport), GFP_KERNEL); + if (!ioport) + return AE_ERROR; + + list_add(&ioport->list, &dev->ioports); + return AE_OK; + } + + case ACPI_RESOURCE_TYPE_END_DEPENDENT: + /* end IO enumeration */ + return AE_OK; + + case ACPI_RESOURCE_TYPE_IRQ: + { + struct acpi_resource_irq *p = &resource->data.irq; + struct sony_pic_irq *interrupt = NULL; + if (!p || !p->interrupt_count) { + /* + * IRQ descriptors may have no IRQ# bits set, + * particularly those those w/ _STA disabled + */ + dprintk("Blank IRQ resource\n"); + return AE_OK; + } + for (i = 0; i < p->interrupt_count; i++) { + if (!p->interrupts[i]) { + printk(KERN_WARNING DRV_PFX + "Invalid IRQ %d\n", + p->interrupts[i]); + continue; + } + interrupt = kzalloc(sizeof(*interrupt), + GFP_KERNEL); + if (!interrupt) + return AE_ERROR; + + list_add(&interrupt->list, &dev->interrupts); + interrupt->irq.triggering = p->triggering; + interrupt->irq.polarity = p->polarity; + interrupt->irq.sharable = p->sharable; + interrupt->irq.interrupt_count = 1; + interrupt->irq.interrupts[0] = p->interrupts[i]; + } + return AE_OK; + } + case ACPI_RESOURCE_TYPE_IO: + { + struct acpi_resource_io *io = &resource->data.io; + struct sony_pic_ioport *ioport = + list_first_entry(&dev->ioports, struct sony_pic_ioport, list); + if (!io) { + dprintk("Blank IO resource\n"); + return AE_OK; + } + + if (!ioport->io1.minimum) { + memcpy(&ioport->io1, io, sizeof(*io)); + dprintk("IO1 at 0x%.4x (0x%.2x)\n", ioport->io1.minimum, + ioport->io1.address_length); + } + else if (!ioport->io2.minimum) { + memcpy(&ioport->io2, io, sizeof(*io)); + dprintk("IO2 at 0x%.4x (0x%.2x)\n", ioport->io2.minimum, + ioport->io2.address_length); + } + else { + printk(KERN_ERR DRV_PFX "Unknown SPIC Type, more than 2 IO Ports\n"); + return AE_ERROR; + } + return AE_OK; + } + default: + dprintk("Resource %d isn't an IRQ nor an IO port\n", + resource->type); + + case ACPI_RESOURCE_TYPE_END_TAG: + return AE_OK; + } + return AE_CTRL_TERMINATE; +} + +static int sony_pic_possible_resources(struct acpi_device *device) +{ + int result = 0; + acpi_status status = AE_OK; + + if (!device) + return -EINVAL; + + /* get device status */ + /* see acpi_pci_link_get_current acpi_pci_link_get_possible */ + dprintk("Evaluating _STA\n"); + result = acpi_bus_get_status(device); + if (result) { + printk(KERN_WARNING DRV_PFX "Unable to read status\n"); + goto end; + } + + if (!device->status.enabled) + dprintk("Device disabled\n"); + else + dprintk("Device enabled\n"); + + /* + * Query and parse 'method' + */ + dprintk("Evaluating %s\n", METHOD_NAME__PRS); + status = acpi_walk_resources(device->handle, METHOD_NAME__PRS, + sony_pic_read_possible_resource, &spic_dev); + if (ACPI_FAILURE(status)) { + printk(KERN_WARNING DRV_PFX + "Failure evaluating %s\n", + METHOD_NAME__PRS); + result = -ENODEV; + } +end: + return result; +} + +/* + * Disable the spic device by calling its _DIS method + */ +static int sony_pic_disable(struct acpi_device *device) +{ + acpi_status ret = acpi_evaluate_object(device->handle, "_DIS", NULL, + NULL); + + if (ACPI_FAILURE(ret) && ret != AE_NOT_FOUND) + return -ENXIO; + + dprintk("Device disabled\n"); + return 0; +} + + +/* + * Based on drivers/acpi/pci_link.c:acpi_pci_link_set + * + * Call _SRS to set current resources + */ +static int sony_pic_enable(struct acpi_device *device, + struct sony_pic_ioport *ioport, struct sony_pic_irq *irq) +{ + acpi_status status; + int result = 0; + /* Type 1 resource layout is: + * IO + * IO + * IRQNoFlags + * End + * + * Type 2 and 3 resource layout is: + * IO + * IRQNoFlags + * End + */ + struct { + struct acpi_resource res1; + struct acpi_resource res2; + struct acpi_resource res3; + struct acpi_resource res4; + } *resource; + struct acpi_buffer buffer = { 0, NULL }; + + if (!ioport || !irq) + return -EINVAL; + + /* init acpi_buffer */ + resource = kzalloc(sizeof(*resource) + 1, GFP_KERNEL); + if (!resource) + return -ENOMEM; + + buffer.length = sizeof(*resource) + 1; + buffer.pointer = resource; + + /* setup Type 1 resources */ + if (spic_dev.control->model == SONYPI_DEVICE_TYPE1) { + + /* setup io resources */ + resource->res1.type = ACPI_RESOURCE_TYPE_IO; + resource->res1.length = sizeof(struct acpi_resource); + memcpy(&resource->res1.data.io, &ioport->io1, + sizeof(struct acpi_resource_io)); + + resource->res2.type = ACPI_RESOURCE_TYPE_IO; + resource->res2.length = sizeof(struct acpi_resource); + memcpy(&resource->res2.data.io, &ioport->io2, + sizeof(struct acpi_resource_io)); + + /* setup irq resource */ + resource->res3.type = ACPI_RESOURCE_TYPE_IRQ; + resource->res3.length = sizeof(struct acpi_resource); + memcpy(&resource->res3.data.irq, &irq->irq, + sizeof(struct acpi_resource_irq)); + /* we requested a shared irq */ + resource->res3.data.irq.sharable = ACPI_SHARED; + + resource->res4.type = ACPI_RESOURCE_TYPE_END_TAG; + + } + /* setup Type 2/3 resources */ + else { + /* setup io resource */ + resource->res1.type = ACPI_RESOURCE_TYPE_IO; + resource->res1.length = sizeof(struct acpi_resource); + memcpy(&resource->res1.data.io, &ioport->io1, + sizeof(struct acpi_resource_io)); + + /* setup irq resource */ + resource->res2.type = ACPI_RESOURCE_TYPE_IRQ; + resource->res2.length = sizeof(struct acpi_resource); + memcpy(&resource->res2.data.irq, &irq->irq, + sizeof(struct acpi_resource_irq)); + /* we requested a shared irq */ + resource->res2.data.irq.sharable = ACPI_SHARED; + + resource->res3.type = ACPI_RESOURCE_TYPE_END_TAG; + } + + /* Attempt to set the resource */ + dprintk("Evaluating _SRS\n"); + status = acpi_set_current_resources(device->handle, &buffer); + + /* check for total failure */ + if (ACPI_FAILURE(status)) { + printk(KERN_ERR DRV_PFX "Error evaluating _SRS\n"); + result = -ENODEV; + goto end; + } + + /* Necessary device initializations calls (from sonypi) */ + sony_pic_call1(0x82); + sony_pic_call2(0x81, 0xff); + sony_pic_call1(compat ? 0x92 : 0x82); + +end: + kfree(resource); + return result; +} + +/***************** + * + * ISR: some event is available + * + *****************/ +static irqreturn_t sony_pic_irq(int irq, void *dev_id) +{ + int i, j; + u8 ev = 0; + u8 data_mask = 0; + u8 device_event = 0; + + struct sony_pic_dev *dev = (struct sony_pic_dev *) dev_id; + + ev = inb_p(dev->cur_ioport->io1.minimum); + if (dev->cur_ioport->io2.minimum) + data_mask = inb_p(dev->cur_ioport->io2.minimum); + else + data_mask = inb_p(dev->cur_ioport->io1.minimum + + dev->control->evport_offset); + + dprintk("event ([%.2x] [%.2x]) at port 0x%.4x(+0x%.2x)\n", + ev, data_mask, dev->cur_ioport->io1.minimum, + dev->control->evport_offset); + + if (ev == 0x00 || ev == 0xff) + return IRQ_HANDLED; + + for (i = 0; dev->control->event_types[i].mask; i++) { + + if ((data_mask & dev->control->event_types[i].data) != + dev->control->event_types[i].data) + continue; + + if (!(mask & dev->control->event_types[i].mask)) + continue; + + for (j = 0; dev->control->event_types[i].events[j].event; j++) { + if (ev == dev->control->event_types[i].events[j].data) { + device_event = + dev->control-> + event_types[i].events[j].event; + goto found; + } + } + } + /* Still not able to decode the event try to pass + * it over to the minidriver + */ + if (dev->control->handle_irq && + dev->control->handle_irq(data_mask, ev) == 0) + return IRQ_HANDLED; + + dprintk("unknown event ([%.2x] [%.2x]) at port 0x%.4x(+0x%.2x)\n", + ev, data_mask, dev->cur_ioport->io1.minimum, + dev->control->evport_offset); + return IRQ_HANDLED; + +found: + sony_laptop_report_input_event(device_event); + acpi_bus_generate_proc_event(dev->acpi_dev, 1, device_event); + sonypi_compat_report_event(device_event); + + return IRQ_HANDLED; +} + +/***************** + * + * ACPI driver + * + *****************/ +static int sony_pic_remove(struct acpi_device *device, int type) +{ + struct sony_pic_ioport *io, *tmp_io; + struct sony_pic_irq *irq, *tmp_irq; + + if (sony_pic_disable(device)) { + printk(KERN_ERR DRV_PFX "Couldn't disable device.\n"); + return -ENXIO; + } + + free_irq(spic_dev.cur_irq->irq.interrupts[0], &spic_dev); + release_region(spic_dev.cur_ioport->io1.minimum, + spic_dev.cur_ioport->io1.address_length); + if (spic_dev.cur_ioport->io2.minimum) + release_region(spic_dev.cur_ioport->io2.minimum, + spic_dev.cur_ioport->io2.address_length); + + sonypi_compat_exit(); + + sony_laptop_remove_input(); + + /* pf attrs */ + sysfs_remove_group(&sony_pf_device->dev.kobj, &spic_attribute_group); + sony_pf_remove(); + + list_for_each_entry_safe(io, tmp_io, &spic_dev.ioports, list) { + list_del(&io->list); + kfree(io); + } + list_for_each_entry_safe(irq, tmp_irq, &spic_dev.interrupts, list) { + list_del(&irq->list); + kfree(irq); + } + spic_dev.cur_ioport = NULL; + spic_dev.cur_irq = NULL; + + dprintk(SONY_PIC_DRIVER_NAME " removed.\n"); + return 0; +} + +static int sony_pic_add(struct acpi_device *device) +{ + int result; + struct sony_pic_ioport *io, *tmp_io; + struct sony_pic_irq *irq, *tmp_irq; + + printk(KERN_INFO DRV_PFX "%s v%s.\n", + SONY_PIC_DRIVER_NAME, SONY_LAPTOP_DRIVER_VERSION); + + spic_dev.acpi_dev = device; + strcpy(acpi_device_class(device), "sony/hotkey"); + sony_pic_detect_device_type(&spic_dev); + mutex_init(&spic_dev.lock); + + /* read _PRS resources */ + result = sony_pic_possible_resources(device); + if (result) { + printk(KERN_ERR DRV_PFX + "Unabe to read possible resources.\n"); + goto err_free_resources; + } + + /* setup input devices and helper fifo */ + result = sony_laptop_setup_input(device); + if (result) { + printk(KERN_ERR DRV_PFX + "Unabe to create input devices.\n"); + goto err_free_resources; + } + + if (sonypi_compat_init()) + goto err_remove_input; + + /* request io port */ + list_for_each_entry_reverse(io, &spic_dev.ioports, list) { + if (request_region(io->io1.minimum, io->io1.address_length, + "Sony Programable I/O Device")) { + dprintk("I/O port1: 0x%.4x (0x%.4x) + 0x%.2x\n", + io->io1.minimum, io->io1.maximum, + io->io1.address_length); + /* Type 1 have 2 ioports */ + if (io->io2.minimum) { + if (request_region(io->io2.minimum, + io->io2.address_length, + "Sony Programable I/O Device")) { + dprintk("I/O port2: 0x%.4x (0x%.4x) + 0x%.2x\n", + io->io2.minimum, io->io2.maximum, + io->io2.address_length); + spic_dev.cur_ioport = io; + break; + } + else { + dprintk("Unable to get I/O port2: " + "0x%.4x (0x%.4x) + 0x%.2x\n", + io->io2.minimum, io->io2.maximum, + io->io2.address_length); + release_region(io->io1.minimum, + io->io1.address_length); + } + } + else { + spic_dev.cur_ioport = io; + break; + } + } + } + if (!spic_dev.cur_ioport) { + printk(KERN_ERR DRV_PFX "Failed to request_region.\n"); + result = -ENODEV; + goto err_remove_compat; + } + + /* request IRQ */ + list_for_each_entry_reverse(irq, &spic_dev.interrupts, list) { + if (!request_irq(irq->irq.interrupts[0], sony_pic_irq, + IRQF_SHARED, "sony-laptop", &spic_dev)) { + dprintk("IRQ: %d - triggering: %d - " + "polarity: %d - shr: %d\n", + irq->irq.interrupts[0], + irq->irq.triggering, + irq->irq.polarity, + irq->irq.sharable); + spic_dev.cur_irq = irq; + break; + } + } + if (!spic_dev.cur_irq) { + printk(KERN_ERR DRV_PFX "Failed to request_irq.\n"); + result = -ENODEV; + goto err_release_region; + } + + /* set resource status _SRS */ + result = sony_pic_enable(device, spic_dev.cur_ioport, spic_dev.cur_irq); + if (result) { + printk(KERN_ERR DRV_PFX "Couldn't enable device.\n"); + goto err_free_irq; + } + + spic_dev.bluetooth_power = -1; + /* create device attributes */ + result = sony_pf_add(); + if (result) + goto err_disable_device; + + result = sysfs_create_group(&sony_pf_device->dev.kobj, &spic_attribute_group); + if (result) + goto err_remove_pf; + + return 0; + +err_remove_pf: + sony_pf_remove(); + +err_disable_device: + sony_pic_disable(device); + +err_free_irq: + free_irq(spic_dev.cur_irq->irq.interrupts[0], &spic_dev); + +err_release_region: + release_region(spic_dev.cur_ioport->io1.minimum, + spic_dev.cur_ioport->io1.address_length); + if (spic_dev.cur_ioport->io2.minimum) + release_region(spic_dev.cur_ioport->io2.minimum, + spic_dev.cur_ioport->io2.address_length); + +err_remove_compat: + sonypi_compat_exit(); + +err_remove_input: + sony_laptop_remove_input(); + +err_free_resources: + list_for_each_entry_safe(io, tmp_io, &spic_dev.ioports, list) { + list_del(&io->list); + kfree(io); + } + list_for_each_entry_safe(irq, tmp_irq, &spic_dev.interrupts, list) { + list_del(&irq->list); + kfree(irq); + } + spic_dev.cur_ioport = NULL; + spic_dev.cur_irq = NULL; + + return result; +} + +static int sony_pic_suspend(struct acpi_device *device, pm_message_t state) +{ + if (sony_pic_disable(device)) + return -ENXIO; + return 0; +} + +static int sony_pic_resume(struct acpi_device *device) +{ + sony_pic_enable(device, spic_dev.cur_ioport, spic_dev.cur_irq); + return 0; +} + +static const struct acpi_device_id sony_pic_device_ids[] = { + {SONY_PIC_HID, 0}, + {"", 0}, +}; + +static struct acpi_driver sony_pic_driver = { + .name = SONY_PIC_DRIVER_NAME, + .class = SONY_PIC_CLASS, + .ids = sony_pic_device_ids, + .owner = THIS_MODULE, + .ops = { + .add = sony_pic_add, + .remove = sony_pic_remove, + .suspend = sony_pic_suspend, + .resume = sony_pic_resume, + }, +}; + +static struct dmi_system_id __initdata sonypi_dmi_table[] = { + { + .ident = "Sony Vaio", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Sony Corporation"), + DMI_MATCH(DMI_PRODUCT_NAME, "PCG-"), + }, + }, + { + .ident = "Sony Vaio", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Sony Corporation"), + DMI_MATCH(DMI_PRODUCT_NAME, "VGN-"), + }, + }, + { } +}; + +static int __init sony_laptop_init(void) +{ + int result; + + if (!no_spic && dmi_check_system(sonypi_dmi_table)) { + result = acpi_bus_register_driver(&sony_pic_driver); + if (result) { + printk(KERN_ERR DRV_PFX + "Unable to register SPIC driver."); + goto out; + } + } + + result = acpi_bus_register_driver(&sony_nc_driver); + if (result) { + printk(KERN_ERR DRV_PFX "Unable to register SNC driver."); + goto out_unregister_pic; + } + + return 0; + +out_unregister_pic: + if (!no_spic) + acpi_bus_unregister_driver(&sony_pic_driver); +out: + return result; +} + +static void __exit sony_laptop_exit(void) +{ + acpi_bus_unregister_driver(&sony_nc_driver); + if (!no_spic) + acpi_bus_unregister_driver(&sony_pic_driver); +} + +module_init(sony_laptop_init); +module_exit(sony_laptop_exit); diff --git a/drivers/platform/x86/tc1100-wmi.c b/drivers/platform/x86/tc1100-wmi.c new file mode 100644 index 000000000000..b4a4aa9ee482 --- /dev/null +++ b/drivers/platform/x86/tc1100-wmi.c @@ -0,0 +1,289 @@ +/* + * HP Compaq TC1100 Tablet WMI Extras Driver + * + * Copyright (C) 2007 Carlos Corbacho <carlos@strangeworlds.co.uk> + * Copyright (C) 2004 Jamey Hicks <jamey.hicks@hp.com> + * Copyright (C) 2001, 2002 Andy Grover <andrew.grover@intel.com> + * Copyright (C) 2001, 2002 Paul Diefenbaugh <paul.s.diefenbaugh@intel.com> + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * 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., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/types.h> +#include <acpi/acpi.h> +#include <acpi/acpi_bus.h> +#include <acpi/acpi_drivers.h> +#include <linux/platform_device.h> + +#define GUID "C364AC71-36DB-495A-8494-B439D472A505" + +#define TC1100_INSTANCE_WIRELESS 1 +#define TC1100_INSTANCE_JOGDIAL 2 + +#define TC1100_LOGPREFIX "tc1100-wmi: " +#define TC1100_INFO KERN_INFO TC1100_LOGPREFIX + +MODULE_AUTHOR("Jamey Hicks, Carlos Corbacho"); +MODULE_DESCRIPTION("HP Compaq TC1100 Tablet WMI Extras"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("wmi:C364AC71-36DB-495A-8494-B439D472A505"); + +static int tc1100_probe(struct platform_device *device); +static int tc1100_remove(struct platform_device *device); +static int tc1100_suspend(struct platform_device *device, pm_message_t state); +static int tc1100_resume(struct platform_device *device); + +static struct platform_driver tc1100_driver = { + .driver = { + .name = "tc1100-wmi", + .owner = THIS_MODULE, + }, + .probe = tc1100_probe, + .remove = tc1100_remove, + .suspend = tc1100_suspend, + .resume = tc1100_resume, +}; + +static struct platform_device *tc1100_device; + +struct tc1100_data { + u32 wireless; + u32 jogdial; +}; + +static struct tc1100_data suspend_data; + +/* -------------------------------------------------------------------------- + Device Management + -------------------------------------------------------------------------- */ + +static int get_state(u32 *out, u8 instance) +{ + u32 tmp; + acpi_status status; + struct acpi_buffer result = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + + if (!out) + return -EINVAL; + + if (instance > 2) + return -ENODEV; + + status = wmi_query_block(GUID, instance, &result); + if (ACPI_FAILURE(status)) + return -ENODEV; + + obj = (union acpi_object *) result.pointer; + if (obj && obj->type == ACPI_TYPE_BUFFER && + obj->buffer.length == sizeof(u32)) { + tmp = *((u32 *) obj->buffer.pointer); + } else { + tmp = 0; + } + + if (result.length > 0 && result.pointer) + kfree(result.pointer); + + switch (instance) { + case TC1100_INSTANCE_WIRELESS: + *out = (tmp == 3) ? 1 : 0; + return 0; + case TC1100_INSTANCE_JOGDIAL: + *out = (tmp == 1) ? 1 : 0; + return 0; + default: + return -ENODEV; + } +} + +static int set_state(u32 *in, u8 instance) +{ + u32 value; + acpi_status status; + struct acpi_buffer input; + + if (!in) + return -EINVAL; + + if (instance > 2) + return -ENODEV; + + switch (instance) { + case TC1100_INSTANCE_WIRELESS: + value = (*in) ? 1 : 2; + break; + case TC1100_INSTANCE_JOGDIAL: + value = (*in) ? 0 : 1; + break; + default: + return -ENODEV; + } + + input.length = sizeof(u32); + input.pointer = &value; + + status = wmi_set_block(GUID, instance, &input); + if (ACPI_FAILURE(status)) + return -ENODEV; + + return 0; +} + +/* -------------------------------------------------------------------------- + FS Interface (/sys) + -------------------------------------------------------------------------- */ + +/* + * Read/ write bool sysfs macro + */ +#define show_set_bool(value, instance) \ +static ssize_t \ +show_bool_##value(struct device *dev, struct device_attribute *attr, \ + char *buf) \ +{ \ + u32 result; \ + acpi_status status = get_state(&result, instance); \ + if (ACPI_SUCCESS(status)) \ + return sprintf(buf, "%d\n", result); \ + return sprintf(buf, "Read error\n"); \ +} \ +\ +static ssize_t \ +set_bool_##value(struct device *dev, struct device_attribute *attr, \ + const char *buf, size_t count) \ +{ \ + u32 tmp = simple_strtoul(buf, NULL, 10); \ + acpi_status status = set_state(&tmp, instance); \ + if (ACPI_FAILURE(status)) \ + return -EINVAL; \ + return count; \ +} \ +static DEVICE_ATTR(value, S_IWUGO | S_IRUGO | S_IWUSR, \ + show_bool_##value, set_bool_##value); + +show_set_bool(wireless, TC1100_INSTANCE_WIRELESS); +show_set_bool(jogdial, TC1100_INSTANCE_JOGDIAL); + +static void remove_fs(void) +{ + device_remove_file(&tc1100_device->dev, &dev_attr_wireless); + device_remove_file(&tc1100_device->dev, &dev_attr_jogdial); +} + +static int add_fs(void) +{ + int ret; + + ret = device_create_file(&tc1100_device->dev, &dev_attr_wireless); + if (ret) + goto add_sysfs_error; + + ret = device_create_file(&tc1100_device->dev, &dev_attr_jogdial); + if (ret) + goto add_sysfs_error; + + return ret; + +add_sysfs_error: + remove_fs(); + return ret; +} + +/* -------------------------------------------------------------------------- + Driver Model + -------------------------------------------------------------------------- */ + +static int tc1100_probe(struct platform_device *device) +{ + int result = 0; + + result = add_fs(); + return result; +} + + +static int tc1100_remove(struct platform_device *device) +{ + remove_fs(); + return 0; +} + +static int tc1100_suspend(struct platform_device *dev, pm_message_t state) +{ + int ret; + + ret = get_state(&suspend_data.wireless, TC1100_INSTANCE_WIRELESS); + if (ret) + return ret; + + ret = get_state(&suspend_data.jogdial, TC1100_INSTANCE_JOGDIAL); + if (ret) + return ret; + + return ret; +} + +static int tc1100_resume(struct platform_device *dev) +{ + int ret; + + ret = set_state(&suspend_data.wireless, TC1100_INSTANCE_WIRELESS); + if (ret) + return ret; + + ret = set_state(&suspend_data.jogdial, TC1100_INSTANCE_JOGDIAL); + if (ret) + return ret; + + return ret; +} + +static int __init tc1100_init(void) +{ + int result = 0; + + if (!wmi_has_guid(GUID)) + return -ENODEV; + + result = platform_driver_register(&tc1100_driver); + if (result) + return result; + + tc1100_device = platform_device_alloc("tc1100-wmi", -1); + platform_device_add(tc1100_device); + + printk(TC1100_INFO "HP Compaq TC1100 Tablet WMI Extras loaded\n"); + + return result; +} + +static void __exit tc1100_exit(void) +{ + platform_device_del(tc1100_device); + platform_driver_unregister(&tc1100_driver); + + printk(TC1100_INFO "HP Compaq TC1100 Tablet WMI Extras unloaded\n"); +} + +module_init(tc1100_init); +module_exit(tc1100_exit); diff --git a/drivers/platform/x86/thinkpad_acpi.c b/drivers/platform/x86/thinkpad_acpi.c new file mode 100644 index 000000000000..3478453eba7a --- /dev/null +++ b/drivers/platform/x86/thinkpad_acpi.c @@ -0,0 +1,6948 @@ +/* + * thinkpad_acpi.c - ThinkPad ACPI Extras + * + * + * Copyright (C) 2004-2005 Borislav Deianov <borislav@users.sf.net> + * Copyright (C) 2006-2008 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 TPACPI_VERSION "0.21" +#define TPACPI_SYSFS_VERSION 0x020200 + +/* + * 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/kthread.h> +#include <linux/freezer.h> +#include <linux/delay.h> + +#include <linux/nvram.h> +#include <linux/proc_fs.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 <asm/uaccess.h> + +#include <linux/dmi.h> +#include <linux/jiffies.h> +#include <linux/workqueue.h> + +#include <acpi/acpi_drivers.h> + +#include <linux/pci_ids.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, +}; + +/* ACPI HIDs */ +#define TPACPI_ACPI_HKEY_HID "IBM0068" + +/* Input IDs */ +#define TPACPI_HKEY_INPUT_PRODUCT 0x5054 /* "TP" */ +#define TPACPI_HKEY_INPUT_VERSION 0x4101 + + +/**************************************************************************** + * 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 + +/* rfkill switches */ +enum { + TPACPI_RFK_BLUETOOTH_SW_ID = 0, + TPACPI_RFK_WWAN_SW_ID, +}; + +/* Debugging */ +#define TPACPI_LOG TPACPI_FILE ": " +#define TPACPI_ERR KERN_ERR TPACPI_LOG +#define TPACPI_NOTICE KERN_NOTICE TPACPI_LOG +#define TPACPI_INFO KERN_INFO TPACPI_LOG +#define TPACPI_DEBUG KERN_DEBUG TPACPI_LOG + +#define TPACPI_DBG_ALL 0xffff +#define TPACPI_DBG_INIT 0x0001 +#define TPACPI_DBG_EXIT 0x0002 +#define dbg_printk(a_dbg_level, format, arg...) \ + do { if (dbg_level & a_dbg_level) \ + printk(TPACPI_DEBUG "%s: " format, __func__ , ## arg); \ + } while (0) +#ifdef CONFIG_THINKPAD_ACPI_DEBUG +#define vdbg_printk(a_dbg_level, format, arg...) \ + dbg_printk(a_dbg_level, format, ## arg) +static const char *str_supported(int is_supported); +#else +#define vdbg_printk(a_dbg_level, format, arg...) +#endif + +#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) (char *); + int (*write) (char *); + void (*exit) (void); + void (*resume) (void); + void (*suspend) (pm_message_t state); + + 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 *); + struct ibm_struct *data; +}; + +static struct { +#ifdef CONFIG_THINKPAD_ACPI_BAY + u32 bay_status:1; + u32 bay_eject:1; + u32 bay_status2:1; + u32 bay_eject2:1; +#endif + u32 bluetooth:1; + u32 hotkey:1; + u32 hotkey_mask:1; + u32 hotkey_wlsw:1; + u32 hotkey_tablet:1; + u32 light:1; + u32 light_status:1; + u32 bright_16levels:1; + u32 bright_acpimode:1; + u32 wan:1; + u32 fan_ctrl_status_undef: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; +} tp_features; + +static struct { + u16 hotkey_mask_ff:1; + u16 bright_cmos_ec_unsync: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; /* Big Endian, TP-1Y = 0x5931, 0 = unknown */ + u16 ec_model; + + 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; + +/* Special LED class that can defer work */ +struct tpacpi_led_classdev { + struct led_classdev led_classdev; + struct work_struct work; + enum led_brightness new_brightness; + unsigned int led; +}; + +/**************************************************************************** + **************************************************************************** + * + * ACPI Helpers and device model + * + **************************************************************************** + ****************************************************************************/ + +/************************************************************************* + * ACPI basic handles + */ + +static acpi_handle root_handle; + +#define TPACPI_HANDLE(object, parent, paths...) \ + static acpi_handle object##_handle; \ + static acpi_handle *object##_parent = &parent##_handle; \ + static char *object##_path; \ + static char *object##_paths[] = { paths } + +TPACPI_HANDLE(ec, root, "\\_SB.PCI0.ISA.EC0", /* 240, 240x */ + "\\_SB.PCI.ISA.EC", /* 570 */ + "\\_SB.PCI0.ISA0.EC0", /* 600e/x, 770e, 770x */ + "\\_SB.PCI0.ISA.EC", /* A21e, A2xm/p, T20-22, X20-21 */ + "\\_SB.PCI0.AD4S.EC0", /* i1400, R30 */ + "\\_SB.PCI0.ICH3.EC0", /* R31 */ + "\\_SB.PCI0.LPC.EC", /* all others */ + ); + +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 */ + +TPACPI_HANDLE(vid, root, "\\_SB.PCI.AGP.VGA", /* 570 */ + "\\_SB.PCI0.AGP0.VID0", /* 600e/x, 770x */ + "\\_SB.PCI0.VID0", /* 770e */ + "\\_SB.PCI0.VID", /* A21e, G4x, R50e, X30, X40 */ + "\\_SB.PCI0.AGP.VID", /* all others */ + ); /* R30, R31 */ + + +/************************************************************************* + * ACPI helpers + */ + +static int acpi_evalf(acpi_handle handle, + void *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) { + printk(TPACPI_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: + printk(TPACPI_ERR "acpi_evalf() called " + "with invalid format character '%c'\n", c); + 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 */ + if (res) + *(int *)res = out_obj.integer.value; + success = status == AE_OK && out_obj.type == ACPI_TYPE_INTEGER; + break; + case 'v': /* void */ + success = status == AE_OK; + break; + /* add more types as needed */ + default: + printk(TPACPI_ERR "acpi_evalf() called " + "with invalid format character '%c'\n", res_type); + return 0; + } + + if (!success && !quiet) + printk(TPACPI_ERR "acpi_evalf(%s, %s, ...) failed: %d\n", + method, fmt0, 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; +} + +#if defined(CONFIG_THINKPAD_ACPI_DOCK) || defined(CONFIG_THINKPAD_ACPI_BAY) +static int _sta(acpi_handle handle) +{ + int status; + + if (!handle || !acpi_evalf(handle, &status, "_STA", "d")) + status = 0; + + return status; +} +#endif + +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), &object##_path) + +static void drv_acpi_handle_init(char *name, + acpi_handle *handle, acpi_handle parent, + char **paths, int num_paths, char **path) +{ + 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)) { + *path = paths[i]; + dbg_printk(TPACPI_DBG_INIT, + "Found ACPI handle %s for %s\n", + *path, name); + return; + } + } + + vdbg_printk(TPACPI_DBG_INIT, "ACPI handle for %s not found\n", + name); + *handle = NULL; +} + +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) { + printk(TPACPI_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) { + printk(TPACPI_NOTICE + "another device driver is already " + "handling %s events\n", ibm->name); + } else { + printk(TPACPI_ERR + "acpi_install_notify_handler(%s) failed: %d\n", + ibm->name, 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) { + printk(TPACPI_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) { + printk(TPACPI_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_procfs_read(char *page, char **start, off_t off, + int count, int *eof, void *data) +{ + struct ibm_struct *ibm = data; + int len; + + if (!ibm || !ibm->read) + return -EINVAL; + + len = ibm->read(page); + if (len < 0) + return len; + + if (len <= off + count) + *eof = 1; + *start = page + off; + len -= off; + if (len > count) + len = count; + if (len < 0) + len = 0; + + return len; +} + +static int dispatch_procfs_write(struct file *file, + const char __user *userbuf, + unsigned long count, void *data) +{ + struct ibm_struct *ibm = data; + char *kernbuf; + int ret; + + if (!ibm || !ibm->write) + 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 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); + +static int tpacpi_suspend_handler(struct platform_device *pdev, + pm_message_t state) +{ + struct ibm_struct *ibm, *itmp; + + list_for_each_entry_safe(ibm, itmp, + &tpacpi_all_drivers, + all_drivers) { + if (ibm->suspend) + (ibm->suspend)(state); + } + + return 0; +} + +static int tpacpi_resume_handler(struct platform_device *pdev) +{ + struct ibm_struct *ibm, *itmp; + + list_for_each_entry_safe(ibm, itmp, + &tpacpi_all_drivers, + all_drivers) { + if (ibm->resume) + (ibm->resume)(); + } + + return 0; +} + +static struct platform_driver tpacpi_pdriver = { + .driver = { + .name = TPACPI_DRVR_NAME, + .owner = THIS_MODULE, + }, + .suspend = tpacpi_suspend_handler, + .resume = tpacpi_resume_handler, +}; + +static struct platform_driver tpacpi_hwmon_pdriver = { + .driver = { + .name = TPACPI_HWMON_DRVR_NAME, + .owner = THIS_MODULE, + }, +}; + +/************************************************************************* + * 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; + + while (*buf && isspace(*buf)) + buf++; + *value = simple_strtoul(buf, &endp, 0); + while (*endp && isspace(*endp)) + 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)) + printk(TPACPI_NOTICE + "ACPI backlight control delay disabled\n"); +} + +static int __init tpacpi_query_bcl_levels(acpi_handle handle) +{ + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + int rc; + + if (ACPI_SUCCESS(acpi_evaluate_object(handle, NULL, NULL, &buffer))) { + obj = (union acpi_object *)buffer.pointer; + if (!obj || (obj->type != ACPI_TYPE_PACKAGE)) { + printk(TPACPI_ERR "Unknown _BCL data, " + "please report this to %s\n", TPACPI_MAIL); + rc = 0; + } else { + rc = obj->package.count; + } + } else { + return 0; + } + + kfree(buffer.pointer); + return rc; +} + +static acpi_status __init tpacpi_acpi_walk_find_bcl(acpi_handle handle, + u32 lvl, void *context, void **rv) +{ + char name[ACPI_PATH_SEGMENT_LENGTH]; + struct acpi_buffer buffer = { sizeof(name), &name }; + + if (ACPI_SUCCESS(acpi_get_name(handle, ACPI_SINGLE_NAME, &buffer)) && + !strncmp("_BCL", name, sizeof(name) - 1)) { + BUG_ON(!rv || !*rv); + **(int **)rv = tpacpi_query_bcl_levels(handle); + return AE_CTRL_TERMINATE; + } else { + return AE_OK; + } +} + +/* + * Returns 0 (no ACPI _BCL or _BCL invalid), or size of brightness map + */ +static int __init tpacpi_check_std_acpi_brightness_support(void) +{ + int status; + int bcl_levels = 0; + void *bcl_ptr = &bcl_levels; + + if (!vid_handle) { + TPACPI_ACPIHANDLE_INIT(vid); + } + if (!vid_handle) + return 0; + + /* + * Search for a _BCL method, and execute it. This is safe on all + * ThinkPads, and as a side-effect, _BCL will place a Lenovo Vista + * BIOS in ACPI backlight control mode. We do NOT have to care + * about calling the _BCL method in an enabled video device, any + * will do for our purposes. + */ + + status = acpi_walk_namespace(ACPI_TYPE_METHOD, vid_handle, 3, + tpacpi_acpi_walk_find_bcl, NULL, + &bcl_ptr); + + if (ACPI_SUCCESS(status) && bcl_levels > 2) { + tp_features.bright_acpimode = 1; + return (bcl_levels - 2); + } + + return 0; +} + +static int __init tpacpi_new_rfkill(const unsigned int id, + struct rfkill **rfk, + const enum rfkill_type rfktype, + const char *name, + int (*toggle_radio)(void *, enum rfkill_state), + int (*get_state)(void *, enum rfkill_state *)) +{ + int res; + enum rfkill_state initial_state; + + *rfk = rfkill_allocate(&tpacpi_pdev->dev, rfktype); + if (!*rfk) { + printk(TPACPI_ERR + "failed to allocate memory for rfkill class\n"); + return -ENOMEM; + } + + (*rfk)->name = name; + (*rfk)->get_state = get_state; + (*rfk)->toggle_radio = toggle_radio; + + if (!get_state(NULL, &initial_state)) + (*rfk)->state = initial_state; + + res = rfkill_register(*rfk); + if (res < 0) { + printk(TPACPI_ERR + "failed to register %s rfkill switch: %d\n", + name, res); + rfkill_free(*rfk); + *rfk = NULL; + return res; + } + + return 0; +} + +/************************************************************************* + * thinkpad-acpi driver attributes + */ + +/* interface_version --------------------------------------------------- */ +static ssize_t tpacpi_driver_interface_version_show( + struct device_driver *drv, + char *buf) +{ + return snprintf(buf, PAGE_SIZE, "0x%08x\n", TPACPI_SYSFS_VERSION); +} + +static DRIVER_ATTR(interface_version, S_IRUGO, + tpacpi_driver_interface_version_show, NULL); + +/* debug_level --------------------------------------------------------- */ +static ssize_t tpacpi_driver_debug_show(struct device_driver *drv, + char *buf) +{ + return snprintf(buf, PAGE_SIZE, "0x%04x\n", dbg_level); +} + +static ssize_t tpacpi_driver_debug_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(debug_level, S_IWUSR | S_IRUGO, + tpacpi_driver_debug_show, tpacpi_driver_debug_store); + +/* version ------------------------------------------------------------- */ +static ssize_t tpacpi_driver_version_show(struct device_driver *drv, + char *buf) +{ + return snprintf(buf, PAGE_SIZE, "%s v%s\n", + TPACPI_DESC, TPACPI_VERSION); +} + +static DRIVER_ATTR(version, S_IRUGO, + tpacpi_driver_version_show, NULL); + +/* --------------------------------------------------------------------- */ + +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++; + } + + 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]); +} + +/**************************************************************************** + **************************************************************************** + * + * Subdrivers + * + **************************************************************************** + ****************************************************************************/ + +/************************************************************************* + * thinkpad-acpi init subdriver + */ + +static int __init thinkpad_acpi_driver_init(struct ibm_init_struct *iibm) +{ + printk(TPACPI_INFO "%s v%s\n", TPACPI_DESC, TPACPI_VERSION); + printk(TPACPI_INFO "%s\n", TPACPI_URL); + + printk(TPACPI_INFO "ThinkPad BIOS %s, EC %s\n", + (thinkpad_id.bios_version_str) ? + thinkpad_id.bios_version_str : "unknown", + (thinkpad_id.ec_version_str) ? + thinkpad_id.ec_version_str : "unknown"); + + if (thinkpad_id.vendor && thinkpad_id.model_str) + printk(TPACPI_INFO "%s %s, model %s\n", + (thinkpad_id.vendor == PCI_VENDOR_ID_IBM) ? + "IBM" : ((thinkpad_id.vendor == + PCI_VENDOR_ID_LENOVO) ? + "Lenovo" : "Unknown vendor"), + thinkpad_id.model_str, + (thinkpad_id.nummodel_str) ? + thinkpad_id.nummodel_str : "unknown"); + + return 0; +} + +static int thinkpad_acpi_driver_read(char *p) +{ + int len = 0; + + len += sprintf(p + len, "driver:\t\t%s\n", TPACPI_DESC); + len += sprintf(p + len, "version:\t%s\n", TPACPI_VERSION); + + return len; +} + +static struct ibm_struct thinkpad_acpi_driver_data = { + .name = "driver", + .read = thinkpad_acpi_driver_read, +}; + +/************************************************************************* + * Hotkey subdriver + */ + +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, +}; + +enum { /* Keys 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_THNKLGHT_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; +}; + +static struct task_struct *tpacpi_hotkey_task; +static u32 hotkey_source_mask; /* bit mask 0=ACPI,1=NVRAM */ +static int hotkey_poll_freq = 10; /* Hz */ +static struct mutex hotkey_thread_mutex; +static struct mutex hotkey_thread_data_mutex; +static unsigned int hotkey_config_change; + +#else /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ + +#define hotkey_source_mask 0U + +#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 int hotkey_orig_status; +static u32 hotkey_orig_mask; +static u32 hotkey_all_mask; +static u32 hotkey_reserved_mask; +static u32 hotkey_mask; + +static unsigned int hotkey_report_mode; + +static u16 *hotkey_keycode_map; + +static struct attribute_set *hotkey_dev_attributes; + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL +#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 +#define HOTKEY_CONFIG_CRITICAL_START +#define HOTKEY_CONFIG_CRITICAL_END +#endif /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ + +/* HKEY.MHKG() return bits */ +#define TP_HOTKEY_TABLET_MASK (1 << 3) + +static int hotkey_get_wlsw(int *status) +{ + if (!acpi_evalf(hkey_handle, status, "WLSW", "d")) + return -EIO; + return 0; +} + +static int hotkey_get_tablet_mode(int *status) +{ + int s; + + if (!acpi_evalf(hkey_handle, &s, "MHKG", "d")) + return -EIO; + + *status = ((s & TP_HOTKEY_TABLET_MASK) != 0); + return 0; +} + +/* + * Call with hotkey_mutex held + */ +static int hotkey_mask_get(void) +{ + u32 m = 0; + + if (tp_features.hotkey_mask) { + if (!acpi_evalf(hkey_handle, &m, "DHKN", "d")) + return -EIO; + } + hotkey_mask = m | (hotkey_source_mask & hotkey_mask); + + return 0; +} + +/* + * Call with hotkey_mutex held + */ +static int hotkey_mask_set(u32 mask) +{ + int i; + int rc = 0; + + if (tp_features.hotkey_mask) { + if (!tp_warned.hotkey_mask_ff && + (mask == 0xffff || mask == 0xffffff || + mask == 0xffffffff)) { + tp_warned.hotkey_mask_ff = 1; + printk(TPACPI_NOTICE + "setting the hotkey mask to 0x%08x is likely " + "not the best way to go about it\n", mask); + printk(TPACPI_NOTICE + "please consider using the driver defaults, " + "and refer to up-to-date thinkpad-acpi " + "documentation\n"); + } + + HOTKEY_CONFIG_CRITICAL_START + for (i = 0; i < 32; i++) { + u32 m = 1 << i; + /* enable in firmware mask only keys not in NVRAM + * mode, but enable the key in the cached hotkey_mask + * regardless of mode, or the key will end up + * disabled by hotkey_mask_get() */ + if (!acpi_evalf(hkey_handle, + NULL, "MHKM", "vdd", i + 1, + !!((mask & ~hotkey_source_mask) & m))) { + rc = -EIO; + break; + } else { + hotkey_mask = (hotkey_mask & ~m) | (mask & m); + } + } + HOTKEY_CONFIG_CRITICAL_END + + /* hotkey_mask_get must be called unconditionally below */ + if (!hotkey_mask_get() && !rc && + (hotkey_mask & ~hotkey_source_mask) != + (mask & ~hotkey_source_mask)) { + printk(TPACPI_NOTICE + "requested hot key mask 0x%08x, but " + "firmware forced it to 0x%08x\n", + mask, hotkey_mask); + } + } else { +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + HOTKEY_CONFIG_CRITICAL_START + hotkey_mask = mask & hotkey_source_mask; + HOTKEY_CONFIG_CRITICAL_END + hotkey_mask_get(); + if (hotkey_mask != mask) { + printk(TPACPI_NOTICE + "requested hot key mask 0x%08x, " + "forced to 0x%08x (NVRAM poll mask is " + "0x%08x): no firmware mask support\n", + mask, hotkey_mask, hotkey_source_mask); + } +#else + hotkey_mask_get(); + rc = -ENXIO; +#endif /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ + } + + 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(int status) +{ + if (!acpi_evalf(hkey_handle, NULL, "MHKC", "vd", status)) + 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); + } +} + +static void tpacpi_input_send_key(unsigned int scancode) +{ + unsigned int keycode; + + keycode = hotkey_keycode_map[scancode]; + + if (keycode != KEY_RESERVED) { + mutex_lock(&tpacpi_inputdev_send_mutex); + + input_report_key(tpacpi_inputdev, keycode, 1); + if (keycode == KEY_UNKNOWN) + input_event(tpacpi_inputdev, EV_MSC, MSC_SCAN, + scancode); + input_sync(tpacpi_inputdev); + + input_report_key(tpacpi_inputdev, keycode, 0); + if (keycode == KEY_UNKNOWN) + input_event(tpacpi_inputdev, EV_MSC, MSC_SCAN, + scancode); + input_sync(tpacpi_inputdev); + + mutex_unlock(&tpacpi_inputdev_send_mutex); + } +} + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL +static struct tp_acpi_drv_struct ibm_hotkey_acpidriver; + +static void tpacpi_hotkey_send_key(unsigned int scancode) +{ + tpacpi_input_send_key(scancode); + if (hotkey_report_mode < 2) { + acpi_bus_generate_proc_event(ibm_hotkey_acpidriver.device, + 0x80, 0x1001 + scancode); + } +} + +static void hotkey_read_nvram(struct tp_nvram_state *n, 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_THNKLGHT_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 ((mask & (1 << __scancode)) && \ + oldn->__member != newn->__member) \ + tpacpi_hotkey_send_key(__scancode); \ + } while (0) + +#define TPACPI_MAY_SEND_KEY(__scancode) \ + do { if (mask & (1 << __scancode)) \ + tpacpi_hotkey_send_key(__scancode); } while (0) + +static void hotkey_compare_and_issue_event(struct tp_nvram_state *oldn, + struct tp_nvram_state *newn, + u32 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 */ + if (oldn->volume_toggle != newn->volume_toggle) { + if (oldn->mute != newn->mute) { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_MUTE); + } + if (oldn->volume_level > newn->volume_level) { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEDOWN); + } else if (oldn->volume_level < newn->volume_level) { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEUP); + } else if (oldn->mute == newn->mute) { + /* repeated key presses that didn't change state */ + if (newn->mute) { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_MUTE); + } else if (newn->volume_level != 0) { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEUP); + } else { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEDOWN); + } + } + } + + /* handle brightness */ + if (oldn->brightness_toggle != newn->brightness_toggle) { + if (oldn->brightness_level < newn->brightness_level) { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNHOME); + } else if (oldn->brightness_level > newn->brightness_level) { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNEND); + } else { + /* repeated key presses that didn't change state */ + if (newn->brightness_level != 0) { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNHOME); + } else { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNEND); + } + } + } +} + +#undef TPACPI_COMPARE_KEY +#undef TPACPI_MAY_SEND_KEY + +static int hotkey_kthread(void *data) +{ + struct tp_nvram_state s[2]; + u32 mask; + unsigned int si, so; + unsigned long t; + unsigned int change_detector, must_reset; + + mutex_lock(&hotkey_thread_mutex); + + 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; + mask = hotkey_source_mask & hotkey_mask; + mutex_unlock(&hotkey_thread_data_mutex); + hotkey_read_nvram(&s[so], mask); + + while (!kthread_should_stop() && hotkey_poll_freq) { + if (t == 0) + t = 1000/hotkey_poll_freq; + t = msleep_interruptible(t); + if (unlikely(kthread_should_stop())) + break; + must_reset = try_to_freeze(); + if (t > 0 && !must_reset) + continue; + + mutex_lock(&hotkey_thread_data_mutex); + if (must_reset || hotkey_config_change != change_detector) { + /* forget old state on thaw or config change */ + si = so; + t = 0; + change_detector = hotkey_config_change; + } + mask = hotkey_source_mask & hotkey_mask; + mutex_unlock(&hotkey_thread_data_mutex); + + if (likely(mask)) { + hotkey_read_nvram(&s[si], mask); + if (likely(si != so)) { + hotkey_compare_and_issue_event(&s[so], &s[si], + mask); + } + } + + so = si; + si ^= 1; + } + +exit: + mutex_unlock(&hotkey_thread_mutex); + return 0; +} + +static void hotkey_poll_stop_sync(void) +{ + if (tpacpi_hotkey_task) { + if (frozen(tpacpi_hotkey_task) || + freezing(tpacpi_hotkey_task)) + thaw_process(tpacpi_hotkey_task); + + kthread_stop(tpacpi_hotkey_task); + tpacpi_hotkey_task = NULL; + mutex_lock(&hotkey_thread_mutex); + /* at this point, the thread did exit */ + mutex_unlock(&hotkey_thread_mutex); + } +} + +/* call with hotkey_mutex held */ +static void hotkey_poll_setup(int may_warn) +{ + if ((hotkey_source_mask & hotkey_mask) != 0 && + hotkey_poll_freq > 0 && + (tpacpi_inputdev->users > 0 || hotkey_report_mode < 2)) { + 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; + printk(TPACPI_ERR + "could not create kernel thread " + "for hotkey polling\n"); + } + } + } else { + hotkey_poll_stop_sync(); + if (may_warn && + hotkey_source_mask != 0 && hotkey_poll_freq == 0) { + printk(TPACPI_NOTICE + "hot keys 0x%08x require polling, " + "which is currently disabled\n", + hotkey_source_mask); + } + } +} + +static void hotkey_poll_setup_safe(int may_warn) +{ + mutex_lock(&hotkey_mutex); + hotkey_poll_setup(may_warn); + mutex_unlock(&hotkey_mutex); +} + +#else /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ + +static void hotkey_poll_setup_safe(int __unused) +{ +} + +#endif /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ + +static int hotkey_inputdev_open(struct input_dev *dev) +{ + switch (tpacpi_lifecycle) { + case TPACPI_LIFE_INIT: + /* + * hotkey_init will call hotkey_poll_setup_safe + * at the appropriate moment + */ + return 0; + case TPACPI_LIFE_EXITING: + return -EBUSY; + case TPACPI_LIFE_RUNNING: + hotkey_poll_setup_safe(0); + return 0; + } + + /* 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_RUNNING) + hotkey_poll_setup_safe(0); +} + +/* sysfs hotkey enable ------------------------------------------------- */ +static ssize_t hotkey_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res, status; + + 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; + int res; + + if (parse_strtoul(buf, 1, &t)) + return -EINVAL; + + res = hotkey_status_set(t); + + return (res) ? res : count; +} + +static struct device_attribute dev_attr_hotkey_enable = + __ATTR(hotkey_enable, S_IWUSR | S_IRUGO, + hotkey_enable_show, hotkey_enable_store); + +/* sysfs hotkey mask --------------------------------------------------- */ +static ssize_t hotkey_mask_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res; + + if (mutex_lock_interruptible(&hotkey_mutex)) + return -ERESTARTSYS; + res = hotkey_mask_get(); + mutex_unlock(&hotkey_mutex); + + return (res)? + res : snprintf(buf, PAGE_SIZE, "0x%08x\n", hotkey_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_interruptible(&hotkey_mutex)) + return -ERESTARTSYS; + + res = hotkey_mask_set(t); + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + hotkey_poll_setup(1); +#endif + + mutex_unlock(&hotkey_mutex); + + return (res) ? res : count; +} + +static struct device_attribute dev_attr_hotkey_mask = + __ATTR(hotkey_mask, S_IWUSR | S_IRUGO, + hotkey_mask_show, hotkey_mask_store); + +/* sysfs hotkey bios_enabled ------------------------------------------- */ +static ssize_t hotkey_bios_enabled_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return snprintf(buf, PAGE_SIZE, "%d\n", hotkey_orig_status); +} + +static struct device_attribute dev_attr_hotkey_bios_enabled = + __ATTR(hotkey_bios_enabled, S_IRUGO, hotkey_bios_enabled_show, NULL); + +/* sysfs hotkey bios_mask ---------------------------------------------- */ +static ssize_t hotkey_bios_mask_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return snprintf(buf, PAGE_SIZE, "0x%08x\n", hotkey_orig_mask); +} + +static struct device_attribute dev_attr_hotkey_bios_mask = + __ATTR(hotkey_bios_mask, S_IRUGO, hotkey_bios_mask_show, NULL); + +/* 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 struct device_attribute dev_attr_hotkey_all_mask = + __ATTR(hotkey_all_mask, S_IRUGO, hotkey_all_mask_show, NULL); + +/* 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 struct device_attribute dev_attr_hotkey_recommended_mask = + __ATTR(hotkey_recommended_mask, S_IRUGO, + hotkey_recommended_mask_show, NULL); + +#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; + + if (parse_strtoul(buf, 0xffffffffUL, &t) || + ((t & ~TPACPI_HKEY_NVRAM_KNOWN_MASK) != 0)) + return -EINVAL; + + if (mutex_lock_interruptible(&hotkey_mutex)) + return -ERESTARTSYS; + + HOTKEY_CONFIG_CRITICAL_START + hotkey_source_mask = t; + HOTKEY_CONFIG_CRITICAL_END + + hotkey_poll_setup(1); + + mutex_unlock(&hotkey_mutex); + + return count; +} + +static struct device_attribute dev_attr_hotkey_source_mask = + __ATTR(hotkey_source_mask, S_IWUSR | S_IRUGO, + hotkey_source_mask_show, hotkey_source_mask_store); + +/* 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_interruptible(&hotkey_mutex)) + return -ERESTARTSYS; + + hotkey_poll_freq = t; + + hotkey_poll_setup(1); + mutex_unlock(&hotkey_mutex); + + return count; +} + +static struct device_attribute dev_attr_hotkey_poll_freq = + __ATTR(hotkey_poll_freq, S_IWUSR | S_IRUGO, + hotkey_poll_freq_show, hotkey_poll_freq_store); + +#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, s; + res = hotkey_get_wlsw(&s); + if (res < 0) + return res; + + return snprintf(buf, PAGE_SIZE, "%d\n", !!s); +} + +static struct device_attribute dev_attr_hotkey_radio_sw = + __ATTR(hotkey_radio_sw, S_IRUGO, hotkey_radio_sw_show, NULL); + +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 struct device_attribute dev_attr_hotkey_tablet_mode = + __ATTR(hotkey_tablet_mode, S_IRUGO, hotkey_tablet_mode_show, NULL); + +static void hotkey_tablet_mode_notify_change(void) +{ + if (tp_features.hotkey_tablet) + sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, + "hotkey_tablet_mode"); +} + +/* sysfs hotkey report_mode -------------------------------------------- */ +static ssize_t hotkey_report_mode_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return snprintf(buf, PAGE_SIZE, "%d\n", + (hotkey_report_mode != 0) ? hotkey_report_mode : 1); +} + +static struct device_attribute dev_attr_hotkey_report_mode = + __ATTR(hotkey_report_mode, S_IRUGO, hotkey_report_mode_show, NULL); + +/* 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 struct device_attribute dev_attr_hotkey_wakeup_reason = + __ATTR(wakeup_reason, S_IRUGO, hotkey_wakeup_reason_show, NULL); + +static void hotkey_wakeup_reason_notify_change(void) +{ + if (tp_features.hotkey_mask) + 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 struct device_attribute dev_attr_hotkey_wakeup_hotunplug_complete = + __ATTR(wakeup_hotunplug_complete, S_IRUGO, + hotkey_wakeup_hotunplug_complete_show, NULL); + +static void hotkey_wakeup_hotunplug_complete_notify_change(void) +{ + if (tp_features.hotkey_mask) + sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, + "wakeup_hotunplug_complete"); +} + +/* --------------------------------------------------------------------- */ + +static struct attribute *hotkey_attributes[] __initdata = { + &dev_attr_hotkey_enable.attr, + &dev_attr_hotkey_bios_enabled.attr, + &dev_attr_hotkey_report_mode.attr, +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + &dev_attr_hotkey_mask.attr, + &dev_attr_hotkey_all_mask.attr, + &dev_attr_hotkey_recommended_mask.attr, + &dev_attr_hotkey_source_mask.attr, + &dev_attr_hotkey_poll_freq.attr, +#endif +}; + +static struct attribute *hotkey_mask_attributes[] __initdata = { + &dev_attr_hotkey_bios_mask.attr, +#ifndef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + &dev_attr_hotkey_mask.attr, + &dev_attr_hotkey_all_mask.attr, + &dev_attr_hotkey_recommended_mask.attr, +#endif + &dev_attr_hotkey_wakeup_reason.attr, + &dev_attr_hotkey_wakeup_hotunplug_complete.attr, +}; + +static void bluetooth_update_rfk(void); +static void wan_update_rfk(void); +static void tpacpi_send_radiosw_update(void) +{ + int wlsw; + + /* Sync these BEFORE sending any rfkill events */ + if (tp_features.bluetooth) + bluetooth_update_rfk(); + if (tp_features.wan) + wan_update_rfk(); + + if (tp_features.hotkey_wlsw && !hotkey_get_wlsw(&wlsw)) { + mutex_lock(&tpacpi_inputdev_send_mutex); + + input_report_switch(tpacpi_inputdev, + SW_RFKILL_ALL, !!wlsw); + input_sync(tpacpi_inputdev); + + mutex_unlock(&tpacpi_inputdev_send_mutex); + } + hotkey_radio_sw_notify_change(); +} + +static void hotkey_exit(void) +{ +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + hotkey_poll_stop_sync(); +#endif + + if (hotkey_dev_attributes) + delete_attr_set(hotkey_dev_attributes, &tpacpi_pdev->dev.kobj); + + kfree(hotkey_keycode_map); + + if (tp_features.hotkey) { + dbg_printk(TPACPI_DBG_EXIT, + "restoring original hot key mask\n"); + /* no short-circuit boolean operator below! */ + if ((hotkey_mask_set(hotkey_orig_mask) | + hotkey_status_set(hotkey_orig_status)) != 0) + printk(TPACPI_ERR + "failed to restore hot key mask " + "to BIOS defaults\n"); + } +} + +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. + */ + static u16 ibm_keycode_map[] __initdata = { + /* Scan Codes 0x00 to 0x0B: ACPI HKEY FN+F1..F12 */ + KEY_FN_F1, KEY_FN_F2, 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, unless + * X.org did some tricks in the radeon BIOS scratch + * registers of *some* models */ + 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_UNKNOWN, + KEY_UNKNOWN, KEY_UNKNOWN, KEY_UNKNOWN, KEY_UNKNOWN, + }; + static u16 lenovo_keycode_map[] __initdata = { + /* Scan Codes 0x00 to 0x0B: ACPI HKEY FN+F1..F12 */ + KEY_FN_F1, KEY_COFFEE, KEY_BATTERY, 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 */ + + /* These either have to go through ACPI video, or + * act like in the IBM ThinkPads, so don't ever + * enable them by default */ + KEY_RESERVED, /* 0x0F: FN+HOME (brightness up) */ + KEY_RESERVED, /* 0x10: FN+END (brightness down) */ + + KEY_RESERVED, /* 0x11: FN+PGUP (thinklight toggle) */ + + KEY_UNKNOWN, /* 0x12: FN+PGDOWN */ + KEY_ZOOM, /* 0x13: FN+SPACE (zoom) */ + + /* Volume: z60/z61, T60 (BIOS version?): firmware always + * react to it and reprograms the built-in *extra* mixer. + * Never map it to control another mixer by default. + * + * T60?, T61, R60?, R61: firmware and EC tries to send + * these over the regular keyboard, so these are no-ops, + * but there are still weird bugs re. MUTE, so do not + * change unless you get test reports from all Lenovo + * models. May cause the BIOS to interfere with the + * HDA mixer. + */ + 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_UNKNOWN, + KEY_UNKNOWN, KEY_UNKNOWN, KEY_UNKNOWN, KEY_UNKNOWN, + }; + +#define TPACPI_HOTKEY_MAP_LEN ARRAY_SIZE(ibm_keycode_map) +#define TPACPI_HOTKEY_MAP_SIZE sizeof(ibm_keycode_map) +#define TPACPI_HOTKEY_MAP_TYPESIZE sizeof(ibm_keycode_map[0]) + + int res, i; + int status; + int hkeyv; + + vdbg_printk(TPACPI_DBG_INIT, "initializing hotkey subdriver\n"); + + BUG_ON(!tpacpi_inputdev); + BUG_ON(tpacpi_inputdev->open != NULL || + tpacpi_inputdev->close != NULL); + + TPACPI_ACPIHANDLE_INIT(hkey); + mutex_init(&hotkey_mutex); + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + mutex_init(&hotkey_thread_mutex); + mutex_init(&hotkey_thread_data_mutex); +#endif + + /* hotkey not supported on 570 */ + tp_features.hotkey = hkey_handle != NULL; + + vdbg_printk(TPACPI_DBG_INIT, "hotkeys are %s\n", + str_supported(tp_features.hotkey)); + + if (!tp_features.hotkey) + return 1; + + tpacpi_disable_brightness_delay(); + + hotkey_dev_attributes = create_attr_set(13, NULL); + if (!hotkey_dev_attributes) + return -ENOMEM; + res = add_many_to_attr_set(hotkey_dev_attributes, + hotkey_attributes, + ARRAY_SIZE(hotkey_attributes)); + if (res) + goto err_exit; + + /* mask not supported on 570, 600e/x, 770e, 770x, A21e, A2xm/p, + A30, R30, R31, T20-22, X20-21, X22-24. Detected by checking + for HKEY interface version 0x100 */ + if (acpi_evalf(hkey_handle, &hkeyv, "MHKV", "qd")) { + if ((hkeyv >> 8) != 1) { + printk(TPACPI_ERR "unknown version of the " + "HKEY interface: 0x%x\n", hkeyv); + printk(TPACPI_ERR "please report this to %s\n", + TPACPI_MAIL); + } else { + /* + * MHKV 0x100 in A31, R40, R40e, + * T4x, X31, and later + */ + tp_features.hotkey_mask = 1; + } + } + + vdbg_printk(TPACPI_DBG_INIT, "hotkey masks are %s\n", + str_supported(tp_features.hotkey_mask)); + + if (tp_features.hotkey_mask) { + if (!acpi_evalf(hkey_handle, &hotkey_all_mask, + "MHKA", "qd")) { + printk(TPACPI_ERR + "missing MHKA handler, " + "please report this to %s\n", + TPACPI_MAIL); + /* FN+F12, FN+F4, FN+F3 */ + hotkey_all_mask = 0x080cU; + } + } + + /* hotkey_source_mask *must* be zero for + * the first hotkey_mask_get */ + res = hotkey_status_get(&hotkey_orig_status); + if (res) + goto err_exit; + + if (tp_features.hotkey_mask) { + res = hotkey_mask_get(); + if (res) + goto err_exit; + + hotkey_orig_mask = hotkey_mask; + res = add_many_to_attr_set( + hotkey_dev_attributes, + hotkey_mask_attributes, + ARRAY_SIZE(hotkey_mask_attributes)); + if (res) + goto err_exit; + } + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + if (tp_features.hotkey_mask) { + hotkey_source_mask = TPACPI_HKEY_NVRAM_GOOD_MASK + & ~hotkey_all_mask; + } else { + hotkey_source_mask = TPACPI_HKEY_NVRAM_GOOD_MASK; + } + + vdbg_printk(TPACPI_DBG_INIT, + "hotkey source mask 0x%08x, polling freq %d\n", + hotkey_source_mask, hotkey_poll_freq); +#endif + + /* Not all thinkpads have a hardware radio switch */ + if (acpi_evalf(hkey_handle, &status, "WLSW", "qd")) { + tp_features.hotkey_wlsw = 1; + printk(TPACPI_INFO + "radio switch found; radios are %s\n", + enabled(status, 0)); + } + if (tp_features.hotkey_wlsw) + res = add_to_attr_set(hotkey_dev_attributes, + &dev_attr_hotkey_radio_sw.attr); + + /* For X41t, X60t, X61t Tablets... */ + if (!res && acpi_evalf(hkey_handle, &status, "MHKG", "qd")) { + tp_features.hotkey_tablet = 1; + printk(TPACPI_INFO + "possible tablet mode switch found; " + "ThinkPad in %s mode\n", + (status & TP_HOTKEY_TABLET_MASK)? + "tablet" : "laptop"); + res = add_to_attr_set(hotkey_dev_attributes, + &dev_attr_hotkey_tablet_mode.attr); + } + + if (!res) + res = register_attr_set_with_sysfs( + hotkey_dev_attributes, + &tpacpi_pdev->dev.kobj); + if (res) + goto err_exit; + + /* Set up key map */ + + hotkey_keycode_map = kmalloc(TPACPI_HOTKEY_MAP_SIZE, + GFP_KERNEL); + if (!hotkey_keycode_map) { + printk(TPACPI_ERR + "failed to allocate memory for key map\n"); + res = -ENOMEM; + goto err_exit; + } + + if (thinkpad_id.vendor == PCI_VENDOR_ID_LENOVO) { + dbg_printk(TPACPI_DBG_INIT, + "using Lenovo default hot key map\n"); + memcpy(hotkey_keycode_map, &lenovo_keycode_map, + TPACPI_HOTKEY_MAP_SIZE); + } else { + dbg_printk(TPACPI_DBG_INIT, + "using IBM default hot key map\n"); + memcpy(hotkey_keycode_map, &ibm_keycode_map, + TPACPI_HOTKEY_MAP_SIZE); + } + + set_bit(EV_KEY, tpacpi_inputdev->evbit); + set_bit(EV_MSC, tpacpi_inputdev->evbit); + set_bit(MSC_SCAN, tpacpi_inputdev->mscbit); + tpacpi_inputdev->keycodesize = TPACPI_HOTKEY_MAP_TYPESIZE; + tpacpi_inputdev->keycodemax = TPACPI_HOTKEY_MAP_LEN; + tpacpi_inputdev->keycode = hotkey_keycode_map; + for (i = 0; i < TPACPI_HOTKEY_MAP_LEN; i++) { + if (hotkey_keycode_map[i] != KEY_RESERVED) { + set_bit(hotkey_keycode_map[i], + tpacpi_inputdev->keybit); + } else { + if (i < sizeof(hotkey_reserved_mask)*8) + hotkey_reserved_mask |= 1 << i; + } + } + + if (tp_features.hotkey_wlsw) { + set_bit(EV_SW, tpacpi_inputdev->evbit); + set_bit(SW_RFKILL_ALL, tpacpi_inputdev->swbit); + } + if (tp_features.hotkey_tablet) { + set_bit(EV_SW, tpacpi_inputdev->evbit); + set_bit(SW_TABLET_MODE, tpacpi_inputdev->swbit); + } + + /* Do not issue duplicate brightness change events to + * userspace */ + if (!tp_features.bright_acpimode) + /* update bright_acpimode... */ + tpacpi_check_std_acpi_brightness_support(); + + if (tp_features.bright_acpimode) { + printk(TPACPI_INFO + "This ThinkPad has standard ACPI backlight " + "brightness control, supported by the ACPI " + "video driver\n"); + printk(TPACPI_NOTICE + "Disabling thinkpad-acpi brightness events " + "by default...\n"); + + /* The hotkey_reserved_mask change below is not + * necessary while the keys are at KEY_RESERVED in the + * default map, but better safe than sorry, leave it + * here as a marker of what we have to do, especially + * when we finally become able to set this at runtime + * on response to X.org requests */ + hotkey_reserved_mask |= + (1 << TP_ACPI_HOTKEYSCAN_FNHOME) + | (1 << TP_ACPI_HOTKEYSCAN_FNEND); + } + + dbg_printk(TPACPI_DBG_INIT, "enabling hot key handling\n"); + res = hotkey_status_set(1); + if (res) { + hotkey_exit(); + return res; + } + res = hotkey_mask_set(((hotkey_all_mask | hotkey_source_mask) + & ~hotkey_reserved_mask) + | hotkey_orig_mask); + if (res < 0 && res != -ENXIO) { + hotkey_exit(); + return res; + } + + dbg_printk(TPACPI_DBG_INIT, + "legacy hot key reporting over procfs %s\n", + (hotkey_report_mode < 2) ? + "enabled" : "disabled"); + + tpacpi_inputdev->open = &hotkey_inputdev_open; + tpacpi_inputdev->close = &hotkey_inputdev_close; + + hotkey_poll_setup_safe(1); + tpacpi_send_radiosw_update(); + tpacpi_input_send_tabletsw(); + + return 0; + +err_exit: + delete_attr_set(hotkey_dev_attributes, &tpacpi_pdev->dev.kobj); + hotkey_dev_attributes = NULL; + + return (res < 0)? res : 1; +} + +static void hotkey_notify(struct ibm_struct *ibm, u32 event) +{ + u32 hkey; + unsigned int scancode; + int send_acpi_ev; + int ignore_acpi_ev; + int unk_ev; + + if (event != 0x80) { + printk(TPACPI_ERR + "unknown HKEY notification event %d\n", event); + /* forward it to userspace, maybe it knows how to handle it */ + acpi_bus_generate_netlink_event( + ibm->acpi->device->pnp.device_class, + ibm->acpi->device->dev.bus_id, + event, 0); + return; + } + + while (1) { + if (!acpi_evalf(hkey_handle, &hkey, "MHKP", "d")) { + printk(TPACPI_ERR "failed to retrieve HKEY event\n"); + return; + } + + if (hkey == 0) { + /* queue empty */ + return; + } + + send_acpi_ev = 1; + ignore_acpi_ev = 0; + unk_ev = 0; + + switch (hkey >> 12) { + case 1: + /* 0x1000-0x1FFF: key presses */ + scancode = hkey & 0xfff; + if (scancode > 0 && scancode < 0x21) { + scancode--; + if (!(hotkey_source_mask & (1 << scancode))) { + tpacpi_input_send_key(scancode); + send_acpi_ev = 0; + } else { + ignore_acpi_ev = 1; + } + } else { + unk_ev = 1; + } + break; + case 2: + /* Wakeup reason */ + switch (hkey) { + case 0x2304: /* suspend, undock */ + case 0x2404: /* hibernation, undock */ + hotkey_wakeup_reason = TP_ACPI_WAKEUP_UNDOCK; + ignore_acpi_ev = 1; + break; + case 0x2305: /* suspend, bay eject */ + case 0x2405: /* hibernation, bay eject */ + hotkey_wakeup_reason = TP_ACPI_WAKEUP_BAYEJ; + ignore_acpi_ev = 1; + break; + default: + unk_ev = 1; + } + if (hotkey_wakeup_reason != TP_ACPI_WAKEUP_NONE) { + printk(TPACPI_INFO + "woke up due to a hot-unplug " + "request...\n"); + hotkey_wakeup_reason_notify_change(); + } + break; + case 3: + /* bay-related wakeups */ + if (hkey == 0x3003) { + hotkey_autosleep_ack = 1; + printk(TPACPI_INFO + "bay ejected\n"); + hotkey_wakeup_hotunplug_complete_notify_change(); + } else { + unk_ev = 1; + } + break; + case 4: + /* dock-related wakeups */ + if (hkey == 0x4003) { + hotkey_autosleep_ack = 1; + printk(TPACPI_INFO + "undocked\n"); + hotkey_wakeup_hotunplug_complete_notify_change(); + } else { + unk_ev = 1; + } + break; + case 5: + /* 0x5000-0x5FFF: human interface helpers */ + switch (hkey) { + case 0x5010: /* Lenovo new BIOS: brightness changed */ + case 0x500b: /* X61t: tablet pen inserted into bay */ + case 0x500c: /* X61t: tablet pen removed from bay */ + break; + case 0x5009: /* X41t-X61t: swivel up (tablet mode) */ + case 0x500a: /* X41t-X61t: swivel down (normal mode) */ + tpacpi_input_send_tabletsw(); + hotkey_tablet_mode_notify_change(); + send_acpi_ev = 0; + break; + case 0x5001: + case 0x5002: + /* LID switch events. Do not propagate */ + ignore_acpi_ev = 1; + break; + default: + unk_ev = 1; + } + break; + case 7: + /* 0x7000-0x7FFF: misc */ + if (tp_features.hotkey_wlsw && hkey == 0x7000) { + tpacpi_send_radiosw_update(); + send_acpi_ev = 0; + break; + } + /* fallthrough to default */ + default: + unk_ev = 1; + } + if (unk_ev) { + printk(TPACPI_NOTICE + "unhandled HKEY event 0x%04x\n", hkey); + } + + /* Legacy events */ + if (!ignore_acpi_ev && + (send_acpi_ev || hotkey_report_mode < 2)) { + acpi_bus_generate_proc_event(ibm->acpi->device, + event, hkey); + } + + /* netlink events */ + if (!ignore_acpi_ev && send_acpi_ev) { + acpi_bus_generate_netlink_event( + ibm->acpi->device->pnp.device_class, + ibm->acpi->device->dev.bus_id, + event, hkey); + } + } +} + +static void hotkey_suspend(pm_message_t state) +{ + /* Do these on suspend, we get the events on early resume! */ + hotkey_wakeup_reason = TP_ACPI_WAKEUP_NONE; + hotkey_autosleep_ack = 0; +} + +static void hotkey_resume(void) +{ + tpacpi_disable_brightness_delay(); + + if (hotkey_mask_get()) + printk(TPACPI_ERR + "error while trying to read hot key mask " + "from firmware\n"); + tpacpi_send_radiosw_update(); + hotkey_tablet_mode_notify_change(); + hotkey_wakeup_reason_notify_change(); + hotkey_wakeup_hotunplug_complete_notify_change(); + hotkey_poll_setup_safe(0); +} + +/* procfs -------------------------------------------------------------- */ +static int hotkey_read(char *p) +{ + int res, status; + int len = 0; + + if (!tp_features.hotkey) { + len += sprintf(p + len, "status:\t\tnot supported\n"); + return len; + } + + if (mutex_lock_interruptible(&hotkey_mutex)) + return -ERESTARTSYS; + res = hotkey_status_get(&status); + if (!res) + res = hotkey_mask_get(); + mutex_unlock(&hotkey_mutex); + if (res) + return res; + + len += sprintf(p + len, "status:\t\t%s\n", enabled(status, 0)); + if (tp_features.hotkey_mask) { + len += sprintf(p + len, "mask:\t\t0x%08x\n", hotkey_mask); + len += sprintf(p + len, + "commands:\tenable, disable, reset, <mask>\n"); + } else { + len += sprintf(p + len, "mask:\t\tnot supported\n"); + len += sprintf(p + len, "commands:\tenable, disable, reset\n"); + } + + return len; +} + +static int hotkey_write(char *buf) +{ + int res, status; + u32 mask; + char *cmd; + + if (!tp_features.hotkey) + return -ENODEV; + + if (mutex_lock_interruptible(&hotkey_mutex)) + return -ERESTARTSYS; + + status = -1; + mask = hotkey_mask; + + res = 0; + while ((cmd = next_cmd(&buf))) { + if (strlencmp(cmd, "enable") == 0) { + status = 1; + } else if (strlencmp(cmd, "disable") == 0) { + status = 0; + } else if (strlencmp(cmd, "reset") == 0) { + status = hotkey_orig_status; + mask = hotkey_orig_mask; + } else if (sscanf(cmd, "0x%x", &mask) == 1) { + /* mask set */ + } else if (sscanf(cmd, "%x", &mask) == 1) { + /* mask set */ + } else { + res = -EINVAL; + goto errexit; + } + } + if (status != -1) + res = hotkey_status_set(status); + + if (!res && mask != hotkey_mask) + res = hotkey_mask_set(mask); + +errexit: + mutex_unlock(&hotkey_mutex); + return res; +} + +static const struct acpi_device_id ibm_htk_device_ids[] = { + {TPACPI_ACPI_HKEY_HID, 0}, + {"", 0}, +}; + +static struct tp_acpi_drv_struct ibm_hotkey_acpidriver = { + .hid = ibm_htk_device_ids, + .notify = hotkey_notify, + .handle = &hkey_handle, + .type = ACPI_DEVICE_NOTIFY, +}; + +static struct ibm_struct hotkey_driver_data = { + .name = "hotkey", + .read = hotkey_read, + .write = hotkey_write, + .exit = hotkey_exit, + .resume = hotkey_resume, + .suspend = hotkey_suspend, + .acpi = &ibm_hotkey_acpidriver, +}; + +/************************************************************************* + * Bluetooth subdriver + */ + +enum { + /* ACPI GBDC/SBDC bits */ + TP_ACPI_BLUETOOTH_HWPRESENT = 0x01, /* Bluetooth hw available */ + TP_ACPI_BLUETOOTH_RADIOSSW = 0x02, /* Bluetooth radio enabled */ + TP_ACPI_BLUETOOTH_UNK = 0x04, /* unknown function */ +}; + +static struct rfkill *tpacpi_bluetooth_rfkill; + +static int bluetooth_get_radiosw(void) +{ + int status; + + if (!tp_features.bluetooth) + return -ENODEV; + + /* WLSW overrides bluetooth in firmware/hardware, reflect that */ + if (tp_features.hotkey_wlsw && !hotkey_get_wlsw(&status) && !status) + return RFKILL_STATE_HARD_BLOCKED; + + if (!acpi_evalf(hkey_handle, &status, "GBDC", "d")) + return -EIO; + + return ((status & TP_ACPI_BLUETOOTH_RADIOSSW) != 0) ? + RFKILL_STATE_UNBLOCKED : RFKILL_STATE_SOFT_BLOCKED; +} + +static void bluetooth_update_rfk(void) +{ + int status; + + if (!tpacpi_bluetooth_rfkill) + return; + + status = bluetooth_get_radiosw(); + if (status < 0) + return; + rfkill_force_state(tpacpi_bluetooth_rfkill, status); +} + +static int bluetooth_set_radiosw(int radio_on, int update_rfk) +{ + int status; + + if (!tp_features.bluetooth) + return -ENODEV; + + /* WLSW overrides bluetooth in firmware/hardware, but there is no + * reason to risk weird behaviour. */ + if (tp_features.hotkey_wlsw && !hotkey_get_wlsw(&status) && !status + && radio_on) + return -EPERM; + + if (!acpi_evalf(hkey_handle, &status, "GBDC", "d")) + return -EIO; + if (radio_on) + status |= TP_ACPI_BLUETOOTH_RADIOSSW; + else + status &= ~TP_ACPI_BLUETOOTH_RADIOSSW; + if (!acpi_evalf(hkey_handle, NULL, "SBDC", "vd", status)) + return -EIO; + + if (update_rfk) + bluetooth_update_rfk(); + + return 0; +} + +/* sysfs bluetooth enable ---------------------------------------------- */ +static ssize_t bluetooth_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int status; + + status = bluetooth_get_radiosw(); + if (status < 0) + return status; + + return snprintf(buf, PAGE_SIZE, "%d\n", + (status == RFKILL_STATE_UNBLOCKED) ? 1 : 0); +} + +static ssize_t bluetooth_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + int res; + + if (parse_strtoul(buf, 1, &t)) + return -EINVAL; + + res = bluetooth_set_radiosw(t, 1); + + return (res) ? res : count; +} + +static struct device_attribute dev_attr_bluetooth_enable = + __ATTR(bluetooth_enable, S_IWUSR | S_IRUGO, + bluetooth_enable_show, bluetooth_enable_store); + +/* --------------------------------------------------------------------- */ + +static struct attribute *bluetooth_attributes[] = { + &dev_attr_bluetooth_enable.attr, + NULL +}; + +static const struct attribute_group bluetooth_attr_group = { + .attrs = bluetooth_attributes, +}; + +static int tpacpi_bluetooth_rfk_get(void *data, enum rfkill_state *state) +{ + int bts = bluetooth_get_radiosw(); + + if (bts < 0) + return bts; + + *state = bts; + return 0; +} + +static int tpacpi_bluetooth_rfk_set(void *data, enum rfkill_state state) +{ + return bluetooth_set_radiosw((state == RFKILL_STATE_UNBLOCKED), 0); +} + +static void bluetooth_exit(void) +{ + if (tpacpi_bluetooth_rfkill) + rfkill_unregister(tpacpi_bluetooth_rfkill); + + sysfs_remove_group(&tpacpi_pdev->dev.kobj, + &bluetooth_attr_group); +} + +static int __init bluetooth_init(struct ibm_init_struct *iibm) +{ + int res; + int status = 0; + + vdbg_printk(TPACPI_DBG_INIT, "initializing bluetooth subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(hkey); + + /* bluetooth not supported on 570, 600e/x, 770e, 770x, A21e, A2xm/p, + G4x, R30, R31, R40e, R50e, T20-22, X20-21 */ + tp_features.bluetooth = hkey_handle && + acpi_evalf(hkey_handle, &status, "GBDC", "qd"); + + vdbg_printk(TPACPI_DBG_INIT, "bluetooth is %s, status 0x%02x\n", + str_supported(tp_features.bluetooth), + status); + + if (tp_features.bluetooth && + !(status & TP_ACPI_BLUETOOTH_HWPRESENT)) { + /* no bluetooth hardware present in system */ + tp_features.bluetooth = 0; + dbg_printk(TPACPI_DBG_INIT, + "bluetooth hardware not installed\n"); + } + + if (!tp_features.bluetooth) + return 1; + + res = sysfs_create_group(&tpacpi_pdev->dev.kobj, + &bluetooth_attr_group); + if (res) + return res; + + res = tpacpi_new_rfkill(TPACPI_RFK_BLUETOOTH_SW_ID, + &tpacpi_bluetooth_rfkill, + RFKILL_TYPE_BLUETOOTH, + "tpacpi_bluetooth_sw", + tpacpi_bluetooth_rfk_set, + tpacpi_bluetooth_rfk_get); + if (res) { + bluetooth_exit(); + return res; + } + + return 0; +} + +/* procfs -------------------------------------------------------------- */ +static int bluetooth_read(char *p) +{ + int len = 0; + int status = bluetooth_get_radiosw(); + + if (!tp_features.bluetooth) + len += sprintf(p + len, "status:\t\tnot supported\n"); + else { + len += sprintf(p + len, "status:\t\t%s\n", + (status == RFKILL_STATE_UNBLOCKED) ? + "enabled" : "disabled"); + len += sprintf(p + len, "commands:\tenable, disable\n"); + } + + return len; +} + +static int bluetooth_write(char *buf) +{ + char *cmd; + + if (!tp_features.bluetooth) + return -ENODEV; + + while ((cmd = next_cmd(&buf))) { + if (strlencmp(cmd, "enable") == 0) { + bluetooth_set_radiosw(1, 1); + } else if (strlencmp(cmd, "disable") == 0) { + bluetooth_set_radiosw(0, 1); + } else + return -EINVAL; + } + + return 0; +} + +static struct ibm_struct bluetooth_driver_data = { + .name = "bluetooth", + .read = bluetooth_read, + .write = bluetooth_write, + .exit = bluetooth_exit, +}; + +/************************************************************************* + * Wan subdriver + */ + +enum { + /* ACPI GWAN/SWAN bits */ + TP_ACPI_WANCARD_HWPRESENT = 0x01, /* Wan hw available */ + TP_ACPI_WANCARD_RADIOSSW = 0x02, /* Wan radio enabled */ + TP_ACPI_WANCARD_UNK = 0x04, /* unknown function */ +}; + +static struct rfkill *tpacpi_wan_rfkill; + +static int wan_get_radiosw(void) +{ + int status; + + if (!tp_features.wan) + return -ENODEV; + + /* WLSW overrides WWAN in firmware/hardware, reflect that */ + if (tp_features.hotkey_wlsw && !hotkey_get_wlsw(&status) && !status) + return RFKILL_STATE_HARD_BLOCKED; + + if (!acpi_evalf(hkey_handle, &status, "GWAN", "d")) + return -EIO; + + return ((status & TP_ACPI_WANCARD_RADIOSSW) != 0) ? + RFKILL_STATE_UNBLOCKED : RFKILL_STATE_SOFT_BLOCKED; +} + +static void wan_update_rfk(void) +{ + int status; + + if (!tpacpi_wan_rfkill) + return; + + status = wan_get_radiosw(); + if (status < 0) + return; + rfkill_force_state(tpacpi_wan_rfkill, status); +} + +static int wan_set_radiosw(int radio_on, int update_rfk) +{ + int status; + + if (!tp_features.wan) + return -ENODEV; + + /* WLSW overrides bluetooth in firmware/hardware, but there is no + * reason to risk weird behaviour. */ + if (tp_features.hotkey_wlsw && !hotkey_get_wlsw(&status) && !status + && radio_on) + return -EPERM; + + if (!acpi_evalf(hkey_handle, &status, "GWAN", "d")) + return -EIO; + if (radio_on) + status |= TP_ACPI_WANCARD_RADIOSSW; + else + status &= ~TP_ACPI_WANCARD_RADIOSSW; + if (!acpi_evalf(hkey_handle, NULL, "SWAN", "vd", status)) + return -EIO; + + if (update_rfk) + wan_update_rfk(); + + return 0; +} + +/* sysfs wan enable ---------------------------------------------------- */ +static ssize_t wan_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int status; + + status = wan_get_radiosw(); + if (status < 0) + return status; + + return snprintf(buf, PAGE_SIZE, "%d\n", + (status == RFKILL_STATE_UNBLOCKED) ? 1 : 0); +} + +static ssize_t wan_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + int res; + + if (parse_strtoul(buf, 1, &t)) + return -EINVAL; + + res = wan_set_radiosw(t, 1); + + return (res) ? res : count; +} + +static struct device_attribute dev_attr_wan_enable = + __ATTR(wwan_enable, S_IWUSR | S_IRUGO, + wan_enable_show, wan_enable_store); + +/* --------------------------------------------------------------------- */ + +static struct attribute *wan_attributes[] = { + &dev_attr_wan_enable.attr, + NULL +}; + +static const struct attribute_group wan_attr_group = { + .attrs = wan_attributes, +}; + +static int tpacpi_wan_rfk_get(void *data, enum rfkill_state *state) +{ + int wans = wan_get_radiosw(); + + if (wans < 0) + return wans; + + *state = wans; + return 0; +} + +static int tpacpi_wan_rfk_set(void *data, enum rfkill_state state) +{ + return wan_set_radiosw((state == RFKILL_STATE_UNBLOCKED), 0); +} + +static void wan_exit(void) +{ + if (tpacpi_wan_rfkill) + rfkill_unregister(tpacpi_wan_rfkill); + + sysfs_remove_group(&tpacpi_pdev->dev.kobj, + &wan_attr_group); +} + +static int __init wan_init(struct ibm_init_struct *iibm) +{ + int res; + int status = 0; + + vdbg_printk(TPACPI_DBG_INIT, "initializing wan subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(hkey); + + tp_features.wan = hkey_handle && + acpi_evalf(hkey_handle, &status, "GWAN", "qd"); + + vdbg_printk(TPACPI_DBG_INIT, "wan is %s, status 0x%02x\n", + str_supported(tp_features.wan), + status); + + if (tp_features.wan && + !(status & TP_ACPI_WANCARD_HWPRESENT)) { + /* no wan hardware present in system */ + tp_features.wan = 0; + dbg_printk(TPACPI_DBG_INIT, + "wan hardware not installed\n"); + } + + if (!tp_features.wan) + return 1; + + res = sysfs_create_group(&tpacpi_pdev->dev.kobj, + &wan_attr_group); + if (res) + return res; + + res = tpacpi_new_rfkill(TPACPI_RFK_WWAN_SW_ID, + &tpacpi_wan_rfkill, + RFKILL_TYPE_WWAN, + "tpacpi_wwan_sw", + tpacpi_wan_rfk_set, + tpacpi_wan_rfk_get); + if (res) { + wan_exit(); + return res; + } + + return 0; +} + +/* procfs -------------------------------------------------------------- */ +static int wan_read(char *p) +{ + int len = 0; + int status = wan_get_radiosw(); + + if (!tp_features.wan) + len += sprintf(p + len, "status:\t\tnot supported\n"); + else { + len += sprintf(p + len, "status:\t\t%s\n", + (status == RFKILL_STATE_UNBLOCKED) ? + "enabled" : "disabled"); + len += sprintf(p + len, "commands:\tenable, disable\n"); + } + + return len; +} + +static int wan_write(char *buf) +{ + char *cmd; + + if (!tp_features.wan) + return -ENODEV; + + while ((cmd = next_cmd(&buf))) { + if (strlencmp(cmd, "enable") == 0) { + wan_set_radiosw(1, 1); + } else if (strlencmp(cmd, "disable") == 0) { + wan_set_radiosw(0, 1); + } else + return -EINVAL; + } + + return 0; +} + +static struct ibm_struct wan_driver_data = { + .name = "wan", + .read = wan_read, + .write = wan_write, + .exit = wan_exit, +}; + +/************************************************************************* + * Video subdriver + */ + +#ifdef CONFIG_THINKPAD_ACPI_VIDEO + +enum video_access_mode { + TPACPI_VIDEO_NONE = 0, + TPACPI_VIDEO_570, /* 570 */ + TPACPI_VIDEO_770, /* 600e/x, 770e, 770x */ + TPACPI_VIDEO_NEW, /* all others */ +}; + +enum { /* video status flags, based on VIDEO_570 */ + TP_ACPI_VIDEO_S_LCD = 0x01, /* LCD output enabled */ + TP_ACPI_VIDEO_S_CRT = 0x02, /* CRT output enabled */ + TP_ACPI_VIDEO_S_DVI = 0x08, /* DVI output enabled */ +}; + +enum { /* TPACPI_VIDEO_570 constants */ + TP_ACPI_VIDEO_570_PHSCMD = 0x87, /* unknown magic constant :( */ + TP_ACPI_VIDEO_570_PHSMASK = 0x03, /* PHS bits that map to + * video_status_flags */ + TP_ACPI_VIDEO_570_PHS2CMD = 0x8b, /* unknown magic constant :( */ + TP_ACPI_VIDEO_570_PHS2SET = 0x80, /* unknown magic constant :( */ +}; + +static enum video_access_mode video_supported; +static int video_orig_autosw; + +static int video_autosw_get(void); +static int video_autosw_set(int enable); + +TPACPI_HANDLE(vid2, root, "\\_SB.PCI0.AGPB.VID"); /* G41 */ + +static int __init video_init(struct ibm_init_struct *iibm) +{ + int ivga; + + vdbg_printk(TPACPI_DBG_INIT, "initializing video subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(vid); + TPACPI_ACPIHANDLE_INIT(vid2); + + if (vid2_handle && acpi_evalf(NULL, &ivga, "\\IVGA", "d") && ivga) + /* G41, assume IVGA doesn't change */ + vid_handle = vid2_handle; + + if (!vid_handle) + /* video switching not supported on R30, R31 */ + video_supported = TPACPI_VIDEO_NONE; + else if (acpi_evalf(vid_handle, &video_orig_autosw, "SWIT", "qd")) + /* 570 */ + video_supported = TPACPI_VIDEO_570; + else if (acpi_evalf(vid_handle, &video_orig_autosw, "^VADL", "qd")) + /* 600e/x, 770e, 770x */ + video_supported = TPACPI_VIDEO_770; + else + /* all others */ + video_supported = TPACPI_VIDEO_NEW; + + vdbg_printk(TPACPI_DBG_INIT, "video is %s, mode %d\n", + str_supported(video_supported != TPACPI_VIDEO_NONE), + video_supported); + + return (video_supported != TPACPI_VIDEO_NONE)? 0 : 1; +} + +static void video_exit(void) +{ + dbg_printk(TPACPI_DBG_EXIT, + "restoring original video autoswitch mode\n"); + if (video_autosw_set(video_orig_autosw)) + printk(TPACPI_ERR "error while trying to restore original " + "video autoswitch mode\n"); +} + +static int video_outputsw_get(void) +{ + int status = 0; + int i; + + switch (video_supported) { + case TPACPI_VIDEO_570: + if (!acpi_evalf(NULL, &i, "\\_SB.PHS", "dd", + TP_ACPI_VIDEO_570_PHSCMD)) + return -EIO; + status = i & TP_ACPI_VIDEO_570_PHSMASK; + break; + case TPACPI_VIDEO_770: + if (!acpi_evalf(NULL, &i, "\\VCDL", "d")) + return -EIO; + if (i) + status |= TP_ACPI_VIDEO_S_LCD; + if (!acpi_evalf(NULL, &i, "\\VCDC", "d")) + return -EIO; + if (i) + status |= TP_ACPI_VIDEO_S_CRT; + break; + case TPACPI_VIDEO_NEW: + if (!acpi_evalf(NULL, NULL, "\\VUPS", "vd", 1) || + !acpi_evalf(NULL, &i, "\\VCDC", "d")) + return -EIO; + if (i) + status |= TP_ACPI_VIDEO_S_CRT; + + if (!acpi_evalf(NULL, NULL, "\\VUPS", "vd", 0) || + !acpi_evalf(NULL, &i, "\\VCDL", "d")) + return -EIO; + if (i) + status |= TP_ACPI_VIDEO_S_LCD; + if (!acpi_evalf(NULL, &i, "\\VCDD", "d")) + return -EIO; + if (i) + status |= TP_ACPI_VIDEO_S_DVI; + break; + default: + return -ENOSYS; + } + + return status; +} + +static int video_outputsw_set(int status) +{ + int autosw; + int res = 0; + + switch (video_supported) { + case TPACPI_VIDEO_570: + res = acpi_evalf(NULL, NULL, + "\\_SB.PHS2", "vdd", + TP_ACPI_VIDEO_570_PHS2CMD, + status | TP_ACPI_VIDEO_570_PHS2SET); + break; + case TPACPI_VIDEO_770: + autosw = video_autosw_get(); + if (autosw < 0) + return autosw; + + res = video_autosw_set(1); + if (res) + return res; + res = acpi_evalf(vid_handle, NULL, + "ASWT", "vdd", status * 0x100, 0); + if (!autosw && video_autosw_set(autosw)) { + printk(TPACPI_ERR + "video auto-switch left enabled due to error\n"); + return -EIO; + } + break; + case TPACPI_VIDEO_NEW: + res = acpi_evalf(NULL, NULL, "\\VUPS", "vd", 0x80) && + acpi_evalf(NULL, NULL, "\\VSDS", "vdd", status, 1); + break; + default: + return -ENOSYS; + } + + return (res)? 0 : -EIO; +} + +static int video_autosw_get(void) +{ + int autosw = 0; + + switch (video_supported) { + case TPACPI_VIDEO_570: + if (!acpi_evalf(vid_handle, &autosw, "SWIT", "d")) + return -EIO; + break; + case TPACPI_VIDEO_770: + case TPACPI_VIDEO_NEW: + if (!acpi_evalf(vid_handle, &autosw, "^VDEE", "d")) + return -EIO; + break; + default: + return -ENOSYS; + } + + return autosw & 1; +} + +static int video_autosw_set(int enable) +{ + if (!acpi_evalf(vid_handle, NULL, "_DOS", "vd", (enable)? 1 : 0)) + return -EIO; + return 0; +} + +static int video_outputsw_cycle(void) +{ + int autosw = video_autosw_get(); + int res; + + if (autosw < 0) + return autosw; + + switch (video_supported) { + case TPACPI_VIDEO_570: + res = video_autosw_set(1); + if (res) + return res; + res = acpi_evalf(ec_handle, NULL, "_Q16", "v"); + break; + case TPACPI_VIDEO_770: + case TPACPI_VIDEO_NEW: + res = video_autosw_set(1); + if (res) + return res; + res = acpi_evalf(vid_handle, NULL, "VSWT", "v"); + break; + default: + return -ENOSYS; + } + if (!autosw && video_autosw_set(autosw)) { + printk(TPACPI_ERR + "video auto-switch left enabled due to error\n"); + return -EIO; + } + + return (res)? 0 : -EIO; +} + +static int video_expand_toggle(void) +{ + switch (video_supported) { + case TPACPI_VIDEO_570: + return acpi_evalf(ec_handle, NULL, "_Q17", "v")? + 0 : -EIO; + case TPACPI_VIDEO_770: + return acpi_evalf(vid_handle, NULL, "VEXP", "v")? + 0 : -EIO; + case TPACPI_VIDEO_NEW: + return acpi_evalf(NULL, NULL, "\\VEXP", "v")? + 0 : -EIO; + default: + return -ENOSYS; + } + /* not reached */ +} + +static int video_read(char *p) +{ + int status, autosw; + int len = 0; + + if (video_supported == TPACPI_VIDEO_NONE) { + len += sprintf(p + len, "status:\t\tnot supported\n"); + return len; + } + + status = video_outputsw_get(); + if (status < 0) + return status; + + autosw = video_autosw_get(); + if (autosw < 0) + return autosw; + + len += sprintf(p + len, "status:\t\tsupported\n"); + len += sprintf(p + len, "lcd:\t\t%s\n", enabled(status, 0)); + len += sprintf(p + len, "crt:\t\t%s\n", enabled(status, 1)); + if (video_supported == TPACPI_VIDEO_NEW) + len += sprintf(p + len, "dvi:\t\t%s\n", enabled(status, 3)); + len += sprintf(p + len, "auto:\t\t%s\n", enabled(autosw, 0)); + len += sprintf(p + len, "commands:\tlcd_enable, lcd_disable\n"); + len += sprintf(p + len, "commands:\tcrt_enable, crt_disable\n"); + if (video_supported == TPACPI_VIDEO_NEW) + len += sprintf(p + len, "commands:\tdvi_enable, dvi_disable\n"); + len += sprintf(p + len, "commands:\tauto_enable, auto_disable\n"); + len += sprintf(p + len, "commands:\tvideo_switch, expand_toggle\n"); + + return len; +} + +static int video_write(char *buf) +{ + char *cmd; + int enable, disable, status; + int res; + + if (video_supported == TPACPI_VIDEO_NONE) + return -ENODEV; + + enable = 0; + disable = 0; + + while ((cmd = next_cmd(&buf))) { + if (strlencmp(cmd, "lcd_enable") == 0) { + enable |= TP_ACPI_VIDEO_S_LCD; + } else if (strlencmp(cmd, "lcd_disable") == 0) { + disable |= TP_ACPI_VIDEO_S_LCD; + } else if (strlencmp(cmd, "crt_enable") == 0) { + enable |= TP_ACPI_VIDEO_S_CRT; + } else if (strlencmp(cmd, "crt_disable") == 0) { + disable |= TP_ACPI_VIDEO_S_CRT; + } else if (video_supported == TPACPI_VIDEO_NEW && + strlencmp(cmd, "dvi_enable") == 0) { + enable |= TP_ACPI_VIDEO_S_DVI; + } else if (video_supported == TPACPI_VIDEO_NEW && + strlencmp(cmd, "dvi_disable") == 0) { + disable |= TP_ACPI_VIDEO_S_DVI; + } else if (strlencmp(cmd, "auto_enable") == 0) { + res = video_autosw_set(1); + if (res) + return res; + } else if (strlencmp(cmd, "auto_disable") == 0) { + res = video_autosw_set(0); + if (res) + return res; + } else if (strlencmp(cmd, "video_switch") == 0) { + res = video_outputsw_cycle(); + if (res) + return res; + } else if (strlencmp(cmd, "expand_toggle") == 0) { + res = video_expand_toggle(); + if (res) + return res; + } else + return -EINVAL; + } + + if (enable || disable) { + status = video_outputsw_get(); + if (status < 0) + return status; + res = video_outputsw_set((status & ~disable) | enable); + if (res) + return res; + } + + return 0; +} + +static struct ibm_struct video_driver_data = { + .name = "video", + .read = video_read, + .write = video_write, + .exit = video_exit, +}; + +#endif /* CONFIG_THINKPAD_ACPI_VIDEO */ + +/************************************************************************* + * Light (thinklight) subdriver + */ + +TPACPI_HANDLE(lght, root, "\\LGHT"); /* A21e, A2xm/p, T20-22, X20-21 */ +TPACPI_HANDLE(ledb, ec, "LEDB"); /* G4x */ + +static int light_get_status(void) +{ + int status = 0; + + if (tp_features.light_status) { + if (!acpi_evalf(ec_handle, &status, "KBLT", "d")) + return -EIO; + return (!!status); + } + + return -ENXIO; +} + +static int light_set_status(int status) +{ + int rc; + + if (tp_features.light) { + if (cmos_handle) { + rc = acpi_evalf(cmos_handle, NULL, NULL, "vd", + (status)? + TP_CMOS_THINKLIGHT_ON : + TP_CMOS_THINKLIGHT_OFF); + } else { + rc = acpi_evalf(lght_handle, NULL, NULL, "vd", + (status)? 1 : 0); + } + return (rc)? 0 : -EIO; + } + + return -ENXIO; +} + +static void light_set_status_worker(struct work_struct *work) +{ + struct tpacpi_led_classdev *data = + container_of(work, struct tpacpi_led_classdev, work); + + if (likely(tpacpi_lifecycle == TPACPI_LIFE_RUNNING)) + light_set_status((data->new_brightness != LED_OFF)); +} + +static void light_sysfs_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct tpacpi_led_classdev *data = + container_of(led_cdev, + struct tpacpi_led_classdev, + led_classdev); + data->new_brightness = brightness; + queue_work(tpacpi_wq, &data->work); +} + +static enum led_brightness light_sysfs_get(struct led_classdev *led_cdev) +{ + return (light_get_status() == 1)? LED_FULL : LED_OFF; +} + +static struct tpacpi_led_classdev tpacpi_led_thinklight = { + .led_classdev = { + .name = "tpacpi::thinklight", + .brightness_set = &light_sysfs_set, + .brightness_get = &light_sysfs_get, + } +}; + +static int __init light_init(struct ibm_init_struct *iibm) +{ + int rc; + + vdbg_printk(TPACPI_DBG_INIT, "initializing light subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(ledb); + TPACPI_ACPIHANDLE_INIT(lght); + TPACPI_ACPIHANDLE_INIT(cmos); + INIT_WORK(&tpacpi_led_thinklight.work, light_set_status_worker); + + /* light not supported on 570, 600e/x, 770e, 770x, G4x, R30, R31 */ + tp_features.light = (cmos_handle || lght_handle) && !ledb_handle; + + if (tp_features.light) + /* light status not supported on + 570, 600e/x, 770e, 770x, G4x, R30, R31, R32, X20 */ + tp_features.light_status = + acpi_evalf(ec_handle, NULL, "KBLT", "qv"); + + vdbg_printk(TPACPI_DBG_INIT, "light is %s, light status is %s\n", + str_supported(tp_features.light), + str_supported(tp_features.light_status)); + + if (!tp_features.light) + return 1; + + rc = led_classdev_register(&tpacpi_pdev->dev, + &tpacpi_led_thinklight.led_classdev); + + if (rc < 0) { + tp_features.light = 0; + tp_features.light_status = 0; + } else { + rc = 0; + } + + return rc; +} + +static void light_exit(void) +{ + led_classdev_unregister(&tpacpi_led_thinklight.led_classdev); + if (work_pending(&tpacpi_led_thinklight.work)) + flush_workqueue(tpacpi_wq); +} + +static int light_read(char *p) +{ + int len = 0; + int status; + + if (!tp_features.light) { + len += sprintf(p + len, "status:\t\tnot supported\n"); + } else if (!tp_features.light_status) { + len += sprintf(p + len, "status:\t\tunknown\n"); + len += sprintf(p + len, "commands:\ton, off\n"); + } else { + status = light_get_status(); + if (status < 0) + return status; + len += sprintf(p + len, "status:\t\t%s\n", onoff(status, 0)); + len += sprintf(p + len, "commands:\ton, off\n"); + } + + return len; +} + +static int light_write(char *buf) +{ + char *cmd; + int newstatus = 0; + + if (!tp_features.light) + return -ENODEV; + + while ((cmd = next_cmd(&buf))) { + if (strlencmp(cmd, "on") == 0) { + newstatus = 1; + } else if (strlencmp(cmd, "off") == 0) { + newstatus = 0; + } else + return -EINVAL; + } + + return light_set_status(newstatus); +} + +static struct ibm_struct light_driver_data = { + .name = "light", + .read = light_read, + .write = light_write, + .exit = light_exit, +}; + +/************************************************************************* + * Dock subdriver + */ + +#ifdef CONFIG_THINKPAD_ACPI_DOCK + +static void dock_notify(struct ibm_struct *ibm, u32 event); +static int dock_read(char *p); +static int dock_write(char *buf); + +TPACPI_HANDLE(dock, root, "\\_SB.GDCK", /* X30, X31, X40 */ + "\\_SB.PCI0.DOCK", /* 600e/x,770e,770x,A2xm/p,T20-22,X20-21 */ + "\\_SB.PCI0.PCI1.DOCK", /* all others */ + "\\_SB.PCI.ISA.SLCE", /* 570 */ + ); /* A21e,G4x,R30,R31,R32,R40,R40e,R50e */ + +/* don't list other alternatives as we install a notify handler on the 570 */ +TPACPI_HANDLE(pci, root, "\\_SB.PCI"); /* 570 */ + +static const struct acpi_device_id ibm_pci_device_ids[] = { + {PCI_ROOT_HID_STRING, 0}, + {"", 0}, +}; + +static struct tp_acpi_drv_struct ibm_dock_acpidriver[2] = { + { + .notify = dock_notify, + .handle = &dock_handle, + .type = ACPI_SYSTEM_NOTIFY, + }, + { + /* THIS ONE MUST NEVER BE USED FOR DRIVER AUTOLOADING. + * We just use it to get notifications of dock hotplug + * in very old thinkpads */ + .hid = ibm_pci_device_ids, + .notify = dock_notify, + .handle = &pci_handle, + .type = ACPI_SYSTEM_NOTIFY, + }, +}; + +static struct ibm_struct dock_driver_data[2] = { + { + .name = "dock", + .read = dock_read, + .write = dock_write, + .acpi = &ibm_dock_acpidriver[0], + }, + { + .name = "dock", + .acpi = &ibm_dock_acpidriver[1], + }, +}; + +#define dock_docked() (_sta(dock_handle) & 1) + +static int __init dock_init(struct ibm_init_struct *iibm) +{ + vdbg_printk(TPACPI_DBG_INIT, "initializing dock subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(dock); + + vdbg_printk(TPACPI_DBG_INIT, "dock is %s\n", + str_supported(dock_handle != NULL)); + + return (dock_handle)? 0 : 1; +} + +static int __init dock_init2(struct ibm_init_struct *iibm) +{ + int dock2_needed; + + vdbg_printk(TPACPI_DBG_INIT, "initializing dock subdriver part 2\n"); + + if (dock_driver_data[0].flags.acpi_driver_registered && + dock_driver_data[0].flags.acpi_notify_installed) { + TPACPI_ACPIHANDLE_INIT(pci); + dock2_needed = (pci_handle != NULL); + vdbg_printk(TPACPI_DBG_INIT, + "dock PCI handler for the TP 570 is %s\n", + str_supported(dock2_needed)); + } else { + vdbg_printk(TPACPI_DBG_INIT, + "dock subdriver part 2 not required\n"); + dock2_needed = 0; + } + + return (dock2_needed)? 0 : 1; +} + +static void dock_notify(struct ibm_struct *ibm, u32 event) +{ + int docked = dock_docked(); + int pci = ibm->acpi->hid && ibm->acpi->device && + acpi_match_device_ids(ibm->acpi->device, ibm_pci_device_ids); + int data; + + if (event == 1 && !pci) /* 570 */ + data = 1; /* button */ + else if (event == 1 && pci) /* 570 */ + data = 3; /* dock */ + else if (event == 3 && docked) + data = 1; /* button */ + else if (event == 3 && !docked) + data = 2; /* undock */ + else if (event == 0 && docked) + data = 3; /* dock */ + else { + printk(TPACPI_ERR "unknown dock event %d, status %d\n", + event, _sta(dock_handle)); + data = 0; /* unknown */ + } + acpi_bus_generate_proc_event(ibm->acpi->device, event, data); + acpi_bus_generate_netlink_event(ibm->acpi->device->pnp.device_class, + ibm->acpi->device->dev.bus_id, + event, data); +} + +static int dock_read(char *p) +{ + int len = 0; + int docked = dock_docked(); + + if (!dock_handle) + len += sprintf(p + len, "status:\t\tnot supported\n"); + else if (!docked) + len += sprintf(p + len, "status:\t\tundocked\n"); + else { + len += sprintf(p + len, "status:\t\tdocked\n"); + len += sprintf(p + len, "commands:\tdock, undock\n"); + } + + return len; +} + +static int dock_write(char *buf) +{ + char *cmd; + + if (!dock_docked()) + return -ENODEV; + + while ((cmd = next_cmd(&buf))) { + if (strlencmp(cmd, "undock") == 0) { + if (!acpi_evalf(dock_handle, NULL, "_DCK", "vd", 0) || + !acpi_evalf(dock_handle, NULL, "_EJ0", "vd", 1)) + return -EIO; + } else if (strlencmp(cmd, "dock") == 0) { + if (!acpi_evalf(dock_handle, NULL, "_DCK", "vd", 1)) + return -EIO; + } else + return -EINVAL; + } + + return 0; +} + +#endif /* CONFIG_THINKPAD_ACPI_DOCK */ + +/************************************************************************* + * Bay subdriver + */ + +#ifdef CONFIG_THINKPAD_ACPI_BAY + +TPACPI_HANDLE(bay, root, "\\_SB.PCI.IDE.SECN.MAST", /* 570 */ + "\\_SB.PCI0.IDE0.IDES.IDSM", /* 600e/x, 770e, 770x */ + "\\_SB.PCI0.SATA.SCND.MSTR", /* T60, X60, Z60 */ + "\\_SB.PCI0.IDE0.SCND.MSTR", /* all others */ + ); /* A21e, R30, R31 */ +TPACPI_HANDLE(bay_ej, bay, "_EJ3", /* 600e/x, A2xm/p, A3x */ + "_EJ0", /* all others */ + ); /* 570,A21e,G4x,R30,R31,R32,R40e,R50e */ +TPACPI_HANDLE(bay2, root, "\\_SB.PCI0.IDE0.PRIM.SLAV", /* A3x, R32 */ + "\\_SB.PCI0.IDE0.IDEP.IDPS", /* 600e/x, 770e, 770x */ + ); /* all others */ +TPACPI_HANDLE(bay2_ej, bay2, "_EJ3", /* 600e/x, 770e, A3x */ + "_EJ0", /* 770x */ + ); /* all others */ + +static int __init bay_init(struct ibm_init_struct *iibm) +{ + vdbg_printk(TPACPI_DBG_INIT, "initializing bay subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(bay); + if (bay_handle) + TPACPI_ACPIHANDLE_INIT(bay_ej); + TPACPI_ACPIHANDLE_INIT(bay2); + if (bay2_handle) + TPACPI_ACPIHANDLE_INIT(bay2_ej); + + tp_features.bay_status = bay_handle && + acpi_evalf(bay_handle, NULL, "_STA", "qv"); + tp_features.bay_status2 = bay2_handle && + acpi_evalf(bay2_handle, NULL, "_STA", "qv"); + + tp_features.bay_eject = bay_handle && bay_ej_handle && + (strlencmp(bay_ej_path, "_EJ0") == 0 || experimental); + tp_features.bay_eject2 = bay2_handle && bay2_ej_handle && + (strlencmp(bay2_ej_path, "_EJ0") == 0 || experimental); + + vdbg_printk(TPACPI_DBG_INIT, + "bay 1: status %s, eject %s; bay 2: status %s, eject %s\n", + str_supported(tp_features.bay_status), + str_supported(tp_features.bay_eject), + str_supported(tp_features.bay_status2), + str_supported(tp_features.bay_eject2)); + + return (tp_features.bay_status || tp_features.bay_eject || + tp_features.bay_status2 || tp_features.bay_eject2)? 0 : 1; +} + +static void bay_notify(struct ibm_struct *ibm, u32 event) +{ + acpi_bus_generate_proc_event(ibm->acpi->device, event, 0); + acpi_bus_generate_netlink_event(ibm->acpi->device->pnp.device_class, + ibm->acpi->device->dev.bus_id, + event, 0); +} + +#define bay_occupied(b) (_sta(b##_handle) & 1) + +static int bay_read(char *p) +{ + int len = 0; + int occupied = bay_occupied(bay); + int occupied2 = bay_occupied(bay2); + int eject, eject2; + + len += sprintf(p + len, "status:\t\t%s\n", + tp_features.bay_status ? + (occupied ? "occupied" : "unoccupied") : + "not supported"); + if (tp_features.bay_status2) + len += sprintf(p + len, "status2:\t%s\n", occupied2 ? + "occupied" : "unoccupied"); + + eject = tp_features.bay_eject && occupied; + eject2 = tp_features.bay_eject2 && occupied2; + + if (eject && eject2) + len += sprintf(p + len, "commands:\teject, eject2\n"); + else if (eject) + len += sprintf(p + len, "commands:\teject\n"); + else if (eject2) + len += sprintf(p + len, "commands:\teject2\n"); + + return len; +} + +static int bay_write(char *buf) +{ + char *cmd; + + if (!tp_features.bay_eject && !tp_features.bay_eject2) + return -ENODEV; + + while ((cmd = next_cmd(&buf))) { + if (tp_features.bay_eject && strlencmp(cmd, "eject") == 0) { + if (!acpi_evalf(bay_ej_handle, NULL, NULL, "vd", 1)) + return -EIO; + } else if (tp_features.bay_eject2 && + strlencmp(cmd, "eject2") == 0) { + if (!acpi_evalf(bay2_ej_handle, NULL, NULL, "vd", 1)) + return -EIO; + } else + return -EINVAL; + } + + return 0; +} + +static struct tp_acpi_drv_struct ibm_bay_acpidriver = { + .notify = bay_notify, + .handle = &bay_handle, + .type = ACPI_SYSTEM_NOTIFY, +}; + +static struct ibm_struct bay_driver_data = { + .name = "bay", + .read = bay_read, + .write = bay_write, + .acpi = &ibm_bay_acpidriver, +}; + +#endif /* CONFIG_THINKPAD_ACPI_BAY */ + +/************************************************************************* + * CMOS subdriver + */ + +/* sysfs cmos_command -------------------------------------------------- */ +static ssize_t cmos_command_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long cmos_cmd; + int res; + + if (parse_strtoul(buf, 21, &cmos_cmd)) + return -EINVAL; + + res = issue_thinkpad_cmos_command(cmos_cmd); + return (res)? res : count; +} + +static struct device_attribute dev_attr_cmos_command = + __ATTR(cmos_command, S_IWUSR, NULL, cmos_command_store); + +/* --------------------------------------------------------------------- */ + +static int __init cmos_init(struct ibm_init_struct *iibm) +{ + int res; + + vdbg_printk(TPACPI_DBG_INIT, + "initializing cmos commands subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(cmos); + + vdbg_printk(TPACPI_DBG_INIT, "cmos commands are %s\n", + str_supported(cmos_handle != NULL)); + + res = device_create_file(&tpacpi_pdev->dev, &dev_attr_cmos_command); + if (res) + return res; + + return (cmos_handle)? 0 : 1; +} + +static void cmos_exit(void) +{ + device_remove_file(&tpacpi_pdev->dev, &dev_attr_cmos_command); +} + +static int cmos_read(char *p) +{ + int len = 0; + + /* cmos not supported on 570, 600e/x, 770e, 770x, A21e, A2xm/p, + R30, R31, T20-22, X20-21 */ + if (!cmos_handle) + len += sprintf(p + len, "status:\t\tnot supported\n"); + else { + len += sprintf(p + len, "status:\t\tsupported\n"); + len += sprintf(p + len, "commands:\t<cmd> (<cmd> is 0-21)\n"); + } + + return len; +} + +static int cmos_write(char *buf) +{ + char *cmd; + int cmos_cmd, res; + + while ((cmd = next_cmd(&buf))) { + if (sscanf(cmd, "%u", &cmos_cmd) == 1 && + cmos_cmd >= 0 && cmos_cmd <= 21) { + /* cmos_cmd set */ + } else + return -EINVAL; + + res = issue_thinkpad_cmos_command(cmos_cmd); + if (res) + return res; + } + + return 0; +} + +static struct ibm_struct cmos_driver_data = { + .name = "cmos", + .read = cmos_read, + .write = cmos_write, + .exit = cmos_exit, +}; + +/************************************************************************* + * LED subdriver + */ + +enum led_access_mode { + TPACPI_LED_NONE = 0, + TPACPI_LED_570, /* 570 */ + TPACPI_LED_OLD, /* 600e/x, 770e, 770x, A21e, A2xm/p, T20-22, X20-21 */ + TPACPI_LED_NEW, /* all others */ +}; + +enum { /* For TPACPI_LED_OLD */ + TPACPI_LED_EC_HLCL = 0x0c, /* EC reg to get led to power on */ + TPACPI_LED_EC_HLBL = 0x0d, /* EC reg to blink a lit led */ + TPACPI_LED_EC_HLMS = 0x0e, /* EC reg to select led to command */ +}; + +enum led_status_t { + TPACPI_LED_OFF = 0, + TPACPI_LED_ON, + TPACPI_LED_BLINK, +}; + +static enum led_access_mode led_supported; + +TPACPI_HANDLE(led, ec, "SLED", /* 570 */ + "SYSL", /* 600e/x, 770e, 770x, A21e, A2xm/p, */ + /* T20-22, X20-21 */ + "LED", /* all others */ + ); /* R30, R31 */ + +#define TPACPI_LED_NUMLEDS 8 +static struct tpacpi_led_classdev *tpacpi_leds; +static enum led_status_t tpacpi_led_state_cache[TPACPI_LED_NUMLEDS]; +static const char * const tpacpi_led_names[TPACPI_LED_NUMLEDS] = { + /* there's a limit of 19 chars + NULL before 2.6.26 */ + "tpacpi::power", + "tpacpi:orange:batt", + "tpacpi:green:batt", + "tpacpi::dock_active", + "tpacpi::bay_active", + "tpacpi::dock_batt", + "tpacpi::unknown_led", + "tpacpi::standby", +}; + +static int led_get_status(const unsigned int led) +{ + int status; + enum led_status_t led_s; + + switch (led_supported) { + case TPACPI_LED_570: + if (!acpi_evalf(ec_handle, + &status, "GLED", "dd", 1 << led)) + return -EIO; + led_s = (status == 0)? + TPACPI_LED_OFF : + ((status == 1)? + TPACPI_LED_ON : + TPACPI_LED_BLINK); + tpacpi_led_state_cache[led] = led_s; + return led_s; + default: + return -ENXIO; + } + + /* not reached */ +} + +static int led_set_status(const unsigned int led, + const enum led_status_t ledstatus) +{ + /* off, on, blink. Index is led_status_t */ + static const unsigned int led_sled_arg1[] = { 0, 1, 3 }; + static const unsigned int led_led_arg1[] = { 0, 0x80, 0xc0 }; + + int rc = 0; + + switch (led_supported) { + case TPACPI_LED_570: + /* 570 */ + if (led > 7) + return -EINVAL; + if (!acpi_evalf(led_handle, NULL, NULL, "vdd", + (1 << led), led_sled_arg1[ledstatus])) + rc = -EIO; + break; + case TPACPI_LED_OLD: + /* 600e/x, 770e, 770x, A21e, A2xm/p, T20-22, X20 */ + if (led > 7) + return -EINVAL; + rc = ec_write(TPACPI_LED_EC_HLMS, (1 << led)); + if (rc >= 0) + rc = ec_write(TPACPI_LED_EC_HLBL, + (ledstatus == TPACPI_LED_BLINK) << led); + if (rc >= 0) + rc = ec_write(TPACPI_LED_EC_HLCL, + (ledstatus != TPACPI_LED_OFF) << led); + break; + case TPACPI_LED_NEW: + /* all others */ + if (!acpi_evalf(led_handle, NULL, NULL, "vdd", + led, led_led_arg1[ledstatus])) + rc = -EIO; + break; + default: + rc = -ENXIO; + } + + if (!rc) + tpacpi_led_state_cache[led] = ledstatus; + + return rc; +} + +static void led_sysfs_set_status(unsigned int led, + enum led_brightness brightness) +{ + led_set_status(led, + (brightness == LED_OFF) ? + TPACPI_LED_OFF : + (tpacpi_led_state_cache[led] == TPACPI_LED_BLINK) ? + TPACPI_LED_BLINK : TPACPI_LED_ON); +} + +static void led_set_status_worker(struct work_struct *work) +{ + struct tpacpi_led_classdev *data = + container_of(work, struct tpacpi_led_classdev, work); + + if (likely(tpacpi_lifecycle == TPACPI_LIFE_RUNNING)) + led_sysfs_set_status(data->led, data->new_brightness); +} + +static void led_sysfs_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct tpacpi_led_classdev *data = container_of(led_cdev, + struct tpacpi_led_classdev, led_classdev); + + data->new_brightness = brightness; + queue_work(tpacpi_wq, &data->work); +} + +static int led_sysfs_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, unsigned long *delay_off) +{ + struct tpacpi_led_classdev *data = container_of(led_cdev, + struct tpacpi_led_classdev, led_classdev); + + /* Can we choose the flash rate? */ + if (*delay_on == 0 && *delay_off == 0) { + /* yes. set them to the hardware blink rate (1 Hz) */ + *delay_on = 500; /* ms */ + *delay_off = 500; /* ms */ + } else if ((*delay_on != 500) || (*delay_off != 500)) + return -EINVAL; + + data->new_brightness = TPACPI_LED_BLINK; + queue_work(tpacpi_wq, &data->work); + + return 0; +} + +static enum led_brightness led_sysfs_get(struct led_classdev *led_cdev) +{ + int rc; + + struct tpacpi_led_classdev *data = container_of(led_cdev, + struct tpacpi_led_classdev, led_classdev); + + rc = led_get_status(data->led); + + if (rc == TPACPI_LED_OFF || rc < 0) + rc = LED_OFF; /* no error handling in led class :( */ + else + rc = LED_FULL; + + return rc; +} + +static void led_exit(void) +{ + unsigned int i; + + for (i = 0; i < TPACPI_LED_NUMLEDS; i++) { + if (tpacpi_leds[i].led_classdev.name) + led_classdev_unregister(&tpacpi_leds[i].led_classdev); + } + + kfree(tpacpi_leds); +} + +static int __init led_init(struct ibm_init_struct *iibm) +{ + unsigned int i; + int rc; + + vdbg_printk(TPACPI_DBG_INIT, "initializing LED subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(led); + + if (!led_handle) + /* led not supported on R30, R31 */ + led_supported = TPACPI_LED_NONE; + else if (strlencmp(led_path, "SLED") == 0) + /* 570 */ + led_supported = TPACPI_LED_570; + else if (strlencmp(led_path, "SYSL") == 0) + /* 600e/x, 770e, 770x, A21e, A2xm/p, T20-22, X20-21 */ + led_supported = TPACPI_LED_OLD; + else + /* all others */ + led_supported = TPACPI_LED_NEW; + + vdbg_printk(TPACPI_DBG_INIT, "LED commands are %s, mode %d\n", + str_supported(led_supported), led_supported); + + tpacpi_leds = kzalloc(sizeof(*tpacpi_leds) * TPACPI_LED_NUMLEDS, + GFP_KERNEL); + if (!tpacpi_leds) { + printk(TPACPI_ERR "Out of memory for LED data\n"); + return -ENOMEM; + } + + for (i = 0; i < TPACPI_LED_NUMLEDS; i++) { + tpacpi_leds[i].led = i; + + tpacpi_leds[i].led_classdev.brightness_set = &led_sysfs_set; + tpacpi_leds[i].led_classdev.blink_set = &led_sysfs_blink_set; + if (led_supported == TPACPI_LED_570) + tpacpi_leds[i].led_classdev.brightness_get = + &led_sysfs_get; + + tpacpi_leds[i].led_classdev.name = tpacpi_led_names[i]; + + INIT_WORK(&tpacpi_leds[i].work, led_set_status_worker); + + rc = led_classdev_register(&tpacpi_pdev->dev, + &tpacpi_leds[i].led_classdev); + if (rc < 0) { + tpacpi_leds[i].led_classdev.name = NULL; + led_exit(); + return rc; + } + } + + return (led_supported != TPACPI_LED_NONE)? 0 : 1; +} + +#define str_led_status(s) \ + ((s) == TPACPI_LED_OFF ? "off" : \ + ((s) == TPACPI_LED_ON ? "on" : "blinking")) + +static int led_read(char *p) +{ + int len = 0; + + if (!led_supported) { + len += sprintf(p + len, "status:\t\tnot supported\n"); + return len; + } + len += sprintf(p + len, "status:\t\tsupported\n"); + + if (led_supported == TPACPI_LED_570) { + /* 570 */ + int i, status; + for (i = 0; i < 8; i++) { + status = led_get_status(i); + if (status < 0) + return -EIO; + len += sprintf(p + len, "%d:\t\t%s\n", + i, str_led_status(status)); + } + } + + len += sprintf(p + len, "commands:\t" + "<led> on, <led> off, <led> blink (<led> is 0-7)\n"); + + return len; +} + +static int led_write(char *buf) +{ + char *cmd; + int led, rc; + enum led_status_t s; + + if (!led_supported) + return -ENODEV; + + while ((cmd = next_cmd(&buf))) { + if (sscanf(cmd, "%d", &led) != 1 || led < 0 || led > 7) + return -EINVAL; + + if (strstr(cmd, "off")) { + s = TPACPI_LED_OFF; + } else if (strstr(cmd, "on")) { + s = TPACPI_LED_ON; + } else if (strstr(cmd, "blink")) { + s = TPACPI_LED_BLINK; + } else { + return -EINVAL; + } + + rc = led_set_status(led, s); + if (rc < 0) + return rc; + } + + return 0; +} + +static struct ibm_struct led_driver_data = { + .name = "led", + .read = led_read, + .write = led_write, + .exit = led_exit, +}; + +/************************************************************************* + * Beep subdriver + */ + +TPACPI_HANDLE(beep, ec, "BEEP"); /* all except R30, R31 */ + +static int __init beep_init(struct ibm_init_struct *iibm) +{ + vdbg_printk(TPACPI_DBG_INIT, "initializing beep subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(beep); + + vdbg_printk(TPACPI_DBG_INIT, "beep is %s\n", + str_supported(beep_handle != NULL)); + + return (beep_handle)? 0 : 1; +} + +static int beep_read(char *p) +{ + int len = 0; + + if (!beep_handle) + len += sprintf(p + len, "status:\t\tnot supported\n"); + else { + len += sprintf(p + len, "status:\t\tsupported\n"); + len += sprintf(p + len, "commands:\t<cmd> (<cmd> is 0-17)\n"); + } + + return len; +} + +static int beep_write(char *buf) +{ + char *cmd; + int beep_cmd; + + if (!beep_handle) + return -ENODEV; + + while ((cmd = next_cmd(&buf))) { + if (sscanf(cmd, "%u", &beep_cmd) == 1 && + beep_cmd >= 0 && beep_cmd <= 17) { + /* beep_cmd set */ + } else + return -EINVAL; + if (!acpi_evalf(beep_handle, NULL, NULL, "vdd", beep_cmd, 0)) + return -EIO; + } + + return 0; +} + +static struct ibm_struct beep_driver_data = { + .name = "beep", + .read = beep_read, + .write = beep_write, +}; + +/************************************************************************* + * Thermal subdriver + */ + +enum thermal_access_mode { + TPACPI_THERMAL_NONE = 0, /* No thermal support */ + TPACPI_THERMAL_ACPI_TMP07, /* Use ACPI TMP0-7 */ + TPACPI_THERMAL_ACPI_UPDT, /* Use ACPI TMP0-7 with UPDT */ + TPACPI_THERMAL_TPEC_8, /* Use ACPI EC regs, 8 sensors */ + TPACPI_THERMAL_TPEC_16, /* Use ACPI EC regs, 16 sensors */ +}; + +enum { /* TPACPI_THERMAL_TPEC_* */ + TP_EC_THERMAL_TMP0 = 0x78, /* ACPI EC regs TMP 0..7 */ + TP_EC_THERMAL_TMP8 = 0xC0, /* ACPI EC regs TMP 8..15 */ + TP_EC_THERMAL_TMP_NA = -128, /* ACPI EC sensor not available */ +}; + +#define TPACPI_MAX_THERMAL_SENSORS 16 /* Max thermal sensors supported */ +struct ibm_thermal_sensors_struct { + s32 temp[TPACPI_MAX_THERMAL_SENSORS]; +}; + +static enum thermal_access_mode thermal_read_mode; + +/* idx is zero-based */ +static int thermal_get_sensor(int idx, s32 *value) +{ + int t; + s8 tmp; + char tmpi[5]; + + t = TP_EC_THERMAL_TMP0; + + switch (thermal_read_mode) { +#if TPACPI_MAX_THERMAL_SENSORS >= 16 + case TPACPI_THERMAL_TPEC_16: + if (idx >= 8 && idx <= 15) { + t = TP_EC_THERMAL_TMP8; + idx -= 8; + } + /* fallthrough */ +#endif + case TPACPI_THERMAL_TPEC_8: + if (idx <= 7) { + if (!acpi_ec_read(t + idx, &tmp)) + return -EIO; + *value = tmp * 1000; + return 0; + } + break; + + case TPACPI_THERMAL_ACPI_UPDT: + if (idx <= 7) { + snprintf(tmpi, sizeof(tmpi), "TMP%c", '0' + idx); + if (!acpi_evalf(ec_handle, NULL, "UPDT", "v")) + return -EIO; + if (!acpi_evalf(ec_handle, &t, tmpi, "d")) + return -EIO; + *value = (t - 2732) * 100; + return 0; + } + break; + + case TPACPI_THERMAL_ACPI_TMP07: + if (idx <= 7) { + snprintf(tmpi, sizeof(tmpi), "TMP%c", '0' + idx); + if (!acpi_evalf(ec_handle, &t, tmpi, "d")) + return -EIO; + if (t > 127 || t < -127) + t = TP_EC_THERMAL_TMP_NA; + *value = t * 1000; + return 0; + } + break; + + case TPACPI_THERMAL_NONE: + default: + return -ENOSYS; + } + + return -EINVAL; +} + +static int thermal_get_sensors(struct ibm_thermal_sensors_struct *s) +{ + int res, i; + int n; + + n = 8; + i = 0; + + if (!s) + return -EINVAL; + + if (thermal_read_mode == TPACPI_THERMAL_TPEC_16) + n = 16; + + for (i = 0 ; i < n; i++) { + res = thermal_get_sensor(i, &s->temp[i]); + if (res) + return res; + } + + return n; +} + +/* sysfs temp##_input -------------------------------------------------- */ + +static ssize_t thermal_temp_input_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct sensor_device_attribute *sensor_attr = + to_sensor_dev_attr(attr); + int idx = sensor_attr->index; + s32 value; + int res; + + res = thermal_get_sensor(idx, &value); + if (res) + return res; + if (value == TP_EC_THERMAL_TMP_NA * 1000) + return -ENXIO; + + return snprintf(buf, PAGE_SIZE, "%d\n", value); +} + +#define THERMAL_SENSOR_ATTR_TEMP(_idxA, _idxB) \ + SENSOR_ATTR(temp##_idxA##_input, S_IRUGO, \ + thermal_temp_input_show, NULL, _idxB) + +static struct sensor_device_attribute sensor_dev_attr_thermal_temp_input[] = { + THERMAL_SENSOR_ATTR_TEMP(1, 0), + THERMAL_SENSOR_ATTR_TEMP(2, 1), + THERMAL_SENSOR_ATTR_TEMP(3, 2), + THERMAL_SENSOR_ATTR_TEMP(4, 3), + THERMAL_SENSOR_ATTR_TEMP(5, 4), + THERMAL_SENSOR_ATTR_TEMP(6, 5), + THERMAL_SENSOR_ATTR_TEMP(7, 6), + THERMAL_SENSOR_ATTR_TEMP(8, 7), + THERMAL_SENSOR_ATTR_TEMP(9, 8), + THERMAL_SENSOR_ATTR_TEMP(10, 9), + THERMAL_SENSOR_ATTR_TEMP(11, 10), + THERMAL_SENSOR_ATTR_TEMP(12, 11), + THERMAL_SENSOR_ATTR_TEMP(13, 12), + THERMAL_SENSOR_ATTR_TEMP(14, 13), + THERMAL_SENSOR_ATTR_TEMP(15, 14), + THERMAL_SENSOR_ATTR_TEMP(16, 15), +}; + +#define THERMAL_ATTRS(X) \ + &sensor_dev_attr_thermal_temp_input[X].dev_attr.attr + +static struct attribute *thermal_temp_input_attr[] = { + THERMAL_ATTRS(8), + THERMAL_ATTRS(9), + THERMAL_ATTRS(10), + THERMAL_ATTRS(11), + THERMAL_ATTRS(12), + THERMAL_ATTRS(13), + THERMAL_ATTRS(14), + THERMAL_ATTRS(15), + THERMAL_ATTRS(0), + THERMAL_ATTRS(1), + THERMAL_ATTRS(2), + THERMAL_ATTRS(3), + THERMAL_ATTRS(4), + THERMAL_ATTRS(5), + THERMAL_ATTRS(6), + THERMAL_ATTRS(7), + NULL +}; + +static const struct attribute_group thermal_temp_input16_group = { + .attrs = thermal_temp_input_attr +}; + +static const struct attribute_group thermal_temp_input8_group = { + .attrs = &thermal_temp_input_attr[8] +}; + +#undef THERMAL_SENSOR_ATTR_TEMP +#undef THERMAL_ATTRS + +/* --------------------------------------------------------------------- */ + +static int __init thermal_init(struct ibm_init_struct *iibm) +{ + u8 t, ta1, ta2; + int i; + int acpi_tmp7; + int res; + + vdbg_printk(TPACPI_DBG_INIT, "initializing thermal subdriver\n"); + + acpi_tmp7 = acpi_evalf(ec_handle, NULL, "TMP7", "qv"); + + if (thinkpad_id.ec_model) { + /* + * Direct EC access mode: sensors at registers + * 0x78-0x7F, 0xC0-0xC7. Registers return 0x00 for + * non-implemented, thermal sensors return 0x80 when + * not available + */ + + ta1 = ta2 = 0; + for (i = 0; i < 8; i++) { + if (acpi_ec_read(TP_EC_THERMAL_TMP0 + i, &t)) { + ta1 |= t; + } else { + ta1 = 0; + break; + } + if (acpi_ec_read(TP_EC_THERMAL_TMP8 + i, &t)) { + ta2 |= t; + } else { + ta1 = 0; + break; + } + } + if (ta1 == 0) { + /* This is sheer paranoia, but we handle it anyway */ + if (acpi_tmp7) { + printk(TPACPI_ERR + "ThinkPad ACPI EC access misbehaving, " + "falling back to ACPI TMPx access " + "mode\n"); + thermal_read_mode = TPACPI_THERMAL_ACPI_TMP07; + } else { + printk(TPACPI_ERR + "ThinkPad ACPI EC access misbehaving, " + "disabling thermal sensors access\n"); + thermal_read_mode = TPACPI_THERMAL_NONE; + } + } else { + thermal_read_mode = + (ta2 != 0) ? + TPACPI_THERMAL_TPEC_16 : TPACPI_THERMAL_TPEC_8; + } + } else if (acpi_tmp7) { + if (acpi_evalf(ec_handle, NULL, "UPDT", "qv")) { + /* 600e/x, 770e, 770x */ + thermal_read_mode = TPACPI_THERMAL_ACPI_UPDT; + } else { + /* Standard ACPI TMPx access, max 8 sensors */ + thermal_read_mode = TPACPI_THERMAL_ACPI_TMP07; + } + } else { + /* temperatures not supported on 570, G4x, R30, R31, R32 */ + thermal_read_mode = TPACPI_THERMAL_NONE; + } + + vdbg_printk(TPACPI_DBG_INIT, "thermal is %s, mode %d\n", + str_supported(thermal_read_mode != TPACPI_THERMAL_NONE), + thermal_read_mode); + + switch (thermal_read_mode) { + case TPACPI_THERMAL_TPEC_16: + res = sysfs_create_group(&tpacpi_sensors_pdev->dev.kobj, + &thermal_temp_input16_group); + if (res) + return res; + break; + case TPACPI_THERMAL_TPEC_8: + case TPACPI_THERMAL_ACPI_TMP07: + case TPACPI_THERMAL_ACPI_UPDT: + res = sysfs_create_group(&tpacpi_sensors_pdev->dev.kobj, + &thermal_temp_input8_group); + if (res) + return res; + break; + case TPACPI_THERMAL_NONE: + default: + return 1; + } + + return 0; +} + +static void thermal_exit(void) +{ + switch (thermal_read_mode) { + case TPACPI_THERMAL_TPEC_16: + sysfs_remove_group(&tpacpi_sensors_pdev->dev.kobj, + &thermal_temp_input16_group); + break; + case TPACPI_THERMAL_TPEC_8: + case TPACPI_THERMAL_ACPI_TMP07: + case TPACPI_THERMAL_ACPI_UPDT: + sysfs_remove_group(&tpacpi_sensors_pdev->dev.kobj, + &thermal_temp_input16_group); + break; + case TPACPI_THERMAL_NONE: + default: + break; + } +} + +static int thermal_read(char *p) +{ + int len = 0; + int n, i; + struct ibm_thermal_sensors_struct t; + + n = thermal_get_sensors(&t); + if (unlikely(n < 0)) + return n; + + len += sprintf(p + len, "temperatures:\t"); + + if (n > 0) { + for (i = 0; i < (n - 1); i++) + len += sprintf(p + len, "%d ", t.temp[i] / 1000); + len += sprintf(p + len, "%d\n", t.temp[i] / 1000); + } else + len += sprintf(p + len, "not supported\n"); + + return len; +} + +static struct ibm_struct thermal_driver_data = { + .name = "thermal", + .read = thermal_read, + .exit = thermal_exit, +}; + +/************************************************************************* + * EC Dump subdriver + */ + +static u8 ecdump_regs[256]; + +static int ecdump_read(char *p) +{ + int len = 0; + int i, j; + u8 v; + + len += sprintf(p + len, "EC " + " +00 +01 +02 +03 +04 +05 +06 +07" + " +08 +09 +0a +0b +0c +0d +0e +0f\n"); + for (i = 0; i < 256; i += 16) { + len += sprintf(p + len, "EC 0x%02x:", i); + for (j = 0; j < 16; j++) { + if (!acpi_ec_read(i + j, &v)) + break; + if (v != ecdump_regs[i + j]) + len += sprintf(p + len, " *%02x", v); + else + len += sprintf(p + len, " %02x", v); + ecdump_regs[i + j] = v; + } + len += sprintf(p + len, "\n"); + if (j != 16) + break; + } + + /* These are way too dangerous to advertise openly... */ +#if 0 + len += sprintf(p + len, "commands:\t0x<offset> 0x<value>" + " (<offset> is 00-ff, <value> is 00-ff)\n"); + len += sprintf(p + len, "commands:\t0x<offset> <value> " + " (<offset> is 00-ff, <value> is 0-255)\n"); +#endif + return len; +} + +static int ecdump_write(char *buf) +{ + char *cmd; + int i, v; + + while ((cmd = next_cmd(&buf))) { + if (sscanf(cmd, "0x%x 0x%x", &i, &v) == 2) { + /* i and v set */ + } else if (sscanf(cmd, "0x%x %u", &i, &v) == 2) { + /* i and v set */ + } else + return -EINVAL; + if (i >= 0 && i < 256 && v >= 0 && v < 256) { + if (!acpi_ec_write(i, v)) + return -EIO; + } else + return -EINVAL; + } + + return 0; +} + +static struct ibm_struct ecdump_driver_data = { + .name = "ecdump", + .read = ecdump_read, + .write = ecdump_write, + .flags.experimental = 1, +}; + +/************************************************************************* + * Backlight/brightness subdriver + */ + +#define TPACPI_BACKLIGHT_DEV_NAME "thinkpad_screen" + +enum { + TP_EC_BACKLIGHT = 0x31, + + /* TP_EC_BACKLIGHT bitmasks */ + TP_EC_BACKLIGHT_LVLMSK = 0x1F, + TP_EC_BACKLIGHT_CMDMSK = 0xE0, + TP_EC_BACKLIGHT_MAPSW = 0x20, +}; + +static struct backlight_device *ibm_backlight_device; +static int brightness_mode; +static unsigned int brightness_enable = 2; /* 2 = auto, 0 = no, 1 = yes */ + +static struct mutex brightness_mutex; + +/* + * ThinkPads can read brightness from two places: EC 0x31, or + * CMOS NVRAM byte 0x5E, bits 0-3. + * + * EC 0x31 has the following layout + * Bit 7: unknown function + * Bit 6: unknown function + * Bit 5: Z: honour scale changes, NZ: ignore scale changes + * Bit 4: must be set to zero to avoid problems + * Bit 3-0: backlight brightness level + * + * brightness_get_raw returns status data in the EC 0x31 layout + */ +static int brightness_get_raw(int *status) +{ + u8 lec = 0, lcmos = 0, level = 0; + + if (brightness_mode & 1) { + if (!acpi_ec_read(TP_EC_BACKLIGHT, &lec)) + return -EIO; + level = lec & TP_EC_BACKLIGHT_LVLMSK; + }; + if (brightness_mode & 2) { + lcmos = (nvram_read_byte(TP_NVRAM_ADDR_BRIGHTNESS) + & TP_NVRAM_MASK_LEVEL_BRIGHTNESS) + >> TP_NVRAM_POS_LEVEL_BRIGHTNESS; + lcmos &= (tp_features.bright_16levels)? 0x0f : 0x07; + level = lcmos; + } + + if (brightness_mode == 3) { + *status = lec; /* Prefer EC, CMOS is just a backing store */ + lec &= TP_EC_BACKLIGHT_LVLMSK; + if (lec == lcmos) + tp_warned.bright_cmos_ec_unsync = 0; + else { + if (!tp_warned.bright_cmos_ec_unsync) { + printk(TPACPI_ERR + "CMOS NVRAM (%u) and EC (%u) do not " + "agree on display brightness level\n", + (unsigned int) lcmos, + (unsigned int) lec); + tp_warned.bright_cmos_ec_unsync = 1; + } + return -EIO; + } + } else { + *status = level; + } + + return 0; +} + +/* May return EINTR which can always be mapped to ERESTARTSYS */ +static int brightness_set(int value) +{ + int cmos_cmd, inc, i, res; + int current_value; + int command_bits; + + if (value > ((tp_features.bright_16levels)? 15 : 7) || + value < 0) + return -EINVAL; + + res = mutex_lock_interruptible(&brightness_mutex); + if (res < 0) + return res; + + res = brightness_get_raw(¤t_value); + if (res < 0) + goto errout; + + command_bits = current_value & TP_EC_BACKLIGHT_CMDMSK; + current_value &= TP_EC_BACKLIGHT_LVLMSK; + + cmos_cmd = value > current_value ? + TP_CMOS_BRIGHTNESS_UP : + TP_CMOS_BRIGHTNESS_DOWN; + inc = (value > current_value)? 1 : -1; + + res = 0; + for (i = current_value; i != value; i += inc) { + if ((brightness_mode & 2) && + issue_thinkpad_cmos_command(cmos_cmd)) { + res = -EIO; + goto errout; + } + if ((brightness_mode & 1) && + !acpi_ec_write(TP_EC_BACKLIGHT, + (i + inc) | command_bits)) { + res = -EIO; + goto errout;; + } + } + +errout: + mutex_unlock(&brightness_mutex); + return res; +} + +/* sysfs backlight class ----------------------------------------------- */ + +static int brightness_update_status(struct backlight_device *bd) +{ + /* it is the backlight class's job (caller) to handle + * EINTR and other errors properly */ + return brightness_set( + (bd->props.fb_blank == FB_BLANK_UNBLANK && + bd->props.power == FB_BLANK_UNBLANK) ? + bd->props.brightness : 0); +} + +static int brightness_get(struct backlight_device *bd) +{ + int status, res; + + res = brightness_get_raw(&status); + if (res < 0) + return 0; /* FIXME: teach backlight about error handling */ + + return status & TP_EC_BACKLIGHT_LVLMSK; +} + +static struct backlight_ops ibm_backlight_data = { + .get_brightness = brightness_get, + .update_status = brightness_update_status, +}; + +/* --------------------------------------------------------------------- */ + +static int __init brightness_init(struct ibm_init_struct *iibm) +{ + int b; + + vdbg_printk(TPACPI_DBG_INIT, "initializing brightness subdriver\n"); + + mutex_init(&brightness_mutex); + + /* + * We always attempt to detect acpi support, so as to switch + * Lenovo Vista BIOS to ACPI brightness mode even if we are not + * going to publish a backlight interface + */ + b = tpacpi_check_std_acpi_brightness_support(); + if (b > 0) { + + if (acpi_video_backlight_support()) { + if (brightness_enable > 1) { + printk(TPACPI_NOTICE + "Standard ACPI backlight interface " + "available, not loading native one.\n"); + return 1; + } else if (brightness_enable == 1) { + printk(TPACPI_NOTICE + "Backlight control force enabled, even if standard " + "ACPI backlight interface is available\n"); + } + } else { + if (brightness_enable > 1) { + printk(TPACPI_NOTICE + "Standard ACPI backlight interface not " + "available, thinkpad_acpi native " + "brightness control enabled\n"); + } + } + } + + if (!brightness_enable) { + dbg_printk(TPACPI_DBG_INIT, + "brightness support disabled by " + "module parameter\n"); + return 1; + } + + if (b > 16) { + printk(TPACPI_ERR + "Unsupported brightness interface, " + "please contact %s\n", TPACPI_MAIL); + return 1; + } + if (b == 16) + tp_features.bright_16levels = 1; + + if (!brightness_mode) { + if (thinkpad_id.vendor == PCI_VENDOR_ID_LENOVO) + brightness_mode = 2; + else + brightness_mode = 3; + + dbg_printk(TPACPI_DBG_INIT, "selected brightness_mode=%d\n", + brightness_mode); + } + + if (brightness_mode > 3) + return -EINVAL; + + if (brightness_get_raw(&b) < 0) + return 1; + + if (tp_features.bright_16levels) + printk(TPACPI_INFO + "detected a 16-level brightness capable ThinkPad\n"); + + ibm_backlight_device = backlight_device_register( + TPACPI_BACKLIGHT_DEV_NAME, NULL, NULL, + &ibm_backlight_data); + if (IS_ERR(ibm_backlight_device)) { + printk(TPACPI_ERR "Could not register backlight device\n"); + return PTR_ERR(ibm_backlight_device); + } + vdbg_printk(TPACPI_DBG_INIT, "brightness is supported\n"); + + ibm_backlight_device->props.max_brightness = + (tp_features.bright_16levels)? 15 : 7; + ibm_backlight_device->props.brightness = b & TP_EC_BACKLIGHT_LVLMSK; + backlight_update_status(ibm_backlight_device); + + return 0; +} + +static void brightness_exit(void) +{ + if (ibm_backlight_device) { + vdbg_printk(TPACPI_DBG_EXIT, + "calling backlight_device_unregister()\n"); + backlight_device_unregister(ibm_backlight_device); + } +} + +static int brightness_read(char *p) +{ + int len = 0; + int level; + + level = brightness_get(NULL); + if (level < 0) { + len += sprintf(p + len, "level:\t\tunreadable\n"); + } else { + len += sprintf(p + len, "level:\t\t%d\n", level); + len += sprintf(p + len, "commands:\tup, down\n"); + len += sprintf(p + len, "commands:\tlevel <level>" + " (<level> is 0-%d)\n", + (tp_features.bright_16levels) ? 15 : 7); + } + + return len; +} + +static int brightness_write(char *buf) +{ + int level; + int rc; + char *cmd; + int max_level = (tp_features.bright_16levels) ? 15 : 7; + + level = brightness_get(NULL); + if (level < 0) + return level; + + while ((cmd = next_cmd(&buf))) { + if (strlencmp(cmd, "up") == 0) { + if (level < max_level) + level++; + } else if (strlencmp(cmd, "down") == 0) { + if (level > 0) + level--; + } else if (sscanf(cmd, "level %d", &level) == 1 && + level >= 0 && level <= max_level) { + /* new level set */ + } else + return -EINVAL; + } + + /* + * Now we know what the final level should be, so we try to set it. + * Doing it this way makes the syscall restartable in case of EINTR + */ + rc = brightness_set(level); + return (rc == -EINTR)? ERESTARTSYS : rc; +} + +static struct ibm_struct brightness_driver_data = { + .name = "brightness", + .read = brightness_read, + .write = brightness_write, + .exit = brightness_exit, +}; + +/************************************************************************* + * Volume subdriver + */ + +static int volume_offset = 0x30; + +static int volume_read(char *p) +{ + int len = 0; + u8 level; + + if (!acpi_ec_read(volume_offset, &level)) { + len += sprintf(p + len, "level:\t\tunreadable\n"); + } else { + len += sprintf(p + len, "level:\t\t%d\n", level & 0xf); + len += sprintf(p + len, "mute:\t\t%s\n", onoff(level, 6)); + len += sprintf(p + len, "commands:\tup, down, mute\n"); + len += sprintf(p + len, "commands:\tlevel <level>" + " (<level> is 0-15)\n"); + } + + return len; +} + +static int volume_write(char *buf) +{ + int cmos_cmd, inc, i; + u8 level, mute; + int new_level, new_mute; + char *cmd; + + while ((cmd = next_cmd(&buf))) { + if (!acpi_ec_read(volume_offset, &level)) + return -EIO; + new_mute = mute = level & 0x40; + new_level = level = level & 0xf; + + if (strlencmp(cmd, "up") == 0) { + if (mute) + new_mute = 0; + else + new_level = level == 15 ? 15 : level + 1; + } else if (strlencmp(cmd, "down") == 0) { + if (mute) + new_mute = 0; + else + new_level = level == 0 ? 0 : level - 1; + } else if (sscanf(cmd, "level %d", &new_level) == 1 && + new_level >= 0 && new_level <= 15) { + /* new_level set */ + } else if (strlencmp(cmd, "mute") == 0) { + new_mute = 0x40; + } else + return -EINVAL; + + if (new_level != level) { + /* mute doesn't change */ + + cmos_cmd = (new_level > level) ? + TP_CMOS_VOLUME_UP : TP_CMOS_VOLUME_DOWN; + inc = new_level > level ? 1 : -1; + + if (mute && (issue_thinkpad_cmos_command(cmos_cmd) || + !acpi_ec_write(volume_offset, level))) + return -EIO; + + for (i = level; i != new_level; i += inc) + if (issue_thinkpad_cmos_command(cmos_cmd) || + !acpi_ec_write(volume_offset, i + inc)) + return -EIO; + + if (mute && + (issue_thinkpad_cmos_command(TP_CMOS_VOLUME_MUTE) || + !acpi_ec_write(volume_offset, new_level + mute))) { + return -EIO; + } + } + + if (new_mute != mute) { + /* level doesn't change */ + + cmos_cmd = (new_mute) ? + TP_CMOS_VOLUME_MUTE : TP_CMOS_VOLUME_UP; + + if (issue_thinkpad_cmos_command(cmos_cmd) || + !acpi_ec_write(volume_offset, level + new_mute)) + return -EIO; + } + } + + return 0; +} + +static struct ibm_struct volume_driver_data = { + .name = "volume", + .read = volume_read, + .write = volume_write, +}; + +/************************************************************************* + * Fan subdriver + */ + +/* + * FAN ACCESS MODES + * + * TPACPI_FAN_RD_ACPI_GFAN: + * ACPI GFAN method: returns fan level + * + * see TPACPI_FAN_WR_ACPI_SFAN + * EC 0x2f (HFSP) not available if GFAN exists + * + * TPACPI_FAN_WR_ACPI_SFAN: + * ACPI SFAN method: sets fan level, 0 (stop) to 7 (max) + * + * EC 0x2f (HFSP) might be available *for reading*, but do not use + * it for writing. + * + * TPACPI_FAN_WR_TPEC: + * ThinkPad EC register 0x2f (HFSP): fan control loop mode + * Supported on almost all ThinkPads + * + * Fan speed changes of any sort (including those caused by the + * disengaged mode) are usually done slowly by the firmware as the + * maximum ammount of fan duty cycle change per second seems to be + * limited. + * + * Reading is not available if GFAN exists. + * Writing is not available if SFAN exists. + * + * Bits + * 7 automatic mode engaged; + * (default operation mode of the ThinkPad) + * fan level is ignored in this mode. + * 6 full speed mode (takes precedence over bit 7); + * not available on all thinkpads. May disable + * the tachometer while the fan controller ramps up + * the speed (which can take up to a few *minutes*). + * Speeds up fan to 100% duty-cycle, which is far above + * the standard RPM levels. It is not impossible that + * it could cause hardware damage. + * 5-3 unused in some models. Extra bits for fan level + * in others, but still useless as all values above + * 7 map to the same speed as level 7 in these models. + * 2-0 fan level (0..7 usually) + * 0x00 = stop + * 0x07 = max (set when temperatures critical) + * Some ThinkPads may have other levels, see + * TPACPI_FAN_WR_ACPI_FANS (X31/X40/X41) + * + * FIRMWARE BUG: on some models, EC 0x2f might not be initialized at + * boot. Apparently the EC does not intialize it, so unless ACPI DSDT + * does so, its initial value is meaningless (0x07). + * + * For firmware bugs, refer to: + * http://thinkwiki.org/wiki/Embedded_Controller_Firmware#Firmware_Issues + * + * ---- + * + * ThinkPad EC register 0x84 (LSB), 0x85 (MSB): + * Main fan tachometer reading (in RPM) + * + * This register is present on all ThinkPads with a new-style EC, and + * it is known not to be present on the A21m/e, and T22, as there is + * something else in offset 0x84 according to the ACPI DSDT. Other + * ThinkPads from this same time period (and earlier) probably lack the + * tachometer as well. + * + * Unfortunately a lot of ThinkPads with new-style ECs but whose firwmare + * was never fixed by IBM to report the EC firmware version string + * probably support the tachometer (like the early X models), so + * detecting it is quite hard. We need more data to know for sure. + * + * FIRMWARE BUG: always read 0x84 first, otherwise incorrect readings + * might result. + * + * FIRMWARE BUG: may go stale while the EC is switching to full speed + * mode. + * + * For firmware bugs, refer to: + * http://thinkwiki.org/wiki/Embedded_Controller_Firmware#Firmware_Issues + * + * TPACPI_FAN_WR_ACPI_FANS: + * ThinkPad X31, X40, X41. Not available in the X60. + * + * FANS ACPI handle: takes three arguments: low speed, medium speed, + * high speed. ACPI DSDT seems to map these three speeds to levels + * as follows: STOP LOW LOW MED MED HIGH HIGH HIGH HIGH + * (this map is stored on FAN0..FAN8 as "0,1,1,2,2,3,3,3,3") + * + * The speeds are stored on handles + * (FANA:FAN9), (FANC:FANB), (FANE:FAND). + * + * There are three default speed sets, acessible as handles: + * FS1L,FS1M,FS1H; FS2L,FS2M,FS2H; FS3L,FS3M,FS3H + * + * ACPI DSDT switches which set is in use depending on various + * factors. + * + * TPACPI_FAN_WR_TPEC is also available and should be used to + * command the fan. The X31/X40/X41 seems to have 8 fan levels, + * but the ACPI tables just mention level 7. + */ + +enum { /* Fan control constants */ + fan_status_offset = 0x2f, /* EC register 0x2f */ + fan_rpm_offset = 0x84, /* EC register 0x84: LSB, 0x85 MSB (RPM) + * 0x84 must be read before 0x85 */ + + TP_EC_FAN_FULLSPEED = 0x40, /* EC fan mode: full speed */ + TP_EC_FAN_AUTO = 0x80, /* EC fan mode: auto fan control */ + + TPACPI_FAN_LAST_LEVEL = 0x100, /* Use cached last-seen fan level */ +}; + +enum fan_status_access_mode { + TPACPI_FAN_NONE = 0, /* No fan status or control */ + TPACPI_FAN_RD_ACPI_GFAN, /* Use ACPI GFAN */ + TPACPI_FAN_RD_TPEC, /* Use ACPI EC regs 0x2f, 0x84-0x85 */ +}; + +enum fan_control_access_mode { + TPACPI_FAN_WR_NONE = 0, /* No fan control */ + TPACPI_FAN_WR_ACPI_SFAN, /* Use ACPI SFAN */ + TPACPI_FAN_WR_TPEC, /* Use ACPI EC reg 0x2f */ + TPACPI_FAN_WR_ACPI_FANS, /* Use ACPI FANS and EC reg 0x2f */ +}; + +enum fan_control_commands { + TPACPI_FAN_CMD_SPEED = 0x0001, /* speed command */ + TPACPI_FAN_CMD_LEVEL = 0x0002, /* level command */ + TPACPI_FAN_CMD_ENABLE = 0x0004, /* enable/disable cmd, + * and also watchdog cmd */ +}; + +static int fan_control_allowed; + +static enum fan_status_access_mode fan_status_access_mode; +static enum fan_control_access_mode fan_control_access_mode; +static enum fan_control_commands fan_control_commands; + +static u8 fan_control_initial_status; +static u8 fan_control_desired_level; +static u8 fan_control_resume_level; +static int fan_watchdog_maxinterval; + +static struct mutex fan_mutex; + +static void fan_watchdog_fire(struct work_struct *ignored); +static DECLARE_DELAYED_WORK(fan_watchdog_task, fan_watchdog_fire); + +TPACPI_HANDLE(fans, ec, "FANS"); /* X31, X40, X41 */ +TPACPI_HANDLE(gfan, ec, "GFAN", /* 570 */ + "\\FSPD", /* 600e/x, 770e, 770x */ + ); /* all others */ +TPACPI_HANDLE(sfan, ec, "SFAN", /* 570 */ + "JFNS", /* 770x-JL */ + ); /* all others */ + +/* + * Call with fan_mutex held + */ +static void fan_update_desired_level(u8 status) +{ + if ((status & + (TP_EC_FAN_AUTO | TP_EC_FAN_FULLSPEED)) == 0) { + if (status > 7) + fan_control_desired_level = 7; + else + fan_control_desired_level = status; + } +} + +static int fan_get_status(u8 *status) +{ + u8 s; + + /* TODO: + * Add TPACPI_FAN_RD_ACPI_FANS ? */ + + switch (fan_status_access_mode) { + case TPACPI_FAN_RD_ACPI_GFAN: + /* 570, 600e/x, 770e, 770x */ + + if (unlikely(!acpi_evalf(gfan_handle, &s, NULL, "d"))) + return -EIO; + + if (likely(status)) + *status = s & 0x07; + + break; + + case TPACPI_FAN_RD_TPEC: + /* all except 570, 600e/x, 770e, 770x */ + if (unlikely(!acpi_ec_read(fan_status_offset, &s))) + return -EIO; + + if (likely(status)) + *status = s; + + break; + + default: + return -ENXIO; + } + + return 0; +} + +static int fan_get_status_safe(u8 *status) +{ + int rc; + u8 s; + + if (mutex_lock_interruptible(&fan_mutex)) + return -ERESTARTSYS; + rc = fan_get_status(&s); + if (!rc) + fan_update_desired_level(s); + mutex_unlock(&fan_mutex); + + if (status) + *status = s; + + return rc; +} + +static int fan_get_speed(unsigned int *speed) +{ + u8 hi, lo; + + switch (fan_status_access_mode) { + case TPACPI_FAN_RD_TPEC: + /* all except 570, 600e/x, 770e, 770x */ + if (unlikely(!acpi_ec_read(fan_rpm_offset, &lo) || + !acpi_ec_read(fan_rpm_offset + 1, &hi))) + return -EIO; + + if (likely(speed)) + *speed = (hi << 8) | lo; + + break; + + default: + return -ENXIO; + } + + return 0; +} + +static int fan_set_level(int level) +{ + if (!fan_control_allowed) + return -EPERM; + + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_SFAN: + if (level >= 0 && level <= 7) { + if (!acpi_evalf(sfan_handle, NULL, NULL, "vd", level)) + return -EIO; + } else + return -EINVAL; + break; + + case TPACPI_FAN_WR_ACPI_FANS: + case TPACPI_FAN_WR_TPEC: + if (!(level & TP_EC_FAN_AUTO) && + !(level & TP_EC_FAN_FULLSPEED) && + ((level < 0) || (level > 7))) + return -EINVAL; + + /* safety net should the EC not support AUTO + * or FULLSPEED mode bits and just ignore them */ + if (level & TP_EC_FAN_FULLSPEED) + level |= 7; /* safety min speed 7 */ + else if (level & TP_EC_FAN_AUTO) + level |= 4; /* safety min speed 4 */ + + if (!acpi_ec_write(fan_status_offset, level)) + return -EIO; + else + tp_features.fan_ctrl_status_undef = 0; + break; + + default: + return -ENXIO; + } + return 0; +} + +static int fan_set_level_safe(int level) +{ + int rc; + + if (!fan_control_allowed) + return -EPERM; + + if (mutex_lock_interruptible(&fan_mutex)) + return -ERESTARTSYS; + + if (level == TPACPI_FAN_LAST_LEVEL) + level = fan_control_desired_level; + + rc = fan_set_level(level); + if (!rc) + fan_update_desired_level(level); + + mutex_unlock(&fan_mutex); + return rc; +} + +static int fan_set_enable(void) +{ + u8 s; + int rc; + + if (!fan_control_allowed) + return -EPERM; + + if (mutex_lock_interruptible(&fan_mutex)) + return -ERESTARTSYS; + + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_FANS: + case TPACPI_FAN_WR_TPEC: + rc = fan_get_status(&s); + if (rc < 0) + break; + + /* Don't go out of emergency fan mode */ + if (s != 7) { + s &= 0x07; + s |= TP_EC_FAN_AUTO | 4; /* min fan speed 4 */ + } + + if (!acpi_ec_write(fan_status_offset, s)) + rc = -EIO; + else { + tp_features.fan_ctrl_status_undef = 0; + rc = 0; + } + break; + + case TPACPI_FAN_WR_ACPI_SFAN: + rc = fan_get_status(&s); + if (rc < 0) + break; + + s &= 0x07; + + /* Set fan to at least level 4 */ + s |= 4; + + if (!acpi_evalf(sfan_handle, NULL, NULL, "vd", s)) + rc = -EIO; + else + rc = 0; + break; + + default: + rc = -ENXIO; + } + + mutex_unlock(&fan_mutex); + return rc; +} + +static int fan_set_disable(void) +{ + int rc; + + if (!fan_control_allowed) + return -EPERM; + + if (mutex_lock_interruptible(&fan_mutex)) + return -ERESTARTSYS; + + rc = 0; + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_FANS: + case TPACPI_FAN_WR_TPEC: + if (!acpi_ec_write(fan_status_offset, 0x00)) + rc = -EIO; + else { + fan_control_desired_level = 0; + tp_features.fan_ctrl_status_undef = 0; + } + break; + + case TPACPI_FAN_WR_ACPI_SFAN: + if (!acpi_evalf(sfan_handle, NULL, NULL, "vd", 0x00)) + rc = -EIO; + else + fan_control_desired_level = 0; + break; + + default: + rc = -ENXIO; + } + + + mutex_unlock(&fan_mutex); + return rc; +} + +static int fan_set_speed(int speed) +{ + int rc; + + if (!fan_control_allowed) + return -EPERM; + + if (mutex_lock_interruptible(&fan_mutex)) + return -ERESTARTSYS; + + rc = 0; + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_FANS: + if (speed >= 0 && speed <= 65535) { + if (!acpi_evalf(fans_handle, NULL, NULL, "vddd", + speed, speed, speed)) + rc = -EIO; + } else + rc = -EINVAL; + break; + + default: + rc = -ENXIO; + } + + mutex_unlock(&fan_mutex); + return rc; +} + +static void fan_watchdog_reset(void) +{ + static int fan_watchdog_active; + + if (fan_control_access_mode == TPACPI_FAN_WR_NONE) + return; + + if (fan_watchdog_active) + cancel_delayed_work(&fan_watchdog_task); + + if (fan_watchdog_maxinterval > 0 && + tpacpi_lifecycle != TPACPI_LIFE_EXITING) { + fan_watchdog_active = 1; + if (!queue_delayed_work(tpacpi_wq, &fan_watchdog_task, + msecs_to_jiffies(fan_watchdog_maxinterval + * 1000))) { + printk(TPACPI_ERR + "failed to queue the fan watchdog, " + "watchdog will not trigger\n"); + } + } else + fan_watchdog_active = 0; +} + +static void fan_watchdog_fire(struct work_struct *ignored) +{ + int rc; + + if (tpacpi_lifecycle != TPACPI_LIFE_RUNNING) + return; + + printk(TPACPI_NOTICE "fan watchdog: enabling fan\n"); + rc = fan_set_enable(); + if (rc < 0) { + printk(TPACPI_ERR "fan watchdog: error %d while enabling fan, " + "will try again later...\n", -rc); + /* reschedule for later */ + fan_watchdog_reset(); + } +} + +/* + * SYSFS fan layout: hwmon compatible (device) + * + * pwm*_enable: + * 0: "disengaged" mode + * 1: manual mode + * 2: native EC "auto" mode (recommended, hardware default) + * + * pwm*: set speed in manual mode, ignored otherwise. + * 0 is level 0; 255 is level 7. Intermediate points done with linear + * interpolation. + * + * fan*_input: tachometer reading, RPM + * + * + * SYSFS fan layout: extensions + * + * fan_watchdog (driver): + * fan watchdog interval in seconds, 0 disables (default), max 120 + */ + +/* sysfs fan pwm1_enable ----------------------------------------------- */ +static ssize_t fan_pwm1_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res, mode; + u8 status; + + res = fan_get_status_safe(&status); + if (res) + return res; + + if (unlikely(tp_features.fan_ctrl_status_undef)) { + if (status != fan_control_initial_status) { + tp_features.fan_ctrl_status_undef = 0; + } else { + /* Return most likely status. In fact, it + * might be the only possible status */ + status = TP_EC_FAN_AUTO; + } + } + + if (status & TP_EC_FAN_FULLSPEED) { + mode = 0; + } else if (status & TP_EC_FAN_AUTO) { + mode = 2; + } else + mode = 1; + + return snprintf(buf, PAGE_SIZE, "%d\n", mode); +} + +static ssize_t fan_pwm1_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + int res, level; + + if (parse_strtoul(buf, 2, &t)) + return -EINVAL; + + switch (t) { + case 0: + level = TP_EC_FAN_FULLSPEED; + break; + case 1: + level = TPACPI_FAN_LAST_LEVEL; + break; + case 2: + level = TP_EC_FAN_AUTO; + break; + case 3: + /* reserved for software-controlled auto mode */ + return -ENOSYS; + default: + return -EINVAL; + } + + res = fan_set_level_safe(level); + if (res == -ENXIO) + return -EINVAL; + else if (res < 0) + return res; + + fan_watchdog_reset(); + + return count; +} + +static struct device_attribute dev_attr_fan_pwm1_enable = + __ATTR(pwm1_enable, S_IWUSR | S_IRUGO, + fan_pwm1_enable_show, fan_pwm1_enable_store); + +/* sysfs fan pwm1 ------------------------------------------------------ */ +static ssize_t fan_pwm1_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res; + u8 status; + + res = fan_get_status_safe(&status); + if (res) + return res; + + if (unlikely(tp_features.fan_ctrl_status_undef)) { + if (status != fan_control_initial_status) { + tp_features.fan_ctrl_status_undef = 0; + } else { + status = TP_EC_FAN_AUTO; + } + } + + if ((status & + (TP_EC_FAN_AUTO | TP_EC_FAN_FULLSPEED)) != 0) + status = fan_control_desired_level; + + if (status > 7) + status = 7; + + return snprintf(buf, PAGE_SIZE, "%u\n", (status * 255) / 7); +} + +static ssize_t fan_pwm1_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long s; + int rc; + u8 status, newlevel; + + if (parse_strtoul(buf, 255, &s)) + return -EINVAL; + + /* scale down from 0-255 to 0-7 */ + newlevel = (s >> 5) & 0x07; + + if (mutex_lock_interruptible(&fan_mutex)) + return -ERESTARTSYS; + + rc = fan_get_status(&status); + if (!rc && (status & + (TP_EC_FAN_AUTO | TP_EC_FAN_FULLSPEED)) == 0) { + rc = fan_set_level(newlevel); + if (rc == -ENXIO) + rc = -EINVAL; + else if (!rc) { + fan_update_desired_level(newlevel); + fan_watchdog_reset(); + } + } + + mutex_unlock(&fan_mutex); + return (rc)? rc : count; +} + +static struct device_attribute dev_attr_fan_pwm1 = + __ATTR(pwm1, S_IWUSR | S_IRUGO, + fan_pwm1_show, fan_pwm1_store); + +/* sysfs fan fan1_input ------------------------------------------------ */ +static ssize_t fan_fan1_input_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res; + unsigned int speed; + + res = fan_get_speed(&speed); + if (res < 0) + return res; + + return snprintf(buf, PAGE_SIZE, "%u\n", speed); +} + +static struct device_attribute dev_attr_fan_fan1_input = + __ATTR(fan1_input, S_IRUGO, + fan_fan1_input_show, NULL); + +/* sysfs fan fan_watchdog (hwmon driver) ------------------------------- */ +static ssize_t fan_fan_watchdog_show(struct device_driver *drv, + char *buf) +{ + return snprintf(buf, PAGE_SIZE, "%u\n", fan_watchdog_maxinterval); +} + +static ssize_t fan_fan_watchdog_store(struct device_driver *drv, + const char *buf, size_t count) +{ + unsigned long t; + + if (parse_strtoul(buf, 120, &t)) + return -EINVAL; + + if (!fan_control_allowed) + return -EPERM; + + fan_watchdog_maxinterval = t; + fan_watchdog_reset(); + + return count; +} + +static DRIVER_ATTR(fan_watchdog, S_IWUSR | S_IRUGO, + fan_fan_watchdog_show, fan_fan_watchdog_store); + +/* --------------------------------------------------------------------- */ +static struct attribute *fan_attributes[] = { + &dev_attr_fan_pwm1_enable.attr, &dev_attr_fan_pwm1.attr, + &dev_attr_fan_fan1_input.attr, + NULL +}; + +static const struct attribute_group fan_attr_group = { + .attrs = fan_attributes, +}; + +static int __init fan_init(struct ibm_init_struct *iibm) +{ + int rc; + + vdbg_printk(TPACPI_DBG_INIT, "initializing fan subdriver\n"); + + mutex_init(&fan_mutex); + fan_status_access_mode = TPACPI_FAN_NONE; + fan_control_access_mode = TPACPI_FAN_WR_NONE; + fan_control_commands = 0; + fan_watchdog_maxinterval = 0; + tp_features.fan_ctrl_status_undef = 0; + fan_control_desired_level = 7; + + TPACPI_ACPIHANDLE_INIT(fans); + TPACPI_ACPIHANDLE_INIT(gfan); + TPACPI_ACPIHANDLE_INIT(sfan); + + if (gfan_handle) { + /* 570, 600e/x, 770e, 770x */ + fan_status_access_mode = TPACPI_FAN_RD_ACPI_GFAN; + } else { + /* all other ThinkPads: note that even old-style + * ThinkPad ECs supports the fan control register */ + if (likely(acpi_ec_read(fan_status_offset, + &fan_control_initial_status))) { + fan_status_access_mode = TPACPI_FAN_RD_TPEC; + + /* In some ThinkPads, neither the EC nor the ACPI + * DSDT initialize the fan status, and it ends up + * being set to 0x07 when it *could* be either + * 0x07 or 0x80. + * + * Enable for TP-1Y (T43), TP-78 (R51e), + * TP-76 (R52), TP-70 (T43, R52), which are known + * to be buggy. */ + if (fan_control_initial_status == 0x07) { + switch (thinkpad_id.ec_model) { + case 0x5931: /* TP-1Y */ + case 0x3837: /* TP-78 */ + case 0x3637: /* TP-76 */ + case 0x3037: /* TP-70 */ + printk(TPACPI_NOTICE + "fan_init: initial fan status " + "is unknown, assuming it is " + "in auto mode\n"); + tp_features.fan_ctrl_status_undef = 1; + ;; + } + } + } else { + printk(TPACPI_ERR + "ThinkPad ACPI EC access misbehaving, " + "fan status and control unavailable\n"); + return 1; + } + } + + if (sfan_handle) { + /* 570, 770x-JL */ + fan_control_access_mode = TPACPI_FAN_WR_ACPI_SFAN; + fan_control_commands |= + TPACPI_FAN_CMD_LEVEL | TPACPI_FAN_CMD_ENABLE; + } else { + if (!gfan_handle) { + /* gfan without sfan means no fan control */ + /* all other models implement TP EC 0x2f control */ + + if (fans_handle) { + /* X31, X40, X41 */ + fan_control_access_mode = + TPACPI_FAN_WR_ACPI_FANS; + fan_control_commands |= + TPACPI_FAN_CMD_SPEED | + TPACPI_FAN_CMD_LEVEL | + TPACPI_FAN_CMD_ENABLE; + } else { + fan_control_access_mode = TPACPI_FAN_WR_TPEC; + fan_control_commands |= + TPACPI_FAN_CMD_LEVEL | + TPACPI_FAN_CMD_ENABLE; + } + } + } + + vdbg_printk(TPACPI_DBG_INIT, "fan is %s, modes %d, %d\n", + str_supported(fan_status_access_mode != TPACPI_FAN_NONE || + fan_control_access_mode != TPACPI_FAN_WR_NONE), + fan_status_access_mode, fan_control_access_mode); + + /* fan control master switch */ + if (!fan_control_allowed) { + fan_control_access_mode = TPACPI_FAN_WR_NONE; + fan_control_commands = 0; + dbg_printk(TPACPI_DBG_INIT, + "fan control features disabled by parameter\n"); + } + + /* update fan_control_desired_level */ + if (fan_status_access_mode != TPACPI_FAN_NONE) + fan_get_status_safe(NULL); + + if (fan_status_access_mode != TPACPI_FAN_NONE || + fan_control_access_mode != TPACPI_FAN_WR_NONE) { + rc = sysfs_create_group(&tpacpi_sensors_pdev->dev.kobj, + &fan_attr_group); + if (rc < 0) + return rc; + + rc = driver_create_file(&tpacpi_hwmon_pdriver.driver, + &driver_attr_fan_watchdog); + if (rc < 0) { + sysfs_remove_group(&tpacpi_sensors_pdev->dev.kobj, + &fan_attr_group); + return rc; + } + return 0; + } else + return 1; +} + +static void fan_exit(void) +{ + vdbg_printk(TPACPI_DBG_EXIT, + "cancelling any pending fan watchdog tasks\n"); + + /* FIXME: can we really do this unconditionally? */ + sysfs_remove_group(&tpacpi_sensors_pdev->dev.kobj, &fan_attr_group); + driver_remove_file(&tpacpi_hwmon_pdriver.driver, + &driver_attr_fan_watchdog); + + cancel_delayed_work(&fan_watchdog_task); + flush_workqueue(tpacpi_wq); +} + +static void fan_suspend(pm_message_t state) +{ + int rc; + + if (!fan_control_allowed) + return; + + /* Store fan status in cache */ + fan_control_resume_level = 0; + rc = fan_get_status_safe(&fan_control_resume_level); + if (rc < 0) + printk(TPACPI_NOTICE + "failed to read fan level for later " + "restore during resume: %d\n", rc); + + /* if it is undefined, don't attempt to restore it. + * KEEP THIS LAST */ + if (tp_features.fan_ctrl_status_undef) + fan_control_resume_level = 0; +} + +static void fan_resume(void) +{ + u8 current_level = 7; + bool do_set = false; + int rc; + + /* DSDT *always* updates status on resume */ + tp_features.fan_ctrl_status_undef = 0; + + if (!fan_control_allowed || + !fan_control_resume_level || + (fan_get_status_safe(¤t_level) < 0)) + return; + + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_SFAN: + /* never decrease fan level */ + do_set = (fan_control_resume_level > current_level); + break; + case TPACPI_FAN_WR_ACPI_FANS: + case TPACPI_FAN_WR_TPEC: + /* never decrease fan level, scale is: + * TP_EC_FAN_FULLSPEED > 7 >= TP_EC_FAN_AUTO + * + * We expect the firmware to set either 7 or AUTO, but we + * handle FULLSPEED out of paranoia. + * + * So, we can safely only restore FULLSPEED or 7, anything + * else could slow the fan. Restoring AUTO is useless, at + * best that's exactly what the DSDT already set (it is the + * slower it uses). + * + * Always keep in mind that the DSDT *will* have set the + * fans to what the vendor supposes is the best level. We + * muck with it only to speed the fan up. + */ + if (fan_control_resume_level != 7 && + !(fan_control_resume_level & TP_EC_FAN_FULLSPEED)) + return; + else + do_set = !(current_level & TP_EC_FAN_FULLSPEED) && + (current_level != fan_control_resume_level); + break; + default: + return; + } + if (do_set) { + printk(TPACPI_NOTICE + "restoring fan level to 0x%02x\n", + fan_control_resume_level); + rc = fan_set_level_safe(fan_control_resume_level); + if (rc < 0) + printk(TPACPI_NOTICE + "failed to restore fan level: %d\n", rc); + } +} + +static int fan_read(char *p) +{ + int len = 0; + int rc; + u8 status; + unsigned int speed = 0; + + switch (fan_status_access_mode) { + case TPACPI_FAN_RD_ACPI_GFAN: + /* 570, 600e/x, 770e, 770x */ + rc = fan_get_status_safe(&status); + if (rc < 0) + return rc; + + len += sprintf(p + len, "status:\t\t%s\n" + "level:\t\t%d\n", + (status != 0) ? "enabled" : "disabled", status); + break; + + case TPACPI_FAN_RD_TPEC: + /* all except 570, 600e/x, 770e, 770x */ + rc = fan_get_status_safe(&status); + if (rc < 0) + return rc; + + if (unlikely(tp_features.fan_ctrl_status_undef)) { + if (status != fan_control_initial_status) + tp_features.fan_ctrl_status_undef = 0; + else + /* Return most likely status. In fact, it + * might be the only possible status */ + status = TP_EC_FAN_AUTO; + } + + len += sprintf(p + len, "status:\t\t%s\n", + (status != 0) ? "enabled" : "disabled"); + + rc = fan_get_speed(&speed); + if (rc < 0) + return rc; + + len += sprintf(p + len, "speed:\t\t%d\n", speed); + + if (status & TP_EC_FAN_FULLSPEED) + /* Disengaged mode takes precedence */ + len += sprintf(p + len, "level:\t\tdisengaged\n"); + else if (status & TP_EC_FAN_AUTO) + len += sprintf(p + len, "level:\t\tauto\n"); + else + len += sprintf(p + len, "level:\t\t%d\n", status); + break; + + case TPACPI_FAN_NONE: + default: + len += sprintf(p + len, "status:\t\tnot supported\n"); + } + + if (fan_control_commands & TPACPI_FAN_CMD_LEVEL) { + len += sprintf(p + len, "commands:\tlevel <level>"); + + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_SFAN: + len += sprintf(p + len, " (<level> is 0-7)\n"); + break; + + default: + len += sprintf(p + len, " (<level> is 0-7, " + "auto, disengaged, full-speed)\n"); + break; + } + } + + if (fan_control_commands & TPACPI_FAN_CMD_ENABLE) + len += sprintf(p + len, "commands:\tenable, disable\n" + "commands:\twatchdog <timeout> (<timeout> " + "is 0 (off), 1-120 (seconds))\n"); + + if (fan_control_commands & TPACPI_FAN_CMD_SPEED) + len += sprintf(p + len, "commands:\tspeed <speed>" + " (<speed> is 0-65535)\n"); + + return len; +} + +static int fan_write_cmd_level(const char *cmd, int *rc) +{ + int level; + + if (strlencmp(cmd, "level auto") == 0) + level = TP_EC_FAN_AUTO; + else if ((strlencmp(cmd, "level disengaged") == 0) | + (strlencmp(cmd, "level full-speed") == 0)) + level = TP_EC_FAN_FULLSPEED; + else if (sscanf(cmd, "level %d", &level) != 1) + return 0; + + *rc = fan_set_level_safe(level); + if (*rc == -ENXIO) + printk(TPACPI_ERR "level command accepted for unsupported " + "access mode %d", fan_control_access_mode); + + return 1; +} + +static int fan_write_cmd_enable(const char *cmd, int *rc) +{ + if (strlencmp(cmd, "enable") != 0) + return 0; + + *rc = fan_set_enable(); + if (*rc == -ENXIO) + printk(TPACPI_ERR "enable command accepted for unsupported " + "access mode %d", fan_control_access_mode); + + return 1; +} + +static int fan_write_cmd_disable(const char *cmd, int *rc) +{ + if (strlencmp(cmd, "disable") != 0) + return 0; + + *rc = fan_set_disable(); + if (*rc == -ENXIO) + printk(TPACPI_ERR "disable command accepted for unsupported " + "access mode %d", fan_control_access_mode); + + return 1; +} + +static int fan_write_cmd_speed(const char *cmd, int *rc) +{ + int speed; + + /* TODO: + * Support speed <low> <medium> <high> ? */ + + if (sscanf(cmd, "speed %d", &speed) != 1) + return 0; + + *rc = fan_set_speed(speed); + if (*rc == -ENXIO) + printk(TPACPI_ERR "speed command accepted for unsupported " + "access mode %d", fan_control_access_mode); + + return 1; +} + +static int fan_write_cmd_watchdog(const char *cmd, int *rc) +{ + int interval; + + if (sscanf(cmd, "watchdog %d", &interval) != 1) + return 0; + + if (interval < 0 || interval > 120) + *rc = -EINVAL; + else + fan_watchdog_maxinterval = interval; + + return 1; +} + +static int fan_write(char *buf) +{ + char *cmd; + int rc = 0; + + while (!rc && (cmd = next_cmd(&buf))) { + if (!((fan_control_commands & TPACPI_FAN_CMD_LEVEL) && + fan_write_cmd_level(cmd, &rc)) && + !((fan_control_commands & TPACPI_FAN_CMD_ENABLE) && + (fan_write_cmd_enable(cmd, &rc) || + fan_write_cmd_disable(cmd, &rc) || + fan_write_cmd_watchdog(cmd, &rc))) && + !((fan_control_commands & TPACPI_FAN_CMD_SPEED) && + fan_write_cmd_speed(cmd, &rc)) + ) + rc = -EINVAL; + else if (!rc) + fan_watchdog_reset(); + } + + return rc; +} + +static struct ibm_struct fan_driver_data = { + .name = "fan", + .read = fan_read, + .write = fan_write, + .exit = fan_exit, + .suspend = fan_suspend, + .resume = fan_resume, +}; + +/**************************************************************************** + **************************************************************************** + * + * Infrastructure + * + **************************************************************************** + ****************************************************************************/ + +/* sysfs name ---------------------------------------------------------- */ +static ssize_t thinkpad_acpi_pdev_name_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return snprintf(buf, PAGE_SIZE, "%s\n", TPACPI_NAME); +} + +static struct device_attribute dev_attr_thinkpad_acpi_pdev_name = + __ATTR(name, S_IRUGO, thinkpad_acpi_pdev_name_show, NULL); + +/* --------------------------------------------------------------------- */ + +/* /proc support */ +static struct proc_dir_entry *proc_dir; + +/* + * Module and infrastructure proble, init and exit handling + */ + +static int force_load; + +#ifdef CONFIG_THINKPAD_ACPI_DEBUG +static const char * __init str_supported(int is_supported) +{ + static char text_unsupported[] __initdata = "not supported"; + + return (is_supported)? &text_unsupported[4] : &text_unsupported[0]; +} +#endif /* CONFIG_THINKPAD_ACPI_DEBUG */ + +static void ibm_exit(struct ibm_struct *ibm) +{ + dbg_printk(TPACPI_DBG_EXIT, "removing %s\n", ibm->name); + + list_del_init(&ibm->all_drivers); + + if (ibm->flags.acpi_notify_installed) { + dbg_printk(TPACPI_DBG_EXIT, + "%s: acpi_remove_notify_handler\n", ibm->name); + BUG_ON(!ibm->acpi); + acpi_remove_notify_handler(*ibm->acpi->handle, + ibm->acpi->type, + dispatch_acpi_notify); + ibm->flags.acpi_notify_installed = 0; + ibm->flags.acpi_notify_installed = 0; + } + + if (ibm->flags.proc_created) { + dbg_printk(TPACPI_DBG_EXIT, + "%s: remove_proc_entry\n", ibm->name); + remove_proc_entry(ibm->name, proc_dir); + ibm->flags.proc_created = 0; + } + + if (ibm->flags.acpi_driver_registered) { + dbg_printk(TPACPI_DBG_EXIT, + "%s: acpi_bus_unregister_driver\n", ibm->name); + BUG_ON(!ibm->acpi); + acpi_bus_unregister_driver(ibm->acpi->driver); + kfree(ibm->acpi->driver); + ibm->acpi->driver = NULL; + ibm->flags.acpi_driver_registered = 0; + } + + if (ibm->flags.init_called && ibm->exit) { + ibm->exit(); + ibm->flags.init_called = 0; + } + + dbg_printk(TPACPI_DBG_INIT, "finished removing %s\n", ibm->name); +} + +static int __init ibm_init(struct ibm_init_struct *iibm) +{ + int ret; + struct ibm_struct *ibm = iibm->data; + struct proc_dir_entry *entry; + + BUG_ON(ibm == NULL); + + INIT_LIST_HEAD(&ibm->all_drivers); + + if (ibm->flags.experimental && !experimental) + return 0; + + dbg_printk(TPACPI_DBG_INIT, + "probing for %s\n", ibm->name); + + if (iibm->init) { + ret = iibm->init(iibm); + if (ret > 0) + return 0; /* probe failed */ + if (ret) + return ret; + + ibm->flags.init_called = 1; + } + + if (ibm->acpi) { + if (ibm->acpi->hid) { + ret = register_tpacpi_subdriver(ibm); + if (ret) + goto err_out; + } + + if (ibm->acpi->notify) { + ret = setup_acpi_notify(ibm); + if (ret == -ENODEV) { + printk(TPACPI_NOTICE "disabling subdriver %s\n", + ibm->name); + ret = 0; + goto err_out; + } + if (ret < 0) + goto err_out; + } + } + + dbg_printk(TPACPI_DBG_INIT, + "%s installed\n", ibm->name); + + if (ibm->read) { + entry = create_proc_entry(ibm->name, + S_IFREG | S_IRUGO | S_IWUSR, + proc_dir); + if (!entry) { + printk(TPACPI_ERR "unable to create proc entry %s\n", + ibm->name); + ret = -ENODEV; + goto err_out; + } + entry->owner = THIS_MODULE; + entry->data = ibm; + entry->read_proc = &dispatch_procfs_read; + if (ibm->write) + entry->write_proc = &dispatch_procfs_write; + ibm->flags.proc_created = 1; + } + + list_add_tail(&ibm->all_drivers, &tpacpi_all_drivers); + + return 0; + +err_out: + dbg_printk(TPACPI_DBG_INIT, + "%s: at error exit path with result %d\n", + ibm->name, ret); + + ibm_exit(ibm); + return (ret < 0)? ret : 0; +} + +/* Probing */ + +/* returns 0 - probe ok, or < 0 - probe error. + * Probe ok doesn't mean thinkpad found. + * On error, kfree() cleanup on tp->* is not performed, caller must do it */ +static int __must_check __init get_thinkpad_model_data( + struct thinkpad_id_data *tp) +{ + const struct dmi_device *dev = NULL; + char ec_fw_string[18]; + char const *s; + + if (!tp) + return -EINVAL; + + memset(tp, 0, sizeof(*tp)); + + if (dmi_name_in_vendors("IBM")) + tp->vendor = PCI_VENDOR_ID_IBM; + else if (dmi_name_in_vendors("LENOVO")) + tp->vendor = PCI_VENDOR_ID_LENOVO; + else + return 0; + + s = dmi_get_system_info(DMI_BIOS_VERSION); + tp->bios_version_str = kstrdup(s, GFP_KERNEL); + if (s && !tp->bios_version_str) + return -ENOMEM; + if (!tp->bios_version_str) + return 0; + tp->bios_model = tp->bios_version_str[0] + | (tp->bios_version_str[1] << 8); + + /* + * ThinkPad T23 or newer, A31 or newer, R50e or newer, + * X32 or newer, all Z series; Some models must have an + * up-to-date BIOS or they will not be detected. + * + * See http://thinkwiki.org/wiki/List_of_DMI_IDs + */ + while ((dev = dmi_find_device(DMI_DEV_TYPE_OEM_STRING, NULL, dev))) { + if (sscanf(dev->name, + "IBM ThinkPad Embedded Controller -[%17c", + ec_fw_string) == 1) { + ec_fw_string[sizeof(ec_fw_string) - 1] = 0; + ec_fw_string[strcspn(ec_fw_string, " ]")] = 0; + + tp->ec_version_str = kstrdup(ec_fw_string, GFP_KERNEL); + if (!tp->ec_version_str) + return -ENOMEM; + tp->ec_model = ec_fw_string[0] + | (ec_fw_string[1] << 8); + break; + } + } + + s = dmi_get_system_info(DMI_PRODUCT_VERSION); + if (s && !strnicmp(s, "ThinkPad", 8)) { + tp->model_str = kstrdup(s, GFP_KERNEL); + if (!tp->model_str) + return -ENOMEM; + } + + s = dmi_get_system_info(DMI_PRODUCT_NAME); + tp->nummodel_str = kstrdup(s, GFP_KERNEL); + if (s && !tp->nummodel_str) + return -ENOMEM; + + return 0; +} + +static int __init probe_for_thinkpad(void) +{ + int is_thinkpad; + + if (acpi_disabled) + return -ENODEV; + + /* + * Non-ancient models have better DMI tagging, but very old models + * don't. + */ + is_thinkpad = (thinkpad_id.model_str != NULL); + + /* ec is required because many other handles are relative to it */ + TPACPI_ACPIHANDLE_INIT(ec); + if (!ec_handle) { + if (is_thinkpad) + printk(TPACPI_ERR + "Not yet supported ThinkPad detected!\n"); + return -ENODEV; + } + + /* + * Risks a regression on very old machines, but reduces potential + * false positives a damn great deal + */ + if (!is_thinkpad) + is_thinkpad = (thinkpad_id.vendor == PCI_VENDOR_ID_IBM); + + if (!is_thinkpad && !force_load) + return -ENODEV; + + return 0; +} + + +/* Module init, exit, parameters */ + +static struct ibm_init_struct ibms_init[] __initdata = { + { + .init = thinkpad_acpi_driver_init, + .data = &thinkpad_acpi_driver_data, + }, + { + .init = hotkey_init, + .data = &hotkey_driver_data, + }, + { + .init = bluetooth_init, + .data = &bluetooth_driver_data, + }, + { + .init = wan_init, + .data = &wan_driver_data, + }, +#ifdef CONFIG_THINKPAD_ACPI_VIDEO + { + .init = video_init, + .data = &video_driver_data, + }, +#endif + { + .init = light_init, + .data = &light_driver_data, + }, +#ifdef CONFIG_THINKPAD_ACPI_DOCK + { + .init = dock_init, + .data = &dock_driver_data[0], + }, + { + .init = dock_init2, + .data = &dock_driver_data[1], + }, +#endif +#ifdef CONFIG_THINKPAD_ACPI_BAY + { + .init = bay_init, + .data = &bay_driver_data, + }, +#endif + { + .init = cmos_init, + .data = &cmos_driver_data, + }, + { + .init = led_init, + .data = &led_driver_data, + }, + { + .init = beep_init, + .data = &beep_driver_data, + }, + { + .init = thermal_init, + .data = &thermal_driver_data, + }, + { + .data = &ecdump_driver_data, + }, + { + .init = brightness_init, + .data = &brightness_driver_data, + }, + { + .data = &volume_driver_data, + }, + { + .init = fan_init, + .data = &fan_driver_data, + }, +}; + +static int __init set_ibm_param(const char *val, struct kernel_param *kp) +{ + unsigned int i; + struct ibm_struct *ibm; + + if (!kp || !kp->name || !val) + return -EINVAL; + + for (i = 0; i < ARRAY_SIZE(ibms_init); i++) { + ibm = ibms_init[i].data; + WARN_ON(ibm == NULL); + + if (!ibm || !ibm->name) + continue; + + if (strcmp(ibm->name, kp->name) == 0 && ibm->write) { + if (strlen(val) > sizeof(ibms_init[i].param) - 2) + return -ENOSPC; + strcpy(ibms_init[i].param, val); + strcat(ibms_init[i].param, ","); + return 0; + } + } + + return -EINVAL; +} + +module_param(experimental, int, 0); +MODULE_PARM_DESC(experimental, + "Enables experimental features when non-zero"); + +module_param_named(debug, dbg_level, uint, 0); +MODULE_PARM_DESC(debug, "Sets debug level bit-mask"); + +module_param(force_load, bool, 0); +MODULE_PARM_DESC(force_load, + "Attempts to load the driver even on a " + "mis-identified ThinkPad when true"); + +module_param_named(fan_control, fan_control_allowed, bool, 0); +MODULE_PARM_DESC(fan_control, + "Enables setting fan parameters features when true"); + +module_param_named(brightness_mode, brightness_mode, int, 0); +MODULE_PARM_DESC(brightness_mode, + "Selects brightness control strategy: " + "0=auto, 1=EC, 2=CMOS, 3=both"); + +module_param(brightness_enable, uint, 0); +MODULE_PARM_DESC(brightness_enable, + "Enables backlight control when 1, disables when 0"); + +module_param(hotkey_report_mode, uint, 0); +MODULE_PARM_DESC(hotkey_report_mode, + "used for backwards compatibility with userspace, " + "see documentation"); + +#define TPACPI_PARAM(feature) \ + module_param_call(feature, set_ibm_param, NULL, NULL, 0); \ + MODULE_PARM_DESC(feature, "Simulates thinkpad-acpi procfs command " \ + "at module load, see documentation") + +TPACPI_PARAM(hotkey); +TPACPI_PARAM(bluetooth); +TPACPI_PARAM(video); +TPACPI_PARAM(light); +#ifdef CONFIG_THINKPAD_ACPI_DOCK +TPACPI_PARAM(dock); +#endif +#ifdef CONFIG_THINKPAD_ACPI_BAY +TPACPI_PARAM(bay); +#endif /* CONFIG_THINKPAD_ACPI_BAY */ +TPACPI_PARAM(cmos); +TPACPI_PARAM(led); +TPACPI_PARAM(beep); +TPACPI_PARAM(ecdump); +TPACPI_PARAM(brightness); +TPACPI_PARAM(volume); +TPACPI_PARAM(fan); + +static void thinkpad_acpi_module_exit(void) +{ + struct ibm_struct *ibm, *itmp; + + tpacpi_lifecycle = TPACPI_LIFE_EXITING; + + list_for_each_entry_safe_reverse(ibm, itmp, + &tpacpi_all_drivers, + all_drivers) { + ibm_exit(ibm); + } + + dbg_printk(TPACPI_DBG_INIT, "finished subdriver exit path...\n"); + + if (tpacpi_inputdev) { + if (tp_features.input_device_registered) + input_unregister_device(tpacpi_inputdev); + else + input_free_device(tpacpi_inputdev); + } + + if (tpacpi_hwmon) + hwmon_device_unregister(tpacpi_hwmon); + + if (tp_features.sensors_pdev_attrs_registered) + device_remove_file(&tpacpi_sensors_pdev->dev, + &dev_attr_thinkpad_acpi_pdev_name); + if (tpacpi_sensors_pdev) + platform_device_unregister(tpacpi_sensors_pdev); + if (tpacpi_pdev) + platform_device_unregister(tpacpi_pdev); + + if (tp_features.sensors_pdrv_attrs_registered) + tpacpi_remove_driver_attributes(&tpacpi_hwmon_pdriver.driver); + if (tp_features.platform_drv_attrs_registered) + tpacpi_remove_driver_attributes(&tpacpi_pdriver.driver); + + if (tp_features.sensors_pdrv_registered) + platform_driver_unregister(&tpacpi_hwmon_pdriver); + + if (tp_features.platform_drv_registered) + platform_driver_unregister(&tpacpi_pdriver); + + if (proc_dir) + remove_proc_entry(TPACPI_PROC_DIR, acpi_root_dir); + + if (tpacpi_wq) + destroy_workqueue(tpacpi_wq); + + kfree(thinkpad_id.bios_version_str); + kfree(thinkpad_id.ec_version_str); + kfree(thinkpad_id.model_str); +} + + +static int __init thinkpad_acpi_module_init(void) +{ + int ret, i; + + tpacpi_lifecycle = TPACPI_LIFE_INIT; + + /* Parameter checking */ + if (hotkey_report_mode > 2) + return -EINVAL; + + /* Driver-level probe */ + + ret = get_thinkpad_model_data(&thinkpad_id); + if (ret) { + printk(TPACPI_ERR + "unable to get DMI data: %d\n", ret); + thinkpad_acpi_module_exit(); + return ret; + } + ret = probe_for_thinkpad(); + if (ret) { + thinkpad_acpi_module_exit(); + return ret; + } + + /* Driver initialization */ + + TPACPI_ACPIHANDLE_INIT(ecrd); + TPACPI_ACPIHANDLE_INIT(ecwr); + + tpacpi_wq = create_singlethread_workqueue(TPACPI_WORKQUEUE_NAME); + if (!tpacpi_wq) { + thinkpad_acpi_module_exit(); + return -ENOMEM; + } + + proc_dir = proc_mkdir(TPACPI_PROC_DIR, acpi_root_dir); + if (!proc_dir) { + printk(TPACPI_ERR + "unable to create proc dir " TPACPI_PROC_DIR); + thinkpad_acpi_module_exit(); + return -ENODEV; + } + proc_dir->owner = THIS_MODULE; + + ret = platform_driver_register(&tpacpi_pdriver); + if (ret) { + printk(TPACPI_ERR + "unable to register main platform driver\n"); + thinkpad_acpi_module_exit(); + return ret; + } + tp_features.platform_drv_registered = 1; + + ret = platform_driver_register(&tpacpi_hwmon_pdriver); + if (ret) { + printk(TPACPI_ERR + "unable to register hwmon platform driver\n"); + thinkpad_acpi_module_exit(); + return ret; + } + tp_features.sensors_pdrv_registered = 1; + + ret = tpacpi_create_driver_attributes(&tpacpi_pdriver.driver); + if (!ret) { + tp_features.platform_drv_attrs_registered = 1; + ret = tpacpi_create_driver_attributes( + &tpacpi_hwmon_pdriver.driver); + } + if (ret) { + printk(TPACPI_ERR + "unable to create sysfs driver attributes\n"); + thinkpad_acpi_module_exit(); + return ret; + } + tp_features.sensors_pdrv_attrs_registered = 1; + + + /* Device initialization */ + tpacpi_pdev = platform_device_register_simple(TPACPI_DRVR_NAME, -1, + NULL, 0); + if (IS_ERR(tpacpi_pdev)) { + ret = PTR_ERR(tpacpi_pdev); + tpacpi_pdev = NULL; + printk(TPACPI_ERR "unable to register platform device\n"); + thinkpad_acpi_module_exit(); + return ret; + } + tpacpi_sensors_pdev = platform_device_register_simple( + TPACPI_HWMON_DRVR_NAME, + -1, NULL, 0); + if (IS_ERR(tpacpi_sensors_pdev)) { + ret = PTR_ERR(tpacpi_sensors_pdev); + tpacpi_sensors_pdev = NULL; + printk(TPACPI_ERR + "unable to register hwmon platform device\n"); + thinkpad_acpi_module_exit(); + return ret; + } + ret = device_create_file(&tpacpi_sensors_pdev->dev, + &dev_attr_thinkpad_acpi_pdev_name); + if (ret) { + printk(TPACPI_ERR + "unable to create sysfs hwmon device attributes\n"); + thinkpad_acpi_module_exit(); + return ret; + } + tp_features.sensors_pdev_attrs_registered = 1; + tpacpi_hwmon = hwmon_device_register(&tpacpi_sensors_pdev->dev); + if (IS_ERR(tpacpi_hwmon)) { + ret = PTR_ERR(tpacpi_hwmon); + tpacpi_hwmon = NULL; + printk(TPACPI_ERR "unable to register hwmon device\n"); + thinkpad_acpi_module_exit(); + return ret; + } + mutex_init(&tpacpi_inputdev_send_mutex); + tpacpi_inputdev = input_allocate_device(); + if (!tpacpi_inputdev) { + printk(TPACPI_ERR "unable to allocate input device\n"); + thinkpad_acpi_module_exit(); + return -ENOMEM; + } else { + /* Prepare input device, but don't register */ + tpacpi_inputdev->name = "ThinkPad Extra Buttons"; + tpacpi_inputdev->phys = TPACPI_DRVR_NAME "/input0"; + tpacpi_inputdev->id.bustype = BUS_HOST; + tpacpi_inputdev->id.vendor = (thinkpad_id.vendor) ? + thinkpad_id.vendor : + PCI_VENDOR_ID_IBM; + tpacpi_inputdev->id.product = TPACPI_HKEY_INPUT_PRODUCT; + tpacpi_inputdev->id.version = TPACPI_HKEY_INPUT_VERSION; + } + for (i = 0; i < ARRAY_SIZE(ibms_init); i++) { + ret = ibm_init(&ibms_init[i]); + if (ret >= 0 && *ibms_init[i].param) + ret = ibms_init[i].data->write(ibms_init[i].param); + if (ret < 0) { + thinkpad_acpi_module_exit(); + return ret; + } + } + ret = input_register_device(tpacpi_inputdev); + if (ret < 0) { + printk(TPACPI_ERR "unable to register input device\n"); + thinkpad_acpi_module_exit(); + return ret; + } else { + tp_features.input_device_registered = 1; + } + + tpacpi_lifecycle = TPACPI_LIFE_RUNNING; + return 0; +} + +/* Please remove this in year 2009 */ +MODULE_ALIAS("ibm_acpi"); + +MODULE_ALIAS(TPACPI_DRVR_SHORTNAME); + +/* + * DMI matching for module autoloading + * + * See http://thinkwiki.org/wiki/List_of_DMI_IDs + * See http://thinkwiki.org/wiki/BIOS_Upgrade_Downloads + * + * Only models listed in thinkwiki will be supported, so add yours + * if it is not there yet. + */ +#define IBM_BIOS_MODULE_ALIAS(__type) \ + MODULE_ALIAS("dmi:bvnIBM:bvr" __type "ET??WW") + +/* Non-ancient thinkpads */ +MODULE_ALIAS("dmi:bvnIBM:*:svnIBM:*:pvrThinkPad*:rvnIBM:*"); +MODULE_ALIAS("dmi:bvnLENOVO:*:svnLENOVO:*:pvrThinkPad*:rvnLENOVO:*"); + +/* Ancient thinkpad BIOSes have to be identified by + * BIOS type or model number, and there are far less + * BIOS types than model numbers... */ +IBM_BIOS_MODULE_ALIAS("I[B,D,H,I,M,N,O,T,W,V,Y,Z]"); +IBM_BIOS_MODULE_ALIAS("1[0,3,6,8,A-G,I,K,M-P,S,T]"); +IBM_BIOS_MODULE_ALIAS("K[U,X-Z]"); + +MODULE_AUTHOR("Borislav Deianov, Henrique de Moraes Holschuh"); +MODULE_DESCRIPTION(TPACPI_DESC); +MODULE_VERSION(TPACPI_VERSION); +MODULE_LICENSE("GPL"); + +module_init(thinkpad_acpi_module_init); +module_exit(thinkpad_acpi_module_exit); diff --git a/drivers/platform/x86/toshiba_acpi.c b/drivers/platform/x86/toshiba_acpi.c new file mode 100644 index 000000000000..40e60fc2e596 --- /dev/null +++ b/drivers/platform/x86/toshiba_acpi.c @@ -0,0 +1,863 @@ +/* + * toshiba_acpi.c - Toshiba Laptop ACPI Extras + * + * + * Copyright (C) 2002-2004 John Belmonte + * Copyright (C) 2008 Philip Langdale + * + * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * + * The devolpment page for this driver is located at + * http://memebeam.org/toys/ToshibaAcpiDriver. + * + * Credits: + * Jonathan A. Buzzard - Toshiba HCI info, and critical tips on reverse + * engineering the Windows drivers + * Yasushi Nagato - changes for linux kernel 2.4 -> 2.5 + * Rob Miller - TV out and hotkeys help + * + * + * TODO + * + */ + +#define TOSHIBA_ACPI_VERSION "0.19" +#define PROC_INTERFACE_VERSION 1 + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/types.h> +#include <linux/proc_fs.h> +#include <linux/backlight.h> +#include <linux/platform_device.h> +#include <linux/rfkill.h> +#include <linux/input-polldev.h> + +#include <asm/uaccess.h> + +#include <acpi/acpi_drivers.h> + +MODULE_AUTHOR("John Belmonte"); +MODULE_DESCRIPTION("Toshiba Laptop ACPI Extras Driver"); +MODULE_LICENSE("GPL"); + +#define MY_LOGPREFIX "toshiba_acpi: " +#define MY_ERR KERN_ERR MY_LOGPREFIX +#define MY_NOTICE KERN_NOTICE MY_LOGPREFIX +#define MY_INFO KERN_INFO MY_LOGPREFIX + +/* Toshiba ACPI method paths */ +#define METHOD_LCD_BRIGHTNESS "\\_SB_.PCI0.VGA_.LCD_._BCM" +#define METHOD_HCI_1 "\\_SB_.VALD.GHCI" +#define METHOD_HCI_2 "\\_SB_.VALZ.GHCI" +#define METHOD_VIDEO_OUT "\\_SB_.VALX.DSSX" + +/* Toshiba HCI interface definitions + * + * HCI is Toshiba's "Hardware Control Interface" which is supposed to + * be uniform across all their models. Ideally we would just call + * dedicated ACPI methods instead of using this primitive interface. + * However the ACPI methods seem to be incomplete in some areas (for + * example they allow setting, but not reading, the LCD brightness value), + * so this is still useful. + */ + +#define HCI_WORDS 6 + +/* operations */ +#define HCI_SET 0xff00 +#define HCI_GET 0xfe00 + +/* return codes */ +#define HCI_SUCCESS 0x0000 +#define HCI_FAILURE 0x1000 +#define HCI_NOT_SUPPORTED 0x8000 +#define HCI_EMPTY 0x8c00 + +/* registers */ +#define HCI_FAN 0x0004 +#define HCI_SYSTEM_EVENT 0x0016 +#define HCI_VIDEO_OUT 0x001c +#define HCI_HOTKEY_EVENT 0x001e +#define HCI_LCD_BRIGHTNESS 0x002a +#define HCI_WIRELESS 0x0056 + +/* field definitions */ +#define HCI_LCD_BRIGHTNESS_BITS 3 +#define HCI_LCD_BRIGHTNESS_SHIFT (16-HCI_LCD_BRIGHTNESS_BITS) +#define HCI_LCD_BRIGHTNESS_LEVELS (1 << HCI_LCD_BRIGHTNESS_BITS) +#define HCI_VIDEO_OUT_LCD 0x1 +#define HCI_VIDEO_OUT_CRT 0x2 +#define HCI_VIDEO_OUT_TV 0x4 +#define HCI_WIRELESS_KILL_SWITCH 0x01 +#define HCI_WIRELESS_BT_PRESENT 0x0f +#define HCI_WIRELESS_BT_ATTACH 0x40 +#define HCI_WIRELESS_BT_POWER 0x80 + +static const struct acpi_device_id toshiba_device_ids[] = { + {"TOS6200", 0}, + {"TOS6208", 0}, + {"TOS1900", 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, toshiba_device_ids); + +/* utility + */ + +static __inline__ void _set_bit(u32 * word, u32 mask, int value) +{ + *word = (*word & ~mask) | (mask * value); +} + +/* acpi interface wrappers + */ + +static int is_valid_acpi_path(const char *methodName) +{ + acpi_handle handle; + acpi_status status; + + status = acpi_get_handle(NULL, (char *)methodName, &handle); + return !ACPI_FAILURE(status); +} + +static int write_acpi_int(const char *methodName, int val) +{ + struct acpi_object_list params; + union acpi_object in_objs[1]; + acpi_status status; + + params.count = ARRAY_SIZE(in_objs); + params.pointer = in_objs; + in_objs[0].type = ACPI_TYPE_INTEGER; + in_objs[0].integer.value = val; + + status = acpi_evaluate_object(NULL, (char *)methodName, ¶ms, NULL); + return (status == AE_OK); +} + +#if 0 +static int read_acpi_int(const char *methodName, int *pVal) +{ + struct acpi_buffer results; + union acpi_object out_objs[1]; + acpi_status status; + + results.length = sizeof(out_objs); + results.pointer = out_objs; + + status = acpi_evaluate_object(0, (char *)methodName, 0, &results); + *pVal = out_objs[0].integer.value; + + return (status == AE_OK) && (out_objs[0].type == ACPI_TYPE_INTEGER); +} +#endif + +static const char *method_hci /*= 0*/ ; + +/* Perform a raw HCI call. Here we don't care about input or output buffer + * format. + */ +static acpi_status hci_raw(const u32 in[HCI_WORDS], u32 out[HCI_WORDS]) +{ + struct acpi_object_list params; + union acpi_object in_objs[HCI_WORDS]; + struct acpi_buffer results; + union acpi_object out_objs[HCI_WORDS + 1]; + acpi_status status; + int i; + + params.count = HCI_WORDS; + params.pointer = in_objs; + for (i = 0; i < HCI_WORDS; ++i) { + in_objs[i].type = ACPI_TYPE_INTEGER; + in_objs[i].integer.value = in[i]; + } + + results.length = sizeof(out_objs); + results.pointer = out_objs; + + status = acpi_evaluate_object(NULL, (char *)method_hci, ¶ms, + &results); + if ((status == AE_OK) && (out_objs->package.count <= HCI_WORDS)) { + for (i = 0; i < out_objs->package.count; ++i) { + out[i] = out_objs->package.elements[i].integer.value; + } + } + + return status; +} + +/* common hci tasks (get or set one or two value) + * + * In addition to the ACPI status, the HCI system returns a result which + * may be useful (such as "not supported"). + */ + +static acpi_status hci_write1(u32 reg, u32 in1, u32 * result) +{ + u32 in[HCI_WORDS] = { HCI_SET, reg, in1, 0, 0, 0 }; + u32 out[HCI_WORDS]; + acpi_status status = hci_raw(in, out); + *result = (status == AE_OK) ? out[0] : HCI_FAILURE; + return status; +} + +static acpi_status hci_read1(u32 reg, u32 * out1, u32 * result) +{ + u32 in[HCI_WORDS] = { HCI_GET, reg, 0, 0, 0, 0 }; + u32 out[HCI_WORDS]; + acpi_status status = hci_raw(in, out); + *out1 = out[2]; + *result = (status == AE_OK) ? out[0] : HCI_FAILURE; + return status; +} + +static acpi_status hci_write2(u32 reg, u32 in1, u32 in2, u32 *result) +{ + u32 in[HCI_WORDS] = { HCI_SET, reg, in1, in2, 0, 0 }; + u32 out[HCI_WORDS]; + acpi_status status = hci_raw(in, out); + *result = (status == AE_OK) ? out[0] : HCI_FAILURE; + return status; +} + +static acpi_status hci_read2(u32 reg, u32 *out1, u32 *out2, u32 *result) +{ + u32 in[HCI_WORDS] = { HCI_GET, reg, *out1, *out2, 0, 0 }; + u32 out[HCI_WORDS]; + acpi_status status = hci_raw(in, out); + *out1 = out[2]; + *out2 = out[3]; + *result = (status == AE_OK) ? out[0] : HCI_FAILURE; + return status; +} + +struct toshiba_acpi_dev { + struct platform_device *p_dev; + struct rfkill *rfk_dev; + struct input_polled_dev *poll_dev; + + const char *bt_name; + const char *rfk_name; + + bool last_rfk_state; + + struct mutex mutex; +}; + +static struct toshiba_acpi_dev toshiba_acpi = { + .bt_name = "Toshiba Bluetooth", + .rfk_name = "Toshiba RFKill Switch", + .last_rfk_state = false, +}; + +/* Bluetooth rfkill handlers */ + +static u32 hci_get_bt_present(bool *present) +{ + u32 hci_result; + u32 value, value2; + + value = 0; + value2 = 0; + hci_read2(HCI_WIRELESS, &value, &value2, &hci_result); + if (hci_result == HCI_SUCCESS) + *present = (value & HCI_WIRELESS_BT_PRESENT) ? true : false; + + return hci_result; +} + +static u32 hci_get_bt_on(bool *on) +{ + u32 hci_result; + u32 value, value2; + + value = 0; + value2 = 0x0001; + hci_read2(HCI_WIRELESS, &value, &value2, &hci_result); + if (hci_result == HCI_SUCCESS) + *on = (value & HCI_WIRELESS_BT_POWER) && + (value & HCI_WIRELESS_BT_ATTACH); + + return hci_result; +} + +static u32 hci_get_radio_state(bool *radio_state) +{ + u32 hci_result; + u32 value, value2; + + value = 0; + value2 = 0x0001; + hci_read2(HCI_WIRELESS, &value, &value2, &hci_result); + + *radio_state = value & HCI_WIRELESS_KILL_SWITCH; + return hci_result; +} + +static int bt_rfkill_toggle_radio(void *data, enum rfkill_state state) +{ + u32 result1, result2; + u32 value; + bool radio_state; + struct toshiba_acpi_dev *dev = data; + + value = (state == RFKILL_STATE_UNBLOCKED); + + if (hci_get_radio_state(&radio_state) != HCI_SUCCESS) + return -EFAULT; + + switch (state) { + case RFKILL_STATE_UNBLOCKED: + if (!radio_state) + return -EPERM; + break; + case RFKILL_STATE_SOFT_BLOCKED: + break; + default: + return -EINVAL; + } + + mutex_lock(&dev->mutex); + hci_write2(HCI_WIRELESS, value, HCI_WIRELESS_BT_POWER, &result1); + hci_write2(HCI_WIRELESS, value, HCI_WIRELESS_BT_ATTACH, &result2); + mutex_unlock(&dev->mutex); + + if (result1 != HCI_SUCCESS || result2 != HCI_SUCCESS) + return -EFAULT; + + return 0; +} + +static void bt_poll_rfkill(struct input_polled_dev *poll_dev) +{ + bool state_changed; + bool new_rfk_state; + bool value; + u32 hci_result; + struct toshiba_acpi_dev *dev = poll_dev->private; + + hci_result = hci_get_radio_state(&value); + if (hci_result != HCI_SUCCESS) + return; /* Can't do anything useful */ + + new_rfk_state = value; + + mutex_lock(&dev->mutex); + state_changed = new_rfk_state != dev->last_rfk_state; + dev->last_rfk_state = new_rfk_state; + mutex_unlock(&dev->mutex); + + if (unlikely(state_changed)) { + rfkill_force_state(dev->rfk_dev, + new_rfk_state ? + RFKILL_STATE_SOFT_BLOCKED : + RFKILL_STATE_HARD_BLOCKED); + input_report_switch(poll_dev->input, SW_RFKILL_ALL, + new_rfk_state); + input_sync(poll_dev->input); + } +} + +static struct proc_dir_entry *toshiba_proc_dir /*= 0*/ ; +static struct backlight_device *toshiba_backlight_device; +static int force_fan; +static int last_key_event; +static int key_event_valid; + +typedef struct _ProcItem { + const char *name; + char *(*read_func) (char *); + unsigned long (*write_func) (const char *, unsigned long); +} ProcItem; + +/* proc file handlers + */ + +static int +dispatch_read(char *page, char **start, off_t off, int count, int *eof, + ProcItem * item) +{ + char *p = page; + int len; + + if (off == 0) + p = item->read_func(p); + + /* ISSUE: I don't understand this code */ + len = (p - page); + if (len <= off + count) + *eof = 1; + *start = page + off; + len -= off; + if (len > count) + len = count; + if (len < 0) + len = 0; + return len; +} + +static int +dispatch_write(struct file *file, const char __user * buffer, + unsigned long count, ProcItem * item) +{ + int result; + char *tmp_buffer; + + /* Arg buffer points to userspace memory, which can't be accessed + * directly. Since we're making a copy, zero-terminate the + * destination so that sscanf can be used on it safely. + */ + tmp_buffer = kmalloc(count + 1, GFP_KERNEL); + if (!tmp_buffer) + return -ENOMEM; + + if (copy_from_user(tmp_buffer, buffer, count)) { + result = -EFAULT; + } else { + tmp_buffer[count] = 0; + result = item->write_func(tmp_buffer, count); + } + kfree(tmp_buffer); + return result; +} + +static int get_lcd(struct backlight_device *bd) +{ + u32 hci_result; + u32 value; + + hci_read1(HCI_LCD_BRIGHTNESS, &value, &hci_result); + if (hci_result == HCI_SUCCESS) { + return (value >> HCI_LCD_BRIGHTNESS_SHIFT); + } else + return -EFAULT; +} + +static char *read_lcd(char *p) +{ + int value = get_lcd(NULL); + + if (value >= 0) { + p += sprintf(p, "brightness: %d\n", value); + p += sprintf(p, "brightness_levels: %d\n", + HCI_LCD_BRIGHTNESS_LEVELS); + } else { + printk(MY_ERR "Error reading LCD brightness\n"); + } + + return p; +} + +static int set_lcd(int value) +{ + u32 hci_result; + + value = value << HCI_LCD_BRIGHTNESS_SHIFT; + hci_write1(HCI_LCD_BRIGHTNESS, value, &hci_result); + if (hci_result != HCI_SUCCESS) + return -EFAULT; + + return 0; +} + +static int set_lcd_status(struct backlight_device *bd) +{ + return set_lcd(bd->props.brightness); +} + +static unsigned long write_lcd(const char *buffer, unsigned long count) +{ + int value; + int ret; + + if (sscanf(buffer, " brightness : %i", &value) == 1 && + value >= 0 && value < HCI_LCD_BRIGHTNESS_LEVELS) { + ret = set_lcd(value); + if (ret == 0) + ret = count; + } else { + ret = -EINVAL; + } + return ret; +} + +static char *read_video(char *p) +{ + u32 hci_result; + u32 value; + + hci_read1(HCI_VIDEO_OUT, &value, &hci_result); + if (hci_result == HCI_SUCCESS) { + int is_lcd = (value & HCI_VIDEO_OUT_LCD) ? 1 : 0; + int is_crt = (value & HCI_VIDEO_OUT_CRT) ? 1 : 0; + int is_tv = (value & HCI_VIDEO_OUT_TV) ? 1 : 0; + p += sprintf(p, "lcd_out: %d\n", is_lcd); + p += sprintf(p, "crt_out: %d\n", is_crt); + p += sprintf(p, "tv_out: %d\n", is_tv); + } else { + printk(MY_ERR "Error reading video out status\n"); + } + + return p; +} + +static unsigned long write_video(const char *buffer, unsigned long count) +{ + int value; + int remain = count; + int lcd_out = -1; + int crt_out = -1; + int tv_out = -1; + u32 hci_result; + u32 video_out; + + /* scan expression. Multiple expressions may be delimited with ; + * + * NOTE: to keep scanning simple, invalid fields are ignored + */ + while (remain) { + if (sscanf(buffer, " lcd_out : %i", &value) == 1) + lcd_out = value & 1; + else if (sscanf(buffer, " crt_out : %i", &value) == 1) + crt_out = value & 1; + else if (sscanf(buffer, " tv_out : %i", &value) == 1) + tv_out = value & 1; + /* advance to one character past the next ; */ + do { + ++buffer; + --remain; + } + while (remain && *(buffer - 1) != ';'); + } + + hci_read1(HCI_VIDEO_OUT, &video_out, &hci_result); + if (hci_result == HCI_SUCCESS) { + unsigned int new_video_out = video_out; + if (lcd_out != -1) + _set_bit(&new_video_out, HCI_VIDEO_OUT_LCD, lcd_out); + if (crt_out != -1) + _set_bit(&new_video_out, HCI_VIDEO_OUT_CRT, crt_out); + if (tv_out != -1) + _set_bit(&new_video_out, HCI_VIDEO_OUT_TV, tv_out); + /* To avoid unnecessary video disruption, only write the new + * video setting if something changed. */ + if (new_video_out != video_out) + write_acpi_int(METHOD_VIDEO_OUT, new_video_out); + } else { + return -EFAULT; + } + + return count; +} + +static char *read_fan(char *p) +{ + u32 hci_result; + u32 value; + + hci_read1(HCI_FAN, &value, &hci_result); + if (hci_result == HCI_SUCCESS) { + p += sprintf(p, "running: %d\n", (value > 0)); + p += sprintf(p, "force_on: %d\n", force_fan); + } else { + printk(MY_ERR "Error reading fan status\n"); + } + + return p; +} + +static unsigned long write_fan(const char *buffer, unsigned long count) +{ + int value; + u32 hci_result; + + if (sscanf(buffer, " force_on : %i", &value) == 1 && + value >= 0 && value <= 1) { + hci_write1(HCI_FAN, value, &hci_result); + if (hci_result != HCI_SUCCESS) + return -EFAULT; + else + force_fan = value; + } else { + return -EINVAL; + } + + return count; +} + +static char *read_keys(char *p) +{ + u32 hci_result; + u32 value; + + if (!key_event_valid) { + hci_read1(HCI_SYSTEM_EVENT, &value, &hci_result); + if (hci_result == HCI_SUCCESS) { + key_event_valid = 1; + last_key_event = value; + } else if (hci_result == HCI_EMPTY) { + /* better luck next time */ + } else if (hci_result == HCI_NOT_SUPPORTED) { + /* This is a workaround for an unresolved issue on + * some machines where system events sporadically + * become disabled. */ + hci_write1(HCI_SYSTEM_EVENT, 1, &hci_result); + printk(MY_NOTICE "Re-enabled hotkeys\n"); + } else { + printk(MY_ERR "Error reading hotkey status\n"); + goto end; + } + } + + p += sprintf(p, "hotkey_ready: %d\n", key_event_valid); + p += sprintf(p, "hotkey: 0x%04x\n", last_key_event); + + end: + return p; +} + +static unsigned long write_keys(const char *buffer, unsigned long count) +{ + int value; + + if (sscanf(buffer, " hotkey_ready : %i", &value) == 1 && value == 0) { + key_event_valid = 0; + } else { + return -EINVAL; + } + + return count; +} + +static char *read_version(char *p) +{ + p += sprintf(p, "driver: %s\n", TOSHIBA_ACPI_VERSION); + p += sprintf(p, "proc_interface: %d\n", + PROC_INTERFACE_VERSION); + return p; +} + +/* proc and module init + */ + +#define PROC_TOSHIBA "toshiba" + +static ProcItem proc_items[] = { + {"lcd", read_lcd, write_lcd}, + {"video", read_video, write_video}, + {"fan", read_fan, write_fan}, + {"keys", read_keys, write_keys}, + {"version", read_version, NULL}, + {NULL} +}; + +static acpi_status __init add_device(void) +{ + struct proc_dir_entry *proc; + ProcItem *item; + + for (item = proc_items; item->name; ++item) { + proc = create_proc_read_entry(item->name, + S_IFREG | S_IRUGO | S_IWUSR, + toshiba_proc_dir, + (read_proc_t *) dispatch_read, + item); + if (proc) + proc->owner = THIS_MODULE; + if (proc && item->write_func) + proc->write_proc = (write_proc_t *) dispatch_write; + } + + return AE_OK; +} + +static acpi_status remove_device(void) +{ + ProcItem *item; + + for (item = proc_items; item->name; ++item) + remove_proc_entry(item->name, toshiba_proc_dir); + return AE_OK; +} + +static struct backlight_ops toshiba_backlight_data = { + .get_brightness = get_lcd, + .update_status = set_lcd_status, +}; + +static void toshiba_acpi_exit(void) +{ + if (toshiba_acpi.poll_dev) { + input_unregister_polled_device(toshiba_acpi.poll_dev); + input_free_polled_device(toshiba_acpi.poll_dev); + } + + if (toshiba_acpi.rfk_dev) + rfkill_unregister(toshiba_acpi.rfk_dev); + + if (toshiba_backlight_device) + backlight_device_unregister(toshiba_backlight_device); + + remove_device(); + + if (toshiba_proc_dir) + remove_proc_entry(PROC_TOSHIBA, acpi_root_dir); + + platform_device_unregister(toshiba_acpi.p_dev); + + return; +} + +static int __init toshiba_acpi_init(void) +{ + acpi_status status = AE_OK; + u32 hci_result; + bool bt_present; + bool bt_on; + bool radio_on; + int ret = 0; + + if (acpi_disabled) + return -ENODEV; + + /* simple device detection: look for HCI method */ + if (is_valid_acpi_path(METHOD_HCI_1)) + method_hci = METHOD_HCI_1; + else if (is_valid_acpi_path(METHOD_HCI_2)) + method_hci = METHOD_HCI_2; + else + return -ENODEV; + + printk(MY_INFO "Toshiba Laptop ACPI Extras version %s\n", + TOSHIBA_ACPI_VERSION); + printk(MY_INFO " HCI method: %s\n", method_hci); + + mutex_init(&toshiba_acpi.mutex); + + toshiba_acpi.p_dev = platform_device_register_simple("toshiba_acpi", + -1, NULL, 0); + if (IS_ERR(toshiba_acpi.p_dev)) { + ret = PTR_ERR(toshiba_acpi.p_dev); + printk(MY_ERR "unable to register platform device\n"); + toshiba_acpi.p_dev = NULL; + toshiba_acpi_exit(); + return ret; + } + + force_fan = 0; + key_event_valid = 0; + + /* enable event fifo */ + hci_write1(HCI_SYSTEM_EVENT, 1, &hci_result); + + toshiba_proc_dir = proc_mkdir(PROC_TOSHIBA, acpi_root_dir); + if (!toshiba_proc_dir) { + toshiba_acpi_exit(); + return -ENODEV; + } else { + toshiba_proc_dir->owner = THIS_MODULE; + status = add_device(); + if (ACPI_FAILURE(status)) { + toshiba_acpi_exit(); + return -ENODEV; + } + } + + toshiba_backlight_device = backlight_device_register("toshiba", + &toshiba_acpi.p_dev->dev, + NULL, + &toshiba_backlight_data); + if (IS_ERR(toshiba_backlight_device)) { + ret = PTR_ERR(toshiba_backlight_device); + + printk(KERN_ERR "Could not register toshiba backlight device\n"); + toshiba_backlight_device = NULL; + toshiba_acpi_exit(); + return ret; + } + toshiba_backlight_device->props.max_brightness = HCI_LCD_BRIGHTNESS_LEVELS - 1; + + /* Register rfkill switch for Bluetooth */ + if (hci_get_bt_present(&bt_present) == HCI_SUCCESS && bt_present) { + toshiba_acpi.rfk_dev = rfkill_allocate(&toshiba_acpi.p_dev->dev, + RFKILL_TYPE_BLUETOOTH); + if (!toshiba_acpi.rfk_dev) { + printk(MY_ERR "unable to allocate rfkill device\n"); + toshiba_acpi_exit(); + return -ENOMEM; + } + + toshiba_acpi.rfk_dev->name = toshiba_acpi.bt_name; + toshiba_acpi.rfk_dev->toggle_radio = bt_rfkill_toggle_radio; + toshiba_acpi.rfk_dev->user_claim_unsupported = 1; + toshiba_acpi.rfk_dev->data = &toshiba_acpi; + + if (hci_get_bt_on(&bt_on) == HCI_SUCCESS && bt_on) { + toshiba_acpi.rfk_dev->state = RFKILL_STATE_UNBLOCKED; + } else if (hci_get_radio_state(&radio_on) == HCI_SUCCESS && + radio_on) { + toshiba_acpi.rfk_dev->state = RFKILL_STATE_SOFT_BLOCKED; + } else { + toshiba_acpi.rfk_dev->state = RFKILL_STATE_HARD_BLOCKED; + } + + ret = rfkill_register(toshiba_acpi.rfk_dev); + if (ret) { + printk(MY_ERR "unable to register rfkill device\n"); + toshiba_acpi_exit(); + return -ENOMEM; + } + + /* Register input device for kill switch */ + toshiba_acpi.poll_dev = input_allocate_polled_device(); + if (!toshiba_acpi.poll_dev) { + printk(MY_ERR + "unable to allocate kill-switch input device\n"); + toshiba_acpi_exit(); + return -ENOMEM; + } + toshiba_acpi.poll_dev->private = &toshiba_acpi; + toshiba_acpi.poll_dev->poll = bt_poll_rfkill; + toshiba_acpi.poll_dev->poll_interval = 1000; /* msecs */ + + toshiba_acpi.poll_dev->input->name = toshiba_acpi.rfk_name; + toshiba_acpi.poll_dev->input->id.bustype = BUS_HOST; + /* Toshiba USB ID */ + toshiba_acpi.poll_dev->input->id.vendor = 0x0930; + set_bit(EV_SW, toshiba_acpi.poll_dev->input->evbit); + set_bit(SW_RFKILL_ALL, toshiba_acpi.poll_dev->input->swbit); + input_report_switch(toshiba_acpi.poll_dev->input, + SW_RFKILL_ALL, TRUE); + input_sync(toshiba_acpi.poll_dev->input); + + ret = input_register_polled_device(toshiba_acpi.poll_dev); + if (ret) { + printk(MY_ERR + "unable to register kill-switch input device\n"); + toshiba_acpi_exit(); + return ret; + } + } + + return 0; +} + +module_init(toshiba_acpi_init); +module_exit(toshiba_acpi_exit); diff --git a/drivers/platform/x86/wmi.c b/drivers/platform/x86/wmi.c new file mode 100644 index 000000000000..8a8b377712c9 --- /dev/null +++ b/drivers/platform/x86/wmi.c @@ -0,0 +1,747 @@ +/* + * ACPI-WMI mapping driver + * + * Copyright (C) 2007-2008 Carlos Corbacho <carlos@strangeworlds.co.uk> + * + * GUID parsing code from ldm.c is: + * Copyright (C) 2001,2002 Richard Russon <ldm@flatcap.org> + * Copyright (c) 2001-2007 Anton Altaparmakov + * Copyright (C) 2001,2002 Jakob Kemi <jakob.kemi@telia.com> + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * 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., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/types.h> +#include <linux/list.h> +#include <linux/acpi.h> +#include <acpi/acpi_bus.h> +#include <acpi/acpi_drivers.h> + +ACPI_MODULE_NAME("wmi"); +MODULE_AUTHOR("Carlos Corbacho"); +MODULE_DESCRIPTION("ACPI-WMI Mapping Driver"); +MODULE_LICENSE("GPL"); + +#define ACPI_WMI_CLASS "wmi" + +#undef PREFIX +#define PREFIX "ACPI: WMI: " + +static DEFINE_MUTEX(wmi_data_lock); + +struct guid_block { + char guid[16]; + union { + char object_id[2]; + struct { + unsigned char notify_id; + unsigned char reserved; + }; + }; + u8 instance_count; + u8 flags; +}; + +struct wmi_block { + struct list_head list; + struct guid_block gblock; + acpi_handle handle; + wmi_notify_handler handler; + void *handler_data; +}; + +static struct wmi_block wmi_blocks; + +/* + * If the GUID data block is marked as expensive, we must enable and + * explicitily disable data collection. + */ +#define ACPI_WMI_EXPENSIVE 0x1 +#define ACPI_WMI_METHOD 0x2 /* GUID is a method */ +#define ACPI_WMI_STRING 0x4 /* GUID takes & returns a string */ +#define ACPI_WMI_EVENT 0x8 /* GUID is an event */ + +static int acpi_wmi_remove(struct acpi_device *device, int type); +static int acpi_wmi_add(struct acpi_device *device); + +static const struct acpi_device_id wmi_device_ids[] = { + {"PNP0C14", 0}, + {"pnp0c14", 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, wmi_device_ids); + +static struct acpi_driver acpi_wmi_driver = { + .name = "wmi", + .class = ACPI_WMI_CLASS, + .ids = wmi_device_ids, + .ops = { + .add = acpi_wmi_add, + .remove = acpi_wmi_remove, + }, +}; + +/* + * GUID parsing functions + */ + +/** + * wmi_parse_hexbyte - Convert a ASCII hex number to a byte + * @src: Pointer to at least 2 characters to convert. + * + * Convert a two character ASCII hex string to a number. + * + * Return: 0-255 Success, the byte was parsed correctly + * -1 Error, an invalid character was supplied + */ +static int wmi_parse_hexbyte(const u8 *src) +{ + unsigned int x; /* For correct wrapping */ + int h; + + /* high part */ + x = src[0]; + if (x - '0' <= '9' - '0') { + h = x - '0'; + } else if (x - 'a' <= 'f' - 'a') { + h = x - 'a' + 10; + } else if (x - 'A' <= 'F' - 'A') { + h = x - 'A' + 10; + } else { + return -1; + } + h <<= 4; + + /* low part */ + x = src[1]; + if (x - '0' <= '9' - '0') + return h | (x - '0'); + if (x - 'a' <= 'f' - 'a') + return h | (x - 'a' + 10); + if (x - 'A' <= 'F' - 'A') + return h | (x - 'A' + 10); + return -1; +} + +/** + * wmi_swap_bytes - Rearrange GUID bytes to match GUID binary + * @src: Memory block holding binary GUID (16 bytes) + * @dest: Memory block to hold byte swapped binary GUID (16 bytes) + * + * Byte swap a binary GUID to match it's real GUID value + */ +static void wmi_swap_bytes(u8 *src, u8 *dest) +{ + int i; + + for (i = 0; i <= 3; i++) + memcpy(dest + i, src + (3 - i), 1); + + for (i = 0; i <= 1; i++) + memcpy(dest + 4 + i, src + (5 - i), 1); + + for (i = 0; i <= 1; i++) + memcpy(dest + 6 + i, src + (7 - i), 1); + + memcpy(dest + 8, src + 8, 8); +} + +/** + * wmi_parse_guid - Convert GUID from ASCII to binary + * @src: 36 char string of the form fa50ff2b-f2e8-45de-83fa-65417f2f49ba + * @dest: Memory block to hold binary GUID (16 bytes) + * + * N.B. The GUID need not be NULL terminated. + * + * Return: 'true' @dest contains binary GUID + * 'false' @dest contents are undefined + */ +static bool wmi_parse_guid(const u8 *src, u8 *dest) +{ + static const int size[] = { 4, 2, 2, 2, 6 }; + int i, j, v; + + if (src[8] != '-' || src[13] != '-' || + src[18] != '-' || src[23] != '-') + return false; + + for (j = 0; j < 5; j++, src++) { + for (i = 0; i < size[j]; i++, src += 2, *dest++ = v) { + v = wmi_parse_hexbyte(src); + if (v < 0) + return false; + } + } + + return true; +} + +static bool find_guid(const char *guid_string, struct wmi_block **out) +{ + char tmp[16], guid_input[16]; + struct wmi_block *wblock; + struct guid_block *block; + struct list_head *p; + + wmi_parse_guid(guid_string, tmp); + wmi_swap_bytes(tmp, guid_input); + + list_for_each(p, &wmi_blocks.list) { + wblock = list_entry(p, struct wmi_block, list); + block = &wblock->gblock; + + if (memcmp(block->guid, guid_input, 16) == 0) { + if (out) + *out = wblock; + return 1; + } + } + return 0; +} + +static acpi_status wmi_method_enable(struct wmi_block *wblock, int enable) +{ + struct guid_block *block = NULL; + char method[5]; + struct acpi_object_list input; + union acpi_object params[1]; + acpi_status status; + acpi_handle handle; + + block = &wblock->gblock; + handle = wblock->handle; + + if (!block) + return AE_NOT_EXIST; + + input.count = 1; + input.pointer = params; + params[0].type = ACPI_TYPE_INTEGER; + params[0].integer.value = enable; + + snprintf(method, 5, "WE%02X", block->notify_id); + status = acpi_evaluate_object(handle, method, &input, NULL); + + if (status != AE_OK && status != AE_NOT_FOUND) + return status; + else + return AE_OK; +} + +/* + * Exported WMI functions + */ +/** + * wmi_evaluate_method - Evaluate a WMI method + * @guid_string: 36 char string of the form fa50ff2b-f2e8-45de-83fa-65417f2f49ba + * @instance: Instance index + * @method_id: Method ID to call + * &in: Buffer containing input for the method call + * &out: Empty buffer to return the method results + * + * Call an ACPI-WMI method + */ +acpi_status wmi_evaluate_method(const char *guid_string, u8 instance, +u32 method_id, const struct acpi_buffer *in, struct acpi_buffer *out) +{ + struct guid_block *block = NULL; + struct wmi_block *wblock = NULL; + acpi_handle handle; + acpi_status status; + struct acpi_object_list input; + union acpi_object params[3]; + char method[4] = "WM"; + + if (!find_guid(guid_string, &wblock)) + return AE_ERROR; + + block = &wblock->gblock; + handle = wblock->handle; + + if (!(block->flags & ACPI_WMI_METHOD)) + return AE_BAD_DATA; + + if (block->instance_count < instance) + return AE_BAD_PARAMETER; + + input.count = 2; + input.pointer = params; + params[0].type = ACPI_TYPE_INTEGER; + params[0].integer.value = instance; + params[1].type = ACPI_TYPE_INTEGER; + params[1].integer.value = method_id; + + if (in) { + input.count = 3; + + if (block->flags & ACPI_WMI_STRING) { + params[2].type = ACPI_TYPE_STRING; + } else { + params[2].type = ACPI_TYPE_BUFFER; + } + params[2].buffer.length = in->length; + params[2].buffer.pointer = in->pointer; + } + + strncat(method, block->object_id, 2); + + status = acpi_evaluate_object(handle, method, &input, out); + + return status; +} +EXPORT_SYMBOL_GPL(wmi_evaluate_method); + +/** + * wmi_query_block - Return contents of a WMI block + * @guid_string: 36 char string of the form fa50ff2b-f2e8-45de-83fa-65417f2f49ba + * @instance: Instance index + * &out: Empty buffer to return the contents of the data block to + * + * Return the contents of an ACPI-WMI data block to a buffer + */ +acpi_status wmi_query_block(const char *guid_string, u8 instance, +struct acpi_buffer *out) +{ + struct guid_block *block = NULL; + struct wmi_block *wblock = NULL; + acpi_handle handle, wc_handle; + acpi_status status, wc_status = AE_ERROR; + struct acpi_object_list input, wc_input; + union acpi_object wc_params[1], wq_params[1]; + char method[4]; + char wc_method[4] = "WC"; + + if (!guid_string || !out) + return AE_BAD_PARAMETER; + + if (!find_guid(guid_string, &wblock)) + return AE_ERROR; + + block = &wblock->gblock; + handle = wblock->handle; + + if (block->instance_count < instance) + return AE_BAD_PARAMETER; + + /* Check GUID is a data block */ + if (block->flags & (ACPI_WMI_EVENT | ACPI_WMI_METHOD)) + return AE_ERROR; + + input.count = 1; + input.pointer = wq_params; + wq_params[0].type = ACPI_TYPE_INTEGER; + wq_params[0].integer.value = instance; + + /* + * If ACPI_WMI_EXPENSIVE, call the relevant WCxx method first to + * enable collection. + */ + if (block->flags & ACPI_WMI_EXPENSIVE) { + wc_input.count = 1; + wc_input.pointer = wc_params; + wc_params[0].type = ACPI_TYPE_INTEGER; + wc_params[0].integer.value = 1; + + strncat(wc_method, block->object_id, 2); + + /* + * Some GUIDs break the specification by declaring themselves + * expensive, but have no corresponding WCxx method. So we + * should not fail if this happens. + */ + wc_status = acpi_get_handle(handle, wc_method, &wc_handle); + if (ACPI_SUCCESS(wc_status)) + wc_status = acpi_evaluate_object(handle, wc_method, + &wc_input, NULL); + } + + strcpy(method, "WQ"); + strncat(method, block->object_id, 2); + + status = acpi_evaluate_object(handle, method, &input, out); + + /* + * If ACPI_WMI_EXPENSIVE, call the relevant WCxx method, even if + * the WQxx method failed - we should disable collection anyway. + */ + if ((block->flags & ACPI_WMI_EXPENSIVE) && ACPI_SUCCESS(wc_status)) { + wc_params[0].integer.value = 0; + status = acpi_evaluate_object(handle, + wc_method, &wc_input, NULL); + } + + return status; +} +EXPORT_SYMBOL_GPL(wmi_query_block); + +/** + * wmi_set_block - Write to a WMI block + * @guid_string: 36 char string of the form fa50ff2b-f2e8-45de-83fa-65417f2f49ba + * @instance: Instance index + * &in: Buffer containing new values for the data block + * + * Write the contents of the input buffer to an ACPI-WMI data block + */ +acpi_status wmi_set_block(const char *guid_string, u8 instance, +const struct acpi_buffer *in) +{ + struct guid_block *block = NULL; + struct wmi_block *wblock = NULL; + acpi_handle handle; + struct acpi_object_list input; + union acpi_object params[2]; + char method[4] = "WS"; + + if (!guid_string || !in) + return AE_BAD_DATA; + + if (!find_guid(guid_string, &wblock)) + return AE_ERROR; + + block = &wblock->gblock; + handle = wblock->handle; + + if (block->instance_count < instance) + return AE_BAD_PARAMETER; + + /* Check GUID is a data block */ + if (block->flags & (ACPI_WMI_EVENT | ACPI_WMI_METHOD)) + return AE_ERROR; + + input.count = 2; + input.pointer = params; + params[0].type = ACPI_TYPE_INTEGER; + params[0].integer.value = instance; + + if (block->flags & ACPI_WMI_STRING) { + params[1].type = ACPI_TYPE_STRING; + } else { + params[1].type = ACPI_TYPE_BUFFER; + } + params[1].buffer.length = in->length; + params[1].buffer.pointer = in->pointer; + + strncat(method, block->object_id, 2); + + return acpi_evaluate_object(handle, method, &input, NULL); +} +EXPORT_SYMBOL_GPL(wmi_set_block); + +/** + * wmi_install_notify_handler - Register handler for WMI events + * @handler: Function to handle notifications + * @data: Data to be returned to handler when event is fired + * + * Register a handler for events sent to the ACPI-WMI mapper device. + */ +acpi_status wmi_install_notify_handler(const char *guid, +wmi_notify_handler handler, void *data) +{ + struct wmi_block *block; + acpi_status status; + + if (!guid || !handler) + return AE_BAD_PARAMETER; + + find_guid(guid, &block); + if (!block) + return AE_NOT_EXIST; + + if (block->handler) + return AE_ALREADY_ACQUIRED; + + block->handler = handler; + block->handler_data = data; + + status = wmi_method_enable(block, 1); + + return status; +} +EXPORT_SYMBOL_GPL(wmi_install_notify_handler); + +/** + * wmi_uninstall_notify_handler - Unregister handler for WMI events + * + * Unregister handler for events sent to the ACPI-WMI mapper device. + */ +acpi_status wmi_remove_notify_handler(const char *guid) +{ + struct wmi_block *block; + acpi_status status; + + if (!guid) + return AE_BAD_PARAMETER; + + find_guid(guid, &block); + if (!block) + return AE_NOT_EXIST; + + if (!block->handler) + return AE_NULL_ENTRY; + + status = wmi_method_enable(block, 0); + + block->handler = NULL; + block->handler_data = NULL; + + return status; +} +EXPORT_SYMBOL_GPL(wmi_remove_notify_handler); + +/** + * wmi_get_event_data - Get WMI data associated with an event + * + * @event - Event to find + * &out - Buffer to hold event data + * + * Returns extra data associated with an event in WMI. + */ +acpi_status wmi_get_event_data(u32 event, struct acpi_buffer *out) +{ + struct acpi_object_list input; + union acpi_object params[1]; + struct guid_block *gblock; + struct wmi_block *wblock; + struct list_head *p; + + input.count = 1; + input.pointer = params; + params[0].type = ACPI_TYPE_INTEGER; + params[0].integer.value = event; + + list_for_each(p, &wmi_blocks.list) { + wblock = list_entry(p, struct wmi_block, list); + gblock = &wblock->gblock; + + if ((gblock->flags & ACPI_WMI_EVENT) && + (gblock->notify_id == event)) + return acpi_evaluate_object(wblock->handle, "_WED", + &input, out); + } + + return AE_NOT_FOUND; +} +EXPORT_SYMBOL_GPL(wmi_get_event_data); + +/** + * wmi_has_guid - Check if a GUID is available + * @guid_string: 36 char string of the form fa50ff2b-f2e8-45de-83fa-65417f2f49ba + * + * Check if a given GUID is defined by _WDG + */ +bool wmi_has_guid(const char *guid_string) +{ + return find_guid(guid_string, NULL); +} +EXPORT_SYMBOL_GPL(wmi_has_guid); + +/* + * Parse the _WDG method for the GUID data blocks + */ +static __init acpi_status parse_wdg(acpi_handle handle) +{ + struct acpi_buffer out = {ACPI_ALLOCATE_BUFFER, NULL}; + union acpi_object *obj; + struct guid_block *gblock; + struct wmi_block *wblock; + acpi_status status; + u32 i, total; + + status = acpi_evaluate_object(handle, "_WDG", NULL, &out); + + if (ACPI_FAILURE(status)) + return status; + + obj = (union acpi_object *) out.pointer; + + if (obj->type != ACPI_TYPE_BUFFER) + return AE_ERROR; + + total = obj->buffer.length / sizeof(struct guid_block); + + gblock = kzalloc(obj->buffer.length, GFP_KERNEL); + if (!gblock) + return AE_NO_MEMORY; + + memcpy(gblock, obj->buffer.pointer, obj->buffer.length); + + for (i = 0; i < total; i++) { + wblock = kzalloc(sizeof(struct wmi_block), GFP_KERNEL); + if (!wblock) + return AE_NO_MEMORY; + + wblock->gblock = gblock[i]; + wblock->handle = handle; + list_add_tail(&wblock->list, &wmi_blocks.list); + } + + kfree(out.pointer); + kfree(gblock); + + return status; +} + +/* + * WMI can have EmbeddedControl access regions. In which case, we just want to + * hand these off to the EC driver. + */ +static acpi_status +acpi_wmi_ec_space_handler(u32 function, acpi_physical_address address, + u32 bits, acpi_integer * value, + void *handler_context, void *region_context) +{ + int result = 0, i = 0; + u8 temp = 0; + + if ((address > 0xFF) || !value) + return AE_BAD_PARAMETER; + + if (function != ACPI_READ && function != ACPI_WRITE) + return AE_BAD_PARAMETER; + + if (bits != 8) + return AE_BAD_PARAMETER; + + if (function == ACPI_READ) { + result = ec_read(address, &temp); + (*value) |= ((acpi_integer)temp) << i; + } else { + temp = 0xff & ((*value) >> i); + result = ec_write(address, temp); + } + + switch (result) { + case -EINVAL: + return AE_BAD_PARAMETER; + break; + case -ENODEV: + return AE_NOT_FOUND; + break; + case -ETIME: + return AE_TIME; + break; + default: + return AE_OK; + } +} + +static void acpi_wmi_notify(acpi_handle handle, u32 event, void *data) +{ + struct guid_block *block; + struct wmi_block *wblock; + struct list_head *p; + struct acpi_device *device = data; + + list_for_each(p, &wmi_blocks.list) { + wblock = list_entry(p, struct wmi_block, list); + block = &wblock->gblock; + + if ((block->flags & ACPI_WMI_EVENT) && + (block->notify_id == event)) { + if (wblock->handler) + wblock->handler(event, wblock->handler_data); + + acpi_bus_generate_netlink_event( + device->pnp.device_class, dev_name(&device->dev), + event, 0); + break; + } + } +} + +static int acpi_wmi_remove(struct acpi_device *device, int type) +{ + acpi_remove_notify_handler(device->handle, ACPI_DEVICE_NOTIFY, + acpi_wmi_notify); + + acpi_remove_address_space_handler(device->handle, + ACPI_ADR_SPACE_EC, &acpi_wmi_ec_space_handler); + + return 0; +} + +static int __init acpi_wmi_add(struct acpi_device *device) +{ + acpi_status status; + int result = 0; + + status = acpi_install_notify_handler(device->handle, ACPI_DEVICE_NOTIFY, + acpi_wmi_notify, device); + if (ACPI_FAILURE(status)) { + printk(KERN_ERR PREFIX "Error installing notify handler\n"); + return -ENODEV; + } + + status = acpi_install_address_space_handler(device->handle, + ACPI_ADR_SPACE_EC, + &acpi_wmi_ec_space_handler, + NULL, NULL); + if (ACPI_FAILURE(status)) + return -ENODEV; + + status = parse_wdg(device->handle); + if (ACPI_FAILURE(status)) { + printk(KERN_ERR PREFIX "Error installing EC region handler\n"); + return -ENODEV; + } + + return result; +} + +static int __init acpi_wmi_init(void) +{ + acpi_status result; + + INIT_LIST_HEAD(&wmi_blocks.list); + + if (acpi_disabled) + return -ENODEV; + + result = acpi_bus_register_driver(&acpi_wmi_driver); + + if (result < 0) { + printk(KERN_INFO PREFIX "Error loading mapper\n"); + } else { + printk(KERN_INFO PREFIX "Mapper loaded\n"); + } + + return result; +} + +static void __exit acpi_wmi_exit(void) +{ + struct list_head *p, *tmp; + struct wmi_block *wblock; + + acpi_bus_unregister_driver(&acpi_wmi_driver); + + list_for_each_safe(p, tmp, &wmi_blocks.list) { + wblock = list_entry(p, struct wmi_block, list); + + list_del(p); + kfree(wblock); + } + + printk(KERN_INFO PREFIX "Mapper unloaded\n"); +} + +subsys_initcall(acpi_wmi_init); +module_exit(acpi_wmi_exit); |