summaryrefslogtreecommitdiffstats
path: root/drivers/platform/x86/msi-ec.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/platform/x86/msi-ec.c')
-rw-r--r--drivers/platform/x86/msi-ec.c896
1 files changed, 896 insertions, 0 deletions
diff --git a/drivers/platform/x86/msi-ec.c b/drivers/platform/x86/msi-ec.c
new file mode 100644
index 0000000000..492eb383ee
--- /dev/null
+++ b/drivers/platform/x86/msi-ec.c
@@ -0,0 +1,896 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/*
+ * msi-ec: MSI laptops' embedded controller driver.
+ *
+ * This driver allows various MSI laptops' functionalities to be
+ * controlled from userspace.
+ *
+ * It contains EC memory configurations for different firmware versions
+ * and exports battery charge thresholds to userspace.
+ *
+ * Copyright (C) 2023 Jose Angel Pastrana <japp0005@red.ujaen.es>
+ * Copyright (C) 2023 Aakash Singh <mail@singhaakash.dev>
+ * Copyright (C) 2023 Nikita Kravets <teackot@gmail.com>
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include "msi-ec.h"
+
+#include <acpi/battery.h>
+#include <linux/acpi.h>
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/seq_file.h>
+#include <linux/string.h>
+
+#define SM_ECO_NAME "eco"
+#define SM_COMFORT_NAME "comfort"
+#define SM_SPORT_NAME "sport"
+#define SM_TURBO_NAME "turbo"
+
+#define FM_AUTO_NAME "auto"
+#define FM_SILENT_NAME "silent"
+#define FM_BASIC_NAME "basic"
+#define FM_ADVANCED_NAME "advanced"
+
+static const char * const ALLOWED_FW_0[] __initconst = {
+ "14C1EMS1.012",
+ "14C1EMS1.101",
+ "14C1EMS1.102",
+ NULL
+};
+
+static struct msi_ec_conf CONF0 __initdata = {
+ .allowed_fw = ALLOWED_FW_0,
+ .charge_control = {
+ .address = 0xef,
+ .offset_start = 0x8a,
+ .offset_end = 0x80,
+ .range_min = 0x8a,
+ .range_max = 0xe4,
+ },
+ .webcam = {
+ .address = 0x2e,
+ .block_address = 0x2f,
+ .bit = 1,
+ },
+ .fn_super_swap = {
+ .address = 0xbf,
+ .bit = 4,
+ },
+ .cooler_boost = {
+ .address = 0x98,
+ .bit = 7,
+ },
+ .shift_mode = {
+ .address = 0xf2,
+ .modes = {
+ { SM_ECO_NAME, 0xc2 },
+ { SM_COMFORT_NAME, 0xc1 },
+ { SM_SPORT_NAME, 0xc0 },
+ MSI_EC_MODE_NULL
+ },
+ },
+ .super_battery = {
+ .address = MSI_EC_ADDR_UNKNOWN, // 0xd5 needs testing
+ },
+ .fan_mode = {
+ .address = 0xf4,
+ .modes = {
+ { FM_AUTO_NAME, 0x0d },
+ { FM_SILENT_NAME, 0x1d },
+ { FM_BASIC_NAME, 0x4d },
+ { FM_ADVANCED_NAME, 0x8d },
+ MSI_EC_MODE_NULL
+ },
+ },
+ .cpu = {
+ .rt_temp_address = 0x68,
+ .rt_fan_speed_address = 0x71,
+ .rt_fan_speed_base_min = 0x19,
+ .rt_fan_speed_base_max = 0x37,
+ .bs_fan_speed_address = 0x89,
+ .bs_fan_speed_base_min = 0x00,
+ .bs_fan_speed_base_max = 0x0f,
+ },
+ .gpu = {
+ .rt_temp_address = 0x80,
+ .rt_fan_speed_address = 0x89,
+ },
+ .leds = {
+ .micmute_led_address = 0x2b,
+ .mute_led_address = 0x2c,
+ .bit = 2,
+ },
+ .kbd_bl = {
+ .bl_mode_address = 0x2c, // ?
+ .bl_modes = { 0x00, 0x08 }, // ?
+ .max_mode = 1, // ?
+ .bl_state_address = 0xf3,
+ .state_base_value = 0x80,
+ .max_state = 3,
+ },
+};
+
+static const char * const ALLOWED_FW_1[] __initconst = {
+ "17F2EMS1.103",
+ "17F2EMS1.104",
+ "17F2EMS1.106",
+ "17F2EMS1.107",
+ NULL
+};
+
+static struct msi_ec_conf CONF1 __initdata = {
+ .allowed_fw = ALLOWED_FW_1,
+ .charge_control = {
+ .address = 0xef,
+ .offset_start = 0x8a,
+ .offset_end = 0x80,
+ .range_min = 0x8a,
+ .range_max = 0xe4,
+ },
+ .webcam = {
+ .address = 0x2e,
+ .block_address = 0x2f,
+ .bit = 1,
+ },
+ .fn_super_swap = {
+ .address = 0xbf,
+ .bit = 4,
+ },
+ .cooler_boost = {
+ .address = 0x98,
+ .bit = 7,
+ },
+ .shift_mode = {
+ .address = 0xf2,
+ .modes = {
+ { SM_ECO_NAME, 0xc2 },
+ { SM_COMFORT_NAME, 0xc1 },
+ { SM_SPORT_NAME, 0xc0 },
+ { SM_TURBO_NAME, 0xc4 },
+ MSI_EC_MODE_NULL
+ },
+ },
+ .super_battery = {
+ .address = MSI_EC_ADDR_UNKNOWN,
+ },
+ .fan_mode = {
+ .address = 0xf4,
+ .modes = {
+ { FM_AUTO_NAME, 0x0d },
+ { FM_BASIC_NAME, 0x4d },
+ { FM_ADVANCED_NAME, 0x8d },
+ MSI_EC_MODE_NULL
+ },
+ },
+ .cpu = {
+ .rt_temp_address = 0x68,
+ .rt_fan_speed_address = 0x71,
+ .rt_fan_speed_base_min = 0x19,
+ .rt_fan_speed_base_max = 0x37,
+ .bs_fan_speed_address = 0x89,
+ .bs_fan_speed_base_min = 0x00,
+ .bs_fan_speed_base_max = 0x0f,
+ },
+ .gpu = {
+ .rt_temp_address = 0x80,
+ .rt_fan_speed_address = 0x89,
+ },
+ .leds = {
+ .micmute_led_address = 0x2b,
+ .mute_led_address = 0x2c,
+ .bit = 2,
+ },
+ .kbd_bl = {
+ .bl_mode_address = 0x2c, // ?
+ .bl_modes = { 0x00, 0x08 }, // ?
+ .max_mode = 1, // ?
+ .bl_state_address = 0xf3,
+ .state_base_value = 0x80,
+ .max_state = 3,
+ },
+};
+
+static const char * const ALLOWED_FW_2[] __initconst = {
+ "1552EMS1.118",
+ NULL
+};
+
+static struct msi_ec_conf CONF2 __initdata = {
+ .allowed_fw = ALLOWED_FW_2,
+ .charge_control = {
+ .address = 0xd7,
+ .offset_start = 0x8a,
+ .offset_end = 0x80,
+ .range_min = 0x8a,
+ .range_max = 0xe4,
+ },
+ .webcam = {
+ .address = 0x2e,
+ .block_address = 0x2f,
+ .bit = 1,
+ },
+ .fn_super_swap = {
+ .address = 0xe8,
+ .bit = 4,
+ },
+ .cooler_boost = {
+ .address = 0x98,
+ .bit = 7,
+ },
+ .shift_mode = {
+ .address = 0xf2,
+ .modes = {
+ { SM_ECO_NAME, 0xc2 },
+ { SM_COMFORT_NAME, 0xc1 },
+ { SM_SPORT_NAME, 0xc0 },
+ MSI_EC_MODE_NULL
+ },
+ },
+ .super_battery = {
+ .address = 0xeb,
+ .mask = 0x0f,
+ },
+ .fan_mode = {
+ .address = 0xd4,
+ .modes = {
+ { FM_AUTO_NAME, 0x0d },
+ { FM_SILENT_NAME, 0x1d },
+ { FM_BASIC_NAME, 0x4d },
+ { FM_ADVANCED_NAME, 0x8d },
+ MSI_EC_MODE_NULL
+ },
+ },
+ .cpu = {
+ .rt_temp_address = 0x68,
+ .rt_fan_speed_address = 0x71,
+ .rt_fan_speed_base_min = 0x19,
+ .rt_fan_speed_base_max = 0x37,
+ .bs_fan_speed_address = 0x89,
+ .bs_fan_speed_base_min = 0x00,
+ .bs_fan_speed_base_max = 0x0f,
+ },
+ .gpu = {
+ .rt_temp_address = 0x80,
+ .rt_fan_speed_address = 0x89,
+ },
+ .leds = {
+ .micmute_led_address = 0x2c,
+ .mute_led_address = 0x2d,
+ .bit = 1,
+ },
+ .kbd_bl = {
+ .bl_mode_address = 0x2c, // ?
+ .bl_modes = { 0x00, 0x08 }, // ?
+ .max_mode = 1, // ?
+ .bl_state_address = 0xd3,
+ .state_base_value = 0x80,
+ .max_state = 3,
+ },
+};
+
+static const char * const ALLOWED_FW_3[] __initconst = {
+ "1592EMS1.111",
+ NULL
+};
+
+static struct msi_ec_conf CONF3 __initdata = {
+ .allowed_fw = ALLOWED_FW_3,
+ .charge_control = {
+ .address = 0xd7,
+ .offset_start = 0x8a,
+ .offset_end = 0x80,
+ .range_min = 0x8a,
+ .range_max = 0xe4,
+ },
+ .webcam = {
+ .address = 0x2e,
+ .block_address = 0x2f,
+ .bit = 1,
+ },
+ .fn_super_swap = {
+ .address = 0xe8,
+ .bit = 4,
+ },
+ .cooler_boost = {
+ .address = 0x98,
+ .bit = 7,
+ },
+ .shift_mode = {
+ .address = 0xd2,
+ .modes = {
+ { SM_ECO_NAME, 0xc2 },
+ { SM_COMFORT_NAME, 0xc1 },
+ { SM_SPORT_NAME, 0xc0 },
+ MSI_EC_MODE_NULL
+ },
+ },
+ .super_battery = {
+ .address = 0xeb,
+ .mask = 0x0f,
+ },
+ .fan_mode = {
+ .address = 0xd4,
+ .modes = {
+ { FM_AUTO_NAME, 0x0d },
+ { FM_SILENT_NAME, 0x1d },
+ { FM_BASIC_NAME, 0x4d },
+ { FM_ADVANCED_NAME, 0x8d },
+ MSI_EC_MODE_NULL
+ },
+ },
+ .cpu = {
+ .rt_temp_address = 0x68,
+ .rt_fan_speed_address = 0xc9,
+ .rt_fan_speed_base_min = 0x19,
+ .rt_fan_speed_base_max = 0x37,
+ .bs_fan_speed_address = 0x89, // ?
+ .bs_fan_speed_base_min = 0x00,
+ .bs_fan_speed_base_max = 0x0f,
+ },
+ .gpu = {
+ .rt_temp_address = 0x80,
+ .rt_fan_speed_address = 0x89,
+ },
+ .leds = {
+ .micmute_led_address = 0x2b,
+ .mute_led_address = 0x2c,
+ .bit = 1,
+ },
+ .kbd_bl = {
+ .bl_mode_address = 0x2c, // ?
+ .bl_modes = { 0x00, 0x08 }, // ?
+ .max_mode = 1, // ?
+ .bl_state_address = 0xd3,
+ .state_base_value = 0x80,
+ .max_state = 3,
+ },
+};
+
+static const char * const ALLOWED_FW_4[] __initconst = {
+ "16V4EMS1.114",
+ NULL
+};
+
+static struct msi_ec_conf CONF4 __initdata = {
+ .allowed_fw = ALLOWED_FW_4,
+ .charge_control = {
+ .address = 0xd7,
+ .offset_start = 0x8a,
+ .offset_end = 0x80,
+ .range_min = 0x8a,
+ .range_max = 0xe4,
+ },
+ .webcam = {
+ .address = 0x2e,
+ .block_address = 0x2f,
+ .bit = 1,
+ },
+ .fn_super_swap = {
+ .address = MSI_EC_ADDR_UNKNOWN, // supported, but unknown
+ .bit = 4,
+ },
+ .cooler_boost = {
+ .address = 0x98,
+ .bit = 7,
+ },
+ .shift_mode = {
+ .address = 0xd2,
+ .modes = {
+ { SM_ECO_NAME, 0xc2 },
+ { SM_COMFORT_NAME, 0xc1 },
+ { SM_SPORT_NAME, 0xc0 },
+ MSI_EC_MODE_NULL
+ },
+ },
+ .super_battery = { // may be supported, but address is unknown
+ .address = MSI_EC_ADDR_UNKNOWN,
+ .mask = 0x0f,
+ },
+ .fan_mode = {
+ .address = 0xd4,
+ .modes = {
+ { FM_AUTO_NAME, 0x0d },
+ { FM_SILENT_NAME, 0x1d },
+ { FM_ADVANCED_NAME, 0x8d },
+ MSI_EC_MODE_NULL
+ },
+ },
+ .cpu = {
+ .rt_temp_address = 0x68, // needs testing
+ .rt_fan_speed_address = 0x71, // needs testing
+ .rt_fan_speed_base_min = 0x19,
+ .rt_fan_speed_base_max = 0x37,
+ .bs_fan_speed_address = MSI_EC_ADDR_UNKNOWN,
+ .bs_fan_speed_base_min = 0x00,
+ .bs_fan_speed_base_max = 0x0f,
+ },
+ .gpu = {
+ .rt_temp_address = 0x80,
+ .rt_fan_speed_address = MSI_EC_ADDR_UNKNOWN,
+ },
+ .leds = {
+ .micmute_led_address = MSI_EC_ADDR_UNKNOWN,
+ .mute_led_address = MSI_EC_ADDR_UNKNOWN,
+ .bit = 1,
+ },
+ .kbd_bl = {
+ .bl_mode_address = MSI_EC_ADDR_UNKNOWN, // ?
+ .bl_modes = { 0x00, 0x08 }, // ?
+ .max_mode = 1, // ?
+ .bl_state_address = MSI_EC_ADDR_UNSUPP, // 0xd3, not functional
+ .state_base_value = 0x80,
+ .max_state = 3,
+ },
+};
+
+static const char * const ALLOWED_FW_5[] __initconst = {
+ "158LEMS1.103",
+ "158LEMS1.105",
+ "158LEMS1.106",
+ NULL
+};
+
+static struct msi_ec_conf CONF5 __initdata = {
+ .allowed_fw = ALLOWED_FW_5,
+ .charge_control = {
+ .address = 0xef,
+ .offset_start = 0x8a,
+ .offset_end = 0x80,
+ .range_min = 0x8a,
+ .range_max = 0xe4,
+ },
+ .webcam = {
+ .address = 0x2e,
+ .block_address = 0x2f,
+ .bit = 1,
+ },
+ .fn_super_swap = { // todo: reverse
+ .address = 0xbf,
+ .bit = 4,
+ },
+ .cooler_boost = {
+ .address = 0x98,
+ .bit = 7,
+ },
+ .shift_mode = {
+ .address = 0xf2,
+ .modes = {
+ { SM_ECO_NAME, 0xc2 },
+ { SM_COMFORT_NAME, 0xc1 },
+ { SM_TURBO_NAME, 0xc4 },
+ MSI_EC_MODE_NULL
+ },
+ },
+ .super_battery = { // unsupported?
+ .address = MSI_EC_ADDR_UNKNOWN,
+ .mask = 0x0f,
+ },
+ .fan_mode = {
+ .address = 0xf4,
+ .modes = {
+ { FM_AUTO_NAME, 0x0d },
+ { FM_SILENT_NAME, 0x1d },
+ { FM_ADVANCED_NAME, 0x8d },
+ MSI_EC_MODE_NULL
+ },
+ },
+ .cpu = {
+ .rt_temp_address = 0x68, // needs testing
+ .rt_fan_speed_address = 0x71, // needs testing
+ .rt_fan_speed_base_min = 0x19,
+ .rt_fan_speed_base_max = 0x37,
+ .bs_fan_speed_address = MSI_EC_ADDR_UNSUPP,
+ .bs_fan_speed_base_min = 0x00,
+ .bs_fan_speed_base_max = 0x0f,
+ },
+ .gpu = {
+ .rt_temp_address = MSI_EC_ADDR_UNKNOWN,
+ .rt_fan_speed_address = MSI_EC_ADDR_UNKNOWN,
+ },
+ .leds = {
+ .micmute_led_address = 0x2b,
+ .mute_led_address = 0x2c,
+ .bit = 2,
+ },
+ .kbd_bl = {
+ .bl_mode_address = MSI_EC_ADDR_UNKNOWN, // ?
+ .bl_modes = { 0x00, 0x08 }, // ?
+ .max_mode = 1, // ?
+ .bl_state_address = MSI_EC_ADDR_UNSUPP, // 0xf3, not functional
+ .state_base_value = 0x80,
+ .max_state = 3,
+ },
+};
+
+static const char * const ALLOWED_FW_6[] __initconst = {
+ "1542EMS1.102",
+ "1542EMS1.104",
+ NULL
+};
+
+static struct msi_ec_conf CONF6 __initdata = {
+ .allowed_fw = ALLOWED_FW_6,
+ .charge_control = {
+ .address = 0xef,
+ .offset_start = 0x8a,
+ .offset_end = 0x80,
+ .range_min = 0x8a,
+ .range_max = 0xe4,
+ },
+ .webcam = {
+ .address = 0x2e,
+ .block_address = MSI_EC_ADDR_UNSUPP,
+ .bit = 1,
+ },
+ .fn_super_swap = {
+ .address = 0xbf, // todo: reverse
+ .bit = 4,
+ },
+ .cooler_boost = {
+ .address = 0x98,
+ .bit = 7,
+ },
+ .shift_mode = {
+ .address = 0xf2,
+ .modes = {
+ { SM_ECO_NAME, 0xc2 },
+ { SM_COMFORT_NAME, 0xc1 },
+ { SM_SPORT_NAME, 0xc0 },
+ { SM_TURBO_NAME, 0xc4 },
+ MSI_EC_MODE_NULL
+ },
+ },
+ .super_battery = {
+ .address = 0xd5,
+ .mask = 0x0f,
+ },
+ .fan_mode = {
+ .address = 0xf4,
+ .modes = {
+ { FM_AUTO_NAME, 0x0d },
+ { FM_SILENT_NAME, 0x1d },
+ { FM_ADVANCED_NAME, 0x8d },
+ MSI_EC_MODE_NULL
+ },
+ },
+ .cpu = {
+ .rt_temp_address = 0x68,
+ .rt_fan_speed_address = 0xc9,
+ .rt_fan_speed_base_min = 0x19,
+ .rt_fan_speed_base_max = 0x37,
+ .bs_fan_speed_address = MSI_EC_ADDR_UNSUPP,
+ .bs_fan_speed_base_min = 0x00,
+ .bs_fan_speed_base_max = 0x0f,
+ },
+ .gpu = {
+ .rt_temp_address = 0x80,
+ .rt_fan_speed_address = MSI_EC_ADDR_UNKNOWN,
+ },
+ .leds = {
+ .micmute_led_address = MSI_EC_ADDR_UNSUPP,
+ .mute_led_address = MSI_EC_ADDR_UNSUPP,
+ .bit = 2,
+ },
+ .kbd_bl = {
+ .bl_mode_address = MSI_EC_ADDR_UNKNOWN, // ?
+ .bl_modes = { 0x00, 0x08 }, // ?
+ .max_mode = 1, // ?
+ .bl_state_address = MSI_EC_ADDR_UNSUPP, // 0xf3, not functional
+ .state_base_value = 0x80,
+ .max_state = 3,
+ },
+};
+
+static const char * const ALLOWED_FW_7[] __initconst = {
+ "17FKEMS1.108",
+ "17FKEMS1.109",
+ "17FKEMS1.10A",
+ NULL
+};
+
+static struct msi_ec_conf CONF7 __initdata = {
+ .allowed_fw = ALLOWED_FW_7,
+ .charge_control = {
+ .address = 0xef,
+ .offset_start = 0x8a,
+ .offset_end = 0x80,
+ .range_min = 0x8a,
+ .range_max = 0xe4,
+ },
+ .webcam = {
+ .address = 0x2e,
+ .block_address = MSI_EC_ADDR_UNSUPP,
+ .bit = 1,
+ },
+ .fn_super_swap = {
+ .address = 0xbf, // needs testing
+ .bit = 4,
+ },
+ .cooler_boost = {
+ .address = 0x98,
+ .bit = 7,
+ },
+ .shift_mode = {
+ .address = 0xf2,
+ .modes = {
+ { SM_ECO_NAME, 0xc2 },
+ { SM_COMFORT_NAME, 0xc1 },
+ { SM_SPORT_NAME, 0xc0 },
+ { SM_TURBO_NAME, 0xc4 },
+ MSI_EC_MODE_NULL
+ },
+ },
+ .super_battery = {
+ .address = MSI_EC_ADDR_UNKNOWN, // 0xd5 but has its own wet of modes
+ .mask = 0x0f,
+ },
+ .fan_mode = {
+ .address = 0xf4,
+ .modes = {
+ { FM_AUTO_NAME, 0x0d }, // d may not be relevant
+ { FM_SILENT_NAME, 0x1d },
+ { FM_ADVANCED_NAME, 0x8d },
+ MSI_EC_MODE_NULL
+ },
+ },
+ .cpu = {
+ .rt_temp_address = 0x68,
+ .rt_fan_speed_address = 0xc9, // needs testing
+ .rt_fan_speed_base_min = 0x19,
+ .rt_fan_speed_base_max = 0x37,
+ .bs_fan_speed_address = MSI_EC_ADDR_UNSUPP,
+ .bs_fan_speed_base_min = 0x00,
+ .bs_fan_speed_base_max = 0x0f,
+ },
+ .gpu = {
+ .rt_temp_address = MSI_EC_ADDR_UNKNOWN,
+ .rt_fan_speed_address = MSI_EC_ADDR_UNKNOWN,
+ },
+ .leds = {
+ .micmute_led_address = MSI_EC_ADDR_UNSUPP,
+ .mute_led_address = 0x2c,
+ .bit = 2,
+ },
+ .kbd_bl = {
+ .bl_mode_address = MSI_EC_ADDR_UNKNOWN, // ?
+ .bl_modes = { 0x00, 0x08 }, // ?
+ .max_mode = 1, // ?
+ .bl_state_address = 0xf3,
+ .state_base_value = 0x80,
+ .max_state = 3,
+ },
+};
+
+static struct msi_ec_conf *CONFIGS[] __initdata = {
+ &CONF0,
+ &CONF1,
+ &CONF2,
+ &CONF3,
+ &CONF4,
+ &CONF5,
+ &CONF6,
+ &CONF7,
+ NULL
+};
+
+static struct msi_ec_conf conf; // current configuration
+
+/*
+ * Helper functions
+ */
+
+static int ec_read_seq(u8 addr, u8 *buf, u8 len)
+{
+ int result;
+
+ for (u8 i = 0; i < len; i++) {
+ result = ec_read(addr + i, buf + i);
+ if (result < 0)
+ return result;
+ }
+
+ return 0;
+}
+
+static int ec_get_firmware_version(u8 buf[MSI_EC_FW_VERSION_LENGTH + 1])
+{
+ int result;
+
+ memset(buf, 0, MSI_EC_FW_VERSION_LENGTH + 1);
+ result = ec_read_seq(MSI_EC_FW_VERSION_ADDRESS,
+ buf,
+ MSI_EC_FW_VERSION_LENGTH);
+ if (result < 0)
+ return result;
+
+ return MSI_EC_FW_VERSION_LENGTH + 1;
+}
+
+/*
+ * Sysfs power_supply subsystem
+ */
+
+static ssize_t charge_control_threshold_show(u8 offset,
+ struct device *device,
+ struct device_attribute *attr,
+ char *buf)
+{
+ u8 rdata;
+ int result;
+
+ result = ec_read(conf.charge_control.address, &rdata);
+ if (result < 0)
+ return result;
+
+ return sysfs_emit(buf, "%i\n", rdata - offset);
+}
+
+static ssize_t charge_control_threshold_store(u8 offset,
+ struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ u8 wdata;
+ int result;
+
+ result = kstrtou8(buf, 10, &wdata);
+ if (result < 0)
+ return result;
+
+ wdata += offset;
+ if (wdata < conf.charge_control.range_min ||
+ wdata > conf.charge_control.range_max)
+ return -EINVAL;
+
+ result = ec_write(conf.charge_control.address, wdata);
+ if (result < 0)
+ return result;
+
+ return count;
+}
+
+static ssize_t charge_control_start_threshold_show(struct device *device,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return charge_control_threshold_show(conf.charge_control.offset_start,
+ device, attr, buf);
+}
+
+static ssize_t charge_control_start_threshold_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ return charge_control_threshold_store(conf.charge_control.offset_start,
+ dev, attr, buf, count);
+}
+
+static ssize_t charge_control_end_threshold_show(struct device *device,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return charge_control_threshold_show(conf.charge_control.offset_end,
+ device, attr, buf);
+}
+
+static ssize_t charge_control_end_threshold_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ return charge_control_threshold_store(conf.charge_control.offset_end,
+ dev, attr, buf, count);
+}
+
+static DEVICE_ATTR_RW(charge_control_start_threshold);
+static DEVICE_ATTR_RW(charge_control_end_threshold);
+
+static struct attribute *msi_battery_attrs[] = {
+ &dev_attr_charge_control_start_threshold.attr,
+ &dev_attr_charge_control_end_threshold.attr,
+ NULL
+};
+
+ATTRIBUTE_GROUPS(msi_battery);
+
+static int msi_battery_add(struct power_supply *battery,
+ struct acpi_battery_hook *hook)
+{
+ return device_add_groups(&battery->dev, msi_battery_groups);
+}
+
+static int msi_battery_remove(struct power_supply *battery,
+ struct acpi_battery_hook *hook)
+{
+ device_remove_groups(&battery->dev, msi_battery_groups);
+ return 0;
+}
+
+static struct acpi_battery_hook battery_hook = {
+ .add_battery = msi_battery_add,
+ .remove_battery = msi_battery_remove,
+ .name = MSI_EC_DRIVER_NAME,
+};
+
+/*
+ * Module load/unload
+ */
+
+static const struct dmi_system_id msi_dmi_table[] __initconst __maybe_unused = {
+ {
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "MICRO-STAR INT"),
+ },
+ },
+ {
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International"),
+ },
+ },
+ {}
+};
+MODULE_DEVICE_TABLE(dmi, msi_dmi_table);
+
+static int __init load_configuration(void)
+{
+ int result;
+
+ u8 fw_version[MSI_EC_FW_VERSION_LENGTH + 1];
+
+ /* get firmware version */
+ result = ec_get_firmware_version(fw_version);
+ if (result < 0)
+ return result;
+
+ /* load the suitable configuration, if exists */
+ for (int i = 0; CONFIGS[i]; i++) {
+ if (match_string(CONFIGS[i]->allowed_fw, -1, fw_version) != -EINVAL) {
+ conf = *CONFIGS[i];
+ conf.allowed_fw = NULL;
+ return 0;
+ }
+ }
+
+ /* config not found */
+
+ for (int i = 0; i < MSI_EC_FW_VERSION_LENGTH; i++) {
+ if (!isgraph(fw_version[i])) {
+ pr_warn("Unable to find a valid firmware version!\n");
+ return -EOPNOTSUPP;
+ }
+ }
+
+ pr_warn("Firmware version is not supported: '%s'\n", fw_version);
+ return -EOPNOTSUPP;
+}
+
+static int __init msi_ec_init(void)
+{
+ int result;
+
+ result = load_configuration();
+ if (result < 0)
+ return result;
+
+ battery_hook_register(&battery_hook);
+ return 0;
+}
+
+static void __exit msi_ec_exit(void)
+{
+ battery_hook_unregister(&battery_hook);
+}
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Jose Angel Pastrana <japp0005@red.ujaen.es>");
+MODULE_AUTHOR("Aakash Singh <mail@singhaakash.dev>");
+MODULE_AUTHOR("Nikita Kravets <teackot@gmail.com>");
+MODULE_DESCRIPTION("MSI Embedded Controller");
+
+module_init(msi_ec_init);
+module_exit(msi_ec_exit);