Внешняя память для Arduino: SD-карта, EEPROM и FRAM

Зачем контроллеру внешняя память, чем различаются SD-карты, EEPROM и FRAM и как записывать и читать данные из микросхемы FM24 по I2C.
Зачем контроллеру внешняя память
Самая очевидная причина — нехватка собственной памяти. Но внешнее хранилище заодно бережёт встроенный EEPROM, ресурс которого конечен: для восьмибитных AVR называют до полумиллиона операций записи, при этом стабильность производитель перестаёт гарантировать уже после нескольких сотен тысяч.
Есть ещё два применения, особенно удобных со съёмными носителями. Первое — журнал: контроллер какое-то время накапливает данные, после чего их извлекают и обрабатывают. Второе — конфигурация. Настройки программы можно записать на носитель и просто подключить его к устройству; это быстрее перепрошивки и ручного ввода, а иногда позволяет обойтись без клавиатуры, энкодера или потенциометра, упростив и схему, и программу.
SD-карта

Главное достоинство карты — объём: можно работать с массивами в мегабайты и гигабайты. Она легко вставляется и вынимается, а записанное читается и редактируется на компьютере без программатора.
Слабые стороны тоже есть. Скорость записи невысока, порядка 4–5 килобайт в секунду, причём ограничивает её не карта, а библиотека и интерфейс Arduino. Ресурс перезаписи составляет от 10 до 50 тысяч циклов в зависимости от карты, что примерно на порядок меньше, чем у EEPROM. Само устройство относительно дорогое, хотя в пересчёте на байт цена ничтожна. Наконец, перед извлечением или отключением питания файл нужно закрыть, что неудобно при нестабильном питании.
Отсюда и область применения: журналы событий, конфигурационные данные, большие объёмы информации — изображения для дисплея, HTML-страницы, G-код для 3D-печати.
Внешний EEPROM

Микросхемы типа 24C32N по объёму, ресурсу и скорости практически не отличаются от встроенной памяти контроллера. Это восьмивыводной корпус с интерфейсом I2C и объёмом от 8 до 64 кбит в зависимости от модели. Работать с ним можно через готовую библиотеку или напрямую средствами Wire — второй способ быстрее и даёт более компактный код.
Такую память используют как расширение собственной или как её замену после износа. В роли съёмного носителя настроек она возможна, но требует аккуратности: частая перестановка изнашивает выводы микросхемы и панельку.
FRAM: быстрая и почти вечная

FRAM выпускается в восьмивыводном корпусе SOP, поэтому для макетной платы нужен адаптер под формат DIP. Продаются и готовые модули, но память в таком виде обходится в десять и более раз дороже и занимает заметно больше места.

Ключевое преимущество — количество циклов перезаписи, которое оценивается в 10 в 14-й степени. Если записывать данные раз в секунду, ресурса хватит более чем на три миллиона лет, поэтому такую память принято считать вечной. Кроме того, FRAM заметно быстрее EEPROM, особенно при записи: она занимает наносекунды. Протокол обмена проще, код короче, а интерфейс тот же — I2C, два вывода Arduino.
FRAM подходит для значений, которые меняются часто: показаний счётчиков, положения стрелок часов, сервоприводов робота, направления камеры. В 3D-принтерах на записи текущей позиции в G-коде основано возобновление печати после отключения питания. Обычно для этого используется EEPROM, и при частой записи его ресурса хватает ненадолго, от нескольких месяцев до года. С FRAM такого ограничения не было бы, и нагрузка на процессор была бы меньше, однако принтеров с такой памятью автору источника найти не удалось.
Подключение микросхемы FM24
В примере используется FM24CL16; в продаже встречаются чипы серии объёмом от 4 до 256 кбит. Подключаются они одинаково, различие в объёме учитывается в программе. Назначение выводов:
- VDD — +5 В;
- VSS — GND;
- C — линия SCL (пин A5 на Arduino UNO);
- D — линия SDA (пин A4 на Arduino UNO);
- HOLD — запрет записи при высоком уровне;
- A0, A1, A2 — выбор адреса микросхемы, что позволяет подключить до 8 чипов одновременно.
Адрес формируется по шаблону 1010(A2)(A1)(A0). Если все три вывода притянуты к нулю, получается 1010000, то есть 0x50. Подтянув к плюсу только A0, получают 1010001, или 0x51. Восемь комбинаций дают восемь адресов на одной шине.
Запись и чтение через Wire
Запись состоит из нескольких шагов. Сначала открывается сеанс передачи: Wire.beginTransmission(0x50). Затем отправляются два байта адреса ячейки; коротко это делается одной строкой Wire.write((byte*)&startAddress, 2), где startAddress — переменная типа unsigned int. После этого передаются сами данные — указатель на первый байт и длина в байтах. Сеанс закрывается вызовом Wire.endTransmission(true), и памяти дают короткую паузу delay(1).
При чтении точно так же открывается сеанс, передаётся двухбайтовый адрес, сеанс завершается, после чего вызывается Wire.requestFrom(0x50, len) и принятые байты по одному складываются в подготовленную переменную.
На этой основе в примере написаны две универсальные функции, FM24C_write и FM24C_read, принимающие адрес, указатель на данные и их размер. Они работают с любыми типами, включая массивы и структуры: в демонстрации по разным адресам записываются число типа int, значение unsigned long, структура из трёх полей и строка. Проверка простая: данные записывают, затем код записи отключают и убеждаются, что после перезапуска и снятия питания значения читаются без изменений. Следить нужно лишь за тем, чтобы не выйти за пределы объёма конкретного чипа.
Чип или модуль
С программной точки зрения микросхема и модуль на её основе одинаковы. На модуле уже установлены джамперы выбора адреса и подтягивающие резисторы, поэтому он удобен для тестов и отладки. В готовом изделии, особенно при большом количестве устройств, выгоднее разводить плату под отдельные чипы: они меньше и дешевле.