EEPROM 24C128 - 1024: универсальный инструмент

kostyamat
Offline
Зарегистрирован: 16.11.2017

Недавно сам встретился с тем фактом, что библиотек много и разных, но некоторые неудобны для использования. А некоторые  работаю очень медленно по причине того, что применяется побайтовая запись\считывание. При этом, по стандарту, после записи микросхеме нужно около 5мс для физической зарядки ячеек. Соответствено, при таком способе записи, структура размером\переменная, размером 43 байта, в количестве более 1000 экземпляров, в 24C512 писались более десяти минут, а на считываниет этой своеобразной БД и поиск между десятью полями в каждоой -  более 17 секунд.

Но есть решение, оказывается - современные 24C128 - 512 обладают возможность записи\считівания в страничном режиме. То-есть, -  за один запрос с ЕЕПРОМ можно считать, или записать в нее, 64 - 128 байт за один цикл. (конкретно размер страниці можно узнать из даташита на конкретный экземпляр)

Перелопатив кучу инфы, нашел несколько вариантов решений, в том числе так называемый "EEPROMAnything.h", описаный тут https://playground.arduino.cc/Code/EEPROMWriteAnything/

в результате родилось это:


#include <Arduino.h>  // for type definitions
#include <Wire.h>

#define deviceAddress 0x50                             // Адресс микросхемы по умолчанию, А0-А2 подключены к GND (или висят в воздухе, что не желательно). 
#define pageSize 128                                   // смотрите даташит на свою микросхему 24Схххх, размер может как 64, так и 128




/*
  Физический буффер i2c шины в Ардуино равен 32 байта, поэтому, воспользоваться прелестями чтения, 64 или 128 байт за раз, не получится.
  К тому же, увеличение буффера до 128 байт было бы слишком расточительно для ОЗУ ардуино.
*/

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

}



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

  byte i = 0, counter = 0;
  uint32_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 / pageSize) + 1) * pageSize) - 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;
  }

  i = 0;
  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(deviceAddress);
    Wire.write((uint32_t)((address) >> 8));
    Wire.write((uint32_t)((address) & 0xFF));
    counter = 0;
    do {
      Wire.write((byte) *p++);
      i++;
      counter++;
    }
    while ((counter < write_size));
    Wire.endTransmission();
    address += write_size;                      // увеличиваем адрес для следующего буфера
    delay(5);                                   // задержка нужна для того, чтобы ЕЕПРОМ успела 
                                                // зарядить ячейки памяти (смотрите даташит своего устройства)
  }
}


 

sadman41
Онлайн
Зарегистрирован: 19.10.2016

Просто интересно - для чего такая конструкция: Wire.write((uint32_t)((address) >> 8));

kostyamat
Offline
Зарегистрирован: 16.11.2017

Способ использования прост до безобразия. Создаем новую вкладку в скетче, обзываем ее к примеру EEPROM24Cxxx.h, потом в основном скетче

#include "EEPROM24Cxxx.h"



void setup() {
  Wire.begin();
  Wire.setClock(800000L);                                   /* I2C на скорости 800кГц. 
  Лучше ставить значение  400000L (400кГц) если используется с дисплеями типа LCD1602 - 2004
  и аналогичными, висящими на той же шине, иначе дисплей зависает намертво, и вешает шину I2C. 
  Скорость записи\считывания соответственно существенно ниже. 
  Некоторые ЕЕПРОМ вполне работают на частоте до 1Мгц */

  Serial.begin(115200); 

  String myString = " Запись в ЕЕПРОМ - OK";

  unsigned int addRec  = 512;                                // Переменная содержит адрес записи в ЕЕПРОМ
  
  EEPROM_put(addRec,  myString);                             // Запись переменной

  delay(1000);

  myString = "новая строка"; 

  Serial.println(myString);
  delay(1000);

  EEPROM_get(addRec, myString);                                // Считывание переменной

  delay(1000);
  Serial.println(myString);

}


void loop () {
}

Функции читают и пишут все подряд - любые данные, любого типа (вплоть до сложных структур, размером от 1-го до 256 байтов. 

 

 

ПС. написано все это для новичков, в первую очередь (сам себя, по факту, еще из этого статуса не исключил), Гуру тутышние "сами с усами". Так что сильно не пинайте, кому-то пригодится. ;)

ППС. @Модераторы:   Закрепить бы где, а то утонет. Вещь вроде полезная.

kostyamat
Offline
Зарегистрирован: 16.11.2017

sadman41 пишет:

Просто интересно - для чего такая конструкция: Wire.write((uint32_t)((address) >> 8));

честно, сам не знаю, срисовал :))

sadman41
Онлайн
Зарегистрирован: 19.10.2016

Тут ЕвгенийП как-то писал, что подобные штуки темплейтами не очень полезно делать. Тем более, что первой операцией у Вас сразу идёт приведение указателя к байтовому. 

size_t EEPROM_put(uint32_t eeaddress, const uint8_t* ptrData, const size_t sizeData) {...; return writtenBytes; } должна меньше бить МК по ресурсам.

sadman41
Онлайн
Зарегистрирован: 19.10.2016

kostyamat пишет:

sadman41 пишет:

Просто интересно - для чего такая конструкция: Wire.write((uint32_t)((address) >> 8));

честно, сам не знаю, срисовал :))

Wire.h : inline size_t write(unsigned long n) { return write((uint8_t)n); }

Logik
Offline
Зарегистрирован: 05.08.2014

Ну вобще работать с ЕЕПРОМ странично - очень верно. Про неуместность шаблонов уже написано. Если уж сильно хочется имет функции прям для классов - ну напишите сразу для буфера а из шаблона уже её вызывайте, всеж компактней. Но куда интересней вот это:

// структура размером\переменная, размером 43 байта

однако

Физический буффер i2c шины в Ардуино равен 32 байта, поэтому, воспользоваться прелестями чтения, 64 или 128 байт за раз, не получится.

Получается не решена задача. Можна бы одной страницей, а прийдется за 2-3 ?

К тому же, увеличение буффера до 128 байт было бы слишком расточительно для ОЗУ ардуино.

И с этим я согласен. Потому логично не делать буферизацию чтения. Тогда и структуру принять можна, и ОЗУ не разбрасыватся. Попробуйте калбэк, вам понравится. Тем более что i2c синхронная шина, она просто просит такое.

 

PRC
Онлайн
Зарегистрирован: 03.02.2019

Сейчас лучше FRAM использовать. При питании работает как ОЗУ, сама при снятии питания сохраняет, а при включении восстанавливает данные. И скорость шины официально до 1МГц.

kostyamat
Offline
Зарегистрирован: 16.11.2017

Logik пишет:

Ну вобще работать с ЕЕПРОМ странично - очень верно. Про неуместность шаблонов уже написано. Если уж сильно хочется имет функции прям для классов - ну напишите сразу для буфера а из шаблона уже её вызывайте, всеж компактней. Но куда интересней вот это:

// структура размером\переменная, размером 43 байта

однако

Физический буффер i2c шины в Ардуино равен 32 байта, поэтому, воспользоваться прелестями чтения, 64 или 128 байт за раз, не получится.

Получается не решена задача. Можна бы одной страницей, а прийдется за 2-3 ?

К тому же, увеличение буффера до 128 байт было бы слишком расточительно для ОЗУ ардуино.

И с этим я согласен. Потому логично не делать буферизацию чтения. Тогда и структуру принять можна, и ОЗУ не разбрасыватся. Попробуйте калбэк, вам понравится. Тем более что i2c синхронная шина, она просто просит такое.

 


А что не так с шаблонами? Можно где то почитать?

Да, 43 байта, такова задача.

Проблема чтения страницами по 64/128 байт действительно решена не полностью, читаем фреймами по 32 байта, но за один вызов функции.

"Попробуйте калбэк, вам понравится. Тем более что i2c синхронная шина, она просто просит такое." - можно поподробнее, особенно интересно что вы имеете в виду под словом "калбек"?

Кстати, запись тех же данных сейчас - около 23 секунд, было больше 10 минут, а скорость поиска 1,2 секунды, а было 17. Если запускать шину на 800мГц, то ищет за 0.7 секунды.

sadman41
Онлайн
Зарегистрирован: 19.10.2016

PRC пишет:

Сейчас лучше FRAM использовать. При питании работает как ОЗУ, сама при снятии питания сохраняет, а при включении восстанавливает данные. И скорость шины официально до 1МГц.

В какой области фрамка сама сохраняет и восстанавливает данные - в ОЗУ микроконтроллера что ли?

Logik
Offline
Зарегистрирован: 05.08.2014

kostyamat пишет:
"Попробуйте калбэк, вам понравится. Тем более что i2c синхронная шина, она просто просит такое." - можно поподробнее

Все просто. При инициализации обмена (далее для определенности пусть чтение будет) вызываем соответствующую функцию с адресом и новый параметр - указатель на функцию принимающую данные, оно и есть калбек. Считали байт - вызвали эту функцию передав этот считаный байт, она уже знает что с ним делать, может и в структуру сложить, а может сразу обработать, зачемто же он нужен. Это простейшая реализация. Она позволяет отказатся от буфера но имеет и недостаток - побайтно может оказатся слишком медленно.

Совершенствуем. Пусть калбек принимает не один байт, а несколько, через небольшой буфер, который сам калбек и укажет и размер которого сама и сообщит. Тогда работа будет такая. Инитим обмен, передаем ей адрес и калбек. Начинается страничное чтение, но сколько считать и куда девать - неизвестно. Поэтому функция чтения вызывает калбек, а он возвращает сколько байт нужно считать и указатель на буфер куда их положить. Это может быть часть структуры, или вся, вобщем по ситуации. Функция страничного чтения считывает сколько нужно и снова вызывает калбек, передав считаные данные. При этом страничное чтение не завершается, оно просто откладывается пока калбек отработает. А синхронная шина это терпит, она нас никуда не торопит! Калбек их может обработать по ходу, а может и просто сохранить, но в любом случае он снова скажет сколько байт нужно и куда их укладывать. И так пока калбек не вернет 0 - чтение закончено. Так можна без буфера сразу в свою структуру читать, а если какие данные не нужны - то и пропустить можна. Насколько стандартная либа i2c к такому готова - незнаю, я её не пользую, вероятно допиливать надо. 

Таким образом можна строить и цепочки по обработке данных. Правда есть один подводный камень, не так давно напоролся. Предполагалось читать с 24с512 данные логирования процесса и выводить на экран график. Я построил вот так цепочку. И оно не заработало. Потому что экран и 24с512 на одном i2c висели, и вывод данных на экран нарушал страничное чтение. Решение очевидное - сделал второй аппаратный i2c и нормас.

Logik
Offline
Зарегистрирован: 05.08.2014

sadman41 пишет:

PRC пишет:

Сейчас лучше FRAM использовать. При питании работает как ОЗУ, сама при снятии питания сохраняет, а при включении восстанавливает данные. И скорость шины официально до 1МГц.

В какой области фрамка сама сохраняет и восстанавливает данные - в ОЗУ микроконтроллера что ли?

)))

В блокнот записывает;)

Загугли бы шоле FRAM, не позорился бы лишний раз.

Logik
Offline
Зарегистрирован: 05.08.2014

kostyamat пишет:
Кстати, запись тех же данных сейчас - около 23 секунд, было больше 10 минут, а скорость поиска 1,2 секунды, а было 17. Если запускать шину на 800мГц, то ищет за 0.7 секунды.

Так страничная работа рулит однозначно!

kostyamat
Offline
Зарегистрирован: 16.11.2017

Ну так... я уже хотел от задачи отказаться, скорость реакции  микроконтроллера с такой памятью сильно не устаривала, но интересно стало. Сейчас, в принципе, скорость практически устраивает.

Кстати, в этих "моих" функциях (хотя чтение я  сам писал, а  запись сильно правил) буфер как таковой даже не применяется. Я вот не помню, буфер I2C аппаратный, или на стеке висит? Но так или иначе он уже висит в памяти, а свой я не создаю, а даже переменную не создаю для возврата, а прямо в область памяти живой переменной данные вписываю. Остальные созданные переменные, при выходе из функции умрут.

Похоже это таки оптимально. Или ошибаюсь?

Кстати, если кто из грамотных людей это все еще причешет - я только рад буду. Может получится практически универсальная библиотека для 24Cxxx. Увы микросхемы поменьше объемом не совместимы остаются, у меня с 24C32 не заработало. Видать страничный режим ей не по зубам, даташит не искал, лень было, не очень то и хотелось, просто воткнул вместо 512, -  нет так нет.

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

я делал с общим кэшем на запись/чтение на 16 байт, скорость мня устроила. 

Logik
Offline
Зарегистрирован: 05.08.2014

kostyamat пишет:

 Я вот не помню, буфер I2C аппаратный, или на стеке висит? 

Вобще и я таких вещей стараюсь не запоминать ;) Но открываю Wire.c,  а в нем

uint8_t TwoWire::rxBuffer[BUFFER_LENGTH];
....
uint8_t TwoWire::txBuffer[BUFFER_LENGTH];

Ну и в Wire.h загляну: #define BUFFER_LENGTH 32

Итого два буфера по 32байта глобально. 

У 24C32 страницы менше, может из-за этого. И кажется адресация немного по другому, т.к. адрес короче.

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

Адресация одинакова от 24с32 до 24с512, адрес 2 байта.  Размер страниц разный. 

24с32 стоит на китайских модулях часов 3231, я с ней баловался. Этот же код без переделок работал на внешних модулях 24с256, вроде ошибок не было, я там пиктограммы храню, при косяках с адресацией все наглядно на экранчике видно. 

kostyamat
Offline
Зарегистрирован: 16.11.2017

Вы правы, в даташите  на 24С32 размер страницы  равен 32 байта, мои функции должны вполне себе работать. Только в 24Cxxx.h нужно дефайн размера страницы в 32 поставить.

kostyamat
Offline
Зарегистрирован: 16.11.2017

Logik пишет:

kostyamat пишет:

 Я вот не помню, буфер I2C аппаратный, или на стеке висит? 

Вобще и я таких вещей стараюсь не запоминать ;) Но открываю Wire.c,  а в нем

uint8_t TwoWire::rxBuffer[BUFFER_LENGTH];
....
uint8_t TwoWire::txBuffer[BUFFER_LENGTH];

Ну и в Wire.h загляну: #define BUFFER_LENGTH 32

Итого два буфера по 32байта глобально. 

У 24C32 страницы менше, может из-за этого. И кажется адресация немного по другому, т.к. адрес короче.

интересный факт, я в библу Wire.h лазил, и размер буфера увеличивал до 64-х, дефайн менял на 64. По факту он все равно 32 байта оставался. Почему ?

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

Не лазил, не знаю.

Feofan
Offline
Зарегистрирован: 28.05.2017
kostyamat пишет:
По факту он все равно 32 байта оставался. Почему?
Попробуйте одновременно изменить в twi.h #define TWI_BUFFER_LENGTH 32. Но эти изменения могут негативно сказаться в других местах.