Уже не раз обсуждались на нашем сайте темы, касающиеся связи микроконтроллеров STM32 и ПК по интерфейсу USB, вот они:
Так вот, сегодня в очередной раз вернемся к этой теме, и задачей сегодняшней статьи станет реализовать Mass Storage Device (MSD) на микроконтроллере STM32. В этой статье я буду использовать STM32F10x и среду разработки Keil, но как и всегда с STM32 (ну или почти всегда) особых проблем с портированием на микроконтроллеры других семейств возникнуть не должно.
При работе с USB за базу возьмем готовые библиотеки от ST. Вообще у них на официальном сайте есть куча готовых проектов под разные отладочные платы с реализацией разных режимов работы USB (и MSD в том числе). Там, в частности, есть примеры Mass Storage Device для следующих случаев:
- в качестве внешней памяти используется SD-карта
- в качестве внешней памяти используется микросхема NAND-памяти
Предлагаю добавить в нашу задачу немного оригинальности ) Поэтому давайте для хранения информации задействуем внутреннюю память микроконтроллера. То есть часть flash-памяти будет определяться как внешний накопитель. Это конечно не очень практично, все-таки объем памяти микроконтроллера измеряется сотнями килобайт, поэтому использовать контроллер для хранения каких-нибудь данных не получится. С другой стороны, решение этой задачи будет полезно для понимания основных принципов, которых нужно придерживаться при реализации драйвера Mass Storage Device на STM32. Кроме того, можно использовать этот пример как базу при написании USB-bootloader'а для перепрограммирования готовых устройств.
В общем, давайте переходить к практической части!
Время традиционной вставки: поскольку компания STMicroelectronics прекратила поддержку библиотеки SPL, которая использовалась в этом курсе, я создал новый, посвященный работе уже с новыми инструментами, так что буду рад видеть вас там - STM32CubeMx. Кроме того, вот глобальная рубрика по STM32, а также несколько статей на смежную тему из нового курса:
В первую очередь, в библиотеках для работы с 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; }
При инициализации мы разблокируем память и после этого можем вызывать функции чтения/записи из SPL. Но для начала нам нужно определить сектора, которые будут использоваться в качестве внешнего накопителя (всю 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.