12 January 2009

USB part 4 – Inner workings of the PicPack USB stack

This tutorial covers the main USB functionality and how the library actually goes about setting up the pic to carry out USB transactions and transfers. You don’t need to understand exactly how all this happens, but it’s helpful to know what the functions do and how to use them.

Our first point is to setup the USB sub-system. As is our convention with the PicPack library, naturally, you call the usb_setup routine:

  void usb_setup() {
      usb_state = st_POWERED;
  

Our state is now powered, meaning that we have power applied over the USB bus. The next task is to initialise the hardware:

      // init hardware
    clear_bit(ucfg, UTRDIS); // enable internal tranceiver
  

Most designs involving pics use the internal transceiver. You can connect the pic to an external piece of hardware, or connect it directly to the chip itself. Here we assume that you’re doing the direct connection. USB has (as of version 2.0) three speeds – low speed (around 1Mbps), full speed (around 12Mbs) and high speed (around 480Mbps). The pics that we’re playing with only operate in the low/full range, meaning that we have to select between low and full speed.

      set_bit(ucfg, FSEN); // clear for low speed, set for high speed
  

In all the examples here, we select full speed. There’s no reason not to, really, and as it happens, selecting full speed gives us many more choices for clock frequency selection. More about that in a later tutorial.

USB indicates its speed request by pull-ups on the data pins. The pic hardware can do this function for us (meaning we don’t need to include external pull-up resistors), providing we enable the on-chip pull-ups:

      set_bit (ucfg, UPUEN); // enable on-chip pull-ups
  

The pic USB Serial Interface Engine (SIE) can double buffer transfers, meaning that one buffer is being used by the SIE while the other is available to the microcontroller. This means faster transfer at the cost of more complex code. In the initial PicPack library, we switch double buffering off, for the sake of simplicity:

    clear_bit(ucfg, PPB1); // disable double buffering
    clear_bit(ucfg, PPB0);

Finally, we set up end point 0 ready to receive and send control transfers:

    set_bit(uep0, EPHSHK); // EP0 handshaking on
    set_bit(uep0, EPOUTEN); // EP0 OUT enable
    set_bit(uep0, EPINEN); // EP0 IN enable
    clear_bit(uep0, EPCONDIS); // EP0 control transfers on (and IN and OUT)
  

Here we enable handshaking (required for control transfers – and all endpoints type except isochronous), enable OUT (from host) and IN (to host) transfers, and enable control transfers on this endpoint.

Due to a bug in the BoostC compiler that prevents compile time initialisation of structures that don’t point to char*, we need some run-time initialisation of this structure.

    ep_out_bd_location[0] = &bd0out;

This continues for all buffer descriptors for IN and OUT endpoints.

In order to kick off interrupts for the USB module, in our main program we turn on interrupts from the USB module:

    turn_usb_ints_on();
    turn_global_ints_on();
  

Finally, to actually start the USB module doing its things, we call usb_enable_module().

void usb_enable_module() {
    uir = 0;
    set_bit(ucon, USBEN); // enable USB serial interface engine (SIE)
    usb_state = st_DEFAULT;
}

Here we clear any previously triggered interrupts, enable the USB SIE module and set our state to Default.

It’s at this point that all the fun starts. The SIE now attaches to the bus by pulling one of the data lines high, and negotiations begin.

1 comment:

Jörgen said...

Nice work and nice reading:-)