ARDUINO EEPROM I2C 24Сxxx. Универсальный инструмент. (библиотека)

EEPROM I2C 24Сxxx. Универсальный инструмент. (библиотека)
Во время создания прошивки для одного прибора, столкнулся с тем фактом, что мне нужно создать импровизированную Базу Данных, количество записей в которую может достигать пару тысяч. Понятно, что для такого количества данных, встроенного ЕЕПРОМ в Atmega328, явно недостаточно. Остановил свой выбор на микросхеме внешнего EEPROM, с поддержкой интерфейса I2C, 24-й серии, а точнее на 24C256. Перебрав кучу разных странных библиотек, понял что задержки чтения\записи меня совершенно не устраивают. К примеру нужный мне массив записей создавался более 10 минут (минут, Карл!), а поиск в этом массиве мог занимать более 15 секунд, что было совершенно неприемлемо. Мне нужна была реакция прибора не более секунды.
Почитав даташит на эти микросхемы, я узнал, что большинство из них умею писать\читать в страничном режиме. В связи с этим у меня встал вопрос о том, что нужно создать свой инструмент, который сможет работать в таком режиме, потому как практически все библиотеки использовали побайтовый способ записи\чтения.
Взяв за основу известный инструмент EEPROMAnything.h написал свой. Прошу любить и жаловать.

В связи с ограничение буфера чтения и записи I2C в ардуино в 32 байта, запись и чтение страницами работает не в полной мере, но скорость все равно выросла в десятки и сотни раз. Так запись нужного мне массива данных, вместо 10 минут, стала занимать несколько десятков секунд, а поиск в такой БД ~ 0,7c., что меня вполне устраивает.

Инструмент достаточно универсален, и пригоден к использованию практически с любой микросхемой серии 24, использующей два байта адреса, как 24c32 так и вплоть до 24c512. Увы, но микросхемы 24с1024 и 24с2048, без специальных ухищрений не могут использоваться с этим инструментом, в связи с ограничениями библиотеки Wire из ARDUINO.

Настройка этого инструмента в вашем проекте очень проста. Вам нужно узнать размер страницы из даташита на вашу микросхему и вписать в дефайн, вписать в дефайн правильный адрес на шине (или оставить дефолтный), посмотреть сколько мс занимает физическая зарядка ячеек (тоже из даташита) и вписать свое значение, или оставить дефолтные 5мс, которых хватает как правило, и все.
Еще не плохо бы глянуть с какой скоростью ваша микросхема может работать с шиной, возможные значения для атмеги 100кГц, 400кГц, 800кГц и 1МГц. Само собой, стоит запускать шину с максимальной скоростью (если скорость действительно важна), но нужно учитывать ограничения других устройств на шине. К примеру, LCD1602 c I2C приблудой не работает с частотой шины выше 400кГц.

Для использования:
#include "EEPROM24xxx.h"


запись EEPROM_put(adress, value);
чтение EEPROM_get(adress, value); // НЕ value = EEPROM_get(adress, value), темплейт пишет на прямую в переменную в памяти.

Размер и тип данных может быть любой, как однобайтный, так и сложные структуры размером до 65,535 байт.
Лично я писал\читал структуры размером в 43 байта.

Буду рад, если этот инструмент будет кому-то полезен.

EEPROM24xxx.h:
#ifndef EEPROM24Cxxx_h
#define EEPROM24Cxxx_h

#include <Arduino.h>                          
#include <Wire.h>

#define DEVICE_ADRESS 0x50          // Адресс микросхемы по умолчанию, А0-А2 подключены к GND (или висят в воздухе, что не желательно).
#define PAGE_SIZE 64                // смотрите даташит на свою микросхему 24Схххх, размер страницы может быть 32 байта, 64 так и 128 байт.
#define WAIT_FOR_FISICAL_WRITE 5    // время миллисекунд для физической зарядки ячеек памяти, зависит от типа микросхемы, смотрите даташит




/*
  Физический буффер i2c шины в Ардуино равен 32 байта. Поэтому пишем блоками по 16 байт (потому как в буфер записи помещаются не только данные,
  но и адрес, все 32 байта буфера не доступны), наполняя страницу за страницей. И только если страница передана полностью, или кончились данные,
  - происходит физическая запись. Объект будет записан полностью за один вызов функции записи.
  Читаем блоками по 32 байта, набивая страницу за страницей, пока весь объект (переменная, структура и т.п.) не будет вычетана полностью за один
  вызов функции чтения.
  Таким образом удается воспользоваться страничным режимом, что резко увеличивает скорость чтения\записи в ЕЕПРОМ, поддерживающую страничный
  режим  записи\чтения данных. В случае чтения\записи больших объектов, скорость увеличивается в несколько десятков, а при записи и сотен, раз.
*/

template <class T> void EEPROM_get(uint16_t eeaddress, T& value) {
  uint16_t num_bytes = sizeof(value);
  byte* p = (byte*)(void*)&value;
  byte countChank = num_bytes / 32;
  byte restChank = num_bytes % 32;
  uint16_t addressChank = 0;
  if (countChank > 0) {
    for (byte i = 0; i < countChank; i++) {
      addressChank = eeaddress + 32 * i;
      Wire.beginTransmission(DEVICE_ADRESS);
      Wire.write((uint8_t)(addressChank >> 8));
      Wire.write((uint8_t)(addressChank & 0xFF));
      Wire.endTransmission();
      Wire.requestFrom(DEVICE_ADRESS, 32);
      while (Wire.available()) *p++ = Wire.read();
    }
  }

  if (restChank > 0) {
    if (countChank > 0)
      addressChank += 32;
    else
      addressChank = eeaddress;


    Wire.beginTransmission(DEVICE_ADRESS);
    Wire.write((unsigned long)((addressChank) >> 8));
    Wire.write((unsigned long)((addressChank) & 0xFF));
    Wire.endTransmission();
    Wire.requestFrom(DEVICE_ADRESS, restChank);
    while (Wire.available()) *p++ = Wire.read();
  }

}



template <class T> void  EEPROM_put(uint16_t eeaddress, const T& value) {
  const byte* p = (const byte*)(const void*)&value;

  byte counter = 0;
  uint16_t address;
  byte page_space;
  byte page = 0;
  byte num_writes;
  uint16_t data_len = 0;
  byte first_write_size;
  byte last_write_size;
  byte write_size;

  // Calculate length of data
  data_len = sizeof(value);

  // Calculate space available in first page
  page_space = int(((eeaddress / PAGE_SIZE) + 1) * PAGE_SIZE) - eeaddress;

  // Calculate first write size
  if (page_space > 16) {
    first_write_size = page_space - ((page_space / 16) * 16);
    if (first_write_size == 0) {
      first_write_size = 16;
    }
  }
  else {
    first_write_size = page_space;
  }

  // calculate size of last write
  if (data_len > first_write_size) {
    last_write_size = (data_len - first_write_size) % 16;
  }

  // Calculate how many writes we need
  if (data_len > first_write_size) {
    num_writes = ((data_len - first_write_size) / 16) + 2;
  }
  else {
    num_writes = 1;
  }


  address = eeaddress;
  for (page = 0; page < num_writes; page++)   {
    if (page == 0) {
      write_size = first_write_size;
    }
    else if (page == (num_writes - 1)) {
      write_size = last_write_size;
    }
    else {
      write_size = 16;
    }

    Wire.beginTransmission(DEVICE_ADRESS);
    Wire.write((uint8_t)((address) >> 8));
    Wire.write((uint8_t)((address) & 0xFF));
    counter = 0;
    do {
      Wire.write((byte) *p++);
      counter++;
    } while ((counter < write_size));
    Wire.endTransmission();
    address += write_size;                                // увеличиваем адрес для записи следующего буфера
    delay(WAIT_FOR_FISICAL_WRITE);                        // задержка нужна для того, чтобы ЕЕПРОМ успела физически зарядить ячейки памяти
  }
}
#endif
UPDATE 25\11\2023 Исправлен баг с нулевым адресом .
 

Вложения

Изменено:

Комментарии

kostyamat

★★★★★★✩
29 Окт 2019
1,098
632
Вот инструмент в виде файла, странно то, что к статье вроде прикреплял, а его не видно.
 

Вложения

Arhat109

★★★★✩✩✩
9 Июн 2019
473
203
Интересно, чем не устроил SD-карт-ридер? Там флешку можно втыкать практически в "гигабайты". И скорострельность доступа пожалуй даже повыше будет .. и заменить не проблема: вынул-вставил другую.

Какие-то ограничения на само устройство?

Ну и ещё, замечание: Ардуины не тянут скорострельность шины I2C на 1Мгц. Тестировал и Мегу и Нано .. верхний "пердел" - 880кГц.
Тестировалось как раз с девайсом LCD1602 .. ;)
 

Старик Похабыч

★★★★★★★
14 Авг 2019
4,262
1,300
Москва
я подключал такую память к регистратору температуры. Брал потому что а) i2c мне проще было подключить по 2 проводам. б) размер был ограничен с) использовал аттини85. Написал 2 функции чтения и записи (если чтение работало с какого то примера, то запись "кушала" часть данных), но изменил буфер для родной библиотеки с 16 на 32 байта (тинни!) . Ну и брал дип-8 с панелькой, что бы можно был менять если что.
 
  • Лойс +1
Реакции: Luaman и Arhat109

Arhat109

★★★★✩✩✩
9 Июн 2019
473
203
Ну .. сильно подозреваю что на Аттини85 запихать работу в SD-картой проблематично. Тут вроде 328-я .. к ней полно типовых библиотек, вполне вменяемого размера. И потом: что делать с вынутой микросхемкой? Её же надо куда-то втыкать, считывать по необходимости. А карта легко втыкается хоть в фотоцифромыльницу. ;)

Мне вообще не понятно применение последовательной SRAM, EEPROM в таких "базах данных" .. скорость доступа никакущая, в чем прелесть? ;)
.. после того как освоил применение 40-рублевой оперативы в своих мегах .. вообще перестал понимать "зачем такое".
 

kalobyte

★★★✩✩✩✩
1 Янв 2020
724
148
так речь идет о пзу, а не озу
пропадет питание и конец твоим данным
или батарейка сядет

я смотрел одну приблуду к древней атс панасоник что ли, сливала она туда логи
а потом надо было включать компутер и чтобы прога с этого логера сливала себе в базу
так там стояла как раз 24 серия как раз для того, что она много циклов выдерживает и не сбрасывается при пропадании питания

поставили новый панас за 5 кусков, так у того такая же херня - надо держать еще целый сервер с виртуалкой с хп, где крутится прога и собирает логи, иначе дольше пары месяцев они не хранятся

я помню только картриджи для денди, иногда там была батарейка и вроде можно было не то сохраняться, не то в озу как раз лежала сама игра и картридж работал, пока батарейка целая

я тоже для одного девайса использовал библиотеку базы данных на 24й серии, хотя она и карточки поддерживает, но мне как раз надо хранить долго и в девайсе, быстрый доступ тоже не нужен
там даже тестовый скетч есть для проверки скорости

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

Старик Похабыч

★★★★★★★
14 Авг 2019
4,262
1,300
Москва
Все от задачи конечно зависит. я хранил "сырые" значения температуры и настройки параметров лога в самом начале, периодичность. кол-во измерений. Мне 1-ой 24-ой хватило бы на 10 лет )
 

kostyamat

★★★★★★✩
29 Окт 2019
1,098
632
Зачем и почему? Потому что гладиолус. 😂
У каждого свои задачи.

Разница между моими темплейтами и другими стандартными библиотеками для атмега и 24с в том, что любая структура пишется/читается за один вызов, и в страничном режиме, что резко повышает скорость записи/считывания. К тому же пишет/читает любой тип и размер данных. Все.
У меня стояла задача хранить несколько сотен, и даже тисячь, пользователей с паролями и телефонами прямо на автономном девайсе, никаких логов девайс не ведёт и на компьютер не сбрасывает. Он находит пользователя у себя в БД и делает что-то, и это нужно делать быстро. Что и было решено. СД-карта не подходила по: цене, скорости (вот не быстрее она), габаритам и атмосферным условиям эксплуатации. Роль играло то, что - 24с были под рукой и уже, копеечная цена на микруху подходящего размера, минимум места и дополнительных компонентов на PCB.
 
Изменено:
  • Лойс +1
Реакции: stepko и Arhat109

Старик Похабыч

★★★★★★★
14 Авг 2019
4,262
1,300
Москва
Кстати, задавался вопросом как определить размер памяти EEPROM на уровне программы, но тогда так ничего и не вышло. Есть идеи?
 

kostyamat

★★★★★★✩
29 Окт 2019
1,098
632
@Старик Похабыч, ну даже не знаю. Думаю никак, на сколько я разобрался в даташите, микруха никакого идента не имеет. Подразумевается, что разработчик сам знает, что он в схему впаял. Вот 25-я серия мне помнится возвращает свой идентификатор. Но 25-я вроде как флеш, а 24-я просто ЕЕПРОМ.
 

Старик Похабыч

★★★★★★★
14 Авг 2019
4,262
1,300
Москва
Я пробовал считывать данные за границей памяти, ошибок не возникает.
Есть вариант пытаться записать и считать данные, но тут нужен какой то алгоритм, что бы лишний раз не тратить "жизни"
 

kostyamat

★★★★★★✩
29 Окт 2019
1,098
632
что бы лишний раз не тратить "жизни"
Там жизней по даташиту несколько миллионов, и минимум 40 лет хранения.
Видится такой алгоритм: есть типовые размеры микросхем - последовательно пишем за областью адресов предполагаемой микросхемы, если чтение вернуло правильные данные, переходим к следующему размеру микросхемы, если чтение вернуло 0 значит перепрыгнули, и последний сетап, вернувший верное значение отвечает правильному типу микрухи. В принципе, это можно сделать достаточно быстро при старте программы.
 

Старик Похабыч

★★★★★★★
14 Авг 2019
4,262
1,300
Москва
@kostyamat, Ну как так, но вот в голове пока не сложилось оптимально , да особо и не думал на самом деле
Я видел примерно так, запускаю программу, читаю с 1-ых адресов какую то информацию установочную, в которой должен быть размер памяти, с CRC оч. желательно, если ее нет или не верная, то запускаю тестирования. к примеру можно каждые 1024 байта, но это будет не оптимально. Надо загуглить типоразмеры стандартные.
Сейчас вот придумал себе еще задачу с ЕЕПРОМ, попробую что то соорудить приличное.
@Arhat109, Вот это правильно. Но вроде бы не перескакивает. Я во всяком случае такого не помню, а попытку писать и читать за гранью скорее всего делал.
 

Старик Похабыч

★★★★★★★
14 Авг 2019
4,262
1,300
Москва
Еще момент, не то что бы списываю, но был интересен один как ты разбиваешь на страницы, что берешь за основу.
Заметил, что адрес ты берешь как unsigned long, а это 32 бита, потом приводишь его все к тем же 16 битам, я у себя делал uint16_t - последнее время перехожу именно на такие описания.
Размер чтения и записи я предпочитаю указывать в запросе, а то вдруг у меня массив из 1000 байт, а считать хочу с 20-го по 30-ый только.

Если вот тут byte countChank = num_bytes / 32; 32 у тебя размер буфера, то лучше использовать BUFFER_LENGTH - такое определение есть в Wire.h и в TinyWireM, где он уже 16 байт изначально.
опять же если сделать чтото типа #define Wire TinyWireM думаю будет работать и с этой библиотекой без проблем. Но это только думаю.

А так отлично работает. Чуть потестировал.

ДОБАВЛЕНО!

Так, нашел еще одну фигню. Решил сравнить скорости твоей и моей читалки (отвлекся и свою писалку еще не доделал новую)
Взял массив 1000 байт и залепил чтение. Так вот у тебя чтение аж в 4 раза быстрее. Ничего не понял. Код хоть разный, но все одно быстрее и все.
Сделал 100 байт.. одинаково+-, 200 байт одинаоков +- 300 байт и тут прорыв у тебя по скорости, быстрее чем 200 байт выходит. в разы.
uint8_t num_bytes = sizeof(value); Вот! т.е. тут ты считаешь, что больше 255 байт записывать не бум.
 
Изменено:
  • Лойс +1
Реакции: Arhat109

kostyamat

★★★★★★✩
29 Окт 2019
1,098
632

@Старик Похабыч,
Честно, я это писал по наитию больше года назад. Комментарии в коде не оставлял, поэтому сейчас этот код как впервые вижу. 🤗 Поэтому с кондачка не отвечу. Нужно садится и анализировать. Выдрал из живого проекта и выложил, чтобы и самому не потерять. Главное работает. А по факту, чувствую там ещё многое можно оптимизировать как по памяти, так и по скорости. Помню только, что при чтении подразумеваю буфер 32 байта (такой выделен в Wire), а при записи использую 16 байтный, потому как в Wire в буфер для записи помещается как как данные так и адрес, а 16 ближайшее кратное 32/64/128 (размерам страниц данных). Ну и когда все страницы добрал и отправил, отправляю остаток. То-есть для тини 16 придется менять на 8. Unsigned long для адресов выделил чтобы помещалась адресация для 24с1024 (17 бит). Но похоже зря, потому что сама Wire принимает адрес в uint16_t, то есть чтобы работали эти микрухи, нужно править саму Wire. Так что unsigned long, если не 24с1024 можно смело заменить на uint16_t.
 
Изменено:

kostyamat

★★★★★★✩
29 Окт 2019
1,098
632
Вот! т.е. тут ты считаешь, что больше 255 байт записывать не бум.
Ну так и в описании написано что "пишет все от 1 до 256 байт" Я больше и не пробовал, но можно попробовать uint8_t на uint16_t заменить и посмотреть что получится. :) Аж самому интересно стало.
 

kostyamat

★★★★★★✩
29 Окт 2019
1,098
632
А ближайшее кратное 32/64/128 это 32 )
При записи - нет. В буфер помещается как данные так и адрес. Поэтому 16. В принципе можно было привести до 24, но это усложняет код, я не стал делать. По той же причине, в Аттини будет 8 (ну или 12 если изголятся).
 

Старик Похабыч

★★★★★★★
14 Авг 2019
4,262
1,300
Москва
Если поменять все работает. Проверил. А про ограничение не читал , там много буков, но мне такое было странно.
Запись сейчас буду свою ковырять. Как сделаю оба два тоже положу тут. пусть будут

а 24 особого смысла нет. что по 16 со станицей 32 пару раз писать, что по 24 и 8 тоже 2. если постранично использовать.

Я там писал выше, что пример готовый у меня кушал данные. вот он как раз куша на границе страниц памяти 24x, поэтому исправил на данные не больше 32 байт и забил пока. вот сейчас делаю по человечески
 

kostyamat

★★★★★★✩
29 Окт 2019
1,098
632
а 24 особого смысла нет. что по 16 со станицей 32 пару раз писать, что по 24 и 8 тоже 2. если постранично использовать.
Вот и я смысл не увидел, поэтому забил размер чанка 16 байт.
Основная задержка при записи, это сама физическая запись, которая занимает аж 5мс.
А темплейт записи, чанками по 16 забивает страницу по полной, а уж потом один лишь раз, между страницами, даёт время микрухе для сглотнуть данные. А времени на набивку страницу тратится относительно мало, поэтому и не парился.