mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
synced 2024-08-24 09:50:04 +00:00
![Thomas Gleixner](/assets/img/avatar_default.png)
Based on 2 normalized pattern(s): 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 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 [no]_[pad]_[ctrl] 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 extracted by the scancode license scanner the SPDX license identifier GPL-2.0-or-later has been chosen to replace the boilerplate/reference in 176 file(s). Signed-off-by: Thomas Gleixner <tglx@linutronix.de> Reviewed-by: Jilayne Lovejoy <opensource@jilayne.com> Reviewed-by: Steve Winslow <swinslow@gmail.com> Reviewed-by: Allison Randal <allison@lohutok.net> Reviewed-by: Kate Stewart <kstewart@linuxfoundation.org> Cc: linux-spdx@vger.kernel.org Link: https://lkml.kernel.org/r/20190519154040.652910950@linutronix.de Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
1179 lines
27 KiB
C
1179 lines
27 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*-*-linux-c-*-*/
|
|
|
|
/*
|
|
Copyright (C) 2006 Lennart Poettering <mzxreary (at) 0pointer (dot) de>
|
|
|
|
*/
|
|
|
|
/*
|
|
* 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.
|
|
*/
|
|
|
|
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
|
|
|
|
#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/rfkill.h>
|
|
#include <linux/i8042.h>
|
|
#include <linux/input.h>
|
|
#include <linux/input/sparse-keymap.h>
|
|
#include <acpi/video.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
|
|
|
|
#define MSI_STANDARD_EC_COMMAND_ADDRESS 0x2e
|
|
#define MSI_STANDARD_EC_BLUETOOTH_MASK (1 << 0)
|
|
#define MSI_STANDARD_EC_WEBCAM_MASK (1 << 1)
|
|
#define MSI_STANDARD_EC_WLAN_MASK (1 << 3)
|
|
#define MSI_STANDARD_EC_3G_MASK (1 << 4)
|
|
|
|
/* For set SCM load flag to disable BIOS fn key */
|
|
#define MSI_STANDARD_EC_SCM_LOAD_ADDRESS 0x2d
|
|
#define MSI_STANDARD_EC_SCM_LOAD_MASK (1 << 0)
|
|
|
|
#define MSI_STANDARD_EC_FUNCTIONS_ADDRESS 0xe4
|
|
/* Power LED is orange - Turbo mode */
|
|
#define MSI_STANDARD_EC_TURBO_MASK (1 << 1)
|
|
/* Power LED is green - ECO mode */
|
|
#define MSI_STANDARD_EC_ECO_MASK (1 << 3)
|
|
/* Touchpad is turned on */
|
|
#define MSI_STANDARD_EC_TOUCHPAD_MASK (1 << 4)
|
|
/* If this bit != bit 1, turbo mode can't be toggled */
|
|
#define MSI_STANDARD_EC_TURBO_COOLDOWN_MASK (1 << 7)
|
|
|
|
#define MSI_STANDARD_EC_FAN_ADDRESS 0x33
|
|
/* If zero, fan rotates at maximal speed */
|
|
#define MSI_STANDARD_EC_AUTOFAN_MASK (1 << 0)
|
|
|
|
#ifdef CONFIG_PM_SLEEP
|
|
static int msi_laptop_resume(struct device *device);
|
|
#endif
|
|
static SIMPLE_DEV_PM_OPS(msi_laptop_pm, NULL, msi_laptop_resume);
|
|
|
|
#define MSI_STANDARD_EC_DEVICES_EXISTS_ADDRESS 0x2f
|
|
|
|
static bool 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)");
|
|
|
|
static const struct key_entry msi_laptop_keymap[] = {
|
|
{KE_KEY, KEY_TOUCHPAD_ON, {KEY_TOUCHPAD_ON} }, /* Touch Pad On */
|
|
{KE_KEY, KEY_TOUCHPAD_OFF, {KEY_TOUCHPAD_OFF} },/* Touch Pad On */
|
|
{KE_END, 0}
|
|
};
|
|
|
|
static struct input_dev *msi_laptop_input_dev;
|
|
|
|
static int wlan_s, bluetooth_s, threeg_s;
|
|
static int threeg_exists;
|
|
static struct rfkill *rfk_wlan, *rfk_bluetooth, *rfk_threeg;
|
|
|
|
/* MSI laptop quirks */
|
|
struct quirk_entry {
|
|
bool old_ec_model;
|
|
|
|
/* Some MSI 3G netbook only have one fn key to control
|
|
* Wlan/Bluetooth/3G, those netbook will load the SCM (windows app) to
|
|
* disable the original Wlan/Bluetooth control by BIOS when user press
|
|
* fn key, then control Wlan/Bluetooth/3G by SCM (software control by
|
|
* OS). Without SCM, user cann't on/off 3G module on those 3G netbook.
|
|
* On Linux, msi-laptop driver will do the same thing to disable the
|
|
* original BIOS control, then might need use HAL or other userland
|
|
* application to do the software control that simulate with SCM.
|
|
* e.g. MSI N034 netbook
|
|
*/
|
|
bool load_scm_model;
|
|
|
|
/* Some MSI laptops need delay before reading from EC */
|
|
bool ec_delay;
|
|
|
|
/* Some MSI Wind netbooks (e.g. MSI Wind U100) need loading SCM to get
|
|
* some features working (e.g. ECO mode), but we cannot change
|
|
* Wlan/Bluetooth state in software and we can only read its state.
|
|
*/
|
|
bool ec_read_only;
|
|
};
|
|
|
|
static struct quirk_entry *quirks;
|
|
|
|
/* 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);
|
|
}
|
|
|
|
static int get_lcd_level(void)
|
|
{
|
|
u8 wdata = 0, rdata;
|
|
int result;
|
|
|
|
result = ec_transaction(MSI_EC_COMMAND_LCD_LEVEL, &wdata, 1,
|
|
&rdata, 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);
|
|
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);
|
|
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);
|
|
}
|
|
|
|
static ssize_t set_device_state(const char *buf, size_t count, u8 mask)
|
|
{
|
|
int status;
|
|
u8 wdata = 0, rdata;
|
|
int result;
|
|
|
|
if (sscanf(buf, "%i", &status) != 1 || (status < 0 || status > 1))
|
|
return -EINVAL;
|
|
|
|
if (quirks->ec_read_only)
|
|
return -EOPNOTSUPP;
|
|
|
|
/* read current device state */
|
|
result = ec_read(MSI_STANDARD_EC_COMMAND_ADDRESS, &rdata);
|
|
if (result < 0)
|
|
return result;
|
|
|
|
if (!!(rdata & mask) != status) {
|
|
/* reverse device bit */
|
|
if (rdata & mask)
|
|
wdata = rdata & ~mask;
|
|
else
|
|
wdata = rdata | mask;
|
|
|
|
result = ec_write(MSI_STANDARD_EC_COMMAND_ADDRESS, wdata);
|
|
if (result < 0)
|
|
return result;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
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);
|
|
if (result < 0)
|
|
return result;
|
|
|
|
if (wlan)
|
|
*wlan = !!(rdata & 8);
|
|
|
|
if (bluetooth)
|
|
*bluetooth = !!(rdata & 128);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int get_wireless_state_ec_standard(void)
|
|
{
|
|
u8 rdata;
|
|
int result;
|
|
|
|
result = ec_read(MSI_STANDARD_EC_COMMAND_ADDRESS, &rdata);
|
|
if (result < 0)
|
|
return result;
|
|
|
|
wlan_s = !!(rdata & MSI_STANDARD_EC_WLAN_MASK);
|
|
|
|
bluetooth_s = !!(rdata & MSI_STANDARD_EC_BLUETOOTH_MASK);
|
|
|
|
threeg_s = !!(rdata & MSI_STANDARD_EC_3G_MASK);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int get_threeg_exists(void)
|
|
{
|
|
u8 rdata;
|
|
int result;
|
|
|
|
result = ec_read(MSI_STANDARD_EC_DEVICES_EXISTS_ADDRESS, &rdata);
|
|
if (result < 0)
|
|
return result;
|
|
|
|
threeg_exists = !!(rdata & MSI_STANDARD_EC_3G_MASK);
|
|
|
|
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 const 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 = 0;
|
|
|
|
if (quirks->old_ec_model) {
|
|
ret = get_wireless_state(&enabled, NULL);
|
|
} else {
|
|
ret = get_wireless_state_ec_standard();
|
|
enabled = wlan_s;
|
|
}
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
return sprintf(buf, "%i\n", enabled);
|
|
}
|
|
|
|
static ssize_t store_wlan(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
return set_device_state(buf, count, MSI_STANDARD_EC_WLAN_MASK);
|
|
}
|
|
|
|
static ssize_t show_bluetooth(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
|
|
int ret, enabled = 0;
|
|
|
|
if (quirks->old_ec_model) {
|
|
ret = get_wireless_state(NULL, &enabled);
|
|
} else {
|
|
ret = get_wireless_state_ec_standard();
|
|
enabled = bluetooth_s;
|
|
}
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
return sprintf(buf, "%i\n", enabled);
|
|
}
|
|
|
|
static ssize_t store_bluetooth(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
return set_device_state(buf, count, MSI_STANDARD_EC_BLUETOOTH_MASK);
|
|
}
|
|
|
|
static ssize_t show_threeg(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
|
|
int ret;
|
|
|
|
/* old msi ec not support 3G */
|
|
if (quirks->old_ec_model)
|
|
return -ENODEV;
|
|
|
|
ret = get_wireless_state_ec_standard();
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
return sprintf(buf, "%i\n", threeg_s);
|
|
}
|
|
|
|
static ssize_t store_threeg(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
return set_device_state(buf, count, MSI_STANDARD_EC_3G_MASK);
|
|
}
|
|
|
|
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 ssize_t show_touchpad(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
|
|
u8 rdata;
|
|
int result;
|
|
|
|
result = ec_read(MSI_STANDARD_EC_FUNCTIONS_ADDRESS, &rdata);
|
|
if (result < 0)
|
|
return result;
|
|
|
|
return sprintf(buf, "%i\n", !!(rdata & MSI_STANDARD_EC_TOUCHPAD_MASK));
|
|
}
|
|
|
|
static ssize_t show_turbo(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
|
|
u8 rdata;
|
|
int result;
|
|
|
|
result = ec_read(MSI_STANDARD_EC_FUNCTIONS_ADDRESS, &rdata);
|
|
if (result < 0)
|
|
return result;
|
|
|
|
return sprintf(buf, "%i\n", !!(rdata & MSI_STANDARD_EC_TURBO_MASK));
|
|
}
|
|
|
|
static ssize_t show_eco(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
|
|
u8 rdata;
|
|
int result;
|
|
|
|
result = ec_read(MSI_STANDARD_EC_FUNCTIONS_ADDRESS, &rdata);
|
|
if (result < 0)
|
|
return result;
|
|
|
|
return sprintf(buf, "%i\n", !!(rdata & MSI_STANDARD_EC_ECO_MASK));
|
|
}
|
|
|
|
static ssize_t show_turbo_cooldown(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
|
|
u8 rdata;
|
|
int result;
|
|
|
|
result = ec_read(MSI_STANDARD_EC_FUNCTIONS_ADDRESS, &rdata);
|
|
if (result < 0)
|
|
return result;
|
|
|
|
return sprintf(buf, "%i\n", (!!(rdata & MSI_STANDARD_EC_TURBO_MASK)) |
|
|
(!!(rdata & MSI_STANDARD_EC_TURBO_COOLDOWN_MASK) << 1));
|
|
}
|
|
|
|
static ssize_t show_auto_fan(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
|
|
u8 rdata;
|
|
int result;
|
|
|
|
result = ec_read(MSI_STANDARD_EC_FAN_ADDRESS, &rdata);
|
|
if (result < 0)
|
|
return result;
|
|
|
|
return sprintf(buf, "%i\n", !!(rdata & MSI_STANDARD_EC_AUTOFAN_MASK));
|
|
}
|
|
|
|
static ssize_t store_auto_fan(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
|
|
int enable, result;
|
|
|
|
if (sscanf(buf, "%i", &enable) != 1 || (enable != (enable & 1)))
|
|
return -EINVAL;
|
|
|
|
result = ec_write(MSI_STANDARD_EC_FAN_ADDRESS, enable);
|
|
if (result < 0)
|
|
return result;
|
|
|
|
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 DEVICE_ATTR(threeg, 0444, show_threeg, NULL);
|
|
static DEVICE_ATTR(touchpad, 0444, show_touchpad, NULL);
|
|
static DEVICE_ATTR(turbo_mode, 0444, show_turbo, NULL);
|
|
static DEVICE_ATTR(eco_mode, 0444, show_eco, NULL);
|
|
static DEVICE_ATTR(turbo_cooldown, 0444, show_turbo_cooldown, NULL);
|
|
static DEVICE_ATTR(auto_fan, 0644, show_auto_fan, store_auto_fan);
|
|
|
|
static struct attribute *msipf_attributes[] = {
|
|
&dev_attr_bluetooth.attr,
|
|
&dev_attr_wlan.attr,
|
|
&dev_attr_touchpad.attr,
|
|
&dev_attr_turbo_mode.attr,
|
|
&dev_attr_eco_mode.attr,
|
|
&dev_attr_turbo_cooldown.attr,
|
|
&dev_attr_auto_fan.attr,
|
|
NULL
|
|
};
|
|
|
|
static struct attribute *msipf_old_attributes[] = {
|
|
&dev_attr_lcd_level.attr,
|
|
&dev_attr_auto_brightness.attr,
|
|
NULL
|
|
};
|
|
|
|
static const struct attribute_group msipf_attribute_group = {
|
|
.attrs = msipf_attributes
|
|
};
|
|
|
|
static const struct attribute_group msipf_old_attribute_group = {
|
|
.attrs = msipf_old_attributes
|
|
};
|
|
|
|
static struct platform_driver msipf_driver = {
|
|
.driver = {
|
|
.name = "msi-laptop-pf",
|
|
.pm = &msi_laptop_pm,
|
|
},
|
|
};
|
|
|
|
static struct platform_device *msipf_device;
|
|
|
|
/* Initialization */
|
|
|
|
static struct quirk_entry quirk_old_ec_model = {
|
|
.old_ec_model = true,
|
|
};
|
|
|
|
static struct quirk_entry quirk_load_scm_model = {
|
|
.load_scm_model = true,
|
|
.ec_delay = true,
|
|
};
|
|
|
|
static struct quirk_entry quirk_load_scm_ro_model = {
|
|
.load_scm_model = true,
|
|
.ec_read_only = true,
|
|
};
|
|
|
|
static int dmi_check_cb(const struct dmi_system_id *dmi)
|
|
{
|
|
pr_info("Identified laptop model '%s'\n", dmi->ident);
|
|
|
|
quirks = dmi->driver_data;
|
|
|
|
return 1;
|
|
}
|
|
|
|
static const struct dmi_system_id msi_dmi_table[] __initconst = {
|
|
{
|
|
.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")
|
|
},
|
|
.driver_data = &quirk_old_ec_model,
|
|
.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")
|
|
},
|
|
.driver_data = &quirk_old_ec_model,
|
|
.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")
|
|
},
|
|
.driver_data = &quirk_old_ec_model,
|
|
.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")
|
|
},
|
|
.driver_data = &quirk_old_ec_model,
|
|
.callback = dmi_check_cb
|
|
},
|
|
{
|
|
.ident = "MSI N034",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR,
|
|
"MICRO-STAR INTERNATIONAL CO., LTD"),
|
|
DMI_MATCH(DMI_PRODUCT_NAME, "MS-N034"),
|
|
DMI_MATCH(DMI_CHASSIS_VENDOR,
|
|
"MICRO-STAR INTERNATIONAL CO., LTD")
|
|
},
|
|
.driver_data = &quirk_load_scm_model,
|
|
.callback = dmi_check_cb
|
|
},
|
|
{
|
|
.ident = "MSI N051",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR,
|
|
"MICRO-STAR INTERNATIONAL CO., LTD"),
|
|
DMI_MATCH(DMI_PRODUCT_NAME, "MS-N051"),
|
|
DMI_MATCH(DMI_CHASSIS_VENDOR,
|
|
"MICRO-STAR INTERNATIONAL CO., LTD")
|
|
},
|
|
.driver_data = &quirk_load_scm_model,
|
|
.callback = dmi_check_cb
|
|
},
|
|
{
|
|
.ident = "MSI N014",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR,
|
|
"MICRO-STAR INTERNATIONAL CO., LTD"),
|
|
DMI_MATCH(DMI_PRODUCT_NAME, "MS-N014"),
|
|
},
|
|
.driver_data = &quirk_load_scm_model,
|
|
.callback = dmi_check_cb
|
|
},
|
|
{
|
|
.ident = "MSI CR620",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR,
|
|
"Micro-Star International"),
|
|
DMI_MATCH(DMI_PRODUCT_NAME, "CR620"),
|
|
},
|
|
.driver_data = &quirk_load_scm_model,
|
|
.callback = dmi_check_cb
|
|
},
|
|
{
|
|
.ident = "MSI U270",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR,
|
|
"Micro-Star International Co., Ltd."),
|
|
DMI_MATCH(DMI_PRODUCT_NAME, "U270 series"),
|
|
},
|
|
.driver_data = &quirk_load_scm_model,
|
|
.callback = dmi_check_cb
|
|
},
|
|
{
|
|
.ident = "MSI U90/U100",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR,
|
|
"MICRO-STAR INTERNATIONAL CO., LTD"),
|
|
DMI_MATCH(DMI_PRODUCT_NAME, "U90/U100"),
|
|
},
|
|
.driver_data = &quirk_load_scm_ro_model,
|
|
.callback = dmi_check_cb
|
|
},
|
|
{ }
|
|
};
|
|
|
|
static int rfkill_bluetooth_set(void *data, bool blocked)
|
|
{
|
|
/* Do something with blocked...*/
|
|
/*
|
|
* blocked == false is on
|
|
* blocked == true is off
|
|
*/
|
|
int result = set_device_state(blocked ? "0" : "1", 0,
|
|
MSI_STANDARD_EC_BLUETOOTH_MASK);
|
|
|
|
return min(result, 0);
|
|
}
|
|
|
|
static int rfkill_wlan_set(void *data, bool blocked)
|
|
{
|
|
int result = set_device_state(blocked ? "0" : "1", 0,
|
|
MSI_STANDARD_EC_WLAN_MASK);
|
|
|
|
return min(result, 0);
|
|
}
|
|
|
|
static int rfkill_threeg_set(void *data, bool blocked)
|
|
{
|
|
int result = set_device_state(blocked ? "0" : "1", 0,
|
|
MSI_STANDARD_EC_3G_MASK);
|
|
|
|
return min(result, 0);
|
|
}
|
|
|
|
static const struct rfkill_ops rfkill_bluetooth_ops = {
|
|
.set_block = rfkill_bluetooth_set
|
|
};
|
|
|
|
static const struct rfkill_ops rfkill_wlan_ops = {
|
|
.set_block = rfkill_wlan_set
|
|
};
|
|
|
|
static const struct rfkill_ops rfkill_threeg_ops = {
|
|
.set_block = rfkill_threeg_set
|
|
};
|
|
|
|
static void rfkill_cleanup(void)
|
|
{
|
|
if (rfk_bluetooth) {
|
|
rfkill_unregister(rfk_bluetooth);
|
|
rfkill_destroy(rfk_bluetooth);
|
|
}
|
|
|
|
if (rfk_threeg) {
|
|
rfkill_unregister(rfk_threeg);
|
|
rfkill_destroy(rfk_threeg);
|
|
}
|
|
|
|
if (rfk_wlan) {
|
|
rfkill_unregister(rfk_wlan);
|
|
rfkill_destroy(rfk_wlan);
|
|
}
|
|
}
|
|
|
|
static bool msi_rfkill_set_state(struct rfkill *rfkill, bool blocked)
|
|
{
|
|
if (quirks->ec_read_only)
|
|
return rfkill_set_hw_state(rfkill, blocked);
|
|
else
|
|
return rfkill_set_sw_state(rfkill, blocked);
|
|
}
|
|
|
|
static void msi_update_rfkill(struct work_struct *ignored)
|
|
{
|
|
get_wireless_state_ec_standard();
|
|
|
|
if (rfk_wlan)
|
|
msi_rfkill_set_state(rfk_wlan, !wlan_s);
|
|
if (rfk_bluetooth)
|
|
msi_rfkill_set_state(rfk_bluetooth, !bluetooth_s);
|
|
if (rfk_threeg)
|
|
msi_rfkill_set_state(rfk_threeg, !threeg_s);
|
|
}
|
|
static DECLARE_DELAYED_WORK(msi_rfkill_dwork, msi_update_rfkill);
|
|
static DECLARE_WORK(msi_rfkill_work, msi_update_rfkill);
|
|
|
|
static void msi_send_touchpad_key(struct work_struct *ignored)
|
|
{
|
|
u8 rdata;
|
|
int result;
|
|
|
|
result = ec_read(MSI_STANDARD_EC_FUNCTIONS_ADDRESS, &rdata);
|
|
if (result < 0)
|
|
return;
|
|
|
|
sparse_keymap_report_event(msi_laptop_input_dev,
|
|
(rdata & MSI_STANDARD_EC_TOUCHPAD_MASK) ?
|
|
KEY_TOUCHPAD_ON : KEY_TOUCHPAD_OFF, 1, true);
|
|
}
|
|
static DECLARE_DELAYED_WORK(msi_touchpad_dwork, msi_send_touchpad_key);
|
|
static DECLARE_WORK(msi_touchpad_work, msi_send_touchpad_key);
|
|
|
|
static bool msi_laptop_i8042_filter(unsigned char data, unsigned char str,
|
|
struct serio *port)
|
|
{
|
|
static bool extended;
|
|
|
|
if (str & I8042_STR_AUXDATA)
|
|
return false;
|
|
|
|
/* 0x54 wwan, 0x62 bluetooth, 0x76 wlan, 0xE4 touchpad toggle*/
|
|
if (unlikely(data == 0xe0)) {
|
|
extended = true;
|
|
return false;
|
|
} else if (unlikely(extended)) {
|
|
extended = false;
|
|
switch (data) {
|
|
case 0xE4:
|
|
if (quirks->ec_delay) {
|
|
schedule_delayed_work(&msi_touchpad_dwork,
|
|
round_jiffies_relative(0.5 * HZ));
|
|
} else
|
|
schedule_work(&msi_touchpad_work);
|
|
break;
|
|
case 0x54:
|
|
case 0x62:
|
|
case 0x76:
|
|
if (quirks->ec_delay) {
|
|
schedule_delayed_work(&msi_rfkill_dwork,
|
|
round_jiffies_relative(0.5 * HZ));
|
|
} else
|
|
schedule_work(&msi_rfkill_work);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void msi_init_rfkill(struct work_struct *ignored)
|
|
{
|
|
if (rfk_wlan) {
|
|
rfkill_set_sw_state(rfk_wlan, !wlan_s);
|
|
rfkill_wlan_set(NULL, !wlan_s);
|
|
}
|
|
if (rfk_bluetooth) {
|
|
rfkill_set_sw_state(rfk_bluetooth, !bluetooth_s);
|
|
rfkill_bluetooth_set(NULL, !bluetooth_s);
|
|
}
|
|
if (rfk_threeg) {
|
|
rfkill_set_sw_state(rfk_threeg, !threeg_s);
|
|
rfkill_threeg_set(NULL, !threeg_s);
|
|
}
|
|
}
|
|
static DECLARE_DELAYED_WORK(msi_rfkill_init, msi_init_rfkill);
|
|
|
|
static int rfkill_init(struct platform_device *sdev)
|
|
{
|
|
/* add rfkill */
|
|
int retval;
|
|
|
|
/* keep the hardware wireless state */
|
|
get_wireless_state_ec_standard();
|
|
|
|
rfk_bluetooth = rfkill_alloc("msi-bluetooth", &sdev->dev,
|
|
RFKILL_TYPE_BLUETOOTH,
|
|
&rfkill_bluetooth_ops, NULL);
|
|
if (!rfk_bluetooth) {
|
|
retval = -ENOMEM;
|
|
goto err_bluetooth;
|
|
}
|
|
retval = rfkill_register(rfk_bluetooth);
|
|
if (retval)
|
|
goto err_bluetooth;
|
|
|
|
rfk_wlan = rfkill_alloc("msi-wlan", &sdev->dev, RFKILL_TYPE_WLAN,
|
|
&rfkill_wlan_ops, NULL);
|
|
if (!rfk_wlan) {
|
|
retval = -ENOMEM;
|
|
goto err_wlan;
|
|
}
|
|
retval = rfkill_register(rfk_wlan);
|
|
if (retval)
|
|
goto err_wlan;
|
|
|
|
if (threeg_exists) {
|
|
rfk_threeg = rfkill_alloc("msi-threeg", &sdev->dev,
|
|
RFKILL_TYPE_WWAN, &rfkill_threeg_ops, NULL);
|
|
if (!rfk_threeg) {
|
|
retval = -ENOMEM;
|
|
goto err_threeg;
|
|
}
|
|
retval = rfkill_register(rfk_threeg);
|
|
if (retval)
|
|
goto err_threeg;
|
|
}
|
|
|
|
/* schedule to run rfkill state initial */
|
|
if (quirks->ec_delay) {
|
|
schedule_delayed_work(&msi_rfkill_init,
|
|
round_jiffies_relative(1 * HZ));
|
|
} else
|
|
schedule_work(&msi_rfkill_work);
|
|
|
|
return 0;
|
|
|
|
err_threeg:
|
|
rfkill_destroy(rfk_threeg);
|
|
if (rfk_wlan)
|
|
rfkill_unregister(rfk_wlan);
|
|
err_wlan:
|
|
rfkill_destroy(rfk_wlan);
|
|
if (rfk_bluetooth)
|
|
rfkill_unregister(rfk_bluetooth);
|
|
err_bluetooth:
|
|
rfkill_destroy(rfk_bluetooth);
|
|
|
|
return retval;
|
|
}
|
|
|
|
#ifdef CONFIG_PM_SLEEP
|
|
static int msi_laptop_resume(struct device *device)
|
|
{
|
|
u8 data;
|
|
int result;
|
|
|
|
if (!quirks->load_scm_model)
|
|
return 0;
|
|
|
|
/* set load SCM to disable hardware control by fn key */
|
|
result = ec_read(MSI_STANDARD_EC_SCM_LOAD_ADDRESS, &data);
|
|
if (result < 0)
|
|
return result;
|
|
|
|
result = ec_write(MSI_STANDARD_EC_SCM_LOAD_ADDRESS,
|
|
data | MSI_STANDARD_EC_SCM_LOAD_MASK);
|
|
if (result < 0)
|
|
return result;
|
|
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
static int __init msi_laptop_input_setup(void)
|
|
{
|
|
int err;
|
|
|
|
msi_laptop_input_dev = input_allocate_device();
|
|
if (!msi_laptop_input_dev)
|
|
return -ENOMEM;
|
|
|
|
msi_laptop_input_dev->name = "MSI Laptop hotkeys";
|
|
msi_laptop_input_dev->phys = "msi-laptop/input0";
|
|
msi_laptop_input_dev->id.bustype = BUS_HOST;
|
|
|
|
err = sparse_keymap_setup(msi_laptop_input_dev,
|
|
msi_laptop_keymap, NULL);
|
|
if (err)
|
|
goto err_free_dev;
|
|
|
|
err = input_register_device(msi_laptop_input_dev);
|
|
if (err)
|
|
goto err_free_dev;
|
|
|
|
return 0;
|
|
|
|
err_free_dev:
|
|
input_free_device(msi_laptop_input_dev);
|
|
return err;
|
|
}
|
|
|
|
static int __init load_scm_model_init(struct platform_device *sdev)
|
|
{
|
|
u8 data;
|
|
int result;
|
|
|
|
if (!quirks->ec_read_only) {
|
|
/* allow userland write sysfs file */
|
|
dev_attr_bluetooth.store = store_bluetooth;
|
|
dev_attr_wlan.store = store_wlan;
|
|
dev_attr_threeg.store = store_threeg;
|
|
dev_attr_bluetooth.attr.mode |= S_IWUSR;
|
|
dev_attr_wlan.attr.mode |= S_IWUSR;
|
|
dev_attr_threeg.attr.mode |= S_IWUSR;
|
|
}
|
|
|
|
/* disable hardware control by fn key */
|
|
result = ec_read(MSI_STANDARD_EC_SCM_LOAD_ADDRESS, &data);
|
|
if (result < 0)
|
|
return result;
|
|
|
|
result = ec_write(MSI_STANDARD_EC_SCM_LOAD_ADDRESS,
|
|
data | MSI_STANDARD_EC_SCM_LOAD_MASK);
|
|
if (result < 0)
|
|
return result;
|
|
|
|
/* initial rfkill */
|
|
result = rfkill_init(sdev);
|
|
if (result < 0)
|
|
goto fail_rfkill;
|
|
|
|
/* setup input device */
|
|
result = msi_laptop_input_setup();
|
|
if (result)
|
|
goto fail_input;
|
|
|
|
result = i8042_install_filter(msi_laptop_i8042_filter);
|
|
if (result) {
|
|
pr_err("Unable to install key filter\n");
|
|
goto fail_filter;
|
|
}
|
|
|
|
return 0;
|
|
|
|
fail_filter:
|
|
input_unregister_device(msi_laptop_input_dev);
|
|
|
|
fail_input:
|
|
rfkill_cleanup();
|
|
|
|
fail_rfkill:
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
static int __init msi_init(void)
|
|
{
|
|
int ret;
|
|
|
|
if (acpi_disabled)
|
|
return -ENODEV;
|
|
|
|
dmi_check_system(msi_dmi_table);
|
|
if (!quirks)
|
|
/* quirks may be NULL if no match in DMI table */
|
|
quirks = &quirk_load_scm_model;
|
|
if (force)
|
|
quirks = &quirk_old_ec_model;
|
|
|
|
if (!quirks->old_ec_model)
|
|
get_threeg_exists();
|
|
|
|
if (auto_brightness < 0 || auto_brightness > 2)
|
|
return -EINVAL;
|
|
|
|
/* Register backlight stuff */
|
|
|
|
if (quirks->old_ec_model ||
|
|
acpi_video_get_backlight_type() == acpi_backlight_vendor) {
|
|
struct backlight_properties props;
|
|
memset(&props, 0, sizeof(struct backlight_properties));
|
|
props.type = BACKLIGHT_PLATFORM;
|
|
props.max_brightness = MSI_LCD_LEVEL_MAX - 1;
|
|
msibl_device = backlight_device_register("msi-laptop-bl", NULL,
|
|
NULL, &msibl_ops,
|
|
&props);
|
|
if (IS_ERR(msibl_device))
|
|
return PTR_ERR(msibl_device);
|
|
}
|
|
|
|
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_device_add;
|
|
|
|
if (quirks->load_scm_model && (load_scm_model_init(msipf_device) < 0)) {
|
|
ret = -EINVAL;
|
|
goto fail_scm_model_init;
|
|
}
|
|
|
|
ret = sysfs_create_group(&msipf_device->dev.kobj,
|
|
&msipf_attribute_group);
|
|
if (ret)
|
|
goto fail_create_group;
|
|
|
|
if (!quirks->old_ec_model) {
|
|
if (threeg_exists)
|
|
ret = device_create_file(&msipf_device->dev,
|
|
&dev_attr_threeg);
|
|
if (ret)
|
|
goto fail_create_attr;
|
|
} else {
|
|
ret = sysfs_create_group(&msipf_device->dev.kobj,
|
|
&msipf_old_attribute_group);
|
|
if (ret)
|
|
goto fail_create_attr;
|
|
|
|
/* 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);
|
|
}
|
|
|
|
pr_info("driver " MSI_DRIVER_VERSION " successfully loaded\n");
|
|
|
|
return 0;
|
|
|
|
fail_create_attr:
|
|
sysfs_remove_group(&msipf_device->dev.kobj, &msipf_attribute_group);
|
|
fail_create_group:
|
|
if (quirks->load_scm_model) {
|
|
i8042_remove_filter(msi_laptop_i8042_filter);
|
|
cancel_delayed_work_sync(&msi_rfkill_dwork);
|
|
cancel_work_sync(&msi_rfkill_work);
|
|
rfkill_cleanup();
|
|
}
|
|
fail_scm_model_init:
|
|
platform_device_del(msipf_device);
|
|
fail_device_add:
|
|
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)
|
|
{
|
|
if (quirks->load_scm_model) {
|
|
i8042_remove_filter(msi_laptop_i8042_filter);
|
|
input_unregister_device(msi_laptop_input_dev);
|
|
cancel_delayed_work_sync(&msi_rfkill_dwork);
|
|
cancel_work_sync(&msi_rfkill_work);
|
|
rfkill_cleanup();
|
|
}
|
|
|
|
sysfs_remove_group(&msipf_device->dev.kobj, &msipf_attribute_group);
|
|
if (!quirks->old_ec_model && threeg_exists)
|
|
device_remove_file(&msipf_device->dev, &dev_attr_threeg);
|
|
platform_device_unregister(msipf_device);
|
|
platform_driver_unregister(&msipf_driver);
|
|
backlight_device_unregister(msibl_device);
|
|
|
|
if (quirks->old_ec_model) {
|
|
/* Enable automatic brightness control again */
|
|
if (auto_brightness != 2)
|
|
set_auto_brightness(1);
|
|
}
|
|
|
|
pr_info("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:*");
|
|
MODULE_ALIAS("dmi:*:svnMICRO-STARINTERNATIONAL*:pnMS-N034:*");
|
|
MODULE_ALIAS("dmi:*:svnMICRO-STARINTERNATIONAL*:pnMS-N051:*");
|
|
MODULE_ALIAS("dmi:*:svnMICRO-STARINTERNATIONAL*:pnMS-N014:*");
|
|
MODULE_ALIAS("dmi:*:svnMicro-StarInternational*:pnCR620:*");
|
|
MODULE_ALIAS("dmi:*:svnMicro-StarInternational*:pnU270series:*");
|
|
MODULE_ALIAS("dmi:*:svnMICRO-STARINTERNATIONAL*:pnU90/U100:*");
|