| // Copyright 2017 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <ddk/binding.h> |
| #include <ddk/debug.h> |
| #include <ddk/platform-defs.h> |
| #include <usb/usb-request.h> |
| #include <fbl/auto_lock.h> |
| #include <hw/reg.h> |
| #include <pretty/hexdump.h> |
| |
| #include <stdlib.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <unistd.h> |
| |
| #include "dwc3.h" |
| #include "dwc3-regs.h" |
| #include "dwc3-types.h" |
| |
| // MMIO indices |
| enum { |
| MMIO_USB3OTG, |
| }; |
| |
| // IRQ indices |
| enum { |
| IRQ_USB3, |
| }; |
| |
| void dwc3_print_status(dwc3_t* dwc) { |
| auto* mmio = dwc3_mmio(dwc); |
| auto dsts = DSTS::Get().ReadFrom(mmio); |
| zxlogf(TRACE, "DSTS: "); |
| zxlogf(TRACE, "USBLNKST: %d ", dsts.USBLNKST()); |
| zxlogf(TRACE, "SOFFN: %d ", dsts.SOFFN()); |
| zxlogf(TRACE, "CONNECTSPD: %d ", dsts.CONNECTSPD()); |
| if (dsts.DCNRD()) zxlogf(TRACE, "DCNRD "); |
| if (dsts.SRE()) zxlogf(TRACE, "SRE "); |
| if (dsts.RSS()) zxlogf(TRACE, "RSS "); |
| if (dsts.SSS()) zxlogf(TRACE, "SSS "); |
| if (dsts.COREIDLE()) zxlogf(TRACE, "COREIDLE "); |
| if (dsts.DEVCTRLHLT()) zxlogf(TRACE, "DEVCTRLHLT "); |
| if (dsts.RXFIFOEMPTY()) zxlogf(TRACE, "RXFIFOEMPTY "); |
| zxlogf(TRACE, "\n"); |
| |
| auto gsts = GSTS::Get().ReadFrom(mmio); |
| zxlogf(TRACE, "GSTS: "); |
| zxlogf(TRACE, "CBELT: %d ", gsts.CBELT()); |
| zxlogf(TRACE, "CURMOD: %d ", gsts.CURMOD()); |
| if (gsts.SSIC_IP()) zxlogf(TRACE, "SSIC_IP "); |
| if (gsts.OTG_IP()) zxlogf(TRACE, "OTG_IP "); |
| if (gsts.BC_IP()) zxlogf(TRACE, "BC_IP "); |
| if (gsts.ADP_IP()) zxlogf(TRACE, "ADP_IP "); |
| if (gsts.Host_IP()) zxlogf(TRACE, "HOST_IP "); |
| if (gsts.Device_IP()) zxlogf(TRACE, "DEVICE_IP "); |
| if (gsts.CSRTimeout()) zxlogf(TRACE, "CSR_TIMEOUT "); |
| if (gsts.BUSERRADDRVLD()) zxlogf(TRACE, "BUSERRADDRVLD "); |
| zxlogf(TRACE, "\n"); |
| } |
| |
| static void dwc3_stop(dwc3_t* dwc) { |
| auto* mmio = dwc3_mmio(dwc); |
| |
| fbl::AutoLock lock(&dwc->lock); |
| |
| DCTL::Get() |
| .ReadFrom(mmio) |
| .set_RUN_STOP(0) |
| .set_CSFTRST(1) |
| .WriteTo(mmio); |
| while (DCTL::Get().ReadFrom(mmio).CSFTRST()) { |
| usleep(1000); |
| } |
| } |
| |
| static void dwc3_start_peripheral_mode(dwc3_t* dwc) { |
| auto* mmio = dwc3_mmio(dwc); |
| |
| dwc->lock.Acquire(); |
| |
| // configure and enable PHYs |
| GUSB2PHYCFG::Get(0).ReadFrom(mmio).set_USBTRDTIM(9).WriteTo(mmio); |
| GUSB3PIPECTL::Get(0) |
| .ReadFrom(mmio) |
| .set_DELAYP1TRANS(0) |
| .set_SUSPENDENABLE(0) |
| .set_LFPSFILTER(1) |
| .set_SS_TX_DE_EMPHASIS(1) |
| .WriteTo(mmio); |
| |
| // configure for device mode |
| GCTL::Get() |
| .FromValue(0) |
| .set_PWRDNSCALE(2) |
| .set_U2RSTECN(1) |
| .set_PRTCAPDIR(GCTL::PRTCAPDIR_DEVICE) |
| .set_U2EXIT_LFPS(1) |
| .WriteTo(mmio); |
| |
| uint32_t nump = 16; |
| uint32_t max_speed = DCFG::DEVSPD_SUPER; |
| DCFG::Get() |
| .ReadFrom(mmio) |
| .set_NUMP(nump) |
| .set_DEVSPD(max_speed) |
| .set_DEVADDR(0) |
| .WriteTo(mmio); |
| |
| dwc3_events_start(dwc); |
| |
| dwc->lock.Release(); |
| |
| dwc3_ep0_start(dwc); |
| |
| dwc->lock.Acquire(); |
| |
| // start the controller |
| DCTL::Get().FromValue(0).set_RUN_STOP(1).WriteTo(mmio); |
| |
| dwc->lock.Release(); |
| } |
| |
| static zx_status_t xhci_get_protocol(void* ctx, uint32_t proto_id, void* protocol) { |
| auto* dwc = static_cast<dwc3_t*>(ctx); |
| // XHCI uses same MMIO and IRQ as dwc3, so we can just share our pdev protoocl |
| // with the XHCI driver |
| return device_get_protocol(dwc->parent, proto_id, protocol); |
| } |
| |
| static void xhci_release(void* ctx) { |
| auto* dwc = static_cast<dwc3_t*>(ctx); |
| fbl::AutoLock lock(&dwc->usb_mode_lock); |
| |
| if (dwc->start_device_on_xhci_release) { |
| dwc3_start_peripheral_mode(dwc); |
| dwc->start_device_on_xhci_release = false; |
| dwc->usb_mode = USB_MODE_PERIPHERAL; |
| } |
| } |
| |
| static zx_protocol_device_t xhci_device_ops = []() { |
| zx_protocol_device_t device; |
| device.version = DEVICE_OPS_VERSION; |
| device.get_protocol = xhci_get_protocol; |
| device.release = xhci_release; |
| return device; |
| }(); |
| |
| static void dwc3_start_host_mode(dwc3_t* dwc) { |
| auto* mmio = dwc3_mmio(dwc); |
| |
| dwc->lock.Acquire(); |
| |
| // configure for host mode |
| GCTL::Get() |
| .FromValue(0) |
| .set_PWRDNSCALE(2) |
| .set_U2RSTECN(1) |
| .set_PRTCAPDIR(GCTL::PRTCAPDIR_HOST) |
| .set_U2EXIT_LFPS(1) |
| .WriteTo(mmio); |
| |
| dwc->lock.Release(); |
| |
| // add a device to bind the XHCI driver |
| ZX_DEBUG_ASSERT(dwc->xhci_dev == nullptr); |
| |
| zx_device_prop_t props[] = { |
| {BIND_PLATFORM_DEV_VID, 0, PDEV_VID_GENERIC}, |
| {BIND_PLATFORM_DEV_PID, 0, PDEV_PID_GENERIC}, |
| {BIND_PLATFORM_DEV_DID, 0, PDEV_DID_USB_XHCI}, |
| }; |
| |
| device_add_args_t args = {}; |
| args.version = DEVICE_ADD_ARGS_VERSION; |
| args.name = "dwc3"; |
| args.proto_id = ZX_PROTOCOL_PDEV; |
| args.ctx = dwc; |
| args.ops = &xhci_device_ops; |
| args.props = props; |
| args.prop_count = countof(props); |
| |
| zx_status_t status = device_add(dwc->parent, &args, &dwc->xhci_dev); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "dwc3_start_host_mode failed to add device for XHCI: %d\n", status); |
| } |
| } |
| |
| void dwc3_usb_reset(dwc3_t* dwc) { |
| zxlogf(INFO, "dwc3_usb_reset\n"); |
| |
| dwc3_ep0_reset(dwc); |
| |
| for (unsigned i = 2; i < countof(dwc->eps); i++) { |
| dwc3_ep_end_transfers(dwc, i, ZX_ERR_IO_NOT_PRESENT); |
| dwc3_ep_set_stall(dwc, i, false); |
| } |
| |
| dwc3_set_address(dwc, 0); |
| dwc3_ep0_start(dwc); |
| usb_dci_interface_set_connected(&dwc->dci_intf, true); |
| } |
| |
| void dwc3_disconnected(dwc3_t* dwc) { |
| zxlogf(INFO, "dwc3_disconnected\n"); |
| |
| dwc3_cmd_ep_end_transfer(dwc, EP0_OUT); |
| dwc->ep0_state = EP0_STATE_NONE; |
| |
| if (dwc->dci_intf.ops) { |
| usb_dci_interface_set_connected(&dwc->dci_intf, false); |
| } |
| |
| for (unsigned i = 2; i < countof(dwc->eps); i++) { |
| dwc3_ep_end_transfers(dwc, i, ZX_ERR_IO_NOT_PRESENT); |
| dwc3_ep_set_stall(dwc, i, false); |
| } |
| } |
| |
| void dwc3_connection_done(dwc3_t* dwc) { |
| auto* mmio = dwc3_mmio(dwc); |
| |
| dwc->lock.Acquire(); |
| |
| uint32_t speed = DSTS::Get().ReadFrom(mmio).CONNECTSPD(); |
| uint16_t ep0_max_packet = 0; |
| |
| switch (speed) { |
| case DSTS::CONNECTSPD_HIGH: |
| dwc->speed = USB_SPEED_HIGH; |
| ep0_max_packet = 64; |
| break; |
| case DSTS::CONNECTSPD_FULL: |
| dwc->speed = USB_SPEED_FULL; |
| ep0_max_packet = 64; |
| break; |
| case DSTS::CONNECTSPD_SUPER: |
| case DSTS::CONNECTSPD_ENHANCED_SUPER: |
| dwc->speed = USB_SPEED_SUPER; |
| ep0_max_packet = 512; |
| break; |
| default: |
| zxlogf(ERROR, "dwc3_connection_done: unsupported speed %u\n", speed); |
| dwc->speed = USB_SPEED_UNDEFINED; |
| break; |
| } |
| |
| dwc->lock.Release(); |
| |
| if (ep0_max_packet) { |
| dwc->eps[EP0_OUT].max_packet_size = ep0_max_packet; |
| dwc->eps[EP0_IN].max_packet_size = ep0_max_packet; |
| dwc3_cmd_ep_set_config(dwc, EP0_OUT, USB_ENDPOINT_CONTROL, ep0_max_packet, 0, true); |
| dwc3_cmd_ep_set_config(dwc, EP0_IN, USB_ENDPOINT_CONTROL, ep0_max_packet, 0, true); |
| } |
| |
| usb_dci_interface_set_speed(&dwc->dci_intf, dwc->speed); |
| } |
| |
| void dwc3_set_address(dwc3_t* dwc, unsigned address) { |
| auto* mmio = dwc3_mmio(dwc); |
| fbl::AutoLock lock(&dwc->lock); |
| |
| DCFG::Get().ReadFrom(mmio).set_DEVADDR(address).WriteTo(mmio); |
| } |
| |
| void dwc3_reset_configuration(dwc3_t* dwc) { |
| auto* mmio = dwc3_mmio(dwc); |
| |
| dwc->lock.Acquire(); |
| |
| // disable all endpoints except EP0_OUT and EP0_IN |
| DALEPENA::Get().FromValue(0).EnableEp(EP0_OUT).EnableEp(EP0_IN).WriteTo(mmio); |
| |
| dwc->lock.Release(); |
| |
| for (unsigned i = 2; i < countof(dwc->eps); i++) { |
| dwc3_ep_end_transfers(dwc, i, ZX_ERR_IO_NOT_PRESENT); |
| dwc3_ep_set_stall(dwc, i, false); |
| } |
| } |
| |
| static void dwc3_request_queue(void* ctx, usb_request_t* req, const usb_request_complete_t* cb) { |
| auto* dwc = static_cast<dwc3_t*>(ctx); |
| auto* req_int = USB_REQ_TO_INTERNAL(req); |
| req_int->complete_cb = *cb; |
| |
| zxlogf(LTRACE, "dwc3_request_queue ep: %u\n", req->header.ep_address); |
| unsigned ep_num = dwc3_ep_num(req->header.ep_address); |
| if (ep_num < 2 || ep_num >= countof(dwc->eps)) { |
| zxlogf(ERROR, "dwc3_request_queue: bad ep address 0x%02X\n", req->header.ep_address); |
| usb_request_complete_new(req, ZX_ERR_INVALID_ARGS, 0, cb); |
| return; |
| } |
| |
| dwc3_ep_queue(dwc, ep_num, req); |
| } |
| |
| static zx_status_t dwc3_set_interface(void* ctx, const usb_dci_interface_t* dci_intf) { |
| auto* dwc = static_cast<dwc3_t*>(ctx); |
| memcpy(&dwc->dci_intf, dci_intf, sizeof(dwc->dci_intf)); |
| return ZX_OK; |
| } |
| |
| static zx_status_t dwc3_config_ep(void* ctx, const usb_endpoint_descriptor_t* ep_desc, |
| const usb_ss_ep_comp_descriptor_t* ss_comp_desc) { |
| auto* dwc = static_cast<dwc3_t*>(ctx); |
| return dwc3_ep_config(dwc, ep_desc, ss_comp_desc); |
| } |
| |
| static zx_status_t dwc3_disable_ep(void* ctx, uint8_t ep_addr) { |
| auto* dwc = static_cast<dwc3_t*>(ctx); |
| return dwc3_ep_disable(dwc, ep_addr); |
| } |
| |
| static zx_status_t dwc3_set_stall(void* ctx, uint8_t ep_address) { |
| auto* dwc = static_cast<dwc3_t*>(ctx); |
| return dwc3_ep_set_stall(dwc, dwc3_ep_num(ep_address), true); |
| } |
| |
| static zx_status_t dwc3_clear_stall(void* ctx, uint8_t ep_address) { |
| auto* dwc = static_cast<dwc3_t*>(ctx); |
| return dwc3_ep_set_stall(dwc, dwc3_ep_num(ep_address), false); |
| } |
| |
| static size_t dwc3_get_request_size(void* ctx) { |
| //Allocate dwc_usb_req_internal_t after usb_request_t, to accommodate queueing in |
| //the dwc3 layer. |
| return sizeof(usb_request_t) + sizeof(dwc_usb_req_internal_t); |
| } |
| |
| usb_dci_protocol_ops_t dwc_dci_ops = { |
| .request_queue = dwc3_request_queue, |
| .set_interface = dwc3_set_interface, |
| .config_ep = dwc3_config_ep, |
| .disable_ep = dwc3_disable_ep, |
| .ep_set_stall = dwc3_set_stall, |
| .ep_clear_stall = dwc3_clear_stall, |
| .get_request_size = dwc3_get_request_size, |
| }; |
| |
| static zx_status_t dwc3_set_mode(void* ctx, usb_mode_t mode) { |
| auto* dwc = static_cast<dwc3_t*>(ctx); |
| zx_status_t status = ZX_OK; |
| |
| if (mode == USB_MODE_OTG) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| fbl::AutoLock lock(&dwc->usb_mode_lock); |
| |
| if (dwc->usb_mode == mode) { |
| return ZX_OK; |
| } |
| |
| // Shutdown if we are in peripheral mode |
| if (dwc->usb_mode == USB_MODE_PERIPHERAL) { |
| dwc3_events_stop(dwc); |
| dwc->irq_handle.reset(); |
| dwc3_disconnected(dwc); |
| dwc3_stop(dwc); |
| } else if (dwc->usb_mode == USB_MODE_HOST) { |
| if (dwc->xhci_dev) { |
| device_remove(dwc->xhci_dev); |
| dwc->xhci_dev = nullptr; |
| |
| if (mode == USB_MODE_PERIPHERAL) { |
| dwc->start_device_on_xhci_release = true; |
| return ZX_OK; |
| } |
| } |
| } |
| |
| dwc->start_device_on_xhci_release = false; |
| if (dwc->ums.ops != nullptr) { |
| status = usb_mode_switch_set_mode(&dwc->ums, mode); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| } |
| |
| if (mode == USB_MODE_PERIPHERAL) { |
| status = pdev_map_interrupt(&dwc->pdev, IRQ_USB3, dwc->irq_handle.reset_and_get_address()); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "dwc3_set_mode: pdev_map_interrupt failed\n"); |
| goto fail; |
| } |
| |
| dwc3_start_peripheral_mode(dwc); |
| } else if (mode == USB_MODE_HOST) { |
| dwc3_start_host_mode(dwc); |
| } |
| |
| dwc->usb_mode = mode; |
| return ZX_OK; |
| |
| fail: |
| if (dwc->ums.ops != nullptr) { |
| usb_mode_switch_set_mode(&dwc->ums, USB_MODE_NONE); |
| } |
| dwc->usb_mode = USB_MODE_NONE; |
| |
| return status; |
| } |
| |
| usb_mode_switch_protocol_ops_t dwc_ums_ops = { |
| .set_mode = dwc3_set_mode, |
| }; |
| |
| static void dwc3_unbind(void* ctx) { |
| auto* dwc = static_cast<dwc3_t*>(ctx); |
| dwc->irq_handle.destroy(); |
| thrd_join(dwc->irq_thread, nullptr); |
| device_remove(dwc->zxdev); |
| } |
| |
| static zx_status_t dwc3_get_protocol(void* ctx, uint32_t proto_id, void* out) { |
| switch (proto_id) { |
| case ZX_PROTOCOL_USB_DCI: { |
| auto proto = static_cast<usb_dci_protocol_t*>(out); |
| proto->ops = &dwc_dci_ops; |
| proto->ctx = ctx; |
| return ZX_OK; |
| } |
| case ZX_PROTOCOL_USB_MODE_SWITCH: { |
| auto proto = static_cast<usb_mode_switch_protocol_t*>(out); |
| proto->ops = &dwc_ums_ops; |
| proto->ctx = ctx; |
| return ZX_OK; |
| } |
| default: |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| } |
| |
| static void dwc3_release(void* ctx) { |
| auto* dwc = static_cast<dwc3_t*>(ctx); |
| |
| for (unsigned i = 0; i < countof(dwc->eps); i++) { |
| dwc3_ep_fifo_release(dwc, i); |
| } |
| io_buffer_release(&dwc->event_buffer); |
| io_buffer_release(&dwc->ep0_buffer); |
| delete dwc; |
| } |
| |
| static zx_protocol_device_t dwc3_device_ops = []() { |
| zx_protocol_device_t device; |
| device.version = DEVICE_OPS_VERSION; |
| device.get_protocol = dwc3_get_protocol; |
| device.release = dwc3_release; |
| return device; |
| }(); |
| |
| zx_status_t dwc3_bind(void* ctx, zx_device_t* parent) { |
| zxlogf(INFO, "dwc3_bind\n"); |
| |
| auto* dwc = new dwc3_t; |
| if (!dwc) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| zx_status_t status = device_get_protocol(parent, ZX_PROTOCOL_PDEV, &dwc->pdev); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| |
| // USB mode switch is optional, so ignore errors here. |
| status = device_get_protocol(parent, ZX_PROTOCOL_USB_MODE_SWITCH, &dwc->ums); |
| if (status != ZX_OK) { |
| dwc->ums.ops = nullptr; |
| } |
| |
| status = pdev_get_bti(&dwc->pdev, 0, dwc->bti_handle.reset_and_get_address()); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| |
| for (uint8_t i = 0; i < countof(dwc->eps); i++) { |
| dwc3_endpoint_t* ep = &dwc->eps[i]; |
| ep->ep_num = i; |
| list_initialize(&ep->queued_reqs); |
| } |
| dwc->parent = parent; |
| dwc->usb_mode = USB_MODE_NONE; |
| |
| mmio_buffer_t mmio; |
| status = pdev_map_mmio_buffer2(&dwc->pdev, MMIO_USB3OTG, ZX_CACHE_POLICY_UNCACHED_DEVICE, |
| &mmio); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "dwc3_bind: pdev_map_mmio_buffer failed\n"); |
| goto fail; |
| } |
| dwc->mmio = ddk::MmioBuffer(mmio); |
| |
| status = io_buffer_init(&dwc->event_buffer, dwc->bti_handle.get(), EVENT_BUFFER_SIZE, |
| IO_BUFFER_RO | IO_BUFFER_CONTIG); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "dwc3_bind: io_buffer_init failed\n"); |
| goto fail; |
| } |
| io_buffer_cache_flush(&dwc->event_buffer, 0, EVENT_BUFFER_SIZE); |
| |
| status = io_buffer_init(&dwc->ep0_buffer, dwc->bti_handle.get(), UINT16_MAX, |
| IO_BUFFER_RW | IO_BUFFER_CONTIG); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "dwc3_bind: io_buffer_init failed\n"); |
| goto fail; |
| } |
| |
| status = dwc3_ep0_init(dwc); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "dwc3_bind: dwc3_ep_init failed\n"); |
| goto fail; |
| } |
| |
| { |
| device_add_args_t args = {}; |
| args.version = DEVICE_ADD_ARGS_VERSION; |
| args.name = "dwc3"; |
| args.ctx = dwc; |
| args.ops = &dwc3_device_ops; |
| args.proto_id = ZX_PROTOCOL_USB_DCI; |
| args.proto_ops = &dwc_dci_ops, |
| |
| status = device_add(parent, &args, &dwc->zxdev); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| } |
| |
| return ZX_OK; |
| |
| fail: |
| zxlogf(ERROR, "dwc3_bind failed %d\n", status); |
| dwc3_release(dwc); |
| return status; |
| } |
| |
| static zx_driver_ops_t dwc3_driver_ops = [](){ |
| zx_driver_ops_t ops; |
| ops.version = DRIVER_OPS_VERSION; |
| ops.bind = dwc3_bind; |
| return ops; |
| }(); |
| |
| // clang-format off |
| ZIRCON_DRIVER_BEGIN(dwc3, dwc3_driver_ops, "zircon", "0.1", 3) |
| BI_ABORT_IF(NE, BIND_PLATFORM_DEV_VID, PDEV_VID_GENERIC), |
| BI_ABORT_IF(NE, BIND_PLATFORM_DEV_PID, PDEV_PID_GENERIC), |
| BI_MATCH_IF(EQ, BIND_PLATFORM_DEV_DID, PDEV_DID_USB_DWC3), |
| ZIRCON_DRIVER_END(dwc3) |
| // clang-format on |