summaryrefslogtreecommitdiffstats
path: root/grub-core/bus/usb/uhci.c
diff options
context:
space:
mode:
Diffstat (limited to 'grub-core/bus/usb/uhci.c')
-rw-r--r--grub-core/bus/usb/uhci.c871
1 files changed, 871 insertions, 0 deletions
diff --git a/grub-core/bus/usb/uhci.c b/grub-core/bus/usb/uhci.c
new file mode 100644
index 0000000..7c5811f
--- /dev/null
+++ b/grub-core/bus/usb/uhci.c
@@ -0,0 +1,871 @@
+/* uhci.c - UHCI Support. */
+/*
+ * GRUB -- GRand Unified Bootloader
+ * Copyright (C) 2008 Free Software Foundation, Inc.
+ *
+ * GRUB is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GRUB is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with GRUB. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <grub/dl.h>
+#include <grub/mm.h>
+#include <grub/misc.h>
+#include <grub/usb.h>
+#include <grub/usbtrans.h>
+#include <grub/pci.h>
+#include <grub/cpu/io.h>
+#include <grub/time.h>
+#include <grub/cpu/pci.h>
+#include <grub/disk.h>
+
+GRUB_MOD_LICENSE ("GPLv3+");
+
+#define GRUB_UHCI_IOMASK (0x7FF << 5)
+
+#define N_QH 256
+#define N_TD 640
+
+typedef enum
+ {
+ GRUB_UHCI_REG_USBCMD = 0x00,
+ GRUB_UHCI_REG_USBINTR = 0x04,
+ GRUB_UHCI_REG_FLBASEADD = 0x08,
+ GRUB_UHCI_REG_PORTSC1 = 0x10,
+ GRUB_UHCI_REG_PORTSC2 = 0x12,
+ GRUB_UHCI_REG_USBLEGSUP = 0xc0
+ } grub_uhci_reg_t;
+
+enum
+ {
+ GRUB_UHCI_DETECT_CHANGED = (1 << 1),
+ GRUB_UHCI_DETECT_HAVE_DEVICE = 1,
+ GRUB_UHCI_DETECT_LOW_SPEED = (1 << 8)
+ };
+
+/* R/WC legacy support bits */
+enum
+ {
+ GRUB_UHCI_LEGSUP_END_A20GATE = (1 << 15),
+ GRUB_UHCI_TRAP_BY_64H_WSTAT = (1 << 11),
+ GRUB_UHCI_TRAP_BY_64H_RSTAT = (1 << 10),
+ GRUB_UHCI_TRAP_BY_60H_WSTAT = (1 << 9),
+ GRUB_UHCI_TRAP_BY_60H_RSTAT = (1 << 8)
+ };
+
+/* Reset all legacy support - clear all R/WC bits and all R/W bits */
+#define GRUB_UHCI_RESET_LEGSUP_SMI ( GRUB_UHCI_LEGSUP_END_A20GATE \
+ | GRUB_UHCI_TRAP_BY_64H_WSTAT \
+ | GRUB_UHCI_TRAP_BY_64H_RSTAT \
+ | GRUB_UHCI_TRAP_BY_60H_WSTAT \
+ | GRUB_UHCI_TRAP_BY_60H_RSTAT )
+
+/* Some UHCI commands */
+#define GRUB_UHCI_CMD_RUN_STOP (1 << 0)
+#define GRUB_UHCI_CMD_HCRESET (1 << 1)
+#define GRUB_UHCI_CMD_MAXP (1 << 7)
+
+/* Important bits in structures */
+#define GRUB_UHCI_LINK_TERMINATE 1
+#define GRUB_UHCI_LINK_QUEUE_HEAD 2
+
+enum
+ {
+ GRUB_UHCI_REG_PORTSC_CONNECT_CHANGED = 0x0002,
+ GRUB_UHCI_REG_PORTSC_PORT_ENABLED = 0x0004,
+ GRUB_UHCI_REG_PORTSC_RESUME = 0x0040,
+ GRUB_UHCI_REG_PORTSC_RESET = 0x0200,
+ GRUB_UHCI_REG_PORTSC_SUSPEND = 0x1000,
+ GRUB_UHCI_REG_PORTSC_RW = GRUB_UHCI_REG_PORTSC_PORT_ENABLED
+ | GRUB_UHCI_REG_PORTSC_RESUME | GRUB_UHCI_REG_PORTSC_RESET
+ | GRUB_UHCI_REG_PORTSC_SUSPEND,
+ /* These bits should not be written as 1 unless we really need it */
+ GRUB_UHCI_PORTSC_RWC = ((1 << 1) | (1 << 3) | (1 << 11) | (3 << 13))
+ };
+
+/* UHCI Queue Head. */
+struct grub_uhci_qh
+{
+ /* Queue head link pointer which points to the next queue head. */
+ grub_uint32_t linkptr;
+
+ /* Queue element link pointer which points to the first data object
+ within the queue. */
+ grub_uint32_t elinkptr;
+
+ /* Queue heads are aligned on 16 bytes, pad so a queue head is 16
+ bytes so we can store many in a 4K page. */
+ grub_uint8_t pad[8];
+} GRUB_PACKED;
+
+/* UHCI Transfer Descriptor. */
+struct grub_uhci_td
+{
+ /* Pointer to the next TD in the list. */
+ grub_uint32_t linkptr;
+
+ /* Control and status bits. */
+ grub_uint32_t ctrl_status;
+
+ /* All information required to transfer the Token packet. */
+ grub_uint32_t token;
+
+ /* A pointer to the data buffer, UHCI requires this pointer to be 32
+ bits. */
+ grub_uint32_t buffer;
+
+ /* Another linkptr that is not overwritten by the Host Controller.
+ This is GRUB specific. */
+ grub_uint32_t linkptr2;
+
+ /* 3 additional 32 bits words reserved for the Host Controller Driver. */
+ grub_uint32_t data[3];
+} GRUB_PACKED;
+
+typedef volatile struct grub_uhci_td *grub_uhci_td_t;
+typedef volatile struct grub_uhci_qh *grub_uhci_qh_t;
+
+struct grub_uhci
+{
+ grub_port_t iobase;
+ volatile grub_uint32_t *framelist_virt;
+ grub_uint32_t framelist_phys;
+ struct grub_pci_dma_chunk *framelist_chunk;
+
+ /* N_QH Queue Heads. */
+ struct grub_pci_dma_chunk *qh_chunk;
+ volatile grub_uhci_qh_t qh_virt;
+ grub_uint32_t qh_phys;
+
+ /* N_TD Transfer Descriptors. */
+ struct grub_pci_dma_chunk *td_chunk;
+ volatile grub_uhci_td_t td_virt;
+ grub_uint32_t td_phys;
+
+ /* Free Transfer Descriptors. */
+ grub_uhci_td_t tdfree;
+
+ int qh_busy[N_QH];
+
+ struct grub_uhci *next;
+};
+
+static struct grub_uhci *uhci;
+
+static grub_uint16_t
+grub_uhci_readreg16 (struct grub_uhci *u, grub_uhci_reg_t reg)
+{
+ return grub_inw (u->iobase + reg);
+}
+
+#if 0
+static grub_uint32_t
+grub_uhci_readreg32 (struct grub_uhci *u, grub_uhci_reg_t reg)
+{
+ return grub_inl (u->iobase + reg);
+}
+#endif
+
+static void
+grub_uhci_writereg16 (struct grub_uhci *u,
+ grub_uhci_reg_t reg, grub_uint16_t val)
+{
+ grub_outw (val, u->iobase + reg);
+}
+
+static void
+grub_uhci_writereg32 (struct grub_uhci *u,
+ grub_uhci_reg_t reg, grub_uint32_t val)
+{
+ grub_outl (val, u->iobase + reg);
+}
+
+/* Iterate over all PCI devices. Determine if a device is an UHCI
+ controller. If this is the case, initialize it. */
+static int
+grub_uhci_pci_iter (grub_pci_device_t dev,
+ grub_pci_id_t pciid __attribute__((unused)),
+ void *data __attribute__ ((unused)))
+{
+ grub_uint32_t class_code;
+ grub_uint32_t class;
+ grub_uint32_t subclass;
+ grub_uint32_t interf;
+ grub_uint32_t base;
+ grub_uint32_t fp;
+ grub_pci_address_t addr;
+ struct grub_uhci *u;
+ int i;
+
+ addr = grub_pci_make_address (dev, GRUB_PCI_REG_CLASS);
+ class_code = grub_pci_read (addr) >> 8;
+
+ interf = class_code & 0xFF;
+ subclass = (class_code >> 8) & 0xFF;
+ class = class_code >> 16;
+
+ /* If this is not an UHCI controller, just return. */
+ if (class != 0x0c || subclass != 0x03 || interf != 0x00)
+ return 0;
+
+ /* Determine IO base address. */
+ addr = grub_pci_make_address (dev, GRUB_PCI_REG_ADDRESS_REG4);
+ base = grub_pci_read (addr);
+ /* Stop if there is no IO space base address defined. */
+ if ((base & GRUB_PCI_ADDR_SPACE_MASK) != GRUB_PCI_ADDR_SPACE_IO)
+ return 0;
+
+ if ((base & GRUB_UHCI_IOMASK) == 0)
+ return 0;
+
+ /* Set bus master - needed for coreboot or broken BIOSes */
+ addr = grub_pci_make_address (dev, GRUB_PCI_REG_COMMAND);
+ grub_pci_write_word(addr, GRUB_PCI_COMMAND_IO_ENABLED
+ | GRUB_PCI_COMMAND_BUS_MASTER
+ | GRUB_PCI_COMMAND_MEM_ENABLED
+ | grub_pci_read_word (addr));
+
+ grub_dprintf ("uhci", "base = %x\n", base);
+
+ /* Allocate memory for the controller and register it. */
+ u = grub_zalloc (sizeof (*u));
+ if (! u)
+ return 1;
+
+ u->iobase = (base & GRUB_UHCI_IOMASK) + GRUB_MACHINE_PCI_IO_BASE;
+
+ /* Reset PIRQ and SMI */
+ addr = grub_pci_make_address (dev, GRUB_UHCI_REG_USBLEGSUP);
+ grub_pci_write_word(addr, GRUB_UHCI_RESET_LEGSUP_SMI);
+ /* Reset the HC */
+ grub_uhci_writereg16(u, GRUB_UHCI_REG_USBCMD, GRUB_UHCI_CMD_HCRESET);
+ grub_millisleep(5);
+ /* Disable interrupts and commands (just to be safe) */
+ grub_uhci_writereg16(u, GRUB_UHCI_REG_USBINTR, 0);
+ /* Finish HC reset, HC remains disabled */
+ grub_uhci_writereg16(u, GRUB_UHCI_REG_USBCMD, 0);
+ /* Read back to be sure PCI write is done */
+ grub_uhci_readreg16(u, GRUB_UHCI_REG_USBCMD);
+
+ /* Reserve a page for the frame list. */
+ u->framelist_chunk = grub_memalign_dma32 (4096, 4096);
+ if (! u->framelist_chunk)
+ goto fail;
+ u->framelist_virt = grub_dma_get_virt (u->framelist_chunk);
+ u->framelist_phys = grub_dma_get_phys (u->framelist_chunk);
+
+ grub_dprintf ("uhci",
+ "class=0x%02x 0x%02x interface 0x%02x base=0x%x framelist=%p\n",
+ class, subclass, interf, u->iobase, u->framelist_virt);
+
+ /* The QH pointer of UHCI is only 32 bits, make sure this
+ code works on on 64 bits architectures. */
+ u->qh_chunk = grub_memalign_dma32 (4096, sizeof(struct grub_uhci_qh) * N_QH);
+ if (! u->qh_chunk)
+ goto fail;
+ u->qh_virt = grub_dma_get_virt (u->qh_chunk);
+ u->qh_phys = grub_dma_get_phys (u->qh_chunk);
+
+ /* The TD pointer of UHCI is only 32 bits, make sure this
+ code works on on 64 bits architectures. */
+ u->td_chunk = grub_memalign_dma32 (4096, sizeof(struct grub_uhci_td) * N_TD);
+ if (! u->td_chunk)
+ goto fail;
+ u->td_virt = grub_dma_get_virt (u->td_chunk);
+ u->td_phys = grub_dma_get_phys (u->td_chunk);
+
+ grub_dprintf ("uhci", "QH=%p, TD=%p\n",
+ u->qh_virt, u->td_virt);
+
+ /* Link all Transfer Descriptors in a list of available Transfer
+ Descriptors. */
+ for (i = 0; i < N_TD; i++)
+ u->td_virt[i].linkptr = u->td_phys + (i + 1) * sizeof(struct grub_uhci_td);
+ u->td_virt[N_TD - 2].linkptr = 0;
+ u->tdfree = u->td_virt;
+
+ /* Setup the frame list pointers. Since no isochronous transfers
+ are and will be supported, they all point to the (same!) queue
+ head. */
+ fp = u->qh_phys & (~15);
+ /* Mark this as a queue head. */
+ fp |= 2;
+ for (i = 0; i < 1024; i++)
+ u->framelist_virt[i] = fp;
+ /* Program the framelist address into the UHCI controller. */
+ grub_uhci_writereg32 (u, GRUB_UHCI_REG_FLBASEADD, u->framelist_phys);
+
+ /* Make the Queue Heads point to each other. */
+ for (i = 0; i < N_QH; i++)
+ {
+ /* Point to the next QH. */
+ u->qh_virt[i].linkptr = ((u->qh_phys
+ + (i + 1) * sizeof(struct grub_uhci_qh))
+ & (~15));
+
+ /* This is a QH. */
+ u->qh_virt[i].linkptr |= GRUB_UHCI_LINK_QUEUE_HEAD;
+
+ /* For the moment, do not point to a Transfer Descriptor. These
+ are set at transfer time, so just terminate it. */
+ u->qh_virt[i].elinkptr = 1;
+ }
+
+ /* The last Queue Head should terminate. */
+ u->qh_virt[N_QH - 1].linkptr = 1;
+
+ /* Enable UHCI again. */
+ grub_uhci_writereg16 (u, GRUB_UHCI_REG_USBCMD,
+ GRUB_UHCI_CMD_RUN_STOP | GRUB_UHCI_CMD_MAXP);
+
+ /* UHCI is initialized and ready for transfers. */
+ grub_dprintf ("uhci", "UHCI initialized\n");
+
+
+#if 0
+ {
+ int i;
+ for (i = 0; i < 10; i++)
+ {
+ grub_uint16_t frnum;
+
+ frnum = grub_uhci_readreg16 (u, 6);
+ grub_dprintf ("uhci", "Framenum=%d\n", frnum);
+ grub_millisleep (100);
+ }
+ }
+#endif
+
+ /* Link to uhci now that initialisation is successful. */
+ u->next = uhci;
+ uhci = u;
+
+ return 0;
+
+ fail:
+ if (u)
+ {
+ grub_dma_free (u->qh_chunk);
+ grub_dma_free (u->framelist_chunk);
+ }
+ grub_free (u);
+
+ return 1;
+}
+
+static void
+grub_uhci_inithw (void)
+{
+ grub_pci_iterate (grub_uhci_pci_iter, NULL);
+}
+
+static grub_uhci_td_t
+grub_alloc_td (struct grub_uhci *u)
+{
+ grub_uhci_td_t ret;
+
+ /* Check if there is a Transfer Descriptor available. */
+ if (! u->tdfree)
+ return NULL;
+
+ ret = u->tdfree;
+ u->tdfree = grub_dma_phys2virt (u->tdfree->linkptr, u->td_chunk);
+
+ return ret;
+}
+
+static void
+grub_free_td (struct grub_uhci *u, grub_uhci_td_t td)
+{
+ td->linkptr = grub_dma_virt2phys (u->tdfree, u->td_chunk);
+ u->tdfree = td;
+}
+
+static void
+grub_free_queue (struct grub_uhci *u, grub_uhci_qh_t qh, grub_uhci_td_t td,
+ grub_usb_transfer_t transfer, grub_size_t *actual)
+{
+ int i; /* Index of TD in transfer */
+
+ u->qh_busy[qh - u->qh_virt] = 0;
+
+ *actual = 0;
+
+ /* Free the TDs in this queue and set last_trans. */
+ for (i=0; td; i++)
+ {
+ grub_uhci_td_t tdprev;
+
+ grub_dprintf ("uhci", "Freeing %p\n", td);
+ /* Check state of TD and possibly set last_trans */
+ if (transfer && (td->linkptr & 1))
+ transfer->last_trans = i;
+
+ *actual += (td->ctrl_status + 1) & 0x7ff;
+
+ /* Unlink the queue. */
+ tdprev = td;
+ if (!td->linkptr2)
+ td = 0;
+ else
+ td = grub_dma_phys2virt (td->linkptr2, u->td_chunk);
+
+ /* Free the TD. */
+ grub_free_td (u, tdprev);
+ }
+}
+
+static grub_uhci_qh_t
+grub_alloc_qh (struct grub_uhci *u,
+ grub_transaction_type_t tr __attribute__((unused)))
+{
+ int i;
+ grub_uhci_qh_t qh;
+
+ /* Look for a Queue Head for this transfer. Skip the first QH if
+ this is a Interrupt Transfer. */
+#if 0
+ if (tr == GRUB_USB_TRANSACTION_TYPE_INTERRUPT)
+ i = 0;
+ else
+#endif
+ i = 1;
+
+ for (; i < N_QH; i++)
+ {
+ if (!u->qh_busy[i])
+ break;
+ }
+ qh = &u->qh_virt[i];
+ if (i == N_QH)
+ {
+ grub_error (GRUB_ERR_OUT_OF_MEMORY,
+ "no free queue heads available");
+ return NULL;
+ }
+
+ u->qh_busy[qh - u->qh_virt] = 1;
+
+ return qh;
+}
+
+static grub_uhci_td_t
+grub_uhci_transaction (struct grub_uhci *u, unsigned int endp,
+ grub_transfer_type_t type, unsigned int addr,
+ unsigned int toggle, grub_size_t size,
+ grub_uint32_t data, grub_usb_speed_t speed)
+{
+ grub_uhci_td_t td;
+ static const unsigned int tf[] = { 0x69, 0xE1, 0x2D };
+
+ /* XXX: Check if data is <4GB. If it isn't, just copy stuff around.
+ This is only relevant for 64 bits architectures. */
+
+ /* Grab a free Transfer Descriptor and initialize it. */
+ td = grub_alloc_td (u);
+ if (! td)
+ {
+ grub_error (GRUB_ERR_OUT_OF_MEMORY,
+ "no transfer descriptors available for UHCI transfer");
+ return 0;
+ }
+
+ grub_dprintf ("uhci",
+ "transaction: endp=%d, type=%d, addr=%d, toggle=%d, size=%lu data=0x%x td=%p\n",
+ endp, type, addr, toggle, (unsigned long) size, data, td);
+
+ /* Don't point to any TD, just terminate. */
+ td->linkptr = 1;
+
+ /* Active! Only retry a transfer 3 times. */
+ td->ctrl_status = (1 << 23) | (3 << 27) |
+ ((speed == GRUB_USB_SPEED_LOW) ? (1 << 26) : 0);
+
+ /* If zero bytes are transmitted, size is 0x7FF. Otherwise size is
+ size-1. */
+ if (size == 0)
+ size = 0x7FF;
+ else
+ size = size - 1;
+
+ /* Setup whatever is required for the token packet. */
+ td->token = ((size << 21) | (toggle << 19) | (endp << 15)
+ | (addr << 8) | tf[type]);
+
+ td->buffer = data;
+
+ return td;
+}
+
+struct grub_uhci_transfer_controller_data
+{
+ grub_uhci_qh_t qh;
+ grub_uhci_td_t td_first;
+};
+
+static grub_usb_err_t
+grub_uhci_setup_transfer (grub_usb_controller_t dev,
+ grub_usb_transfer_t transfer)
+{
+ struct grub_uhci *u = (struct grub_uhci *) dev->data;
+ grub_uhci_td_t td;
+ grub_uhci_td_t td_prev = NULL;
+ int i;
+ struct grub_uhci_transfer_controller_data *cdata;
+
+ cdata = grub_malloc (sizeof (*cdata));
+ if (!cdata)
+ return GRUB_USB_ERR_INTERNAL;
+
+ cdata->td_first = NULL;
+
+ /* Allocate a queue head for the transfer queue. */
+ cdata->qh = grub_alloc_qh (u, GRUB_USB_TRANSACTION_TYPE_CONTROL);
+ if (! cdata->qh)
+ {
+ grub_free (cdata);
+ return GRUB_USB_ERR_INTERNAL;
+ }
+
+ grub_dprintf ("uhci", "transfer, iobase:%08x\n", u->iobase);
+
+ for (i = 0; i < transfer->transcnt; i++)
+ {
+ grub_usb_transaction_t tr = &transfer->transactions[i];
+
+ td = grub_uhci_transaction (u, transfer->endpoint & 15, tr->pid,
+ transfer->devaddr, tr->toggle,
+ tr->size, tr->data,
+ transfer->dev->speed);
+ if (! td)
+ {
+ grub_size_t actual = 0;
+ /* Terminate and free. */
+ if (td_prev)
+ {
+ td_prev->linkptr2 = 0;
+ td_prev->linkptr = 1;
+ }
+
+ if (cdata->td_first)
+ grub_free_queue (u, cdata->qh, cdata->td_first, NULL, &actual);
+
+ grub_free (cdata);
+ return GRUB_USB_ERR_INTERNAL;
+ }
+
+ if (! cdata->td_first)
+ cdata->td_first = td;
+ else
+ {
+ td_prev->linkptr2 = grub_dma_virt2phys (td, u->td_chunk);
+ td_prev->linkptr = grub_dma_virt2phys (td, u->td_chunk);
+ td_prev->linkptr |= 4;
+ }
+ td_prev = td;
+ }
+ td_prev->linkptr2 = 0;
+ td_prev->linkptr = 1;
+
+ grub_dprintf ("uhci", "setup transaction %d\n", transfer->type);
+
+ /* Link it into the queue and terminate. Now the transaction can
+ take place. */
+ cdata->qh->elinkptr = grub_dma_virt2phys (cdata->td_first, u->td_chunk);
+
+ grub_dprintf ("uhci", "initiate transaction\n");
+
+ transfer->controller_data = cdata;
+
+ return GRUB_USB_ERR_NONE;
+}
+
+static grub_usb_err_t
+grub_uhci_check_transfer (grub_usb_controller_t dev,
+ grub_usb_transfer_t transfer,
+ grub_size_t *actual)
+{
+ struct grub_uhci *u = (struct grub_uhci *) dev->data;
+ grub_uhci_td_t errtd;
+ struct grub_uhci_transfer_controller_data *cdata = transfer->controller_data;
+
+ *actual = 0;
+
+ if (cdata->qh->elinkptr & ~0x0f)
+ errtd = grub_dma_phys2virt (cdata->qh->elinkptr & ~0x0f, u->qh_chunk);
+ else
+ errtd = 0;
+
+ if (errtd)
+ {
+ grub_dprintf ("uhci", ">t status=0x%02x data=0x%02x td=%p, %x\n",
+ errtd->ctrl_status, errtd->buffer & (~15), errtd,
+ cdata->qh->elinkptr);
+ }
+
+ /* Check if the transaction completed. */
+ if (cdata->qh->elinkptr & 1)
+ {
+ grub_dprintf ("uhci", "transaction complete\n");
+
+ /* Place the QH back in the free list and deallocate the associated
+ TDs. */
+ cdata->qh->elinkptr = 1;
+ grub_free_queue (u, cdata->qh, cdata->td_first, transfer, actual);
+ grub_free (cdata);
+ return GRUB_USB_ERR_NONE;
+ }
+
+ if (errtd && !(errtd->ctrl_status & (1 << 23)))
+ {
+ grub_usb_err_t err = GRUB_USB_ERR_NONE;
+
+ /* Check if the endpoint is stalled. */
+ if (errtd->ctrl_status & (1 << 22))
+ err = GRUB_USB_ERR_STALL;
+
+ /* Check if an error related to the data buffer occurred. */
+ else if (errtd->ctrl_status & (1 << 21))
+ err = GRUB_USB_ERR_DATA;
+
+ /* Check if a babble error occurred. */
+ else if (errtd->ctrl_status & (1 << 20))
+ err = GRUB_USB_ERR_BABBLE;
+
+ /* Check if a NAK occurred. */
+ else if (errtd->ctrl_status & (1 << 19))
+ err = GRUB_USB_ERR_NAK;
+
+ /* Check if a timeout occurred. */
+ else if (errtd->ctrl_status & (1 << 18))
+ err = GRUB_USB_ERR_TIMEOUT;
+
+ /* Check if a bitstuff error occurred. */
+ else if (errtd->ctrl_status & (1 << 17))
+ err = GRUB_USB_ERR_BITSTUFF;
+
+ if (err)
+ {
+ grub_dprintf ("uhci", "transaction failed\n");
+
+ /* Place the QH back in the free list and deallocate the associated
+ TDs. */
+ cdata->qh->elinkptr = 1;
+ grub_free_queue (u, cdata->qh, cdata->td_first, transfer, actual);
+ grub_free (cdata);
+
+ return err;
+ }
+ }
+
+ /* Fall through, no errors occurred, so the QH might be
+ updated. */
+ grub_dprintf ("uhci", "transaction fallthrough\n");
+
+ return GRUB_USB_ERR_WAIT;
+}
+
+static grub_usb_err_t
+grub_uhci_cancel_transfer (grub_usb_controller_t dev,
+ grub_usb_transfer_t transfer)
+{
+ struct grub_uhci *u = (struct grub_uhci *) dev->data;
+ grub_size_t actual;
+ struct grub_uhci_transfer_controller_data *cdata = transfer->controller_data;
+
+ grub_dprintf ("uhci", "transaction cancel\n");
+
+ /* Place the QH back in the free list and deallocate the associated
+ TDs. */
+ cdata->qh->elinkptr = 1;
+ grub_free_queue (u, cdata->qh, cdata->td_first, transfer, &actual);
+ grub_free (cdata);
+
+ return GRUB_USB_ERR_NONE;
+}
+
+static int
+grub_uhci_iterate (grub_usb_controller_iterate_hook_t hook, void *hook_data)
+{
+ struct grub_uhci *u;
+ struct grub_usb_controller dev;
+
+ for (u = uhci; u; u = u->next)
+ {
+ dev.data = u;
+ if (hook (&dev, hook_data))
+ return 1;
+ }
+
+ return 0;
+}
+
+static grub_usb_err_t
+grub_uhci_portstatus (grub_usb_controller_t dev,
+ unsigned int port, unsigned int enable)
+{
+ struct grub_uhci *u = (struct grub_uhci *) dev->data;
+ int reg;
+ unsigned int status;
+ grub_uint64_t endtime;
+
+ grub_dprintf ("uhci", "portstatus, iobase:%08x\n", u->iobase);
+
+ grub_dprintf ("uhci", "enable=%d port=%d\n", enable, port);
+
+ if (port == 0)
+ reg = GRUB_UHCI_REG_PORTSC1;
+ else if (port == 1)
+ reg = GRUB_UHCI_REG_PORTSC2;
+ else
+ return GRUB_USB_ERR_INTERNAL;
+
+ status = grub_uhci_readreg16 (u, reg);
+ grub_dprintf ("uhci", "detect=0x%02x\n", status);
+
+ if (!enable) /* We don't need reset port */
+ {
+ /* Disable the port. */
+ grub_uhci_writereg16 (u, reg, 0 << 2);
+ grub_dprintf ("uhci", "waiting for the port to be disabled\n");
+ endtime = grub_get_time_ms () + 1000;
+ while ((grub_uhci_readreg16 (u, reg) & (1 << 2)))
+ if (grub_get_time_ms () > endtime)
+ return GRUB_USB_ERR_TIMEOUT;
+
+ status = grub_uhci_readreg16 (u, reg);
+ grub_dprintf ("uhci", ">3detect=0x%02x\n", status);
+ return GRUB_USB_ERR_NONE;
+ }
+
+ /* Reset the port. */
+ status = grub_uhci_readreg16 (u, reg) & ~GRUB_UHCI_PORTSC_RWC;
+ grub_uhci_writereg16 (u, reg, status | (1 << 9));
+ grub_uhci_readreg16 (u, reg); /* Ensure it is writen... */
+
+ /* Wait for the reset to complete. XXX: How long exactly? */
+ grub_millisleep (50); /* For root hub should be nominaly 50ms */
+ status = grub_uhci_readreg16 (u, reg) & ~GRUB_UHCI_PORTSC_RWC;
+ grub_uhci_writereg16 (u, reg, status & ~(1 << 9));
+ grub_uhci_readreg16 (u, reg); /* Ensure it is writen... */
+
+ /* Note: some debug prints were removed because they affected reset/enable timing. */
+
+ grub_millisleep (1); /* Probably not needed at all or only few microsecs. */
+
+ /* Reset bits Connect & Enable Status Change */
+ status = grub_uhci_readreg16 (u, reg) & ~GRUB_UHCI_PORTSC_RWC;
+ grub_uhci_writereg16 (u, reg, status | (1 << 3) | GRUB_UHCI_REG_PORTSC_CONNECT_CHANGED);
+ grub_uhci_readreg16 (u, reg); /* Ensure it is writen... */
+
+ /* Enable the port. */
+ status = grub_uhci_readreg16 (u, reg) & ~GRUB_UHCI_PORTSC_RWC;
+ grub_uhci_writereg16 (u, reg, status | (1 << 2));
+ grub_uhci_readreg16 (u, reg); /* Ensure it is writen... */
+
+ endtime = grub_get_time_ms () + 1000;
+ while (! ((status = grub_uhci_readreg16 (u, reg)) & (1 << 2)))
+ if (grub_get_time_ms () > endtime)
+ return GRUB_USB_ERR_TIMEOUT;
+
+ /* Reset recovery time */
+ grub_millisleep (10);
+
+ /* Read final port status */
+ status = grub_uhci_readreg16 (u, reg);
+ grub_dprintf ("uhci", ">3detect=0x%02x\n", status);
+
+
+ return GRUB_USB_ERR_NONE;
+}
+
+static grub_usb_speed_t
+grub_uhci_detect_dev (grub_usb_controller_t dev, int port, int *changed)
+{
+ struct grub_uhci *u = (struct grub_uhci *) dev->data;
+ int reg;
+ unsigned int status;
+
+ grub_dprintf ("uhci", "detect_dev, iobase:%08x\n", u->iobase);
+
+ if (port == 0)
+ reg = GRUB_UHCI_REG_PORTSC1;
+ else if (port == 1)
+ reg = GRUB_UHCI_REG_PORTSC2;
+ else
+ return GRUB_USB_SPEED_NONE;
+
+ status = grub_uhci_readreg16 (u, reg);
+
+ grub_dprintf ("uhci", "detect=0x%02x port=%d\n", status, port);
+
+ /* Connect Status Change bit - it detects change of connection */
+ if (status & GRUB_UHCI_DETECT_CHANGED)
+ {
+ *changed = 1;
+ /* Reset bit Connect Status Change */
+ grub_uhci_writereg16 (u, reg, (status & GRUB_UHCI_REG_PORTSC_RW)
+ | GRUB_UHCI_REG_PORTSC_CONNECT_CHANGED);
+ }
+ else
+ *changed = 0;
+
+ if (! (status & GRUB_UHCI_DETECT_HAVE_DEVICE))
+ return GRUB_USB_SPEED_NONE;
+ else if (status & GRUB_UHCI_DETECT_LOW_SPEED)
+ return GRUB_USB_SPEED_LOW;
+ else
+ return GRUB_USB_SPEED_FULL;
+}
+
+static int
+grub_uhci_hubports (grub_usb_controller_t dev __attribute__((unused)))
+{
+ /* The root hub has exactly two ports. */
+ return 2;
+}
+
+
+static struct grub_usb_controller_dev usb_controller =
+{
+ .name = "uhci",
+ .iterate = grub_uhci_iterate,
+ .setup_transfer = grub_uhci_setup_transfer,
+ .check_transfer = grub_uhci_check_transfer,
+ .cancel_transfer = grub_uhci_cancel_transfer,
+ .hubports = grub_uhci_hubports,
+ .portstatus = grub_uhci_portstatus,
+ .detect_dev = grub_uhci_detect_dev,
+ /* estimated max. count of TDs for one bulk transfer */
+ .max_bulk_tds = N_TD * 3 / 4
+};
+
+GRUB_MOD_INIT(uhci)
+{
+ grub_stop_disk_firmware ();
+
+ grub_uhci_inithw ();
+ grub_usb_controller_dev_register (&usb_controller);
+ grub_dprintf ("uhci", "registered\n");
+}
+
+GRUB_MOD_FINI(uhci)
+{
+ struct grub_uhci *u;
+
+ /* Disable all UHCI controllers. */
+ for (u = uhci; u; u = u->next)
+ grub_uhci_writereg16 (u, GRUB_UHCI_REG_USBCMD, 0);
+
+ /* Unregister the controller. */
+ grub_usb_controller_dev_unregister (&usb_controller);
+}