// 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 #include #include #include #include #include #include #include #include #include #include #include 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 "); MODULE_LICENSE("GPL");