Надійне зберігання та оновлення даних у флеш пам'яті мікроконтролерів STM32 і MSP430

Часто виникає завдання зберегти змінювані дані, наприклад конфігурацію, у флеш пам'яті мікроконтролера. Рішення здається простим, проте забезпечити надійність оновлення даних за умови, що харчування може відключитися в будь-який момент, виявляється досить нетривіально, і навіть використання контрольних сум не вирішує проблему повністю. З цієї статті ви дізнаєтеся

  • як влаштована флеш пам'ять
  • до яких проблем призводить вимикання живлення в момент запису або стирання
  • як ці проблеми вирішуються

Для бажаючих застосувати на практиці - працюючий код під 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 окремих контрольних сектора. Помилок за час тестування виявлено не було.