STM32 + Параллельная NAND Flash + файловая система Yaffs. Часть вторая



Еще несколько слов о принципе работы файловой системы Yaffs, структура каталогов и теория портирования. Основной источник — это исчерпывающая документация на официальном сайте. Начало здесь.



Как работает yaffs


На самом деле существуют две версии файловой системы Yaffs1 и Yaffs2. Разница между ними в том, что Yaffs2 немного сложнее и более продвинута, а Yaffs1 по-проще и легче портируется. Предпочтение было отдано Yaffs1, поэтому о ней и пойдет речь.
Как уже было сказано ранее, NAND Flash разбита на страницы, которые являются минимальным фрагментом для записи, при этом минимальный объем стирания — это блок. Yaffs же в свою очередь оперирует таким понятием, как «кусок» (chunk), который тоже не делим и как правило совпадает по размеру с размером страницы. Когда программист создает или изменяет файл, то происходит активное формирование таких кусков и запись их в страницы памяти. Но тут есть одна хитрость. Чтобы ее понять немного рассуждений.

Предположим у нас есть архив измерений за последние 10 минут, за час за сутки и за неделю. Логично было бы разместить каждый из этих архивов в разные участки памяти так, чтобы они друг другу не мешали, но в этом случае участок с 10-минутным архивом будет будет стираться и записываться каждую минуту. Более того, поскольку нельзя стереть менее чем блок, когда понадобиться удалить из архива устаревшую запись, вместе с ней удалятся и все остальные записи, если они лежат в одном блоке. Что делать? Буферизировать весь архив перед стиранием блока? Ну хорошо, пусть так, но ведь в добавок окажется, что участок памяти с 10-минутным архивом начнет изнашиваться гораздо быстрее, чем все остальные. 100 тысяч циклов записи-стирания это хоть и много, но не бесконечно. При этом остальные блоки будут практически простаивать без дела.

Именно поэтому Yaffs снабжает каждый свой кусок (chunk) дополнительной информацией, которую в числе прочего размещает в spare области NAND'ы. Эту дополнительную информацию авторы файловой системы зовут тегами (tags). Список тэгов, для каждого куска следующий:
  • Object Id (obj_id) — Идентификатор объекта, которому принадлежит кусок (chunk'а). В роли объекта, как правило выступают файлы.
  • Chunk Id (chunk_id) — Идентификатор куска (chunk'а), он несет в себе информацию в каком месте файла этот кусок лежит. Так если Chunk Id = 0, то в этом куске скорее всего заголовок файла со служебной информацией, если 1, то первый кусок с содержимым файла.
  • Deletion Marker (is_deleted) — Маркер удаления. Это, пожалуй, самый интересный тэг. Если он взведен, то это значит, что информация в этом куске (chunk'е) уже не актуальна. Почему не актуальна? Вероятнее всего файл был изменен или удален.
  • Byte Count: (n_bytes) — Количество байт данных в этом куске. Их не может быть больше, чем максимальный размер куска равный размеру страницы, но вполне возможно, что может быть и меньше.
  • Serial Number (serial_number) — Уникальный серийный номер куска (chunk'а), нужен на случай, если chunk_id и obj_id совпадут с другим chunk'ом. Он кстати есть только в Yaffs1.


Думаю не трудно догадаться, как эти тэги помогают не «затирать до дыр» отдельные участки памяти, а использовать ее равномерно. Каждый раз, когда файл меняется/удаляется, те chunk'и, которые потеряли актуальность помечаются в тэге is_deleted, как удаленные, а обновленная информация попадает в новый кусок. Таким образом, при чтении файла Yaffs пробегается по флешке и поднимает все связанные с ним актуальные куски, а при изменении помечает часть удаленными и часть создает новые. Как следствие файл «размазывается» по всему объему NAND Flash, уменьшая износ отдельных участков.

Когда же зовется операция стирания блока? Справедливый вопрос. Учитывая тот факт, что актуальные данные могут лежать в одном блоке с неактуальными, просто так блок не сотрешь. Тут в работу вступает сборщик мусора. Его задача отслеживать блоки, в которых скопилось большое количество кусков помеченных взведенным тэгом is_deleted. Когда такой блок появляется, сборщик мусора собирает остатки «нормальных» кусков и перекидывает их в пустой блок, после этого вызывает стирание «замусоренного» блока.

А как с учетом бэдов? Тут тоже все достаточно однозначно. Для определения битый блок или нет Yaffs смотрит в spare область страницы. Если страница не тронута, то первые три байта должны быть установлены в значение 0xFF, а если запись производилась, то там должно лежать посчитанное значение ECC. Если по результатам чтения ECC не сошлось, то в самой первой странице блока в 4 и 5 байте spare области будет записано значение 0x59 ('Y'), которое впоследствии будет служить сигналом о том, что блок битый.

Структура каталогов


Все источники для скачивания перечислены здесь. По непонятной причине склонировать сам репозитарий не удалось, так что просто пришлось скачать архив тут.

Структура каталогов библиотеки изобилует файлами, но для работы нужна лишь их малая часть и она находится в ./direct. Вот только нюанс: для того, чтобы там оказались действительно все необходимые файлы, нужно запустить скрипт ./direct/handle_common.sh. После того, как он отработает, в папке ./direct появятся дополнительные исходники. Все необходимые заголовочные файлы, которые понадобятся при сборке тоже будут там же, поэтому в CMAKE-файле, MAKE-файле или параметрах проекта, в зависимости от предпочтений, нужно будет прописать только путь к этому каталогу. Подключить нужно будет следующие исходники:

  • yaffsfs.c
  • yaffs_allocator.c
  • yaffs_attribs.c
  • yaffs_bitmap.c
  • yaffs_checkptrw.c
  • yaffs_ecc.c
  • yaffs_error.c
  • yaffs_guts.c
  • yaffs_hweight.c
  • yaffs_nameval.c
  • yaffs_nand.c
  • yaffs_packedtags1.c
  • yaffs_packedtags2.c
  • yaffs_summary.c
  • yaffs_tagscompat.c
  • yaffs_tagsmarshall.c
  • yaffs_verify.c
  • yaffs_yaffs1.c
  • yaffs_yaffs2.c


Портирование


Разработчики утверждают, что несмотря на то, что Yaffs изначально проектировался под Linux, все что зависит от операционной системы аккуратно вынесено в отдельные модули. И, если есть желание натянуть их файловую систему на «голое» железо с RTOS или без, то нет никаких проблем, нужно лишь воспользоваться модулем Yaffs Direct Interface (YDI) и со своей стороны обеспечить несколько функций для доступа к «железу» и ОС.

Yaffs Direct Interface

Для «связи» с ОС нужны следующие функции.

Функции для регистрации и считывания ошибок, которые произошли во время работы файловой системы:

void yaffsfs_SetError(int err);
int yaffsfs_GetLastError(void);
void yaffs_bug_fn(const char *file_name, int line_no);

Назначение этих функций простое — передать операционной системе, что что-то не ладное с файловой системой. Это может быть полезно в Linux, где регистрация ошибок можно организоваться, например, через syslog. В случае с микроконтроллером можно просто оставить их пустыми, хотя это дело вкусов.

Функции исключающего доступа:

void yaffsfs_Lock(void);
void yaffsfs_Unlock(void);

Они нужны для исключения одновременного доступа к файловой системе из разных задач. Логично туда поместить работу с мьютексом.

Работа со временем:

u32 yaffsfs_CurrentTime(void);


Инициализация всего, что связано с ОС, в нашем случае уместно проинициализировать мьютекс для исключаеющего доступа:

void yaffsfs_OSInitialisation(void);


Ну и наконец, функции для динамического выделения и освобождения памяти:

void *yaffsfs_malloc(size_t size);
void yaffsfs_free(void *ptr);


Возможно наличие функции динамической работы с памятью могут вызвать недоумение, но проблем тут нет. Во-первых, динамическое выделение памяти в эмбеде совсем не беда, если вы не собираетесь освобождать память. А ведь именно так и будет, если в приложении нужно лишь единожды создать/открыть файл. А во-вторых даже, если и нужно на ходу создавать новые файлы, старые удалять, создавать еще, открывать и тому подобным образом всячески вызывать динамическое выделение и освобождение памяти, можно написать функции выделения памяти так, чтобы они брали память из статического пулла, с фиксированными размером выделяемого или освобождаемого куска.

Для связи с «железом» нужна одна функция:

int yaffs_start_up(void);

Это может показаться странным, но всех вышеперечисленных функций достаточно, чтобы программа собралась. Но получится своего рода «пустышка». На самом деле, чтобы действительно привязаться к интерфейсу NAND Flash, нужно к файловой системе «добавить устройство». Под «устройством» подразумевается указатель на структуру типа:

struct yaffs_dev *dev;

Структура, на которую ссылается этот указатель несет в себе информацию о памяти такие как размер страницы, размер блока, размер области spare и т.д. Но самое главное в этой структуре есть указали на функции непосредственного доступа к NAND Flash. Прототипы этих функции следующие.
Запись страницы (chunk'a):

	int (*drv_write_chunk_fn) (struct yaffs_dev *dev, int nand_chunk,
				   const u8 *data, int data_len,
				   const u8 *oob, int oob_len);

Чтение страницы (chunk'a):

	int (*drv_read_chunk_fn) (struct yaffs_dev *dev, int nand_chunk,
				   u8 *data, int data_len,
				   u8 *oob, int oob_len,
				   enum yaffs_ecc_result *ecc_result);

Стирание блока:

	int (*drv_erase_fn) (struct yaffs_dev *dev, int block_no);

Пометить блок как битый:

int (*drv_mark_bad_fn) (struct yaffs_dev *dev, int block_no);

Проверить не битый ли блок:

int (*drv_check_bad_fn) (struct yaffs_dev *dev, int block_no);

Инициализация и деиницализация NAND'ы:

int (*drv_initialise_fn) (struct yaffs_dev *dev);
int (*drv_deinitialise_fn) (struct yaffs_dev *dev);


Таким образом, чтобы «добавить устройство» нужно заполнить структуру типа struct yaffs_dev необходимыми параметрами, положить в нее указатели на функции по работе с памятью и передать указатель на эту структуру файловой системе. Удобнее всего это сделать в функции yaffs_start_up().
Для пояснения сказанного приведу пример:

int yaffs_start_up(void)
{
	struct yaffs_dev *dev = &devNand128w3a2bn6e;

	dev->param.name = dev_name;
	dev->driver_context = NULL;
	dev->param.start_block = 0;
	dev->param.end_block = 1023;
	dev->param.chunks_per_block = 32;
	dev->param.total_bytes_per_chunk = 512;
	dev->param.spare_bytes_per_chunk = 16;
	dev->param.is_yaffs2 = 0;
	dev->param.use_nand_ecc = 0;
	dev->param.n_reserved_blocks = 5;
	dev->param.inband_tags = 0;
	dev->param.n_caches = 10;

	dev->tagger.write_chunk_tags_fn = NULL;
	dev->tagger.read_chunk_tags_fn = NULL;
	dev->tagger.query_block_fn = NULL;
	dev->tagger.mark_bad_fn = NULL;
	yaffs_tags_compat_install(dev);
        /* Это мои функции для доступа к NAND FLash */
	dev->drv.drv_write_chunk_fn = yaffs_nand_drv_WriteChunk;
	dev->drv.drv_read_chunk_fn = yaffs_nand_drv_ReadChunk;
	dev->drv.drv_erase_fn = yaffs_nand_drv_EraseBlock;
	dev->drv.drv_mark_bad_fn = yaffs_nand_drv_MarkBad;
	dev->drv.drv_check_bad_fn = yaffs_nand_drv_CheckBad;
	dev->drv.drv_initialise_fn = yaffs_nand_drv_Initialise;
	dev->drv.drv_deinitialise_fn = yaffs_nand_drv_Deinitialise;

	/* The yaffs device has been configured, install it into yaffs */
	yaffs_add_device(dev);

	return YAFFS_OK;
}


Если все вышеописанное обеспечено, то можно считать, что файловая система портирована. Продолжение следует...

Содержание


Часть первая
Часть вторая
Часть третья

Комментарии (5)

RSS свернуть / развернуть
А почему битым метится сразу весь блок, а не отдельная страничка?
+1
  • avatar
  • Vga
  • 30 августа 2017, 13:01
Считается, сам не знаю почему, что если в блоке встретилась хотя бы одна битая страница, то этот блок лучше обходить стороной. По крайней мере в Yaffs так поступают.
0
Вы знаете, я только что подумал и понял почему. Если помечать битыми страницы, то при стирании блока признак битости потеряется и нужно будет снова отрабатывать алгоритм по отбраковыванию страницы.
+1
Можно перед стиранием их прочитать, а после — записать. Проблема, правда, в том, что они могут и не записаться — ячейки-то изношены.
Ну и плюс к тому, в блоке, по видимому, все страницы примерно одинаково изношены и если сдохла одна — остальные тоже на ладан дышат.
0
все верно. циклы считаются по количеству стираний.
+1
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.