STM32CubeMX, TrueStudio and Custom HID Code


Last Update: Aug 9, 2020 @ 14:48 Preliminary information, subject to change.

This is an Application Note for USB Device Developers.

Readers are expected to be familiar with using STM32CubeMX to select a target MCU and define the clocks, GPIO pins, Connectivity and Middlewares. In-depth knowledge of USB descriptor structures and terminology is also expected.

We have moved Decaf USB firmware development from the older CMSIS library code base for all current and new design boards to STM32CubeMX (MX) and TrueStudio (TS).

This App Note discusses the issues in setting up, modifying and building a working USB CustomHID device using MX and TS. We do not discuss the intricacies of USB Reports, Features and similar except in passing, and refer to you to Jan Axelson, “USB Complete-Fourth Edition” or later.

We also advise you to locate the STM document UM1732 Rev.4 or later, as this contains a reasonable introduction to the STM32 Cube USB Libraries.

There are many benefits to MX , notably

  • it dramatically simplifies the clock setup, I/O, peripheral and connectivity configuration of the target MCU.
  • a complete template source code base and TS project is created, and compiles successfully.
  • USB device only, or FreeRTOS+USB device code can be generated.

There are also two downsides to MX which have to be handled.

  • The MX generated code base is very different to the old CMSIS, in that there is a Core firmware and a Hardware Abstraction Layer (HAL) for the specific MCU chip. The various USB Descriptor formats remain unchanged.
  • Discovering what in the project code base has to be changed and added to create a fully functioning device has quite a learning curve, but then so did using the old CMSIS libraries.

All Decaf boards are USB Human Interface Devices (HID), with sensor/control and option LCDs, which means bi-directional USB communications are required.

To do this, the MX project is configured as a USB CustomHID. An MX project can also be configured as a USB HID: while this is useful as an initial learning project, it is not suitable for devices such as the Decaf boards without major modifications.

Searching the Net for clues to creating a usable USB CustomHID will present a lot of USB HID blogs and similar pages. Some merely list the steps to configure MX, create the code base and build, unmodified, using TS. Very few have the modifications and descriptions of what is needed to make a usable device.

In the following note we will make the occasional reference the USB HID project as the USB CustomHID project is described.

Our initial aim was to understand how to create a working CustomHID for the Decaf IOC-M-2 board with the same functionality as the old firmware on the IOC-M, as this was the simplest device. Specifically,

  • Create a device that the host Windows 10 PC can successfully enumerate.
  • Set up the Report Descriptor that defines the I/O for the IOC-M-2.
  • Set up the Endpoint processing for all Reports.
  • Successfully run the IOC-M test functions against the IOC-M-2 using PiXCL 19 libraries.

With all the above, refactoring the old code to MX and TS for all the other Decaf boards, esspecially those with LCDs, will be simplified.

What follows is a discussion of the issues we discovered and the solutions needed to resolve them. We don’t discuss the MX configuration of the hardware: we configured it to suit the Decaf IOC-M-2 which uses a 72 MHz STM32F103C8T6, and provides 8 digital inputs, 8 digital outputs (active H), 2 device control outputs (active L), 2 analog inputs (0-3.3v). Four wire SPI is configured. Sixteen Reports (i.e. USB commands) that support the IOC-M-2 I/O and control functions are specified.

The generated code has two main directories where development changes are done:

<Project_Name>\Src and \Inc which is the top firmware code level, and where main.c and other related files are located.

Middleware … STM32_USB_Device_Library…Class\Src and \Inc which are the interface between your firmware (main.c etc) and the MX Core (i.e. Low Level) functions.

There is not (and ideally should not) be any requirement to modify the Low Level code base.

Step #1: Initial mods to the generated code

The USB Report Descriptor size is specified in a #define statement in usbd_conf.h. This will be changed below.

#define USBD_CUSTOM_HID_REPORT_DESC_SIZE  02U

This value is used in the set of Config Descriptors (in
Middleware … usbd_customhid.c) and is a two byte value. As generated, this is

USBD_CUSTOM_HID_REPORT_DESC_SIZE, /* wItemLength of Report Descriptor */
0x00,

This works if the size of the Report Descriptor is less than 256 bytes, for example if your device has to respond with a small set of commands. In most cases, a comprehensive command set will require a Report Descriptor size substantially larger than 256 bytes. The general solution is to modify the two lines of code as follows:

LOBYTE(USBD_CUSTOM_HID_REPORT_DESC_SIZE), /* wItemLength */
HIBYTE(USBD_CUSTOM_HID_REPORT_DESC_SIZE),

This simple code adjustment allows the Report Descriptor to be any size needed. As you define the command set in the Report Descriptor, you also update the #define statement with the latest size e.g.

#define USBD_CUSTOM_HID_REPORT_DESC_SIZE  363U

If the LOBYTE, HIBYTE macros are not used for SIZE > 255, the compiler issues a WARNING:

"Large integer implicitly truncated to unsigned type [-Woverflow]"

Optional Step: Set the device serial number string size

The STM mcu has a chip unique 96 bit serial number. In the code as generated, this is converted to a 12 character string which is returned in the Device Descriptor when the board is enumerated. Earlier Decaf boards generated a 24 character string, so we adjust the code, as follows.

In usbd_descr.h

#define USB_SIZ_STRING_SERIAL    0x32  // 24 Unicode chars + 2

In usbd_descr.c

static void Get_SerialNum(void)
{
  uint32_t deviceserial0, deviceserial1, deviceserial2;
  deviceserial0 = *(uint32_t *) DEVICE_ID1;
  deviceserial1 = *(uint32_t *) DEVICE_ID2;
  deviceserial2 = *(uint32_t *) DEVICE_ID3;
  if (deviceserial0 != 0)
  {
	IntToUnicode(deviceserial0, &USBD_StringSerial[2], 8);
	IntToUnicode(deviceserial1, &USBD_StringSerial[18], 8);
	IntToUnicode(deviceserial2, &USBD_StringSerial[34], 8);
  }
}

Step #2: Set your device name strings

These are a set of #define statements in usbd_desc.c that the device passes back to the host PC during the enumeration phase.

Step #3: Set the OUT and IN endpoint report sizes.

As generated, the report sizes are defined as 2. This has to be changed to suit your device. A typical (and max) value is 64 for Full Speed devices, and more for High Speed devices . There’s also some code changes that reflect the new values. In file usbd_customhid.h

 #define CUSTOM_HID_EPIN_ADDR                 0x81U     // IN Endpoint
#define CUSTOM_HID_EPIN_SIZE 0x24U // 36 bytes
#define CUSTOM_HID_EPOUT_ADDR 0x01U // OUT Endpoint
#define CUSTOM_HID_EPOUT_SIZE 0x40U // 64 bytes

The ADDR values are the default generated values, and are suitable for just about all situations. The SIZE values are defined when the command report set is created. For USB Full Speed, EPOUT_SIZE is maximum 64 bytes.

Step #4: Set the OUT report buffer size

As generated, the OUT report buffer size is set to 2, as is the template code for the Report Descriptor where all the supported OUT and IN reports are defined. In file usbd_conf.h

#define USBD_CUSTOMHID_OUTREPORT_BUF_SIZE     64U
#define USBD_CUSTOM_HID_REPORT_DESC_SIZE 2U
// Increases as IN and OUT reports are added.

The OUTREPORT_BUFF_SIZE is used to create the structure that contains the commands sent to the device. Even when no reports have been defined, this value has to be present. REPORT_DESCR_SIZE is absolutely critical, and is the size in bytes of the defined Report Descriptor in file usbd_custom_hid_if.c as generated.

 static uint8_t CUSTOM_HID_ReportDesc_FS[USBD_CUSTOM_HID_REPORT_DESC_SIZE]
{ 0x00,
0xC0 /* End COLLECTION */
};

The above Report Descriptor is where the OUT and IN descriptors are defined, and the number of bytes has to be correct. e.g. for the command set we created for the IOC-M-2, this is 424, so we need …

 define USBD_CUSTOM_HID_REPORT_DESC_SIZE       424U 

Step #5: More changes for the OUT Report Buffer

In file MiddleWares...usbd_customhid.h, the structure below is declared and modified.

 typedef struct _USBD_CUSTOM_HID_Itf
 {
   uint8_t *pReport;
   int8_t (* Init) (void);
   int8_t (* DeInit) (void);
   // PiXCL: the output was originally 2 bytes, but now needs 
   // a buffer CUSTOM_HID_EPOUT_SIZE
   // int8_t (* OutEvent) (uint8_t, uint8_t); Changed to below
   int8_t (* OutEvent) (uint8_t*); // See used in usbd_customhid.c
 } USBD_CUSTOM_HID_ItfTypeDef;

The change to the OutEvent buffer to an array from 2 bytes also requires updates to usbd_customhid.c

static uint8_t  
USBD_CUSTOM_HID_DataOut(USBD_HandleTypeDef *pdev, uint8_t epnum)
{
USBD_CUSTOM_HID_HandleTypeDef *hhid =
(USBD_CUSTOM_HID_HandleTypeDef *)pdev->pClassData;
/*
PiXCL: Original default code. Replaced because hhid->Report_buf is an array, not two bytes
((USBD_CUSTOM_HID_ItfTypeDef *)pdev->pUserData)->OutEvent(hhid->Report_buf[0], hhid->Report_buf[1]);
*/
((USBD_CUSTOM_HID_ItfTypeDef *)pdev->pUserData)->OutEvent(hhid->Report_buf);
USBD_LL_PrepareReceive(pdev, CUSTOM_HID_EPOUT_ADDR, hhid->Report_buf,
USBD_CUSTOMHID_OUTREPORT_BUF_SIZE);
return USBD_OK;
}

and also


static uint8_t USBD_CUSTOM_HID_EP0_RxReady(USBD_HandleTypeDef *pdev)
{
USBD_CUSTOM_HID_HandleTypeDef *hhid = (USBD_CUSTOM_HID_HandleTypeDef *)pdev->pClassData;
if (hhid->IsReportAvailable == 1U)
{
((USBD_CUSTOM_HID_ItfTypeDef *)pdev->pUserData)->OutEvent(hhid->Report_buf);
hhid->IsReportAvailable = 0U;
}
return USBD_OK;
}

Step #6: A discussion of OUT and IN Report handling.

MX code in very general terms consists of a three modules:

  • your device independent code
  • MiddleWare; and
  • Core code (a.k.a. Low Level) which is device dependent.

MiddleWare code communicates with your device code and the Core. You will need to make a few mods in this area. You should NEVER need to modify anything in the Core code area.

When a USB device is working properly, end user app code sends reports OUT to the device. Some OUT reports simply instruct the device to do something, like turn on an LED, and no response i.e. an IN report, needs to be sent to the user app on the host PC. In the paragraphs below, we’ll describe where the OUT reports are received, how they are decoded, and how an IN report is constructed and transmitted.

Step #7: Create a single command in the Report Descriptor

A simple OUT report that requests the device mcu Flash and Sram sizes requires an IN report to be constructed and transmitted. We’ll set up the Report Descriptor OUT and IN reports in usbd_custom_hid_if.c followed by the handing of the reports .

__ALIGN_BEGIN static uint8_t CUSTOM_HID_ReportDesc_FS[USBD_CUSTOM_HID_REPORT_DESC_SIZE] __ALIGN_END =
{
/* USER CODE BEGIN 0 */
0x06, 0x00, 0xff, // USAGE_PAGE (Vendor Defined Page: 0xFF00)
0x09, 0x01, // USAGE Vendor Defined
0xa1, 0x01, // COLLECTION (Application)
/* Above, bytes 0 - 6 */
// 07
// (OUT Report) Request the Flash + SRAM size (20 bytes)
0x85, 0xE3, // REPORT_ID (227)
0x09, 0x02, // USAGE Board Control
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x01, // REPORT_COUNT (1)
0xB1, 0x20, // FEATURE (no preferred state)
0x85, 0xE3, // REPORT_ID (227)
0x09, 0x02, // same USAGE (Board Control) at top
0x91, 0x20, // OUTPUT (no preferred state)
// 27
// (IN Report) Return the cpu flash, sram size, in KB (15 bytes)
0x85, 0xE4, // REPORT_ID (228)
0x09, 0x02, // USAGE Board Control
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0x00, 0x08, // LOGICAL_MAXIMUM (2048)
0x75, 0x10, // REPORT_SIZE (16) bits per report item
0x95, 0x03, // REPORT_COUNT (3) dummy + FlashSize + SramSize
0x81, 0x02, // INPUT (Data,Var,Abs,Vol)
// 42 Add 1 (because byte counts are 0-based), hence
// Adjust value of USBD_CUSTOM_HID_REPORT_DESC_SIZE = 43 in "usbd_conf.h" as needed.
0xC0 /* END_COLLECTION */
};

Note how the byte counts (07, 27, 42) are shown, plus the size of the report definition. This is an ideal way to keep track of the count as reports are added. Report definitions don’t have to be in ID order, but it does make it easier to keep track of the ID numbers.

Step #8: Processing OUT report, generating IN report

The host PC sends and OUT report to the device, eventually the ReportID is decoded, and the requested action occurs: for example setting a digital output state. If the request is get a digital input state, the relevent pin has to be accessed, and an IN report prepared and sent.

This happens in a usbd_custom_hid_if.c function, shown below. Send_Buffer[40] is declared elsewhere.

extern uint8_t Send_Buffer[];
static int8_t CUSTOM_HID_OutEvent_FS(uint8_t* state)
{
uint16_t flashSize = 0, outReportSize = 0;
uint8_t* Receive_Buffer = state;
switch (Receive_Buffer[0])
{
case 0xE3: // get the mpu flash + sram size
Send_Buffer[0] = 0xE4; // IN Report ID
Send_Buffer[1] = 0x00; // dummy
Send_Buffer[2] = 0x01; //tells host apps the data is ready
flashSize = *(uint16_t*)0x1FFFF7E0; // Cortex-M3 chips only
Send_Buffer[3] = *(uint8_t*)0x1FFFF7E0; // Flash LOBYTE
Send_Buffer[4] = *(uint8_t*)0x1FFFF7E1; // Flash HIBYTE
switch (flashSize)
{
case 64:
case 128:
Send_Buffer[5] = 20;
break;
case 512:
Send_Buffer[5] = 64;
break;
case 768:
case 1024:
Send_Buffer[5] = 96;
break;
} // Set the end of data
Send_Buffer[6] = 0x00;
outReportSize = 7;
USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS,Send_Buffer, outReportSize);
}
return (USBD_OK);
}

In the above code fragment, note the following:

  • Receive_Buffer is a pointer that is copied from the state argument.
  • Receive_Buffer[0] is the OUT reportID
  • the target mcu Flash size is located at a specific memory address
  • SRam size does not seem to be stored, so several options are provided for (in our case) multiple chips.
  • Send_Buffer[1] and [2] are specific to the Decaf IOC SDK library code and how an IN report is received.

Step #9: Adjust the Real Time Clock

The Real Time Clock (RTC) is running about 6% faster with the new firmware when compared with the old firmware on identical hardware. In the MX clock setup, the nominal 72MHz clock is reduced to 40kHz to drive the RTC. The problem is that it’s NOT 40kHz exactly, and it has to be adjusted so the RTC is accurate.

In main.c  MX_RTC_Init(...) we have the default
hrtc.Init.AsynchPreDiv = RTC_AUTO_1_SECOND;
which has to be changed to
hrtc.Init.AsynchPreDiv = 41900;
We started with 40000, and adjusted it till the time calculated matched the host PC (assumed accurate), an IOC-M and the IOC-M-2. In other words, the 8MHz crystal from the 72MHz clock is generated has an approximate 6% frequency tolerance. Using the approximate 40kHz clock, a 1 second trigger each AsynchPreDiv count is passed to the RTC.

In testing a series of IOC-M-2 boards, we found there was variance between the clocks of each board. The RTC should be at least as accurate as a PC clock, so we added a Report that allows the AsynchPreDiv value to be written to the IOC-M-2, and stored in the high end of Flash memory. When the board boots up, the Flash AsynchPreDiv value is used in MX_RTC_Init. We’ve also created a PiXCL 20 app to calculate the corrected value and write it to the board.

During testing we spotted a curious feature of the MX code that initializes the RTC counter. Time/date counts traditionally start 01 Jan 1970. ST Microsystems, a French company, starts the RTC at 01 Jan 1792. This is of course significant as 1792 was the year France became a Republic.

Step #10: Build the firmware and test enumeration

The aim is to first check that the device is recognized by Windows 10, even if it actually does not do anything useful just yet. The next steps are to test an IOC-M with old firmware against the IOC-M-2 with new firmware and verify that the operation status is the same for all reports.

Final Step

Our intention is to make this project code available to the developer community, via GitHub.

Since you are here, do please have a look around the rest of our site.

Do you have a question on this? Contact us.