diff options
author | Ian Moffett <ian@osmora.org> | 2024-04-03 00:29:47 -0400 |
---|---|---|
committer | Ian Moffett <ian@osmora.org> | 2024-04-03 00:29:47 -0400 |
commit | bebd07fc2ecf53d4e345d3eca68e76ce006b0b10 (patch) | |
tree | 12395037c7123b512e6151852b3b400e7039ef03 /sys/dev/usb/xhci.c | |
parent | 9147e8ff6a175867ec7bf3b67ed481886f457c91 (diff) |
kernel: usb: Add initial xHCI driver code
Signed-off-by: Ian Moffett <ian@osmora.org>
Diffstat (limited to 'sys/dev/usb/xhci.c')
-rw-r--r-- | sys/dev/usb/xhci.c | 384 |
1 files changed, 384 insertions, 0 deletions
diff --git a/sys/dev/usb/xhci.c b/sys/dev/usb/xhci.c new file mode 100644 index 0000000..d657b46 --- /dev/null +++ b/sys/dev/usb/xhci.c @@ -0,0 +1,384 @@ +/* + * 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/driver.h> +#include <dev/pci/pci.h> +#include <sys/syslog.h> +#include <sys/timer.h> +#include <dev/usb/xhciregs.h> +#include <dev/usb/xhcivar.h> +#include <vm/physseg.h> +#include <vm/dynalloc.h> +#include <vm/page.h> +#include <vm/vm.h> +#include <string.h> +#include <assert.h> + +__MODULE_NAME("xhci"); +__KERNEL_META("$Hyra$: xhci.c, Ian Marco Moffett, " + "xHCI driver"); + +static struct pci_device *hci_dev; +static struct timer driver_tmr; + +static inline uint32_t * +xhci_get_portsc(struct xhci_hc *hc, uint8_t portno) +{ + if (portno > hc->maxports) { + portno = hc->maxports; + } + return XHCI_BASE_OFF(hc->opregs, 0x400 + (0x10 * (portno - 1))); +} + +/* + * Set event ring segment table base + * address. + * + * @hc: Host controler descriptor. + * @pa: Physical address. + */ +static inline void +xhci_set_erst_base(struct xhci_hc *hc, uintptr_t pa) +{ + struct xhci_caps *caps = XHCI_CAPS(hc->base); + void *runtime_base = XHCI_RTS(hc->base, caps->rtsoff); + uintptr_t *erstba; + + /* + * The spec states that the Event Ring Segment Table + * Base Address register is at 'runtime_base + 0x30 + + * (32 * interrupter)'. See the xHCI spec, section 5.5.2.3.2 + */ + erstba = XHCI_BASE_OFF(runtime_base, 0x30); + *erstba = pa; +} + +/* + * Set up an xHCI event ring segment. + */ +static int +xhci_init_evring_segment(struct xhci_evring_segment *seg) +{ + size_t evring_size; + void *event_ring; + + evring_size = XHCI_TRB_SIZE * XHCI_EVRING_LEN; + event_ring = dynalloc_memalign(evring_size, 64); + + seg->base = VIRT_TO_PHYS(event_ring); + seg->size = XHCI_EVRING_LEN; + return 0; +} + +/* + * Submit a command by pushing a TRB to the + * command ring. + * + * @hc: Host controller descriptor. + * @trb: Transfer Request Block of command. + */ +static int +xhci_submit_cmd(struct xhci_hc *hc, struct xhci_trb trb) +{ + volatile uint32_t *cmd_db; + struct xhci_caps *caps = XHCI_CAPS(hc->base); + + /* Push the TRB to the command ring */ + cmd_db = XHCI_CMD_DB(hc->base, caps->dboff); + hc->cmd_ring[hc->cmd_ptr++] = trb; + + /* Wrap if needed */ + if (hc->cmd_ptr >= XHCI_CMDRING_LEN) { + hc->cmd_ptr = 0; + } + + /* Ring the command doorbell */ + *cmd_db = 0; + return 0; +} + +/* + * 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 = XHCI_BASE_OFF(hc->base, cap_ptr*4); + while (cap_ptr != 0) { + val = *p; + dword2 = *((uint32_t *)XHCI_BASE_OFF(p, 8)); + + /* Get the next cap */ + p = XHCI_BASE_OFF(p, XHCI_PROTO_NEXT(val) * 4); + cap_ptr = XHCI_PROTO_NEXT(val); + + if (XHCI_PROTO_ID(val) != XHCI_ECAP_PROTO) { + /* Not a Supported Protocol Capability */ + continue; + } + + if (hc->protocnt > XHCI_MAX_PROTOS) { + /* Too many protocols */ + break; + } + + 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); + } + + return 0; +} + +/* + * Set of xHCI scratchpad buffers. + */ +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, max_bufs; + uintptr_t buffer_array; + + max_bufs_lo = XHCI_MAX_SP_LO(caps->hcsparams2); + 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 need any + * scratchpad buffers so we can just return + * early. + */ + return 0; + } + + KINFO("HC requires %d max scratchpad buffers(s)\n", max_bufs); + buffer_array = vm_alloc_pageframe(max_bufs); + if (buffer_array == 0) { + KERR("Failed to allocate scratchpad buffer(s)\n"); + return -1; + } + + vm_zero_page(PHYS_TO_VIRT(buffer_array), max_bufs); + hc->dcbaap[0] = buffer_array; + return 0; +} + +/* + * Init USB ports on the root hub. + */ +static int +xhci_init_ports(struct xhci_hc *hc) +{ + struct xhci_caps *caps = XHCI_CAPS(hc->base); + size_t maxports = XHCI_MAXPORTS(caps->hcsparams1); + uint32_t *portsc; + + for (size_t i = 1; i < maxports; ++i) { + portsc = xhci_get_portsc(hc, i); + if (__TEST(*portsc, XHCI_PORTSC_CCS)) { + KINFO("Device connected on port %d, resetting...\n", i); + *portsc |= XHCI_PORTSC_PR; + } + } + + return 0; +} + +/* + * Poll USBSTS.HCH to be val + * + * Returns 0 on success. Non-zero values returned + * indicate a hang. + */ +static int +xhci_poll_hch(struct xhci_hc *hc, uint8_t val) +{ + struct xhci_opregs *opregs = hc->opregs; + size_t time_waiting = 0; + + while ((opregs->usbsts & USBSTS_HCH) != val) { + if (time_waiting >= XHCI_TIMEOUT) { + /* Hang */ + return -1; + } + + time_waiting += 50; + driver_tmr.msleep(50); + } + + return 0; +} + +/* + * Start up the host controller by setting + * the USBCMD run/stop bit. + */ +static int +xhci_start_hc(struct xhci_hc *hc) +{ + struct xhci_opregs *opregs = hc->opregs; + int status; + + opregs->usbcmd |= USBCMD_RUN; + + if ((status = xhci_poll_hch(hc, 0)) != 0) { + return status; + } + + return 0; +} + +static int +xhci_reset_hc(struct xhci_hc *hc) +{ + struct xhci_opregs *opregs = hc->opregs; + size_t time_waiting = 0; /* In ms */ + + KINFO("Resetting host controller...\n"); + + /* + * Set USBCMD.HCRST to reset the controller and + * wait for it to become zero. + */ + opregs->usbcmd |= USBCMD_HCRST; + while (1) { + if (!__TEST(opregs->usbcmd, USBCMD_HCRST)) { + /* Reset is complete */ + break; + } + if (time_waiting >= XHCI_TIMEOUT) { + KERR("Hang while polling USBCMD.HCRST to be zero\n"); + return -1; + } + driver_tmr.msleep(50); + time_waiting += 50; + } + return 0; +} + +static int +xhci_init_hc(struct xhci_hc *hc) +{ + struct xhci_caps *caps; + struct xhci_opregs *opregs; + size_t dcbaa_size, cmdring_size; + size_t evring_segment_sz; + + /* Get some information from the controller */ + caps = XHCI_CAPS(hc->base); + hc->caplen = caps->caplength; + hc->maxslots = XHCI_MAXSLOTS(caps->hcsparams1); + hc->maxports = XHCI_MAXSLOTS(caps->hcsparams1); + + opregs->config |= hc->maxslots; + + /* Fetch the opregs */ + opregs = XHCI_OPBASE(hc->base, hc->caplen); + hc->opregs = XHCI_OPBASE(hc->base, hc->caplen); + + if (xhci_reset_hc(hc) != 0) { + return -1; + } + + /* Set cmdring state */ + hc->cycle = 0; + hc->cmd_ptr = 0; + + /* Allocate and set DCBAA */ + dcbaa_size = sizeof(uintptr_t) * hc->maxslots; + hc->dcbaap = dynalloc_memalign(dcbaa_size, 0x1000); + __assert(hc->dcbaap != NULL); + opregs->dcbaa_ptr = VIRT_TO_PHYS(hc->dcbaap); + + xhci_init_scratchpads(hc); + cmdring_size = XHCI_TRB_SIZE * XHCI_CMDRING_LEN; + + /* Create command ring */ + hc->cmd_ring = dynalloc_memalign(cmdring_size, 0x1000); + __assert(hc->cmd_ring != NULL); + opregs->cmd_ring = VIRT_TO_PHYS(hc->cmd_ring); + + /* Allocate event ring segment */ + evring_segment_sz = sizeof(struct xhci_evring_segment); + hc->evring_seg = dynalloc_memalign(evring_segment_sz, 0x1000); + xhci_init_evring_segment(hc->evring_seg); + + /* Tell the HC about the event ring segment */ + __assert(hc->evring_seg != NULL); + xhci_set_erst_base(hc, VIRT_TO_PHYS(hc->evring_seg)); + + /* We're ready, start up the HC and ports */ + xhci_start_hc(hc); + xhci_parse_ecp(hc); + xhci_init_ports(hc); + return 0; +} + +static int +xhci_init(void) +{ + uintptr_t bar0, bar1, base; + struct xhci_hc hc; + struct pci_lookup hc_lookup = { + .pci_class = 0x0C, + .pci_subclass = 0x03 + }; + + /* Find the host controller on the bus */ + hci_dev = pci_get_device(hc_lookup, PCI_CLASS | PCI_SUBCLASS); + if (hci_dev == NULL) { + return -1; + } + + bar0 = hci_dev->bar[0] & ~7; + bar1 = hci_dev->bar[1] & ~7; + base = __COMBINE32(bar1, bar0); + hc.base = PHYS_TO_VIRT(base); + KINFO("xHCI HC base @ 0x%p\n", base); + + if (req_timer(TIMER_GP, &driver_tmr) != 0) { + KERR("Failed to fetch general purpose timer\n"); + return -1; + } + if (driver_tmr.msleep == NULL) { + KERR("Timer does not have msleep()\n"); + return -1; + } + + return xhci_init_hc(&hc); +} + +DRIVER_EXPORT(xhci_init); |