USB with STM32

 

USB - basic description:

USB is probably the most common interface nowadays. Possibly due to the versatile protocol which can handle many different peripherals: flash drives, printers or keyboards... Also, it is self-configuring (no data format or speed needs to be selected), reliable (no data lost during transfer), and pretty fast. Since this protocol is so versatile software is not simple and it's not trivial to implement it on your own. Fortunately, this is also a reason why there are ready-to-go libraries for this protocol which takes care of all complicated and specific aspects of implementation. In this post, I want to show how to use the STM library for USB and how to modify it to use more than just one class - create a simple "composite" device.

How to get STM32 Library:

For USB there is always one host and one or more devices connected to it. STM can be programmed as both but for now, we will discuss only the device option. Because the USB standard is widely used for multipurpose, a few classes were defined which decide how communication is realised in detail. For our purpose, Communication Device Class (CDC), Device Firmware Upgrade (DFU) and Mass Storage Class (MSC) will be interesting (names are not universal and rather specific for STM32).  You can download the library directly from the STM site or use CubeMX to generate code with the necessary libraries. Since we need to add the HAL library and generate an initialization code, let's use CubeMX.

Create a new project for your MCU. Then we need to select USB_OTG_FS from the Connectivity tab and choose Device_Only for Mode.


Then in the Middleware and Software Packs tab, we select USB_DEVICE and choose one of the interesting classes.



Then you can choose your preferred settings and generate the code. In the created directory, besides the typical code, there are 2 new folders: Middlewares and USB_DEVICE. HAL library (necessary for USB libraries) can be found in the Drivers directory. If you want you can copy these 3 folders into your project (I've created a separate directory for all USB files). Remember to add all executables and header files e.g. in CmakeList.txt. Now, if you want to use only one class you are ready to go and you can start using USB (check below CDC, MSC or DFU examples). 

How do you combine more classes?

Naturally,  for bigger projects, there is a need for the usage of 2 or more classes in one program. When this post was created, there was no official library from STM32 for the composite devices (combining a few classes). However, projects that use a few classes exist so at least it is possible, right? - Yes, it is possible but requires a bit of change in libraries. Quick Note: I highly recommend finding a well-described project with a well-tested and reliable library for USB composite devices (not mine). However, if, like me, you are unable to search for a ready-to-use and reliable solution and have something of the masochist in you - let's dive in!

For convenience, the process will describe combining MSC and CDC libraries but it can be done with all others (at least with DFU).
  1. Use CubeMX and create a project with one of the classes (e.g. CDC), copy folders to your project,
  2. Use CubeMX and create a new project with the next desired class (MSC),
  3. The class folder (MSC) can be copied straight into \Middlewares\ST\STM32_USB_Device_Library\Class\ or wherever you want to keep it,
  4. Files that don't duplicate can be copied  USB_files\USB_DEVICE\App\usbd_storage_if.c and USB_files\USB_DEVICE\App\usbd_storage_if.h,
  5.  Add all of the new executables and include directories in CMakeList.txt (see code below),
  6. Some of the files need to be modified (see picture below).

// CMakeLists.txt
...
add_executable(${TARGET_ELF}
	USB_files/USB_DEVICE/App/usb_device.c 
    USB_files/USB_DEVICE/App/usbd_desc.c 
    USB_files/USB_DEVICE/App/usbd_cdc_if.c 
    USB_files/USB_DEVICE/App/usbd_storage_if.c
    USB_files/USB_DEVICE/Target/usbd_conf.c 
    USB_files/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pcd.c 
    USB_files/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pcd_ex.c 
    USB_files/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_usb.c 
    USB_files/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc.c 
    USB_files/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_gpio.c
    USB_files/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_cortex.c 
    USB_files/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c 
    USB_files/Middlewares/ST/STM32_USB_Device_Library/Core/Src/usbd_core.c 
    USB_files/Middlewares/ST/STM32_USB_Device_Library/Core/Src/usbd_ctlreq.c 
    USB_files/Middlewares/ST/STM32_USB_Device_Library/Core/Src/usbd_ioreq.c 
    USB_files/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c 
    USB_files/Middlewares/ST/STM32_USB_Device_Library/Class/MSC/Src/usbd_msc_bot.c
    USB_files/Middlewares/ST/STM32_USB_Device_Library/Class/MSC/Src/usbd_msc_data.c
    USB_files/Middlewares/ST/STM32_USB_Device_Library/Class/MSC/Src/usbd_msc_scsi.c
    USB_files/Middlewares/ST/STM32_USB_Device_Library/Class/MSC/Src/usbd_msc.c 
)
target_include_directories(${TARGET_ELF} PUBLIC
    USB_files/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc
    USB_files/Middlewares/ST/STM32_USB_Device_Library/Class/MSC/Inc
    USB_files/Middlewares/ST/STM32_USB_Device_Library/Core/Inc
    USB_files/STM32F4xx_HAL_Driver/Inc
    USB_files/STM32F4xx_HAL_Driver/Inc/Legacy
    USB_files/USB_DEVICE/App
    USB_files/USB_DEVICE/Target
)
List of added files (green) and modified (yellow)

Some functions are defined for both classes identically so I've moved them to the common file usbd_core.c.

usbd_core.h added code

usb_core.c added code

For others, there is a need to distinguish when we want to use a particular class. So I've created another file: usb.h with enumeration type. You can define it in any other file but I will be extending this file in the future so for me it was convenient. Additionally, I've created a struct for USB and global object main_usb (in usb.c) which contains the current class of the USB. Now you can add this header to some other files.
// usb.h
#ifndef USB_H_
#define USB_H_

typedef enum {
    USB_CLASS_CDC,
    USB_CLASS_DFU,
    USB_CLASS_MSC
}usb_class_e;

typedef enum
{
    USB_STATE_NOT_CONNECTED,
    USB_STATE_COMMUNICATION,
    USB_STATE_IDLE,
    USB_COUNT
} usb_state_e;

typedef struct
{
    usb_class_e class;
    bool connected;
    usb_state_e status;
    uint8_t data_to_send[50];
    uint8_t data_to_send_len;
    uint8_t data_received[50];
    uint8_t data_received_len;
}usb_t;

extern usb_t main_usb;

#endif /* USB_H_ */
A descriptor is a struct that gives the host information about USB (name, manufacturer, etc.). For each class, it has to be a little different so I've changed usbd_desc.c:
// usbd_desc.c
...
#define USBD_PID_FS_CDC     22336
#define USBD_PRODUCT_STRING_FS_CDC    "STM32 Virtual ComPort"
#define USBD_CONFIGURATION_STRING_FS_CDC     "CDC Config"
#define USBD_INTERFACE_STRING_FS_CDC     "CDC Interface"

#define USBD_PID_FS_MSC     22314
#define USBD_PRODUCT_STRING_FS_MSC     "STM32 Mass Storage"
#define USBD_CONFIGURATION_STRING_FS_MSC     "MSC Config"
#define USBD_INTERFACE_STRING_FS_MSC     "MSC Interface"
...
//  by default values are set for CDC
//  before each initialization values will be set for desired class (see preinit() function)
__ALIGN_BEGIN uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END = {
  ...
  LOBYTE(USBD_PID_FS_CDC),        /*idProduct*/
  HIBYTE(USBD_PID_FS_CDC),        /*idProduct*/
  ...
  uint8_t * USBD_FS_ProductStrDescriptor(USBD_SpeedTypeDef speed, uint16_t * length)
{
  if (main_usb.class == USB_CLASS_CDC) {
    if (speed == 0)
    {
      USBD_GetString((uint8_t*)USBD_PRODUCT_STRING_FS_CDC, USBD_StrDesc, length);
    }
    else
    {
      USBD_GetString((uint8_t*)USBD_PRODUCT_STRING_FS_CDC, USBD_StrDesc, length);
    }
  }
  else if (main_usb.class == USB_CLASS_MSC) {
    if (speed == 0)
    {
      USBD_GetString((uint8_t*)USBD_PRODUCT_STRING_FS_MSC, USBD_StrDesc, length);
    }
    else
    {
      USBD_GetString((uint8_t*)USBD_PRODUCT_STRING_FS_MSC, USBD_StrDesc, length);
    }
  }
  return USBD_StrDesc;
}
...
uint8_t * USBD_FS_ConfigStrDescriptor(USBD_SpeedTypeDef speed, uint16_t * length)
{
  if (main_usb.class == USB_CLASS_CDC) {
    if (speed == USBD_SPEED_HIGH)
    {
      USBD_GetString((uint8_t*)USBD_CONFIGURATION_STRING_FS_CDC, USBD_StrDesc, length);
    }
    else
    {
      USBD_GetString((uint8_t*)USBD_CONFIGURATION_STRING_FS_CDC, USBD_StrDesc, length);
    }
  }
  else if (main_usb.class == USB_CLASS_MSC) {
    if (speed == USBD_SPEED_HIGH)
    {
      USBD_GetString((uint8_t*)USBD_CONFIGURATION_STRING_FS_MSC, USBD_StrDesc, length);
    }
    else
    {
      USBD_GetString((uint8_t*)USBD_CONFIGURATION_STRING_FS_MSC, USBD_StrDesc, length);
    }
  }
  return USBD_StrDesc;
}
...
uint8_t * USBD_FS_InterfaceStrDescriptor(USBD_SpeedTypeDef speed, uint16_t * length)
{
  if (main_usb.class == USB_CLASS_CDC) {
    if (speed == 0)
    {
      USBD_GetString((uint8_t*)USBD_INTERFACE_STRING_FS_CDC, USBD_StrDesc, length);
    }
    else
    {
      USBD_GetString((uint8_t*)USBD_INTERFACE_STRING_FS_CDC, USBD_StrDesc, length);
    }
  }
  else if (main_usb.class == USB_CLASS_MSC) {
    if (speed == 0)
    {
      USBD_GetString((uint8_t*)USBD_INTERFACE_STRING_FS_MSC, USBD_StrDesc, length);
    }
    else
    {
      USBD_GetString((uint8_t*)USBD_INTERFACE_STRING_FS_MSC, USBD_StrDesc, length);
    }
  }
  return USBD_StrDesc;
}
...
//	added preinitialization for descriptor (change values in global table before initialization):
void USBD_Descriptor_preinit(usb_class_e class)
{
  switch (class)
  {
  case USB_CLASS_CDC:
    main_usb.class = USB_CLASS_CDC;
    USBD_FS_DeviceDesc[4] = 0x02;                             /*bDeviceClass CDC*/
    USBD_FS_DeviceDesc[5] = 0x02;                             /*bDeviceSubClass CDC*/
    USBD_FS_DeviceDesc[10] = LOBYTE(USBD_PID_FS_CDC);         /*idProduct*/
    USBD_FS_DeviceDesc[11] = HIBYTE(USBD_PID_FS_CDC);         /*idProduct*/
    break;
  case USB_CLASS_MSC:
    main_usb.class = USB_CLASS_MSC;
    USBD_FS_DeviceDesc[4] = 0x00;                             /*bDeviceClass MSC*/
    USBD_FS_DeviceDesc[5] = 0x00;                             /*bDeviceSubClass MSC*/
    USBD_FS_DeviceDesc[10] = LOBYTE(USBD_PID_FS_MSC);         /*idProduct*/
    USBD_FS_DeviceDesc[11] = HIBYTE(USBD_PID_FS_MSC);         /*idProduct*/
    break;
  default:
    break;
  }
}
and a header usbd_desc.h:
// usbd_desc.h
/* USER CODE BEGIN INCLUDE */
#include "usb.h"
/* USER CODE END INCLUDE */
...
/* USER CODE BEGIN EXPORTED_FUNCTIONS */
void USBD_Descriptor_preinit(usb_class_e class);
/* USER CODE END EXPORTED_FUNCTIONS */
In usbd_device.c I've changed the initialization function. It takes one argument (desire class of USB) and also changes the descriptor (function USBD_Descriptor_preinit(class)):
// usbd_device.c
/* USER CODE BEGIN INCLUDE */
#include "usbd_msc.h"
#include "usbd_storage_if.h"
/* USER CODE END INCLUDE */
...
/* USER CODE BEGIN 0 */
//	Error_Handler() definition and declaration moved to usbd_core.c
/* USER CODE END 0 */
...
void MX_USB_DEVICE_Init(usb_class_e class)
{
    /* USER CODE BEGIN USB_DEVICE_Init_PreTreatment */
    USBD_Descriptor_preinit(class);
    /* USER CODE END USB_DEVICE_Init_PreTreatment */
    /* Init Device Library, add supported class and start the library. */
    if (USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS) != USBD_OK)
    {
        Error_Handler();
    }
    switch (class)
    {
    case USB_CLASS_CDC:
        if (USBD_RegisterClass(&hUsbDeviceFS, &USBD_CDC) != USBD_OK)
        {
            Error_Handler();
        }
        if (USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS) != USBD_OK)
        {
            Error_Handler();
        }
        break;
    case USB_CLASS_MSC:
        if (USBD_RegisterClass(&hUsbDeviceFS, &USBD_MSC) != USBD_OK)
        {
            Error_Handler();
        }
        if (USBD_MSC_RegisterStorage(&hUsbDeviceFS, &USBD_Storage_Interface_fops_FS) != USBD_OK)
        {
            Error_Handler();
        }
        //  to make writing to FLASH possible it is needed to change priorities of interrupts:
        NVIC_SetPriority(OTG_FS_IRQn, 10);
        NVIC_SetPriority(DMA1_Stream5_IRQn, 9);
        break;
    default:
        break;
    }
    if (USBD_Start(&hUsbDeviceFS) != USBD_OK)
    {
        Error_Handler();
    }
    /* USER CODE BEGIN USB_DEVICE_Init_PostTreatment */
    /* USER CODE END USB_DEVICE_Init_PostTreatment */
}
and a header usbd_device.h:
// usbd_device.h
/* USER CODE BEGIN INCLUDE */
#include "usb.h"
/* USER CODE END INCLUDE */
...
void MX_USB_DEVICE_Init(usb_class_e class);
I've combined functions for static allocation in usbd_conf.c (not used anywhere to my knowledge):
//	usbd_conf.c
/* USER CODE BEGIN Includes */
#include "usbd_msc.h"
#include "usb.h"
/* USER CODE END Includes */
...
void* USBD_static_malloc(uint32_t size)
{
  if (main_usb.class == USB_CLASS_CDC) {
    static uint32_t mem[(sizeof(USBD_CDC_HandleTypeDef) / 4) + 1];/* On 32-bit boundary */
    return mem;
  }
  else if (main_usb.class == USB_CLASS_MSC) {
    static uint32_t mem[(sizeof(USBD_MSC_BOT_HandleTypeDef) / 4) + 1];/* On 32-bit boundary */
    return mem;
  }
  else {
    return NULL;
  }
And that's all modifications needed to marry these 2 classes! I want to mention that this is not the best way to create a composite device and to be honest it is not really a composite device. When you want to use USB as CDC you initiate it with the argument USB_CLASS_CDC and if you want MSC you will call initialization fun again with USB_CLASS_MSC (check code below). So this implementation just toggles between classes. The host doesn't recognize it as a composite device. The real composite device is probably possible to achieve with the STM32 library (it has functions and structures for it and you can turn it on with #define USE_USBD_COMPOSITE). However, for me, it's too complicated for now. I want USB to work as CDC most of the time and only when I want to read black box data I will change it into MSC, once upon a time turn on DFU and update the firmware. So it should be good enough. 
//	main.c
int main(void){
	setup();
    HAL_Init();
    ...
    MX_USB_DEVICE_Init(USB_CLASS_CDC)
	//	use USB for communication
    USB_DevDisconnect(USB_OTG_FS);	//	end USB conection
    USB_ResetPort(USB_OTG_FS);		//	reset 
	MX_USB_DEVICE_Init(USB_CLASS_MSC);	// initialize MSC device
	// do something with USB as MSC
    USB_DevDisconnect(USB_OTG_FS);	//	end USB connection
    USB_ResetPort(USB_OTG_FS);		//	reset 
	MX_USB_DEVICE_Init(USB_CLASS_CDC);	// initialize CDC device
	//	go back to communcation 
    while(1){
    ...
    }
}


CDC class - forget about UART!

Usually, when we communicate Nucleo or Arduino with a PC we use UART. However, although we send a real UART signal from a microcontroller then it's converted to a USB signal by a built-in converter. Why? - because most of the modern PCs don't have an RS-232 interface. Instead, USB with Communication Device Class creates a virtual COM port which behaves as RS-232. Microcontroller still sends a UART signal which is much simpler than USB protocol and PC applications can still read this signal as serial data (choose baud rate and other settings). However, for many STM32 we can use USB protocol directly and benefit from it (faster and more reliable connection). Let's do this!

Initialization was shown above. Now after connecting your USB to the computer device should be recognized and a virtual COM port should be created. If not, you have to download drivers from the ST site.


Now we need to handle data transmission - nothing simpler! Add header file usbd_cdc_if.h and use function uint8_t CDC_Transmit_FS(uint8_t *Buf, uint16_t Len)
//	main.c
#include "usbd_cdc_if.h"
int main(void){
	setup();
    HAL_Init();
    ...
    MX_USB_DEVICE_Init(USB_CLASS_CDC);
	...
	uint8_t data_to_send[20] = "Hello world!";
    uint16_t data_to_send_len = strlen((char*)data_to_send);
    while(1){
    	CDC_Transmit_FS(data_to_send, data_to_send_len);
    }
}
Open your favourite Serial monitoring app (it can be an Arduino serial monitor or any other) set your COM port and don't care about anything else (bound rate, data size...) click Open.

Data received through USB on PC

Reception is a little more complicated. We need a buffer for received data and some kind of flag that data was received. Let's see the main_usb object and function for the reception. 
//	usb.h
typedef struct
{
    usb_class_e class;
    bool connected;
    usb_state_e status;
    uint8_t data_to_send[50];
    uint8_t data_to_send_len;
    uint8_t data_received[50];
    uint8_t data_received_len;
}usb_t;
extern usb_t main_usb;
//	usbb_cdc_if.c
  static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t* Len)
{
  /* USER CODE BEGIN 6 */
  USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
  USBD_CDC_ReceivePacket(&hUsbDeviceFS);
  //  my own code for handling received data:
  strlcpy((char*)main_usb.data_received, (const char*)Buf, (*Len) + 1);
  main_usb.data_received_len = *Len;
  //  end of my code
  return (USBD_OK);
  /* USER CODE END 6 */
}
Data will be transferred into the main_usb.data_received array and the length of the received data will be treated as a flag. Now we can add a function for sending back when we receive something.
//	main.c
int main(void){
	setup();
	HAL_Init();
	...
	MX_USB_DEVICE_Init(USB_CLASS_CDC);
	...
	while(1){
		if (main_usb.data_received_len > 0) {
			strcpy((char*)main_usb.data_to_send, "\n\rreceived:\n\r\"");
			strcat((char*)main_usb.data_to_send, (char*)main_usb.data_received);
			CDC_Transmit_FS(main_usb.data_to_send, strlen((char*)main_usb.data_to_send));
			main_usb.data_received_len = 0;
		}
	}
}
And this is what we see in the serial monitor:



MSC - copy&paste data like a pro

Do you want to see your microcontroller like a flash drive and be able to create new folders or files, and copy and paste them from the PC level? Mass Storage Class is specially designed for data transfer between memories. It ensures that any data migrations are totally uncorrupted. 

After adding all necessary files (as it was described above) we need to provide functions for writing and reading. The host (PC) wants to be able to write and then read a selected number of bytes from the given address. From its perspective, memory has to be a list of addresses and when it writes 2 bytes (0xFA, 0xCD) to address 0x002 it can read from 0x003 address second of them (0xCD). Where we will write these data, in reality, doesn't matter - whether it will be internal RAM, FLASH, or SD card or even another microcontroller connected with ours through Bluetooth. The host gives an address, a pointer to buffer with data and a number of data to write. It's up to us how to handle this data. 

The easiest solution is to use internal RAM. First, we need to define an array for our storage. In usbd_storage_if.c we can define the size of the block and the number of blocks in our memory. What is a block and how to define it? In reality, memory usually isn't a long list of addresses, more often it is divided into pages/sectors/blocks containing some amount of bytes (256, 512, 4096, ...). In the case of NOR or NAND memory writing operations need to be performed for a whole page/sector/block. Therefore it would be so impractical to write smaller chunks of data than this page/sector/block. So we can define a block size which will be the smallest data pack that USB will access. If the size of the block is let's say 512 bytes and we want to use 100KB of memory we define 200 blocks. When we know the sizes we can define our array.
//	usbd_storage_if.c
#define STORAGE_BLK_NBR                  0xC8  	//  0xC8 -> 200 blocks
#define STORAGE_BLK_SIZ                  0x200  //  0x200 -> 512-byte sector 

/* USER CODE BEGIN PRIVATE_DEFINES */
uint8_t buffer[STORAGE_BLK_NBR * STORAGE_BLK_SIZ]; // allocate array for 100 KB of data

Next, it's time to define writing and reading functions. Since we are using RAM we can use memcpy(). The host will call these functions with a pointer to data, the address of the block and the number of blocks to read or write. Since we defined block size it is important to multiply the address by its value (the host will give blk_addr  = 3 but the third block has an address 3*STORAGE_BLK_SIZ in our memory and that is an address we want to access). The same situation is with a number of data to copy - the host gives a number of blocks to copy but each block contains STORAGE_BLK_SIZ bytes.
// 	usbd_storage_if.c
...
int8_t STORAGE_Read_FS(uint8_t lun, uint8_t* buf, uint32_t blk_addr, uint16_t blk_len)
{
  /* USER CODE BEGIN 6 */
  UNUSED(lun);
  memcpy(buf, &buffer[blk_addr * STORAGE_BLK_SIZ], blk_len * STORAGE_BLK_SIZ);
  return (USBD_OK);
  /* USER CODE END 6 */
}

int8_t STORAGE_Write_FS(uint8_t lun, uint8_t* buf, uint32_t blk_addr, uint16_t blk_len)
{
  /* USER CODE BEGIN 7 */
  UNUSED(lun);
  memcpy(&buffer[blk_addr * STORAGE_BLK_SIZ], buf, blk_len * STORAGE_BLK_SIZ);
  return (USBD_OK);
  /* USER CODE END 7 */
}
Now we are ready to test it! Initiate USB as MSC and connect to the PC. The message should pop out that the driver has to be formatted - let's do this. We define the file system and allocation size (you can choose what you want or leave default). 



After successful formatting, you should see your drive with free space available. Now you can create a file, save it, disconnect your USB and connect it again. 


Your file is still there! You can copy it to your desktop or delete it - so convenient!

External flash?

If you want to use an external flash memory just modify the block size and their number. Next, you can enter your functions for reading and writing. Remember that at default USB interrupt is set as the most important (priority = 0). If your functions use interrupts you have to change priorities (for example during an initialization). Additionally, USB disables other interrupts so we need to enable it before writing or reading (depends when you need interrupts).
// 	usbd_device.c
...
void MX_USB_DEVICE_Init(usb_class_e class)
{ 
  ...
  case USB_CLASS_MSC:
    if (USBD_RegisterClass(&hUsbDeviceFS, &USBD_MSC) != USBD_OK)
    {
      Error_Handler();
    }
    if (USBD_MSC_RegisterStorage(&hUsbDeviceFS, &USBD_Storage_Interface_fops_FS) != USBD_OK)
    {
      Error_Handler();
    }
    //  to make writing to FLASH possible it is needed to change priorities of interrupts:
    NVIC_SetPriority(OTG_FS_IRQn, 10);
    NVIC_SetPriority(DMA1_Stream5_IRQn, 9);

    break;
    ...
}
// 	usbd_storage_if.c
...
#define STORAGE_BLK_NBR                  0x1000  //  0x1000->4096 sectors (for W25Q128JV)
#define STORAGE_BLK_SIZ                  0x1000   //  0x1000->4096-byte (4KB sector) 

#define USE_EXTERNAL_FLASH         //  if you want use external flash module: USE_EXTERNAL_FLASH  and if internal RAM memory: USE_INTERNAL_RAM
...
int8_t STORAGE_Read_FS(uint8_t lun, uint8_t* buf, uint32_t blk_addr, uint16_t blk_len)
{
  /* USER CODE BEGIN 6 */
  UNUSED(lun);
#if defined(USE_INTERNAL_RAM)
  memcpy(buf, &buffer[blk_addr * STORAGE_BLK_SIZ], blk_len * STORAGE_BLK_SIZ);
#elif defined(USE_EXTERNAL_FLASH)
  W25Q128_read_data(blk_addr * STORAGE_BLK_SIZ, buf, STORAGE_BLK_SIZ * blk_len);
#endif
  return (USBD_OK);
  /* USER CODE END 6 */
}

int8_t STORAGE_Write_FS(uint8_t lun, uint8_t* buf, uint32_t blk_addr, uint16_t blk_len)
{
  /* USER CODE BEGIN 7 */
  UNUSED(lun);
#if defined(USE_INTERNAL_RAM)
  memcpy(&buffer[blk_addr * STORAGE_BLK_SIZ], buf, blk_len * STORAGE_BLK_SIZ);
#elif defined(USE_EXTERNAL_FLASH)
  NVIC_EnableIRQ(DMA1_Stream5_IRQn);  //  important because USB disable all interupts (and we want to use DMA int.)
  W25Q128_modify_data(blk_addr * STORAGE_BLK_SIZ, buf, STORAGE_BLK_SIZ * blk_len);
#endif
  return (USBD_OK);
  /* USER CODE END 7 */
}
How to write functions, used to write and read to Flash, will be described another time but you can use any library for that. 

DFU - update the firmware with USB!

So far, for updating firmware, we have been using ST-LINK but what if we want to update soft in the released version of our hardware? We can leave the ST-LINK connector and do it as always but for many cases, this is not an option. Fortunately, Bootloader exists and we can use one of the serial interfaces to update software. What is a bootloader? it is software in ROM memory (you can not remove it) which allows us to write new data into FLASH of our microcontroller without any external programmer (upper part of Nucleo board or USB dongle or original ST programmer). STM allows it to be programmed with UART, USB or CAN. For now, we will see how to use USB. 
Firstly you need to add the necessary files (use CubeMX and choose DFU for USB). If you followed the above process of combining CDC and MSC class you can do the same with DFU class. When you initiate USB in DFU mode it will be recognised by PC.


But this is only the USB configuration. The next thing is to turn on the bootloader - the BOOT0 pin needs to be pulled up and BOOT1 pulled down. You can find all the information in the reference manual (BOOT0 is dedicated but BOOT1 is shared with one of the GPIO pins). For off-shelf FC all connections are handled by pressing a boot button and powering up FC (in my case BOOT1 is hardwired to GND and the Boot button pulls BOOT0 high).


Then we can open STM32Cubeprogrammer (you can download it from the ST site) and connect with a board (the device should be detected and some information about it will pop out in the right bottom corner). Now you are ready to flash your new software into a board. 

After connecting with the bootloader

After building your project you will get the binary file with the whole project. STM32CubeProgrammer accepts files .bin, .hex, .elf. After enabling the bootloader and reaching the connection you give the path to a binary file, select preferred options (you can leave as default) and start programming.



And that's it! You know how to use the STM USB library. How to communicate via USB, use your microcontroller as a storage device and even update the firmware without an external programmer. Moreover, you can do all these things in one program! All of these things were implemented and used in my drone so you can check it out in real application on my github.

Comments

Popular posts from this blog

Hardware - how to start?

Filters - Theory