Recently i got into USB device driver development for linux, I’ve always wanted to know how USB works at hardware as well as software level.

So i fired up google and looked up for USB specifications, and general documentation for it.

The best resource’s on the internet that i found were www.usbmadesimple.co.uk and the book USB complete by Jan Alexson.

The guys at usbmadesimple.co.uk explained the gist of USB in a very easy to understand manner, I highly recommend anyone going into USB to give it a go, then the book USB Complete gives detailed overview of every concept. Apart from that, there is the USB specification available from usb.org but it is too big of a read just to get a general idea of how stuff works under the hood.

I had a STM32 blue pill lying around me that has a USB FS peripheral build in, I thought why not use it to experiment with USB and get a basic example running which can send and recieve data. At the end of this tutorial, you will have a STM32 blupill configured as a USB device and a program running on a host PC can communicate with the bluepill over USB to turn the LED on PC13 on/off.

So i hooked up the blue pill to my Stlink V2 via SWD and fired up STM32CubeMX IDE.

I Started a new project selecting STM32F103C8 as my MCU, after the project was initialised the first thing we need to do is to configure the clocks.

I would be running my blue pill at 72Mhz, so i select Crystal/Ceramic resonator in HSE(RCC) and then configure the clocks and PLL so that i get a SYSCLK of 72Mhz. Make sure the USB prescaler is at 1.5 and thus the USB clock is 48Mhz.

After that configure PC13 as GPIO output and assign it a label such as USR_LED.

Then we need to go to Connectivity and enable USB FS Device. After that we need to select our Middleware which would be Custom HID Class under USB_DEVICE.

If you are new to USB, then you should first check out the links i’ve posted up top to get a overview, It would help you understand better what comes now.

Although i’ve selected the Custom HID class, we won’t be dealing with HID, we will setup our own custom device discriptor,configuration discriptor, interface discriptor and endpoints.

So let’s change the device discriptor first, Under your project tree go to USB_DEVICE/App/ and open usbd_desc.c and look for the following:

/** @defgroup USBD_DESC_Private_Defines USBD_DESC_Private_Defines
  * @brief Private defines.
  * @{
  */

#define USBD_VID     1155
#define USBD_LANGID_STRING     1033
#define USBD_MANUFACTURER_STRING     "STMicroelectronics"
#define USBD_PID_FS     65297
#define USBD_PRODUCT_STRING_FS     "STM32 Custom Human interface"
#define USBD_CONFIGURATION_STRING_FS     "Custom HID Config"
#define USBD_INTERFACE_STRING_FS     "Custom HID Interface"

You can change the VID and PID here or you can do that in the USB_DEVICE parameter Settings. You can also assign any product string, configuration string and interface string you like here.

After that navigate to Middlewares/ST/STM32_USB_Device_Library/Class/CustomHID/Src/usbd_customhid.c and open it.

/* USB CUSTOM_HID device FS Configuration Descriptor */
__ALIGN_BEGIN static uint8_t USBD_CUSTOM_HID_CfgFSDesc[USB_CUSTOM_HID_CONFIG_DESC_SIZ] __ALIGN_END =
{
  0x09, /* bLength: Configuration Descriptor size */
  USB_DESC_TYPE_CONFIGURATION, /* bDescriptorType: Configuration */
  USB_CUSTOM_HID_CONFIG_DESC_SIZ,
  /* wTotalLength: Bytes returned */
  0x00,
  0x01,         /*bNumInterfaces: 1 interface*/
  0x01,         /*bConfigurationValue: Configuration value*/
  0x00,         /*iConfiguration: Index of string descriptor describing
  the configuration*/
  0xC0,         /*bmAttributes: bus powered */
  0xFA,         /*MaxPower 500 mA: this current is used for detecting Vbus*/

  /************** Descriptor of CUSTOM HID interface ****************/
  /* 09 */
  0x09,         /*bLength: Interface Descriptor size*/
  USB_DESC_TYPE_INTERFACE,/*bDescriptorType: Interface descriptor type*/
  0x00,         /*bInterfaceNumber: Number of Interface*/
  0x00,         /*bAlternateSetting: Alternate setting*/
  0x02,         /*bNumEndpoints*/
  0xFF,         /*bInterfaceClass: Vendor Specific */
  0x00,         /*bInterfaceSubClass : 1=BOOT, 0=no boot*/
  0x00,         /*nInterfaceProtocol : 0=none, 1=keyboard, 2=mouse*/
  0,            /*iInterface: Index of string descriptor*/
  /******************** Descriptor of CUSTOM_HID *************************/
  /* 18 */
  /******************** Descriptor of Custom HID endpoints ********************/
  /* 27 */
  0x07,          /*bLength: Endpoint Descriptor size*/
  USB_DESC_TYPE_ENDPOINT, /*bDescriptorType:*/

  CUSTOM_HID_EPIN_ADDR,     /*bEndpointAddress: Endpoint Address (IN)*/
  0x02,          /*bmAttributes: Bulk endpoint*/
  CUSTOM_HID_EPIN_SIZE, /*wMaxPacketSize: 64 Byte max */
  0x00,
  CUSTOM_HID_FS_BINTERVAL,          /*bInterval: Polling Interval */
  /* 34 */

  0x07,          /* bLength: Endpoint Descriptor size */
  USB_DESC_TYPE_ENDPOINT, /* bDescriptorType: */
  CUSTOM_HID_EPOUT_ADDR,  /*bEndpointAddress: Endpoint Address (OUT)*/
  0x02, /* bmAttributes: Bulk endpoint */
  CUSTOM_HID_EPOUT_SIZE,  /* wMaxPacketSize: 64 Bytes max  */
  0x00,
  CUSTOM_HID_FS_BINTERVAL,  /* bInterval: Polling Interval */
  /* 41 */
};

Scroll down and look for the FS configuration descriptor like above and change yours to the one i have set up above. The default configuration descriptor sets up 2 Interupt endpoints and we change them to bulk endpoints, then we change the interface class from HID class to Vendor Specific class (0xFF) and we set the max bus power to 500ma from 100ma. You can change the MaxPacketSize to whatever you want(there is a limit), I’ve kept it 64 bytes, you need to change the CUSTOM_HID_EPOUT_SIZE and set it to 0x40(64).

This whole data structure combines the configuration descriptor,interface descriptor and the endpoint descriptor.

Next, go to your main.c file and declare the following somewhere


#include "usbd_customhid.h"
extern USBD_HandleTypeDef hUsbDeviceFS;
uint8_t USB_RX_Ready;
uint8_t USB_RX_Buffer[USBD_CUSTOMHID_OUTREPORT_BUF_SIZE];
uint8_t USB_TX_Buffer[USBD_CUSTOMHID_OUTREPORT_BUF_SIZE];

These will be the buffers used to recieve and transmit data, the USB_RX_Ready is used as a flag to know when we have recieved any data so that it can be read.

Dont forget to change the value of USBD_CUSTOMHID_OUTREPORT_BUF_SIZE to 0x40(64) same as our endpoint size.

Open the file usbd_custom_hid_if.c and up on top somewhere add these lines..


extern uint8_t USB_RX_Buffer[USBD_CUSTOMHID_OUTREPORT_BUF_SIZE];
extern uint8_t USB_TX_Buffer[USBD_CUSTOMHID_OUTREPORT_BUF_SIZE];
extern uint8_t USB_RX_Ready;

Down below find a function named CUSTOM_HID_OutEvent_FS and replace it with this, Also change its function prototype at the beginning to match the new arguments description.


static int8_t CUSTOM_HID_OutEvent_FS(uint8_t *buffer, uint32_t len)
{
  /* USER CODE BEGIN 6 */
  USB_RX_Ready = 1;
  return (USBD_OK);
  /* USER CODE END 6 */
}

Now go back to usbd_customhid.c and replace the default USBD_CUSTOM_HID_DataOut function with the following.

static uint8_t  USBD_CUSTOM_HID_DataOut(USBD_HandleTypeDef *pdev,
                                        uint8_t epnum)
{

  USBD_CUSTOM_HID_HandleTypeDef     *hhid = (USBD_CUSTOM_HID_HandleTypeDef *)pdev->pClassData;

  ((USBD_CUSTOM_HID_ItfTypeDef *)pdev->pUserData)->OutEvent(USB_RX_Buffer,USBD_CUSTOMHID_OUTREPORT_BUF_SIZE);

  USBD_LL_PrepareReceive(pdev, CUSTOM_HID_EPOUT_ADDR, USB_RX_Buffer,
                         USBD_CUSTOMHID_OUTREPORT_BUF_SIZE);

  return USBD_OK;
}

Now we are ready to send and receive data. Lets edit main.c

/* Infinite loop */
  /* USER CODE BEGIN WHILE */
  memset(USB_RX_Buffer,0,64);

  sprintf(USB_TX_Buffer,"Hello from STM32!");

  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

 if(USB_RX_Ready)
 {
 HAL_UART_Transmit(&huart1, "Data: ", 7, 10);
 HAL_UART_Transmit(&huart1, USB_RX_Buffer, 64, 50);
 HAL_UART_Transmit(&huart1, "\n", 2, 10);
 USB_RX_Ready = 0;
 if(USB_RX_Buffer[0] == '0')
 {
 HAL_GPIO_WritePin(USR_LED_GPIO_Port, USR_LED_Pin, GPIO_PIN_RESET);
 }else if(USB_RX_Buffer[0] == '1')
 {
 HAL_GPIO_WritePin(USR_LED_GPIO_Port, USR_LED_Pin, GPIO_PIN_SET);
 }else
 {

 }

 }
 USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS,USB_TX_Buffer, 64);
 HAL_Delay(10);
  }
  /* USER CODE END 3 */
  

This will be our main loop. Now compile the code and flash it to the blue pill.

If everything was done right, when you plug in the device in a linux based distribution, you can run the following to test if everything was set up correctly.

user@machine:/# lsusb -d 0483:ff11 -v
Bus 001 Device 003: ID 0483:ff11 STMicroelectronics
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               2.00
  bDeviceClass            0 (Defined at Interface level)
  bDeviceSubClass         0
  bDeviceProtocol         0
  bMaxPacketSize0        64
  idVendor           0x0483 STMicroelectronics
  idProduct          0xff11
  bcdDevice            2.00
  iManufacturer           1 STMicroelectronics
  iProduct                2 STM32 Custom Human interface
  iSerial                 3 8D73339B5552
  bNumConfigurations      1
  Configuration Descriptor:
    bLength                 9
    bDescriptorType         2
    wTotalLength           32
    bNumInterfaces          1
    bConfigurationValue     1
    iConfiguration          0
    bmAttributes         0xc0
      Self Powered
    MaxPower              500mA
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           2
      bInterfaceClass       255 Vendor Specific Class
      bInterfaceSubClass      0
      bInterfaceProtocol      0
      iInterface              0
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x81  EP 1 IN
        bmAttributes            2
          Transfer Type            Bulk
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval               1
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x01  EP 1 OUT
        bmAttributes            2
          Transfer Type            Bulk
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval               1
Device Status:     0x0000
  (Bus Powered)
  

Thus we have our STM32 blue pill ready for action!

In a following tutorial i will write a linux USB driver to communicate with our STM32 from a linux system and read and write to it to turn the LED on/off.

If you want the source for this project as a reference you can get it from my github here: https://github.com/maglash64/STM32_Custom_USB