Friday, 23 November 2018

Getting started with USB Protocols

Foreword

In the previous post we managed to implement the flashing cursor and keyboard interaction.

At this point in time a keypress can only be simulated by running a program on the ARM core that writes a value to a specific register and we are not yet at a point of integrating a physical keyboard to our C64 system.

The goal I have in mind is to integrate with a USB keyboard. To do this there is an easy way and a difficult way.

The easy way is way is to make use of PetaLinux, a Linux distribution provided by Xilinx for the Zynq processor. Going this route will give you some USB and keyboard drivers simplifying our integration with a USB keyboard, avoiding to worry about the technicalities of the USB protocol.

Then there is the difficult route, trying to access the USB keyboard in Standalone mode. In standalone mode you cannot make use of the drivers that comes bundled with Linux, and you sort of need to re-invent the wheel for USB keyboard interaction.

Re-inventing the wheel is not really cool, but it gave me some second thoughts. I have been using USB devices for almost 18 years without knowing  how the communication work between the PC and a USB device.

Going the difficult route is actually an opportunity to learn how USB works and in the next couple of posts (two or three?) we are going to do just that.

We are going to start off with a bit of theory on USB protocols and will gradually work our way to a practical implementation.

I am not going to implement a full USB protocol stack, but just bear minimum that is necessary to catch keystrokes from a USB keyboard.

A note about the source code

I have received a couple of requests to publish the full source code for the project in its current state.

I recently done via the following link on Github: https://github.com/ovalcode/c64fpga

Within the Readme.md I am going some instructions on how to create the project files and building the project.

USB Protocol Overview

When you plug a USB keyboard into your PC your PC is known as the USB host and the keyboard as the USB device. This relationship is also depicted by the block diagram below:


A USB can support one or more functions shown by the blue blocks above. Let us have a quick look at an example where a USB device can support more than one function.

Say, for example, a manufacturer brings out a USB-webcam. The manufacturer might also decide to also ship the device drivers on the web cam itself and surface it to your PC as a Mass Storage Device.

Cool solution, but how will your PC differentiate between these two functionalities on the same set of USB wires?

The answer is to give each functionality an endpoint number. When your PC communicates with the USB device, it always need to provide an endpoint number so that the USB device knows for which function the message is intended for.

Let us now move onto the topic on how USB devices are addressed. USB devices are connected to the PC in a star topology.

Star topology is the same topology used on a Commodore 64 to attach multiple drives and printers to the single serial port on the C64.

A quirk with the star topology is that all devices can see all traffic of each other. To avoid confusion a unique address needs to be assigned to each device.

On C64 disk drives you make use of jumpers on each drive to assign the address.

On USB devices you don't  have jumpers. So, how are the addresses assigned?

The answer is that you just need to reset a device, then it will be in the default address state and respond on requests on Address zero.

At this point the alert reader will say: 'Aha! You just said USB uses a start topology, so won't reset signal reset all the USB devices?'

Yes it is, but a USB reset signal is one of the signals a USB host has finer control of, and you can limit a reset to a specific to a specific USB port.

Let us conclude our Overview of USB by having a look at how communication is orchestrated between the devices and the Host.

USB communication among the devices and the host is orchestrated by means of Host polling.

This means that the host initiates all communication. Even if a USB device have some information that urgently needs the attention of the host it needs to wait for the host to ask it for the information.

EHCI

Back to the ZYBO board. If you have a look in the ZYNQ 7000 manual, you will see that it provide some information on how to establish communication between your Zybo board and a USB device.

However, when working on your USB implementation, you will probably find that the information provided within this Technical Reference manual is simply not enough to give you a clear direction on where to go.

Doing an Internet search on how to implement USB on the Zybo board will probably also not be fruitful either.

I almost went into despair over this, till I found that the USB specific registers on the Zynq is not specific to the Zynq only, but follows a specific standard known as ECHI (Enhanced Host Controller Interface).

The thought that the USB implementation was not Zynq specific, actually widened my horizon and immediately was able to find more implementation examples. In fact, I could find a nice example within the Linux source tree.

To communicate with USB devices, the EHCI standard defines two schedules into which you can queue USB communication requests: Periodic schedule & Asynchronous schedule.

You make use of the Periodic schedule if you want to poll specific USB devices for information at specific time intervals. This will typically be for USB devices like USB sound cards giving a stream of information at a fixed data rate. The following diagram taken from the Zynq technical reference explains how the periodic schedule is implemented:
We will cover in more detail how this schedule is setup at a later stage.

There might be cases where you don't want to poll a USB device constantly for information, but in a more adhoc fashion as the need arises. For this you will make use of asynchronous queues. The following diagram, also takne from the Zynq reference manual, explains how asynchronous queues works:

The interesting part in the diagram is where it mentions Insert and Remove QH's as needed, just reiterating its adhoc nature. When you are at a point of not needing any information from the Async Queue at the moment, you will just have a Queue Head pointing to itself been in one or other Halt state.

When you suddenly need some information again from a particular USB device, you can add a Queue head to the Queue, which will be processed and the Queue will return back to a halt state.

We will cover setup of the Asynchronous queue also at a later stage.

Writing some code

We have covered quite of theory. Let us now see if we can start writing some code.

Programming a USB interface have quite some detail, and one can a bit overwhelmed so that you don't know where to start. But, we can always start with small steps.

Let us start with the following:

  • Getting Caching right
  • Enabling Interrupts
Starting with getting the Caching right. In order to set up a periodic queue or an async queue, you need to write some data structures directly to SDRAM. When an ARM core writes these structures, the data-structures might not end up in SDRAM straight away, but will linger for some time in an L1 or an L2 datacache.

There are a couple of ways to deal with this potential caching issue. I am just going to take a simple approach and disable the Data Cache all together:

#include <stdio.h>
#include "xil_exception.h"
#include "xparameters.h"
#include "platform.h"
#include "xil_printf.h"
#include "xil_cache.h"
#include "xil_io.h"
#include "xscugic.h"
#include "xgpiops.h"
#include <unistd.h>

int main()
{
    Xil_DCacheDisable();
    init_platform();
    cleanup_platform();
    return 0;
}


I have added so long most of the common headers we will need over time for our USB exercise. The headers together with the associated libraries is provided by the Xilinx SDK when you compile your program as a standalone.

Let us us now move onto interrupts. The USB module present within the Zynq provides interrupts for two timer expiry events, and USB events like when transfers is completed. These are very useful interrupts indeed which we would like to intercept.

It is therefore necessary to enable the above mentioned interrupts and ensure one of our custom methods gets called when they happen.

To configure interrupts on the Zynq (and probably most ARM based SoC's) is quite a mission. The complexity is illustrated by the following block diagram:

So, in effect you need to figure how to program the Generic Interrupt controller and then how to enable interrupts on the ARM processor.

Luckily the Xilinx SDK provided some wrappers for shielding most of the complexity for us.

A strip down version for enabling the interrupts will looks as follows:

...
int help;
int myhelp;
XScuGic_Config *IntcConfig;
XScuGic INTCInst;
...
void state_machine();
...
void initint() {

 IntcConfig = XScuGic_LookupConfig(0);
 int status;
 myhelp = 1;

 status = XScuGic_CfgInitialize(&INTCInst, IntcConfig, IntcConfig->CpuBaseAddress);
 Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
         (Xil_ExceptionHandler)XScuGic_InterruptHandler,
      &INTCInst);
 Xil_ExceptionEnable();
 status = XScuGic_Connect(&INTCInst,
         53,
         (Xil_ExceptionHandler)state_machine,
         (void *)myhelp);
 XScuGic_Enable(&INTCInst, 53);

}
...
void state_machine() {
}
...
int main()
{
    Xil_DCacheDisable();
    init_platform();
    initint();
    cleanup_platform();
    return 0;
}


Within the method initint we basically configure the GIC and enable interrupts on the ARM processor.

Also, in the code we are enabling Shared Peripheral Interrupt (SPI) #53. All USB block related interrupts will trigger via this interrupt.

We also configure so that our custom method state_machine will be called each time a  SPI interrupt #53 happens. We will fill the method state_machine during the course of time.

You might realise that the method call XScuGic_Connect accepts a fourth parameter, which in this case we just passed a pointer to an integer called myhelp.

Usually for this parameter you will pass a pointer to a driver structure and when an interrupt happens, your interrupt handler (which in this case is state_machine) will receive this pointer as a parameter.

In our case we will not be using this parameter in our interrupt handler. Instead, we be implement a state machine within our interrupt handler and define a global status variable that will keep track of the current state.

One final thing needs to be done with our interrupt initialisation and this is to enable the applicable USB interrupts.

We would like to enable the General Purpose Timer Interrupt 0 (GP0). We also would like to enable Async Interrupts so that an interrupt is triggered when an Asynchronous transfer has completed.

This would be enabled as follows:

void initint() {
...
 u32 in2 = Xil_In32(0xE0002148) | (1<<24) | (1<<18);
 Xil_Out32(0xE0002148, in2); //enable
}


Scheduling Timers

During our journey to create a USB interface one of the things we will often do is to schedule a timer to wait a certain amount of time before performing the next task.

One can certainly use the sleep or usleep function provided by the SDK wrappers, but I am not so sure how accurate those are.

For the purpose of scheding timers, I am going to make use of General purpose timer 0 provided by the USB block.

This timer works in a very similar fashion as timers you find on a CIA 6526. You load a timer value into a load register, and then force this value to load into a running a timer register. The timer will count down from the predetermined value until it reaches zero and cause an interrupt.

On the Zynq, the timer load value register is located at 0xe0002080. This counter clocks at 1MHz (exactly the same as the CIA on C64). This register is 24 bits wide and can thereforebe set to up to 16 seconds.

To read the current value of the timer you need to read memory location 0xe0002084. The current timer value is present in the lower 24 bits. Bit 31 and bit 30 of this register is also of impotance for us:

  • Bit 31: Timer enable
  • Bit 30:  Timer reset. When setting this bit to a one the timer will be reloaded with the value stored in location 0xe0002080
With this information, it is clear that we should setup the timer using the following steps:

  • Load the required timer value into 0xe0002080
  • Reload the timer by writing 1 to bit 30 of 0xe0002084
  • Start the timer by writing a 1 to bit 31 of 0xe0002084
This translates to the following method:

void scheduleTimer(int usec) {
 //set timer value
 Xil_Out32(0xE0002080, usec);
 //reload timer
 Xil_Out32(0xE0002084, 0x40000000);
 Xil_Out32(0xE0002084, 0x80000000);
}

We can take this method for a test run by making the following changes in code. Let us assume we want to wait 3 seconds:

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

  printf("Timer finished\n");
}

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


I added the usleep in the main method so that our program isn't exited prematurely.

After 3 seconds the message Timer finished will be displayed on the console.

The write to location 0xe0002144 at the beginning of the state machine needs to done to ensure that the interrupt that just happened is cleared. Without this state_machine will be executed in an endless loop.

In Summary

In this post we covered some theory regarding USB.

We also started to write some for disabling data caching and enabling the appropriate USB interrupts.

In the next post we will implement functionality for resetting a USB device and configuration info from it in its default state.

Till next time!

No comments:

Post a Comment