blob: 308b2fda3055c5419e2d3fb71002bc3729d333d8 [file] [log] [blame] [edit]
// 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");