mirror of https://github.com/Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1179 lines
27 KiB
1179 lines
27 KiB
// 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:*");
|
|
|