| /* 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); |
| } |