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.
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.
void usb_get_descriptor_callback(uns8 descriptor_type,
uns8 descriptor_num,
uns8 **rtn_descriptor_ptr,
uns16 *rtn_descriptor_size) {
The standard descriptors are defined in pic_usb.h. Here’s the 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
typedef struct _device_descriptor {
uns8 length,
descriptor_type;
uns16 usb_version; // BCD
uns8 device_class,
device_subclass,
device_protocol;
uns8 max_packet_size_ep0;
uns16 vendor_id,
product_id,
device_release; // BCD
uns8 manufacturer_string_id,
product_string_id,
serial_string_id,
num_configurations;
} device_descriptor;
Now, back in usb_config_mouse.c, we define our device descriptor like this:
In our get descriptor callback function, here’s where we return this data:
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
};
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.
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);
break;
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
which turns on the weak pull-up resistors for port B inputs, which we then make inputs by:
clear_bit(intcon2, RBPU);
and kick off the whole USB excitement 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);
Nothing will happen from a USB perspective until we enable the USB module:
usb_setup();
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.
usb_enable_module();
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.