-
-
Notifications
You must be signed in to change notification settings - Fork 2
08. String Table and Last Steps
Lastly, we will implement the string table and pass all the calls through to the usb_config
, so it can decide where to route each endpoint. Also we'll add a working Virtual COM Port into the mix to get a working device.
The string table contains readable strings to describe the device. At the index 0xEE is the Microsoft OS Descriptor, which allows for tighter integration with the OS. The zero-string contains a list of all supported languages.
For strings, we will only support english localization. This can be easily expanded if necessary. First we will add the functionality to provide strings in the usb_config
typedef struct {
unsigned char Length;
unsigned char Type;
} USB_DESCRIPTOR_STRINGS;
char *USB_GetString(char index, short lcid, short *length) {
// Strings need to be in unicode (thus prefixed with u"...")
// The length is double the character count + 2 — or use VSCode which will show the number of bytes on hover
if (index == 1) {
*length = 10;
return u"Test";
} else if (index == 2) {
*length = 28;
return u"My Controller";
}
return 0;
}
char *USB_GetOSDescriptor(short *length) {
return 0;
}
The strings need to be in Unicode, otherwise they will show as gibberish. Now we can expand the Get Descriptor
-Request to also answer string requests. We need to take care, not to send more characters than were requested, otherwise the os might shut the device down because of USB_BABBLE_DETECTED
.
case 0x06: // Get Descriptor
switch (setup->DescriptorType) {
case 0x01: { // Device Descriptor
...
} break;
case 0x02: { // Configuration Descriptor
...
} break;
case 0x03: // String Descriptor
if (setup->DescriptorIndex == 0) { // Get supported Languages
USB_DESCRIPTOR_STRINGS data = {
.Length = 4,
.Type = 0x03,
};
// Don't use prepare transfer, as the variable (buffer) will become invalid once we leave this block
USB_CopyMemory(&data, EP0_Buf[1], 2);
*((short *)(&EP0_Buf[1][2])) = 0x0409;
BTable[0].COUNT_TX = 4;
USB_SetEP(&USB->EP0R, USB_EP_TX_VALID, USB_EP_TX_VALID);
} else {
short length = 0;
char *data = 0;
if (setup->DescriptorIndex == 0xEE) { // Microsoft OS Descriptor
data = USB_GetOSDescriptor(&length);
} else {
data = USB_GetString(setup->DescriptorIndex, setup->Index, &length);
}
short txLength = length + 2;
txLength = MIN(length, setup->Length);
USB_DESCRIPTOR_STRINGS header = {
.Length = length,
.Type = 0x03};
USB_CopyMemory(data, EP0_Buf[1] + 2, txLength - 2);
USB_CopyMemory(&header, EP0_Buf[1], 2);
BTable->COUNT_TX = txLength;
USB_SetEP(&USB->EP0R, USB_EP_TX_VALID, USB_EP_TX_VALID);
}
break;
case 0x06: // Device Qualifier Descriptor
USB_SetEP(&USB->EP0R, USB_EP_TX_STALL, USB_EP_TX_VALID);
break;
}
break;
First we need to configure the additional endpoints. For this we add a callback in the usb_config
and usb
code respectively.
usb.c / usb.h
typedef struct {
unsigned char EP;
unsigned char RxBufferSize;
unsigned char TxBufferSize;
unsigned short Type;
void (*RxCallback)(short length);
} USB_CONFIG_EP;
typedef struct {
char *Buffer;
char Size;
void (*RxCallback)(char ep, short length);
void (*TxCallback)(char ep, short length);
} USB_BufferConfig;
static USB_BufferConfig Buffers[16] = {0};
static void USB_DistributeBuffers() {
// This function will organize the USB-SRAM and assign RX- and TX-Buffers
int addr = __USBBUF_BEGIN + sizeof(BTable);
for (int i = 0; i < 16; i++) {
if (Buffers[i].Size > 0) {
Buffers[i].Buffer = addr;
addr += Buffers[i].Size;
if (addr & 0x01)
addr++;
} else {
Buffers[i].Buffer = 0x00;
}
}
}
void USB_SetEPConfig(USB_CONFIG_EP config) {
if (config.EP > 0 && config.EP < 8) {
unsigned char rxSize = config.RxBufferSize;
unsigned char txSize = config.TxBufferSize;
if (rxSize & 0x01)
rxSize++;
if (txSize & 0x01)
txSize++;
Buffers[config.EP * 2].Size = config.RxBufferSize;
Buffers[config.EP * 2 + 1].Size = config.TxBufferSize;
Buffers[config.EP * 2].CompleteCallback = config.RxCallback;
Buffers[config.EP * 2 + 1].CompleteCallback = 0;
USB_DistributeBuffers();
if (rxSize > 0) {
BTable[config.EP].ADDR_RX = __MEM2USB(Buffers[config.EP * 2].Buffer);
}
if (txSize > 0) {
BTable[config.EP].ADDR_TX = __MEM2USB(Buffers[config.EP * 2 + 1].Buffer);
}
BTable[config.EP].COUNT_TX = 0;
if (rxSize < 64) {
BTable[config.EP].COUNT_RX = (rxSize / 2) << 10;
} else {
BTable[config.EP].COUNT_RX = (1 << 15) | (((rxSize / 32) - 1) << 10);
}
// only allow to set ep type & kind
short epConfig = config.Type & 0x0700;
epConfig |= USB_EP_TX_NAK;
epConfig |= config.EP;
if (rxSize > 0) {
epConfig |= USB_EP_RX_VALID;
}
USB_SetEP((&USB->EP0R) + 2 * config.EP, epConfig, USB_EP_DTOG_RX | USB_EP_RX_VALID | USB_EP_TYPE_MASK | USB_EP_KIND | USB_EP_DTOG_TX | USB_EP_TX_VALID | 0x000F);
}
}
usb_config.c
void USB_ConfigureEndpoints() {
// Configure all endpoints and route their reception to the functions that need them
USB_CONFIG_EP Notification = {
.EP = 1,
.RxBufferSize = 0,
.TxBufferSize = 8,
.Type = USB_EP_INTERRUPT};
USB_CONFIG_EP DataEP = {
.EP = 2,
.RxBufferSize = 64,
.TxBufferSize = 64,
.RxCallback = CDC_HandlePacket,
.Type = USB_EP_BULK};
USB_SetEPConfig(Notification);
USB_SetEPConfig(DataEP);
}
Lastly we need to expand the USB-Reset to also configure the endpoints via the config-callback.
void USB_LP_IRQHandler() {
if ((USB->ISTR & USB_ISTR_RESET) != 0) {
...
Buffers[0].Buffer = EP0_Buf[0];
Buffers[0].Size = 64;
Buffers[1].Buffer = EP0_Buf[1];
Buffers[1].Size = 64;
// Prepare for a setup packet (RX = Valid, TX = NAK)
USB_SetEP(&USB->EP0R, USB_EP_CONTROL | USB_EP_RX_VALID | USB_EP_TX_NAK, USB_EP_TYPE_MASK | USB_EP_RX_VALID | USB_EP_TX_VALID);
USB_ConfigureEndpoints();
// Enable USB functionality and set address to 0
DeviceState = 0;
USB->DADDR = USB_DADDR_EF;
...
Now we'll add some functions to send and receive data, from status packets as well as other endpoints.
Status packets can obtain an optional data stage that will follow the status message. Currently we handle the status packet as soon as it arrives, but e.g. for the Set Line Coding
-Request we need to wait for the data. Therefore we will only call the setup handler once we received all expected data.
First, add the expected maximum number of data bytes during a setup to the config, as we need to allocate some memory for it.
#define USB_MaxControlData 64
Then create said buffer in the usb.c
typedef struct {
USB_SETUP_PACKET Setup;
USB_TRANSFER_STATE Transfer;
USB_TRANSFER_STATE Receive;
} USB_CONTROL_STATE;
static char ControlDataBuffer[USB_MaxControlData] = {0};
void USB_Init() {
...
ControlState.Receive.Buffer = ControlDataBuffer;
...
}
Then expand the USB_HandleControl
to wait for optional data.
static void USB_HandleControl() {
if (USB->EP0R & USB_EP_CTR_RX) {
// We received a control message
if (USB->EP0R & USB_EP_SETUP) {
// On Setup, ditch all running receptions and start anew
USB_SETUP_PACKET *setup = EP0_Buf[0];
USB_HandleSetup(setup);
USB_CopyMemory(setup, &ControlState.Setup, sizeof(USB_SETUP_PACKET));
ControlState.Transfer.Length = 0;
ControlState.Receive.Length = 0;
// If this is an OUT Transfer and we expect data, postpone handling the setup until the data arrives
if ((setup->RequestType & 0x80) == 0 && setup->Length > 0) {
ControlState.Receive.Length = setup->Length;
ControlState.Receive.BytesSent = 0;
} else {
USB_HandleSetup(&ControlState.Setup);
}
} else {
// Check if we are expecting data for a setup-packet. If so, read it and call the Setup-Handler once the transfer is complete
if (ControlState.Receive.Length > 0) {
if (ControlState.Receive.BytesSent < USB_MaxControlData) {
USB_CopyMemory(EP0_Buf[0], ControlState.Receive.Buffer + ControlState.Receive.BytesSent, MIN(USB_MaxControlData - ControlState.Receive.BytesSent, BTable[0].COUNT_RX & 0x1FF));
ControlState.Receive.BytesSent += MIN(USB_MaxControlData - ControlState.Receive.BytesSent, BTable[0].COUNT_RX & 0x1FF);
}
if (ControlState.Receive.BytesSent >= ControlState.Receive.Length) {
USB_HandleSetup(&ControlState.Setup);
ControlState.Receive.Length = 0;
} else if (ControlState.Receive.BytesSent >= USB_MaxControlData) {
USB_SetEP(&USB->EP0R, USB_EP_TX_STALL, USB_EP_TX_VALID);
ControlState.Receive.Length = 0;
}
}
}
USB_SetEP(&USB->EP0R, USB_EP_RX_VALID, USB_EP_CTR_RX | USB_EP_RX_VALID);
}
if (USB->EP0R & USB_EP_CTR_TX) {
...
}
}
Thats it for the control data. Now to receiving and sending all the other data. We first have to store the Transfer-State of all Endpoints somewhere.
static USB_TRANSFER_STATE Transfers[7] = {0};
The Transmit-Function will prepare the transfer structure and initiate the first chunk.
void USB_Transmit(char ep, char *buffer, short length) {
// Prepare the transfer metadata and initiate the chunked transfer
if (ep == 0) {
ControlState.Transfer.Buffer = buffer;
ControlState.Transfer.BytesSent = 0;
ControlState.Transfer.Length = length;
USB_PrepareTransfer(&ControlState.Transfer, &USB->EP0R, EP0_Buf[1], &BTable[0].COUNT_TX, 64);
} else if (ep < 8) {
Transfers[ep - 1].Buffer = buffer;
Transfers[ep - 1].Length = length;
Transfers[ep - 1].BytesSent = 0;
USB_PrepareTransfer(&Transfers[ep - 1], (&USB->EP0R) + ep * 2, Buffers[ep * 2 + 1].Buffer, &BTable[ep].COUNT_TX, Buffers[ep * 2 + 1].Size);
}
}
char USB_IsTransmitPending(char ep) {
USB_TRANSFER_STATE* tx;
if (ep == 0) {
return ControlState.Transfer.Length > 0;
tx = &ControlState.Transfer;
} else {
return Transfers[ep - 1].Length > 0;
tx = &Transfers[ep - 1];
}
return tx->Length > 0;
}
At this point we will also implement an optional timeout mechanism. If we use IsTransmitPending
on a broken transmission, the call will block the next transmission indefinitely otherwise. If you fire your transmissions slowly, there is no need to wait for older transmissions. But in fast transmissions you might corrupt the ongoing transmission if you don't check if the line is free. For this we first define the threshold in milliseconds:
// Disable this define to disable the timeout feature
#define USB_TXTIMEOUT 50
Then we add the necessary extensions to all the definitions and functions:
#ifdef USB_TXTIMEOUT
extern unsigned int sys_now();
#endif
typedef struct {
unsigned short Length;
unsigned short BytesSent;
#ifdef USB_TXTIMEOUT
unsigned int Timeout;
#endif
unsigned char *Buffer;
} USB_TRANSFER_STATE;
static void USB_PrepareTransfer(USB_TRANSFER_STATE *transfer, short *ep, char *txBuffer, short *txBufferCount, short txBufferSize) {
// Check if there is still data to transmit and if so transmit the next chunk of data
*txBufferCount = MIN(txBufferSize, transfer->Length - transfer->BytesSent);
#ifdef USB_TXTIMEOUT
transfer->Timeout = sys_now();
#endif
[...]
}
char USB_IsTransmitPending(char ep) {
USB_TRANSFER_STATE* tx;
if (ep == 0) {
return ControlState.Transfer.Length > 0;
tx = &ControlState.Transfer;
} else {
return Transfers[ep - 1].Length > 0;
tx = &Transfers[ep - 1];
}
#ifdef USB_TXTIMEOUT
if(sys_now() - tx->Timeout > USB_TXTIMEOUT) {
tx->Length = 0;
}
#endif
return tx->Length > 0;
}
Reception on the contrary is quite straight-forward
void USB_Fetch(char ep, char *buffer, short *length) {
// Read data from the RX Buffer
if (ep >= 0 && ep < 8) {
short rxcount = BTable[ep].COUNT_RX & 0x1FF;
*length = MIN(rxcount, *length);
USB_CopyMemory(Buffers[ep * 2].Buffer, buffer, *length);
}
}
First we need to activate the HighPriority-Interrupt in USB_Init
which is called on Isochronous and Bulk-Transfers.
NVIC_SetPriority(USB_HP_IRQn, 8);
NVIC_EnableIRQ(USB_HP_IRQn);
The HP-Handler will be our default handler for handling everything but control transfers. Therefore we expand the LP-Handler CTR to route all trafic on endpoints other than the control endpoint to the HP-Handler.
void USB_LP_IRQHandler() {
if ((USB->ISTR & USB_ISTR_RESET) != 0) {
...
} else if ((USB->ISTR & USB_ISTR_CTR) != 0) {
// Route EP0 to the control handler, everything else to the HP handler
if ((USB->ISTR & USB_ISTR_EP_ID) == 0) {
USB_HandleControl();
} else {
USB_HP_IRQHandler();
}
} ...
}
Then we implement the RX- and TX-Behaviour of the HP-Handler to route RX to the handler specified in the buffer and to continue TX-Transfers if necessary.
void USB_HP_IRQHandler() {
// Only take care of regular transmissions
if ((USB->ISTR & USB_ISTR_CTR) != 0) {
char ep = USB->ISTR & USB_ISTR_EP_ID;
if (ep > 0 && ep < 8) {
// On RX, call the registered callback if available
if ((*(&USB->EP0R + ep * 2) & USB_EP_CTR_RX) != 0) {
if (Buffers[ep * 2].CompleteCallback != 0) {
Buffers[ep * 2].CompleteCallback(ep, BTable[ep].COUNT_RX & 0x01FF);
}
USB_SetEP(&USB->EP0R + ep * 2, USB_EP_RX_VALID, USB_EP_CTR_RX | USB_EP_RX_VALID);
}
// On TX, check if there is some remaining data to be sent in the pending Transfers
if ((*(&USB->EP0R + ep * 2) & USB_EP_CTR_TX) != 0) {
if (Transfers[ep - 1].Length > 0) {
if (Transfers[ep - 1].Length > Transfers[ep - 1].BytesSent) {
USB_PrepareTransfer(&Transfers[ep - 1], &USB->EP0R + ep * 2, &Buffers[ep * 2 + 1].Buffer, &BTable[ep].COUNT_TX, Buffers[ep * 2 + 1].Size);
} else if (Transfers[ep - 1].Length == Transfers[ep - 1].BytesSent) {
char length = Transfers[ep - 1].Length;
Transfers[ep - 1].Length = 0;
if (Buffers[ep * 2 + 1].CompleteCallback != 0) {
Buffers[ep * 2 + 1].CompleteCallback(ep, length);
}
// if complete and no new TX, add one empty packet to flush queue, send tx complete signal
if (Transfers[ep - 1].Length == 0) {
BTable[ep].COUNT_TX = 0;
USB_SetEP(&USB->EP0R + ep * 2, USB_EP_TX_VALID, USB_EP_TX_VALID);
}
}
}
USB_SetEP(&USB->EP0R + ep * 2, 0x00, USB_EP_CTR_TX);
}
}
}
}
Now the last packets to route are interface and class-specific setup packets. For this we need a handler in the usb_config
and some logic in the USB_HandleSetup
.
usb_config.c
char USB_HandleClassSetup(USB_SETUP_PACKET *setup, char *data, short length) {
// Route the setup packets based on the Interface / Class Index
return CDC_SetupPacket(setup, data, length);
}
usb.c / usb.h
#define USB_OK 0
#define USB_BUSY 1
#define USB_ERR 2
static void USB_HandleSetup(USB_SETUP_PACKET *setup) {
if ((setup->RequestType & 0x60) != 0) {
// Class and interface setup packets are redirected to the class specific implementation
char ret = USB_HandleClassSetup(setup, ControlState.Receive.Buffer, ControlState.Receive.Length);
if ((setup->RequestType & 0x80) == 0) {
if (ret == USB_OK) {
BTable[0].COUNT_TX = 0;
USB_SetEP(&USB->EP0R, USB_EP_TX_VALID, USB_EP_TX_VALID);
} else if (ret == USB_BUSY) {
USB_SetEP(&USB->EP0R, USB_EP_TX_NAK, USB_EP_TX_VALID);
} else {
USB_SetEP(&USB->EP0R, USB_EP_TX_STALL, USB_EP_TX_VALID);
}
}
} else if ((setup->RequestType & 0x60) == 0) {
if ((setup->RequestType & 0x0F) == 0) { // Device Requests
...
The implementation for the COM-Port is rather short and easy, now that the hard stuff is taken care of.
cdc_device.c
#include "cdc_device.h"
char buffer[64];
char lineCoding[7];
char CDC_SetupPacket(USB_SETUP_PACKET *setup, char *data, short length) {
// Windows requires us to remember the line coding
switch (setup->Request) {
case CDC_CONFIG_CONTROLLINESTATE:
break;
case CDC_CONFIG_GETLINECODING:
USB_Transmit(0, lineCoding, 7);
break;
case CDC_CONFIG_SETLINECODING:
for (int i = 0; i < 7; i++) {
lineCoding[i] = data[i];
}
return USB_OK;
break;
}
}
void CDC_HandlePacket(short length) {
// Just mirror the text
USB_Fetch(2, buffer, &length);
// do NOT busy wait. We are still in the ISR, it will never clear.
if (!USB_IsTransmitPending(2)) {
USB_Transmit(2, buffer, length);
}
}
You should now be able to connect to the COM-Port using your preferred terminal. It should mirror your keystrokes.
If it does not connect, check if the line coding is getting set and read correctly, as windows requires this to work. If it doesn't, check the return codes of the commands in Wireshark (USBpcap).
A known limitation is that the device will only mirror 64 characters at once. If you send more in one go (using e.g. HTerm) it will only mirror the first 64, because the device has not yet sent the first chunk when the second arrives. The STM32-Middleware has the same limitation for a dumb mirroring implementation.