Page 1 of 2
Trying to implement a EHCI driver
Posted: Thu Oct 13, 2022 5:37 am
by Bonfra
So as the title states, I embarked on implementing a (minimal) EHCI driver. I spent some days reading through the specs and looking at some implementations on GitHub to better understand the concepts around this controller. Still, I think I lack some previous knowledge. For example, this is a doubt that future me will have: even if I studied a bit the USB protocol I have no idea what to do with the controller once is configured; will I have two functionalities like SendPacket() and PollPacket() and receive interrupts when something is plugged/unplugged and that's it?
Anyway, my main concern is that every source of information I find online targets 32-bit architectures so I have to do some adjustments.
For example the operation register CTRLDSSEGMENT is always set to 0. This register is described by the specs as
<<Program the CTRLDSSEGMENT register with 4-Gigabyte segment where all of the interface data structures are allocated.>>
so it appears to be the upper part of some other register. makes sense every example sets it to zero since they all are targeting 32-bit architectures. What the specs aren't making clear about this register is to which other register this one is the upper part:
<<This 32-bit register corresponds to the most significant address bits [63:32] for all EHCI data structures>>
<<this register is used with the link pointers to construct 64-bit addresses to EHCI control data structures>>
does this mean that all data structures used by the controller needs to reside in the same 4G segment? isn't this a bit too constraining? not that I intend to scatter things around but with memory allocation, it may happen I guess.
As you can see I have a lot of confusion in my mind. I don't have a specific question to ask here I'd just like to have something like a checklist of things to do and how to do them correctly.
An example I can make (with the only thing I'm sure about) is for the process of resetting the controller:
- Clear the RUN/STOP bit in the USBCMD register to signal the controller it needs to finish whatever it's doing and stop executing stuff
- wait for the HCHALTED bit to be set in USBSTS as an indicator that the controller indeed stopped executing as requested above
- set HCRESET in USBCMD to issue a controller reset
- wait for HCRESET in USBCMD to be 0 (30ms or fail) meaning the reset was completed
- check that all registers equals the default value after reset indicated in the specs 4.1
this imo is the only clear part in the specs XD
PS:
Little side note about the default value:
the specs state that after a reset USBSTS should be equal to 0x1000, meaning that only the HCHALTED should be set. In all of my tests (using QEMU) after a reset this register is always set to 0x1004, so also the Port Change Detect bit is set. Is this normal?
Re: Trying to implement a EHCI driver
Posted: Thu Oct 13, 2022 6:05 am
by devc1
Before reading, I already implemented an EHCI driver and I will be happy to help you if you have a problem.
Re: Trying to implement a EHCI driver
Posted: Thu Oct 13, 2022 6:11 am
by devc1
CRLDSSEGMENT Contains the upper 32 bit of the Periodic Queue and frame list pointers, it is only usable when the EHC supports 64 bit addressing.
Yes, it is so simple :
You will reset the controller, foreach device you will receive USB_CONNECT_STATUS_CHANGE interrupt, you will just Setup the device then load its driver.
Setting up the device :
You can look in the wiki for the packet structures in the USB articles.
You will create a control queue with the Packet type SETUP_PACKET. Then send your commands, one of my previous faults (or misunderstandings) were that I was sending the USB header packets (PING, DATA0...) and none worked because of that, the EHCI already does that. So you will just send your packet request structures.
GET_CONFIGURATION, GET_DESCRIPTOR...
Re: Trying to implement a EHCI driver
Posted: Thu Oct 13, 2022 6:24 am
by Bonfra
Awesome! thanks a lot!
Well, it doesn't appear to be this hard. It's hard to believe that there are these few guides online for a thing that seems to be easier than ahci which instead is pretty well documented.
May I take a look at your implementation? while going further in my journey some more doubts could be answered by looking at others' code bases
Re: Trying to implement a EHCI driver
Posted: Thu Oct 13, 2022 1:01 pm
by devc1
My code base is private for some purposes, however I'll share with you some functions:
Resetting the EHC :
Code: Select all
BOOL EhciResetController(EHCI_DEVICE* Ehci){
Ehci->OperationnalRegisters->UsbCommand.RunStop = 0; // Stop the USB Controller
while(!Ehci->OperationnalRegisters->UsbStatus.HcHalted);
EhciAckAllInterrupts(Ehci);
Ehci->OperationnalRegisters->UsbCommand.HostControllerReset = 1;
while(Ehci->OperationnalRegisters->UsbCommand.HostControllerReset);
// All registers will be set to their initial value
return TRUE;
}
Handling EHCI Port Change interrupt which signals if a device is connected or disconnected :
The port must be reset before detecting connection status.
Code: Select all
if(!EhciResetPort(Ehci, i)){
SystemDebugPrint(L"Failed to reset port %d", i);
}
if(!Ehci->OperationnalRegisters->PortStatusControl[i].PortEnabled) {
SystemDebugPrint(L"Resetted port is not enabled.");
continue;
}
if(Ehci->OperationnalRegisters->PortStatusControl[i].CurrentConnectStatus == 1){
SystemDebugPrint(L"EHCI Device Connected to port %d. Line Status : %d", i, Ehci->OperationnalRegisters->PortStatusControl[i].LineStatus);
EhciHandleDeviceConnection(Ehci, i);
}else{
// TODO : Handle device disconnection SystemDebugPrint(L"EHCI Device Disconnected from port %d", i);
}
Ehci->OperationnalRegisters->PortStatusControl[i].ConnectStatusChange = 1;
}
Re: Trying to implement a EHCI driver
Posted: Thu Oct 13, 2022 1:06 pm
by devc1
Communicating with the device and reading its identification data :
Code: Select all
KERNELSTATUS EhciHandleDeviceConnection(EHCI_DEVICE* Ehci, DWORD PortNumber){
RFEHCI_ASYNC_QUEUE ControlQueue = EhciCreateAsynchronousQueue(Ehci, 0, 0, 0, USB_HIGH_SPEED);
RFEHCI_ASYNC_TRANSFER ControlTransfer = EhciCreateControlTransfer(ControlQueue, USB_SETUP);
if(!ControlTransfer || !ControlQueue) {
SystemDebugPrint(L"Failed to create async control queue or async control transfer");
return KERNEL_SERR;
}
USB_DEVICE_DESCRIPTOR DeviceDescriptor = {0};
USB_DEVICE_REQUEST DeviceRequest = {0};
DeviceRequest.Request = USB_DEVICE_GET_DESCRIPTOR;
DeviceRequest.RequestType = 0x80;
DeviceRequest.Value = 1 << 8;
DeviceRequest.Length = 0x12;
// SystemDebugPrint(L"USB_GET_DESCRIPTOR");
int Status = EhciControlDeviceRequest(ControlTransfer, &DeviceRequest, 0x12, &DeviceDescriptor);
if(Status != 0) return Status;
// Set Device Address
DeviceRequest.Request = USB_DEVICE_SET_ADDRESS;
DeviceRequest.RequestType = 0;
DeviceRequest.Value = EhciAllocateDeviceAddress(Ehci);
UINT DeviceAddress = DeviceRequest.Value;
DeviceRequest.Length = 0;
// SystemDebugPrint(L"DEVICE_ADDRESS : %x", DeviceRequest.Value);
Status = EhciControlDeviceRequest(ControlTransfer, &DeviceRequest, 0, NULL);
if(Status != 0) return Status;
// Set Device Address
ControlQueue->QueueHead->DeviceAddress = DeviceAddress;
// // Set Device configuration
// DeviceRequest.Request = USB_DEVICE_SET_CONFIGURATION;
// DeviceRequest.Value = 1;
// Status = EhciControlDeviceRequest(ControlTransfer, &DeviceRequest, 0, NULL);
// SystemDebugPrint(L"LENGTH : %x, DESCRIPTOR_TYPE : %x, EP0_MAX_PACKET_SIZE : %x", (UINT64)DeviceDescriptor.Length, (UINT64)DeviceDescriptor.DescriptorType, (UINT64)DeviceDescriptor.Endpoint0MaxPacketSize);
SystemDebugPrint(L"PRODUCT_ID : %x, NUM_CONFIGS : %x", DeviceDescriptor.ProductId, DeviceDescriptor.NumConfigurations);
SystemDebugPrint(L"CLASS : %x, SUB_CLASS : %x, VENDOR_ID : %x, DEVICE_PROTOCOL : %x", (UINT64)DeviceDescriptor.DeviceClass, (UINT64)DeviceDescriptor.DeviceSubclass, (UINT64)DeviceDescriptor.VendorId, (UINT64)DeviceDescriptor.DeviceProtocol);
USB_STRING_DESCRIPTOR String = {0};
if(DeviceDescriptor.SerialNumber){
EhciControlGetString(ControlTransfer, &String, DeviceDescriptor.SerialNumber);
SystemDebugPrint(L"SERIAL_NUMBER : %ls", String.String);
}
if(DeviceDescriptor.Manufacturer){
EhciControlGetString(ControlTransfer, &String, DeviceDescriptor.Manufacturer);
SystemDebugPrint(L"MANUFACTURER : %ls", String.String);
}
if(DeviceDescriptor.Product){
EhciControlGetString(ControlTransfer, &String, DeviceDescriptor.Product);
SystemDebugPrint(L"PRODUCT_NAME : %ls", String.String);
}
...... // Loading the driver and queues
Control device request :
Code: Select all
int EhciControlDeviceRequest(RFEHCI_ASYNC_TRANSFER Transfer, void* DeviceRequest, UINT NumBytes, void* Buffer){
// Refresh some data
Transfer->Qtd->BufferPointer0 = (UINT32)(UINT64)Transfer->Buffer;
Transfer->Qtd->ExtendedBufferPointer0 = (UINT32)((UINT64)Transfer->Buffer >> 32);
Transfer->Qtd->DataToggle = 0;
Transfer->Qtd->CurrentPage = 0;
Transfer->AsyncQueue->QueueHead->PortNumber = 0;
Transfer->Qtd->PidCode = EHCI_SETUP;
// Send DEVICE_REQUEST
if(EhciControlWrite(Transfer, DeviceRequest, 8) != 1) return -1;
// Set PID_CODE to IN & Receive data from the device
Transfer->Qtd->PidCode = EHCI_IN;
if(EhciControlRead(Transfer, Buffer, NumBytes) != 1) return -1;
return 0;
}
Reading/Writing control transfer data:
Code: Select all
int EhciControlWrite(RFEHCI_ASYNC_TRANSFER Transfer, void* Buffer, UINT NumBytes){
memcpy(CURRENT_QTD_PTR(Transfer->Qtd), Buffer, NumBytes);
Transfer->Qtd->TotalBytesToTransfer = NumBytes;
Transfer->Qtd->Active = 1;
for(UINT64 i = 0;Transfer->Qtd->Active;i++){
if(Transfer->Qtd->TransactionError){
SystemDebugPrint(L"TRANSACTION_ERROR");
return 0;
}
if(Transfer->Qtd->DataBufferError){
SystemDebugPrint(L"DATA_BUFFER_ERROR");
return 0;
}
if(Transfer->Qtd->BabbleDetected){
SystemDebugPrint(L"BABBLE_DETECTED");
return 0;
}
if(Transfer->Qtd->Halted){
SystemDebugPrint(L"QTD_HALTED.");
}
if(i > 1000) {
SystemDebugPrint(L"EHCI CONTROL WRITE : Device Response time exceeded. (ACTIVE = %x)", Transfer->Qtd->Active);
for(UINT c = 0;c<10;c++){
SystemDebugPrint(L"ASYNCQ : %x, QH_CQTD : %x, QH_NQTD : %x, QTD : %x, ASYNCADDR : %x", Transfer->AsyncQueue->QueueHead, Transfer->AsyncQueue->QueueHead->CurrentQtdPointer, Transfer->AsyncQueue->QueueHead->HcOverlayArea.NextQtdPointer, Transfer->Qtd, Transfer->AsyncQueue->Ehci->OperationnalRegisters->AsyncListAddress);
Sleep(100);
}
return 0;
}
Sleep(1);
}
return 1;
}
int EhciControlRead(RFEHCI_ASYNC_TRANSFER Transfer, void* Buffer, UINT8 NumBytes){
// Transfer->Qtd->BufferPointer0 = (UINT32)(UINT64)Transfer->Buffer;
// Transfer->Qtd->ExtendedBufferPointer0 = (UINT32)((UINT64)Transfer->Buffer >> 32);
void* Src = CURRENT_QTD_PTR(Transfer->Qtd);
Transfer->Qtd->TotalBytesToTransfer = NumBytes;
Transfer->Qtd->DataToggle = 1;
Transfer->Qtd->Active = 1;
for(UINT64 i = 0;Transfer->Qtd->Active;i++){
if(Transfer->Qtd->TransactionError){
SystemDebugPrint(L"TRANSACTION_ERROR");
return 0;
}
if(Transfer->Qtd->DataBufferError){
SystemDebugPrint(L"DATA_BUFFER_ERROR");
return 0;
}
if(Transfer->Qtd->BabbleDetected){
SystemDebugPrint(L"BABBLE_DETECTED");
return 0;
}
if(Transfer->Qtd->Halted){
SystemDebugPrint(L"QTD_HALTED.");
}
if(i > 1000) {
SystemDebugPrint(L"EHCI CONTROL WRITE : Device Response time exceeded. (ACTIVE = %x)", Transfer->Qtd->Active);
return 0;
}
Sleep(1);
}
memcpy(Buffer, Src, NumBytes);
Transfer->Qtd->DataToggle = 0;
return 1;
}
Reading a string output from usb device request :
Code: Select all
int EhciControlGetString(RFEHCI_ASYNC_TRANSFER ControlTransfer, void* StringDescriptor, UINT StringIndex){
USB_DEVICE_REQUEST Request = {0};
Request.Request = USB_DEVICE_GET_DESCRIPTOR;
Request.RequestType = 0x80;
Request.Value = 3 << 8 | StringIndex;
Request.Length = 8; // STEP 1 : GetStringLength
int Status = EhciControlDeviceRequest(ControlTransfer, &Request, 8, StringDescriptor);
if(Status != 0) return Status;
USB_STRING_DESCRIPTOR* StringDesc = StringDescriptor;
Request.Length = StringDesc->Length; // STEP 2 : GetString By StringDescriptorLength
ZeroMemory(StringDesc, Request.Length + 2);
Status = EhciControlDeviceRequest(ControlTransfer, &Request, Request.Length, StringDesc);
return Status;
}
Re: Trying to implement a EHCI driver
Posted: Thu Oct 13, 2022 1:09 pm
by devc1
The driver is still incomplete and I'll update it when I finish my kernel redesigning.
There are very few problems in real hardware, however this should help you as it follows exactly (or almost) the specs.
You should also read the software part in the USB2 Spec
Re: Trying to implement a EHCI driver
Posted: Thu Oct 13, 2022 1:56 pm
by nexos
TBH EHCI is a bad choice for your first USB implementation. xHCI is the most practical (albeit most complex) first choice, and UHCI or OHCI are the simplest. I would recommend OHCI over UHCI as it seems more common on hardware (AFAICT). EHCI is a bad choice because in order to be functional with all devices, you need a OHCI / UHCI driver as well. Not to mention that there are other oddities which have been discussed on this forum before.
Well, it doesn't appear to be this hard. It's hard to believe that there are these few guides online for a thing that seems to be easier than ahci which instead is pretty well documented.
USB is actually the most complicated hardware devices used by the PC except for ACPI. AHCI is probably simpler to get up and running with. There's a good reason for this too: USB is completely generic and hence must be the jack of all trades. That adds a lot of complexity.
Re: Trying to implement a EHCI driver
Posted: Thu Oct 13, 2022 4:12 pm
by BenLunt
I agree with nexos. The ECHI is not a good choice as your first implementation, if that is the case. Depending on the hardware, it may require a companion controller (OHCI or UHCI) or an external hub driver, just to see a simple device like the common mouse or keyboard.
Most hardware now, even back ten years or so, will have an xHCI. With its circular rings for transporting data, I believe it to be easier to understand than the old OHCI or UHCI, though as mentioned, the setup and initialization is a bit more complex.
As for which of the two types, UHCI or OHCI, the former is an Intel based implementation and will occupy most Intel based hardware. The latter was not initially involved with Intel, so it will occupy most non-Intel based systems; AMD, etc.
If you are implementing USB for a learning purpose, which I suggest and admire if you are, I would start with the older hardware. Most any emulator will still emulate them. The UHCI is more of a software based implementation, requiring a bit more software, while the OHCI is a bit more hardware based, letting the hardware do more of the work.
The EHCI, in my humble opinion, was a hodge-podge hack to try to implement newer, faster hardware, while still being backward compatible. Again in my humble opinion, it failed. My suggestion would be to read over some of its general documentation to get an idea of its implementation, then move on to the xHCI.
The xHCI finally got the implementation correct. There is very little software based difference in speeds. The speed difference is handled by hardware. As long as you implement the two different register sets, their differences, and know when to use one or the other, you can handle any speed device with relative ease, as it pertains to speed.
Most modern emulators will implement all four types, though there are a
few quirks you will need to know.
I will admit (again and again, for those of us who have been here a while), my most enjoyment from this hobby of ours was the implementation and documentation of the USB. I don't know it all and learn something new each time I dig into it, but it is an enjoyable topic, at least for me.
I hope you find the same enjoyment,
Ben
-
https://www.fysnet.net/the_universal_serial_bus.htm
Re: Trying to implement a EHCI driver
Posted: Fri Oct 14, 2022 1:16 am
by Bonfra
devc1 wrote:The driver is still incomplete and I'll update it when I finish my kernel redesigning.
Thanks a lot! I really appreciate that!
nexos wrote:TBH EHCI is a bad choice for your first USB implementation.
BenLunt wrote:I agree with nexos. The ECHI is not a good choice as your first implementation, if that is the case.
Yup, now I can 100% agree. It indeed is my first implementation. I discarded xHCI from the start since the goal for my OS is to be able to run not only in emulators but also on a crap PC I use for general testing, this very PC does not have any xHCI controller. So the choice was usb1: two standards; usb2: one standard. My innocent mind choosed wrong XD. Anyway, for compatibility reasons, I surely want to also implement OHCI and UHCI after EHCI (and maybe, one day, XHCI as soon as I'll get my hands on some hardware to test it)
Reading through the code posted here and some other sources online I may have found some partial answers that will allow me to stipulate some other questions in the future XD now in the process of setting all the data structures correctly before starting to probe for devices.
Usually, when I implement a driver I don't interrupt immediately if it's not strictly needed and instead choose to take a more linear approach. As for AHCI I just wait in spinloops while a command is in process. If I don't consider plug & play can I use the same approach here? linear implementation first and as soon as I understand the concept better switch to async/interrupts?
btw Ben a friend of mine gifted me a copy of your USB book its helping me a lot in the process
Re: Trying to implement a EHCI driver
Posted: Fri Oct 14, 2022 5:08 am
by devc1
Talking about AHCI, if your OS is multitasking it is very inefficient to wait on spin loops, it may even slow down the controller itself (just my imagination).
I use interrupts with AHCI, and it is much faster I don't know why but my code reads 500 mbps on a 300 mbps hard drive and it reads it with success, maybe in the futur I will discover my error.
However, AHCI is one of my drivers that work perfectly on any VM and Real Computer, and I am willing to make ATAPI support soon.
Re: Trying to implement a EHCI driver
Posted: Fri Oct 14, 2022 7:58 am
by nexos
devc1 wrote:it may even slow down the controller itself
No, why would it do that? PIO is typically
faster that DMA transfers from the controller's POV.
devc1 wrote:I use interrupts with AHCI, and it is much faster I don't know why but my code reads 500 mbps on a 300 mbps hard drive and it reads it with success,
That's impossible. If your testing in a VM, it because the buffer cache / sparse file support are hiding much of the cost.
Re: Trying to implement a EHCI driver
Posted: Fri Oct 14, 2022 9:14 am
by devc1
It is tested with almost 1gbps on qemu and 500 mbps on an old laptop
Re: Trying to implement a EHCI driver
Posted: Fri Oct 14, 2022 10:06 am
by nexos
devc1 wrote:It is tested with almost 1gbps on qemu and 500 mbps on an old laptop
Ah, I just figured the reason behind the inflated numbers on real hardware: the hard drives' internal cache. It probably is hiding some of the cost by caching data. Note that I may be wrong on this. It's just an educated guess.
Re: Trying to implement a EHCI driver
Posted: Fri Oct 14, 2022 10:08 am
by devc1
I am just curious, I can share with you the driver to check its code if you want.
However, there are no performance problems with interrupt because hard drives are already slower.