Интерфейс USB. Завершение реализации.


Итак, нам осталось разобраться с процессом обработки стандартных запросов USB и с дескрипторами. Давайте сначала разберемся с теорией, а потом подробно разберем пример обработки конкретного запроса.

Обработка запросов.

Процесс обработки запроса состоит из трех фаз (SETUP, DATA, STATUS), причем фаза DATA является опциональной.


Как мы помним из предыдущей статьи, для передачи запросов хост использует специальный TOKEN пакет — TOKEN SETUP. Собственно говоря, первая фаза состоит из того, что хост посылает TOKEN SETUP, затем посылает пакет данных (8 байт) который содержит запрос. Устройство подтверждает получение запроса ACK пакетом. Структуру запроса мы рассмотрим ниже, пока перейдем к следующей фазе – фазе обмена данными.

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

Хост, отравив запрос в SETUP фазе, знает тип запроса и, соответственно, знает: нужна ли для данного типа запроса фаза обмена данными и в какую сторону будет происходить обмен.

Если запрос предполагает передачу дополнительных параметров — хост отправляет TOKEN OUT и пакет данных содержащий дополнительные параметры запроса. Устройство подтверждает получение пакета данных, после чего хост может послать следующий пакет данных и т. д.
Соответственно, если запрос предполагает получение ответа от устройства, хост посылает TOKEN IN и ожидает пакет данных от устройства.

Последняя (STATUS) фаза нужна для подтверждения корректности обработки всего запроса. На данной фазе устройство или хост просто посылает пакет данных нулевой длины, что свидетельствует об успешной обработке запроса на логическом уровне. Если запрос предполагал получение ответа от устройства – то данный пакет посылает хост, подтверждая, что ответ получен/обработан. В другом случае – пакет нулевой длины шлет устройство, подтверждая, что запрос (запрос и дополнительные параметры) получены и обработаны.

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

Теперь заглянем «внутрь» запроса (8 байт данных полученных устройством в SETUP фазе). Запрос состоит из следующих полей:

bmRequestType — 1 байт
bRequest – 1 байт
wValue – 2 байта, WORD
wIndex – 2 байта, WORD
wLength – 2 байта, WORD

Теперь подробно:

bmRequestType – битовое поле, которое содержит характеристики запроса:

Бит 7: Направление передачи данных в DATA фазе:
0 = от хоста к устройству
1 = от устройства к хосту

Биты 6...5: Тип запроса
0 = стандартный запрос USB
1 = стандартный запрос для определенного класса устройств USB
2 = пользовательский запрос
3 = зарезервировано

Биты 4...0: Кому адресован запрос
0 = устройству
1 = интерфейсу
2 = конечной точке
3 = другое
4...31 = зарезервировано

bRequest – уникальный код запроса (например, 0 = GET_STATUS, 5 = SET_ADDRESS, 6 = GET_DESCRIPTOR и т. д.)

Здесь следует пояснить, зачем нужны биты 4..0 поля bmRequestType. Например, для получения статуса хост посылает запрос GET_STATUS (bRequest = 0). Значение бит 4...0 поля bmRequestType определяет, статус чего именно хочет получить хост: 0 – статус устройства, 1 – статус интерфейса, 2 – статус контрольной точки и т. д.). Таким образом, «тип запроса» определяется битами 4..0 поля bmRequestType и полем bRequest.

wValue – данное поле может использоваться для передачи параметров запроса. Например, для запроса SET_ADDRESS данное поле содержит адрес, который нужно присвоить устройству. Если параметры запроса не помещаются в 2 байта wValue, то хост их передает в DATA фазе.

wIndex – значение данного поля зависит от типа запроса. Например, для запроса GET_STATUS к конечной точке, данное поле содержит индекс конечной точки, статус которой хост хочет получить.

wLength – данное поле содержит размер данных, которые будут переданы хостом в DATA фазе либо которые хост ожидает получить в DATA фазе.

Для примера, рассмотрим алгоритм обработки запроса GET_DESCRIPTOR.

1. Хост посылает устройству запрос:

bmRequestType = 0x80
bRequest = 6 (GET_DESCRIPTOR)
wValue = 0x100 (тип дескриптора, который хост хочет получить, в данном случае дескриптор устройства)
wIndex = 0 (в данном случае параметр не используется)
wLength = 18 (в ответ хост хочет получить 18 байт)

Из запроса видно, что хост хочет получить дескриптор нашего устройства и что за SETUP фазой предполагается DATA фаза «от устройства к хосту».

2. Устройство посылает хосту 18 байт дескриптора (DATA фаза).

3. Хост подтверждает успешное завершение обработки запроса, отправив устройству пакет данных нулевой длины (STATUS фаза).

Будем считать, что с обработкой запросов мы разобрались. Все запросы подробно разбирать мы не будем, т. к. это очень объемный материал. Детально все типы стандартных запросов описаны в разделе 9.3 официальной спецификации.

Касательно нашего CDC устройства — помимо стандартных запросов USB, для нашего класса устройств предусмотрено несколько дополнительных запросов (SET_LINE_CODING, GET_LINE_CODING, SET_CONTROL_LINE_STATE). Эти запросы предназначены для установки параметров (Baudrate, Stop Bits, Parity, Data bits) нашего виртуального COM-порта. В нашей реализации эти параметры нам не нужны, но обрабатывать эти запросы мы обязаны, поэтому реализуем обработку в виде «заглушек».

Дескрипторы

Теперь поговорим о дескрипторах.

Стандартом определены следующие типы дескрипторов:

— Дескрипторы устройства
— Дескрипторы конфигурации
— Дескрипторы интерфейса
— Дескрипторы конечной точки
— Строковые дескрипторы

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

Дескрипторы конфигурации описывают конфигурации нашего устройства.

Здесь остановимся немного подробнее. Дело в том, что устройство может поддерживать несколько альтернативных конфигураций. Например, в конфигурации «1» устройство питается от интерфейса USB, а в конфигурации «2» – от внешнего источника. Или в конфигурации «1» устройство для обмена данными с хостом использует дополнительные контрольные точки типа bulk, а в конфигурации «2» – весь обмен идет через «нулевую конечную точку».

Для каждой из конфигураций устройство хранит свой дескриптор конфигурации. Хост получает по запросу дескрипторы всех конфигураций и выбирает одну из возможных конфигураций (логика выбора «рабочей» конфигурации из возможных заложена в драйвере устройства). После этого, хост назначает устройству номер текущей конфигурации (отправив стандартный запрос SET_CONFIGURATION) и дальнейшая работа устройства происходит в соответствии с выбранной хостом конфигурацией.

Для каждой конфигурации устройства определен один или несколько «интерфейсов». Интерфейс определяет, какие контрольные точки будут использоваться для обмена данными между хостом и устройством. Эта информация и есть «дескриптор интерфейса».

Информация о каждой конечной точке интерфейса называется «дескриптор конечной точки». Для каждой конечной точки дескриптор описывает тип конечной точки, ее параметры (такие как максимальный размер буфера обмена и т. д.)


Каждый дескриптор конфигурации содержит в себе дескрипторы интерфейса, а каждый дескриптор интерфейса содержит дескрипторы конечных точек.

Строковые дескрипторы опциональны, они позволяют получить хосту, в виде UNICODE строки, название устройства, название производителя устройства, серийный номер устройства.

Для примера, рассмотрим «дескриптор устройства», пример которого уже приводился в одной из статей.



static const BYTE devDescriptor[] = {
        /* Device descriptor */
        0x12,   // длина дескриптора в байтах;
        0x01,   // тип дескриптора: 1-Device descriptor
        0x00,   // 2 байта - версия USB: 2.0
        0x02,   //
        0x02,   // класс устройства: 2-CDC class code
        0x00,   // подкласс: 0-CDC class sub code
        0x00,   // протокол: 0-CDC Device protocol
        0x08,   // максимальный размер пакета для "нулевой конечной точки"
        0xEB,   // 2 байта - код производителя VID
        0x03,   //
        0x27,   // 2 байта - код устройства PID
        0x61,   //
        0x10,   // 2 байта - версия (ревизия) устройства
        0x01,   //
        0x01,   // индекс строки с названием производителя
        0x02,   // индекс строки с названием устройства
        0x03,   // индекс строки с серийным номером устройства
        0x01    // количество поддерживаемых конфигураций
};



Итак, в дескрипторе содержится следующая ключевая информация:

1. Устройство поддерживает версию стандарта 2.0
2. Устройство относится к классу CDC
3. Код производителя 0x03EB (Atmel)
4. Код продукта (в рамках производителя) 0x6127
5. Код ревизии устройства 0x0110
6. Устройство поддерживает одну конфигурацию

Компания Atmel позволяет использовать свой код производителя (VID) при создании USB устройств на базе ее продукции, правда с небольшими оговорками. Остальные параметры (Код продукта, Код ревизии устройства) выбраны «с потолка» :)

Комбинация VID/PID используется ОС для идентификации устройства и поиска «подходящего» драйвера. В некоторых случаях, для поиска драйвера используется информация о классе устройства (например, для HID и MSD устройств).

Описывать «побитно» все дескрипторы мы не будем, эта информация подробно описана в разделе 9.6 спецификации.

Теперь фрагмент кода — реализации данного уровня логики USB.



static void USB_Enumerate(UsbRequest * Request) {

	WORD wStatus;

	switch ((Request->bRequest << 8) | Request->bmRequestType) {
		//Запрос дескриптора
		case STD_GET_DESCRIPTOR:
			switch(Request->wValue) {
				case 0x100:
					//Запрос дескриптора устройства
					USB_CfgSend(devDescriptor, MIN(sizeof(devDescriptor), Request->wLength), NULL, NULL);
					break;
				case 0x200:
					//Запрос дескриптора конфигурации
					USB_CfgSend(cfgDescriptor, MIN(sizeof(cfgDescriptor), Request->wLength), NULL, NULL);
					break;
				case 0x300:
					//Запрос строкового дескриптора
					USB_CfgSend(strLanguage, MIN(sizeof(strLanguage), Request->wLength), NULL, NULL);
					break;
				case 0x301:
					//Запрос строкового дескриптора
					USB_CfgSend(strManufacturer, MIN(sizeof(strManufacturer), Request->wLength), NULL, NULL);
					break;
				case 0x302:
					//Запрос строкового дескриптора
					USB_CfgSend(strProduct, MIN(sizeof(strProduct), Request->wLength), NULL, NULL);
					break;
				case 0x303:
					//Запрос строкового дескриптора
					USB_CfgSend(strSerial, MIN(sizeof(strSerial), Request->wLength), NULL, NULL);
					break;
				default:
					//Неизвестный тип дескриптора
					USB_CfgSendStall();
					break;
			}
			break;
		case STD_SET_ADDRESS:
			//Установка адреса устройства
			USB_CfgSendZlp(USB_SetAddress, (void *) (DWORD) Request->wValue);
			break;
		case STD_SET_CONFIGURATION:
			//Установка конфигурации устройства
			USB_CfgSendZlp(USB_SetConfig, (void *) (DWORD) Request->wValue);
			break;
		case STD_GET_CONFIGURATION:
			//Запрос номера конфигурации устройства
			USB_CfgSend((BYTE *) &(CurrentConfiguration), sizeof(CurrentConfiguration), NULL, NULL);
			break;
		case STD_GET_STATUS_ZERO:
			//Запрос статуса устройства
			wStatus = 0;
			USB_CfgSend((BYTE *) &wStatus, sizeof(wStatus), NULL, NULL);
			break;
		case STD_GET_STATUS_INTERFACE:
			//Запрос статуса интерфейса
			wStatus = 0;
			USB_CfgSend((BYTE *) &wStatus, sizeof(wStatus), NULL, NULL);
			break;
		case STD_GET_STATUS_ENDPOINT:
			//Запрос статуса конечной точки
			wStatus = 0;
			Request->wIndex &= 0x0F;
			if ((AT91C_BASE_UDP->UDP_GLBSTATE & AT91C_UDP_CONFG) && (Request->wIndex <= 3)) {
				wStatus = (AT91C_BASE_UDP->UDP_CSR[Request->wIndex] & AT91C_UDP_EPEDS) ? 0 : 1;
				USB_CfgSend((BYTE *) &wStatus, sizeof(wStatus), NULL, NULL);
			}
			else if ((AT91C_BASE_UDP->UDP_GLBSTATE & AT91C_UDP_FADDEN) && (Request->wIndex == 0)) {
				wStatus = (AT91C_BASE_UDP->UDP_CSR[Request->wIndex] & AT91C_UDP_EPEDS) ? 0 : 1;
				USB_CfgSend((BYTE *) &wStatus, sizeof(wStatus), NULL, NULL);
			}
			else
				USB_CfgSendStall();
			break;
		
		//… часть кода пропущена …


		//Запросы специфические для CDC устройств
		case SET_LINE_CODING:
			USB_CfgRecv((BYTE *) &line, MIN(sizeof(line), Request->wLength), CDC_SetLineCodiing, NULL);
			break;

		case GET_LINE_CODING:
			USB_CfgSend((BYTE *) &line, MIN(sizeof(line), Request->wLength), NULL, NULL);
			break;

		case SET_CONTROL_LINE_STATE:
			USB_CfgSendZlp(NULL, NULL);
			break;

		default:
			_DBG("[USB ENUM REQUEST %04X]\n", (Request->bRequest << 8) | Request->bmRequestType);
			USB_CfgSendStall();
			break;
	}
}


Эта статья завершает цикл.

Как и обещал – выкладываю исходные коды примера реализации (не стоит забывать, что это лишь пример, реализация очень упрощена). Для демонстрации работы стека в main.c реализована простая эхо-отвечалка, все данные полученные от ПК устройство отправляет обратно :)

Вместе с исходниками лежит файл USBCDC.inf – этот файл нужен ОС Windows для того, чтобы связать VID/PID нашего устройства со стандартным драйвером виртуального USB COM-порта (usbser.sys)

  • +8
  • 15 ноября 2011, 17:35
  • e_mc2
  • 1
Файлы в топике: USBCDC.zip

Комментарии (28)

RSS свернуть / развернуть
Хм… Даже не верится, что все так просто. Буду пробовать. :) Правда, мне придется все это портировать на STM32.
0
  • avatar
  • _YS_
  • 15 ноября 2011, 20:17
Пробуйте! Если что — обращайтесь, буду рад помочь.
0
Спасибо, если чего — буду спрашивать.
0
Для стм32 есть еще пример от Кейла, еоторый собран подобным образом без использования библиотек. Можно еще его покурить.
0
Самая большая сложность с USB на STM32 — это то, что USB переферия адресует буфер в 16битном режиме. а ядро в 32битном.
Т.е.если мы выделили буфер на 8кб, понастоящему в памяти будет выделено 16кб.
И данные будут записаны 2х байтным пропуском.
Например, если с USB пришло:
0x0A 0x0B 0x0C 0xD,
то в памяти будет:
0x0A 0x0B 0x00 0x00 0x0C 0xD 0x00 0x00
Это относится как к Tx, так и к Rx буферу. В датащите про это очень непонятно написано.
Проще догадаться по исходникам USB стэка и дампам памяти:)
Смотрите usb_mem.c, там две функции UserToPMABufferCopy и PMAToUserBufferCopy.
P.S.: стэк от ST — ужасен.
0
Да, библиотека от STM для USB оставляет желать лучшего.
Может кто знает более адекватные сторонние библиотеки для STM?
0
Ага, 500 строк только на инициализацию — это вобще отличная библиотека:)

Я поискал альтернативы — ничего не нашел. Решил под совои цели сделать небольшой велосипед, простенькую библиотеку для бесклассовых устройств и только с bulk эндпоинтами.
0
Кто то может залить чтобы уже был готовый проект, а то создал проект и получаю:
lib_AT91SAM7X256.h(56): warning: #260-D: explicit type is missing («int» assumed)



показывает что здесь ошибка -> __inline unsigned int AT91F_AIC_ConfigureItH
0
  • avatar
  • VIC
  • 16 ноября 2011, 19:18
А вы кокой компилятор используете?
Пример, который я выложил, рассчитан на GCC, Makefile лежит вместе с исходниками.
0
Keil
0
С Keil я не работал. Если хотите, могу скомпилировать код примера для AT91SAM7X256 и выложить бинарь прошивки.
0
да нет не нужно, хочеться разобраться в программе…
0
в чём отличие прошивки для работы usb как CDC и как, например, HID? только в указании класса устройства в дескрипторе устройства? или если поменять класс на HID и больше ничего, то работать не будет?
0
Если просто поменять класс в дескрипторе – работать не будет.
1. Для каждого класса есть специфические запросы USB, которые должны обрабатывать устройства данного класса.
2. Для каждого класса определен свой интерфейс (кол-во используемых конечных точек, их типы, назначение)
3. Ну, и, соответственно, логика работы самого устройства отличается.
0
С помощью HID можно обмениваться с у стройством информацией типа string или массив char? или для этого подходит только cdc?
0
И HID и CDC устройства могут передавать любе данные. «string или массив char» это не более чем представление информации на уровне ЯП, с точки зрения USB это просто биты/байты.
0
в чем же тогда преимущество CDC?
0
Ну, абстрактно сравнивать классы устройств некорректно т. к. каждый класс создавался под конкретные задачи. Например, какие преимущества у MSD перед HID (грубо говоря, у флешки перед клавиатурой)?

В случае CDC – можно «закосить» под СОМ порт, и программы ориентированные на работу с СОМ портом (коих существует множество) будут работать с устройством как с «родным». Теоретически, HID устройства будут проигрывать CDC по скорости обмена (хотя я не замерял скорость и она может завесить от реализации драйвера и т. д.)

Выбор класса устройства (или реализация своего, специфического, интерфейса) зависит от конкретной задачи.
0
Преимущество CDC в простоте программирования со стороны ПК — можно использовать стандартный API для работы с COM-портом, не заморачиваясь на штуки вроде libusb.

Кстати, я как раз хочу портировать (попутно в нем разобравшись) этот пример на STM32 как софтовую альтернативу FT232.
0
В проекте после описания первого дескриптора интерфейса описаны ещё 4 Functional Descriptor. Объясните, зачем они нужны?
0
Это «Class-Specific» дескрипторы, я совсем забыл о них упомянуть.
В данном случае это дескрипторы специфические для класса CDC. Посмотреть значения полей этих дескрипторов можно в разделе 5.2.3 спецификации СDC
0
кто то может переделать под Keil?
0
  • avatar
  • VIC
  • 30 ноября 2011, 18:54
как работает callback функция в данном случае?
эта запись TrasferCallback callback создаёт указатель на функцию с названием callback и аргументами
(BYTE * Buffer, WORD Size, void * Param, BYTE Status)?
что возращает эта функция, когда записываем Endpoint[ep].Callback? непонятно, потому что не понятно какие действия выполняет функция callback с параметрами, которые ей передаются? и она, вроде, ничего не должна возвращать, написано же typedef void…
0
  • avatar
  • kooos
  • 01 декабря 2011, 01:06
разобрался.
0
Для Keil потребовалось убрать строки
#define __inline static inline
и почему-то исправить конструкции вида for(BYTE i = 0; i < 8; i++) на
BYTE i;
for(i = 0; i < 8; i++)
Правда симуляция не пошла — ругается на строку AT91C_BASE_UDP->UDP_RSTEP. Мол к адресу 0xFFFB0028: no 'write' и 'read' permission.
Интересно, это из-за особенность симуляции?
0
VIC пробовал портировать код под Keil.

Запустилось, но с костылями (вылезли абсолютно непонятные для меня моменты, которые мы обошли, но с помощью жутких костылей). Если будет время – я установлю Keil и попробую разобраться.

Правда, проблем с AT91C_BASE_UDP->UDP_RSTEP, на сколько я помню, не было (но мы тестировались на живом девайсе, не в симуляторе).
0
Вот под keil проверял на at91sam7s64 docs.google.com/open?id=0B7wmsb2L7GVtLTZjN2hLT0lRS3FzWEZzZ1hXblRKdw
0
Плиз выложите где нибудь подоступнее
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.