Внутри Ардуины есть 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 циклами записи.
Услужливый Aliexpress привёз мне вот такой модуль. Память в виде модуля дорогая весьма, кстати. Сам же чип стоит 16р/шт. В микросхеме 512 байт, то есть, вроде и немного, но с учётом бесконечному числу записей вполне достаточно.
Погуглив на тему готового чего-то для этого чипа я не нашёл ничего. Отличная кошка, решил я, буду на ней тренироваться! Открыл доку по Wire, даташит по FM24, чей-то проект EEPROM/I2C с похожим интерфейсом и набросал класс для FRAM.
Проект на гитхабе: 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);
}
Так как на модуле, кроме чипа и разъёмов, практически ничего нет, уже хочу купить несколько микросхем. Нравятся.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (43)
kail
11.01.2017 23:14+2Обмануть судьбу можно, как мне кажется, просто зануляя предыдущее значение. Суммарное снижение ресурса получится в 2 раза или даже чуть меньше.
А моточасы, надо полагать, величина всегда возрастающая, поэтому можно просто писать в следующую ячейку и потом искать наибольшее значение.
Впрочем, попробовать новые штуки — тоже хорошо :]nwwind
12.01.2017 13:03+1МЧ и одометры растут конечно, но им тоже нужен сброс. Есть одометр поездки, сбрасывается перед стартом. Есть МЧ после ТО, тоже сбрасываются после того самого ТО.
imwode
12.01.2017 00:37Я не понял — надо два занчения хранить при выключении питания? И ради этого сыр бор?
В чем проблема хранить показания одометра в еепроме (не обязательно внутреннем — их на любой плате россыпь обычно, спаял да прикрутил на соплях) по смещению 0x00, отвести на это целых четыре байта, например, а часы хранить в структуре по смещению 0x04???nwwind
12.01.2017 13:05Там больше. Три одометра, два счётчика МЧ.
Может ещё что-то всплывёт.
Микросхема стоит 16р/ш при покупке от 10 штук. Я её потом напаяю прямо на плату, когда буду разводить. Главное — код мизерный, память не ест, гемора нет.latonita
12.01.2017 15:34может хранить в микросхеме часов с батарейкой, и изредка сбрасывать на епром?
вот например, RTC + i2c + 64 bytes RAM backed by battery
http://www.nxp.com/documents/data_sheet/PCF85363A.pdf
батарейки живут долго, там успеешь и имплементировать отслеживание разряда батареии и приглашение её поменять.nwwind
12.01.2017 15:38А что будет с памятью в pcf85363a, если сядет батарейка или надо просто её поменять?
Батарейка от вибрации (мотоцикл) может и отскочить на ходу. Лишиться часов не так страшно, как одометров и моточасов.
Бакап в епроме будет ээээ сильно устаревшим же.
Опять гемор, зачем, если есть fram?
Sinatr
12.01.2017 13:05У EEPROM время жизни скажем ~100 тысяч циклов, если обновлять одну и ту же ячейку раз в секунду, то хватит примерно на сутки. Идея заключается в том, чтобы использовать новую позицию для записи каждый раз, 1К памяти хватит на 3 года.
Простейший алгоритм реализации — это ввести некий счетчик, который увеличивается каждый раз при сохранении структуры (содержащей данные и сообственно счетчик). Если счетчик дошел до 100.000 — инкрементируется позиция структуры (которую можно хранить по фиксированному адресу, т.к. запись туда происходит редко).nwwind
12.01.2017 13:06Как раз примерно такое я описал только что в статье по просьбе комментирующего.
latonita
12.01.2017 15:23+1У EEPROM время жизни скажем ~100 тысяч циклов
не надо забывать, что это применимо к одному биту. и лет 50-60 назад господин Грей придумал, как уменьшить износ физических переключателей для бинарных счетчиков.
если мы просто инкременитируем байт — то каждый раз младший бит мигает. и в конце концов умирает, а старшие разряды живы.
в итоге, добавив минимальную логику на преобразование gray-binary мы можем растянуть срок жизни еще в 8 раз )
Википедия — Код Греяnwwind
12.01.2017 15:30+1Это понятно.
Но использование fram добавляет 16р к цене устройства и позволяет не париться с проблемами еепрома навсегда.
grossws
12.01.2017 16:17Идея заключается в том, чтобы использовать новую позицию для записи каждый раз, 1К памяти хватит на 3 года.
Всё хорошо, если стирание/запись не постраничная, как это часто бывает. Но на таких размерах это обычно не так.
nwwind
12.01.2017 17:31Даже три года — это очень очень мало.
Вы видели автомобиль с электронным одометром, который дохнет через три года? Я нет.
ra3vld
12.01.2017 13:16+1Причины сыра-бора описаны во втором абзаце — ограниченный ресурс еепрома. Если в мегу писать раз в 10 секунд, то гарантированно проработает она только ~11 дней(100 000 циклов/ячейку / 3600 / 24 * 10) и даже установка внешней еепромки увеличит ресурс всего в 10 раз (для первой загуглившейся AT24C16B). Так что проблему это не решит.
Хотя всё же проще кольцевой буфер или, ИМХО, правильнее детектировать отключение питания и тогда всё сохранять, например, как тут http://radiokot.ru/forum/viewtopic.php?p=1120034#p1120034nwwind
12.01.2017 13:20Писать по пропаданию питания — хорошая идея, но… ненадёжно.
Я это планирую сделать, но с FRAM как-то заметно проще получается.
Keroro
12.01.2017 07:32Сначала я хотел обмануть судьбу записывая структурку данных в разные места 1К памяти чипа по кругу. Упёрся в то, что указатель надо где-то хранить тоже, а данные достаточно случайные, чтобы использовать какой-то маркер для последовательного поиска.
Можно было, наверное, два кольцевых буфера завести, один с указателем (увеличивается равномерно, при поиске искать самое большое число, которое не FF), а второй собственно с данными.
AlexPodvorny
12.01.2017 13:07+2Из личного опыта FRAM память довольно чувствительна к помехам по напряжению питания. Если использовать для установки на мотоцикл, это необходимо учесть.
nwwind
12.01.2017 13:11Я на входе ставлю стабилизатор (китайский импульсный понижатель постоянного напряжения с ШИМ) и толстый конденсатор для дополнительного сглаживания и питания в случае чего. На вход, где 12В, через делитель подключаю ногу ардуины. Если на ней пропало напряжение, значит у нас есть несколько миллисекунд на запись состояния и самоубийство. Экранчик питается отдельно. Думаю как лучше сделать.
Andrew1997
12.01.2017 13:46+1А еще лучше к пакету записываемых данных добавить контрольную сумму CRC16 (чтобы быстрее считать можно использовать табличный метод). При скачке питания или внезапном отключении питания может записаться только часть данных, а не весь пакет.
nwwind
12.01.2017 13:47Хорошая мысль, кстати.
Добавлю в проект, спасибо.
Тогда надо писать в циклический буфер как минимум на две структурки таких. Для бакапа.
nwwind
12.01.2017 13:25Обновил статью. Добавил ссылки на алиекспресс и примеры решения с EEPROM или Flash.
r00tGER
12.01.2017 14:57Зачем постоянно в EEPROM писать — при включении прочитали последнее значение, при выключении записали.
Нужно только реализовать «реакцию» на отключение питания, и схему на поддержание питания Ардуины, что бы успела скинуть последнее значение в EEPROM.nwwind
12.01.2017 15:24Если бы было плевать на потерю этих данных, я бы так и сделал. Вероятность потери же весьма велика. Права на ошибку нет, что-то сглючило с еепромом и прощай. Это всё-таки не микроволновка, а мотоцикл.
r00tGER
12.01.2017 15:43И Ваш способ не избавляет от вероятности потери данных:
...«что-то сглючило с FRAM и прощай»...
Помехи обмена по I2C, блокирующая реализация Wire библиотеки…nwwind
12.01.2017 17:30i2c с подтверждением работает. Я в библиотеке верю в качество передачи, но надо убеждаться. Это в ближайшее время пофиксю.
Всё глючит, потом бакап в eeprom — это хорошо, но только как бакап.
sir66
13.01.2017 03:35Я в своём проекте ambox.me делаю именно так как предложенной выше. Плюс еже простейшая схема из электролита и диода на входечтобы питание не пропадало неожиданно. АЦП и источник опорного в Ардуине, благо есть. Пр правильной организации кольцевого буфера можно не только повысить ресурс eeprom на 2.5 порядка, но и гарантировать откат на предыдущее значение, если что.
nwwind
13.01.2017 14:27Да, я всё это уже и сам придумал, когда статью написал. Я просто не стал бороться с еепромом, а решил проблему тупо железкой, с которой биться не нужно. :)
Вот, рекомендую.
Viral
12.01.2017 15:24Любопытная память, спасибо. Хотя раз уж тут речь о микроконтроллере, то это десятки миллиампер потребляемого тока, а значит, если добавить хороший емкий конденсатор в цепь питания, то при выключении внешнего питания у вас будет несколько секунд на то, чтобы разобрать все данные в SRAM и записать в EEPROM. При таком подходе срок её жизни существенно увеличится.
nwwind
12.01.2017 15:27Нескольких секунд не будет, будут миллисекунды. Писать надо по прерыванию так как до чтения ноги питания может не дойти. Прерываний внешних всего два, оба заняты.
Способ с FRAM надёжнее заметно. Конечно же, намного проще.
Одно другое не исключает, даже наоборот. :) Я собираюсь писать и туда и туда. Просто fram на i2c и шина тоже может сглючить даже на 100кГц.latonita
12.01.2017 17:35по поводу «Прерываний внешних всего два, оба заняты.» не совсем верно ))
прерываний больше, просто их на одну строчку кода сложнее обрабатывать))))
не ограничивайте себя 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, чтобы толстый даташит не перекапыватьnwwind
12.01.2017 17:39Я в курсе про пинченч и векторы.
Я к тому, что те два без всякого вектора, хардкорного программирования, группирования, разбора в функции, с какой ноги пришло оно.
Если нет фрама, то да, надо упарываться со схемотехникой и ломать голову над живучестью еепрома. А если есть, то не надо :)
cavin
Странно, что готовые модули продаются по 10 баксов штука. Сам FM24CL16 стоит $1 в розницу и $0.5 оптом на али. Он имеет настолько простую схему подключения, что платить в 10-20 раз за модуль это слишком.
P.S. А почему бы в данной ситуации вместо оперативки microsd? Туда же можно писать уже что угодно — хоть GPS хоть коды ODB2 хоть данные гироскопа-акселерометра.
irbisadm
Там I2C, перемычки, ручное изготовление…
cavin
берем breakout ценой примерно ноль, перемыкиваем прямо на внешний контактах, останется только напряжение подтянуть и все.
nwwind
Это что за зверь?
nwwind
Да, меня тоже поразила разница в цене микрухи и сборки.
За чип хотят 16р/штука.
SD хорошо, но там слишком много контактов. Это же мотоцикл, более того, эндуро. Там такие вибрации, что контакты противопоказаны.
Ну и жрёт память SD в ардуине и флеш жрёт и быстродействие совсем не то. Здесь же i2c одна на всех, две ножки всего. Никаких файловых систем, просто пиу и готово.
latonita
насколько я понимаю, только промышленные micro sd карты (с соответствующей ценой) сравнимы по циклам записи с eeprom. у дешевых карт — циклов записи меньше.
тут, конечно, может помочь больший размер карты — можно устраивать wear leveling с циклическими буферами.
но в этом случае возникает вопрос про хранение указателя ) или поиска последней записи по сигнатуре )
в принципе, указатель в данном случае:
а. можно хранить в памяти, а записывать только по прерыванию на понижении питания
б. использовать код грея для указателя — чтобы уменьшить износ младших бит в ячейке
недавно был пост про малинку и readonly fs — там обсуждали износостойкость и были ссылки на информацию по картам
https://geektimes.ru/post/283802/
nwwind
Про беду с хранением указателя я писал выше. Можно писать блоками на sizeof некую структуру и в ней держать байт маркера. О, сейчас напишу в самой статейке.
nwwind
Написал в тексте статьи два способа организации циклической записи без хранения указателя.