diff options
Diffstat (limited to 'drivers/platform/arm64')
| -rw-r--r-- | drivers/platform/arm64/Kconfig | 106 | ||||
| -rw-r--r-- | drivers/platform/arm64/Makefile | 12 | ||||
| -rw-r--r-- | drivers/platform/arm64/acer-aspire1-ec.c | 562 | ||||
| -rw-r--r-- | drivers/platform/arm64/huawei-gaokun-ec.c | 827 | ||||
| -rw-r--r-- | drivers/platform/arm64/lenovo-thinkpad-t14s.c | 662 | ||||
| -rw-r--r-- | drivers/platform/arm64/lenovo-yoga-c630.c | 257 | ||||
| -rw-r--r-- | drivers/platform/arm64/qcom-hamoa-ec.c | 451 |
7 files changed, 2877 insertions, 0 deletions
diff --git a/drivers/platform/arm64/Kconfig b/drivers/platform/arm64/Kconfig new file mode 100644 index 000000000..e32e01b2a --- /dev/null +++ b/drivers/platform/arm64/Kconfig @@ -0,0 +1,106 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# EC-like Drivers for aarch64 based devices. +# + +menuconfig ARM64_PLATFORM_DEVICES + bool "ARM64 Platform-Specific Device Drivers" + depends on ARM64 || COMPILE_TEST + default ARM64 + help + Say Y here to get to see options for platform-specific device drivers + for arm64 based devices, primarily EC-like device drivers. + This option alone does not add any kernel code. + + If you say N, all options in this submenu will be skipped and disabled. + +if ARM64_PLATFORM_DEVICES + +config EC_ACER_ASPIRE1 + tristate "Acer Aspire 1 Embedded Controller driver" + depends on ARCH_QCOM || COMPILE_TEST + depends on I2C + depends on DRM + depends on POWER_SUPPLY + depends on INPUT + help + Say Y here to enable the EC driver for the (Snapdragon-based) + Acer Aspire 1 laptop. The EC handles battery and charging + monitoring as well as some misc functions like the lid sensor + and USB Type-C DP HPD events. + + This driver provides battery and AC status support for the mentioned + laptop where this information is not properly exposed via the + standard ACPI devices. + +config EC_HUAWEI_GAOKUN + tristate "Huawei Matebook E Go Embedded Controller driver" + depends on ARCH_QCOM || COMPILE_TEST + depends on I2C + depends on INPUT + depends on HWMON + select AUXILIARY_BUS + + help + Say Y here to enable the EC driver for the Huawei Matebook E Go + which is a sc8280xp-based 2-in-1 tablet. The driver handles battery + (information, charge control) and USB Type-C DP HPD events as well + as some misc functions like the lid sensor and temperature sensors, + etc. + + This driver provides battery and AC status support for the mentioned + laptop where this information is not properly exposed via the + standard ACPI devices. + + Say M or Y here to include this support. + +config EC_LENOVO_YOGA_C630 + tristate "Lenovo Yoga C630 Embedded Controller driver" + depends on ARCH_QCOM || COMPILE_TEST + depends on I2C + select AUXILIARY_BUS + help + Driver for the Embedded Controller in the Qualcomm Snapdragon-based + Lenovo Yoga C630, which provides battery and power adapter + information. + + This driver provides battery and AC status support for the mentioned + laptop where this information is not properly exposed via the + standard ACPI devices. + + Say M or Y here to include this support. + +config EC_LENOVO_THINKPAD_T14S + tristate "Lenovo Thinkpad T14s Embedded Controller driver" + depends on ARCH_QCOM || COMPILE_TEST + depends on I2C + depends on INPUT + select INPUT_SPARSEKMAP + select LEDS_CLASS + select NEW_LEDS + select SND_CTL_LED if SND + help + Driver for the Embedded Controller in the Qualcomm Snapdragon-based + Lenovo Thinkpad T14s, which provides access to keyboard backlight + and status LEDs. + + 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. + +config EC_QCOM_HAMOA + tristate "Embedded Controller driver for Qualcomm Hamoa/Glymur reference devices" + depends on ARCH_QCOM || COMPILE_TEST + depends on I2C + depends on THERMAL || THERMAL=n + help + Say M or Y here to enable the Embedded Controller driver for Qualcomm + Snapdragon-based Hamoa/Glymur reference devices. The driver handles fan + control, temperature sensors, access to EC state changes and supports + reporting suspend entry/exit to the EC. + + This driver currently supports Hamoa/Purwa/Glymur reference devices. + +endif # ARM64_PLATFORM_DEVICES diff --git a/drivers/platform/arm64/Makefile b/drivers/platform/arm64/Makefile new file mode 100644 index 000000000..7681be4a4 --- /dev/null +++ b/drivers/platform/arm64/Makefile @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Makefile for linux/drivers/platform/arm64 +# +# This dir should only include drivers for EC-like devices. +# + +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_QCOM_HAMOA) += qcom-hamoa-ec.o diff --git a/drivers/platform/arm64/acer-aspire1-ec.c b/drivers/platform/arm64/acer-aspire1-ec.c new file mode 100644 index 000000000..08d0b155a --- /dev/null +++ b/drivers/platform/arm64/acer-aspire1-ec.c @@ -0,0 +1,562 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (c) 2024, Nikita Travkin <nikita@trvn.ru> */ + +#include <linux/unaligned.h> +#include <drm/drm_bridge.h> +#include <linux/bits.h> +#include <linux/delay.h> +#include <linux/i2c.h> +#include <linux/input.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/usb/typec_mux.h> +#include <linux/workqueue_types.h> + +#define MILLI_TO_MICRO 1000 + +#define ASPIRE_EC_EVENT 0x05 + +#define ASPIRE_EC_EVENT_WATCHDOG 0x20 +#define ASPIRE_EC_EVENT_KBD_BKL_ON 0x57 +#define ASPIRE_EC_EVENT_KBD_BKL_OFF 0x58 +#define ASPIRE_EC_EVENT_LID_CLOSE 0x9b +#define ASPIRE_EC_EVENT_LID_OPEN 0x9c +#define ASPIRE_EC_EVENT_BKL_UNBLANKED 0x9d +#define ASPIRE_EC_EVENT_BKL_BLANKED 0x9e +#define ASPIRE_EC_EVENT_FG_INF_CHG 0x85 +#define ASPIRE_EC_EVENT_FG_STA_CHG 0xc6 +#define ASPIRE_EC_EVENT_HPD_DIS 0xa3 +#define ASPIRE_EC_EVENT_HPD_CON 0xa4 + +#define ASPIRE_EC_FG_DYNAMIC 0x07 +#define ASPIRE_EC_FG_STATIC 0x08 + +#define ASPIRE_EC_FG_FLAG_PRESENT BIT(0) +#define ASPIRE_EC_FG_FLAG_FULL BIT(1) +#define ASPIRE_EC_FG_FLAG_DISCHARGING BIT(2) +#define ASPIRE_EC_FG_FLAG_CHARGING BIT(3) + +#define ASPIRE_EC_RAM_READ 0x20 +#define ASPIRE_EC_RAM_WRITE 0x21 + +#define ASPIRE_EC_RAM_WATCHDOG 0x19 +#define ASPIRE_EC_WATCHDOG_BIT BIT(6) + +#define ASPIRE_EC_RAM_KBD_MODE 0x43 + +#define ASPIRE_EC_RAM_KBD_FN_EN BIT(0) +#define ASPIRE_EC_RAM_KBD_MEDIA_ON_TOP BIT(5) +#define ASPIRE_EC_RAM_KBD_ALWAYS_SET BIT(6) +#define ASPIRE_EC_RAM_KBD_NUM_LAYER_EN BIT(7) + +#define ASPIRE_EC_RAM_KBD_MODE_2 0x60 + +#define ASPIRE_EC_RAM_KBD_MEDIA_NOTIFY BIT(3) + +#define ASPIRE_EC_RAM_HPD_STATUS 0xf4 +#define ASPIRE_EC_HPD_CONNECTED 0x03 + +#define ASPIRE_EC_RAM_LID_STATUS 0x4c +#define ASPIRE_EC_LID_OPEN BIT(6) + +#define ASPIRE_EC_RAM_ADP 0x40 +#define ASPIRE_EC_AC_STATUS BIT(0) + +struct aspire_ec { + struct i2c_client *client; + struct power_supply *bat_psy; + struct power_supply *adp_psy; + struct input_dev *idev; + + bool bridge_configured; + struct drm_bridge bridge; + struct work_struct work; +}; + +static int aspire_ec_ram_read(struct i2c_client *client, u8 off, u8 *data, u8 data_len) +{ + i2c_smbus_write_byte_data(client, ASPIRE_EC_RAM_READ, off); + i2c_smbus_read_i2c_block_data(client, ASPIRE_EC_RAM_READ, data_len, data); + return 0; +} + +static int aspire_ec_ram_write(struct i2c_client *client, u8 off, u8 data) +{ + u8 tmp[2] = {off, data}; + + i2c_smbus_write_i2c_block_data(client, ASPIRE_EC_RAM_WRITE, sizeof(tmp), tmp); + return 0; +} + +static irqreturn_t aspire_ec_irq_handler(int irq, void *data) +{ + struct aspire_ec *ec = data; + int id; + u8 tmp; + + /* + * The original ACPI firmware actually has a small sleep in the handler. + * + * It seems like in most cases it's not needed but when the device + * just exits suspend, our i2c driver has a brief time where data + * transfer is not possible yet. So this delay allows us to suppress + * quite a bunch of spurious error messages in dmesg. Thus it's kept. + */ + usleep_range(15000, 30000); + + id = i2c_smbus_read_byte_data(ec->client, ASPIRE_EC_EVENT); + if (id < 0) { + dev_err(&ec->client->dev, "Failed to read event id: %pe\n", ERR_PTR(id)); + return IRQ_HANDLED; + } + + switch (id) { + case 0x0: /* No event */ + break; + + case ASPIRE_EC_EVENT_WATCHDOG: + /* + * Here acpi responds to the event and clears some bit. + * Notify (\_SB.I2C3.BAT1, 0x81) // Information Change + * Notify (\_SB.I2C3.ADP1, 0x80) // Status Change + */ + aspire_ec_ram_read(ec->client, ASPIRE_EC_RAM_WATCHDOG, &tmp, sizeof(tmp)); + tmp &= ~ASPIRE_EC_WATCHDOG_BIT; + aspire_ec_ram_write(ec->client, ASPIRE_EC_RAM_WATCHDOG, tmp); + break; + + case ASPIRE_EC_EVENT_LID_CLOSE: + /* Notify (\_SB.LID0, 0x80) // Status Change */ + input_report_switch(ec->idev, SW_LID, 1); + input_sync(ec->idev); + break; + + case ASPIRE_EC_EVENT_LID_OPEN: + /* Notify (\_SB.LID0, 0x80) // Status Change */ + input_report_switch(ec->idev, SW_LID, 0); + input_sync(ec->idev); + break; + + case ASPIRE_EC_EVENT_FG_INF_CHG: + /* Notify (\_SB.I2C3.BAT1, 0x81) // Information Change */ + fallthrough; + case ASPIRE_EC_EVENT_FG_STA_CHG: + /* Notify (\_SB.I2C3.BAT1, 0x80) // Status Change */ + power_supply_changed(ec->bat_psy); + power_supply_changed(ec->adp_psy); + break; + + case ASPIRE_EC_EVENT_HPD_DIS: + if (ec->bridge_configured) + drm_bridge_hpd_notify(&ec->bridge, connector_status_disconnected); + break; + + case ASPIRE_EC_EVENT_HPD_CON: + if (ec->bridge_configured) + drm_bridge_hpd_notify(&ec->bridge, connector_status_connected); + break; + + case ASPIRE_EC_EVENT_BKL_BLANKED: + case ASPIRE_EC_EVENT_BKL_UNBLANKED: + /* Display backlight blanked on FN+F6. No action needed. */ + break; + + case ASPIRE_EC_EVENT_KBD_BKL_ON: + case ASPIRE_EC_EVENT_KBD_BKL_OFF: + /* + * There is a keyboard backlight connector on Aspire 1 that is + * controlled by FN+F8. There is no kb backlight on the device though. + * Seems like this is used on other devices like Acer Spin 7. + * No action needed. + */ + break; + + default: + dev_warn(&ec->client->dev, "Unknown event id=0x%x\n", id); + } + + return IRQ_HANDLED; +} + +/* + * Power supply. + */ + +struct aspire_ec_bat_psy_static_data { + u8 unk1; + u8 flags; + __le16 unk2; + __le16 voltage_design; + __le16 capacity_full; + __le16 unk3; + __le16 serial; + u8 model_id; + u8 vendor_id; +} __packed; + +static const char * const aspire_ec_bat_psy_battery_model[] = { + "AP18C4K", + "AP18C8K", + "AP19B8K", + "AP16M4J", + "AP16M5J", +}; + +static const char * const aspire_ec_bat_psy_battery_vendor[] = { + "SANYO", + "SONY", + "PANASONIC", + "SAMSUNG", + "SIMPLO", + "MOTOROLA", + "CELXPERT", + "LGC", + "GETAC", + "MURATA", +}; + +struct aspire_ec_bat_psy_dynamic_data { + u8 unk1; + u8 flags; + u8 unk2; + __le16 capacity_now; + __le16 voltage_now; + __le16 current_now; + __le16 unk3; + __le16 unk4; +} __packed; + +static int aspire_ec_bat_psy_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct aspire_ec *ec = power_supply_get_drvdata(psy); + struct aspire_ec_bat_psy_static_data sdat; + struct aspire_ec_bat_psy_dynamic_data ddat; + int str_index = 0; + + i2c_smbus_read_i2c_block_data(ec->client, ASPIRE_EC_FG_STATIC, sizeof(sdat), (u8 *)&sdat); + i2c_smbus_read_i2c_block_data(ec->client, ASPIRE_EC_FG_DYNAMIC, sizeof(ddat), (u8 *)&ddat); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + if (ddat.flags & ASPIRE_EC_FG_FLAG_CHARGING) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else if (ddat.flags & ASPIRE_EC_FG_FLAG_DISCHARGING) + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + else if (ddat.flags & ASPIRE_EC_FG_FLAG_FULL) + val->intval = POWER_SUPPLY_STATUS_FULL; + break; + + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = get_unaligned_le16(&ddat.voltage_now) * MILLI_TO_MICRO; + break; + + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = le16_to_cpu(sdat.voltage_design) * MILLI_TO_MICRO; + break; + + case POWER_SUPPLY_PROP_CHARGE_NOW: + val->intval = get_unaligned_le16(&ddat.capacity_now) * MILLI_TO_MICRO; + break; + + case POWER_SUPPLY_PROP_CHARGE_FULL: + val->intval = le16_to_cpu(sdat.capacity_full) * MILLI_TO_MICRO; + break; + + case POWER_SUPPLY_PROP_CAPACITY: + val->intval = get_unaligned_le16(&ddat.capacity_now) * 100; + val->intval /= le16_to_cpu(sdat.capacity_full); + break; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + val->intval = (s16)get_unaligned_le16(&ddat.current_now) * MILLI_TO_MICRO; + break; + + case POWER_SUPPLY_PROP_PRESENT: + val->intval = !!(ddat.flags & ASPIRE_EC_FG_FLAG_PRESENT); + break; + + case POWER_SUPPLY_PROP_SCOPE: + val->intval = POWER_SUPPLY_SCOPE_SYSTEM; + break; + + case POWER_SUPPLY_PROP_MODEL_NAME: + str_index = sdat.model_id - 1; + + if (str_index >= 0 && str_index < ARRAY_SIZE(aspire_ec_bat_psy_battery_model)) + val->strval = aspire_ec_bat_psy_battery_model[str_index]; + else + val->strval = "Unknown"; + break; + + case POWER_SUPPLY_PROP_MANUFACTURER: + str_index = sdat.vendor_id - 3; /* ACPI uses 3 as an offset here. */ + + if (str_index >= 0 && str_index < ARRAY_SIZE(aspire_ec_bat_psy_battery_vendor)) + val->strval = aspire_ec_bat_psy_battery_vendor[str_index]; + else + val->strval = "Unknown"; + break; + + default: + return -EINVAL; + } + + return 0; +} + +static enum power_supply_property aspire_ec_bat_psy_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_SCOPE, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +static const struct power_supply_desc aspire_ec_bat_psy_desc = { + .name = "aspire-ec-bat", + .type = POWER_SUPPLY_TYPE_BATTERY, + .get_property = aspire_ec_bat_psy_get_property, + .properties = aspire_ec_bat_psy_props, + .num_properties = ARRAY_SIZE(aspire_ec_bat_psy_props), +}; + +static int aspire_ec_adp_psy_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct aspire_ec *ec = power_supply_get_drvdata(psy); + u8 tmp; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + aspire_ec_ram_read(ec->client, ASPIRE_EC_RAM_ADP, &tmp, sizeof(tmp)); + val->intval = !!(tmp & ASPIRE_EC_AC_STATUS); + break; + + default: + return -EINVAL; + } + + return 0; +} + +static enum power_supply_property aspire_ec_adp_psy_props[] = { + POWER_SUPPLY_PROP_ONLINE, +}; + +static const struct power_supply_desc aspire_ec_adp_psy_desc = { + .name = "aspire-ec-adp", + .type = POWER_SUPPLY_TYPE_MAINS, + .get_property = aspire_ec_adp_psy_get_property, + .properties = aspire_ec_adp_psy_props, + .num_properties = ARRAY_SIZE(aspire_ec_adp_psy_props), +}; + +/* + * USB-C DP Alt mode HPD. + */ + +static int aspire_ec_bridge_attach(struct drm_bridge *bridge, struct drm_encoder *encoder, + enum drm_bridge_attach_flags flags) +{ + return flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR ? 0 : -EINVAL; +} + +static void aspire_ec_bridge_update_hpd_work(struct work_struct *work) +{ + struct aspire_ec *ec = container_of(work, struct aspire_ec, work); + u8 tmp; + + aspire_ec_ram_read(ec->client, ASPIRE_EC_RAM_HPD_STATUS, &tmp, sizeof(tmp)); + if (tmp == ASPIRE_EC_HPD_CONNECTED) + drm_bridge_hpd_notify(&ec->bridge, connector_status_connected); + else + drm_bridge_hpd_notify(&ec->bridge, connector_status_disconnected); +} + +static void aspire_ec_bridge_hpd_enable(struct drm_bridge *bridge) +{ + struct aspire_ec *ec = container_of(bridge, struct aspire_ec, bridge); + + schedule_work(&ec->work); +} + +static const struct drm_bridge_funcs aspire_ec_bridge_funcs = { + .hpd_enable = aspire_ec_bridge_hpd_enable, + .attach = aspire_ec_bridge_attach, +}; + +/* + * Sysfs attributes. + */ + +static ssize_t fn_lock_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct aspire_ec *ec = i2c_get_clientdata(to_i2c_client(dev)); + u8 tmp; + + aspire_ec_ram_read(ec->client, ASPIRE_EC_RAM_KBD_MODE, &tmp, sizeof(tmp)); + + return sysfs_emit(buf, "%u\n", !(tmp & ASPIRE_EC_RAM_KBD_MEDIA_ON_TOP)); +} + +static ssize_t fn_lock_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct aspire_ec *ec = i2c_get_clientdata(to_i2c_client(dev)); + u8 tmp; + + bool state; + int ret; + + ret = kstrtobool(buf, &state); + if (ret) + return ret; + + aspire_ec_ram_read(ec->client, ASPIRE_EC_RAM_KBD_MODE, &tmp, sizeof(tmp)); + + if (state) + tmp &= ~ASPIRE_EC_RAM_KBD_MEDIA_ON_TOP; + else + tmp |= ASPIRE_EC_RAM_KBD_MEDIA_ON_TOP; + + aspire_ec_ram_write(ec->client, ASPIRE_EC_RAM_KBD_MODE, tmp); + + return count; +} + +static DEVICE_ATTR_RW(fn_lock); + +static struct attribute *aspire_ec_attrs[] = { + &dev_attr_fn_lock.attr, + NULL +}; +ATTRIBUTE_GROUPS(aspire_ec); + +static int aspire_ec_probe(struct i2c_client *client) +{ + struct power_supply_config psy_cfg = {0}; + struct device *dev = &client->dev; + struct fwnode_handle *fwnode; + struct aspire_ec *ec; + int ret; + u8 tmp; + + ec = devm_drm_bridge_alloc(dev, struct aspire_ec, bridge, &aspire_ec_bridge_funcs); + if (IS_ERR(ec)) + return PTR_ERR(ec); + + ec->client = client; + i2c_set_clientdata(client, ec); + + /* Battery status reports */ + psy_cfg.drv_data = ec; + ec->bat_psy = devm_power_supply_register(dev, &aspire_ec_bat_psy_desc, &psy_cfg); + if (IS_ERR(ec->bat_psy)) + return dev_err_probe(dev, PTR_ERR(ec->bat_psy), + "Failed to register battery power supply\n"); + + ec->adp_psy = devm_power_supply_register(dev, &aspire_ec_adp_psy_desc, &psy_cfg); + if (IS_ERR(ec->adp_psy)) + return dev_err_probe(dev, PTR_ERR(ec->adp_psy), + "Failed to register AC power supply\n"); + + /* Lid switch */ + ec->idev = devm_input_allocate_device(dev); + if (!ec->idev) + return -ENOMEM; + + ec->idev->name = "aspire-ec"; + ec->idev->phys = "aspire-ec/input0"; + input_set_capability(ec->idev, EV_SW, SW_LID); + + ret = input_register_device(ec->idev); + if (ret) + return dev_err_probe(dev, ret, "Input device register failed\n"); + + /* Enable the keyboard fn keys */ + tmp = ASPIRE_EC_RAM_KBD_FN_EN | ASPIRE_EC_RAM_KBD_ALWAYS_SET; + tmp |= ASPIRE_EC_RAM_KBD_MEDIA_ON_TOP; + aspire_ec_ram_write(client, ASPIRE_EC_RAM_KBD_MODE, tmp); + + aspire_ec_ram_read(client, ASPIRE_EC_RAM_KBD_MODE_2, &tmp, sizeof(tmp)); + tmp |= ASPIRE_EC_RAM_KBD_MEDIA_NOTIFY; + aspire_ec_ram_write(client, ASPIRE_EC_RAM_KBD_MODE_2, tmp); + + /* External Type-C display attach reports */ + fwnode = device_get_named_child_node(dev, "connector"); + if (fwnode) { + INIT_WORK(&ec->work, aspire_ec_bridge_update_hpd_work); + ec->bridge.of_node = to_of_node(fwnode); + ec->bridge.ops = DRM_BRIDGE_OP_HPD; + ec->bridge.type = DRM_MODE_CONNECTOR_USB; + + ret = devm_drm_bridge_add(dev, &ec->bridge); + if (ret) { + fwnode_handle_put(fwnode); + return dev_err_probe(dev, ret, "Failed to register drm bridge\n"); + } + + ec->bridge_configured = true; + } + + ret = devm_request_threaded_irq(dev, client->irq, NULL, + aspire_ec_irq_handler, IRQF_ONESHOT, + dev_name(dev), ec); + if (ret) + return dev_err_probe(dev, ret, "Failed to request irq\n"); + + return 0; +} + +static int aspire_ec_resume(struct device *dev) +{ + struct aspire_ec *ec = i2c_get_clientdata(to_i2c_client(dev)); + u8 tmp; + + aspire_ec_ram_read(ec->client, ASPIRE_EC_RAM_LID_STATUS, &tmp, sizeof(tmp)); + input_report_switch(ec->idev, SW_LID, !!(tmp & ASPIRE_EC_LID_OPEN)); + input_sync(ec->idev); + + return 0; +} + +static const struct i2c_device_id aspire_ec_id[] = { + { .name = "aspire1-ec" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, aspire_ec_id); + +static const struct of_device_id aspire_ec_of_match[] = { + { .compatible = "acer,aspire1-ec", }, + { } +}; +MODULE_DEVICE_TABLE(of, aspire_ec_of_match); + +static DEFINE_SIMPLE_DEV_PM_OPS(aspire_ec_pm_ops, NULL, aspire_ec_resume); + +static struct i2c_driver aspire_ec_driver = { + .driver = { + .name = "aspire-ec", + .of_match_table = aspire_ec_of_match, + .pm = pm_sleep_ptr(&aspire_ec_pm_ops), + .dev_groups = aspire_ec_groups, + }, + .probe = aspire_ec_probe, + .id_table = aspire_ec_id, +}; +module_i2c_driver(aspire_ec_driver); + +MODULE_DESCRIPTION("Acer Aspire 1 embedded controller"); +MODULE_AUTHOR("Nikita Travkin <nikita@trvn.ru>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/arm64/huawei-gaokun-ec.c b/drivers/platform/arm64/huawei-gaokun-ec.c new file mode 100644 index 000000000..80a8ba8b8 --- /dev/null +++ b/drivers/platform/arm64/huawei-gaokun-ec.c @@ -0,0 +1,827 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * huawei-gaokun-ec - An EC driver for HUAWEI Matebook E Go + * + * Copyright (C) 2024-2025 Pengyu Luo <mitltlatltl@gmail.com> + */ + +#include <linux/auxiliary_bus.h> +#include <linux/cleanup.h> +#include <linux/delay.h> +#include <linux/device.h> +#include <linux/hwmon.h> +#include <linux/hwmon-sysfs.h> +#include <linux/i2c.h> +#include <linux/input.h> +#include <linux/notifier.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/platform_data/huawei-gaokun-ec.h> + +#define EC_EVENT 0x06 + +/* Also can be found in ACPI specification 12.3 */ +#define EC_READ 0x80 +#define EC_WRITE 0x81 +#define EC_BURST 0x82 +#define EC_QUERY 0x84 + +#define EC_FN_LOCK_ON 0x5A +#define EC_FN_LOCK_OFF 0x55 +#define EC_FN_LOCK_READ 0x6B +#define EC_FN_LOCK_WRITE 0x6C + +#define EC_EVENT_LID 0x81 + +#define EC_LID_STATE 0x80 +#define EC_LID_OPEN BIT(1) + +#define EC_TEMP_REG 0x61 + +#define EC_STANDBY_REG 0xB2 +#define EC_STANDBY_ENTER 0xDB +#define EC_STANDBY_EXIT 0xEB + +enum gaokun_ec_smart_charge_cmd { + SMART_CHARGE_DATA_WRITE = 0xE3, + SMART_CHARGE_DATA_READ, + SMART_CHARGE_ENABLE_WRITE, + SMART_CHARGE_ENABLE_READ, +}; + +enum gaokun_ec_ucsi_cmd { + UCSI_REG_WRITE = 0xD2, + UCSI_REG_READ, + UCSI_DATA_WRITE, + UCSI_DATA_READ, +}; + +#define UCSI_REG_SIZE 7 + +/* + * For tx, command sequences are arranged as + * {master_cmd, slave_cmd, data_len, data_seq} + */ +#define REQ_HDR_SIZE 3 +#define INPUT_SIZE_OFFSET 2 +#define REQ_LEN(req) (REQ_HDR_SIZE + (req)[INPUT_SIZE_OFFSET]) + +/* + * For rx, data sequences are arranged as + * {status, data_len(unreliable), data_seq} + */ +#define RESP_HDR_SIZE 2 + +#define MKREQ(REG0, REG1, SIZE, ...) \ +{ \ + REG0, REG1, SIZE, \ + /* ## will remove comma when SIZE is 0 */ \ + ## __VA_ARGS__, \ + /* make sure len(pkt[3:]) >= SIZE */ \ + [3 + (SIZE)] = 0, \ +} + +#define MKRESP(SIZE) \ +{ \ + [RESP_HDR_SIZE + (SIZE) - 1] = 0, \ +} + +/* Possible size 1, 4, 20, 24. Most of the time, the size is 1. */ +static inline void refill_req(u8 *dest, const u8 *src, size_t size) +{ + memcpy(dest + REQ_HDR_SIZE, src, size); +} + +static inline void refill_req_byte(u8 *dest, const u8 *src) +{ + dest[REQ_HDR_SIZE] = *src; +} + +/* Possible size 1, 2, 4, 7, 20. Most of the time, the size is 1. */ +static inline void extr_resp(u8 *dest, const u8 *src, size_t size) +{ + memcpy(dest, src + RESP_HDR_SIZE, size); +} + +static inline void extr_resp_byte(u8 *dest, const u8 *src) +{ + *dest = src[RESP_HDR_SIZE]; +} + +static inline void *extr_resp_shallow(const u8 *src) +{ + return (void *)(src + RESP_HDR_SIZE); +} + +struct gaokun_ec { + struct i2c_client *client; + struct mutex lock; /* EC transaction lock */ + struct blocking_notifier_head notifier_list; + struct device *hwmon_dev; + struct input_dev *idev; + bool suspended; +}; + +static int gaokun_ec_request(struct gaokun_ec *ec, const u8 *req, + size_t resp_len, u8 *resp) +{ + struct i2c_client *client = ec->client; + struct i2c_msg msgs[] = { + { + .addr = client->addr, + .flags = client->flags, + .len = REQ_LEN(req), + .buf = (void *)req, + }, { + .addr = client->addr, + .flags = client->flags | I2C_M_RD, + .len = resp_len, + .buf = resp, + }, + }; + int ret; + + guard(mutex)(&ec->lock); + ret = i2c_transfer(client->adapter, msgs, ARRAY_SIZE(msgs)); + if (ret != ARRAY_SIZE(msgs)) { + dev_err(&client->dev, "I2C transfer error %d\n", ret); + goto out_after_break; + } + + ret = *resp; + if (ret) + dev_err(&client->dev, "EC transaction error %d\n", ret); + +out_after_break: + usleep_range(2000, 2500); /* have a break, ACPI did this */ + + return ret; +} + +/* -------------------------------------------------------------------------- */ +/* Common API */ + +/** + * gaokun_ec_read - Read from EC + * @ec: The gaokun_ec structure + * @req: The sequence to request + * @resp_len: The size to read + * @resp: The buffer to store response sequence + * + * This function is used to read data after writing a magic sequence to EC. + * All EC operations depend on this function. + * + * Huawei uses magic sequences everywhere to complete various functions, all + * these sequences are passed to ECCD(a ACPI method which is quiet similar + * to gaokun_ec_request), there is no good abstraction to generalize these + * sequences, so just wrap it for now. Almost all magic sequences are kept + * in this file. + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_read(struct gaokun_ec *ec, const u8 *req, + size_t resp_len, u8 *resp) +{ + return gaokun_ec_request(ec, req, resp_len, resp); +} +EXPORT_SYMBOL_GPL(gaokun_ec_read); + +/** + * gaokun_ec_write - Write to EC + * @ec: The gaokun_ec structure + * @req: The sequence to request + * + * This function has no big difference from gaokun_ec_read. When caller care + * only write status and no actual data are returned, then use it. + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_write(struct gaokun_ec *ec, const u8 *req) +{ + u8 ec_resp[] = MKRESP(0); + + return gaokun_ec_request(ec, req, sizeof(ec_resp), ec_resp); +} +EXPORT_SYMBOL_GPL(gaokun_ec_write); + +int gaokun_ec_read_byte(struct gaokun_ec *ec, const u8 *req, u8 *byte) +{ + int ret; + u8 ec_resp[] = MKRESP(sizeof(*byte)); + + ret = gaokun_ec_read(ec, req, sizeof(ec_resp), ec_resp); + extr_resp_byte(byte, ec_resp); + + return ret; +} +EXPORT_SYMBOL_GPL(gaokun_ec_read_byte); + +/** + * gaokun_ec_register_notify - Register a notifier callback for EC events. + * @ec: The gaokun_ec structure + * @nb: Notifier block pointer to register + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_register_notify(struct gaokun_ec *ec, struct notifier_block *nb) +{ + return blocking_notifier_chain_register(&ec->notifier_list, nb); +} +EXPORT_SYMBOL_GPL(gaokun_ec_register_notify); + +/** + * gaokun_ec_unregister_notify - Unregister notifier callback for EC events. + * @ec: The gaokun_ec structure + * @nb: Notifier block pointer to unregister + * + * Unregister a notifier callback that was previously registered with + * gaokun_ec_register_notify(). + */ +void gaokun_ec_unregister_notify(struct gaokun_ec *ec, struct notifier_block *nb) +{ + blocking_notifier_chain_unregister(&ec->notifier_list, nb); +} +EXPORT_SYMBOL_GPL(gaokun_ec_unregister_notify); + +/* -------------------------------------------------------------------------- */ +/* API for PSY */ + +/** + * gaokun_ec_psy_multi_read - Read contiguous registers + * @ec: The gaokun_ec structure + * @reg: The start register + * @resp_len: The number of registers to be read + * @resp: The buffer to store response sequence + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_psy_multi_read(struct gaokun_ec *ec, u8 reg, + size_t resp_len, u8 *resp) +{ + u8 ec_req[] = MKREQ(0x02, EC_READ, 1, 0); + u8 ec_resp[] = MKRESP(1); + int i, ret; + + for (i = 0; i < resp_len; ++i, reg++) { + refill_req_byte(ec_req, ®); + ret = gaokun_ec_read(ec, ec_req, sizeof(ec_resp), ec_resp); + if (ret) + return ret; + extr_resp_byte(&resp[i], ec_resp); + } + + return 0; +} +EXPORT_SYMBOL_GPL(gaokun_ec_psy_multi_read); + +/* Smart charge */ + +/** + * gaokun_ec_psy_get_smart_charge - Get smart charge data from EC + * @ec: The gaokun_ec structure + * @resp: The buffer to store response sequence (mode, delay, start, end) + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_psy_get_smart_charge(struct gaokun_ec *ec, + u8 resp[GAOKUN_SMART_CHARGE_DATA_SIZE]) +{ + /* GBCM */ + u8 ec_req[] = MKREQ(0x02, SMART_CHARGE_DATA_READ, 0); + u8 ec_resp[] = MKRESP(GAOKUN_SMART_CHARGE_DATA_SIZE); + int ret; + + ret = gaokun_ec_read(ec, ec_req, sizeof(ec_resp), ec_resp); + if (ret) + return ret; + + extr_resp(resp, ec_resp, GAOKUN_SMART_CHARGE_DATA_SIZE); + + return 0; +} +EXPORT_SYMBOL_GPL(gaokun_ec_psy_get_smart_charge); + +static inline bool validate_battery_threshold_range(u8 start, u8 end) +{ + return end != 0 && start <= end && end <= 100; +} + +/** + * gaokun_ec_psy_set_smart_charge - Set smart charge data + * @ec: The gaokun_ec structure + * @req: The sequence to request (mode, delay, start, end) + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_psy_set_smart_charge(struct gaokun_ec *ec, + const u8 req[GAOKUN_SMART_CHARGE_DATA_SIZE]) +{ + /* SBCM */ + u8 ec_req[] = MKREQ(0x02, SMART_CHARGE_DATA_WRITE, + GAOKUN_SMART_CHARGE_DATA_SIZE); + + if (!validate_battery_threshold_range(req[2], req[3])) + return -EINVAL; + + refill_req(ec_req, req, GAOKUN_SMART_CHARGE_DATA_SIZE); + + return gaokun_ec_write(ec, ec_req); +} +EXPORT_SYMBOL_GPL(gaokun_ec_psy_set_smart_charge); + +/* Smart charge enable */ + +/** + * gaokun_ec_psy_get_smart_charge_enable - Get smart charge state + * @ec: The gaokun_ec structure + * @on: The state + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_psy_get_smart_charge_enable(struct gaokun_ec *ec, bool *on) +{ + /* GBAC */ + u8 ec_req[] = MKREQ(0x02, SMART_CHARGE_ENABLE_READ, 0); + u8 state; + int ret; + + ret = gaokun_ec_read_byte(ec, ec_req, &state); + if (ret) + return ret; + + *on = !!state; + + return 0; +} +EXPORT_SYMBOL_GPL(gaokun_ec_psy_get_smart_charge_enable); + +/** + * gaokun_ec_psy_set_smart_charge_enable - Set smart charge state + * @ec: The gaokun_ec structure + * @on: The state + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_psy_set_smart_charge_enable(struct gaokun_ec *ec, bool on) +{ + /* SBAC */ + u8 ec_req[] = MKREQ(0x02, SMART_CHARGE_ENABLE_WRITE, 1, on); + + return gaokun_ec_write(ec, ec_req); +} +EXPORT_SYMBOL_GPL(gaokun_ec_psy_set_smart_charge_enable); + +/* -------------------------------------------------------------------------- */ +/* API for UCSI */ + +/** + * gaokun_ec_ucsi_read - Read UCSI data from EC + * @ec: The gaokun_ec structure + * @resp: The buffer to store response sequence + * + * Read CCI and MSGI (used by UCSI subdriver). + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_ucsi_read(struct gaokun_ec *ec, + u8 resp[GAOKUN_UCSI_READ_SIZE]) +{ + u8 ec_req[] = MKREQ(0x03, UCSI_DATA_READ, 0); + u8 ec_resp[] = MKRESP(GAOKUN_UCSI_READ_SIZE); + int ret; + + ret = gaokun_ec_read(ec, ec_req, sizeof(ec_resp), ec_resp); + if (ret) + return ret; + + extr_resp(resp, ec_resp, GAOKUN_UCSI_READ_SIZE); + return 0; +} +EXPORT_SYMBOL_GPL(gaokun_ec_ucsi_read); + +/** + * gaokun_ec_ucsi_write - Write UCSI data to EC + * @ec: The gaokun_ec structure + * @req: The sequence to request + * + * Write CTRL and MSGO (used by UCSI subdriver). + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_ucsi_write(struct gaokun_ec *ec, + const u8 req[GAOKUN_UCSI_WRITE_SIZE]) +{ + u8 ec_req[] = MKREQ(0x03, UCSI_DATA_WRITE, GAOKUN_UCSI_WRITE_SIZE); + + refill_req(ec_req, req, GAOKUN_UCSI_WRITE_SIZE); + + return gaokun_ec_write(ec, ec_req); +} +EXPORT_SYMBOL_GPL(gaokun_ec_ucsi_write); + +/** + * gaokun_ec_ucsi_get_reg - Get UCSI register from EC + * @ec: The gaokun_ec structure + * @ureg: The gaokun ucsi register + * + * Get UCSI register data (used by UCSI subdriver). + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_ucsi_get_reg(struct gaokun_ec *ec, struct gaokun_ucsi_reg *ureg) +{ + u8 ec_req[] = MKREQ(0x03, UCSI_REG_READ, 0); + u8 ec_resp[] = MKRESP(UCSI_REG_SIZE); + int ret; + + ret = gaokun_ec_read(ec, ec_req, sizeof(ec_resp), ec_resp); + if (ret) + return ret; + + extr_resp((u8 *)ureg, ec_resp, UCSI_REG_SIZE); + + return 0; +} +EXPORT_SYMBOL_GPL(gaokun_ec_ucsi_get_reg); + +/** + * gaokun_ec_ucsi_pan_ack - Ack pin assignment notifications from EC + * @ec: The gaokun_ec structure + * @port_id: The port id receiving and handling the notifications + * + * Ack pin assignment notifications (used by UCSI subdriver). + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_ucsi_pan_ack(struct gaokun_ec *ec, int port_id) +{ + u8 ec_req[] = MKREQ(0x03, UCSI_REG_WRITE, 1); + u8 data = 1 << port_id; + + if (port_id == GAOKUN_UCSI_NO_PORT_UPDATE) + data = 0; + + refill_req_byte(ec_req, &data); + + return gaokun_ec_write(ec, ec_req); +} +EXPORT_SYMBOL_GPL(gaokun_ec_ucsi_pan_ack); + +/* -------------------------------------------------------------------------- */ +/* EC Sysfs */ + +/* Fn lock */ +static int gaokun_ec_get_fn_lock(struct gaokun_ec *ec, bool *on) +{ + /* GFRS */ + u8 ec_req[] = MKREQ(0x02, EC_FN_LOCK_READ, 0); + int ret; + u8 state; + + ret = gaokun_ec_read_byte(ec, ec_req, &state); + if (ret) + return ret; + + if (state == EC_FN_LOCK_ON) + *on = true; + else if (state == EC_FN_LOCK_OFF) + *on = false; + else + return -EIO; + + return 0; +} + +static int gaokun_ec_set_fn_lock(struct gaokun_ec *ec, bool on) +{ + /* SFRS */ + u8 ec_req[] = MKREQ(0x02, EC_FN_LOCK_WRITE, 1, + on ? EC_FN_LOCK_ON : EC_FN_LOCK_OFF); + + return gaokun_ec_write(ec, ec_req); +} + +static ssize_t fn_lock_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct gaokun_ec *ec = dev_get_drvdata(dev); + bool on; + int ret; + + ret = gaokun_ec_get_fn_lock(ec, &on); + if (ret) + return ret; + + return sysfs_emit(buf, "%d\n", on); +} + +static ssize_t fn_lock_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + struct gaokun_ec *ec = dev_get_drvdata(dev); + bool on; + int ret; + + if (kstrtobool(buf, &on)) + return -EINVAL; + + ret = gaokun_ec_set_fn_lock(ec, on); + if (ret) + return ret; + + return size; +} + +static DEVICE_ATTR_RW(fn_lock); + +static struct attribute *gaokun_ec_attrs[] = { + &dev_attr_fn_lock.attr, + NULL, +}; +ATTRIBUTE_GROUPS(gaokun_ec); + +/* -------------------------------------------------------------------------- */ +/* Thermal Zone HwMon */ + +/* Range from 0 to 0x2C, partially valid */ +static const u8 temp_reg[] = { + 0x05, 0x07, 0x08, 0x0E, 0x0F, 0x12, 0x15, 0x1E, + 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, + 0x27, 0x28, 0x29, 0x2A +}; + +static int gaokun_ec_get_temp(struct gaokun_ec *ec, u8 idx, long *temp) +{ + /* GTMP */ + u8 ec_req[] = MKREQ(0x02, EC_TEMP_REG, 1, temp_reg[idx]); + u8 ec_resp[] = MKRESP(sizeof(__le16)); + __le16 *tmp; + int ret; + + ret = gaokun_ec_read(ec, ec_req, sizeof(ec_resp), ec_resp); + if (ret) + return ret; + + tmp = (__le16 *)extr_resp_shallow(ec_resp); + *temp = le16_to_cpu(*tmp) * 100; /* convert to HwMon's unit */ + + return 0; +} + +static umode_t +gaokun_ec_hwmon_is_visible(const void *data, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + return type == hwmon_temp ? 0444 : 0; +} + +static int +gaokun_ec_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct gaokun_ec *ec = dev_get_drvdata(dev); + + if (type == hwmon_temp) + return gaokun_ec_get_temp(ec, channel, val); + + return -EINVAL; +} + +static const struct hwmon_ops gaokun_ec_hwmon_ops = { + .is_visible = gaokun_ec_hwmon_is_visible, + .read = gaokun_ec_hwmon_read, +}; + +static u32 gaokun_ec_temp_config[] = { + [0 ... ARRAY_SIZE(temp_reg) - 1] = HWMON_T_INPUT, + 0 +}; + +static const struct hwmon_channel_info gaokun_ec_temp = { + .type = hwmon_temp, + .config = gaokun_ec_temp_config, +}; + +static const struct hwmon_channel_info * const gaokun_ec_hwmon_info[] = { + &gaokun_ec_temp, + NULL +}; + +static const struct hwmon_chip_info gaokun_ec_hwmon_chip_info = { + .ops = &gaokun_ec_hwmon_ops, + .info = gaokun_ec_hwmon_info, +}; + +/* -------------------------------------------------------------------------- */ +/* Modern Standby */ + +static int gaokun_ec_suspend(struct device *dev) +{ + struct gaokun_ec *ec = dev_get_drvdata(dev); + u8 ec_req[] = MKREQ(0x02, EC_STANDBY_REG, 1, EC_STANDBY_ENTER); + int ret; + + if (ec->suspended) + return 0; + + ret = gaokun_ec_write(ec, ec_req); + if (ret) + return ret; + + ec->suspended = true; + + return 0; +} + +static int gaokun_ec_resume(struct device *dev) +{ + struct gaokun_ec *ec = dev_get_drvdata(dev); + u8 ec_req[] = MKREQ(0x02, EC_STANDBY_REG, 1, EC_STANDBY_EXIT); + int ret; + int i; + + if (!ec->suspended) + return 0; + + for (i = 0; i < 3; ++i) { + ret = gaokun_ec_write(ec, ec_req); + if (ret == 0) + break; + + msleep(100); /* EC need time to resume */ + } + + ec->suspended = false; + + return 0; +} + +static void gaokun_aux_release(struct device *dev) +{ + struct auxiliary_device *adev = to_auxiliary_dev(dev); + + of_node_put(dev->of_node); + kfree(adev); +} + +static void gaokun_aux_remove(void *data) +{ + struct auxiliary_device *adev = data; + + auxiliary_device_delete(adev); + auxiliary_device_uninit(adev); +} + +static int gaokun_aux_init(struct device *parent, const char *name, + struct gaokun_ec *ec) +{ + struct auxiliary_device *adev; + int ret; + + adev = kzalloc_obj(*adev); + if (!adev) + return -ENOMEM; + + adev->name = name; + adev->id = 0; + adev->dev.parent = parent; + adev->dev.release = gaokun_aux_release; + adev->dev.platform_data = ec; + /* Allow aux devices to access parent's DT nodes directly */ + device_set_of_node_from_dev(&adev->dev, parent); + + ret = auxiliary_device_init(adev); + if (ret) { + of_node_put(adev->dev.of_node); + kfree(adev); + return ret; + } + + ret = auxiliary_device_add(adev); + if (ret) { + auxiliary_device_uninit(adev); + return ret; + } + + return devm_add_action_or_reset(parent, gaokun_aux_remove, adev); +} + +/* -------------------------------------------------------------------------- */ +/* EC */ + +static irqreturn_t gaokun_ec_irq_handler(int irq, void *data) +{ + struct gaokun_ec *ec = data; + u8 ec_req[] = MKREQ(EC_EVENT, EC_QUERY, 0); + u8 status, id; + int ret; + + ret = gaokun_ec_read_byte(ec, ec_req, &id); + if (ret) + return IRQ_HANDLED; + + switch (id) { + case 0x0: /* No event */ + break; + + case EC_EVENT_LID: + gaokun_ec_psy_read_byte(ec, EC_LID_STATE, &status); + status &= EC_LID_OPEN; + input_report_switch(ec->idev, SW_LID, !status); + input_sync(ec->idev); + break; + + default: + blocking_notifier_call_chain(&ec->notifier_list, id, ec); + } + + return IRQ_HANDLED; +} + +static int gaokun_ec_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct gaokun_ec *ec; + int ret; + + ec = devm_kzalloc(dev, sizeof(*ec), GFP_KERNEL); + if (!ec) + return -ENOMEM; + + ret = devm_mutex_init(dev, &ec->lock); + if (ret) + return ret; + + ec->client = client; + i2c_set_clientdata(client, ec); + BLOCKING_INIT_NOTIFIER_HEAD(&ec->notifier_list); + + /* Lid switch */ + ec->idev = devm_input_allocate_device(dev); + if (!ec->idev) + return -ENOMEM; + + ec->idev->name = "LID"; + ec->idev->phys = "gaokun-ec/input0"; + input_set_capability(ec->idev, EV_SW, SW_LID); + + ret = input_register_device(ec->idev); + if (ret) + return dev_err_probe(dev, ret, "Failed to register input device\n"); + + ret = gaokun_aux_init(dev, GAOKUN_DEV_PSY, ec); + if (ret) + return ret; + + ret = gaokun_aux_init(dev, GAOKUN_DEV_UCSI, ec); + if (ret) + return ret; + + ret = devm_request_threaded_irq(dev, client->irq, NULL, + gaokun_ec_irq_handler, IRQF_ONESHOT, + dev_name(dev), ec); + if (ret) + return dev_err_probe(dev, ret, "Failed to request IRQ\n"); + + ec->hwmon_dev = devm_hwmon_device_register_with_info(dev, "gaokun_ec_hwmon", + ec, &gaokun_ec_hwmon_chip_info, NULL); + if (IS_ERR(ec->hwmon_dev)) + return dev_err_probe(dev, PTR_ERR(ec->hwmon_dev), + "Failed to register hwmon device\n"); + + return 0; +} + +static const struct i2c_device_id gaokun_ec_id[] = { + { .name = "gaokun-ec" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, gaokun_ec_id); + +static const struct of_device_id gaokun_ec_of_match[] = { + { .compatible = "huawei,gaokun3-ec", }, + { } +}; +MODULE_DEVICE_TABLE(of, gaokun_ec_of_match); + +static const struct dev_pm_ops gaokun_ec_pm_ops = { + NOIRQ_SYSTEM_SLEEP_PM_OPS(gaokun_ec_suspend, gaokun_ec_resume) +}; + +static struct i2c_driver gaokun_ec_driver = { + .driver = { + .name = "gaokun-ec", + .of_match_table = gaokun_ec_of_match, + .pm = &gaokun_ec_pm_ops, + .dev_groups = gaokun_ec_groups, + }, + .probe = gaokun_ec_probe, + .id_table = gaokun_ec_id, +}; +module_i2c_driver(gaokun_ec_driver); + +MODULE_DESCRIPTION("HUAWEI Matebook E Go EC driver"); +MODULE_AUTHOR("Pengyu Luo <mitltlatltl@gmail.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/arm64/lenovo-thinkpad-t14s.c b/drivers/platform/arm64/lenovo-thinkpad-t14s.c new file mode 100644 index 000000000..e7acb66b7 --- /dev/null +++ b/drivers/platform/arm64/lenovo-thinkpad-t14s.c @@ -0,0 +1,662 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2025, Sebastian Reichel + */ + +#include <linux/bitfield.h> +#include <linux/bits.h> +#include <linux/cleanup.h> +#include <linux/container_of.h> +#include <linux/device.h> +#include <linux/delay.h> +#include <linux/dev_printk.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/input.h> +#include <linux/input/sparse-keymap.h> +#include <linux/interrupt.h> +#include <linux/leds.h> +#include <linux/lockdep.h> +#include <linux/module.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <linux/pm.h> + +#define T14S_EC_CMD_ECRD 0x02 +#define T14S_EC_CMD_ECWR 0x03 +#define T14S_EC_CMD_EVT 0xf0 + +#define T14S_EC_REG_LED 0x0c +#define T14S_EC_REG_KBD_BL1 0x0d +#define T14S_EC_REG_MODERN_STANDBY 0xe0 +#define T14S_EC_MODERN_STANDBY_ENTRY BIT(1) +#define T14S_EC_MODERN_STANDBY_EXIT BIT(0) +#define T14S_EC_REG_KBD_BL2 0xe1 +#define T14S_EC_KBD_BL1_MASK GENMASK_U8(7, 6) +#define T14S_EC_KBD_BL2_MASK GENMASK_U8(3, 2) +#define T14S_EC_REG_AUD 0x30 +#define T14S_EC_MIC_MUTE_LED BIT(5) +#define T14S_EC_SPK_MUTE_LED BIT(6) + +#define T14S_EC_EVT_NONE 0x00 +#define T14S_EC_EVT_KEY_FN_4 0x13 +#define T14S_EC_EVT_KEY_FN_F7 0x16 +#define T14S_EC_EVT_KEY_FN_SPACE 0x1f +#define T14S_EC_EVT_KEY_TP_DOUBLE_TAP 0x20 +#define T14S_EC_EVT_AC_CONNECTED 0x26 +#define T14S_EC_EVT_AC_DISCONNECTED 0x27 +#define T14S_EC_EVT_KEY_POWER 0x28 +#define T14S_EC_EVT_LID_OPEN 0x2a +#define T14S_EC_EVT_LID_CLOSED 0x2b +#define T14S_EC_EVT_THERMAL_TZ40 0x5c +#define T14S_EC_EVT_THERMAL_TZ42 0x5d +#define T14S_EC_EVT_THERMAL_TZ39 0x5e +#define T14S_EC_EVT_KEY_FN_F12 0x62 +#define T14S_EC_EVT_KEY_FN_TAB 0x63 +#define T14S_EC_EVT_KEY_FN_F8 0x64 +#define T14S_EC_EVT_KEY_FN_F10 0x65 +#define T14S_EC_EVT_KEY_FN_F4 0x6a +#define T14S_EC_EVT_KEY_FN_D 0x6b +#define T14S_EC_EVT_KEY_FN_T 0x6c +#define T14S_EC_EVT_KEY_FN_H 0x6d +#define T14S_EC_EVT_KEY_FN_M 0x6e +#define T14S_EC_EVT_KEY_FN_L 0x6f +#define T14S_EC_EVT_KEY_FN_RIGHT_SHIFT 0x71 +#define T14S_EC_EVT_KEY_FN_ESC 0x74 +#define T14S_EC_EVT_KEY_FN_N 0x79 +#define T14S_EC_EVT_KEY_FN_F11 0x7a +#define T14S_EC_EVT_KEY_FN_G 0x7e + +/* Hardware LED blink rate is 1 Hz (500ms off, 500ms on) */ +#define T14S_EC_BLINK_RATE_ON_OFF_MS 500 + +/* + * Add a virtual offset on all key event codes for sparse keymap handling, + * since the sparse keymap infrastructure does not map some raw key event + * codes used by the EC. For example 0x16 (T14S_EC_EVT_KEY_FN_F7) is mapped + * to KEY_MUTE if no offset is applied. + */ +#define T14S_EC_KEY_EVT_OFFSET 0x1000 +#define T14S_EC_KEY_ENTRY(key, value) \ + { KE_KEY, T14S_EC_KEY_EVT_OFFSET + T14S_EC_EVT_KEY_##key, { value } } + +enum t14s_ec_led_status_t { + T14S_EC_LED_OFF = 0x00, + T14S_EC_LED_ON = 0x80, + T14S_EC_LED_BLINK = 0xc0, +}; + +struct t14s_ec_led_classdev { + struct led_classdev led_classdev; + int led; + enum t14s_ec_led_status_t cache; + struct t14s_ec *ec; +}; + +struct t14s_ec { + struct regmap *regmap; + struct device *dev; + struct t14s_ec_led_classdev led_pwr_btn; + struct t14s_ec_led_classdev led_chrg_orange; + struct t14s_ec_led_classdev led_chrg_white; + struct t14s_ec_led_classdev led_lid_logo_dot; + struct led_classdev kbd_backlight; + struct led_classdev led_mic_mute; + struct led_classdev led_spk_mute; + struct input_dev *inputdev; +}; + +static const struct regmap_config t14s_ec_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = 0xff, +}; + +static int t14s_ec_write(void *context, unsigned int reg, + unsigned int val) +{ + struct t14s_ec *ec = context; + struct i2c_client *client = to_i2c_client(ec->dev); + u8 buf[5] = {T14S_EC_CMD_ECWR, reg, 0x00, 0x01, val}; + int ret; + + ret = i2c_master_send(client, buf, sizeof(buf)); + if (ret < 0) + return ret; + + fsleep(10000); + return 0; +} + +static int t14s_ec_read(void *context, unsigned int reg, + unsigned int *val) +{ + struct t14s_ec *ec = context; + struct i2c_client *client = to_i2c_client(ec->dev); + u8 buf[4] = {T14S_EC_CMD_ECRD, reg, 0x00, 0x01}; + struct i2c_msg request, response; + u8 result; + int ret; + + request.addr = client->addr; + request.flags = I2C_M_STOP; + request.len = sizeof(buf); + request.buf = buf; + response.addr = client->addr; + response.flags = I2C_M_RD; + response.len = 1; + response.buf = &result; + + i2c_lock_bus(client->adapter, I2C_LOCK_SEGMENT); + + ret = __i2c_transfer(client->adapter, &request, 1); + if (ret < 0) + goto out; + + ret = __i2c_transfer(client->adapter, &response, 1); + if (ret < 0) + goto out; + + *val = result; + ret = 0; + +out: + i2c_unlock_bus(client->adapter, I2C_LOCK_SEGMENT); + fsleep(10000); + return ret; +} + +static const struct regmap_bus t14s_ec_regmap_bus = { + .reg_write = t14s_ec_write, + .reg_read = t14s_ec_read, +}; + +static int t14s_ec_read_evt(struct t14s_ec *ec, u8 *val) +{ + struct i2c_client *client = to_i2c_client(ec->dev); + u8 buf[4] = {T14S_EC_CMD_EVT, 0x00, 0x00, 0x01}; + struct i2c_msg request, response; + int ret; + + request.addr = client->addr; + request.flags = I2C_M_STOP; + request.len = sizeof(buf); + request.buf = buf; + response.addr = client->addr; + response.flags = I2C_M_RD; + response.len = 1; + response.buf = val; + + i2c_lock_bus(client->adapter, I2C_LOCK_SEGMENT); + + ret = __i2c_transfer(client->adapter, &request, 1); + if (ret < 0) + goto out; + + ret = __i2c_transfer(client->adapter, &response, 1); + if (ret < 0) + goto out; + + fsleep(10000); + + ret = 0; + +out: + i2c_unlock_bus(client->adapter, I2C_LOCK_SEGMENT); + return ret; +} + +static void t14s_ec_write_sequence(struct t14s_ec *ec, u8 reg, u8 val, u8 cnt) +{ + int i; + + for (i = 0; i < cnt; i++) + regmap_write(ec->regmap, reg, val); +} + +static int t14s_led_set_status(struct t14s_ec *ec, + struct t14s_ec_led_classdev *led, + const enum t14s_ec_led_status_t ledstatus) +{ + int ret; + + ret = regmap_write(ec->regmap, T14S_EC_REG_LED, + led->led | ledstatus); + if (ret < 0) + return ret; + + led->cache = ledstatus; + return 0; +} + +static int t14s_led_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct t14s_ec_led_classdev *led = container_of(led_cdev, + struct t14s_ec_led_classdev, led_classdev); + enum t14s_ec_led_status_t new_state; + + if (brightness == LED_OFF) + new_state = T14S_EC_LED_OFF; + else if (led->cache == T14S_EC_LED_BLINK) + new_state = T14S_EC_LED_BLINK; + else + new_state = T14S_EC_LED_ON; + + return t14s_led_set_status(led->ec, led, new_state); +} + +static int t14s_led_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + struct t14s_ec_led_classdev *led = container_of(led_cdev, + struct t14s_ec_led_classdev, led_classdev); + + if (*delay_on == 0 && *delay_off == 0) { + /* Userspace does not provide a blink rate; we can choose it */ + *delay_on = T14S_EC_BLINK_RATE_ON_OFF_MS; + *delay_off = T14S_EC_BLINK_RATE_ON_OFF_MS; + } else if ((*delay_on != T14S_EC_BLINK_RATE_ON_OFF_MS) || + (*delay_off != T14S_EC_BLINK_RATE_ON_OFF_MS)) + return -EINVAL; + + return t14s_led_set_status(led->ec, led, T14S_EC_LED_BLINK); +} + +static int t14s_init_led(struct t14s_ec *ec, struct t14s_ec_led_classdev *led, + u8 id, const char *name) +{ + led->led_classdev.name = name; + led->led_classdev.flags = LED_RETAIN_AT_SHUTDOWN; + led->led_classdev.max_brightness = 1; + led->led_classdev.brightness_set_blocking = t14s_led_brightness_set; + led->led_classdev.blink_set = t14s_led_blink_set; + led->ec = ec; + led->led = id; + + return devm_led_classdev_register(ec->dev, &led->led_classdev); +} + +static int t14s_leds_probe(struct t14s_ec *ec) +{ + int ret; + + ret = t14s_init_led(ec, &ec->led_pwr_btn, 0, "platform::power"); + if (ret) + return ret; + + ret = t14s_init_led(ec, &ec->led_chrg_orange, 1, + "platform:amber:battery-charging"); + if (ret) + return ret; + + ret = t14s_init_led(ec, &ec->led_chrg_white, 2, + "platform:white:battery-full"); + if (ret) + return ret; + + ret = t14s_init_led(ec, &ec->led_lid_logo_dot, 10, + "platform::lid_logo_dot"); + if (ret) + return ret; + + return 0; +} + +static int t14s_kbd_bl_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct t14s_ec *ec = container_of(led_cdev, struct t14s_ec, + kbd_backlight); + int ret; + u8 val; + + val = FIELD_PREP(T14S_EC_KBD_BL1_MASK, brightness); + ret = regmap_update_bits(ec->regmap, T14S_EC_REG_KBD_BL1, + T14S_EC_KBD_BL1_MASK, val); + if (ret < 0) + return ret; + + val = FIELD_PREP(T14S_EC_KBD_BL2_MASK, brightness); + ret = regmap_update_bits(ec->regmap, T14S_EC_REG_KBD_BL2, + T14S_EC_KBD_BL2_MASK, val); + if (ret < 0) + return ret; + + return 0; +} + +static enum led_brightness t14s_kbd_bl_get(struct led_classdev *led_cdev) +{ + struct t14s_ec *ec = container_of(led_cdev, struct t14s_ec, + kbd_backlight); + unsigned int val; + int ret; + + ret = regmap_read(ec->regmap, T14S_EC_REG_KBD_BL1, &val); + if (ret < 0) + return ret; + + return FIELD_GET(T14S_EC_KBD_BL1_MASK, val); +} + +static void t14s_kbd_bl_update(struct t14s_ec *ec) +{ + enum led_brightness brightness = t14s_kbd_bl_get(&ec->kbd_backlight); + + led_classdev_notify_brightness_hw_changed(&ec->kbd_backlight, brightness); +} + +static int t14s_kbd_backlight_probe(struct t14s_ec *ec) +{ + ec->kbd_backlight.name = "platform::kbd_backlight"; + ec->kbd_backlight.flags = LED_BRIGHT_HW_CHANGED; + ec->kbd_backlight.max_brightness = 2; + ec->kbd_backlight.brightness_set_blocking = t14s_kbd_bl_set; + ec->kbd_backlight.brightness_get = t14s_kbd_bl_get; + + return devm_led_classdev_register(ec->dev, &ec->kbd_backlight); +} + +static enum led_brightness t14s_audio_led_get(struct t14s_ec *ec, u8 led_bit) +{ + unsigned int val; + int ret; + + ret = regmap_read(ec->regmap, T14S_EC_REG_AUD, &val); + if (ret < 0) + return ret; + + return !!(val & led_bit) ? LED_ON : LED_OFF; +} + +static enum led_brightness t14s_audio_led_set(struct t14s_ec *ec, + u8 led_mask, + enum led_brightness brightness) +{ + return regmap_assign_bits(ec->regmap, T14S_EC_REG_AUD, led_mask, brightness > 0); +} + +static enum led_brightness t14s_mic_mute_led_get(struct led_classdev *led_cdev) +{ + struct t14s_ec *ec = container_of(led_cdev, struct t14s_ec, + led_mic_mute); + + return t14s_audio_led_get(ec, T14S_EC_MIC_MUTE_LED); +} + +static int t14s_mic_mute_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct t14s_ec *ec = container_of(led_cdev, struct t14s_ec, + led_mic_mute); + + return t14s_audio_led_set(ec, T14S_EC_MIC_MUTE_LED, brightness); +} + +static enum led_brightness t14s_spk_mute_led_get(struct led_classdev *led_cdev) +{ + struct t14s_ec *ec = container_of(led_cdev, struct t14s_ec, + led_spk_mute); + + return t14s_audio_led_get(ec, T14S_EC_SPK_MUTE_LED); +} + +static int t14s_spk_mute_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct t14s_ec *ec = container_of(led_cdev, struct t14s_ec, + led_spk_mute); + + return t14s_audio_led_set(ec, T14S_EC_SPK_MUTE_LED, brightness); +} + +static int t14s_kbd_audio_led_probe(struct t14s_ec *ec) +{ + int ret; + + ec->led_mic_mute.name = "platform::micmute"; + ec->led_mic_mute.max_brightness = 1; + ec->led_mic_mute.default_trigger = "audio-micmute"; + ec->led_mic_mute.brightness_set_blocking = t14s_mic_mute_led_set; + ec->led_mic_mute.brightness_get = t14s_mic_mute_led_get; + + ec->led_spk_mute.name = "platform::mute"; + ec->led_spk_mute.max_brightness = 1; + ec->led_spk_mute.default_trigger = "audio-mute"; + ec->led_spk_mute.brightness_set_blocking = t14s_spk_mute_led_set; + ec->led_spk_mute.brightness_get = t14s_spk_mute_led_get; + + ret = devm_led_classdev_register(ec->dev, &ec->led_mic_mute); + if (ret) + return ret; + + return devm_led_classdev_register(ec->dev, &ec->led_spk_mute); +} + +static const struct key_entry t14s_keymap[] = { + T14S_EC_KEY_ENTRY(FN_4, KEY_SLEEP), + T14S_EC_KEY_ENTRY(FN_N, KEY_VENDOR), + T14S_EC_KEY_ENTRY(FN_F4, KEY_MICMUTE), + T14S_EC_KEY_ENTRY(FN_F7, KEY_SWITCHVIDEOMODE), + T14S_EC_KEY_ENTRY(FN_F8, KEY_PERFORMANCE), + T14S_EC_KEY_ENTRY(FN_F10, KEY_SELECTIVE_SCREENSHOT), + T14S_EC_KEY_ENTRY(FN_F11, KEY_LINK_PHONE), + T14S_EC_KEY_ENTRY(FN_F12, KEY_BOOKMARKS), + T14S_EC_KEY_ENTRY(FN_SPACE, KEY_KBDILLUMTOGGLE), + T14S_EC_KEY_ENTRY(FN_ESC, KEY_FN_ESC), + T14S_EC_KEY_ENTRY(FN_TAB, KEY_ZOOM), + T14S_EC_KEY_ENTRY(FN_RIGHT_SHIFT, KEY_FN_RIGHT_SHIFT), + T14S_EC_KEY_ENTRY(TP_DOUBLE_TAP, KEY_PROG4), + { KE_END } +}; + +static int t14s_input_probe(struct t14s_ec *ec) +{ + int ret; + + ec->inputdev = devm_input_allocate_device(ec->dev); + if (!ec->inputdev) + return -ENOMEM; + + ec->inputdev->name = "ThinkPad Extra Buttons"; + ec->inputdev->phys = "thinkpad/input0"; + ec->inputdev->id.bustype = BUS_HOST; + ec->inputdev->dev.parent = ec->dev; + + ret = sparse_keymap_setup(ec->inputdev, t14s_keymap, NULL); + if (ret) + return ret; + + return input_register_device(ec->inputdev); +} + +static irqreturn_t t14s_ec_irq_handler(int irq, void *data) +{ + struct t14s_ec *ec = data; + int ret; + u8 val; + + ret = t14s_ec_read_evt(ec, &val); + if (ret < 0) { + dev_err(ec->dev, "Failed to read event\n"); + return IRQ_HANDLED; + } + + switch (val) { + case T14S_EC_EVT_NONE: + break; + case T14S_EC_EVT_KEY_FN_SPACE: + t14s_kbd_bl_update(ec); + fallthrough; + case T14S_EC_EVT_KEY_FN_F4: + case T14S_EC_EVT_KEY_FN_F7: + case T14S_EC_EVT_KEY_FN_4: + case T14S_EC_EVT_KEY_FN_F8: + case T14S_EC_EVT_KEY_FN_F12: + case T14S_EC_EVT_KEY_FN_TAB: + case T14S_EC_EVT_KEY_FN_F10: + case T14S_EC_EVT_KEY_FN_N: + case T14S_EC_EVT_KEY_FN_F11: + case T14S_EC_EVT_KEY_FN_ESC: + case T14S_EC_EVT_KEY_FN_RIGHT_SHIFT: + case T14S_EC_EVT_KEY_TP_DOUBLE_TAP: + sparse_keymap_report_event(ec->inputdev, + T14S_EC_KEY_EVT_OFFSET + val, 1, true); + break; + case T14S_EC_EVT_AC_CONNECTED: + dev_dbg(ec->dev, "AC connected\n"); + break; + case T14S_EC_EVT_AC_DISCONNECTED: + dev_dbg(ec->dev, "AC disconnected\n"); + break; + case T14S_EC_EVT_KEY_POWER: + dev_dbg(ec->dev, "power button\n"); + break; + case T14S_EC_EVT_LID_OPEN: + dev_dbg(ec->dev, "LID open\n"); + break; + case T14S_EC_EVT_LID_CLOSED: + dev_dbg(ec->dev, "LID closed\n"); + break; + case T14S_EC_EVT_THERMAL_TZ40: + dev_dbg(ec->dev, "Thermal Zone 40 Status Change Event (CPU/GPU)\n"); + break; + case T14S_EC_EVT_THERMAL_TZ42: + dev_dbg(ec->dev, "Thermal Zone 42 Status Change Event (Battery)\n"); + break; + case T14S_EC_EVT_THERMAL_TZ39: + dev_dbg(ec->dev, "Thermal Zone 39 Status Change Event (CPU/GPU)\n"); + break; + case T14S_EC_EVT_KEY_FN_G: + dev_dbg(ec->dev, "FN + G - toggle double-tapping\n"); + break; + case T14S_EC_EVT_KEY_FN_L: + dev_dbg(ec->dev, "FN + L - low performance mode\n"); + break; + case T14S_EC_EVT_KEY_FN_M: + dev_dbg(ec->dev, "FN + M - medium performance mode\n"); + break; + case T14S_EC_EVT_KEY_FN_H: + dev_dbg(ec->dev, "FN + H - high performance mode\n"); + break; + case T14S_EC_EVT_KEY_FN_T: + dev_dbg(ec->dev, "FN + T - toggle intelligent cooling mode\n"); + break; + case T14S_EC_EVT_KEY_FN_D: + dev_dbg(ec->dev, "FN + D - toggle privacy guard mode\n"); + break; + default: + dev_info(ec->dev, "Unknown EC event: 0x%02x\n", val); + break; + } + + return IRQ_HANDLED; +} + +static int t14s_ec_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct t14s_ec *ec; + int ret; + + ec = devm_kzalloc(dev, sizeof(*ec), GFP_KERNEL); + if (!ec) + return -ENOMEM; + + ec->dev = dev; + i2c_set_clientdata(client, ec); + + ec->regmap = devm_regmap_init(dev, &t14s_ec_regmap_bus, + ec, &t14s_ec_regmap_config); + if (IS_ERR(ec->regmap)) + return dev_err_probe(dev, PTR_ERR(ec->regmap), + "Failed to init regmap\n"); + + ret = t14s_leds_probe(ec); + if (ret < 0) + return ret; + + ret = t14s_kbd_backlight_probe(ec); + if (ret < 0) + return ret; + + ret = t14s_kbd_audio_led_probe(ec); + if (ret < 0) + return ret; + + ret = t14s_input_probe(ec); + if (ret < 0) + return ret; + + ret = devm_request_threaded_irq(dev, client->irq, NULL, + t14s_ec_irq_handler, + IRQF_ONESHOT, dev_name(dev), ec); + if (ret < 0) + return dev_err_probe(dev, ret, "Failed to get IRQ\n"); + + /* + * Disable wakeup support by default, because the driver currently does + * not support masking any events and the laptop should not wake up when + * the LID is closed. + */ + device_wakeup_disable(dev); + + return 0; +} + +static int t14s_ec_suspend(struct device *dev) +{ + struct t14s_ec *ec = dev_get_drvdata(dev); + + led_classdev_suspend(&ec->kbd_backlight); + + t14s_ec_write_sequence(ec, T14S_EC_REG_MODERN_STANDBY, + T14S_EC_MODERN_STANDBY_ENTRY, 3); + + return 0; +} + +static int t14s_ec_resume(struct device *dev) +{ + struct t14s_ec *ec = dev_get_drvdata(dev); + + t14s_ec_write_sequence(ec, T14S_EC_REG_MODERN_STANDBY, + T14S_EC_MODERN_STANDBY_EXIT, 3); + + led_classdev_resume(&ec->kbd_backlight); + + return 0; +} + +static const struct of_device_id t14s_ec_of_match[] = { + { .compatible = "lenovo,thinkpad-t14s-ec" }, + {} +}; +MODULE_DEVICE_TABLE(of, t14s_ec_of_match); + +static const struct i2c_device_id t14s_ec_i2c_id_table[] = { + { .name = "thinkpad-t14s-ec" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, t14s_ec_i2c_id_table); + +static const struct dev_pm_ops t14s_ec_pm_ops = { + SYSTEM_SLEEP_PM_OPS(t14s_ec_suspend, t14s_ec_resume) +}; + +static struct i2c_driver t14s_ec_i2c_driver = { + .driver = { + .name = "thinkpad-t14s-ec", + .of_match_table = t14s_ec_of_match, + .pm = &t14s_ec_pm_ops, + }, + .probe = t14s_ec_probe, + .id_table = t14s_ec_i2c_id_table, +}; +module_i2c_driver(t14s_ec_i2c_driver); + +MODULE_AUTHOR("Sebastian Reichel <sre@kernel.org>"); +MODULE_DESCRIPTION("Lenovo Thinkpad T14s Embedded Controller"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/arm64/lenovo-yoga-c630.c b/drivers/platform/arm64/lenovo-yoga-c630.c new file mode 100644 index 000000000..a8600a977 --- /dev/null +++ b/drivers/platform/arm64/lenovo-yoga-c630.c @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2022-2024, Linaro Ltd + * Authors: + * Bjorn Andersson + * Dmitry Baryshkov + */ +#include <linux/auxiliary_bus.h> +#include <linux/cleanup.h> +#include <linux/device.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/irqreturn.h> +#include <linux/lockdep.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/notifier.h> +#include <linux/slab.h> +#include <linux/platform_data/lenovo-yoga-c630.h> + +#define LENOVO_EC_RESPONSE_REG 0x01 +#define LENOVO_EC_REQUEST_REG 0x02 + +#define LENOVO_EC_UCSI_WRITE 0x20 +#define LENOVO_EC_UCSI_READ 0x21 + +#define LENOVO_EC_READ_REG 0xb0 +#define LENOVO_EC_REQUEST_NEXT_EVENT 0x84 + +#define LENOVO_EC_UCSI_VERSION 0x20 + +struct yoga_c630_ec { + struct i2c_client *client; + struct mutex lock; + struct blocking_notifier_head notifier_list; +}; + +static int yoga_c630_ec_request(struct yoga_c630_ec *ec, u8 *req, size_t req_len, + u8 *resp, size_t resp_len) +{ + int ret; + + lockdep_assert_held(&ec->lock); + + ret = i2c_smbus_write_i2c_block_data(ec->client, LENOVO_EC_REQUEST_REG, + req_len, req); + if (ret < 0) + return ret; + + return i2c_smbus_read_i2c_block_data(ec->client, LENOVO_EC_RESPONSE_REG, + resp_len, resp); +} + +int yoga_c630_ec_read8(struct yoga_c630_ec *ec, u8 addr) +{ + u8 req[2] = { LENOVO_EC_READ_REG, }; + int ret; + u8 val; + + guard(mutex)(&ec->lock); + + req[1] = addr; + ret = yoga_c630_ec_request(ec, req, sizeof(req), &val, 1); + if (ret < 0) + return ret; + + return val; +} +EXPORT_SYMBOL_GPL(yoga_c630_ec_read8); + +int yoga_c630_ec_read16(struct yoga_c630_ec *ec, u8 addr) +{ + u8 req[2] = { LENOVO_EC_READ_REG, }; + int ret; + u8 msb; + u8 lsb; + + /* don't overflow the address */ + if (addr == 0xff) + return -EINVAL; + + guard(mutex)(&ec->lock); + + req[1] = addr; + ret = yoga_c630_ec_request(ec, req, sizeof(req), &lsb, 1); + if (ret < 0) + return ret; + + req[1] = addr + 1; + ret = yoga_c630_ec_request(ec, req, sizeof(req), &msb, 1); + if (ret < 0) + return ret; + + return msb << 8 | lsb; +} +EXPORT_SYMBOL_GPL(yoga_c630_ec_read16); + +u16 yoga_c630_ec_ucsi_get_version(struct yoga_c630_ec *ec) +{ + u8 req[3] = { 0xb3, 0xf2, }; + int ret; + u8 msb; + u8 lsb; + + guard(mutex)(&ec->lock); + + req[2] = LENOVO_EC_UCSI_VERSION; + ret = yoga_c630_ec_request(ec, req, sizeof(req), &lsb, 1); + if (ret < 0) + return ret; + + req[2] = LENOVO_EC_UCSI_VERSION + 1; + ret = yoga_c630_ec_request(ec, req, sizeof(req), &msb, 1); + if (ret < 0) + return ret; + + return msb << 8 | lsb; +} +EXPORT_SYMBOL_GPL(yoga_c630_ec_ucsi_get_version); + +int yoga_c630_ec_ucsi_write(struct yoga_c630_ec *ec, + const u8 req[YOGA_C630_UCSI_WRITE_SIZE]) +{ + int ret; + + mutex_lock(&ec->lock); + ret = i2c_smbus_write_i2c_block_data(ec->client, LENOVO_EC_UCSI_WRITE, + YOGA_C630_UCSI_WRITE_SIZE, req); + mutex_unlock(&ec->lock); + + return ret < 0 ? ret : 0; +} +EXPORT_SYMBOL_GPL(yoga_c630_ec_ucsi_write); + +int yoga_c630_ec_ucsi_read(struct yoga_c630_ec *ec, + u8 resp[YOGA_C630_UCSI_READ_SIZE]) +{ + int ret; + + mutex_lock(&ec->lock); + ret = i2c_smbus_read_i2c_block_data(ec->client, LENOVO_EC_UCSI_READ, + YOGA_C630_UCSI_READ_SIZE, resp); + mutex_unlock(&ec->lock); + + return ret < 0 ? ret : 0; +} +EXPORT_SYMBOL_GPL(yoga_c630_ec_ucsi_read); + +static irqreturn_t yoga_c630_ec_thread_intr(int irq, void *data) +{ + u8 req[] = { LENOVO_EC_REQUEST_NEXT_EVENT }; + struct yoga_c630_ec *ec = data; + u8 event; + int ret; + + mutex_lock(&ec->lock); + ret = yoga_c630_ec_request(ec, req, sizeof(req), &event, 1); + mutex_unlock(&ec->lock); + if (ret < 0) + return IRQ_HANDLED; + + blocking_notifier_call_chain(&ec->notifier_list, event, ec); + + return IRQ_HANDLED; +} + +/** + * yoga_c630_ec_register_notify - Register a notifier callback for EC events. + * @ec: Yoga C630 EC + * @nb: Notifier block pointer to register + * + * Return: 0 on success or negative error code. + */ +int yoga_c630_ec_register_notify(struct yoga_c630_ec *ec, struct notifier_block *nb) +{ + return blocking_notifier_chain_register(&ec->notifier_list, nb); +} +EXPORT_SYMBOL_GPL(yoga_c630_ec_register_notify); + +/** + * yoga_c630_ec_unregister_notify - Unregister notifier callback for EC events. + * @ec: Yoga C630 EC + * @nb: Notifier block pointer to unregister + * + * Unregister a notifier callback that was previously registered with + * yoga_c630_ec_register_notify(). + */ +void yoga_c630_ec_unregister_notify(struct yoga_c630_ec *ec, struct notifier_block *nb) +{ + blocking_notifier_chain_unregister(&ec->notifier_list, nb); +} +EXPORT_SYMBOL_GPL(yoga_c630_ec_unregister_notify); + +static int yoga_c630_aux_init(struct device *parent, const char *name, + struct yoga_c630_ec *ec) +{ + struct auxiliary_device *adev; + + adev = devm_auxiliary_device_create(parent, name, ec); + if (!adev) + return -ENODEV; + + return 0; +} + +static int yoga_c630_ec_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct yoga_c630_ec *ec; + int ret; + + ec = devm_kzalloc(dev, sizeof(*ec), GFP_KERNEL); + if (!ec) + return -ENOMEM; + + mutex_init(&ec->lock); + ec->client = client; + BLOCKING_INIT_NOTIFIER_HEAD(&ec->notifier_list); + + ret = devm_request_threaded_irq(dev, client->irq, + NULL, yoga_c630_ec_thread_intr, + IRQF_ONESHOT, "yoga_c630_ec", ec); + if (ret < 0) + return dev_err_probe(dev, ret, "unable to request irq\n"); + + ret = yoga_c630_aux_init(dev, YOGA_C630_DEV_PSY, ec); + if (ret) + return ret; + + return yoga_c630_aux_init(dev, YOGA_C630_DEV_UCSI, ec); +} + + +static const struct of_device_id yoga_c630_ec_of_match[] = { + { .compatible = "lenovo,yoga-c630-ec" }, + {} +}; +MODULE_DEVICE_TABLE(of, yoga_c630_ec_of_match); + +static const struct i2c_device_id yoga_c630_ec_i2c_id_table[] = { + { .name = "yoga-c630-ec" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, yoga_c630_ec_i2c_id_table); + +static struct i2c_driver yoga_c630_ec_i2c_driver = { + .driver = { + .name = "yoga-c630-ec", + .of_match_table = yoga_c630_ec_of_match + }, + .probe = yoga_c630_ec_probe, + .id_table = yoga_c630_ec_i2c_id_table, +}; +module_i2c_driver(yoga_c630_ec_i2c_driver); + +MODULE_DESCRIPTION("Lenovo Yoga C630 Embedded Controller"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/arm64/qcom-hamoa-ec.c b/drivers/platform/arm64/qcom-hamoa-ec.c new file mode 100644 index 000000000..5ca7308c6 --- /dev/null +++ b/drivers/platform/arm64/qcom-hamoa-ec.c @@ -0,0 +1,451 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2024 Maya Matuszczyk <maccraft123mc@gmail.com> + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + */ + +#include <linux/bitfield.h> +#include <linux/bits.h> +#include <linux/device.h> +#include <linux/dev_printk.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/pm.h> +#include <linux/slab.h> +#include <linux/thermal.h> + +#define EC_SCI_EVT_READ_CMD 0x05 +#define EC_FW_VERSION_CMD 0x0e +#define EC_MODERN_STANDBY_CMD 0x23 +#define EC_FAN_DBG_CONTROL_CMD 0x30 +#define EC_SCI_EVT_CONTROL_CMD 0x35 +#define EC_THERMAL_CAP_CMD 0x42 + +#define EC_FW_VERSION_RESP_LEN 4 +#define EC_THERMAL_CAP_RESP_LEN 3 +#define EC_FAN_DEBUG_CMD_LEN 6 +#define EC_FAN_SPEED_DATA_SIZE 4 + +#define EC_MODERN_STANDBY_ENTER 0x01 +#define EC_MODERN_STANDBY_EXIT 0x00 + +#define EC_FAN_DEBUG_MODE_OFF 0 +#define EC_FAN_DEBUG_MODE_ON BIT(0) +#define EC_FAN_ON BIT(1) +#define EC_FAN_DEBUG_TYPE_PWM BIT(2) +#define EC_MAX_FAN_CNT 2 +#define EC_FAN_NAME_SIZE 20 +#define EC_FAN_MAX_PWM 255 + +enum qcom_ec_sci_events { + EC_FAN1_STATUS_CHANGE_EVT = 0x30, + EC_FAN2_STATUS_CHANGE_EVT, + EC_FAN1_SPEED_CHANGE_EVT, + EC_FAN2_SPEED_CHANGE_EVT, + EC_NEW_LUT_SET_EVT, + EC_FAN_PROFILE_SWITCH_EVT, + EC_THERMISTOR_1_THRESHOLD_CROSS_EVT, + EC_THERMISTOR_2_THRESHOLD_CROSS_EVT, + EC_THERMISTOR_3_THRESHOLD_CROSS_EVT, + /* Reserved: 0x39 - 0x3c/0x3f */ + EC_RECOVERED_FROM_RESET_EVT = 0x3d, +}; + +struct qcom_ec_version { + u8 main_version; + u8 sub_version; + u8 test_version; +}; + +struct qcom_ec_thermal_cap { +#define EC_THERMAL_FAN_CNT(x) (FIELD_GET(GENMASK(1, 0), (x))) +#define EC_THERMAL_FAN_TYPE(x) (FIELD_GET(GENMASK(4, 2), (x))) +#define EC_THERMAL_THERMISTOR_MASK(x) (FIELD_GET(GENMASK(7, 0), (x))) + u8 fan_cnt; + u8 fan_type; + u8 thermistor_mask; +}; + +struct qcom_ec_cooling_dev { + struct thermal_cooling_device *cdev; + struct device *parent_dev; + u8 fan_id; + u8 state; +}; + +struct qcom_ec { + struct qcom_ec_cooling_dev *ec_cdev; + struct qcom_ec_thermal_cap thermal_cap; + struct qcom_ec_version version; + struct i2c_client *client; +}; + +static int qcom_ec_read(struct qcom_ec *ec, u8 cmd, u8 resp_len, u8 *resp) +{ + int ret; + + ret = i2c_smbus_read_i2c_block_data(ec->client, cmd, resp_len, resp); + if (ret < 0) + return ret; + else if (ret == 0 || ret == 0xff) + return -EOPNOTSUPP; + + if (resp[0] >= resp_len) + return -EINVAL; + + return 0; +} + +/* + * EC Device Firmware Version: + * + * Read Response: + * ---------------------------------------------------------------------- + * | Offset | Name | Description | + * ---------------------------------------------------------------------- + * | 0x00 | Byte count | Number of bytes in response | + * | | | (excluding byte count) | + * ---------------------------------------------------------------------- + * | 0x01 | Test-version | Test-version of EC firmware | + * ---------------------------------------------------------------------- + * | 0x02 | Sub-version | Sub-version of EC firmware | + * ---------------------------------------------------------------------- + * | 0x03 | Main-version | Main-version of EC firmware | + * ---------------------------------------------------------------------- + * + */ +static int qcom_ec_read_fw_version(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct qcom_ec *ec = i2c_get_clientdata(client); + struct qcom_ec_version *version = &ec->version; + u8 resp[EC_FW_VERSION_RESP_LEN]; + int ret; + + ret = qcom_ec_read(ec, EC_FW_VERSION_CMD, EC_FW_VERSION_RESP_LEN, resp); + if (ret < 0) + return ret; + + version->main_version = resp[3]; + version->sub_version = resp[2]; + version->test_version = resp[1]; + + dev_dbg(dev, "EC Version %d.%d.%d\n", + version->main_version, version->sub_version, version->test_version); + + return 0; +} + +/* + * EC Device Thermal Capabilities: + * + * Read Response: + * ------------------------------------------------------------------------------ + * | Offset | Name | Description | + * ------------------------------------------------------------------------------ + * | 0x00 | Byte count | Number of bytes in response | + * | | | (excluding byte count) | + * ------------------------------------------------------------------------------ + * | 0x02 (LSB) | EC Thermal | Bit 0-1: Number of fans | + * | 0x03 | Capabilities | Bit 2-4: Type of fan | + * | | | Bit 5-6: Reserved | + * | | | Bit 7: Data Valid/Invalid | + * | | | (Valid - 1, Invalid - 0) | + * | | | Bit 8-15: Thermistor 0 - 7 presence | + * | | | (1 present, 0 absent) | + * ------------------------------------------------------------------------------ + * + */ +static int qcom_ec_thermal_capabilities(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct qcom_ec *ec = i2c_get_clientdata(client); + struct qcom_ec_thermal_cap *cap = &ec->thermal_cap; + u8 resp[EC_THERMAL_CAP_RESP_LEN]; + int ret; + + ret = qcom_ec_read(ec, EC_THERMAL_CAP_CMD, EC_THERMAL_CAP_RESP_LEN, resp); + if (ret < 0) + return ret; + + cap->fan_cnt = min(EC_MAX_FAN_CNT, EC_THERMAL_FAN_CNT(resp[1])); + cap->fan_type = EC_THERMAL_FAN_TYPE(resp[1]); + cap->thermistor_mask = EC_THERMAL_THERMISTOR_MASK(resp[2]); + + dev_dbg(dev, "Fan count: %d Fan Type: %d Thermistor Mask: %x\n", + cap->fan_cnt, cap->fan_type, cap->thermistor_mask); + + return 0; +} + +static irqreturn_t qcom_ec_irq(int irq, void *data) +{ + struct qcom_ec *ec = data; + struct device *dev = &ec->client->dev; + int val; + + val = i2c_smbus_read_byte_data(ec->client, EC_SCI_EVT_READ_CMD); + if (val < 0) { + dev_err_ratelimited(dev, "Failed to read EC SCI Event: %d\n", val); + return IRQ_HANDLED; + } + + switch (val) { + case EC_FAN1_STATUS_CHANGE_EVT: + dev_dbg_ratelimited(dev, "Fan1 status changed\n"); + break; + case EC_FAN2_STATUS_CHANGE_EVT: + dev_dbg_ratelimited(dev, "Fan2 status changed\n"); + break; + case EC_FAN1_SPEED_CHANGE_EVT: + dev_dbg_ratelimited(dev, "Fan1 speed crossed low/high trip point\n"); + break; + case EC_FAN2_SPEED_CHANGE_EVT: + dev_dbg_ratelimited(dev, "Fan2 speed crossed low/high trip point\n"); + break; + case EC_NEW_LUT_SET_EVT: + dev_dbg_ratelimited(dev, "New LUT set\n"); + break; + case EC_FAN_PROFILE_SWITCH_EVT: + dev_dbg_ratelimited(dev, "FAN Profile switched\n"); + break; + case EC_THERMISTOR_1_THRESHOLD_CROSS_EVT: + dev_dbg_ratelimited(dev, "Thermistor 1 threshold crossed\n"); + break; + case EC_THERMISTOR_2_THRESHOLD_CROSS_EVT: + dev_dbg_ratelimited(dev, "Thermistor 2 threshold crossed\n"); + break; + case EC_THERMISTOR_3_THRESHOLD_CROSS_EVT: + dev_dbg_ratelimited(dev, "Thermistor 3 threshold crossed\n"); + break; + case EC_RECOVERED_FROM_RESET_EVT: + dev_dbg_ratelimited(dev, "EC recovered from reset\n"); + break; + default: + dev_notice_ratelimited(dev, "Unknown EC event: %d\n", val); + break; + } + + return IRQ_HANDLED; +} + +static int qcom_ec_sci_evt_control(struct device *dev, bool enable) +{ + struct i2c_client *client = to_i2c_client(dev); + + return i2c_smbus_write_byte_data(client, EC_SCI_EVT_CONTROL_CMD, enable ? 1 : 0); +} + +static int qcom_ec_fan_get_max_state(struct thermal_cooling_device *cdev, unsigned long *state) +{ + *state = EC_FAN_MAX_PWM; + + return 0; +} + +static int qcom_ec_fan_get_cur_state(struct thermal_cooling_device *cdev, unsigned long *state) +{ + struct qcom_ec_cooling_dev *ec_cdev = cdev->devdata; + + *state = ec_cdev->state; + + return 0; +} + +/* + * Fan Debug control command: + * + * Command Payload: + * -------------------------------------------------------------------------------------- + * | Offset | Name | Description | + * -------------------------------------------------------------------------------------- + * | 0x00 | Command | Fan control command | + * -------------------------------------------------------------------------------------- + * | 0x01 | Fan ID | 0x1 : Fan 1 | + * | | | 0x2 : Fan 2 | + * -------------------------------------------------------------------------------------- + * | 0x02 | Byte count = 4| Size of data to set fan speed | + * -------------------------------------------------------------------------------------- + * | 0x03 | Mode | Bit 0: Debug Mode On/Off (0 - OFF, 1 - ON ) | + * | | | Bit 1: Fan On/Off (0 - Off, 1 - ON) | + * | | | Bit 2: Debug Type (0 - RPM, 1 - PWM) | + * -------------------------------------------------------------------------------------- + * | 0x04 (LSB) | Speed in RPM | RPM value, if mode selected is RPM | + * | 0x05 | | | + * -------------------------------------------------------------------------------------- + * | 0x06 | Speed in PWM | PWM value, if mode selected is PWM (0 - 255) | + * ______________________________________________________________________________________ + * + */ +static int qcom_ec_fan_debug_mode_off(struct qcom_ec_cooling_dev *ec_cdev) +{ + struct device *dev = ec_cdev->parent_dev; + struct i2c_client *client = to_i2c_client(dev); + u8 request[6] = { ec_cdev->fan_id, EC_FAN_SPEED_DATA_SIZE, + EC_FAN_DEBUG_MODE_OFF, 0, 0, 0 }; + int ret; + + ret = i2c_smbus_write_i2c_block_data(client, EC_FAN_DBG_CONTROL_CMD, + sizeof(request), request); + if (ret) { + dev_err(dev, "Failed to turn off fan%d debug mode: %d\n", + ec_cdev->fan_id, ret); + } + + return ret; +} + +static int qcom_ec_fan_set_cur_state(struct thermal_cooling_device *cdev, unsigned long state) +{ + struct qcom_ec_cooling_dev *ec_cdev = cdev->devdata; + struct device *dev = ec_cdev->parent_dev; + struct i2c_client *client = to_i2c_client(dev); + u8 request[6] = { ec_cdev->fan_id, EC_FAN_SPEED_DATA_SIZE, + EC_FAN_DEBUG_MODE_ON | EC_FAN_ON | EC_FAN_DEBUG_TYPE_PWM, + 0, 0, state }; + int ret; + + ret = i2c_smbus_write_i2c_block_data(client, EC_FAN_DBG_CONTROL_CMD, + sizeof(request), request); + if (ret) { + dev_err(dev, "Failed to set fan pwm: %d\n", ret); + return ret; + } + + ec_cdev->state = state; + + return 0; +} + +static const struct thermal_cooling_device_ops qcom_ec_thermal_ops = { + .get_max_state = qcom_ec_fan_get_max_state, + .get_cur_state = qcom_ec_fan_get_cur_state, + .set_cur_state = qcom_ec_fan_set_cur_state, +}; + +static int qcom_ec_resume(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + + return i2c_smbus_write_byte_data(client, EC_MODERN_STANDBY_CMD, + EC_MODERN_STANDBY_EXIT); +} + +static int qcom_ec_suspend(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + + return i2c_smbus_write_byte_data(client, EC_MODERN_STANDBY_CMD, + EC_MODERN_STANDBY_ENTER); +} + +static int qcom_ec_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct qcom_ec *ec; + unsigned int i; + int ret; + + ec = devm_kzalloc(dev, sizeof(*ec), GFP_KERNEL); + if (!ec) + return -ENOMEM; + + ec->client = client; + + ret = devm_request_threaded_irq(dev, client->irq, NULL, qcom_ec_irq, + IRQF_ONESHOT, "qcom_ec", ec); + if (ret < 0) + return ret; + + i2c_set_clientdata(client, ec); + + ret = qcom_ec_read_fw_version(dev); + if (ret < 0) + return dev_err_probe(dev, ret, "Failed to read EC firmware version\n"); + + ret = qcom_ec_sci_evt_control(dev, true); + if (ret < 0) + return dev_err_probe(dev, ret, "Failed to enable SCI events\n"); + + ret = qcom_ec_thermal_capabilities(dev); + if (ret < 0) + return dev_err_probe(dev, ret, "Failed to read thermal capabilities\n"); + + if (ec->thermal_cap.fan_cnt == 0) { + dev_warn(dev, FW_BUG "Failed to get fan count, firmware update required\n"); + return 0; + } + + ec->ec_cdev = devm_kcalloc(dev, ec->thermal_cap.fan_cnt, sizeof(*ec->ec_cdev), GFP_KERNEL); + if (!ec->ec_cdev) + return -ENOMEM; + + for (i = 0; i < ec->thermal_cap.fan_cnt; i++) { + struct qcom_ec_cooling_dev *ec_cdev = &ec->ec_cdev[i]; + char name[EC_FAN_NAME_SIZE]; + + scnprintf(name, sizeof(name), "qcom_ec_fan_%u", i); + ec_cdev->fan_id = i + 1; + ec_cdev->parent_dev = dev; + + ec_cdev->cdev = devm_thermal_of_child_cooling_device_register(dev, NULL, name, ec_cdev, + &qcom_ec_thermal_ops); + if (IS_ERR(ec_cdev->cdev)) { + return dev_err_probe(dev, PTR_ERR(ec_cdev->cdev), + "Failed to register fan%d cooling device\n", i); + } + } + + return 0; +} + +static void qcom_ec_remove(struct i2c_client *client) +{ + struct qcom_ec *ec = i2c_get_clientdata(client); + struct device *dev = &client->dev; + int ret; + + ret = qcom_ec_sci_evt_control(dev, false); + if (ret < 0) + dev_err(dev, "Failed to disable SCI events: %d\n", ret); + + for (int i = 0; i < ec->thermal_cap.fan_cnt; i++) { + struct qcom_ec_cooling_dev *ec_cdev = &ec->ec_cdev[i]; + + qcom_ec_fan_debug_mode_off(ec_cdev); + } +} + +static const struct of_device_id qcom_ec_of_match[] = { + { .compatible = "qcom,hamoa-crd-ec" }, + {} +}; +MODULE_DEVICE_TABLE(of, qcom_ec_of_match); + +static const struct i2c_device_id qcom_ec_i2c_id_table[] = { + { "qcom-hamoa-ec", }, + {} +}; +MODULE_DEVICE_TABLE(i2c, qcom_ec_i2c_id_table); + +static DEFINE_SIMPLE_DEV_PM_OPS(qcom_ec_pm_ops, + qcom_ec_suspend, + qcom_ec_resume); + +static struct i2c_driver qcom_ec_i2c_driver = { + .driver = { + .name = "qcom-hamoa-ec", + .of_match_table = qcom_ec_of_match, + .pm = &qcom_ec_pm_ops, + }, + .probe = qcom_ec_probe, + .remove = qcom_ec_remove, + .id_table = qcom_ec_i2c_id_table, +}; +module_i2c_driver(qcom_ec_i2c_driver); + +MODULE_DESCRIPTION("QCOM Hamoa Embedded Controller"); +MODULE_LICENSE("GPL"); |
