Часто возникает задача сохранить изменяемые данные, например конфигурацию, во флэш памяти микроконтроллера. Решение кажется простым, однако обеспечить надежность обновления данных при условии, что питание может отключиться в любой момент, оказывается весьма нетривиально, и даже использование контрольных сумм не решает проблему полностью. Из этой статьи вы узнаете
  • как устроена флэш память
  • к каким проблемам приводит выключение питания в момент записи или стирания
  • как эти проблемы решаются

Для желающих применить на практике — работающий код под STM32F4

Классический подход к проблеме заключается в том, чтобы писать данные на флэш, сопровождая их контрольными суммами, чтобы можно было проверить целостность данных при чтении. Именно на таком подходе основана схема, предложенная автором для микроконтроллеров MSP430. Однако, она имеет 2 недостатка — сложность, обусловленная стремлением сэкономить память за счет хранения данных по частям, которые могут обновляться независимо, и отсутствие строгих гарантий целостности данных при отключении питания в момент записи. Под целостностью мы здесь понимаем следующее:
  • данные оказываются либо записаны либо нет
  • статус операции не меняется со временем, т.е. если данные записаны, они доступны всегда, если же нет, то они вдруг не появятся в будущем
Поэтому, при разработке очередного устройства на базе STM32 было решено сделать вторую попытку строгого решения этой задачи, свободного от упомянутых недостатков. Но сначала мы рассмотрим, как устроена флэш память, чтобы понять суть проблем, с которыми мы имеем дело.

Как работает флэш память


В основе флэш памяти лежит особая модификация транзистора с изолированным затвором (МОП-транзистора). Классический МОП-транзистор формируется на кремниевой пластине, покрытой слоем окисла, который играет роль изолятора. Поверх окисла напыляется электрод, называемый затвором. Подачей напряжения на этот электрод, можно управлять током, текущим между двумя электродами на кремниевой пластине — стоком и истоком. Происходит это потому, что положительный заряд затвора притягивает электроны и под затвором образуется проводящий канал из электронов. Если убрать напряжение с затвора, проводящий канал пропадает.



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



Электроны попадают на плавающий затвор в процессе записи данных, туннелируя через изолятор. Этот процесс наглядно показан в фильме Чародеи — главное хорошо разогнаться, видеть цель и не замечать препятствий. Разгоняются электроны при пропускании тока в канале.



Со стиранием сложнее — ведь нам нужно не поселить электроны на затворе, а убрать их оттуда, значит разогнать их никак не получится. Поэтому мы просто формируем положительный потенциал в канале и ждем, когда электроны притянутся и протуннелируют в канал. Вот почему стирание занимает на несколько порядков большее время, чем запись. Для нашего STM32 это время от долей секунд до секунд. Более сложные устройства вроде SSD дисков поддерживают некоторый запас стертых транзисторов, но если они заканчиваются, время выполнения операций записи радикально увеличивается.

Чтобы сэкономить время, стирают память большими блоками — секторами. В случае STM32 минимальный размер — 16 килобайт — имеют 4 сектора, расположенные по младшим адресам. Записывать наш STM32 умеет по одному байту, по два, или четыре. Стертый транзистор читается как логическая единица. Соответственно, при записи мы поселяем электроны на затворы тех транзисторов, которые соответствуют логическим нулям в записываемых данных. Отсюда интересное наблюдение — мы можем выставлять в ноль биты в одном и том же байте один за другим, а не все сразу. Обратная операция — выставить нулевой бит в единицу — невозможна без стирания. При записи единичного бита содержимое памяти не изменяется.

Проблема стабильности чтений


Что же произойдет, если мы выключим питание в момент записи данных? Понятно, что часть данных окажется незаписанной. А что призойдет с тем байтом или словом, которое мы записывали в момент выключения питания? При записи на плавающий затвор может попасть разное количество электронов. Много электронов читается как 0, мало — как 1, значит есть и некоторое пограничное количество электронов. Если до выключения питания на затвор попадет количество электронов, близкое к пограничному, то при чтении мы можем получать как 0, так и 1 в зависимости от совершенно случайных факторов. Со временем заряд будет стекать с затвора, так что вероятность прочитать 1 будет расти. Эта крайне неприятная особенность делает ненадежной даже схему с использованием контрольной суммы. Если питание отключилось в момент записи последнего слова нашего пакета с данными, которые мы можем сопроводить любым количеством проверочной информации, то мы можем сегодня прочитать наши данные, а завтра нет, или наоборот. Более того, нас ждут неприятности и при записи в область, которую мы считаем стертой, потому что она сегодня читается как все единицы — ведь завтра там могут проступить нули и испортить наши данные.

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

Реализация с гарантией целостности данных


Теперь мы готовы рассмотреть схему хранения данных, которая гарантирует целостность данных в смысле, обсуждавшемся выше. Поскольку STM не скупится на размер флэша, было решено упростить конструкцию, отказавшись от экономии, и использовать модель, где все данные объединены в единственную структуру фиксированного размера. При обновлении данных мы записываем всю структуру целиком. Разные версии данных записываются последовательно в предварительно стертую область флэш памяти. Актуальными считаются данные, записанные последними.

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



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

К чему такие сложности, может спросить любознательный читатель. Ведь мы можем просто переписать контрольную сумму, и она гарантированно не будет меняться со временем. Да, действительно, но нам придется делать это каждый раз. Цель состоит в том, чтобы
  • Не делать лишних записей
  • Иметь возможность понять, почему не совпадает контрольная сумма. Если при этом проверочный байт правильный, то несовпадение контрольной суммы однозначно указывает на то, что содержимое сектора повреждено или не до конца стерто.

Второй статусный бит — флаг продолжения — позволяет определить, можно ли считать стертой память, из которой читаются все единицы. Перед тем, как записывать следующий блок данных, мы устанавливаем этот флаг (сбрасываем бит в 0). Если при чтении мы видим в этом бите 1, значит мы никогда не пытались писать в следующий байт. Ну а как быть, если сектор изначально пуст — можем мы считать его стертым или нет? Конечно же нет! Но с этим рецедивом паранойи справиться легче всего — мы просто сотрем его еще раз перед тем, как что то записать.

Итак, мы можем гарантировать, что будучи однажды прочитанными, данные будут читаться и дальше. Однако, с прочими свойствами надежного хранилища данных все не так радужно. Очевидна проблема с необходимостью периодически стирать сектор, когда там заканчивается место. Если при этом выключится питание, мы не только не запишем новые данные, но и потеряем старые. Несколько менее очевидна проблема с неправильно записанными данными (в результате отключения питания во время записи). Мы не можем гарантировать, что со временем там не проступят отсутствующие биты и мы не начнем читать эти данные как правильные. Может показаться, что дополнительные статусные биты, маркирующие запись как неправильную, могут спасти положение, однако это не так. Ведь питание может пропасть и при записи этих дополнительных бит, и в итоге проблем станет только больше. Схема, описанная выше, успешно использует корректирующие записи только потому, что они записывают ровно те же данные, что и первоначальная запись, поэтому при любой последовательности отключений питания последняя успешная запись переводит флэш в стабильное состояние. Конечно, и в таком виде описанное хранилище может использоваться в приложениях не предъявляющих повышенные требования к надежности хранения. Но оказывается, что на базе двух хранилищ описанного типа можно создать более надежный вариант, лишенный описанных недостатков. Схема такого хранилища показана на следующем рисунке.



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

Тестовый проект


Лежит тут. Проект создан с помощью STM32CubeMX под компилятор IAR EWARM для платы STM32-H405. Использование STM32CubeMX для компиляции проекта оставило только положительные эмоции. Особенно радует дерево клоков — та часть, которая раньше была для меня областью магии, теперь упростилась до нескольких кликов мышью. Проект легко адаптировать под другие процессоры STM32 или компиляторы просто перегенерив его с помощью STM32CubeMX. Код хранилища данных легко адаптировать и под другие архитектуры, поскольку работа с флэшом вынесена в отдельный модуль с абстрактным интерфейсом. В составе пректа есть автоматический тест хранилища данных, который использует сторожевой таймер для сброса процессора в случайный момент времени. Кроме того, в проекте есть тестовая реализация USB CDC протокола, которая просто отсылает назад все принятые строки. Я добавил ее, поскольку меня интересовали 2 вопроса. Во-первых, что происходит с известными мне проблемами в реализации USB стэка. Оказалось, что ничего — старые проблемы не исправляются, новых не появляется. Видимо такова политика кампании — кто знает про ZLP — сделает сам, кто не знает — заплатит за поддержку. Во-вторых, было интересно, как стирание флэша влияет на работу USB, ведь при этом процессор может останавливать выборку команд из флэша. Оказалось, что не влияет.

Обновление — вариант для MSP430


Тестовый проект для MSP430 добавлен в репозиторий. Он отличается только модулем, реализующим операции с флэш памятью, остальной код общий. Испытан на LaunchPad-е. Тест взводил таймер, а выход таймера был подключен напрямую к земле. Питание на плату было подано через резистор 510 ом, так что при срабатывании таймера питание радикально проседало, и микроконтроллер ресетился, прерывая все текущие операции с флэшом. Тест успешно выполнил миллион записей на флэш, на что ушло три с половиной часа. За это время произошло около 20000 стираний сектора размером 512 байт, питание выключалось примерно 5000 раз. Результаты проверялись путем сравнения с записями в 2 отдельных контрольных сектора. Ошибок за время тестирования выявлено не было.

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


  1. FishDude
    09.07.2015 23:36

    А почему бы не сделать так: [запись данных] [пауза] [запись контрольного байта «0»] — и всё?
    (Недоумение вызывает необходимость записи контрольной суммы).


    1. oleg_v Автор
      10.07.2015 10:21
      +2

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


    1. Alexeyslav
      10.07.2015 10:25
      +1

      Контрольная сумма нужна. Рано или поздно у флеша исчерпается ресурс, и тогда появятся битые ячейки, которые и выявит контрольная сумма. У встроенного в контроллер флеша ресурс очень маленький, поскольку не предполагается интенсивная работа с ним. Записывать настройки раз в сутки — ресурса хватит надолго, а если делать это каждую секунду то проблемы появятся довольно быстро.


  1. GarryC
    10.07.2015 10:37

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