Tutorials in this section:
In this example, we’ll have a look at the USB library to create a simple mouse. The PLT-1003 platform has two buttons, so we can’t make a whole mouse, but we can certainly move the cursor around. 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 PIC on a breadboard with a few switches if you preferred, in which case you’d be able to do more than move the cursor left and right!
First of all, let's look at the way we handle delivering the descriptors to the host. This is called enumeration in USB land, and is the first thing that happens when a USB device gets plugged in.
Pull up the ea_plt1003_usb_mouse demo (from the PicPack library - you can find it in the Tutorials | Downloads section) 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) {
The usb_get_descriptor_callback function is called when a descriptor is requested by the host. 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, 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, the descriptors really are just a chunk of bytes, so you don’t really 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, the motto is: get things working, then get them working smaller/faster. 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 };
This data is returned in the usb_get_descriptor_callback:
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;
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 (you can do it, it just takes a whole tonne of instructions).
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. As it happens, almost all USB devices have just 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 USB hardware 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 (and sent). 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 get the USB subsystem ready:
usb_setup();
Nothing will happen from a USB perspective until we enable the USB module:
usb_enable_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 mouse 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. Remember the descriptors passed during enumeration indicated to the host that we’re going to use this endpoint for what’s known as a “report”. 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.
This demo implements everything USB-wise that is absolutely required, and nothing that isn’t. You can plug a this into both Windows and Linux and it will work perfectly fine (so long as you consider a mouse that moves in one dimension "fine"). In the next tutorial, we’ll dig a little deeper into the PicPack USB library.