Устройство USB на ассемблере Печать
Добавил(а) Каменяр Сергей   

Желание создать свое USB-устройство рано или поздно овладевает умами многих радиолюбителей, увлекающихся программированием микроконтроллеров. Обычно USB-устройство это микроконтроллер (далее МК) с записанной в него программой (firmware), и с ней обычно взаимодействует программа на компьютере (ПО хоста), хотя иногда с устройством напрямую работает операционная система, если это USB HID мышь или клавиатура.

Примечание: расшифровку многих непонятных сокращений и терминов USB см. в статье [7], также обращайтесь к разделу "Словарик" в конце статьи. Для некоторых терминов оставлены их английские обозначения – для упрощения описания.

[Общие замечания по аппаратуре и программному обеспечению]

В качестве МК я выбрал AT90USB162 с аппаратным USB интерфейсом на борту. Есть и чисто программные решения на обычных микроконтроллерах AVR, например V-USB [6], но был выбран аппаратный интерфейс . Причина в том, что большая часть V-USB реализована на языке C (мне больше нравится ассемблер), и из-за того, что аппаратный интерфейс гораздо мощнее в плане скорости работы.

В качестве отладочной платы была выбрана макетка AVR-USB162 [4].

Чаще всего при создании устройств USB и ПО хоста для взаимодействия с ними выбирают готовые библиотеки и готовые примеры хоста. Это понятно – объем работы уменьшается, гораздо легче получить надежное и рабочее решение задачи. Здесь судьба благоволит Си-программистам, к их услугам куча материалов, примеров, библиотек [6, 8, 9].

Но как быть мне, приверженцу ассемблера? Изучать Си?! Я поддался было этому настроению, ибо смущало распространенное в сети мнение, будто на ассемблере сложно или даже не реально написать программу для создания устройства USB. Какое-то время колебался, пытался разобраться в хитросплетениях демонстрационного пакета STK526-series2-hidio-2_0_2-doc [] от компании Atmel, но безуспешно. В результате решил изобрести свой велосипед, где программа будет полностью написана на ассемблере. Вооружившись теорией, несколько месяцев, шаг за шагом, методом проб и ошибок, я создавал свою программу. МК позволяет себя многократно перепрограммировать, программа писалась методом "написал-скомпилировал-прошил-незаработало-написал..." и так далее по кругу, до получения нужного результата. ИМХО, все получилось проще и изящней, чем упомянутая программа на языке C от ATMEL. По крайней мере код получился намного компактнее.

Несмотря на всю проделанную работу, остались неясности и вопросы, на которые мне не удалось ответить - не все было понятно в техническом описании на МК (даташит doc\AT90USB162-doc7707.pdf []). Статья не претендует на полную достоверность, изложил лишь свое понимание материала. Надеюсь, кому-нибудь оно пригодится, ведь в конце концов, все работает... почти: есть проблемы со свежими операционными системами Windows.

Уже когда заканчивалась работа над статьей, выяснилось, что описанное здесь устройство не определяется вовсе, или определяется только после многократных попыток соединения с компьютером под управлением ОС Windows 7 (под Windows XP проблем нет). Пока с этим вопросом не разбирался.

Разработанное устройство относится к классу USB HID. Это популярный класс у начинающих разработчиков, потому что позволяет быстро получить работоспособное решение без необходимости написания драйвера (для данного класса устройств в WINDOWS уже имеется стандартный набор драйверов, может потребоваться – и то не всегда – только файл описания *.inf устройства).

ПО хоста, написанное для этого USB HID устройства, просто демонстрирует сам процесс обмена данными через конечные точки USB. В целях упрощения описания устройство не выполняет каких-либо сложных функций. Таким образом, проект легче приспособить для обмена любыми данными, в зависимости от конкретной задачи. Например, можно получать данные от различных датчиков и отправлять их в компьютер для обработки – получится устройство ввода (так работают клавиатура или мышь USB, и другие USB-устройства ввода). Можно принимать команды от компьютера, чтобы управлять каким-либо другим устройством или процессом – получится устройство вывода. В общем, описанный в статье проект можно взять за основу при разработке своих функциональных устройств, способных обмениваться нужной информацией, для чего проект должен быть дополнен соответствующим прикладным кодом.

Прерывания и опрос флагов событий. Как обрабатывать события USB – с помощью опроса значений флагов в регистрах или по прерываниям? Как известно, МК может реализовывать свои алгоритмы работы с внешними устройствами одним из двух способов: периодический, достаточно частый опрос периферийного устройства на предмет готовности к обмену, либо по прерываниям, когда периферийное устройство само является инициатором обмена и формирует запрос на прерывание. Здесь под "периферийным устройством" подразумевается внутренняя периферия самого МК: таймеры, модуль UART, АЦП, модуль USB и т. д., а не устройства, присоединенные к нему проводками (хотя в определенных случаях они также могут генерировать прерывания по перепаду уровня на выводе порта GPIO).

Моя программа написана по классической методике обработки прерываний - тело программы представляет собой пустой цикл, содержащий всего две команды: sleep и rjmp SLP, а вся работа возложена на обработку двух прерываний от модуля USB. ИМХО, этот способ предпочтительнее, поскольку позволяет рационально распределять процессорное время, не тратить его на бесконечный опрос регистров периферии на предмет готовности к обмену. Обмен данными с хостом производится только в подпрограмме-обработчике прерывания USB_ENDPOINT от соответствующей конечной точки. Все остальное время, а это 99.9%, МК пребывает в состоянии idle (состояние сна), т. е. бездействует. Возьмем, к примеру, цикл передачи данных хосту в рабочем режиме (после прохождения процедуры энумерации). Здесь подпрограмма-обработчик USB_ENDPOINT для точки EP1 вызывается всякий раз при приходе запроса IN от хоста (с интервалом, прописанным в дескрипторе конечной точки EP1). На обработку этого прерывания МК затрачивает всего 3.3 мкс (при тактовой частоте 16 МГц). Если интервал опроса задать, к примеру, 30 мс, то получается, что МК бездействует мс в каждом цикле почти на 99% процессорного времени! Это свободное время можно использовать под какие-нибудь прикладные цели и задачи, если добавить соответствующий код. Код этот может работать как с периферией МК, так и с внешними устройствами. Добавив, например, сюда подпрограмму обмена по интерфейсу SPI (I2C/TWI, UART и т. д.), мы сможем получать данные от всевозможных датчиков или АЦП, для последующей передачи этих данных хосту.

[Описание firmware микроконтроллера]

Блок векторов прерываний. Начало программы типично для большинства ассемблерных программ для AVR-микроконтроллеров. Это прежде всего директива .INCLUDE, указывающая транслятору на необходимость вставить в наш программный код файл usb162def.inc. Файл содержит описание констант, рабочих регистров, векторов прерываний нашего микроконтроллера. Это своего рода база данных, посредством которой транслятор отождествляет наш код с конкретной моделью МК.

Директива .CSEG задает начало сегмента кода нашей программы или просто – начало программы. Директива .ORG $000 присваивает счетчику команд значение $000. По этому адресу выбирается команда rjmp RESET, и происходит переход на подпрограмму обработки сброса - метка RESET. RESET это главная подпрограмма, цель которой – установка и настройка периферийных устройств, регистров, портов, режимов энергопотребления МК под выполнение нашей конкретной программы (задачи). Помимо RESET, эта секция программы содержит еще два вектора:

;------------------------------------------------
;Вектора сброса и обработчиков прерываний:
;------------------------------------------------
.ORG $000
   rjmp RESET
 
.ORG $016
   rjmp USB_GENERAL     ;обработка общих событий
 
.ORG $018
   rjmp USB_ENDPOINT    ;обработка прерывания конечной точки

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

Стек. Первое, что нужно сделать сразу после сброса – организовать стек программы. В стек процессор записывает адреса возврата из подпрограмм и обработчиков прерываний. В указатель стека SP помещается адрес последней ячейки ОЗУ – константа RAMEND, определенная в файле usb162def.inc. Обнуляются регистры R1, R2, R25 и R26 для дальнейшего использования. Так как МК получает питание 5V от шины , то очисткой бита REGDIS регистра REGCR включается внутренний регулятор напряжения модуля USB 3.3V, чтобы обеспечить необходимый уровень на выводах D+ и D- интерфейса USB.

Примечание: в случае, когда схема питается стабилизированным напряжением 3.3V, внутренний регулятор может быть отключен установкой бита REGDIS. Это можно и не делать, поскольку бит REGDIS по умолчанию всегда сброшен, и его сброс здесь показан просто для примера.

Регистр REGCR относится к дополнительным регистрам ввода/вывода, которые занимают адресное пространство памяти данных, начиная с адреса 0х0060, до адреса 0х00FF. Однобайтные команды IN и OUT к таким регистрам не применимы, поскольку команды IN и OUT способны адресовать только первые 64 регистра из пространства ввода/вывода. Соответственно обращение к этим дополнительным регистрам производится как к ячейкам памяти, с помощью команд STS (непосредственная запись в память данных) и LDS (непосредственное чтение памяти данных). В нашей программе командой sts REGCR, R16 мы записываем значение R16 с предварительно сброшенным битом REGDIS в регистр REGCR (имя регистра REGCR, как и все мнемонические имена регистров и битов процессора, автоматически подставляются из подключаемого файла usb162def.inc).

WDT. Следующий блок команд выключает сторожевой таймер (WatchDog Timer, WDT). Этот код целиком взят из оригинального даташита на МК AT90USB162 стр.55 []. Сторожевой таймер в данной версии программы не используется. Будет задействован сторожевой таймер или нет – решает разработчик, однако в любом случае упомянутый блок команд обязательно должен быть включен в процедуру инициализации МК при запуске, поскольку взведенный по умолчанию таймер WDT, не будучи своевременно сброшенным, сам периодически сбрасывает МК, не давая ему приступить к обмену данными с хостом.

Примечание: включен по умолчанию таймер WDT после сброса, или нет, можно настроить фюзами МК AT90USB162.

GPIO (порты ввода/вывода). Далее производится настройка порта D, к выводам PD4..PD0 которого подключены пять индикаторных светодиодов (рис. 1). Разряды DDRD4..DDRD0 регистра DDRD конфигурируются как выходы в состоянии ноль (PORTD=0). Светодиоды были задействованы на этапе написания и отладки программы для индикации стадий обмена пакетами между устройством и хостом, состояния отдельных битов в управляющих регистрах модуля USB и т. д. Каждый из светодиодов разработчик может использовать по своему усмотрению для сигнализации о каких-нибудь событиях в программе. Если такой необходимости нет, то светодиоды можно не устанавливать, а эти четыре команды настройки порта исключить из программы.

AT90USB162 USB HID test LEDs

Рис. 1. Подключение индикационных светодиодов (для отладки программы).

Тактовый генератор. Для работы интерфейса USB требуется частота 48 МГц, которая вырабатывается генератором с ФАПЧ (PLL), см. схему 6-6 на стр. 37 даташита []. Эта частота получается с помощью деления и умножения частоты кварцевого резонатора, подключенного к МК. В нашем случае используется кварц на 16 МГц. Чтобы запустить генератор, необходимо обеспечить на его входе частоту 8 МГц и разрешить его работу. 8 МГц получаются делением тактовой частоты МК на 2. Для этого в разряды предделителя частоты PLLP2, PLLP1, PLLP0 регистра PLLCSR записывается значение 001b. Если у Вас используется кварцевый резонатор на 8 МГц, то делить частоту не нужно.

Работу генератора разрешаем установкой в единицу разряда PLLE того же регистра (команды ldi R16,(0 << PLLP2)+(0 << PLLP1)+(1 << PLLP0)+(1 << PLLE) и out PLLCSR, R16). Регистр PLLCSR имеет также разряд PLOCK, в котором мы ожидаем появление единицы (цикл bit_lock), свидетельствующей о захвате частоты в петле ФАПЧ. Захват частоты это событие, после которого начинается процесс устойчивой генерации.

USB. Затем идут стандартные процедуры включения модуля USB (команды lds R16, USBCON, sbr R16, 1 << USBE и sts USBCON, R16), разрешение тактирования (команды cbr R16, 1 << FRZCLK и sts USBCON, R16) и подключения (команды lds R16, UDCON, cbr R16, 1 << DETACH и sts UDCON, R16).

После обнаружения нового устройства на шине, перед началом обмена, хост производит сброс шины переводом линий D+ и D- в состояние однополярного нуля (Single-ended 0) на время не менее 10 мс. Чтобы идти дальше, нам необходимо отследить момент окончания этого события, для чего организован цикл r_bus, в котором непрерывно опрашивается состояние бита EORSTI регистра UDINT. Установка в единицу этого бита производится аппаратно по окончании сброса на шине и служит условием выхода из цикла. Подтверждаем выход из состояния сброса записью нуля в разряд EORSTI двумя последующими командами cbr R16, 1 << EORSTI и sts UDINT, R16.

Теперь можно приступать к конфигурации конечной точки 0 (EP0, управляющая конечная точка, control endpoint). Процедура конфигурации оформлена в виде подпрограммы (её вызов осуществляется командой rcall conf_ep_0), и построена согласно блок-схемы алгоритма, взятого из даташита [] (показан на рис. 2).

AT90USB162 Endpoint Setup fig02

Рис. 2. Блок-схема алгоритма настройки конечной точки.

Конфигурирование конечных точек выглядит довольно просто: командами пересылки записываются нужные значения в определенные регистры конечной точки, и в определенном порядке. Первая команда sts UENUM, R1 выбирает конечную точку 0 записью нуля (R1=0) в регистр номера конечной точки UENUM. Номер выбранной конечной точки в регистре UENUM определяется тремя младшими его разрядами - EPNUM0, EPNUM1, EPMUM2 (cм. таблицу 1).

Таблица 1. Настройка конечных точек (EP0..EP4 это конечные точки, EPNUMx это значения разрядов регистра UENUM).

  EPNUM0 EPNUM1 EPNUM2
EP0 0 0 0
EP1 0 0 1
EP2 0 1 0
EP3 0 1 1
EP4 1 0 0

Следующие четыре команды проверяют бит активации EPEN регистра UECONX для конечной точки 0, который должен быть установлен в единицу. Если бит EPEN не установлен, то он устанавливается. Командой ldi R16,(0 << EPTYPE1)+(0 << EPTYPE0)+(0 << EPDIR) в регистре UECFG0X задатся тип и направление для EP0. Разряды EPTYPE0, EPTYPE1 определяют тип, а разряд EPDIR направление конфигурируемой конечной точки, и для EP0 они должны быть равны нулю. Далее настраивается регистр UECFG1X. Три его разряда - EPSIZE0, EPSIZE1 и EPSIZE2 – определяют размер конечной точки. Два разряда - EPBK0 и EPBK1 - задают количество банков памяти для этой точки, в соответствии с ее размером. Разряд ALLOC - выделяет ей память в пространстве DPRAM. Банк конечной точки это определенное количество ячеек в DPRAM, предназначенных для буферизации входных и выходных данных. После выполнения команд ldi R16, 0x22 и sts UECFG1X, R16, разряды EPSIZE0, EPSIZE1 и EPSIZE2 принимают соответственно значения - 0b010, определяя размер EP0 - 32 байта, разряды EPBK0 и EPBK1 принимают значения 0b00, определяя количество банков EP0 - 1 банк, разряд ALLOC принимает значение 0b1 - выделяя ей память в DPRAM.

Наконец, завершающим этапом конфигурирования конечной точки является проверка бита CFGOK в регистре UESTA0X. Установка этого бита производится аппаратно при условии, что в регистре UECFG1X задано корректное (по отношению к размеру выделенной буферной памяти) значение размера конечной точки и соответствующее ему количество банков памяти. Состояние разряда CFGOK проверяется в цикле test_c_ep0. При установке его в единицу конечная точка 0 считается сконфигурированной, и производится выход из цикла и из подпрограммы.

Из Википедии: двухпортовая память это такой тип памяти с произвольным доступом к ячейкам (random-access memory, RAM), которая позволяет производить одновременно 2 операции (два чтения, две записи, или одно чтение и одна запись) по разным адресам своего массива памяти. Эти две операции происходят одновременно (или почти одновременно), в отличие от однопортовой памяти RAM, у которой в любой момент времени может происходить только одна операция с массивом памяти.

Video RAM, или VRAM это общая форма двухпортовой динамической RAM, наиболее часть используемой для видеопамяти. Такая память позволяет процессору рисовать картинку в памяти, и одновременно читать память для вывода изображения на экран.

Кроме VRAM, большинство других типов двухпортовой RAM основывается на статической технологии RAM.

Большинство современных процессоров реализуют свои регистры как маленькую двухпортовую (или многопортовую) RAM.

По возвращению на стадию инициализации в разряд RSTCPU регистра UDCON запитсывается 1 (команды lds R16, UDCON, sbr R16, 1 << RSTCPU и sts UDCON, R16), разрешая сброс ядра МК при возникновении сброса на шине. В ходе отладки программы выяснилось, что если этого не сделать, то устройство USB, оставленное присоединенным к компьютеру при перезагрузке операционной системы, не будет опознано. Далее разрешается обработка прерывания по событиям приостановки и сброса, для чего записываются единицы в соответствующие разряды регистра UDIEN (команды ldi R24, (1 << SUSPE)+(1 << EORSTE) и sts UDIEN, R24). Аналогично, установкой в единицу разряда RXSTPE регистра UEIENX, разрешается прерывание по приходу пакета SETUP от хоста (команды ldi R24, 1 << RXSTPE и sts UEIENX, R24).

Поскольку работа программы организована по прерываниям и делать больше ничего не требуется, то имеет смысл переводить МК в режим пониженного энергопотребления - всякий раз при выходе из обработчика прерывания. Перевод в режим Idle разрешается записью единицы в разряд SE регистра SMCR (командами ldi R16, 1 << SE и out SMCR, R16). Теперь на выходе из подпрограммы обработки прерывания процессор встречает в основном цикле программы команду sleep, и переходит в режим пониженного энергопотребления (Idle) - до появления следующего запроса на прерывание. В режиме Idle любой запрос на прерывание пробуждает МК для выполнения своей подпрограммы обработки прерывания.

Завершается стадия инициализации командой sei, которая глобально разрешает прерывания. После инициализации процессор начинает выполнять тело бесконечного цикла, который содержит только две команды - sleep и rjmp SLP.

Теперь рассмотрим код обработчиков прерываний USB_GENERAL и USB_ENDPOINT.

USB_GENERAL. Эта подпрограмма является общим обработчиком прерываний модуля USB, и призвана обрабатывать события, возникающие на шине. Каждое событие на шине характеризуется определенным ее состоянием. Модуль USB различает шесть состояний на шине, по которым делается вывод о наступлении того или иного события. Например, отсутствие в течение 3 мс пакетов SOF (Start Of Frame, начало фрейма) свидетельствует о переводе шины хостом в неактивное состояние (т. е. обмен данными прекращается, и устройство в целях энергосбережения должно перейти в режим сна), это так называемое событие приостановки. Желая возобновить работу, хост устанавливает между линиями D+ и D- уровень напряжения -3.0V (дифференциальный ноль) на время не менее 20 мс, с последующим сигналом конца пакета EOP (End Of Packet) - событие возобновления работы, и т. д.

Для идентификации возникшего события и формирования запроса на прерывание предназначен регистр UDINT. В этом регистре каждому внешнему событию ставится в соответствие определенный разряд, который принимает единичное значение при наступлении определенного события. В нашей программе обработчик USB_GENERAL вызывается при возникновении одного из трех событий - окончание сброса (END_OF_RESET), приостановка (SUSPEND), пробуждение (WAKE_UP). Работа обработчика сводится к тому, чтобы по наличию единицы в проверяемом разряде регистра UDINT определить, какое событие произошло, и выполнить соответствующие событию действия.

Обработка сброса по шине USB. Начинается подпрограмма с выявления события окончания сброса на шине. Для этого командами lds R24, UDINT и sbrs R24, 3 проверяется разряд EORSTI, единица в котором свидетельствует о наступлении определенного события. После этого очищается разряд EORSTI, и происходит выход из подпрограммы (rjmp vihod_ug). Если же EORSTI равен нулю, то сброса на шине не было, и обработчик USB_GENERAL был вызван другим событием.

Обработка события приостановки. Продолжается проверка других условий возникновения прерывания - происходит переход на метку next_ev_1, где командой sbrs R24, 0 проверяется наличие единицы в разряде SUSPI, тем самым определяется следующее возможное событие - приостановка. Хост инициирует состояние приостановку всякий раз, когда компьютер (с присоединенным к нему нашим устройством) переходит в энергосберегающий режим (PowerDown). Если событие приостановки произошло (SUSPI = 1), то необходимо выполнить последовательность операций по переводу модуля USB в режим SUSPEND, пользуясь методикой, описанной в даташите [] (пункт 19.10, стр. 195). Сначала очищается бит приостановки командами cbr R24, 1 << SUSPI и sts UDINT, R24. Затем останавливается тактирование периферийного устройства USB командами lds R24, USBCON, sbr R24, 1 << FRZCLK, sts USBCON, R24, и запрещается работа внутреннего генератора блока USB с системой ФАПЧ (PLL) командами in R16,PLLCSR, cbr R16, 1 << PLLE и out PLLCSR, R16. Для того, чтобы возобновить работу устройства при выходе компьютера из состояния "Выключено" (или режима сна), разрешается прерывание по событию пробуждения (команды lds R24, UDIEN, sbr R24, 1 << WAKEUPE и sts UDIEN, R24). Обратие внимание, что прерывание по событию WAKE_UP (пробуждение) разрешается, только если устройство USB находится в состоянии SUSPEND (приостановка). После этого происходит выход из обработчика прерывания в основной цикл (rjmp vihod_ug), который переводит устройство в режим сна. Теперь наше устройство остановлено и пребывает в режиме с низким энергопотреблением (sleep), ожидая событие пробуждения (WAKE_UP). Хост инициирует состояние пробуждения на шине всегда, когда компьютер переходит из энергосберегающего режима (Power Down) в рабочий.

Обработка события пробуждения. Предположим, что устройство USB находится в состоянии SUSPEND. Тогда, обнаружив на шине состояние WAKE_UP, модуль USB генерирует запрос на прерывание. В этом случае проверка разрядов регистра UDINT приведет к переходу по метке next_ev_2, где будет найдера единица в разряде WAKEUPI (команда sbrs R24, 4), т. е. имеет место событие пробуждения с последующим возобновлением работы. Для пробуждения выполняется ряд действий, так же определенных в даташите [] (пункт 19.10, стр. 195). Разрешается работу генератора с системой ФАПЧ (PLL) - команды in R16, PLLCSR, sbr R16, 1 << PLLE, out PLLCSR, R16. Ожидается захват в петле ФАПЧ в цикле lock_bit. Разрешается тактирование модуля USB командами cbr R24, 1 << FRZCLK и sts USBCON, R24. Сбрасывается разряд WAKEUPI, вызвавший прерывание - команды lds R24, UDINT, cbr R24, 1 << WAKEUPI, sts UDINT, R24. Запрещается прерывание по событию пробуждения - команды lds R24, UDIEN, cbr R24, 1 << WAKEUPE и sts UDIEN, R24. Выбирается (активируется) нулевая конечная точка командой sts UENUM, R1. Это делается в связи с тем, что до входа в состояние приостановки (SUSPEND) весь обмен с устройством происходил через конечные точки EP1 и EP2, а конечная точка EP0 была деактивирована. Возобновив работу устройства USB по событию пробуждения (WAKE_UP), хост будет пытаться организовать канал через конечную точку EP0, чтобы отправить новый запрос. Если EP0 не будет выбрана, то хост не сможет получить к ней доступ, что приводит к ошибке и зависанию ПО хоста. Запрос, который посылает хост сразу после пробуждения, называется SET_IDLE (он будет описан далее при рассмотрении обработчика прерывания USB_ENDPOINT). Завершается процедура пробуждения записью единицы в регистр R2 командой inc R2. Эта единица выполняет роль флага, и будет анализироваться впоследствии, при обработке запроса SET_IDLE. Давайте перейдем к рассмотрению к самой основной части программы, определяющей функционирование всего устройства USB - обработчику прерывания USB_ENDPOINT.

USB_ENDPOINT. ЭНУМЕРАЦИЯ. Обмен информацией между хостом и устройством USB осуществляется через конечные точки. Конечная точка, готовая к обмену, генерирует запрос на прерывание, вызывая тем самым обработчик USB_ENDPOINT, в котором и происходит обмен данными. Прежде чем приступить к обработке запроса, нужно вначале узнать, от какой конечной точки он поступил. Для этой цели служит регистр UEINT, где котором в порядке возрастания определены биты прерываний конечных точек. Если запрос вызван конечной точкой EP0, то будет установлен нулевой бит этого регистра, если конечной точкой EP1 - первый, и т. д.

Если прерывание инициировано конечной точкой EP0 (две первые команды - lds R24, UEINT и cpi R24, 1), то происходит передача управления ep_0. В нашей программе ЕР0 может генерировать запрос в одном единственном случае - принят пакет SETUP от хоста (разряд RXSTPI регистра UEINTX равен единице), и содержимое этого пакета уже находится в ее буфере (банке).

Любые запросы от хоста выполняются при помощи контрольных транзакций (подробнее можно почитать в [1], это великолепная серия статей, посвященных программированию USB устройств и опубликованных в журнале "Современная электроника" с октября 2004 г.). Транзакция представляет из себя сеанс обмена данными, и в зависимости от запроса она может содержать две или три фазы:

1. Фаза SETUP, в которой мы принимаем пакет SETUP, содержащий запрос к устройству. Эта фаза обязательная.
2. Фаза DATA (данные), в которой мы отправляем, то, что требует хост. Эта фаза не обязательная.
3. Фаза STATUS, которая нам говорит, что хост закончил принимать данные (фаза DATA закончена) и ожидает подтверждения завершения транзакции. Эта фаза обязательная.

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

Обработка фазы SETUP. Первой фазой в любой контрольной транзакции идет фаза SETUP, а упомянутый пакет SETUP не является отдельной информационной посылкой, и передается строго в этой фазе, как пакет данных, следующий за пакетом запроса с маркером SETUP. Стандартно пакет SETUP состоит из 8 байт, содержащих запрос к устройству и предваряется маркером DATA0. В литературе пакет SETUP еще называют конфигурационным пакетом.

При получении пакета можно приступать к его расшифровке. В пакете SETUP имеется следующая последовательность из 8 байтов:

0x80 0x06 0x00 0x01 0x00 0x00 0x40 0x00

где

0x80 - поле bmRequestType - стандартный запрос к устройству.
0x06 - поле bRequest - номер запроса - GET_DESCRIPTOR.
0x00 0x01 - поле wValue - тип дескриптора, старший (второй) байт - DEVICE_DESCRIPTOR.
0x00 0x00 - поле wIndex - зависит от запроса - не используется.
0x40 0x00 - поле wLength - количество байт, требуемое хостом в ответном пакете - 64 байта.

Первые два байта конфигурационного пакета выбираются из буфера через регистр UEDATX двумя командами lds R17, UEDATX и lds R16, UEDATX. Таким образом, значение поля bRequest теперь находится в регистре R16, а значение поля bmRequestType - в регистре R17. Первым всегда проверяется равенство R16 числу 0х06 (cpi R16, 0x06), что соответствует запросу GET_DESCRIPTOR. Если равенство установлено, то происходит переход на метку get_dsc, где проверяется уже поле bmRequestType (cpi R17, 0x80). Значение 0x80 в регистре R17 означает переход на метку stand_desc. В метке stand_desc запускается подпрограмма read_buff, в которой считываются из буфера остальные 6 байт конфигурационного пакета в регистры R18 - R23 (lds R18, UEDATX - lds R23, UEDATX). Командами lds R24, UEINTX, cbr R24, 1 << RXSTPI и sts UEINTX, R24 сбрасывается разряд RXSTPI регистра UEINTX, подтверждая прием пакета SETUP (ACK), и очищая буфер ЕР0.

На выходе из подпрограммы read_buff, проверяется значение регистра R19 (cpi R19, 1), содержащего теперь тип дескриптора. Если значение оказывается равным 0x01, тогда нужно обработать дескриптор устройства (DEVICE_DESCRIPTOR), и происходит переход на метку d_dev. Значение младшего байта поля wLength (и соответственно регистра R22) в первом, полученном нами конфигурационном пакете, равно 0x40 или 64 в десятичной системе. Таким образом, хост требует от нас дескриптор устройства размерностью 64 байта.

Обработка фазы DATA. Конфигурационный пакет успешно принят и расшифрован. Наступает черед фазы DATA (данных). В метке d_dev проверяется состояние флага TXINI регистра UEINTX (команды lds R24, UEINTX и sbrs R24, 0). Его единичное состояние говорит нам о том, что был принят пакет IN, и передатчик модуля USB готов принять данные для отправки. Эти данные, представленные в виде массивов констант, и есть дескрипторы нашего устройства. Массивы записаны в память программ, начиная с адреса $300, после программного кода (директива .ORG $300), и извлекаются от туда один за другим, по мере поступления запросов от хоста.

Примечание: директива без .ORG с адресом $300 использовалась не случайно. Без этой директивый константы окажутся сразу за кодом. Однако в моем варианте программы директива нужна, т. к. количество отправленных байт в метке d_dev проверяется в регистре R30. И этот регистр, содержащий младший байт адреса константы, должен иметь начальное нулевое значение. Без .ORG $300 (можно использовать и другое значение адреса, $400, $500 и т. д.) нулевое значение R30 не гарантируется.

Первый массив (метка dev_desc) является дескриптором нашего устройства, и он имеет размер 18 байт (стандартное значение). Разберем по байтам, что находится в дескрипторе устройства.

0x12 - (bLength) - размер дескриптора в байтах - 18 байт;
0х01 - (bDescriptorType) - тип дескриптора - DEVICE_DESCRIPTOR;
0х00, 0х02 - (bcdUSB) - номер спецификации USB, которой удовлетворяет устройство, в двоично-десятичном формате – USB 2.0 на скорости Full Speed;
0х00 - (bDeviceClass) - класс прибора - не указывается;
0х00 - (bDeviceSubClass) - подкласс прибора - не указывается;
0х00 - (bDeviceProtocol) - протокол прибора - не указывается;
0х20 - (bMaxPacketSize0) - размер пакета для конечной точки 0 - 32 байта;
0xEB, 0x03 - (idVendor) - идентификатор производителя устройства (VID) - Atmel Corp.;
0x13, 0x20 - (idProduct) - идентификатор продукта (PID) - стандартное HID-устройство;
0x00, 0x10 - (bcdDevice) - номер версии устройства - взят из демо-программы Atmel;
0х01 - (iManufacturer) - индекс текстовой строки производителя (строковые дескрипторы рассмотрим позднее);
0х02 - (iProduct) - индекс текстовой строки изделия;
0х03 - (iSerialNumber) - индекс текстовой строки серийного номера;
0х01 - (bNumConfigurations) - количество возможных конфигураций - одна конфигурация.

Для отправки дескриптора устройства в индексный регистр Z загружается адрес первого элемента нашего дескриптора (команды ldi R31, high(dev_desc*2) и ldi R30, low(dev_desc*2)). Командой lpm R24,Z+ пересылается значение, находящееся по этому адресу (число 0x12) в регистр R24, и происходит переход на следующий адрес с инкрементом регистра Z (косвенная адресация с постинкрементом). Затем значение из регистра R24 помещается в регистр данных UEDATX модуля USB для последующей записи в буферную память ЕР0. Этот цикл (out_data_d) повторяется 18 раз, пока не будут записаны в буфер все 18 байт дескриптора (cpi R30, 18). После этого программно сбрасывается разряд TXINI (команды lds R24, UEINTX, cbr R24, 1 << TXINI и sts UEINTX, R24) для отправки данных хосту, и происходит переход на подпрограмму w_nakout (rcall w_nakout).

Обработка фазы STATUS. Наступает очередь фазы STATUS, и нам нужно определить момент ее наступления и обработать. Это и является задачей подпрограммы w_nakout. Обратимся к временной диаграмме из даташита [] (рис. 3):

AT90USB162 data communication fig03

Рис. 3. Диаграмма последовательности обмена данными.

Здесь видно, что при переходе от фазы DATA к фазе STATUS хост меняет направление передачи данных - пакеты IN меняются на пакеты OUT. Именно это обстоятельство используется в подпрограмме w_nakout, чтобы отследить окончание фазы данных. Кстати аналогичным образом происходит переход DATA -> STATUS и в контрольной транзакции с фазой записи данных - пакеты OUT меняются на IN (смотрите соответствующую временную диаграмму в даташите []).

После того, как хост принял от нас необходимое количество данных, он инициирует фазу STATUS, посылая нам первый пакет OUT (и пакет нулевой длины - согласно спецификации), на который контроллер всегда отвечает пакетом NAK (показано на диаграмме). Одновременно в регистре UEINTX аппаратно устанавливается флаг NAKOUTI. Факт установки этого флага отслеживается в первых трех строчках подпрограммы w_nakout. Установленный флаг NAKOUTI сбрасывается (cbr R24, 1 << NAKOUTI, sts UEINTX, R24) для приема следующего пакета. Хост в свою очередь, получив от нас пакет NAK, повторяет запрос, посылая второй пакет OUT.

Приход второго пакета мы ожидается в цикле w_out проверкой значения разряда RXOUTI регистра UEINTX (команды lds R24, UEINTX, sbrs R24, 2) до появления в нем единицы. Установка этого разряда в единицу свидетельствует о получении модулем USB пакета OUT. Когда второй пакет получен, командами cbr R24, 1 << RXOUTI и sts UEINTX, R24 очищается разряд RXOUTI, генерируя маркер подтверждения ACK. Транзакция завершена. Далее осуществляется выход из подпрограммы (ret) и выход из обработчика прерывания (rjmp vihod_ep) в основной цикл сна программы.

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

Вторым запросом при энумерации, после получения урезанного дескриптора устройства, является назначение адреса устройству на шине. В данном случае хост инициирует контрольную транзакцию с двумя фазами. Первая фаза - SETUP - отправка конфигурационного пакета с запросом к устройству USB. Вторая фаза - STATUS - подтверждение. Конфигурационный пакет теперь представляет собой следующую последовательность байтов:

0х00 0х05 0хXX 0х00 0х00 0х00 0х00 0х00

где

0x05 (второй байт) - поле bRequest - номер запроса - SET_ADDRESS.
0xXX (третий байт) - младший байт поля wValue - адрес, назначенный устройству. Может иметь любое значение от 1 до 127.

Остальные байты имеют нулевые значения и не рассматриваются.

Получив пакет SETUP и имея в своем буфере его содержимое, конечная точка EP0 генерирует запрос на прерывание. Это выводит МК из режима сна, и запускает обработчик прерывания по метке USB_ENDPOINT. Здесь проверяется, от какой конечной точки поступил запрос (lds R24, UEINT, cpi R24, 1), и происходит переход на метку ep_0. Через регистр UEDATX в регистры R17, R16 загружаются первые два байта конфигурационного пакета из буфера ЕР0. Первым проверяется значение R16. Если оно не равно 0х06, то происходит переход на метку next_0. Значение 0х05 показывает запрос SET_ADDRESS, при этом произойдет переход на метку set_adr. Новое значение адреса на шине идет следующим (третьим) байтом в конфигурационном пакете. Оно считывается из буфера командой lds R24, UEDATX и переписывается в регистр USB-адреса устройства UDADDR командой sts UDADDR, R24. Сбрасывается разряд RXSTPI регистра UEINTX, подтверждая прием пакета SETUP (команды lds R24, UEINTX, cbr R24, 1 << RXSTPI и sts UEINTX, R24). Получив подтверждение (ACK), хост сразу переходит на фазу STATUS (фаза данных отсутствует), отправляя пакет IN пока еще по нулевому адресу.

В описании сказано, что контроллер не подтверждает прием первого IN-пакета фазы STATUS, посылая маркер NAK. Однако в ходе написания программы опытным путем выяснилось, что по факту этого не происходит - флаг NAKINI остается в нулевом состоянии. Остается только убедиться, что разряд TXINI установлен (lds R24, UEINTX, sbrs R24, 0) и затем сразу сбросить его (cbr R24, 1 << TXINI). Этим действием посылается хосту пакет нулевой длины (так называемый ZLP - Zero Length Packet). Он отличается отсутствием данных - между приемом IN-пакета и сбросом TXINI отсутствует стадия записи данных в буфер конечной точки для дальнейшей отправки (какой мы ее видим например, при передаче дескриптора). Согласно спецификации USB отправка ZLP в ответ на пакет IN, является обязательным условием выполнения запроса SET_ADDRESS. Этим мы как бы сообщаем хосту, что запрос выполнен, и готовы к дальнейшему обмену уже по новому адресу. Затем после проверки (в метке vt) аппаратной установки разряда TXINI регистра UEINTX, отдельно командами lds R24, UDADDR, ori R24, 0x80 и sts UDADDR, R24, записывается единица в разряд ADDEN регистра UDADDR - для активации нового адреса. На этом работа подпрограммы закончена. Происходит выход из обработчика прерывания (rjmp vihod_ep) до появления нового пакета SETUP. Теперь наше устройство считается адресованным, и все последующие запросы будут приходить к нему уже по назначенному хостом новому адресу. Адрес содержится, как мы знаем, в пакете запроса с маркером SETUP.

Отправка декскриптора устройства. Новый, третий по счету пакет SETUP, приходит с запросом GET_DESCRIPTOR, как уже упоминалось ранее:

0x80 0x06 0x00 0x01 0x00 0x00 0x12 0x00

где

0x80 - поле bmRequestType - стандартный запрос, адресованное устройству.
0x06 - поле bRequest - номер запроса - GET_DESCRIPTOR.
0x00 0x01 - поле wValue - тип дескриптора (старший байт) - DEVICE_DESCRIPTOR.
0x00 0x00 - поле wIndex - зависит от запроса - не используется.
0x12 0x00 - поле wLength - количество байт, требуемое хостом в ответном пакете - 18 байт.

При рассматрении полученного конфигурационного пакета оказывается, что он имеет в поле wLength (младший байт) новое значение длины дескриптора - 18 байт (0х12). Это длина дескриптора нашего устройства, которую мы отправили при первом его запросе. Механизм обработки здесь в точности повторяет обработку первого запроса, т. е. здесь мы вновь проходим все рассмотренные ранее стадии:

[СПИСОК №1, обработка контрольных транзакций]
- получение пакета SETUP;
- вызов обработчика USB_ENDPOINT
- чтение запроса (конфигурационного пакета) и его анализ;
- отправка дескриптора;
- подтверждение завершения транзакции;
- выход из обработчика.

Вообще все контрольные транзакции, которые требуют отправки дескриптора (а таковых большинство), всегда имеют три фазы, и обрабатываются строго в соответствие с этим списком. Запомним этот список, мы часто будем упоминать его далее по тексту, под названием - СПИСОК №1. Что касается транзакции с запросом SET_ADDRESS, то ее мы уже рассмотрели. Транзакцию с запросом SET_CONFIGURATION мы еще рассмотрим.

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

Отправка дескриптора конфигурации. После получения полноценного дескриптора устройства хост запросит дескриптор конфигурации, и для этого сформирует новый запрос - GET_CONFIGURATION. Полученный конфигурационный пакет теперь выглядит так:

0x80 0x06 0x00 0x02 0x00 0x00 0xFF 0x00

где

0x80 - поле bmRequestType - стандартный запрос, адресованный устройству.
0x06 - поле bRequest - номер запроса - GET_DESCRIPTOR.
0x00 0x02 - поле wValue - тип дескриптора (старший байт) - CONFIGURATION_DESCRIPTOR.
0x00 0x00 - поле wIndex - зависит от запроса - не используется.
0xFF 0x00 - поле wLength - количество байт, требуемое хостом в ответном пакете - взято условно-максимальным.

До вызова подпрограммы чтения запроса - read_buff по метке stand_desc все идет по отработанной схеме, согласно СПИСКУ №1. После прочтения запроса выясняется, что содержимое регистра R19 равно двум (0х02). Значения регистров R20 - R23 можно уже не проверять. Происходит переход Идем на метку d_con для выполнения запроса GET_CONFIGURATION_DESCRIPTOR.

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

Первым идет дескриптор конфигурации длиной 9 байт:

0х09 - длина дескриптора конфигурации - 9 байт;
0х02 - тип дескриптора - CONFIGURATION_DESCRIPTOR;
0х29, 0х00 - общая длина дескриптора конфигурации – суммарная длина всех пяти дескрипторов, входящих в его состав - 41 байт;
0х01 - количество интерфейсов в конфигурации - 1;
0х01 - номер нашей единственной конфигурации - 1;
0х00 - индекс строки, описывающей нашу конфигурацию. Не используется;
0х80 - устройство получает питание от шины;
0х32 - и потребляет ток 100 mA.

Дальше идет дескриптор интерфейса, длиной 9 байт:

0х09 - длина дескриптора интерфейса - 9 байт;
0х04 - тип дескриптора - INTERFACE_DESCRIPTOR;
0х00 - используем единственный интерфейс с номером 0;
0х00 - используем единственную альтернативную установку с номером 0. Понятие "альтернативная установка" - описано в [1];
0х02 - количество используемых конечных точек, исключая EP0 - 2 точки;
0х03 - класс интерфейса - 3 для HID-устройств;
0х00 - подкласс интерфейса - 0;
0х00 - протокол интерфейса - 0;
0х00 - индекс строки, описывающей интерфейс (отсутствует).

Дальше идет HID-дескриптор, длиной 9 байт:

0х09 - длина HID-дескриптора - 9 байт;
0х21 - тип дескриптора - HID_DESCRIPTOR;
0х00, 0х01 - номер версии HID - стандартное значение - 0х0100;
0х00 - код страны, для локализации устройства. Не локализовано -0;
0х01 - количество дескрипторов репорта нашего устройства - 1;
0х22 - тип дескриптора - REPORT_DESCRIPTOR;
0х22, 0х00 - длина дескриптора репорта (младший байт) - 34 байта.

Дальше идет дескриптор конечной точки 1, длиной 7 байт:

0х07 - длина дескриптора EP1 - 7 байт;
0х05 - тип дескриптора ENDPOINT_DESCRIPTOR;
0х81 - адрес конечной точки и ее направление - IN;
0х03 - тип передачи - Interrupt (по прерываниям);
0х08, 0х00 - максимальный размер пакета для EP1 (младший байт) - 8 байт;
0х0F - частота опроса хостом конечной точки 1 на предмет готовности данных - 16 мс.

Последним идет дескриптор конечной точки 2, длиной 7 байт:

0х07 - длина дескриптора EP2 - 7 байт;
0х05 - тип дескриптора ENDPOINT_DESCRIPTOR;
0х02 - адрес конечной точки и ее направление - OUT;
0х03 - тип передачи - Interrupt (по прерываниям);
0х08, 0х00 - максимальный размер пакета для EP2 (младший байт) - 8 байт;
0х0F - частота опроса хостом конечной точки 2 на предмет готовности данных - 16 мс..

Теперь по имеющимся данным можно изобразить структуру нашего устройства - рис. 4 (см. [1]):

AT90USB162 logic structure fig04

Рис. 4. Логическая структура устройства USB.

Метка d_con: после контрольной проверки разряда TXINI на наличие единицы (получен пакет IN), командами lds R24, UEINTX и sbrs R24, 0, адрес первого байта дескриптора конфигурации загружается в индексный регистр Z (команды ldi R31, high(con_desc*2) и ldi R30, low(con_desc*2)). Далее профилактически очищается регистр R23 (clr R23 - необязательная команда) и проверяется значение R22. Оно после прочтения конфигурационного пакета равно не девяти, а 0хFF, поэтому происходит переход на метку out_dat_c.

Здесь все работает так же, как при отправке дескриптора устройства. Однако есть особенность – длина дескриптора конфигурации - 41 байт, но размер ЕР0 всего 32 байта. Поэтому мы не можем передать дескриптор одним пакетом, придется его передавать двумя кусками. Сначала последовательно загружаются первые 32 байта нашего дескриптора в буфер для отправки (цикл out_dat_c). После чего первый блок данных дескриптора отправляется хосту, для чего сбрасывается разряд TXINI после выхода из цикла. Получив порцию информации и зная, что это не все (хосту уже известна длина дескриптора), хост посылает второй пакет IN, чтобы получить оставшуюся часть. Момент прихода пакета IN мы отслеживается по аппаратной установке в единицу все того же разряда TXINI, циклическим опросом по метке d_con_1. Затем аналогичным образом загружается в буфер EP0 оставшиеся 9 байт дескриптора (цикл out_dat_c1). Обратите внимание, что счетчик - регистр R23 - перед входом в цикл out_dat_c1, содержит число 32 - количество уже отправленных байт. Для отправки загруженных данных снова сбрасывается разряд TXINI после выхода из цикла out_dat_c1. Дальше последует фаза STATUS, поэтому происходит переход на уже знакомую подпрограмму w_nakout (rcall w_nakout). Далее происходит выход из обработчика прерывания (rjmp vihod_ep). Все просто, не правда ли? А теперь посмотрите на тот же фрагмент кода, написанного на Си в демонстрационной программе STK526-series2-hidio-2_0_2-doc. Есть разница?

Примечание редактора: мне не кажется все это таким уж простым. ИМХО, применять готовые библиотеки и примеры кода на C гораздо проще.

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

При рассмотрении дескриптора устройства мы отложили описание строковых дескрипторов. Теперь попробуем с ними разобраться. Строковые дескрипторы содержат необязательную текстовую (цифро-буквенную) информацию, предназначенную для восприятия ее человеком - пользователем данного устройства. Однако, чтобы наше устройство появилось в Диспетчере Устройств Windows не как безликое USB-устройство ввода, а устройство с заданным названием, информацией о производителе и другой информацией, недостаточно прописать дескрипторы строк. Для этого еще придется потрудиться над INF-файлом, что здесь не рассматривается.

Строковые дескрипторы не являются обязательными, и если мы не хотим иметь с ними дело, то поля iManufacturer, iProduct, iSerialNumber дескриптора устройства должны содержать нулевые значения. В этом случае хост просто не будет посылать запрос на получение дескрипторов строк. Наше изделие имеет три строковых дескриптора, и три вышеупомянутых поля дескриптора устройства содержат соответствующие им индексы. Еще раз их запишем:

iManufacturer - 0х01 - индекс текстовой строки производителя,
iProduct - 0х02 - индекс текстовой строки изделия,
iSerialNumber - 0х03 - индекс текстовой строки серийного номера,

При запросе дескрипторов строк, хост вставляет индекс в пакет SETUP в качестве параметра, по которому устройство USB распознает, какой из дескрипторов требуется хосту. Для простоты в дальнейшем, дескриптор строки будем просто называть строкой.

Если в дескрипторе устройства имеется индекс строки серийного номера (iSerialNumber), то хост запрашивает эту строку первой, формируя пакет SETUP следующего содержания:

0x80 0x06 0x03 0x03 0x00 0x00 0xFF 0x00

где

0х03 - младший байт поля wValue (третий слева) - индекс дескриптора запрашиваемой строки - строка с индексом три;
0х03 - старший байт поля wValue (четвертый слева) - тип дескриптора - STRING_DESCRIPTOR;
0xFF - младший байт поля wLength (предпоследний) - количество байт, требуемое хостом в ответном пакете. Условно-максимальное.

Примечание: строка серийного номера часто используется операционной системой Windows для создания уникального идентификатора устройства USB и запоминания этого идентификатора в реестре. Благодаря наличию этого идентификатора операционная система распознает устройство при повторном подключении, и может присвоить ему одно и то же имя. Например, с помощью серийного номера устройству виртуального COM-порта назначается всегда один и тот же номер (COM5, COM6 и т. д.), независимо от того, в какой порт USB устройство было подключено.

Обработка запроса выполняется согласно СПИСКУ №1. Первые два пункта выполняются до метки stand_desc. Здесь после прочтения конфигурационного пакета (rcall read_buff) выясняется, что регистр R19 содержит значение 0х03 (старший байт поля wValue). Далее происходит переходим на метку d_str, где последовательно проверяется значение регистра R18, который содержит теперь (после rcall read_buff) тот самый индекс строки, которую затребовал хост. На метке s_3 в регистре R18 обнаруживается число 0х03, что и является индексом текстовой строки серийного номера (iSerialNumber).

Строка с серийным номером расположена в памяти программ на метке str_3:

0x0A - (bLength) - длина текстовой строки серийного номера - 10 байт;
0х03 - старший байт поля wValue - тип дескриптора - STRING_DESCRIPTOR;
0x30, 0x00 - цифра 0 в формате UNICODE (0х0030),
0x30, 0x00 - цифра 0 в формате UNICODE (0х0030),
0x30, 0x00 - цифра 0 в формате UNICODE (0х0030),
0x30, 0x00 - цифра 0 в формате UNICODE (0х0030), т. е. имеем нулевой серийный номер.

Адрес первого элемента строки загружается в Z-регистр (команды ldi R31, high(str_3*2) и ldi R30, low(str_3*2)), в счетчик байт (R20) загружается длина строки - 10 байт (ldi R20, 10) и происходит переход на метку d_str_1 для отправки данных хосту. После стандартной проверки разряда TXINI происходит переход на метку out_st, где последовательно, байт за байтом, строка дескриптора копируется в буфер ЕР0, пока не обнулится значение счетчика R20. Десять скопированных байт отправляются хосту как обычно, сбрасом TXINI (команды lds R24, UEINTX, cbr R24, 1 << TXINI, sts UEINTX, R24). Затем опять проверяется значение R18, и так как оно не равно двум, происходит переход к ожиданию фазы SETUP для завершения транзакции (rcall w_nakout, rjmp vihod_ep). Почему именно двум? Потому что вторая строка - строка с индексом два (iProduct) имеет длину 34 байта, что превышает размер ЕР0 - 32 байта. И эту строку, как в случае с дескриптором конфигурации, не получится отправить за один раз, что будет рассмотрено дальше. А пока, отправив строку с индексом три, происходит возврат в основной цикл сна программы с ожиданием нового пакета SETUP.

Кодировка строк дескрипторов. Прежде чем запрашивать остальные строки, хост должен знать, на каком языке они написаны. Для этого он вначале посылает запрос на получение строки с индексом ноль - специального дескриптора, который содержит идентификатор языка (согласно принятого стандарта). Его присутствие обязательно, если устройство имеет хотя бы один строковый дескриптор - iManufacturer либо iProduct.

Хост отправляет пакет SETUP такого содержания:

0x80 0x06 0x00 0x03 0x00 0x00 0xFF 0x00

где

0х00 - младший байт поля wValue (третий слева) - индекс дескриптора запрашиваемой строки - строка с индексом ноль;
0х03 - старший байт поля wValue (четвертый слева) - тип дескриптора - STRING_DESCRIPTOR;
0xFF - младший байт поля wLength (седьмой слева) - количество байт, требуемое хостом в ответном пакете. Условно-максимальное.

Как обычно, выполняются первые два пункта СПИСКА №1, до метки stand_desc. После прочтения конфигурационного пакета (rcall read_buff), по проверке содержимомого регистра R19 (0х03) происходит переход на метку d_str. Здесь проверяется значение в регистре R18 - оно оказывается равным 0х00. Это означает, что хосту требуется строка с индексом 0. Таковая у нас имеется, и располагается в памяти программ по метке str_0. Строка имеет всего четыре байта:

0х04 0х03 0х09 0х04

где

0х04 - длина данного дескриптора - 4 байта;
0х03 - тип дескриптора - STRING_DESCRIPTOR;
0х09, 0х04 - идентификатор английского языка - 0409.

Если наше устройство имеет строковые дескрипторы (строки) на русском языке, то мы должны использовать идентификатор русского языка - 0419, и тогда строка с индексом ноль примет вид:

0х04 0х03 0х19 0х04

Для отправки строки в Z-регистр загружается адрес первого ее элемента (ldi R31, high(str_0*2) и ldi R30, low(str_0*2)). В счетчик - регистр R20 - записывается значение длины строки - 4 байта. Далее происходит переход на метку d_str_1 и снова в цикле, байт за байтом, переписывается наша строка в буфер конечной точки EP0, пока не обнулится счетчик R20. Затем пакет со строкой отправляется хсоту (те же три команды - lds R24, UEINTX, cbr R24, 1 << TXINI, sts UEINTX, R24). Поскольку значение R18 (при последующей проверке) не равно 2, осуществляется переход к фазе SETUP (rcall w_nakout) и выход в основной цикл (rjmp vihod_ep).

Третий пакет SETUP будет содержать запрос на получение строки с индексом 2, т. е. текстовой строки с описанием изделия (iProduct):

0x80 0x06 0x02 0x03 0x00 0x00 0xFF 0x00

Здесь 0х02 (младший байт поля wValue) это индекс дескриптора запрашиваемой строки - строка с индексом два.

После приема и анализа пакета на метке s_2 в регистре R18 обнаруживается значение 0х02. На строку в памяти программ указывает метка str_2, в Z-регистр загружается адрес первого значения строки командами ldiR31, high(str_2*2), ldi R30, low(str_2*2). Содержание строки:

0х22 - (bLength) - длина текстовой строки изделия - 34 байта;
0х03 - старший байт поля wValue - тип дескриптора - STRING_DESCRIPTOR;
0x41, 0x00 - латинская буква "А" в формате UNICODE (0х0041);
0x56, 0x00 - латинская буква "V" в формате UNICODE (0х0056);
0x52, 0x00 - латинская буква "R" в формате UNICODE (0х0052);
0x20, 0x00 - символ пробела в формате UNICODE (0х0020);
0x55, 0x00 - латинская буква "U" в формате UNICODE (0х0055);
0x53, 0x00 - латинская буква "S" в формате UNICODE (0х0053);
0x42, 0x00 - латинская буква "B" в формате UNICODE (0х0042);
0x20, 0x00 - символ пробела в формате UNICODE (0х0020);
0x48, 0x00 - латинская буква "H" в формате UNICODE (0х0048);
0x49, 0x00 - латинская буква "I" в формате UNICODE (0х0049);
0x44, 0x00 - латинская буква "D" в формате UNICODE (0х0044);
0x20, 0x00 - символ пробела в формате UNICODE (0х0020);
0x44, 0x00 - латинская буква "D" в формате UNICODE (0х0044);
0x45, 0x00 - латинская буква "E" в формате UNICODE (0х0045);
0x4D, 0x00 - латинская буква "M" в формате UNICODE (0х004D);
0x4F, 0x00 - латинская буква "O" в формате UNICODE (0х004F).

Опять длина строки превышает размер конечной точки ноль, поэтому отправка строки разбивается на два пакета размерами 32 и 2 байта. Первый пакет 32 байта (по размеру конечной точки EP0) отсылается в метках d_str_1 и out_st, так же, как отсылались строки ранее. Затем снова проверяется R18, и поскольку его значение равно 0х02, то происхрдит переход на метку d_str_2. Здесь ожидается приход от хоста следующего пакета IN (команды lds R24, UEINTX и sbrs R24, 0), после чего по метке out_st_1 досылаются хосту оставшиеся два байта строки изделия - второй пакет. Далее обрабатывается фаза SETUP (rcall w_nakout), и происходит выход из обработчика прерывания (rjmp vihod_ep).

Интересным остается тот факт, что хост в процессе энумерации не запрашивает оставшуюся строку с названием производителя (iManufacturer). Однако она может быть прочитана в любой момент по запросу какого-либо приложения, например UsbTreeView (см. эту программу в архиве []). Эта строка имеется в нашем устройстве, и она располагается в памяти программ на метке str_1:

0x0C - (bLength) - длина текстовой строки производителя - 12 байт;
0х03 - старший байт поля wValue - тип дескриптора - STRING_DESCRIPTOR;
0x41, 0x00 - латинская буква "А" в формате UNICODE (0х0041);
0x54, 0x00 - латинская буква "Т" в формате UNICODE (0х0054);
0x4D, 0x00 - латинская буква "M" в формате UNICODE (0х004D);
0x45, 0x00 - латинская буква "E" в формате UNICODE (0х0045);
0x4C, 0x00 - латинская буква "L" в формате UNICODE (0х004C);

Все процедуры по отправке строки производителя аналогичны тем, что мы выполняли для отправки предыдущих строк.

В заключении сформулируем общие правила, которыми нужно пользоваться для написания своих строковых дескрипторов:

1. Название производителя, название изделия, серийный номер в текстовых строках могут быть какими угодно. Но они должны быть написаны на языке, для которого имеется идентификатор (в строке с индексом 0). В строке можно использовать буквы и цифры.
2. После того, как подготовили строку нового названия или серийного номера, нужно подсчитать общее количество байт в этой строке, и записать количество (в HEX-формате) в поле bLength соответствующей текстовой строки.
3. В счетчик R20 перед отправкой строки (в метках s_1, s_2, s_3), необходимо записать новое значение длины строки. Оно должно совпадать со значением поля bLength соответствующей строки (если длина строки меньше размера конечной точки ноль).

Передачей строковых дескрипторов (строк) заканчивается общий процесс энумерации. ИМХО на практике он не выглядит таким сложным, как в теории или на языке Си из примера STK526-series2-hidio-2_0_2-doc.

Активация конфигурации устройства USB. После получения инфоримации об устройстве через чтение дескрипиторов, операционная система подбирает необходимый драйвер для устройства USB, на который возлагается вся дальнейшая работа по взаимодействию с ним. Также операционная система выбирает в устройстве нужную конфигурацию.

Все по порядку: получаем пакет SETUP:

0x80 0x06 0x00 0x01 0x00 0x00 0x12 0x00

где

0x80 - поле bmRequestType - стандартный запрос, адресованный устройству.
0x06 - поле bRequest - номер запроса - GET_DESCRIPTOR.
0x00 0x01 - поле wValue - тип дескриптора (старший байт) - DEVICE_DESCRIPTOR.
0x00 0x00 - поле wIndex - зависит от запроса - не используется.
0x12 0x00 - поле wLength - количество байт, требуемое хостом в ответном пакете - 18 байт.

Такой пакет мы уже получали от хоста после присвоения адреса устройству во время процедуры энумерации. А значит механизм его обработки (и отправки требуемого дескриптора) нам хорошо известен. Он здесь полностью повторяется, и поэтому не требует дополнительного описания. После отправки дескриптора устройства (который находится в памяти программ по метке dev_desc), происходит возврат в основной цикл сна до появления следующего пакета SETUP. Этот пакет вскоре приходит с запросом на получение дескриптора конфигурации:

0x80 0x06 0x00 0x02 0x00 0x00 0x09 0x00

где

0x80 - поле bmRequestType - стандартный запрос, адресованный устройству.
0x06 - поле bRequest - номер запроса - GET_DESCRIPTOR.
0x00 0x02 - поле wValue - тип дескриптора (старший байт) - CONFIGURATION_DESCRIPTOR.
0x00 0x00 - поле wIndex - зависит от запроса - не используется.
0x09 0x00 - поле wLength - количество байт, требуемое хостом в ответном пакете - 9 байт.

На этот раз хосту требуется только первые 9 байт дескриптора. По стандартной схеме вызывается обработчик прерывания USB_ENDPOINT. После проверки, что запрос вызван конечной точкой EP0 (команды lds R24, UEINT и cpi R24,1), происходит переходна метку ep_0, копирование 2 байт прочитанного пакета в регистры R16, R17, и остальные действия происходят согласно СПИСКУ №1 до метки d_con. В метке d_con с оценкой состояния разряда TXINI (команды lds R24, UEINTX и sbrs R24, 0) в Z-регистр загружается адрес первого элемента дескриптора конфигурации - ldi R31,high(con_desc*2) и ldi R30,low(con_desc*2) и  очищается счетчик - регистр R23 (необязательная команда). Затем проверяется, равно ли содержимое регистра R22 девяти (cpi R22, 9). Если да, то после прочтения конфигурационного пакета подпрограммой read_buff происходит переход на метку con_drv. Здесь известным способом из памяти программ загружается первые девять значений дескриптора в буфер конечной точки EP0 (для этого используется счетчик R23) и сбрасом разряда TXINI (команды lds R24, UEINTX, cbr R24, 1 << TXINI и sts UEINTX, R24) эти данные отправляются хосту. Дальше снова вызывается подпрограмма w_nakout для завершения транзакции, и происходит выход из обработчика прерывания (rjmp vihod_ep).

Следующий пакет SETUP содержит запрос на получение уже полного дескриптора конфигурации длиной 41 байт, который мы уже отправляли при энумерации:

0x80 0x06 0x00 0x02 0x00 0x00 0x29 0x00

где

0x80 - поле bmRequestType - стандартный запрос, с передачей данных от устройства к хосту.
0x06 - поле bRequest - номер запроса - GET_DESCRIPTOR.
0x00 0x02 - поле wValue - тип дескриптора (старший байт) - CONFIGURATION_DESCRIPTOR.
0x00 0x00 - поле wIndex - зависит от запроса - не используется.
0x29 0x00 - поле wLength - количество байт, требуемое хостом в ответном пакете - 41 байт.

Уже отработанный порядок действий (из СПИСКА №1), приводит нас на метку out_dat_c, где отправляется наш длинный дескриптор двумя пакетами - 32 байта и 9 байт - точно так же, как мы это делали в первый раз. Далее происходит вызов w_nakout и возврат в основной цикл сна.

Затем приходит новый запрос:

0x00 0x09 0x01 0x00 0x00 0x00 0x00 0x00

где

0x00 - поле bmRequestType - стандартный запрос с передачей данных от хоста к устройству.
0x09 - поле bRequest - номер запроса - SET_CONFIGURATION.
0x01 0x00 - поле wValue - номер новой конфигурации (младший байт).
0x00 0x00 - поле wIndex - зависит от запроса - не используется.
0x00 0x00 - поле wLength - количество байт, посылаемое хостом - ноль байт.

Этот запрос устанавливает новую конфигурацию устройства. При выполнении запроса нам необходимо сконфигурировать все конечные точки, относящиеся к новой выбранной конфигурации. Происходит запуск обработчика прерывания USB_ENDPOINT. Здесь все делается как обычно - проверяется, от какой конечной точки поступил запрос, происходит переход на метку ep_0 и чтение в регистры R16, R17 первых двух значений байт конфигурационного пакета. После проверок содержимого регистра R16 (поле bRequest), происходит переход на метку next_1, где устанавливается равенство регистров R16 и R17 значениям 0х09 и 0х00 соответственно. Далее происходит переход на метку set_cfg. Контрольная транзакция с запросом SET_CONFIGURATION имеет две фазы, и поэтому для нее характерен тот же процесс обработки, который мы рассматривали для запроса SET_ADDRESS. Поэтому на метке set_cfg сначала сбрасывается разряд RXSTPI, чем подтверждается подтвердив прием пакета SETUP (три первых команды). Затем в наступившей фазе STATUS, ожидается первый IN-пакет (метка s_cfg), и после его получения (TXINI = 1) сразу же сбрасывается разряд TXINI (команды cbr R24, 1 << TXINI и sts UEINTX, R24), чем хосту отправляется пакет нулевой длины (ZLP).

Все делается по аналогии с рассмотренным ранее процессом конфигурирования конечной точки EP0.

EP1. Сначала выбирается конечная точка EP1  командами ldi R16, 1 и sts UENUM, R16, проверяется наличие единицы в разряде EPEN регистра UECONX - команды lds R16, UECONX, sbrs R16, 0. Если единица в разряде EPEN отсутствует, то она устанавливается (ori R16, 0x01, sts UECONX, R16), разрешая конфигурацию конечной точки EP1. Дальше происходит сама конфигурация. Командами ldi R16, (1 << EPTYPE1)+(1 << EPTYPE0)+(1 << EPDIR) и sts UECFG0X, R16 задаем тип и направление конечной точки в регистре UECFG0X - конечная точка один получает тип Interrupt и направление IN. Двумя следующими командами - ldi R16, 0x02 и sts UECFG1X, R16 в регистре UECFG1X задается размер конечной точки EP1 - 8 байт (EPSIZE0 = 0, EPSIZE1 = 0, EPSIZE2 = 0) и выделяется память под её буфер (ALLOC = 1). Процесс конфигурации завершен EP1. Если все заданные параметры для конечной точки корректны, то разряд CFGOK регистра UESTA0X установится в единицу, и конечная точка будет считаться сконфигурированной. Этот факт проверяется в метке test_c_ep1.

EP2. Затем по такому же сценарию, конфигурируется конечную точка EP2: сначала она выбирается (ldi R16, 2, sts UENUM, R16), разрешается конфигурация через разряд EPEN (ori R16, 0x01, sts UECONX, R16). В регистре UECFG0X задается тип конечной точки - Interrupt и направление OUT (ldi R16, (1 << EPTYPE1)+(1 << EPTYPE0)+(0 << EPDIR), sts UECFG0X, R16), а в регистре UECFG1X задается размер 8 байт, и так же выделяется память для конечной точки (ALLOC = 1). Завершается конфигурация проверкой разряда CFGOK регистра UESTA0X по метке test_c_ep2. Этот разряд, установленный в единицу, говорит нам об успешном завершении процесса конфигурации конечной точки.

Теперь наше устройство перешло в новое состояние - сконфигурировано (Configured). Однако, не все запросы хоста еще выполнены. Поскольку запросы посылаются строго на конечную точку EP0, то для продолжения работы необходимо вновь ее активизировать. EP0 выбирается командой sts UENUM, R1, и происходит выход из обработчика прерывания в основной цикл сна.

Обработка запросов класса HID. Наше устройство USB относится к классу HID. Устройства этого класса помимо стандартных запросов, поддерживают ряд специфических, к которым относится запрос SET_IDLE:

0x21 0x0A 0x00 0x00 0x00 0x00 0x00 0x00

где

0x21 - поле bmRequestType - запрос, относящийся к HID-устройству.
0x09 - поле bRequest - номер запроса - SET_IDLE.

Значения остальных полей нас не интересуют. В обработчике прерывания (вызванного как обычно приходом пакета SETUP) в R16 и R17 обнаруживаются значения 0x0A и 0x21 соответственно (метка next_2), после чего происходит переход на метку set_idle, где подтверждается прием пакета SETUP - первые три команды. Затем, убедившись в готовности передатчика (команды lds R24, UEINTX и sbrs R24, 0), отправляется хосту пакет нулевой длины (команды cbr R24, 1 << TXINI и sts UEINTX, R24), подтверждая тем самым исполнение запроса SET_IDLE. Далее необходимо проверить значение флага - регистра R2 (tst R2). Единица в этом регистре показывает, что текущий запрос SET_IDLE был послан хостом сразу после пробуждения (WAKE_UP) через выбранную перед этим пробуждением конечную точку EP0 (вспомните описание USB_GENERAL). Практика показала, что в этом случае никаких запросов от хоста больше не последует, и для дальнейшего успешного обмена (без зависаний ПО хоста) необходимо вновь выбрать конечную точку EP2. Это осуществляется командами ldi R16, 2 и sts UENUM, R16 после возврата R2 в исходное состояние (clr R2). Затем происходит выход из обработчика прерывания и возврат в основной цикл сна. Если же значение флага - регистра R2 - нулевое, то это означает, что идет начатый стандартный процесс идентификации HID-устройства, и программа уходит из прерывания в основной цикл, оставляя активной ЕР0, с ожиданием прихода следующего пакета SETUP с новым (и последним) запросом.

Обработка запроса GET_DESCRIPTOR_REPORT. В устройствах, относящихся к HID-классу, обмен данными между конечной точкой IN (у нас это EP1), конечной точкой OUT (у нас это EP2) и хостом осуществляется посредством специальных информационных посылок, равных размеру конечной точки (8 байт), именуемых репортами. Представление данных в репорте подчиняется определенным правилам, которые описаны в специфическом дескрипторе репорта (REPORT_DESCRIPTOR), характерном только для HID-класса. Правила эти должны быть известны хосту, поэтому до начала обмена через точки IN и OUT он посылает последний запрос - GET_DESCRIPTOR_REPORT:

0x81 0x06 0x00 0x22 0x00 0x00 0x62 0x00

где

0x81 - поле bmRequestType - стандартный запрос для интерфейса, с передачей данных от устройства к хосту.
0x06 - поле bRequest - номер запроса - GET_DESCRIPTOR.
0x00 0x22 - поле wValue - тип дескриптора (старший байт) - REPORT_DESCRIPTOR.
0x00 0x00 - поле wIndex - зависит от запроса - не используется.
0x62 0x00 - поле wLength - количество байт, требуемое хостом в ответном пакете – 98 (34 байта + 64 байта).

Замечание: длина нашего дескриптора репорта - 34 байта, и поэтому мы логично ожидаем в младшем байте поля wLength значение 0х22 (34). Однако хост (по пока не выясненной причине) требует 98 байт дескриптора (0х62), что на 64 байта больше фактического значения. Тем не менее мы отправляем только имеющиеся 34 байта (ни больше, ни меньше), и хост остается "доволен". Дескриптор репорта идет последним в нашем блоке дескрипторов, и он находится по адресу, заданному меткой rep_desc. За его основу был взят дескриптор из демонстрационной программы STK526-series2-hidio-2_0_2-doc (файл usb_descriptors.c), затем дескриптор был немного урезан (исключена секция Feature report), и теперь выглядит так:

1. 0x06, 0x00, 0xFF Usage Page (Vendordefined)
2. 0x09, 0x01 Usage (UsageID - 1)
3. 0xA1, 0x01 Collection (Application)
4. 0x09, 0x02 Usage (UsageID - 2)
5. 0x15, 0x00 Logical Minimum (0)
6. 0x26, 0xFF, 0x00 Logical Maximum (255)
7. 0x75, 0x08 Report Size (8)
8. 0x95, 0x08 Report Count (8)
9. 0x81, 0x02 IN report (Data, Variable, Absolute)
10. 0x09, 0x03 Usage (UsageID - 3)
11. 0x15, 0x00 Logical Minimum (0)
12. 0x26, 0xFF, 0x00 Logical Maximum (255)
13. 0x75, 0x08 Report Size (8)
14. 0x95, 0x08 Report Count (8)
15. 0x91, 0x02 OUT report (Data, Variable, Absolute)
16. 0xC0 End Collection

Опишу то, что удалось понять в дескрипторе репорта.

Строка 1 - поле Usage Page - задает некоторый класс устройств с общими характеристиками. Первый байт этого поля может иметь значение 0х05 (как правило) или 0х06 (у нас). Два младших бита первого байта показывают количество оставшихся байтов в этом поле. Число 0х06, например, в двоичном представлении - 00000110b. Два младших разряда (10b) это десятичное число 2. Значит после первого байта (0х06) следуют еще два - 0х00 и 0хFF. В нашем случае устройство не относится к какому-то определенному классу, а его назначение определяется производителем (Vendordefined).

Строка 2 - поле Usage - задает подкласс устройств или функций, которые функционируют в пределах класса, указанного в поле Usage Page. Здесь первый байт - идентификатор поля - как правило имеет значение 0х09. Второй байт - идентификатор собственно устройства или функции, который задает их назначение. В нашем дескрипторе эти идентификаторы, идущие в порядке возрастания (строки 2, 4, 10), показаны для примера и не задействованы.

Строка 3 - поле Collection - задает начало группы элементов одного типа. Тип указывается во втором байте. У нас - группа элементов прикладного типа (Application), с идентификатором 0х01.

Строка 5 - поле Logical Minimum - определяем минимальное значение в каждом принимаемом байте, в логических единицах. Минимальное значение задается вторым байтом и равно нулю (0х00). Здесь не забываем тот факт, что направление передачи берется относительно хоста. То есть IN-репорт это пакет данных, получаемых хостом.

Строка 6 - поле Logical Maximum - определяем максимальное значение в каждом принимаемом байте, в логических единицах. Максимальное значение задается вторым байтом и равна 255 (0хFF). Если первый байт этого поля (0х26) представить в двоичном виде - 00011010b, то увидим, что два младших разряда есть десятичное число два. Это означает, как мы уже знаем, присутствие еще двух байтов после первого - 0xFF и 0x00, причем назначение последнего не установлено.

Строка 7 - поле Report Size - задаем размер единицы (одного элемента данных) принимаемой информации в битах - 8 бит или 1 байт.

Строка 8 - поле Report Count - задаем количество единиц принимаемой информации - 8 единиц. Таким образом количество принимаемых байтов в репорте - восемь. И размер репорта составляет восемь байт.

Строка 9 - поле IN report, говорит нам, что все рассмотренные ранее строки, начиная с 4-ой, относятся к IN-репорту. В первом байте поля (0х81) старшая тетрада (так называемое поле тега = 1000b) означает тип репорта - IN. Младшая (0001b) содержит в двух младших битах единицу, что означает, что за первым байтом (0х81), последует только один байт со значением - 0х02. Это самый ответственный байт. В нем сгруппированы биты (данных), отвечающие за определенные характеристики и представление данных в репорте ( в данном случае в IN-репорте). Подробно все они описаны в [3]. Мы лишь отметим, что число 0х02 во втором байте означает, что данные репорта могут изменяться (Data), представлены, как восемь отдельных восьмибитных элементов (Variable), и значения их берутся относительно нуля (Absolute). Вот все, что касается IN-репорта.

Строки 11 .. 14 в точности повторяют поля, которые мы уже рассмотрели раньше. Только теперь они относятся к OUT-репорту, то есть пакету данных, получаемому устройством.

Строка 15 - поле OUT report. Для нее справедливо все, что раньше было сказано про строку девять. Отличие только в первом байте (0х91), старшая тетрада которого - 1001b - обозначает тип репорта - OUT.

Строка 16 - поле End Collection - определяет конец группы элементов одного типа.

Обработка запроса GET_DESCRIPTOR_REPORT ничем не отличается от обработки других запросов. После получения очередного пакета SETUP выполняются два первых пункта из СПИСКА №1. После проверки значения поля bRequest в регистре R16 (cpi R16, 0x06), которое равняется 0х06, происходит переход на метку get_dsc. Там, уже в регистре R17, проверяется значение поля bmRequestType, и если оно равно 0х81, то это означает, что данный дескриптор относится к интерфейсу. Происходит переход на метку int_desc, где считываются остальные 6 байт конфигурационного пакета (вызовом подпрограммы read_buff). Однако нас интересуют только два из них - тип дескриптора в регистре R19, и количество байт для отправки хосту в регистре R22. Если значения в этих регистрах совпадают с ожидаемыми, то происходит переход на метку d_rep_1, где проверяется, что получен пакет IN, и проверяется готовность передатчика (lds R24, UEINTX и sbrs R24, 0), очищается счетный регистр R23 (clr R23) и в Z-регистр загружается значение адреса первого элемента дескриптора репорта - команды ldi R31, high(rep_desc*2) и ldi R30, low(rep_desc*2). Длина дескриптора здесь снова превышает размер конечной точки (34 против 32), поэтому отправляться дескрипутор будет двумя кусками (как когда-то отправляли дескриптор конфигурации). Сначала первые 32 байта дескриптора переписываются в буфер конечной точки EP0 (цикл out_dat_r) и этот первый блок отправляется хосту (lds R24, UEINTX, cbr R24, 1 << TXINI и sts UEINTX, R24). Затем, после прихода следующего пакета IN и готовности передатчика (цикл d_rep_2), переписываеются в буфер EP0 оставшиеся два байта (цикл out_dat_r1). Обратите внимание, что значение счетчика байтов (R23) до этого было равно 32. Затем хосту отправляются два байта (lds R24, UEINTX, cbr R24, 1 << TXINI и sts UEINTX, R24) и далее как обычно отрабатывается фаза SETUP (rcall w_nakout), чем завершается транзакция.

На практике установлено, что после отправки дескриптора репорта пакеты SETUP больше не поступают, соответственно никаких запросов к устройству не предъявляется. Поэтому по-видимому устройство USB успешно прошло все этапы энумерации и HID-идентификации. Теперь оно считается работоспособным? и готовым к использованию в соответствиb со своим функциональным назначением. Можно приступать непосредственно к обмену данными с устройством через заявленные конечные точки один (IN) и два (OUT), это так называемый РАБОЧИЙ РЕЖИМ. Поэтому после отправки дескриптора репорта и завершения транзакции (rcall w_nakout) выбирается для обмена конечная точка EP2 (ldi R16, 2 и sts UENUM, R16), и ей разрешается генерировать запрос на прерывание по приходу пакета OUT (lds R24, UEIENX, sbr R24, 1 << RXOUTE и sts UEIENX, R24). Далее происходит выход из обработчика прерывания в основной цикл сна (rjmp vihod_ep). Теперь вместо конечной точки EP0 источником прерываний у нас становится конечная точка EP2. Вспомним, что хост посылает запрос OUT, когда хочет передать нам данные, и делает это с интервалом, не превышающим указанный интервал в дескрипторе конечной точки.

И последнее замечание. Поскольку никакие запросы, кроме описанных здесь, от хоста не поступают, в программе они не принимаются и не обрабатываются. Однако, если неподдерживаемый запрос все же поступит (что не наблюдалось) - устройство отвечает на него маркером STALL (метки n_3 и n_4).

[РАБОЧИЙ РЕЖИМ]

Рабочий режим - основной режим работы нашего устройства, в котором реализуется его функционал. В этом режиме обмен происходит уже под управлением пользовательского приложения (ПО хоста). Пользовательское приложение это обычный исполняемый файл (.exe), который был написан в программной среде RadASM/MASM32 (среда разработки значения не имеет; это может быть Си, Delphi и т. д.).

Процесс установки и настройки RadASM/MASM32 хорошо описан в статье [11]. Далее процесс по шагам:

1. Запустите RadASM, в меню File выберите New Project

2. В открывшемся диалоге выберите тип проекта WIN32(no res) и имя проекта, кликните Next, оставшиеся шаги оставьте по умолчанию.

3. Открывается окно редактора программы, в нем редактируется код. Откройте исходный код ПО хоста из архива [].

4. Комбинацией клавиш Ctrl+F5 запустите процесс ассемблирования, линковки и создания рабочих файлов. Для каждой из этих процедур есть также отдельная кнопка. Если нет ошибок, то в результате получится исполняемый файл *.exe, который автоматически запустится.

Обмен с HID-устройством происходит по прерываниям/запросам от хоста (с определенным в дескрипторе конечной точки интервалом). В нашем случае после процедуры энумерации для первоначального обмена была выбрана EP2 (OUT) т. е. мы ожидаем от хоста запрос OUT. Когда он поступит, данные репорта уже находятся в буфере EP2, и мы их просто переписываем в регистры R3..R10. После этого выбирается конечная точка точку EP1 (IN), и от хоста ожидается уже запрос IN, по которому данные из регистров R3..R10 переписываются в буфер EP1, и далее отправляются хосту. Затем опять выбирается EP2, и весь процесс повторяется.

Назначение нашего демонстрационного ПО хоста состоит в том, чтобы передать устройству текущие координаты указателя мыши (OUT-репорт), принять их обратно от устройства (IN-репорт) и вывести в рабочую область окна программы. Внешний вид запущенного приложения (с подключенным устройством) показан на рис. 5.

AT90USB162 host software fig05

Рис. 5. ПО хоста, работающее с устройством USB.

Здесь в левом верхнем углу рабочей области надпись Connected означает, что устройство, имеющее VID - 03EB (Atmel Corp.) и PID - 2013 (HID-Device), подключено к компьютеру. Правее этой надписи отображаются текущие координаты указателя мыши. В левом нижнем углу рабочей области мы видим все восемь байт IN-репорта, т. е. то, что мы получаем от устройства. Координаты указателя - последние четыре байта.

Передача координат указателя мыши происходит при нажатии на ее левую кнопку в пределах рабочей области приложения. Это действие приводит к формированию OUT-репорта и последующей его передаче (через драйвер) в устройство. Принятый устройством пакет OUT запускает обработчик прерываний USB_ENDPOINT, где выясняеется (в метке ep_2), что источником прерывания была конечная точка два (ЕР2), и происходит переход на метку ep_out. Здесь последовательно проверяется установка в единицу разряда RXOUTI - получен пакет OUT (команды lds R24, UEINTX и sbrs R24, 2) и разряда FIFOCON - данные пакета сохранены в банке ЕР2 (команда sbrs R24, 7). Затем сбрасывается разряд RXOUTI для подтверждения прерывания - команды lds R24, UEINTX, cbr R24, 1 << RXOUTI и sts UEINTX, R24. Сохраненные в буфере данные переписываются в регистры R3..R10 (lds R3, UEDATX, ..., lds R10, UEDATX) и сбрасывается разряд FIFOCON (команды lds R24, UEINTX, cbr R24, 1 << FIFOCON и sts UEINTX, R24).

Данные приняты. Координаты указателя мыши теперь размещены в регистрах R7..R10 следующим образом: R7 – младший байт координаты X, R8 - старший байт координаты X, R9 - младший байт координаты Y, R10 - старший байт координаты Y. Теперь, чтобы отправить координаты обратно хосту, при получении пакета IN нужно активировать конечную точку EP1 (команды ldi R16, 1 и sts UENUM, R16) и разрешить от нее прерывание (команды lds R24, UEIENX, ldi R24, 1 << TXINE и sts UEIENX, R24). Далее происходит возврат из обработчика прерывания в основной цикл сна (rjmp vihod_ep).

Приложение на компьютере написано таким образом, что через определенное время (примерно 20 .. 50 мс) после отправки OUT-репорта оно формирует запрос на получение данных от устройства - IN-репорт. Приход пакета IN от хоста выводит МК из режима сна, запускается переход на обработчик прерывания USB_ENDPOINT. Здесь происходит проверка, что прерывание было вызвано конечной точкой EP1 (команда cpi R24, 2) и происходит переход на метку ep_in (rjmp ep_in). По метке ep_in проверяется состояние разрядов FIFOCON и TXINI регистра UEINTX - команды lds R24, UEINTX, sbrs R24, 7 и sbrs R24, 0, единицы в которых говорят о том, что банк ЕР1 свободен, и получен пакет IN. Банк загружается данными, ранее полученными от хоста, из регистров R3..R10 (sts UEDATX, R3, ..., sts UEDATX, R10), затем последовательно сбрасываются разряды TXINI (команды lds R24, UEINTX, cbr R24, 1 << TXINI и sts UEINTX, R24) и FIFOCON (cbr R24, 1 << FIFOCON и sts UEINTX, R24). Если данные успешно отправлены, то разряды TXINI и FIFOCON аппаратно установятся в единицы, это проверяется в цикле по метке no_in. Теперь нужно ожидать новую порцию данных от хоста. Для этого разрешается прерывание по приходу пакета OUT (lds R24, UEIENX, ldi R24, 1 << RXOUTE и sts UEIENX, R24) и выбирается конечная точка EP2 (ldi R16, 2, sts UENUM, R16). Далее происходит выход из обработчика прерывания в основной цикл сна (rjmp vihod_ep).

Программа для МК написана в AVR-Studio, Ver. 4.13.571 (можно использовать версию 4.19, или более поздние версии Atmel Studio). Мне для реализации проекта потребовался только asm-файл (firmware\MyUSB_0\MyUSB_0.asm, см. архив []), и больше ничего. При запуске AVR-Studio открывается диалог - создать New Project либо открыть имеющийся. Создаем новый проект. Открывается текстовый редактор, в котором пишем и редактируем наш файл с расширением asm. Затем нажимаем в строке меню кнопку "Assemble". Начинается процесс сборки проекта и создания необходимых рабочих файлов, из которых нам интересен только файл прошивки МК, имеющий расширение hex. Его-то мы и "заливаем" в МК через FLIP. Все, больше ничего не требуется. МК, как мы знаем, обладает способностью программироваться по интерфейсу USB, благодаря встроенному загрузчику (bootloader), посредством специальной программки FLIP. Скачал FLIP Ver. 3.4.7., который после инсталляции автоматически интегрировался в AVR-Studio (в панели инструментов появился значок).

Скачать утилиту FLIP можно с сайта Atmel, также можете скачать архив []. Соедините Вашу плату кабелем USB с компьютером. Чтобы записать в МК нашу программу после ассемблирования ее в AVR-Studio, щелкните на значке FLIP на панели инструментов открывается окно программы (рис. 6).

Run FLIP from AVR Studio fig06

Рис. 6. Рабочее окно утилиты программирования FLIP.

Далее действуйте по инструкции к макетной плате AVR-USB162 и программе FLIP [4].

ПО хоста - стандартное оконное приложение, написанное на ассемблере в среде RadASM/ MASM32. Это бесплатный пакет для разработки WINDOWS-приложений на ассемблере. Запускаем его, создаем проект. В открывшемся текстовом редакторе пишем программу, которая теперь требует подключения необходимых для работы файлов и библиотек. Ассемблируем. И получаем теперь исполняемый файл с расширением exe. Запускаем его и видим окно, изображенное на рис. 5. Все операции по обмену данными с нашим HID-устройством осуществляем через функции из динамически подключаемой библиотеки AtUsbHid.dll. Вообще говоря, пользовательское приложение - отдельная большая тема и в рамках данной статьи не рассматривается. Однако тем разработчикам, которые захотят научиться писать собственные Windows-приложения на ассемблере рекомендую отличную книгу [5].

По умолчанию в память чипа AT90USB162 на заводе Atmel уже прошили загрузчик, благодаря чему Вы можете перепрограммировать микроконтроллер через USB, без программатора. Также все фьюзы установлены в правильное значение, чтобы гарантировать возможность работы чипа как устройства USB.

Но если по какой-то причине у Вас "слетел" загрузчик или неправильно оказались установлены фьюзы, то перешить загрузчик и/или исправить значение фьюзов можно только с помощью программатора – последовательного или параллельного.

Чаще всего используется последовательный программатор ISP. Я использовал ранее купленный когда-то STK500 (хотя можно использовать любой ISP-программатор). Подключите программатор через 6-пиновый ISP-разъем к макетной плате AVR-USB162, как показано на рис. 7.

AT90USB162 ISP program via STK500 fig07

Рис. 7. Подключение к программируемому чипу платы AVR-USB162.

Затем, щелчком мыши на значке "Connect to..." (в панели инструментов AVR-Studio) устанавливаем связь с программатором STK500, во вкладке "Main" выбираем тип МК (рис. 8) .

AT90USB162 run AVRISP from AVR Studio fig08

Рис. 8. Выбор типа микроконтроллера для программирования.

Выбираем вкладку "Fuses", задаем нужные значения и нажимаем "Program" (рис. 9).

AT90USB162 AVRISP write fuses fig09

Рис. 9. Восстановление значения фьюзов.

Примечание: значение по умолчания для фьюзов микроконтроллеров можно узнать с помощью сайта AVR fuse calculator [10].

Кроме установки FUSE-битов, в режиме ISP-программирования можно записать в МК и саму программу-загрузчик, если по какой-либо причине она была стерта. Здесь нужно выбрать вкладку "Program", в секции "Flash" указать путь к файлу загрузчика и нажать "Program" (рис. 10).

AT90USB162 AVRISP write DFU FLIP bootloader fig10

Рис. 10. Запись загрузчика DFU Flip.

;Демонстрационная реализация устройства USB HID на ассемблере для AT90USB162.
;Автор Каменяр Сергей <batonator@yandex.ru> (15 июля 2016 г.)
;Размер EP0=32 байта.
;.include "iousb162.h"
.include "usb162def.inc"
 
.CSEG

;------------------------------------------------
;Вектора сброса и обработчиков прерываний:
;------------------------------------------------
.ORG $000
   rjmp RESET
.ORG $016
   rjmp USB_GENERAL     ;обработка общих событий
.ORG $018
   rjmp USB_ENDPOINT    ;обработка прерывания конечной точки

EP0   EQU   EPNUM0      ;конечная точка управления
EP1   EQU   EPNUM1      ;конечная точка IN
EP2   EQU   EPNUM2      ;конечная точка OUT

CMD_USB_SET_ADDRESS        EQU   0x05
CMD_USB_GET_DESCRIPTOR     EQU   0x06

;------------------------------------------------
;Начало основной программы:
;------------------------------------------------
RESET:
   ldi R16, high(RAMEND)
   out SPH, R16
   ldi R16, low(RAMEND)
   out SPL, R16
   clr R1
   clr R2
   clr R25
   clr R26
 
   ldi R16, 0 << REGDIS
   sts REGCR, R16
 
   cli
   wdr
   in R16, MCUSR
   andi R16, (0xff & (0 << WDRF))
   out MCUSR, R16
   lds R16, WDTCSR
   ori R16, (1 << WDCE)|(1 << WDE)
   sts WDTCSR, R16
   ldi R16, (0 << WDE) 
   sts WDTCSR, R16
   ldi R16, 0x1F   ;00011111
   out DDRD, R16
   ldi R16, 0      ;00000000
   out PORTD, R16
   ldi R16, (0 << PLLP2)+(0 << PLLP1)+(1 << PLLP0)+(1 << PLLE)
   out PLLCSR, R16
bit_lock:
   in R16, PLLCSR
   sbrs R16, 0
   rjmp bit_lock
   lds R16, USBCON
   sbr R16, 1 << USBE
   sts USBCON, R16
   cbr R16, 1 << FRZCLK
   sts USBCON, R16
   lds R16, UDCON
   cbr R16, 1 << DETACH
   sts UDCON, R16
r_bus:
   lds R16, UDINT
   sbrs R16, 3
   rjmp r_bus
   cbr R16, 1 << EORSTI
   sts UDINT, R16
   rcall conf_ep_0
   lds R16, UDCON
   sbr R16, 1 << RSTCPU
   sts UDCON, R16
   ldi R24, (1 << SUSPE)+(1 << EORSTE)
   sts UDIEN, R24
   ldi R24, 1 << RXSTPE
   sts UEIENX, R24
   ldi R16, 1 << SE
   out SMCR, R16
   sei
SLP:
   sleep
   rjmp SLP
   
;------------------------------------------------
;Обработчик общих прерываний USB
;------------------------------------------------
USB_GENERAL:
   lds R24, UDINT 
   sbrs R24, 3
   rjmp next_ev_1
   cbr R24, 1 << EORSTI 
   sts UDINT, R24
   rjmp vihod_ug

next_ev_1:
   sbrs R24, 0
   rjmp next_ev_2
   cbr R24, 1 << SUSPI
   sts UDINT, R24
   lds R24, USBCON
   sbr R24, 1 << FRZCLK
   sts USBCON, R24
   in R16, PLLCSR
   cbr R16, 1 << PLLE
   out PLLCSR, R16
   lds R24, UDIEN
   sbr R24, 1 << WAKEUPE
   sts UDIEN, R24
   rjmp vihod_ug
   
next_ev_2:
   sbrs R24, 4
   rjmp vihod_ug
   in R16, PLLCSR
   sbr R16, 1 << PLLE
   out PLLCSR, R16
lock_bit:
   in R16, PLLCSR
   sbrs R16, 0
   rjmp lock_bit
   lds R24, USBCON 
   cbr R24, 1 << FRZCLK 
   sts USBCON, R24
   lds R24, UDINT
   cbr R24, 1 << WAKEUPI
   sts UDINT, R24
   lds R24, UDIEN
   cbr R24, 1 << WAKEUPE
   sts UDIEN, R24
   sts UENUM, R1
   inc R2
vihod_ug:
   reti

;------------------------------------------------
; Обработчик прерывания конечной точки 
;------------------------------------------------
USB_ENDPOINT:
   lds R24, UEINT
   cpi R24, 1 << EP0
   brne ep_1
   rjmp ep_0

ep_1:
   cpi R24, 1 << EP1
   brne ep_2
   rjmp ep_in

ep_2:
   cpi R24, 1 << EP2
   brne ep_no
   rjmp ep_out

ep_no:
   rjmp vihod_ep

ep_0:
; Управляющая конечная точка
   lds R17, UEDATX
   lds R16, UEDATX

   cpi R16, CMD_USB_GET_DESCRIPTOR
   brne next_0
   rjmp get_dsc

next_0:
   cpi R16, CMD_USB_SET_ADDRESS
   brne next_1
   rjmp set_adr

next_1:
   cpi R16, 0x09
   brne next_2
   cpi R17, 0x00
   brne next_2
   rjmp set_cfg
   
next_2:
   cpi R16, 0x0A
   brne next_3
   cpi R17, 0x21
   brne next_3
   rjmp set_idle

next_3:
   rjmp n_3

get_dsc:
   cpi R17, 0x80         ;Get_Stand_device
   brne sl_1
   rjmp stand_desc

sl_1:
   cpi R17, 0x81
   brne sl_2
   rjmp int_desc
   
sl_2:
   rjmp vihod_ep
   
stand_desc:
   rcall read_buff
   cpi R19, 1
   brne n_1
   rjmp d_dev

n_1:
   cpi R19, 2
   brne n_2
   rjmp d_con

n_2:
   cpi R19, 3
   brne n_4
   rjmp d_str
   
n_3:
   lds R24, UEINTX
   cbr R24, 1 << RXSTPI
   sts UEINTX, R24
n_4:
   lds R24, UEINTX
   sbrs R24, 0
   rjmp n_4
   lds R24, UECONX
   sbr R24, 1 << STALLRQ
   sts UECONX, R24
   rjmp vihod_ep

d_dev:
   lds R24, UEINTX
   sbrs R24, 0
   rjmp d_dev
   ldi R31, high(dev_desc*2)
   ldi R30, low(dev_desc*2)
out_data_d:
   lpm R24, Z+
   sts UEDATX, R24
   cpi R30, 18
   brne out_data_d
   lds R24, UEINTX
   cbr R24, 1 << TXINI
   sts UEINTX, R24
   rcall w_nakout
   rjmp vihod_ep

d_con:
   lds R24, UEINTX
   sbrs R24, 0
   rjmp d_con
   ldi R31, high(con_desc*2)
   ldi R30, low(con_desc*2)
   clr R23
   cpi R22, 9
   breq con_drv
out_dat_c:
   lpm R24, Z+
   sts UEDATX, R24
   inc R23
   cpi R23, 32
   brne out_dat_c
   lds R24, UEINTX
   cbr R24, 1 << TXINI
   sts UEINTX, R24
d_con_1:
   lds R24, UEINTX
   sbrs R24, 0  
   rjmp d_con_1
out_dat_c1:
   lpm R24, Z+
   sts UEDATX, R24
   inc R23
   cpi R23, 41
   brne out_dat_c1
   lds R24, UEINTX
   cbr R24, 1 << TXINI
   sts UEINTX, R24
   rcall w_nakout
   rjmp vihod_ep
   
con_drv:
   lpm R24, Z+
   sts UEDATX, R24
   inc R23
   cpi R23, 9
   brne con_drv
   lds R24, UEINTX
   cbr R24, 1 << TXINI
   sts UEINTX, R24
   rcall w_nakout
   rjmp vihod_ep

d_str:
   cpi R18, 0
   brne s_1
   ldi R31, high(str_0*2)
   ldi R30, low(str_0*2)
   ldi R20, 4
   rjmp d_str_1

s_1:
   cpi R18, 1
   brne s_2
   ldi R31, high(str_1*2)
   ldi R30, low(str_1*2)
   ldi R20, 12
   rjmp d_str_1

s_2:
   cpi R18, 2
   brne s_3
   ldi R31, high(str_2*2)
   ldi R30, low(str_2*2)
   ldi R20, 32
   rjmp d_str_1

s_3:
   cpi R18, 3
   brne s_4
   ldi R31, high(str_3*2)
   ldi R30, low(str_3*2)
   ldi R20, 10
   rjmp d_str_1

s_4:
   rjmp vihod_ep

d_str_1:
   lds R24, UEINTX 
   sbrs R24, 0
   rjmp d_str_1
out_st:
   lpm R24, Z+
   sts UEDATX, R24
   dec R20
   brne out_st
   lds R24, UEINTX
   cbr R24, 1 << TXINI
   sts UEINTX, R24
   cpi R18, 2
   breq d_str_2
   rcall w_nakout
   rjmp vihod_ep

d_str_2:
   lds R24, UEINTX
   sbrs R24, 0
   rjmp d_str_2
out_st_1:
   lpm R24, Z+
   sts UEDATX, R24
   inc R20
   cpi R20, 2
   brne out_st_1
   lds R24, UEINTX
   cbr R24, 1 << TXINI
   sts UEINTX, R24
   rcall w_nakout
   rjmp vihod_ep

int_desc:
   rcall read_buff
   cpi R19, 0x22
   breq n_5
   rjmp vihod_ep

n_5:
   cpi R22,0x62
   breq d_rep_1
   rjmp vihod_ep

d_rep_1:
   sbi PORTD, 4
   lds R24, UEINTX
   sbrs R24, 0
   rjmp d_rep_1
   clr R23
   ldi R31, high(rep_desc*2)
   ldi R30, low(rep_desc*2)
out_dat_r:
   lpm R24, Z+
   sts UEDATX, R24
   inc R23
   cpi R23, 32
   brne out_dat_r
   lds R24, UEINTX
   cbr R24, 1 << TXINI
   sts UEINTX, R24
d_rep_2:
   lds R24, UEINTX   
   sbrs R24, 0 
   rjmp d_rep_2
out_dat_r1:
   lpm R24, Z+
   sts UEDATX, R24
   inc R23
   cpi R23, 34
   brne out_dat_r1
   lds R24, UEINTX
   cbr R24, 1 << TXINI
   sts UEINTX, R24
   rcall w_nakout
   ldi R16, 2
   sts UENUM, R16
   ldi R24, 1 << RXOUTE
   sts UEIENX, R24
   rjmp vihod_ep

ep_out:
   lds R24, UEINTX
   sbrs R24, 2           ;RXOUTI
   rjmp ep_out
   sbrs R24, 7           ;FIFOCON
   rjmp ep_out
   lds R24, UEINTX
   cbr R24, 1 << RXOUTI
   sts UEINTX, R24
   lds R3, UEDATX
   lds R4, UEDATX
   lds R5, UEDATX
   lds R6, UEDATX
   lds R7, UEDATX
   lds R8, UEDATX
   lds R9, UEDATX
   lds R10, UEDATX
   lds R24, UEINTX
   cbr R24, 1 << FIFOCON
   sts UEINTX, R24
   ldi R16, 1
   sts UENUM, R16
   ldi R24, 1 << TXINE
   sts UEIENX, R24
   rjmp vihod_ep 

ep_in:
   lds R24, UEINTX
   sbrs R24, 7       ;FIFOCON
   rjmp ep_in
   sbrs R24, 0       ;TXINI
   rjmp ep_in
   sts UEDATX, R3
   sts UEDATX, R4
   sts UEDATX, R5
   sts UEDATX, R6
   sts UEDATX, R7
   sts UEDATX, R8
   sts UEDATX, R9
   sts UEDATX, R10
   lds R24, UEINTX
   cbr R24, 1 << TXINI
   sts UEINTX, R24
   cbr R24, 1 << FIFOCON
   sts 0x00E8, R24
no_in:
   lds R24, UEINTX
   sbrs R24, 0       ;TXINI
   rjmp no_in
   sbrs R24, 7       ;FIFOCON
   rjmp no_in
   ldi R24, 1 << RXOUTE
   sts UEIENX, R24
   ldi R16, 2
   sts UENUM, R16
   rjmp vihod_ep

set_adr:
   lds R24, UEDATX  
   andi R24, 0x7F
   sts UDADDR, R24  
   lds R24, UEINTX  
   cbr R24, 1 << RXSTPI
   sts UEINTX, R24
   lds R24, UEINTX
   sbrs R24, 0   
   rjmp vihod_ep
   cbr R24, 1 << TXINI
   sts UEINTX, R24
vt:
   lds R24, UEINTX
   sbrs R24, 0
   rjmp vt
   lds R24, UDADDR
   ori R24, 0x80
   sts UDADDR, R24
   rjmp vihod_ep

set_cfg:
   lds R24, UEINTX
   cbr R24, 1 << RXSTPI
   sts UEINTX, R24
s_cfg:
   lds R24, UEINTX
   sbrs R24, 0
   rjmp s_cfg
   cbr R24, 1 << TXINI
   sts UEINTX, R24
   ldi R16, 1
   sts UENUM, R16
   lds R16, UECONX  
   sbrs R16, 0
   ori R16, 0x01
   sts UECONX, R16 
   ldi R16, (1 << EPTYPE1)+(1 << EPTYPE0)+(1 << EPDIR)
   sts UECFG0X, R16
   ldi R16, 0x02
   sts UECFG1X, R16
test_c_ep1:
   lds R16, UESTA0X 
   sbrs R16, 7 
   rjmp test_c_ep1
   ldi R16, 2
   sts UENUM, R16
   lds R16, UECONX
   sbrs R16, 0
   ori R16, 0x01
   sts UECONX, R16
   ldi R16, (1 << EPTYPE1)+(1 << EPTYPE0)+(0 << EPDIR)
   sts UECFG0X, R16 
   ldi R16, 0x02
   sts UECFG1X, R16
test_c_ep2:
   lds R16, UESTA0X
   sbrs R16, 7 
   rjmp test_c_ep2
   sts UENUM, R1  
   rjmp vihod_ep

set_idle:
   lds R24, UEINTX 
   cbr R24, 1 << RXSTPI
   sts UEINTX, R24
   lds R24, UEINTX
   sbrs R24, 0   
   rjmp vihod_ep
   cbr R24, 1 << TXINI
   sts UEINTX, R24
   tst R2
   breq vihod_ep
   clr R2
   ldi R16, 2
   sts UENUM, R16
vihod_ep:
   reti

conf_ep_0:
   sts UENUM, R1
   lds R16, UECONX  
   sbrs R16, 0
   ori R16, 0x01
   sts UECONX, R16
   ldi R16, (0 << EPTYPE1)+(0 << EPTYPE0)+(0 << EPDIR)
   sts UECFG0X, R16
   ldi R16, 0x22
   sts UECFG1X, R16
test_c_ep0:
   lds R16, UESTA0X
   sbrs R16, 7           ;если CFGOK=1 - EP0 сконфигурирована - идем дальше.
   rjmp test_c_ep0
   ret

w_nakout:
   lds R24, UEINTX
   sbrs R24, 4
   rjmp w_nakout
   cbr R24, 1 << NAKOUTI
   sts UEINTX, R24
w_out:
   lds R24, UEINTX
   sbrs R24, 2
   rjmp w_out
   cbr R24, 1 << RXOUTI
   sts UEINTX, R24
   ret

read_buff:
   lds R18, UEDATX    ;String_type
   lds R19, UEDATX    ;Desc_type=01
   lds R20, UEDATX    ;Dummy_byte
   lds R21, UEDATX    ;Dummy_byte
   lds R22, UEDATX    ;LSB(wLength)
   lds R23, UEDATX    ;MSB(wLength)
   lds R24, UEINTX
   cbr R24, 1 << RXSTPI
   sts UEINTX, R24
   ret
   
.ORG $300
dev_desc:
   .DB 0x12,0x01,0x00,0x02,0x00,0x00,0x00,0x20,0xEB,0x03
   .DB 0x13,0x20,0x00,0x10,0x01,0x02,0x03,0x01
con_desc:
   .DB 0x09,0x02,0x29,0x00,0x01,0x01,0x00,0x80,0x32,0x09
   .DB 0x04,0x00,0x00,0x02,0x03,0x00,0x00,0x00,0x09,0x21
   .DB 0x00,0x01,0x00,0x01,0x22,0x22,0x00,0x07,0x05,0x81
   .DB 0x03,0x08,0x00,0x0F,0x07,0x05,0x02,0x03,0x08,0x00
   .DB 0x0F
str_0:
   .DB 0x04,0x03,0x09,0x04
str_1:
   .DB 0x0C,0x03,0x41,0x00,0x54,0x00,0x4D,0x00,0x45,0x00
   .DB 0x4C,0x00
str_2:
   .DB 0x22,0x03,0x41,0x00,0x56,0x00,0x52,0x00,0x20,0x00
   .DB 0x55,0x00,0x53,0x00,0x42,0x00,0x20,0x00,0x48,0x00
   .DB 0x49,0x00,0x44,0x00,0x20,0x00,0x44,0x00,0x45,0x00
   .DB 0x4D,0x00,0x4F,0x00
str_3:
   .DB 0x0A,0x03,0x30,0x00,0x30,0x00,0x30,0x00,0x30,0x00
rep_desc:
   .DB 0x06,0x00,0xFF,0x09,0x00,0xA1,0x01,0x09,0x00,0x15
   .DB 0x00,0x26,0xFF,0x00,0x75,0x08,0x95,0x08,0x81,0x02
   .DB 0x09,0x00,0x15,0x00,0x26,0xFF,0x00,0x75,0x08,0x95
   .DB 0x08,0x91,0x02,0xC0

.EXIT

[Словарик]

ADC Analog-to-Digital Converter, АЦП, аналого-цифровой преобразователь.

bit stuffing вставка бит по специальному алгоритму в последовательном потоке бит на линии. Применяется для целей синхронизации или уменьшения/устранения постоянной составляющей в сигнале.

bus powered devices устройства USB, получающие питание +5V от шины USB (линия VBUS). Питающее напряжение на линии VBUS формирует хост.

daisy chained термин, относящийся к линейной (иногда кольцевой) топологии сети. Иногда такую топологию называют шлейфовой. Дословный перевод daisy chain – "цепочка маргариток", что означает венок из маргариток.

downstream нисходящее соединение. Такое соединение имеет место для хоста по отношению к устройству USB.

Endpoint, конечная точка специфическое понятие стандарта USB, символизирующее источник или приемник потока данных.

EPx EndPoint (конечная точка) номер x.

errata исправления, добавления к стандарту.

Feature фича, какая-нибудь возможность (особенность) устройства.

Feature Selector селектор фичи, число, от которого зависит выбор какой-нибудь возможности (особенности) устройства.

FLASH энергонезависимая память для хранения программ или данных.

FrameWork фреймворк, рабочая среда, рабочее окружение.

handshaking "рукопожатие", процедура установления связи.

Host хост, главное устройство на шине. Обычно это компьютер.

ICD In Circuit Debug, внутрисхемная отладка.

ISR Interrupt Service Routine, подпрограмма обработки прерывания.

ISP In-System Programming, программирование в системе. Так называется последовательный интерфейс программирования микроконтроллера.

Latency латентность, время ожидания на обработку, время задержки.

LDO regulator, Low Dropout регулятор, имеющий маленькое падение напряжения, достаточное для нормальной работы (стабилизации).

LSB Least Significant Byte, младший значащий байт. Иногда, в зависимости от контекста, это может относиться к биту, т. е. будет означать младший значащий бит.

MSB Most Significant Byte, старший значащий байт. Иногда, в зависимости от контекста, это может относиться к биту, т. е. будет означать старший значащий бит.

OTP ROM однократно программируемая память (обычно для программы).

Padding дополнение байтами до нужного количества (обычно нулевыми байтами).

payload полезная нагрузка, передаваемые в потоке данные.

PCB печатная плата.

peer-to-peer возможность обмена данными между равноправными устройствами.

Pipe(s) буквальный перевод "труба" ("трубы"). Означает специальным образом сформированные частные потоки данных через интерфейс USB. Иногда pipe называют каналом.

plug штеккер, коннектор на конце кабеля или на торце устройства USB (например, флешки). Этот коннектор втыкается в сокет, который обычно ставится на корпусе устройства (компьютера или периферии USB).

pull up, pull-up нагрузочный для сигнальной линии резистор, подключенный между линией сигнала и плюсом питания.

pull down, pull-down нагрузочный для сигнальной линии резистор, подключенный между линией сигнала и землей.

self powered devices устройства USB, питающиеся от отдельного источника (не от шины USB).

socket сокет, разъем на корпусе компьютера или периферии USB, куда втыкается plug (коннектор).

status reporting получение информации о статусе устройства.

tiered star топология многоярусная звезда.

token символ.

Token Packet символ, показывающий, какие дальше идут данные.

trade-off компромисс.

upstream восходящее соединение. Такое соединение имеет место для устройства USB по отношению к хосту.

Автор: Каменяр Сергей, batonator@yandex.ru.

[Ссылки и литература]

1. Чекунов Д. Программисту USB-устройств.
2. Немоляев А.В. Популярно о USB.
3. Агуров П.В. - Интерфейс USB. Практика использования и программирования.
4. Макетная плата AVR-USB162.
5. Ю. Магда. Ассемблер. Разработка и оптимизация Windows-приложений.
6. V-USB - A Firmware-Only USB Driver for Atmel AVR Microcontrollers site: obdev.at.
7. USB in a NutShell - путеводитель по стандарту USB.
8. LUFA - бесплатная библиотека USB для микроконтроллеров Atmel AVR.
9. Библиотеки для управления устройствами USB HID.
10. Engbedded Atmel AVR® Fuse Calculator site:engbedded.com.
11. Как настроить RadAsm под MASM32 site:devprog.wordpress.com.