Flash memory as mass storage device using STM32 USB Device Library

Ehsan Norouzi picture Ehsan Norouzi · Jun 12, 2017 · Viewed 12.6k times · Source

There is this flash memory IC on my board, which is connected to my STM32F04 ARM processor. USB port of the processor is available for the user. I want my flash memory to be detected as storage device when connected to PC via USB.

As the first step I defined my USB class as MSC in my program which works fine. Since when I connect my board to PC, it detects a mass storage device connected, giving an error that "You should format the disc before using it".

Now the question is that, how I can define my flash as 'the storage' to my processor. The following would probably be a part of your answer: -usbd_msc_storage_template.c -FAT file system

I am using STM32F446 processor. FREERTOS and FATFS. Windows 10 on my PC.

Thanks in advance :)

Answer

Jacek Ślimok picture Jacek Ślimok · Jun 12, 2017

First of all - if you only need the flash memory to be visible on your PC as mass storage device then you don't need FatFS, as it is used to access storage in a file-by-file manner from the MCU. When PC accesses the storage devices it manages the filesystem(s) on it by itself and you may choose which kind of filesystem is going to be used when formatting the drive. Down at the low level when communicating with the storage itself all it does is telling the storage to "read/write X bytes from Y address". All the device needs to do is to write or read given data and return the result of the operation.

USB Mass Storage device class

This USB class exposes your device to the host as a storage device, allowing it to read or write given number of bytes from/to specified address. In case of STM32F4 you've mentioned, the functions you need to implement are the following (based on STM32Cube library):

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;

As you've mentioned, there is a USBD_MSC_Template_fops.c / .h files that provide a sample empty template for you to implement, the most important functions being Read and Write where the real "work" is done. To initialize your deivce to be shown as a USB Mass Storage device when connected to a PC host all that's left is: initializing the USB itself (USBD_Init), registering MSC device class (USBD_RegisterClass), registering said structure in the driver (USBD_MSC_RegisterStorage) and starting the USB device process for the driver (USBD_Start) when a connection to the host is detected. There are numerous examples that do just that - see reference implementations for Discovery or Eval boards. You seem to have done that correctly, as the host propery detected your device as USB MSC device and reported it as not formatted.

The reason your system says that the drive is not formatted is because the empty implementation in the usbd_msc_storage_template.c file returns successful execution (return code 0) for STORAGE_Read function, but does not actually perform any read - no data is sent back. While this may vary from host to host depending on the operating system, the most likely scenarios are that you'll either see a message about storage not being formatted or data being corrupted.

Interfacing USB Mass Storage device callbacks with physical memory

As mentioned above, calling USBD_MSC_RegisterStorage will register your struct in the USB MSC device class driver. At this point the driver itself will call your provided functions at appropriate moments - whenever requested by the host. If the target memory was an SD card the natural step would be to first implement functions accessing your SD card. Once those functions are tested and proven to work all that's left would be to put them inside the USB MSC device Read and Write functions and - assuming correct interrupt priorities - it should generally work "out of the box". The system should be able to format the card and later read and write files to it, all through your MCU.

It works the same way for any type of memory you choose. The only requirement is implementing the USBD_StorageTypeDef callback functions exactly as they are. This means that the host may choose to write arbitrary number of random bytes at any address within the reported address space and you either fully obey (write all data as it is) and return "successful execution" or return an error, which most likely will mean that your drive will get unmounted and the user will be prompted with an error message. In case of reads this means that if the host requests X number of bytes from Y address, the device needs to return exactly that amount of data. This means that if your memory type is not perfectly suitable for this kind of access, there will be more work that will have to be done in the layer accessing the physical memory in order to obey the USB MSC interface. All that naturally leads us to the last point below.

Flash memory as filesystem storage

For the flash memories where you access the raw data directly there are certain drawbacks that make them not perfectly suitable for filesystem applications. Those come from the way these memories are constructed. Although achievable, there will be additional steps that will have to be done in order to hide those imperfections:

  1. Writing "1"s individually - Flash memory when accessed directly only allows you to write "0" bits under given address. Once certain bit has been flipped to "0", it can no longer be invidually flipped back to "1". In order to do so, the entire block of data needs to be erased first. Depending on the flash memory part, this will typically be areas of 512, 4096 etc. bytes. This means that if you wanted to change given byte from 1 (binary 0000 0001) to 4 (binary 0000 0100), you'd have to do a read-erase-write of the whole sector. For you this means that if even one of the bits that the hosts requests to write needs to be flipped from "0" to "1", you need to first erase that area.

  2. Random access - Depending on the type of memory (NOR/NAND) you may or may not be able to access data randomly. In particular, for NOR flashes you may read or write data individually, while for NAND memories due to how the cells are interconnected only page access is allowed. This means that you may have to read or write more data than necessary.

  3. Write endurance - flash memories have a certain number of write cycles for each cell. This means that if you constantly write data to the same address you may very quicky exceed this limit. This is particularily important for filesystems like FAT where the FAT area will be constantly written to. This is solved by implementing some form of wear leveling, where physical sector writes are distributed evenly. You may of course choose to make it read-only by returning true from IsWriteProtected, if that's possible for your applicaiton.

Now as for how current SD cards achieve all this - all SD cards nowadays that I'm aware of contain a simple microcontroller (some kind of 8081, ARM7 or similar) that implements everything above plus the SD protocol. When talking to the card, you don't really talk to the raw memory but instead you communicate with the MCU sitting between you and your data. Its role is to present you with an illusion of perfect continuous data.