RomeoGolf

Пн 09 Январь 2017

USB-polygon-6: Обмен по USB, поиск среди устройств

А что мы, собственно, нашли?

В предыдущем выпуске получилось завести пример кода на С++, который в состоянии обнаружить подключенную флэшку. А что он еще может обнаружить?

Закрою в коде примера условие

if( !_tcscmp(lpBuffer, _T("USBSTOR")) )

которое отсеивает устройства, не имеющие в своем «FriendlyName» подстроки USBSTOR. Консольный вывод программы у меня показывает, что обнаружен CD-ROM (судя по «\?\ide#diskst»), а жесткий диск не видит. Если воткнуть флэшку — то и ее тоже, конечно, находит.

Попробую заменить строку

    const GUID InterfaceGuid = { 0x53F56307,0xB6BF,0x11D0,{0x94,0xF2,0x00,0xA0,0xC9,0x1E,0xFB,0x8B} };

на

    // \\?\usbstor#disk&ven_<name>&rev_0001#7&4e...
    // GUID_DEVINTERFACE_DISK
    //const GUID InterfaceGuid = { 0x53F56307,0xB6BF,0x11D0,{0x94,0xF2,0x00,0xA0,0xC9,0x1E,0xFB,0x8B} };

    // \\?\storage#removablemedia#8...
    // GUID_DEVINTERFACE_VOLUME
    // нельзя FriendlyName
    const GUID InterfaceGuid = { 0x53f5630dL, 0xb6bf, 0x11d0, {0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b} };

для экспериментов. Первый вариант будет искать диски, а второй — логические тома.

Запуск программы с открытым вторым вариантом InterfaceGuid показывает флэшку (если вставлена) и все разделы жесткого диска. Ах да, и CD-ROM тоже.

По идее, именно второй вариант мне и нужен, потому что надо открыть устройство с определенной буквой, а буквы присваиваются разделам, а не устройствам, пусть даже у моей самоделки одному устройству соответствует один «раздел», как, впрочем, у большинства флэшек. Но при использовании второго варианта недопустим параметр SPDRP_FRIENDLYNAME для функции SetupDiGetDeviceRegistryProperty.

Более того, мне вообще не нравится использование SetupDiGetDeviceRegistryProperty: во-первых, работа с реестром в разных версиях Windows (XP и 7) несколько отличается, и неизвестно, какие неожиданности может подкинуть попытка обеспечить «кроссплатформенность» хотя бы в пределах Windows; во-вторых, в реестр это самое «дружественное имя» записывается при первом обнаружении устройства и определении, какой драйвер для него использовать, а мне хотелось бы иметь возможность идентификации платы при каждом включении, то есть, искать идентификатор именно на устройстве, а не в реестре.

Как спросить устройство?

А как обратиться к устройству? Видимо, надо сначала получить его дескриптор через CreateFile. Параметр-путь у нас уже есть после вызова SetupDiGetDeviceInterfaceDetail. То есть, обращение к реестру уже не нужно, пробуем по очереди открыть каждое обнаруженное устройство из класса «том накопителя». Если не откроется — значит, и не надо. Если откроется — его можно будет спросить.

Вот это вот:

        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;
            }
        }

удаляем, следующий аналогичный блок с SPDRP_FRIENDLYNAME вместо SPDRP_ENUMERATOR_NAME_tprintf после него) — тоже, закрытое комментарием условие отсева USBSTOR — туда же. И LocalFreeIf(lpBuffer); также можно удалить вместе с объявлением буфера lpBuffer вверху кода, этот буфер теперь не нужен.

Короче, от всего условия отсева остается только _tprintf(_T("%s\n"), pDeviceInterfaceDetailData->DevicePath);. Причем, эту строчку имеет смысл сдвинуть на ступень влево, удалить обрамляющие скобки, оставшиеся от условия и добавить пояснение, что именно мы выводим в консоль.

Затем пытаемся открыть устройство, выведем результат попытки, и закрываем, если открылось.

Итого, вместо условия отсева и отображения имени флэшки получаем следующее:

        _tprintf(_T("pDeviceInterfaceDetailData->DevicePath: %s\n"), pDeviceInterfaceDetailData->DevicePath);

        HANDLE hDevice=CreateFile(
                pDeviceInterfaceDetailData->DevicePath,
                GENERIC_READ | GENERIC_WRITE,
                FILE_SHARE_READ or FILE_SHARE_WRITE, //0,
                NULL,
                OPEN_EXISTING,
                0, //FILE_FLAG_WRITE_THROUGH | FILE_FLAG_NO_BUFFERING, //FILE_ATTRIBUTE_NORMAL,
                NULL
                );

        if(hDevice == INVALID_HANDLE_VALUE) {
            _tprintf(_T("CreateFile failed! \n"));
        } else {
            _tprintf(_T("CreateFile done! \n"));
            CloseHandle(hDevice);
        }

В результате обнаруживаются CD-ROM, воткнутая флэшка, мое устройство и разделы жесткого диска, причем, с разделами диска получение дескриптора не работает. А отличить флэшку от моей платы по выводу программы практически нереально. Их строки отличаются только цифрами после \\?\storage#removablemedia#.

Причем, в Windows 7 обнаруженные CD-ROM, флэшка и самоделка определяются программой так:

pDeviceInterfaceDetailData->DevicePath: \\?\ide#cdromtsstcorp_cddvdw_sh-224bb________________sb00____#5&135419d0&0&1.0.0#{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}
CreateFile done! 
pDeviceInterfaceDetailData->DevicePath: \\?\storage#volume#_??_usbstor#disk&ven_generic&prod_flash_disk&rev_8.07#15e1289b&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}#{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}
CreateFile done! 
pDeviceInterfaceDetailData->DevicePath: \\?\storage#volume#_??_usbstor#disk&ven_lufa&prod_dataflash_disk&rev_0.00#955373038393518170f0&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}#{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}
CreateFile done! 

То есть, имеется информация о disk&ven, и уже можно найти нужное устройство, но мне этого мало, и есть необходимость использовать Windows XP, где эти данные отличаются.

Готовлю дальнейшую доработку — убираю лишнюю информацию из консоли: закрываю печать «CreateFile failed!» и переношу отображение DevicePath непосредственно перед «CreateFile done!», делаю коммит.

Теперь у нас есть дескрипторы обнаруженных устройств, с которыми можно работать. Где-то там прячется самодельная плата, надо ее как-то идентифицировать. В этом поможет запрос INQUIRY.

Запрос INQUIRY и как это делается

ПК общается с USB-устройствами, используя интерфейс SCSII. Ну, то есть, не только с USB, но с ними тоже. Протокол SCSII предполагает обмен при помощи команд: компьютер посылает команду устройству, оно отвечает. Среди команд есть Inquiry — запрос основных характеристик устройства. По этим характеристикам устройство можно идентифицировать. А чтобы отправить SCSII-команду, нужно использовать функцию DeviceIoControl. Сама по себе эта функция не страшная, а вот ее параметры… Надо подготовить изрядных размеров структуру и заполнить ее начальные значения, потом забрать результат.

В общем, проще пояснить комментариями в коде.

Для использования SCSII-интерфейса надо подключить #include <ntddscsi.h>, и в свойствах проекта указать путь к этому заголовочнику: Project -> Properties, в окне свойств в левой панели выбрать C/C++ Build -> Settings, и на вкладке Tool Settings выбрать GCC C++ Compiler -> Includes, для Include paths (-I) выбрать в файловой системе C:\MinGW\include\ddk.

Чуть ниже объявим такую структуру:

// Это структура _SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER,
// которая eсть в spti.h из состава Windows DDK
// и нет в MinGW
struct scsi_st
{
    SCSI_PASS_THROUGH_DIRECT t_spti;
    DWORD tmp;              // realign buffer to double word boundary
    byte sensebuf[32];
} myspti;

Можно, конечно, подключить заголовочник spti.h, но его надо сначала где-то найти, потом куда-то положить… Пусть так будет.

И между _tprintf(_T("CreateFile done! \n")); и CloseHandle(hDevice); вставляем такой код:

            char vbuf[512];             // буфер для принятого пакета
            unsigned long returned;     // место под количество прочитанных байт

            memset(&myspti, 0, sizeof(scsi_st));    // инициализация структуры
            myspti.t_spti.Length = sizeof(SCSI_PASS_THROUGH_DIRECT); // длина
            myspti.t_spti.Lun = 0;          // логический номер устройства (в одном устройстве может быть несколько логических)
            myspti.t_spti.TargetId = 0;     // целевой контроллер или устройство на шине
            myspti.t_spti.PathId = 0;       // SCSII-порт или шина для запроса.
            myspti.t_spti.CdbLength = 6;    // длина command descriptor block (CDB, для кодов команд до 0x1F длина 6)
            myspti.t_spti.DataIn = SCSI_IOCTL_DATA_IN;  // на прием
            myspti.t_spti.SenseInfoLength = 32;     // длина блока sensebuf и его смещение
            myspti.t_spti.SenseInfoOffset = sizeof(SCSI_PASS_THROUGH_DIRECT) + sizeof(DWORD);
            myspti.t_spti.TimeOutValue = 10;        // таймаут ожидания окончания операции
            myspti.t_spti.DataTransferLength = 36;  // длина данных для обмена
            myspti.t_spti.DataBuffer = vbuf;        // указатель на буфер данных
            // собственно CDB (блок описания команды):
            myspti.t_spti.Cdb[0] = 0x12;    // код команды INQUIRY
            myspti.t_spti.Cdb[4] = 0x24;    // 36 - размер данных

            if (DeviceIoControl(
                        hDevice,                        // дескриптор устройства
                        IOCTL_SCSI_PASS_THROUGH_DIRECT, // dwIoControlCode управляющий код операции - интерфейс для отправки CDB
                        &myspti,                        // входной буфер
                        sizeof(scsi_st),                // его размер
                        &myspti,                        // выходной буфер
                        sizeof(scsi_st),                // его размер
                        &returned,                      // сколько данных передано (надо бы сверить)
                        NULL)) {                        // указатель на OVERLAPPED, не нужно.

                vbuf[36] = 0;                               // для удобства вывода в консоль
                _tprintf(_T("PDT = %x\n"), vbuf[0]);        // тип устройства SCSII
                _tprintf(_T("RMB = %x\n"), (vbuf[1] & 0x080) >> 7); // съемный/нет
                _tprintf(_T("ver. SPC = %x\n"), vbuf[2]);   // версия SPC
                _tprintf(_T("vendor = %s\n"), &vbuf[8]);    // строковое обозначение производителя
                _tprintf(_T("product = %s\n"), &vbuf[16]);  // строковое обозначение продукта
                _tprintf(_T("ver = %s\n"), &vbuf[32]);      // строковое обозначение версии

                if (!_tcscmp(&vbuf[8], _T("LUFA")) ){       // поиск своего устройства
                    _tprintf(_T("--- This is my device! ---\n"));
                }
                _tprintf(_T("\n"));                         // для удобочитаемости пустая строка
            }

Таким образом, посредством DeviceIoControl выдается SCSII-команда INQUIRY, которая требует вернуть структуру с данными об устройстве. Причем, собственно команда лежит в блоке описания команды CDB, который хотя и имеет размер (16 байтов, насколько я помню), но конкретно для INQUIRY нужны только два: в нулевом передается код команды INQUIRY и в четвертом — размер принимаемых данных.

Ответ на команду INQUIRY окажется в буфере vbuf и имеет условно фиксированный размер, то есть, должна быть как минимум 36 байтов, хотя может быть и больше и содержать дополнительную информацию. Но обойдемся минимальной частью. Вот что можно оттуда извлечь:

  • В младшем (нулевом) байте (в его младших пяти разрядах) хранится PDT — PERIPHERAL DEVICE TYPE, тип устройства. 0 — устройства прямого доступа (например, магнитные диски, флэшки тоже), 1 — устройства последовательного доступа, типа стриммеров, 2 — принтеры… Много их. Вот, например, из обнаруженного, кроме 0, тип 5: CD-ROM.
  • 7 разряд первого байта указывает единицей на то, что устройство съемное.
  • Второй байт хранит версию стандарта SPC, причем, 4 соответствует SPC-2, а 5 — SPC-3.
  • В четвертом байте лежит длина дополнительных данных, тех, что за пределами 36 байтов. Причем, не просто так, а еще и вычисления какие-то требуются, но это неважно, потому что не нужно.
  • С 8 по 15 байт занимает обозначение производителя VENDOR IDENTIFICATION.
  • С 16 по 31 байт занимает обозначение изделия PRODUCT IDENTIFICATION.
  • С 32 по 35 байт — версия изделия PRODUCT REVISION LEVEL.

Остальное или не используется, или не интересно, потому что не нужно.

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

Вот пример того, что вывел текущий код под Windows 7:

pDeviceInterfaceDetailData->DevicePath: \\?\ide#cdromtsstcorp_cddvdw_sh-224bb________________sb00____#5&135419d0&0&1.0.0#{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}
CreateFile done! 
PDT = 5
RMB = 1
ver. SPC = 0
vendor = TSSTcorpCDDVDW SH-224BB SB00
product = CDDVDW SH-224BB SB00
ver = SB00

pDeviceInterfaceDetailData->DevicePath: \\?\storage#volume#_??_usbstor#disk&ven_generic&prod_flash_disk&rev_8.07#15e1289b&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}#{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}
CreateFile done! 
PDT = 0
RMB = 1
ver. SPC = 4
vendor = Generic Flash Disk      8.07
product = Flash Disk      8.07
ver = 8.07

pDeviceInterfaceDetailData->DevicePath: \\?\storage#volume#_??_usbstor#disk&ven_lufa&prod_dataflash_disk&rev_0.00#955373038393518170f0&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}#{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}
CreateFile done! 
PDT = 0
RMB = 1
ver. SPC = 0
vendor = LUFA
product = Dataflash Disk
ver = 0.00
--- This is my device! ---

Получилась работоспособная версия, которая находит нужную плату среди подключенных устройств. Это будет коммит с тегом v0.2.

Теперь пссле строчки _tprintf(_T("--- This is my device! ---\n")); можно добавлять код, работающий с макетной платой. Добавлю для начала попытку записи и чтения. А так как ReadFile — функция довольно сложная, выполняющая не только собственно чтение, полагаю, она не заработает. Поэтому сделаю опять через DeviceIoControl:

                    // ----- работа с устройством -----
                    BOOL result;
                    UCHAR q[512 * 4 * 2];
                    DWORD q1, q2 = 0;
                    q1 = 512 * 2 * 1;
                    q[0] = 1;

                    ZeroMemory(&myspti, sizeof(scsi_st));

                    myspti.t_spti.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
                    myspti.t_spti.PathId = 0;
                    myspti.t_spti.TargetId = 0;
                    myspti.t_spti.Lun = 0;
                    myspti.t_spti.CdbLength = 10;
                    myspti.t_spti.DataIn = SCSI_IOCTL_DATA_OUT;
                    myspti.t_spti.SenseInfoLength = 32;
                    myspti.t_spti.DataTransferLength = q1;
                    myspti.t_spti.TimeOutValue = 10;
                    myspti.t_spti.DataBuffer = &q;
                    myspti.t_spti.SenseInfoOffset = sizeof(SCSI_PASS_THROUGH_DIRECT) + sizeof(DWORD);
                    myspti.t_spti.Cdb[0] = 0x2a; //SCSIOP_WRITE;

                    myspti.t_spti.Cdb[2] = 0x00;
                    myspti.t_spti.Cdb[3] = 0x00;
                    myspti.t_spti.Cdb[4] = 0x00;
                    myspti.t_spti.Cdb[5] = 0x00;

                    myspti.t_spti.Cdb[7] = 0x00;
                    myspti.t_spti.Cdb[8] = 0x02;

                    ULONG length = sizeof(scsi_st);
                    result = DeviceIoControl(hDevice,
                            IOCTL_SCSI_PASS_THROUGH_DIRECT,
                            &myspti,
                            length,
                            &myspti,
                            length,
                            &q2,
                            FALSE);
                    if (result==0) {
                        OutFormatMsg("Write Error DevIoCtl");
                    } else {
                        _tprintf("Write done\n");
                        _tprintf("len = %lu\n", q2);
                    }

                    q2=0;
                    myspti.t_spti.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
                    myspti.t_spti.PathId = 0;
                    myspti.t_spti.TargetId = 0;
                    myspti.t_spti.Lun = 0;
                    myspti.t_spti.CdbLength = 10;
                    myspti.t_spti.DataIn = SCSI_IOCTL_DATA_IN;
                    myspti.t_spti.SenseInfoLength = 32;
                    myspti.t_spti.DataTransferLength = q1;
                    myspti.t_spti.TimeOutValue = 10;
                    myspti.t_spti.DataBuffer = &q;
                    myspti.t_spti.SenseInfoOffset = sizeof(SCSI_PASS_THROUGH_DIRECT) + sizeof(DWORD);

                    myspti.t_spti.Cdb[0] = 0x28; //SCSIOP_READ;

                    myspti.t_spti.Cdb[2] = 0x00;
                    myspti.t_spti.Cdb[3] = 0x00;
                    myspti.t_spti.Cdb[4] = 0x00;
                    myspti.t_spti.Cdb[5] = 0x00;

                    myspti.t_spti.Cdb[7] = 0x00;
                    myspti.t_spti.Cdb[8] = 0x02;

                    result = DeviceIoControl(hDevice,
                            IOCTL_SCSI_PASS_THROUGH_DIRECT,
                            &myspti,
                            q1,
                            &myspti,
                            q1,
                            &q2,
                            FALSE);
                    if (result == 0) {
                        OutFormatMsg("Read Error DevIoCtl");
                    } else {
                        _tprintf("data_2 = %x\n", q[0]);
                        _tprintf("len = %lu\n", q2);
                    }
                    // --------------------------------

Нулевой байт CDB, как обычно, код команды; первый байт поделен на поля, но в них все равно должны быть везде нули; в байтах со второго по пятый для команд записи и чтения передается logical block assress — логический адрес первого блока данных на устройстве; шестой байт зарезервирован; в седьмом и восьмом байтах — размер посылки в логических блоках, старшим вперед. Пока неважно, что в этих адресах, все равно обмен еще не сможет сработать.

В q будет буфер, q1 и q2 — размер данных и количество реально переданых.

DeviceIoControl выполняется без ошибок (в Windows XP), но читается в первом байте буфера единица, которая была туда записана в коде, то есть, операция чтения не перезаписала ее. Да и длина данных почему-то равна 80. То есть, обмен не работает. Этого и следовало ожидать, потому что код платы его еще не поддерживает. Я даже немного удивлен, что ошибок не возникло.

Впрочем, при проверке в Windows 7 запись не работает:

Write Error DevIoCtl: Неверная функция.


data_2 = 1
len = 80

Пора коммитить код и переходить к реализации обмена со стороны контроллера.


Теги: