USB-polygon-8: Обмен по USB, еще о взаимодействии с «чистым» устройством
Завершая этап
В результате проделанной работы получилось «сырое» с точки зрения файловой системы MassStorage-устройство, приспособленное к обмену информацией с ПК по USB. С определенными ограничениями, конечно: во-первых, требуется некоторый самописный код для ПК, причем, требующий прав администратора; во-вторых, на устройстве программа тоже выглядит не особенно изящно, нужно городить какие-то конструкции для идентификации запрашиваемой и получаемой информации по ее адресу, а также какое-то распределение в адресном пространстве источников и приемников информации. Ну, это уже зависит от конкретной задачи, а текущая задача — учебная — фактически выполнена.
Теперь хотелось бы развить тему: сделать устройство, более точно имитирующее флэш-накопитель. То есть, надо добавить некоторое подобие файловой системы.
Однако перед этим хотелось бы провести еще пару экспериментов с «сырым» устройством.
Физическое устройство
Первая «доделка» касается кода для ПК. Доступ к устройству осуществляется при помощи дескриптора (HANDLE), который возвращает функция CreateFile
. Этой функции сейчас передается в качестве параметра DevicePath
, который, в свою очередь, возвращается функцией SetupDiEnumDeviceInterfaces
, принимающей среди параметров в том числе GUID интерфейса, можно диска, можно тома.
Хочу попробовать достучаться до платы, как до физического устройства, PhysicalDrive. Есть мнение, что это будет более единообразно для разных версий ОС Windows, а также должно снять сомнения, к чему же правильнее обращаться — к диску или к тому.
Для этого снова пригодится функция DeviceIoControl
, которой вторым параметром нужно передать IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS
.
В коде после комментария // ----- работа с устройством -----
надо добавить объявление новых переменных:
int diskNum = 0; // количество дисков
char physicalDrive[50] = { 0 }; // строка под "\\\\.\\PhysicalDrive%d"
DWORD bufsize, bufsizeret; // размеры буфера
VOLUME_DISK_EXTENTS buf_ioctl; // буфер для GET_VOLUME_DISK_EXTENTS
bufsize = sizeof(VOLUME_DISK_EXTENTS);
Однако парочку появившихся типов этот код не знает. Не буду подключать соответствующий заголовок, просто запишу после объявления struct scsi_st
:
typedef struct _DISK_EXTENT {
DWORD DiskNumber;
LARGE_INTEGER StartingOffset;
LARGE_INTEGER ExtentLength;
} DISK_EXTENT,*PDISK_EXTENT;
typedef struct _VOLUME_DISK_EXTENTS { DWORD NumberOfDiskExtents; DISK_EXTENT Extents[ANYSIZE_ARRAY];
} VOLUME_DISK_EXTENTS;
А также такие #define
после всех #include
:
#define IOCTL_VOLUME_BASE ((ULONG) 'V')
#define IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS \
CTL_CODE(IOCTL_VOLUME_BASE, 0, METHOD_BUFFERED, FILE_ANY_ACCESS)
Теперь ниже // ----- работа с устройством -----
после обявления переменных можно добавить код:
result = DeviceIoControl(hDevice,
IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,
NULL,
0,
&buf_ioctl,
bufsize,
&bufsizeret,
NULL
);
if (result==0) {
OutFormatMsg("PhysicDiscErr");
} else {
_tprintf("DiscNum = %lu\n", buf_ioctl.NumberOfDiskExtents);
diskNum = buf_ioctl.Extents[0].DiskNumber;
_tprintf("Disk = %d\n", diskNum);
if (diskNum != 0) {
sprintf(physicalDrive, "\\\\.\\PhysicalDrive%d", diskNum);
}
_tprintf("physicalDrive: %s\n", physicalDrive);
}
Теперь в консольном выводе появятся новые строки:
. . .
--- This is my device! ---
DiscNum = 1
Disk = 1
physicalDrive: \\.\PhysicalDrive1
WriteFile done
len = 1024
ReadFile done
data_2 = 0
len = 1024
То есть, дисков 1, номер диска 1, строка для CreateFile
сформирована. Можно сразу после пытаться получить дескриптор устройства по номеру физического диска:
HANDLE hDevice2=CreateFile(
physicalDrive,
GENERIC_READ | GENERIC_WRITE,
//0,
//FILE_SHARE_READ or FILE_SHARE_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_FLAG_WRITE_THROUGH | FILE_FLAG_NO_BUFFERING,
NULL
);
if(hDevice2 == INVALID_HANDLE_VALUE) {
OutFormatMsg("CreateFile 2 Error");
} else {
_tprintf(_T("CreateFile 2 done! \n"));
// сюда вставить ReadFile и WriteFile;
// сюда же можно и обе
// DeviceIoControl с IOCTL_SCSI_PASS_THROUGH_DIRECT
CloseHandle(hDevice2);
}
И в имеющихся ReadFile
и WriteFile
после их перестановки на новое место (указанное комментарием в коде) заменяю hDevice
на hDevice2
.
Проверено — работает. Делает все то же, что и предыдущая версия.
Еще можно попробовать заблокировать и размонтировать устройство при помощи DeviceIoControl
с параметрами FSCTL_LOCK_VOLUME
и FSCTL_DISMOUNT_VOLUME
соответственно, но это уже другая история.
Пользовательская SCSI-команда
Теперь обращусь к устройству. До сих пор оно выполняло только стандартные команды, передаваемые при помощи ScsiPassThroughDirect
, это WRITE(10)
с кодом 0x2A и READ (10)
с кодом 0x28.
А если мне надо выполнить какое-то свое действие, не предусмотренное набором SCSI-команд? Или, скажем, надо именно записать, но, допустим, в нулевой «сектор», а операционная система после очередного обновления безопасности не разрешает этого делать, чтобы сильно умелые ручки не портили MBR устройств? «Сектор» здесь написал в кавычках, потому что нет никаких секторов в помине, есть только используемые в обмене адреса данных, которые в нормальном устройстве могли бы действительно указывать на сектор.
Пробую: в SCSI-команде записи закрываю строку с кодом команды, а вместо нее пишу ее копию, но с кодом 0xC1: команды от 0xC0 до 0xFF вроде бы vendor cpecific.
Отчет о выполнении операции записи получен, однако на светодиодной индикации платы не видно, чтобы что-то записалось. Пора править код программы для устройства.
Открываю /Lib/SCSI.c
, нахожу там функцию bool SCSI_DecodeSCSICommand(void)
, в ней переключатель switch (CommandBlock.SCSICommandData[0])
, благо, кроме него там почти ничего нет. Сразу под строкой case SCSI_CMD_WRITE_10:
добавляю строку case 0xC1:
. Тем самым заставляю устройство реагировать на команду 0xC1 так же, как и на команду 0x2A.
Проверяю — работает. То есть, запись снова проходит, и на светодиодах отображается переданный с ПК байт.
Таким образом можно дублировать имеющиеся команды, можно писать совсем собственные. Для самописных команд можно особым образом обрабатывать Command Descriptor Block (CDB), а как это сделать — можно подсмотреть в используемой здесь функции SCSI_Command_ReadWrite_10
, например. Там есть ближе к началу строки
BlockAddress = SwapEndian_32(*(uint32_t*)&CommandBlock.SCSICommandData[2]);
TotalBlocks = SwapEndian_16(*(uint16_t*)&CommandBlock.SCSICommandData[7]);
В них функция извлекает адрес и длину из указателя на 2 и 7 байты CDB. Можно напихать в CDB для самописной команды свою информацию в нужные места, а в обработчике вытащить ее аналогичным образом, зная место и размер.
Ну вот, теперь с «сырым» устройством практически всё. Что касается кода — надо закоммитить текущие изменения в git с тегами «v0.3» для обеих программ.
Далее начнется работа по имитации файловой системы на устройстве, несмотря на отсутствие памяти для этой самой файловой системы.