Что такое USB и как его надкусить, чтоб не поломать зубы.
Данная тема рассчитана на людей, которые программируют на ASM. Не скажу что я куру в программировании USB, а всего лишь пока познаю этот темный мир. Поэтому, если у кого есть что сказать, дополнить или исправить - пишите в личку или в постах к этой теме.
Предыстория.
Все началось с того, что попал ко мне в руки чип AT90USB646. Долго я ходил вокруг него и не знал с какой стороны к нему подойти, чтоб полностью понять, как все же работает этот загадочный USB. Поиски по просторам интернета ни к чему не привели. Везде попадались примеры на Си языке, что для меня было тяжко в понимании всего процесса. (Если же кого-то интересует именно Си, смотрите в сторону V-USB, LUFA, и сайт ATMEL с его кучей примеров). После долгих подходов к примеру от ATMEL (
http://www.atmel.com/tools/AT90USBKEY.aspx) и его дебагинга, дело пошло в более понятном направлении. Вот этим я и хочу со всеми поделиться (может кому и поможет).
Начало.
Для начала хочу поблагодарить сайты (точнее их авторов) за их работу. Вот ссылки, (на русском) по которым можно неплохо подтянуть свои знания по USB/
1.
http://www.gaw.ru/html.cgi/txt/doc/micros/avr/at90usb/index.htm
2.
http://microsin.ru/content/view/1107/44/
Для более детального ознакомления -
http://www.usb.org (это уже на Английском)
Поехали.
Прежде чем получить что-то по USB, его надо для начала инициализировать. Вот пример:
Код:
|
SetIOBit CLKPR,CLKPCE ; макрос установки одного бита в регистре (Регистр, Бит)
clr r16
sts CLKPR,r16 ; Очищаем делитель тактовой частоты
ldi r16,(1‹‹UIMOD)|(1‹‹UVREGE) ; Выбираем режим Device и встроенный сабилизатор напряжения
sts UHWCON,r16
;Usb_disable
; макрос сброса нескольких бит в регистре (Регистр, Биты)
ClrIOMBit USBCON,(1‹‹USBE)|(1‹‹OTGPADE) ; Отключаем полностью USB модуль
SetIOBit UDCON,DETACH ; Отключаем выводы D+ и D-
SetIOMBit PLLCSR,(3‹‹PLLP0) ; Устанавливаем делитель тактовой частоты на 4 ((CLK/PLLP0)*24 = 48Mhz)
SetIOBit PLLCSR,PLLE ; Разрешаем работу PLL
PLL_START:
IN R0,PLLCSR
SBRS R0,PLOCK ; Если генератор вышел на нужный режим – пойдем дальше
RJMP PLL_START
;Usb_enable
SetIOMBit USBCON,(1‹‹USBE)|(1‹‹OTGPADE) ; Включаем USB модуль
ClrIOBit USBCON,FRZCLK ; Включаем синхронизацию USB модуля
;Usb_select_device
ClrIOBit USBCON,HOST ; выбираем режим Device
ldi r16,0
sts UENUM,r16 ; Выбираем EndPoint 0
SetIOBit UECONX,EPEN ; Активируем ее
ldi r16,0x00
sts UECFG0X,r16 ; Ее тип будет Control и направление OUT
ldi r16,0x00
sts UECFG1X,r16 ;
SetIOMBit UECFG1X,(3‹‹EPSIZE0) ; Размер ее будет 64 байта
SetIOBit UECFG1X,ALLOC ; Указываем контроллеру чтоб принял параметры и выделил память. Результат отображается в UESTA0X, CFGOK
ldi r16,(1‹‹EORSTE)
sts UDIEN,r16 ; Разрешаем прерывания USB (End of Reset)
mov r3,r16
;Usb_select_endpoint(EP_CONTROL = 0)
ldi r16,0
sts UENUM,r16 ; Выбираем EndPoint 0 (мало ли что там случилось)
USB_wait_vbus:
LDS R16,USBSTA
SBRS R16,VBUS ; Если на ноге VBUS устаканилось напряжение, продолжаем
rjmp USB_wait_vbus
;Usb_attach
ClrIOMBit UDCON,(1‹‹DETACH)|(1‹‹LSM) ; Подключаем D+ и D- к шине и выбираем скорость FullSpeed (12Mbps)
Sei |
Все. Теперь мы можем принимать и отправлять данные через EP0 (EndPoint0)/ ЕР0 является двунаправленной и существует всегда, даже когда устройство не сконфигурировано. В нее приходят SETUP пакеты (токены) с определенными параметрами. Чтобы отловить пакет SETUP в EP0 используем следующую процедуру
Код:
|
wait_req1:
ldi r16,0
sts UENUM,r16
LDS R16,UEINTX
SBRS R16,RXSTPI
RJMP wait_req1 |
Установка бита RXSTPI в регистре UEINTX означает что пришел SETUP пакет и нам его надо обработать. SETUP пакет всегда имеет стандартную длину в 8 байт Первый же пакет который мы получим будет в виде:
80 06 00 01 00 00 40 00 (это все в Hex формате)
Что означают эти цифры:
1й байт (80) – Тип запроса
2й байт (06) – Сам запрос. В данном случае GetDescriptor
3й и 4й байты (LSB – 00, MSB – 01) wValue. В данном запросе важен только MSB (01) – и он означает что нам нужен дескриптор устройства (DescriptorDevice). (подробнее о запросах и их вариантах можно почитать тут -
http://microsin.net/programming/AVR/usb-in-a-nutshell-part2.html (Глава 6: запросы USB)
5й и 6й байт - (LSB – 00, MSB – 00) wIndex. Используется только в запросе GetDescriptorString и будет содержать значение LanguageId.
7й и 8й байт - (LSB – 40, MSB – 00) - wLength. – Число байт которые HOST хочет получить от нашего устройства. (прим. Мы его занесем в регистр X)
Ниже будут описания дескрипторов использованных в примере «STK526-series2-hidkbd-2_0_2-doc» от фирмы ATMEL
На самом деле в первом запросе, HOST интересует только первые 8 байт. Процедура ответа на запрос выглядит следующим образом:
Код:
|
ldz16 (T_DescriptorDevice‹‹1) ; В регистре Z указываем на расположение даскриптора
ldy16 0x0012 ; В регистре Y указываем его длину
GetDescriptorData:
ClrIOBit UEINTX,RXSTPI ; Подтверждаем принятие SETUP пакета
cp XL,YL ; Если HOST запросил больше чем размер дескриптора
cpc XH,YH ; занесем в X размер дескриптора. Если меньше – в Х
brlo GDD0 ; будет число бай, которое запросил HOST
mov XL,YL
mov XH,YH
GDD0: ; Подождем когда буфер FIFO будет готов принять данные
LDS R16,UEINTX ;
SBRS R16,TXINI ;
RJMP GDD0 ;
GDD1:
LDS R16,UEINTX
andi r16,0b10000001
sts UEINTX,r16
LPM r16,Z+ ; Заносим данные в буфер через регистр UEDATX
STS UEDATX,r16
sbiw X,1
brne GDD1
ClrIOBit UEINTX,TXINI ; Сообщаем нашему контроллеру что он может передать данные
GDD2:
LDS R16,UEINTX ; Ждем пока HOST получит данные и ответит нам что все отлично
SBRS R16,RXOUTI ; путем отправки ZLP (ZeroLenghtPacket). Об этом просигнализирует
RJMP GDD2 ; установка бита RXOUTI в регистре UEINTX
ClrIOBit UEINTX,RXOUTI ; Сбросим биты RXOUTI и NAKOUTI
ClrIOBit UEINTX,NAKOUTI
rjmp wait_req1 |
После этого HOST активирует на шине USB Reset. Это вызовет в контроллере генерацию прерывания по вектору 0x0014 (USB_GENaddr). По сути, это нам ничего не дает и на это событие HOSTу отвечать не нужно. Надо только сбросить флаг EORSTI в регистре UDINT и переопределить EP0 (тип, направление, размер). Я этого сначала не сделал и это были мои первые грабли. Как они проявились – читаем далее.
На этом этапе наше устройство находится на этапе «Подключено»
После USB Reset сразу получим запрос на присвоение адреса нашему устройству. Выглядит он следующим образом:
00 05 01 00 00 00 00 00
Из всего этого нас интересуют только первые 3 байта
1й байт (00) – Тип запроса 7й бит = 0 означает что HOST передает информацию (подробнее о запросах и их вариантах можно почитать тут -
http://microsin.net/programming/AVR/usb-in-a-nutshell-part2.html (Глава 6: запросы USB)
2й байт (05) – Сам запрос. В данном случае SetAddress
3й байт(LSB – 01) LSB wValue. – Это наш адрес устройства (может быть от 1 до 127). Все дальнейшие запросы будут поступать уже на адресованное устройство
Процедура установки адреса следующая.
Код:
|
SetAddress:
TST R17 ; в R17 находится 1 байт (00)
BREQ SetAddress1
RJMP SetAddress3
SetAddress1:
andi r19,0x7F ; в R19 находится наш присвоенный адрес (01)
sts UDADDR,r19
ClrIOBit UEINTX,RXSTPI ; Подтверждаем принятие SETUP пакета
ClrIOBit UEINTX,TXINI ; Отправляем HOSTу ZLP пакет
SetAddress2:
LDS R24,UEINTX
SBRS R24,TXINI ; ждем пока все данные уйдут (на ZLP ответ HOST не отвечает)
RJMP SetAddress2
;Usb_enable_address
SetIOBit UDADDR,ADDEN ; и только после этого мы можем активировать наш присвоенный адрес
SetAddress3:
rjmp wait_req |
На этом этапе наше устройство принимает статус «Адресовано»
Сразу после присвоения адреса, HOST отправляет уже адресованному устройству запрос «GetDescriptorDevice», но уже c определенной длиной, которую он вычислил из первого запроса. Если Вы по какой-то причине не получили этот запрос, значит Вы неправильно присвоили адрес своему устройству. Это либо неверный адрес, либо вы присвоили полученный адрес одновременно с установкой бита ADDEN.
Пример запроса:
80 06 00 01 00 00 12 00
Как отвечать на запрос, было рассмотрено выше.
Вот тут и вылезли мои первые грабли. После ответа за запрос «GetDescriptorDevice», HOST по непонятной причине устанавливал на шине статус «USB Reset» и все начиналось сначала. Так продолжалось 3 раза, после чего HOST плевал на мое устройство. Я сначала косил на неправильный дескриптор, потом что данные как-то не так передаются хосту, потом пошли подозрения на фазу луны, расположение звезд и прочей неведомой силы. Оказалось все намного проще: после того как HOST устанавливает на шине статус USB Reset и контролер реагирует на это генерацией прерывания, он (контроллер) еще сбрасывает EP0 в исходное состояние, но при этом оставляет ее в активном состоянии. А в исходном состоянии, размер FIFO буфера равен 8байт. Вот и получалось что после адресации и запроса «GetDescriptorDevice» я отправлял только 8 байт из 14. Добавив в прерывание заново инициализацию EP0, все пошло дальше как по маслу.
Вот список всех запросов, на этапе определения моего контроллера
80 06 00 01 00 00 40 00 - GetDeviceDescroptor
00 05 01 00 00 00 00 00 - SetAddress
80 06 00 01 00 00 12 00 - GetDeviceDescroptor
80 06 00 02 00 00 09 00 - GetConfigurationDescroptor
80 06 00 03 00 00 FF 00 - GetStringDescroptor
80 06 03 03 09 04 FF 00 - GetStringDescroptor индексом 3 и LanguageID 0409
80 06 00 02 00 00 FF 00 - GetConfigurationDescroptor
80 06 00 06 00 00 0A 00 - GetDescroptor устройства при работе в другом скоростном режиме
80 06 00 03 00 00 FF 00 - GetStringDescroptor
80 06 02 03 09 04 FF 00 - GetStringDescroptor индексом 2 и LanguageID 0409
80 06 00 03 00 00 FF 00 - GetStringDescroptor
80 06 02 03 09 04 FF 00 - GetStringDescroptor индексом 2 и LanguageID 0409
80 06 00 01 00 00 12 00 - GetDeviceDescroptor
80 06 00 02 00 00 09 00 - GetConfigurationDescroptor
80 06 00 02 00 00 22 00 - GetConfigurationDescroptor
00 09 01 00 00 00 00 00 - SetConfiguration (устройство сконфигурировано)
21 0A 00 00 00 00 00 00 - GetInterface
81 06 00 22 00 00 7B 00 - GetHidReportDescriptor
21 09 00 02 00 00 01 00 - SetConfigurationInterface
После этого мое устройство определилось в системе как HID Keyboard. Осталось научится передавать данные в мое устройство и читать из него. Об результатах буду писать по мере их достижения.
А теперь сами дескрипторы:
[HTML]T_DescriptorDevice:
/*
.db 0x12 ; Длина поля дескриптора (1
1 ; Тип дескриптора (1)
0x0200 ; Версия спецификации USB в BCD-формате (0200h означает USB 2.0
03 ; Код класса устройства HID (Human Interface Device) -
http://www.usb.org/developers/define.../#BaseClass03h
0 ; Код подкласса устройства
1 ; Код протокола устройства (00h None, 01h Keyboard, 02h Mouse)
0x40 ; Максимальный размер пакета для EP0 (8, 16, 32 или 64)
0x03EB ; Идентификатор производителя (Vendor ID) (VID_ATMEL)
0x2017 ; Идентификатор продукта (Product ID) (PID_MegaHIDKeyboard)
0x1000 ; Версия устройства в BCD-формате
0x01 ; Индекс строкового дескриптора производителя
0x02 ; Индекс строкового дескриптора продукта
0x03 ; Индекс строкового дескриптора серийного номера
1 ; Число возможных конфигураций устройства на данной скорости
*/
.db 0x12,0x01,0x00,0x02,0x00,0x00,0x00,0x40,0xEB,0x03, 0x17,0x20,0x00,0x05,0x01,0x02,0x03,0x01
T_DescriptorConfiguration:
/*
.db 0x09 ; Размер данного дескриптора (9)
.db 0x02 ; Тип дескриптора (2 или 7)
.dw 0x0022 ; Общее число байтов полного описания данной конфигурации
.db 0x01 ; Количество интерфейсов в данной конфигурации
.db 0x01 ; Номер данной конфигурации. Данное значение используется требованием SET_CONFIGURATION
.db 0x00 ; Индекс строки, описывающей данную конфигурацию
.db 0xA0 ; Битовое поле, характеризующее конфигурацию.
; Распределение бит:
; D7 – зарезервировано (установлено в 1);
; D6 – признак наличия собственного источника питания;
; D5 – признак разрешения сообщения хосту о выходе устройства из режима «сна»;
; D4...D0 – зарезервированы (сброшены в 0)
.db 0x32 ; Значение максимального потребляемого тока от шины
; USB (для устройства, не имеющего собственного
; источника питания). Значение, указываемое в этом поле,
; равно половине реального потребления, т.е. число 50
; означает 100 мА
T_DescriptorInterface
.db 0x09 ; Размер данного дескриптора (9)
.db 0x04 ; Тип дескриптора (4)
.db 0x00 ; Номер интерфейса
.db 0x00 ; Номер альтернативной установки
.db 0x01 ; Число конечных точек, используемых интерфейсом (исключая EP0)
.db 0x03 ; Класс интерфейса
.db 0x00 ; Подласс интерфейса
.db 0x01 ; Код протокола
.db 0x00 ; Индекс строкового дескриптора интерфейса
T_DescriptorUnknown ????????
.db 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x3B, 0x00,
T_DescriptorEndPoint
.db 0x07 ; Размер дескриптора (7)
.db 0x05 ; Тип дескриптора (5)
.db 0x81 ; Адрес точки:
; Биты [3:0] — номер точки;
; Биты[6:4] — резерв (0);
; Бит 7 — направление (игнорируется для EP0):
; 0 = OUT, 1 = IN
.db 0x03 ; Битовое поле, характеризующее точку. Распределение бит:
; D7, D6 – зарезервированы (сброшены в 0);
; D5, D4 – функция, выполняемая точкой:
; 00 – точка данных;
; 01 – точка обратной связи;
; 10 – точка данных с неявной обратной связью;
; 11 – зарезервировано
; D3, D2 – тип синхронизации хоста и точки:
; 00 – без синхронизации;
; 01 – асинхронный;
; 10 – адаптивный;
; 11 – синхронный
; D1, D0 – тип обмена данными:
; 00 – контрольный;
; 01 – изохронный;
; 10 – bulk;
; 11 – interrupt
.dw 0x0008 ; Битовое поле, характеризующее размер пакета
; передаваемых данных. Распределение бит:
; D15…D13 – зарезервированы (сброшены в 0);
; D12, D11 – количество дополнительных передач:
; 00 – нет дополнительных передач;
; 01 – 1 дополнительная передача (всего 2 передачи),
; 10 – 2 дополнительные передачи (всего 3 передачи);
; 11 – зарезервировано.
; D10...D0 – размер пакета в байтах
.db 0x02 ; Для точек периодических транзакций — интервал
; обслуживания, для HS точек
; непериодического вывода — допустимая частота
; посылки NAK
*/
.db 0x09, 0x02, 0x22, 0x00, 0x01, 0x01, 0x00, 0xA0, \
0x32, 0x09, 0x04, 0x00, 0x00, 0x01, 0x03, 0x00, \
0x01, 0x00, 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, \
0x22, 0x3B, 0x00, 0x07, 0x05, 0x81, 0x03, 0x08, \
0x00, 0x02
T_DescroptorStringUserLanguageId:
.db 0x04, 0x03, 0x09, 0x04
T_DescriptorUserManufacturerString:
// ATMEL
.db 0x0C, 0x03, 0x41, 0x00, 0x54, 0x00, 0x4D, 0x00, 0x45, 0x00, 0x4C, 0x00
T_DescriptorUserProductString:
// AVR USB KEYBOARD DEMO
.db 0x2C, 0x03, 0x41, 0x00, 0x56, 0x00, 0x52, 0x00, \
0x20, 0x00, 0x55, 0x00, 0x53, 0x00, 0x42, 0x00, \
0x20, 0x00, 0x46, 0x00, 0x45, 0x00, 0x59, 0x00, \
0x42, 0x00, 0x4F, 0x00, 0x41, 0x00, 0x52, 0x00, \
0x44, 0x00, 0x20, 0x00, 0x44, 0x00, 0x45, 0x00, \
0x4D, 0x00, 0x4F, 0x00
T_usb_hid_report_descriptor_kbd:
.db 0x05,0x01 /* Usage Page (Generic Desktop) */
.db 0x09,0x06 /* Usage (Keyboard) */
.db 0xA1,0x01 /* Collection (Application) */
.db 0x05,0x07 /* Usage Page (Keyboard) */
.db 0x19,224 /* Usage Minimum (224) */
.db 0x29,231 /* Usage Maximum (231) */
.db 0x15,0x00 /* Logical Minimum (0) */
.db 0x25,0x01 /* Logical Maximum (1) */
.db 0x75,0x01 /* Report Size (1) */
.db 0x95,0x08 /* Report Count (
*/
.db 0x81,0x02 /* Input (Data, Variable, Absolute) */
.db 0x81,0x01 /* Input (Constant) */
.db 0x19,0x00 /* Usage Minimum (0) */
.db 0x29,101 /* Usage Maximum (101) */
.db 0x15,0x00 /* Logical Minimum (0) */
.db 0x25,101 /* Logical Maximum (101) */
.db 0x75,0x08 /* Report Size (
*/
.db 0x95,0x06 /* Report Count (6) */
.db 0x81,0x00 /* Input (Data, Array) */
.db 0x05,0x08 /* Usage Page (LED) */
.db 0x19,0x01 /* Usage Minimum (1) */
.db 0x29,0x05 /* Usage Maximum (5) */
.db 0x15,0x00 /* Logical Minimum (0) */
.db 0x25,0x01 /* Logical Maximum (1) */
.db 0x75,0x01 /* Report Size (1) */
.db 0x95,0x05 /* Report Count (5) */
.db 0x91,0x02 /* Output (Data, Variable, Absolute) */
.db 0x95,0x03 /* Report Count (3) */
.db 0x91,0x01 /* Output (Constant) */
.db 0xC0,0x00 /* End Collection */
[/HTML]
Осталось выяснить пару вопросов:
1. Как отправить данные HOSTу, которые больше чем буфер FIFO
2. Как общаться с USB устройством.
Примечания, замечания, дополнения и изменения пишите в личку или постах к теме
Отдельные благодатности;
1. Авторам сайта «microsin» -
http://microsin.net/
2. Авторам статьи «Полное описание контроллера интерфейса USB 2.0 Host/Device/OTG в AVR-микроконтроллерах AT90USB646/647/1286/1287» -
http://www.gaw.ru/html.cgi/txt/doc/micros/avr/at90usb/index.htm
3. Авторам статьи «Программисту USB_устройств» -
WWW.SOEL.RU www.wonderland.md/usb/USB.rar
Мой пример в AVR Studio 4 –
www.wonderland.md/usb/usb_project.rar