Universal Host Controller Interface
Universal Host Controller Interface (UHCI) was created by Intel as an implementation of the USB 1.0 host controller interface. Along with OHCI, it makes up the USB 1.0 standard.
Technical Details
The UHCI specification defines a set of I/O mapped registers that allow communication between the controller and the operating system. The base address for these registers can be found by searching the PCI controller for a specific VendorID/DeviceID combination, or for a specific ClassID/SubclassID/Interface combination. All UHCI PCI controllers will have a Class ID of 0x0C, a Subclass ID of 0x03, and an Interface value of 0x00. The PCI Configuration space for this device will contain the I/O port address information in BAR4.
I/O Registers
Offset (Hex) | Name | Description | Lenght |
---|---|---|---|
00 | USBCMD | Usb Command | 2 bytes |
02 | USBSTS | Usb Status | 2 bytes |
04 | USBINTR | Usb Interrupt Enable | 2 bytes |
06 | FRNUM | Frame Number | 2 bytes |
08 | FRBASEADD | Frame List Base Address | 4 bytes |
0C | SOFMOD | Start Of Frame Modify | 1 byte |
10 | PORTSC1 | Port 1 Status/Control | 2 bytes |
12 | PORTSC2 | Port 2 Status/Control | 2 bytes |
Command Register
Bits | Name | Description |
---|---|---|
15-8 | Reserved | |
7 | Max Packet Size | 0 = Max Size is 32 bits 1 = Max Size is 64 bits |
6 | Configure Flag | |
5 | Software Debug | |
4 | Global Resume | |
3 | Global Suspend | |
2 | Global Reset | |
1 | Host Controller Reset | This bit will clear after reset is done |
0 | Run | 1 = Controller execute Frame List Entries |
Status Register
You can clear those bits by writing 1 to them.
Bits | Name | Description |
---|---|---|
15-6 | Reserved | |
5 | Halted | Indicates status of Frame List execution |
4 | Process Error | |
3 | System Error | |
2 | Resume Detected | |
1 | Error Interrupt | |
0 | Interrupt |
Interrupt Enable Register
If bit is set, it mean that this interrupt will be generated
Bits | Name | Description |
---|---|---|
15-4 | Reserved | |
3 | Short Packet | Interrupt when short packet was transferred |
2 | Complete Transfer | Interrupt after transfer of packet with IOC |
1 | Resume | |
0 | Timeout CRC |
Frame Number
Number of processed entry of Frame List.
Frame List Base Address
32-bit physical adress of Frame List. Address has to be aligned to 4 Kb (first 12 bits are zero). The Frame List must contain 1024 entries.
Start Of Frame
This port sets timing of frame. Should be 0x40.
Port 1/2 Status/Control Registers
Bits | Name | Description |
---|---|---|
15-13 | Reserved | |
12 | Suspend | |
11-10 | Reserved | |
9 | Reset | |
8 | Low Speed Device | 0 = Full Speed Device 1 = Low Speed Device |
7 | Reserved | Always set, used to detect number of ports |
6 | Resume Detected | |
5-4 | Line Status | |
3 | Port Enable Changed | Bit 2 was changed, write-clear bit |
2 | Device Enable | |
1 | Connection Status Change | Bit 0 was changed, write-clear bit |
0 | Connection Status |
Memory structures
Frame List Entry
Bits | Name | Description |
---|---|---|
31-4 | Physical Address to Memory Structure | |
3-2 | Reserved | |
1 | Memory Structure Type | 0 = Transfer Descriptor 1 = Queue Head |
0 | Frame Enable | 0 = Frame Is Valid 1 = Empty Frame |
UHCI Queue Head
Offset (Hex) | Name | Description |
---|---|---|
00 | Horizontal Pointer | Same structure as Frame List Entry |
04 | Vertical Pointer | Same structure as Frame List Entry |
UHCI Transfer Descriptor
Offset (Hex) | Name | Description |
---|---|---|
00 | Next Descriptor | See below |
04 | Status | See below |
08 | Packet Header | See below |
12 | Buffer Address | 32-bit address of data buffer |
16 | System Use | 128-bit area reserved for use by the system |
UHCI Transfer Descriptor Next Descriptor
Bits | Name | Description |
---|---|---|
31:4 | Physical Address of Memory Structure | |
3 | Reserved | |
2 | Depth First | Controller will continue execution to next TD pointed by this TD |
1 | Memory Structure Type | 0 = Transfer Descriptor 1 = Queue Head |
0 | Terminate | 0 = Pointer is valid 1 = This TD do not point do anything |
UHCI Transfer Descriptor Status
Bits | Name | Description |
---|---|---|
31-30 | Reserved | |
29 | Short Packet Detect | 1 = If SPD, continue execution from horizontal QH pointer |
28-27 | Error Counter | |
26 | Low Speed | 1 = This is transfer to Low speed device |
25 | Is Isochronous | |
24 | Interrupt On Complete | |
23 | Active | This packet waits to be transferred by UHCI controller |
22 | Stalled | |
21 | Data Buffer Error | |
20 | Babble Detected | |
19 | Non-Acknowledged | |
18 | Timeout CRC | |
17 | Bit Stuff Error | |
16-11 | Reserved | |
10-0 | Actual Length | Length of transferred bytes-1 |
UHCI Transfer Descriptor Packet Header
Bits | Name | Description |
---|---|---|
31-21 | Maximum Length | (Length - 1) so 7 for low speed, 63 for full speed |
20 | Reserved | |
19 | Data Toggle | |
18-15 | Endpoint | |
14-8 | Device | |
7-0 | Packet Type | 0x69 = IN, 0xE1 = OUT, 0x2D = SETUP, other values are invalid |
Initalization
When you find UHCI controller, you need to initalize it by following steps:
- Disable BIOS Legacy USB Emulation by writing value 0x2000 to PCI register 0xC0
- Set I/O busmastering in PCI
- Reset controller by Host Controller Reset (bit will self-clear) and Global Reset (you need to clear bit after 10ms)
- Allocate memory for Frame List, fill it with empty Frame Entries (every entry will have bit 0 set) and write it's physical address to FRBASEADD
- Enable interrupts by setting bits in USBINTR
- Find out number of ports on controller. This value is not saved anywhere, so you need to continue to access new port registers, and check if they have bit 7 set and their value is not 0xFFFF. If those conditions are met, it means that you find valid port register.
- Start controller by setting bit 0 and bit 7 (to enable transfers with 64 bytes) in USBCMD
When you initalized controller, you can start periodically check Connection Status Change of ports. If it is set, it means, that device was either connected, or disconnected. So you need to check Connection Status to determine what happened. In both cases, you will need to clear Connection Status Change. You have to do it in separate write beside any other action, so for example, you can not clear Connection Status Change and set Reset of port in same write. Then if there is connected new device, you need to enable it by following sequence:
- Set Reset bit
- Wait 100 ms
- Clear Reset bit
- Wait 50 ms
- Set Device Enable bit
- Wait until this bit becomes set
When Device Enable bit is set, it means that you have successfully enabled device that is in zero address mode, so you can start transfers to it. At first you would want to read USB Device Descriptor, and then Set Address to something other than 0. If Device Enable bit is not set after 100 ms, you can assume that there happened some error.
If you want to disable device (for example because it do not responds to transfers), you simply need to clear Device Enable bit.
Transfers
At beginning of frame, UHCI controller looks at frame number at FRNUM and then looks at Frame Entry of this frame. If it is valid, it then continues to execute transfer structure it points to. If it moves on transfer descriptor, and this transfer descriptor has active bit set, controller will process this descriptor. If it moves on Queue Head, it will continue down by vertical pointer. When it will find first transfer descriptor with active bit, it will transfer it. Then it will look at Depth bit in transfer descriptor. If it is clear, it will move by horizontal pointer in Queue Head, if it is set, it will move to structure pointed to by descriptor. When last transfer descriptor is processed (terminate bit is set), controller continues through horizonal pointer of last loaded Queue Head.
Execution of one frame lasts 1 millisecond. If controller did not went through all memory structures, it will abandon execution of them and move on next frame. After every frame is number in FRNUM incremented, and rolls over after value 1023, because there are 1024 frame entries.
Control and bulk transfers
You need to create Queue Head which vertical pointer points to first Transfer Descriptor, and then all Transfer Descriptors are connected after one another. Then you need to insert Queue Head to be executed in every frame for fastest transfer. You will probably want interrupt fired after execution of last Transfer Descriptor in list.
Example of control transfer that transfers USB Descriptor with length 18 bytes from low speed device:
FRAME ENTRY: 0x10000 | (1 << 1) (Queue Head at 0x10000)
0x10000: QUEUE HEAD
Horizontal Pointer: 0x00000001 (invalid pointer)
Vertical Pointer: 0x20000 (Transfer Descriptor at 0x20000)
0x20000: TRANSFER DESCRIPTOR
Type: Setup
Toggle: 0
Length of transfer: 8 bytes
Pointer: 0x20020 | (1 << 2) (Transfer Descriptor at 0x20020, Depth bit set)
0x20020: TRANSFER DESCRIPTOR
Type: In
Toggle: 1
Length of transfer: 8 bytes
Pointer: 0x20040 | (1 << 2) (Transfer Descriptor at 0x20060, Depth bit set)
0x20040: TRANSFER DESCRIPTOR
Type: In
Toggle: 0
Length of transfer: 8 bytes
Pointer: 0x20060 | (1 << 2) (Transfer Descriptor at 0x20080, Depth bit set)
0x20060: TRANSFER DESCRIPTOR
Type: In
Toggle: 1
Length of transfer: 2 bytes
Pointer: 0x20080 | (1 << 2) (Transfer Descriptor at 0x20080, Depth bit set)
0x20080: TRANSFER DESCRIPTOR
Type: Out
Toggle: 1
Length of transfer: 0 bytes
Pointer: 0x00000001 (invalid pointer)
Interrupt transfers
You need to create Queue Head that points to one Transfer Descriptor. Then you need to insert Queue Head to be executed every X millisecond as endpoint needs. It is okay to request transfer a bit earlier, e.g. endpoint require execution every 10ms, but you ask for it every 8ms. You will probably want interrupt fired after execution of this Transfer Descriptor. Unlike OHCI and EHCI, UHCI controller will not reverse toggle bit after transfer, so when you will reactivate Transfer Descriptor, you will need to do it by yourself.
Isochronous transfers
Isochronous transfers are organized as list of Transfer Descriptors that are directly pointed to from Frame Entry before any Queue Head is processed. It is because it is mandatory for controller to transfer all isochronous transfers in one frame to assure same data rate every millisecond.
Frame Memory Structure
One way of managing transfers is to have few Queue Heads that will be transferred at 2X milliseconds, like Queue Head that will be transferred every millisecond, Queue Head that will be transferred every 2 milliseconds, Queue Head at every 4 milliseconds and so on. If you manage Queue Heads in this way, you can be sure that when any of them is called, in same frame should be also called all Queue Heads with lower millisecond time. So after you create them, you can connect them by horizontal pointers from slowest to fastest:
QH 32 ms -> QH 16 ms -> QH 8 ms -> QH 4 ms -> QH 2 ms -> QH 1 ms -> invalid pointer
All vertical pointers needs to be invalid. Now you can insert those Queue Heads to Frame List. To ensure that every Queue Head is called at right time, you should insert Queue Heads to frames that are divisible by their millisecond number. So for example frame 1 should point to QH 1 ms, frame 4 should point to QH 4 ms, QH 2 ms and QH 1 ms. Because you already connected slower Queue Heads to faster Queue Heads, you can insert slowest Queue Head to Frame list. Here is example how to do this:
#define UHCI_QUEUE_HEAD_POINTER (1 << 1)
uint32_t *frame_list = (uint32_t *) uhci_controller_frame_list_base;
for(int i = 0; i < 1024; i++) {
if((i % 32) == 0) {
frame_list[i] = (&queue_head_32_ms | UHCI_QUEUE_HEAD_POINTER);
}
else if((i % 16) == 0) {
frame_list[i] = (&queue_head_16_ms | UHCI_QUEUE_HEAD_POINTER);
}
else if((i % 8) == 0) {
frame_list[i] = (&queue_head_8_ms | UHCI_QUEUE_HEAD_POINTER);
}
else if((i % 4) == 0) {
frame_list[i] = (&queue_head_4_ms | UHCI_QUEUE_HEAD_POINTER);
}
else if((i % 2) == 0) {
frame_list[i] = (&queue_head_2_ms | UHCI_QUEUE_HEAD_POINTER);
}
else {
frame_list[i] = (&queue_head_1_ms | UHCI_QUEUE_HEAD_POINTER);
}
}
Now when you have Queue Head for transfer, you can easily insert it to transfer structure. For example if you want to insert new queue head to be transferred every 8 milliseconds, you simply do:
new_queue_head->horizontal_pointer = queue_head_8_ms->horizontal_pointer;
queue_head_8_ms->horizontal_pointer = (&new_queue_head | UHCI_QUEUE_HEAD_POINTER);
And when you need to remove Queue Head after transfer is complete, you simply do:
// previous_queue_head points to queue head which we want to remove
previous_queue_head->horizontal_pointer = queue_head_to_be_removed->horizontal_pointer;
When removing Queue Heads you need to be sure that controller is not now processing them. One way is to mark all Transfer Descriptor pointers as invalid, so even if controller is actually processing one of them, it will immediately stop, and move on to next Queue Head.
If you want to transfer isochronous transfer, you need to prepare all transfer descriptors for particular frame, then set value from that Frame Entry to last transfer descriptor pointer, and then insert pointer to first descriptor to Frame Entry. After frame is done, all isochronous descriptors should be transferred, and you will need to prepare new descriptors for new frame. It is good idea to set descriptors for at least two frames ahead, so when your interrupt routine updates new frame, you will not miss actually processed frame.