5 August 2008

USB Part 2 - The Joy Mouse

In this example, I’m going to show the use of the library with the TechToys USB experimenter’s board. This is a great board to try these sorts of experiments with – it has an SD card slot to play with mass storage class devices (along with understanding the SD card protocol) and a 5 way joystick, supporting up, down, left, right and select. This allows us to simulate a mouse with our joystick. Since the mouse is part of the USB specification that Windows supports without (third party) drivers, this is a nice way to get our feet wet with USB without having to do any hard work on the Windows side. You could also prototype this circuit using a 18f4550 pic on a breadboard with a few switches if you preferred.

Let’s look firstly at the way we handle delivering the descriptors to the host. Pull up the usb_joy_mouse demo and have a look in the usb_config_mouse.c file. Here’s where all the mouse specific stuff sits to hide it away from the main program.

void usb_get_descriptor_callback(uns8 descriptor_type,
                                 uns8 descriptor_num,
                                 uns8 **rtn_descriptor_ptr,
                                 uns16 *rtn_descriptor_size) {
You have to supply a usb_get_descriptor_callback function in your program. The PicPack library will call it when it has received a request for a particular descriptor. Your job is to return a pointer to where to find the descriptor, and how big the descriptor is.

The standard descriptors are defined in pic_usb.h. Here’s the device descriptor:

typedef struct _device_descriptor {
    uns8 length,
    uns16 usb_version; // BCD
    uns8 device_class,
    uns8 max_packet_size_ep0;
    uns16 vendor_id,
            device_release; // BCD
    uns8 manufacturer_string_id,
} device_descriptor;
Now, the descriptors really are just a chunk of bytes, so you don’t need to use proper C structs like this to hold them. You could happily use a string of bytes and return a pointer to that along with the length. The reason we use C structs here is that you are much less likely to make a mistake getting things working. Once you have things working, feel free to replace the C structs with data structures that take less space or are based in ROM (with appropriate changes to the library). As always, my motto is get things working, then get them working smaller/faster. Did you know you can hang Windows by plugging in a device with a dodgy descriptor in it? I didn’t, until I started this USB work. Believe me, it’s a frustrating exercise! Who would have thought that Windows would be so easily duped? So, in these examples, we always use the structs to ensure we have everything right in the descriptors

Now, back in usb_config_mouse.c, we define our device descriptor like this:

device_descriptor my_device_descriptor = {
    sizeof(my_device_descriptor), // 18 bytes long
    dt_DEVICE, // DEVICE 01h
    0x0110, // usb version 1.10
    0, // class
    0, // subclass
    0, // protocol
    8, // max packet size for end point 0
    0x04d8, // Microchip's vendor
    0x000C, // Microchip's product
    0x0200, // version 2.0 of the product
    1, // string 1 for manufacturer
    2, // string 2 for product
    0, // string 3 for serial number
    1 // number of configurations
In our get descriptor callback function, here’s where we return this data:

void usb_get_descriptor_callback(uns8 descriptor_type, uns8 descriptor_num,
                                 uns8 **rtn_descriptor_ptr, uns16 *rtn_descriptor_size) {
         uns8 *descriptor_ptr;
    uns16 descriptor_size;
         descriptor_ptr = (uns8 *) 0; // this means we didn't find it
    switch (descriptor_type) {
        case dt_DEVICE:
            serial_print_str(" Device ");
            descriptor_ptr = (uns8 *)&my_device_descriptor;
            descriptor_size = sizeof(my_device_descriptor);
Notice that we use temporary variables for the descriptor pointer and its size. It’s only at the end of the function that we copy these into the rtn_descriptor_ptr and rtn_descriptor_size. This saves instructions since the pic instruction set doesn’t make it particularly easy to deal with double-dereferenced data.

Have a look through the rest of the function. You can see how we return descriptors for the device, but also notice how when the host requests the configuration descriptor, it actually gets sent the configuration descriptor, the endpoint descriptors along with any class descriptors as well! The function also returns string descriptors, which are used to identify the device in nice plain language. Note that the string descriptors are in Unicode – a 16 bit value for each character. Luckily for us, in English, all you need to do is at a \0 null to each character. To be fair, almost all USB devices have only English string descriptors.

Once enumeration has finished, it is simply a matter of sending data when we want to indicate that the mouse has moved or a button has been pressed. The trick with all USB transfers is that you need to put the data into a buffer before it is requested. In this case, it is not so much of a problem since the pic will NAK any request for data when the endpoint has not been “primed” (or “armed” – all data loaded into the buffer and the pic USB engine informed that it now has control of the buffer).

The host will ask for data at the interval specified in the descriptors. This does mean that there’s a time gap between when we want to send data and when it actually gets requested. This is the side-effect of a system where the host controls all the transfers. This latency is not going to be noticed for mice or keyboards, but can make a difference for time-critical transfers like MIDI data or even serial data. You can send a bunch of data really quickly – but only so often.

In any case, getting back to the joy mouse, notice that we

clear_bit(intcon2, RBPU);
which turns on the weak pull-up resistors for port B inputs, which we then make inputs by:

make_input(JOY_PORT, UP_PIN);
make_input(JOY_PORT, DOWN_PIN);
make_input(JOY_PORT, LEFT_PIN);
make_input(JOY_PORT, RIGHT_PIN);
make_input(JOY_PORT, CENTER_PIN);
and kick off the whole USB excitement by:

Nothing will happen from a USB perspective until we enable the USB module:

This routine allows you to “soft-insert” the device. It can be plugged in, powered and running, but only when you enable the USB serial interface module, will the USB side of things kick into life.

When there has actually been some joystick movement or the select button pressed or released, this data is sent to the PC using the usb_send_data routine:
usb_send_data(1, (uns8 *)&buffer, 3, /*first*/ 0); // ep 1

The first parameter is the endpoint number. In this case, we’re sending data from endpoint 1. We also pass a pointer to the buffer, the size of the buffer, and a helper to indicate whether this transfer is the first one. This is important since USB uses two alternating packet types (DATA0 and DATA1) when sending data so that it knows if one was lost. The PicPack library sets up endpoints so that you normally don’t need to know if you’re sending the first packet or not (the data packet type is set to the DATA1 one on initialisation, so that before the first packet is sent, it is toggled to become DATA0. However, there may be occasions that you need to force the packet data type back to DATA0, in which case, pass 1 for the last parameter.

The JoyMouse implements everything USB-wise that is absolutely required, and nothing that isn’t. You can plug a JoyMouse into both Windows and Linux and it will work perfectly fine. In the next tutorial, we’ll dig a little deeper into the PicPack usb library.

1 comment:

Unknown said...

i am having problem with pic18f2550
with internal 3.3v transreceiver on
when i connect device it's gets filed . USBView show the deivce but could not be started