USB-polygon-11: Начало имитации ФС
Проект продолжается
Работа над программой для платы была приостановлена на выпуске 8 цикла.
Программа для микроконтроллера AT90USB162 в ее нынешнем состоянии позволяет определить устройство на компьютере, как запоминающее устройство USB без файловой системы, из-за чего Windows 7 при подключении платы предлагает ее отформатировать. С платой можно вести какой-никакой информационный обмен в обе стороны, правда, для этого нужно написать специальную программу для ПК, работа над которой также приостановлена в восьмом выпуске.
Если же ПК обнаружит, что на плате есть ФС FAT32 (даже если на самом деле ее нет), то можно читать с устройства данные, видимые на компьютере, как файлы, и записывать как бы файлы на устройство, причем, стандартными средствами, например, файловым менеджером. Насчет записи у меня, правда, пока есть небольшие сомнения в простоте реализации, но попробую позже. Начну, однако, с чтения, но до чтения надо еще заставить ПК увидеть FAT32.
Незаметные изменения
В папке usb-polygon-embed/MassStorage/Lib
остались файлы (кода на С и заголовочный) модуля DataflashManager
Он уже не нужен, важные строки из него перенесены в модуль SCSI
, подключение этих файлов через #include
закрыто комментарием. Удаляю это всё — файлы и закомментированное подключение — и компилирую. Работает. Добавляю два файла: fake_fs.c
и fake_fs.h
, пока пустые. Подключаю новый заголовочник в SCSI.h
вместо DataflashManager
.
То, что получилось, компилируется и работает: по сути, ничего не изменилось. Но сохраняю этот коммит в GIT, чтобы обозначить начало очередного этапа.
Далее, в fake_fs.h
надо вставить традиционный финт ушами для предотвращения повторного включения заголовочников:
#ifndef _FAKE_FS_H_
#define _FAKE_FS_H_
в начале файла и
#endif
в конце. А между ними нужно вставить вырезанный из SCCI.h
кусок, который был туда вставлен из DataflashManager
и ..\..\LUFA\CodeTemplates\DriverStubs\Dataflash.h
:
/* ----- instead DataflashManager ----- */
/* from ..\..\LUFA\CodeTemplates\DriverStubs\Dataflash.h */
/* Public Interface - May be used in end-application: */
/* Macros: */
/** Constant indicating the total number of dataflash ICs mounted on the selected board. */
#define DATAFLASH_TOTALCHIPS 1 // TODO: Replace with the number of Dataflashes on the board, max 2
/** Mask for no dataflash chip selected. */
#define DATAFLASH_NO_CHIP 0
/** Mask for the first dataflash chip selected. */
#define DATAFLASH_CHIP1 1 // TODO: Replace with mask with the pin attached to the first Dataflash /CS set
/** Mask for the second dataflash chip selected. */
#define DATAFLASH_CHIP2 2 // TODO: Replace with mask with the pin attached to the second Dataflash /CS set
/** Internal main memory page size for the board's dataflash ICs. */
#define DATAFLASH_PAGE_SIZE 1024 // TODO: Replace with the page size for the Dataflash ICs
/** Total number of pages inside each of the board's dataflash ICs. */
#define DATAFLASH_PAGES 8192 // TODO: Replace with the total number of pages inside one of the Dataflash ICs
/* from DataflashManager.h */
/* Preprocessor Checks: */
#if (DATAFLASH_PAGE_SIZE % 16)
#error Dataflash page size must be a multiple of 16 bytes.
#endif
/* Defines: */
/** Total number of bytes of the storage medium, comprised of one or more Dataflash ICs. */
#define VIRTUAL_MEMORY_BYTES ((uint32_t)DATAFLASH_PAGES * DATAFLASH_PAGE_SIZE * DATAFLASH_TOTALCHIPS)
/** Block size of the device. This is kept at 512 to remain compatible with the OS despite the underlying
* storage media (Dataflash) using a different native block size. Do not change this value.
*/
#define VIRTUAL_MEMORY_BLOCK_SIZE 512
/** Total number of blocks of the virtual memory for reporting to the host as the device's total capacity. Do not
* change this value; change VIRTUAL_MEMORY_BYTES instead to alter the media size.
*/
#define VIRTUAL_MEMORY_BLOCKS (VIRTUAL_MEMORY_BYTES / VIRTUAL_MEMORY_BLOCK_SIZE)
/** Blocks in each LUN, calculated from the total capacity divided by the total number of Logical Units in the device. */
#define LUN_MEDIA_BLOCKS (VIRTUAL_MEMORY_BLOCKS / TOTAL_LUNS)
/* Function Prototypes: */
void WriteBlocks(const uint32_t BlockAddress,
uint16_t TotalBlocks);
void ReadBlocks(const uint32_t BlockAddress,
uint16_t TotalBlocks);
/* variables */
extern uint8_t data_PC;
extern uint8_t data_device;
/* ------------------------------------ */
А в файл fake_fs.c
из SCSI.c
переношу код функций WriteBlocks
и ReadBlocks
.
В начале файла fake_fs.c
должна быть строка #include "fake_fs.h"
, а внутри конструкции #ifndef _FAKE_FS_H_
файла fake_fs.h
должна быть строка #include "../MassStorage.h"
. И надо не забыть поправить Makefile, добавив к определению SRC
новый модуль:
SRC = $(TARGET).c Descriptors.c Lib/SCSI.c Lib/fake_fs.c $(LUFA_SRC_USB)
В результате этих изменений проект компилируется, загружается и работает. Отличий в работе от версии, которая была до изменений, не видно, и не должно быть видно. Это только подготовка. Но уже можно сохранить еще один GIT-коммит.
До сих пор были только перестановки кусков кода из файла в файл. Пора начать вносить изменения в сам код. Добавляю объявления двух функций:
/* data "device -> PC" */
uint8_t * prepare_data(uint8_t * data_buf, uint32_t BlockAddress, uint8_t BytesInBlockDiv16);
/* data "PC -> device" */
void process_data(uint8_t * data_buf, uint32_t BlockAddress, uint8_t BytesInBlockDiv16);
Эти функции вызываются в ReadBlocks
и WriteBlocks
соответственно, в первом случае перед блоком операций Endpoint_Write_8
, во втором — после блока Endpoint_Read_8
, а строчки, которые раньше отвечали за обмен с ПК байтами data_PC
и data_device
теперь будут вызываться из них:
uint8_t * prepare_data(uint8_t * data_buf, uint32_t BlockAddress, uint8_t BytesInBlockDiv16){
data_buf[0] = data_device;
return data_buf;
}
void process_data(uint8_t * data_buf, uint32_t BlockAddress, uint8_t BytesInBlockDiv16){
if (first) {
first = false;
data_PC = data_buf[0];
}
}
Еще переменную first
надо объявить не в функции WriteBlocks
, где она была до этого, а в начале файла, добавив static
в начале объявления. Это заготовки функций, которые будут отвечать за подготовку данных для передачи в ПК и обработки принятых из ПК данных.
Для нормальной работы с данными функциям нужно знать не только содржимое блока данных, но и запрошенный адрес. Этот адрес, разделенный на адрес сектора (BlockAddress
) и смещение 16-байтовой пачки внутри сектора (BytesInBlockDiv16
), передается функции в виде параметра, хотя пока и не используется.
То, что получилось в итоге, компилируется, запускается и работает так же, как и до изменений. По-прежнему доступен обмен однобайтными данными в обе стороны при помощи самодельной программы на C++, описанной в выпусках 5, 6 и 8. Сохраняю очередной коммит, но пора вносить серьезные дополнения.
Имитация ФС
То, что делает функция обработки полученных данных process_data
в рамках имитации ФС неважно. А вот подготовка данных для передачи — это то, что в первую очередь интересует. Нужно, чтобы на запрос MBR она подсовывала что-то, похожее на MBR, при запросе FAT — что-то, похожее на FAT и так далее.
А что может запросить ОС? MBR, загрузочную запись раздела, FAT, корневой каталог — это минимум. Значит, для начала в fake_fs.c
определяю константы, которые описал в предыдущем выпуске:
#define ATTR_READ 0x01
#define ATTR_HIDDEN 0x02
#define ATTR_SYSTEM 0x04
#define ATTR_VOL_LABEL 0x08
#define ATTR_DIR 0x10
#define ATTR_ARCHIVE 0x20
#define ATTR_LONG_FNAME 0x0F
#define BYTES_PER_SECTOR 512
#define BYTES_PER_SECT_SHIFT 9
#define SECTORS_PER_CLUSTER 8
#define SECTORS_PER_CLUST_SHIFT 3
#define BYTES_PER_CLUSTER (BYTES_PER_SECTOR * SECTORS_PER_CLUSTER)
#define BYTES_PER_CLUST_SHIFT (BYTES_PER_SECT_SHIFT + SECTORS_PER_CLUST_SHIFT)
/* должно быть кратно секторам на кластер */
#define SECTORS_PER_FAT 8
#define MBR_SECTOR 0
#define BOOT_SECTOR 62
#define FAT1_SECTOR (BOOT_SECTOR + 1)
#define FAT2_SECTOR (FAT1_SECTOR + SECTORS_PER_FAT)
#define ROOT_SECTOR (FAT2_SECTOR + SECTORS_PER_FAT)
#define ROOT_CLUSTER (SECTORS_PER_FAT * 2 / SECTORS_PER_CLUSTER)
/* предполагается, что root-каталог занимает 1 сектор */
/*
*
* область данных и соответствующие ей 32-р. блоки фат:
*
* 0 кластер - фат1
* 1 кластер - фат2
* 2 кластер - корневой каталог
* 3 кластер - первый файл
*
*/
Далее - функции подготовки данных при попытке чтения MBR, загрузочного сектора раздела и FAT, которые передают набор байтов, соответствующий запрошенным адресам, как описано в предыдущем выпуске. Здесь важна колонка «r:c» таблиц 3 и 4, где «r» соответствует пачке байтов BytesInBlockDiv16
, а «c» — байту внутри пачки:
uint8_t * read_mbr(uint8_t * data_buf, uint8_t BytesInBlockDiv16){
memset(data_buf, 0, 16); /* по умолчанию нули */
if (BytesInBlockDiv16 == 28) {
data_buf[2] = 0x0C; /* type FAT32 with LBA */
data_buf[6] = BOOT_SECTOR; /* start LBA */
data_buf[11] = 0x10; /* num of sect */
}
if (BytesInBlockDiv16 == 31) { /* part 4, signature */
data_buf[14] = 0x55;
data_buf[15] = 0xAA;
}
return data_buf;
}
uint8_t * read_boot_sect(uint8_t * data_buf, uint8_t BytesInBlockDiv16){
memset(data_buf, 0, 16);
if (BytesInBlockDiv16 == 0) {
data_buf[0] = 0xEB;
data_buf[1] = 0x58;
data_buf[2] = 0x90;
data_buf[3] = 'M';
data_buf[4] = 'S';
data_buf[5] = 'D';
data_buf[6] = 'O';
data_buf[7] = 'S';
data_buf[8] = '5';
data_buf[9] = '.';
data_buf[10] = '0';
data_buf[12] = 0x02;
data_buf[13] = 0x08;
data_buf[14] = 0x01;
}
if (BytesInBlockDiv16 == 1) {
data_buf[0] = 0x02;
data_buf[5] = 0xF8;
data_buf[8] = 63;
data_buf[10] = 0xFF;
data_buf[12] = 62;
}
if (BytesInBlockDiv16 == 2) {
data_buf[1] = 0x10;
data_buf[4] = 0x08;
data_buf[12] = 0x02;
}
if (BytesInBlockDiv16 == 3) {
/*data_buf[0] = 0x01;*/
/*data_buf[2] = 0x06;*/
}
if (BytesInBlockDiv16 == 4) {
data_buf[0] = 0x80;
data_buf[2] = 0x29;
/* datetime (vol id) */
data_buf[3] = 148;
data_buf[4] = 14;
data_buf[5] = 13;
data_buf[6] = 8;
/* vol label */
data_buf[7] = 'N';
data_buf[8] = 'O';
data_buf[9] = 0x20;
data_buf[10] = 'N';
data_buf[11] = 'A';
data_buf[12] = 'M';
data_buf[13] = 'E';
data_buf[14] = ' ';
data_buf[15] = ' ';
}
if (BytesInBlockDiv16 == 5) {
/* vol label */
data_buf[0] = ' ';
data_buf[1] = ' ';
/* fs type */
data_buf[2] = 'F';
data_buf[3] = 'A';
data_buf[4] = 'T';
data_buf[5] = '3';
data_buf[6] = '2';
data_buf[7] = ' ';
data_buf[8] = ' ';
data_buf[9] = ' ';
}
if (BytesInBlockDiv16 == 31) {
data_buf[14] = 0x55;
data_buf[15] = 0xAA;
}
return data_buf;
}
uint8_t * read_fat(uint8_t * data_buf, uint32_t BlockAddress, uint8_t BytesInBlockDiv16) {
memset(data_buf, 0, 16);
if ((BlockAddress == 0) && (BytesInBlockDiv16 == 0)) {
/* first fat */
data_buf[0] = 0xF8;
data_buf[1] = 0xFF;
data_buf[2] = 0xFF;
data_buf[3] = 0x0F;
/* second fat */
data_buf[4] = 0xFF;
data_buf[5] = 0xFF;
data_buf[6] = 0xFF;
data_buf[7] = 0x0F;
/* root dir */
data_buf[8] = 0xFF;
data_buf[9] = 0xFF;
data_buf[10] = 0xFF;
data_buf[11] = 0x0F;
/* file1 */
data_buf[12] = 0x00;
data_buf[13] = 0x00;
data_buf[14] = 0x00;
data_buf[15] = 0x00;
}
return data_buf;
}
А вызвать эти функции нужно в подходящий момент, то есть, когда запрашивают соответствующие адреса. Поэтому правлю prepare_data
, теперь она выглядит так:
uint8_t * prepare_data(uint8_t * data_buf, uint32_t BlockAddress, uint8_t BytesInBlockDiv16){
uint8_t *pdata;
if (BlockAddress == MBR_SECTOR) {
pdata = read_mbr(data_buf, BytesInBlockDiv16);
return pdata;
} else
if (BlockAddress == BOOT_SECTOR) {
pdata = read_boot_sect(data_buf, BytesInBlockDiv16);
return pdata;
} else
if ((BlockAddress >= FAT1_SECTOR) && (BlockAddress < (FAT2_SECTOR))) {
pdata = read_fat(data_buf, BlockAddress - FAT1_SECTOR, BytesInBlockDiv16);
return pdata;
} else
if ((BlockAddress >= (FAT2_SECTOR)) && (BlockAddress < (ROOT_SECTOR))) {
pdata = read_fat(data_buf, BlockAddress - FAT2_SECTOR, BytesInBlockDiv16);
return pdata;
} else {
memset(data_buf, 0, 16);
return data_buf;
};
}
И, чтобы не задумываться, в каком месте файла разместить определение, вставлю в начале файла их объявление:
uint8_t * read_mbr(uint8_t * data_buf, uint8_t BytesInBlockDiv16);
uint8_t * read_boot_sect(uint8_t * data_buf, uint8_t BytesInBlockDiv16);
uint8_t * read_fat(uint8_t * data_buf, uint32_t BlockAddress, uint8_t BytesInBlockDiv16);
Еще надо убрать модификатор const
с параметров BlockAddress
функций ReadBlocks
и WriteBlocks
, а также в конце этих функций, после строки TotalBlocks--;
добавить BlockAddress++;
, потому что до сих пор никто не требовал переходить к следующему сектору в процессе чтения.
Теперь после компиляции, перепрошивки и перезапуска устройство уже гораздо больше похоже на флэшку. Вот как оно выгляжит при подключении в Windows 7:
И его свойства:
Правда, в Windows XP оно не пустое, а наоборот, полное:
Имитация корневого каталога
Добавлю заготовку для имитации содержимого корневого каталога. Пока что вставлю туда один описатель файла, соответствующий идентификатору тома. Объявляю функцию для чтения из области данных:
uint8_t * read_data(uint8_t * data_buf, uint32_t BlockAddress, uint8_t BytesInBlockDiv16);
В функции prepare_data
перед последним else
вставляю четыре строчки:
} else
if (BlockAddress >= (ROOT_SECTOR)) {
pdata = read_data(data_buf, BlockAddress, BytesInBlockDiv16);
return pdata;
И сама функция:
uint8_t * read_data(uint8_t * data_buf, uint32_t BlockAddress, uint8_t BytesInBlockDiv16) {
memset(data_buf, 0, 16);
uint32_t size;
if ((BlockAddress == ROOT_SECTOR) && (BytesInBlockDiv16 == 0)) {
/* root, chunck 1 */
data_buf[0] = 'L';
data_buf[1] = 'U';
data_buf[2] = 'F';
data_buf[3] = 'A';
data_buf[4] = '_';
data_buf[5] = '1';
data_buf[6] = ' ';
data_buf[7] = ' ';
data_buf[8] = ' ';
data_buf[9] = ' ';
data_buf[10] = ' ';
data_buf[11] = 0x08;
} else
if ((BlockAddress == ROOT_SECTOR) && (BytesInBlockDiv16 == 1)) {
/* root, chunck 2 */
/* все нули */
}
return data_buf;
}
В Windows XP «флэшка» по-прежнему полная, однако у нее появилось имя.
Дальше надо имитировать наполнение «флэшки» файлами. Но хотелось бы сделать это наполнение более-менее управляемым, чтобы изменить эти «файлы» было не слишком сложно. Задача вырисовывается посложнее, чем имитация описателя тома в корневом каталоге, продолжу позже.