File 0006-asus-vivobook-s15-add-wip-EC-driver.patch of Package linux-qcom-laptops
From c3c965bee4e00282713449372bdac923b25d3820 Mon Sep 17 00:00:00 2001
From: binarycraft007 <elliot.huang.signed@gmail.com>
Date: Thu, 12 Feb 2026 01:01:06 +0100
Subject: [PATCH 6/9] asus-vivobook-s15: add wip EC driver
---
.../dts/qcom/x1e80100-asus-vivobook-s15.dts | 7 +-
drivers/platform/arm64/Kconfig | 15 ++
drivers/platform/arm64/Makefile | 1 +
drivers/platform/arm64/asus-vivobook-s15.c | 219 ++++++++++++++++++
4 files changed, 241 insertions(+), 1 deletion(-)
create mode 100644 drivers/platform/arm64/asus-vivobook-s15.c
diff --git a/arch/arm64/boot/dts/qcom/x1e80100-asus-vivobook-s15.dts b/arch/arm64/boot/dts/qcom/x1e80100-asus-vivobook-s15.dts
index 238798a4d..7e5b4416d 100644
--- a/arch/arm64/boot/dts/qcom/x1e80100-asus-vivobook-s15.dts
+++ b/arch/arm64/boot/dts/qcom/x1e80100-asus-vivobook-s15.dts
@@ -936,7 +936,12 @@ eusb6_repeater: redriver@4f {
pinctrl-names = "default";
};
- /* EC @ 0x76 */
+ asus_ec: ec@76 {
+ compatible = "asus,vivobook-s15-ec";
+ reg = <0x76>;
+ /* List of thermal zone names to monitor */
+ thermal-zone-names = "cpu0-0-top-thermal", "gpuss-0-thermal";
+ };
};
&i2c7 {
diff --git a/drivers/platform/arm64/Kconfig b/drivers/platform/arm64/Kconfig
index 10f905d7d..fa4ceb3f3 100644
--- a/drivers/platform/arm64/Kconfig
+++ b/drivers/platform/arm64/Kconfig
@@ -90,4 +90,19 @@ config EC_LENOVO_THINKPAD_T14S
Say M or Y here to include this support.
+config EC_ASUS_VIVOBOOK_S15
+ tristate "Asus Vivobook S15 Embedded Controller driver"
+ depends on ARCH_QCOM || COMPILE_TEST
+ depends on I2C
+ depends on INPUT
+ help
+ Driver for the Embedded Controller in the Qualcomm Snapdragon-based
+ Asus Vivobook S15, which provides access to fan control.
+
+ This driver provides support for the mentioned laptop where this
+ information is not properly exposed via the standard Qualcomm
+ devices.
+
+ Say M or Y here to include this support.
+
endif # ARM64_PLATFORM_DEVICES
diff --git a/drivers/platform/arm64/Makefile b/drivers/platform/arm64/Makefile
index 60c131cff..bd6dc3128 100644
--- a/drivers/platform/arm64/Makefile
+++ b/drivers/platform/arm64/Makefile
@@ -9,3 +9,4 @@ obj-$(CONFIG_EC_ACER_ASPIRE1) += acer-aspire1-ec.o
obj-$(CONFIG_EC_HUAWEI_GAOKUN) += huawei-gaokun-ec.o
obj-$(CONFIG_EC_LENOVO_YOGA_C630) += lenovo-yoga-c630.o
obj-$(CONFIG_EC_LENOVO_THINKPAD_T14S) += lenovo-thinkpad-t14s.o
+obj-$(CONFIG_EC_ASUS_VIVOBOOK_S15) += asus-vivobook-s15.o
diff --git a/drivers/platform/arm64/asus-vivobook-s15.c b/drivers/platform/arm64/asus-vivobook-s15.c
new file mode 100644
index 000000000..d38d41a58
--- /dev/null
+++ b/drivers/platform/arm64/asus-vivobook-s15.c
@@ -0,0 +1,219 @@
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/i2c.h>
+#include <linux/thermal.h>
+#include <linux/workqueue.h>
+#include <linux/slab.h>
+#include <linux/of.h>
+#include <linux/of_device.h>
+
+#define DRIVER_NAME "asus_vivobook_ec"
+
+/* I2C Command Definitions */
+#define CMD_SET_TEMP 0x20
+#define CMD_SET_SUSPEND 0x23
+
+/* Constants */
+#define HEARTBEAT_PERIOD_MS 2000
+
+struct asus_ec_tz {
+ struct list_head list;
+ struct thermal_zone_device *tz;
+};
+
+struct asus_ec_data {
+ struct i2c_client *client;
+ struct delayed_work heartbeat_work;
+ struct list_head tz_list;
+ struct mutex lock;
+};
+
+static int asus_ec_send_temp(struct asus_ec_data *data)
+{
+ struct asus_ec_tz *node;
+ int max_temp = 0; /* milli-Celsius */
+ int ret;
+ int temp_deci;
+ u8 buf[5];
+
+ /* Find maximum temperature across all monitored zones */
+ list_for_each_entry(node, &data->tz_list, list) {
+ int temp;
+ ret = thermal_zone_get_temp(node->tz, &temp);
+ if (ret == 0) {
+ if (temp > max_temp)
+ max_temp = temp;
+ }
+ }
+
+ /*
+ * Convert to deci-Celsius (0.1 C) as expected by EC.
+ * Kernel temp is milli-Celsius (1000 = 1 C).
+ * So deci-Celsius = milli-Celsius / 100.
+ */
+ temp_deci = max_temp / 100;
+ if (temp_deci < 0)
+ temp_deci = 0;
+ if (temp_deci > 2000)
+ temp_deci = 2000;
+
+ buf[0] = CMD_SET_TEMP;
+ buf[1] = 0x01;
+ buf[2] = 0x02;
+ buf[3] = temp_deci & 0xff;
+ buf[4] = (temp_deci >> 8) & 0xff;
+
+ ret = i2c_master_send(data->client, buf, sizeof(buf));
+ if (ret < 0)
+ dev_err_ratelimited(&data->client->dev,
+ "Failed to send temp: %d", ret);
+
+ return ret;
+}
+
+static void asus_ec_heartbeat(struct work_struct *work)
+{
+ struct asus_ec_data *data =
+ container_of(work, struct asus_ec_data, heartbeat_work.work);
+
+ mutex_lock(&data->lock);
+ asus_ec_send_temp(data);
+ mutex_unlock(&data->lock);
+
+ schedule_delayed_work(&data->heartbeat_work,
+ msecs_to_jiffies(HEARTBEAT_PERIOD_MS));
+}
+
+static int asus_ec_set_suspend(struct asus_ec_data *data, bool active)
+{
+ u8 buf[2];
+ int ret;
+
+ buf[0] = CMD_SET_SUSPEND;
+ buf[1] = active ? 0x01 : 0x00;
+
+ ret = i2c_master_send(data->client, buf, sizeof(buf));
+ if (ret < 0)
+ dev_err(&data->client->dev, "Failed to set suspend mode %d: %d",
+ active, ret);
+
+ return ret;
+}
+
+static int asus_ec_probe(struct i2c_client *client)
+{
+ struct asus_ec_data *data;
+ struct device_node *np = client->dev.of_node;
+ const char *name;
+ int count;
+ int j;
+
+ if (!np)
+ return -ENODEV;
+
+ data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
+ if (!data)
+ return -ENOMEM;
+
+ data->client = client;
+ mutex_init(&data->lock);
+ INIT_LIST_HEAD(&data->tz_list);
+ INIT_DELAYED_WORK(&data->heartbeat_work, asus_ec_heartbeat);
+ i2c_set_clientdata(client, data);
+
+ /*
+ * This allows specifying specific thermal zones like "cpu0-0-top-thermal"
+ * in the DTS.
+ */
+ count = of_property_count_strings(np, "thermal-zone-names");
+ if (count < 0)
+ count = 0;
+
+ for (j = 0; j < count; j++) {
+ of_property_read_string_index(np, "thermal-zone-names", j,
+ &name);
+
+ /*
+ * Note: thermal_zone_get_zone_by_name is exported in many kernels.
+ * If not available, additional logic would be needed to iterate
+ * thermal zones.
+ */
+ struct thermal_zone_device *tz =
+ thermal_zone_get_zone_by_name(name);
+ if (!IS_ERR(tz) && tz) {
+ struct asus_ec_tz *item = devm_kzalloc(
+ &client->dev, sizeof(*item), GFP_KERNEL);
+ if (item) {
+ item->tz = tz;
+ list_add_tail(&item->list, &data->tz_list);
+ dev_info(&client->dev,
+ "Monitoring thermal zone: %s", name);
+ }
+ } else {
+ dev_warn(&client->dev,
+ "Could not find thermal zone: %s", name);
+ }
+ }
+
+ if (list_empty(&data->tz_list))
+ dev_warn(&client->dev,
+ "No thermal zones found. Sending 0 temp.");
+
+ dev_info(&client->dev, "Starting EC heartbeat");
+ schedule_delayed_work(&data->heartbeat_work, 0);
+
+ return 0;
+}
+
+static void asus_ec_remove(struct i2c_client *client)
+{
+ struct asus_ec_data *data = i2c_get_clientdata(client);
+
+ cancel_delayed_work_sync(&data->heartbeat_work);
+}
+
+static int __maybe_unused asus_ec_suspend(struct device *dev)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct asus_ec_data *data = i2c_get_clientdata(client);
+
+ cancel_delayed_work_sync(&data->heartbeat_work);
+ asus_ec_set_suspend(data, true);
+
+ return 0;
+}
+
+static int __maybe_unused asus_ec_resume(struct device *dev)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct asus_ec_data *data = i2c_get_clientdata(client);
+
+ asus_ec_set_suspend(data, false);
+ schedule_delayed_work(&data->heartbeat_work, 0);
+
+ return 0;
+}
+
+static SIMPLE_DEV_PM_OPS(asus_ec_pm_ops, asus_ec_suspend, asus_ec_resume);
+
+static const struct of_device_id asus_ec_of_match[] = {
+ { .compatible = "asus,vivobook-s15-ec" },
+ {}
+};
+MODULE_DEVICE_TABLE(of, asus_ec_of_match);
+
+static struct i2c_driver asus_ec_driver = {
+ .driver = {
+ .name = DRIVER_NAME,
+ .of_match_table = asus_ec_of_match,
+ .pm = &asus_ec_pm_ops,
+ },
+ .probe = asus_ec_probe,
+ .remove = asus_ec_remove,
+};
+
+module_i2c_driver(asus_ec_driver);
+
+MODULE_AUTHOR("Elliot Huang");
+MODULE_DESCRIPTION("ASUS Vivobook S 15 EC Driver");
+MODULE_LICENSE("GPL");
--
2.53.0