mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-02-17 09:00:22 -05:00
The Nuvoton NCT6694 provides an USB interface to the host to access its features. Sub-devices can use the USB functions nct6694_read_msg() and nct6694_write_msg() to issue a command. They can also request interrupt that will be called when the USB device receives its interrupt pipe. Signed-off-by: Ming Yu <a0282524688@gmail.com> Link: https://lore.kernel.org/r/20250912091952.1169369-2-a0282524688@gmail.com Signed-off-by: Lee Jones <lee@kernel.org>
389 lines
10 KiB
C
389 lines
10 KiB
C
// 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");
|