STM32 and USB-HID - it's easy

  • Tutorial
It's 2014, and for connecting microcontrollers with a PC, the most popular means is a regular serial port. It’s easy to start working with it, it is easy to understand to primitiveness - just a stream of bytes.
However, all modern standards have eliminated the COM port from the PC and you have to use USB-UART adapters to get access to your project on the MK. He is not always at hand. Such an adapter does not always work stably due to problems with the drivers. There are other disadvantages.
But every time it comes to talking about using USB or a serial port, there are many fans of the logical simplicity of UART. And they have good reason. However, it’s good to have an alternative?

I have long been asked to tell how to organize a packet data exchange between a PC and MK using the example of STM32F103. I will give a finished working draft and tell you how to adapt it for my needs. And you yourself decide whether you need it or not.

We have a board with a modern inexpensive STM32F103C8 microcontroller with built-in USB hardware support, I talked about it earlier.


I said that the serial port has other drawbacks:
-often the COM port is missing from the PC or laptop
- the device needs to be
powered separately, even if having a COM port in the PC, it is necessary to coordinate the signal levels: the PC uses the RS232 interface with differential signal levels of + 15V and -15V, and microcontrollers use TTL levels (+ 5V, + 3.3V, unipolar).
- Often dozens of virtual COM ports are formed in the system and finding the port that matches your device can be difficult.
In turn, USB has been with us for many years and has its advantages:
-Possibility of supplying power from the HOST device
-Convenient implementation of packet exchange
-Possibility of simultaneous connection to the device by several programs
-Ability to uniquely identify the connected device
-Hardware support in many modern MK, which eliminates the need for adapters
The USB functionality is extremely rich, but this gives rise to a problem - it’s not as easy to understand as with a serial interface. There is a separate class of devices - USB-HID, which do not require the installation of drivers, are specifically designed for interaction with humans and various input-output devices. Ideal for organizing data exchange with MK. Personally, I like batch sharing. This is a convenient abstraction. In addition, disassembling packet messages is somewhat easier and more convenient than working with a simple stream of bytes.

HID Profile Selection


USB-HID is a fairly extensive class of devices, so first of all we will have to choose which device we will create.
We can create emulation of the keyboard, mouse, joystick and other input devices, and we can create our own device so as not to depend on the rather rigid framework of the standard and freely exchange data with a PC.
I will tell you how to make a Custom HID device. This gives maximum freedom. In order not to delay the article, I will try to tell as briefly as possible - there are a lot of descriptions of the standard on the network and without me, but personally they helped me a little when it was necessary to solve a specific problem.

Project structure


I use EmBlocks for development under STM32. You can use any convenient environment, the project is not very difficult to adapt.
The following are added to the basic structure of the project:
  • The USB-FS folder with the STM32F10x, STM32L1xx and STM32F3xx USB-FS-Device Driver library version 4.0.0.
  • The Inc and Src folders contain the files:
    platform_config.h - here are the definitions for a specific board and the MK family
    stm32_it.h, stm32_it.c - the interrupt handlers
    usb_conf.h, usb_endp.c are defined here - the endpoints, sizes are defined here and the addresses of their buffers, handler functions
    usb_desc.h, usb_desc.c - information about the device itself is collected here - how it will be determined when connected to a PC and the size and format of data packets
    hw_config.c are determined - here all the work with sending data to PC
    hw_config.h, usb_istr.h, usb_prop.h, usb_pwr.h
    usb_istr.c, usb_prop.c, usb_pwr.c - need to work on USB-FS iblioteki, but not necessarily to go into them

We add all these files to any project using USB.

USB initialization


For the correct operation of the USB module, the frequency of the MK is important. Not all frequencies allow you to correctly set the USB clock. In our case, a crystal oscillator at 8 MHz is used and the MK operates at a frequency of 72 MHz, and a USB module at 48 MHz.
In main.c, just include a few lines of code
main.c
/* Includes ------------------------------------------------------------------*/
#include "hw_config.h"
#include "usb_lib.h"
#include "usb_pwr.h"
/* Private variables ---------------------------------------------------------*/
__IO uint8_t PrevXferComplete = 1;
int main(void)
{
  Set_System();
  USB_Interrupts_Config();
  Set_USBClock();
  USB_Init();
  while (1)
  {
    if (bDeviceState == CONFIGURED)
    {
      if (PrevXferComplete)
      {
        RHIDCheckState();
      }
    }
  }
}


In the Set_System () function , the pin of the D + line pull -up to the power is configured to programmatically connect / disconnect the device from the PC (it is not used in our board), the interrupt is configured, and the LEDs and buttons for the demo project are initialized.
In USB_Interrupts_Config (), interrupts are configured depending on the MK family (F10x, F37x, L1x are supported).
The USB_Init () function starts the USB module. If you need to temporarily disable USB work for debugging, just comment out this line.
Then, in an endless loop, it is checked whether it was possible to configure the USB module when connected to a PC. If everything worked correctly and the device connected successfully, the PC is turned on and is not in power saving mode, then the state will be CONFIGURED.
Next, it is checked whether the previous data transfer to the PC was completed and if so, then a new packet is being prepared for sending in the RHIDCheckState () function

Packet size and transmission frequency


The USB-HID device cannot initiate the transfer itself, because The bus is coordinated by the host device - the PC. Therefore, when preparing the USB descriptor of our device, we write how often we need to interrogate our device. According to the specification, the maximum polling frequency is 1 kHz and the maximum size of a packet transmitted at a time is 64 bytes. If this is not enough, you will have to use other modes of operation - such as a USB bulk, but there you can’t do without drivers.
Three handles are responsible for setting up interaction with a PC:
Device descriptor
/* USB Standard Device Descriptor */
const uint8_t RHID_DeviceDescriptor[RHID_SIZ_DEVICE_DESC] =
  {
		    RHID_SIZ_DEVICE_DESC,         // общая длина дескриптора устройства в байтах
		    USB_DEVICE_DESCRIPTOR_TYPE, // bDescriptorType - показывает, что это за дескриптор. В данном случае - Device descriptor
		    0x00, 0x02,                 // bcdUSB - какую версию стандарта USB поддерживает устройство. 2.0
			// класс, подкласс устройства и протокол, по стандарту USB. У нас нули, означает каждый интерфейс сам за себя
		    0x00,                       //bDeviceClass
		    0x00,                       //bDeviceSubClass
		    0x00,                       //bDeviceProtocol
		    0x40,                       //bMaxPacketSize - максимальный размер пакетов для Endpoint 0 (при конфигурировании)
			// те самые пресловутые VID и PID,  по которым и определяется, что же это за устройство.
		    0x83, 0x04,                 //idVendor (0x0483)
		    0x11, 0x57,                 //idProduct (0x5711)
		    DEVICE_VER_L, DEVICE_VER_H,                 // bcdDevice rel. DEVICE_VER_H.DEVICE_VER_L  номер релиза устройства
			// дальше идут индексы строк, описывающих производителя, устройство и серийный номер.
			// Отображаются в свойствах устройства в диспетчере устройств
			// А по серийному номеру подключенные устройства с одинаковым VID/PID различаются системой.
		    1,                          //Index of string descriptor describing manufacturer
		    2,                          //Index of string descriptor describing product
		    3,                          //Index of string descriptor describing the device serial number
		    0x01                        // bNumConfigurations - количество возможных конфигураций. У нас одна.
  }
  ; /* CustomHID_DeviceDescriptor */


Everything is pretty transparent in the comments. Pay attention to DEVICE_VER_L, DEVICE_VER_H - these are constants from usb_desc.h, which you can change to identify the version of your device.

Configuration Descriptor (describes device capabilities)
/* USB Configuration Descriptor */
/*   All Descriptors (Configuration, Interface, Endpoint, Class, Vendor */
const uint8_t RHID_ConfigDescriptor[RHID_SIZ_CONFIG_DESC] =
  {
		    0x09, 			// bLength: длина дескриптора конфигурации
		    USB_CONFIGURATION_DESCRIPTOR_TYPE, // bDescriptorType: тип дескриптора - конфигурация
		    RHID_SIZ_CONFIG_DESC, 0x00, // wTotalLength: общий размер всего дерева под данной конфигурацией в байтах
		    0x01,         // bNumInterfaces: в конфигурации всего один интерфейс
		    0x01,         // bConfigurationValue: индекс данной конфигурации
		    0x00,         // iConfiguration: индекс строки, которая описывает эту конфигурацию
		    0xE0,         // bmAttributes: признак того, что устройство будет питаться от шины USB
		    0x32,         // MaxPower 100 mA: и ему хватит 100 мА
				/************** Дескриптор интерфейса ****************/
				0x09,         // bLength: размер дескриптора интерфейса
				USB_INTERFACE_DESCRIPTOR_TYPE, // bDescriptorType: тип дескриптора - интерфейс
				0x00,         // bInterfaceNumber: порядковый номер интерфейса - 0
				0x00,         // bAlternateSetting: признак альтернативного интерфейса, у нас не используется
				0x02,         // bNumEndpoints - количество эндпоинтов.
				0x03,         // bInterfaceClass: класс интерфеса - HID
				// если бы мы косили под стандартное устройство, например клавиатуру или мышь, то надо было бы указать правильно класс и подкласс
				// а так у нас общее HID-устройство
				0x00,         // bInterfaceSubClass : подкласс интерфейса.
				0x00,         // nInterfaceProtocol : протокол интерфейса
				0,            // iInterface: индекс строки, описывающей интерфейс
					// теперь отдельный дескриптор для уточнения того, что данный интерфейс - это HID устройство
					/******************** HID дескриптор ********************/
					0x09,         // bLength: длина HID-дескриптора
					HID_DESCRIPTOR_TYPE, // bDescriptorType: тип дескриптора - HID
					0x01, 0x01,   // bcdHID: номер версии HID 1.1
					0x00,         // bCountryCode: код страны (если нужен)
					0x01,         // bNumDescriptors: Сколько дальше будет report дескрипторов
						HID_REPORT_DESCRIPTOR_TYPE,         // bDescriptorType: Тип дескриптора - report
						RHID_SIZ_REPORT_DESC,	0x00, // wItemLength: длина report-дескриптора
					/******************** дескриптор конечных точек (endpoints) ********************/
					0x07,          // bLength: длина дескриптора
					USB_ENDPOINT_DESCRIPTOR_TYPE, // тип дескриптора - endpoints
					0x81,          // bEndpointAddress: адрес конечной точки и направление 1(IN)
					0x03,          // bmAttributes: тип конечной точки - Interrupt endpoint
					wMaxPacketSize, 0x00,    // wMaxPacketSize:  Bytes max
					0x20,          // bInterval: Polling Interval (32 ms)
          0x07,	/* bLength: Endpoint Descriptor size */
          USB_ENDPOINT_DESCRIPTOR_TYPE,	/* bDescriptorType: */
            /*	Endpoint descriptor type */
          0x01,	/* bEndpointAddress: */
            /*	Endpoint Address (OUT) */
          0x03,	/* bmAttributes: Interrupt endpoint */
          wMaxPacketSize,	/* wMaxPacketSize:  Bytes max  */
          0x00,
          0x20,	/* bInterval: Polling Interval (32 ms) */
}
  ; /* RHID_ConfigDescriptor */


Here it is worth paying attention to the wMaxPacketSize constant - it determines the maximum packet size that we will exchange with the PC. The project is configured so that when it changes, the size of the buffers also changes. But do not forget that you should not specify more than 0x40 by the standard. Be careful with this constant - if the transmitted packet is different in size, there will be problems!
The next constant with the comment bInterval is the polling period of the device in milliseconds. 32ms are set for our device.

Report handle (describes the protocol)
const uint8_t RHID_ReportDescriptor[RHID_SIZ_REPORT_DESC] =
  {
    0x06, 0x00, 0xff,              // USAGE_PAGE (Generic Desktop)
    0x09, 0x01,                    // USAGE (Vendor Usage 1)
    0xa1, 0x01,                    // COLLECTION (Application)
    0x85, 0x01,                    //   REPORT_ID (1)
    0x09, 0x01,                    //   USAGE (Vendor Usage 1)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x95, 0x01,                    //   REPORT_COUNT (1)
    0xb1, 0x82,                    //   FEATURE (Data,Var,Abs,Vol)
    0x85, 0x01,                    //   REPORT_ID (1)
    0x09, 0x01,                    //   USAGE (Vendor Usage 1)
    0x91, 0x82,                    //   OUTPUT (Data,Var,Abs,Vol)
    0x85, 0x02,                    //   REPORT_ID (2)
    0x09, 0x02,                    //   USAGE (Vendor Usage 2)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x95, 0x01,                    //   REPORT_COUNT (1)
    0xb1, 0x82,                    //   FEATURE (Data,Var,Abs,Vol)
    0x85, 0x02,                    //   REPORT_ID (2)
    0x09, 0x02,                    //   USAGE (Vendor Usage 2)
    0x91, 0x82,                    //   OUTPUT (Data,Var,Abs,Vol)
    0x85, 0x03,                    //   REPORT_ID (3)
    0x09, 0x03,                    //   USAGE (Vendor Usage 3)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x26, 0xff, 0x00,              //   LOGICAL_MAXIMUM (255)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x95, RPT3_COUNT,                    //   REPORT_COUNT (N)
    0xb1, 0x82,                    //   FEATURE (Data,Var,Abs,Vol)
    0x85, 0x03,                    //   REPORT_ID (3)
    0x09, 0x03,                    //   USAGE (Vendor Usage 3)
    0x91, 0x82,                    //   OUTPUT (Data,Var,Abs,Vol)
    0x85, 0x04,                    //   REPORT_ID (4)
    0x09, 0x04,                    //   USAGE (Vendor Usage 4)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x95, RPT4_COUNT,                    //   REPORT_COUNT (N)
    0x81, 0x82,                    //   INPUT (Data,Var,Abs,Vol)
    0xc0                           // END_COLLECTION
}


This is the most important descriptor - it describes the exchange protocol and device functionality. Its formation is not an easy task. If you make a mistake when forming the descriptor, the device will stop working. The descriptor format is very hard. There is even a special HID Descriptor tool utility . And at the root of the project is the file "RHID.hid" with the descriptor described above for editing in this utility. But if you don’t understand what you’re doing, it’s better not to get involved.
For simplicity, I made two constants:
RPT3_COUNT - the size of the OUTPUT buffer in bytes for transferring the packet to the MK (in the example - 1 byte)
RPT4_COUNT - the size of the INPUT buffer in bytes for transferring the packet to the PC (in the example - 4 bytes) The
size of any of these buffers should not exceedwMaxPacketSize . Less is possible.
By the way, you can turn Custom HID into another HID device, for example, a keyboard or joystick, only by rewriting ReportDescriptor and changing the class and subclass of the device in the configuration descriptor.

What is Report?


The host (PC) and device (MK) exchange data packets of a predefined structure - report. There can be a lot of packages, they can be provided for all occasions - for example, a package with data about some events in the device, a package with data that was requested by the PC, a package with a command for MK. Anything. But the structure of all packages must be described in the structure of RHID_ReportDescriptor.
PC and MK distinguish between reports by ID, which is the first byte in the packet.
In our example, there are 4 types of reports:
  • REPORT_ID = 1 and 2 - MK command to turn on / off LED1 / LED2. It contains a 1-bit field with the desired LED status and supports sending both by the SET_REPORT method and the SET_FEATURE method (more on this later).
  • REPORT_ID = 3 - transfers one byte to MK. Just to show how to transmit MK data. We will pass the position of the slider.
  • REPORT_ID = 4 is a report for transferring PC data. It returns information about the current state of the LEDs, buttons (if any) and returns the data transmitted in the report with ID = 3 bytes to show that the data has been received.

If you have not fully figured out how to create a report descriptor, then just change the constants RPT3_COUNT and RPT4_COUNT, setting the desired size of outgoing and incoming (from the point of view of the PC) packets. Other reports can simply not be touched, they will not hurt. Do not forget that the first byte must be a report ID.

Exchange cycle


So, we configured our device by setting the PID, VID, version number, configured the sizes of incoming and outgoing packets and are ready to work.
Every 32ms, as we requested in the configuration descriptor, the host will poll us and in the RHIDCheckState function we check - if we have something to send, we form a data packet for the host.
RHIDCheckState - data sending function
/*******************************************************************************
* Function Name : RHIDCheckState.
* Description   : Decodes the RHID state.
* Input         : None.
* Output        : None.
* Return value  : The state value.
*******************************************************************************/
uint16_t btn1_prev, btn2_prev;
uint8_t Buffer[RPT4_COUNT+1];
uint8_t RHIDCheckState(void)
{
    uint16_t btn1=0, btn2=0;
    btn1 = GPIO_ReadInputDataBit(BTN1_PORT, BTN1_PIN);
    btn2 = GPIO_ReadInputDataBit(BTN2_PORT, BTN2_PIN);
    Buffer[0] = 4;
    Buffer[1] = btn1;
    Buffer[2] = btn2;
    Buffer[3] = (GPIO_ReadInputDataBit(LED_PORT, LED1_PIN) | GPIO_ReadInputDataBit(LED_PORT, LED2_PIN)<<1);
    /* Reset the control token to inform upper layer that a transfer is ongoing */
    PrevXferComplete = 0;
    /* Copy buffer date info in ENDP1 Tx Packet Memory Area*/
    USB_SIL_Write(EP1_IN, Buffer, RPT4_COUNT+1);
    /* Enable endpoint for transmission */
    SetEPTxValid(ENDP1);
    return (btn1 | btn2<<1);
}


The uint8_t Buffer [RPT4_COUNT + 1] array is defined as the size of the payload of the incoming (always considered from the host's point of view) packet + byte ID. This is important - if the buffer size is different - there will be problems. Therefore, to change the buffer size, edit the constant value in usb_desc.h.
In the function, we collect data in a package, set the flag PrevXferComplete = 0, indicating that the data is being sent and call the USB_SIL_Write and SetEPTxValid library functions to send data to the host.
That's all, the data transfer to the host is over.

Receiving data is a little more complicated - there are two ways to send data to the device - one of them is to use the features of the device described in the report descriptor, with the corresponding parameters through the SET_FEAUTRE function . This is some abstraction, for beautifully managing the device with a bunch of functions, so that you can call meaningful functions, and not just send a stream of bytes.
The second way is to work with the device as a file - just write the package to it as a file. This method is called SET_REPORT . In fact, it works a little slower.
Our device supports both methods, as we told the host in the report descriptor.

Processing SET_FEATURE

Data sent by the SET_FEAUTRE method is processed in usb_prop.c

function HID_Status_In
/*******************************************************************************
* Function Name  : HID_Status_In.
* Description    : HID status IN routine.
* Input          : None.
* Output         : None.
* Return         : None.
*******************************************************************************/
void HID_Status_In(void)
{
  BitAction Led_State;
  if (Report_Buf[1] == 0)
  {
    Led_State = Bit_RESET;
  }
  else
  {
    Led_State = Bit_SET;
  }
  switch (Report_Buf[0])
  {
    case 1: /* Led 1 */
     if (Led_State != Bit_RESET)
     {
       GPIO_SetBits(LED_PORT,LED1_PIN);
     }
     else
     {
       GPIO_ResetBits(LED_PORT,LED1_PIN);
     }
     break;
    case 2: /* Led 2 */
     if (Led_State != Bit_RESET)
     {
       GPIO_SetBits(LED_PORT,LED2_PIN);
     }
     else
     {
       GPIO_ResetBits(LED_PORT,LED2_PIN);
     }
      break;
    case 3: /* Led 1&2 */
       Buffer[4]=Report_Buf[1];
     break;
  }
}


Here we check the first byte in the report and in accordance with it we process the rest of the packet - we control the LEDs or just take the byte sent to us by the host and put it in the packet for later sending back to the RHIDCheckState functions.
Under Report_Buf, wMaxPacketSize bytes are reserved so that any packet that the host sends to us can fit.

Data sent by the SET_REPORT method is processed in usb_endp.c
function EP1_OUT_Callback
/*******************************************************************************
* Function Name  : EP1_OUT_Callback.
* Description    : EP1 OUT Callback Routine.
* Input          : None.
* Output         : None.
* Return         : None.
*******************************************************************************/
void EP1_OUT_Callback(void)
{
  BitAction Led_State;
  /* Read received data (2 bytes) */
  USB_SIL_Read(EP1_OUT, Receive_Buffer);
  if (Receive_Buffer[1] == 0)
  {
    Led_State = Bit_RESET;
  }
  else
  {
    Led_State = Bit_SET;
  }
  switch (Receive_Buffer[0])
  {
    case 1: /* Led 1 */
     if (Led_State != Bit_RESET)
     {
       GPIO_SetBits(LED_PORT,LED1_PIN);
     }
     else
     {
       GPIO_ResetBits(LED_PORT,LED1_PIN);
     }
     break;
    case 2: /* Led 2 */
     if (Led_State != Bit_RESET)
     {
       GPIO_SetBits(LED_PORT,LED2_PIN);
     }
     else
     {
       GPIO_ResetBits(LED_PORT,LED2_PIN);
     }
      break;
    case 3: /* Led 1&2 */
        Buffer[4]=Receive_Buffer[1];
     break;
  }
  SetEPRxStatus(ENDP1, EP_RX_VALID);
}


Here it is almost the same, only you need to pick up the data yourself by calling USB_SIL_Read (EP1_OUT, Receive_Buffer) and at the end inform that we ended up calling SetEPRxStatus (ENDP1, EP_RX_VALID);

We learned how to configure the device, transmit and receive data in packets of the right size with the frequency we need.
We assemble the project and flash it into the device.
It will work something like this:


The project supports interaction with the USB HID Demonstrator utility from ST Microelectronics.
The Device capabilities page displays the capabilities described in Report Descriptor.
Input / Output transfer allows you to manually send data to the device and see the packet that comes from it.
Graphic view allows you to control the LEDs, checkboxes Led 1, Led 2, setting the corresponding Report ID, as well as transfer bytes with a slider (ReportID = 3)


. I also wrote a small demo software that automatically determines the connection to the computer and disconnects our device by its VID and PID, displays the status - connected / disconnected by the indicator next to the Auto Connect

checkbox The Send using radio button allows you to choose the method of sending data to the device.
Report: displays the packet received from the device byte, starting with ReportID.
Clicking on the LEDs below - we control the LEDs of the device. Their status displays the current status of the device. It is read from the report from the device.
Moving the slider, we send Report with ID = 3 and the value corresponding to the position of the slider. The device will return this value in 4 bytes of the report.
The combo box displays the HID devices found in the system and if our device is found, its name is displayed.

You can download everything you need on GitHub . Composed of :
DT - HID Descriptor tool
tstHID-STM32F103 - project for EmBlocks
USB HID Demonstrator - utility from ST Microelectronics
HIDSTM32.exe - my demo software on Delphi has a similar functionality, but does not require configuration.

If you still have questions, write in the comments. I will try to answer. I tried not to drown the essence in a bunch of little things, so that a common understanding would develop. The rest can already be understood by studying the project. But if you need to quickly make your device, and there is no time to climb into the jungle - all that you need, I have described.

PSBy default, when the host goes into power saving mode, the device falls asleep with it, and if you connect the device to a sleeping PC, it will also go to sleep. Therefore, if we simply plug a power supply into the device or power it from the battery, it will not work, assuming that it is connected to a sleeping PC (configuration packages will not come from the PSU for sure). I changed the library so that the device worked even when connecting just a PSU. Therefore, the device will work both when connected to a PC and autonomously. (It took me a long time to figure this out.)

Also popular now: