14.5 USB Debugger – Protocol Design

Now that you got your joystick working, you will probably be able to figure out how to construct a mouse or a keyboard device on your own. Basically, if you want to create an input only type of device, you can use the Simple Joystick example as a guideline. But what happens when you need to talk back and forth from your computer through the USB port?

To make a custom communications device, we need the device to act as both an input and an output. Because most of the provisions in the USB HID standard are for input devices, we cannot use a joystick, mouse or keyboard as a starting point. However, we can still use the HID for both inputs and outputs because both IN and OUT type endpoints are supported in the HID standard. Instead of declaring the device as defined device class (e.g. HID Joystick class, or HID Keyboard class etc), we will need to declare it as a vendor defined class. This way the operating system will still use the standard HID drivers during enumeration but will not attempt to use the device as a mouse, keyboard or joystick. This also means that we will need to write custom code on the host side to access the input and outputs of the USB device.

Let’s take a copy of the Simple Joystick code and start modifying the code to suite our needs. The plan is to create a device that takes IN and OUT type interrupt transfers (the Simple Joystick only allowed IN type transfers). On top of the USB protocol, I will also have to specify a custom communications protocol for communicating between the host and device.

A Simple Protocol Design

Before we get into any coding, let’s design a communications protocol to get data in and out of our USB device. I would like something simple and easy to understand, but have enough features so that it can be used in many different applications. This will also determine how many bytes I will need to specify in my Report Descriptor.

It would be nice to be able to send and receive at least 8 bytes of data in one transfer. This number is picked arbitrarily. Please remember that the USB interrupt transfers allow up to 64 total bytes per transfer (for full speed), so in theory we could actually increase the number of bytes transferred in one interrupt. With each of the 8 bytes of data, we need to attach some additional information. Let’s add a byte that determines the command, as well as an address field. Since 1 bytes’ worth of address seems very limited as you can only access 256 (2^8) addresses, we’ll use two bytes for the address information. Finally, we might want one more byte reserved for a command modifier. The command modifier is only used during readpage and writepage commands, and indicates how many bytes we would like to read/write during the page operation. I will explain how each field is used in just a little bit. For now, just keep in mind that we will require:

1 command byte + 2 address bytes + 1 command modifier byte + 8 data bytes = 12 total bytes

Let’s take a look at a couple of examples of how a communications would occur. First we need to define several commands and constants:

#define CMD_WR		0xA0
#define CMD_WRPAGE	0xA1
#define CMD_RD		0xB0
#define CMD_RDPAGE	0xB1

#define CMD_SUCCESS	0x10
#define CMD_ERROR	0x20

#define CMD_RESET	0x80

Remember that the host ALWAYS initiates the communications in a USB connection. Our device is exactly the same. We will also follow a strict format for all transfers. The first byte is always the command byte. Byte 2 and 3 will always indicate the address, and byte 4 will be used for the command modifier. The 8 bytes of data come thereafter. As an example, for the host to issue a read command at address 0x5AA5 to the USB device, the 12 bytes would look like this:

From host:	0xB0	0x5A	0xA5	0x00	(8 bytes of dummy data)
(12 bytes total)

Because we are merely reading data from the device, the 8 bytes of data is disregarded. The device will respond with reply as:

From device:	0xB0	0x10	0x00	0x00	(1 byte reply data)	(7 bytes of dummy data)
(12 bytes total)

The device must confirm that it has received the read command by replying with the read command in the command byte position. Since the host already issued the address, there is no need to reply back the address again. Instead we send a “success” reply, meaning that no errors has occurred during the read. There is also no need to use the command modifier. The first data byte is the data at address 0x5AA5. The 7 remaining data bytes are not used. Now let’s take a look at a writepage command at address 0×8080 with 8 bytes along with the reply from the device:

From host:	0xA1	0x80	0x80	0x08	(8 bytes of write data)
From device:	0xA1	0x10	0x00	0x00	(8 bytes of dummy data)

In this case, we used the modifier byte to indicate how many bytes in the writepage that the host would like to write to the device. The device responds with a 0xA1 command (writepage), confirming that the command has been received. The data is written to address 0×8080 to 0×8087.

The reset command puts the communications protocol to a known state. Because of the interrupt nature of the transfer, the reset command must be issued after each read/write command. A reset command will look like this:

From host: 	0x80	0x00	0x00	0x00	(8 bytes of dummy data)
From device:	0x80	0x00	0x00	0x00	(8 bytes of dummy data)

During a data exchange between the host and device, the device CANNOT know whether the host has actually received the data. This is because the USB transfers might be ignored during high CPU usage or might have been lost in the communications. To avoid this, the device must keep on sending the read/write reply until a reset message has been received from the host. After the reset message, all command and data is reset to 0×00. For example, let’s take a look at what might happen during a real set of transfers between a host and device.

At the beginning the host sends the read command, and the device replies with the data requested by the host:

From host:	0xB1	0x80	0x80	0x08	(8 bytes of dummy data)
From device:	0xB1	0x10	0x00	0x00	(8 bytes of reply data)

At this point, the device does not know whether the host has received the data. It must keep on sending the same data until a reset command acknowledges the receipt of the read data. The next set of transfers might look like this:

From device:	0xB1	0x10	0x00	0x00	(8 bytes of reply data)
From device:	0xB1	0x10	0x00	0x00	(8 bytes of reply data)

Finally, the host indicates that it has processed the data and replies with a reset:

From host: 	0x80	0x10	0x00	0x00	(8 bytes of dummy data)
From device:	0x80	0x00	0x00	0x00	(8 bytes of dummy data)
From device:	0x00	0x00	0x00	0x00	(8 bytes of dummy data)

The device acknowledges the reset command with reset command reply. The transfers after the reset are all 0×00. The host is now ready to send the next commands. The protocol is rather wasteful in terms of bytes used, but it should work rather efficiently if we use mostly writepage and readpage type commands. Since the transfers are interrupt based, wasted bytes are not really an issue anyways. We get 12 bytes of transfer on every interrupt regardless of bandwidth (up to 64 bytes if you decide to add more data bytes to this protocol).

Implementation

Now that we have defined the protocol, let’s write some code to implement our protocol. First we need to define a struct type to represent our 12 bytes of data.

//packet typedef
typedef union _USB_CONTROLS_TYPEDEF
{
	struct
	{
	    BYTE cmd;
		BYTE mod[3];
	    BYTE data[8];
	}	packet;    
} USB_CONTROLS;

I have combined the address and command modifier byte into a single array, but the rest of the stuff is pretty self explanatory. Next we construct a set of functions that read, write and handle the data as described by the protocol.

USB_CONTROLS in_packet;
USB_CONTROLS out_packet;

//main processing function for usb
void UniProcessRx(USB_CONTROLS *in, USB_CONTROLS const *out)
{
	switch(out->packet.cmd)
	{
		default:
		
		//reset commands, address and options
		case(CMD_RESET):
			in->packet.cmd = 0;
			in->packet.mod[0] = 0;
			in->packet.mod[1] = 0;
			in->packet.mod[2] = 0;
			in->packet.data[0] = 0;
			in->packet.data[1] = 0;
			in->packet.data[2] = 0;
			in->packet.data[3] = 0;
			in->packet.data[4] = 0;
			in->packet.data[5] = 0;
			in->packet.data[6] = 0;
			in->packet.data[7] = 0;
			in->packet.data[8] = 0;
			in->packet.data[9] = 0;
			in->packet.data[10] = 0;
			in->packet.data[11] = 0;
			break;
			
		//call write function pointer, then check for error
		case(CMD_WR):
			in->packet.cmd = CMD_WR;
			cmd_wr(out);
			in->packet.mod[0] = CMD_SUCCESS;
			in->packet.mod[1] = 0;
			in->packet.mod[2] = 0;
			break;
		
		//call write pagefunction pointer, then check for error
		case(CMD_WRPAGE):
			in->packet.cmd = CMD_WRPAGE;
			cmd_wrPage(out);
			in->packet.mod[0] = CMD_SUCCESS;
			in->packet.mod[1] = 0;
			in->packet.mod[2] = 0;
			break;
		
		//call write function pointer, then check for error
		case(CMD_RD):
			in->packet.cmd = CMD_RD;
			cmd_rd(in, out);
			in->packet.mod[0] = CMD_SUCCESS;
			in->packet.mod[1] = 0;
			in->packet.mod[2] = 0;
			break;
		
		//call write pagefunction pointer, then check for error
		case(CMD_RDPAGE):
			in->packet.cmd = CMD_RDPAGE;
			cmd_rdPage(in, out);
			in->packet.mod[0] = CMD_SUCCESS;
			in->packet.mod[1] = 0;
			in->packet.mod[2] = 0;
			break;
	}	
}

//control register write functions
void cmd_wr (USB_CONTROLS const *cmd){
	ctrlWrPtr(cmd->packet.mod[0]*256 + cmd->packet.mod[1], cmd->packet.data[0]);
}

void cmd_wrPage (USB_CONTROLS const *cmd){
	unsigned char i;
	for(i = 0; i < cmd->packet.mod[2]; i ++){
		ctrlWrPtr(cmd->packet.mod[0]*256 + cmd->packet.mod[1] + i, cmd->packet.data[i]);
	}	
}
//control register read functions
void cmd_rd (USB_CONTROLS *res, USB_CONTROLS const *cmd){
	res->packet.data[0] = ctrlRdPtr(cmd->packet.mod[0]*256 + cmd->packet.mod[1]);
}
void cmd_rdPage (USB_CONTROLS *res, USB_CONTROLS const *cmd){
	int i;
	for(i = 0; i < cmd->packet.mod[2]; i ++)
		res->packet.data[i] = ctrlRdPtr(cmd->packet.mod[0]*256 + cmd->packet.mod[1]+i);
}

The functions take care of all the reading, writing as well as reset for our protocol. In the next section, I will show you how to put everything together. I will modify the report descriptor and build a program using our new protocol.

Table of Contents

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>