(UTF-8 Russian) DKFS-51 - файловая система (C) Дмитрий Корабельников d_korabelnikov@mail.ru Данная файловая система написана для 8051 (MSC-51) совместимых микроконтроллеров и предназначена для подключения FLASH памяти данных большого объема. Система обеспечивает минимальный набор функций для работы с файлами, аналогичный функциям MS-DOS для персональных компьютеров. Т е - это дисковая система для 8051 процессора на основе FLASH. Однако, данная система НЕ совместима с MS-DOS по формату данных на диске и в существующем виде НЕ предназначена для подключения MMC и SD карт памяти. Данная система предназначена прежде всего для подключения встроенных в микросистему микросхем FLASH. Так как файловая система написана полностью на Си (за исключением собственно программ обмена с FLASH, которые частично написаны на ассемблере), то она может быть адаптирована на другие 8 и 16 битные процессоры и практически любую FLASH память. Минимальные требования к памяти ROM примерно 8KB (вместе с стандартными библиотеками Keil C) RAM примерно 30 байт (или меньше) Размер диска Тип fat Размер Сектора Размер fat Общая требуемая XRAM Число файлов (байт) (байт) (включая dir,fat,diskbuf,) (переменные ... примерно) 4MB fat16 1024 4096*2=8192 12KB до 128 (или больше) 1MB fat16 512 2048*2=4096 6KB до 64 (или больше) 256KB fat16 256 1024*2=2048 4KB до 32 (или больше) 64KB (*) fat8 256 256 1KB до 16 16KB (*) fat8tiny 128 128 500 байт до 8 Как можно заметить, требования к XRAM примерно пропорциональны квадратному корню емкости диска (при увеличении размера сектора с ростом емкости), т е увеличение размера диска в 4 раза требует увеличения размера сектора (кластера) в 2 раза и увеличения длины fat в 2 раза При постоянном размере сектора увеличение размера диска в 4 раза требует увеличения размера fat в те же 4 раза (*) возможно создание нескольких дисков на FLASH большей емкости при той же требуемой XRAM Максимальная длина имени файла fat16: 12 fat8: 5 1]*** Проект DOSXRAM *** В каталоге DOSXRAM находится проект, в котором в качестве диска используется массив в XRAM demodisk а процедуры чтения и записи сектора сводятся к пересылке данных внутри XRAM. Такой подход не имеет практического значения (если не считать любителей подпитывать XRAM батарейкой в выключенном состоянии), но позволяет продемонстрировать работу программы в эмуляторе. Откройте файл dosxram.Uv2 с помощью ide Keil uVision (конкретно использовалась uVision2 v 2.33, компиляторы C, ASM v 7.03) Это должна быть полностью рабочая среда (не демо), т к необходимо скомпилировать около 10 KB кода Если в программах не видно русских букв, то нужно исправить файл uv2.exe (не забудьте сохранить резервную копию !) С помощью программы типа Hiew нужно найти внутри uv2.exe строки Courier,байт 0 и заменить их на System,байт 0 Скомпилируйте программу. Запустите Debug/StartStop Debug Session Включите окно Serial Window #1 (кнопочка с синей единичкой) Нажмите Run Теперь программа демонстрирует возможности файловой системы. (И первым делом она пишет "Отформатируем весь диск" Конечно, речь идет о демодиске эмулируемой 51-системы) Если программа идет слишком быстро (а так оно и есть, потому что я ее и так хорошо помню :) ) нужно в главном файле dosxram.c поменять значение Timer0Reload и перекомпилировать. Демонстрационные тесты. Тест 0 - игры с CRC16 Каждый сектор данных в системе защищен контрольной суммой CRC16 Т е, если длина сектора =256 байт, то на диск пишется 256 байт данных + 2 байт контрольной суммы. Целью этого теста является показать ценность алгоритма CRC16 (а не каких-то кривых "сумм по XOR" и "просто сумм") Мы видим следующее: 1) Контрольная сумма от массива нулевых байт равна 0 (независимо от числа байт) 2) Контрольная сумма от массива байт 0xFF не равна нулю, т е алгоритм отслеживает инверсию всех бит 3) При случайном сбое в виде изменения одного бита контрольная сумма не просто меняется, но меняеется "очень заметно" Тест 1 - операции с секторами Поддерживаются 3 операции с секторами Читать сектор uns char secread(unsigned char *sectorbuf,uns int sector) Писать сектор uns char secwrite(unsigned char *sectorbuf,uns int sector) Форматировать сектор uns char secformat(unsigned char *sectorbuf,uns int sector) Нумерация секторов начинается с 1 и кончается maxsector. Например, если используется FLASH память объема 64KB и сектор длиной 256 байт, то число секторов вычисляется как 65536/(256+2)=254.015, целая часть =254 Т е maxsector=254, номера секторов от 1 до 254 (но FAT8 допускает максимальный номер сектора только 253, см ниже) Функция форматирования сначала пытается записать сектор из 0xFF (и тут же прочитать), а затем сектор из 0x00 (и тут же прочитать). Функция secwrite возвращает F_SECTOR_NOT_FOUND если был запрошен неверный номер сектора и 0 при норме. Функция secread возвращает F_SECTOR_NOT_FOUND если был запрошен неверный номер сектора, F_CRC_ERROR если был обнаружена ошибка CRC и 0 при норме. Таким образом неисправность FLASH диска (если она конечно возникла) может быть легко отловлена. Реально число секторов на диске можно довести до примерно до 4000 (см ниже), что при размере сектора до 4096 байт дает максимально 16MB используемой FLASH памяти Но тут в игру вступает быстродействие 8051 контроллеров при работе с I2C / SPI. Т к реальная скорость обмена 8051 по I2C / SPI (без аппаратной поддержки) порядка 1000 байт/сек - 10000 байт/сек, то только на форматирование 16MB ушло бы несколько часов. Реальная емкость FLASH для средних систем - 256КБ, максимально 4МБ (а впрочем, можно ставить FLASH с обменом по 4 или 8 бит и иметь хорошее быстродействие, но см раздел "stat51" про отвратительные программно-аппаратные решения), минимально 16КБ, как в данной демо-программе (тоже может быть полезно) Однако, можно снизить требования к XRAM создав на одной "большой" FLASH несколько логических дисков (как это обычно делают в MS-DOS - Windows) Например, на памяти 256KB можно создать 4 диска по 64КБ. При этом на программу переключения дисков потребуется буквально несколько байт, т е требуемая XRAM равна все тому же 1 килобайту (а не четырем) Тест 2 - форматирование диска (микросистемы) diskformat(uns char drvnumber) форматирует диск - drvnumber - имя диска - может быть (unsigned char)1,2,3... или 'A','B','C'...,что более привычно для разработчика. Это требуется только в условиях производства или ремонта (или разработки :) Заметим еще раз, что форматирование например 128КБ I2C памяти занимает несколько минут. Если в процессе форматирования будут обнаружены сбойные участки, то они будут помечены как плохие (если аппаратура вообще не работает, то это, повторюсь, моментально обнаружится) Естественно, что все прочие команды файловой системы работают только после форматирования диска. В законченной программе это надо проверять (тем фактом, что (dirread()==0) && (fatread()==0) && (fat[0]==firstdatasector)) Более быстро исправность диска можно проверить функцией f_setdrive(uns char drvnumber) Функция возвращает 1, если диск отформатирован и 0 - если не форматирован или нет обмена с FLASH. При нормальном завершении эта же функция устанавливает диск в качестве текущего. Тест 3 - файловые операции Демонстрируются основные приемы работы с файлами. f_setdrive(uns char drvnumber) - устанавливает диск в качестве текущего. Возвращает 1 при норме и 0 при ошибке чтения dir и fat f_create(uns char *fname,long maxisize) создает файл с именем fname и максимального размера maxisize Т е здесь программист явно указывает какой максимальный размер может принять этот файл. Этим достигаются две вещи 1) Диск никогда "случайно" не переполняется в процессе записи 2) fat (см ниже) перезаписывается только при создании и удалении файлов, а в остальных случаях только читается. Это сделано для увеличения надежности char f_exists(uns char *fname) возвращает ненулевое значение (номер файла в каталоге) если файл есть и 0 если файла нет. Очень полезная функция, странно что в стандартных файловых системах (типа Windows :)) такой нет. f_delete(uns char *fname) удаляет файл. f_open(uns char *fname) открывает файл для записи/чтения. Возвращает 1 при норме и 0 если файл не найден Одновременно можно открыть только 1 файл. Это ограничение связано с тем, что имеется только 1 буфер диска (все-таки - микроконтроллер :) ) uns char f_read(uns char *buf,uns int nbytes) читает из файла nbytes байт в буфер buf Заметим, что указатель buf не обязательно должен указывать на XRAM, это может быть и RAM (проверьте) uns char f_write(uns char *buf,uns int nbytes) пишет из buf nbytes байт в файл. Аналогичное замечание. Это связано с тем, что Keil C использует указатели, в которых хранится тип памяти. uns char f_seek(long pos) - переместить указатель внутри файла. Это центральная функция файловой системы. Она вызывается из f_read и f_write при чтении (записи) каждого байта данных. Не очень быстро, зато просто и надежно. Функция может быть вызвана явно для произвольного доступа внутри файла. bit f_close() закрывает файл Как можно заметить (см fileio2.c) функция позволяет использовать часы реального времени (фирмы Dallas Semiconductor :) ) для указания времени закрытия файла (но этот код закомментирован) Для этого надо считать данные с часов а затем преобразовать эти данные в секунды "в стиле UNIX" В UNIX время считается в секундах. 1 января 1970 года 00:00:00 соответствует 0 секунд. Таким образом очень легко вести подчет времени в 32 битном (4 байта) формате. Правда, допустимы только даты от 2000 года (1 января 00:00:00) до 2037 года (до 31 декабря 23:59:59), т к используется signed long в качестве значения секунд (max=0x7fffffff). При желании можно подправить программу (см unixtime.c Не забудьте, что 2100 год - НЕ високосный, куда-то далеко мы ушли от главной темы) Внутренние команды системы uns char dirread() читать каталог uns char fatread() читать fat При норме возвращают 0 При ошибке возвращают код ошибки Как уже было сказано, используются при старте микросистемы для проверки форматирован ли диск (см выше) В остальное время вызываются неявно. uns char dirwrite() писать каталог uns char fatread() писать fat При норме возвращают 0 При ошибке возвращают код ошибки вызываются неявно (из команд файловой системы) void dircmd() Выводит список файлов на последовательный порт. Аналогична команде dir MS-DOS Используется при отладке работы с помощью терминальной программы. Тест 4 - Так что же такое эти dir и fat ? dir - это каталог диска, содержащий информацию о файлах. Одна файловая запись определена следующим образом: #ifndef USEFAT8 //структура записи файла в каталоге (должна иметь длину 32 байта) struct filerectype {uns char name[13];uns char attr;long time;long size;long maxsize;int cluster;uns long unused;}; #else //структура записи файла в каталоге (должна иметь длину 16 байт) struct filerectype {uns char name[6];uns char attr;long time;uns int size;uns int maxsize;uns char cluster;}; #endif name - имя файла attr - атрибут файла (в данной демо не используется) time - время последнего закрытия файла в формате UNIX (в данной демо не используется, тк нет часов) size - размер файла в байтах maxsize - максимально допустимый размер, который может иметь этот файл cluster - начальный сектор файла unused - не используется В максимальном варианте (32 байта на запись) можно использовать имя файла длиной до 12 байт (в том числе имена типа MS-DOS: до 8 - имя, ".", до 3 - расширение) В минимальном варианте (16 байт на запись) можно использовать имя файла длиной до 5 байт. При хранении в каталоге имена преобразуются в верхний регистр (upper case). Имена могут содержать только латинские буквы (не русские !) Количество файлов в каталоге (maxfiles) выбирается таким образом, чтобы каталог занимал целое число секторов. Как можно видеть, каталог размером 1 кбайт может хранить 32 (и даже 64 в минимальном варианте) файла, что вполне достаточно для микросистемы. Удаленная запись (стертый файл) помечается в каталоге так: name[0]=1, остальные байты записи =0 Таким образом dir определен как struct filerectype dir[maxfiles],wdir; т е это просто массив записей. fat (file allocation table - таблица размещения файлов) определяется следующим образом (данное описание не имеет ничего общего со стандартом Microsoft) Допустим, мы создаем файл, который начинается с сектора 3 и занимает 4 сектора:3,4,5,6 Тогда 1) В каталог пишется начальный сектор файла wdir.cluster=3 2) В fat пишется следующая информация: fat[3]=4 fat[4]=5 fat[5]=6 fat[6]=0xFF (END_SECTOR) Т е номер первого сектора файла хранится в каталоге, а каждый последующий сектор находится в таблице fat: if (fat[sector]==END_SECTOR) { то это последний сектор файла } else {nextsector=fat[sector]; .....} Зачем это нужно. Предположим, мы создали три файла по file1,file2,file3 по 7 секторов в каждом 111111122222223333333 А затем file2 удалили с диска <.....> 1111111.......3333333 Предположим, мы теперь хотим создать файл file4 размером 10 секторов. Очевидно, что нужно использовать пустые 7 секторов v fat[17]=18 // file3 fat[18]=19 fat[19]=20 fat[20]=21 fat[21]=22 fat[22]=23 fat[23]=END_SECTOR fat[24]=28<--------------v fat[25]=29 fat[26]=END_SECTOR Таким образом, fat позволяет распределять секторы внутри области данных совершенно произвольным образом, а не последовательно. Существуют следующие зарезервированные значения для fat END_SECTOR=0xFFFF BAD_SECTOR=0xFFFE - неисправный сектор (содержащий неисправные ячейки FLASH) 0x0000 - свободный сектор Что произойдет, если в какой-то момент (например при отключении питания) dir или fat будут разрушены ? Если система только читает файлы из FLASH, то dir и fat тоже только читаются. Поэтому никакой проблемы нет - при повторном включении микросистема считает dir и fat и всё придет в норму. Но если мы пишем область dir или fat и в этот момент отключается питание, то происходит катастрофа - все данные становятся недоступны. Поэтому используются следующие решения. 1) fat изменяется (пишется) только в процессе создания / удаления файлов В процессе записи файлов fat не изменяется 2) В законченной высоконадежной системе требуется резервирование (создание нескольких одинаковых) fat и dir Microsoft в своих традиционных решениях (FAT12,FAT16) использует 1 корневой каталог (dir) и 2 FAT Это ненадежный подход. Более реально в современных условиях несколько dir (2,3,4) fat (2,3,4) и многократное резервирование секторов (см ниже) Что призойдет, если питание будет отключено в процессе записи в файл ? В лучшем случае, это произойдет в момент, когда пишется сектор файла. Если мы после включения системы попытаемся прочитать файл заново, то функция secread возвратит на "недописанном" секторе F_CRC_ERROR, что можно легко отловить из прикладной программы. Но восстановить при этом данные не удастся. В худшем случае, это произойдет в момент, когда пишется dir. В этом случае пропадут все файлы. Как можно восстанавливать информацию при аварии ? Единственный реальный способ восстановления - это полное резервирование информации. В данной demo secwrite пишет информацию в одну область flash, а secread - читает из 1 области flash, возвращая либо 0 (норма), либо F_CRC_ERROR. Но легко реализуема схема, при которой secwrite пишет одну и ту же информацию в 2 (3,4) сектора FLASH, а secread читает данные из 2 (3,4) секторов одновременно. Если при этом в каком-то из секторов обнаружена плохая CRC, то возрвращаются данные из резервного сектора, а плохой переписывается правильными данными. Возможно даже голосование по схеме "2 из 3", "3 из 4" и т д. В любом случае, информация, которая "не успела записаться" исчезнет, но система при этом сохранит исправность. Тест 5 - добавление информации в файл. Как можно заметить, при открытии файла не указывается явно - открывается он для чтения или записи. Это создает небольшую сложность при добавлении новых данных в конец файла. Данный пример просто показывает, как это сделать, используя внутренние переменные файловой системы. Проблема медленного процессора, опция #USEFAT8 для маленьких дисков. На тестах можно заметить тот простой факт, что скорость записи-чтения данных для системы в режиме FAT16 - где-то 1 - 1.5 KB/sec даже для имитации диска внутри XRAM. Это связано с тем, что процессор 8051 не имеет 16разрядных регистров (в отличие от классического 8битного процессора 8080, Z80). Сравните например код для пересылки память-память для 8051 и Z80 Для 8051 movsample:mov DPTR,#srcmem ; адрес памяти, откуда пересылаются байты mov r1,dph mov r2,dpl mov dptr,#dstmem ; адрес памяти, куда пересылаются байты mov r3,dph mov r4,dpl mov dptr,#memlen ; число пересылаемых байт mov r5,dph mov r6,dpl movbyte:mov a,r5 ; проверка счетчик байт = 0 ? orl a,r6 ; jz m2 ; да - выход из цикла mov DPH,r1 mov DPL,r2 movx a,@dptr ; вроде бы одна команда 8) inc DPTR mov r1,dph mov r2,dpl mov DPH,r3 mov DPL,r4 movx @dptr,a ; мы наконец-то переслали 1 байт inc DPTR mov r3,dph mov r4,dpl dec R6 cjne R6,#0FFh,movbyte dec R5 jmp movbyte m2: ret ; наконец-то пересылка закончена Оттранслируем для большей убедительности: 2000 1 srcmem equ 2000h 4000 2 dstmem equ 4000h 0200 3 memlen equ 200h 4 0000 902000 5 movsample:mov DPTR,#srcmem ; адрес памяти, откуда пересылаются байты 0003 A983 6 mov r1,dph 0005 AA82 7 mov r2,dpl 0007 904000 8 mov dptr,#dstmem ; адрес памяти, куда пересылаются байты 000A AB83 9 mov r3,dph 000C AC82 10 mov r4,dpl 000E 900200 11 mov dptr,#memlen ; число пересылаемых байт 0011 AD83 12 mov r5,dph 0013 AE82 13 mov r6,dpl 0015 ED 14 movbyte:mov a,r5 ; проверка счетчик байт = 0 ? 0016 4E 15 orl a,r6 ; 0017 601B 16 jz m2 ; да - не выполнять из цикл 17 0019 8983 18 mov DPH,r1 001B 8A82 19 mov DPL,r2 001D E0 20 movx a,@dptr ; вроде бы одна команда 8) 001E A3 21 inc DPTR 001F A983 22 mov r1,dph 0021 AA82 23 mov r2,dpl 24 0023 8B83 25 mov DPH,r3 0025 8C82 26 mov DPL,r4 0027 F0 27 movx @dptr,a ; мы наконец-то переслали 1 байт 0028 A3 28 inc DPTR 0029 AB83 29 mov r3,dph 002B AC82 30 mov r4,dpl 002D 1E 31 dec R6 ; уменьшить 16 битный счетчик на 1 002E BEFFE4 32 cjne R6,#0FFh,movbyte 0031 1D 33 dec R5 0032 80E1 34 jmp movbyte 0034 22 35 m2: ret ; наконец-то пересылка закончена А теперь та же операция по пересылке байт для процессора Z80 0001 0000 .org 0 0002 0000 srcmem .equ 2000h 0003 0000 dstmem .equ 4000h 0004 0000 memlen .equ 200h 0005 0000 0006 0000 21 00 20 ld hl,srcmem 0007 0003 11 00 40 ld de,dstmem 0008 0006 01 00 02 ld bc,memlen 0009 0009 ED B0 ldir 0010 000B .end Мы видим программу в 5 раз короче, причем собственно пересылку данных выполняет 1 команда, а не цикл из 19 команд. Мораль: быстродействие процессоров типа 51 в несколько раз (иногда в 10-20 раз) меньше, чем Z80, даже с учетом современных версий процессоров "1 команда за 4 такта, кварц до 40 мегагерц" Поэтому для задач, которые требуют большего быстродействия, но не очень большой FLASH, система была доработана для использования в основном байтовых переменных (внутри самой файловой системы). Этот режим включается командой #define USEFAT8 в основной программе или в файле filesys.h В режиме FAT8 система имеет в 2 раза большее быстродействие но имеет ограничения: 1) Размер файла не превышает 65535 байт (0xFFFF) 2) Число секторов не превышает 253, что при секторе в 512 байт дает максимальную емкость диска 512*(253-1-1)=128512 байт (при условии, что 1 сектор занимает dir, 1 сектор занимает fat) Однако, можно взять FLASH большой емкости и создать на ней несколько одинаковых (по емкости) дисков. 3) Имя файла ограничено 5 символами. 2] *** Проект DOS24C_FAT8R *** В каталоге DOS24C_FAT8R находится проект, который демонстрирует работу файловой системы с микросхемой Atmel 24C512 Для установки программы на реальную микросистему требуется следующее: 1) Микросистема должна иметь по крайней мере 2 KB XRAM При необходимости замените значение XDATALEN в файле Startup.a51 2) Микросистема должна иметь порт RS-232 для связи с компьютером 3) В файле i2cdemo.c укажите правильный адрес выбора FLASH #define ichip2 0xA0 Допустимы: 0xA0,0xA2,0xA4,0xA6 в зависимости от адресных выводов FLASH(см pdf) Для микросхем 24256 (32 KB) и 24128 (16 KB) нужно также заменить фрагмент в функции iwrite if ((addr & 0x7F)==0) на if ((addr & 0x3F)==0) т к микросхемы 24256 и 24128 имеют размер страницы 64 байта (см ниже) Также для этих микросхем следует изменить размер диска для 24256 (32 KB) #define demomaxsector 127 для 24128 (16 KB) #define demomaxsector 63 4) Файл asmcode.a51 содержит функции работы с I2C Здесь нужно указать правильные значения SDA и SCL (выводы порта) В файле asmcode.a51 находятся следующие функции, связанные с I2C Старт I2C void cl_start(); Стоп I2C bit cl_stop(); возвращает состояние линии SDA Писать 1 байт на I2C bit wrdata(unsigned char dat); возвращает 0 - есть подвеждение от slave I2C (SDA=0 в момент 9го импульса SCL), 1 - нет подтверждения Писать 2 байта (адреса памяти FLASH) на I2C bit wraddr(unsigned int addr); возвращает 0 - есть подвеждение от slave I2C (SDA=0 в момент 9го импульса SCL), 1 - нет подтверждения Читать nbytes байт из I2C в XRAM с адресом буфера memory void rdata2(unsigned char *memory,unsigned int nbytes); 5) В файле dos24c.c (главном файле программы) следует убрать (закомментитровать) строку textinputemulate=1; чтобы программа реально обращалась к COM порту, а не имитировала ввод. Работа с микросистемой с помощью программы HyperTerminal. При правильном подключении (надеюсь, Вы точно знаете, как подключать кассу к компьютеру через COM порт) HyperTerminal используется следующим образом Создайте новое подключение Название - имя файла, в котором будет храниться данная информация Код страны, города, телефон - не важно Подключение - прямое соединение (с правильным номером порта) Настройка порта скорость - в соответствии с микросистемой. В нашем случае 19200, но на всякий случай проверьте соответствие типа процессора, кварца и настроек (файл dos24c.c функция comspeed) Биты данных - 8 Четность - нет Стоп-бит - 1 Управление потоком - нет Далее: Вид - Шрифт Courier, набор символов - кириллица Далее нажимаем кнопку Связь и включаем микросистему. Если аппаратура и кабель в норме, то система выдает A> и далее можно тестировать насколько правильно работают команды (см cmd.c) Что же дает файловая система (помимо очевидного удобства написания прикладных программ) при использовании FLASH памяти ? Рассмотрим процедуру записи для Atmel 24512 bit iwrite(unsigned char *memory,unsigned int addr,unsigned int nbytes) {data unsigned int cnt;bit first; flash_timeout=0; flash_retry=F_RETRYCNT; // ожидать освобождения шины I2C (повторять пока SDA не установится в 1) do {if (flash_retry==0) {flash_timeout=1;return 1;}} while (cl_stop()==0); //ожидать готовности 24C512 (повторять пока не придет 0 от чипа при передаче кода) flash_retry=F_RETRYCNT; // timeout = 100 timer interrupts (100 milliseconds) do {cl_stop();cl_start();if (flash_retry==0) {flash_timeout=1;return 1;}} while (wrdata(ichip2)); //записать начальный адрес wraddr(addr);first=1; for (cnt=0;cnt