STM32 и USB. Mass storage device.

Уже не раз обсуждались на нашем сайте темы, касающиеся связи микроконтроллеров STM32 и ПК по интерфейсу USB, вот они:

Так вот, сегодня в очередной раз вернемся к этой теме, и задачей сегодняшней статьи станет реализовать Mass Storage Device (MSD) на микроконтроллере STM32 😉

Микроконтроллеры STM32 и USB

В этой статье я буду использовать STM32F10x и среду разработки Keil, но как и всегда с STM32 (ну или почти всегда) особых проблем с портированием на микроконтроллеры других семейств возникнуть не должно )

Как и обычно при работе с USB за базу возьмем готовые библиотеки от ST. Вообще у них на официальном сайте есть куча готовых проектов под разные отладочные платы с реализацией разных режимов работы USB (и MSD в том числе). Там, в частности, есть примеры Mass Storage Device для следующих случаев:

  • в качестве внешней памяти используется SD-карта
  • в качестве внешней памяти используется микросхема NAND-памяти

Предлагаю добавить в нашу задачу немного оригинальности 😉 Поэтому давайте для хранения информации задействуем внутреннюю память микроконтроллера. То есть часть flash-памяти будет определяться как внешний накопитель. Это конечно не очень практично, все-таки объем памяти микроконтроллера измеряется сотнями килобайт, поэтому использовать контроллер для хранения каких-нибудь данных не получится. С другой стороны, решение этой задачи будет полезно для понимания основных принципов, которых нужно придерживаться при реализации драйвера Mass Storage Device на STM32. Кроме того, можно использовать этот пример как базу при написании USB-bootloader’а для перепрограммирования готовых устройств.

В общем, давайте уже перейдем к практической части 😉

В первую очередь в библиотеках для работы с MSD нужно настроить USB Disconnect Pin, а точнее, задать тот порт ввода-вывода и тот номер пина, который задействован на используемой плате для программного отключения/подключения USB. На моей плате для этого используется вывод микроконтроллера PA10, поэтому в файле platform_config.h я определил:

#define USB_DISCONNECT                      GPIOA  
#define USB_DISCONNECT_PIN                  GPIO_Pin_10
#define RCC_APB2Periph_GPIO_DISCONNECT      RCC_APB2Periph_GPIOA

Непосредственная настройка этого вывода производится в функции USB_Disconnect_Config():

void USB_Disconnect_Config(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;
 
  /* Enable USB_DISCONNECT GPIO clock */
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIO_DISCONNECT, ENABLE);
 
  /* USB_DISCONNECT_PIN used as USB pull-up */
  GPIO_InitStructure.GPIO_Pin = USB_DISCONNECT_PIN;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
  GPIO_Init(USB_DISCONNECT, &GPIO_InitStructure);
}

Здесь нам ничего менять и настраивать дополнительно не требуется =).

Переходим к функциям USB. И тут нас интересует файл mass_mal.c. В этом файле нам нужно реализовать функции записи/чтения и инициализации, в соответствии с тем типом памяти, который мы будем использовать. То есть если бы мы хотели использовать внешнюю карту памяти, то нам нужно было использовать функции записи/чтения SD-карты. Поскольку мы будем работать с внутренней flash-памятью контроллера, то и использовать мы будем соответствующие функции.

Как вы помните из статьи, посвященной работе с flash (ссылка), для того, чтобы начать работать с памятью ее нужно разблокировать. Поэтому функция MAL_Init() будет выглядеть так:

uint16_t MAL_Init(uint8_t lun)
{
  switch (lun)
  {
    case 0:
      FLASH_Unlock(); 
      break;
    case 1:
      return MAL_FAIL;
    default:
      return MAL_FAIL;
  }
  return MAL_OK;
}

При инициализации мы разблокируем память и после этого можем вызывать функции чтения/записи из Standard Peripheral Library. Но для начала нам нужно определить сектора, которые будут использоваться в качестве внешнего накопителя (всю flash-память мы использовать не можем, поскольку часть памяти будет занята нашей программой). Итак, в файле mass_mal.h задаем:

#define FLASH_DISK_START_ADDRESS              0x0800A000     /* Flash start address */
#define FLASH_DISK_SIZE                       221184         
#define FLASH_PAGE_SIZE                       2048         /* 2K per page */

Flash-память у нас начинается с адреса 0x08000000. Для основной прошивки мы выделили 40 кБ (адреса с 0x08000000 — по 0x0800A000). Общий объем памяти моего контроллера — 256 кБ. Тогда объем внешнего диска получаем равным 221184 байт (256 кБ — 40 кБ). А размер страницы памяти для данного контроллера составляет 2кБ или 2048 байт. Все эти данные мы и задали в файле mass_mal.h.

В общем то, теперь нам осталось только написать функции чтения и записи — MAL_Write() и MAL_Read(). Тут нам надо учесть несколько факторов:

  • перед началом записи страницы, в которые будет произведена запись, необходимо предварительно очистить
  • запись и чтение производятся целыми секторами
  • перед вызовом функций работы с flash-памятью необходимо убедиться, что предыдущая операция завершена (тут нам нужна функция FLASH_WaitForLastOperation()).

Учтем все вышеперечисленное и в итоге получаем следующую реализацию функций:

/*******************************************************************************
* Function Name  : MAL_Write
* Description    : Write sectors
* Input          : None
* Output         : None
* Return         : None
*******************************************************************************/
uint16_t MAL_Write(uint8_t lun, uint32_t Memory_Offset, uint32_t *Writebuff, uint16_t Transfer_Length)
{
  uint16_t i;
  switch (lun)
  {
    case 0:	
      for(i = 0; i < Transfer_Length; i += FLASH_PAGE_SIZE)
      { 
        if (FLASH_WaitForLastOperation(WAIT_TIMEOUT) != FLASH_TIMEOUT)
        {
          FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
        } 	
        FLASH_ErasePage(FLASH_DISK_START_ADDRESS + Memory_Offset + i); 
      }			
 
      for(i = 0; i < Transfer_Length; i += 4)
      {  		
        if(FLASH_WaitForLastOperation(WAIT_TIMEOUT) != FLASH_TIMEOUT) 	
        {
          FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); 
        }
        FLASH_ProgramWord(FLASH_DISK_START_ADDRESS + Memory_Offset + i , Writebuff[i >> 2]); 
     }
 
     break;
   case 1:
     break;
   default:
     return MAL_FAIL;
  }
  return MAL_OK;
}
 
/*******************************************************************************
* Function Name  : MAL_Read
* Description    : Read sectors
* Input          : None
* Output         : None
* Return         : Buffer pointer
*******************************************************************************/
uint16_t MAL_Read(uint8_t lun, uint32_t Memory_Offset, uint32_t *Readbuff, uint16_t Transfer_Length)
{
  uint16_t i;
  switch (lun)
  {
    case 0:
      for(i = 0; i < Transfer_Length; i += 4) 
      {
        Readbuff[i >> 2] = ((vu32*)(FLASH_DISK_START_ADDRESS + Memory_Offset))[i >> 2]; 
      }
      break;
    case 1:
      break;
    default:
      return MAL_FAIL;
  }
  return MAL_OK;
}

У нас осталась еще одна важная функция, необходимая для работы с MSD, а именно функция MAL_GetStatus(). В этой функции мы должны передать в специальные переменные значения, соответствующие размеру памяти, размеру страницы, а также количеству блоков:

uint16_t MAL_GetStatus (uint8_t lun)
{
  if (lun == 0)
  {		 
      Mass_Block_Count[0] = FLASH_DISK_SIZE / FLASH_PAGE_SIZE; 
      Mass_Block_Size[0] =  FLASH_PAGE_SIZE; 
      Mass_Memory_Size[0] = FLASH_DISK_SIZE; 
      return MAL_OK;
  }
 
  return MAL_FAIL;
}

Собираем проект, прошиваем микроконтроллер и видим, что в диспетчере устройств у нас появилось новое запоминающее устройство. А кроме того, система обнаружила внешний накопитель, объем которого соответствует тому значению, которое мы определили в нашей программе. Таким образом, цель сегодняшней статьи можно считать достигнутой 😉

Как и всегда, прилагаю готовый проект для среды разработки Keil:

USB_MassStorage_Project — проект MSD для STM32.

Понравилась статья? Поделись с друзьями!

STM32 и USB. Mass storage device.: 38 комментариев
  1. Спасибо за статью! Скажите, можно ли наладить передачу данных с контроллера на пк через usb в режиме full speed usb2.0! Без эмуляции последовательного порта!

  2. Спасибо за статью! Переделал под Stm32F303VC
    Поругался компилятор на «FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR» из строки FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
    оставил FLASH_ClearFlag(FLASH_FLAG_EOP );
    что за флаги за что отвечают?
    P.S. После форматирования из 216 кб осталось 192 кб для чтения, записи. Т.е. при создании USB bootloader-a и записи туда bin файла программа явно будет стартовать не с адреса 0x0800A000

    • Ну да, FatFs же занимает место в памяти, поэтому выходит меньше. Если на базе этого делать бутлодер, то наверно лучше в проект бутлодера добавить драйвер FatFs и уже с его помощью открывать файл с карты и считывать прошивку. Хотя так тоже будут нюансы…

  3. Коля Соколов спасибо вам за проект
    Только-бы там ошибочку исправить — буфер для записи сектора во флеш память — 2 килобайта, у вас в проекте 64*8=512, странно что этот проект корректно работает

    • Ответил в группе ВК, продублирую тут, на всякий случай.
      Буфер — uint32_t, соответственно размер получается — 4 * 64 * 8 — 2048 байт.

  4. Скачал Ваши исходники и заметил, что в файле hw_config.c в ф-ции Set_System только:
    void Set_System(void)
    {
    SystemInit();

    /* Enable and Disconnect Line GPIO clock */
    USB_Disconnect_Config();

    Led_Config();
    /* MAL configuration */
    MAL_Config();
    }
    Например, в примере от ST там содержится следующее:
    EXTI_ClearITPendingBit(EXTI_Line18);
    EXTI_InitStructure.EXTI_Line = EXTI_Line18;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStructure);

    без, которого по идее USB устройство будет не корректно определяться.
    И еще в ф-ции MAL_Init только «анлок» флэш памяти…а где инициализации работы узб в качестве внешнего накопителя?

  5. А как FatFS поможет с местом под FAT разметку? В любом случае же нужно хранить где FAT разметку.
    Если только сделать «хитрости» в духе: сохраняем начало разметки, а остальную часть просто выдаем нулями.
    После прошивки у меня получилось порядка 20 страниц (проц 1кБ страница) заполненных нулями.

  6. Большое спасибо за статью!!!
    Очень интересна тема с USB и в особенности mass storage.

    А какая скорость получается у данного девайса, это USB 2.0?

  7. А каким образом прикрутить FAT FS, чтобы во флэш можно было писать файл с данными, а при подключении к компьютеру считывать его на ПК?

    • В FatFs в функциях f_write(), f_read() поместить вызовы функции для записи/чтения flash(). Но нужно учитывать, что перед записью какой-то информации во Flash предварительно ее надо очистить, а очистка происходит секторами…

    • Вряд ли… Честно говоря, это не очень хорошая идея писать во внутреннюю флэш файлы… Тогда уж лучше внешнюю NAND память поставить (или SD, или еще какую-нибудь).

    • Внешняя память — совсем другое дело ) Можно поискать в официальных примерах от ST, для SPI SD-карты там точно есть готовый драйвер, может и другие есть примеры.

  8. Здравствуйте.

    Перенес проект в Eclips под gcc, получил hard fault, методом тыка выяснил: если закоментировать строчку
    _SetCNTR(wInterrupt_Mask); в функции PowerOn то hard faulta нету. Куда копать?
    И есче если у меня страница = 1к то мне нужно BULK_MAX_PACKET_SIZE изменить на 32 ?

  9. Вопрос снят, разобрался. Теперь виндовс не удается завершить форматирование.

  10. Здравствуйте.
    Подскажите, а что за плату вы используете?
    STM32VLDISCOVERY?

  11. Здравствуйте. Пытаюсь этот проект адаптировать под CubeMX.
    Модифицировал только файл usbd_stotage_if.c

    Задал следующее
    #define FLASH_DISK_SIZE 14
    #define FLASH_DISK_START_ADDRESS 0x0800C800 // 50page

    int8_t STORAGE_Read_FS (uint8_t lun,
    uint8_t *buf,
    uint32_t blk_addr,
    uint16_t blk_len)
    {
    /* USER CODE BEGIN 6 */

    uint16_t i;
    switch (lun)
    {
    case 0:
    for(i = 0; i > 2] = ((uint16_t*)(FLASH_DISK_START_ADDRESS + blk_addr))[i >> 2];
    }
    break;
    case 1:
    break;
    default:
    return (USBD_FAIL);
    }

    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 */

    uint32_t PAGEError = 0;
    uint16_t i;

    EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
    EraseInitStruct.NbPages = 1;

    switch (lun)
    {
    case 0:
    for(i = 0; i < blk_len; i += FLASH_PAGE_SIZE)
    {
    EraseInitStruct.PageAddress = FLASH_DISK_START_ADDRESS + blk_addr + i;
    HAL_FLASHEx_Erase(&EraseInitStruct, &PAGEError);
    }

    for(i = 0; i > 2]);
    }
    break;
    case 1:
    break;
    default:
    return (USBD_FAIL);
    }

    return (USBD_OK);
    /* USER CODE END 7 */
    }

    int8_t STORAGE_GetCapacity_FS (uint8_t lun, uint32_t *block_num, uint16_t *block_size)
    {
    /* USER CODE BEGIN 3 */
    if (lun == 0)
    {
    *block_num = FLASH_DISK_SIZE;
    *block_size = FLASH_PAGE_SIZE;
    return (USBD_OK);
    }
    return (USBD_FAIL);
    /* USER CODE END 3 */
    }

    В итоге на ПК отображается носитель, и видит, что 14 Кб но не может отформатировать.
    Подскажите, будьте добры, что я делаю не так?
    Предполагаю, что банально буфер у меня не задан для USB нужного размера. В данном режиме USB банально не пойму где он задается. В общем подскажите, заранее спасибо!

  12. И опять у меня вопросы.
    1) А что тут означает сдвиг вправо на два?
    2) Цифра 4 в данном цикле
    for(i = 0; i < Transfer_Length; i += 4)
    это количество бит?
    3) Почему в новых функциях приема и передачи, сгенеренных кубом, аргумент *buf 8-битный? По сколько бит вообще идет и передача по USB?
    4) Буфер приема и передачи где-то регулируется? Если нет, то как он определяется?
    Заранее спасибо, извиняюсь за дилетанствкий

  13. Ладно, более менее разобрался, что тут зачем было.
    Не понятно осталось следующее:
    1) Как все таки задается буфер приема и передачи данных. Как и чем определяется его величина и где он задается?
    2) Форматирование у меня не происходит в частности по той причине, что аргумент blk_len функции STORAGE_Write_FS всегда равен нулю. Как так? Может надо чего подправить или 14 Кб слишком маленькая величина для файловой системы FAT? Не понятно
    Заранее спасибо автору за ответ!

    • А то, что я на почту написал, удалось поправить?

      По поводу буфера все надо искать в исходниках — разбираться как вызывается функция чтения/записи, из какого места и т. д.

  14. Ну, теоретически удалось поправить. Но проверить это нет возможности, так как приходит blk_len равное нулю и соответственно в цикл ни заходим и ничего не пишем. Можете что-нибудь по этому поводу сказать?
    Ну и по буферу, все таки важен он? Мне так и не удалось понять, где он определяется. Все очень накручено, можете подсказать

  15. Уважаемые, чего делать-то? Подскажите. Нашел я предположительно размер буфера. По идее его рулит параметр MSC_MAX_FS_PACKET. И обращений при его изменении стало больше в функцию записи при форматировании с ПК. Да только blk_len все равно равен 0. Что за бред? Может есть минимальный таки размер кластера, меньше которого нельзя? Уважаемый Aveal, скажите более конкретно, что я делаю не так

      • Уважаемый Aveal, вы можете сделать данный пример, только на CubeMX. Буду очень благодарен, так как судя по форумам не я один не понимаю как это все запустить, а для вас это по идее очень просто и не займет много времени. Все очевидно несколько сложнее, чем описано в данной статье ну или я полнейший идиот. Заранее спасибо!

        • Я постараюсь, но не обещаю и, к сожалению, точно не смогу в будни. Я думаю, копать тут надо в направлении размера ячейки памяти. Возможно такая проблема получается из-за того, что размер страницы памяти больше 512 байт, а драйвер USB все пытается подогнать под 512 байт — что-то в этом роде. Можно попробовать в Кубе увеличить размер пакета, если там вообще есть такая настройка.

  16. Здравствуйте. Докладываю сводки с фронтов. Несмотря на все расспросы, внятного объяснения, чего сделать, чтобы заработало, мне к сожалению никто так и не дал (ну или я слишком туп). Помогли товарищи китайцы (или кто они там).
    http://www.openedv.com/thread-76304-1-1.html
    Они, чтоб не заморачиваться, просто взяли в памяти (оперативной) массив создали и его форматировали. И о чудо, все работает (соответственно надо проц что то типа F4, чтоб все влезло). На этом примере просто и внятно можно понять, как все это функционирует. Выяснил следующее:
    1) Размер массива должен быть не менее 20Кб + место под ваши данные. 20Кб это я так понял место под таблицу файловой системы.
    2) Размер блока (STORAGE_BLK_SIZ) работает (у меня во всяком случае) только 512 бит. Не больше и не меньше. Мутил, крутил, менял размер буфера. Ничего не помогло. Если кто-то подскажет как сделать так, чтобы работало при другой цифорке, буду благодарен.
    3) Форматируется с компа только в режиме Быстрое. Обычное форматирование не прокатывает. Тоже бы было интересно узнать, почему так.
    Как-то так

  17. Здравствуйте! Вы пишите: В первую очередь в библиотеках для работы с MSD нужно настроить USB Disconnect Pin. Насколько это важно? Если я правильно понимаю, это для того, чтобы не перевтыкать USB вручную. Если нет, можно увидеть схему включения?

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *