LPCXpresso Урок 10. USB-MSC. Разбираем пример.

Продолжим знакомство с LPC13xx в среде разработки CodeRed. На сей раз в рамках курса для новичков изучим пример использования встроенного класса USB mass storage (или попросту USB-флешки).

В контроллер LPC13xx встроен драйвер для USB-MSC класса. Этот драйвер используется USB загрузчиком, но и вы его можете использовать в своих целях.

Изложенный материал базируется на аппноуте AN10905 (код), который вы можете найти в разделе Application Notes сайта NXP.

Схема

Для работы нам потребуется собрать схему, приведённую в предидущем уроке. В зависимости от ваших навыков работы с паяльником и понимания схемотехники можете собрать как упрощенную версию, так и полную. Отличие в обоих случаях одно и важное: FT/GPIO должен быть отключен от земли (переключатель в положение «выполнение»).

При использовании упрощенной схемы у вас возникнут некоторые проблемы с работой устройства при перезапуске отладчика.

Готовим пример

Извлекаем из архива AN10905_lpc1300_usbmemrom.zip архив lpcxpresso_usbmemrom.zip Импортируем из этого архива пример usbmemrom. Для работы нам так же потребуется CMSIS. Все остальные проекты могут быть спокойно закрыты.

Тех, кому сильно не терпится ждет облом. Связан он с тем, что памяти под образ диска в примере выделено 13*512байт = 6.5кБ, а данных самого образа числится размер 12кБайт. Это сносит винде крышу и она предлагает диск отформатировать. Но вся проблема в том, что и форматирование не помогает.

Перед запуском немного дорабатываем код:
В файле diskimg.h (строка 17) изменяем константу количества блоков нашем образе диска на 32:

#define MSC_BlockCount  32
В файле diskimg.h (строка 21) в объявление образа диска DiskImage определяем константным т.к. 16 кБайт уже не поместится в оперативной памяти
extern const unsigned char DiskImage[MSC_MemorySize];   /* Disk Image */
В файле diskimg.c (строка 6) образ диска так же делаем константным
const unsigned char DiskImage[MSC_MemorySize] = {
В файле msccallback.c (строка 52) в функции MSC_MemoryWrite комментируем строку записи, т.к. обназ константный.
//DiskImage[offset+n] = src[n];

Доработанный код есть в прикреплённом архиве.

Запускаем проект

Компилируем пример, запускаем на исполнении. Пошаговую отладку при первом запуске тут лучше не использовать. Надо дать системе возможность корректно настроить драйвер для устройства.

После подключения добавленного USB разъема к компьютеру у вас появится съемный диск (при первом подключении так же будет установлен соответствующий драйвер). На диске будет расположен текстовый файл README.TXT, гласящий:

This is a USB Memory Device demonstration for
the NXP NXP13XX Board with NXP LPC1343.

На этот диск можете попробовать что-нибудь записать - только бестолку. Функцию записи мы отключили.

Возможные проблемы

При подключении устройства у вас появляется USB-HID устройство или виртуальный COM-порт. Это связано с тем, что у вас на компьютере уже запускался другой пример. Вам надо выбрать пункт "Устранение неполадок" либо удалить драйвер вручную и повторно подключить устройство.

ОС Windows предлагает отформатировать диск - значит вы запустили проект без описанных выше изменений.

Разбор кода

Начнем сразу с основного файла usbmemrom_main.c, и по мере необходимости будем рассматривать остальные.

Функция mian начинается с инициализации структуры MscDevInfo, имеющей тип MSC_DEVICE_INFO. Данный тип определён в файле rom_drivers.h и нужен для конфигурирования (настройки) встроенного в контроллер драйвера Mass Storage Class (USB MSC). И так, собственно инициализация:

MscDevInfo.idVendor = USB_VENDOR_ID;	// VID, PID и Device ID стройства (что это и зачем описано в вики)
MscDevInfo.idProduct = USB_PROD_ID;	//  используемые константы определены в файле config.h
MscDevInfo.bcdDevice = USB_DEVICE;	//
MscDevInfo.StrDescPtr = (uint32_t)&USB_StringDescriptor[0];	// Строка описания устройства, определена в usbdesc.c, нужна для отображения на компе
MscDevInfo.MSCInquiryStr = (uint32_t)&InquiryStr[0];	// определена в msccallback.c (тоже где-то отображалась, забыл где)
MscDevInfo.BlockSize = MSC_BlockSize;	// Размер блока в нашем USB-диске (определен в diskimg.h)
MscDevInfo.BlockCount = MSC_BlockCount;	// Количество блоков в нашем USB-диске (определён в diskimg.h)
MscDevInfo.MemorySize = MSC_MemorySize;	// Общий размер памяти в нашем USB-диске (определен в diskimg.h)
MscDevInfo.MSC_Read = MSC_MemoryRead;	// Функция, корорая будет вызвана при чтении данных компьютером с нашего USB-диска
MscDevInfo.MSC_Write = MSC_MemoryWrite;	// Функция, корорая будет вызвана при записи данных компьютером на наш USB-диск

функции рассмотрим чуть попозже, а пока к структуре DeviceInfo, описанной как MSC_DEVICE_INFO. Данный тип так же определен в файле rom_drivers.h и тоже предназначен для определения типа требуемого устройства.

DeviceInfo.DevType = USB_DEVICE_CLASS_STORAGE;	// Сообщаем встроенному драйверу USB что нам требуется функционал для USB-диска
DeviceInfo.DevDetailPtr = (uint32_t)&MscDevInfo;	// Передаем встроенному драйверу USB указатель на настройки

Далее идет включение тактирования требуемой нам для работы периферии, а это первый 32битный таймер (используется для служебных целей драйвером USB, мы должны таймер разрешить, но сами при этом не можем его использовать), порты ввода-вывода, подсистема USB:

LPC_SYSCON->SYSAHBCLKCTRL |= (EN_TIMER32_1 | EN_IOCON | EN_USBREG);

После чего вызываются функция настройки встроенного драйвера:

(*rom)->pUSBD->init_clk_pins();		// настройка периферии (PLL и выводов)
for (n = 0; n < 75; n++) { }		// Необходимая небольшая задержка
(*rom)->pUSBD->init(&DeviceInfo);	// Инициализация встроенного драйвера в соответствии с заполненной конфигурацией
init_msdstate();			// Инициализация "машины состояний" для корректной работы USB-MSC

Всё это дело определено так же в файле rom_drivers.h и предназначено для работы со встроенным драйвером USB. Указатель rom инициализируется константой (ROM **)0x1fff1ff8. Это ни что иное, как адрес в ROM памяти, по которому расположена структура, содержащая адреса встроенных в ROM функций. Адрес определён в документации к контроллеру. init_msdstate() определена как макрос, обнуляющий ячейку памяти, которую встроенный драйвер будет использовать в личных целях.

По завершении инициализации подключаемся к USB шине

(*rom)->pUSBD->connect(TRUE);

после этого нам остаётся только ждать. Для этих целей используется команда WFI (Wait For Interrupt) которая усыпляет ядро до возникновения прерываний. Вся периферия же продолжает работать дальше.

while (1)		// бесконечный цикл
	__WFI();	// в ожидании событий

Но для того, что бы драйвер USB обработал возникающие от USB прерывания, нам надо ещё добавить обработчик прерываний, в котором будем просто вызывать зашитую в ROM функцию обработки прерываний:


void USB_IRQHandler(void) {
(*rom)->pUSBD->isr();
}

Всё. С USB разобрались. Осталась мелочь. А именно образ нашего диска и функции для работы с ним. Параметры (геометрия диска) и описание нашего образа приведены в файле diskimg.h. Так у нас получается 32 блока (не забываем про исправления) по 512 байт и того 32 * 512 = 16 кБайт диск:

#define MSC_BlockCount  32
#define MSC_BlockSize   512
#define MSC_MemorySize  (MSC_BlockCount*MSC_BlockSize)
extern unsigned char DiskImage[MSC_MemorySize];   // описываем образ диска

Образ находится в файле diskimg.c (так же не забываем про исправления). Данный образ для нас любезно подготовили создатели примера, приводить содержимое здесь не буду.

Ну и в конце то концов, USB драйвер надо научить работать с нашим образом. Для этих целей используются уже упомянутые функции MSC_MemoryRead и MSC_MemoryWrite, описанные в msccallback.h и реализованные в msccallback.c. Так чтение образа выглядит следующим образом:

void MSC_MemoryRead (uint32_t offset, uint8_t dst[], uint32_t length) {
  uint32_t n;

  for (n = 0; n < length; n++)
  {
    dst[n] = DiskImage[offset+n];
  }
}
Эта функция просто копирует из образа в буфер запрошенную область диска (область с позиции offset и длиной length байт).

Функция записи ни чем не сложнее:

void MSC_MemoryWrite (uint32_t offset, uint8_t src[], uint32_t length) {
  uint32_t n;

  for (n = 0; n < length; n++)
  {
    //DiskImage[offset+n] = src[n];	// Отключили, т.к. не можем писать в константный массив
  }
}
Её задача записать данные из буфера в указанную область диска (область с позиции offset и длиной length байт). Только вот само копирование мы удаляем (комментируем), т.к. мы не можем писать в константный массив. Просто учтем на будущее, для чего эта функция нужна в оригинале.

Линкер

Не рассмотренным остался следующий момент. Как гласит даташит и аппноут, встроенный драйвер USB-MSC использует в служебных целях первые 384 байта оперативной памяти контролера. И если мы ничего не предпримем, то ту же самую область задействует линкер для переменных нашего кода, он то не знает, что в контроллере будет кто-то использовать эту область. Тут то и будет лажа.

Что бы разрешить возникшую проблему, нам надо попросить линкер не использовать данную область для переменных кода. Как это сделать, описано в разделе 3.1.1 аппноута AN10905.

В качестве заключения

Встроенный драйвер позволяет нам не знать ничего про то, как работает USB шина и осуществляет обмен тот или иной класс устройств. Нам не надо писать код реализации протокола. Драйвер предоставляет нам более простой и понятный интерфейс взаимодействия, да к тому же экономит нам Flash память для реализации собственных задач.

Недосмотр в примере немного усложняет "быстрый" старт, но эта ошибка находится довольно просто в отладчике пли добавлении отладочного вывода. Наверное, поэтому она до сих пор и не исправлена, а может, просто никто и не жаловался.

Отключение записи несколько сократило функционал и привело к некорректной работе нашего устройства при удалении и копировании на него файлов. Но это только учебный пример, а не конечное устройство.

В контроллере LPC13xx так же присутствует драйвер USB-HID класса. Для него существует аппноут AN10904. Предлагаю вам ознакомиться с ним самостоятельно. Не смотря на то, что это довольно популярный способ подключения самодельных устройств к компьютеру, в своем цикле я его рассматривать не буду.

Файлы: usbmemrom.zip