Вот и закончился трехнедельный марафон изучения USB, определенно продвижение есть, пусть и небольшое. Наверняка некоторые поворчат — снова не разобрался в сути вопроса, а уже пишешь статью. В общем все что я делаю — делается на основе личного интереса, если будет великая цель разобраться досконально, значит будем разбираться.
Вообще я очень был удивлен, когда начал изучать тему USB, да общие слова можно прочитать на каждом углу, а вот куда то глубже — информацию приходится собирать по крупицам, понятно что дело может затянуться на долго, я не могу себе позволить писать статьи по полгода. Поэтому что есть то есть, если мои потуги вдохновят кого то, или кому нибудь есть что сказать по этому поводу — милости просим. Если найдется причина дополнить — дополним.
Итак, начнем с общеизвестных вещей. Что же такое USB? На физическом уровне (можно пощупать) — это разъем, через который мы можем подключать разные типы устройств. Это могут быть мышки, клавиатуры, джойстики, вентиляторы, жесткие диски, флешки, звуковые карты, осциллографы, модемы и прочее, т.е. вариантов очень много.
С точки зрения самого разъема, должны присутствовать 4 провода: +5В, GND, D+, D-. Таким образом от USB(+5В и GND) можно питать различные устройства рассчитанные на 5В, но нужно учесть что есть ограничение по потребляемому току. По D+ D- передаются данные. На длину соединительного кабеля влияет скорость обмена и наличие экрана.
Важно знать, что кроме того что это разъем, это еще и соответствующее железо. Существует несколько версий USB и если ваша материнская плата поддерживает USB 2.0, а подключаемое устройство 1.1, то работать они будут на 1.1.
Существует градация по скорости:
Low-speed: 10—1500 Кбит/c
Full-speed: 0,5—12 Мбит/с
High-speed: 25—480 Мбит/с
Разница между 2.0 и USB1.1 в том, что последние не поддерживают High-speed, тем не менее эту версию активно юзают, например в той же библиотеке V-USB, которая позволяет AVR микроконтроллерам не имеющим железа, использовать программную версию USB(ногодрыг).
Передача данных организована так, что один всегда ведущий(host), второй ведомый. Поэтому если вы умудритесь подключить к телефону флешку, но при этом в телефоне не реализована функция хоста, то флешку вы никогда не увидите. Нужно понимать что это реализовано в железе. Поэтому если вам нужен телефон умеющий читать внешние накопители, то придется искать такой телефон, который это умеет.
Но мало иметь железо, любое USB устройство нуждается в программном обеспечении, иначе как возможно подключить сразу столько устройств в одно гнездо. Для этого нужен драйвер позволяющий правильно распознавать, что это за устройство и как распорядиться его ресурсами. Некоторые устройства не требуют дополнительных драйверов — они имеются в системе и называются стандартными, некоторые требуют отдельной установки, но важно понимать — если нет драйвера, то ничего работать не будет.
Как же система понимает какой драйвер нужно подсунуть? Очень просто, каждое устройство имеет уникальный код производителя VID и код устройства PID. При подключении девайсина передает эти данные хосту, а хост уже сам устанавливает нужные дрова. Как узнать какие VID и PID мне нужно задать? Проще всего — выдергивать из каких то примеров. Как узнать их значения в системе? Посмотреть в диспетчере устройств
Но дрова это только прослойка, если у вас некое уникальное устройство и ваша операционная система ничего о нем не знает, то нужна будет программа которая обеспечит общение между устройством и пользователем. Например, какая нибудь миди клавиатура, драйвер обеспечит передачу данных, а обработку звуков должна обеспечить некая музыкальная программа, которая по нажатию клавиш будет выдавать звук.
С точки зрения микроконтроллера есть несколько путей:
если ваш микроконтроллер не поддерживает USB: ваш путь V-USB это библиотека для AVR микроконтроллеров.
Если поддерживает USB:
— брать примеры с сайта атмела из апнотов.
— брать примеры CAVR.
— брать примеры из библиотеки LUFA
Все три варианта с точки зрения документации говенные. С атмеловскими даже пробовать не стал. Примеры CAVR, у меня заработали — выбираешь генератором кода пример, создаешь, правишь под свои нужды. В статье про CDC процесс описан, поэтому нет смысла повторяться. Намного больший интерес представляет LUFA, это либа с открытым кодом, бесплатная, выпущена в качестве дополнения к Atmel Studio. Подозреваю, что примеры CAVR сделаны на основе именно луфы, больно уж список примеров похож. Весь прикол этой либы что открываешь нужный проект — правишь под себя и юзаешь.
Итак поехали, устанавливаем либу через Extension manager, придется регаться
В результате в меню New появится New example project запускаем его и увидим кучу примеров по USB
Пример 1. CDC
Об этой теме мы уже разговаривали, на всякий случай упомяну, выбираем Virtual serial CDC demo, создаем проект.
Вначале можно охренеть от проекта, но на самом деле юзать — проще некуда, после создания проекта, в настройках выбираем свой мк, у меня at90usb162, далее открываем VirtualSerial.c и все фразы касаемые LED и Joystik вычищаем ибо эта хрень относится к плате AT90USBKey, ибо все примеры заточены под эту плату. Все остальное не трогаем. В основном цикле пишем отправку строки hello, через fputs, т.е. ничем не отличается от работы с обычным юартом. После загрузки в мк, устанавливаем драйвер будет лежать из папки проекта
for (;;) { /* Must throw away unused bytes from the host, or it will lock up while waiting for the device */ CDC_Device_ReceiveByte(&VirtualSerial_CDC_Interface); CDC_Device_USBTask(&VirtualSerial_CDC_Interface); USB_USBTask(); /* Write the string to the virtual COM port via the created character stream */ fputs("hello\n\r", &USBSerialStream); } |
Пример 2. HID mouse
Следующий пример мышь, стандартный драйвер. Опять же что касается джойстика и светодиодов можно вычищать. Внутри mouse.c нас ожидает такая функция
bool CALLBACK_HID_Device_CreateHIDReport(USB_ClassInfo_HID_Device_t* const HIDInterfaceInfo, uint8_t* const ReportID, const uint8_t ReportType, void* ReportData, uint16_t* const ReportSize) { USB_MouseReport_Data_t* MouseReport = (USB_MouseReport_Data_t*)ReportData; uint8_t JoyStatus_LCL = Joystick_GetStatus(); uint8_t ButtonStatus_LCL = Buttons_GetStatus(); if (JoyStatus_LCL & JOY_UP) MouseReport->Y = -1; else if (JoyStatus_LCL & JOY_DOWN) MouseReport->Y = 1; if (JoyStatus_LCL & JOY_LEFT) MouseReport->X = -1; else if (JoyStatus_LCL & JOY_RIGHT) MouseReport->X = 1; if (JoyStatus_LCL & JOY_PRESS) MouseReport->Button |= (1 << 0); if (ButtonStatus_LCL & BUTTONS_BUTTON1) MouseReport->Button |= (1 << 1); *ReportSize = sizeof(USB_MouseReport_Data_t); return true; } |
Это все таже обработка клавиш установленных на плате AT90USBKey. Допустим мы хотим по нажатию PB0 двигать курсов вверх, тогда функция станет такой:
bool CALLBACK_HID_Device_CreateHIDReport(USB_ClassInfo_HID_Device_t* const HIDInterfaceInfo, uint8_t* const ReportID, const uint8_t ReportType, void* ReportData, uint16_t* const ReportSize) { USB_MouseReport_Data_t* MouseReport = (USB_MouseReport_Data_t*)ReportData; if (PINB.0 == 0) MouseReport->Y = -1; *ReportSize = sizeof(USB_MouseReport_Data_t); return true; } |
Я думаю идея понятна.
Пример 3. HID keyboard
Следующий пример клавиатура, стандартный драйвер. Кстати одно устройство может совмещать в себе и мышь и клавиатуру.
С клавиатурой тот же прикол что и с мышью, имеется функция посылающая сканкоды нажатых клавиш в зависимости от нажатий джойстика на плате.
bool CALLBACK_HID_Device_CreateHIDReport(USB_ClassInfo_HID_Device_t* const HIDInterfaceInfo, uint8_t* const ReportID, const uint8_t ReportType, void* ReportData, uint16_t* const ReportSize) { USB_KeyboardReport_Data_t* KeyboardReport = (USB_KeyboardReport_Data_t*)ReportData; uint8_t JoyStatus_LCL = Joystick_GetStatus(); uint8_t ButtonStatus_LCL = Buttons_GetStatus(); uint8_t UsedKeyCodes = 0; if (JoyStatus_LCL & JOY_UP) KeyboardReport->KeyCode[UsedKeyCodes++] = HID_KEYBOARD_SC_A; else if (JoyStatus_LCL & JOY_DOWN) KeyboardReport->KeyCode[UsedKeyCodes++] = HID_KEYBOARD_SC_B; if (JoyStatus_LCL & JOY_LEFT) KeyboardReport->KeyCode[UsedKeyCodes++] = HID_KEYBOARD_SC_C; else if (JoyStatus_LCL & JOY_RIGHT) KeyboardReport->KeyCode[UsedKeyCodes++] = HID_KEYBOARD_SC_D; if (JoyStatus_LCL & JOY_PRESS) KeyboardReport->KeyCode[UsedKeyCodes++] = HID_KEYBOARD_SC_E; if (ButtonStatus_LCL & BUTTONS_BUTTON1) KeyboardReport->KeyCode[UsedKeyCodes++] = HID_KEYBOARD_SC_F; if (UsedKeyCodes) KeyboardReport->Modifier = HID_KEYBOARD_MODIFIER_LEFTSHIFT; *ReportSize = sizeof(USB_KeyboardReport_Data_t); return false; } |
Нам нужно постоянно посылать одну клавишу A по нажатию PB0.
bool CALLBACK_HID_Device_CreateHIDReport(USB_ClassInfo_HID_Device_t* const HIDInterfaceInfo, uint8_t* const ReportID, const uint8_t ReportType, void* ReportData, uint16_t* const ReportSize) { USB_KeyboardReport_Data_t* KeyboardReport = (USB_KeyboardReport_Data_t*)ReportData; uint8_t UsedKeyCodes = 0; if (PINB.0==0) { KeyboardReport->KeyCode[UsedKeyCodes++] = HID_KEYBOARD_SC_A; } if (UsedKeyCodes) KeyboardReport->Modifier = HID_KEYBOARD_MODIFIER_LEFTSHIFT; *ReportSize = sizeof(USB_KeyboardReport_Data_t); return false; } |
Т.е. сначала уходит нажатие, потом отжатие. Также есть пример джойстика, он аналогичен клавиатуре.
Ну и пожалуй самый вкусный пример Пример 4. Generic HID. Теоретически он позволяет посылать и принимать произвольные данные. Из всего нам важны две функции. В первой мы производим прием данных от хоста, во второй отправку.
//здесь производится обработка принимаемых от хоста данных void ProcessGenericHIDReport(uint8_t* DataArray) { if (DataArray[0] == 1) { PORTD.0 = 1; } } //здесь мы отправляем данные хосту void CreateGenericHIDReport(uint8_t* DataArray) { DataArray[0] = temperature; } |
Таким образом вся работа заключается в том, чтобы внутри одной из функций прочитать массив Data, а в другой записать. Такой массив в который производится прием и передача называется конечной точкой (Endpoint). В даташите на at90usb162 сказано, что Programmable maximum packet size from 8 to 64 bytes — максимальный размер пакета 64 байта, пакет передается 1 раз за мс, т.е. с одной конечной точки можно выжать 64кбайта в секунду. Но здесь есть тонкости которые, пока не понятны, из даташита:
– 1 endpoint of 64 bytes max,
(default control endpoint)
– 2 endpoints of 64 bytes max, (one bank)
– 2 endpoints of 64 bytes max, (one or two banks)
Допустим, endpoint 0 каких то внутренних нужд, endpoint 1 для приема, endpoint 2 для передачи это минимум для того чтобы организовать обмен в обе стороны, так во что насчет еще двух конечных точек, подозреваю что могу их использовать ибо:
The USB device controller supports full speed data transfers. In addition to the default control
endpoint, it provides four other endpoints, which can be configured in control, bulk, interrupt or isochronous modes:
• Endpoint 0: programmable size FIFO up to 64 bytes, default control endpoint
• Endpoints 1 and 2: programmable size FIFO up to 64 bytes.
• Endpoints 3 and 4: programmable size FIFO up to 64 bytes with ping-pong mode.
На вопрос как, история тихо умалчивает 🙂
В тексте выше видно что каждая конечная точка может быть настроена на несколько режимов:
bulk — для передачи больших объемов данных, есть проверка на ошибки, время доставки не ограничено.
control transfer — для настройки устройства и получения информации в процессе работы, по умолчанию endpoint0
interrupt transfer — для передачи небольших данных, устройство опрашивается с некой периодичностью, время доставки ограничено
isochronous transfer — для передачи больших объемов, возможна потеря данных
По умолчанию в Generic Hid установлена Interrupt endpoint, когда я экпериментировал с стм32 мне показалось что с bulk работать на много проще, я понимаю что настройки производятся в Descriptors.c, но как это сделать пока не догнал. Также довольно подозрительно то, что в CAVR в визарде нельзя сменить Endpoint с interrupt на что то другое, что как бе намекает откуда эти примеры. В общем эту часть оставим до лучших времен.
Пожалуй оставшаяся часть это ПК. Лучше чем libusbdotnet я ничего не нашел, скачиваем ее. Перед использованием устройства в своих программах нужно установить фильтр install-filter-win, это прога находится в папке libusbdotnet, видимо эта программка позволяет использовать ресурсы USB. Интересная фишка со стороны мк, если в репорте отсылать одни и те же данные, то они отошлются 1 раз, дальше данные уходить не будут, поэтому нужно как то чередовать данные, хотя бы так
bool CALLBACK_HID_Device_CreateHIDReport(USB_ClassInfo_HID_Device_t* const HIDInterfaceInfo, uint8_t* const ReportID, const uint8_t ReportType, void* ReportData, uint16_t* const ReportSize) { uint8_t* Data = (uint8_t*)ReportData; static x = 0; if (x == 0) { Data[0]='h'; Data[1]='e'; Data[2]='l'; Data[3]='l'; Data[4]='o'; Data[5]=' '; Data[6]=' '; Data[7]=' '; x = 1; } else { Data[0]='h'; Data[1]='e'; Data[2]='l'; Data[3]='l'; Data[4]='o'; Data[5]='!'; Data[6]=' '; Data[7]=' '; x = 0; } *ReportSize = GENERIC_REPORT_SIZE; return false; } |
Со стороны ПК запускаем пример из libusbdotnet из папки C:\Program Files\LibUsbDotNet\Src\Examples\Read.Only, чуть правим его под свой VID и PID.
using System; using System.Text; using LibUsbDotNet; using LibUsbDotNet.Main; namespace Examples { internal class ReadPolling { public static UsbDevice MyUsbDevice; #region SET YOUR USB Vendor and Product ID! public static UsbDeviceFinder MyUsbFinder = new UsbDeviceFinder(0x03EB, 0x204F); #endregion public static void Main(string[] args) { ErrorCode ec = ErrorCode.None; try { // Find and open the usb device. MyUsbDevice = UsbDevice.OpenUsbDevice(MyUsbFinder); // If the device is open and ready if (MyUsbDevice == null) throw new Exception("Device Not Found."); // If this is a "whole" usb device (libusb-win32, linux libusb-1.0) // it exposes an IUsbDevice interface. If not (WinUSB) the // 'wholeUsbDevice' variable will be null indicating this is // an interface of a device; it does not require or support // configuration and interface selection. IUsbDevice wholeUsbDevice = MyUsbDevice as IUsbDevice; if (!ReferenceEquals(wholeUsbDevice, null)) { // This is a "whole" USB device. Before it can be used, // the desired configuration and interface must be selected. // Select config #1 wholeUsbDevice.SetConfiguration(1); // Claim interface #0. wholeUsbDevice.ClaimInterface(0); } // open read endpoint 1. UsbEndpointReader reader = MyUsbDevice.OpenEndpointReader(ReadEndpointID.Ep01); byte[] readBuffer = new byte[8]; int bytesRead; // If the device hasn't sent data in the last 5 seconds, // a timeout error (ec = IoTimedOut) will occur. reader.Read(readBuffer, 5000, out bytesRead); Console.WriteLine("{0} bytes read", bytesRead); // Write that output to the console. Console.Write(Encoding.Default.GetString(readBuffer, 0, bytesRead)); Console.WriteLine("\r\nDone!\r\n"); } catch (Exception ex) { Console.WriteLine(); Console.WriteLine((ec != ErrorCode.None ? ec + ":" : String.Empty) + ex.Message); } finally { if (MyUsbDevice != null) { if (MyUsbDevice.IsOpen) { // If this is a "whole" usb device (libusb-win32, linux libusb-1.0) // it exposes an IUsbDevice interface. If not (WinUSB) the // 'wholeUsbDevice' variable will be null indicating this is // an interface of a device; it does not require or support // configuration and interface selection. IUsbDevice wholeUsbDevice = MyUsbDevice as IUsbDevice; if (!ReferenceEquals(wholeUsbDevice, null)) { // Release interface #0. wholeUsbDevice.ReleaseInterface(0); } MyUsbDevice.Close(); } MyUsbDevice = null; // Free usb resources UsbDevice.Exit(); } // Wait for user input.. Console.ReadKey(); } } } } |
Исходник в принципе понятен — читать данные, если за 5 секунд ничего не пришло, выдать ошибку.
В результате можно увидеть полученные от мк данные:
Увы сколько бы я не бился запустить запись данных на мк мне не удалось. В общем получилось не густо не жидко, желающие могут сделать на основе примеров луфы собственную мышь, клавиатуру, джойстик, usb-uart или сделать произвольное устройство, например термометр и выводить информацию на ПК, думаю это огромный плюс, надеюсь когда нибудь дополню статью записью и чтением GENERIC HID…
Ждем полный функционал 🙄
Такой вопрос: при создании проекта из примера автоматом нам в папку кидает все необходимые библиотеки, а где сам пример, файла типа main нету — значит нужно создать свой main.c и там уже писать все инклуды и главный цикл?
а, блин, просто VirtualSerial.c и VirtualSerial.h это не библиотека, а сам пример
А частоту кварца какую ставить в проеусе