/* 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 . */ #include #include #include #include #include #include #include #include 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_FLBASEADD = 0x08, GRUB_UHCI_REG_PORTSC1 = 0x10, GRUB_UHCI_REG_PORTSC2 = 0x12 } grub_uhci_reg_t; #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]; } __attribute__ ((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]; } __attribute__ ((packed)); typedef volatile struct grub_uhci_td *grub_uhci_td_t; typedef volatile struct grub_uhci_qh *grub_uhci_qh_t; struct grub_uhci { int iobase; grub_uint32_t *framelist; /* N_QH Queue Heads. */ grub_uhci_qh_t qh; /* N_TD Transfer Descriptors. */ grub_uhci_td_t td; /* 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); } static grub_err_t grub_uhci_portstatus (grub_usb_controller_t dev, unsigned int port, unsigned int enable); /* Iterate over all PCI devices. Determine if a device is an UHCI controller. If this is the case, initialize it. */ static int NESTED_FUNC_ATTR grub_uhci_pci_iter (grub_pci_device_t dev, grub_pci_id_t pciid __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 & 1)) return 0; /* Allocate memory for the controller and register it. */ u = grub_zalloc (sizeof (*u)); if (! u) return 1; u->iobase = base & GRUB_UHCI_IOMASK; /* Reserve a page for the frame list. */ u->framelist = grub_memalign (4096, 4096); if (! u->framelist) goto fail; grub_dprintf ("uhci", "class=0x%02x 0x%02x interface 0x%02x base=0x%x framelist=%p\n", class, subclass, interf, u->iobase, u->framelist); /* The framelist pointer of UHCI is only 32 bits, make sure this code works on on 64 bits architectures. */ #if GRUB_CPU_SIZEOF_VOID_P == 8 if ((grub_uint64_t) u->framelist >> 32) { grub_error (GRUB_ERR_OUT_OF_MEMORY, "allocated frame list memory not <4GB"); goto fail; } #endif /* The QH pointer of UHCI is only 32 bits, make sure this code works on on 64 bits architectures. */ u->qh = (grub_uhci_qh_t) grub_memalign (4096, sizeof(struct grub_uhci_qh)*N_QH); if (! u->qh) goto fail; #if GRUB_CPU_SIZEOF_VOID_P == 8 if ((grub_uint64_t) u->qh >> 32) { grub_error (GRUB_ERR_OUT_OF_MEMORY, "allocated QH memory not <4GB"); goto fail; } #endif /* The TD pointer of UHCI is only 32 bits, make sure this code works on on 64 bits architectures. */ u->td = (grub_uhci_td_t) grub_memalign (4096, sizeof(struct grub_uhci_td)*N_TD); if (! u->td) goto fail; #if GRUB_CPU_SIZEOF_VOID_P == 8 if ((grub_uint64_t) u->td >> 32) { grub_error (GRUB_ERR_OUT_OF_MEMORY, "allocated TD memory not <4GB"); goto fail; } #endif grub_dprintf ("uhci", "QH=%p, TD=%p\n", u->qh, u->td); /* Link all Transfer Descriptors in a list of available Transfer Descriptors. */ for (i = 0; i < N_TD; i++) u->td[i].linkptr = (grub_uint32_t) (grub_addr_t) &u->td[i + 1]; u->td[N_TD - 2].linkptr = 0; u->tdfree = u->td; /* Make sure UHCI is disabled! */ grub_uhci_writereg16 (u, GRUB_UHCI_REG_USBCMD, 0); /* Setup the frame list pointers. Since no isochronous transfers are and will be supported, they all point to the (same!) queue head. */ fp = (grub_uint32_t) (grub_addr_t) u->qh & (~15); /* Mark this as a queue head. */ fp |= 2; for (i = 0; i < 1024; i++) u->framelist[i] = fp; /* Program the framelist address into the UHCI controller. */ grub_uhci_writereg32 (u, GRUB_UHCI_REG_FLBASEADD, (grub_uint32_t) (grub_addr_t) u->framelist); /* Make the Queue Heads point to each other. */ for (i = 0; i < N_QH; i++) { /* Point to the next QH. */ u->qh[i].linkptr = (grub_uint32_t) (grub_addr_t) (&u->qh[i + 1]) & (~15); /* This is a QH. */ u->qh[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[i].elinkptr = 1; } /* The last Queue Head should terminate. */ u->qh[N_QH - 1].linkptr = 1; /* Enable UHCI again. */ grub_uhci_writereg16 (u, GRUB_UHCI_REG_USBCMD, 1 | (1 << 7)); /* 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_free ((void *) u->qh); grub_free (u->framelist); } grub_free (u); return 1; } static void grub_uhci_inithw (void) { grub_pci_iterate (grub_uhci_pci_iter); } 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_uhci_td_t) (grub_addr_t) u->tdfree->linkptr; return ret; } static void grub_free_td (struct grub_uhci *u, grub_uhci_td_t td) { td->linkptr = (grub_uint32_t) (grub_addr_t) u->tdfree; 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] = 0; *actual = 0; /* Free the TDs in this queue and set last_trans. */ for (i=0; td; i++) { grub_uhci_td_t tdprev; /* 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; td = (grub_uhci_td_t) (grub_addr_t) td->linkptr2; /* 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[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] = 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. */ 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_uint32_t) (grub_addr_t) td; td_prev->linkptr = (grub_uint32_t) (grub_addr_t) td; 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_uint32_t) (grub_addr_t) cdata->td_first; 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; errtd = (grub_uhci_td_t) (grub_addr_t) (cdata->qh->elinkptr & ~0x0f); grub_dprintf ("uhci", ">t status=0x%02x data=0x%02x td=%p\n", errtd->ctrl_status, errtd->buffer & (~15), errtd); /* 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; } grub_dprintf ("uhci", "t status=0x%02x\n", errtd->ctrl_status); if (!(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 (int (*hook) (grub_usb_controller_t dev)) { struct grub_uhci *u; struct grub_usb_controller dev; for (u = uhci; u; u = u->next) { dev.data = u; if (hook (&dev)) return 1; } return 0; } static grub_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_error (GRUB_ERR_OUT_OF_RANGE, "UHCI Root Hub port does not exist"); 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_error (GRUB_ERR_IO, "UHCI Timed out - disable"); status = grub_uhci_readreg16 (u, reg); grub_dprintf ("uhci", ">3detect=0x%02x\n", status); return GRUB_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_error (GRUB_ERR_IO, "UHCI Timed out - enable"); /* 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_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 & (1 << 1)) { *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 & 1)) return GRUB_USB_SPEED_NONE; else if (status & (1 << 8)) 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 }; GRUB_MOD_INIT(uhci) { 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); }