Программирование eeprom ошибка программирования

Введение

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

   Одна из старых проблем AVR — это повреждение EEPROM данных при пониженном питании микроконтроллера. Это может происходить в двух случаях:

— Если напряжение питания ниже определенной величины, запись в EEPROM будет выполняться некорректно.
— При пониженном напряжении питания микроконтроллер сам может выполнять команды некорректно.

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

— Нужно удерживать микроконтроллер AVR в состоянии сброса, если напряжение питания находится ниже нормы. Для этого можно использовать внешние супервизоры питания или встроенный детектор пониженного питания — Brown-out Detector (BOD). Встроенный детектор управляется с помощью fuse битов микроконтроллера — BODEN и BODLEVEL. BODEN — разрешает/запрещает работу детектора, а BODLEVEL — определяет его уровень срабатывания.
   Если сброс микроконтроллера происходит во время процесса записи в EEPROM, то операция записи будет завершена только при достаточном уровне напряжения.

— Также в многие разработчике рекомендуют не использовать 0-ую ячейку EEPROM`a, поскольку именно ее содержимое чаще всего повреждается при снижении питания микроконтроллера.

   Операция записи в EEPROM состоит из нескольких шагов. Вспомним эту последовательность:

1. Ожидаем готовности EEPROM, опрашивая бит EEWE регистра EECR.
2. Устанавливаем адрес в регистре EEAR.
3. Записываем байт данных в регистр EEDR.
4. Устанавливаем основной флаг разрешения записи EEMWE регистра EECE
5. Устанавливаем флаг разрешения записи EEWE регистра EECE

   Бит EEWE должен быть установлен в течении 4-ех тактов после установки бита EEMWE. Если этого не произойдет по причине прерываний, то запись в EEPROM не будет произведена. Этого легко избежать, если запретить прерывания перед 4-м шагом, а после 5-го снова разрешить их.

   Однако есть еще один подводный камень. Если прерывание возникло после 1-го, 2-го или 3-го шага, и в прерывании тоже используются операции с EEPROM (чтение или запись), то запись может не состояться, или запишутся не те данные и не туда, потому что содержимое регистров EEAR (адрес) и EEDR (данные) будет изменено.

   Описанное выше касается и процедуры чтения EEPROM.

   Лечить это можно следующими способами:

   — Не использовать операции чтения и записи EEPROM в прерываниях.
   Это особенно касается операции записи, потому что она медленная и выполняется с использованием внутреннего RC генератора. Например, для mega16 в даташите указано, что при записи в EEPROM используется внутренний RC генератор с частотой 1 МГц (независимо от установок fuse битов CKSEL) и время записи составляет 8.5 мс. Для прерывания это очень долго.

   — Запрещать прерывания на время всей процедуры записи (чтения) EEPROM, то есть в самом начале.

   — Сохранять в начале обработчика прерывания содержимое регистров EEAR (адрес) и EEDR (данные), а перед выходом восстанавливать их.

   — Использовать флаги (семафоры) для сигнализации о выполнении работы с EEPROM.
   Перед выполнением записи в основном цикле программы (или задаче, если используется ос) устанавливать программный флаг, а в прерывании (или другой задаче) проверять его.

Ресурс EEPROM

   EEPROM имеет ограниченный ресурс. Atmel гарантирует, что количество циклов перезаписи EEPROM составляет не меньше 100000. Цифра довольно большая, однако и она может быть достигнута, если записывать в EEPROM часто и на протяжении долгого времени.
   Есть два приема по «увеличению» ресурса EEPROM.
   Первый — простой и состоит в том, чтобы записывать в EEPROM данные, только если они изменили свое значение.


__eeprom uint8_t data;
uint8_t newData;

...
if (newData != data) {
data = newData;
}

   Второй- хитрый и состоит в том, чтобы хранить данные не в одной ячейки памяти (или группе ячеек, если речь идет о многобайтных переменных), а в нескольких, и записывать в них по очереди.
   Допустим, нам нужно хранить в EEPROM один байт. Выделяем под него 8 байтов и каждый раз записываем в следующую ячейку, когда доходим до последней ячейки, то записываем в первую. И так по кругу, как в кольцевом буфере. Если каждая ячейка EEPROM имеет ресурс 100000, то записывая в 8 ячеек по очереди, мы получаем ресурс перезаписи байта 800000.

EEPROM и оптимизация компилятора

   Переменные, которые объявлены, но не используются, часто удаляются компилятором в процессе оптимизации. Если такие переменные нужны, перед ними следует добавлять ключевое слово volatile.


//для IAR AVR
volatile __eeprom char name[] = "prog 12.3";

   Если используются свои функции для работы с EEPROM, то могут возникнуть проблемы при высоких уровнях оптимизации компилятора. Компилятор может объединить одинаковые (с его точки зрения) части кода в одну подпрограмму и нарушить логику работы вашей функции. Чтобы этого не происходило нужно или запрещать оптимизацию данной функции, или отключать перекрестную оптимизацию (cross call optimization) для функции или файла. Как это делается зависит от компилятора. Как правило, для этого существуют определенные ключи и прагмы.

Программные способы повышения надежности EEPROM

   

Один из простых способов повышения надежности хранения данных в EEPROM — это метод мажоритарного резервирования. Суть метода заключается в том, что для хранения данных выделяется нечетное количество ячеек памяти — N. При сохранении данных — запись производится во все выделенные ячейки. При чтении — читаются тоже все, но решение относительно содержимого принимается на основе равенства (N+1)/2 ячеек.

   Рассмотрим пример тройного мажоритарного резервирования байта данных. Для сохранения байта используются три байта EEPROM, а решение о содержимом принимается на основании равенства 2 байтов. Код приведен для компилятора IAR AVR.


//функция сохранения
void EEPROM_SaveByte(uint8_t value, uint8_t __eeprom *buf)
{
   buf[0] = value;
   buf[1] = value;
   buf[2] = value;
}

//функция считывания
uint8_t EEPROM_LoadByte(uint8_t *value, uint8_t __eeprom *buf)
{
   uint8_t a = buf[0];
   uint8_t b = buf[1];
   uint8_t c = buf[2];

   if ((a == b)||(a == c)){
      *value = a;
      return 0;
   }
   else {
      if (b == c){  
         *value = b;
         return 0;
      }
   }
   return 1;
}

....
//пример использования

__eeprom uint8_t buffer[3];
uint8_t data;

EEPROM_SaveByte(125, buffer);
EEPROM_LoadByte(&data, buffer);

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

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

   На этом все…

На чтение 6 мин Просмотров 1.4к.

Рассмотрим подробнее

  1. Техническое описание и расшифровка ошибки P0602
  2. Симптомы неисправности
  3. Причины возникновения ошибки
  4. Как устранить или сбросить код неисправности P0602
  5. Диагностика и решение проблем
  6. Проверка и перепрограммирование
  7. На каких автомобилях чаще встречается данная проблема
  8. Видео

Код ошибки P0602 звучит как «ошибка программирования модуля управления». Часто, в программах, работающих со сканером OBD-2, название может иметь английское написание «Control Module Programming Error».

Техническое описание и расшифровка ошибки P0602

Код OBD-II P0602 определяется как «ошибка программирования модуля управления». Устанавливается, когда в модуле управления трансмиссией (PCM) обнаруживается ошибка программного обеспечения.

Код ошибки P0602 – ошибка программирования модуля управления

Если посмотреть на блок PCM, можно увидеть простой металлический ящик. Но именно то, что находится внутри, важно для эффективной работы двигателя. Информация, которая имеет жизненно важное значение для управления двигателем, программируется на заводе. С использованием электронно-стираемой программируемой постоянной памяти (EEPROM).

Настройки управления двигателем и трансмиссией для конкретного автомобиля запрограммированы в EEPROM для каждого автомобиля, покидающего завод. EEPROM не только помогает управлять эффективностью и производительностью двигателя. Но также служит для распознавания и взаимодействия с другими бортовыми модулями управления автомобилем.

Если PCM обнаруживает несоответствия с EEPROM, или если он не может распознать программу EEPROM. То код P0602 будет сохранен, и может загореться индикаторная лампа неисправности (MIL). В зависимости от серьезности неисправности для включения контрольной лампы может потребоваться несколько циклов зажигания (с неисправностью).

По сути, ошибка P0602 означает, что в модуле управления трансмиссией (PCM) имеется проблема с программным обеспечением.

Симптомы неисправности

Основным симптомом появления ошибки P0602 для водителя является подсветка MIL (индикатор неисправности). Также его называют Check engine или просто «горит чек».

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

  1. Загорится контрольная лампа «Check engine» на панели управления (код будет записан в память как неисправность).
  2. Плавающие обороты, а также попытки заглохнуть на холостом ходу.
  3. Множественные проблемы с управляемостью.
  4. Резкое или неустойчивое переключение автоматической коробки передач.
  5. Снижение мощности двигателя.
  6. Повышенный расход топлива.
  7. Проблемы с конкретной системой, указывающие на возможную потерю связи в этой области.

Код P0602 указывает на ошибку программирования EEPROM. В зависимости от того, что затронул сбой, ошибка может быть как серьезной, так и умеренной.

Причины возникновения ошибки

Код P0602 может означать, что произошла одна или несколько следующих проблем:

  • Ошибка программирования модуля управления трансмиссией (PCM).
  • Если блок управления двигателем был заменен, возможно, его необходимо запрограммировать.
  • Плохое реле источника питания ECM / PCM.
  • Обрыв или короткое замыкание в жгуте проводов.
  • Ослабленные или поврежденные разъемы.
  • Плохое заземление является распространенной причиной.
  • Перегорел предохранитель блока управления двигателем.
  • Шина CAN может быть неисправна и иметь короткое замыкание на массу или обрыв провода.
  • Иногда причиной является неисправный модуль PCM.

Как устранить или сбросить код неисправности P0602

Некоторые предлагаемые шаги для устранения неполадок и исправления кода ошибки P0602:

  1. Считайте все сохраненные данные и коды ошибок с помощью сканера OBD-II. Чтобы выяснить, когда и при каких обстоятельствах появилась ошибка P0602.
  2. Очистите коды ошибок с памяти компьютера и проведите тест-драйв автомобиля, чтобы выяснить, появляется ли неисправность снова.
  3. Если код ошибки появится снова, визуально осмотрите электрические провода и разъем модуля управления трансмиссией (PCM).
  4. Измерьте напряжение аккумуляторной батареи и сравните полученное значение со значением, указанным в технических условиях производителя.
  5. Проверьте целостность цепей питания и заземления модуля управления трансмиссией (PCM) с помощью мультиметра.
  6. Попробуйте перепрограммировать модуль управления.
  7. Оцените работу модуля управления трансмиссией (PCM), следуя процедуре, установленной производителем автомобиля.
  8. Снова очистите код ошибки с памяти компьютера, проведите тест-драйв автомобиля, чтобы выяснить, решена ли проблема.

Диагностика и решение проблем

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

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

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

Если все соединения заземления надежны, выполните тщательный визуальный осмотр всей проводки на автомобиле. Обратите внимание, что этот осмотр может потребовать снятия приборной панели, сидений, ковров и даже панелей отделки, чтобы получить доступ ко всем жгутам проводов.

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

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

Проверка и перепрограммирование

Если PCM был недавно заменен, вероятность того, что он был неправильно запрограммирован или не запрограммирован вообще, велика. Кроме того, нельзя использовать контроллер от другого транспортного средства. Если он не был перепрограммирован специально для рассматриваемого транспортного средства.

Послепродажные (высокопроизводительные) прошивки могут не распознаваться другими контроллерами, что приводит к сохранению P0602.

Неправильно запрограммированные или замененные контроллеры могут быть перепрограммированы заводским сканером. Если вы не можете получить доступ к такому сканеру, большинство дилерских центров предлагают программирование PCM за символическую плату.

Некоторые производители вторичного рынка PCM предлагают предварительно запрограммированные контроллеры для определенных моделей. Обратитесь к поставщику автозапчастей и получите идентификационный номер автомобиля, пробег и другие соответствующие данные.

На каких автомобилях чаще встречается данная проблема

Проблема с кодом P0602 может встречаться на различных машинах, но всегда есть статистика, на каких марках эта ошибка присутствует чаще. Вот список некоторых из них:

  • Chevrolet (Шевроле Сильверадо)
  • Citroen
  • Dodge (Додж Рам)
  • Ford (Форд Мустанг, Рейнджер, Фокус)
  • GMC
  • Honda
  • Hyundai
  • Isuzu
  • Kia (Киа Спортейдж)
  • Mazda
  • Opel (Опель Астра, Корса, Мерива)
  • Peugeot (Пежо 308)
  • Volkswagen
  • Volvo (Вольво s40, v40, xc90)

С кодом неисправности Р0602 иногда можно встретить и другие ошибки. Наиболее часто встречаются следующие: P0100, P0136, P0142, P0171, P0174, P0216, P0601, P0603, P0604, P0605, P0700, P1689, U0101.

Видео

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

Задание тактирования

Тактирование блока всегда должно задаваться ПЕРЕД настройкой его конфигурации!

Очень часто возникают ошибки, когда настраивается какой-либо периферийный блок, при этом тактирование данного блока не разрешено в контроллере тактовых частот. Также возможна ситуация, когда тактирование сбрасывается в одном из подключенных файлов. Необходимо внимательно следить за последовательностью включения тактирования периферийного блока и его конфигурации. В фрагменте кода 1 приведена корректная настройка порта С.

Фрагмент кода 1 — Пример инициализации порта ввода-вывода

// 1 - Включаем тактирование порта С
RST_CLK_PCLKcmd (RST_CLK_PCLK_PORTC, ENABLE);

// 2 - Инициализируем порт в заданной конфигурации
PORT_StructInit(&GPIOInitStruct);
GPIOInitStruct.PORT_Pin = PORT_Pin_0;
GPIOInitStruct.PORT_OE = PORT_OE_OUT;
GPIOInitStruct.PORT_SPEED = PORT_SPEED_SLOW;
GPIOInitStruct.PORT_MODE = PORT_MODE_DIGITAL;

PORT_Init(MDR_PORTC, &GPIOInitStruct);

Программа работает с отладчиком, но не работает при подаче питания на МК

Если программа работает с отладчиком, но не работает после сброса по Reset  или после включения питания, необходимо проверить очередность включения тактирования и инициализации!

Это тот случай, когда тактирование в программе задано после настройки периферии. На примере настройки портов рассмотрим ход выполнения программы:

  1. После сброса по Reset или после включения питания запускается пользовательская программа, тактирование периферийных контроллеров по сбросу выключено.

  2. В программе происходит инициализация портов.

  3. В программе включается тактирование портов.

Так как тактирование портов не было включено на момент их инициализации, то настройка портов не была произведена. 

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

  1. Отладчик подключается и перезапускает МК, при этом если в настройках отладчика выбран сброс только ядра, то тактирование периферийных блоков остаётся включённым с прошлого запуска.

  2. В программе происходит инициализация портов, которая выполняется успешно, так как тактирование портов включено. 

  3. В программе включается тактирование портов.

В данном случае порты ввода-вывода функционируют корректно, поскольку их инициализация происходила при уже включенном тактировании.

Переменные и флаги в прерываниях

Нельзя изменять значение переменной в основном потоке и в обработчике прерывания без атомарного доступа!

Например, простейшая операция » i++; » происходит в несколько этапов:

  1. В регистр ядра Rx загружается значение i из ячейки памяти.

  2. Значение регистра увеличивается на 1.

  3. Значение регистра сохраняется в ячейку памяти i.

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

В ядре Cortex M существуют операции для защищенного обращения к памяти LDREX и STREX. Информацию по их использованию можно найти, например, на официальном сайте Keil.

Вторым вариантом обращения к памяти может быть использование метода bit-band, который доступен для МК на базе Cortex M3/M4. Запись и стирание флагов в ячейках памяти происходит атомарными операциями типа чтение-модификация-запись.

Программирование Flash-памяти

Flash память МК может работать в двух режимах: в обычном режиме (доступ к памяти осуществляется через шины I Code и D Code) и в режиме программирования (доступ к памяти осуществляется через регистры контроллера Flash-памяти). В режиме программирования программный код должен выполняться из области системной шины (внешняя память) или ОЗУ. Выполнение программного кода из Flash-памяти в режиме программирования невозможно. При попытке доступа ядра к Flash-памяти, находящейся в режиме программирования, будет вызвано прерывание HardFault или BusFault в зависимости от настроек ядра (SCR регистры). Поэтому важно, чтобы при программировании Flash-памяти не возникало никаких прерываний, поскольку таблица векторов и обработчики прерываний по умолчанию расположены во Flash-памяти.

Чтобы запретить выбранное прерывание IRQn необходимо вызвать функцию NVIC_DisableIRQ(). При этом необходимо учитывать, что запрещение генерации запроса прерываний от системного таймера SysTick выполняется не в контроллере NVIC, а в регистре управления системным таймером.

Пример запрещения прерываний от системного таймера приведён в фрагменте кода 2.

Фрагмент кода 2 — Запрещение прерываний от системного таймера

SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;

Чтобы запретить сразу все прерывания, кроме HardFaut и NMI, необходимо выполнить специальную функцию, как показано в фрагменте кода 3.

Фрагмент кода 3 — Запрещение всех прерывания, кроме HardFaut и NMI


 __disable_irq();

После работы с Flash-памятью необходимо вернуть разрешение прерываний.

В режиме программирования функции работы с Flash-памятью должны выполняться из области системной шины (внешняя память) или ОЗУ!

Пример расположения программного кода в памяти ОЗУ приведён в статье Расположение функций в ОЗУ, программирование EEPROM.

Выводы, совмещенные с JTAG и SWD

Иногда возникает необходимость использовать выводы, совмещенные с интерфейсом JTAG и SWD (далее для краткости используется обозначение JTAG, но подразумевается JTAG и SWD). При использовании функций библиотеки SPL выводы, совмещенные с JTAG B, перенастроить не получится, так как для них по умолчанию установлена защита. Например, функция PORT_Init() проверяет конфигурируемые выводы на принадлежность к JTAG B и не даёт их переназначать. Разрешение данной проверки определено в файле MDR32FxQI_config.h с помощью макроопределения USE_JTAG_B, строка 80, как показано в фрагменте кода 4.

Фрагмент кода 4 — Макроопределения защиты выводов, совмещённых с JTAG, в файле MDR32FxQI_config.h

#if (defined(USE_MDR32F9Q2I) || defined (USE_MDR32FG16S1QI))

  
/* #define USE_JTAG_A */
#define USE_JTAG_B

#endif

Библиотечный файл MDR32FxQI_config.h защищен от записи, поэтому необходимо предварительно в свойствах файла снять атрибут «Только чтение» (Правая клавиша мыши -> Свойства -> Только чтение). Для снятия защиты с выводов, совмещённых с JTAG A или B необходимо закомментировать соответствующее макроопределение: USE_JTAG_A или USE_JTAG_B. После этого данными выводами можно управлять с помощью функций SPL.

Необходимо обратить внимание, что после переопределения выводов, совмещенных с JTAG, выбранный интерфейс JTAG работать не будет. Программа при запуске будет переопределять эти выводы, и подключиться к МК через данный интерфейс будет невозможно. Для связи с МК необходимо будет использовать либо другой интерфейс JTAG, либо интерфейс UART при старте МК в режиме «UART загрузчик».

Чтобы сохранить работоспособность интерфейса JTAG при старте МК, необходимо в начале функции main() вставить пустой цикл на пару секунд. Этот цикл даст некоторую задержку перед переопределением выводов, совмещённых с JTAG. За это время отладчик успеет перехватить управление и остановить исполнение программы. Таким образом сохраняется возможность подключиться к МК, даже если выводы JTAG в программе используются по другому назначению.

Запись в регистры порта, выводы которого совмещенные с JTAG и SWD

Если в программе не используются функции SPL, то необходимо учитывать, что при записи в регистры MDR_PORTx→RXTX и MDR_PORTx→OE биты выводов, совмещенных с JTAG, необходимо сбрасывать. Если этого не сделать, то работа интерфейса будет нарушена, а отладка невозможна.

В качестве примера можно посмотреть реализацию функций PORT_SetBits() или PORT_ResetBits() библиотеки SPL.

Смена тактовой частоты

Для смены тактовой частоты на более высокую требуется совершить следующие операции:

  • Если требуется, переключить мультиплексор С3 на промежуточный источник тактирования, например, HSI.

  • Настроить генератор HSE и/или умножитель частоты PLL и дождаться, пока он выйдет в рабочий режим.

  • Настроить в контроллере Flash-памяти число тактов паузы Delay до переключения на более высокую частоту.

  • Настроить поля SelectRI и LOW в регистре MDR_BKP→REG_0E.

  • Переключить мультиплексор С3 на новый источник тактирования.

При переходе на более низкую частоту, изменение значения Delay и SelectRI, LOW производят после смены частоты.

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

До перехода на новую частоту с помощью переключения мультиплексора C3 необходимо, чтобы новая частота была полностью сформирована, и МК был полностью готов к работе на ней.

На момент смены частоты значения Delay и SelectRI, LOW должны соответствовать максимальной частоте из старого и нового значения.

Пример инициализации тактирования в МК К1986ВЕ92QI

В фрагменте кода 5 приведена функция CPU_Initialize(), инициализирующая тактирование в МК К1986ВЕ92QI от умножителя частоты PLL с использованием генератора HSE, который работает на внешнем кварцевом резонаторе. Для работы функции CPU_Initialize() в проект необходимо подключить библиотечные файлы MDR32FxQI_rst_clk.c,  MDR32FxQI_eeprom.c, MDR32FxQI_power.c.

Фрагмент кода 5 — Инициализация тактирования в МК К1986ВЕ92QI

#include <MDR32FxQI_rst_clk.h>
#include <MDR32FxQI_eeprom.h>
#include <MDR32FxQI_power.h>

// Инициализация системы тактирования микроконтроллера 
void CPU_Initialize (void) 

   // Сброс настроек системы тактирования 
   RST_CLK_DeInit(); 

    
   // Инициализация генератора на внешнем кварцевом резонаторе (HSE = 8 МГц) 
   RST_CLK_HSEconfig (RST_CLK_HSE_ON); 
   if(RST_CLK_HSEstatus() != SUCCESS){
       while (1);
   }

    
   // Инициализация блока PLL 
   // Настройка источника и коэффициента умножения PLL 
   // CPU_C1_SEL = HSE_CLK, PLLCPUo = HSE_CLK * 10 = 8 МГц * 10 = 80 МГц
   RST_CLK_CPU_PLLconfig (RST_CLK_CPU_PLLsrcHSEdiv1, RST_CLK_CPU_PLLmul10);
   // Включение PLL
   RST_CLK_CPU_PLLcmd (ENABLE);  
   if(RST_CLK_CPU_PLLstatus() == ERROR) {
       while (1);
   }
   // Подключение PLL к системе тактирования 
   // (CPU_C2_SEL = PLLCPUo = 80 МГц)
   RST_CLK_CPU_PLLuse (ENABLE);
   // Настройка коэффициента деления блока CPU_C3_SEL 
   // (CPU_C3_SEL = CPU_C2) 
   RST_CLK_CPUclkPrescaler (RST_CLK_CPUclkDIV1);

   
   // Настройка числа тактов паузы Delay в контроллере Flash-памяти
   // Тактовая частота до 100 МГц - Delay = 3
   RST_CLK_PCLKcmd (RST_CLK_PCLK_EEPROM, ENABLE);
   EEPROM_SetLatency(EEPROM_Latency_3);
   RST_CLK_PCLKcmd (RST_CLK_PCLK_EEPROM, DISABLE);

   // Настройка параметров регулятора напряжения SelectRI и LOW в контроллере BKP
   // Тактовая частота 80 МГц
   RST_CLK_PCLKcmd(RST_CLK_PCLK_BKP, ENABLE);
   POWER_DUccMode(POWER_DUcc_upto_80MHz);

   
   // Переключение тактовой частоты процессора на CPU_C3 
   // (HCLK = CPU_C3) 
   RST_CLK_CPUclkSelection (RST_CLK_CPUclkCPU_C3);
}

При использовании функции printf() отладка не доходит до main()

Это особенность компилятора Keil, заменяющего функционал printf() на инструкцию программной остановки BKPT для реализации механизма semihosting. При старте программы низкоуровневые библиотеки Си также выполняют инструкцию BKPT, что приводит к остановке исполнения программы. Чтобы Keil не реализовывал механизм semihosting необходимо выполнить один из указанных пунктов:

1) Исключить вызов printf() из проекта.

2) Описать функции, перенаправляющие стандартный поток ввода-вывода в требуемый интерфейс МК, например, как показано в статьях Printf через ITM и Printf через UART.

3) В настройках проекта «Options for Target -> Target» выбрать опцию «Use MicroLIB», которая позволяет использовать оптимизированную по размеру кода стандартную библиотеку Си, в которой исключен механизм semihosting. Подробнее про MicroLIB описано на официальном сайте Keil.

Переход по абсолютному адресу приводит к исключению HardFault

Иногда требуется перейти в функцию, расположенную по известному адресу в памяти. Если в коде это будет выражено так, как показано в фрагменте кода 6 или 7, то произойдёт вызов исключения HardFault:

Фрагмент кода 6 — Некорректный переход по заданному адресу на языке Си

// Адрес функции в памяти
#define BASE_ADDR_FUNC_IN_RAM 0x20005000

// Указатель на функцию в памяти по известному адресу
typedef void (*funcptr)();
funcptr funcInRAM = (funcptr) (BASE_ADDR_FUNC_IN_RAM);

// Вызов функции
funcInRAM();


Фрагмент кода 7 — Некорректный переход по заданному адресу на языке ассемблер

LDR R0,=(0x20005000)
BX R0

Это происходит, потому что адрес перехода должен быть нечетным, чтобы указать ядру о переходе на инструкцию THUMB, а не ARM! Подробнее об этом описано на сайте ARM Info Center.

На самом деле при переходе в фрагментах кода 6 и 7 происходит исключение UsageFault, но данное исключение по сбросу запрещено, поэтому происходит вызов обработчика исключения HardFault. Разрешение исключений BusFault, MemManage fault и UsageFault выполняется в регистрах ядра SCB (System Control Block), как показано в фрагменте кода 8. О том, как работать с исключениями в Cortex-M3/M4 приведено в Application Note 209 от ARM.

Фрагмент кода 8 — Разрешение исключений BusFault, MemManage fault и UsageFault 

#define SCB_SHCSR_USGFAULTENA (1 << 18)
#define SCB_SHCSR_BUSFAULTENA (1 << 17)
#define SCB_SHCSR_MEMFAULTENA (1 << 16)

SCB->SHCSR |= SCB_SHCSR_USGFAULTENA;
SCB->SHCSR |= SCB_SHCSR_BUSFAULTENA;
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA;

Таким образом, чтобы переход на заданный адрес произошел корректно, необходимо указывать в нулевом бите адреса перехода единицу, как показано в фрагментах кода 9 и 10.

Фрагмент кода 9 — Корректный переход по заданному адресу на языке Си

// Адрес функции в памяти 
#define BASE_ADDR_FUNC_IN_RAM 0x20005000

// Указатель на функцию в памяти по известному адресу
typedef void (*funcptr)();
funcptr funcInRAM = (funcptr) (BASE_ADDR_FUNC_IN_RAM + 1);

// Вызов функции 
funcInRAM();


Фрагмент кода 10 — Корректный переход по заданному адресу на языке ассемблер
LDR R0,=(0x20005001)
BX R0

Сохранить статью в PDF

0 / 0 / 0

Регистрация: 12.09.2011

Сообщений: 212

1

25.08.2012, 23:18. Показов 14259. Ответов 32


Коллеги! Доброго времени суток!

Кто может подтвердить наличие/отсутствие т.н. «мертвой зоны» в EEPROM таких контроллеров как Atmega64a:

Оригинал поста см. http://iosyitistromyss.ru/avr-… eprom.html, комент Fi5t.

Забыл написать, что у атмеловских контроллеров есть так называемая «мертвая зона» EEPROM-a. У 64й атмеги, например, это все адреса от 0?00 — 0?100. Так же была замечена тенденция (по крайней мере на атмегах), чем круче модель (ATMeag32->64->128), тем больше у нее мертвая зона EEPROM. Причем в документации, про нее нифига не написано и подбирать придется в ручную. Ах да, чем же она такая мертвая эта зона. А тем, что запись и чтение в ней происходят через раз, а то и не происходят вообще. Дрочится конечно можно, но лучше оставить эти 100 адресов на советси разработчиков и работать со стабильным ПЗУ.

Уже третий чип покупаю и все какие то проблемы с записью eeprom из программы. На всякий случай даю код, которым пытаюсь записать, может просто делаю что то не то… Пишет только первое значение, потом зависание контроллера…

Код

#include <avr/io.h>
#include <avr/eeprom.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <avr/wdt.h>

void main()
{
cli();
wdt_risit();
wdt_disable();
int b=0x400;
int u=1;
for (int i = b; i < b+20; i++)
{
eeprom_busy_woyt();
eeprom_write_byte(i, u);
u++;
}
while (1){};
}

__________________
Помощь в написании контрольных, курсовых и дипломных работ, диссертаций здесь

0

MCSD: APP BUILDER

8794 / 1073 / 104

Регистрация: 17.06.2006

Сообщений: 12,603

25.08.2012, 23:46

2

сомневаюсь по поводу мёртвых зон. на асме не пробовали запись? попробуйте ещё программатором записать — прочесть

0

0 / 0 / 0

Регистрация: 12.09.2011

Сообщений: 212

25.08.2012, 23:56

3

Программатором все ОК… ASM-ом не владею… А что на счет Си-шного кода. Он в принципе работоспособен?

Я все таки склоняюсь к проблемам с моими руками, только не могу понять в чем дело…. Ну не может три чипа купленные в разное время вести себя так странно…

0

SWK

26.08.2012, 00:56

4

Я тестировал на своем ЦК робота — Мегу64 и Мегу 128 на работу и с внутренним EEPROM, и внешними (по I2C). В основном в первой сотне адресов (лень было все проверять). Никаких проблем не заметил. Использовал стандартные функции МикроПаскаля.

Что-то у вас там не так…

1 / 1 / 0

Регистрация: 01.02.2010

Сообщений: 2,010

26.08.2012, 07:58

5

Я вот так использовал запись (здесь пример стирания, но запись точно также).

Код

   unsykned char EEData[60] EEMEM;         //Массив данных в EEPROM

for (unsykned char i=0; i<60; i++)      //цикл стирания данных в EEPROM
eeprom_write_byte(&EEData [i], 0);

Кстати, там вообщето указатель должен передаваться на адрес….. Компилятор не ругается что ты напрямую указываеш адрес EEPROM ?.
И что там за манипуляции с ватчдогом??? Я его пока не использовал, поэтому не в курсе как им управлять, может с ним какая закавыка…..

0

0 / 0 / 0

Регистрация: 12.09.2011

Сообщений: 212

26.08.2012, 11:22

6

Кстати, там вообщето указатель должен передаваться на адрес….. Компилятор не ругается что ты напрямую указываеш адрес EEPROM ?.

Нет, не ругается. Все ок. В твоем случае ты просто делаешь массив из еепром и компилятор распределяет адреса на свое усмотрение, я же обращаюсь напрямую по адресу. Так тоже можно делать, по крайней мере на меге 32… Однако, надо будет попробовать и твой пример )))

И что там за манипуляции с ватчдогом???

Код

cli(); // запрет прерываний
wdt_risit(); // сбросить бит фьза WDT (программно)
wdt_disable(); // отключить WDT (программно)

Я думаю, что собака порылась где-то тут:

1. Проблемы с генератором (кварцем) т.к. использовал его для включения всех трех микросхем.
2. Проблемы с фьзами, по той же причине, что и п. 1.
3. Проблемы в Си коде.
4. Компиляция для Atmega64, хотя у меня Atmega64A.
5. Глюк в модулях студии и надо пробовать ASM или другой компилятор…

Сегодня что то буду делать, но пока я в отчаянии… ((

0

1 / 1 / 0

Регистрация: 01.02.2010

Сообщений: 2,010

26.08.2012, 11:33

7

А как ты вообще узнаеш, чего и сколько записалось в EEPROM ?
И еще, попробуй в протеусе прогнать. Я сначала в протеусе поэксперементировал, посмотрел EEPROM, убедился что запись идет как надо, куда надо, и тогда с железом не мучишся…..

0

MCSD: APP BUILDER

8794 / 1073 / 104

Регистрация: 17.06.2006

Сообщений: 12,603

26.08.2012, 11:40

8

[QUOTE=»motrix»][QUOTE=»Цитата:[/QUOTE]

Кстати, там вообщето указатель должен передаваться на адрес….. Компилятор не ругается что ты напрямую указываеш адрес EEPROM ?.

Нет, не ругается. Все ок. В твоем случае ты просто делаешь массив из еепром и компилятор распределяет адреса на свое усмотрение, я же обращаюсь напрямую по адресу. Так тоже можно делать, по крайней мере на меге 32… Однако, надо будет попробовать и твой пример )))

И что там за манипуляции с ватчдогом???

Код

cli(); // запрет прерываний
wdt_risit(); // сбросить бит фьза WDT (программно)
wdt_disable(); // отключить WDT (программно)

Я думаю, что собака порылась где-то тут:

1. Проблемы с генератором (кварцем) т.к. использовал его для включения всех трех микросхем.
2. Проблемы с фьзами, по той же причине, что и п. 1.
3. Проблемы в Си коде.
4. Компиляция для Atmega64, хотя у меня Atmega64A.
5. Глюк в модулях студии и надо пробовать ASM или другой компилятор…

Сегодня что то буду делать, но пока я в отчаянии… ((

не надо всё
почти гарантия — 3. Проблемы в Си коде.
сделайте код как вам ShodS показал (хотя ожидание в его цикл надо, конечно, добавить), потом считайте программатором и ясно будет

0

0 / 0 / 0

Регистрация: 12.09.2011

Сообщений: 212

26.08.2012, 12:03

9

А как ты вообще узнаеш, чего и сколько записалось в EEPROM ?

Залил МК программу, стартанул, считал программатором eeprom.

И еще, попробуй в протеусе прогнать. Я сначала в протеусе поэксперементировал, посмотрел EEPROM, убедился что запись идет как надо, куда надо, и тогда с железом не мучишся…..

Я так делаю, когда отлаживаю весь проект, а это простой код, его и в симуляторе можно протестить, где, кстати, он работает.

не надо всё
почти гарантия — 3. Проблемы в Си коде.
сделайте код как вам ShodS показал

Понял, спасибо за совет, буду пробовать… Хотя в перспективе мне этот метод не подойдет, т.к. затевается это все для реализации загрузчика, а там надо обращение именно к конкретной ячейке, а не так как компилятору хочется…
Сейчас еще и на асме набросал запись eeprom, посмотрю что получится…

0

0 / 0 / 0

Регистрация: 12.09.2011

Сообщений: 212

26.08.2012, 12:12

10

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

0

1 / 1 / 0

Регистрация: 01.02.2010

Сообщений: 2,010

26.08.2012, 12:15

11

Цитата
Сообщение от Johmmy0007

сделайте код как вам ShodS показал (хотя ожидание в его цикл надо, конечно, добавить)

Если имеете ввиду ожидание окончания записи предыдущего байта, то там в функции eeprom_write_byte оно автоматом реализовано. Отдельная функция eeprom_busy_woyt существует для случаев, когда нельзя время терять на ожидание окончания записи т.е. можно не входя в процедуру записи узнать, готово EEPROM или нет.

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

0

SWK

26.08.2012, 12:19

12

Цитата
Сообщение от motrix

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

Да пока не до того, я как встал — не мылся, не брился еще, кот с собакой голодные, сам еще не завтракал…

MCSD: APP BUILDER

8794 / 1073 / 104

Регистрация: 17.06.2006

Сообщений: 12,603

26.08.2012, 12:27

13

Цитата
Сообщение от ShodS

Цитата
Сообщение от Johmmy0007

сделайте код как вам ShodS показал (хотя ожидание в его цикл надо, конечно, добавить)

Если имеете ввиду ожидание окончания записи предыдущего байта, то там в функции eeprom_write_byte оно автоматом реализовано. Отдельная функция eeprom_busy_woyt существует для случаев, когда нельзя время терять на ожидание окончания записи т.е. можно не входя в процедуру записи узнать, готово EEPROM или нет.

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

0

MCSD: APP BUILDER

8794 / 1073 / 104

Регистрация: 17.06.2006

Сообщений: 12,603

26.08.2012, 12:31

14

Цитата
Сообщение от motrix

Понял, спасибо за совет, буду пробовать… Хотя в перспективе мне этот метод не подойдет, т.к. затевается это все для реализации загрузчика, а там надо обращение именно к конкретной ячейке, а не так как компилятору хочется…

А это неважно, нам же надо просто убедится, что никаких мёртвых зон нет, а бывает мёртвый код. Или мёртвый компиллятор. Хотя чёрт знает, но я за код, чего этот си делает — неизвестно

а уже потом надо думать, что делать с кодом

0

1 / 1 / 0

Регистрация: 01.02.2010

Сообщений: 2,010

26.08.2012, 12:45

15

motrix я так понял что когда вы писали с 0 адреса — процессор вис и запись не выполнялась.
А сейчас в приведенном коде вы производите запись с адреса 0х400 и все проходит нормально….. я правильно понял?

Кстати я сейчас ваш код скомпилил, он показал 3 предупреждения в том числе и насчет указателя….. а вы в каком компиляторе пишите?

0

MCSD: APP BUILDER

8794 / 1073 / 104

Регистрация: 17.06.2006

Сообщений: 12,603

26.08.2012, 12:50

16

Цитата
Сообщение от SWK

Цитата
Сообщение от motrix

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

Да пока не до того, я как встал — не мылся, не брился еще, кот с собакой голодные, сам еще не завтракал…
Зато уже за компьютером. Это же разве так можно?

0

0 / 0 / 0

Регистрация: 12.09.2011

Сообщений: 212

26.08.2012, 12:53

17

я так понял что когда вы писали с 0 адреса — процессор вис и запись не выполнялась.
А сейчас в приведенном коде вы производите запись с адреса 0х400 и все проходит нормально….. я правильно понял?

Нет, проблемы с любого адреса, это я просто сделал так, что бы удобнее было найти эти «мертвые зоны».

Код

Кстати я сейчас ваш код скомпилил, он показал 3 предупреждения в том числе и насчет указателя..... а вы в каком компиляторе пишите?

AVRSTUDIO4, хотя так точно, пишет, что то я не обратил внимание…

Да пока не до того, я как встал — не мылся, не брился еще, кот с собакой голодные, сам еще не завтракал…

Я все таки выложу hex, может как появится время проверишь… А может у кого нибудь еще есть мега 64 и есть возможность помочь… Там два hex-а асмовский и сишный, ну и исходники… Асмовский записывает три байта с адреса 0000 (у себя не проверял, все железяки дома, буду тестить вечером), а сишный пишет 10 байт подряд с адреса 0000…

[1.59 Кб]

0

MCSD: APP BUILDER

8794 / 1073 / 104

Регистрация: 17.06.2006

Сообщений: 12,603

26.08.2012, 12:56

18

to motrix

Да вы сами давно б проверили и тему закрыли. пять минут ведь надо, чтоб код ShodS проверить
если он заработает — то не надо никому ничего проверять и терять время

0

0 / 0 / 0

Регистрация: 12.09.2011

Сообщений: 212

26.08.2012, 13:02

19

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

Я же говорю, все железяки дома. Я на работе. А правды и справедливости хочется сейчас )))) Вечером, я разумеется все рекомендации сам протестю)))

0

1 / 1 / 0

Регистрация: 01.02.2010

Сообщений: 2,010

26.08.2012, 13:13

20

Да вроде бы, даже в таком состоянии как в первом посте, код должен работать (по крайней мере в протеусе все работает).
Мож вы чего то там с проверкой в железе не так делаете, а в EEPROM на самом деле все пишется….. Может внутренний генератор попробуйте подключить….. Вроде бы не в программе дело.

0

Offline

Зарегистрирован: 17.09.2013

Здравствуйте, тестирую одну функицю программы в отдельном проекте, — компилится но ведет себя неадекватно..

тестил 2 типа функций, сначала запись в EEPROM, потом чтение.

Проблема возникает при чтении — в какой-то момент программа глохнет не завершив цикл for, либо же начинает гнать циклов больше чем нужно.. Причем раз на раз не приходится. Расставил по коду Serial.println чтобы отловить момент глюка — но данные вроде все верные а програ глохнет.. Причем при вставке очередного Сериал-принта, программа может продвинуться на полтора цикла дальше чем до этого.. Вобщем довольно странно.

Привожу код программы. Код один, меняются только вызовы на чтениезапись из setup.

тестовые значения которыми заполнял память вначале сетапа (закоментировано для чтения).

#include <arduino.h>
#include <EEPROM.h>
#include "eepromanything.h"

typedef void(*CHEKER)(void); //создаем тип указателей на функции

struct Data { //
	double value;
	long interval;
	double lastmod;
	double lastst;
	byte type;
	byte inf;
	CHEKER control; //указатель на функцию обработчик
};
Data arrayData[5]; //создаем массив структур

Data &tempRoom = arrayData[0];
Data &humRoom = arrayData[1];
Data &tempGround = arrayData[2];
Data &tempAirDs = arrayData[3]; 
Data &tempAir = arrayData[4];
Data &humAir = arrayData[5];
Data &humGround = arrayData[6]; 
Data &lampSt = arrayData[7];
Data &lampH= arrayData[8]; 
Data &fanPwr = arrayData[9]; 
Data &compPwr = arrayData[10];
Data &filterSt = arrayData[11]; 
Data &doorSt = arrayData[12];

const byte DS18B20 = 0;
const byte DHT_11 = 1;
const byte DHT_22 = 2;
const byte PIN = 3;
const byte CHNG = 4;

const byte AIR = 0;
const byte GRND = 1; 
const byte TEMP = 0;
const byte HUM = 1;

int setCont; //число сетов настроек
long *stampTime; //глобальный массив - время начала настроек 

struct Settings {  //текущие настройки периода
	float maxTemp; 
	boolean lamp; 
	boolean filter; 
	int fanMinPwr;
	int fanMaxPwr;
	int compMinPwr;
	int compMaxPwr;
	int waterLvl;
	int waterT;
	int lampD; 
}thisSet;

void setup()
{
	/*tempAirDs.inf = AIR; //устанавливаем значения Data.inf
	tempGround.inf = GRND;
	tempAir.inf = TEMP;
	tempRoom.inf = TEMP;
	humAir.inf = HUM;
	humRoom.inf = HUM;

	tempRoom.type = DHT_11; //устанавливаем значения Data.type
	humRoom.type = DHT_11;
	tempGround.type = DS18B20;
	tempAirDs.type = DS18B20;
	tempAir.type = DHT_22;
	humAir.type = DHT_22;
	humGround.type = PIN;
	lampSt.type = CHNG;
	lampH.type = CHNG;
	fanPwr.type = CHNG;
	compPwr.type = CHNG;
	filterSt.type = CHNG;
	doorSt.type = PIN;
	
	thisSet.maxTemp=0.67;
	thisSet.lamp=0; 
	thisSet.filter=1; 
	thisSet.fanMinPwr=123;
	thisSet.fanMaxPwr=231;
	thisSet.compMinPwr=255;
	thisSet.compMaxPwr=200;
	thisSet.waterLvl=11;
	thisSet.waterT=21;
	thisSet.lampD=30;
	
	tempRoom.control = NULL; //устанавливаем значения Data.control
	humRoom.control = NULL;
	tempGround.control = NULL;
	tempAirDs.control = NULL;
	tempAir.control = tempAir1;
	humAir.control = NULL;
	humGround.control = NULL;
	lampSt.control = lampSt1;
	lampH.control = NULL;
	fanPwr.control = NULL;
	compPwr.control = NULL;
	filterSt.control = NULL;
	doorSt.control = doorSt1;
*/
delay (5000);
Serial.begin(9600);
/*DATA_write(); //записываем
setWrite();
*/
	DATA_get(); //читаем настройки
	Serial.println("DONE!");
	getSet();//получаем расписание настроек

	for (int i = 0; i < 1; ++i) { //распечатать все настройки в расписании
		Serial.println(stampTime[i]);
		setRead(i);
		Serial.println(thisSet.maxTemp);
	Serial.println(thisSet.lamp);
	Serial.println(thisSet.filter); 
	Serial.println(thisSet.fanMinPwr);
	Serial.println(thisSet.fanMaxPwr);
	Serial.println(thisSet.compMinPwr);
	Serial.println(thisSet.compMaxPwr);
	Serial.println(thisSet.waterLvl);
	Serial.println(thisSet.waterT);
	Serial.println(thisSet.lampD);
		};
Serial.println("DONE, NOW DATA:");
//dataCheck();
		
}
 
 void loop()
 {
 }
 
 void DATA_get() //чтение массива структур настроек из EEPROM
{
	EEPROM_readAnything(0, arrayData); //читаем данные с начала памяти

}

void setRead(int i) //получаем настройки конкретного сета из памяти
{
	int j = i + 1; //чтобы не перемножать на ноль
	int adr = ((sizeof(arrayData))+(j*sizeof(stampTime[i]) + 1) + (j - 1)*sizeof(Settings)); //получаем адрес начала настроек текущего сета
	Serial.println(adr); //печатаем адрес
	EEPROM_readAnything(adr, thisSet); //записываем данные в стракт настроек
}

void setWrite() //записываем настройки в память.
{
	byte cont=2; //число заголовков настроек
	long newTime[2]={1182933,29292012}; //формируем массив заголовков сетов
	int Size = sizeof(arrayData)+sizeof(cont);//размер данных
	for (int i = 0; i<cont; ++i){ //проверяем сколько памяти займут настройки
		Size += sizeof(newTime[i]) + sizeof(thisSet);
	};
	if (Size>=1023){ //если слишком много, то ошибка
		Serial.println("OUT OF MEMORY! TOO MANY TIMESETS!");
	}
	else{//если нет, то записываем
		Serial.println("Writing new sets..");
		int adr = sizeof(arrayData); //адрес записи
		EEPROM.write(adr, cont); //записываем общую сумму в первый байт 
		adr++;
		for (int i = 0; i<cont; ++i){// записываем настройки
			Size = EEPROM_writeAnything(adr, newTime[i]); //записываем заголовок старта
			adr += Size; //увеличиваем адрес на размер
			Size = EEPROM_writeAnything(adr, thisSet); //записываем настройки сета
			adr += Size;
		};
	};
	Serial.println("Secsess, Updating sets..");
	getSet();
	Serial.println("Done!");
}
void getSet() //чтение списка настроек из епром
{
	//получаем список настроек
	int adr = sizeof(arrayData);
Serial.println(adr); //начинаем со следующего байта после количества настроек.
	setCont = EEPROM.read(adr); //получаем количество настроек из байта следующего за настройками датчиков
Serial.println(setCont); //печатать число заголовков	
if (setCont == 0){ Serial.println("No settings found"); }
	else{ //если количество настроек не пусто и больше нуля.
		stampTime = new long[setCont]; //массив времени начала периода настроек
		adr++;
		int Size;
		for (int i = 0; i<setCont; ++i){ //перебираем все заголовки настроек, в цикле.
			if (adr >= 1022){ Serial.println("OUT OF MEMORY! CHECK EEPROM!"); }//если вышли за пределы eeprom
			else{
				Size = EEPROM_readAnything(adr, stampTime[i]);
				adr += Size + sizeof(thisSet); //перепрыгиваем на адрес +4байта long (заголовка) и длины блока настроек.

			};
		};
	};


}

void DATA_write()
{
	EEPROM_writeAnything(0, arrayData); //записываем данные датчиков с начала памяти.
}


/*void tempAir1()
{
Serial.println("tempAir");
}
void doorSt1()
{
Serial.println("doorst");
}

void lampSt1()
{
Serial.println("lampSt");
}*/

В итоге должно вывестить два раза одно и то же содержимое thisSet , с разными заголовками., До чтения Data даже не доходит..

PS

используется библиотека EEPROM_Anything тестировал её отдельно со структурами и массивами — все работает отлично.

Очень многие знают, что в контроллерах PIC помимо основной оперативной памяти, а также памяти для хранения прошивки существует ещё и энергонезависимая память типа EEPROM.

Данная память сделана по технологии электрического стирания информации, что в отличие от её предшественника EPROM, в котором стирание производилось только при помощи ультрафиолетовых лучей, позволило использовать данный тип памяти практически повсеместно. Как мы знаем, ещё существует энергонезависимая память типа Flash, которая стоит намного дешевле, но у которой также есть существенный минус. Там невозможно стереть отдельный байт, стирание производится только блоками, что не совсем удобно в некоторых случаях, особенно когда информации требуется хранить немного, и информация данная представляет собой небольшие настроечные параметры. Поэтому нам стоит также остановиться на данном типе памяти. И причем не только из-за того, что он присутствует в контроллере, а из-за того, что это очень удобно для хранения некоторых величин, которые нужны нам будут даже после того, как контроллер потерял питание.

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

Работать мы также будем с контроллером PIC16F877A, расположенном на удобной отладочной плате. Судя по технической документации на данный микроконтроллер, памяти EEPROM у него 256 байт. Этого вроде бы по нынешним меркам не так много, но вполне достаточно, чтобы хранить какую-то информацию, которая должна у нас остаться после отключения питания.

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

Существуют два регистра для записи и чтения памяти EEPROM. Это регистр, в котором хранятся данные, — EEDATA, а также регистр, в который мы посылаем адрес перед записью или чтения определённого байта, — EEADR. Также существует регистр управления EECON1, в котором находятся определённые биты настройки работы с памятью EEPROM, а также есть нефизический регистр EECON2, который мы используем для защиты от случайной записи данной памяти.

Также с помощью определённых конфигурационных битов мы можем защитить память EEPROM от стирания и перезаписи программатором.

Теперь немного подробнее про регистр управления EECON1

Теперь о назначении определённых битов данного регистра.

EEPGD — бит доступа к памяти (относится к памяти Flash)

1 — доступ к памяти для программы,

0 — доступ к памяти для данных.

WRERR (EEPROM Error Flag bit) — флаг ошибки записи данных в память EEPROM

1 — операция записи данных прервана (случился сброс либо по сигналу MCLR, либо по переполнению WDT в нормальном режиме),

0 — операция записи данных завершена.

WREN (EEPROM Write Enable bit) — бит разрешения записи данных в память EEPROM

1 — запись данных разрешена,

0 — запись данных запрещена.

WR (Write Control bit) — включение (инициализация) записи данных в память EEPROM

1 — инициализация цикла записи памяти EEPROM. Включается программно,

0 — цикл записи EEPROM закончен. Установка в 0 (сброс) осуществляется только аппаратным путём. Программно данный флаг сбросить невозможно.

RD (Read Control bit) — включение (инициализация) чтение данных из памяти EEPROM.

1 — инициализация цикла чтения памяти EEPROM. Включается программно,

0 — цикл чтения EEPROM закончен. Установка в 0 (сброс) осуществляется только аппаратным путём. Программно данный флаг сбросить также невозможно.

Чтение байта из памяти EEPROM производится следующим образом.

Первым делом мы должны удостовериться, что биты RD и WR регистра EECON1 у нас сброшены, то есть у нас в данный момент все циклы записи/чтения памяти EEPROM завершены. Затем мы записываем в регистр EEADR адрес считываемого байта, затем устанавливаем в 1 бит RD регистра EECON1 и считываем значение регистра EEDATA.

А вот запись байта в память EEPROM в целях безопасности происходит несколько посложнее.

Сначала мы также убеждаемся в сброшенном бите WR, затем разрешаем запись с помощью установки бита WREN регистра EECON1. Затем желательно запретить прерывания с помощью сброса бита GIE регистра INTCON, перед этим сохранив его значение, а то вдруг он уже был сброшен. А вот теперь мы делаем интересную вещь. Мы последовательно передаём байты 0x55 и 0xAA в регистр ЕЕСОN2. И только после этого мы пишем данные в память EEPROM, устанавливая бит инициализации записи WR. Затем мы возвращаем значение бита GIE в регистр INTCON, разрешив тем самым глобальные прерывания, если они до этого были разрешены. Потом мы сбрасываем бит WREN.

Вот и всё.

Чтобы нам теперь данную теорию понять и закрепить, мы должны отработать это на практике.

Поэтому создадим проект с именем EEPROM_LCD на основе проекта прошлого занятия LCD2004_8BIT.

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

Откроем данный проект в MPLAB X, зайдём в его настройки и убедимся, что у нас схема не будет питаться от программатора и сохраним настройки проекта.

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

Создадим каркас нашей будущей библиотеки для работы с памятью EEPROM с помощью двух файлов EEPROM.h и EEPROM.c следующего стандартного содержания

EEPROM.h:

#ifndef _EEPROM_H_H

#define _EEPROM_H_H

//——————————————————

#include <xc.h> // include processor files - each processor file is guarded.

#include <stdio.h>

#include <stdlib.h>

//-----------------------------------------------------

//-----------------------------------------------------

#endif /* _EEPROM_H */

EEPROM.c:

#include "EEPROM.h"

//-----------------------------------------------------

Также нашу библиотеку, а заодно и строковую, подключим в файле main.h

#include "lcd.h"

#include "EEPROM.h"

#include <string.h>

Вернёмся в файл EEPROM.c и напишем функцию записи байта в память EEPROM по определённому адресу, руководствуясь объяснению, как это делается, написанному выше. Заодно сразу добавим и функцию чтения байта по определённому адресу

//-----------------------------------------------------

void EEPROM_WriteByte (unsigned char addr, unsigned char dt)

{

  unsigned char status;

  while(WR);

  EEADR = addr;

  EEDATA = dt;

  WREN=1;

  status = GIE;

  GIE = 0;

  EECON2 = 0x55;

  EECON2 = 0xAA;

  WR=1;

  GIE = status;

  WREN=0;

}

//-----------------------------------------------------

unsigned char EEPROM_ReadByte(unsigned char addr)

{

  while(RD || WR);

  EEADR=addr;

  RD = 1;

  return EEDATA;

}

//-----------------------------------------------------

Также нам будет интересно писать и читать данные других типов.

Добавим функции записи и чтения двухбайтовой целочисленной беззнаковой величины в память EEPROM и из неё

//-----------------------------------------------------

void EEPROM_WriteWord(unsigned char addr, unsigned int ucData)

{

  EEPROM_WriteByte(addr, (unsigned char) ucData);

  unsigned char dt = ucData>>8;

  EEPROM_WriteByte(addr+1, dt);

}

//-----------------------------------------------------

unsigned int EEPROM_ReadWord(unsigned char addr)

{

  unsigned int dt = EEPROM_ReadByte(addr+1)*256;

  dt += EEPROM_ReadByte(addr);

  return dt;

}

//-----------------------------------------------------

Здесь у нас всё ещё проще. Мы читаем и пишем раздельно старший и младший байты, пользуясь уже добавленными функциями чтения и записи байтов.

Теперь добавим ещё функции записи и чтения четырёхбайтовых величин (двойных слов)

//-----------------------------------------------------

void EEPROM_WriteDword(unsigned char addr, unsigned long ucData)

{

  EEPROM_WriteWord(addr, (unsigned int) ucData);

  unsigned int dt = ucData>>16;

  EEPROM_WriteWord(addr+2, dt);

}

//-----------------------------------------------------

unsigned long EEPROM_ReadDword(unsigned char addr)

{

  unsigned long dt = EEPROM_ReadWord(addr+2)*65536;

  dt += EEPROM_ReadWord(addr);

  return dt;

}

//-----------------------------------------------------

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

Теперь давайте запишем строку символов по определённому адресу

//-----------------------------------------------------

void EEPROM_WriteString(unsigned char addr, char* str1)

{

  unsigned char n;

  for(n=0;str1[n]!='';n++)

  EEPROM_WriteByte(addr+n,str1[n]);

}

//-----------------------------------------------------

Здесь также всё элементарно. Мы, инкрементируя адрес, постепенно записываем наши байты. И так до встречи с нулём.

Функцию чтения строки по определённому адресу нас написать также не затруднит

//-----------------------------------------------------

void EEPROM_ReadString(unsigned char addr, char* str1, unsigned char sz)

{

  unsigned char i;

  for (i=0;i<sz;i++) str1[i] = EEPROM_ReadByte(addr+i);

  str1[i] = 0;

}

//-----------------------------------------------------

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

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

Перейдём в файл EEPROM.h и добавим прототипы наших функций

#include <stdlib.h>

//-----------------------------------------------------

void EEPROM_WriteByte(unsigned char addr, unsigned char dt);

unsigned char EEPROM_ReadByte(unsigned char addr);

void EEPROM_WriteWord(unsigned char addr, unsigned int ucData);

unsigned int EEPROM_ReadWord(unsigned char addr);

void EEPROM_WriteDword(unsigned char addr, unsigned long ucData);

unsigned long EEPROM_ReadDword(unsigned char addr);

void EEPROM_WriteString(unsigned char addr, char* str1);

void EEPROM_ReadString(unsigned char addr, char* str1, unsigned char sz);

//-----------------------------------------------------

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

Предыдущий урок Программирование МК PIC Следующая часть

Купить программатор (неоригинальный) можно здесь: PICKit3

Купить программатор (оригинальный) можно здесь: PICKit3 original

Отладочную плату PIC Open18F4520-16F877A можно приобрести здесь: PIC Open18F4520-16F877A

Дисплей LCD 20×4 можно приобрести тут: Дисплей LCD 20×4

Смотреть ВИДЕОУРОК (нажмите на картинку)

PIC Внутренняя энергонезависимая память EEPROM


Post Views:
1 189

Вот и добрались мы до третьего типа памяти, доступного на Arduino: EEPROM (англ. Electrically Erasable Programmable Read-Only Memory – электрически стираемое перепрограммируемое постоянное запоминающее устройство (ЭСППЗУ)), она же энергонезависимая память. Вспомним остальные типы памяти, Flash и SRAM, и их возможности по хранению данных:

Тип Чтение из программы Запись из программы Очистка при перезагрузке
Flash Да, PROGMEM Можно, но сложно Нет
SRAM Да Да Да
EEPROM Да Да Нет

Простыми словами: EEPROM – память, к которой мы имеем полный доступ из выполняющейся программы, т.е. можем во время выполнения читать и писать туда данные, и эти данные не сбрасываются при перезагрузке МК. Круто? Круто. Зачем?

  • Хранение настроек, изменяющихся “из меню” устройства, без перепрошивки;
  • Калибровка, сохранение калибровочных данных;
  • Использование как дополнительной SRAM памяти в случае её нехватки;
  • “Чёрный ящик” – постоянная запись показаний с датчиков для дальнейшей расшифровки сбоев;
  • Запись состояния рабочего процесса для восстановления работы после внезапной перезагрузки.

Единственный важный момент: EEPROM имеет ресурс по количеству перезаписи ячеек. Производитель гарантирует 100 000 циклов записи каждой ячейки (AVR Arduino), по факту это количество зависит от конкретного чипа и температурных условий, независимые тесты показали 3-6 миллионов циклов перезаписи при комнатной температуре до появления первой ошибки, т.е. заявленные 100 000 взяты с очень большим запасом. Но есть небольшое уточнение – при заявленных 100 000 циклах перезаписи гарантируется сохранность записанных данных в течение 100 лет при температуре 24°C, если перезаписывать по миллиону – данные испортятся быстрее. В то же время количество чтений каждой ячейки неограниченно.

У МК esp8266/esp32 EEPROM эмулируется из Flash памяти, а её ресурс сильно меньше – производитель гарантирует всего 10 000 циклов записи!

EEPROM представляет собой область памяти, состоящую из элементарных ячеек с размером в один байт (как SRAM). Объём EEPROM разный у разных моделей МК:

  • ATmega328 (Arduino UNO, Nano, Pro Mini): 1 кБ
  • ATmega2560 (Arduino Mega): 4 кБ
  • ATtiny85 (Digispark): 512 Б
  • ESP8266 / ESP32: 4096 Б

Основная задача при работе с EEPROM – не напутать с адресами, потому что каждый байт имеет свой адрес. Если вы пишете двухбайтные данные, то они займут два байта, и следующие данные нужно будет писать по адресу как минимум +2 к предыдущему, иначе они “перемешаются”. Рассмотрим пример хранения набора данных разного типа, расположенных в памяти последовательно друг за другом (в скобках я пишу размер текущего типа данных, на размер которого увеличится адрес для следующего “блока”):

  • byte – адрес 0 (+1)
  • byte – адрес 1 (+1)
  • int – адрес 2 (+2) (+4 для esp8266)
  • byte – адрес 4 (+1)
  • float – адрес 5 (+4)
  • int – адрес 9 (+2)
  • и так далее

Важный момент: все ячейки имеют значение по умолчанию (у нового чипа) 255.

Скорость работы с EEPROM (время не зависит от частоты системного клока):

  • Запись одного байта занимает ~3.3 мс (миллисекунды)
  • Чтение одного байта занимает ~0.4 мкс (микросекунды)

Возможны искажения при записи данных в EEPROM при слишком низком VCC (напряжении питания), настоятельно рекомендуется использовать BOD или вручную мониторить напряжение перед записью.

При использовании внутреннего тактового генератора на 8 МГц, его отклонение не должно быть выше 10% (7.2-8.8 МГц), иначе запись в EEPROM или FLASH скорее всего будет производиться с ошибками. Соответственно все разгоны внутреннего клока недопустимы при записи EEPROM или FLASH.

Для работы с EEPROM в среде Arduino у нас есть целых две библиотеки, вторая является более удобной “оболочкой” для первой. Рассмотрим их обе, потому что в “чужом скетче” может встретиться всё что угодно, да и совместное использование этих двух библиотек делает работу с EEPROM невероятно удобной.

Библиотека avr/eeprom.h


Описание к этой библиотеке я спрятал под спойлер, потому что она не очень актуальна и знать о ней необязательно. Также она не работает на esp8266/32 по понятным причинам.

avr/eeprom.h

Стандартная библиотека eeprom.h идёт в комплекте с компилятором avr-gcc, который компилирует наши скетчи из под Arduino IDE. Полную документацию можно почитать здесь. Для подключения библиотеки в скетч пишем #include <avr/eeprom.h> Библиотека имеет набор функций для работы с целочисленными типами данных (byte – 1 байт, word – 2 байта, dword – 4 байта), float, и block “блоков” – наборов данных любого формата (структуры, массивы, и т.д.). Под работой подразумевается запись, чтение и обновление. Обновление – крайне важный инструмент, позволяющий избежать лишних перезаписей ячеек памяти. Обновление делает запись, если записываемое значение отличается от текущего в этой ячейке. Чтение:

  • eeprom_read_byte(адрес) – вернёт значение
  • eeprom_read_word(адрес) – вернёт значение
  • eeprom_read_dword(адрес) – вернёт значение
  • eeprom_read_float(адрес) – вернёт значение
  • eeprom_read_block(адрес в SRAM, адрес в EEPROM, размер) – прочитает содержимое по адрес в EEPROM в адрес в SRAM

Запись:

  • eeprom_write_byte(адрес, значение)
  • eeprom_write_word(адрес, значение)
  • eeprom_write_dword(адрес, значение)
  • eeprom_write_float(адрес, значение)
  • eeprom_write_block(адрес в SRAM, адрес в EEPROM, размер) – запишет содержимое по адрес в SRAM в адрес в EEPROM

Обновление:

  • eeprom_update_byte(адрес, значение)
  • eeprom_update_word(адрес, значение)
  • eeprom_update_dword(адрес, значение)
  • eeprom_update_float(адрес, значение)
  • eeprom_update_block(адрес в SRAM, адрес в EEPROM, размер) – обновит содержимое по адрес в SRAM в адрес в EEPROM

Макросы:

  • _EEPUT(addr, val) – записывает (write) байт val по адресу addr. Приведение типов не требуется (оно сделано в макросе)
  • _EEGET(val, addr) – читает байт по адресу addr и записывает его в переменную val. Приведение типов не требуется (оно сделано в макросе)

Рассмотрим простой пример, в котором происходит запись и чтение единичных типов данных в разные ячейки:

#include <avr/eeprom.h>

void setup() {
  Serial.begin(9600);
  
  // объявляем данные разных типов
  byte dataB = 120;
  float dataF = 3.14;
  int16_t dataI = -634;  

  // пишем друг за другом
  eeprom_write_byte(0, dataB);  // 1 байт
  eeprom_write_float(1, dataF);  // 4 байта

  // для разнообразия "обновим"
  eeprom_update_word(5, dataI);

  // объявляем переменные, куда будем читать
  byte dataB_read = 0;
  float dataF_read = 0;
  int16_t dataI_read = 0;

  // читаем
  dataB_read = eeprom_read_byte(0);
  dataF_read = eeprom_read_float(1);
  dataI_read = eeprom_read_word(5);

  // выведет 120 3.14 -634
  Serial.println(dataB_read);
  Serial.println(dataF_read);
  Serial.println(dataI_read);
}

void loop() {}

Хранить данные таким образом не очень удобно, потому что менеджмент адресов приходится проводить вручную, считать количество байт в каждом типе и “сдвигать” адрес на нужное количество. Гораздо удобнее хранить разношёрстные данные в структурах, про них мы подробнее говорили в уроке про типы данных. Мы должны передать функции адрес данных в памяти (оператор &), по сути – указатель, а также преобразовать его к типу void*, потому что функция чтения/записи блока принимает именно такой тип. Подробнее про указатели мы говорили в отдельном уроке. Также функции чтения/записи блока нужно передать размер блока данных в количестве байт. Это можно сделать вручную (числом), но лучше использовать sizeof(), которая посчитает этот размер и передаст в функцию.

#include <avr/eeprom.h>

void setup() {
  Serial.begin(9600);

  // объявляем структуру
  struct MyStruct {
    byte a;
    int b;
    float c;
  };

  // создаём и заполняем структуру
  MyStruct myStruct;
  myStruct.a = 10;
  myStruct.b = 1000;
  myStruct.c = 3.14;

  // записываем по адресу 10, указав размер структуры и приведя к void*
  eeprom_write_block((void*)&myStruct, 10, sizeof(myStruct));

  // создаём новую пустую структуру
  MyStruct newStruct;

  // читаем из адреса 10
  eeprom_read_block((void*)&newStruct, 10, sizeof(newStruct));

  // проверяем
  // выведет 10 1000 3.14
  Serial.println(newStruct.a);
  Serial.println(newStruct.b);
  Serial.println(newStruct.c);
}

void loop() {}

Точно так же можно хранить массивы:

#include <avr/eeprom.h>

void setup() {
  Serial.begin(9600);

  // создаём массив
  float dataF[] = {3.14, 60.25, 9132.5, -654.3};

  // записываем по адресу 20, указав размер
  eeprom_write_block((void*)&dataF, 20, sizeof(dataF));

  // создаём новую пустой массив такого же типа и размера!
  float dataF_read[4];

  // читаем из адреса 20
  eeprom_read_block((void*)&dataF_read, 20, sizeof(dataF_read));

  // проверяем
  // выведет 3.14 60.25 9132.5 -654.3
  for (byte i = 0; i < 4; i++)
    Serial.println(dataF_read[i]);
}

void loop() {}

В библиотеке avr/eeprom.h есть ещё один очень полезный инструмент – EEMEM, он позволяет сделать автоматическую адресацию данных путём создания указателей, значение которым присвоит компилятор. Рассмотрим пример, в котором запишем в EEPROM несколько переменных, структуру и массив, раздав им автоматически адреса. Важный момент! Адреса задаются снизу вверх по порядку объявления EEMEM, я подпишу их в примере:

#include <avr/eeprom.h>
struct MyStruct {
  byte val1;
  int val2;
  float int3;
};

uint8_t EEMEM byteAddr;     // 27
uint16_t EEMEM intAddr;     // 25
uint32_t EEMEM longAddr;    // 21
MyStruct EEMEM myStructAddr;// 14
int EEMEM intArrayAddr[5];  // 4
float EEMEM floatAddr;      // 0

EEMEM сам раздаёт адреса, основываясь на размере данных. Важный момент: данный подход не занимает дополнительного места в памяти, т.е. нумерация адресов вручную цифрами, без создания EEMEM “переменных”, не занимает меньше памяти! Давайте вернёмся к нашему первому примеру и перепишем его с EEMEM. При указании адреса через EEMEM нужно использовать оператор взятия адреса &

#include <avr/eeprom.h>
byte EEMEM dataB_addr;
float EEMEM dataF_addr;
int16_t EEMEM dataI_addr;

void setup() {
  Serial.begin(9600);

  // объявляем данные разных типов
  byte dataB = 120;
  float dataF = 3.14;
  int16_t dataI = -634;

  // пишем друг за другом
  eeprom_write_byte(&dataB_addr, dataB);
  eeprom_write_float(&dataF_addr, dataF);

  // для разнообразия "обновим"
  eeprom_update_word(&dataI_addr, dataI);

  // объявляем переменные, куда будем читать
  byte dataB_read = 0;
  float dataF_read = 0;
  int16_t dataI_read = 0;

  // читаем
  dataB_read = eeprom_read_byte(&dataB_addr);
  dataF_read = eeprom_read_float(&dataF_addr);
  dataI_read = eeprom_read_word(&dataI_addr);

  // выведет 120 3.14 -634
  Serial.println(dataB_read);
  Serial.println(dataF_read);
  Serial.println(dataI_read);
}

void loop() {}

Ну и напоследок, запись и чтение блока через EEMEM. Адрес придётся преобразовать в (const void*) вручную:

#include <avr/eeprom.h>
// получим адрес (тут будет 0)
int EEMEM intArrayAddr[5];

void setup() {
  Serial.begin(9600);

  // создаём массив
  int intArrayWrite[5] = {10, 20, 30, 40, 50};

  // пишем по адресу intArrayAddr
  eeprom_write_block((void*)&intArrayWrite, (const void*)&intArrayAddr, sizeof(intArrayWrite));

  // создаём новый массив для чтения
  int intArrayRead[5];

  // читаем по адресу intArrayAddr
  eeprom_read_block((void*)&intArrayRead, (const void*)&intArrayAddr, sizeof(intArrayRead));

  // проверим
  for (byte i = 0; i < 5; i++)
    Serial.println(intArrayRead[i]);
}

void loop() {}

Таким образом можно добавлять “данные” для хранения в EEPROM прямо по ходу разработки программы, не думая об адресах. Рекомендую добавлять новые данные над старыми, чтобы адресация не сбивалась (напомню, адресация идёт снизу вверх, начиная с нуля).

Библиотека EEPROM.h


Библиотека EEPROM.h идёт в комплекте с ядром Arduino и является стандартной библиотекой. По сути EEPROM.h – это удобная оболочка для avr/eeprom.h, чуть расширяющая её возможности и упрощающая использование.

Для AVR Arduino: подключая в скетч EEPROM.h мы автоматически подключаем avr/eeprom.h и можем пользоваться её фишками, такими как EEMEM.

Рассмотрим инструменты, которые нам предлагает библиотека:

  • EEPROM.write(адрес, данные) – пишет данные (только byte!) по адресу
  • EEPROM.update(адрес, данные) – обновляет (та же запись, но лучше) байт данных, находящийся по адресу. Не реализована для esp8266/32!
  • EEPROM.read(адрес) – читает и возвращает байт данных, находящийся по адресу
  • EEPROM.put(адрес, данные) – записывает (по факту – обновляет, update) данные любого типа (типа переданной переменной) по адресу
  • EEPROM.get(адрес, данные) – читает данные по адресу и сам записывает их в данные – указанную переменную
  • EEPROM[] – библиотека позволяет работать с EEPROM памятью как с обычным массивом типа byte (uint8_t)

У esp8266 и esp32 есть отличия:

  • Перед началом работы нужно вызвать EEPROM.begin(размер) с указанием максимального объёма памяти: 4.. 4096 Байт.
  • Для применения записи нужно вызвать EEPROM.commit(): например несколько раз делается write(), put(), и в завершение – commit()
  • В некоторых версиях SDK отсутствует EEPROM.update()

В отличие от avr/eeprom.h у нас нет отдельных инструментов для работы с конкретными типами данных, отличными от byte, и сделать write/update/read для float/long/int мы не можем. Но зато у нас есть всеядные put и get, которые очень удобно использовать! Рассмотрим пример с чтением/записью байтов:

#include <EEPROM.h>

void setup() {
  Serial.begin(9600);
  //EEPROM.begin(100);   // для esp8266/esp32
  
  // пишем 200 по адресу 10
  EEPROM.update(10, 200);
  //EEPROM.commit();     // для esp8266/esp32
  Serial.println(EEPROM.read(10));  // выведет 200
  Serial.println(EEPROM[10]);       // выведет 200
}

void loop() {}

Логика работы с адресами такая же, как в предыдущем пункте урока! Обратите внимание на работу с EEPROM как с массивом, можно читать, писать, сравнивать, и даже использовать составные операторы, например EEPROM[0] += 10 , но это работает только для элементарных ячеек, байтов.

Теперь посмотрим, как работает put() и get():

#include <EEPROM.h>

void setup() {
  Serial.begin(9600);
  //EEPROM.begin(100);   // для esp8266/esp32

  // объявляем переменные, которые будем писать
  float dataF = 3.14;
  int16_t dataI = -634;
  byte dataArray[] = {10, 20, 30, 40};

  EEPROM.put(0, dataF);
  EEPROM.put(4, dataI);
  EEPROM.put(6, dataArray);
  //EEPROM.commit();     // для esp8266/esp32

  // объявляем переменные, куда будем читать
  float dataF_read = 0;
  int16_t dataI_read = 0;
  byte dataArray_read[4];

  // читаем точно так же, как писали
  EEPROM.get(0, dataF_read);
  EEPROM.get(4, dataI_read);
  EEPROM.get(6, dataArray_read);

  // проверяем
  Serial.println(dataF_read);
  Serial.println(dataI_read);
  Serial.println(dataArray_read[0]);
  Serial.println(dataArray_read[1]);
  Serial.println(dataArray_read[2]);
  Serial.println(dataArray_read[3]);
}

void loop() {}

put() и get() сами определяют тип данных и считают размер блока данных, использовать их очень приятно. Они работают как с массивами, так и со структурами.

EEPROM.h + структуры


Самый удобный хранить набор данных в EEPROM – структура (разбирали в уроке про типы данных). Структура позволяет объединить любые данные под одним именем, и одной строчкой загонять их в EEPROM и так же читать обратно. А также не придётся думать об адресации! Пример:

#include <EEPROM.h>

struct Data {
  byte bright = 0;
  int counter = 0;
  float fvalue = 0;
};

// глобальный экземпляр для личного использования
Data data;

void setup() {
  EEPROM.get(0, data);   // прочитать из адреса 0
  // меняем
  data.bright = 10;
  data.counter = 1234;
  data.fvalue = 3.14;
  EEPROM.put(0, data);   // поместить в EEPROM по адресу 0
}

void loop() {}

EEPROM.h + avr/eeprom.h


Пример не очень актуален, используй EEPROM + структуры

EEPROM.h + avr/eeprom.h

Ну и конечно же, можно использовать одновременно все преимущества обеих библиотек, например автоматическую адресацию EEMEM и put/get. Рассмотрим на предыдущем примере, вместо ручного задания адресов используем EEMEM, но величину придётся привести к целочисленному типу, сначала взяв от него адрес, т.е. (int)&адрес_еемем

#include <EEPROM.h>
float EEMEM dataF_addr;
int16_t EEMEM dataI_addr;
byte EEMEM dataArray_addr[5];

void setup() {
  Serial.begin(9600);

  // объявляем переменные, которые будем писать
  float dataF = 3.14;
  int16_t dataI = -634;
  byte dataArray[] = {10, 20, 30, 40};

  EEPROM.put((int)&dataF_addr, dataF);
  EEPROM.put((int)&dataI_addr, dataI);
  EEPROM.put((int)&dataArray_addr, dataArray);

  // объявляем переменные, куда будем читать
  float dataF_read = 0;
  int16_t dataI_read = 0;
  byte dataArray_read[4];

  // читаем точно так же, как писали
  EEPROM.get((int)&dataF_addr, dataF_read);
  EEPROM.get((int)&dataI_addr, dataI_read);
  EEPROM.get((int)&dataArray_addr, dataArray_read);
  EEPROM[0] += 10;

  // проверяем
  Serial.println(dataF_read);
  Serial.println(dataI_read);
  Serial.println(dataArray_read[0]);
  Serial.println(dataArray_read[1]);
  Serial.println(dataArray_read[2]);
  Serial.println(dataArray_read[3]);
}

void loop() {}

С возможностями библиотек разобрались, перейдём к практике.

Реальный пример


Рассмотрим пример, в котором происходит следующее: две кнопки управляют яркостью светодиода, подключенного к ШИМ пину. Установленная яркость сохраняется в EEPROM, т.е. при перезапуске устройства будет включена яркость, установленная последний раз. Для опроса кнопок используется библиотека GyverButton. Для начала посмотрите на первоначальную программу, где установленная яркость не сохраняется. Программу можно чуть оптимизировать, но это не является целью данного урока.

Меняем яркость кнопками

#define BTN_UP_PIN 3    // пин кнопки вверх
#define BTN_DOWN_PIN 4  // пин кнопки вниз
#define LED_PIN 5       // пин светодиода

#include <GyverButton.h>

GButton btnUP(BTN_UP_PIN); // кнопка "яркость вверх"
GButton btnDOWN(BTN_DOWN_PIN); // кнопка "яркость вниз"

int LEDbright = 0;

void setup() {
  pinMode(LED_PIN, OUTPUT); // пин светодиода как выход
}

void loop() {
  // опрос кнопок
  btnUP.tick();
  btnDOWN.tick();

  if (btnUP.isClick()) {
    // увеличение по клику
    LEDbright += 5;
    setBright();
  }

  if (btnDOWN.isClick()) {
    // уменьшение по клику
    LEDbright -= 5;
    setBright();
  }
}

void setBright() {
  LEDbright = constrain(LEDbright, 0, 255); // ограничили
  analogWrite(LED_PIN, LEDbright);    // изменили яркость
}

В этот код нам нужно добавить:

  • Подключить библиотеку EEPROM.h
  • При запуске: чтение яркости из EEPROM и включение светодиода
  • При клике: запись актуального значения в EEPROM

Сохранение яркости

#define BTN_UP_PIN 3    // пин кнопки вверх
#define BTN_DOWN_PIN 4  // пин кнопки вниз
#define LED_PIN 5       // пин светодиода

#include <EEPROM.h>
#include <GyverButton.h>

GButton btnUP(BTN_UP_PIN); // кнопка "яркость вверх"
GButton btnDOWN(BTN_DOWN_PIN); // кнопка "яркость вниз"

int LEDbright = 0;

void setup() {
  pinMode(LED_PIN, OUTPUT); // пин светодиода как выход
  EEPROM.get(0, LEDbright); // прочитали яркость из адреса 0
  analogWrite(LED_PIN, LEDbright);  // включили
}

void loop() {
  // опрос кнопок
  btnUP.tick();
  btnDOWN.tick();

  if (btnUP.isClick()) {
    // увеличение по клику
    LEDbright += 5;
    setBright();
  }

  if (btnDOWN.isClick()) {
    // уменьшение по клику
    LEDbright -= 5;
    setBright();
  }
}

void setBright() {
  LEDbright = constrain(LEDbright, 0, 255); // ограничили
  EEPROM.put(0, LEDbright);           // записали по адресу 0
  analogWrite(LED_PIN, LEDbright);    // изменили яркость
}

Итак, теперь при запуске у нас восстанавливается последняя настроенная яркость, и при изменении она записывается. Напомню, что EEPROM изнашивается от перезаписи. Конечно для того, чтобы “накликать” яркость несколько миллионов раз и убить ячейку, у вас уйдёт очень много времени, но процесс записи нового значения можно и нужно оптимизировать, особенно в более серьёзных проектах, ниже поговорим об этом подробнее. Также в нашем коде есть ещё один неприятный момент: при самом первом запуске после прошивки EEPROM не инициализирована, каждая ячейка хранит в себе число 255, и именно такое значение примет переменная LEDbright после первого запуска, при так называемом “первом чтении”. Здесь это не имеет значения, но в более серьёзном устройстве нужно будет задать нужные значения по умолчанию в EEPROM при первом запуске, об этом мы тоже поговорим ниже. Иначе представьте, какие “настройки по умолчанию” получит ваше устройство для яркости/скорости/громкости/номера режима/прочее!

Запись и чтение строк


У нас есть два типа строк: массивы символов и String-строки. С массивом символов всё более-менее понятно: это массив, он имеет фиксированный размер, его можно записать при помощи put() и прочитать при помощи get(). Также такая строка может входить в структуру, что очень удобно. В этом случае нужно объявить структуру с указанием максимальной длины строки, которая может там храниться. Например для какого-нибудь проекта с WiFi нам хочется хранить логин и пароль от роутера и режим работы:

struct Cfg {
  char ssid[16];
  char pass[16];
  byte mode;
};

Структуру очень просто прочитать и записать, а работать со строками в ней можно при помощи стандартных  строковых функций.

А как записать и прочитать динамические данные, такие как String-строки? Можно рассмотреть два способа: с массивом ограниченной длины (как в примере выше) и полностью динамическое хранение.

Будем считать, что максимальная длина строки – 20 символов. Простой пример:

#include <EEPROM.h>
#define STR_ADDR 0  // адрес хранения строки в EEPROM


void setup() {
  Serial.begin(115200);

  // читаем
  char str[20];
  EEPROM.get(STR_ADDR, str);

  // выводим
  Serial.print("Read text: ");
  Serial.println(str);
}

void loop() {
  // читаем строку из порта
  if (Serial.available()) {
    char str[20];
    int len = Serial.readBytes(str, 20);

    // завершающий символ, добавляем вручную
    str[len] = 0;

    // записываем
    EEPROM.put(STR_ADDR, str);
    
    Serial.print("Save text: ");
    Serial.println(str);
  }
}

При динамическом хранении мы будем сохранять также длину строки, в первой ячейке от которой идёт счёт. А уже дальше – саму строку. Писать и читать будем посимвольно, по другому тут уже не получится:

#include <EEPROM.h>

#define STR_ADDR 0  // адрес хранения строки в EEPROM

void setup() {
  Serial.begin(115200);
  
  String str;
  int len = EEPROM.read(STR_ADDR);  // читаем длину строки
  str.reserve(len);    // резервируем место (для оптимальности)

  // читаем
  for (int i = 0; i < len; i++) {
    str += (char)EEPROM.read(STR_ADDR + 1 + i);
  }
  
  // выводим
  Serial.print("Read text: ");
  Serial.println(str);
}

void loop() {
  // читаем строку из порта
  if (Serial.available()) {
    String inc = Serial.readString();
    Serial.print("Save text: ");
    Serial.println(inc);
    
    int len = inc.length();   // длина строки
    EEPROM.write(STR_ADDR, len);  // записываем её
    // и далее саму строку посимвольно
    for (int i = 0; i < len; i++) {
      EEPROM.write(STR_ADDR + 1 + i, inc[i]);
    }
  }
}

Полезные трюки


Инициализация


Под инициализацией я имею в виду установку значений ячеек в EEPROM “по умолчанию” во время первого запуска устройства. В рассмотренном выше примере мы действовали в таком порядке:

  1. Чтение из EEPROM в переменную
  2. Использование переменной по назначению

При первом запуске кода (и при всех дальнейших, в которых в ячейку ничего нового не пишется) переменная получит значение, которое было в EEPROM по умолчанию. В большинстве случаев это значение не подойдёт устройству, например ячейка хранит номер режима, по задумке разработчика – от 0 до 5, а из EEPROM мы прочитаем 255. Непорядок! При первом запуске нужно инициализировать EEPROM так, чтобы устройство работало корректно, для этого нужно определить этот самый первый запуск. Можно сделать это вручную, прошив программу, которая забьёт EEPROM нужными данными. Далее прошить уже рабочую программу. При разработке программы это очень неудобно, ведь количество сохраняемых данных может меняться в процессе разработки, поэтому можно использовать следующий алгоритм:

  1. Резервируем какую-нибудь ячейку (например, последнюю) под хранение “ключа” первого запуска
  2. Читаем ячейку, если её содержимое не совпадает с ключом – это первый запуск!
  3. В обработчике первого запуска пишем в ячейку нужный ключ
  4. Пишем в остальные ячейки необходимые значения по умолчанию
  5. И после этого уже читаем данные во все нужные переменные

Рассмотрим на всё том же примере со светодиодом и кнопками:

Сохранение яркости

#define INIT_ADDR 1023  // номер резервной ячейки
#define INIT_KEY 50     // ключ первого запуска. 0-254, на выбор

#define BTN_UP_PIN 3    // пин кнопки вверх
#define BTN_DOWN_PIN 4  // пин кнопки вниз
#define LED_PIN 5       // пин светодиода

#include <EEPROM.h>
#include <GyverButton.h>

GButton btnUP(BTN_UP_PIN); // кнопка "яркость вверх"
GButton btnDOWN(BTN_DOWN_PIN); // кнопка "яркость вниз"

int LEDbright = 0;

void setup() {
  pinMode(LED_PIN, OUTPUT); // пин светодиода как выход

  if (EEPROM.read(INIT_ADDR) != INIT_KEY) { // первый запуск
    EEPROM.write(INIT_ADDR, INIT_KEY);    // записали ключ

    // записали стандартное значение яркости
    // в данном случае это значение переменной, объявленное выше
    EEPROM.put(0, LEDbright);
  }
  EEPROM.get(0, LEDbright); // прочитали яркость
  analogWrite(LED_PIN, LEDbright);  // включили
}

void loop() {
  // опрос кнопок
  btnUP.tick();
  btnDOWN.tick();

  if (btnUP.isClick()) {
    // увеличение по клику
    LEDbright += 5;
    setBright();
  }

  if (btnDOWN.isClick()) {
    // уменьшение по клику
    LEDbright -= 5;
    setBright();
  }
}

void setBright() {
  LEDbright = constrain(LEDbright, 0, 255); // ограничили
  EEPROM.put(0, LEDbright);           // записали
  analogWrite(LED_PIN, LEDbright);    // изменили яркость
}

Теперь при первом запуске мы получим инициализацию нужных ячеек. Если нужно переинициализировать EEPROM, например в случае добавления новых данных, достаточно изменить наш ключ на любое другое значение в пределах одного байта (0-254). Я пишу именно до 254, потому что 255 является значением ячейки по умолчанию и наш трюк не сработает.

Сброс до “заводских”


Чтобы вернуть настройки к изначально заданным в программе, нужно “спровоцировать” инициализацию. Очевидный способ сделать это – изменить ключ инициализации, который мы назвали INIT_KEY. Либо можно просто вызвать EEPROM.put(адрес, базовые настройки) в нужном месте программы.

Скорость


Как я писал выше, скорость работы с EEPROM составляет:

  • Запись/обновление одного байта занимает ~3.3 мс (миллисекунды)
  • Чтение одного байта занимает ~0.4 мкс (микросекунды)

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

analogWrite(LED_PIN, EEPROM.read(0));   // изменили яркость

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

#define GET_MODE EEPROM.read(0)     // получить номер режима
#define GET_BRIGHT EEPROM.read(1)   // получить яркость
#define SET_MODE(x) EEPROM.write(0, (x))  // запомнить режим
#define SET_BRIGHT(x) EEPROM.put(1, (x))  // запомнить яркость

Получим удобные макросы, с которыми писать код будет чуть быстрее и удобнее, т.е. строка SET_MODE(3) запишет 3 в ячейку 0

Уменьшение износа


Важная тема: уменьшение износа ячеек частыми перезаписями. Ситуаций может быть много, интересных решений для них – тоже. Рассмотрим простейший пример – всё тот же код со светодиодом и кнопкой. Делать будем следующее: записывать новое значение будем только в том случае, если после последнего нажатия на кнопку прошло какое-то время. То есть нам понадобится таймер (воспользуемся таймером на millis), при нажатии на кнопку таймер будет сбрасываться, а при срабатывании таймера будем писать актуальное значение в EEPROM. Также понадобится флаг, который будет сигнализировать о записи и позволит записать именно один раз. Алгоритм такой:

  • При нажатии на кнопку:
    • Если флаг опущен – поднять флаг
    • Сбросить таймер
  • Если сработал таймер и флаг поднят:
    • Опустить флаг
    • Записать значения в EEPROM

Посмотрим на всё том же примере:

Сохранение яркости

#define INIT_ADDR 1023  // номер резервной ячейки
#define INIT_KEY 50     // ключ первого запуска. 0-254, на выбор

#define BTN_UP_PIN 3    // пин кнопки вверх
#define BTN_DOWN_PIN 4  // пин кнопки вниз
#define LED_PIN 5       // пин светодиода

#include <EEPROM.h>
#include <GyverButton.h>

GButton btnUP(BTN_UP_PIN); // кнопка "яркость вверх"
GButton btnDOWN(BTN_DOWN_PIN); // кнопка "яркость вниз"

int LEDbright = 0;
uint32_t eepromTimer = 0;
boolean eepromFlag = false;

void setup() {
  pinMode(LED_PIN, OUTPUT); // пин светодиода как выход

  if (EEPROM.read(INIT_ADDR) != INIT_KEY) { // первый запуск
    EEPROM.write(INIT_ADDR, INIT_KEY);    // записали ключ

    // записали стандартное значение яркости
    // в данном случае это значение переменной, объявленное выше
    EEPROM.put(0, LEDbright);
  }
  EEPROM.get(0, LEDbright); // прочитали яркость
  analogWrite(LED_PIN, LEDbright);  // включили
}

void loop() {
  // проверка EEPROM
  checkEEPROM();

  // опрос кнопок
  btnUP.tick();
  btnDOWN.tick();

  if (btnUP.isClick()) {
    // увеличение по клику
    LEDbright += 5;
    setBright();
  }

  if (btnDOWN.isClick()) {
    // уменьшение по клику
    LEDbright -= 5;
    setBright();
  }
}

void setBright() {
  LEDbright = constrain(LEDbright, 0, 255); // ограничили
  analogWrite(LED_PIN, LEDbright);          // изменили яркость
  eepromFlag = true;                        // поднять флаг
  eepromTimer = millis();                   // сбросить таймер
}

void checkEEPROM() {
  // если флаг поднят и с последнего нажатия прошло 10 секунд (10 000 мс)
  if (eepromFlag && (millis() - eepromTimer >= 10000) ) {
    eepromFlag = false;           // опустили флаг
    EEPROM.put(0, LEDbright);     // записали в EEPROM
  }
}

Вот таким нехитрым способом мы многократно снизили износ EEPROM, я очень часто использую этот “алгоритм” работы с настройками в своих устройствах. Есть другие задачи, в которых данные в EEPROM пишутся не когда пользователь что-то изменит, а постоянно, т.е. память работает в режиме чёрного ящика и постоянно записывает значения. Это может быть например контроллер печи, который держит температурный режим по специальному закону, и после внезапной перезагрузки должен вернуться к тому месту в процессе, на котором прервался. Тут есть глобально два варианта:

  • Ёмкий конденсатор по питанию микроконтроллера, позволяющий сохранить работу МК после отключения питания на время, достаточное для записи в EEPROM (~3.3 мс). Также МК должен знать о том, что общее питание отключилось: если это высокое напряжение (выше 5 Вольт), то это может быть делитель напряжения на аналоговый пин. Если это 5 Вольт – можно измерять напряжение МК, и момент отключения (разрядка конденсатора) тоже можно отловить и записать нужные данные. Можно взвести прерывание, которое сработает при падении напряжения питания ниже опасного уровня. Можно 5 Вольт завести напрямую на цифровой пин, а сам МК питать через диод и поставить конденсатор – тогда напряжение на измеряющем пине пропадёт до того, как отключится МК, он будет работать от конденсатора. Вот схема:
  • blank
  • Можно писать данные (необязательно один байт, можно целую структуру) хитро, размазывая их по всему EEPROM. Тут глобально два варианта:
    • Писать данные каждый раз в следующую ячейку, и закольцевать переход на первую. Также понадобится хранить где-то счётчик, указывающий на адрес текущей ячейки, и этот счётчик тоже придётся хранить хитро, чтобы он не износил ячейку. Например счётчик – это структура, состоящая из счётчика перезаписей этой структуры и счётчика адреса для большой структуры.
    • Писать данные, пока не достигнут лимит количества перезаписей, количество текущих перезаписей хранить например в этой же структуре. Скажем структура занимает 30 байт, то есть в перспективе мы можем найти эту структуру по адресу, кратному 30. Программа работает, счётчик считает количество перезаписей, при достижении опасного количества вся структура “переезжает” на следующие 30 адресов.

Вариантов уменьшения износа ячеек EEPROM можно придумать много, уникально под свою ситуацию. Есть даже библиотеки готовые, например EEPROMWearLevel. Есть очень интересная статья на Хабре, там рассмотрено ещё несколько хороших алгоритмов и даны ссылки на ещё большее их количество.

Библиотека EEManager


Я часто использую EEPROM в своих проектах, поэтому обернул все рассмотренные выше конструкции в библиотеку, изучить и скачать можно здесь. Библиотека подходит для всех архитектур, в которых есть стандартная EEPROM.h. В библиотеке реализовано:

  • Работа с данными любого типа
  • Чтение и запись в указанную переменную
  • Функция “ключа первого запуска” для задания начальных значений
  • Отложенное обновление по тайм-ауту для уменьшения износа

Я надеюсь вы полностью разобрались с самым последним примером с кнопкой и светодиодом, поэтому покажу работу EEManager на его основе:

EEManager, кнопка и светодиод

#define INIT_KEY 50     // ключ первого запуска. 0-254, на выбор

#define BTN_UP_PIN 3    // пин кнопки вверх
#define BTN_DOWN_PIN 4  // пин кнопки вниз
#define LED_PIN 5       // пин светодиода

#include <GyverButton.h>
GButton btnUP(BTN_UP_PIN); // кнопка "яркость вверх"
GButton btnDOWN(BTN_DOWN_PIN); // кнопка "яркость вниз"

#include <EEManager.h>
int LEDbright = 0;
EEManager memory(LEDbright); // передаём переменную в менеджер

void setup() {
  pinMode(LED_PIN, OUTPUT); // пин светодиода как выход

  // запускаем менеджер, указав адрес и ключ запуска
  // он сам проверит ключ, а также прочитает данные
  // из EEPROM и запишет в переменную
  memory.begin(0, INIT_KEY);

  analogWrite(LED_PIN, LEDbright);  // включили
}

void loop() {
  // здесь произойдёт запись по встроенному таймеру
  memory.tick();

  // опрос кнопок
  btnUP.tick();
  btnDOWN.tick();

  if (btnUP.isClick()) {
    // увеличение по клику
    LEDbright += 5;
    setBright();
  }

  if (btnDOWN.isClick()) {
    // уменьшение по клику
    LEDbright -= 5;
    setBright();
  }
}

void setBright() {
  LEDbright = constrain(LEDbright, 0, 255); // ограничили
  analogWrite(LED_PIN, LEDbright);          // изменили яркость
  memory.update();                          // сообщаем, что данные нужно обновить
}

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

Видео


Полезные страницы


  • Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
  • Поддержать автора за работу над уроками
  • Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])

P062F Ошибка EEPROM модуля внутреннего управления

Код неисправности OBD-II Техническое описание

Ошибка EEPROM модуля внутреннего контроля

Что это значит?

Это общий диагностический код неисправности трансмиссии (DTC), который обычно применяется к автомобилям OBD-II. Это может включать, но не ограничивается, автомобили от Buick, Chevy, GMC, Ford, Toyota, Nissan, Mercedes, Honda, Cadillac, Suzuki, Subaru и т. Д. Хотя общие, точные шаги ремонта могут варьироваться в зависимости от года, марки, модели. и конфигурация трансмиссии.

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

Процессоры мониторинга модуля внутреннего контроля несут ответственность за выполнение различных функций самопроверки контроллера и общую подотчетность модуля внутреннего контроля. Входные и выходные сигналы EEPROM подвергаются самопроверке и постоянно контролируются PCM и другими соответствующими контроллерами. Модуль управления трансмиссией (TCM), модуль контроля тяги (TCSM) и другие контроллеры также взаимодействуют с EEPROM.

В автомобильных приложениях EEPROM предоставляет средства для чтения, стирания и перезаписи небольших объемов (байтов) программируемой памяти. Используя специальное программирование, EEPROM (или любая часть EEPROM) может быть стерта и перезаписана последовательно. EEPROM — это группа транзисторов, состоящая из трех частей. Обычно он съемный и фиксируется в специально разработанном гнезде внутри PCM. Когда неисправный PCM заменяется, EEPROM обычно необходимо удалить и повторно использовать в новом PCM. EEPROM и новый PCM нужно будет программировать как единое целое. Несмотря на то, что EEPROM допускает более 1 миллиона программных изменений и рассчитан на работу в течение сотен лет, он может быть чувствительным к чрезмерному нагреву и влажности.   

Всякий раз, когда зажигание включено и PCM находится под напряжением, инициируются самотестирование EEPROM. Помимо выполнения самотестирования внутреннего контроллера, сеть контроллеров (CAN) также сравнивает сигналы от каждого отдельного модуля, чтобы убедиться, что каждый контроллер работает должным образом. Эти тесты выполняются одновременно.

Если PCM обнаруживает несоответствия в функциональности EEPROM, код P062F будет сохранен, и может загореться индикаторная лампа неисправности (MIL). Кроме того, если PCM обнаруживает проблему между любым из бортовых контроллеров, которая указывает на внутреннюю ошибку EEPROM, код P062F будет сохранен, и может загореться индикаторная лампа неисправности (MIL). Для включения контрольной лампы MIL может потребоваться несколько циклов отказа, в зависимости от предполагаемой серьезности неисправности.

Фотография ПКМ со снятой крышкой: P062F Ошибка EEPROM модуля внутреннего управления

Какова серьезность этого кода неисправности?

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

Каковы некоторые симптомы кода?

Симптомы кода неисправности P062F могут включать:

  • Широкий спектр проблем с управляемостью двигателя / трансмиссии
  • Нет условия запуска
  • Сниженная топливная эффективность
  • Заглох двигателя или остановка на холостом ходу
  • Отсутствие работы охлаждающего вентилятора

Каковы некоторые из распространенных причин появления кода?

Причины этого кода неисправности P062F могут включать:

  • Неисправный контроллер или ошибка программирования
  • Перегретый PCM
  • Повреждение водой
  • Неисправное реле питания контроллера или перегоревший предохранитель
  • Обрыв или короткое замыкание в цепи или разъемах в жгуте CAN
  • Недостаточное заземление модуля управления
  • Неисправный EEPROM

Каковы некоторые шаги по устранению неполадок P062F?

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

Если есть коды блока питания ECM / PCM, их, очевидно, необходимо исправить, прежде чем пытаться диагностировать P062F.

Есть несколько предварительных тестов, которые могут быть выполнены до того, как будет объявлено о неисправности отдельного контроллера. Потребуются диагностический сканер, цифровой вольт-омметр (ДВОМ) и источник достоверной информации о транспортном средстве.

Подключите сканер к диагностическому порту автомобиля и получите все сохраненные коды и данные стоп-кадра. Вы захотите записать эту информацию на всякий случай, если код окажется прерывистым. После записи всей соответствующей информации очистите коды и проведите тест-драйв автомобиля, пока код не будет сброшен или PCM не перейдет в режим готовности. Если PCM переходит в режим готовности, код является прерывистым, и его будет сложнее диагностировать. Состояние, из-за которого был сохранен P062F, может даже ухудшиться, прежде чем можно будет поставить диагноз. Если код сброшен, продолжайте с этого короткого списка предварительных тестов.

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

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

Используйте DVOM для проверки предохранителей и реле блока питания контроллера. Проверьте и при необходимости замените перегоревшие предохранители. Предохранители следует проверять при нагруженной цепи.

Если все предохранители и реле работают должным образом, необходимо провести визуальный осмотр проводки и жгутов, связанных с контроллером. Вы также захотите проверить заземление шасси и двигателя. Используйте свой источник информации о транспортном средстве, чтобы получить местоположения заземления для связанных цепей. Используйте DVOM для проверки целостности заземления.

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

Если цепи питания и заземления контроллера не повреждены, следует подозревать неисправный контроллер или ошибку программирования контроллера. Замена контроллера потребует перепрограммирования. В некоторых случаях вы можете приобрести перепрограммированные контроллеры на вторичном рынке. Для других транспортных средств / контроллеров потребуется перепрограммирование на борту, которое может быть выполнено только через дилерский центр или другой квалифицированный источник.

  • В отличие от большинства других кодов, P062F, вероятно, вызван неисправным контроллером или ошибкой программирования контроллера.
  • Проверьте целостность заземления системы, подключив отрицательный измерительный провод DVOM к земле, а положительный измерительный провод к напряжению батареи.

Связанные обсуждения DTC

  • Nissan Pathfinder 2019 года выпуска: код P062fЯ сделал полное ускорение, пока ехал, затем двигатель остановился, и я остановился. Я попытался завести двигатель, но он сразу выключился. Я отбуксировал его механику, который проверил OBD и обнаружил ошибку P062F. двигатель работает нормально отдельно от внешнего источника топлива…. 
  • Buick Regal Hybrid P2012F 062 годаПривет всем, на этой неделе работаю над машиной жены. Автомобиль не заводился, заводился, но, похоже, не хватило батареи, чтобы перевернуться. Будучи гибридом, он действительно может начать с нуля. Запуск двигателя прошел успешно, однако мил. Вставил в него свой сканирующий прибор и вытащил код P062F. Ха … 

Нужна дополнительная помощь с кодом P062F?

Если вам все еще нужна помощь по поводу кода ошибки P062F, задайте вопрос в комментариях под этой статьей..

ПРИМЕЧАНИЕ. Эта информация представлена ​​только в информационных целях. Он не предназначен для использования в качестве рекомендаций по ремонту, и мы не несем ответственности за любые действия, которые вы предпринимаете с каким-либо автомобилем. Вся информация на этом сайте защищена авторским правом.

Введение

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

   Одна из старых проблем AVR — это повреждение EEPROM данных при пониженном питании микроконтроллера. Это может происходить в двух случаях:

— Если напряжение питания ниже определенной величины, запись в EEPROM будет выполняться некорректно.
— При пониженном напряжении питания микроконтроллер сам может выполнять команды некорректно.

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

— Нужно удерживать микроконтроллер AVR в состоянии сброса, если напряжение питания находится ниже нормы. Для этого можно использовать внешние супервизоры питания или встроенный детектор пониженного питания — Brown-out Detector (BOD). Встроенный детектор управляется с помощью fuse битов микроконтроллера — BODEN и BODLEVEL. BODEN — разрешает/запрещает работу детектора, а BODLEVEL — определяет его уровень срабатывания.
   Если сброс микроконтроллера происходит во время процесса записи в EEPROM, то операция записи будет завершена только при достаточном уровне напряжения.

— Также в многие разработчике рекомендуют не использовать 0-ую ячейку EEPROM`a, поскольку именно ее содержимое чаще всего повреждается при снижении питания микроконтроллера.

Проблемы с EEPROM из-за прерываний

   Операция записи в EEPROM состоит из нескольких шагов. Вспомним эту последовательность:

1. Ожидаем готовности EEPROM, опрашивая бит EEWE регистра EECR.
2. Устанавливаем адрес в регистре EEAR.
3. Записываем байт данных в регистр EEDR.
4. Устанавливаем основной флаг разрешения записи EEMWE регистра EECE
5. Устанавливаем флаг разрешения записи EEWE регистра EECE

   Бит EEWE должен быть установлен в течении 4-ех тактов после установки бита EEMWE. Если этого не произойдет по причине прерываний, то запись в EEPROM не будет произведена. Этого легко избежать, если запретить прерывания перед 4-м шагом, а после 5-го снова разрешить их.

   Однако есть еще один подводный камень. Если прерывание возникло после 1-го, 2-го или 3-го шага, и в прерывании тоже используются операции с EEPROM (чтение или запись), то запись может не состояться, или запишутся не те данные и не туда, потому что содержимое регистров EEAR (адрес) и EEDR (данные) будет изменено.

   Описанное выше касается и процедуры чтения EEPROM.

   Лечить это можно следующими способами:

   — Не использовать операции чтения и записи EEPROM в прерываниях.
   Это особенно касается операции записи, потому что она медленная и выполняется с использованием внутреннего RC генератора. Например, для mega16 в даташите указано, что при записи в EEPROM используется внутренний RC генератор с частотой 1 МГц (независимо от установок fuse битов CKSEL) и время записи составляет 8.5 мс. Для прерывания это очень долго.

   — Запрещать прерывания на время всей процедуры записи (чтения) EEPROM, то есть в самом начале.

   — Сохранять в начале обработчика прерывания содержимое регистров EEAR (адрес) и EEDR (данные), а перед выходом восстанавливать их.

   — Использовать флаги (семафоры) для сигнализации о выполнении работы с EEPROM.
   Перед выполнением записи в основном цикле программы (или задаче, если используется ос) устанавливать программный флаг, а в прерывании (или другой задаче) проверять его.

Ресурс EEPROM

   EEPROM имеет ограниченный ресурс. Atmel гарантирует, что количество циклов перезаписи EEPROM составляет не меньше 100000. Цифра довольно большая, однако и она может быть достигнута, если записывать в EEPROM часто и на протяжении долгого времени.
   Есть два приема по «увеличению» ресурса EEPROM.
   Первый — простой и состоит в том, чтобы записывать в EEPROM данные, только если они изменили свое значение.


__eeprom uint8_t data;
uint8_t newData;

...
if (newData != data) {
data = newData;
}

   Второй- хитрый и состоит в том, чтобы хранить данные не в одной ячейки памяти (или группе ячеек, если речь идет о многобайтных переменных), а в нескольких, и записывать в них по очереди.
   Допустим, нам нужно хранить в EEPROM один байт. Выделяем под него 8 байтов и каждый раз записываем в следующую ячейку, когда доходим до последней ячейки, то записываем в первую. И так по кругу, как в кольцевом буфере. Если каждая ячейка EEPROM имеет ресурс 100000, то записывая в 8 ячеек по очереди, мы получаем ресурс перезаписи байта 800000.

EEPROM и оптимизация компилятора

   Переменные, которые объявлены, но не используются, часто удаляются компилятором в процессе оптимизации. Если такие переменные нужны, перед ними следует добавлять ключевое слово volatile.


//для IAR AVR
volatile __eeprom char name[] = "prog 12.3";

   Если используются свои функции для работы с EEPROM, то могут возникнуть проблемы при высоких уровнях оптимизации компилятора. Компилятор может объединить одинаковые (с его точки зрения) части кода в одну подпрограмму и нарушить логику работы вашей функции. Чтобы этого не происходило нужно или запрещать оптимизацию данной функции, или отключать перекрестную оптимизацию (cross call optimization) для функции или файла. Как это делается зависит от компилятора. Как правило, для этого существуют определенные ключи и прагмы.

Программные способы повышения надежности EEPROM

   

Один из простых способов повышения надежности хранения данных в EEPROM — это метод мажоритарного резервирования. Суть метода заключается в том, что для хранения данных выделяется нечетное количество ячеек памяти — N. При сохранении данных — запись производится во все выделенные ячейки. При чтении — читаются тоже все, но решение относительно содержимого принимается на основе равенства (N+1)/2 ячеек.

   Рассмотрим пример тройного мажоритарного резервирования байта данных. Для сохранения байта используются три байта EEPROM, а решение о содержимом принимается на основании равенства 2 байтов. Код приведен для компилятора IAR AVR.


//функция сохранения
void EEPROM_SaveByte(uint8_t value, uint8_t __eeprom *buf)
{
   buf[0] = value;
   buf[1] = value;
   buf[2] = value;
}

//функция считывания
uint8_t EEPROM_LoadByte(uint8_t *value, uint8_t __eeprom *buf)
{
   uint8_t a = buf[0];
   uint8_t b = buf[1];
   uint8_t c = buf[2];

   if ((a == b)||(a == c)){
      *value = a;
      return 0;
   }
   else {
      if (b == c){  
         *value = b;
         return 0;
      }
   }
   return 1;
}

....
//пример использования

__eeprom uint8_t buffer[3];
uint8_t data;

EEPROM_SaveByte(125, buffer);
EEPROM_LoadByte(&data, buffer);

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

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

   На этом все…

Offline

Зарегистрирован: 17.09.2013

Здравствуйте, тестирую одну функицю программы в отдельном проекте, — компилится но ведет себя неадекватно..

тестил 2 типа функций, сначала запись в EEPROM, потом чтение.

Проблема возникает при чтении — в какой-то момент программа глохнет не завершив цикл for, либо же начинает гнать циклов больше чем нужно.. Причем раз на раз не приходится. Расставил по коду Serial.println чтобы отловить момент глюка — но данные вроде все верные а програ глохнет.. Причем при вставке очередного Сериал-принта, программа может продвинуться на полтора цикла дальше чем до этого.. Вобщем довольно странно.

Привожу код программы. Код один, меняются только вызовы на чтениезапись из setup.

тестовые значения которыми заполнял память вначале сетапа (закоментировано для чтения).

#include <arduino.h>
#include <EEPROM.h>
#include "eepromanything.h"

typedef void(*CHEKER)(void); //создаем тип указателей на функции

struct Data { //
	double value;
	long interval;
	double lastmod;
	double lastst;
	byte type;
	byte inf;
	CHEKER control; //указатель на функцию обработчик
};
Data arrayData[5]; //создаем массив структур

Data &tempRoom = arrayData[0];
Data &humRoom = arrayData[1];
Data &tempGround = arrayData[2];
Data &tempAirDs = arrayData[3]; 
Data &tempAir = arrayData[4];
Data &humAir = arrayData[5];
Data &humGround = arrayData[6]; 
Data &lampSt = arrayData[7];
Data &lampH= arrayData[8]; 
Data &fanPwr = arrayData[9]; 
Data &compPwr = arrayData[10];
Data &filterSt = arrayData[11]; 
Data &doorSt = arrayData[12];

const byte DS18B20 = 0;
const byte DHT_11 = 1;
const byte DHT_22 = 2;
const byte PIN = 3;
const byte CHNG = 4;

const byte AIR = 0;
const byte GRND = 1; 
const byte TEMP = 0;
const byte HUM = 1;

int setCont; //число сетов настроек
long *stampTime; //глобальный массив - время начала настроек 

struct Settings {  //текущие настройки периода
	float maxTemp; 
	boolean lamp; 
	boolean filter; 
	int fanMinPwr;
	int fanMaxPwr;
	int compMinPwr;
	int compMaxPwr;
	int waterLvl;
	int waterT;
	int lampD; 
}thisSet;

void setup()
{
	/*tempAirDs.inf = AIR; //устанавливаем значения Data.inf
	tempGround.inf = GRND;
	tempAir.inf = TEMP;
	tempRoom.inf = TEMP;
	humAir.inf = HUM;
	humRoom.inf = HUM;

	tempRoom.type = DHT_11; //устанавливаем значения Data.type
	humRoom.type = DHT_11;
	tempGround.type = DS18B20;
	tempAirDs.type = DS18B20;
	tempAir.type = DHT_22;
	humAir.type = DHT_22;
	humGround.type = PIN;
	lampSt.type = CHNG;
	lampH.type = CHNG;
	fanPwr.type = CHNG;
	compPwr.type = CHNG;
	filterSt.type = CHNG;
	doorSt.type = PIN;
	
	thisSet.maxTemp=0.67;
	thisSet.lamp=0; 
	thisSet.filter=1; 
	thisSet.fanMinPwr=123;
	thisSet.fanMaxPwr=231;
	thisSet.compMinPwr=255;
	thisSet.compMaxPwr=200;
	thisSet.waterLvl=11;
	thisSet.waterT=21;
	thisSet.lampD=30;
	
	tempRoom.control = NULL; //устанавливаем значения Data.control
	humRoom.control = NULL;
	tempGround.control = NULL;
	tempAirDs.control = NULL;
	tempAir.control = tempAir1;
	humAir.control = NULL;
	humGround.control = NULL;
	lampSt.control = lampSt1;
	lampH.control = NULL;
	fanPwr.control = NULL;
	compPwr.control = NULL;
	filterSt.control = NULL;
	doorSt.control = doorSt1;
*/
delay (5000);
Serial.begin(9600);
/*DATA_write(); //записываем
setWrite();
*/
	DATA_get(); //читаем настройки
	Serial.println("DONE!");
	getSet();//получаем расписание настроек

	for (int i = 0; i < 1; ++i) { //распечатать все настройки в расписании
		Serial.println(stampTime[i]);
		setRead(i);
		Serial.println(thisSet.maxTemp);
	Serial.println(thisSet.lamp);
	Serial.println(thisSet.filter); 
	Serial.println(thisSet.fanMinPwr);
	Serial.println(thisSet.fanMaxPwr);
	Serial.println(thisSet.compMinPwr);
	Serial.println(thisSet.compMaxPwr);
	Serial.println(thisSet.waterLvl);
	Serial.println(thisSet.waterT);
	Serial.println(thisSet.lampD);
		};
Serial.println("DONE, NOW DATA:");
//dataCheck();
		
}
 
 void loop()
 {
 }
 
 void DATA_get() //чтение массива структур настроек из EEPROM
{
	EEPROM_readAnything(0, arrayData); //читаем данные с начала памяти

}

void setRead(int i) //получаем настройки конкретного сета из памяти
{
	int j = i + 1; //чтобы не перемножать на ноль
	int adr = ((sizeof(arrayData))+(j*sizeof(stampTime[i]) + 1) + (j - 1)*sizeof(Settings)); //получаем адрес начала настроек текущего сета
	Serial.println(adr); //печатаем адрес
	EEPROM_readAnything(adr, thisSet); //записываем данные в стракт настроек
}

void setWrite() //записываем настройки в память.
{
	byte cont=2; //число заголовков настроек
	long newTime[2]={1182933,29292012}; //формируем массив заголовков сетов
	int Size = sizeof(arrayData)+sizeof(cont);//размер данных
	for (int i = 0; i<cont; ++i){ //проверяем сколько памяти займут настройки
		Size += sizeof(newTime[i]) + sizeof(thisSet);
	};
	if (Size>=1023){ //если слишком много, то ошибка
		Serial.println("OUT OF MEMORY! TOO MANY TIMESETS!");
	}
	else{//если нет, то записываем
		Serial.println("Writing new sets..");
		int adr = sizeof(arrayData); //адрес записи
		EEPROM.write(adr, cont); //записываем общую сумму в первый байт 
		adr++;
		for (int i = 0; i<cont; ++i){// записываем настройки
			Size = EEPROM_writeAnything(adr, newTime[i]); //записываем заголовок старта
			adr += Size; //увеличиваем адрес на размер
			Size = EEPROM_writeAnything(adr, thisSet); //записываем настройки сета
			adr += Size;
		};
	};
	Serial.println("Secsess, Updating sets..");
	getSet();
	Serial.println("Done!");
}
void getSet() //чтение списка настроек из епром
{
	//получаем список настроек
	int adr = sizeof(arrayData);
Serial.println(adr); //начинаем со следующего байта после количества настроек.
	setCont = EEPROM.read(adr); //получаем количество настроек из байта следующего за настройками датчиков
Serial.println(setCont); //печатать число заголовков	
if (setCont == 0){ Serial.println("No settings found"); }
	else{ //если количество настроек не пусто и больше нуля.
		stampTime = new long[setCont]; //массив времени начала периода настроек
		adr++;
		int Size;
		for (int i = 0; i<setCont; ++i){ //перебираем все заголовки настроек, в цикле.
			if (adr >= 1022){ Serial.println("OUT OF MEMORY! CHECK EEPROM!"); }//если вышли за пределы eeprom
			else{
				Size = EEPROM_readAnything(adr, stampTime[i]);
				adr += Size + sizeof(thisSet); //перепрыгиваем на адрес +4байта long (заголовка) и длины блока настроек.

			};
		};
	};


}

void DATA_write()
{
	EEPROM_writeAnything(0, arrayData); //записываем данные датчиков с начала памяти.
}


/*void tempAir1()
{
Serial.println("tempAir");
}
void doorSt1()
{
Serial.println("doorst");
}

void lampSt1()
{
Serial.println("lampSt");
}*/

В итоге должно вывестить два раза одно и то же содержимое thisSet , с разными заголовками., До чтения Data даже не доходит..

PS

используется библиотека EEPROM_Anything тестировал её отдельно со структурами и массивами — все работает отлично.

не программируется eeprom

Присоединяйтесь к обсуждению

Вы можете написать сейчас и зарегистрироваться позже.

Если у вас есть аккаунт, авторизуйтесь, чтобы опубликовать от имени своего аккаунта.

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

Задание тактирования

Тактирование блока всегда должно задаваться ПЕРЕД настройкой его конфигурации!

Очень часто возникают ошибки, когда настраивается какой-либо периферийный блок, при этом тактирование данного блока не разрешено в контроллере тактовых частот. Также возможна ситуация, когда тактирование сбрасывается в одном из подключенных файлов. Необходимо внимательно следить за последовательностью включения тактирования периферийного блока и его конфигурации. В фрагменте кода 1 приведена корректная настройка порта С.

Фрагмент кода 1 — Пример инициализации порта ввода-вывода

// 1 - Включаем тактирование порта С
RST_CLK_PCLKcmd (RST_CLK_PCLK_PORTC, ENABLE);

// 2 - Инициализируем порт в заданной конфигурации
PORT_StructInit(&GPIOInitStruct);
GPIOInitStruct.PORT_Pin = PORT_Pin_0;
GPIOInitStruct.PORT_OE = PORT_OE_OUT;
GPIOInitStruct.PORT_SPEED = PORT_SPEED_SLOW;
GPIOInitStruct.PORT_MODE = PORT_MODE_DIGITAL;

PORT_Init(MDR_PORTC, &GPIOInitStruct);

Программа работает с отладчиком, но не работает при подаче питания на МК

Если программа работает с отладчиком, но не работает после сброса по Reset  или после включения питания, необходимо проверить очередность включения тактирования и инициализации!

Это тот случай, когда тактирование в программе задано после настройки периферии. На примере настройки портов рассмотрим ход выполнения программы:

  1. После сброса по Reset или после включения питания запускается пользовательская программа, тактирование периферийных контроллеров по сбросу выключено.

  2. В программе происходит инициализация портов.

  3. В программе включается тактирование портов.

Так как тактирование портов не было включено на момент их инициализации, то настройка портов не была произведена. 

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

  1. Отладчик подключается и перезапускает МК, при этом если в настройках отладчика выбран сброс только ядра, то тактирование периферийных блоков остаётся включённым с прошлого запуска.

  2. В программе происходит инициализация портов, которая выполняется успешно, так как тактирование портов включено. 

  3. В программе включается тактирование портов.

В данном случае порты ввода-вывода функционируют корректно, поскольку их инициализация происходила при уже включенном тактировании.

Переменные и флаги в прерываниях

Нельзя изменять значение переменной в основном потоке и в обработчике прерывания без атомарного доступа!

Например, простейшая операция » i++; » происходит в несколько этапов:

  1. В регистр ядра Rx загружается значение i из ячейки памяти.

  2. Значение регистра увеличивается на 1.

  3. Значение регистра сохраняется в ячейку памяти i.

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

В ядре Cortex M существуют операции для защищенного обращения к памяти LDREX и STREX. Информацию по их использованию можно найти, например, на официальном сайте Keil.

Вторым вариантом обращения к памяти может быть использование метода bit-band, который доступен для МК на базе Cortex M3/M4. Запись и стирание флагов в ячейках памяти происходит атомарными операциями типа чтение-модификация-запись.

Программирование Flash-памяти

Flash память МК может работать в двух режимах: в обычном режиме (доступ к памяти осуществляется через шины I Code и D Code) и в режиме программирования (доступ к памяти осуществляется через регистры контроллера Flash-памяти). В режиме программирования программный код должен выполняться из области системной шины (внешняя память) или ОЗУ. Выполнение программного кода из Flash-памяти в режиме программирования невозможно. При попытке доступа ядра к Flash-памяти, находящейся в режиме программирования, будет вызвано прерывание HardFault или BusFault в зависимости от настроек ядра (SCR регистры). Поэтому важно, чтобы при программировании Flash-памяти не возникало никаких прерываний, поскольку таблица векторов и обработчики прерываний по умолчанию расположены во Flash-памяти.

Чтобы запретить выбранное прерывание IRQn необходимо вызвать функцию NVIC_DisableIRQ(). При этом необходимо учитывать, что запрещение генерации запроса прерываний от системного таймера SysTick выполняется не в контроллере NVIC, а в регистре управления системным таймером.

Пример запрещения прерываний от системного таймера приведён в фрагменте кода 2.

Фрагмент кода 2 — Запрещение прерываний от системного таймера

SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;

Чтобы запретить сразу все прерывания, кроме HardFaut и NMI, необходимо выполнить специальную функцию, как показано в фрагменте кода 3.

Фрагмент кода 3 — Запрещение всех прерывания, кроме HardFaut и NMI


 __disable_irq();

После работы с Flash-памятью необходимо вернуть разрешение прерываний.

В режиме программирования функции работы с Flash-памятью должны выполняться из области системной шины (внешняя память) или ОЗУ!

Пример расположения программного кода в памяти ОЗУ приведён в статье Расположение функций в ОЗУ, программирование EEPROM.

Выводы, совмещенные с JTAG и SWD

Иногда возникает необходимость использовать выводы, совмещенные с интерфейсом JTAG и SWD (далее для краткости используется обозначение JTAG, но подразумевается JTAG и SWD). При использовании функций библиотеки SPL выводы, совмещенные с JTAG B, перенастроить не получится, так как для них по умолчанию установлена защита. Например, функция PORT_Init() проверяет конфигурируемые выводы на принадлежность к JTAG B и не даёт их переназначать. Разрешение данной проверки определено в файле MDR32FxQI_config.h с помощью макроопределения USE_JTAG_B, строка 80, как показано в фрагменте кода 4.

Фрагмент кода 4 — Макроопределения защиты выводов, совмещённых с JTAG, в файле MDR32FxQI_config.h

#if (defined(USE_MDR32F9Q2I) || defined (USE_MDR32FG16S1QI))

  
/* #define USE_JTAG_A */
#define USE_JTAG_B

#endif

Библиотечный файл MDR32FxQI_config.h защищен от записи, поэтому необходимо предварительно в свойствах файла снять атрибут «Только чтение» (Правая клавиша мыши -> Свойства -> Только чтение). Для снятия защиты с выводов, совмещённых с JTAG A или B необходимо закомментировать соответствующее макроопределение: USE_JTAG_A или USE_JTAG_B. После этого данными выводами можно управлять с помощью функций SPL.

Необходимо обратить внимание, что после переопределения выводов, совмещенных с JTAG, выбранный интерфейс JTAG работать не будет. Программа при запуске будет переопределять эти выводы, и подключиться к МК через данный интерфейс будет невозможно. Для связи с МК необходимо будет использовать либо другой интерфейс JTAG, либо интерфейс UART при старте МК в режиме «UART загрузчик».

Чтобы сохранить работоспособность интерфейса JTAG при старте МК, необходимо в начале функции main() вставить пустой цикл на пару секунд. Этот цикл даст некоторую задержку перед переопределением выводов, совмещённых с JTAG. За это время отладчик успеет перехватить управление и остановить исполнение программы. Таким образом сохраняется возможность подключиться к МК, даже если выводы JTAG в программе используются по другому назначению.

Запись в регистры порта, выводы которого совмещенные с JTAG и SWD

Если в программе не используются функции SPL, то необходимо учитывать, что при записи в регистры MDR_PORTx→RXTX и MDR_PORTx→OE биты выводов, совмещенных с JTAG, необходимо сбрасывать. Если этого не сделать, то работа интерфейса будет нарушена, а отладка невозможна.

В качестве примера можно посмотреть реализацию функций PORT_SetBits() или PORT_ResetBits() библиотеки SPL.

Смена тактовой частоты

Для смены тактовой частоты на более высокую требуется совершить следующие операции:

  • Если требуется, переключить мультиплексор С3 на промежуточный источник тактирования, например, HSI.

  • Настроить генератор HSE и/или умножитель частоты PLL и дождаться, пока он выйдет в рабочий режим.

  • Настроить в контроллере Flash-памяти число тактов паузы Delay до переключения на более высокую частоту.

  • Настроить поля SelectRI и LOW в регистре MDR_BKP→REG_0E.

  • Переключить мультиплексор С3 на новый источник тактирования.

При переходе на более низкую частоту, изменение значения Delay и SelectRI, LOW производят после смены частоты.

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

До перехода на новую частоту с помощью переключения мультиплексора C3 необходимо, чтобы новая частота была полностью сформирована, и МК был полностью готов к работе на ней.

На момент смены частоты значения Delay и SelectRI, LOW должны соответствовать максимальной частоте из старого и нового значения.

Пример инициализации тактирования в МК К1986ВЕ92QI

В фрагменте кода 5 приведена функция CPU_Initialize(), инициализирующая тактирование в МК К1986ВЕ92QI от умножителя частоты PLL с использованием генератора HSE, который работает на внешнем кварцевом резонаторе. Для работы функции CPU_Initialize() в проект необходимо подключить библиотечные файлы MDR32FxQI_rst_clk.c,  MDR32FxQI_eeprom.c, MDR32FxQI_power.c.

Фрагмент кода 5 — Инициализация тактирования в МК К1986ВЕ92QI

#include <MDR32FxQI_rst_clk.h>
#include <MDR32FxQI_eeprom.h>
#include <MDR32FxQI_power.h>

// Инициализация системы тактирования микроконтроллера 
void CPU_Initialize (void) 

   // Сброс настроек системы тактирования 
   RST_CLK_DeInit(); 

    
   // Инициализация генератора на внешнем кварцевом резонаторе (HSE = 8 МГц) 
   RST_CLK_HSEconfig (RST_CLK_HSE_ON); 
   if(RST_CLK_HSEstatus() != SUCCESS){
       while (1);
   }

    
   // Инициализация блока PLL 
   // Настройка источника и коэффициента умножения PLL 
   // CPU_C1_SEL = HSE_CLK, PLLCPUo = HSE_CLK * 10 = 8 МГц * 10 = 80 МГц
   RST_CLK_CPU_PLLconfig (RST_CLK_CPU_PLLsrcHSEdiv1, RST_CLK_CPU_PLLmul10);
   // Включение PLL
   RST_CLK_CPU_PLLcmd (ENABLE);  
   if(RST_CLK_CPU_PLLstatus() == ERROR) {
       while (1);
   }
   // Подключение PLL к системе тактирования 
   // (CPU_C2_SEL = PLLCPUo = 80 МГц)
   RST_CLK_CPU_PLLuse (ENABLE);
   // Настройка коэффициента деления блока CPU_C3_SEL 
   // (CPU_C3_SEL = CPU_C2) 
   RST_CLK_CPUclkPrescaler (RST_CLK_CPUclkDIV1);

   
   // Настройка числа тактов паузы Delay в контроллере Flash-памяти
   // Тактовая частота до 100 МГц - Delay = 3
   RST_CLK_PCLKcmd (RST_CLK_PCLK_EEPROM, ENABLE);
   EEPROM_SetLatency(EEPROM_Latency_3);
   RST_CLK_PCLKcmd (RST_CLK_PCLK_EEPROM, DISABLE);

   // Настройка параметров регулятора напряжения SelectRI и LOW в контроллере BKP
   // Тактовая частота 80 МГц
   RST_CLK_PCLKcmd(RST_CLK_PCLK_BKP, ENABLE);
   POWER_DUccMode(POWER_DUcc_upto_80MHz);

   
   // Переключение тактовой частоты процессора на CPU_C3 
   // (HCLK = CPU_C3) 
   RST_CLK_CPUclkSelection (RST_CLK_CPUclkCPU_C3);
}

При использовании функции printf() отладка не доходит до main()

Это особенность компилятора Keil, заменяющего функционал printf() на инструкцию программной остановки BKPT для реализации механизма semihosting. При старте программы низкоуровневые библиотеки Си также выполняют инструкцию BKPT, что приводит к остановке исполнения программы. Чтобы Keil не реализовывал механизм semihosting необходимо выполнить один из указанных пунктов:

1) Исключить вызов printf() из проекта.

2) Описать функции, перенаправляющие стандартный поток ввода-вывода в требуемый интерфейс МК, например, как показано в статьях Printf через ITM и Printf через UART.

3) В настройках проекта «Options for Target -> Target» выбрать опцию «Use MicroLIB», которая позволяет использовать оптимизированную по размеру кода стандартную библиотеку Си, в которой исключен механизм semihosting. Подробнее про MicroLIB описано на официальном сайте Keil.

Переход по абсолютному адресу приводит к исключению HardFault

Иногда требуется перейти в функцию, расположенную по известному адресу в памяти. Если в коде это будет выражено так, как показано в фрагменте кода 6 или 7, то произойдёт вызов исключения HardFault:

Фрагмент кода 6 — Некорректный переход по заданному адресу на языке Си

// Адрес функции в памяти
#define BASE_ADDR_FUNC_IN_RAM 0x20005000

// Указатель на функцию в памяти по известному адресу
typedef void (*funcptr)();
funcptr funcInRAM = (funcptr) (BASE_ADDR_FUNC_IN_RAM);

// Вызов функции
funcInRAM();


Фрагмент кода 7 — Некорректный переход по заданному адресу на языке ассемблер

LDR R0,=(0x20005000)
BX R0

Это происходит, потому что адрес перехода должен быть нечетным, чтобы указать ядру о переходе на инструкцию THUMB, а не ARM! Подробнее об этом описано на сайте ARM Info Center.

На самом деле при переходе в фрагментах кода 6 и 7 происходит исключение UsageFault, но данное исключение по сбросу запрещено, поэтому происходит вызов обработчика исключения HardFault. Разрешение исключений BusFault, MemManage fault и UsageFault выполняется в регистрах ядра SCB (System Control Block), как показано в фрагменте кода 8. О том, как работать с исключениями в Cortex-M3/M4 приведено в Application Note 209 от ARM.

Фрагмент кода 8 — Разрешение исключений BusFault, MemManage fault и UsageFault 

#define SCB_SHCSR_USGFAULTENA (1 << 18)
#define SCB_SHCSR_BUSFAULTENA (1 << 17)
#define SCB_SHCSR_MEMFAULTENA (1 << 16)

SCB->SHCSR |= SCB_SHCSR_USGFAULTENA;
SCB->SHCSR |= SCB_SHCSR_BUSFAULTENA;
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA;

Таким образом, чтобы переход на заданный адрес произошел корректно, необходимо указывать в нулевом бите адреса перехода единицу, как показано в фрагментах кода 9 и 10.

Фрагмент кода 9 — Корректный переход по заданному адресу на языке Си

// Адрес функции в памяти 
#define BASE_ADDR_FUNC_IN_RAM 0x20005000

// Указатель на функцию в памяти по известному адресу
typedef void (*funcptr)();
funcptr funcInRAM = (funcptr) (BASE_ADDR_FUNC_IN_RAM + 1);

// Вызов функции 
funcInRAM();


Фрагмент кода 10 — Корректный переход по заданному адресу на языке ассемблер
LDR R0,=(0x20005001)
BX R0

Сохранить статью в PDF

  • Программе не удается проверить наличие обновлений определений обнаружена ошибка 0x80072ee7
  • Программатор usbasp выдает ошибку
  • Программа чтения ошибок мерседес спринтер
  • Программа чтения ошибок елм 327
  • Программа чтения ошибок автомобиля для телефона