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
Пора коммитить код и переходить к реализации обмена со стороны контроллера.