diff options
author | Ian Moffett <ian@osmora.org> | 2024-12-20 01:10:29 -0500 |
---|---|---|
committer | Ian Moffett <ian@osmora.org> | 2024-12-20 01:16:44 -0500 |
commit | bdbf3af5b918c8a2a930927f8d146db1645aa785 (patch) | |
tree | 76578bab26284f61e5c9cbad21d18641b372334f /sys/dev/usb | |
parent | a3c9b47cc63f67a09abd098ef25a7e5827f0c185 (diff) |
kernel: xhci: Add initial xhci/usb3 driver impl
Signed-off-by: Ian Moffett <ian@osmora.org>
Diffstat (limited to 'sys/dev/usb')
-rw-r--r-- | sys/dev/usb/xhci.c | 448 |
1 files changed, 448 insertions, 0 deletions
diff --git a/sys/dev/usb/xhci.c b/sys/dev/usb/xhci.c new file mode 100644 index 0000000..50ddac6 --- /dev/null +++ b/sys/dev/usb/xhci.c @@ -0,0 +1,448 @@ +/* + * Copyright (c) 2023-2024 Ian Marco Moffett and the Osmora Team. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of Hyra nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include <sys/types.h> +#include <sys/param.h> +#include <sys/driver.h> +#include <sys/errno.h> +#include <sys/syslog.h> +#include <sys/mmio.h> +#include <dev/timer.h> +#include <dev/usb/xhciregs.h> +#include <dev/usb/xhcivar.h> +#include <dev/pci/pci.h> +#include <vm/physmem.h> +#include <vm/dynalloc.h> +#include <assert.h> +#include <string.h> + +#define pr_trace(fmt, ...) kprintf("xhci: " fmt, ##__VA_ARGS__) +#define pr_error(...) pr_trace(__VA_ARGS__) + +/* Debug macro */ +#if defined(XHCI_DEBUG) +#define pr_debug(...) pr_trace(__VA_ARGS__) +#else +#define pr_debug(...) __nothing +#endif /* XHCI_DEBUG */ + +static struct pci_device *hci_dev; +static struct timer tmr; + +/* + * Get port status and control register for a specific + * port. + */ +static inline uint32_t * +xhci_get_portsc(struct xhci_hc *hc, uint8_t portno) +{ + if (portno > hc->maxports) { + portno = hc->maxports; + } + + return PTR_OFFSET(hc->opregs, 0x400 + (0x10 * (portno - 1))); +} + +static int +xhci_poll32(volatile uint32_t *reg, uint32_t bits, bool pollset) +{ + size_t usec_start, usec; + size_t elapsed_msec; + uint32_t val; + bool tmp; + + usec_start = tmr.get_time_usec(); + + for (;;) { + val = mmio_read32(reg); + tmp = (pollset) ? ISSET(val, bits) : !ISSET(val, bits); + + usec = tmr.get_time_usec(); + elapsed_msec = (usec - usec_start) / 1000; + + /* If tmp is set, the register updated in time */ + if (tmp) { + break; + } + + /* Exit with an error if we timeout */ + if (elapsed_msec > XHCI_TIMEOUT) { + return -ETIME; + } + } + + return val; +} + +/* + * Parse xHCI extended caps. + */ +static int +xhci_parse_ecp(struct xhci_hc *hc) +{ + struct xhci_caps *caps = XHCI_CAPS(hc->base); + struct xhci_proto *proto; + uint32_t *p, val, dword2; + uint32_t cap_ptr = XHCI_ECP(caps->hccparams1); + + p = PTR_OFFSET(hc->base, cap_ptr * 4); + while (cap_ptr != 0) { + val = mmio_read32(p); + dword2 = *((uint32_t *)PTR_OFFSET(p, 8)); + + /* Get the next cap */ + p = PTR_OFFSET(p, XHCI_PROTO_NEXT(val) * 4); + cap_ptr = XHCI_PROTO_NEXT(val); + + switch (XHCI_PROTO_ID(val)) { + case XHCI_ECAP_PROTO: + if (hc->protocnt > XHCI_MAX_PROTOS) { + /* Too many protocols */ + return 0; + } + + proto = &hc->protos[hc->protocnt++]; + proto->major = XHCI_PROTO_MAJOR(val); + proto->port_count = XHCI_PROTO_PORTCNT(dword2); + proto->port_start = XHCI_PROTO_PORTOFF(dword2); + + pr_trace("USB %d port start: %d\n", + proto->major, proto->port_start); + pr_trace("USB %d port count: %d\n", + proto->major, proto->port_count); + break; + case XHCI_ECAP_USBLEGSUP: + /* Begin xHC BIOS handoff to us */ + pr_trace("Establishing xHC ownership...\n"); + val |= XHCI_OS_SEM; + mmio_write32(p, val); + + /* Ensure the xHC responded correctly */ + if (xhci_poll32(p, XHCI_OS_SEM, 1) < 0) + return -EIO; + if (xhci_poll32(p, XHCI_BIOS_SEM, 0) < 0) + return -EIO; + + break; + } + } + return 0; +} + +/* + * Init set of scratchpad buffers for the + * xHC. + */ +static int +xhci_init_scratchpads(struct xhci_hc *hc) +{ + struct xhci_caps *caps = XHCI_CAPS(hc->base); + uint16_t max_bufs_lo, max_bufs_hi; + uint16_t max_bufs; + uintptr_t *bufarr, tmp; + + max_bufs_lo = XHCI_MAX_SP_LO(caps->hcsparams1); + max_bufs_hi = XHCI_MAX_SP_HI(caps->hcsparams2); + max_bufs = (max_bufs_hi << 5) | max_bufs_lo; + if (max_bufs == 0) { + /* + * Some emulators like QEMU don't require any + * scratchpad buffers. + */ + return 0; + } + + pr_trace("Using %d pages for xHC scratchpads\n"); + bufarr = dynalloc_memalign(sizeof(uintptr_t)*max_bufs, 0x1000); + if (bufarr == NULL) { + pr_error("Failed to allocate scratchpad buffer array\n"); + return -1; + } + + for (size_t i = 0; i < max_bufs; ++i) { + tmp = vm_alloc_frame(1); + memset(PHYS_TO_VIRT(tmp), 0, 0x1000); + if (tmp == 0) { + /* TODO: Shutdown, free memory */ + pr_error("Failed to fill scratchpad buffer array\n"); + return -1; + } + bufarr[i] = tmp; + } + + hc->dcbaap[0] = VIRT_TO_PHYS(bufarr); + return 0; +} + +/* + * Allocate device context base array. + */ +static uintptr_t +xhci_alloc_dcbaa(struct xhci_hc *hc) +{ + size_t size; + + size = sizeof(uintptr_t) * hc->maxslots; + hc->dcbaap = dynalloc_memalign(size, 0x1000); + __assert(hc->dcbaap != NULL); + return VIRT_TO_PHYS(hc->dcbaap); +} + +/* + * Sets up the event ring. + */ +static void +xhci_init_evring(struct xhci_hc *hc) +{ + struct xhci_caps *caps = XHCI_CAPS(hc->base); + struct xhci_evring_segment *seg; + uint64_t *erdp, *erstba; + uint32_t *erst_size; + void *runtime = XHCI_RTS(hc->base, caps->rtsoff); + size_t size; + + size = XHCI_EVRING_LEN * XHCI_TRB_SIZE; + seg = dynalloc_memalign(size, 64); + memset(seg, 0, size); + + /* Set the size of the event ring segment table */ + erst_size = PTR_OFFSET(runtime, 0x28); + mmio_write16(erst_size, 1); + + /* Setup the event ring segment */ + memset(seg, 0, size); + seg->base = VIRT_TO_PHYS(seg); + seg->size = XHCI_EVRING_LEN; + + /* Setup the event ring dequeue pointer */ + erdp = PTR_OFFSET(runtime, 0x38); + mmio_write64(erdp, seg->base); + + /* Point ERSTBA to our event ring segment */ + erstba = PTR_OFFSET(runtime, 0x30); + mmio_write64(erstba, VIRT_TO_PHYS(seg)); + hc->evring = PHYS_TO_VIRT(seg->base); +} + +/* + * Allocate command ring and sets 'hc->cmdring' + * to the virtual address. + * + * Returns the physical address. + */ +static uintptr_t +xhci_alloc_cmdring(struct xhci_hc *hc) +{ + size_t size; + + size = XHCI_TRB_SIZE * XHCI_CMDRING_LEN; + hc->cmdring = dynalloc_memalign(size, 0x1000); + __assert(hc->cmdring != NULL); + return VIRT_TO_PHYS(hc->cmdring); +} + +/* + * Perform an xHC reset and return 0 on + * success. + */ +static int +xhci_reset(struct xhci_hc *hc) +{ + struct xhci_opregs *opregs = hc->opregs; + uint32_t usbcmd; + int error; + + /* Make sure a reset isn't already in progress */ + usbcmd = mmio_read32(&opregs->usbcmd); + if (ISSET(usbcmd, USBCMD_HCRST)) + return -EBUSY; + + usbcmd |= USBCMD_HCRST; + mmio_write32(&opregs->usbcmd, usbcmd); + + /* Wait until xHC finishes */ + error = xhci_poll32(&opregs->usbcmd, USBCMD_HCRST, false); + if (error < 0) { + pr_error("xhci_reset: xHC reset timeout\n"); + return error; + } + + return 0; +} + +/* + * Start up the host controller and put it into + * a running state. Returns 0 on success. + */ +static int +xhci_start_hc(struct xhci_hc *hc) +{ + struct xhci_opregs *opregs = hc->opregs; + uint32_t usbcmd; + + /* Don't start up if we are already running */ + usbcmd = mmio_read32(&opregs->usbcmd); + if (ISSET(usbcmd, USBCMD_RUN)) + return -EBUSY; + + usbcmd |= USBCMD_RUN; + mmio_write32(&opregs->usbcmd, usbcmd); + return 0; +} + +static int +xhci_init_ports(struct xhci_hc *hc) +{ + const char *devtype; + uint32_t *portsc_p; + uint32_t portsc; + + for (size_t i = 1; i < hc->maxports; ++i) { + portsc_p = xhci_get_portsc(hc, i); + portsc = mmio_read32(portsc_p); + + /* + * If the current connect status of a port is + * set, we know we have some sort of device + * connected to it. + */ + if (ISSET(portsc, XHCI_PORTSC_CCS)) { + devtype = (ISSET(portsc, XHCI_PORTSC_DR)) + ? "removable" : "non-removable"; + + pr_trace("Detected %s USB device on port %d\n", devtype, i); + pr_trace("Resetting port %d...\n", i); + portsc |= XHCI_PORTSC_PR; + mmio_write32(portsc_p, portsc); + } + } + + return 0; +} + +/* + * Initialize the xHCI controller. + */ +static int +xhci_init_hc(struct xhci_hc *hc) +{ + int error; + uint8_t caplength; + uint32_t config; + uintptr_t dcbaap, cmdring; + struct xhci_caps *caps; + struct xhci_opregs *opregs; + + caps = (struct xhci_caps *)hc->base; + caplength = mmio_read8(&caps->caplength); + opregs = PTR_OFFSET(caps, caplength); + + hc->caps = caps; + hc->opregs = opregs; + + /* + * If the Operational Base is not dword aligned then + * we can assume that perhaps the controller is faulty + * and giving bogus values. + */ + if (!PTR_ALIGNED(hc->opregs, 4)) { + pr_error("xhci_init_hc: fatal: Got bad operational base\n"); + return -1; + } + + pr_trace("Resetting xHC chip...\n"); + if ((error = xhci_reset(hc)) != 0) { + return error; + } + + hc->maxslots = XHCI_MAXSLOTS(caps->hcsparams1); + hc->maxports = XHCI_MAXPORTS(caps->hcsparams1); + + /* Set CONFIG.MaxSlotsEn to all of them */ + config = mmio_read32(&opregs->config); + config |= hc->maxslots; + mmio_write32(&opregs->config, config); + + /* Set device context base array */ + dcbaap = xhci_alloc_dcbaa(hc); + mmio_write64(&opregs->dcbaa_ptr, dcbaap); + + /* Try to set up scratchpad buffer array */ + if ((error = xhci_init_scratchpads(hc)) != 0) { + return error; + } + + /* Setup the command ring */ + cmdring = xhci_alloc_cmdring(hc); + mmio_write64(&opregs->cmd_ring, cmdring); + hc->cr_cycle = 1; + + xhci_init_evring(hc); + xhci_parse_ecp(hc); + xhci_start_hc(hc); + xhci_init_ports(hc); + return 0; +} + +static int +xhci_init(void) +{ + int error; + struct xhci_hc xhc = {0}; + struct pci_lookup lookup = { + .pci_class = 0x0C, + .pci_subclass = 0x03 + }; + + /* Find the host controller on the bus */ + hci_dev = pci_get_device(lookup, PCI_CLASS | PCI_SUBCLASS); + if (hci_dev == NULL) { + return -1; + } + + if ((error = pci_map_bar(hci_dev, 0, (void *)&xhc.base)) != 0) { + return error; + } + + /* Try to request a general purpose timer */ + if (req_timer(TIMER_GP, &tmr) != TMRR_SUCCESS) { + pr_error("Failed to fetch general purpose timer\n"); + return -ENODEV; + } + + /* Ensure it has get_time_usec() */ + if (tmr.get_time_usec == NULL) { + pr_error("General purpose timer has no get_time_usec()\n"); + return -ENODEV; + } + + return xhci_init_hc(&xhc); +} + +DRIVER_EXPORT(xhci_init); |