В виду того, что нашел сразу несколько ссылок на свои топики по созданию клавиатуры и мышки PS/2 на Arduino, я решил написать простенький топик про создание на V-USB составного HID-устройства.
Я уже писал как запустить V-USB на Atmega, используя Atmel studio 6. Повторяться не буду. Просто опишу, что на Atmega можно легко эмулировать одновременно по USB и мышь, и клавиатуру! Да собственно аналогичным образом они эмулируются на любом другом контроллере, с поддержкой USB.
![](http://rukodelie-ds.ru/platy/usb_test.jpg)
Скажу лишь, что мой готовый пример для Atmega32a, не Arduino, хотя на arduino такое тоже возможно, но чутка сложнее. Первое на что надо обратить внимание - это, что я использовал стандартную обвязку для V-USB (см. документацию v-usb), с 12Mhz кристаллом. Параметры
Код: Выделить всё
#define USB_CFG_IOPORTNAME D
#define USB_CFG_DMINUS_BIT 4
#define USB_CFG_DPLUS_BIT 2
Имеются 4 кнопки и 4 светодиода. Светодиоды просто отображают включенный режим. 2 кнопки, сделаны просто ради эксперимента. Еще 2 кнопки запускают некие действия клавиатуры или мыши, в том числе одновременно. 1-я запускает мышку по кругу (пример от V-USB), а 2-я запускает печать текста клавиатурой.
Причем печать текста я сделал используя спец. комбинации для ввода символов. Из-за этого текст вводится медленно, но не зависит от включенной в текущий момент раскладки на компе. Попробуйте сами открыть блокнот, зажать левый ALT, набрать код 068, отпустить ALT и у Вас введется символ "D". У меня контроллер выводит текст: "DTViMS - Super and Nastya - beloved wife". Показывал жене - она оценила
![Smile :)](./images/smilies/icon_e_smile.gif)
В момент написания поста, при тесте, обнаружил что в windows 7x64 почему-то не везде корректно выводится текст, т.е. в блокноте, все хорошо, а вот в notepad++ пустые символы выводятся, хотя в ручную, мной, нормально получалось в него вывести символ "D", см. выше.
Итак. Если у Вас получилось запустить V-USB, то у Вас получится сделать и следующее. Нужно правильно составить HID-заголовок для нашего устройства. Любое HID устройство может быть составным и состоять из нескольких HID устройств, но будет отличаться обработка пакетов контроллером. Каждому устройству присваивается номер и каждый пакет для устройства и от устройства, как бы увеличивается на 1 байт. Это первый байт в пакете, который должен содержать номер устройства с которым идет диалог.
Сперва зададим заголовок:
Код: Выделить всё
PROGMEM const char usbHidReportDescriptor[119] = { /* USB report descriptor, size must match usbconfig.h */
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x06, // USAGE (Keyboard)
0xa1, 0x01, // COLLECTION (Application)
0x85, 0x01, // REPORT_ID (1)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)
0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x08, // REPORT_COUNT (8)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x08, // REPORT_SIZE (8)
0x81, 0x03, // INPUT (Cnst,Var,Abs)
0x95, 0x05, // REPORT_COUNT (5)
0x75, 0x01, // REPORT_SIZE (1)
0x05, 0x08, // USAGE_PAGE (LEDs)
0x19, 0x01, // USAGE_MINIMUM (Num Lock)
0x29, 0x05, // USAGE_MAXIMUM (Kana)
0x91, 0x02, // OUTPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x03, // REPORT_SIZE (3)
0x91, 0x03, // OUTPUT (Cnst,Var,Abs)
0x95, 0x05, // REPORT_COUNT (5)
0x75, 0x08, // REPORT_SIZE (8)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x65, // LOGICAL_MAXIMUM (101)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))
0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
0x81, 0x00, // INPUT (Data,Ary,Abs)
0xc0, // END_COLLECTION
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x02, // USAGE (Mouse)
0xa1, 0x01, // COLLECTION (Application)
0x09, 0x01, // USAGE (Pointer)
0xA1, 0x00, // COLLECTION (Physical)
0x85, 0x02, // REPORT_ID (2)
0x05, 0x09, // USAGE_PAGE (Button)
0x19, 0x01, // USAGE_MINIMUM
0x29, 0x03, // USAGE_MAXIMUM
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x95, 0x03, // REPORT_COUNT (3)
0x75, 0x01, // REPORT_SIZE (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x05, // REPORT_SIZE (5)
0x81, 0x03, // INPUT (Const,Var,Abs)
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x30, // USAGE (X)
0x09, 0x31, // USAGE (Y)
0x09, 0x38, // USAGE (Wheel)
0x15, 0x81, // LOGICAL_MINIMUM (-127)
0x25, 0x7F, // LOGICAL_MAXIMUM (127)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x03, // REPORT_COUNT (3)
0x81, 0x06, // INPUT (Data,Var,Rel)
0xC0, // END_COLLECTION
0xC0, // END COLLECTION
};
ВНИМАНИЕ!!! Этот заголовок - это самое главное для составного HID-устройства! Все написанное ниже это мой бред на тему о конкретной реализации на V-USB. Причем делал я это все достаточно давно и подзабыл уже что от куда и почему, потому что-то мог вспомнить неверно. На текущий момент предпочитаю использовать аппаратный USB на STM32 - это и дешевле и значительно надежнее. Есть у меня некая система мониторинга на Atmega8, подключенная по USB к компу как HID-устройство, так пришлось делать, чтобы оно мониторило само себя и, как только отваливается USB, контроллер перезагружается и работа восстанавливается, что работает успешно уже 2 года.
Фразой (кодом) "USAGE (Keyboard)" закодирован формат пакета для клавиатуры, а фразой "USAGE (Mouse)", соответственно мышь. "REPORT_ID (2)" - это и есть тот самый пресловутый идентификатор пакета (в данном случае мыши). Внутри раздела "COLLECTION (Physical)" описывается каждый байт в пакете. По этому описанию операционная система будет знать где какие данные Вы передаете с контроллера.
Если Вы изменили размер переменной "usbHidReportDescriptor[119]", не забудьте везде изменить этот параметр (в файле "usbconfig.h" константа "#define USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH 119".
В файле usbdrv.c есть также заголовок самого USB устройства как типа:
Код: Выделить всё
PROGMEM const char usbDescriptorConfiguration[] = { /* USB configuration descriptor */
9, /* sizeof(usbDescriptorConfiguration): length of descriptor in bytes */
USBDESCR_CONFIG, /* descriptor type */
18 + 7 * USB_CFG_HAVE_INTRIN_ENDPOINT + 7 * USB_CFG_HAVE_INTRIN_ENDPOINT3 +
(USB_CFG_DESCR_PROPS_HID & 0xff), 0,
/* total length of data returned (including inlined descriptors) */
1, /* number of interfaces in this configuration */
1, /* index of this configuration */
0, /* configuration name string index */
#if USB_CFG_IS_SELF_POWERED
(1 << 7) | USBATTR_SELFPOWER, /* attributes */
#else
(1 << 7), /* attributes */
#endif
USB_CFG_MAX_BUS_POWER/2, /* max USB current in 2mA units */
/* interface descriptor follows inline: */
9, /* sizeof(usbDescrInterface): length of descriptor in bytes */
USBDESCR_INTERFACE, /* descriptor type */
0, /* index of this interface */
0, /* alternate setting for this interface */
USB_CFG_HAVE_INTRIN_ENDPOINT + USB_CFG_HAVE_INTRIN_ENDPOINT3, /* endpoints excl 0: number of endpoint descriptors to follow */
USB_CFG_INTERFACE_CLASS,
USB_CFG_INTERFACE_SUBCLASS,
USB_CFG_INTERFACE_PROTOCOL,
0, /* string index for interface */
#if (USB_CFG_DESCR_PROPS_HID & 0xff) /* HID descriptor */
9, /* sizeof(usbDescrHID): length of descriptor in bytes */
USBDESCR_HID, /* descriptor type: HID */
0x01, 0x01, /* BCD representation of HID version */
0x00, /* target country code */
0x01, /* number of HID Report (or other HID class) Descriptor infos to follow */
0x22, /* descriptor type: report */
USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH, 0, /* total length of report descriptor */
#endif
#if USB_CFG_HAVE_INTRIN_ENDPOINT /* endpoint descriptor for endpoint 1 */
7, /* sizeof(usbDescrEndpoint) */
USBDESCR_ENDPOINT, /* descriptor type = endpoint */
(char)0x81, /* IN endpoint number 1 */
0x03, /* attrib: Interrupt endpoint */
8, 0, /* maximum packet size */
USB_CFG_INTR_POLL_INTERVAL, /* in ms */
#endif
#if USB_CFG_HAVE_INTRIN_ENDPOINT3 /* endpoint descriptor for endpoint 3 */
7, /* sizeof(usbDescrEndpoint) */
USBDESCR_ENDPOINT, /* descriptor type = endpoint */
(char)(0x80 | USB_CFG_EP3_NUMBER), /* IN endpoint number 3 */
0x03, /* attrib: Interrupt endpoint */
8, 0, /* maximum packet size */
USB_CFG_INTR_POLL_INTERVAL, /* in ms */
#endif
};
#endif
Здесь описаны конечные точки USB и какого рода и в каком направлении они ходят. Детали описывать не буду - читайте статьи и книги по USB, но в данном случае надо так. Конфиг взят из того же примера V-USB с мышкой и вроде не менялся, уже не помню точно, потому пусть просто побудет тута
![Smile :)](./images/smilies/icon_e_smile.gif)
У меня настроено использование 1-й конечной точки "#USB_CFG_HAVE_INTRIN_ENDPOINT 1", через нее ходит 8 байт к компу, и обратно, но обратно реально ходит 1 байт, в котором передаются состояния всяких scroll lock, Num lock и Caps lock. Если вспомним про "usbHidReportDescriptor", то для клавиатуры там расписаны INPUT и OUTPUT - это форматы пакетов для 1-й конечной точки. Соответственно, описывают данные передаваемые в направлении к компу и от него. Комп предает устройству состояние индикаторов (lock), а устройство их может установить, нажав соответствующие клавиши. Что-то тут (про INPUT и OUTPUT) я уже детали подзабыл: для подробностей топаем в документацию по USB. Конечная точка 3 не используется, а на большее V-USB и не способна.
Что еще надо знать?
в файле "hid_mouse_gcc.c" (основной файл проекта) есть ряд функций, которые будут вызываться библиотекой V-USB, если они определены в проекте. Если не определены, то V-USB отвечает на соответствующие запросы хостом "USB_NO_MSG".
"usbFunctionSetup()" - вызывается для получения/отправки setup-пакета или (она же) конечной точки 0. Используется для всех служебных USB пакетов. Тут лучше ничего не менять. А вот все что касается Конечной точки 1 бегает через функции "usbFunctionRead()" и "usbFunctionWrite()" - эти функции вызываются при отправке или получении данных. В примере, контроллер реагирует только на Num lock. Кстати, это единственная обратная связь от компа к клавиатуре, других, так сказать, пользовательских данных ОС клавиатуре не отправляет. Например, если к компу подключено несколько клавиатур, то при нажатии на одной из клав Num lock, ОС отправит всем клавиатурам сообщение, что надо включить соответствующий индикатор, а вернее статусы сразу всех индикаторов. Эта функция используется различными автономными устройствами эмулирующими клавиатуру для получения, как правило, одной команды для активации, например, двойное нажатие scroll lock на нормальной клаве и Ваше устройство начинает что-то печатать, или крутить мышкой, или еще что захотите...
Отправляются сообщения также и в ручную через функцию "usbSetInterrupt((void *)&reportBuffer, sizeof(reportBuffer));". Происходит это примерно так: Хост, т.е. комп, запрашивает устройство а не хочет ли оно что-то отправить (что вообще буфер на отправку пуст и готов к установке на отправку, говорит функция "usbInterruptIsReady()") и, если пользователь через функцию "usbSetInterrupt()" заполнил буфер данных, то V-USB эти данные отправит. "usbFunctionWrite()" вызывается когда хост отправляет порцию данных к устройству, а в нашем случае так приходят данные о состоянии индикаторов. "usbFunctionRead(uchar *data, uchar len)" вызывается для отправки данных хосту, необходимо заполнить массив "*data" и вернуть размер заполненных данных "return len;" (тоже что и "usbSetInterrupt", только по событию, по прерыванию). Как бы то ни было, но всегда отправку данных на комп инициализирует сам комп. Устройство само отправить ничего не сможет, пока комп не запросит эту информацию. Вы не можете сформировать произвольный пакет данных и отправить его на комп - это комп говорит, что ждет от устройства данные, а какие именно в какой момент - это как договоритесь
![Smile :)](./images/smilies/icon_e_smile.gif)
Код: Выделить всё
if(PINA & 0b00000001 && button1==0){
button1=1; //Кнопку отпустили
if(button1on){
button1on=0;
}else{
button1on=1;
}
}else if(!(PINA & 0b00000001) && button1) {
button1=0; //Кнопку нажали, сбросили флаг 1 раз
}
if(PINA & 0b00000100 && button2==0){
button2=1; //Кнопку отпустили
if(button2on){
button2on=0;
}else{
button2on=1;
}
}else if(!(PINA & 0b00000100) && button2) {
button2=0; //Кнопку нажали, сбросили флаг 1 раз
}
Тут в главной процедуре main обработчики нажатия кнопок. Тут видно, что одна кнопка висит на PA0, а вторая на PA2. Чтобы не было дребезга контактов я параллельно кнопкам поставил по конденсатору 0.1mF. Плюс в главном цикле полно разных задержек, потому нажатия кнопок обрабатываются четко.
Код: Выделить всё
testpoll++;
if(testpoll>3000){
PORTA |= 0b00010000;
cli();
usbDeviceDisconnect();
_delay_ms(100);
usbDeviceConnect();
sei();
testpoll=0;
//PORTA &= ~ 0b00010000;
}
Эта шняга отвечает за проблемы с USB. Это решение не самое лучшее, но большую часть проблем отлавливает по принципу WatchDog. Счетчик testpoll все время увеличивается. В любой функции обработки входящего пакета от хоста testpoll сбрасывается на ноль. Таким образом, если все хорошо, то счетчик не успевает дойти до критичных 3000. Если счетчик все-таки добежит до 3000, то запускается процедура переподключения USB.
Далее прикладываю весь проект для Atmel Studio. Предупреждаю, что весь проект тестовый, т.е. там куча какой-то шняги, которую я сам не помню для чего добавил - все тестировал, пробовал, учился. Потом отпала необходимость в конкретном решении, да и на STM32 мне понравилось работать больше. Контроллеры STM32 дешевле, толще (по возможностям, а не по размеру) и USB там почти везде есть аппаратный.