RomeoGolf

Вт 27 Декабрь 2016

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!».

new-prj

Сразу лезу в настройки проекта, потому что уже в курсе, что собранный код не запустится. Project -> Properties, и в окне Properties for usb-polygon в правой панели выбираю C/C++ Build -> Settings, на вкладке Tool Settings выбираю MinGW C++ Linker -> Miscellaneous и в поле Linker flags вписываю -static-libgcc -static-libstdc++, иначе придется соответствующие библиотеки таскать вместе с проектом. Предпочитаю прилинковать статически.

linker-flag

Заодно на этой же вкладке выбираю GCC C++ Compiler -> Dialect, и в поле Language standard ставлю ISO C++11 (-srd=c++0x), пригодится.

dialect

Теперь можно и 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).

Пока что так, расширять буду позже.


Теги: