mirror-linux/drivers/platform/x86/ayaneo-ec.c

594 lines
13 KiB
C

// SPDX-License-Identifier: GPL-2.0+
/*
* Platform driver for the Embedded Controller (EC) of Ayaneo devices. Handles
* hwmon (fan speed, fan control), battery charge limits, and magic module
* control (connected modules, controller disconnection).
*
* Copyright (C) 2025 Antheas Kapenekakis <lkml@antheas.dev>
*/
#include <linux/acpi.h>
#include <linux/bits.h>
#include <linux/dmi.h>
#include <linux/err.h>
#include <linux/hwmon.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/pm.h>
#include <linux/power_supply.h>
#include <linux/sysfs.h>
#include <acpi/battery.h>
#define AYANEO_PWM_ENABLE_REG 0x4A
#define AYANEO_PWM_REG 0x4B
#define AYANEO_PWM_MODE_AUTO 0x00
#define AYANEO_PWM_MODE_MANUAL 0x01
#define AYANEO_FAN_REG 0x76
#define EC_CHARGE_CONTROL_BEHAVIOURS \
(BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO) | \
BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE))
#define AYANEO_CHARGE_REG 0x1e
#define AYANEO_CHARGE_VAL_AUTO 0xaa
#define AYANEO_CHARGE_VAL_INHIBIT 0x55
#define AYANEO_POWER_REG 0x2d
#define AYANEO_POWER_OFF 0xfe
#define AYANEO_POWER_ON 0xff
#define AYANEO_MODULE_REG 0x2f
#define AYANEO_MODULE_LEFT BIT(0)
#define AYANEO_MODULE_RIGHT BIT(1)
#define AYANEO_MODULE_MASK (AYANEO_MODULE_LEFT | AYANEO_MODULE_RIGHT)
struct ayaneo_ec_quirk {
bool has_fan_control;
bool has_charge_control;
bool has_magic_modules;
};
struct ayaneo_ec_platform_data {
struct platform_device *pdev;
struct ayaneo_ec_quirk *quirks;
struct acpi_battery_hook battery_hook;
// Protects access to restore_pwm
struct mutex hwmon_lock;
bool restore_charge_limit;
bool restore_pwm;
};
static const struct ayaneo_ec_quirk quirk_fan = {
.has_fan_control = true,
};
static const struct ayaneo_ec_quirk quirk_charge_limit = {
.has_fan_control = true,
.has_charge_control = true,
};
static const struct ayaneo_ec_quirk quirk_ayaneo3 = {
.has_fan_control = true,
.has_charge_control = true,
.has_magic_modules = true,
};
static const struct dmi_system_id dmi_table[] = {
{
.matches = {
DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"),
},
.driver_data = (void *)&quirk_fan,
},
{
.matches = {
DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
DMI_MATCH(DMI_BOARD_NAME, "FLIP"),
},
.driver_data = (void *)&quirk_fan,
},
{
.matches = {
DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
DMI_MATCH(DMI_BOARD_NAME, "GEEK"),
},
.driver_data = (void *)&quirk_fan,
},
{
.matches = {
DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"),
},
.driver_data = (void *)&quirk_charge_limit,
},
{
.matches = {
DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR 1S"),
},
.driver_data = (void *)&quirk_charge_limit,
},
{
.matches = {
DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
DMI_EXACT_MATCH(DMI_BOARD_NAME, "AB05-Mendocino"),
},
.driver_data = (void *)&quirk_charge_limit,
},
{
.matches = {
DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"),
},
.driver_data = (void *)&quirk_charge_limit,
},
{
.matches = {
DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"),
},
.driver_data = (void *)&quirk_charge_limit,
},
{
.matches = {
DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
},
.driver_data = (void *)&quirk_ayaneo3,
},
{},
};
/* Callbacks for hwmon interface */
static umode_t ayaneo_ec_hwmon_is_visible(const void *drvdata,
enum hwmon_sensor_types type, u32 attr,
int channel)
{
switch (type) {
case hwmon_fan:
return 0444;
case hwmon_pwm:
return 0644;
default:
return 0;
}
}
static int ayaneo_ec_read(struct device *dev, enum hwmon_sensor_types type,
u32 attr, int channel, long *val)
{
u8 tmp;
int ret;
switch (type) {
case hwmon_fan:
switch (attr) {
case hwmon_fan_input:
ret = ec_read(AYANEO_FAN_REG, &tmp);
if (ret)
return ret;
*val = tmp << 8;
ret = ec_read(AYANEO_FAN_REG + 1, &tmp);
if (ret)
return ret;
*val |= tmp;
return 0;
default:
break;
}
break;
case hwmon_pwm:
switch (attr) {
case hwmon_pwm_input:
ret = ec_read(AYANEO_PWM_REG, &tmp);
if (ret)
return ret;
if (tmp > 100)
return -EIO;
*val = (255 * tmp) / 100;
return 0;
case hwmon_pwm_enable:
ret = ec_read(AYANEO_PWM_ENABLE_REG, &tmp);
if (ret)
return ret;
if (tmp == AYANEO_PWM_MODE_MANUAL)
*val = 1;
else if (tmp == AYANEO_PWM_MODE_AUTO)
*val = 2;
else
return -EIO;
return 0;
default:
break;
}
break;
default:
break;
}
return -EOPNOTSUPP;
}
static int ayaneo_ec_write(struct device *dev, enum hwmon_sensor_types type,
u32 attr, int channel, long val)
{
struct ayaneo_ec_platform_data *data = dev_get_drvdata(dev);
int ret;
guard(mutex)(&data->hwmon_lock);
switch (type) {
case hwmon_pwm:
switch (attr) {
case hwmon_pwm_enable:
data->restore_pwm = false;
switch (val) {
case 1:
return ec_write(AYANEO_PWM_ENABLE_REG,
AYANEO_PWM_MODE_MANUAL);
case 2:
return ec_write(AYANEO_PWM_ENABLE_REG,
AYANEO_PWM_MODE_AUTO);
default:
return -EINVAL;
}
case hwmon_pwm_input:
if (val < 0 || val > 255)
return -EINVAL;
if (data->restore_pwm) {
/*
* Defer restoring PWM control to after
* userspace resumes successfully
*/
ret = ec_write(AYANEO_PWM_ENABLE_REG,
AYANEO_PWM_MODE_MANUAL);
if (ret)
return ret;
data->restore_pwm = false;
}
return ec_write(AYANEO_PWM_REG, (val * 100) / 255);
default:
break;
}
break;
default:
break;
}
return -EOPNOTSUPP;
}
static const struct hwmon_ops ayaneo_ec_hwmon_ops = {
.is_visible = ayaneo_ec_hwmon_is_visible,
.read = ayaneo_ec_read,
.write = ayaneo_ec_write,
};
static const struct hwmon_channel_info *const ayaneo_ec_sensors[] = {
HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT),
HWMON_CHANNEL_INFO(pwm, HWMON_PWM_INPUT | HWMON_PWM_ENABLE),
NULL,
};
static const struct hwmon_chip_info ayaneo_ec_chip_info = {
.ops = &ayaneo_ec_hwmon_ops,
.info = ayaneo_ec_sensors,
};
static int ayaneo_psy_ext_get_prop(struct power_supply *psy,
const struct power_supply_ext *ext,
void *data,
enum power_supply_property psp,
union power_supply_propval *val)
{
int ret;
u8 tmp;
switch (psp) {
case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR:
ret = ec_read(AYANEO_CHARGE_REG, &tmp);
if (ret)
return ret;
if (tmp == AYANEO_CHARGE_VAL_INHIBIT)
val->intval = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE;
else
val->intval = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO;
return 0;
default:
return -EINVAL;
}
}
static int ayaneo_psy_ext_set_prop(struct power_supply *psy,
const struct power_supply_ext *ext,
void *data,
enum power_supply_property psp,
const union power_supply_propval *val)
{
u8 raw_val;
switch (psp) {
case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR:
switch (val->intval) {
case POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO:
raw_val = AYANEO_CHARGE_VAL_AUTO;
break;
case POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE:
raw_val = AYANEO_CHARGE_VAL_INHIBIT;
break;
default:
return -EINVAL;
}
return ec_write(AYANEO_CHARGE_REG, raw_val);
default:
return -EINVAL;
}
}
static int ayaneo_psy_prop_is_writeable(struct power_supply *psy,
const struct power_supply_ext *ext,
void *data,
enum power_supply_property psp)
{
return true;
}
static const enum power_supply_property ayaneo_psy_ext_props[] = {
POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR,
};
static const struct power_supply_ext ayaneo_psy_ext = {
.name = "ayaneo-charge-control",
.properties = ayaneo_psy_ext_props,
.num_properties = ARRAY_SIZE(ayaneo_psy_ext_props),
.charge_behaviours = EC_CHARGE_CONTROL_BEHAVIOURS,
.get_property = ayaneo_psy_ext_get_prop,
.set_property = ayaneo_psy_ext_set_prop,
.property_is_writeable = ayaneo_psy_prop_is_writeable,
};
static int ayaneo_add_battery(struct power_supply *battery,
struct acpi_battery_hook *hook)
{
struct ayaneo_ec_platform_data *data =
container_of(hook, struct ayaneo_ec_platform_data, battery_hook);
return power_supply_register_extension(battery, &ayaneo_psy_ext,
&data->pdev->dev, NULL);
}
static int ayaneo_remove_battery(struct power_supply *battery,
struct acpi_battery_hook *hook)
{
power_supply_unregister_extension(battery, &ayaneo_psy_ext);
return 0;
}
static ssize_t controller_power_store(struct device *dev,
struct device_attribute *attr,
const char *buf,
size_t count)
{
bool value;
int ret;
ret = kstrtobool(buf, &value);
if (ret)
return ret;
ret = ec_write(AYANEO_POWER_REG, value ? AYANEO_POWER_ON : AYANEO_POWER_OFF);
if (ret)
return ret;
return count;
}
static ssize_t controller_power_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
int ret;
u8 val;
ret = ec_read(AYANEO_POWER_REG, &val);
if (ret)
return ret;
return sysfs_emit(buf, "%d\n", val == AYANEO_POWER_ON);
}
static DEVICE_ATTR_RW(controller_power);
static ssize_t controller_modules_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
u8 unconnected_modules;
char *out;
int ret;
ret = ec_read(AYANEO_MODULE_REG, &unconnected_modules);
if (ret)
return ret;
switch (~unconnected_modules & AYANEO_MODULE_MASK) {
case AYANEO_MODULE_LEFT | AYANEO_MODULE_RIGHT:
out = "both";
break;
case AYANEO_MODULE_LEFT:
out = "left";
break;
case AYANEO_MODULE_RIGHT:
out = "right";
break;
default:
out = "none";
break;
}
return sysfs_emit(buf, "%s\n", out);
}
static DEVICE_ATTR_RO(controller_modules);
static struct attribute *aya_mm_attrs[] = {
&dev_attr_controller_power.attr,
&dev_attr_controller_modules.attr,
NULL
};
static umode_t aya_mm_is_visible(struct kobject *kobj,
struct attribute *attr, int n)
{
struct device *dev = kobj_to_dev(kobj);
struct platform_device *pdev = to_platform_device(dev);
struct ayaneo_ec_platform_data *data = platform_get_drvdata(pdev);
if (data->quirks->has_magic_modules)
return attr->mode;
return 0;
}
static const struct attribute_group aya_mm_attribute_group = {
.is_visible = aya_mm_is_visible,
.attrs = aya_mm_attrs,
};
static const struct attribute_group *ayaneo_ec_groups[] = {
&aya_mm_attribute_group,
NULL
};
static int ayaneo_ec_probe(struct platform_device *pdev)
{
const struct dmi_system_id *dmi_entry;
struct ayaneo_ec_platform_data *data;
struct device *hwdev;
int ret;
dmi_entry = dmi_first_match(dmi_table);
if (!dmi_entry)
return -ENODEV;
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->pdev = pdev;
data->quirks = dmi_entry->driver_data;
ret = devm_mutex_init(&pdev->dev, &data->hwmon_lock);
if (ret)
return ret;
platform_set_drvdata(pdev, data);
if (data->quirks->has_fan_control) {
hwdev = devm_hwmon_device_register_with_info(&pdev->dev,
"ayaneo_ec", data, &ayaneo_ec_chip_info, NULL);
if (IS_ERR(hwdev))
return PTR_ERR(hwdev);
}
if (data->quirks->has_charge_control) {
data->battery_hook.add_battery = ayaneo_add_battery;
data->battery_hook.remove_battery = ayaneo_remove_battery;
data->battery_hook.name = "Ayaneo Battery";
ret = devm_battery_hook_register(&pdev->dev, &data->battery_hook);
if (ret)
return ret;
}
return 0;
}
static int ayaneo_freeze(struct device *dev)
{
struct platform_device *pdev = to_platform_device(dev);
struct ayaneo_ec_platform_data *data = platform_get_drvdata(pdev);
int ret;
u8 tmp;
if (data->quirks->has_charge_control) {
ret = ec_read(AYANEO_CHARGE_REG, &tmp);
if (ret)
return ret;
data->restore_charge_limit = tmp == AYANEO_CHARGE_VAL_INHIBIT;
}
if (data->quirks->has_fan_control) {
ret = ec_read(AYANEO_PWM_ENABLE_REG, &tmp);
if (ret)
return ret;
data->restore_pwm = tmp == AYANEO_PWM_MODE_MANUAL;
/*
* Release the fan when entering hibernation to avoid
* overheating if hibernation fails and hangs.
*/
if (data->restore_pwm) {
ret = ec_write(AYANEO_PWM_ENABLE_REG, AYANEO_PWM_MODE_AUTO);
if (ret)
return ret;
}
}
return 0;
}
static int ayaneo_restore(struct device *dev)
{
struct platform_device *pdev = to_platform_device(dev);
struct ayaneo_ec_platform_data *data = platform_get_drvdata(pdev);
int ret;
if (data->quirks->has_charge_control && data->restore_charge_limit) {
ret = ec_write(AYANEO_CHARGE_REG, AYANEO_CHARGE_VAL_INHIBIT);
if (ret)
return ret;
}
return 0;
}
static const struct dev_pm_ops ayaneo_pm_ops = {
.freeze = ayaneo_freeze,
.restore = ayaneo_restore,
};
static struct platform_driver ayaneo_platform_driver = {
.driver = {
.name = "ayaneo-ec",
.dev_groups = ayaneo_ec_groups,
.pm = pm_sleep_ptr(&ayaneo_pm_ops),
},
.probe = ayaneo_ec_probe,
};
static struct platform_device *ayaneo_platform_device;
static int __init ayaneo_ec_init(void)
{
ayaneo_platform_device =
platform_create_bundle(&ayaneo_platform_driver,
ayaneo_ec_probe, NULL, 0, NULL, 0);
return PTR_ERR_OR_ZERO(ayaneo_platform_device);
}
static void __exit ayaneo_ec_exit(void)
{
platform_device_unregister(ayaneo_platform_device);
platform_driver_unregister(&ayaneo_platform_driver);
}
MODULE_DEVICE_TABLE(dmi, dmi_table);
module_init(ayaneo_ec_init);
module_exit(ayaneo_ec_exit);
MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>");
MODULE_DESCRIPTION("Ayaneo Embedded Controller (EC) platform features");
MODULE_LICENSE("GPL");