Contributors: 1
Author Tokens Token Proportion Commits Commit Proportion
Ming Yu 1767 100.00% 1 100.00%
Total 1767 1


// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (C) 2025 Nuvoton Technology Corp.
 *
 * Nuvoton NCT6694 core driver using USB interface to provide
 * access to the NCT6694 hardware monitoring and control features.
 *
 * The NCT6694 is an integrated controller that provides GPIO, I2C,
 * CAN, WDT, HWMON and RTC management.
 */

#include <linux/bits.h>
#include <linux/interrupt.h>
#include <linux/idr.h>
#include <linux/irq.h>
#include <linux/irqdomain.h>
#include <linux/kernel.h>
#include <linux/mfd/core.h>
#include <linux/mfd/nct6694.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/usb.h>

static const struct mfd_cell nct6694_devs[] = {
	MFD_CELL_NAME("nct6694-gpio"),
	MFD_CELL_NAME("nct6694-gpio"),
	MFD_CELL_NAME("nct6694-gpio"),
	MFD_CELL_NAME("nct6694-gpio"),
	MFD_CELL_NAME("nct6694-gpio"),
	MFD_CELL_NAME("nct6694-gpio"),
	MFD_CELL_NAME("nct6694-gpio"),
	MFD_CELL_NAME("nct6694-gpio"),
	MFD_CELL_NAME("nct6694-gpio"),
	MFD_CELL_NAME("nct6694-gpio"),
	MFD_CELL_NAME("nct6694-gpio"),
	MFD_CELL_NAME("nct6694-gpio"),
	MFD_CELL_NAME("nct6694-gpio"),
	MFD_CELL_NAME("nct6694-gpio"),
	MFD_CELL_NAME("nct6694-gpio"),
	MFD_CELL_NAME("nct6694-gpio"),

	MFD_CELL_NAME("nct6694-i2c"),
	MFD_CELL_NAME("nct6694-i2c"),
	MFD_CELL_NAME("nct6694-i2c"),
	MFD_CELL_NAME("nct6694-i2c"),
	MFD_CELL_NAME("nct6694-i2c"),
	MFD_CELL_NAME("nct6694-i2c"),

	MFD_CELL_NAME("nct6694-canfd"),
	MFD_CELL_NAME("nct6694-canfd"),

	MFD_CELL_NAME("nct6694-wdt"),
	MFD_CELL_NAME("nct6694-wdt"),

	MFD_CELL_NAME("nct6694-hwmon"),

	MFD_CELL_NAME("nct6694-rtc"),
};

static int nct6694_response_err_handling(struct nct6694 *nct6694, unsigned char err_status)
{
	switch (err_status) {
	case NCT6694_NO_ERROR:
		return 0;
	case NCT6694_NOT_SUPPORT_ERROR:
		dev_err(nct6694->dev, "Command is not supported!\n");
		break;
	case NCT6694_NO_RESPONSE_ERROR:
		dev_warn(nct6694->dev, "Command received no response!\n");
		break;
	case NCT6694_TIMEOUT_ERROR:
		dev_warn(nct6694->dev, "Command timed out!\n");
		break;
	case NCT6694_PENDING:
		dev_err(nct6694->dev, "Command is pending!\n");
		break;
	default:
		return -EINVAL;
	}

	return -EIO;
}

/**
 * nct6694_read_msg() - Read message from NCT6694 device
 * @nct6694: NCT6694 device pointer
 * @cmd_hd: command header structure
 * @buf: buffer to store the response data
 *
 * Sends a command to the NCT6694 device and reads the response.
 * The command header is specified in @cmd_hd, and the response
 * data is stored in @buf.
 *
 * Return: Negative value on error or 0 on success.
 */
int nct6694_read_msg(struct nct6694 *nct6694, const struct nct6694_cmd_header *cmd_hd, void *buf)
{
	union nct6694_usb_msg *msg = nct6694->usb_msg;
	struct usb_device *udev = nct6694->udev;
	int tx_len, rx_len, ret;

	guard(mutex)(&nct6694->access_lock);

	memcpy(&msg->cmd_header, cmd_hd, sizeof(*cmd_hd));
	msg->cmd_header.hctrl = NCT6694_HCTRL_GET;

	/* Send command packet to USB device */
	ret = usb_bulk_msg(udev, usb_sndbulkpipe(udev, NCT6694_BULK_OUT_EP), &msg->cmd_header,
			   sizeof(*msg), &tx_len, NCT6694_URB_TIMEOUT);
	if (ret)
		return ret;

	/* Receive response packet from USB device */
	ret = usb_bulk_msg(udev, usb_rcvbulkpipe(udev, NCT6694_BULK_IN_EP), &msg->response_header,
			   sizeof(*msg), &rx_len, NCT6694_URB_TIMEOUT);
	if (ret)
		return ret;

	/* Receive data packet from USB device */
	ret = usb_bulk_msg(udev, usb_rcvbulkpipe(udev, NCT6694_BULK_IN_EP), buf,
			   le16_to_cpu(cmd_hd->len), &rx_len, NCT6694_URB_TIMEOUT);
	if (ret)
		return ret;

	if (rx_len != le16_to_cpu(cmd_hd->len)) {
		dev_err(nct6694->dev, "Expected received length %d, but got %d\n",
			le16_to_cpu(cmd_hd->len), rx_len);
		return -EIO;
	}

	return nct6694_response_err_handling(nct6694, msg->response_header.sts);
}
EXPORT_SYMBOL_GPL(nct6694_read_msg);

/**
 * nct6694_write_msg() - Write message to NCT6694 device
 * @nct6694: NCT6694 device pointer
 * @cmd_hd: command header structure
 * @buf: buffer containing the data to be sent
 *
 * Sends a command to the NCT6694 device and writes the data
 * from @buf. The command header is specified in @cmd_hd.
 *
 * Return: Negative value on error or 0 on success.
 */
int nct6694_write_msg(struct nct6694 *nct6694, const struct nct6694_cmd_header *cmd_hd, void *buf)
{
	union nct6694_usb_msg *msg = nct6694->usb_msg;
	struct usb_device *udev = nct6694->udev;
	int tx_len, rx_len, ret;

	guard(mutex)(&nct6694->access_lock);

	memcpy(&msg->cmd_header, cmd_hd, sizeof(*cmd_hd));
	msg->cmd_header.hctrl = NCT6694_HCTRL_SET;

	/* Send command packet to USB device */
	ret = usb_bulk_msg(udev, usb_sndbulkpipe(udev, NCT6694_BULK_OUT_EP), &msg->cmd_header,
			   sizeof(*msg), &tx_len, NCT6694_URB_TIMEOUT);
	if (ret)
		return ret;

	/* Send data packet to USB device */
	ret = usb_bulk_msg(udev, usb_sndbulkpipe(udev, NCT6694_BULK_OUT_EP), buf,
			   le16_to_cpu(cmd_hd->len), &tx_len, NCT6694_URB_TIMEOUT);
	if (ret)
		return ret;

	/* Receive response packet from USB device */
	ret = usb_bulk_msg(udev, usb_rcvbulkpipe(udev, NCT6694_BULK_IN_EP), &msg->response_header,
			   sizeof(*msg), &rx_len, NCT6694_URB_TIMEOUT);
	if (ret)
		return ret;

	/* Receive data packet from USB device */
	ret = usb_bulk_msg(udev, usb_rcvbulkpipe(udev, NCT6694_BULK_IN_EP), buf,
			   le16_to_cpu(cmd_hd->len), &rx_len, NCT6694_URB_TIMEOUT);
	if (ret)
		return ret;

	if (rx_len != le16_to_cpu(cmd_hd->len)) {
		dev_err(nct6694->dev, "Expected transmitted length %d, but got %d\n",
			le16_to_cpu(cmd_hd->len), rx_len);
		return -EIO;
	}

	return nct6694_response_err_handling(nct6694, msg->response_header.sts);
}
EXPORT_SYMBOL_GPL(nct6694_write_msg);

static void usb_int_callback(struct urb *urb)
{
	struct nct6694 *nct6694 = urb->context;
	__le32 *status_le = urb->transfer_buffer;
	u32 int_status;
	int ret;

	switch (urb->status) {
	case 0:
		break;
	case -ECONNRESET:
	case -ENOENT:
	case -ESHUTDOWN:
		return;
	default:
		goto resubmit;
	}

	int_status = le32_to_cpu(*status_le);

	while (int_status) {
		int irq = __ffs(int_status);

		generic_handle_irq_safe(irq_find_mapping(nct6694->domain, irq));
		int_status &= ~BIT(irq);
	}

resubmit:
	ret = usb_submit_urb(urb, GFP_ATOMIC);
	if (ret)
		dev_warn(nct6694->dev, "Failed to resubmit urb, status %pe",  ERR_PTR(ret));
}

static void nct6694_irq_enable(struct irq_data *data)
{
	struct nct6694 *nct6694 = irq_data_get_irq_chip_data(data);
	irq_hw_number_t hwirq = irqd_to_hwirq(data);

	guard(spinlock_irqsave)(&nct6694->irq_lock);

	nct6694->irq_enable |= BIT(hwirq);
}

static void nct6694_irq_disable(struct irq_data *data)
{
	struct nct6694 *nct6694 = irq_data_get_irq_chip_data(data);
	irq_hw_number_t hwirq = irqd_to_hwirq(data);

	guard(spinlock_irqsave)(&nct6694->irq_lock);

	nct6694->irq_enable &= ~BIT(hwirq);
}

static const struct irq_chip nct6694_irq_chip = {
	.name = "nct6694-irq",
	.flags = IRQCHIP_SKIP_SET_WAKE,
	.irq_enable = nct6694_irq_enable,
	.irq_disable = nct6694_irq_disable,
};

static int nct6694_irq_domain_map(struct irq_domain *d, unsigned int irq, irq_hw_number_t hw)
{
	struct nct6694 *nct6694 = d->host_data;

	irq_set_chip_data(irq, nct6694);
	irq_set_chip_and_handler(irq, &nct6694_irq_chip, handle_simple_irq);

	return 0;
}

static void nct6694_irq_domain_unmap(struct irq_domain *d, unsigned int irq)
{
	irq_set_chip_and_handler(irq, NULL, NULL);
	irq_set_chip_data(irq, NULL);
}

static const struct irq_domain_ops nct6694_irq_domain_ops = {
	.map	= nct6694_irq_domain_map,
	.unmap	= nct6694_irq_domain_unmap,
};

static int nct6694_usb_probe(struct usb_interface *iface,
			     const struct usb_device_id *id)
{
	struct usb_device *udev = interface_to_usbdev(iface);
	struct usb_endpoint_descriptor *int_endpoint;
	struct usb_host_interface *interface;
	struct device *dev = &iface->dev;
	struct nct6694 *nct6694;
	int ret;

	nct6694 = devm_kzalloc(dev, sizeof(*nct6694), GFP_KERNEL);
	if (!nct6694)
		return -ENOMEM;

	nct6694->usb_msg = devm_kzalloc(dev, sizeof(union nct6694_usb_msg), GFP_KERNEL);
	if (!nct6694->usb_msg)
		return -ENOMEM;

	nct6694->int_buffer = devm_kzalloc(dev, sizeof(*nct6694->int_buffer), GFP_KERNEL);
	if (!nct6694->int_buffer)
		return -ENOMEM;

	nct6694->int_in_urb = usb_alloc_urb(0, GFP_KERNEL);
	if (!nct6694->int_in_urb)
		return -ENOMEM;

	nct6694->domain = irq_domain_create_simple(NULL, NCT6694_NR_IRQS, 0,
						   &nct6694_irq_domain_ops,
						   nct6694);
	if (!nct6694->domain) {
		ret = -ENODEV;
		goto err_urb;
	}

	nct6694->dev = dev;
	nct6694->udev = udev;

	ida_init(&nct6694->gpio_ida);
	ida_init(&nct6694->i2c_ida);
	ida_init(&nct6694->canfd_ida);
	ida_init(&nct6694->wdt_ida);

	spin_lock_init(&nct6694->irq_lock);

	ret = devm_mutex_init(dev, &nct6694->access_lock);
	if (ret)
		goto err_ida;

	interface = iface->cur_altsetting;

	int_endpoint = &interface->endpoint[0].desc;
	if (!usb_endpoint_is_int_in(int_endpoint)) {
		ret = -ENODEV;
		goto err_ida;
	}

	usb_fill_int_urb(nct6694->int_in_urb, udev, usb_rcvintpipe(udev, NCT6694_INT_IN_EP),
			 nct6694->int_buffer, sizeof(*nct6694->int_buffer), usb_int_callback,
			 nct6694, int_endpoint->bInterval);

	ret = usb_submit_urb(nct6694->int_in_urb, GFP_KERNEL);
	if (ret)
		goto err_ida;

	usb_set_intfdata(iface, nct6694);

	ret = mfd_add_hotplug_devices(dev, nct6694_devs, ARRAY_SIZE(nct6694_devs));
	if (ret)
		goto err_mfd;

	return 0;

err_mfd:
	usb_kill_urb(nct6694->int_in_urb);
err_ida:
	ida_destroy(&nct6694->wdt_ida);
	ida_destroy(&nct6694->canfd_ida);
	ida_destroy(&nct6694->i2c_ida);
	ida_destroy(&nct6694->gpio_ida);
	irq_domain_remove(nct6694->domain);
err_urb:
	usb_free_urb(nct6694->int_in_urb);
	return ret;
}

static void nct6694_usb_disconnect(struct usb_interface *iface)
{
	struct nct6694 *nct6694 = usb_get_intfdata(iface);

	mfd_remove_devices(nct6694->dev);
	usb_kill_urb(nct6694->int_in_urb);
	ida_destroy(&nct6694->wdt_ida);
	ida_destroy(&nct6694->canfd_ida);
	ida_destroy(&nct6694->i2c_ida);
	ida_destroy(&nct6694->gpio_ida);
	irq_domain_remove(nct6694->domain);
	usb_free_urb(nct6694->int_in_urb);
}

static const struct usb_device_id nct6694_ids[] = {
	{ USB_DEVICE_AND_INTERFACE_INFO(NCT6694_VENDOR_ID, NCT6694_PRODUCT_ID, 0xFF, 0x00, 0x00) },
	{ }
};
MODULE_DEVICE_TABLE(usb, nct6694_ids);

static struct usb_driver nct6694_usb_driver = {
	.name		= "nct6694",
	.id_table	= nct6694_ids,
	.probe		= nct6694_usb_probe,
	.disconnect	= nct6694_usb_disconnect,
};
module_usb_driver(nct6694_usb_driver);

MODULE_DESCRIPTION("Nuvoton NCT6694 core driver");
MODULE_AUTHOR("Ming Yu <tmyu0@nuvoton.com>");
MODULE_LICENSE("GPL");