| // SPDX-License-Identifier: GPL-2.0+ |
| /* |
| * aspeed-vhub -- Driver for Aspeed SoC "vHub" USB gadget |
| * |
| * core.c - Top level support |
| * |
| * Copyright 2017 IBM Corporation |
| */ |
| |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/platform_device.h> |
| #include <linux/delay.h> |
| #include <linux/ioport.h> |
| #include <linux/slab.h> |
| #include <linux/errno.h> |
| #include <linux/list.h> |
| #include <linux/interrupt.h> |
| #include <linux/proc_fs.h> |
| #include <linux/prefetch.h> |
| #include <linux/clk.h> |
| #include <linux/usb/gadget.h> |
| #include <linux/of.h> |
| #include <linux/regmap.h> |
| #include <linux/dma-mapping.h> |
| |
| #include "vhub.h" |
| |
| void ast_vhub_done(struct ast_vhub_ep *ep, struct ast_vhub_req *req, |
| int status) |
| { |
| bool internal = req->internal; |
| struct ast_vhub *vhub = ep->vhub; |
| |
| EPVDBG(ep, "completing request @%p, status %d\n", req, status); |
| |
| list_del_init(&req->queue); |
| |
| if ((req->req.status == -EINPROGRESS) || (status == -EOVERFLOW)) |
| req->req.status = status; |
| |
| if (req->req.dma) { |
| if (!WARN_ON(!ep->dev)) |
| usb_gadget_unmap_request_by_dev(&vhub->pdev->dev, |
| &req->req, ep->epn.is_in); |
| req->req.dma = 0; |
| } |
| |
| /* |
| * If this isn't an internal EP0 request, call the core |
| * to call the gadget completion. |
| */ |
| if (!internal) { |
| spin_unlock(&ep->vhub->lock); |
| usb_gadget_giveback_request(&ep->ep, &req->req); |
| spin_lock(&ep->vhub->lock); |
| } |
| } |
| |
| void ast_vhub_nuke(struct ast_vhub_ep *ep, int status) |
| { |
| struct ast_vhub_req *req; |
| int count = 0; |
| |
| /* Beware, lock will be dropped & req-acquired by done() */ |
| while (!list_empty(&ep->queue)) { |
| req = list_first_entry(&ep->queue, struct ast_vhub_req, queue); |
| ast_vhub_done(ep, req, status); |
| count++; |
| } |
| if (count) |
| EPDBG(ep, "Nuked %d request(s)\n", count); |
| } |
| |
| struct usb_request *ast_vhub_alloc_request(struct usb_ep *u_ep, |
| gfp_t gfp_flags) |
| { |
| struct ast_vhub_req *req; |
| |
| req = kzalloc(sizeof(*req), gfp_flags); |
| if (!req) |
| return NULL; |
| return &req->req; |
| } |
| |
| void ast_vhub_free_request(struct usb_ep *u_ep, struct usb_request *u_req) |
| { |
| struct ast_vhub_req *req = to_ast_req(u_req); |
| |
| kfree(req); |
| } |
| |
| static irqreturn_t ast_vhub_irq(int irq, void *data) |
| { |
| struct ast_vhub *vhub = data; |
| irqreturn_t iret = IRQ_NONE; |
| u32 i, istat; |
| |
| /* Stale interrupt while tearing down */ |
| if (!vhub->ep0_bufs) |
| return IRQ_NONE; |
| |
| spin_lock(&vhub->lock); |
| |
| /* Read and ACK interrupts */ |
| istat = readl(vhub->regs + AST_VHUB_ISR); |
| if (!istat) |
| goto bail; |
| writel(istat, vhub->regs + AST_VHUB_ISR); |
| iret = IRQ_HANDLED; |
| |
| UDCVDBG(vhub, "irq status=%08x, ep_acks=%08x ep_nacks=%08x\n", |
| istat, |
| readl(vhub->regs + AST_VHUB_EP_ACK_ISR), |
| readl(vhub->regs + AST_VHUB_EP_NACK_ISR)); |
| |
| /* Handle generic EPs first */ |
| if (istat & VHUB_IRQ_EP_POOL_ACK_STALL) { |
| u32 ep_acks = readl(vhub->regs + AST_VHUB_EP_ACK_ISR); |
| writel(ep_acks, vhub->regs + AST_VHUB_EP_ACK_ISR); |
| |
| for (i = 0; ep_acks && i < vhub->max_epns; i++) { |
| u32 mask = VHUB_EP_IRQ(i); |
| if (ep_acks & mask) { |
| ast_vhub_epn_ack_irq(&vhub->epns[i]); |
| ep_acks &= ~mask; |
| } |
| } |
| } |
| |
| /* Handle device interrupts */ |
| if (istat & vhub->port_irq_mask) { |
| for (i = 0; i < vhub->max_ports; i++) { |
| if (istat & VHUB_DEV_IRQ(i)) |
| ast_vhub_dev_irq(&vhub->ports[i].dev); |
| } |
| } |
| |
| /* Handle top-level vHub EP0 interrupts */ |
| if (istat & (VHUB_IRQ_HUB_EP0_OUT_ACK_STALL | |
| VHUB_IRQ_HUB_EP0_IN_ACK_STALL | |
| VHUB_IRQ_HUB_EP0_SETUP)) { |
| if (istat & VHUB_IRQ_HUB_EP0_IN_ACK_STALL) |
| ast_vhub_ep0_handle_ack(&vhub->ep0, true); |
| if (istat & VHUB_IRQ_HUB_EP0_OUT_ACK_STALL) |
| ast_vhub_ep0_handle_ack(&vhub->ep0, false); |
| if (istat & VHUB_IRQ_HUB_EP0_SETUP) |
| ast_vhub_ep0_handle_setup(&vhub->ep0); |
| } |
| |
| /* Various top level bus events */ |
| if (istat & (VHUB_IRQ_BUS_RESUME | |
| VHUB_IRQ_BUS_SUSPEND | |
| VHUB_IRQ_BUS_RESET)) { |
| if (istat & VHUB_IRQ_BUS_RESUME) |
| ast_vhub_hub_resume(vhub); |
| if (istat & VHUB_IRQ_BUS_SUSPEND) |
| ast_vhub_hub_suspend(vhub); |
| if (istat & VHUB_IRQ_BUS_RESET) |
| ast_vhub_hub_reset(vhub); |
| } |
| |
| bail: |
| spin_unlock(&vhub->lock); |
| return iret; |
| } |
| |
| void ast_vhub_init_hw(struct ast_vhub *vhub) |
| { |
| u32 ctrl, port_mask, epn_mask; |
| |
| UDCDBG(vhub,"(Re)Starting HW ...\n"); |
| |
| /* Enable PHY */ |
| ctrl = VHUB_CTRL_PHY_CLK | |
| VHUB_CTRL_PHY_RESET_DIS; |
| |
| /* |
| * We do *NOT* set the VHUB_CTRL_CLK_STOP_SUSPEND bit |
| * to stop the logic clock during suspend because |
| * it causes the registers to become inaccessible and |
| * we haven't yet figured out a good wayt to bring the |
| * controller back into life to issue a wakeup. |
| */ |
| |
| /* |
| * Set some ISO & split control bits according to Aspeed |
| * recommendation |
| * |
| * VHUB_CTRL_ISO_RSP_CTRL: When set tells the HW to respond |
| * with 0 bytes data packet to ISO IN endpoints when no data |
| * is available. |
| * |
| * VHUB_CTRL_SPLIT_IN: This makes a SOF complete a split IN |
| * transaction. |
| */ |
| ctrl |= VHUB_CTRL_ISO_RSP_CTRL | VHUB_CTRL_SPLIT_IN; |
| writel(ctrl, vhub->regs + AST_VHUB_CTRL); |
| udelay(1); |
| |
| /* Set descriptor ring size */ |
| if (AST_VHUB_DESCS_COUNT == 256) { |
| ctrl |= VHUB_CTRL_LONG_DESC; |
| writel(ctrl, vhub->regs + AST_VHUB_CTRL); |
| } else { |
| BUILD_BUG_ON(AST_VHUB_DESCS_COUNT != 32); |
| } |
| |
| /* Reset all devices */ |
| port_mask = GENMASK(vhub->max_ports, 1); |
| writel(VHUB_SW_RESET_ROOT_HUB | |
| VHUB_SW_RESET_DMA_CONTROLLER | |
| VHUB_SW_RESET_EP_POOL | |
| port_mask, vhub->regs + AST_VHUB_SW_RESET); |
| udelay(1); |
| writel(0, vhub->regs + AST_VHUB_SW_RESET); |
| |
| /* Disable and cleanup EP ACK/NACK interrupts */ |
| epn_mask = GENMASK(vhub->max_epns - 1, 0); |
| writel(0, vhub->regs + AST_VHUB_EP_ACK_IER); |
| writel(0, vhub->regs + AST_VHUB_EP_NACK_IER); |
| writel(epn_mask, vhub->regs + AST_VHUB_EP_ACK_ISR); |
| writel(epn_mask, vhub->regs + AST_VHUB_EP_NACK_ISR); |
| |
| /* Default settings for EP0, enable HW hub EP1 */ |
| writel(0, vhub->regs + AST_VHUB_EP0_CTRL); |
| writel(VHUB_EP1_CTRL_RESET_TOGGLE | |
| VHUB_EP1_CTRL_ENABLE, |
| vhub->regs + AST_VHUB_EP1_CTRL); |
| writel(0, vhub->regs + AST_VHUB_EP1_STS_CHG); |
| |
| /* Configure EP0 DMA buffer */ |
| writel(vhub->ep0.buf_dma, vhub->regs + AST_VHUB_EP0_DATA); |
| |
| /* Clear address */ |
| writel(0, vhub->regs + AST_VHUB_CONF); |
| |
| /* Pullup hub (activate on host) */ |
| if (vhub->force_usb1) |
| ctrl |= VHUB_CTRL_FULL_SPEED_ONLY; |
| |
| ctrl |= VHUB_CTRL_UPSTREAM_CONNECT; |
| writel(ctrl, vhub->regs + AST_VHUB_CTRL); |
| |
| /* Enable some interrupts */ |
| writel(VHUB_IRQ_HUB_EP0_IN_ACK_STALL | |
| VHUB_IRQ_HUB_EP0_OUT_ACK_STALL | |
| VHUB_IRQ_HUB_EP0_SETUP | |
| VHUB_IRQ_EP_POOL_ACK_STALL | |
| VHUB_IRQ_BUS_RESUME | |
| VHUB_IRQ_BUS_SUSPEND | |
| VHUB_IRQ_BUS_RESET, |
| vhub->regs + AST_VHUB_IER); |
| } |
| |
| static void ast_vhub_remove(struct platform_device *pdev) |
| { |
| struct ast_vhub *vhub = platform_get_drvdata(pdev); |
| unsigned long flags; |
| int i; |
| |
| if (!vhub || !vhub->regs) |
| return; |
| |
| /* Remove devices */ |
| for (i = 0; i < vhub->max_ports; i++) |
| ast_vhub_del_dev(&vhub->ports[i].dev); |
| |
| spin_lock_irqsave(&vhub->lock, flags); |
| |
| /* Mask & ack all interrupts */ |
| writel(0, vhub->regs + AST_VHUB_IER); |
| writel(VHUB_IRQ_ACK_ALL, vhub->regs + AST_VHUB_ISR); |
| |
| /* Pull device, leave PHY enabled */ |
| writel(VHUB_CTRL_PHY_CLK | |
| VHUB_CTRL_PHY_RESET_DIS, |
| vhub->regs + AST_VHUB_CTRL); |
| |
| if (vhub->clk) |
| clk_disable_unprepare(vhub->clk); |
| |
| spin_unlock_irqrestore(&vhub->lock, flags); |
| |
| if (vhub->ep0_bufs) |
| dma_free_coherent(&pdev->dev, |
| AST_VHUB_EP0_MAX_PACKET * |
| (vhub->max_ports + 1), |
| vhub->ep0_bufs, |
| vhub->ep0_bufs_dma); |
| vhub->ep0_bufs = NULL; |
| } |
| |
| static int ast_vhub_probe(struct platform_device *pdev) |
| { |
| enum usb_device_speed max_speed; |
| struct ast_vhub *vhub; |
| struct resource *res; |
| int i, rc = 0; |
| const struct device_node *np = pdev->dev.of_node; |
| |
| vhub = devm_kzalloc(&pdev->dev, sizeof(*vhub), GFP_KERNEL); |
| if (!vhub) |
| return -ENOMEM; |
| |
| rc = of_property_read_u32(np, "aspeed,vhub-downstream-ports", |
| &vhub->max_ports); |
| if (rc < 0) |
| vhub->max_ports = AST_VHUB_NUM_PORTS; |
| |
| vhub->ports = devm_kcalloc(&pdev->dev, vhub->max_ports, |
| sizeof(*vhub->ports), GFP_KERNEL); |
| if (!vhub->ports) |
| return -ENOMEM; |
| |
| rc = of_property_read_u32(np, "aspeed,vhub-generic-endpoints", |
| &vhub->max_epns); |
| if (rc < 0) |
| vhub->max_epns = AST_VHUB_NUM_GEN_EPs; |
| |
| vhub->epns = devm_kcalloc(&pdev->dev, vhub->max_epns, |
| sizeof(*vhub->epns), GFP_KERNEL); |
| if (!vhub->epns) |
| return -ENOMEM; |
| |
| spin_lock_init(&vhub->lock); |
| vhub->pdev = pdev; |
| vhub->port_irq_mask = GENMASK(VHUB_IRQ_DEV1_BIT + vhub->max_ports - 1, |
| VHUB_IRQ_DEV1_BIT); |
| |
| vhub->regs = devm_platform_get_and_ioremap_resource(pdev, 0, &res); |
| if (IS_ERR(vhub->regs)) { |
| dev_err(&pdev->dev, "Failed to map resources\n"); |
| return PTR_ERR(vhub->regs); |
| } |
| UDCDBG(vhub, "vHub@%pR mapped @%p\n", res, vhub->regs); |
| |
| platform_set_drvdata(pdev, vhub); |
| |
| vhub->clk = devm_clk_get(&pdev->dev, NULL); |
| if (IS_ERR(vhub->clk)) { |
| rc = PTR_ERR(vhub->clk); |
| goto err; |
| } |
| rc = clk_prepare_enable(vhub->clk); |
| if (rc) { |
| dev_err(&pdev->dev, "Error couldn't enable clock (%d)\n", rc); |
| goto err; |
| } |
| |
| /* Check if we need to limit the HW to USB1 */ |
| max_speed = usb_get_maximum_speed(&pdev->dev); |
| if (max_speed != USB_SPEED_UNKNOWN && max_speed < USB_SPEED_HIGH) |
| vhub->force_usb1 = true; |
| |
| /* Mask & ack all interrupts before installing the handler */ |
| writel(0, vhub->regs + AST_VHUB_IER); |
| writel(VHUB_IRQ_ACK_ALL, vhub->regs + AST_VHUB_ISR); |
| |
| /* Find interrupt and install handler */ |
| vhub->irq = platform_get_irq(pdev, 0); |
| if (vhub->irq < 0) { |
| rc = vhub->irq; |
| goto err; |
| } |
| rc = devm_request_irq(&pdev->dev, vhub->irq, ast_vhub_irq, 0, |
| KBUILD_MODNAME, vhub); |
| if (rc) { |
| dev_err(&pdev->dev, "Failed to request interrupt\n"); |
| goto err; |
| } |
| |
| /* |
| * Allocate DMA buffers for all EP0s in one chunk, |
| * one per port and one for the vHub itself |
| */ |
| vhub->ep0_bufs = dma_alloc_coherent(&pdev->dev, |
| AST_VHUB_EP0_MAX_PACKET * |
| (vhub->max_ports + 1), |
| &vhub->ep0_bufs_dma, GFP_KERNEL); |
| if (!vhub->ep0_bufs) { |
| dev_err(&pdev->dev, "Failed to allocate EP0 DMA buffers\n"); |
| rc = -ENOMEM; |
| goto err; |
| } |
| UDCVDBG(vhub, "EP0 DMA buffers @%p (DMA 0x%08x)\n", |
| vhub->ep0_bufs, (u32)vhub->ep0_bufs_dma); |
| |
| /* Init vHub EP0 */ |
| ast_vhub_init_ep0(vhub, &vhub->ep0, NULL); |
| |
| /* Init devices */ |
| for (i = 0; i < vhub->max_ports && rc == 0; i++) |
| rc = ast_vhub_init_dev(vhub, i); |
| if (rc) |
| goto err; |
| |
| /* Init hub emulation */ |
| rc = ast_vhub_init_hub(vhub); |
| if (rc) |
| goto err; |
| |
| /* Initialize HW */ |
| ast_vhub_init_hw(vhub); |
| |
| dev_info(&pdev->dev, "Initialized virtual hub in USB%d mode\n", |
| vhub->force_usb1 ? 1 : 2); |
| |
| return 0; |
| err: |
| ast_vhub_remove(pdev); |
| return rc; |
| } |
| |
| static const struct of_device_id ast_vhub_dt_ids[] = { |
| { |
| .compatible = "aspeed,ast2400-usb-vhub", |
| }, |
| { |
| .compatible = "aspeed,ast2500-usb-vhub", |
| }, |
| { |
| .compatible = "aspeed,ast2600-usb-vhub", |
| }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(of, ast_vhub_dt_ids); |
| |
| static struct platform_driver ast_vhub_driver = { |
| .probe = ast_vhub_probe, |
| .remove_new = ast_vhub_remove, |
| .driver = { |
| .name = KBUILD_MODNAME, |
| .of_match_table = ast_vhub_dt_ids, |
| }, |
| }; |
| module_platform_driver(ast_vhub_driver); |
| |
| MODULE_DESCRIPTION("Aspeed vHub udc driver"); |
| MODULE_AUTHOR("Benjamin Herrenschmidt <benh@kernel.crashing.org>"); |
| MODULE_LICENSE("GPL"); |