CDC + MSC USB Composite Device on STM32 HAL

  • Tutorial
image

I would like to believe that at least half of the readers can decipher at least half of the title of the article :) Who is not in the know - I will explain. My device must implement two USB functions at once:

  • Mass Storage Device (aka Mass Storage Class - MSC ). I want my device to pretend to be a regular flash drive and give files with data that are on the SD card.
  • Another function is a virtual COM port (it is also called Communication Device Class - CDC in USB terminology ). Through this channel I have any kind of debiting conclusion, which is convenient to watch with a regular terminal.

In most examples of working with USB, only one type of device is implemented - a USB flash drive, mouse, custom HID device, or virtual COM port. But to find a sane explanation of how to implement at least two functions at the same time was not so simple. In my article, I would like to fill this gap.

I will describe the creation of a composite USB device based on the STM32 microcontroller, but the approach itself will also be applicable to other microcontrollers. In the article I will analyze in detail each of the classes separately, as well as the principle of constructing composite devices. But first things first.

So let's go!

Bit of theory


The USB interface is very complex, multi-level and multi-faceted. With a swoop it can not be overpowered. In one of the articles (I forgot, however, in which) I saw a phrase in the style of “read this article 2 times, and then again in the morning.” Yes, he’s like that, you won’t be able to do it the first time. Personally, my interface is more or less laid out only after a couple of months of active digging and reading specifications.

I am still not an expert in USB, and therefore I would recommend referring to articles that would tell in more detail the essence of what is happening. I will only point out the most important places and briefly explain how it works - for the most part what I have plunged into myself. First of all, I would recommend Usb in a nutshell ( translation ), as well as USB Made Simple(I have not read it myself, but many recommend it). We will also need specifications for specific classes of USB devices .

Probably the most important thing about the USB interface is the handle. More precisely, even a descriptor package. When a device connects to the bus, the host requests device descriptors that describe the device’s capabilities, exchange rates, polling frequency, which interfaces the device implements, and much more. The descriptor thing is important and very tender - even a mistake in one byte will lead to the fact that the device will not work.

The device describes itself using several descriptors of different types:

  • Device Descriptor - describes the device as a whole, its name, manufacturer, serial number. String data is described by separate string descriptors (String Descriptor)

  • Configuration Descriptor — A device can have one or more configurations. Each configuration determines the speed of communication with the device, a set of interfaces and power settings. So, for example, a laptop that runs on battery power may ask the device (choose a configuration) to use a lower exchange rate and switch to its own power source (instead of a laptop battery). Of course, this only works if the device provides such a configuration.

  • Interface descriptor - describes the interface of communication with the device. There can be several interfaces. For example, different functions (MSC, CDC, HID) will implement their interfaces. Some functions (for example, CDC or DFU) implement several interfaces at once for their work. In our case of a composite device, we will need to implement several interfaces from different functions at once and make them get along with each other.

  • Descriptor endpoint (Endpoint descriptor) - describes the communication channel within a specific interface, sets the packet size, describes the parameters of interrupts. Using endpoints we will receive and receive data.

  • There are a bunch of different descriptors that describe individual aspects of specific interfaces.

The host requests descriptors in a single stream of bytes. It is very important that the descriptors go in a certain order within the same configuration, otherwise the host will get confused which descriptor is for what. Each configuration consists of a configuration descriptor and a set of descriptors describing the interfaces. Each interface is described by an interface descriptor and a set of endpoint descriptors. Each entity carries its descriptors nearby.

You also need to understand that USB is a host-oriented protocol. Device configuration, reception, transmission - everything in USB is controlled by the host. For us, this means that there is no control flow from the side of the microcontroller - all USB work is based on interrupts and callbacks. And this, in turn, means that we do not want to start long-playing operations and we need to be very careful when interacting with other interrupts (take into account priority, and so on). However, try not to fall to such a low level.

Also, host orientation is also manifested in the name of the functions. In USB terminology, the direction from the host to the device is called OUT, although for the controller this is a technique. Conversely, the direction from the device to the host is called IN, although for us this means sending data. So in the microcontroller, the DataOut () function actually receives data, and DataIn () sends. But this is so, by the way - we will use ready-made code.

CDC - virtual COM port


Probably taking and immediately casting the composite device as a whole will not work - there are too many nuances and pitfalls. I think it would be better to debug each interface individually, and then move on to a composite device. I'll start with CDC, because it does not require any dependencies.

I recently moved to STM32 Cube - a package of low-level drivers for STM32. It contains USB management code with the implementation of certain classes of USB devices. Take the template implementation of USB Core and CDC and start sawing for yourself. The blanks are in the \ Middlewares \ ST \ STM32_USB_Device_Library directory. I use Cube for STM32F1 series controllers , Cube version 1.6 (April 2017), bundled USB library version 2.4.2 (December 2015)

The template implementation of the library involves writing your own code in files called template. Without an understanding of the whole library and the principles of USB, this is difficult to do. But we will go easier - we will generate these files using the CubeMX graphical configurator .

The implementation provided by CubeMX is ready to work right out of the box. It’s even a little disappointing that I didn’t have to write any code. We'll have to learn CDC using an example of a fully finished implementation. Let's take a look at the most interesting places in the generated code.

First, let's look at the descriptors that are in the files usbd_desc.c (device descriptor) and usbd_cdc.c (configuration descriptors, interfaces, endpoints). In the article usb in a nutshell ( in Russian) there is a very detailed description of all descriptors. I will not describe each field individually, I will focus only on the most important and interesting fields.

Device descriptor
#define USBD_VID     1155
#define USBD_LANGID_STRING     1033
#define USBD_MANUFACTURER_STRING     "STMicroelectronics"
#define USBD_PID_FS     22336
#define USBD_PRODUCT_STRING_FS     "STM32 Virtual ComPort"
#define USBD_SERIALNUMBER_STRING_FS     "00000000001A"
#define USBD_CONFIGURATION_STRING_FS     "CDC Config"
#define USBD_INTERFACE_STRING_FS     "CDC Interface"
#define USBD_MAX_NUM_CONFIGURATION     1
/* USB Standard Device Descriptor */
__ALIGN_BEGIN uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
 {
   0x12,                       /*bLength */
   USB_DESC_TYPE_DEVICE,       /*bDescriptorType*/
   0x00,                       /* bcdUSB */  
   0x02,
   0x02,                        /*bDeviceClass*/
   0x02,                       /*bDeviceSubClass*/
   0x00,                       /*bDeviceProtocol*/
   USB_MAX_EP0_SIZE,          /*bMaxPacketSize*/
   LOBYTE(USBD_VID),           /*idVendor*/
   HIBYTE(USBD_VID),           /*idVendor*/
   LOBYTE(USBD_PID_FS),           /*idVendor*/
   HIBYTE(USBD_PID_FS),           /*idVendor*/
   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*/
 } ;
/* USB_DeviceDescriptor */


Here we are interested in the following fields:

  • bDeviceClass, bDeviceSubClass and bDeviceProtocol - describe to the host what kind of device we have, what it can do and which drivers to load. In this case, it says that our device implements Communication Device Class, which means that the host needs to make a virtual COM port and connect it to this device.

  • PID (Product ID) and VID (Vendor ID) - in these fields, the host distinguishes between different devices connected to the system. Devices at the same time implement the same class. They say that for devices sold on the market it is very important to have unique VID / PIDs, but I did not find out who and where these IDs give out. For a home device in a single copy, the default values ​​are sufficient.

Please note that string constants (device name, serial number) are not registered in the descriptor itself. Lines are described by separate descriptors, and all the rest only indicate the index of the line. In the case of a library from ST, a string descriptor is generated on the fly (grrrrrr), so I won’t give it.

Configuration descriptor
__ALIGN_BEGIN const uint8_t USBD_CDC_CfgHSDesc[USB_CDC_CONFIG_DESC_SIZ] __ALIGN_END =
{
 /*Configuration Descriptor*/
 0x09,   /* bLength: Configuration Descriptor size */
 USB_DESC_TYPE_CONFIGURATION,      /* bDescriptorType: Configuration */
 USB_CDC_CONFIG_DESC_SIZ,                /* wTotalLength:no of returned bytes */
 0x00,
 0x02,   /* bNumInterfaces: 2 interface */
 0x01,   /* bConfigurationValue: Configuration value */
 0x00,   /* iConfiguration: Index of string descriptor describing the configuration */
 0xC0,   /* bmAttributes: self powered */
 0x32,   /* MaxPower 100 mA */


Here we are interested in the following:

  • wTotalLength - the size of the entire descriptor package for this configuration - so that the host knows where this configuration ends and the next one begins. We will need to fix it when we make a composite device. Let me remind you that all interfaces for this configuration should be a solid block, and the value wTotalLength determines the length of this block.

  • bNumInterfaces: The Communication Device class is implemented using two interfaces. One for management, another for the actual data being sent.

  • bmAttributes and MaxPower indicate that our device has its own power supply, but at the same time wants to consume up to 100 mA from the USB port. These parameters will obviously have to play around in the future.

Next comes the handle to the first of the CDC interfaces. This class of devices can implement several different communication models (telephone, direct connection, multi-way connection), but in our case it will be the Abstract Control Model.

CDC management interface handle
/*Interface Descriptor */
 0x09,   /* bLength: Interface Descriptor size */
 USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: Interface */
 /* Interface descriptor type */
 0x00,   /* bInterfaceNumber: Number of Interface */
 0x00,   /* bAlternateSetting: Alternate setting */
 0x01,   /* bNumEndpoints: One endpoints used */
 0x02,   /* bInterfaceClass: Communication Interface Class */
 0x02,   /* bInterfaceSubClass: Abstract Control Model */
 0x01,   /* bInterfaceProtocol: Common AT commands */
 0x00,   /* iInterface: */


Only one endpoint (bNumEndpoints) lives in this interface. But first comes a series of functional descriptors - settings specific to this class of devices.

Functional descriptors
 /*Header Functional Descriptor*/
 0x05,   /* bLength: Endpoint Descriptor size */
 0x24,   /* bDescriptorType: CS_INTERFACE */
 0x00,   /* bDescriptorSubtype: Header Func Desc */
 0x10,   /* bcdCDC: spec release number */
 0x01,
 /*Call Management Functional Descriptor*/
 0x05,   /* bFunctionLength */
 0x24,   /* bDescriptorType: CS_INTERFACE */
 0x01,   /* bDescriptorSubtype: Call Management Func Desc */
 0x00,   /* bmCapabilities: D0+D1 */
 0x01,   /* bDataInterface: 1 */
 /*ACM Functional Descriptor*/
 0x04,   /* bFunctionLength */
 0x24,   /* bDescriptorType: CS_INTERFACE */
 0x02,   /* bDescriptorSubtype: Abstract Control Management desc */
 0x02,   /* bmCapabilities */
 /*Union Functional Descriptor*/
 0x05,   /* bFunctionLength */
 0x24,   /* bDescriptorType: CS_INTERFACE */
 0x06,   /* bDescriptorSubtype: Union func desc */
 0x00,   /* bMasterInterface: Communication class interface */
 0x01,   /* bSlaveInterface0: Data Class Interface */


It says that our device does not know about the concept of “call” (in the sense of making a phone call), but at the same time understands the command line parameters (speed, stop bits, DTR / CTS bits). The last descriptor describes which of the two CDC interfaces is the control, and where the data runs. In general, here we are not interested in anything and we will not change anything.

Finally, the endpoint descriptor for the control interface
 /*Endpoint 2 Descriptor*/
 0x07,                           /* bLength: Endpoint Descriptor size */
 USB_DESC_TYPE_ENDPOINT,   /* bDescriptorType: Endpoint */
 CDC_CMD_EP,                     /* bEndpointAddress */
 0x03,                           /* bmAttributes: Interrupt */
 LOBYTE(CDC_CMD_PACKET_SIZE),     /* wMaxPacketSize: */
 HIBYTE(CDC_CMD_PACKET_SIZE),
 0x10,                           /* bInterval: */


It says that this endpoint is used for interrupts. The host will poll the device once every 0x10 (16) ms asking if the device requires attention. Also, control teams will walk through this endpoint.

The description of the second interface (where the data runs) will be simpler

CDC data interface and its endpoints
/*Data class interface descriptor*/
 0x09,   /* bLength: Endpoint Descriptor size */
 USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: */
 0x01,   /* bInterfaceNumber: Number of Interface */
 0x00,   /* bAlternateSetting: Alternate setting */
 0x02,   /* bNumEndpoints: Two endpoints used */
 0x0A,   /* bInterfaceClass: CDC */
 0x00,   /* bInterfaceSubClass: */
 0x00,   /* bInterfaceProtocol: */
 0x00,   /* iInterface: */
 /*Endpoint OUT Descriptor*/
 0x07,   /* bLength: Endpoint Descriptor size */
 USB_DESC_TYPE_ENDPOINT,      /* bDescriptorType: Endpoint */
 CDC_OUT_EP,                        /* bEndpointAddress */
 0x02,                              /* bmAttributes: Bulk */
 LOBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),  /* wMaxPacketSize: */
 HIBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),
 0x00,                              /* bInterval: ignore for Bulk transfer */
 /*Endpoint IN Descriptor*/
 0x07,   /* bLength: Endpoint Descriptor size */
 USB_DESC_TYPE_ENDPOINT,      /* bDescriptorType: Endpoint */
 CDC_IN_EP,                         /* bEndpointAddress */
 0x02,                              /* bmAttributes: Bulk */
 LOBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),  /* wMaxPacketSize: */
 HIBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),
 0x00                               /* bInterval: ignore for Bulk transfer */


Two endpoints of the bulk type live in the interface - one for reception, the second for transmission. In fact, in USB terminology, this is one endpoint, just two-way.

I won’t explain how it all works, at least because I don’t fully understand it myself (for example, how the host finds out how much data needs to be taken from the device). Most importantly, the library implements everything for us. Let's take a better look at architecture.

The USB library from ST is extremely layered. I would highlight such architectural levels

  • Class Driver (in the case of CDC, these are usbd_cdc and usbd_cdc_if files): they implement the logic of a specific class of devices - CDC for virtual COM port, MSC for storage devices, HID for keyboards / mice and any specific devices with a user interface.

  • USB Core (usbd_core.c, usbd_ctlreq.c, usbd_ioreq.c): implements the general logic of operation of all classes of USB devices, can send the requested descriptors to the host, processes requests from the host and configures the USB device as a whole. It also redirects data streams from the class driver level to the underlying levels and vice versa.

  • USB HW Driver (usbd_conf.c): The overlying layers are platform independent and work the same way for several series of microcontrollers. The code does not have low-level function calls for a specific microcontroller. The usbd_conf.c file implements a layer between USB Core and HAL, a library of low-level drivers for the selected microcontroller. Basically, there are simple wrappers who redirect calls from top to bottom and callbacks from bottom to top.

  • HAL (stm32f1xx_hal_pcd.c, stm32f1xx_ll_usb.c): engaged in communication with the microcontroller hardware, operates with registers and responds to interrupts.

At this stage, we will be interested in only the topmost layer and one function from usbd_conf.c. Let's start with the last one:

USBD_LL_Init () function
/**
 * @brief  Initializes the Low Level portion of the Device driver.
 * @param  pdev: Device handle
 * @retval USBD Status
 */
USBD_StatusTypeDef  USBD_LL_Init (USBD_HandleTypeDef *pdev)
{
 /* Init USB_IP */
 /* Link The driver to the stack */
 hpcd_USB_FS.pData = pdev;
 pdev->pData = &hpcd_USB_FS;
 hpcd_USB_FS.Instance = USB;
 hpcd_USB_FS.Init.dev_endpoints = 8;
 hpcd_USB_FS.Init.speed = PCD_SPEED_FULL;
 hpcd_USB_FS.Init.ep0_mps = DEP0CTL_MPS_8;
 hpcd_USB_FS.Init.low_power_enable = DISABLE;
 hpcd_USB_FS.Init.lpm_enable = DISABLE;
 hpcd_USB_FS.Init.battery_charging_enable = DISABLE;
 if (HAL_PCD_Init(&hpcd_USB_FS) != HAL_OK)
 {
        	Error_Handler();
 }
 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x00 , PCD_SNG_BUF, 0x18);
 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x80 , PCD_SNG_BUF, 0x58);
 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x81 , PCD_SNG_BUF, 0xC0);  
 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x01 , PCD_SNG_BUF, 0x110);
 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x82 , PCD_SNG_BUF, 0x100);  
 return USBD_OK;
}


This function initializes the USB peripherals of the microcontroller. Most interesting is the series of calls to the HAL_PCDEx_PMAConfig () function. The fact is that on board the microcontroller there is a whole 512 bytes of memory allocated specifically for USB buffers (this memory is called PMA - Packet Memory Area). But since the device does not know in advance how many endpoints will be and what their parameters will be, this memory is not allocated. Therefore, before working with USB memory must be allocated according to the selected parameters.

But what’s strange, only 2 endpoints were announced, and the calls were 5. Where did the extra come from? In fact, there are no unnecessary ones. The fact is that each USB device must have one two-way endpoint through which the device is initialized and then controlled. This endpoint is always numbered 0. This function does not initialize endpoints, but buffers. For the zero endpoint, 2 buffers are created - 0x00 for reception and 0x80 for transmission (the most significant bit indicates the direction of transmission, the lowest bits indicate the number of the endpoint). The remaining 3 calls describe the buffers for endpoint 1 (receiving and transmitting data) and endpoint 2 (receiving commands and sending status - this happens synchronously, so there is only one buffer)

The last parameter in each call indicates the offset of the endpoint buffer in the shared buffer. On the forums, I saw the questions “what is this magic constant 0x18 (starting address of the first buffer)?”. I will consider this issue in detail later. Now I’ll just say that the first 0x18 bytes of the PMA memory is occupied by the buffer allocation table.

But these are all the guts and other entrails. And what is outside?

The user code operates with the receive and transmit functions, which are located in the usbd_cdc_if.c file. So that the device can send data to the virtual COM port towards the host, we were provided with the function CDC_Transmit_FS ()

Function CDC_Transmit_FS ()
/**
 * @brief  CDC_Transmit_FS
 *         Data send over USB IN endpoint are sent over CDC interface
 *         through this function.           
 *         @note
 *         
 *                 
 * @param  Buf: Buffer of data to be send
 * @param  Len: Number of data to be send (in bytes)
 * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL or USBD_BUSY
 */
uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)
{
 uint8_t result = USBD_OK;
 USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData;
 if (hcdc->TxState != 0){
   return USBD_BUSY;
 }
 USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);
 result = USBD_CDC_TransmitPacket(&hUsbDeviceFS);
 return result;
}


Reception is a bit more complicated: the USB core will pull the CDC_Receive_FS () function as data is received. In this function you need to add your own code, which will process the received data. Or call a callback that will handle the processing, for example like this:

Function CDC_Receive_FS ()
/**
 * @brief  CDC_Receive_FS
 *         Data received over USB OUT endpoint are sent over CDC interface
 *         through this function.
 *           
 *         @note
 *         This function will block any OUT packet reception on USB endpoint
 *         untill exiting this function. If you exit this function before transfer
 *         is complete on CDC interface (ie. using DMA controller) it will result
 *         in receiving more data while previous ones are still not sent.
 *                 
 * @param  Buf: Buffer of data to be received
 * @param  Len: Number of data received (in bytes)
 * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL
 */
static int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len)
{
 uint16_t len = *Len;
 CDCDataReceivedCallback(Buf, len);
 // Prepare for next reception
 USBD_CDC_ReceivePacket(&hUsbDeviceFS);
 return (USBD_OK);
}


Please note that these functions work with byte arrays without any structure. In my case, I needed to send strings. To make this convenient, I wrote an analogue of the printf function, which formatted the string and sent it to the port. To speed up, I was also puzzled by double buffering. Read more here in the sections "USB double-buffered" and "printf".

Also in the same file are the initialization / deinitialization functions of the virtual COM port, as well as the function of changing port parameters (speed, parity, stop bits, etc.). The default implementation does not limit itself in speed and it suits me. Initialization is just as good. Leave it as it is.

The final barcode is the code that runs it all.

the code that runs it all
        	USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS);
        	USBD_RegisterClass(&hUsbDeviceFS, &USBD_CDC);
        	USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS);
        	USBD_Start(&hUsbDeviceFS);


Here, in turn, different driver levels are initialized. The last command includes USB interrupts. It is important to understand that all work with USB occurs upon request from the host. In this case, an interrupt is called inside the driver, which in turn either processes the request itself or delegates it to another code via a callback.

For this to work, you need a driver from the operating system. As a rule, this is a standard driver and the system can pick up the device without a special installation procedure. As far as I understand, a Virtual COM Port driver from STM (delivered with ST Flash Utility) was already installed on my system and my device was picked up on its own. On Linux, everything also started up with a half kick.

MSC - storage device


Everything was simple with the CDC driver - the device, as a rule, is itself the end user of the data (for example, receives commands from the host) or a generator (for example, sends sensor readings to the host).

Mass Storage Class will be a bit more complicated. The MSC driver is just a layer between the host and the USB bus on the one hand, and the storage device on the other. It can be an SD card connected via SDIO, SPI Flash, it can be a RAM Drive, a disk drive, or maybe even a network drive. In general, in most cases, the storage device will be represented by a certain driver (usually non-trivial), which we will need to dock with the implementation of MSC.

My device uses an SD card connected via SPI. To access the file on this map, I use the SdFat library. It is also divided into several levels of abstraction:

  • The user is provided with the File class, through which you can create / open files, read and write data. Client code is not steamed up by interaction with the storage medium and subtleties of the file system.

  • The Volume class deals with the entire kitchen for servicing the file system, directory, clusters, FAT, and so on. Communication with the data carrier is delegated to the lower levels.

  • SD card driver - this component knows how to communicate with the card, which commands to send to it, and which ones to listen to answers. The library provides several types of drivers for cards connected via SPI and SDIO. Theoretically, you can substitute your driver, for example, for a RAM disk.

  • The overlying layers are cross-platform, they do not know anything about how exactly the data will be written to or read from the card. This allows you to build a library for different platforms (both Arduino and others). For a specific platform or microcontroller, you can write a driver that will implement data transfer through the necessary interface. By default, the library provides several drivers, including for Arduino SPI, but I got confused and wrote my driver with preference and poetesses through DMA transfer based on HAL.

  • Finally, the HAL provides work with the registers of a specific microcontroller

In the case of USB Mass Storage, we will not work with files on a USB flash drive - the host will do all the work on interpreting the file system. The device will receive requests to read or write a specific data block. So we will be interested in the levels from the map driver and below.

The implementation of MSC requires a certain interface from the repository - to be able to read and write, to give its size and status. Approximately the same features are provided by the SD card driver interface of the SdFat library. It remains only to write an adapter that will lead one interface to another.

They decided on the direction of movement. Let's deal with the implementation. I again used the CubeMX configurator and generated the necessary files for the USB component. We begin the study, of course, with descriptors.

Device descriptor
* USB Standard Device Descriptor */
__ALIGN_BEGIN uint8_t USBD_FS_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_FS),           /*idVendor*/
   HIBYTE(USBD_PID_FS),           /*idVendor*/
   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*/
 } ;
/* USB_DeviceDescriptor */


The device descriptor has not changed much. The only difference is in the fields defining the device class - now the device class as a whole is not defined (zeros in bDeviceClass), but will be set at the interface level (this is a specification requirement ).

Configuration descriptor
 0x09,   /* bLength: Configuation Descriptor size */
 USB_DESC_TYPE_CONFIGURATION,   /* bDescriptorType: Configuration */
 USB_MSC_CONFIG_DESC_SIZ,
 0x00,
 0x01,   /* bNumInterfaces: 1 interface */
 0x01,   /* bConfigurationValue: */
 0x04,   /* iConfiguration: */
 0xC0,   /* bmAttributes: */
 0x32,   /* MaxPower 100 mA */


It is very similar to a similar descriptor from CDC - the number of interfaces (1) and the bus power parameters (up to 100 mA) are determined

Interface descriptor
 0x09,   /* bLength: Interface Descriptor size */
 0x04,   /* bDescriptorType: */
 0x00,   /* bInterfaceNumber: Number of Interface */
 0x00,   /* bAlternateSetting: Alternate setting */
 0x02,   /* bNumEndpoints*/
 0x08,   /* bInterfaceClass: MSC Class */
 0x06,   /* bInterfaceSubClass : SCSI transparent*/
 0x50,   /* nInterfaceProtocol */
 0x05,          /* iInterface: */


The interface descriptor declares 2 endpoints (one on each side of the transmission). The handle also determines which particular Mass Storage subclass is Bulk Only Transport. I did not find a descriptive description of exactly what kind of subclass this is. I assume that this is a device that communicates only through two-way data transmission through 2 endpoints (while other models can also use interrupts). The protocol in this communication is SCSI commands.

Endpoint descriptors
 0x07,   /*Endpoint descriptor length = 7*/
 0x05,   /*Endpoint descriptor type */
 MSC_EPIN_ADDR,   /*Endpoint address (IN, address 1) */
 0x02,   /*Bulk endpoint type */
 LOBYTE(MSC_MAX_FS_PACKET),
 HIBYTE(MSC_MAX_FS_PACKET),
 0x00,   /*Polling interval in milliseconds */
 0x07,   /*Endpoint descriptor length = 7 */
 0x05,   /*Endpoint descriptor type */
 MSC_EPOUT_ADDR,   /*Endpoint address (OUT, address 1) */
 0x02,   /*Bulk endpoint type */
 LOBYTE(MSC_MAX_FS_PACKET),
 HIBYTE(MSC_MAX_FS_PACKET),
 0x00     /*Polling interval in milliseconds*/


Two endpoints of the Bulk type are defined here - the USB interface does not guarantee speed at such endpoints, but does guarantee the delivery of data. The packet size is set to 64 bytes.

Since we are talking about endpoints, it’s worth looking into the usbd_conf.c file where the corresponding PMA buffers are determined

Configure PMA buffers
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x00 , PCD_SNG_BUF, 0x18);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x80 , PCD_SNG_BUF, 0x58);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x81 , PCD_SNG_BUF, 0x98);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x01 , PCD_SNG_BUF, 0xD8);


Now let's look at MSC from the other side. This USB class receives read / write commands from the host and translates their specialized interface - USBD_StorageTypeDef. We can only substitute our implementation.

Device interface
/** @defgroup USB_CORE_Exported_Types
  * @{
  */ 
typedef struct _USBD_STORAGE
{
  int8_t (* Init) (uint8_t lun);
  int8_t (* GetCapacity) (uint8_t lun, uint32_t *block_num, uint16_t *block_size);
  int8_t (* IsReady) (uint8_t lun);
  int8_t (* IsWriteProtected) (uint8_t lun);
  int8_t (* Read) (uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len);
  int8_t (* Write)(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len);
  int8_t (* GetMaxLun)(void);
  int8_t *pInquiry;
}USBD_StorageTypeDef;


Since this is C, not C ++, each of these entries is a pointer to a corresponding function. As I said, we need to write an adapter that will drive the MSC interface to the SD card interface.

Let's start implementing the interface. The first is the initialization function

Initialization function
int8_t SD_MSC_Init (uint8_t lun)
{
       	(void)lun; // Not used
//    	if(!initSD())
//            	return USBD_FAIL;
       	return (USBD_OK);
}


So the SD card could be initialized right from here if it were a quick operation. But in the case of an SD card, this may not always be the case. In addition, do not forget that all these functions are callbacks and are called from the USB interrupt, and interrupts should not be blocked for a long time. Therefore, I call the initSD () function directly from main () before USB initialization, and SD_MSC_Init () does nothing for me

Map initialization
SdFatSPIDriver spiDriver;
SdSpiCard card;
bool initSD()
{
       	return card.begin(&spiDriver, PA4, SPI_FULL_SPEED);
}


There may seem to be too many different drivers, but let me remind you of the architecture. The SdSpiCard class from the SdFat library knows how to communicate with the SD card via SPI, when and which command to send and what to wait for a response. But he does not know how to work with SPI itself. For these purposes, I wrote the SdFatSPIDriver class, which implements communication with the card via SPI and data transfer via DMA.

Move on.

Card Volume Function
int8_t SD_MSC_GetCapacity (uint8_t lun, uint32_t *block_num, uint16_t *block_size)
{
       	(void)lun; // Not used
       	*block_num  = card.cardSize();
       	*block_size = 512;
       	return (USBD_OK);
}


The implementation of SD_MSC_GetCapacity () is trivial - SdSpiCard can return the size of the map immediately in blocks

Read and write functions
int8_t SD_MSC_Read (uint8_t lun,
                                                               	uint8_t *buf,
                                                               	uint32_t blk_addr,
                                                               	uint16_t blk_len)
{
       	(void)lun; // Not used
       	if(!card.readBlocks(blk_addr, buf, blk_len))
               	return USBD_FAIL;
       	return (USBD_OK);
}
int8_t SD_MSC_Write (uint8_t lun,
                                                               	uint8_t *buf,
                                                               	uint32_t blk_addr,
                                                               	uint16_t blk_len)
{
       	(void)lun; // Not used
       	if(!card.writeBlocks(blk_addr, buf, blk_len))
               	return USBD_FAIL;
       	return (USBD_OK);
}


Reading and writing is also implemented quite simply.

More features
int8_t  SD_MSC_IsReady (uint8_t lun)
{
       	(void)lun; // Not used
       	return (USBD_OK);
}
int8_t  SD_MSC_IsWriteProtected (uint8_t lun)
{
       	(void)lun; // Not used
       	return (USBD_OK); // Never write protected
}


Our card is always ready (although in the future I will look more closely at the status) and is not write protected.

Another one
int8_t SD_MSC_GetMaxLun (void)
{
       	return 0; // We have just 1 Logic unit number (LUN) which is zero
}


LUN - Logic Unit Number. Theoretically, our storage device may consist of several media (for example, hard drives in a raid). All SCSI protocol functions indicate which medium it wants to work with. The GetMaxLun function returns the number of the last device (number of devices minus 1). We have one flash drive, therefore we return 0.

And the last thing.

Storage descriptor
const uint8_t SD_MSC_Inquirydata[] = {/* 36 */
/* LUN 0 */
0x00,
0x80,
0x02,
0x02,
(STANDARD_INQUIRY_DATA_LEN - 5),
0x00,
0x00,
0x00,
'S', 'T', 'M', ' ', ' ', ' ', ' ', ' ', /* Manufacturer : 8 bytes */
'P', 'r', 'o', 'd', 'u', 'c', 't', ' ', /* Product      : 16 Bytes */
' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
'0', '.', '0' ,'1'                     /* Version      : 4 Bytes */
};


To be honest, I didn’t really understand why it was needed. Looking into the SCSI specification, I saw a lot of meaning fields that I did not understand. From what I mastered, it describes a standard device with direct (not sequential) access, and which can be removed (removable). Fortunately, in all the examples that I saw this array is the same, so let it be. Debugged after all.

Now all this needs to be initialized correctly

Initialization
        	USBD_StatusTypeDef res = USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS);
        	USBD_RegisterClass(&hUsbDeviceFS, &USBD_MSC);
        	USBD_MSC_RegisterStorage(&hUsbDeviceFS, &SdMscDriver);
        	USBD_Start(&hUsbDeviceFS);


We connect, we check. Everything works, though very slowly - the connected disk opens for about 50 seconds. This is partly due to the fact that the linear speed of reading a flash drive through such an interface is about 200 kb / s. When the USB Mass Storage device is connected to the computer, the operating system reads the FAT table. I use a flash drive with 8 gig, and there FAT is already 7.5 megabytes. Plus reading MBR, boot sectors, file tables - that’s almost 50 seconds.

I also had to disable DMA when working with an SD card - it is not so simple there with its inclusion. The fact is that my driver implementation (as it turned out) cannot work from an interrupt, and in USB everything works only through interrupts. Even the banal HAL_Delay () does not work, because it is also tied to interrupts, not to mention synchronization using FreeRTOS. This will need to be redone, but this is a different story and it does not apply to the USB composite device. As I redo it, I’ll definitely write an article about it and leave a link here.

UPDATE: as promised here link . I managed to pump speed up to 650kb / s

CDC + MSC Composite Device


And now with all this garbage we’ll try to take off (C) a joke.

So, we already know how to build USB devices that can implement either CDC or MSC. Let's try to make a composite device that implements both interfaces simultaneously. I looked at several other projects that implemented a composite USB device and, it seems to me, their approach makes sense. Namely: to implement its own class driver, which will implement both the functionality.

We will take the blank for the class from the STM32 Cube package (Middlewares \ ST \ STM32_USB_Device_Library \ Class \ Template). The filling will be creatively reworked code from here .

The structure of the USB device will be as follows:

  • We will have only one configuration, and in it 3 interfaces
  • One interface implements MSC
    • It has one bi-directional endpoint - for transmitting and receiving
  • CDC is implemented by two interfaces.
    • The first to control. It has one unidirectional endpoint to control the interface.
    • Second CDC interface for data. It has a bi-directional endpoint - for transmitting and receiving
  • Another endpoint is needed to control the device as a whole (implemented by the core of the USB library)

image
A beautiful picture that describes an example of a description of a composite device. Adapted from the IAD specification.

For ease of use, we will declare the endpoint and interface numbers in the code.

Interface and Endpoint Numbers
#define MSC_INTERFACE_IDX 0x0                            	// Index of MSC interface
#define CDC_INTERFACE_IDX 0x1                            	// Index of CDC interface
// endpoints numbers
// endpoints numbers
#define MSC_EP_IDX                      0x01
#define CDC_CMD_EP_IDX                  0x02
#define CDC_EP_IDX                      0x03
#define IN_EP_DIR						0x80 // Adds a direction bit
#define MSC_OUT_EP                      MSC_EP_IDX                  /* EP1 for BULK OUT */
#define MSC_IN_EP                       MSC_EP_IDX | IN_EP_DIR      /* EP1 for BULK IN */
#define CDC_CMD_EP                      CDC_CMD_EP_IDX| IN_EP_DIR   /* EP2 for CDC commands */
#define CDC_OUT_EP                      CDC_EP_IDX                  /* EP3 for data OUT */
#define CDC_IN_EP                       CDC_EP_IDX | IN_EP_DIR      /* EP3 for data IN */


Endpoint numbering repeats the numbering of interfaces. We will use No. 1 for MSC, No. 2 for CDC control, and No. 3 for data transmission through CDC. There is also a zero endpoint for general device management, but it is processed in the bowels of the USB kernel and it is not necessary to declare these numbers.

The USB library interface from ST is poor. In some cases, the endpoint numbers are used with the flag of the direction of transmission — the set high bit means the direction of IN — towards the host (I set the constant IN_EP_DIR for this). However, other functions simply use the endpoint number. Unlike the original design, I preferred to separate all these numbers and use the correct constants in the right places. Where constants with the suffix EP_IDX are used, the direction flag is not used.

IMPORTANT! Although according to the USB specification, the numbers of the endpoints can be anything, it’s better to arrange them sequentially and in the same order in which they are declared in the descriptors. This knowledge was given to me by a week of hard debug, when the Windows USB driver stubbornly broke into the wrong end point and nothing worked.

Let's start with descriptors as usual. Most of the descriptors will live in our class implementation (usbd_msc_cdc.c), but the device descriptor and some global things are defined in the USB kernel in the usbd_desc.c file

First a bit of constants
#define USBD_VID                        0x0483
#define USBD_PID                        0x5741
#define USBD_LANGID_STRING              0x409
#define USBD_MANUFACTURER_STRING        "STMicroelectronics"
#define USBD_PRODUCT_FS_STRING          "Composite MSC CDC"
#define USBD_SERIALNUMBER_FS_STRING     "00000000055C"
#define USBD_CONFIGURATION_FS_STRING    "Config Name"
#define USBD_INTERFACE_FS_STRING        "Interface Name"


Device descriptor
__ALIGN_BEGIN const uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
{
       	0x12,                       /*bLength */
       	USB_DESC_TYPE_DEVICE,       /*bDescriptorType*/
       	0x00,                       /*bcdUSB */
       	0x02,
       	0xEF,                       /*bDeviceClass*/
       	0x02,                       /*bDeviceSubClass*/
       	0x01,                       /*bDeviceProtocol*/
       	USB_MAX_EP0_SIZE,      /*bMaxPacketSize*/
       	LOBYTE(USBD_VID),           /*idVendor*/
       	HIBYTE(USBD_VID),           /*idVendor*/
       	LOBYTE(USBD_PID),           /*idVendor*/
       	HIBYTE(USBD_PID),           /*idVendor*/
       	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*/
};


In general, everything is the same here, only the fields that define the device class (bDeviceClass) differ. Now these fields indicate that this is a composite device. The host will need to work hard, figure out all the other descriptors and load the correct drivers for each of the components. The bDeviceProtocol field means that parts of the composite device will be described by a special descriptor - the Interface Association Descriptor. About him a little lower.

The configuration descriptor is about the same as before, the difference is only in the number of interfaces. Now we have 3 of them

Configuration descriptor
#define USB_MSC_CDC_CONFIG_DESC_SIZ       98
/* USB MSC+CDC device Configuration Descriptor */
static const uint8_t USBD_MSC_CDC_CfgDesc[USB_MSC_CDC_CONFIG_DESC_SIZ] =
{
	0x09,         /* bLength: Configuation Descriptor size */
	USB_DESC_TYPE_CONFIGURATION, /* bDescriptorType: Configuration */
	USB_MSC_CDC_CONFIG_DESC_SIZ, /* wTotalLength: Bytes returned */
	0x00,
	0x03,         /*bNumInterfaces: 3 interface*/
	0x01,         /*bConfigurationValue: Configuration value*/
	0x02,         /*iConfiguration: Index of string descriptor describing the configuration*/
	0xC0,         /*bmAttributes: bus powered and Supports Remote Wakeup */
	0x32,         /*MaxPower 100 mA: this current is used for detecting Vbus*/
	/* 09 bytes */


Next comes the announcement of the interface and endpoints for the MSC. I don’t know why in this order (first MSC then CDC). So it was in one of the examples that I found, and copied from there. In theory, the order of the interfaces does not matter. The main thing is that they carry all their additional descriptors nearby. Well, jokes with the numbering of end points also matter.

MSC Descriptors
	/********************  Mass Storage interface ********************/
	0x09,   /* bLength: Interface Descriptor size */
	0x04,   /* bDescriptorType: */
	MSC_INTERFACE_IDX,   /* bInterfaceNumber: Number of Interface */
	0x00,   /* bAlternateSetting: Alternate setting */
	0x02,   /* bNumEndpoints*/
	0x08,   /* bInterfaceClass: MSC Class */
	0x06,   /* bInterfaceSubClass : SCSI transparent command set*/
	0x50,   /* nInterfaceProtocol */
	USBD_IDX_INTERFACE_STR,	/* iInterface: */
	/* 09 bytes */
	/********************  Mass Storage Endpoints ********************/
	0x07,   /*Endpoint descriptor length = 7*/
	0x05,   /*Endpoint descriptor type */
	MSC_IN_EP,   /*Endpoint address (IN, address 1) */
	0x02,   /*Bulk endpoint type */
	LOBYTE(USB_MAX_PACKET_SIZE),
	HIBYTE(USB_MAX_PACKET_SIZE),
	0x00,   /*Polling interval in milliseconds */
	/* 07 bytes */
	0x07,   /*Endpoint descriptor length = 7 */
	0x05,   /*Endpoint descriptor type */
	MSC_OUT_EP,   /*Endpoint address (OUT, address 1) */
	0x02,   /*Bulk endpoint type */
	LOBYTE(USB_MAX_PACKET_SIZE),
	HIBYTE(USB_MAX_PACKET_SIZE),
	0x00,     /*Polling interval in milliseconds*/
	/* 07 bytes */


MSC descriptors are no different from those in the previous section.

And here comes a new type of descriptor - IAD (Interface Association Descriptor) - an interface association descriptor. Association here is not in the sense of organization, but in the sense of which interface to associate with which function.

Interface Association Descriptor
      	/******** IAD should be positioned just before the CDC interfaces ******
                               	IAD to associate the two CDC interfaces */
	0x08, /* bLength */
	0x0B, /* bDescriptorType */
	CDC_INTERFACE_IDX, /* bFirstInterface */
	0x02, /* bInterfaceCount */
	0x02, /* bFunctionClass */
	0x02, /* bFunctionSubClass */
	0x01, /* bFunctionProtocol */
	0x00, /* iFunction (Index of string descriptor describing this function) */
	/* 08 bytes */


This tricky descriptor tells the host that the description of the previous function of the USB device (MSC) is over and now there will be a completely different function. And it is immediately indicated which one - CDC. It also indicates the number of interfaces associated with it and the index of the first one.

IAD descriptor is not needed for MSC, because there is only one interface. But IAD is needed for the CDC to group 2 interfaces into one function. This is stated in the specification of this descriptor.

Finally, the CDC descriptors. They are fully consistent with descriptors for a single CDC function, accurate to interface numbers and endpoints.

CDC Descriptors
	/********************  CDC interfaces ********************/
	/*Interface Descriptor */
	0x09,   /* bLength: Interface Descriptor size */
	USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: Interface */
	/* Interface descriptor type */
	CDC_INTERFACE_IDX,   /* bInterfaceNumber: Number of Interface */
	0x00,   /* bAlternateSetting: Alternate setting */
	0x01,   /* bNumEndpoints: One endpoints used */
	0x02,   /* bInterfaceClass: Communication Interface Class */
	0x02,   /* bInterfaceSubClass: Abstract Control Model */
	0x01,   /* bInterfaceProtocol: Common AT commands */
	0x01,   /* iInterface: */
	/* 09 bytes */
	/*Header Functional Descriptor*/
	0x05,   /* bLength: Endpoint Descriptor size */
	0x24,   /* bDescriptorType: CS_INTERFACE */
	0x00,   /* bDescriptorSubtype: Header Func Desc */
	0x10,   /* bcdCDC: spec release number */
	0x01,
	/* 05 bytes */
	/*Call Management Functional Descriptor*/
	0x05,   /* bFunctionLength */
	0x24,   /* bDescriptorType: CS_INTERFACE */
	0x01,   /* bDescriptorSubtype: Call Management Func Desc */
	0x00,   /* bmCapabilities: D0+D1 */
	CDC_INTERFACE_IDX + 1,   /* bDataInterface: 2 */
	/* 05 bytes */
	/*ACM Functional Descriptor*/
	0x04,   /* bFunctionLength */
	0x24,   /* bDescriptorType: CS_INTERFACE */
	0x02,   /* bDescriptorSubtype: Abstract Control Management desc */
	0x02,   /* bmCapabilities */
	/* 04 bytes */
	/*Union Functional Descriptor*/
	0x05,   /* bFunctionLength */
	0x24,   /* bDescriptorType: CS_INTERFACE */
	0x06,   /* bDescriptorSubtype: Union func desc */
	CDC_INTERFACE_IDX,   /* bMasterInterface: Communication class interface */
	CDC_INTERFACE_IDX + 1,   /* bSlaveInterface0: Data Class Interface */
	/* 05 bytes */
	/*Endpoint 2 Descriptor*/
	0x07,                          /* bLength: Endpoint Descriptor size */
	USB_DESC_TYPE_ENDPOINT,        /* bDescriptorType: Endpoint */
	CDC_CMD_EP,                    /* bEndpointAddress */
	0x03,                          /* bmAttributes: Interrupt */
	LOBYTE(CDC_CMD_PACKET_SIZE),   /* wMaxPacketSize: */
	HIBYTE(CDC_CMD_PACKET_SIZE),
	0x10,                          /* bInterval: */
	/* 07 bytes */
	/*Data class interface descriptor*/
	0x09,   /* bLength: Endpoint Descriptor size */
	USB_DESC_TYPE_INTERFACE,       /* bDescriptorType: */
	CDC_INTERFACE_IDX + 1,         /* bInterfaceNumber: Number of Interface */
	0x00,                          /* bAlternateSetting: Alternate setting */
	0x02,                          /* bNumEndpoints: Two endpoints used */
	0x0A,                          /* bInterfaceClass: CDC */
	0x00,                          /* bInterfaceSubClass: */
	0x00,                          /* bInterfaceProtocol: */
	0x00,                          /* iInterface: */
	/* 09 bytes */
	/*Endpoint OUT Descriptor*/
	0x07,   /* bLength: Endpoint Descriptor size */
	USB_DESC_TYPE_ENDPOINT,        /* bDescriptorType: Endpoint */
	CDC_OUT_EP,                    /* bEndpointAddress */
	0x02,                          /* bmAttributes: Bulk */
	LOBYTE(CDC_DATA_PACKET_SIZE),  /* wMaxPacketSize: */
	HIBYTE(CDC_DATA_PACKET_SIZE),
	0x00,                          /* bInterval: ignore for Bulk transfer */
	/* 07 bytes */
	/*Endpoint IN Descriptor*/
	0x07,   /* bLength: Endpoint Descriptor size */
	USB_DESC_TYPE_ENDPOINT,        /* bDescriptorType: Endpoint */
	CDC_IN_EP,                     /* bEndpointAddress */
	0x02,                          /* bmAttributes: Bulk */
	LOBYTE(CDC_DATA_PACKET_SIZE),  /* wMaxPacketSize: */
	HIBYTE(CDC_DATA_PACKET_SIZE),
	0x00,                          /* bInterval */
	/* 07 bytes */


When all the descriptors are ready, you can calculate the total size of the configuration.
#define USB_CDC_CONFIG_DESC_SIZ       98

Let's move on to writing code. The USB kernel communicates with class drivers using this interface

Class driver interface
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);  
const uint8_t  *(*GetHSConfigDescriptor)(uint16_t *length);
const uint8_t  *(*GetFSConfigDescriptor)(uint16_t *length);
const uint8_t  *(*GetOtherSpeedConfigDescriptor)(uint16_t *length);
const uint8_t  *(*GetDeviceQualifierDescriptor)(uint16_t *length);
#if (USBD_SUPPORT_USER_STRING == 1)
uint8_t  *(*GetUsrStrDescriptor)(struct _USBD_HandleTypeDef *pdev ,uint8_t index,  uint16_t *length);  
#endif
} USBD_ClassTypeDef;


Depending on the status or event on the USB bus, the kernel calls the corresponding function.

Any architectural problem can be solved by introducing an additional abstract layer ... (C) another joke

Of course, we will not implement all the functionality as a whole - the existing code will be responsible for implementing the CDC and MSC classes. We will only write a layer that will redirect calls to either one or another implementation.

Initialization and deinitialization
/**
* @brief  USBD_MSC_CDC_Init
*         Initialize the MSC+CDC interface
* @param  pdev: device instance
* @param  cfgidx: Configuration index
* @retval status
*/
static uint8_t  USBD_MSC_CDC_Init (USBD_HandleTypeDef *pdev,
                                                                                            	  uint8_t cfgidx)
{
       	/* MSC initialization */
       	uint8_t ret = USBD_MSC_Init (pdev, cfgidx);
       	if(ret != USBD_OK)
               	return ret;
       	/* CDC initialization */
       	ret = USBD_CDC_Init (pdev, cfgidx);
       	if(ret != USBD_OK)
               	return ret;
       	return USBD_OK;
}
/**
* @brief  USBD_MSC_CDC_Init
*         DeInitialize the MSC+CDC layer
* @param  pdev: device instance
* @param  cfgidx: Configuration index
* @retval status
*/
static uint8_t  USBD_MSC_CDC_DeInit (USBD_HandleTypeDef *pdev,
                                                                                                        	uint8_t cfgidx)
{
       	/* MSC De-initialization */
       	USBD_MSC_DeInit(pdev, cfgidx);
       	/* CDC De-initialization */
       	USBD_CDC_DeInit(pdev, cfgidx);
       	return USBD_OK;
}


Everything is simple here: we initialize (de-initialize) both classes. The called functions themselves will create / delete their endpoints.

Perhaps the most difficult function will be Setup.

Setup handler
/**
 * @brief  USBD_MSC_CDC_Setup
 *         Handle the MSC+CDC specific requests
 * @param  pdev: instance
 * @param  req: usb requests
 * @retval status
 */
static uint8_t  USBD_MSC_CDC_Setup (USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req)
{
	// Route requests to MSC interface or its endpoints to MSC class implementaion
	if(((req->bmRequest & USB_REQ_RECIPIENT_MASK) == USB_REQ_RECIPIENT_INTERFACE && req->wIndex == MSC_INTERFACE_IDX) ||
		((req->bmRequest & USB_REQ_RECIPIENT_MASK) == USB_REQ_RECIPIENT_ENDPOINT && ((req->wIndex & 0x7F) == MSC_EP_IDX)))
	{
		return USBD_MSC_Setup(pdev, req);
	}
	return USBD_CDC_Setup(pdev, req);
}


This is a callback to one of the standard requests via the USB bus, but this request is very multifaceted. This can be either receiving data (get) or setting (Set). This may be a request to the device as a whole, to one of its interfaces or endpoints. Also here can come as a standard request defined by the basic USB specification, or specific to a specific device or class. More details here (Section “Setup Package”).

Due to the abundance of various cases, the structure of the Setup package handler is very complex. Here it is impossible to write one if or switch. In the USB kernel code, processing is spread out over 3-4 large functions and in certain cases is transferred to a separate specialized processor (there are about a dozen more of them). The only good news is that only an insignificant part of requests are passed to the class driver level.

I spied on which packets go through this function and it seems that you can navigate by the recipient. If the receiver of the packet is an interface - in the wIndex field there will be an interface number, if it is an endpoint, then in wIndex there will be an endpoint number. Based on this, we redirect requests to the appropriate handler.

By the way, for this to work, you must not forget to change the define, which determines the number of interfaces, otherwise the request simply won’t reach and cut off inside the USB kernel

#define USBD_MAX_NUM_INTERFACES 	3

Calling DataIn and DataOut is easier. There is the number of the endpoint - on it and determine where to redirect the request

DataIn () and DataOut ()
/**
 * @brief  USBD_MSC_CDC_DataIn
 *         handle data IN Stage
 * @param  pdev: device instance
 * @param  epnum: endpoint index
 * @retval status
 */
static uint8_t  USBD_MSC_CDC_DataIn (USBD_HandleTypeDef *pdev,
									 uint8_t epnum)
{
	if(epnum == MSC_EP_IDX)
		return USBD_MSC_DataIn(pdev, epnum);
	return USBD_CDC_DataIn(pdev, epnum);
}
/**
 * @brief  USBD_MSC_CDC_DataOut
 *         handle data OUT Stage
 * @param  pdev: device instance
 * @param  epnum: endpoint index
 * @retval status
 */
static uint8_t  USBD_MSC_CDC_DataOut (USBD_HandleTypeDef *pdev,
									  uint8_t epnum)
{
	if(epnum == MSC_EP_IDX)
		return USBD_MSC_DataOut(pdev, epnum);
	return USBD_CDC_DataOut(pdev, epnum);
}


Note that the transmit direction flag is not used in the endpoint number. Those. even if some functions use MSC_IN_EP (0x81), then in this function you need to use MSC_EP_IDX (0x01).

Sometimes the data comes to the zero end point and there is a special callback for this. I don’t know what I would do if both classes (both CDC and MSC) had handlers for this case - the interface or endpoint number is not specified in such a request. It would be impossible to understand to whom the request is addressed. Fortunately, only a CDC class can handle such a request - here we send it

EP0_RxReady handler
/**
  * @brief  USBD_MSC_CDC_EP0_RxReady
  *     	handle EP0 Rx Ready event
  * @param  pdev: device instance
  * @retval status
  */
static uint8_t  USBD_MSC_CDC_EP0_RxReady (USBD_HandleTypeDef *pdev)
{
    	return USBD_CDC_EP0_RxReady(pdev);
}


We will no longer have non-trivial handlers. There are a couple of getters for descriptors, but their code is standard and not of interest. Fill in the “table of virtual functions”

Function Pointer Table
USBD_ClassTypeDef  USBD_MSC_CDC_ClassDriver =
{
    	USBD_MSC_CDC_Init,
    	USBD_MSC_CDC_DeInit,
    	USBD_MSC_CDC_Setup,
    	NULL, //USBD_MSC_CDC_EP0_TxReady,
    	USBD_MSC_CDC_EP0_RxReady,
    	USBD_MSC_CDC_DataIn,
    	USBD_MSC_CDC_DataOut,
    	NULL, //USBD_MSC_CDC_SOF,
    	NULL, //USBD_MSC_CDC_IsoINIncomplete,
    	NULL, //USBD_MSC_CDC_IsoOutIncomplete,
    	USBD_MSC_CDC_GetCfgDesc,
    	USBD_MSC_CDC_GetCfgDesc,
    	USBD_MSC_CDC_GetCfgDesc,
    	USBD_MSC_CDC_GetDeviceQualifierDesc,
};


Now initialization code

Initialization code
USBD_Init(&hUsbDeviceFS, &FS_Desc, 0);
USBD_RegisterClass(&hUsbDeviceFS, &USBD_MSC_CDC_ClassDriver);
USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS);
USBD_MSC_RegisterStorage(&hUsbDeviceFS, &SdMscDriver);
USBD_Start(&hUsbDeviceFS);


We initialize the USB kernel, install our class driver for it, and configure the secondary interfaces. All? No, not all. In this form, it will not start.

Here's the thing. Each class has a certain amount of private data - the state of the driver, some variables that should be available in different functions of the driver. Moreover, these cannot be just global variables - they are tied to a specific USB device (otherwise it would be impossible to operate with several devices at once, if necessary). Therefore, several fields for such a case were entered in the USB handle at once.

USB Handle Data Fields
/* USB Device handle structure */
typedef struct _USBD_HandleTypeDef
{
...
  void       	         *pClassData; 
  void                	*pUserData;
  void                	*pData; 
} USBD_HandleTypeDef;

The problem is that each class considers these fields its property and clings its structure there.

There are several ways to solve this. Comrades from here generally pushed all the code from both drivers (CDC and MSC) into their class implementation to figure out what was happening on the fly. Another approach is to put structures in these fields in which there is room for data of both classes. Then partially used this approach, in addition also a part of the data transferred to the global variables (which is ok if we have only one USB port)

We'll go by easier. If class drivers want exclusive fields - give them these fields

'Right' USB handle fields
typedef struct _USBD_HandleTypeDef
{
...
  USBD_MSC_BOT_HandleTypeDef	*pClassDataMSC;
  const USBD_StorageTypeDef 	*pClassSpecificInterfaceMSC;
  USBD_CDC_HandleTypeDef    	*pClassDataCDC;
  const USBD_CDC_ItfTypeDef 	*pClassSpecificInterfaceCDC;
  PCD_HandleTypeDef         	*pPCDHandle;
} USBD_HandleTypeDef;


First, I gave each class its fields - let them torment them as they want. Secondly, I named these fields according to what really lies in them - no UserData there, but a pointer to the interface.

Of course, on the pros, it would be more beautiful and more elegant (with the same memory and CPU consumption). but C can be done humanly. Since I started my little hands in the handle structure, I changed the incomprehensible void * to human types (by the way, the void * pData field is now humanly called pPCDHandle with the corresponding type). And const also arranged where necessary. True, I had to tinker with forward declarations.

About the organization of the project. In some IDEs, a project can be constructed as follows. The USB library and class driver sources come with the STM32 Cube, but some files are suggested to be written to the user. It may happen that the library lies somewhere in a common location and is used by several projects. It is worthwhile to understand that now we are changing the code of the USB library and therefore it is better to have our own copy so as not to disturb anyone.

Of course, renaming fields should be reflected in the driver code. But here everything is simple - contextual replacement solves the problem.

The main thing here is not to overdo it. I've changed hands, looking at each use. There I found a “bug” in the code, repaired it, and then 3 days debazed in an attempt to understand why it does not work.

Type 'bug'
USBD_StatusTypeDef USBD_LL_Reset(USBD_HandleTypeDef  *pdev)
{
...
  if (pdev->pClassData)
	pdev->pClass->DeInit(pdev, pdev->dev_config); 
 ...
}

Here it was all right - we check pClassData, and we turn to pClass. If you fix it (check pClass), it won’t work. Those. pClassData is a kind of marker that the class is initialized.

Returning to our driver. Since Init () initializes both pClassDataXXX variables, you can check any in this code.

UPDATE: An important nuance noticed by fronders .
In the original class implementations (CDC, HID, MSC, and almost all the others) in the initialization functions (for example, USBD_CDC_Init ()), the buffer for the pClassDataXXX field is allocated using USBD_malloc (), which in the template implementation is defined in malloc (). It seems like nothing special - we selected a piece and use its address.

Standard implementation of USBD_CDC_Init ()

static uint8_t  USBD_CDC_Init (USBD_HandleTypeDef *pdev, 
                               uint8_t cfgidx)
{
...
  pdev->pClassData = USBD_malloc(sizeof (USBD_CDC_HandleTypeDef));
...
}


But, in some projects (including examples from STMicroelectronics itself) they decided to save on memory and wrote their implementation of the allocator

not very correct allocator

void *USBD_static_malloc(uint32_t size) {
    static uint32_t mem[(sizeof(USBD_CDC_HandleTypeDef)/4+1)];/* On 32-bit boundary */
    return mem;
}


In principle, this approach will work, but only so far we have only one device class. As soon as several classes try to “allocate” memory through such an allocator, everything will break, because multiple classes will torment the same buffer.

In fact, it will only be necessary to allocate memory if you are building a device with several identical functions - for example, a device that implements two or more CDCs. Well, maybe it will still be needed in some exotic cases, when interfaces are created and deleted on the fly. In all other cases (of which the vast majority) I would not bother with allocating memory and allocated the buffer statically. In my project, I did this (at the same time, the data types were whitened-colored):

static allocation

// A CDC object
USBD_CDC_HandleTypeDef cdcInstance;
...
uint8_t  USBD_CDC_Init (USBD_HandleTypeDef *pdev,
						uint8_t cfgidx)
{
...
  pdev->pClassDataCDC = &cdcInstance;  
  hcdc = pdev->pClassDataCDC;
...
}



Final touch - allocation of PMA buffers

PMA Distribution
 HAL_PCDEx_PMAConfig(pdev->pPCDHandle , 0x00 , PCD_SNG_BUF, 0x20);
 HAL_PCDEx_PMAConfig(pdev->pPCDHandle , 0x80 , PCD_SNG_BUF, 0x60);
 HAL_PCDEx_PMAConfig(pdev->pPCDHandle , MSC_IN_EP,  PCD_SNG_BUF, 0xA0);
 HAL_PCDEx_PMAConfig(pdev->pPCDHandle , MSC_OUT_EP, PCD_SNG_BUF, 0xE0);
 HAL_PCDEx_PMAConfig(pdev->pPCDHandle, CDC_CMD_EP, PCD_SNG_BUF, 0x100);
 HAL_PCDEx_PMAConfig(pdev->pPCDHandle, CDC_IN_EP,  PCD_SNG_BUF, 0x140);
 HAL_PCDEx_PMAConfig(pdev->pPCDHandle, CDC_OUT_EP, PCD_SNG_BUF, 0x180);


For our endpoints, we need 7 buffers - 2 to the zero endpoint (control point), 2 to MSC and 3 to CDC. But the most interesting thing here is the starting addresses (the last parameter). For some reason, this nuance is carefully managed by all the tutorials. The datasheet says about the allocation of buffers in the PMA and how it looks at the register level, but there is no information on how to use the corresponding functions from the HAL. Fill this gap.

So. The controller has a special memory - PMA (Packet Memory Area). This is such a memory where the program can write data, and the USB peripherals read it (and vice versa). This memory is not allocated in advance, because different endpoints can be configured for different packet sizes. Therefore, there is a table BTABLE which indicates where which buffer is located. Moreover, this table itself is also located in the PMA. The table can be moved and placed anywhere in the PMA, but the HAL can only place it at the very beginning.

image
Picture from the Reference Manual STM32F103 Series Microcontrollers

So how do you calculate buffer offsets? The size of the table depends on the number of endpoints used. Each endpoint in the table is represented by a record of 4 16-bit values ​​(2 for reception and 2 for transmission, even if one of the directions is not used). We use 4 endpoints - zero, MSC and two for CDC (do not confuse with the number of buffers - we have 7 of them - two per endpoint, but one point is unidirectional, so it only has one buffer). So the size of the table will be 4 points * 4 records * 2 bytes = 32 bytes.

As I have already said, HAL can locate only at the beginning of the PMA area. So the first buffer we can arrange only at offset 0x20 (32 bytes - table size). Endpoint buffers can be placed anywhere in the PMA memory, as long as they do not overlap each other. Each endpoint determines the maximum packet size that it is ready to process, the buffer must be equal to or greater than this size.

I arranged the buffers in 64-byte increments (the maximum recommended buffer size for USB Full Speed ​​devices), but for some endpoints I could have done less. So, there is not a lot of data running along the CDC control endpoint (CDC_CMD_PACKET_SIZE is 8 bytes), so the buffer can be made only 8 bytes. However, I was not sorry and 32 bytes - just to get round numbers.

It's time to compile and run. My Windows immediately identified the device itself, I also saw 2 components. It's a good news. But there is a bad one. If the Mass Storage device was detected immediately, the CDC is not.

image

It doesn’t matter - you just need to slip the correct driver into Windows. In fact, the device is standard and a special driver is not needed. It’s enough just to connect this device with a standard driver (in our case it will be usbser.sys)

. Actually, I don’t understand much in this kitchen. In principle, you need to download the STMicroelectronics Virtual COM Port driver from the ST site. The driver is installed in C: \ Program Files (x86) \ STMicroelectronics \ Software \ Virtual comport driver, but inside there is a stmcdc.inf file - that’s what we need. In this file in two sections there is a line of the form

%DESCRIPTION%=DriverInstall,USB\VID_0483&PID_5740

So it connects our VID / PID with the device driver. Only this is not enough - you still need to specify the number of the interface that controls the CDC. In my case, this is the first interface (null is responsible for MSC). To do this, the line should look like this.

%DESCRIPTION%=DriverInstall,USB\VID_0483&PID_5741&MI_01

In fact, the original line can not be changed, but simply
add lines to the appropriate sections.

After all the preparations, we find the non-working device in the device list, please update the driver, indicate the directory where the inf file is located and voila - the driver is installed. Windows itself will assign this device the name COMxx - you can take your favorite terminal and open this COM port.

With Linux, everything is simpler - everything starts there without dancing with a tambourine drivers.

UPDATE by fronders: In Windows 10, everything also starts up on its own. Moreover, ST themselves do not recommend their vcp driver for 10k but offer to use the standard one.

Conclusion


I’ve seen posts on some forums like “how complicated everything is in this USB, some drivers ... I’m better off using registers right now”. Guys, it's not so simple. The register level is probably the easiest part. But besides it, there is a huge layer of logic that the device should implement. And here without the knowledge of protocols and many hundreds of pages of specifications in any way.

But it is not all that bad. People already took care and wrote all the logic. In most cases, it remains only to substitute the desired values ​​and adjust some parameters. Yes, the library from ST is still a monster. But after a thoughtful reading of USB In A Nutshell, a couple of specifications for a specific class of devices and working with a sniffer, many things fall into place. The library begins to look more or less slim. You can even make a custom class driver with relatively little effort, which we did with success.

I did an implementation of a composite CDC + MSC device, but about the same approach can be applied to other combinations - CDC + HID, MSC + Audio, CDC + MSC + HID and others. My implementation is designed to work on microcontrollers of the STM32F103 series, but the principle itself can be adapted for other microcontrollers (including and not STM32).

In this article, I did not set myself the task of telling how USB works in all details - firstly, there are articles and books that tell it better (I touched on only a small part), and secondly, a lot of things are better to draw from the primary sources (specifications) .

Instead of retelling the specifications, I tried to describe how the implementation of the USB stack from ST works. I also tried to pay attention to special moments and tell why this is done so.

I have long doubted whether to tick the “Tutorial”. On the one hand, I give recommendations and step-by-step instructions, draw attention to special points and provide links to the source. On the other hand, I cannot provide a ready-made library for downloading and embedding in my projects.

The fact is that in the process of working on my project I did a good job with a file, a jigsaw and other tools on this library. I threw out a lot of code that is not needed in my device, changed a part, repaired some things that I did not like. Now the USB library is very different from the one on the ST website. Some of the changes are specific to my project and may not be suitable for other situations. However, welcome to my repository- study, copy to yourself, ask questions, suggest improvements.

Finally, I want to express gratitude to all those who helped me in one way or another with my implementation. Thanks guys!

Also popular now: