| // 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/protocol/platform-devices.h> |
| #include <ddk/protocol/usb-function.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_wait_bits(volatile uint32_t* ptr, uint32_t bits, uint32_t expected) { |
| uint32_t value = DWC3_READ32(ptr); |
| while ((value & bits) != expected) { |
| usleep(1000); |
| value = DWC3_READ32(ptr); |
| } |
| } |
| |
| void dwc3_print_status(dwc3_t* dwc) { |
| volatile void* mmio = dwc3_mmio(dwc); |
| uint32_t status = DWC3_READ32(mmio + DSTS); |
| dprintf(TRACE, "DSTS: "); |
| dprintf(TRACE, "USBLNKST: %d ", DSTS_USBLNKST(status)); |
| dprintf(TRACE, "SOFFN: %d ", DSTS_SOFFN(status)); |
| dprintf(TRACE, "CONNECTSPD: %d ", DSTS_CONNECTSPD(status)); |
| if (status & DSTS_DCNRD) dprintf(TRACE, "DCNRD "); |
| if (status & DSTS_SRE) dprintf(TRACE, "SRE "); |
| if (status & DSTS_RSS) dprintf(TRACE, "RSS "); |
| if (status & DSTS_SSS) dprintf(TRACE, "SSS "); |
| if (status & DSTS_COREIDLE) dprintf(TRACE, "COREIDLE "); |
| if (status & DSTS_DEVCTRLHLT) dprintf(TRACE, "DEVCTRLHLT "); |
| if (status & DSTS_RXFIFOEMPTY) dprintf(TRACE, "RXFIFOEMPTY "); |
| dprintf(TRACE, "\n"); |
| |
| status = DWC3_READ32(mmio + GSTS); |
| dprintf(TRACE, "GSTS: "); |
| dprintf(TRACE, "CBELT: %d ", GSTS_CBELT(status)); |
| dprintf(TRACE, "CURMOD: %d ", GSTS_CURMOD(status)); |
| if (status & GSTS_SSIC_IP) dprintf(TRACE, "SSIC_IP "); |
| if (status & GSTS_OTG_IP) dprintf(TRACE, "OTG_IP "); |
| if (status & GSTS_BC_IP) dprintf(TRACE, "BC_IP "); |
| if (status & GSTS_ADP_IP) dprintf(TRACE, "ADP_IP "); |
| if (status & GSTS_HOST_IP) dprintf(TRACE, "HOST_IP "); |
| if (status & GSTS_DEVICE_IP) dprintf(TRACE, "DEVICE_IP "); |
| if (status & GSTS_CSR_TIMEOUT) dprintf(TRACE, "CSR_TIMEOUT "); |
| if (status & GSTS_BUSERRADDRVLD) dprintf(TRACE, "BUSERRADDRVLD "); |
| dprintf(TRACE, "\n"); |
| } |
| |
| static zx_status_t dwc3_start(dwc3_t* dwc) { |
| volatile void* mmio = dwc3_mmio(dwc); |
| uint32_t temp; |
| |
| mtx_lock(&dwc->lock); |
| |
| temp = DWC3_READ32(mmio + DCTL); |
| temp &= ~DCTL_RUN_STOP; |
| temp |= DCTL_CSFTRST; |
| DWC3_WRITE32(mmio + DCTL, temp); |
| dwc3_wait_bits(mmio + DCTL, DCTL_CSFTRST, 0); |
| |
| // configure and enable PHYs |
| temp = DWC3_READ32(mmio + GUSB2PHYCFG(0)); |
| temp &= ~(GUSB2PHYCFG_USBTRDTIM_MASK | GUSB2PHYCFG_SUSPENDUSB20); |
| temp |= GUSB2PHYCFG_USBTRDTIM(9); |
| DWC3_WRITE32(mmio + GUSB2PHYCFG(0), temp); |
| |
| temp = DWC3_READ32(mmio + GUSB3PIPECTL(0)); |
| temp &= ~(GUSB3PIPECTL_DELAYP1TRANS | GUSB3PIPECTL_SUSPENDENABLE); |
| temp |= GUSB3PIPECTL_LFPSFILTER | GUSB3PIPECTL_SS_TX_DE_EMPHASIS(1); |
| DWC3_WRITE32(mmio + GUSB3PIPECTL(0), temp); |
| |
| // configure for device mode |
| DWC3_WRITE32(mmio + GCTL, GCTL_U2EXIT_LFPS | GCTL_PRTCAPDIR_DEVICE | GCTL_U2RSTECN | |
| GCTL_PWRDNSCALE(2)); |
| |
| temp = DWC3_READ32(mmio + DCFG); |
| uint32_t nump = 16; |
| uint32_t max_speed = DCFG_DEVSPD_SUPER; |
| temp &= ~DWC3_MASK(DCFG_NUMP_START, DCFG_NUMP_BITS); |
| temp |= nump << DCFG_NUMP_START; |
| temp &= ~DWC3_MASK(DCFG_DEVSPD_START, DCFG_DEVSPD_BITS); |
| temp |= max_speed << DCFG_DEVSPD_START; |
| temp &= ~DWC3_MASK(DCFG_DEVADDR_START, DCFG_DEVADDR_BITS); // clear address |
| DWC3_WRITE32(mmio + DCFG, temp); |
| |
| dwc3_events_start(dwc); |
| mtx_unlock(&dwc->lock); |
| |
| dwc3_ep0_start(dwc); |
| |
| mtx_lock(&dwc->lock); |
| |
| // start the controller |
| DWC3_WRITE32(mmio + DCTL, DCTL_RUN_STOP); |
| |
| mtx_unlock(&dwc->lock); |
| |
| return ZX_OK; |
| } |
| |
| void dwc3_usb_reset(dwc3_t* dwc) { |
| dprintf(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_set_connected(&dwc->dci_intf, true); |
| } |
| |
| void dwc3_disconnected(dwc3_t* dwc) { |
| dprintf(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_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) { |
| volatile void* mmio = dwc3_mmio(dwc); |
| |
| mtx_lock(&dwc->lock); |
| uint32_t status = DWC3_READ32(mmio + DSTS); |
| uint32_t speed = DSTS_CONNECTSPD(status); |
| unsigned 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: |
| dprintf(ERROR, "dwc3_connection_done: unsupported speed %u\n", speed); |
| dwc->speed = USB_SPEED_UNDEFINED; |
| break; |
| } |
| |
| mtx_unlock(&dwc->lock); |
| |
| 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_set_speed(&dwc->dci_intf, dwc->speed); |
| } |
| |
| void dwc3_set_address(dwc3_t* dwc, unsigned address) { |
| volatile void* mmio = dwc3_mmio(dwc); |
| mtx_lock(&dwc->lock); |
| DWC3_SET_BITS32(mmio + DCFG, DCFG_DEVADDR_START, DCFG_DEVADDR_BITS, address); |
| mtx_unlock(&dwc->lock); |
| } |
| |
| void dwc3_reset_configuration(dwc3_t* dwc) { |
| volatile void* mmio = dwc3_mmio(dwc); |
| |
| mtx_lock(&dwc->lock); |
| |
| // disable all endpoints except EP0_OUT and EP0_IN |
| DWC3_WRITE32(mmio + DALEPENA, (1 << EP0_OUT) | (1 << EP0_IN)); |
| |
| mtx_unlock(&dwc->lock); |
| |
| 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 zx_status_t dwc3_set_interface(void* ctx, usb_dci_interface_t* dci_intf) { |
| dwc3_t* dwc = ctx; |
| memcpy(&dwc->dci_intf, dci_intf, sizeof(dwc->dci_intf)); |
| return ZX_OK; |
| } |
| |
| static zx_status_t dwc3_config_ep(void* ctx, usb_endpoint_descriptor_t* ep_desc, |
| usb_ss_ep_comp_descriptor_t* ss_comp_desc) { |
| dwc3_t* dwc = ctx; |
| return dwc3_ep_config(dwc, ep_desc, ss_comp_desc); |
| } |
| |
| static zx_status_t dwc3_disable_ep(void* ctx, uint8_t ep_addr) { |
| dwc3_t* dwc = ctx; |
| return dwc3_ep_disable(dwc, ep_addr); |
| } |
| |
| static zx_status_t dwc_set_enabled(void* ctx, bool enabled) { |
| dwc3_t* dwc = ctx; |
| |
| if (enabled) { |
| return dwc3_start(dwc); |
| } else { |
| // TODO(voydanoff) more cleanup to do here? |
| dwc3_disconnected(dwc); |
| return ZX_OK; |
| } |
| } |
| |
| static zx_status_t dwc3_set_stall(void* ctx, uint8_t ep_address) { |
| dwc3_t* dwc = 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) { |
| dwc3_t* dwc = ctx; |
| return dwc3_ep_set_stall(dwc, dwc3_ep_num(ep_address), false); |
| } |
| |
| usb_dci_protocol_ops_t dwc_dci_protocol = { |
| .set_interface = dwc3_set_interface, |
| .config_ep = dwc3_config_ep, |
| .disable_ep = dwc3_disable_ep, |
| .set_enabled = dwc_set_enabled, |
| .ep_set_stall = dwc3_set_stall, |
| .ep_clear_stall = dwc3_clear_stall, |
| }; |
| |
| static void dwc3_unbind(void* ctx) { |
| dwc3_t* dwc = ctx; |
| |
| zx_interrupt_signal(dwc->irq_handle); |
| thrd_join(dwc->irq_thread, NULL); |
| device_remove(dwc->mxdev); |
| } |
| |
| static void dwc3_iotxn_queue(void* ctx, iotxn_t* txn) { |
| dwc3_t* dwc = ctx; |
| |
| if (txn->protocol != ZX_PROTOCOL_USB_FUNCTION) { |
| iotxn_complete(txn, ZX_ERR_NOT_SUPPORTED, 0); |
| return; |
| } |
| usb_function_protocol_data_t* data = iotxn_pdata(txn, usb_function_protocol_data_t); |
| dprintf(LTRACE, "dwc3_iotxn_queue ep: %u\n", data->ep_address); |
| unsigned ep_num = dwc3_ep_num(data->ep_address); |
| if (ep_num < 2 || ep_num >= countof(dwc->eps)) { |
| dprintf(ERROR, "dwc3_iotxn_queue: bad ep address 0x%02X\n", data->ep_address); |
| iotxn_complete(txn, ZX_ERR_INVALID_ARGS, 0); |
| return; |
| } |
| |
| dwc3_ep_queue(dwc, ep_num, txn); |
| } |
| |
| static void dwc3_release(void* ctx) { |
| dwc3_t* dwc = 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); |
| |
| pdev_mmio_buffer_release(&dwc->mmio); |
| zx_handle_close(dwc->irq_handle); |
| free(dwc); |
| } |
| |
| static zx_protocol_device_t dwc3_device_proto = { |
| .version = DEVICE_OPS_VERSION, |
| .iotxn_queue = dwc3_iotxn_queue, |
| .release = dwc3_release, |
| }; |
| |
| static zx_status_t dwc3_bind(void* ctx, zx_device_t* dev, void** cookie) { |
| dprintf(INFO, "dwc3_bind\n"); |
| |
| dwc3_t* dwc = calloc(1, sizeof(dwc3_t)); |
| if (!dwc) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| platform_device_protocol_t pdev; |
| zx_status_t status = device_get_protocol(dev, ZX_PROTOCOL_PLATFORM_DEV, &pdev); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| |
| mtx_init(&dwc->lock, mtx_plain); |
| for (unsigned i = 0; i < countof(dwc->eps); i++) { |
| dwc3_endpoint_t* ep = &dwc->eps[i]; |
| ep->ep_num = i; |
| mtx_init(&ep->lock, mtx_plain); |
| list_initialize(&ep->queued_txns); |
| } |
| |
| status = pdev_map_mmio_buffer(&pdev, MMIO_USB3OTG, ZX_CACHE_POLICY_UNCACHED_DEVICE, |
| &dwc->mmio); |
| if (status != ZX_OK) { |
| dprintf(ERROR, "dwc3_bind: pdev_map_mmio_buffer failed\n"); |
| goto fail; |
| } |
| |
| status = pdev_map_interrupt(&pdev, IRQ_USB3, &dwc->irq_handle); |
| if (status != ZX_OK) { |
| dprintf(ERROR, "dwc3_bind: pdev_map_interrupt failed\n"); |
| goto fail; |
| } |
| |
| status = io_buffer_init(&dwc->event_buffer, EVENT_BUFFER_SIZE, IO_BUFFER_RO); |
| if (status != ZX_OK) { |
| dprintf(ERROR, "dwc3_bind: io_buffer_init failed\n"); |
| goto fail; |
| } |
| io_buffer_cache_op(&dwc->event_buffer, ZX_VMO_OP_CACHE_CLEAN, 0, EVENT_BUFFER_SIZE); |
| |
| status = io_buffer_init(&dwc->ep0_buffer, 65536, IO_BUFFER_RW); |
| if (status != ZX_OK) { |
| dprintf(ERROR, "dwc3_bind: io_buffer_init failed\n"); |
| goto fail; |
| } |
| |
| status = dwc3_ep0_init(dwc); |
| if (status != ZX_OK) { |
| dprintf(ERROR, "dwc3_bind: dwc3_ep_init failed\n"); |
| goto fail; |
| } |
| |
| device_add_args_t args = { |
| .version = DEVICE_ADD_ARGS_VERSION, |
| .name = "dwc3", |
| .ctx = dwc, |
| .ops = &dwc3_device_proto, |
| .proto_id = ZX_PROTOCOL_USB_DCI, |
| .proto_ops = &dwc_dci_protocol, |
| }; |
| |
| status = device_add(dev, &args, &dwc->mxdev); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| |
| return ZX_OK; |
| |
| fail: |
| dprintf(ERROR, "dwc3_bind failed %d\n", status); |
| dwc3_release(dwc); |
| return status; |
| } |
| |
| static zx_driver_ops_t dwc3_driver_ops = { |
| .version = DRIVER_OPS_VERSION, |
| .bind = dwc3_bind, |
| }; |
| |
| // The formatter does not play nice with these macros. |
| // 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 |