Contributors: 3
Author Tokens Token Proportion Commits Commit Proportion
Antheas Kapenekakis 2238 99.33% 6 66.67%
Derek J. Clark 14 0.62% 2 22.22%
Joaquín Ignacio Aramendía 1 0.04% 1 11.11%
Total 2253 9


// 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");