The Linux Kernel Mailing List
 help / color / mirror / Atom feed
From: Lee Jones <lee@kernel.org>
To: Svyatoslav Ryhel <clamor95@gmail.com>
Cc: "Rob Herring" <robh@kernel.org>,
	"Krzysztof Kozlowski" <krzk+dt@kernel.org>,
	"Conor Dooley" <conor+dt@kernel.org>,
	"Dmitry Torokhov" <dmitry.torokhov@gmail.com>,
	"Pavel Machek" <pavel@kernel.org>,
	"Sebastian Reichel" <sre@kernel.org>,
	"Ion Agorria" <ion@agorria.com>,
	"Michał Mirosław" <mirq-linux@rere.qmqm.pl>,
	devicetree@vger.kernel.org, linux-kernel@vger.kernel.org,
	linux-input@vger.kernel.org, linux-leds@vger.kernel.org,
	linux-pm@vger.kernel.org
Subject: Re: [PATCH v6 2/7] mfd: Add driver for ASUS Transformer embedded controller
Date: Thu, 14 May 2026 11:02:05 +0100	[thread overview]
Message-ID: <20260514100205.GG305027@google.com> (raw)
In-Reply-To: <20260502124055.22475-3-clamor95@gmail.com>

On Sat, 02 May 2026, Svyatoslav Ryhel wrote:

> From: Michał Mirosław <mirq-linux@rere.qmqm.pl>
> 
> Support Nuvoton NPCE795-based ECs as used in Asus Transformer TF201,
> TF300T, TF300TG, TF300TL and TF700T pad and dock, as well as TF101 dock
> and TF600T, P1801-T and TF701T pad. This is a glue driver handling
> detection and common operations for EC's functions.
> 
> Co-developed-by: Svyatoslav Ryhel <clamor95@gmail.com>
> Signed-off-by: Svyatoslav Ryhel <clamor95@gmail.com>
> Signed-off-by: Michał Mirosław <mirq-linux@rere.qmqm.pl>
> ---
>  drivers/mfd/Kconfig                     |  14 +
>  drivers/mfd/Makefile                    |   1 +
>  drivers/mfd/asus-transformer-ec.c       | 762 ++++++++++++++++++++++++
>  include/linux/mfd/asus-transformer-ec.h | 162 +++++
>  4 files changed, 939 insertions(+)
>  create mode 100644 drivers/mfd/asus-transformer-ec.c
>  create mode 100644 include/linux/mfd/asus-transformer-ec.h
> 
> diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
> index 7192c9d1d268..5aa4facfd2df 100644
> --- a/drivers/mfd/Kconfig
> +++ b/drivers/mfd/Kconfig
> @@ -137,6 +137,20 @@ config MFD_AAT2870_CORE
>  	  additional drivers must be enabled in order to use the
>  	  functionality of the device.
>  
> +config MFD_ASUS_TRANSFORMER_EC
> +	tristate "ASUS Transformer's embedded controller"
> +	depends on I2C && OF
> +	help
> +	  Support ECs found in ASUS Transformer's Pad and Mobile Dock.
> +
> +	  This provides shared glue for functional part drivers:
> +	    asus-transformer-ec-kbc, asus-transformer-ec-keys,
> +	    leds-asus-transformer-ec, asus-transformer-ec-battery
> +	    and asus-transformer-ec-charger.

A 760 line driver deserves more than just a list of leaf drivers.

> +	  This driver can also be built as a module. If so, the module
> +	  will be called asus-transformer-ec.
> +
>  config MFD_AT91_USART
>  	tristate "AT91 USART Driver"
>  	select MFD_CORE
> diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
> index e75e8045c28a..fd80088d8a9a 100644
> --- a/drivers/mfd/Makefile
> +++ b/drivers/mfd/Makefile
> @@ -10,6 +10,7 @@ obj-$(CONFIG_MFD_88PM805)	+= 88pm805.o 88pm80x.o
>  obj-$(CONFIG_MFD_88PM886_PMIC)	+= 88pm886.o
>  obj-$(CONFIG_MFD_ACT8945A)	+= act8945a.o
>  obj-$(CONFIG_MFD_SM501)		+= sm501.o
> +obj-$(CONFIG_MFD_ASUS_TRANSFORMER_EC)	+= asus-transformer-ec.o
>  obj-$(CONFIG_ARCH_BCM2835)	+= bcm2835-pm.o
>  obj-$(CONFIG_MFD_BCM590XX)	+= bcm590xx.o
>  obj-$(CONFIG_MFD_BD9571MWV)	+= bd9571mwv.o
> diff --git a/drivers/mfd/asus-transformer-ec.c b/drivers/mfd/asus-transformer-ec.c
> new file mode 100644
> index 000000000000..75aa7ab99387
> --- /dev/null
> +++ b/drivers/mfd/asus-transformer-ec.c
> @@ -0,0 +1,762 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +
> +#include <linux/array_size.h>
> +#include <linux/debugfs.h>
> +#include <linux/delay.h>
> +#include <linux/gpio/consumer.h>
> +#include <linux/device.h>

Should we sort the '#include' directives alphabetically? For instance,
'device.h' should typically appear before 'gpio/consumer.h'.

> +#include <linux/err.h>
> +#include <linux/i2c.h>
> +#include <linux/interrupt.h>
> +#include <linux/mfd/asus-transformer-ec.h>
> +#include <linux/mfd/core.h>
> +#include <linux/mod_devicetable.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/property.h>
> +#include <linux/slab.h>
> +#include <linux/string.h>
> +#include <linux/types.h>
> +#include <linux/unaligned.h>
> +
> +#define ASUSEC_RSP_BUFFER_SIZE		8
> +
> +struct asus_ec_chip_data {
> +	const char *name;
> +	const struct mfd_cell *mfd_devices;

Use global `static const structs` instead.

Also, please do not pass MFD registration data through another
registration API like DT.  It opens the gates to too much hackery.  I'm
not saying that _this_ driver would do such a thing, but it's easier
just to keep the blanket rule in place.

> +	unsigned int num_devices;
> +	bool clr_fmode; /* clear Factory Mode bit in EC control register */
> +};
> +
> +struct asus_ec_data {
> +	struct asusec_info info;

You have 'data' and 'info' which a) using non-forthcoming nomenclature
and doesn't tell me anything and then you b) put 'info' in the device's
driver_data attribute which is very confusing.  driver_data should be
for what we call ddata which I assume is expressed as 'data' here.

> +	struct mutex ecreq_lock; /* prevent simultaneous access */
> +	struct gpio_desc *ecreq;

If I hadn't seen the declaration, I'd have no idea this was a GPIO
descriptor.  Please improve the nomenclature throughout.

> +	struct i2c_client *self;

Again, please use standard naming conventions:

% git grep "struct i2c_client" | grep "\*self" | wc -l
0

% git grep "struct i2c_client" | grep "\*client" | wc -l
6304

% git grep "struct i2c_client" | grep "\*i2c" | wc -l
903

> +	const struct asus_ec_chip_data *data;

'data', 'priv' and 'info' should be improved.

> +	char ec_data[DOCKRAM_ENTRY_BUFSIZE];

An array of chars called 'data'.  This could be anything.

> +	bool logging_disabled;

This debugging tool is probably never going to be used again.

Keep it local.

> +};
> +
> +struct dockram_ec_data {
> +	struct mutex ctl_lock; /* prevent simultaneous access */
> +	char ctl_data[DOCKRAM_ENTRY_BUFSIZE];
> +};
> +
> +#define to_ec_data(ec) \
> +	container_of(ec, struct asus_ec_data, info)
> +
> +/**
> + * asus_dockram_read - Read a register from the DockRAM device.
> + * @client: Handle to the DockRAM device.
> + * @reg: Register to read.
> + * @buf: Byte array into which data will be read; must be large enough to
> + *	 hold the data returned by the DockRAM.
> + *
> + * This executes the DockRAM read based on the SMBus "block read" protocol
> + * or its emulation. It extracts DOCKRAM_ENTRY_SIZE bytes from the set
> + * register address.
> + *
> + * Returns a negative errno code else zero on success.
> + */
> +int asus_dockram_read(struct i2c_client *client, int reg, char *buf)
> +{

Have you considered using Regmap for register access instead of
implementing custom functions?  Remaps already deals with caching and
locking mechanisms that you'd get for free.

This looks like it would be replaced with devm_regmap_init_i2c().

> +	struct device *dev = &client->dev;
> +	int ret;
> +
> +	memset(buf, 0, DOCKRAM_ENTRY_BUFSIZE);
> +	ret = i2c_smbus_read_i2c_block_data(client, reg,
> +					    DOCKRAM_ENTRY_BUFSIZE, buf);
> +	if (ret < 0)
> +		return ret;
> +
> +	if (buf[0] > DOCKRAM_ENTRY_SIZE) {
> +		dev_err(dev, "bad data len; buffer: %*ph; ret: %d\n",
> +			DOCKRAM_ENTRY_BUFSIZE, buf, ret);
> +		return -EPROTO;
> +	}
> +
> +	dev_dbg(dev, "got data; buffer: %*ph; ret: %d\n",
> +		DOCKRAM_ENTRY_BUFSIZE, buf, ret);

Please remove all of these debug messages.

> +
> +	return 0;
> +}
> +EXPORT_SYMBOL_GPL(asus_dockram_read);
> +
> +/**
> + * asus_dockram_write - Write a byte array to a register of the DockRAM device.
> + * @client: Handle to the DockRAM device.
> + * @reg: Register to write to.
> + * @buf: Byte array to be written (up to DOCKRAM_ENTRY_SIZE bytes).
> + *
> + * This executes the DockRAM write based on the SMBus "block write"
> + * protocol or its emulation. It writes DOCKRAM_ENTRY_SIZE bytes to the
> + * specified register address.
> + *
> + * Returns a negative errno code else zero on success.
> + */
> +int asus_dockram_write(struct i2c_client *client, int reg, const char *buf)
> +{
> +	if (buf[0] > DOCKRAM_ENTRY_SIZE)
> +		return -EINVAL;
> +
> +	dev_dbg(&client->dev, "sending data; buffer: %*ph\n", buf[0] + 1, buf);
> +
> +	return i2c_smbus_write_i2c_block_data(client, reg, buf[0] + 1, buf);
> +}
> +EXPORT_SYMBOL_GPL(asus_dockram_write);
> +
> +/**
> + * asus_dockram_access_ctl - Read from or write to the DockRAM control register.
> + * @client: Handle to the DockRAM device.
> + * @out: Pointer to a variable where the register value will be stored.
> + * @mask: Bitmask of bits to be cleared.
> + * @xor: Bitmask of bits to be set (via XOR).
> + *
> + * This performs a control register read if @out is provided and both @mask
> + * and @xor are zero. Otherwise, it performs a control register update if
> + * @mask and @xor are provided.
> + *
> + * Returns a negative errno code else zero on success.
> + */
> +int asus_dockram_access_ctl(struct i2c_client *client, u64 *out, u64 mask,
> +			    u64 xor)
> +{
> +	struct dockram_ec_data *priv = i2c_get_clientdata(client);
> +	char *buf = priv->ctl_data;
> +	u64 val;
> +	int ret = 0;
> +
> +	guard(mutex)(&priv->ctl_lock);
> +
> +	ret = asus_dockram_read(client, ASUSEC_DOCKRAM_CONTROL, buf);
> +	if (ret < 0)

Could we check for errors using 'if (ret)' instead of 'if (ret < 0)' here,
unless a positive return value has a specific meaning we need to handle?

> +		goto exit;
> +
> +	if (buf[0] != ASUSEC_CTL_SIZE) {
> +		ret = -EPROTO;
> +		goto exit;
> +	}
> +
> +	val = get_unaligned_le64(buf + 1);
> +
> +	if (out)
> +		*out = val;
> +
> +	if (mask || xor) {
> +		put_unaligned_le64((val & ~mask) ^ xor, buf + 1);
> +		ret = asus_dockram_write(client, ASUSEC_DOCKRAM_CONTROL, buf);
> +	}
> +
> +exit:
> +	if (ret < 0)
> +		dev_err(&client->dev, "Failed to access control flags: %d\n",
> +			ret);
> +
> +	return ret;
> +}
> +EXPORT_SYMBOL_GPL(asus_dockram_access_ctl);
> +
> +static void asus_ec_remove_notifier(struct device *dev, void *res)
> +{
> +	struct asusec_info *ec = dev_get_drvdata(dev->parent);
> +	struct notifier_block **nb = res;
> +
> +	blocking_notifier_chain_unregister(&ec->notify_list, *nb);
> +}
> +
> +/**
> + * devm_asus_ec_register_notifier - Managed registration of notifier to an
> + *				    ASUS EC blocking notifier chain.
> + * @pdev: Device requesting the notifier (used for resource management).
> + * @nb: Notifier block to be registered.
> + *
> + * Register a notifier to the ASUS EC blocking notifier chain. The notifier
> + * will be automatically unregistered when the requesting device is detached.
> + *
> + * Return: 0 on success or a negative error code on failure.
> + */
> +int devm_asus_ec_register_notifier(struct platform_device *pdev,
> +				   struct notifier_block *nb)
> +{

Hand-rolling devres managed resources is usually reserved for subsystem
level API calls.  Why do the usual device driver .remove() handling work
for you?

> +	struct asusec_info *ec = dev_get_drvdata(pdev->dev.parent);
> +	struct notifier_block **res;
> +	int ret;
> +
> +	res = devres_alloc(asus_ec_remove_notifier, sizeof(*res), GFP_KERNEL);
> +	if (!res)
> +		return -ENOMEM;
> +
> +	*res = nb;
> +	ret = blocking_notifier_chain_register(&ec->notify_list, nb);
> +	if (ret) {
> +		devres_free(res);
> +		return ret;
> +	}
> +
> +	devres_add(&pdev->dev, res);
> +
> +	return 0;
> +}
> +EXPORT_SYMBOL_GPL(devm_asus_ec_register_notifier);
> +
> +static int asus_ec_signal_request(const struct asusec_info *ec)
> +{
> +	struct asus_ec_data *priv = to_ec_data(ec);
> +
> +	guard(mutex)(&priv->ecreq_lock);
> +
> +	dev_dbg(&priv->self->dev, "EC request\n");
> +
> +	gpiod_set_value_cansleep(priv->ecreq, 1);
> +	msleep(50);
> +
> +	gpiod_set_value_cansleep(priv->ecreq, 0);
> +	msleep(200);
> +
> +	return 0;
> +}
> +
> +static int asus_ec_write(struct asus_ec_data *priv, u16 data)
> +{
> +	int ret = i2c_smbus_write_word_data(priv->self, ASUSEC_WRITE_BUF, data);
> +
> +	dev_dbg(&priv->self->dev, "EC write: %04x, ret = %d\n", data, ret);
> +
> +	return ret;
> +}
> +
> +static int asus_ec_read(struct asus_ec_data *priv, bool in_irq)
> +{
> +	int ret = i2c_smbus_read_i2c_block_data(priv->self, ASUSEC_READ_BUF,
> +						sizeof(priv->ec_data),
> +						priv->ec_data);
> +
> +	dev_dbg(&priv->self->dev, "EC read: %*ph, ret = %d%s\n",
> +		sizeof(priv->ec_data), priv->ec_data,
> +		ret, in_irq ? "; in irq" : "");
> +
> +	return ret;
> +}

No abstractions for the sake of it.  All this goes away with Regmap anyway.
> +
> +/**
> + * asus_ec_i2c_command - Send a 16-bit command to the ASUS EC.
> + * @ec: Pointer to the shared ASUS EC structure.
> + * @data: The 16-bit command (word) to be sent.
> + *
> + * Return: 0 on success or a negative error code on failure.
> + */
> +int asus_ec_i2c_command(const struct asusec_info *ec, u16 data)
> +{
> +	return asus_ec_write(to_ec_data(ec), data);
> +}

Is this wrapper function strictly necessary? We should try to avoid
superfluous abstractions that do nothing but call another function.

> +EXPORT_SYMBOL_GPL(asus_ec_i2c_command);
> +
> +static void asus_ec_clear_buffer(struct asus_ec_data *priv)
> +{
> +	int retry = ASUSEC_RSP_BUFFER_SIZE;
> +
> +	while (retry--) {
> +		if (asus_ec_read(priv, false) < 0)
> +			continue;
> +
> +		if (priv->ec_data[1] & ASUSEC_OBF_MASK)
> +			continue;
> +
> +		break;
> +	}
> +}
> +
> +static int asus_ec_log_info(struct asus_ec_data *priv, unsigned int reg,
> +			    const char *name, char **out)
> +{
> +	char buf[DOCKRAM_ENTRY_BUFSIZE];
> +	int ret;
> +
> +	ret = asus_dockram_read(priv->info.dockram, reg, buf);
> +	if (ret < 0)
> +		return ret;
> +
> +	if (!priv->logging_disabled)
> +		dev_info(&priv->self->dev, "%-14s: %.*s\n", name,
> +			 buf[0], buf + 1);
> +
> +	if (out)
> +		*out = kstrndup(buf + 1, buf[0], GFP_KERNEL);
> +
> +	return 0;
> +}
> +
> +static int asus_ec_reset(struct asus_ec_data *priv)
> +{
> +	int retry, ret;
> +
> +	for (retry = 0; retry < 3; retry++) {
> +		ret = asus_ec_write(priv, 0);
> +		if (!ret)
> +			return 0;
> +
> +		msleep(300);
> +	}
> +
> +	return ret;
> +}
> +
> +static int asus_ec_magic_debug(struct asus_ec_data *priv)
> +{
> +	u64 flag;
> +	int ret;
> +
> +	ret = asus_ec_get_ctl(&priv->info, &flag);
> +	if (ret < 0)
> +		return ret;
> +
> +	flag &= ASUSEC_CTL_SUSB_MODE;
> +	dev_info(&priv->self->dev, "EC FW behaviour: %s\n",
> +		 flag ? "susb on when receive ec_req" :
> +		 "susb on when system wakeup");
> +
> +	return 0;
> +}
> +
> +static int asus_ec_set_factory_mode(struct asus_ec_data *priv, bool on)
> +{
> +	dev_info(&priv->self->dev, "Entering %s mode.\n", on ? "factory" :
> +		 "normal");
> +
> +	return asus_ec_update_ctl(&priv->info, ASUSEC_CTL_FACTORY_MODE,
> +				  on ? ASUSEC_CTL_FACTORY_MODE : 0);
> +}
> +
> +static int asus_ec_detect(struct asus_ec_data *priv)
> +{
> +	char *model = NULL;
> +	int ret;
> +
> +	ret = asus_ec_reset(priv);
> +	if (ret)
> +		goto err_exit;
> +
> +	asus_ec_clear_buffer(priv);
> +
> +	ret = asus_ec_log_info(priv, ASUSEC_DOCKRAM_INFO_MODEL, "model", &model);
> +	if (ret)
> +		goto err_exit;
> +
> +	ret = asus_ec_log_info(priv, ASUSEC_DOCKRAM_INFO_FW, "FW version", NULL);
> +	if (ret)
> +		goto err_exit;
> +
> +	ret = asus_ec_log_info(priv, ASUSEC_DOCKRAM_INFO_CFGFMT, "Config format", NULL);
> +	if (ret)
> +		goto err_exit;
> +
> +	ret = asus_ec_log_info(priv, ASUSEC_DOCKRAM_INFO_HW, "HW version", NULL);
> +	if (ret)
> +		goto err_exit;
> +
> +	priv->logging_disabled = true;
> +
> +	ret = asus_ec_magic_debug(priv);
> +	if (ret)
> +		goto err_exit;
> +
> +	priv->info.model = model;
> +	priv->info.name = priv->data->name;
> +
> +	if (priv->data->clr_fmode)
> +		asus_ec_set_factory_mode(priv, false);
> +
> +err_exit:
> +	if (ret)
> +		dev_err(&priv->self->dev, "failed to access EC: %d\n", ret);
> +
> +	return ret;
> +}
> +
> +static void asus_ec_handle_smi(struct asus_ec_data *priv, unsigned int code)
> +{
> +	dev_dbg(&priv->self->dev, "SMI interrupt: 0x%02x\n", code);
> +
> +	switch (code) {
> +	case ASUSEC_SMI_HANDSHAKE:
> +	case ASUSEC_SMI_RESET:
> +		asus_ec_detect(priv);
> +		break;
> +	}
> +}
> +
> +static irqreturn_t asus_ec_interrupt(int irq, void *dev_id)
> +{
> +	struct asus_ec_data *priv = dev_id;
> +	unsigned long notify_action;
> +	int ret;
> +
> +	ret = asus_ec_read(priv, true);
> +	if (ret <= 0 || !(priv->ec_data[1] & ASUSEC_OBF_MASK))
> +		return IRQ_NONE;
> +
> +	notify_action = priv->ec_data[1];
> +	if (notify_action & ASUSEC_SMI_MASK) {
> +		unsigned int code = priv->ec_data[2];
> +
> +		asus_ec_handle_smi(priv, code);
> +
> +		notify_action |= code << 8;
> +		dev_dbg(&priv->self->dev, "SMI code: 0x%02x\n", code);
> +	}
> +
> +	blocking_notifier_call_chain(&priv->info.notify_list,
> +				     notify_action, priv->ec_data);
> +
> +	return IRQ_HANDLED;
> +}
> +
> +static ssize_t dockram_read(struct file *filp, char __user *buf,
> +			    size_t count, loff_t *ppos)
> +{
> +	struct i2c_client *client = filp->private_data;
> +	unsigned int reg, rsize;
> +	ssize_t n_read = 0, val;
> +	loff_t off = *ppos;
> +	char *data;
> +	int ret;
> +
> +	reg = off / DOCKRAM_ENTRY_SIZE;
> +	off %= DOCKRAM_ENTRY_SIZE;
> +	rsize = DOCKRAM_ENTRIES * DOCKRAM_ENTRY_SIZE;
> +
> +	if (!count)
> +		return 0;
> +
> +	data = kmalloc(DOCKRAM_ENTRY_BUFSIZE, GFP_KERNEL);
> +
> +	while (reg < DOCKRAM_ENTRIES) {
> +		unsigned int len = DOCKRAM_ENTRY_SIZE - off;
> +
> +		if (len > rsize)
> +			len = rsize;
> +
> +		ret = asus_dockram_read(client, reg, data);
> +		if (ret < 0) {
> +			if (!n_read)
> +				n_read = ret;
> +			break;
> +		}
> +
> +		val = copy_to_user(buf, data + 1 + off, len);
> +		if (val == len)
> +			return -EFAULT;
> +
> +		*ppos += len;
> +		n_read += len;
> +
> +		if (len == rsize)
> +			break;
> +
> +		rsize -= len;
> +		buf += len;
> +		off = 0;
> +		++reg;
> +	}
> +
> +	kfree(data);
> +
> +	return n_read;
> +}
> +
> +static int dockram_write_one(struct i2c_client *client, int reg,
> +			     const char __user *buf, size_t count)
> +{
> +	struct dockram_ec_data *priv = i2c_get_clientdata(client);
> +	int ret;
> +
> +	if (!count || count > DOCKRAM_ENTRY_SIZE)
> +		return -EINVAL;
> +	if (buf[0] != count - 1)
> +		return -EINVAL;
> +
> +	guard(mutex)(&priv->ctl_lock);
> +
> +	priv->ctl_data[0] = (u8)count;
> +	memcpy(priv->ctl_data + 1, buf, count);
> +	ret = asus_dockram_write(client, reg, priv->ctl_data);
> +
> +	return ret;
> +}
> +
> +static ssize_t dockram_write(struct file *filp, const char __user *buf,
> +			     size_t count, loff_t *ppos)
> +{
> +	struct i2c_client *client = filp->private_data;
> +	unsigned int reg;
> +	loff_t off = *ppos;
> +	int ret;
> +
> +	if (off % DOCKRAM_ENTRY_SIZE != 0)
> +		return -EINVAL;
> +
> +	reg = off / DOCKRAM_ENTRY_SIZE;
> +	if (reg >= DOCKRAM_ENTRIES)
> +		return -EINVAL;
> +
> +	ret = dockram_write_one(client, reg, buf, count);
> +
> +	return ret < 0 ? ret : count;
> +}
> +
> +static const struct debugfs_short_fops dockram_fops = {
> +	.read	= dockram_read,
> +	.write	= dockram_write,
> +	.llseek	= default_llseek,
> +};

You should not be giving userspace free reign over device memory.

If you switch to Regmap, you can use regmap-debugfs.c for this.

> +static int control_reg_get(void *ec, u64 *val)
> +{
> +	return asus_ec_get_ctl(ec, val);
> +}
> +
> +static int control_reg_set(void *ec, u64 val)
> +{
> +	return asus_ec_update_ctl(ec, ~0ull, val);
> +}
> +
> +DEFINE_DEBUGFS_ATTRIBUTE(control_reg_fops, control_reg_get,
> +			 control_reg_set, "%016llx\n");
> +
> +static int ec_request_set(void *ec, u64 val)
> +{
> +	if (val)
> +		asus_ec_signal_request(ec);
> +
> +	return 0;
> +}
> +
> +DEFINE_DEBUGFS_ATTRIBUTE(ec_request_fops, NULL, ec_request_set, "%llu\n");
> +
> +static int ec_irq_set(void *ec, u64 val)
> +{
> +	struct asus_ec_data *priv = to_ec_data(ec);
> +
> +	if (val)
> +		irq_wake_thread(priv->self->irq, priv);
> +
> +	return 0;
> +}
> +
> +DEFINE_DEBUGFS_ATTRIBUTE(ec_irq_fops, NULL, ec_irq_set, "%llu\n");
> +
> +static void asus_ec_debugfs_remove(void *debugfs_root)
> +{
> +	debugfs_remove_recursive(debugfs_root);
> +}
> +
> +static void devm_asus_ec_debugfs_init(struct device *dev)
> +{
> +	struct asusec_info *ec = dev_get_drvdata(dev);
> +	struct asus_ec_data *priv = to_ec_data(ec);
> +	struct dentry *debugfs_root, *dockram_dir;
> +	char *name = devm_kasprintf(dev, GFP_KERNEL, "asus-ec-%s",
> +				    priv->data->name);
> +
> +	debugfs_root = debugfs_create_dir(name, NULL);
> +	dockram_dir = debugfs_create_dir("dockram", debugfs_root);
> +
> +	debugfs_create_file("ec_irq", 0200, debugfs_root, ec,
> +			    &ec_irq_fops);
> +	debugfs_create_file("ec_request", 0200, debugfs_root, ec,
> +			    &ec_request_fops);
> +	debugfs_create_file("control_reg", 0644, dockram_dir, ec,
> +			    &control_reg_fops);
> +	debugfs_create_file("dockram", 0644, dockram_dir,
> +			    priv->info.dockram, &dockram_fops);
> +
> +	devm_add_action_or_reset(dev, asus_ec_debugfs_remove, debugfs_root);
> +}

Why is this being controlled via debugfs?

Use the correct kernel APIs instead.

If this is just for development, keep it and all of the extra verbose
printing in the BSP tree.  It does not belong in the upstream kernel.

> +static void asus_ec_release_dockram_dev(void *client)
> +{
> +	i2c_unregister_device(client);
> +}
> +
> +static struct i2c_client *devm_asus_dockram_get(struct device *dev)
> +{
> +	struct i2c_client *parent = to_i2c_client(dev);
> +	struct i2c_client *dockram;
> +	struct dockram_ec_data *priv;
> +	int ret;
> +
> +	dockram = i2c_new_ancillary_device(parent, "dockram",
> +					   parent->addr + 2);
> +	if (IS_ERR(dockram))
> +		return dockram;
> +
> +	ret = devm_add_action_or_reset(dev, asus_ec_release_dockram_dev,
> +				       dockram);
> +	if (ret)
> +		return ERR_PTR(ret);
> +
> +	priv = devm_kzalloc(&dockram->dev, sizeof(*priv), GFP_KERNEL);
> +	if (!priv)
> +		return ERR_PTR(-ENOMEM);
> +
> +	i2c_set_clientdata(dockram, priv);
> +	mutex_init(&priv->ctl_lock);
> +
> +	return dockram;
> +}
> +
> +static int asus_ec_probe(struct i2c_client *client)
> +{
> +	struct device *dev = &client->dev;
> +	struct asus_ec_data *priv;

Could we use a clearer, more specific name for the private data variable
instead of the generic term 'priv'? Using something like 'ddata' is usually
preferred.

> +	int ret;
> +
> +	if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_I2C_BLOCK))
> +		return dev_err_probe(dev, -ENXIO,
> +			"I2C bus is missing required SMBus block mode support\n");
> +
> +	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
> +	if (!priv)
> +		return -ENOMEM;
> +
> +	priv->data = device_get_match_data(dev);
> +	if (!priv->data)
> +		return -ENODEV;
> +
> +	i2c_set_clientdata(client, priv);
> +	priv->self = client;
> +
> +	priv->info.dockram = devm_asus_dockram_get(dev);
> +	if (IS_ERR(priv->info.dockram))
> +		return dev_err_probe(dev, PTR_ERR(priv->info.dockram),
> +				     "failed to get dockram\n");
> +
> +	priv->ecreq = devm_gpiod_get(dev, "request", GPIOD_OUT_LOW);
> +	if (IS_ERR(priv->ecreq))
> +		return dev_err_probe(dev, PTR_ERR(priv->ecreq),
> +				     "failed to get request GPIO\n");

"get" or "request"

> +	BLOCKING_INIT_NOTIFIER_HEAD(&priv->info.notify_list);
> +	mutex_init(&priv->ecreq_lock);
> +
> +	asus_ec_signal_request(&priv->info);
> +
> +	ret = asus_ec_detect(priv);
> +	if (ret)
> +		return dev_err_probe(dev, ret, "failed to detect EC version\n");
> +
> +	ret = devm_request_threaded_irq(dev, client->irq, NULL,
> +					&asus_ec_interrupt,
> +					IRQF_ONESHOT | IRQF_SHARED,
> +					client->name, priv);
> +	if (ret)
> +		return dev_err_probe(dev, ret, "failed to register IRQ\n");
> +
> +	/* Parent I2C controller uses DMA, ASUS EC and child devices do not */
> +	client->dev.coherent_dma_mask = 0;
> +	client->dev.dma_mask = &client->dev.coherent_dma_mask;
> +
> +	if (IS_ENABLED(CONFIG_DEBUG_FS))
> +		devm_asus_ec_debugfs_init(dev);
> +
> +	return devm_mfd_add_devices(dev, 0, priv->data->mfd_devices,
> +				    priv->data->num_devices, NULL, 0, NULL);
> +}
> +
> +static const struct mfd_cell asus_ec_sl101_dock_mfd_devices[] = {
> +	{
> +		.name = "asus-transformer-ec-kbc",
> +	},
> +};
> +
> +static const struct asus_ec_chip_data asus_ec_sl101_dock_data = {
> +	.name = "dock",
> +	.mfd_devices = asus_ec_sl101_dock_mfd_devices,
> +	.num_devices = ARRAY_SIZE(asus_ec_sl101_dock_mfd_devices),
> +	.clr_fmode = false,
> +};
> +
> +static const struct mfd_cell asus_ec_tf101_dock_mfd_devices[] = {
> +	{
> +		.name = "asus-transformer-ec-battery",
> +		.id = 1,

Why doesn't PLATFORM_DEVID_AUTO already do the right thing?

> +	}, {
> +		.name = "asus-transformer-ec-charger",
> +		.id = 1,
> +	}, {
> +		.name = "asus-transformer-ec-led",
> +		.id = 1,
> +	}, {
> +		.name = "asus-transformer-ec-keys",
> +	}, {
> +		.name = "asus-transformer-ec-kbc",
> +	},
> +};
> +
> +static const struct asus_ec_chip_data asus_ec_tf101_dock_data = {
> +	.name = "dock",
> +	.mfd_devices = asus_ec_tf101_dock_mfd_devices,
> +	.num_devices = ARRAY_SIZE(asus_ec_tf101_dock_mfd_devices),
> +	.clr_fmode = false,
> +};
> +
> +static const struct mfd_cell asus_ec_tf201_pad_mfd_devices[] = {
> +	{
> +		.name = "asus-transformer-ec-battery",
> +		.id = 0,
> +	}, {
> +		.name = "asus-transformer-ec-led",
> +		.id = 0,
> +	},
> +};
> +
> +static const struct asus_ec_chip_data asus_ec_tf201_pad_data = {
> +	.name = "pad",
> +	.mfd_devices = asus_ec_tf201_pad_mfd_devices,
> +	.num_devices = ARRAY_SIZE(asus_ec_tf201_pad_mfd_devices),
> +	.clr_fmode = true,
> +};
> +
> +static const struct mfd_cell asus_ec_tf600t_pad_mfd_devices[] = {
> +	{
> +		.name = "asus-transformer-ec-battery",
> +		.id = 0,
> +	}, {
> +		.name = "asus-transformer-ec-charger",
> +		.id = 0,
> +	}, {
> +		.name = "asus-transformer-ec-led",
> +		.id = 0,
> +	},
> +};
> +
> +static const struct asus_ec_chip_data asus_ec_tf600t_pad_data = {
> +	.name = "pad",
> +	.mfd_devices = asus_ec_tf600t_pad_mfd_devices,
> +	.num_devices = ARRAY_SIZE(asus_ec_tf600t_pad_mfd_devices),
> +	.clr_fmode = true,
> +};
> +
> +static const struct of_device_id asus_ec_match[] = {
> +	{ .compatible = "asus,sl101-ec-dock", .data = &asus_ec_sl101_dock_data },
> +	{ .compatible = "asus,tf101-ec-dock", .data = &asus_ec_tf101_dock_data },

Could we use the match table's data field to store an 'enum' or integer ID
instead of passing platform data via the '.data' field? We could then use a
'switch' statement in the C code to select the correct 'mfd_cell' array.

> +	{ .compatible = "asus,tf201-ec-pad", .data = &asus_ec_tf201_pad_data },
> +	{ .compatible = "asus,tf600t-ec-pad", .data = &asus_ec_tf600t_pad_data },
> +	{ }
> +};
> +MODULE_DEVICE_TABLE(of, asus_ec_match);
> +
> +static struct i2c_driver asus_ec_driver = {
> +	.driver	= {
> +		.name = "asus-transformer-ec",
> +		.of_match_table = asus_ec_match,
> +	},
> +	.probe = asus_ec_probe,
> +};
> +module_i2c_driver(asus_ec_driver);
> +
> +MODULE_AUTHOR("Michał Mirosław <mirq-linux@rere.qmqm.pl>");
> +MODULE_AUTHOR("Svyatoslav Ryhel <clamor95@gmail.com>");
> +MODULE_DESCRIPTION("ASUS Transformer's EC driver");
> +MODULE_LICENSE("GPL");
> diff --git a/include/linux/mfd/asus-transformer-ec.h b/include/linux/mfd/asus-transformer-ec.h
> new file mode 100644
> index 000000000000..0a72de40352e
> --- /dev/null
> +++ b/include/linux/mfd/asus-transformer-ec.h
> @@ -0,0 +1,162 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef __MFD_ASUS_TRANSFORMER_EC_H
> +#define __MFD_ASUS_TRANSFORMER_EC_H
> +
> +#include <linux/notifier.h>
> +#include <linux/platform_device.h>
> +#include <linux/workqueue.h>
> +
> +struct i2c_client;
> +
> +struct asusec_info {
> +	const char *model;
> +	const char *name;
> +	struct i2c_client *dockram;
> +	struct workqueue_struct *wq;
> +	struct blocking_notifier_head notify_list;
> +};
> +
> +#define DOCKRAM_ENTRIES			0x100
> +#define DOCKRAM_ENTRY_SIZE		32
> +#define DOCKRAM_ENTRY_BUFSIZE		(DOCKRAM_ENTRY_SIZE + 1)
> +
> +/* interrupt sources */
> +#define ASUSEC_OBF_MASK			BIT(0)
> +#define ASUSEC_KEY_MASK			BIT(2)
> +#define ASUSEC_KBC_MASK			BIT(3)
> +#define ASUSEC_AUX_MASK			BIT(5)
> +#define ASUSEC_SCI_MASK			BIT(6)
> +#define ASUSEC_SMI_MASK			BIT(7)
> +
> +/* SMI notification codes */
> +#define ASUSEC_SMI_POWER_NOTIFY		0x31	/* [un]plugging USB cable */
> +#define ASUSEC_SMI_HANDSHAKE		0x50	/* response to ec_req edge */
> +#define ASUSEC_SMI_WAKE			0x53
> +#define ASUSEC_SMI_RESET		0x5f
> +#define ASUSEC_SMI_ADAPTER_EVENT	0x60	/* [un]plugging charger to dock */
> +#define ASUSEC_SMI_BACKLIGHT_ON		0x63
> +#define ASUSEC_SMI_AUDIO_DOCK_IN	0x70
> +
> +#define ASUSEC_SMI_ACTION(code)		(ASUSEC_SMI_MASK | ASUSEC_OBF_MASK | \
> +					(ASUSEC_SMI_##code << 8))
> +
> +/* control register [0x0a] layout */
> +#define ASUSEC_CTL_SIZE			8
> +
> +/*
> + * EC reports power from 40-pin connector in the LSB of the control
> + * register.  The following values have been observed (xor 0x02):
> + *
> + * PAD-ec no-plug  0x40 / PAD-ec DOCK     0x20 / DOCK-ec no-plug 0x40
> + * PAD-ec AC       0x25 / PAD-ec DOCK+AC  0x24 / DOCK-ec AC      0x25
> + * PAD-ec USB      0x45 / PAD-ec DOCK+USB 0x24 / DOCK-ec USB     0x41
> + */
> +
> +#define ASUSEC_CTL_DIRECT_POWER_SOURCE	BIT_ULL(0)
> +#define ASUSEC_STAT_CHARGING		BIT_ULL(2)
> +#define ASUSEC_CTL_FULL_POWER_SOURCE	BIT_ULL(5)
> +#define ASUSEC_CTL_SUSB_MODE		BIT_ULL(9)
> +#define ASUSEC_CMD_SUSPEND_S3		BIT_ULL(33)
> +#define ASUSEC_CTL_TEST_DISCHARGE	BIT_ULL(35)
> +#define ASUSEC_CMD_SUSPEND_INHIBIT	BIT_ULL(37)
> +#define ASUSEC_CTL_FACTORY_MODE		BIT_ULL(38)
> +#define ASUSEC_CTL_KEEP_AWAKE		BIT_ULL(39)
> +#define ASUSEC_CTL_USB_CHARGE		BIT_ULL(40)
> +#define ASUSEC_CTL_LED_BLINK		BIT_ULL(40)
> +#define ASUSEC_CTL_LED_AMBER		BIT_ULL(41)
> +#define ASUSEC_CTL_LED_GREEN		BIT_ULL(42)
> +#define ASUSEC_CMD_SWITCH_HDMI		BIT_ULL(56)
> +#define ASUSEC_CMD_WIN_SHUTDOWN		BIT_ULL(62)
> +
> +#define ASUSEC_DOCKRAM_INFO_MODEL	0x01
> +#define ASUSEC_DOCKRAM_INFO_FW		0x02
> +#define ASUSEC_DOCKRAM_INFO_CFGFMT	0x03
> +#define ASUSEC_DOCKRAM_INFO_HW		0x04
> +#define ASUSEC_DOCKRAM_CONTROL		0x0a
> +#define ASUSEC_DOCKRAM_BATT_CTL		0x14
> +
> +#define ASUSEC_WRITE_BUF		0x64
> +#define ASUSEC_READ_BUF			0x6a
> +
> +/* dockram comm */
> +int asus_dockram_read(struct i2c_client *client, int reg, char *buf);
> +int asus_dockram_write(struct i2c_client *client, int reg, const char *buf);
> +int asus_dockram_access_ctl(struct i2c_client *client,
> +			    u64 *out, u64 mask, u64 xor);
> +
> +/* EC public API */
> +
> +/**
> + * cell_to_ec - Request the shared ASUS EC structure via a subdevice's pdev.
> + * @pdev: EC subdevice pdev requesting access to the shared ASUS EC structure.
> + *
> + * Returns a pointer to the asusec_info structure.
> + */
> +static inline struct asusec_info *cell_to_ec(struct platform_device *pdev)
> +{
> +	return dev_get_drvdata(pdev->dev.parent);
> +}

This is both misleading and already exists.

> +
> +/**
> + * asus_ec_get_ctl - Read from the DockRAM control register.
> + * @ec:  Pointer to the shared ASUS EC structure.
> + * @out: Pointer to the variable where the register value will be stored.
> + *
> + * Performs a control register read and stores the value in @out.
> + *
> + * Return: 0 on success, or a negative errno code on failure.
> + */
> +static inline int asus_ec_get_ctl(const struct asusec_info *ec, u64 *out)
> +{
> +	return asus_dockram_access_ctl(ec->dockram, out, 0, 0);
> +}
> +
> +/**
> + * asus_ec_update_ctl - Update the DockRAM control register.
> + * @ec:   Pointer to the shared ASUS EC structure.
> + * @mask: Bitmask of bits to be cleared.
> + * @xor:  Bitmask of bits to be toggled or set (via XOR).
> + *
> + * Performs a read-modify-write update on the control register using
> + * the provided @mask and @xor values.
> + *
> + * Return: 0 on success, or a negative errno code on failure.
> + */
> +static inline int asus_ec_update_ctl(const struct asusec_info *ec,
> +				     u64 mask, u64 xor)
> +{
> +	return asus_dockram_access_ctl(ec->dockram, NULL, mask, xor);
> +}
> +
> +/**
> + * asus_ec_set_ctl_bits - Sets bits of the DockRAM control register.
> + * @ec:   Pointer to the shared ASUS EC structure.
> + * @mask: Bitmask of bits to be set.
> + *
> + * Sets bits of the control register using the provided @mask value.
> + *
> + * Return: 0 on success, or a negative errno code on failure.
> + */
> +static inline int asus_ec_set_ctl_bits(const struct asusec_info *ec, u64 mask)
> +{
> +	return asus_dockram_access_ctl(ec->dockram, NULL, mask, mask);
> +}
> +
> +/**
> + * asus_ec_clear_ctl_bits - Clears bits of the DockRAM control register.
> + * @ec:   Pointer to the shared ASUS EC structure.
> + * @mask: Bitmask of bits to be cleared.
> + *
> + * Clears bits of the control register using the provided @mask value.
> + *
> + * Return: 0 on success, or a negative errno code on failure.
> + */
> +static inline int asus_ec_clear_ctl_bits(const struct asusec_info *ec, u64 mask)
> +{
> +	return asus_dockram_access_ctl(ec->dockram, NULL, mask, 0);
> +}

We don't typically allow abstraction for the sake of abstraction.

> +int asus_ec_i2c_command(const struct asusec_info *ec, u16 data);
> +int devm_asus_ec_register_notifier(struct platform_device *dev,
> +				   struct notifier_block *nb);
> +#endif /* __MFD_ASUS_TRANSFORMER_EC_H */
> -- 
> 2.51.0
> 
> 

-- 
Lee Jones

       reply	other threads:[~2026-05-14 10:02 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
     [not found] <20260502124055.22475-1-clamor95@gmail.com>
     [not found] ` <20260502124055.22475-3-clamor95@gmail.com>
2026-05-14 10:02   ` Lee Jones [this message]
2026-05-14 10:31     ` [PATCH v6 2/7] mfd: Add driver for ASUS Transformer embedded controller Svyatoslav Ryhel
2026-05-14 15:50       ` Lee Jones
2026-05-14 16:06         ` Svyatoslav Ryhel
2026-05-14 11:02     ` Svyatoslav Ryhel
     [not found] ` <20260502124055.22475-6-clamor95@gmail.com>
2026-05-14 10:09   ` [PATCH v6 5/7] leds: Add driver for ASUS Transformer LEDs Lee Jones

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260514100205.GG305027@google.com \
    --to=lee@kernel.org \
    --cc=clamor95@gmail.com \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=dmitry.torokhov@gmail.com \
    --cc=ion@agorria.com \
    --cc=krzk+dt@kernel.org \
    --cc=linux-input@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-leds@vger.kernel.org \
    --cc=linux-pm@vger.kernel.org \
    --cc=mirq-linux@rere.qmqm.pl \
    --cc=pavel@kernel.org \
    --cc=robh@kernel.org \
    --cc=sre@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox