Friday, 30 November 2018

Resetting a USB device and reading Config Data

Foreword

In the previous post we discussed a bit of theory surrounding USB communications and started to implement some interrupts from the USB functional block in the Zynq.

In this post we will get a bit more practical and see how to reset a USB device and to read configuration information from it.

In this post we will not be implementing functionality to detect when a device is plugged or unplugged, in order to keep things simple. We will thus assume a USB keyboard is attached on the USB port when we start up.

The Life Cycle of a USB Device

To get a bit of context of for this post, let us look at the life cycle of a USB device. This is illustrated by the following state diagram (taken from the official USB 2.0 specification):


The attached state is the state when you have just attached a USB device into the USB port.

Provided the USB host enabled a voltage between the VCC pin and GND pin on the USB port, the USB port will power up and enter the Powered state shortly after attachment.

It should be noted, though, that when a USB Host was just powered up, no voltage will be present on the VCC+GND pins. It is up to you to configure the USB Hub so that power is enabled over these two pins.

Once a USB is in the powered state, it still will not respond to any Host commands over the ports. You first need to apply a reset over the USB port so that the device enter the Default state.

When a USB device is in the default state, it will respond to traffic on device address 0 and on endpoint 0.

It should be noted that the USB device will not stay in the default state for long, probably for a couple of tens of milliseconds, at most. It is up to you to get the device to the address state as soon as possible.

At the addressed state the USB will be assigned a non zero-address and all subsequent communication will be directly to this new address.

For the USB device to become fully functional it needs to transition to the configured state.

For the purpose of this post, we will just be moving to the default state and requesting a device descriptor and requesting a configuration descriptor. More on these descriptors later on.

Switching Port power on and resetting device

Let us get to writing some code.

First thing we should do, is to switch the USB module to Host mode. For this we need to use the lower three bits of register 0xe00021a8. The function of these three bits is defined as follows:


  • 00 (default): Idle
  • 01: resrerved
  • 10: Controller device mode
  • 11: Controller in host mode
This corresponds to the following code:

void initUsb() {
 Xil_Out32(0xE00021A8, 3);//set to host mode
}

int main()
{
      Xil_DCacheDisable();
      init_platform();
      initint();
      initUsb();
      usleep(100000000);
      cleanup_platform();
      return 0;
}


Next, we should switch on the port power. Bit 12 of register 0xE0002184 performs this task for us. So let us extend our method initUsb:

void initUsb() {
 Xil_Out32(0xE00021A8, 3);//set to host mode
 u32 in2 = Xil_In32(0xE0002184) | 4096;
 Xil_Out32(0xE0002184, in2); //switch port power on
}


The code above will bring our USB device into the power state. Next we need to reset the device to bring it into the Default state. Bit 8 of register 0xe0002184 is used to initiate port reset. So, let us create the following method to assert reset and to de-assert the reset:

void set_port_reset_state(int do_reset) {
  u32 in2;
  if (do_reset) {
 in2 = Xil_In32(0xE0002184) | 256;
 Xil_Out32(0xE0002184, in2);
  } else {
 in2 = Xil_In32(0xE0002184) & (~256);
 Xil_Out32(0xE0002184, in2);
  }

}


Now, if one read through the USB 2.0 specification, it looks like we need to allow at least 12ms for USB device to come out of reset. Here we will make use of the state_machine method we defined in the previous post to assist in scheduling the 12ms delay:

...
void state_machine() {
   u32 in2 = Xil_In32(0xE0002144) | (1<<24) | (1<<18);
   Xil_Out32(0xE0002144, in2); //clear

   if (status == 0) {
    set_port_reset_state(1);
    scheduleTimer(12000);
    status = 1;
    return;
   } else if (status == 1) {
    set_port_reset_state(0);
    status = 2;
   } else if (status == 2) {
    printf("\n");
   }


}
...
int main()
{
     Xil_DCacheDisable();
     init_platform();
     initint();
     initUsb();
     status = 0;
     state_machine();  
     usleep(100000000);
     cleanup_platform();
     return 0;
}


A you might remember from the previous post, we are using the state_machine method as a callback when an interrupt happens within the USB block of the Zynq. Here, however we are also calling it from the main method. We do this just as the initial state for our state machine.

In this initial state we assert the port reset and schedule the timer for 12ms. After the 12ms an interrupts will trigger and the state_machine method will be called again. This time around we will de-assert the port reset. At this stage our USB keyboard should be in the default state listening for USB traffic on address 0.

It is in this default state we can read the device descriptor and the configuration descriptor from the USB device.

In order to read these descriptors from the device, we need to schedule an asynchronous schedule, which we briefly touched on in the previous post. To schedule this, we need to know more about the following datastructures: Q Head (QH) and q Transfer descriptors (qTD). We will discuss this in the next section

Q Heads and Transfer descriptors

Let us have a look at the QH and qTD data structures.

First the QH data structure. This structure is discussed within the Zynq TRM on page 463. The following table summarises this data-structure:

The first word in the structure is a pointer to the next QH. For our Asynchronous schedule, the first QH will just be pointing to itself.

The second word have a couple of fields of importance:

  • RL (NAK counter reload): For our case we will just use a value of 15
  • C (Control endpoint flag). Set this field to a one if it is a non High Speed, control endpoint. We will indeed set this field to one in our case.
  • Maximum Packet length: We will be setting this field to 8.
  • H (Head of reclamation list). Set this value to one, since we be having one, and one only QH
  • DTC (Data toggle control). Set to one
  • EPS (End Point Speed):
    • 00: Full Speed
    • 01: Low Speed (What we will be using)
    • 10: High Speed
    • 11: Reserved
  • EndPt (End point address): Since we will be using using the Control Endpoint, this value will be set to zero
  • I: Set to zero
  • Device Address: Since we will be operating when the device is in the default state, we will use device address zero
For the third word, I will not going into detail. We will just be using the value 0x40000000.

You will see that the remaining words is coloured in grey and according to the legend this means Host Controller Read/Write. We will leave all these zero, accept for Next qTD Pointer, in which we will specify the first qTD.

Let us now move unto the qTD structure. This structure is discussed on page 459 in the Zynq TRM. The following table summarise this data-structure:


The first word is a pointer to the next qTD structure.

The third word contains the following fields:
  • DT: Data toggle
  • Total Bytes: To Bytes to receive from or send to USB device
  • IOC: Cause an interrupt when this transfer is finished
  • C_Page (Current Page): Index to the current buffer (e.g. 0 to 4)
  • Cerr (Error counter)
  • PID (PID Code): More on this in the following section
    • 00 Out
    • 01 In
    • 02 Setup
  • Status
  • Buffer Pointers 0 to 4: Four pointers, each of whicg points to a 4KB buffer. This contains the data received from or to send to the USB device that is assoaiated with this transfer descriptor.

Data Transfers from USB devices

One of the data-structures we covered in the previous section was transfer descriptors. A Transfer descriptor is what its name implies, that is to transfer data to or from the USB device.

According to the USB 2.0 specification, you get a couple of different types of Transfer, but in this post we will only focusing on one type: Control Transfers. The following web page does quite a good job of explaining control transfers, together with some diagrams:

https://www.beyondlogic.org/usbnutshell/usb4.shtml#Control

This web page basically states that a Control Transfer can be broken down into a couple stages. To narrow down the number of scenarios, I am just going to focus on one particular use case: Getting a device descriptor from the USB device via the Control endpoint.

With this use case in mind, let us have a look at the different stages for a Control transfer.

Setup stage

The following diagram taken from previously mentioned web page, explains the setup setup stage:

The setup stage starts by issuing a setup token to the USB device. This indicates to the USB device what kind of data is about to follow.

Next follows a data packet. In our use case where want to request a device descriptor, this packet would contain this request to the USB device as such.

The USB would acknowledge the whole request with a ACK packet, indiacted by the white block in the diagram.

This whole stage would be taken care of by a transfer descriptor as discussed in the previous section. Interestingly, for this stage a PID would specified.

The data packet for this request would be contained in a buffer pointed to buffer pointer 0 contained in word 3 of the relevant qTD.

The Data Stage

The following diagram explain the Data stage:

This diagram from Beyondlogic outlines two scenarios. The first scenario is when we expect data back from the USB device and the second scenario is is we are required to send data to the USB device after the setup phase.

For our Use case we are only interested in the first scenario. For this we need need to setup a qTD with a PID of one. The Buffer pointer in this qTD will be a pointer to the buffer that will receive the data from the USB device during the Data Packet Phase.

Don't worry about the Handshake packet for now. This will be covered in the next phase.

The Status Stage

With our Use case we are receiving data from the USB Device, so it is up to us to acknowledge this data during the status stage. The following diagram illustrates this:


For this stage we need to create a qTD with a PID of zero. Since we will be sending a data packet of zero length, we don't need to specify a valid buffer pointer in this qTD.

Initialising the Async queue

Now with a bit of theory behind, let us write some again. This time we will initialise the async queue.

Since our application is a bare-metal application, we will not be making use of malloc calls to allocate memory for our data structures. Instead, we will use some specific memory locations for our data-structures.

We start off by clearing the memory region we will be using for our data structures:

void initUsb() {
 Xil_Out32(0xE00021A8, 3);//set to host mode
 u32 in2 = Xil_In32(0xE0002184) | 4096;
 Xil_Out32(0xE0002184, in2); //switch port power on

 for (int i = 0; i < 1000000; i = i + 4) {
  u32 current = 0x300000 + i;
  u32 *currentword;
  hello = currentword;
  *currentword = 0;
 }
}


This will clear 1 Million bytes worth of words to zero starting at address 0x300000.

Next, we should setup a QH and a couple of qTD's. To assist us with this, we first need to create a helper data structure, making it easy to navigate through the 4 byte word nature of these data-structures:

struct QStruct {
  u32 word0;
  u32 word1;
  u32 word2;
  u32 word3;
  u32 word4;
  u32 word5;
  u32 word6;
  u32 word7;
};


We can now continue and create a QH:

void initUsb() {
...
 struct QStruct *qh;
 qh = 0x300000;
 qh->word0 = 0x300002;
 qh->word1 = 0xf808d000; //enable H bit -> head of reclamation
 qh->word2 = 0x40000000;
 qh->word3 = 0;
 qh->word4 = 0x300040;// pointer to halt qtd
 qh->word5 = 1;// no alternate

}


This QH starts at memory location 0x300000. The next pointer points back to itself (e.g. the first word).

You will also release that this pointer ends with 2 instead of zero. This is because bit 1 and 2 actually represents the head type of the pointer, which in this case is a QH.

Word 4 is a pointer to the first qTD of this QH, which starts at address 0x300040.

Let us now have a look at the qTD:

void initUsb() {
...
 struct QStruct *qTD;
 qTD = 0x300040;
 qTD->word0 = 1; //next qtd + terminate
 qTD->word1 = 0; // alternate pointer
 qTD->word2 = 0x40; //halt value// setup packet 80 to activate
}


Word one has the value 1, menaing there is not a valid next qTD.

For word2 we specify a value of 0x40. This create us a async schedule in the halt state. We can now enable the async schedule:

void initUsb() {
...
 Xil_Out32(0xE0002158,0x300000); // set async base
 in2 = Xil_In32(0xE0002140) | 0x1;
 Xil_Out32(0xE0002140,in2); //enable rs bit
 in2 = Xil_In32(0xE0002140) | 0x20;
 Xil_Out32(0xE0002140,in2); // enable async processing

}


The async schedule is started by setting bit 5 of register 0xe0002140. Once enabled, the scheduler looks at register 0xe0002158 as the location for the first QH.

As mentioned, this async is now in the halt state. We need to add additional qTD's to make this schedule do something useful.

We will cover this in the next section.

Setting up a Transfer

Int he previous section we managed to enable an async, although not a very useful one: everything is in the halted state!

Let us start by creating a method for enabling a useful transfer:

void schedTransfer(int setup, int direction, int size, u32 qh_add) {

}

Setup specify whether we should add a setup qTD.

Direction specify whether we want to receive or send data. For receiving direction should be a 1.

Size is the number of bytes we want to send or receive.

qh_add is the address of the QH at which we want to add the qTD's.

If we require a setup token, we convert the halt qTD to a setup qTD:

void schedTransfer(int setup, int direction, int size, u32 qh_add) {
   struct QStruct *qh;
   qh = qh_add;
   u32 first_qtd = qh->word4;
   struct QStruct *firstTD;
   struct QStruct *nextTD;
   firstTD = first_qtd;
   nextTD = first_qtd;
   if (setup) {
     firstTD->word0 = calNextPointer(first_qtd); //next qtd + terminate
     firstTD->word1 = 1; // alternate pointer
     firstTD->word2 = 0x00080240; //with setup keep haleted/non active till everything setup
     firstTD->word3 = 0x301000; //buffer for setup command

    }
}

You will see that the lower eight bits of word2 is still 0x40. This means that our queue will remain in the halt state till we change it another value.

Next, we should add the remaining qTD's:

void schedTransfer(int setup, int direction, int size, u32 qh_add) {
...
    if (size > 0) {
       if (setup)
         nextTD = calNextPointer(first_qtd);

       nextTD->word0 = calNextPointer(nextTD); //next qtd + terminate
       nextTD->word1 = 1; // alternate pointer

       nextTD->word2 = (size << 16) | (direction << 8) | (nextTD == firstTD ? 0x40 : 0x80) | 0x80000000;


       if (direction == 0)
   nextTD->word2 = nextTD->word2 | 0x8000;
       nextTD->word3 = setup ? 0x302000 : 0x301000; //buffer for setup command

       nextTD = calNextPointer(nextTD);

       if (direction == 1) {
   nextTD->word0 = calNextPointer(nextTD); //next qtd + terminate
   nextTD->word1 = 1; // alternate pointer
   nextTD->word2 = 0x80008080; //with setup keep haleted/non active till everything setup
   nextTD->word3 = 0x301000; //buffer for setup command
       }
    } else {
     //size = 0
     nextTD = calNextPointer(first_qtd);
        nextTD->word0 = calNextPointer(nextTD); //next qtd + terminate
        nextTD->word1 = 1; // alternate pointer
        nextTD->word2 = (0 << 16) | (1 << 8) | (nextTD == firstTD ? 0x40 : 0x80) | 0x80000000 | 0x8000;

    }
    if (nextTD == firstTD)
     nextTD->word2 = nextTD->word2 | 0x8000;
 nextTD = calNextPointer(nextTD);
 nextTD->word0 = 1; //next qtd + terminate
 nextTD->word1 = 1; // alternate pointer
 nextTD->word2 = 0x40; //with setup keep haleted/non active till everything setup
 nextTD->word3 = 0x301000; //buffer for setup command
}

The last qTD is again a halt qTD.

This code is also written as such so that the last qtd executed creates an interrupt.

Once all the qTD's has been setup, we can mark the first qTD in the sequence as runnable:

  firstTD->word2 = (firstTD->word2 & (~0x40)) | 0x80;

One final method that should be implemented is calNextPointer:

u32 calNextPointer(u32 currentpointer) {
 currentpointer = currentpointer - 0x300040;
 currentpointer = currentpointer + 0x20;
 if (currentpointer > 0x200)
  currentpointer = 0;
 return currentpointer + (u32)0x300040;
}


This method advances to the next address and return to 0x300040 after a couple of advances, in effect simulating a circular buffer.

Reading a descriptor from USB Device

With the method created in the previous section, we can now use it to read a descriptor from the USB device.

To get the descriptor we need to schedule the transfer with the command request stored in a buffer. The USB 2.0 spec give us an indication on how this request should look like (on page 250):


Let us have a look at the values. We start with bmRequestType with value 0x80.

For bmRequest we need to use the constant GET_DESCRIPTOR. To get this value scroll down to the next page of the USB 2.0 spec and you will see the value is 6.

Descriptor Type is retrieved from the next table and have value 1 for Descriptor type DEVICE.

Wlength has value 0x12.

We can now modify our state_machine method to send a DEVICE_DESCRIPTOR request:

void state_machine() {
   u32 in2 = Xil_In32(0xE0002144) | (1<<24) | (1<<18);
   Xil_Out32(0xE0002144, in2); //clear

   if (status == 0) {
    set_port_reset_state(1);
    scheduleTimer(12000);
    status = 1;
    return;
   } else if (status == 1) {
    set_port_reset_state(0);
    status = 2;
    //device descriptor
                  Xil_Out32(0x301000, 0x01000680);
                  Xil_Out32(0x301004, 0x00120000);
                  schedTransfer(1,1,0x12, 0x300000);
   } else if (status == 2) {
    printf("\n");
   }
}


As seen, we are writing the request to the buffer at address 0x301000.

The descriptor returned by the USB device will be stored at location 0x302000. The else statement for status 2 can be used to print the contents of this buffer. Let us have a look at the contents of the buffer:

302000:   01100112
302004:   08000000
302008:   0C231A2C
30200C:   02010110
302010:   00000100
302014:   00000000

Since bytes are stored with an ARM core is little endian, for each word you should read the bytes from right to left.

So, we start off with 0x12 which is the length of the Descriptor.

0x1 is the Descriptor typem which in this case is DEVICE.

The next two bytes indicates the USB version which in this case is 1.1.

The next couple of bytes gives information of the Device class which is zero for three bytes. This means more info about the device is provided in the Configuration descriptor.

Following that is the maximum packet size which is 8 bytes.

Then there is a couple of vendor, product versions.

The last number of the descriptor is a 1 meaning that there is only one possible configuration.

That concludes our discussion on getting and reading the DEVICE descriptor.

Reading the Configuration Descriptor

Time for us to read the configuration descriptor. For this wee need to modify our state_machine method again:

void state_machine() {
   u32 in2 = Xil_In32(0xE0002144) | (1<<24) | (1<<18);
   Xil_Out32(0xE0002144, in2); //clear
   if (status == 0) {
     set_port_reset_state(1);
     scheduleTimer(12000);
     status = 1;
     return;
   } else if (status == 1) {
     set_port_reset_state(0);
     status = 2;
     //configuration descriptor
     Xil_Out32(0x301000, 0x02000680);
     Xil_Out32(0x301004, 0x003b0000);
     schedTransfer(1,1,0x3b, 0x300000);
   } else if (status == 2) {
     printf("\n");
   }
}


This time our we get back 0x3b bytes. In effect this data contains a number of descriptors where each one start with the number of bytes:

09 02 3B 00 02 01 00 A0 32
09 04 00 00 01 03 01 01 00
09 21 10 01 00 01 22 36 00
07 05 81 03 08 00 0A
09 04 01 00 01 03 00 00 00
09 21 10 01 00 01 22 32 00
07 05 82 03 08 00 0A

Let us discuss these descriptors.

The first descriptor:

  • Descriptor type: 2 -> Configuration descriptor
  • 0x3b -> overall length of all desriptors
  • 02 -> number of interfaces
  • 01 -> value to select this configuration
  • 00 -> string index for textual description. In this case none available
  • 0xA0 -> couple of attributes
  • 0x32 -> max power in 2mA units. In this case 100mA

From the configuration descriptor we see that two interfaces are defined. Each interface has the descriptor type number 4. Further more, byte 5 of this interface descriptor specify the interface type. For both these interfaces this is type 3, which is an HID(Human interface device).

The next two bytes of interest of the Interface descriptors is byte 6 and 7. For the first interface descriptor these bytes are 1 and 1, whereas for the second one it is 0 and 0.

For the first interface these two bytes corresponds to the following:

  • Boot interface Subclass
  • Keyboard
This type of Interface is a simplified keyboard interface for BIOS's and we will indeed use this Interface for our design.

Let us now go and have a look at the endpoint for this Interface.

07 05 81 03 08 00 0A

0x81 specify that this is an IN endpoint and the address of this endpoint is 1.

The 3 specifies that this in an interrupt endpoint.

The 8 specifies that the maximum packet size for this endpoint is 8 bytes.

The 0x0A specify the polling interval in milliseconds. Thus in this case the polling interval is 10 milliseconds.

In Summary

In this post we managed to read a couple of descriptors from a USB keyboard and isolated an endpoint that we can use to read keystrokes from the keyboard.

In the next post we will attempt to read keystrokes from the USB keyboard.

Till next time!

No comments:

Post a Comment