Внешняя eeprom. Микроконтроллеры STM32: работа с внешним EEPROM. Особый случай для счётчика числа записей

Faq 18.03.2019
Faq

Когда нужно длительное время хранить какие-нибудь рабочие данные, не боясь их потерять, используют штатную, встроенную в микроконтроллер, EEPROM память. Обычно размер этой памяти не велик и его хватает только для хранения каких-то небольших по объему данных, например пользовательских настроек или т.п. А если нужно хранить данные размером десяток килобайт, то понятно что встроенной памятью не обойтись и нужно подключать внешнее устройство хранения. И тут как нельзя лучше подходят внешние микросхемы EEPROM. Например микросхемы из серии 24LCxx от компании Microchip. Эти микросхемы поддерживают подключение по протоколу I2C и умеют хранить от 512 байт до 128 килобайт данных. К тому же, старшие модели могут работать в связке из себе подобных, таким образом размер памяти может быть увеличен за счет присвоения микросхемам памяти индивидуального адреса на I2C шине. В младших же моделях, все микросхемы имеют фиксированный адрес 1010.


В номенклатуре Microchip серии 24LC числовое значение после буквенного индекса обозначает объем памяти в килобитах . Так, подопытная микросхема 24LC08 имеет на борту 8 килобит пространства под хранение данных (или 1 килобайт).

Подключение микросхемы

Данная микросхема выпускается в различных корпусах: DIP, SOIC, TSOP, DFN. Для каждого восьминогового типа корпуса сохраняется распиновка контактов.

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

Назначение выводов микросхемы смотрим ниже:

A0, A1, A2 - в данной микросхеме не используются, в старших моделях они служат для присвоения микросхеме индивидуального адреса на I2C шине.

SDA – линия данных

SCL – линия тактовых импульсов

WP – защита от записи. Если на данный вывод подан логический 0, то запись в память разрешена. Если подать логическую единицу, то возможно только чтение из памяти.

Vcc – питание микросхемы. Напряжение может быть в пределах от 2.5 вольта до 5.5 вольта.

Vss – земля.

К микроконтроллеру микросхема подключаются следуя традициям протокола I2C, тоесть сигнальные линии подтягиваются к шине питания через резисторы номиналом 4,7к. Неиспользуемые выводы A0, A1, A2 можно посадить на землю (IC1 в примере Attiny2313)


Организация памяти

Для того чтобы понять принцип работы с микросхемами памяти, нужно разобраться как происходит адресация внутри микросхемы. В подопытной микросхеме 24LC08 все пространство памяти поделено на 4 блока по 256 байт в каждом блоке. Каждый блок имеет свой адрес.


При обращении к микросхеме ведущее устройство (микроконтроллер) отправляет адрес устройства (он у нас фиксированный 1010) и адрес блока с которым нужно работать.

Затем отправляется адрес ячейки в которую нужно записать/прочитать данные. Что нужно сделать с данными - прочитать или записать - зависит от бита в конце посылки. Разберем на примерах.

Запись данных

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

  1. Отослать стартовый бит
  2. Отослать адрес микросхемы + адрес блока памяти. В конце посылки должен стоять 0 (бит записи)
  3. Отослать адрес ячейки памяти в которую будет производится запись
  4. Отослать байт данных
  5. Отослать стоповый бит

К примеру запишем один байт &hFF в первую ячейку памяти первого блока (адрес блока &b000 , адрес ячейки &h00 ).


$regfile = "2313def.dat"
$crystal = 1000000


Config Sda = Portb . 7 "I2C Data
Config Scl = Portb . 6 "I2C Clock

Wait 1

"работа с микросхемой
I2cstart "даем сигнал старт i2c шине
I2cwbyte & B10100000
I2cwbyte & H00 "отправляем адрес ячейки
I2cwbyte & HFF "отправляем байт, который нужно записать
I2cstop "останавливаем работу i2c

End

Постраничная запись

Для увеличения скорости записи данных существует метод постраничной записи. Одна страница - это область из 16 байт (один столбец на картинке выше). При постраничной записи адрес записываемой ячейки увеличивается автоматически, поэтому
не нужно каждый раз вручную прописывать адрес. Для записи одной страницы отправляем адрес первой ячейки и затем 16 раз отправляем необходимые данные, причем если отправить 17 байт, то последний байт перезапишет первый и т.д. Для примера запишем первую страницу первого блока. Адрес первой ячейки
&h00 .


$regfile = "2313def.dat"
$crystal = 1000000

"конфигурируем scl и sda пины
Config Sda = Portb . 7 "I2C Data
Config Scl = Portb . 6 "I2C Clock

Wait 1

"работа с микросхемой
I2cstart "даем сигнал старт i2c шине
I2cwbyte & B10100000 "отправляем адрес микросхемы и адрес блока
I2cwbyte & H00 "отправляем адрес первой ячейки

I2cwbyte & HF0 "отправляем 1 байт
I2cwbyte & HF1 "отправляем 2 байт
I2cwbyte & HF2 "отправляем 3 байт
I2cwbyte & HF3 "отправляем 4 байт
I2cwbyte & HF4 "отправляем 5 байт
I2cwbyte & HF5 "отправляем 6 байт
I2cwbyte & HF6 "отправляем 7 байт
I2cwbyte & HF7 "отправляем 8 байт
I2cwbyte & HF8 "отправляем 9 байт
I2cwbyte & HF9 "отправляем 10 байт
I2cwbyte & HFA "отправляем 11 байт
I2cwbyte & HFB "отправляем 12 байт
I2cwbyte & HFC "отправляем 13 байт
I2cwbyte & HFD "отправляем 14 байт
I2cwbyte & HFE "отправляем 15 байт
I2cwbyte & HFF "отправляем 16 байт

I2cstop "останавливаем работу i2c

End


Здесь записывается вся первая страница числами от 240 (в шестнадцатеричной системе F0) до 255 (FF).

Чтение данных

Теперь разберем как прочитать записанные в микросхему данные. Для чтения одного байта нужно проделать следующее:

  1. Сконфигурировать интерфейс I2C
  2. Отправить стартовый бит
  3. Отправить адрес микросхемы + адрес блока памяти откуда нужно читать
  4. Отправить адрес ячейки памяти
  5. Снова отправить стартовый бит
  6. Отправить адрес микросхемы и адрес блока памяти с битом «чтение»
  7. Получить байт
  8. Отправить стоповый бит

Чтение первой ячейки первого блока будет выглядеть так:


$regfile = "2313def.dat"
$crystal = 1000000

Dim A As Byte "переменная для хранения прочитанного байта

"конфигурируем scl и sda пины
Config Sda = Portb . 7 "I2C Data
Config Scl = Portb . 6 "I2C Clock

Wait 1

"работа с микросхемой

I2cstart "даем сигнал старт i2c шине
I2cwbyte & B10100000 "отправляем адрес микросхемы и адрес блока
I2cwbyte & H00 "отправляем адрес ячейки

EEPROM — это энергонезавимая память с электрическим стиранием информации. Количество циклов записи-стирания в этих микросхемах достигает 1000000 раз. Заминающие ячейки в них, также как и в постоянных запоминающих устройствах с электрическим стиранием EPROM, реализуются на основе транзисторов с плавающим затвором. Внутреннее устройство этой запоминающей ячейки приведено на рисунке 1:


Рисунок 1. Запоминающая ячейка ПЗУ с электрическим стиранием (EEPROM)

Ячейка EEPROM памяти представляет собой МОП транзистор, в котором затвор выполняется из поликристаллического кремния. Затем в процессе изготовления микросхемы этот затвор окисляется и в результате он будет окружен оксидом кремния — диэлектриком с прекрасными изолирующими свойствами. В транзисторе с плавающим затвором при полностью стертом ПЗУ, заряда в "плавающем" затворе нет, и поэтому данный транзистор ток не проводит. При программировании, на второй затвор, находящийся над "плавающим" затвором, подаётся высокое напряжение и в него за счет туннельного эффекта индуцируются заряды. После снятия программирующего напряжения индуцированный заряд остаётся на плавающем затворе, и, следовательно, транзистор остаётся в проводящем состоянии. Заряд на его плавающем затворе может храниться десятки лет.

Подобная ячейка памяти применялась в ПЗУ с ультрафиолетовым стиранием (EPROM). В ячейке памяти с электрическим стиранием возможна не только запись, но и стирание информации. Стирание информации производится подачей на программирующий затвор напряжения, противоположного напряжению записи. В отличие от ПЗУ с ультрафиолетовым стиранием, время стирания информации в EEPROM памяти составляет около 10 мс.

Структурная схема энергонезависимой памяти с электрическим стиранием не отличается от структурной схемы масочного ПЗУ. Единственное отличие — вместо плавкой перемычки используется описанная выше ячейка. Ее упрощенная структурная схема приведена на рисунке 2.



Рисунок 2. Упрощенная структурная схема EEPROM

В качестве примера микросхем EEPROM памяти можно назвать отечественные микросхемы 573РР3, 558РР3 и зарубежные микросхемы серий AT28с010, AT28с040 фирмы Atmel, HN58V1001 фирмы Hitachi Semiconductor, X28C010 фирмы Intersil Corporation. В EEPROM памяти чаще всего хранятся пользовательские данные в сотовых аппаратах, которые не должны стираться при выключении питания (например адресные книги), конфигурационная информация роутеров или сотовых аппаратов, реже эти микросхемы применяются в качестве конфигурационной памяти FPGA или хранения данных DSP. EEPROM изображаются на принципиальных схемах как показано на рисунке 3.


Рисунок 3. Условно-графическое обозначение электрически стираемого постоянного запоминающего устройства

Чтение информации из параллельной EEPROM памяти производится аналогично чтению из масочного ПЗУ. Сначала на шине адреса выставляется адрес считываемой ячейки памяти в двоичном коде A0...A9, затем подается сигнал чтения RD. Сигнал выбора кристалла CS обычно используется в качестве дополнительного адресного провода для обращения к микросхеме. Временные диаграммы сигналов на входах и выходах этого вида ПЗУ приведены на рисунке 4.



Рисунок 4. Временные диаграммы сигналов чтения информации из EEPROM памяти

На рисунке 5 приведен чертеж типового корпуса микросхемы параллельной EEPROM памяти.


Рисунок 5. Чертеж корпуса микросхемы параллельной EEPROM

Обычно данные, которые хранятся в EEPROM требуются достаточно редко. Время считывания при этом не критично. Поэтому в ряде случаев адрес и данные передаются в микросхему и обратно через последовательный порт. Это позволяет уменьшить габариты микросхем за счет уменьшения количества внешних выводов. При этом используются два вида последовательных портов — SPI порт и I2C порт (микросхемы 25сXX и 24cXX серий соответственно). Зарубежной серии 24cXX соответствует отечественная серия микросхем 558РРX.

Внутренняя схема микросхем серии 24сXX (например AT24C01) приведена на рисунке 6.



Рисунок 6. Внутренняя схема микросхемы AT24C01

Подобные микросхемы широко используются для сохранения настроек телевизоров, в качестве памяти plug and play в компьютерах и ноутбуках, конфигурационной памяти ПЛИС и сигнальных процессоров (DSP). Применение последовательной EEPROM памяти позволило значительно уменьшить стоимость данных устройств и увеличить удобство работы с ними. Пример расположения данной микросхемы на печатной плате карты памяти компьютера приведен на рисунке 7.



Рисунок 7. EEPROM на печатной плате карты памяти компьютера

На рисунке 8 приведена схема электронной карты с применением внешней EEPROM микросхемы.


Рисунок 8. Схема электронной карты с применением внешней EEPROM

На данной схеме микроконтроллер PIC16F84 осуществляет обмен данными с EEPROM памятью 24LC16B. В таких устройствах, как SIM-карта, уже не применяется внешняя микросхема памяти. В SIM-картах сотовых аппаратов используется внутренняя EEPROM память однокристального микроконтроллера. Это позволяет максимально снизить цену данного устройства.

Схема управления для электрически стираемых программируемых ПЗУ получилась сложная, поэтому наметилось два направления развития этих микросхем:

  1. ЕСППЗУ (EEPROM) - электрически стираемое программируемое постоянное запоминающее устройство
  2. FLASH-ПЗУ

FLASH - ПЗУ отличаются от ЭСППЗУ тем, что стирание производится не каждой ячейки отдельно, а всей микросхемы в целом или блока запоминающей матрицы этой микросхемы, как это делалось в РПЗУ.


Рисунок 9. Условно-графическое обозначение FLASH памяти

При обращении к постоянному запоминающему устройству сначала необходимо выставить адрес ячейки памяти на шине адреса, а затем произвести операцию чтения из микросхемы. Эта временная диаграмма приведена на рисунке 11.



Рисунок 10. Временные диаграммы сигналов чтения информации из ПЗУ

На рисунке 10 стрелочками показана последовательность, в которой должны формироваться управляющие сигналы. На этом рисунке RD - это сигнал чтения, A - сигналы выбора адреса ячейки (так как отдельные биты в шине адреса могут принимать разные значения, то показаны пути перехода как в единичное, так и в нулевое состояние), D - выходная информация, считанная из выбранной ячейки ПЗУ.

Литература:

Вместе со статьей "Постоянные запоминающие устройства (ПЗУ)" читают:

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

Внутри Ардуины есть 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 можно сделав для этого функцию и поместив промежуточные структурки в стеке в локальных переменных её.

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

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

#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; 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); }
Так как на модуле, кроме чипа и разъёмов, практически ничего нет, уже хочу купить несколько микросхем. Нравятся.

Одна из проблем с микроконтроллерами STM32 заключается в том, что большинство из них не имеют встроенного EEPROM . Исключением являются только микроконтроллеры серий STM32L0 и STM32L1 с ультра низким энергопотреблением. Это довольно странно после работы с AVR, где EEPROM есть у всех микроконтроллеров. Существует несколько решений, но в рамках этой заметки мы рассмотрим самое очевидное — использование внешнего EEPROM на примере чипа с I2C-интерфейсом 24LC64.

Цифра 64 в названии говорит о том, что устройство имеет 64 килобит памяти, или 8 Кбайт. Есть аналогичные чипы от разных производителей и с разными объемами памяти — я видел от 128 байт (например, M24C01 от компании ST) до 256 Кбайт (AT24CM02 производства Atmel). В плане распиновки и интерфейса все они абсолютно взаимозаменяемы. Далее я буду говорить о 24LC64, производимом компанией Microchip, так как сам использовал именно его.

Распиновка 24LC64 выглядит так (даташит ):

VSS и VCC, понятно, представляют собой минус и плюс питания, а SDA и SCL — это I2C шина. Устройство имеет I2C-адрес 0b1010zyx, где значения x, y и z определяются тем, к чему подключены пины A0, A1 и A2. Если пин подключен к земле, соответствующий бит адреса равен нулю, если же к плюсу — то единице. Таким образом, устройство может иметь адрес от 0 x 50 до 0 x 57. Наконец, пин WP — это write protection. Если пин подключен к земле, разрешено как чтение, так и запись. Если же пин подключен к плюсу, устройство доступно только на чтение.

Создаем новый проект в STM32CubeMX для вашей отладочной платы. Лично я все также использую плату Nucleo-F411RE, но если вы используете другую, отличия в проекте должны быть минимальными. В Peripherals включаем устройство I2C1, выбрав в выпадающем списке «I2C» вместо «Disable». Также мы будем передавать что-то компьютеру, поэтому включаем устройство USART2, как делали это в заметке Микроконтроллеры STM32: обмен данными по UART . Чтобы плату можно было использовать с Arduino-шилдами, несущими какие-то I2C-устройства, пины I2C1 нужно переназначить на PB9 и PB8 (по умолчанию будут назначены другие: PB7 и PB6). В итоге должна получиться такая картинка:

Затем генерируем код и подправляем Makefile, как обычно. Я лично просто взял Makefile из исходников к заметке про UART и дописал недостающие файлы в C_SOURCES, а именно:

$(FIRMWARE)/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_i2c.c \
$(FIRMWARE)/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_i2c_ex.c \

Для подключения 24LC64 к плате Nucleo я воспользовался Proto Shield от Arduino. Пришлось прорезать в нем отверстие Dremel’ем, чтобы иметь доступ к кнопкам на отладочной плате. В итоге у меня получился вот такой сэндвич:

Кто-то из читателей мог обратить внимание на то, что в I2C-шине должны использоваться резисторы, подтягивающие SCL и SDA к плюсу, но в этом проекте мы их не используем. Дело в том, что для моей платы и использованного в ней микроконтроллера STM32CubeMX автоматически включает встроенные подтягивающие резисторы на соответствующих пинах.

Убедиться в этом несложно, посмотрев код процедуры HAL_I2C_MspInit в файле./Src/stm32f4xx_hal_msp.c:

// ...
/**I2C1 GPIO Configuration
PB8 ------> I2C1_SCL
PB9 ------> I2C1_SDA
*/

GPIO_InitStruct.Pin = GPIO_PIN_8| GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
HAL_GPIO_Init(GPIOB, & GPIO_InitStruct) ;
// ...

Правда, не так очевидно, почему процедура HAL_I2C_MspInit вообще откуда-то вызывается. Ответ можно найти в файле stm32f4xx_hal_i2c.c где эта процедура объявляется с атрибутом __weak и вызывается из процедуры HAL_I2C_Init. Атрибут __weak работает таким образом, что при сборке не-weak процедура из кода нашего проекта подменяет собой weak-процедуру из HAL, за счет чего она и будет вызвана.

Заметьте, однако, что встроенные подтягивающие резисторы доступны не во всех микроконтроллерах STM32. Насколько мне известно, для серии STM32F1 это работать не будет, и придется все-таки использовать внешние подтягивающие резисторы.

Наконец, рассмотрим основной код прошивки:

void init() {
const char wmsg = "Some data" ;
char rmsg[ sizeof (wmsg) ] ;
// HAL expects address to be shifted one bit to the left
uint16_t devAddr = (0x50 << 1 ) ;
uint16_t memAddr = 0x0100 ;
HAL_StatusTypeDef status;

// Hint: try to comment this line
HAL_I2C_Mem_Write(& hi2c1, devAddr, memAddr, I2C_MEMADD_SIZE_16BIT,
(uint8_t * ) wmsg, sizeof (wmsg) , HAL_MAX_DELAY) ;

for (;; ) { // wait...
status = HAL_I2C_IsDeviceReady(& hi2c1, devAddr, 1 ,
HAL_MAX_DELAY) ;
if (status == HAL_OK)
break ;
}

HAL_I2C_Mem_Read(& hi2c1, devAddr, memAddr, I2C_MEMADD_SIZE_16BIT,
(uint8_t * ) rmsg, sizeof (rmsg) , HAL_MAX_DELAY) ;

if (memcmp (rmsg, wmsg, sizeof (rmsg) ) == 0 ) {
const char result = "Test passed!\r \n " ;

HAL_MAX_DELAY) ;
} else {
const char result = "Test failed:(\r \n " ;
HAL_UART_Transmit(& huart2, (uint8_t * ) result, sizeof (result) - 1 ,
HAL_MAX_DELAY) ;
}
}

Для работы с внешней памятью в HAL предусмотрены специальные процедуры HAL_I2C_Mem_Read и HAL_I2C_Mem_Write. Заметьте, что эти процедуры работают с I2C адресами, сдвинутыми на 1 бит влево. Связано это с тем, что в протоколе I2C семибитный адрес устройства и бит операции (чтение или запись) передаются в одном байте. Использование «сдвинутых» адресов позволяет выполнять чуть меньше ассемблерных инструкций, что нередко бывает важно в разработке встраиваемых систем. Еще стоит обратить внимание на то, что перед чтением с устройства мы должны дождаться его готовности с помощью процедуры HAL_I2C_IsDeviceReady. Наконец, здесь я забил на коды возврата большинства использованных процедур, чего в боевом коде, пожалуй, делать не стоит.

Fun fact! Вдумчивый читатель, конечно же, обратил внимание на тот факт, что адрес памяти имеет тип uint16_t . Спрашивается, как можно адресовать им более 64 Кбайт памяти, например, те же 256 Кбайт у AT24CM02? Само собой разумеется, никак. Чипы, имеющие более 64 Кбайт памяти, начинают использовать для адресации младшие биты I2С-адреса устройства. То есть, с точки зрения нашего кода, они будут выглядеть, как 2 или более отдельных I2C-устройства. Соответствующие пины, определяющие адрес устройства, при этом являются NC, то есть, ни к чему не подключаются.

При работе с EEPROM нужно учитывать еще пару важных моментов:

  • 24LC64 и его родственники хранят данные в страницах по 32 байта. За один вызов HAL_I2C_Mem_Write вы можете записать только одну страницу, причем нужно учитывать не только размер данных, но и их выравнивание. Если требутеся записать больше одной страницы, процедуру нужно вызывать в цикле. На чтение, насколько я смог выяснить, таких ограничений нет — читать можно сколько угодно и по любым смещениям;
  • Запись в EEPROM может быть прервана в любой момент (сел аккумулятор, пользователь выдернул кабель питания, …). Поэтому в боевом коде стоит хранить вместе с данными их контрольную сумму. В случае, если будет записана только часть данных, это позволит обнаружить проблему и использовать хотя бы параметры по умолчанию, а не какой-то мусор.

Полную версию исходников к этой заметке вы найдете на GitHub . В качестве небольшого домашнего задания можете модифицировать прошивку так, чтобы она дампила все содержимое EEPROM и передавала его по UART. Код форматирования бинарных данных в стиле того, как это делает hexdump, можно взять из поста Перехват сетевого трафика при помощи библиотеки libpcap (процедура print_data_hex). В итоге должен получиться приятный такой отладочный инструмент — простенький, но со вкусом.

А доводилось ли вам работать с внешней памятью, и если да, то с какой именно?

Дополнение: Также вас могут заинтересовать посты

12 октября 2015 в 14:27

Как избежать износа EEPROM

  • Электроника для начинающих
  • Tutorial

Резюме: Если вы периодически обновляете некоторое значение в EEPROM каждые несколько минут (или несколько секунд), вы можете столкнуться с проблемой износа ячеек EEPROM. Чтобы избежать этого, требуется снижать частоту записей в ячейку. Для некоторых типов EEPROM даже частота записи чаще чем один раз в час может быть проблемой.

Когда вы записываете данные, время летит быстро

EEPROM повсеместно используется для сохранения параметров настройки и журнала работы во встраиваемых системах. К примеру, вы можете хотеть функцию «черного ящика», для записи последних данных на момент аварии или потери питания. Я видел спецификации с требованием записывать подобные данные каждые несколько секунд.

Но проблема в том, что EEPROM имеет ограниченный ресурс числа записей. После 100,000 или миллиона записей (зависит от конкретного чипа), некоторые из ваших систем начнут испытывать проблемы с отказом EEPROM. (Посмотрите в даташит, чтобы узнать конкретную цифру. Если вы хотите выпустить большое число устройств, «наихудший случай», вероятно, более важен чем «типичный»). Миллион записей кажется большой цифрой, но на самом деле он закончится очень быстро. Давайте посмотрим на примере, предположив, что нам нужно сохранять измеренное напряжение в одну ячейку каждые 15 секунд.

1,000,000 записей при одной записи в 15 секунд дают записи в минуту:
1,000,000 / (4 * 60 минут/час * 24 часа/день) = 173.6 дней.
Другими словами, ваша EEPROM исчерпает резерв в миллион записей менее чем через 6 месяцев.

Ниже приведен график, показывающая время до износа (в годах), основанный на периоде обновления конкретной ячейки EEPROM. Ограничительная линия для продукта с продолжительностью жизни 10 лет составляет одно обновление каждые 5 минут 15 секунд для микросхемы с ресурсом 1 миллион записей. Для EEPROM с ресурсом 100К можно обновлять конкретную ячейку не чаще одного раза в 52 минуты. Это означает, что не стоит и надеяться обновлять ячейку каждые несколько секунд, если вы хотите, чтобы ваш продукт работал годы, а не месяцы. Вышесказанное масштабируется линейно, правда, в настоящем приборе имеются еще и вторичные факторы, такие как температура и режим доступа.

Уменьшить частоту

Самый безболезненный способ решить проблему-это просто записывать данные реже. В некоторых случаях требования к системе это позволяют. Или можно записывать только при каких-либо больших изменениях. Однако, с записью, привязанной к событиям, помните о возможном сценарии, при котором значение будет постоянно колебаться, и вызовет поток событий, которые приведут к износу EEPROM.
(Будет неплохо, если вы сможете определить, сколько раз производилась запись в EEPROM. Но это потребует счётчика, который будет храниться в EEPROM… при этом проблема превращается проблему износа счётчика.)

Прерывание по снижению уровня питания

В некоторых процессорах имеется прерывание по низкому уровню питания, которое можно использовать для записи одного последнего значения в EEPROM, в то время как система выключается по потере питания. В общем случае, вы храните интересующее значение в ОЗУ, и сохраняете его в EEPROM только при выключении питания. Или, возможно, вы записываете EEPROM время от времени, и записываете другую копию в EEPROM как часть процедуры выключения, чтобы убедиться, что самые последние данные запишутся.
Важно убедиться, что есть большой конденсатор по питанию, который будет поддерживать напряжение, достаточное для программирования EEPROM достаточно продолжительное время. Это может сработать, если вам нужно записать одно или два значения, но не большой блок данных. Осторожно, тут имеется большое пространство для ошибки!

Кольцевой буфер

Классическое решение проблемы износа-использовать кольцевой буфер FIFO, содержащий N последних записей значения. Так-же понадобится сохранять указатель на конец буфера в EEPROM. Это уменьшает износ EEPROM на величину, пропорциональную числу копий в этом буфере. Например, если буфер проходит через 10 различных адресов для сохранения одного значения, каждая конкретная ячейка модифицируется в 10 раз реже, и ресурс записи возрастает в 10 раз. Вам также понадобится отдельный счётчик или отметка времени для каждой из 10 копий, чтобы можно было определить, которая из них последняя на момент выключения. Другими словами, понадобится два буфера, один для значения, и один для счетчика. (Если сохранять счетчик по одному и тому-же адресу, это приведёт к его износу, т.к. он должен увеличиваться при каждом цикле записи.) Недостаток этого метода в том, что нужно в 10 раз больше места чтобы получить в 10 раз большую продолжительность жизни. Можно проявить смекалку, и упаковать счетчик вместе с данными. Если вы записываете большое количество данных, добавление нескольких байт для счетчика - не такая уж большая проблема. Но в любом случае, понадобится много EEPROM.
Atmel приготовил аппноут, содержащий все кровавые подробности:
AVR-101: High Endurance EEPROM Storage: www.atmel.com/images/doc2526.pdf

Особый случай для счётчика числа записей

Иногда нужно сохранить счётчик, а не сами значения. К примеру, вы можете хотеть знать число включений прибора, или время работы вашего устройства. Самое плохое в счётчиках, это то, что у них постоянно меняется младший значащий бит, изнашивая младшие ячейки EEPROM быстрее. Но и тут возможно применить некоторые трюки. В аппноуте от Microchip есть несколько умных идей, таких как использование кода Грея, чтобы только один бит из многобайтового счётчика менялся при изменении значения счетчика. Также они рекомендуют использовать корректирующие коды для компенсации износа. (Я не знаю, насколько эффективно будет применение таких кодов, т.к. это будет зависеть от того, насколько независимы будут ошибки в битах в байтах счётчика, используйте на свой страх и риск, прим. авт.). Смотри аппноут: ww1.microchip.com/downloads/en/AppNotes/01449A.pdf

Примечание: для тех, кто хотел бы узнать больше, Microchip подготовил документ, содержащий детальную информацию об устройстве ячеек EEPROM и их износе с диаграммами:
ftp.microchip.com/tools/memory/total50/tutorial.html

Дайте мне знать, если у вас имеются какие-либо интересные идеи по поводу борьбы с износом EEPROM.

Источик: Phil Koopman, «Better Embedded System SW»
betterembsw.blogspot.ru/2015/07/avoiding-eeprom-wearout.html

Примечание переводчика: в последние годы появились микросхемы EEPROM со страничной организацией стирания (подобной микросхемам FLASH), где логически можно адресовать ячейки (читать, записывать и стирать) побайтно, но при этом микросхема невидимо для пользователя стирает всю страницу целиком и перезаписывает новыми данными. Т.е. стерев ячейки по адресу 0, мы фактически стёрли и перезаписали ячейки с адресами 0...255 (при размере страницы 256 байт), поэтому трюк с буфером в этом случае не поможет. При исчерпании ресурс записей у такой микросхемы выходит из строя не одна ячейка, а вся страница целиком. В даташитах для таких микросхем ресурс записи указан для страницы , а не для конкретной ячейки. Смотри, например, даташит на 25LC1024 от Microchip.

Теги:

  • встраиваемые системы
  • eeprom
  • перевод
Добавить метки

Рекомендуем почитать

Наверх