USB-polygon-5: Обмен по USB, поиск устройства
Подготовка
На текущий момент самодельная отладочная плата на контроллере AT90USB162 при подключении к ПК определяется в Windows, как USB MassStorage-устройство, попросту — флэшка, и даже получает букву диска. Надо каким-то образом начинать обмен реальными данными, не только служебными. Зажечь нужные светодиоды по команде с компьютера, получить на экране число нажатий кнопки на устройстве, что-нибудь подобное для начала.
Можно попробовать заставить плату прикинуться «настоящим» устройством с файлами и читать-писать эти «файлы» средствами операционной системы. Но это я отложу на попозже. Сперва попробую обмен с физическим устройством, а не с файлами на нем.
Для этого сперва надо подготовить инструмент. Придется писать некоторый код для компьютера. Остановлюсь пока на Windows по ряду причин. Основная причина среди них — у меня уже есть под рукой кое-какая заготовка, в которой можно подглядеть идеи. Эта заготовка мне не очень нравится, но работает, и на нее можно опираться.
Упомянутая заготовка написана на С++. Опять же, придется использовать API-функции операционной системы, что проще всего делать также на С++: в MSDN расписаны параметры функций и есть примеры, и ориентировано все на С++, а для, скажем, Delphi (или Lazarus) придется переписывать. Полагаю, и примеры на всякого рода форумах тоже проще будет найти на том же С++. А я уже давно на нем ничего приличного не писал, и версию от Microsoft ставить не желаю.
Зато у меня уже есть Eclipse, который используется для Java (в том числе, под Android) и таких проектов на Python, которые сложнее пары скриптовых файлов. Буду в качестве среды разработки использовать его же.
Однако Eclipse — это только среда, сам по себе он не будет ничего компилировать. А в качестве компилятора традиционно (хотя и не обязательно) в связке с ним используется GCC. Для Windows этот компилятор (строго говоря — семейство компиляторов) сам по себе (по крайней мере, пока что) не бывает, а появляется в составе линуксообразного окружения, MinGW или Cygwin. Причем, Cygwin у меня уже имеется, и используется в том числе при работе с этой самоделкой — выбор и настройка инструментария описана в одном из предыдущих выпусков.
Сделал пробный проектик под С++ и начал как-то с ним упражняться. Угрохал на это дело изрядно времени. Эх, стоило пошагово описывать все подводные камни на этом пути, нехилая лоция бы могла получиться… Короче, при всем моем уважении к этим продуктам (Eclipse и GCC), настройка их (особенно под Windows) — это замечательный способ провести долгие зимние вечера, если больше нечем их занять.
Темная тема оформления редактора кода не влияет на тему оформления остальных окон и панелей Eclipse, за них отвечает оконный менеджер, либо надо менять руками отдельно множество строк в конфигурации в разных местах и записывать при этом, что на что менял. Подтянуть настройки, сделанные в рабочем пространстве Java, для рабочего пространства С++ у меня так и не получилось. В настройках проекта надо не забывать добавлять опцию статической линковки некоторых библиотек, иначе не заработает на другой машине. Да и на этой же, впрочем, тоже. Надо не забывать указывать про с++11, чтобы иметь возможность использовать современные фишки языка. И все это и тому подобное вылезало достаточно внезапно и с недостаточно очевидными причинами, просто не знаю, что бы я делал без StackOverflow.
Наконец, что-то как-то заработало, стали получаться простые консольные приложения. В процессе выяснилось, что GCC в Cygwin и в MinGW имеют заметные отличия — бинарники библиотек отличаются, заголовочные файлы к этим библиотекам тоже. При попытке компиляции примеров, слизанных с форумов и сделанных в MS Visual Studio, компилятор из MinGW вызывал меньше проблем, так что пришлось устанавливать еще и этот набор софта.
Итак, для написания кода на ПК буду использовать ОС Windows (XP и 7), Eclipse, GCC из состава MinGW. Все это было постепенно установлено и худо-бедно настроено. Не так, чтобы работать было комфортно, но вполне удовлетворительно, а чисто визуальные примочки (типа стилей подсветки синтаксиса или темной цветовой темы) прикручу как-нибудь позже, если будет лишнее время и желание.
Дальше предполагаю такую последовательность действий:
- Обнаружить подключенные USB-устройства. Как вариант — сузить поиск до MassStorage-устройств. Поначалу плата-самоделка даже не нужна, хотя что-то типа флэшки желательно подключить. Сойдет и вмонтированный в корпус системника кардридер, который тоже USB-устройство.
- Среди подключенных устройств найти свое по какому-либо идентификатору. Тут уже плату надо подключить, но можно ничего в ней не модифицировать.
- Открыть это устройство для обмена. То есть, получить его дескриптор, который можно передать функциям Readfile/Writefile. Здесь тоже плата нужна.
- Выполнить обмен без ошибок. Вот на этом этапе скорее всего придется что-то править в программе контроллера отладочной платы.
- Использовать результаты обмена: считать состояние кнопок, зажечь определенные светодиоды. Тут дописывать программу контроллера придется однозначно, без вариантов.
- В процессе решать возникающие проблемы, обеспечивать совместимость хотя бы в двух версиях Windows.
Заготовка проекта
Запускаю Eclipse, создаю новый проект: File -> New -> C++ Project
, получаю заготовку исходного кода, выдающего в консоли сакраментальное «Hello, World!».
Сразу лезу в настройки проекта, потому что уже в курсе, что собранный код не запустится. Project -> Properties
, и в окне Properties for usb-polygon
в правой панели выбираю C/C++ Build -> Settings
, на вкладке Tool Settings
выбираю MinGW C++ Linker -> Miscellaneous
и в поле Linker flags
вписываю -static-libgcc -static-libstdc++
, иначе придется соответствующие библиотеки таскать вместе с проектом. Предпочитаю прилинковать статически.
Заодно на этой же вкладке выбираю GCC C++ Compiler -> Dialect
, и в поле Language standard
ставлю ISO C++11 (-srd=c++0x)
, пригодится.
Теперь можно и Project -> Build Project
, и Run As -> Local C/C++ Application
, и в консоли, представленной одним из окошек среды Eclipse, появится то, что просили в тринадцатой строчке автоматически сгенерированного кода, то есть, «!!!Hello, word!!!».
Создаю в папке проекта git-репозиторий и сохраняю это все в виде начального коммита.
Начинаем искать
Перед тем, как искать устройства USB, подключенные к компьютеру, стоит поискать в интернете, как это делают другие. Вариантов довольно много. Я порылся в форумах, нашел несколько работоспособных решений, остановился на одном, на фундаменте которого начал строить дальше. Код и небольшое обсуждение было обнаружено здесь: CyberForum.ru
И вот код, который я взял за основу:
#include <windows.h>
#include <Setupapi.h>
#include <tchar.h>
#include <iostream>
void OutFormatMsg(const TCHAR *Msg){
LPVOID lpMsgBuf;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
GetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(TCHAR*) &lpMsgBuf,
0,
NULL
);
_tprintf(_T("%s: %s\n"), Msg, (TCHAR*)lpMsgBuf);
LocalFree(lpMsgBuf);
}
#define LocalFreeIf(Pointer) if(Pointer) { LocalFree(Pointer); Pointer = NULL; }
int main()
{
setlocale(0, "");
PSP_DEVICE_INTERFACE_DETAIL_DATA pDeviceInterfaceDetailData = NULL;
SP_DEVICE_INTERFACE_DATA DeviceInterfaceData;
SP_DEVINFO_DATA DeviceInfoData;
HDEVINFO hDevInfo;
TCHAR *lpBuffer = NULL;
const GUID InterfaceGuid = { 0x53F56307,0xB6BF,0x11D0,{0x94,0xF2,0x00,0xA0,0xC9,0x1E,0xFB,0x8B} };
hDevInfo = SetupDiGetClassDevs( &InterfaceGuid,0, 0, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE );
if (hDevInfo == INVALID_HANDLE_VALUE) {
OutFormatMsg(_T("SetupDiGetClassDevs"));
return 1;
}
DeviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
DeviceInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
for(DWORD i = 0; SetupDiEnumDeviceInterfaces(hDevInfo, 0, &InterfaceGuid, i, &DeviceInterfaceData); ++i)
{
ULONG RequiredLength = 0;
while ( !SetupDiGetDeviceInterfaceDetail( hDevInfo,
&DeviceInterfaceData, pDeviceInterfaceDetailData, RequiredLength, &RequiredLength,
&DeviceInfoData ) )
{
if( GetLastError() == ERROR_INSUFFICIENT_BUFFER )
{
LocalFreeIf( pDeviceInterfaceDetailData );
pDeviceInterfaceDetailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA)LocalAlloc(LMEM_FIXED, RequiredLength );
pDeviceInterfaceDetailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
} else {
OutFormatMsg(_T("SetupDiGetDeviceInterfaceDetail"));
continue;
}
}
RequiredLength = 0;
while ( !SetupDiGetDeviceRegistryProperty(hDevInfo,
&DeviceInfoData, SPDRP_ENUMERATOR_NAME, NULL,
(PBYTE)lpBuffer, RequiredLength, &RequiredLength) )
{
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
{
LocalFreeIf(lpBuffer);
lpBuffer = (TCHAR*)LocalAlloc(LPTR, (RequiredLength + 1) * sizeof(TCHAR));
} else {
OutFormatMsg(_T("SetupDiGetDeviceRegistryProperty"));
break;
}
}
if( !_tcscmp(lpBuffer, _T("USBSTOR")) )
{
// Точно USB накопитель...
_tprintf(_T("%s\n"), pDeviceInterfaceDetailData->DevicePath);
/* \\?\usbstor#disk&ven_generic&prod_usb_flash_disk&rev_0.00#000000000000ec&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b} */
RequiredLength = 0;
while ( !SetupDiGetDeviceRegistryProperty(hDevInfo,
&DeviceInfoData, SPDRP_FRIENDLYNAME, NULL,
(PBYTE)lpBuffer, RequiredLength, &RequiredLength) )
{
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
{
LocalFreeIf(lpBuffer);
lpBuffer = (TCHAR*)LocalAlloc(LPTR, (RequiredLength + 1) * sizeof(TCHAR));
} else {
OutFormatMsg(_T("SetupDiGetDeviceRegistryProperty2"));
break;
}
}
_tprintf(_T("%s\n"),lpBuffer);
}
LocalFreeIf(lpBuffer);
LocalFreeIf(pDeviceInterfaceDetailData);
}
SetupDiDestroyDeviceInfoList(hDevInfo);
return 0;
}
Запуск и отладка
Теперь надо заставить этот код заработать в Eclipse.
В файле usb-polygon.cpp
убираю автоматически созданный код и вставляю код из форума. Куча красных подчеркиваний. Сохраняю файл. Куча красного исчезает, но Eclipse начинает показывать свой норов: подчеркивает красным _tprintf
(«Function ‘printf’ could not be resolved»), хотя tchar.h
подключен. Подключаю дополнительно #include <stdio.h>
, и Eclipse перестает считать, что это «not resolved». Убираю это подключение — _tprintf
не подчеркивается. Чудеса… Ладно, оставляю на всякий случай.
Теперь надо разобраться с функциями SetupDi…, которые считаются неопределенными. Дело в библиотеке SetupAPI, точнее, в ее отсутствии в проекте. Надо добавить в свойства проекта: Project -> Properties
, в окне свойств в левой панели выбрать C/C++ Build -> Settings
, и на вкладке Tool Settings
выбрать MinGW C++ Linker -> Libraries
, для Library search path (-L)
выбрать в файловой системе C:\MinGW\lib
, а для Libraries (-l)
ввести руками setupapi
.
Пересобираю проект. Компиляция проходит успешно. Запускаю — в консоли чисто. Вставляю флэшку, запускаю проект еще раз, получаю (в Windows XP) что-то типа
\\?\usbstor#disk&ven_kingston&prod_dt_101_ii&rev_1.00#001cc05fe930f961715b0183&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}
Kingston DT 101 II USB Device
Пробую воткнуть свою плату и снова запустить программу, в консоли вижу следующее:
\\?\usbstor#disk&ven_lufa&prod_dataflash_disk&rev_0.00#955373038393518170f0&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}
LUFA Dataflash Disk USB Device
Надо сказать, в Windows 7 результат получается такой же, вот только ждать, когда система выделит плате букву диска, приходится долго, да еще и надо каждый раз отказываться от форматирования.
Ну что же, нашли. И код нашли подходящий, и флешку с его помощью тоже нашли, и даже плату свою. Замечательно, начало положено, надо сохранить наработки в git.
Коротко о коде
Как говорил Козьма Прутков, «Бросая в воду камешки, смотри на круги, ими образуемые; иначе такое бросание будет пустою забавою».
Тупо использовать чужой код — это очень нехорошо, надо хотя бы разобраться, как он работает. Это не трудно, с учетом того, что предварительно был проведен поиск по MSDN и разного рода литературе и форумам на тему решения задачи. Например, Guidelines for Using SetupAPI, опять же, Using Device Installation Functions. Я уже и сам подходил к подобному решению, но медленно и некрасиво.
OutFormatMsg
— вспомогательная функция для форматированного вывода сообщений об ошибках, если они возникнут.
В теле функции main
сперва встречаем SetupDiGetClassDevs
— это функция, возвращающая дескриптор (hDevInfo
) набора данных обо всех устройствах указанного класса device setup class или device interface class. Этот Device Information Set — довольно сложная структура, подробно описанная в MSDN, даже с картинкой.
Класс задается своим глобально уникальным идентификатором InterfaceGuid
, который определен чуть выше по коду. Это GUID_DEVINTERFACE_DISK
, идентификатор дисковых устройств, но можно попробовать еще GUID_DEVINTERFACE_VOLUME
, равный { 0x53f5630dL, 0xb6bf, 0x11d0, {0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b} }
и соответствующий томам накопителей.
Далее, SetupDiEnumDeviceInterfaces
позволяет перебрать все имеющиеся устройства интересующего класса. Эта функция последовательно (в цикле) выдает информацию в структуре SP_DEVICE_INTERFACE_DATA, и возвращает TRUE до тех пор, пока не закончатся соответствующие устройства.
Чтобы из полученной структуры вытащить детальную информацию об устройстве, используется функция SetupDiGetDeviceInterfaceDetail
, возвращающая структуру SP_DEVICE_INTERFACE_DETAIL_DATA
, в которой хранится, собственно, только DevicePath
— нуль-терминированная строка, содержащая device interface path, который можно передать в качестве параметра для CreateFile
. Вот только работает эта функция довольно ректально: сначала ее нужно вызвать и получить ошибку, зато узнать, сколько места понадобится под упомянутую структуру с деталями, потом вызвать еще раз с полученным размером, чтобы получить саму структуру ради упомянутой строки.
SetupDiGetDeviceRegistryProperty
поищет в системном реестре и вернет заданные в параметрах свойства (в нашем случае SPDRP_ENUMERATOR_NAME
, то есть имя перечислителя) для указанного устройства. Вызывается тоже через одно место, дважды, первый раз для получения размера.
Далее перечислитель сверяется на наличие подстроки <<USBSTOR>>
, чтобы убедиться, что это именно что-то флэшкообразное. Перечислитель (enumerator) — системный компонент, связанный с однотипными PnP-устройствами. Насколько я понял, именно он отвечает за то, какой драйвер будет вызван для работы с определенным устройством.
Если в процессе последовательного перебора мы (ну, не совсем мы — система…) наткнулись на флэшку (USBSTOR
), то сначала выдадим с помощью _tprintf
ее DevicePath.
Потом еще разок (ну, то есть пару раз) вызовем SetupDiGetDeviceRegistryProperty
, но попросим другое свойство — SPDRP_FRIENDLYNAME
, то есть строку с удобочитаемым именем устройства — и выдадим второй строкой его.
Под конец приберемся за собой: подчистим выделенную память: как строковую (макросом LocalFreeIf
), так и выделенную под информацию об устройствах (функцией SetupDiDestroyDeviceInfoList
).
Пока что так, расширять буду позже.