summaryrefslogtreecommitdiff
path: root/sys/dev
diff options
context:
space:
mode:
authorIan Moffett <ian@osmora.org>2024-04-03 00:29:47 -0400
committerIan Moffett <ian@osmora.org>2024-04-03 00:29:47 -0400
commitbebd07fc2ecf53d4e345d3eca68e76ce006b0b10 (patch)
tree12395037c7123b512e6151852b3b400e7039ef03 /sys/dev
parent9147e8ff6a175867ec7bf3b67ed481886f457c91 (diff)
kernel: usb: Add initial xHCI driver code
Signed-off-by: Ian Moffett <ian@osmora.org>
Diffstat (limited to 'sys/dev')
-rw-r--r--sys/dev/usb/xhci.c384
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);