From 274c63e367aacbd50fb35f25da896fead08a6c3d Mon Sep 17 00:00:00 2001 From: Nico Huber Date: Wed, 20 Jun 2012 14:58:21 +0200 Subject: libpayload: Add support for interrupt transfers in OHCI This adds support for usb interrupt transfers to the OHCI driver. Basically this enables support for HID keyboard devices. For each interrupt transfer endpoint, two queues of transfer descriptors (TDs) are maintained: the first with initialized TDs is linked to the periodic schedule of the host controller (HC), the second holds processed TDs which will be polled by the usb class driver. The HC moves processed TDs from its schedule to a done queue. We periodically fetch all TDs from the done queue, to put them on the queue associated with the endpoint, where they can be polled from. Fully processed TDs (i.e. which have gone throuch all of this) will be reinitialized and put on the first queue again. Change-Id: Iaab72c04087b36c9f0f6e539e31b47060c190015 Signed-off-by: Nico Huber Reviewed-on: http://review.coreboot.org/1128 Reviewed-by: Stefan Reinauer Tested-by: build bot (Jenkins) --- payloads/libpayload/drivers/usb/ohci.c | 230 ++++++++++++++++++++++++- payloads/libpayload/drivers/usb/ohci_private.h | 1 + 2 files changed, 223 insertions(+), 8 deletions(-) (limited to 'payloads/libpayload') diff --git a/payloads/libpayload/drivers/usb/ohci.c b/payloads/libpayload/drivers/usb/ohci.c index 6d98cc1100..0f4886fc63 100644 --- a/payloads/libpayload/drivers/usb/ohci.c +++ b/payloads/libpayload/drivers/usb/ohci.c @@ -144,6 +144,8 @@ ohci_init (pcidev_t addr) OHCI_INST (controller)->periodic_ed = periodic_ed; OHCI_INST (controller)->opreg->HcHCCA = virt_to_phys(OHCI_INST (controller)->hcca); + /* Make sure periodic schedule is enabled. */ + OHCI_INST (controller)->opreg->HcControl |= PeriodicListEnable; OHCI_INST (controller)->opreg->HcControl &= ~IsochronousEnable; // unused by this driver // disable everything, contrary to what OHCI spec says in 5.1.1.4, as we don't need IRQs OHCI_INST (controller)->opreg->HcInterruptEnable = 1<<31; @@ -477,33 +479,215 @@ ohci_bulk (endpoint_t *ep, int dalen, u8 *data, int finalize) return failure; } + +struct _intr_queue; + +struct _intrq_td { + volatile td_t td; + u8 *data; + struct _intrq_td *next; + struct _intr_queue *intrq; +}; + +struct _intr_queue { + volatile ed_t ed; + struct _intrq_td *head; + struct _intrq_td *tail; + u8 *data; + int reqsize; + endpoint_t *endp; + unsigned int remaining_tds; +}; + +typedef struct _intrq_td intrq_td_t; +typedef struct _intr_queue intr_queue_t; + +#define INTRQ_TD_FROM_TD(x) ((intrq_td_t *)x) + +static void +ohci_fill_intrq_td(intrq_td_t *const td, intr_queue_t *const intrq, + u8 *const data) +{ + memset(td, 0, sizeof(*td)); + td->td.config = TD_QUEUETYPE_INTR | + (intrq->endp->direction == IN + ? TD_DIRECTION_IN : TD_DIRECTION_OUT) | + TD_DELAY_INTERRUPT_ZERO | + TD_TOGGLE_FROM_ED | + TD_CC_NOACCESS; + td->td.current_buffer_pointer = virt_to_phys(data); + td->td.buffer_end = td->td.current_buffer_pointer + intrq->reqsize - 1; + td->intrq = intrq; + td->data = data; +} + /* create and hook-up an intr queue into device schedule */ -static void* -ohci_create_intr_queue (endpoint_t *ep, int reqsize, int reqcount, int reqtiming) +static void * +ohci_create_intr_queue(endpoint_t *const ep, const int reqsize, + const int reqcount, const int reqtiming) { - return NULL; + int i; + intrq_td_t *first_td = NULL, *last_td = NULL; + + if (reqsize > 4096) + return NULL; + + intr_queue_t *const intrq = + (intr_queue_t *)memalign(sizeof(intrq->ed), sizeof(*intrq)); + memset(intrq, 0, sizeof(*intrq)); + intrq->data = (u8 *)malloc(reqcount * reqsize); + intrq->reqsize = reqsize; + intrq->endp = ep; + + /* Create #reqcount TDs. */ + u8 *cur_data = intrq->data; + for (i = 0; i < reqcount; ++i) { + intrq_td_t *const td = memalign(sizeof(td->td), sizeof(*td)); + ++intrq->remaining_tds; + ohci_fill_intrq_td(td, intrq, cur_data); + cur_data += reqsize; + if (!first_td) + first_td = td; + else + last_td->td.next_td = virt_to_phys(&td->td); + last_td = td; + } + + /* Create last, dummy TD. */ + intrq_td_t *dummy_td = memalign(sizeof(dummy_td->td), sizeof(*dummy_td)); + memset(dummy_td, 0, sizeof(*dummy_td)); + dummy_td->intrq = intrq; + if (last_td) + last_td->td.next_td = virt_to_phys(&dummy_td->td); + last_td = dummy_td; + + /* Initialize ED. */ + intrq->ed.config = (ep->dev->address << ED_FUNC_SHIFT) | + ((ep->endpoint & 0xf) << ED_EP_SHIFT) | + (((ep->direction == IN) ? OHCI_IN : OHCI_OUT) << ED_DIR_SHIFT) | + (ep->dev->speed ? ED_LOWSPEED : 0) | + (ep->maxpacketsize << ED_MPS_SHIFT); + intrq->ed.tail_pointer = virt_to_phys(last_td); + intrq->ed.head_pointer = virt_to_phys(first_td) | + (ep->toggle ? ED_TOGGLE : 0); + + /* Insert ED into periodic table. */ + int nothing_placed = 1; + ohci_t *const ohci = OHCI_INST(ep->dev->controller); + u32 *const intr_table = ohci->hcca->HccaInterruptTable; + const u32 dummy_ptr = virt_to_phys(ohci->periodic_ed); + for (i = 0; i < 32; i += reqtiming) { + /* Advance to the next free position. */ + while ((i < 32) && (intr_table[i] != dummy_ptr)) ++i; + if (i < 32) { + intr_table[i] = virt_to_phys(&intrq->ed); + nothing_placed = 0; + } + } + if (nothing_placed) { + printf("Error: Failed to place ohci interrupt endpoint " + "descriptor into periodic table: no space left\n"); + ohci_destroy_intr_queue(ep, intrq); + return NULL; + } + + return intrq; } /* remove queue from device schedule, dropping all data that came in */ static void -ohci_destroy_intr_queue (endpoint_t *ep, void *q_) +ohci_destroy_intr_queue(endpoint_t *const ep, void *const q_) { + intr_queue_t *const intrq = (intr_queue_t *)q_; + + int i; + + /* Remove interrupt queue from periodic table. */ + ohci_t *const ohci = OHCI_INST(ep->dev->controller); + u32 *const intr_table = ohci->hcca->HccaInterruptTable; + for (i=0; i < 32; ++i) { + if (intr_table[i] == virt_to_phys(intrq)) + intr_table[i] = virt_to_phys(ohci->periodic_ed); + } + /* Wait for frame to finish. */ + mdelay(1); + + /* Free unprocessed TDs. */ + while ((intrq->ed.head_pointer & ~0x3) != intrq->ed.tail_pointer) { + td_t *const cur_td = + (td_t *)phys_to_virt(intrq->ed.head_pointer & ~0x3); + intrq->ed.head_pointer = cur_td->next_td; + free(INTRQ_TD_FROM_TD(cur_td)); + --intrq->remaining_tds; + } + /* Free final, dummy TD. */ + free(phys_to_virt(intrq->ed.head_pointer & ~0x3)); + /* Free data buffer. */ + free(intrq->data); + + /* Process done queue and free processed TDs. */ + ohci_process_done_queue(ohci, 1); + while (intrq->head) { + intrq_td_t *const cur_td = intrq->head; + intrq->head = intrq->head->next; + free(cur_td); + --intrq->remaining_tds; + } + if (intrq->remaining_tds) { + printf("error: ohci_destroy_intr_queue(): " + "freed all but %d TDs.\n", intrq->remaining_tds); + } + + free(intrq); + + /* Save data toggle. */ + ep->toggle = intrq->ed.head_pointer & ED_TOGGLE; } /* read one intr-packet from queue, if available. extend the queue for new input. return NULL if nothing new available. Recommended use: while (data=poll_intr_queue(q)) process(data); */ -static u8* -ohci_poll_intr_queue (void *q_) +static u8 * +ohci_poll_intr_queue(void *const q_) { - return NULL; + intr_queue_t *const intrq = (intr_queue_t *)q_; + + u8 *data = NULL; + + /* Process done queue first, then check if we have work to do. */ + ohci_process_done_queue(OHCI_INST(intrq->endp->dev->controller), 0); + + if (intrq->head) { + /* Save pointer to processed TD and advance. */ + intrq_td_t *const cur_td = intrq->head; + intrq->head = cur_td->next; + + /* Return data buffer of this TD. */ + data = cur_td->data; + + /* Requeue this TD (i.e. copy to dummy and requeue as dummy). */ + intrq_td_t *const dummy_td = + INTRQ_TD_FROM_TD(phys_to_virt(intrq->ed.tail_pointer)); + ohci_fill_intrq_td(dummy_td, intrq, cur_td->data); + /* Reset all but intrq pointer (i.e. init as dummy). */ + memset(cur_td, 0, sizeof(*cur_td)); + cur_td->intrq = intrq; + /* Insert into interrupt queue as dummy. */ + dummy_td->td.next_td = virt_to_phys(&cur_td->td); + intrq->ed.tail_pointer = virt_to_phys(&cur_td->td); + } + + return data; } static void ohci_process_done_queue(ohci_t *const ohci, const int spew_debug) { - int i; + int i, j; + + /* Temporary queue of interrupt queue TDs (to reverse order). */ + intrq_td_t *temp_tdq = NULL; /* Check if done head has been written. */ if (!(ohci->opreg->HcInterruptStatus & WritebackDoneHead)) @@ -527,6 +711,11 @@ ohci_process_done_queue(ohci_t *const ohci, const int spew_debug) /* Free processed async TDs. */ free((void *)done_td); break; + case TD_QUEUETYPE_INTR: + /* Save done TD if it comes from an interrupt queue. */ + INTRQ_TD_FROM_TD(done_td)->next = temp_tdq; + temp_tdq = INTRQ_TD_FROM_TD(done_td); + break; default: break; } @@ -534,5 +723,30 @@ ohci_process_done_queue(ohci_t *const ohci, const int spew_debug) } if (spew_debug) debug("Processed %d done TDs.\n", i); + + j = 0; + /* Process interrupt queue TDs in right order. */ + while (temp_tdq) { + /* Save pointer of current TD and advance. */ + intrq_td_t *const cur_td = temp_tdq; + temp_tdq = temp_tdq->next; + + /* The interrupt queue for the current TD. */ + intr_queue_t *const intrq = cur_td->intrq; + /* Append to interrupt queue. */ + if (!intrq->head) { + /* First element. */ + intrq->head = intrq->tail = cur_td; + } else { + /* Insert at tail. */ + intrq->tail->next = cur_td; + intrq->tail = cur_td; + } + /* It's always the last element. */ + cur_td->next = NULL; + ++j; + } + if (spew_debug) + debug("processed %d done tds, %d intr tds thereof.\n", i, j); } diff --git a/payloads/libpayload/drivers/usb/ohci_private.h b/payloads/libpayload/drivers/usb/ohci_private.h index 3826db08ee..a32203c582 100644 --- a/payloads/libpayload/drivers/usb/ohci_private.h +++ b/payloads/libpayload/drivers/usb/ohci_private.h @@ -231,6 +231,7 @@ #define TD_QUEUETYPE_SHIFT 0 #define TD_QUEUETYPE_MASK MASK(TD_QUEUETYPE_SHIFT, 2) #define TD_QUEUETYPE_ASYNC (0 << TD_QUEUETYPE_SHIFT) +#define TD_QUEUETYPE_INTR (1 << TD_QUEUETYPE_SHIFT) #define TD_DIRECTION_SHIFT 19 #define TD_DIRECTION_MASK MASK(TD_DIRECTION_SHIFT, 2) -- cgit v1.2.3