Custom class for STM32 USB device library

In case you’ve ever tried to build a USB device with a STM32 microcontroller chances are high that you’ve used the STM32 USB device library before. The library is part of what ST calls middlewares and each STM32Cube firmware package folder contains a copy of it (/Middlewares/ST/STM32_USB_Device_Library).

In combination with ST’s development platform STM32CubeIDE which now integrates STM32CubeMX (the initialization code generator) the device library allows for some very quick prototyping by basically letting you build a project skeleton without writing a single line of code. There are some caveats though…

If you’re somewhat familiar with USB you’ll have a notion of USB classes. A USB class basically tells the host what kind of device it is connected to. Depending on the class the host might load different device drivers in order to support the devices functionality. Now the curious thing about STM32CubeMX is that you absolutely must pick a device class otherwise the USB library won’t be part of code generation. Chances are the class you need is not supported or even if it is, its implementation doesn’t do what you want. Now you might get tempted to start with a class similar to your needs and simply edit descriptors, callbacks and so forth till it does work but sadly there are no customization points for editing files like there usually are with STM32CubeMX. So any time you rerun code generation all your changes will be gone. This means that you shouldn’t rely on STM32CubeMX to create USB code (apart from hardware initialization) if the functionality you need isn’t exactly covered by one of the selectable classes. The good news is that the device library itself is rather extensible and at least mediocre documented. Furthermore ST has a STM32 USB training playlist on their YouTube channel which (if you can take the horrible accent) contains some useful information.

Before we dive into the nitty-gritty of the library to add our own custom class let’s establish a baseline for what we’re actually trying to do. I’ve come up with the totally impractical example of a USB device which receives ASCII characters and echos them back with inverted case. So lowercase characters become uppercase and uppercase become lowercase. We will avoid writing our own device driver by using libusb to transmit and receive data. To add some interactivity the input string will be typed by the user and the result will be displayed in a terminal. So far so pointless. I’ll have the example running on a NUCLEO-H743ZI2 board I recently bought but any STM32 should do as long as you initialize USB (and its clock) correctly. I’ll also use STM32CubeIDE to setup the project and STM32CubeMX to generate all but the USB code (apart from initialization). To keep track of every step you can clone the git repository where each commit corresponds to a single bullet point.

  1. Initial commit
  2. Default initialized NUCLEO-H743ZI2 board
  3. Clocks, Ethernet and FreeRTOS
  4. USB device core
  5. Lower layer interface
  6. Lower layer event callbacks
  7. USB device descriptor
  8. USB device class
  9. USB configuration, interface and endpoint descriptors
  10. USB device class events
  11. Invert case and USB transmit
  12. PyUSB and udev

Initial commit

Creating a new GitLab project with a README leaves you with an initial commit.

Default initialized NUCLEO-H743ZI2 board

Let’s start by creating a new STM32 Project for the NUCLEO-H743ZI2 board in STM32CubeIDE. Compared to the Discovery boards their ain’t much on the Nucleo ones and the H743ZI2 is no exception. This is what the default initialized board looks like in the pinout view.

STM32CubeMX default initialized NUCLEO-H743ZI2

We’re going to strip it down even further.

Clocks, Ethernet and FreeRTOS

I’ve removed Ethernet and the low-speed external clock so that only 3 things remain:

  • Full speed USB
  • UART3 which conveniently gives us a virtual com port (115k baud) through an embedded STLINK-V3
  • 3 LEDs and a blue push button

Afterwards I adjusted the CPU and AHB/APB clocks to max speed because they all defaulted to only 64MHz. I’ve also added FreeRTOS which requires the HAL timebase to use a timer other than the dedicated SysTick (I picked TIM17).

To confirm that all changes work I’m going to periodically blink the LEDs on the board and print some UART message through the virtual com port. Now it’s time to run the code generation for the first time and actually write something.

Since we’ve added FreeRTOS as middleware the code generator creates a default task in main.c with an infinite loop for us.

void StartDefaultTask(void *argument)
{
  /* USER CODE BEGIN 5 */
  /* Infinite loop */
  for(;;)
  {
    osDelay(1);
  }
  /* USER CODE END 5 */ 
}

Code within the /* USER CODE */ comments is kept upon rerunning the code generation so we are free to modify everything between them. Because the 1ms delay is way too short to see the LEDs toggling with the naked eye we will increase it to 1000ms. Then we just call the HAL functions for the serial transmission with our UART handler huart3 and to toggle the outputs LD1_Pin, LD2_Pin and LD3_Pin.

void StartDefaultTask(void *argument)
{
  /* USER CODE BEGIN 5 */
  /* Infinite loop */
  for(;;)
  {
    osDelay(1000);
    HAL_UART_Transmit(&huart3, "Stm32CustomUSBDeviceClass\n", 26, 1000);
    HAL_GPIO_TogglePin(LD1_GPIO_Port, LD1_Pin);
    HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
    HAL_GPIO_TogglePin(LD3_GPIO_Port, LD3_Pin);
  }
  /* USER CODE END 5 */ 
}

If everything works correctly you should be able to receive the string inside a terminal and see the three LEDs blinking once per second.

USB device core

Now the fun part begins and we’re going to deliberately break our build. But before we do so we’ll try to get an overview over the library architecture and look at its customization points. The following figure is taken from the USB library device manual (UM1734) and represents the overall architecture.

USB device library architecture

The library is split into the two parts core and class. Those parts are also matched by the structure of the STM32_USB_Device_Library folder which contains a Core and Class subfolder. The USB Device Core is responsible for handling most USB internals which means it does the standard requests for you. Whatever can’t be processed by the core itself is dispatched to the USB Device Class which (hopefully) is registered and linked to the core.

Sadly things are not as perfectly detached as the architecture overview makes them seem. Let’s take a look at another figure from ST’s STM32 USB training material. This example shows the same architecture we’ve seen before but now with a concrete class (CDC) and some .c filenames attached.

USB device library architecture – CDC class example

The problem that’s revealed here is that we have to pretty much touch every layer but the bottom HAL one. We have to write the whole application layer which means writing the USB descriptors, calling some APIs to link the different parts together (e.g. registering our class) and, optionally, create a class interface (usbd_cdc_if.c). Inside the device driver layer we need to make sure to handle all the requests the core pipes through to our class. This basically means handling the endpoints our class is going to use correctly. Thirdly we need to write what ST calls the “lower layer interface” which basically is an adapter between the core and HAL.

To keep the changes between commits small I’ve started by only adding the core part of the library to the project. This is what the original folder structure from my current STM32H7 firmware (1.5.0) looks like.

.
├── Class
│   ├── AUDIO
│   ├── CDC
│   ├── CustomHID
│   ├── DFU
│   ├── HID
│   ├── MSC
│   └── Template
├── Core
│   ├── Inc
│   │   ├── usbd_conf_template.h
│   │   ├── usbd_core.h
│   │   ├── usbd_ctlreq.h
│   │   ├── usbd_def.h
│   │   ├── usbd_desc_template.h
│   │   └── usbd_ioreq.h
│   └── Src
│       ├── usbd_conf_template.c
│       ├── usbd_core.c
│       ├── usbd_ctlreq.c
│       ├── usbd_desc_template.c
│       └── usbd_ioreq.c
└── Release_Notes.html

Notice the _template.h/c files? Those will be our starting point for writing the lower layer interface and the USB device descriptor shortly. Since we’re going to modify those files I’ve moved them out of the device library and removed the _template suffix.

One of the things I like about all the code from ST is that it’s Doxygen compliant. I’ll make use of that and added a doxyfile (./Doxygen/Stm32CustomUSBDeviceClass.doxyfile) which runs on the library source. This will allow us to generate some graphical representations of the huge structures used inside the code.

Lower layer interface

Let’s start with implementing the lower layer interface which means modifying the usbd_conf.h/c files. As I mentioned before this layer is simply an adapter between the core and HAL. It is also comparatively easy to write because with few exceptions it’s just forwarding arguments. The list of declarations we need to cover can be found in usbd_core.h. Don’t worry about the other function prototypes I haven’t listed here yet, we’ll get to those later.

USBD_StatusTypeDef  USBD_LL_Init(USBD_HandleTypeDef *pdev);
USBD_StatusTypeDef  USBD_LL_DeInit(USBD_HandleTypeDef *pdev);
USBD_StatusTypeDef  USBD_LL_Start(USBD_HandleTypeDef *pdev);
USBD_StatusTypeDef  USBD_LL_Stop(USBD_HandleTypeDef *pdev);
USBD_StatusTypeDef  USBD_LL_OpenEP(USBD_HandleTypeDef *pdev,
                                   uint8_t  ep_addr,
                                   uint8_t  ep_type,
                                   uint16_t ep_mps);
USBD_StatusTypeDef  USBD_LL_CloseEP(USBD_HandleTypeDef *pdev, uint8_t ep_addr);
USBD_StatusTypeDef  USBD_LL_FlushEP(USBD_HandleTypeDef *pdev, uint8_t ep_addr);
USBD_StatusTypeDef  USBD_LL_StallEP(USBD_HandleTypeDef *pdev, uint8_t ep_addr);
USBD_StatusTypeDef  USBD_LL_ClearStallEP(USBD_HandleTypeDef *pdev, uint8_t ep_addr);
uint8_t             USBD_LL_IsStallEP(USBD_HandleTypeDef *pdev, uint8_t ep_addr);
USBD_StatusTypeDef  USBD_LL_SetUSBAddress(USBD_HandleTypeDef *pdev, uint8_t dev_addr);
USBD_StatusTypeDef  USBD_LL_Transmit(USBD_HandleTypeDef *pdev,
                                     uint8_t  ep_addr,
                                     uint8_t  *pbuf,
                                     uint16_t  size);
USBD_StatusTypeDef  USBD_LL_PrepareReceive(USBD_HandleTypeDef *pdev,
                                           uint8_t  ep_addr,
                                           uint8_t  *pbuf,
                                           uint16_t  size);
uint32_t USBD_LL_GetRxDataSize(USBD_HandleTypeDef *pdev, uint8_t  ep_addr);
void  USBD_LL_Delay(uint32_t Delay);

Since the HAL functions return a different status enumeration than the USB device core there is one more static function right at the beginning of usbd_conf.c which converts between them.

/*
typedef enum              typedef enum
{                         {
  HAL_OK       = 0x00,      USBD_OK   = 0U,
  HAL_ERROR    = 0x01,      USBD_BUSY,
  HAL_BUSY     = 0x02,      USBD_FAIL
  HAL_TIMEOUT  = 0x03     
} HAL_StatusTypeDef;      } USBD_StatusTypeDef;
*/
static USBD_StatusTypeDef USBD_Get_USB_Status(HAL_StatusTypeDef hal_status)
{
  if (hal_status == HAL_OK)
    return USBD_OK;
  else if (hal_status == HAL_BUSY)
    return USBD_BUSY;
  else
    return USBD_FAIL;
}

The typical pattern to implement any of the declarations is simply to forward the call to the HAL layer and then, if necessary, convert the return value. There are just three exceptions from this.

The first one is USBD_LL_Init.

extern PCD_HandleTypeDef hpcd_USB_OTG_FS;

USBD_StatusTypeDef USBD_LL_Init(USBD_HandleTypeDef *pdev)
{
  hpcd_USB_OTG_FS.pData = pdev;
  pdev->pData = &hpcd_USB_OTG_FS;
  HAL_PCDEx_SetRxFiFo(&hpcd_USB_OTG_FS, 1024 / 4);
  HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_FS, 0, 1024 / 4);
  HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_FS, 1, 1024 / 4);  
  return USBD_OK;
}

There are mainly two things happening here.

The first one is about linking handlers. When you’ve picked a USB class inside STM32CubeMX and therefor get a generated version of usbd_conf.c this is usually the function where the actual USB hardware initialization is performed. When we arrive at this point we’ve already done this though. Our generated code which initializes the USB_OTG_FS peripheral for us can be found in main.c and has already been called. However, there is another very important thing STM32CubeMX didn’t do for us. We need to link the peripheral to the device handler and vice versa. The peripheral handler is called hpcd_USB_OTG_FS by default and was already created by the code generator. The device handler is passed as argument to the function. Both handlers contain a void* pointer called pData where this link must be placed. Although I’m not very fond of this design choice this pattern will come up again.

The other thing we must take care of is distributing the dedicated USB FIFO RAM. Now this is tricky and seems to cause a lot of confusion. The STM32H743 processor comes with 4kB of dedicated RAM with a FIFO control mechanism. From that 4kB pool the amount of RAM for either all OUT endpoints or each individual IN endpoint can be freely allocated. To be honest I had first completely ignored any of that and assumed that after reset the default setting would be some kind of uniform distribution. Oh boy was I wrong. Right after reset all OUT endpoints and IN endpoint 0 each reserve 4kB. All other IN endpoints contain a reset value of 0 although the datasheet says otherwise. Unsurprisingly nothing worked on my first try so I had to rebase back to this commit and fix this issue. I ended up giving all OUT endpoints , IN endpoint 0 and IN endpoint 1 each 1kB of RAM. A word of caution, the HAL API expects the RAM size to be in words!

The second function which slightly deviates from our text-book approach are actually two functions, USBD_LL_OpenEP and USBD_LL_CloseEP.

USBD_StatusTypeDef USBD_LL_OpenEP(USBD_HandleTypeDef *pdev, uint8_t ep_addr,
                                  uint8_t ep_type, uint16_t ep_mps)
{
  HAL_StatusTypeDef const hal_status = HAL_PCD_EP_Open(pdev->pData, ep_addr, ep_mps, ep_type);
  pdev->ep_in[ep_addr & 0x7F].is_used = 1;
  return USBD_Get_USB_Status(hal_status);
}

USBD_StatusTypeDef USBD_LL_CloseEP(USBD_HandleTypeDef *pdev, uint8_t ep_addr)
{
  HAL_StatusTypeDef const hal_status = HAL_PCD_EP_Close(pdev->pData, ep_addr);
  pdev->ep_in[ep_addr & 0x7F].is_used = 0;
  return USBD_Get_USB_Status(hal_status);
}

There is a flag for each endpoint which indicates whether the endpoint is currently in use or not. Writing that flag when opening or closing an endpoint seems like a good idea.

The third function which we should probably take a closer look at is USBD_LL_IsStallEP.

uint8_t USBD_LL_IsStallEP(USBD_HandleTypeDef *pdev, uint8_t ep_addr)
{
  PCD_HandleTypeDef const *hpcd = pdev->pData;
  return ep_addr & 0x80 ? hpcd->IN_ep[ep_addr & 0x7F].is_stall 
                        : hpcd->OUT_ep[ep_addr & 0x7F].is_stall;
}

This status function makes use of the link to the peripheral handler which contains an array of IN and OUT endpoints which again each contain a stall flag. To differentiate between IN and OUT endpoints the first bit of the endpoint address is checked.

The only change usbd_conf.h needed was to include the right HAL header for the used microcontroller family (in my case to stm32h7xx.h).

Lower layer event callbacks

Now if you’ve looked closely at the architecture figure of the CDC class example you’ll have noticed that the lower layer interface must actually be bidirectional. There is an arrow from usbd_conf.c to stm32f4xx_hal_pcd.c and one going back. So far we’ve only called HAL functions from the USB device core layer but not the other way round. To change that we’ll need to implement a couple of event callbacks which will get called inside the USB interrupt handler. Those callbacks must then be forwarded to the following functions we haven’t covered yet.

USBD_StatusTypeDef USBD_LL_SetupStage(USBD_HandleTypeDef *pdev, uint8_t *psetup);
USBD_StatusTypeDef USBD_LL_DataOutStage(USBD_HandleTypeDef *pdev, uint8_t epnum, uint8_t *pdata);
USBD_StatusTypeDef USBD_LL_DataInStage(USBD_HandleTypeDef *pdev, uint8_t epnum, uint8_t *pdata);
USBD_StatusTypeDef USBD_LL_Reset(USBD_HandleTypeDef  *pdev);
USBD_StatusTypeDef USBD_LL_SetSpeed(USBD_HandleTypeDef  *pdev, USBD_SpeedTypeDef speed);
USBD_StatusTypeDef USBD_LL_Suspend(USBD_HandleTypeDef  *pdev);
USBD_StatusTypeDef USBD_LL_Resume(USBD_HandleTypeDef  *pdev);
USBD_StatusTypeDef USBD_LL_SOF(USBD_HandleTypeDef  *pdev);
USBD_StatusTypeDef USBD_LL_IsoINIncomplete(USBD_HandleTypeDef  *pdev, uint8_t epnum);
USBD_StatusTypeDef USBD_LL_IsoOUTIncomplete(USBD_HandleTypeDef  *pdev, uint8_t epnum);
USBD_StatusTypeDef USBD_LL_DevConnected(USBD_HandleTypeDef  *pdev);
USBD_StatusTypeDef USBD_LL_DevDisconnected(USBD_HandleTypeDef  *pdev);

Historically ST provides two different ways to implement those callbacks. You either overwrite a weak symbol buried somewhere in the HAL driver or you register the callbacks in the peripheral handler (which requires enabling a macro). I went with the first option and copied all the callback functions with weak symbols inside stm32h7xx_hal_pcd.c to a new file called usbd_pcd.c.

Again the naming convention makes it quite easy to link the HAL and USBD functions. Just remember that the device handler is linked to the peripheral handler through its void* pointer pData if you need to access it.

There is one declaration in the core layer which doesn’t have a HAL counterpart and that’s USBD_LL_SetSpeed. Theoretically USB speeds can change during resets (e.g. from full to high speed) so it makes sense to put this call into the reset callback but since we’re going to stick with full speed putting it inside the setup stage callback would work as well.

void HAL_PCD_ResetCallback(PCD_HandleTypeDef *hpcd)
{
  USBD_LL_SetSpeed(hpcd->pData, USBD_SPEED_FULL);
  USBD_LL_Reset(hpcd->pData);
}

USB device descriptor

Now that we’ve successfully forwarded USB requests to the actual hardware driver and fed events back into the device library we can move on to writing our USB device descriptor. The distinction of descriptor types is important because there are also configuration-, interface- and endpoint descriptors which we’ll need to implement later as well. In case you keep confusing the descriptor hierarchy like I do take a look at the following figure from KEILs USB component documentation which clears things up.

USB descriptor hierarchy

Again ST provides a pair of header/source templates for us to modify (usbd_desc.h/c). Right at the top of the source file you’ll find a couple of definitions for various strings and, more importantly, the vendor and product ID. I’ll keep the vendor but change the product ID to something ST doesn’t already use. You can check all PIDs registered to ST on the USB ID database. Feel free to modify the strings as you wish but keep them shorter than USBD_MAX_STR_DESC_SIZ / 2 – 2. That’s the size of the local buffer USBD_StrDesc which is used as intermediate storage. Since we are only going to use full speed USB I’ve deleted all occurrences and uses of the high speed strings from the original template.

#define USBD_VID                      0x0483
#define USBD_PID                      0x002A
#define USBD_LANGID_STRING            0x0409
#define USBD_MANUFACTURER_STRING      "STMicroelectronics"
#define USBD_PRODUCT_FS_STRING        "Stm32CustomUSBDeviceClass"
#define USBD_CONFIGURATION_FS_STRING  "Stm32CustomUSBDeviceClass Config"
#define USBD_INTERFACE_FS_STRING      "Stm32CustomUSBDeviceClass Interface"

Now how exactly does this device descriptor stuff work? Well, there is a struct of type USBD_DescriptorsTypeDef which contains some accessor functions to the device descriptor and all the strings. The instance of the accessor struct is called Class_Desc and will be shared through the usbd_desc.h header.

typedef struct
{
  uint8_t  *(*GetDeviceDescriptor)(USBD_SpeedTypeDef speed, uint16_t *length);
  uint8_t  *(*GetLangIDStrDescriptor)(USBD_SpeedTypeDef speed, uint16_t *length);
  uint8_t  *(*GetManufacturerStrDescriptor)(USBD_SpeedTypeDef speed, uint16_t *length);
  uint8_t  *(*GetProductStrDescriptor)(USBD_SpeedTypeDef speed, uint16_t *length);
  uint8_t  *(*GetSerialStrDescriptor)(USBD_SpeedTypeDef speed, uint16_t *length);
  uint8_t  *(*GetConfigurationStrDescriptor)(USBD_SpeedTypeDef speed, uint16_t *length);
  uint8_t  *(*GetInterfaceStrDescriptor)(USBD_SpeedTypeDef speed, uint16_t *length);
#if (USBD_LPM_ENABLED == 1U)
  uint8_t  *(*GetBOSDescriptor)(USBD_SpeedTypeDef speed, uint16_t *length);
#endif
} USBD_DescriptorsTypeDef;

Then there is the actual device descriptor which is an aligned byte array of length 18. Again KEIL does a much better job at documenting these structures so I suggest to take a look at the device descriptor documentation if you’re curious about specific fields.

#define __ALIGN_BEGIN
#define __ALIGN_END      __attribute__ ((aligned (4)))
#define USB_LEN_DEV_DESC 0x12U

__ALIGN_BEGIN uint8_t USBD_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
{
  0x12,                       /* bLength */
  USB_DESC_TYPE_DEVICE,       /* bDescriptorType */
  0x00,                       /* bcdUSB */
  0x02,
  0x00,                       /* bDeviceClass */
  0x00,                       /* bDeviceSubClass */
  0x00,                       /* bDeviceProtocol */
  USB_MAX_EP0_SIZE,           /* bMaxPacketSize */
  LOBYTE(USBD_VID),           /* idVendor */
  HIBYTE(USBD_VID),           /* idVendor */
  LOBYTE(USBD_PID),           /* idProduct */
  HIBYTE(USBD_PID),           /* idProduct */
  0x00,                       /* bcdDevice rel. 2.00 */
  0x02,
  USBD_IDX_MFC_STR,           /* Index of manufacturer string */
  USBD_IDX_PRODUCT_STR,       /* Index of product string */
  USBD_IDX_SERIAL_STR,        /* Index of serial number string */
  USBD_MAX_NUM_CONFIGURATION  /* bNumConfigurations */
};

The important bits for us are the device class, subclass and protocol. Let’s revisit the USB classes again and consider our options. There are two classes which might cover our needs. There is Communications and CDC Control (0x02) which, according to the second column, can be used at the device- and interface descriptor. And there is CDC-Data (0x0A) which can only be used at the interface descriptor. Now what’s the difference between those two? Sadly the information here is a little sparse so we’re forced to dig deeper and read the device class specification for CDC devices available here.

Chapter 1 and 3 of that specification contain a pretty good summery on when to use which class. It basically states to use the communication interface and optionally device class if your use case matches any of those in the document and the data class for everything else. Since all use cases described there are some sort of telecommunication and networking services we will stick with the data class.

The final question that arises is what class code to use in the device descriptor when the CDC-Data (0x0A) class is only allowed inside interface descriptors? Well, simply none. According to the standard a device might leave the triplet class, subclass and protocol (highlighted above) set to 0x00. This configuration tells the USB host to “skip” the device descriptor and look directly at the interface descriptor to figure out how to talk to the device.

Before we move on to writing our class there is a nasty default left in the usbd_desc.h file we need to change. Currently the DEVICE_IDx macros point to addresses that don’t even exist on the chip. Reading from that address would cause a hardfault exception. We’re going to point those addresses at the UID of the STM32H743ZI processor by using the UID_BASE macro.

#define         DEVICE_ID1          (UID_BASE)
#define         DEVICE_ID2          (UID_BASE + 0x4)
#define         DEVICE_ID3          (UID_BASE + 0x8)

USB device class

Now that we’ve got our device descriptor we can start with the USB class. Once again we’re looking for some template files to start from. This time we will copy two files from the Class subfolder of the STM32 USB device library to our project. I’ve renamed the files to usbd_class.h/c.

.
├── Class
│   ├── AUDIO
│   ├── CDC
│   ├── CustomHID
│   ├── DFU
│   ├── HID
│   ├── MSC
│   └── Template
│       ├── Inc
│       │   └── usbd_template.h
│       └── Src
│           └── usbd_template.c
├── Core
└── Release_Notes.html

Before I forget I’ll also turn on the USB interrupt which will make the code generator create an USB interrupt handler inside stm32h7xx_it.c for us and enable the interrupt during initialization.

void OTG_FS_IRQHandler(void)
{
  /* USER CODE BEGIN OTG_FS_IRQn 0 */

  /* USER CODE END OTG_FS_IRQn 0 */
  HAL_PCD_IRQHandler(&hpcd_USB_OTG_FS);
  /* USER CODE BEGIN OTG_FS_IRQn 1 */

  /* USER CODE END OTG_FS_IRQn 1 */
}

USB configuration, interface and endpoint descriptors

For our class to work we’ll need to implement a couple of functions and the configuration-, interface- and endpoint descriptors. I’ll start with the latter. All three descriptor types will be joined in a single byte array. This time the length of the array will not be a fixed constant but will be depending on the number of configurations, interfaces and endpoints defined. For our single configuration with one interface and two endpoints we’ll need 32 bytes of storage. I’ve adjusted the macro USB_TEMPLATE_CONFIG_DESC_SIZ inside usbd_class.h accordingly.

__ALIGN_BEGIN static uint8_t USBD_TEMPLATE_CfgDesc[USB_TEMPLATE_CONFIG_DESC_SIZ] __ALIGN_END =
{
  0x09,                          /* bLength: Configuation Descriptor size */
  USB_DESC_TYPE_CONFIGURATION,   /* bDescriptorType: Configuration */
  USB_TEMPLATE_CONFIG_DESC_SIZ,  /* wTotalLength: Bytes returned */
  0x00,
  0x01,                          /*bNumInterfaces: 1 interface*/
  0x01,                          /*bConfigurationValue: Configuration value*/
  0x00,                          /*iConfiguration */
  0xC0,                          /*bmAttributes: bus powered and Supports Remote Wakeup */
  0x32,                          /*MaxPower 100 mA: this current is used for detecting Vbus*/

  /* Interface */
  0x09,                     /* bLength */
  USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: */
  0x01,                     /* bInterfaceNumber */
  0x00,                     /* bAlternateSetting */
  0x02,                     /* bNumEndpoints */
  0x0A,                     /* bInterfaceClass */
  0x00,                     /* bInterfaceSubClass */
  0x00,                     /* bInterfaceProtocol */
  0x00,                     /* iInterface */

  /* Endpoint OUT */
  0x07,                            /* bLength */
  USB_DESC_TYPE_ENDPOINT,          /* bDescriptorType */
  TEMPLATE_EPOUT_ADDR,             /* bEndpointAddress */
  0x02,                            /* bmAttributes */
  LOBYTE(USB_FS_MAX_PACKET_SIZE),  /* wMaxPacketSize */
  HIBYTE(USB_FS_MAX_PACKET_SIZE),
  0x00,                            /* bInterval */

  /* Endpoint IN */
  0x07,                             /* bLength */
  USB_DESC_TYPE_ENDPOINT,           /* bDescriptorType */
  TEMPLATE_EPIN_ADDR,               /* bEndpointAddress */
  0x02,                             /* bmAttributes */
  LOBYTE(USB_FS_MAX_PACKET_SIZE),   /* wMaxPacketSize */
  HIBYTE(USB_FS_MAX_PACKET_SIZE),
  0x00                              /* bInterval */
};

I know that those descriptors are ugly things to look at but let’s try to extract the important bytes. Inside the interface part of the descriptor I’ve set the interface class code to 0x0A which corresponds to the CDC-Data interface I’ve explained earlier. The two defined endpoints (1 OUT, 1 IN) are both of type bulk and have their addresses defined by the two macros TEMPLATE_EPOUT_ADDR and TEMPLATE_EPIN_ADDR. Theoretically the device library would support up to 16 endpoints for each direction.

Although our class won’t do anything useful yet this might be a good moment to check if we get any response from our board at all. In order to do that we’ll need to follow the initialization order described on page 20 of the library device manual. The following three API calls are necessary to get the library running.

USBD_StatusTypeDef USBD_Init(USBD_HandleTypeDef *pdev, USBD_DescriptorsTypeDef *pdesc, uint8_t id);
USBD_StatusTypeDef USBD_RegisterClass(USBD_HandleTypeDef *pdev, USBD_ClassTypeDef *pclass);
USBD_StatusTypeDef USBD_Start(USBD_HandleTypeDef *pdev);

Apart from calling into the lower layer those three functions are mainly responsible for joining all the things we’ve done so far. This might become clearer when looking at an exploded view of the struct USBD_HandleTypeDef.

USBD_HandleTypeDef

USBD_Init sets the pointer pDesc and USBD_RegisterClass sets pClass (_Device_cb is just a typedef of USBD_ClassTypeDef). Since pData is already linked to the peripheral handler all three layers can now communicate. ST also uses the pointers pClassData and pUserData in their examples for linking a class interface and data but there is already too much void* voodoo going on so I’m not going to do that.

I’ve added the API calls in main.c right after the USB hardware initialization. If all things went well you’ll see the device appear with the product string you’ve chosen.

[~]$ lsusb
Bus 008 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 007 Device 004: ID 0483:374e STMicroelectronics STLINK-V3
Bus 007 Device 006: ID 0483:002a STMicroelectronics Stm32CustomUSBDeviceClass
Bus 007 Device 002: ID 046d:c018 Logitech, Inc. Optical Wheel Mouse

USB device class events

To get some actual behavior from our class we’ll need to implement a couple of events the core might send us. Right at the top of the usbd_class.c file you’ll find a bunch of function prototypes which represent all possible events.

static uint8_t  USBD_TEMPLATE_Init(USBD_HandleTypeDef *pdev, uint8_t cfgidx);
static uint8_t  USBD_TEMPLATE_DeInit(USBD_HandleTypeDef *pdev, uint8_t cfgidx);
static uint8_t  USBD_TEMPLATE_Setup(USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req);
static uint8_t  *USBD_TEMPLATE_GetCfgDesc(uint16_t *length);
static uint8_t  *USBD_TEMPLATE_GetDeviceQualifierDesc(uint16_t *length);
static uint8_t  USBD_TEMPLATE_DataIn(USBD_HandleTypeDef *pdev, uint8_t epnum);
static uint8_t  USBD_TEMPLATE_DataOut(USBD_HandleTypeDef *pdev, uint8_t epnum);
static uint8_t  USBD_TEMPLATE_EP0_RxReady(USBD_HandleTypeDef *pdev);
static uint8_t  USBD_TEMPLATE_EP0_TxReady(USBD_HandleTypeDef *pdev);
static uint8_t  USBD_TEMPLATE_SOF(USBD_HandleTypeDef *pdev);
static uint8_t  USBD_TEMPLATE_IsoINIncomplete(USBD_HandleTypeDef *pdev, uint8_t epnum);
static uint8_t  USBD_TEMPLATE_IsoOutIncomplete(USBD_HandleTypeDef *pdev, uint8_t epnum);

The good news is that you can delete most of them and only keep the highlighted ones. We will neither need the control endpoint events (EP0) nor the start-of-frame (SOF) or isochronous (Iso) ones.

Again there will be an accessor struct containing all the function pointers. This time the struct is of type USBD_ClassTypeDef.

typedef struct _Device_cb
{
  uint8_t (*Init)(struct _USBD_HandleTypeDef *pdev, uint8_t cfgidx);
  uint8_t (*DeInit)(struct _USBD_HandleTypeDef *pdev, uint8_t cfgidx);
  /* Control Endpoints*/
  uint8_t (*Setup)(struct _USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef  *req);
  uint8_t (*EP0_TxSent)(struct _USBD_HandleTypeDef *pdev);
  uint8_t (*EP0_RxReady)(struct _USBD_HandleTypeDef *pdev);
  /* Class Specific Endpoints*/
  uint8_t (*DataIn)(struct _USBD_HandleTypeDef *pdev, uint8_t epnum);
  uint8_t (*DataOut)(struct _USBD_HandleTypeDef *pdev, uint8_t epnum);
  uint8_t (*SOF)(struct _USBD_HandleTypeDef *pdev);
  uint8_t (*IsoINIncomplete)(struct _USBD_HandleTypeDef *pdev, uint8_t epnum);
  uint8_t (*IsoOUTIncomplete)(struct _USBD_HandleTypeDef *pdev, uint8_t epnum);
  uint8_t  *(*GetHSConfigDescriptor)(uint16_t *length);
  uint8_t  *(*GetFSConfigDescriptor)(uint16_t *length);
  uint8_t  *(*GetOtherSpeedConfigDescriptor)(uint16_t *length);
  uint8_t  *(*GetDeviceQualifierDescriptor)(uint16_t *length);
#if (USBD_SUPPORT_USER_STRING_DESC == 1U)
  uint8_t  *(*GetUsrStrDescriptor)(struct _USBD_HandleTypeDef *pdev, uint8_t index,  uint16_t *length);
#endif
} USBD_ClassTypeDef;

Be careful to place the pointer to the full speed configuration descriptor function at the right line I’ve highlighted here. Since there are four functions with the same signature in a row you wouldn’t even get a compiler warning if you’re off by one.

Let’s go through the implementation of the interesting class events. We’ll start with USBD_TEMPLATE_Init.

static uint8_t  USBD_TEMPLATE_Init(USBD_HandleTypeDef *pdev,
                                   uint8_t cfgidx)
{
  USBD_LL_OpenEP(pdev, TEMPLATE_EPIN_ADDR, USBD_EP_TYPE_BULK, USB_FS_MAX_PACKET_SIZE);
  USBD_LL_OpenEP(pdev, TEMPLATE_EPOUT_ADDR, USBD_EP_TYPE_BULK, USB_FS_MAX_PACKET_SIZE);
  USBD_LL_PrepareReceive(pdev, TEMPLATE_EPOUT_ADDR, data_out_buffer, USB_FS_MAX_PACKET_SIZE);
  return USBD_OK;
}

As the name suggests this event needs to initialize something. In this case it means that we’ll need some calls to the lower layer to open the two endpoints we’ve defined in our descriptors. We’ll also tell the lower layer to start preparing for reception of the very first packet. The USBD_LL_PrepareReceive function expects a pointer to a buffer where the received data should be stored. I’ve created a local byte array called data_out_buffer. With only 512 bytes this buffer is rather small for USB but more than sufficient for our use case.

The function USBD_TEMPLATE_DataIn could almost be empty if it weren’t for a small detail concerning bulk transfers.

static uint8_t  USBD_TEMPLATE_DataIn(USBD_HandleTypeDef *pdev,
                                     uint8_t epnum)
{
  if (pdev->ep_in[epnum].total_length && 
      !(pdev->ep_in[epnum].total_length % USB_FS_MAX_PACKET_SIZE))
  {
    pdev->ep_in[epnum].total_length = 0;
    USBD_LL_Transmit(pdev, epnum, NULL, 0);
  }
  else
    data_in_busy = false;

  return USBD_OK;
}

The USB standard says that bulk transfers can never end with a packet with the maximum bulk packet size. Since we’ve set our endpoint packet size to the maximum full speed USB supports this means we can’t transmit multiples of 64 bytes without some special end packet. This end packet is called zero length packet (ZLP) and prepared inside the if-statement. Only when the transfer is really complete we can clear the busy flag data_in_busy.

Analogous to an IN event there is also an OUT event which is handled inside USBD_TEMPLATE_DataOut.

static uint8_t  USBD_TEMPLATE_DataOut(USBD_HandleTypeDef *pdev,
                                      uint8_t epnum)
{
  size_t const bytes_received = USBD_LL_GetRxDataSize(pdev, epnum);
  xStreamBufferSendFromISR(xStreamBuffer, data_out_buffer, bytes_received, NULL);
  USBD_LL_PrepareReceive(pdev, TEMPLATE_EPOUT_ADDR, data_out_buffer, USB_FS_MAX_PACKET_SIZE);
  return USBD_OK;
}

To send the received data to a task where we can later manipulate it we’ll use a FreeRTOS feature called Stream Buffers. The stream buffer handler xStreamBuffer will be created and initialized in the main.c file. The number of actually received bytes we need to pass to the stream buffer API can be get from the lower layer function USBD_LL_GetRxDataSize. Before we return from the function we prepare the reception of the next packet.

Invert case and USB transmit

Before we implement the task which inverts the case of the received string for us we’ll need to add one more function to our USB class which transmits data. We’ll need a wrapper around the lower layer function USBD_LL_Transmit.

uint8_t  USBD_TEMPLATE_Transmit(USBD_HandleTypeDef *pdev, uint8_t* buf, uint16_t length)
{
  if (data_in_busy)
    return USBD_BUSY;

  data_in_busy = true;
  pdev->ep_in[TEMPLATE_EPIN_ADDR & 0x7F].total_length = length;
  return USBD_LL_Transmit(pdev, TEMPLATE_EPIN_ADDR, buf, length);
}

Inside that wrapper we will first check our busy flag. We can only start a new transmission when the last one was finished. The library internals depend on the length parameter of the endpoint being set so we must do that before we call the lower layer function. By the way the length here is completely arbitrary. You’re not restricted by the bulk packet length in any way. The library will split the data into multiple packets if it has to.

The transmit function we just added will be used in the new FreeRTOS task StartInvertAsciiTask. I’ve used STM32CubeMX to create it. Since we’ll need a local buffer which can hold the received USB data we’ll need quite a lot of stack. I’ve set the tasks stack size to 768 words which equals 3kB.

void StartInvertAsciiTask(void *argument)
{
  /* USER CODE BEGIN StartInvertAsciiTask */
  uint8_t buffer[512];
  /* Infinite loop */
  for(;;)
  {
    size_t count = xStreamBufferReceive(xStreamBuffer, buffer, 512, 2);
    if(!count)
      continue;

    invert_case(buffer, count);

    for(;;)
    {
      uint8_t status = USBD_TEMPLATE_Transmit(&hUsbDeviceFS, buffer, count);
      if (status == USBD_OK)
        break;
      osDelay(2);
    }
  }
  /* USER CODE END StartInvertAsciiTask */
}

Inside the task we continuously try to read up to 512 bytes from our steam buffer. The stream buffer API is non-blocking. The last parameter of the stream buffer receive function can be used to specify how many ticks the call should wait for additional data. Once we’ve red some data we will invert the case of all letters and transmit it back. Since the USB transmit function might return a busy status we need to check its return value. In case it really is busy we wait two ticks before we try again.

The code we’ve written so far should suffice for the USB device side but we still need the host side to interact with it.

PyUSB and udev

To get our interactive console we will write a little Python script which will make use of PyUSB. The following code is mostly a copy and paste from the PyUSB tutorial.

import usb.core
import usb.util

# find our device
dev = usb.core.find(idVendor=1155, idProduct=42)

# set the active configuration. With no arguments, the first
# configuration will be the active one
dev.set_configuration()

# get an endpoint instance
cfg = dev.get_active_configuration()
intf = cfg[(0, 0)]

ep_out = usb.util.find_descriptor(
    intf,
    # match the first OUT endpoint
    custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress)
    == usb.util.ENDPOINT_OUT,
)

ep_in = usb.util.find_descriptor(
    intf,
    # match the first IN endpoint
    custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress)
    == usb.util.ENDPOINT_IN,
)

assert ep_out is not None
assert ep_in is not None

while True:
    str_tx = input("TX: ")
    if str_tx == "exit":
        print("exiting...")
        break
    ep_out.write(str_tx + "\n")
    raw_rx = ep_in.read(len(str_tx) + 1, 1000)
    str_rx = str(raw_rx, "utf-8")
    print("RX: " + str_rx)

The setup part of the code searches for our device by vendor and product ID. We then go ahead and read the configuration and interface descriptor before searching for the first OUT and IN endpoint. Since we only got one each we can be sure that the found endpoints are the ones we’re looking for. The following endless loop then simply reads from stdin and transmits the input to the USB device. A response is red and printed back to stdout.

Now before we go ahead and run the script we’ve got to take care of one more thing. We’ll need to write a udev rule with read and write permissions so that the device manager lets us talk to our device. I’ve added the following rule /etc/udev/rules.d/99-st.rules

SUBSYSTEM=="usb", ATTRS{idVendor}=="0483", MODE="0666"

This will allow us to run the script and finally see if everything worked. The following figure shows some captured output from my terminal.