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