11 января в 19:51

FRAM через I2C для Arduino как замена EEPROM

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

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

Сначала я хотел обмануть судьбу записывая структурку данных в разные места 1К памяти чипа по кругу. Упёрся в то, что указатель надо где-то хранить тоже, а данные достаточно случайные, чтобы использовать какой-то маркер для последовательного поиска.

Необходимость хранения указателя можно обмануть разными способами. Например так:

struct MarkedSavedData {
  byte marker; // показывает, занято место или нет.
  struct SavedData {
    // Собственно данные для сохранения
  }
} data;


Структуркой MarkedSavedData заполняется eerpom или флеш или что-то по кругу. Чтобы не писать указатель, в свободных записях делаем data.marker=0x00, а в занятой текущей data.marker=0xff, например. В процессе работы, конечно же, запись идёт по указателям, а при старте контроллера просто поиском по всей памяти ищется структура с data.marker==0xff — это последние правильные данные. Плохо, что каждый раз две записи получаются тк надо обнулить data.marker освобождаемой записи.

Есть вариант с последовательным счётчиком.

struct MarkedSavedData {
  unsugned int counter; // последовательный номер записи.
  struct SavedData {
    // Собственно данные для сохранения
  }
} data;


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

Всё это хорошо, но это припарки.

Коллеги из НТЦ Метротек подсказали поискать FRAM. Это ферритовая память с бешеным быстродействием и 1014 циклами записи.

image

Услужливый Aliexpress привёз мне вот такой модуль. Память в виде модуля дорогая весьма, кстати. Сам же чип стоит 16р/шт. В микросхеме 512 байт, то есть, вроде и немного, но с учётом бесконечному числу записей вполне достаточно.

Погуглив на тему готового чего-то для этого чипа я не нашёл ничего. Отличная кошка, решил я, буду на ней тренироваться! Открыл доку по Wire, даташит по FM24, чей-то проект EEPROM/I2C с похожим интерфейсом и набросал класс для FRAM.

image

Проект на гитхабе: github.com/nw-wind/FM24I2C

Пример прилагается вот такой.

#include "FM24I2C.h"

// Объект для платы. Адрес в i2c.
FM24I2C fm(0x57);

void setup() {
  Wire.begin();
  Serial.begin(9600);
  char str1[]="12345678901234567890";
  char str2[]="qwertyuiopasdfghjklzxcvbnm";
  int a1=0x00; // Первый в FRAM
  int a2=0x40; // Второй адрес в FRAM
  fm.pack(a1,str1,strlen(str1)+1); // Пишем в память
  delay(5);
  fm.pack(a2,str2,strlen(str2)+1); // Пишем вторую строку
  delay(5);
  char buf[80];
  fm.unpack(a2,buf,strlen(str2)+1); // Читаем вторую
  Serial.println(str2);
  fm.unpack(a1,buf,strlen(str1)+1); // Читаем первую
  Serial.println(str1);
}

Протокол i2c для FRAM сильно проще, чем для EEPROM. Память работает быстрее передачи данных по шине и можно лить хоть все 2К ардуининых мозгов за один раз. Польза от моего кода хоть в том, что нет лишнего разбиения на блоки по 32 байта или вообще побайтной передачи.

class FM24I2C {
  private:
    int id;
  public:
    FM24I2C(int id_addr);
    ~FM24I2C();
    void pack(int addr, void* data, int len);     // Упаковать данные в FRAM
    int unpack(int addr, void* data, int len);    // Распаковать из FRAM. Возвращает количество переданных байтов.
    // Это уже специально для меня, пишет беззнаковые длинные целые.
    void inline writeUnsignedLong(int addr, unsigned long data) {
      pack(addr, (void*)&data, sizeof(unsigned long));
    } 
    // И читает.
    unsigned long inline readUnsignedLong(int addr) {
      unsigned long data;
      return unpack(addr, (void*)&data, sizeof(unsigned long)) == sizeof(unsigned long) ? data : 0UL;
    }
    // Можно для других типов сделать чтение/запись, но мне было не нужно, а флеш занимает. 
   // Каждый же может унаследовать класс и дописать сам.
};

Кода же немножко совсем.

void FM24I2C::pack(int addr, void* data, int len) {
  Wire.beginTransmission(id);
  Wire.write((byte*)&addr,2);
  Wire.write((byte*)data,len); // Наверное, стоит всё же unsigned int использовать :)
  Wire.endTransmission(true);
}

int FM24I2C::unpack(int addr, void* data, int len) {
  int rc;
  byte *p;
  Wire.beginTransmission(id);
  Wire.write((byte*)&addr,2);
  Wire.endTransmission(false);
  Wire.requestFrom(id,len);
  // Здесь можно поспорить про замену rc на p-data :)
  for (rc=0, p=(byte*)data; Wire.available() && rc < len; rc++, p++) {
    *p=Wire.read();
  }
  return(rc);
}

Так как на модуле, кроме чипа и разъёмов, практически ничего нет, уже хочу купить несколько микросхем. Нравятся.
Есть в этом смысл?

Проголосовал 81 человек. Воздержалось 42 человека.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Сергей Келер @nwwind
карма
18,0
рейтинг 0,0
Сисадмин
Похожие публикации
Самое читаемое Разработка

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

  • +1
    Странно, что готовые модули продаются по 10 баксов штука. Сам FM24CL16 стоит $1 в розницу и $0.5 оптом на али. Он имеет настолько простую схему подключения, что платить в 10-20 раз за модуль это слишком.

    P.S. А почему бы в данной ситуации вместо оперативки microsd? Туда же можно писать уже что угодно — хоть GPS хоть коды ODB2 хоть данные гироскопа-акселерометра.
    • 0
      Там I2C, перемычки, ручное изготовление…
      • 0
        берем breakout ценой примерно ноль, перемыкиваем прямо на внешний контактах, останется только напряжение подтянуть и все.
        • 0
          Это что за зверь?
    • +1
      Да, меня тоже поразила разница в цене микрухи и сборки.
      За чип хотят 16р/штука.

      SD хорошо, но там слишком много контактов. Это же мотоцикл, более того, эндуро. Там такие вибрации, что контакты противопоказаны.

      Ну и жрёт память SD в ардуине и флеш жрёт и быстродействие совсем не то. Здесь же i2c одна на всех, две ножки всего. Никаких файловых систем, просто пиу и готово.
    • 0
      P.S. А почему бы в данной ситуации вместо оперативки microsd?

      насколько я понимаю, только промышленные micro sd карты (с соответствующей ценой) сравнимы по циклам записи с eeprom. у дешевых карт — циклов записи меньше.
      тут, конечно, может помочь больший размер карты — можно устраивать wear leveling с циклическими буферами.
      но в этом случае возникает вопрос про хранение указателя ) или поиска последней записи по сигнатуре )
      в принципе, указатель в данном случае:
      а. можно хранить в памяти, а записывать только по прерыванию на понижении питания
      б. использовать код грея для указателя — чтобы уменьшить износ младших бит в ячейке

      недавно был пост про малинку и readonly fs — там обсуждали износостойкость и были ссылки на информацию по картам
      https://geektimes.ru/post/283802/
      • 0
        Про беду с хранением указателя я писал выше. Можно писать блоками на sizeof некую структуру и в ней держать байт маркера. О, сейчас напишу в самой статейке.
      • 0
        Написал в тексте статьи два способа организации циклической записи без хранения указателя.
  • +2
    Обмануть судьбу можно, как мне кажется, просто зануляя предыдущее значение. Суммарное снижение ресурса получится в 2 раза или даже чуть меньше.
    А моточасы, надо полагать, величина всегда возрастающая, поэтому можно просто писать в следующую ячейку и потом искать наибольшее значение.

    Впрочем, попробовать новые штуки — тоже хорошо :]
    • +1
      МЧ и одометры растут конечно, но им тоже нужен сброс. Есть одометр поездки, сбрасывается перед стартом. Есть МЧ после ТО, тоже сбрасываются после того самого ТО.
      • 0
        Хорошо, при сбросе (записи нуля) просто занулить все ячейки.
        • 0
          Я думал занулить в sram, а запишет оно уже по расписанию.
  • 0
    Я не понял — надо два занчения хранить при выключении питания? И ради этого сыр бор?
    В чем проблема хранить показания одометра в еепроме (не обязательно внутреннем — их на любой плате россыпь обычно, спаял да прикрутил на соплях) по смещению 0x00, отвести на это целых четыре байта, например, а часы хранить в структуре по смещению 0x04???
    • 0
      Там больше. Три одометра, два счётчика МЧ.
      Может ещё что-то всплывёт.
      Микросхема стоит 16р/ш при покупке от 10 штук. Я её потом напаяю прямо на плату, когда буду разводить. Главное — код мизерный, память не ест, гемора нет.
      • 0
        может хранить в микросхеме часов с батарейкой, и изредка сбрасывать на епром?
        вот например, RTC + i2c + 64 bytes RAM backed by battery
        http://www.nxp.com/documents/data_sheet/PCF85363A.pdf

        батарейки живут долго, там успеешь и имплементировать отслеживание разряда батареии и приглашение её поменять.
        • 0
          А что будет с памятью в pcf85363a, если сядет батарейка или надо просто её поменять?
          Батарейка от вибрации (мотоцикл) может и отскочить на ходу. Лишиться часов не так страшно, как одометров и моточасов.
          Бакап в епроме будет ээээ сильно устаревшим же.

          Опять гемор, зачем, если есть fram?
    • 0
      У EEPROM время жизни скажем ~100 тысяч циклов, если обновлять одну и ту же ячейку раз в секунду, то хватит примерно на сутки. Идея заключается в том, чтобы использовать новую позицию для записи каждый раз, 1К памяти хватит на 3 года.

      Простейший алгоритм реализации — это ввести некий счетчик, который увеличивается каждый раз при сохранении структуры (содержащей данные и сообственно счетчик). Если счетчик дошел до 100.000 — инкрементируется позиция структуры (которую можно хранить по фиксированному адресу, т.к. запись туда происходит редко).
      • 0
        Как раз примерно такое я описал только что в статье по просьбе комментирующего.
      • +1
        У EEPROM время жизни скажем ~100 тысяч циклов

        не надо забывать, что это применимо к одному биту. и лет 50-60 назад господин Грей придумал, как уменьшить износ физических переключателей для бинарных счетчиков.

        если мы просто инкременитируем байт — то каждый раз младший бит мигает. и в конце концов умирает, а старшие разряды живы.
        в итоге, добавив минимальную логику на преобразование gray-binary мы можем растянуть срок жизни еще в 8 раз )

        Википедия — Код Грея
        • +1
          Это понятно.
          Но использование fram добавляет 16р к цене устройства и позволяет не париться с проблемами еепрома навсегда.
      • 0
        Идея заключается в том, чтобы использовать новую позицию для записи каждый раз, 1К памяти хватит на 3 года.

        Всё хорошо, если стирание/запись не постраничная, как это часто бывает. Но на таких размерах это обычно не так.

        • 0
          Даже три года — это очень очень мало.
          Вы видели автомобиль с электронным одометром, который дохнет через три года? Я нет.
    • +1
      Причины сыра-бора описаны во втором абзаце — ограниченный ресурс еепрома. Если в мегу писать раз в 10 секунд, то гарантированно проработает она только ~11 дней(100 000 циклов/ячейку / 3600 / 24 * 10) и даже установка внешней еепромки увеличит ресурс всего в 10 раз (для первой загуглившейся AT24C16B). Так что проблему это не решит.
      Хотя всё же проще кольцевой буфер или, ИМХО, правильнее детектировать отключение питания и тогда всё сохранять, например, как тут http://radiokot.ru/forum/viewtopic.php?p=1120034#p1120034
      • 0
        Писать по пропаданию питания — хорошая идея, но… ненадёжно.
        Я это планирую сделать, но с FRAM как-то заметно проще получается.
  • +1
    Уже было напрягся увидеть что-то такое, а оказывается вон оно как
    • 0
      Я про это думал. У нас есть в музее в конторе.
  • 0
    Сначала я хотел обмануть судьбу записывая структурку данных в разные места 1К памяти чипа по кругу. Упёрся в то, что указатель надо где-то хранить тоже, а данные достаточно случайные, чтобы использовать какой-то маркер для последовательного поиска.

    Можно было, наверное, два кольцевых буфера завести, один с указателем (увеличивается равномерно, при поиске искать самое большое число, которое не FF), а второй собственно с данными.
    • 0
      Есть средство лучше. Описал в статейке, посмотри обновление.
    • 0
      Я делаю примерно так. Два однобайтовых каунтера — глобальный в ячейке 0 и локальный в самой структуре (которую будем двигать по eeprom). Глобальный задает номер блока в который пишем наши данные (ну то есть смещение блока данных). После каждой записи увеличиваем локальный счетчик и перезаписываем его в месте с данными. Как только дошли до 256 увеличиваем глобальный счетчик и переходим к следующему блоку сбросив локальный счетчик в 0. Дошли до конца памяти — глобальный счетчик в 0 и дальше по циклу. Глобальный счетчик перезаписывается только 1 раз на 256 записей. Локальный каждый раз, но он после каждых 256 записей переезжает на новое место.
  • +2
    Из личного опыта FRAM память довольно чувствительна к помехам по напряжению питания. Если использовать для установки на мотоцикл, это необходимо учесть.
    • 0
      Я на входе ставлю стабилизатор (китайский импульсный понижатель постоянного напряжения с ШИМ) и толстый конденсатор для дополнительного сглаживания и питания в случае чего. На вход, где 12В, через делитель подключаю ногу ардуины. Если на ней пропало напряжение, значит у нас есть несколько миллисекунд на запись состояния и самоубийство. Экранчик питается отдельно. Думаю как лучше сделать.
      • +1
        А еще лучше к пакету записываемых данных добавить контрольную сумму CRC16 (чтобы быстрее считать можно использовать табличный метод). При скачке питания или внезапном отключении питания может записаться только часть данных, а не весь пакет.
        • 0
          Хорошая мысль, кстати.
          Добавлю в проект, спасибо.
          Тогда надо писать в циклический буфер как минимум на две структурки таких. Для бакапа.
  • 0
    Обновил статью. Добавил ссылки на алиекспресс и примеры решения с EEPROM или Flash.
  • 0
    Зачем постоянно в EEPROM писать — при включении прочитали последнее значение, при выключении записали.
    Нужно только реализовать «реакцию» на отключение питания, и схему на поддержание питания Ардуины, что бы успела скинуть последнее значение в EEPROM.
    • 0
      Если бы было плевать на потерю этих данных, я бы так и сделал. Вероятность потери же весьма велика. Права на ошибку нет, что-то сглючило с еепромом и прощай. Это всё-таки не микроволновка, а мотоцикл.
      • 0
        И Ваш способ не избавляет от вероятности потери данных:
        ...«что-то сглючило с FRAM и прощай»...

        Помехи обмена по I2C, блокирующая реализация Wire библиотеки…
        • 0
          i2c с подтверждением работает. Я в библиотеке верю в качество передачи, но надо убеждаться. Это в ближайшее время пофиксю.
          Всё глючит, потом бакап в eeprom — это хорошо, но только как бакап.
      • 0

        Я в своём проекте ambox.me делаю именно так как предложенной выше. Плюс еже простейшая схема из электролита и диода на входечтобы питание не пропадало неожиданно. АЦП и источник опорного в Ардуине, благо есть. Пр правильной организации кольцевого буфера можно не только повысить ресурс eeprom на 2.5 порядка, но и гарантировать откат на предыдущее значение, если что.

        • 0
          Да, я всё это уже и сам придумал, когда статью написал. Я просто не стал бороться с еепромом, а решил проблему тупо железкой, с которой биться не нужно. :)
          Вот, рекомендую.
  • 0
    Любопытная память, спасибо. Хотя раз уж тут речь о микроконтроллере, то это десятки миллиампер потребляемого тока, а значит, если добавить хороший емкий конденсатор в цепь питания, то при выключении внешнего питания у вас будет несколько секунд на то, чтобы разобрать все данные в SRAM и записать в EEPROM. При таком подходе срок её жизни существенно увеличится.
    • 0
      Нескольких секунд не будет, будут миллисекунды. Писать надо по прерыванию так как до чтения ноги питания может не дойти. Прерываний внешних всего два, оба заняты.
      Способ с FRAM надёжнее заметно. Конечно же, намного проще.
      Одно другое не исключает, даже наоборот. :) Я собираюсь писать и туда и туда. Просто fram на i2c и шина тоже может сглючить даже на 100кГц.
      • 0
        по поводу «Прерываний внешних всего два, оба заняты.» не совсем верно ))
        прерываний больше, просто их на одну строчку кода сложнее обрабатывать))))
        не ограничивайте себя int0 & int1
        ловите pin change на порт и просто проверяйте нужную ногу

        для атмеги328
        4 Pin Change Interrupt Request 0 (pins D8 to D13) (PCINT0_vect)
        5 Pin Change Interrupt Request 1 (pins A0 to A5) (PCINT1_vect)
        6 Pin Change Interrupt Request 2 (pins D0 to D7) (PCINT2_vect)

        тут есть прекрасный саммари http://gammon.com.au/interrupts, чтобы толстый даташит не перекапывать
        • 0
          Я в курсе про пинченч и векторы.
          Я к тому, что те два без всякого вектора, хардкорного программирования, группирования, разбора в функции, с какой ноги пришло оно.
          Если нет фрама, то да, надо упарываться со схемотехникой и ломать голову над живучестью еепрома. А если есть, то не надо :)
      • 0
        Это какой конденсатор поставите. На 33000 микрофарад Ардуина живет больше 5 секунд. Вполне достаточно для сохранения, можно даже и меньше конденсатор поставить.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.