Pull to refresh

Микро БД для конфигурации на микроконтроллере

Reading time 6 min
Views 20K
Многие разработчики программ для микроконтроллеров сталкивались с проблемой хранения конфигурационных данных. Это могут быть калибровочные коэффициенты измерительного алгоритма или просто последний выбранный пользователем пункт меню. Для микроконтроллера, имеющего возможность записывать в собственную flash-память, решение кажется простым — стираем сегмент и пишем туда все, что нужно. Однако, если необходимо еще и обеспечить отказоустойчивость по отношению к выключению питания в произвольный момент, задача становится нетривиальной, — по сути необходимо реализовать маленькую базу данных с механизмом обеспечения атомарности операций записи и восстановлением после сбоев. Решение этой задачи для микроконтроллеров семейства MSP430 — под катом. По количеству используемых ресурсов оно подходит даже для самых младших членов этого семейства — с размером RAM от 256 байт и falsh-памяти от 8Kb. В качестве бонуса — интерфейс командной строки (через UART) для чтения и записи конфигурации.

Формат данных


Мы ограничимся хранением двухбайтовых целых чисел и строк длиной не более 15 символов, что вполне достаточно для большинства приложений. При желании этот набор легко расширить путем незначительных модификаций кода. Хранимые данные будут идентифицироваться по имени и возможно одному или двум индексам. Максимальный индекс мы также ограничим 15-ю, при этом целочисленные параметры смогут иметь 2 индекса, а вот строки только один — место второго индекса займет длина строки.

Первое, что приходит в голову — это хранить сначала имя, потом данные. Но в нашем случае такой подход категорически неэффективен. Дело в том, что мы не можем просто переписать старые данные. Нам придется писать новое значение на новое место и уже потом разбираться, которое из них самое последнее. А это значит, что одно и то же имя нам возможно придется записать много раз. Поэтому мы будем хранить имена отдельно введя новую сущность — тип конфигурационных данных. Тип — это имя плюс описание того, что под этим именем может храниться. А именно, базовый тип — целое или строка, а также размерность индексов — 0, 1 или 2 для целых, 0 или 1 для строк. Кроме того, в описателе типа будет храниться значение по умолчанию, которое мы будем возвращать при чтении данных, которые еще не записывались. Таблица типов будет помещена во flash память еще на этапе компиляции и меняться не будет никогда. При сохранении данных мы просто сошлемся на тип, указав его индекс в таблице, для которого нам понадобится один байт. Еще в один байт мы упакуем 2 индекса экземпляра этого типа или индекс и длину строки. Дальше собственно будут записаны сами данные.

Организация памяти


Прежде чем что то записать во flash-память, ее необходимо стереть. Стирание происходит сегментами по 512 байт. Это означает, что для хранения данных нам этих сегментов понадобится как минимум 2. Один будет хранить данные, пока второй мы будем стирать. Если данных много — возьмем 2 рабочие области из N сегментов. Пока одна хранит данные, вторую можно стирать. В целом алгоритм следующий — пишем данные последовательно в рабочую область, после того, как место заканчивается, стираем нерабочую область, сохраняем там последние версии всех данных из рабочей области (снапшот), после чего меняем рабочую область. При этом у нас остается единственная проблема — как потом выбрать рабочую область, которая была записана последней?

Порядковые номера


Стандартное решение проблемы поиска последней версии объекта состоит в присваивании версиям порядковых номеров. Оно хорошо работает, если эти номера достаточно длинные, чтобы не повторяться в течении всего срока эксплуатации программы. В нашем случае нам трудно позволить себе номера длиннее 2 байт. Это означает, что фактически номера будут располагаться по кругу с длиной окружности 65536. Если наши экземпляры конфигурационных данных занимают меньше половины такой окружности, то их можно упорядочить по степени свежести, а если больше — то уже нет. Это означает, что копируя данные в снапшот, мы вынуждены обновить их порядковые номера. Но тогда, если операция копирования будет прервана на середине, возникнет полная путаница — часть данных будет иметь более свежие копии, а часть нет, и мы не сможем просто выбрать одну рабочую область из двух. Но это еще пол беды. Самое неприятное — это то, к каким результатам приводит выключение питания в произвольный момент.

Контрольные суммы


Чтобы понять масштаб бедствия, автором был написан автоматический тест, реализующий 2 модели аварийного прерывания работы программы — сброс из обработчика прерывания и перебои в цепи питания. Первый способ не приводил к чему то, с чем не могла справиться даже простая реализация на основе порядковых номеров. Что не удивительно, если учесть, что при выполнении операций стирания и записи во flash выполнение программы из того же flash-а просто останавливается, и у нас нет никаких шансов прервать эти операции посередине. Поэтому был реализован второй, более радикальный метод. Питание на микроконтроллер было подано через резистор в 510ом, а одна из универсальных ног была соединена с землей. Для симуляции перебоя с питанием эта нога включалась на вывод, и на нее подавался высокий уровень. В результате ток потребления резко возрастал, а питающее напряжение падало ниже допустимого. В итоге выяснилось, что самое плохое, что может случиться, — это неполное стирание сегмента, в результате которого его содержимое может стать каким угодно. В результате на первый план вышло использование контрольных сумм для обнаружения этой ситуации, а от использования порядковых номеров было решено отказаться. Контрольная сумма CRC16 записывается после данных, таким образом на каждую запись мы имеем 4 байта дополнительной информации — 2 байта заголовка и 2 байта контрольной суммы. Однако, сами по себе контрольные суммы не решают проблему выбора рабочей области после сбоя.

Статусные метки


Для того, чтобы пометить рабочую область, было решено просто записывать в нее специальную 'статусную метку'. Ее формат похож на формат данных с той лишь разницей, что даных там нет, а индекс типа равен максимально возможному, т.е. 255. Поскольку возможна ситуация, когда у нас будет 2 помеченные области, мы введем еще одну метку, которой будет помечаться область, в которую мы более не собираемся ничего записывать. Первую метку мы назовем открывающей, а вторую закрывающей. При создании снапшота сначала запишем закрывающую метку в старую рабочую область, а затем создадим снапшот в новой и запишем в нее открывающую метку. Завершим переключение рабочей области мы записью завершающей метки в старую рабочую область. Смысл этого действия станет ясен ниже.

Латентные ошибки


При перебоях питания во время записи могут возникать 'латентные' ошибки. Если инжекция заряда в плавающий затвор элемета flash-памяти не завершилась, то какое то время из нее могут читаться правильные данные, а потом начнут читаться неправильные. Появление такой ошибки в середине рабочей области будет иметь фатальные последствия. Однако есть простой способ избежать продолжения записи после данных, запись которых могла быть прервана. Для этого и предназначена завершающая метка. Поскольку она пишется последней в область, которая уже не является рабочей, если мы почитали завершающую метку, то запись метки, открывающей рабочую область, гарантированно не была прервана. Эту комбинацию из открывающей метки в рабочей области и завершающей в нерабочей, мы будем называть стабильным состоянием. Если при старте мы видим нестабильное состояние, то прежде, чем писать новые данные, мы создаем снапшот и меняем рабочую область, тем самым приводя систему в стабильное состояние.

Использование


Создаем таблицу типов со значениями по умолчанию:

#include "cfg_types.h"
const struct cfg_type g_cfg_types[] = {
	CFG_STR("название", ""),
	CFG_INT("счетчик", 1),
	CFG_INT_I("массив", 0),
	...
	CFG_TYPE_END
};
unsigned g_cfg_ntypes = CFG_NTYPES;

Создаем хранилище конфигурации:

#include "config.h"
#pragma data_alignment=FLASH_SEG_SZ
static __no_init const char cfg_storage[2][CFG_BUFF_SIZE] @ "CODE";
struct config g_cfg = {{
	{cfg_storage[0]},
	{cfg_storage[1]}
}};

Инициализируем его:

#include "config.h"
extern struct config g_cfg;
cfg_init(&g_cfg);

Читаем данные:

const char* name = cfg_get_str(&g_cfg, get_cfg_type("название"));
const struct cfg_type* cnt_t = get_cfg_type("счетчик");
int cnt = cfg_get_val(&g_cfg, cnt_t);

Обновляем данные:

cfg_put_val(&g_cfg, cnt_t, cnt+1);


Результаты тестирования


Тестирование путем записи 5 миллионов значений с симуляцией перебоев питания 20 тысяч раз не выявило каких либо проблем, после чего эксперимент был остановлен во избежание полной выработки ресурса flash-памяти.

Исходный код


Лежит тут. Проект для IAR рассчитан на MSP430G2553, в качестве аппаратной платформы автор использовал последнюю версию MSP-EXP430G2 LaunchPad. Проект реализует автоматический тест, но вы можете его легко адаптировать под собственные задачи. В нем реализован также интерфейс командной строки (через UART) для доступа к конфигурации. Команда types печатает список типов, команда cfg печатает текущее содержимое хранилища, команда set обновляет конфигурацию. Команда help печатает справку по командам.
Tags:
Hubs:
+24
Comments 3
Comments Comments 3

Articles