[Solved] xHCI Interrupt Troubles

Question about which tools to use, bugs, the best way to implement a function, etc should go here. Don't forget to see if your question is answered in the wiki first! When in doubt post here.
foliagecanine
Member
Member
Posts: 148
Joined: Sun Aug 23, 2020 4:35 pm

[Solved] xHCI Interrupt Troubles

Post by foliagecanine »

Hello,
So recently I finished a UHCI driver (at least as far as I need for now).
I asked around what people thought would be the next easiest *HCI driver to implement, and I got xHCI.

So I've been working on an xHCI driver and am having a lot of trouble understanding it (it is a LOT more complicated than UHCI).

Still, I've managed to get to a point where I can reset ports and have them become active.
However, I'm really getting stuck on how command and event rings work, especially cycles, when they change, etc.

I recently found a bug where I was putting virtual addresses instead of physical addresses into the Interrupter 0 registers (for the dequeue pointer and segment table pointer registers).
In fixing this bug I now get interrupts. That is a good thing. The bad thing is i KEEP getting interrupts.
Before I even try to put anything on the command ring, I'm getting interrupts.
I think I've pinpointed where the interrupts start to when the port gets reset.
I added a debug line to check whether there was an event TRB on the event ring, and based off the cycle number there was exactly one.

From what I can tell, to acknowledge an interrupt I need to clear any bits in the USB Status register and the Interrupt Pending bit of the interrupter that fired. At this point I should process any event TRBs. Then I need to increment the interrupter's dequeue pointer to the last event TRB processed.

Should the EHB (Event Handler Busy) bit be set for the interrupter's dequeue pointer? Right now I write the address or-ed with the EHB bit, which I believe is Write-Clear.
Also, what happens when the dequeue pointer is incremented past the end of the segment? Is the software supposed to reset it somehow?

For reference, here's my xHCI interrupt function

Code: Select all

void xhci_interrupt() {
	dbgprintf("[xHCI] USB INTERRUPT\n");
	for (uint8_t i = 0; i < USB_MAX_CTRLRS; i++) {
		xhci_controller *xc = get_xhci_controller(i);
		if (!xc->hcops)
			continue;
		_wr32(xc->hcops+XHCI_HCOPS_USBSTS,_rd32(xc->hcops+XHCI_HCOPS_USBSTS));
		if (_rd32(xc->runtime+XHCI_RUNTIME_IR0+XHCI_INTREG_IMR)&(XHCI_INTREG_IMR_EN|XHCI_INTREG_IMR_IP)==(XHCI_INTREG_IMR_EN|XHCI_INTREG_IMR_IP)) {
			_wr32(xc->runtime+XHCI_RUNTIME_IR0+XHCI_INTREG_IMR,_rd32(xc->runtime+XHCI_RUNTIME_IR0+XHCI_INTREG_IMR));
			while (xc->cevttrb->command&XHCI_TRB_CYCLE==xc->evtcycle) {
				xc->cevttrb += sizeof(xhci_trb);
				dbgprintf("Processed TRB\n");
			}
			_wr64(xc->runtime+XHCI_RUNTIME_IR0+XHCI_INTREG_ERDQPTR,((uint64_t)(uint32_t)get_phys_addr(xc->cevttrb-sizeof(xhci_trb)))|XHCI_INTREG_ERDQPTR_EHBSY,xc);
		}
	}
}
Any other code can be found at https://github.com/foliagecanine/tritiu ... 386/xhci.c or in that repository.

What am I doing wrong?
Last edited by foliagecanine on Thu Nov 19, 2020 7:05 pm, edited 1 time in total.
My OS: TritiumOS
https://github.com/foliagecanine/tritium-os
void warranty(laptop_t laptop) { if (laptop.broken) return laptop; }
I don't get it: Why's the warranty void?
User avatar
BenLunt
Member
Member
Posts: 941
Joined: Sat Nov 22, 2014 6:33 pm
Location: USA
Contact:

Re: xHCI Interrupt Troubles

Post by BenLunt »

foliagecanine wrote:From what I can tell, to acknowledge an interrupt I need to clear any bits in the USB Status register and the Interrupt Pending bit of the interrupter that fired. At this point I should process any event TRBs. Then I need to increment the interrupter's dequeue pointer to the last event TRB processed.
Hi,

In what order do you clear these bits? If you clear the Interrupt Pending bit first, the controller may fire another interrupt since the Status bit is still set. You must clear the bits in the correct order.
foliagecanine wrote:Should the EHB (Event Handler Busy) bit be set for the interrupter's dequeue pointer? Right now I write the address or-ed with the EHB bit, which I believe is Write-Clear.
As you state, the EHB bit is Write-Clear. This means you write a 1 to it to clear it. You should clear it after you have processed the TRB(s) in the Event Ring.

For example, when receiving an interrupt:
1) Clear the Status register bit
2) Clear the Interrupt Management bits (Write the *_MAN_IE and *_MAN_IP bits)
3) Process the TD(s)
4) Update the Dequeue Pointer to point to the last TD processed, clearing the EHB bit at the same time
foliagecanine wrote:Also, what happens when the dequeue pointer is incremented past the end of the segment? Is the software supposed to reset it somehow?
Depending on the ring type. If it is a Segmented Ring, the controller will move to the next segment, which could be the first of the ring. If it is a standard ring, you must place a "LINK" TD as the last TD so that the controller moves to the next portion of the ring, usually the beginning of the ring.

The controller also checks the "Update Cycle Bit" bit portion of this LINK TRB to see if it should toggle the cycle bit, which you should.

Hope this helps,
Ben
- http://www.fysnet.net/the_universal_serial_bus.htm
foliagecanine
Member
Member
Posts: 148
Joined: Sun Aug 23, 2020 4:35 pm

Re: xHCI Interrupt Troubles

Post by foliagecanine »

Thank you for replying.
BenLunt wrote: If it is a Segmented Ring, the controller will move to the next segment, which could be the first of the ring.
So if I understand this right, if there were only one Event Ring Segment it would go back to the start of the ring? Would it flip the Cycle bit?
BenLunt wrote: In what order do you clear these bits? If you clear the Interrupt Pending bit first, the controller may fire another interrupt since the Status bit is still set. You must clear the bits in the correct order.
I believe I'm doing it in the right order.
Just to check, here's what I'm doing:
  • Find an active controller
  • (Newly added) Check to make sure USBStatus is non-zero. If not, go to on to next controller
  • Write the value of USBStatus to the USBStatus register. This should clear any Write-Clear bits.
  • Read Interrupter 0's Interrupter Management Register and check to make sure BOTH the IE and IP bits are set
  • Write the value of Interrupter 0's Interrupter Management Register (which should be 3, due to above check) to the Interrupter 0 Interrupter Management Register. Doing this should clear the IP bit but leave the IE bit on.
  • For now, it just dumps the event TRBs to the serial console. On this note, is there anything that needs to be done to process the event TRB so that the interrupt stops?
  • Write a 64 bit value of the physical address of the last event TRB processed ORed with the EHB bit to the Interrupter 0 Event Ring Dequeue Pointer register
Here's the Event TRB dump after a port reset (is that supposed to happen?):

Code: Select all

Processed TRB
Param: 0x5000000
Status: 0x1000000
Command: 0x8801
Which is TRB type 0x22? Invalid Stream ID?
I'm very confused.
My OS: TritiumOS
https://github.com/foliagecanine/tritium-os
void warranty(laptop_t laptop) { if (laptop.broken) return laptop; }
I don't get it: Why's the warranty void?
User avatar
BenLunt
Member
Member
Posts: 941
Joined: Sat Nov 22, 2014 6:33 pm
Location: USA
Contact:

Re: xHCI Interrupt Troubles

Post by BenLunt »

foliagecanine wrote:
BenLunt wrote: If it is a Segmented Ring, the controller will move to the next segment, which could be the first of the ring.
So if I understand this right, if there were only one Event Ring Segment it would go back to the start of the ring? Would it flip the Cycle bit?
In theory, one of two things must happen for this to work correctly:
1) Either the controller must toggle the Cycle Bit expected *or*
2) You must clear the toggle bit in, at the very least, the first entry in the ring.

Imagine for a moment that you have created an empty ring, each entry's Cycle bit being clear. (The Producer's and the Consumer's Cycle bit being set)
Now imagine that you (Producer) insert entries into the ring, with the Cycle bit set, indicating to the controller (Consumer) to execute the entry.
Now imagine that you have just ran through the whole ring, the controller just executed the last entry in the ring. The controller moves to the first entry in the ring and checks the Cycle bit in this first entry.
Since this entry and each entry thereafter, has a Cycle bit set, currently indicating that the controller needs to execute that entry, the controller is going to try to execute the *whole* ring again.

There must be a mechanism in place to toggle the Cycle bit so that this does not happen. Luckily, there is such a mechanism.

In Regular Rings, this is the LINK TRB. In Segmented Rings, the Cycle bit is assumed toggled when starting over, moving to the first entry in the first segment, scenario 1 mentioned above.

Also, a few notes. A Segment must contain at least 16 entries and must not cross a 64k boundary. All entries within a segment must be within a 64k block, not crossing a 64k boundary.

Ben
foliagecanine
Member
Member
Posts: 148
Joined: Sun Aug 23, 2020 4:35 pm

Re: xHCI Interrupt Troubles

Post by foliagecanine »

BenLunt wrote:A Segment must contain at least 16 entries and must not cross a 64k boundary. All entries within a segment must be within a 64k block, not crossing a 64k boundary.
What do you mean by 16 entries?

To create an event ring, I allocate one page (4096 bytes, 4096b aligned) for the Event Ring Segment Table. Then I allocate 80 pages (16+64), and use this formula to get the base address of the Event Ring:

Code: Select all

(event_ring_alloc_address + (16*4096)) & 0xFFFF0000
to get the 64 KiB aligned address. I then store the physical address in the first entry in the Event Ring Segment Table. Then offset 8 from that I store 4096. Is this correct?

Edits: Fix formula
My OS: TritiumOS
https://github.com/foliagecanine/tritium-os
void warranty(laptop_t laptop) { if (laptop.broken) return laptop; }
I don't get it: Why's the warranty void?
User avatar
BenLunt
Member
Member
Posts: 941
Joined: Sat Nov 22, 2014 6:33 pm
Location: USA
Contact:

Re: xHCI Interrupt Troubles

Post by BenLunt »

foliagecanine wrote:
BenLunt wrote:A Segment must contain at least 16 entries and must not cross a 64k boundary. All entries within a segment must be within a 64k block, not crossing a 64k boundary.
What do you mean by 16 entries?
Each segment must be large enough to hold 16 TRBs. i.e.: A segment must have at least 16 entries. A segment cannot be less than 16 entries in size. The specification does not say, but I am guessing that the firmware would cache 16 entries at a time and when loading this cache, it must not cross a 64k boundary. Therefore, you should not have a segment that starts at an address that is less than 16 entries from a 64k boundary.
foliagecanine wrote:To create an event ring, I allocate one page (4096 bytes, 4096b aligned) for the Event Ring Segment Table. Then I allocate 80 pages (16+64), and use this formula to get the base address of the Event Ring:

Code: Select all

(event_ring_alloc_address + (16*4096)) & 0xFFFF0000
to get the 64 KiB aligned address. I then store the physical address in the first entry in the Event Ring Segment Table. Then offset 8 from that I store 4096. Is this correct?
Edits: Fix formula
Your Segment Table must be aligned on a 64-byte address, and must not contain more than the count of entries the controller specifies in the Config/Operational registers. Each entry must point to a 64-byte aligned address containing at least enough room for 16 TRBs *and* this address plus the size you specify for this segment, must not cross a 64k boundary.

If you allocate a 4k page of memory for your Segment Table, you can have up to MAX_SEGMENTs entries in this table.
Then if you allocate sixteen 4k pages for each entry, making sure that the first page is 64k aligned, you can then have 4,096 entries per segment.

If you choose to only have one segment with 4,096 entries, this is perfectly fine. As a hobby OS, these 4,096 entries are well more than enough to keep up with anything that would be inserted to the ring.

Ben
- http://www.fysnet.net/the_universal_serial_bus.htm
foliagecanine
Member
Member
Posts: 148
Joined: Sun Aug 23, 2020 4:35 pm

Re: xHCI Interrupt Troubles

Post by foliagecanine »

I did a little more testing, and it looks like the both the USBStatus EINT bit and the IR0 (Interrupter 0) IP bit are being set when I write the Event Ring Dequeue Pointer value (event_ring_base_address | EHB) to the IR0 Event Ring Dequeue Pointer Register. This is valid for the first interrupt (which does come with an Event TRB).

However, when the next interrupts occur, it does not give any more TRBs to process so the same value is written to the IR0 Event Ring Dequeue Pointer Register. In your book you say that the software should not write the same value unless the event ring is either full or empty. I don't know what to do at this point. What could be causing the next interrupts?
My OS: TritiumOS
https://github.com/foliagecanine/tritium-os
void warranty(laptop_t laptop) { if (laptop.broken) return laptop; }
I don't get it: Why's the warranty void?
foliagecanine
Member
Member
Posts: 148
Joined: Sun Aug 23, 2020 4:35 pm

Re: xHCI Interrupt Troubles

Post by foliagecanine »

Alright. I think I figured out why it was causing multiple interrupts.

Code: Select all

xc->cevttrb += sizeof(xhci_trb)
This code was the culprit. I had declared cevttrb (pointer to the current event TRB) as such:

Code: Select all

xhci_trb *cevttrb
So when I added sizeof(xhci_trb), it actually added 16 TIMES 16 rather than just 16.

I figured this out by recompiling QEMU with xHCI Debug on (I should have done this before, it's so much easier!). I noticed that it checked whether the dequeue pointer index was EQUAL to the enqueue pointer index. When I debugged it using GDB I realized two things:
1. You are supposed to write the address of the TRB after the last processed TRB into the dequeue register, not the address OF the TRB.
2. When I fixed the first problem, I realized that the dequeue pointer was much more than 16 bytes above the base address.

-------------------------------------------------


I also figured out why I wasn't getting an interrupt when I tried sending a command TRB: I forgot to set the cycle bit. #-o

Anyways, thanks for all your help. Now I understand the event ring a lot better. Hopefully I won't need to post here again too soon :) .
My OS: TritiumOS
https://github.com/foliagecanine/tritium-os
void warranty(laptop_t laptop) { if (laptop.broken) return laptop; }
I don't get it: Why's the warranty void?
User avatar
BenLunt
Member
Member
Posts: 941
Joined: Sat Nov 22, 2014 6:33 pm
Location: USA
Contact:

Re: xHCI Interrupt Troubles

Post by BenLunt »

Glad to hear you figure it out. I am sure it was something you didn't even thing of looking at until you looked at some debug logs. I can relate to that.
foliagecanine wrote:1. You are supposed to write the address of the TRB after the last processed TRB into the dequeue register, not the address OF the TRB.
I would have to check my notes to be sure, bit I am pretty sure that the above is a wrong statement. You are to write the address of the last processed TRB, *not* the next TRB.

...

Yes, I had to go check. The specification, version 1.0, section 5.5.2.3.3, states:
When software finishes processing an Event TRB, it will write the address of that Event TRB to the ERDP.
Ben
- http://www.fysnet.net/osdesign_book_series.htm
foliagecanine
Member
Member
Posts: 148
Joined: Sun Aug 23, 2020 4:35 pm

Re: xHCI Interrupt Troubles

Post by foliagecanine »

Hmm. I wouldn't suspect a QEMU bug though.
QEMU Source Code:

Code: Select all

dma_addr_t erdp = xhci_addr64(intr->erdp_low, intr->erdp_high);
unsigned int dp_idx = (erdp - intr->er_start) / TRB_SIZE;
if (erdp >= intr->er_start &&
    erdp < (intr->er_start + TRB_SIZE * intr->er_size) &&
    dp_idx != intr->er_ep_idx) {
        xhci_intr_raise(xhci, v);
dp_idx is the dequeue pointer index. 0 means DQP == Event Ring Start. 1 means DQP == Event Ring Start + sizeof(TRB).
er_ep_idx is the enqueue pointer index stored inside the virtual controller.
v is the interrupter, in this case IR_0

When I debugged it originally when I was writing the address of the last TRB processed, it evaluated as so:

Code: Select all

unsigned int dp_idx = (edrp{0x840000} - intr->er_start{0x840000} / TRB_SIZE{16}
if (edrp{0x840000} >= intr->er_start{0x840000} &&
    edrp{0x840000} < (intr->er_start{0x840000} + TRB_SIZE{16} * intr->er_size{4096} &&
    dp_idx{0} != intr->er_ep_idx{1})
        xhci_intr_raise(xhci, v);
or

Code: Select all

dp_idx = 0;
if (0x840000 >= 0x840000 &&
    0x840000 < 0x850000 &&
    0 != 1)
        xhci_intr_raise(xhci, 0);
My OS: TritiumOS
https://github.com/foliagecanine/tritium-os
void warranty(laptop_t laptop) { if (laptop.broken) return laptop; }
I don't get it: Why's the warranty void?
foliagecanine
Member
Member
Posts: 148
Joined: Sun Aug 23, 2020 4:35 pm

Re: xHCI Interrupt Troubles

Post by foliagecanine »

Also (sort of off topic, but), I know with UHCI you can choose the address of the device when sending a set address command.
Can you also do that with xHCI, or is it given automatically?
My OS: TritiumOS
https://github.com/foliagecanine/tritium-os
void warranty(laptop_t laptop) { if (laptop.broken) return laptop; }
I don't get it: Why's the warranty void?
nullplan
Member
Member
Posts: 1790
Joined: Wed Aug 30, 2017 8:24 am

Re: xHCI Interrupt Troubles

Post by nullplan »

foliagecanine wrote:Also (sort of off topic, but), I know with UHCI you can choose the address of the device when sending a set address command.
Can you also do that with xHCI, or is it given automatically?
No and no. You cannot set an address, but you have to send a special "Set Address" command. I fail to remember the specifics at the moment, but you have to tell the xHCI to give an address to the new device, and the address is chosen by the xHCI. Supposedly it makes virtualization easier.
Carpe diem!
iloveosdev
Posts: 9
Joined: Fri Apr 03, 2020 2:01 pm

Re: xHCI Interrupt Troubles

Post by iloveosdev »

you're right. you should advance the dq pointer to a trb that is yet to be processed.

you can read about it in the 4.9.4. section in the spec.

it says that "processed" also includes the cycle bit mismatch.

so from the original perspective it's a trb that is not yet processed. but another perspective, where a mismatched cycle bit is counted as having been "processed" then you have advanced the dq pointer to a processed trb.
Last edited by iloveosdev on Tue Sep 29, 2020 12:41 pm, edited 1 time in total.
no work and no formal education in schools, regarding this. this is a hobby.
iloveosdev. osdev means os development. I love this site as well, of course :)
foliagecanine
Member
Member
Posts: 148
Joined: Sun Aug 23, 2020 4:35 pm

Re: xHCI Interrupt Troubles

Post by foliagecanine »

@nullplan That's what I figured, but wasn't quite sure.

@iloveosdev I think I see what you're saying:
Intel xHCI Specification wrote:If the Cycle bit of the Event TRB pointed to by the Event Ring Dequeue Pointer equals CCS, then the Event TRB is a valid event, software processes it and advances the Event Ring Dequeue Pointer.
Therefore if the Cycle bit of the zeroeth Event TRB is active, you would process it then you would still advance the DQ pointer.

------------------------------------------

By the way Ben, I don't think your GDevDesc xHCI program works on QEMU:

Code: Select all

qemu-system-i386 USB/freedos_hd.img -device qemu-xhci,id=xhci -device usb-mouse,bus=xhci.0

Code: Select all

GD_xHCI -- xHCI: Get Device Descriptor.   v1.10.20
Forever Young Software        (C)opyright 1984-2016

PCI: Found a USB controller entry: Bus = 0, device = 4, function = 0
  Found xHCI controller at 0xFEBB0000
C:\>
My OS: TritiumOS
https://github.com/foliagecanine/tritium-os
void warranty(laptop_t laptop) { if (laptop.broken) return laptop; }
I don't get it: Why's the warranty void?
iloveosdev
Posts: 9
Joined: Fri Apr 03, 2020 2:01 pm

Re: xHCI Interrupt Troubles

Post by iloveosdev »

If the Cycle bit of the Event TRB pointed to by the Event Ring Dequeue Pointer equals CCS, then the Event TRB is a valid event, software processes it and advances the Event Ring Dequeue Pointer.

aha, that's actually not what i meant since it can be interpreted as if it can be another handled trb at the next location. what i meant is somewhere at the description of the event ring(s) initialization. and the picture at the page before that seems to indicate that the event ring pointer is at a trb that has a cycle bit mismatch, and the trb might as well not contain anything but that.
no work and no formal education in schools, regarding this. this is a hobby.
iloveosdev. osdev means os development. I love this site as well, of course :)
Post Reply