blob: adceeeb04fe2c29a6f429e99028a0db9a5792a3a [file] [log] [blame]
// 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 "src/devices/usb/drivers/dwc3/dwc3.h"
#include <lib/ddk/binding_driver.h>
#include <lib/fit/defer.h>
#include <lib/zx/clock.h>
#include <fbl/auto_lock.h>
#include <usb/usb.h>
#include "src/devices/usb/drivers/dwc3/dwc3-regs.h"
namespace dwc3 {
zx_status_t Dwc3::Create(void* ctx, zx_device_t* parent) {
auto dev = std::make_unique<Dwc3>(parent);
if (zx_status_t status = dev->AcquirePDevResources(); status != ZX_OK) {
zxlogf(ERROR, "Dwc3 Create failed (%s)", zx_status_get_string(status));
return status;
}
if (zx_status_t status = dev->DdkAdd("dwc3"); status != ZX_OK) {
zxlogf(ERROR, "DdkAdd failed: %s", zx_status_get_string(status));
return status;
}
// devmgr is now in charge of the device.
[[maybe_unused]] auto* _ = dev.release();
return ZX_OK;
}
zx_status_t Dwc3::AcquirePDevResources() {
zx::result pdev_result = ddk::PDevFidl::Create(parent());
if (pdev_result.is_error()) {
zxlogf(ERROR, "could not get pdev %s", pdev_result.status_string());
return pdev_result.error_value();
}
pdev_ = std::move(pdev_result.value());
if (zx_status_t status = pdev_.MapMmio(0, &mmio_); status != ZX_OK) {
zxlogf(ERROR, "MapMmio failed: %s", zx_status_get_string(status));
return status;
}
if (zx_status_t status = pdev_.GetBti(0, &bti_); status != ZX_OK) {
zxlogf(ERROR, "GetBti failed: %s", zx_status_get_string(status));
return status;
}
if (zx_status_t status = pdev_.GetInterrupt(0, 0, &irq_); status != ZX_OK) {
zxlogf(ERROR, "GetInterrupt failed: %s", zx_status_get_string(status));
return status;
}
if (zx_status_t status = zx::port::create(ZX_PORT_BIND_TO_INTERRUPT, &irq_port_);
status != ZX_OK) {
zxlogf(ERROR, "zx::port::create failed: %s", zx_status_get_string(status));
return status;
}
if (zx_status_t status = irq_.bind(irq_port_, 0, ZX_INTERRUPT_BIND); status != ZX_OK) {
zxlogf(ERROR, "irq bind to port failed: %s", zx_status_get_string(status));
return status;
}
irq_bound_to_port_ = true;
return ZX_OK;
}
zx_status_t Dwc3::Init() {
// Start by identifying our hardware and making sure that we recognize it, and
// it is a version that we know we can support. Then, reset the hardware so
// that we know it is in a good state.
uint32_t ep_count{0};
{
fbl::AutoLock lock(&lock_);
// Now that we have our registers, check to make sure that we are running on
// a version of the hardware that we support.
if (zx_status_t status = CheckHwVersion(); status != ZX_OK) {
zxlogf(ERROR, "CheckHwVersion failed: %s", zx_status_get_string(status));
return status;
}
// Now that we have our registers, reset the hardware. This will ensure that
// we are starting from a known state moving forward.
if (zx_status_t status = ResetHw(); status != ZX_OK) {
zxlogf(ERROR, "HW Reset Failed: %s", zx_status_get_string(status));
return status;
}
// Finally, figure out the number of endpoints that this version of the
// controller supports.
ep_count = GHWPARAMS3::Get().ReadFrom(get_mmio()).DWC_USB31_NUM_EPS();
}
if (ep_count < (kUserEndpointStartNum + 1)) {
zxlogf(ERROR, "HW supports only %u physical endpoints, but at least %u are needed to operate.",
ep_count, (kUserEndpointStartNum + 1));
return ZX_ERR_NOT_SUPPORTED;
}
// Now go ahead and allocate the user endpoint storage.
user_endpoints_.Init(ep_count - kUserEndpointStartNum);
// Now that we have our BTI, and have reset our hardware, we can go ahead and
// release the quarantine on any pages which may have been previously pinned
// by this BTI.
if (zx_status_t status = bti_.release_quarantine(); status != ZX_OK) {
zxlogf(ERROR, "Release quarantine failed: %s", zx_status_get_string(status));
return status;
}
// If something goes wrong after this point, make sure to release any of our
// allocated IoBuffers.
auto cleanup = fit::defer([this]() { ReleaseResources(); });
// Strictly speaking, we should not need RW access to this buffer.
// Unfortunately, attempting to writeback and invalidate the cache before
// reading anything from the buffer produces a page fault right if this buffer
// is mapped read only, so for now, we keep the buffer mapped RW.
if (zx_status_t status =
event_buffer_.Init(bti_.get(), kEventBufferSize, IO_BUFFER_RW | IO_BUFFER_CONTIG);
status != ZX_OK) {
zxlogf(ERROR, "event_buffer init failed: %s", zx_status_get_string(status));
return status;
}
event_buffer_.CacheFlushInvalidate(0, kEventBufferSize);
// Now that we have allocated our event buffer, we have at least one region
// pinned. We need to be sure to place the hardware into reset before
// unpinning the memory during shutdown.
has_pinned_memory_ = true;
{
fbl::AutoLock lock(&ep0_.lock);
if (zx_status_t status =
ep0_.buffer.Init(bti_.get(), UINT16_MAX, IO_BUFFER_RW | IO_BUFFER_CONTIG);
status != ZX_OK) {
zxlogf(ERROR, "ep0_buffer init failed: %s", zx_status_get_string(status));
return status;
}
}
if (zx_status_t status = Ep0Init(); status != ZX_OK) {
zxlogf(ERROR, "Ep0Init init failed: %s", zx_status_get_string(status));
return status;
}
StartPeripheralMode();
// Start the interrupt thread.
auto irq_thunk = +[](void* arg) -> int { return static_cast<Dwc3*>(arg)->IrqThread(); };
if (int rc = thrd_create_with_name(&irq_thread_, irq_thunk, static_cast<void*>(this),
"dwc3-interrupt-thread");
rc != thrd_success) {
return ZX_ERR_INTERNAL;
}
irq_thread_started_.store(true);
// Things went well. Cancel our cleanup routine.
cleanup.cancel();
return ZX_OK;
}
void Dwc3::ReleaseResources() {
// The IRQ thread had better not be running at this point.
ZX_ASSERT(!irq_thread_started_.load());
// Unbind the interrupt from the interrupt port.
if (irq_bound_to_port_) {
irq_.bind(irq_port_, 0, ZX_INTERRUPT_UNBIND);
irq_bound_to_port_ = false;
}
{
fbl::AutoLock lock(&lock_);
// If we managed to get our registers mapped, place the device into reset so
// we are certain that there is no DMA going on in the background.
if (mmio_.has_value()) {
if (zx_status_t status = ResetHw(); status != ZX_OK) {
// Deliberately panic and terminate this driver if we fail to place the
// hardware into reset at this point and we have any pinned memory.. We do this
// deliberately because, if we cannot put the hardware into reset, it may still be accessing
// pages we previously pinned using DMA. If we are on a system with no
// IOMMU, deliberately terminating the process will ensure that our
// pinned pages are quarantined instead of being returned to the page
// pool.
if (has_pinned_memory_) {
zxlogf(ERROR,
"Failed to place HW into reset during shutdown (%s), self-terminating in order to "
"ensure quarantine",
zx_status_get_string(status));
ZX_ASSERT(false);
}
}
}
}
// Now go ahead and release any buffers we may have pinned.
{
fbl::AutoLock lock(&ep0_.lock);
ep0_.out.enabled = false;
ep0_.in.enabled = false;
ep0_.buffer.release();
ep0_.shared_fifo.Release();
}
for (UserEndpoint& uep : user_endpoints_) {
fbl::AutoLock lock(&uep.lock);
uep.fifo.Release();
uep.ep.enabled = false;
}
event_buffer_.release();
has_pinned_memory_ = false;
}
zx_status_t Dwc3::CheckHwVersion() {
auto* mmio = get_mmio();
const uint32_t ip_version = USB31_VER_NUMBER::Get().ReadFrom(mmio).IPVERSION();
auto is_ascii_digit = [](char val) -> bool { return (val >= '0') && (val <= '9'); };
auto is_ascii_letter = [](char val) -> bool {
return ((val >= 'A') && (val <= 'Z')) || ((val >= 'a') && (val <= 'z'));
};
const char c1 = static_cast<char>((ip_version >> 24) & 0xFF);
const char c2 = static_cast<char>((ip_version >> 16) & 0xFF);
const char c3 = static_cast<char>((ip_version >> 8) & 0xFF);
const char c4 = static_cast<char>(ip_version & 0xFF);
// Format defined by section 1.3.44 of the DWC3 Programming Guide
if (!is_ascii_digit(c1) || !is_ascii_digit(c2) || !is_ascii_digit(c3) ||
(!is_ascii_letter(c4) && (c4 != '*'))) {
zxlogf(ERROR, "Unrecognized USB IP Version 0x%08x", ip_version);
return ZX_ERR_NOT_SUPPORTED;
}
const int major = c1 - '0';
const int minor = ((c2 - '0') * 10) + (c3 - '0');
if (major != 1) {
zxlogf(ERROR, "Unsupported USB IP Version %d.%02d%c", major, minor, c4);
return ZX_ERR_NOT_SUPPORTED;
}
zxlogf(INFO, "Detected DWC3 IP version %d.%02d%c", major, minor, c4);
return ZX_OK;
}
zx_status_t Dwc3::ResetHw() {
auto* mmio = get_mmio();
// Clear the run/stop bit and request a software reset.
DCTL::Get().ReadFrom(mmio).set_RUN_STOP(0).set_CSFTRST(1).WriteTo(mmio);
// HW will clear the software reset bit when it is finished with the reset
// process.
zx::time start = zx::clock::get_monotonic();
while (DCTL::Get().ReadFrom(mmio).CSFTRST()) {
if ((zx::clock::get_monotonic() - start) >= kHwResetTimeout) {
return ZX_ERR_TIMED_OUT;
}
usleep(1000);
}
return ZX_OK;
}
void Dwc3::SetDeviceAddress(uint32_t address) {
auto* mmio = get_mmio();
DCFG::Get().ReadFrom(mmio).set_DEVADDR(address).WriteTo(mmio);
}
void Dwc3::StartPeripheralMode() {
{
fbl::AutoLock lock(&lock_);
auto* mmio = get_mmio();
// configure and enable PHYs
GUSB2PHYCFG::Get(0)
.ReadFrom(mmio)
.set_USBTRDTIM(9) // USB2.0 Turn-around time == 9 phy clocks
.set_ULPIAUTORES(0) // No auto resume
.WriteTo(mmio);
GUSB3PIPECTL::Get(0)
.ReadFrom(mmio)
.set_DELAYP1TRANS(0)
.set_SUSPENDENABLE(0)
.set_LFPSFILTER(1)
.set_SS_TX_DE_EMPHASIS(1)
.WriteTo(mmio);
// TODO(johngro): This is the number of receive buffers. Why do we set it to 16?
constexpr uint32_t nump = 16;
DCFG::Get()
.ReadFrom(mmio)
.set_NUMP(nump) // number of receive buffers
.set_DEVSPD(DCFG::DEVSPD_SUPER) // max speed is 5Gbps USB3.1
.set_DEVADDR(0) // device address is 0
.WriteTo(mmio);
// Program the location of the event buffer, then enable event delivery.
StartEvents();
}
Ep0Start();
{
// Set the run/stop bit to start the controller
fbl::AutoLock lock(&lock_);
auto* mmio = get_mmio();
DCTL::Get().FromValue(0).set_RUN_STOP(1).WriteTo(mmio);
}
}
void Dwc3::ResetConfiguration() {
{
fbl::AutoLock lock(&lock_);
auto* mmio = get_mmio();
// disable all endpoints except EP0_OUT and EP0_IN
DALEPENA::Get().FromValue(0).EnableEp(kEp0Out).EnableEp(kEp0In).WriteTo(mmio);
}
for (UserEndpoint& uep : user_endpoints_) {
fbl::AutoLock lock(&uep.lock);
EpEndTransfers(uep.ep, ZX_ERR_IO_NOT_PRESENT);
EpSetStall(uep.ep, false);
}
}
void Dwc3::HandleResetEvent() {
zxlogf(INFO, "Dwc3::HandleResetEvent");
Ep0Reset();
for (UserEndpoint& uep : user_endpoints_) {
fbl::AutoLock lock(&uep.lock);
EpEndTransfers(uep.ep, ZX_ERR_IO_NOT_PRESENT);
EpSetStall(uep.ep, false);
}
{
fbl::AutoLock lock(&lock_);
SetDeviceAddress(0);
}
Ep0Start();
{
fbl::AutoLock lock(&dci_lock_);
if (dci_intf_) {
dci_intf_->SetConnected(true);
}
}
}
void Dwc3::HandleConnectionDoneEvent() {
uint16_t ep0_max_packet = 0;
usb_speed_t new_speed = USB_SPEED_UNDEFINED;
{
fbl::AutoLock lock(&lock_);
auto* mmio = get_mmio();
uint32_t speed = DSTS::Get().ReadFrom(mmio).CONNECTSPD();
switch (speed) {
case DSTS::CONNECTSPD_HIGH:
new_speed = USB_SPEED_HIGH;
ep0_max_packet = 64;
break;
case DSTS::CONNECTSPD_FULL:
new_speed = USB_SPEED_FULL;
ep0_max_packet = 64;
break;
case DSTS::CONNECTSPD_SUPER:
new_speed = USB_SPEED_SUPER;
ep0_max_packet = 512;
break;
case DSTS::CONNECTSPD_ENHANCED_SUPER:
new_speed = USB_SPEED_ENHANCED_SUPER;
ep0_max_packet = 512;
break;
default:
zxlogf(ERROR, "unsupported speed %u", speed);
break;
}
new_speed = USB_SPEED_UNDEFINED;
}
if (ep0_max_packet) {
fbl::AutoLock lock(&ep0_.lock);
std::array eps{&ep0_.out, &ep0_.in};
for (Endpoint* ep : eps) {
ep->type = USB_ENDPOINT_CONTROL;
ep->interval = 0;
ep->max_packet_size = ep0_max_packet;
CmdEpSetConfig(*ep, true);
}
ep0_.cur_speed = new_speed;
}
{
fbl::AutoLock lock(&dci_lock_);
if (dci_intf_) {
dci_intf_->SetSpeed(new_speed);
}
}
}
void Dwc3::HandleDisconnectedEvent() {
zxlogf(INFO, "Dwc3::HandleDisconnectedEvent");
{
fbl::AutoLock ep0_lock(&ep0_.lock);
CmdEpEndTransfer(ep0_.out);
ep0_.state = Ep0::State::None;
}
{
fbl::AutoLock lock(&dci_lock_);
if (dci_intf_) {
dci_intf_->SetConnected(false);
}
}
for (UserEndpoint& uep : user_endpoints_) {
fbl::AutoLock lock(&uep.lock);
EpEndTransfers(uep.ep, ZX_ERR_IO_NOT_PRESENT);
EpSetStall(uep.ep, false);
}
}
void Dwc3::DdkInit(ddk::InitTxn txn) {
if (zx_status_t status = Init(); status != ZX_OK) {
txn.Reply(status);
} else {
zxlogf(INFO, "Dwc3 Init Succeeded");
txn.Reply(ZX_OK);
}
}
void Dwc3::DdkUnbind(ddk::UnbindTxn txn) {
{
fbl::AutoLock lock(&dci_lock_);
dci_intf_.reset();
}
if (irq_thread_started_.load()) {
zx_status_t status = SignalIrqThread(IrqSignal::Exit);
// if we can't signal the thread, we are not going to be able to shut down
// and we should just terminate the process instead.
ZX_ASSERT(status == ZX_OK);
thrd_join(irq_thread_, nullptr);
irq_thread_started_.store(false);
}
txn.Reply();
}
void Dwc3::DdkRelease() {
ReleaseResources();
delete this;
}
void Dwc3::UsbDciRequestQueue(usb_request_t* usb_req, const usb_request_complete_callback_t* cb) {
Request req{usb_req, *cb, sizeof(*usb_req)};
zx_status_t queue_result = [&]() {
const uint8_t ep_num = UsbAddressToEpNum(req.request()->header.ep_address);
UserEndpoint* const uep = get_user_endpoint(ep_num);
if (uep == nullptr) {
zxlogf(ERROR, "Dwc3::UsbDciRequestQueue: bad ep address 0x%02X", ep_num);
return ZX_ERR_INVALID_ARGS;
}
const zx_off_t length = req.request()->header.length;
zxlogf(SERIAL, "UsbDciRequestQueue ep %u length %zu", ep_num, length);
{
fbl::AutoLock lock(&uep->lock);
if (!uep->ep.enabled) {
zxlogf(ERROR, "Dwc3: ep(%u) not enabled!", ep_num);
return ZX_ERR_BAD_STATE;
}
// OUT transactions must have length > 0 and multiple of max packet size
if (uep->ep.IsOutput()) {
if (length == 0 || ((length % uep->ep.max_packet_size) != 0)) {
zxlogf(ERROR, "Dwc3: OUT transfers must be multiple of max packet size (len %ld mps %hu)",
length, uep->ep.max_packet_size);
return ZX_ERR_INVALID_ARGS;
}
}
// Add the request to our queue of pending requests. Then, if we are
// configured, kick the queue to make sure it is running. Do not fail the
// request! In particular, during the set interface callback to the CDC
// driver, the driver will attempt to queue a request. We are (at this
// point in time) not _technically_ configured yet. We declare ourselves
// to be configured only after our call into the CDC client succeeds. So,
// if we fail requests because we are not yet configured yet (as the dwc2
// driver does), we are just going to end up in the infinite recursion or
// deadlock traps described below.
uep->ep.queued_reqs.push(std::move(req));
if (configured_) {
UserEpQueueNext(*uep);
}
return ZX_OK;
}
}();
if (queue_result != ZX_OK) {
ZX_DEBUG_ASSERT(false);
req.request()->response.status = queue_result;
req.request()->response.actual = 0;
pending_completions_.push(std::move(req));
if (zx_status_t status = SignalIrqThread(IrqSignal::Wakeup); status != ZX_OK) {
zxlogf(DEBUG, "Failed to signal IRQ thread %s", zx_status_get_string(status));
}
}
}
zx_status_t Dwc3::UsbDciSetInterface(const usb_dci_interface_protocol_t* interface) {
fbl::AutoLock lock(&dci_lock_);
if (dci_intf_) {
zxlogf(ERROR, "Dwc3: DCI Interface already set");
return ZX_ERR_BAD_STATE;
}
dci_intf_ = ddk::UsbDciInterfaceProtocolClient(interface);
return ZX_OK;
}
zx_status_t Dwc3::UsbDciConfigEp(const usb_endpoint_descriptor_t* ep_desc,
const usb_ss_ep_comp_descriptor_t* ss_comp_desc) {
const uint8_t ep_num = UsbAddressToEpNum(ep_desc->b_endpoint_address);
UserEndpoint* const uep = get_user_endpoint(ep_num);
if (uep == nullptr) {
return ZX_ERR_INVALID_ARGS;
}
uint8_t ep_type = usb_ep_type(ep_desc);
if (ep_type == USB_ENDPOINT_ISOCHRONOUS) {
zxlogf(ERROR, "isochronous endpoints are not supported");
return ZX_ERR_NOT_SUPPORTED;
}
fbl::AutoLock lock(&uep->lock);
if (zx_status_t status = uep->fifo.Init(bti_); status != ZX_OK) {
zxlogf(ERROR, "fifo init failed %s", zx_status_get_string(status));
return status;
}
uep->ep.max_packet_size = usb_ep_max_packet(ep_desc);
uep->ep.type = ep_type;
uep->ep.interval = ep_desc->b_interval;
// TODO(voydanoff) USB3 support
uep->ep.enabled = true;
// TODO(johngro): What protects this configured_ state from a locking/threading perspective?
if (configured_) {
UserEpQueueNext(*uep);
}
return ZX_OK;
}
zx_status_t Dwc3::UsbDciDisableEp(uint8_t ep_address) {
const uint8_t ep_num = UsbAddressToEpNum(ep_address);
UserEndpoint* const uep = get_user_endpoint(ep_num);
if (uep == nullptr) {
return ZX_ERR_INVALID_ARGS;
}
RequestQueue to_complete;
{
fbl::AutoLock lock(&uep->lock);
to_complete = UserEpCancelAllLocked(*uep);
uep->fifo.Release();
uep->ep.enabled = false;
}
to_complete.CompleteAll(ZX_ERR_IO_NOT_PRESENT, 0);
return ZX_OK;
}
zx_status_t Dwc3::UsbDciEpSetStall(uint8_t ep_address) {
const uint8_t ep_num = UsbAddressToEpNum(ep_address);
UserEndpoint* const uep = get_user_endpoint(ep_num);
if (uep == nullptr) {
return ZX_ERR_INVALID_ARGS;
}
fbl::AutoLock lock(&uep->lock);
return EpSetStall(uep->ep, true);
}
zx_status_t Dwc3::UsbDciEpClearStall(uint8_t ep_address) {
const uint8_t ep_num = UsbAddressToEpNum(ep_address);
UserEndpoint* const uep = get_user_endpoint(ep_num);
if (uep == nullptr) {
return ZX_ERR_INVALID_ARGS;
}
fbl::AutoLock lock(&uep->lock);
return EpSetStall(uep->ep, false);
}
size_t Dwc3::UsbDciGetRequestSize() { return Request::RequestSize(sizeof(usb_request_t)); }
zx_status_t Dwc3::UsbDciCancelAll(uint8_t ep_address) {
const uint8_t ep_num = UsbAddressToEpNum(ep_address);
UserEndpoint* const uep = get_user_endpoint(ep_num);
if (uep == nullptr) {
return ZX_ERR_INVALID_ARGS;
}
return UserEpCancelAll(*uep);
}
static constexpr zx_driver_ops_t driver_ops = []() {
zx_driver_ops_t ops = {};
ops.version = DRIVER_OPS_VERSION;
ops.bind = Dwc3::Create;
return ops;
}();
} // namespace dwc3
ZIRCON_DRIVER(dwc3, dwc3::driver_ops, "zircon", "0.1");