RomeoGolf

Пн 03 Июль 2017

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:

Device in Win7

И его свойства:

Device in Win7

Правда, в Windows XP оно не пустое, а наоборот, полное:

Device in WinXP

Имитация корневого каталога

Добавлю заготовку для имитации содержимого корневого каталога. Пока что вставлю туда один описатель файла, соответствующий идентификатору тома. Объявляю функцию для чтения из области данных:

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 «флэшка» по-прежнему полная, однако у нее появилось имя.

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


Теги: