AVR100: доступ к энергонезависимой памяти EEPROM Печать
Добавил(а) microsin   

В даташите AVR100 рассматриваются следующие возможности по доступу к памяти EEPROM в микроконтроллерах AVR:

• Доступ к ячейкам памяти в произвольном порядке (Random Read/Write).
• Последовательное чтение и запись ячеек (Sequential Read/Write).
• Рабочий тест/пример работы с памятью EEPROM.

Прим. переводчика: в оригинальном даташите были ошибки и несоответствия с листингом подпрограмм avr100.asm [2]. Все замеченные ошибки исправлены.

Этот апноут содержит готовые подпрограммы для доступа к памяти EEPROM. Реализовано 2 типа доступа на чтение и запись:

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

Апноут AVR100 содержит 4 подпрограммы, которые подробно описываются в последующих секциях. Подпрограммы написаны на языке ассемблера. Этот апноут подойдет для доступа к EEPROM на всех микроконтроллерах AVR.

Примечание: в последних моделях микроконтроллеров бит EEWE в регистре EECR называется EEPE, и EEMWE называется EEMPE. Также в последних моделях регистр EECR содержит 2 дополнительных бита для установки режима программирования: EEPM0 и EEPM1. Эти 2 бита необходимо инициализировать перед установкой EEPE.

[Запись по произвольному адресу: подпрограмма EEWrite]

Перед вызовом этой подпрограммы необходимо сначала установить 3 регистровые переменные:

EEdwr – записываемые данные
EEawr – младший байт адреса ячейки, куда осуществляется запись
EEawrh – старший байт адреса ячейки, куда осуществляется запись

Перед записью подпрограмма ждет, пока EEPROM не будет готова к программированию путем опроса бита EEWE (EEPROM Write Enable) в регистре управления EEPROM (EEPROM Control Register, EECR). Когда EEWE равен 0, содержимое EEdwr передается в регистр данных EEPROM (EEPROM Data Register, EEDR), и содержимое регистровой пары EEawrh:EEawr передается в регистр адреса EEPROM (EEPROM Address Register, EEARH:EEARL). Сначала устанавливается бит разрешения записи EEMWE (EEPROM Master Write Enable), затем устанавливается бит строба записи EEWE в регистре EECR. Алгоритм работы подпрограммы см. на рис. 1.

AVR100-EEWrite

Рис. 1. Алгоритм работы подпрограммы EEWrite.

[Чтение по произвольному адресу: подпрограмма EERead]

Перед вызовом этой подпрограммы необходимо сначала установить 2 регистровые переменные:

EEard – младший байт адреса ячейки, откуда осуществляется чтение
EEardh – старший байт адреса ячейки, откуда осуществляется чтение

Перед чтением подпрограмма ожидает готовности EEPROM путем опроса бита EEWE (EEPROM Write Enable) в регистре управления EEPROM (EEPROM Control Register, EECR). Когда EEWE равен 0, содержимое регистровой пары EEardh:EEard передается в регистр адреса EEPROM (EEPROM Address Register, EEARH:EEARL). Затем устанавливается бит строба чтения EERE в регистре EECR. В следующей инструкции содержимое регистра EEDR передается в регистровую переменную EEdrd - это считанные данные. Алгоритм работы подпрограммы см. на рис. 2.

AVR100-EERead

Рис. 2. Алгоритм работы подпрограммы EERead.

[Последовательная запись: подпрограмма EEWrite_seq]

Перед вызовом этой подпрограммы необходимо сначала установить 1 регистровую переменную:

EEdwr_s – записываемые данные

Перед записью подпрограмма ожидает готовности EEPROM к программированию путем опроса бита EEWE (EEPROM Write Enable) в регистре управления EEPROM (EEPROM Control Register, EECR). Когда EEWE равен 0, содержимое регистра адреса EEPROM (EEPROM Address Register, EEARH:EEARL) читается в регистровую переменную EEwtmph:EEwtmp, она инкрементируется и записывается обратно в EEARH:EEARL. Этим инкрементируется текущий адрес EEPROM на единицу. Содержимое EEdwr_s передается в регистр данных EEPROM (EEPROM Data Register, EEDR), затем по порядку устанавливаются сначала бит EEWE в регистре EECR, затем бит EEMWE. Алгоритм работы подпрограммы см. на рис. 3.

AVR100-EEWrite seq

Рис. 3. Алгоритм работы подпрограммы EEWrite_seq.

[Последовательное чтение: подпрограмма EERead_seq]

Перед чтением подпрограмма ожидает готовности EEPROM путем опроса бита EEWE (EEPROM Write Enable) в регистре управления EEPROM (EEPROM Control Register, EECR). Когда EEWE равен 0, текущий адрес EEPROM инкрементируется следующим образом: содержимое регистра адреса EEARH:EEARL передается в регистровую переменную EErtmph:EErtmp, эта переменная увеличивается на 1 и записывается обратно в EEARH:EEARL. Затем подпрограмма устанавливает строб чтения EEPROM (EEPROM Read Strobe, EERE). И наконец, прочитанные данные EEPROM передаются из EEDR в регистровую переменную EEdrd_s.Алгоритм работы подпрограммы см. на рис. 4.

AVR100-EERead seq

Рис. 4. Алгоритм работы подпрограммы EERead_seq.

[Оптимизация кода под различные типы микроконтроллеров]

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

В микроконтроллере AT90S1200, к примеру, нет бита EEMWE в регистре EECR, так что его устанавливать не нужно. Подробнее про работу с битами регистров EEPROM на чтение и запись проконсультируйтесь с даташитом на соответствующий микроконтроллер.

В файле исходного кода на ассемблере avr100.asm [2] есть готовая программа, которая вызывает все 4 описанные функции в качестве теста и примера для использования. Тест можно скомпилировать и запустить в симуляторе среды разработки AVR Studio®. Тест также содержит комментарии, касающиеся портирования кода на другие микроконтроллеры семейства AVR.

Прим. переводчика: если Вы счастливый обладатель аппаратного отладчика наподобие AVR Dragon или JTAGICE mkII, и используете современный микроконтроллер с возможностью отладки, то можете запустить тест прямо в микроконтроллере.

Примечание: если Ваш код начнет запись в EEPROM сразу после сброса (включения питания), то имейте в виду следующее: если содержимое EEPROM было ранее запрограммировано на заводе во время производства, то микроконтроллер может быстро изменить код за короткое время после программирования. Когда программатор затем проверит содержимое EEPROM, то проверка может не совпасть, потому что содержимое EEPROM уже может быть изменено микроконтроллером. Также имейте в виду, что некоторые программаторы ISP (In-System Programmer, программатор, который может программировать микроконтроллер прямо в рабочей системе) могут позволить микроконтроллеру короткое время выполнения кода после каждого шага в процессе программирования и верификации.

Таблица 1. Затраты процессорного времени и памяти.

Место
использования 
Размер
кода
Затраты
циклов 
Используемые
регистры 
Описание
Reset 8 слов 8 R16 Пример инициализации
Main 39 слов - R16, R19, R20 Пример программы, которая работает с EEPROM
Всего 87 слов - R0, R16..R20, R24, R25

Таблица 2. Использование периферийных ресурсов микроконтроллера.

Периферия Описание Использование
прерываний
8 ножек порта ввода/вывода B Выходы для подключения светодиодов, показывающих значение данных (нужно только в качестве примера) -
1 ножка порта D Вход для подключения кнопки (только для примера) -
10 байт EEPROM Целевые ячейки EEPROM (только для примера)  -

;****************************************************************************
;* ;* Программа теста и примера работы с памятью EEPROM ;* ;**************************************************************************** ;***** Регистровые переменные основной программы .def counter =r19 .def temp =r20 ;***** Здесь начинается код программы RESET: ;***** Инициализация указателя стека (stack pointer) ;* В этом примере stack pointer устанавливается на самый старший адрес ;* внутренней памяти SRAM. Закомментируйте этот код для устройств, ;* у которых отсутствует SRAM. ldi r16,high(RAMEND) ; старший байт надо устанавливать только если out SPH,r16 ; размер RAM больше 256 байт ldi r16,low(RAMEND) out SPL,r16 ;***** Инициализация порта B ;* Порт B используется для отображения результатов проверки работы ;* подпрограмм чтения и записи EEPROM. ldi r16,0xff ; DDRB=0xff -> порт B работает как выход out DDRB,r16 ;***** Инициализация порта D ; Разряд 0 порта D используется для запуска тест-программы (это вход). ldi r16,0xff ; все pull-up резисторы порта D разрешены out PORTD,r16 ;***** Запуск программы main: in r16,PIND ; ожидание, пока пользователь не нажмет sbrc r16,0 ; кнопку, замыкающую разряд 0 порта D rjmp main ; на GND... ;***** Программирование данных EEPROM по произвольному адресу ldi EEdwr,$aa ldi EEawrh,$00 ldi EEawr,$10 rcall EEWrite ; сохранение $aa в ячейку EEPROM с адресом $0010 ;***** Чтение по произвольному адресу ldi EEardh,$00 ldi EEard,$10 rcall EERead ; чтение по адресу $0010 out PORTB,EEdrd ; вывод значения в ножки порта B ;***** Заполнение памяти EEPROM по адресам 1..128 данными $55, $aa, $55, $aa, ... EEWrite_wait: sbic EECR,EEWE ; если EEWE не очищен, rjmp EEWrite_wait ; то подождем еще ; Предыдущие действия по проверке EEWE на 0 можно пропустить, если ; гарантируется, что до этого не было операции записи, когда мы сейчас ; будем менять регистры EEARL и EEARH. ldi counter,63 ; инициализация счетчика цикла clr temp out EEARH,temp ; EEARH < - $00 clr temp out EEARL,temp ; EEARL < - $00 (начальный адрес -1) loop1: ldi EEdwr_s,$55 rcall EEWrite_seq ; запишем в EEPROM байт $55 ldi EEdwr_s,$aa rcall EEWrite_seq ; запишем в EEPROM байт $aa dec counter ; декремент счетчика brne loop1 ; если счетчик не равен 0, то продолжим цикл ;***** Копирование первых 10 байт из EEPROM в регистры r1-r11 EERead_wait: sbic EECR,EEWE ; если бит EEWE не очищен, rjmp EERead_wait ; то подождем ; Предыдущие действия по проверке EEWE на 0 можно пропустить, если ; гарантируется, что до этого не было операции записи, когда мы сейчас ; будем менять регистры EEARL и EEARH. clr temp out EEARH,temp ; EEARH < - $00 ldi temp,$00 out EEARL,temp ; EEARL < - $00 (начальный адрес -1) clr ZH ldi ZL,1 ; Z-pointer указывает на регистр r1 loop2: rcall EERead_seq ; прочитать данные из EEPROM st Z,EEdrd_s ; сохранить их в SRAM inc ZL cpi ZL,12 ; дошли до конца? brne loop2 ; если нет, продолжим цикл forever: rjmp forever ; В это место управление попадет тогда, когда ; все действия программы завершены.

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

Проверка достоверности данных. Уже давно придумана технология, которая позволяет проверить целостность некоторого блока данных - контрольная сумма. Есть множество вариантов подсчета контрольных сумм, но для AVR вполне подойдет стандартный алгоритм CRC-16-CCITT (полином x16 + x12 + x5 + 1, этот алгоритм реализован в модуле crc16.c проекта [3]).

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

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

Инициализация данных EEPROM. Когда в EEPROM хранятся некоторые данные, то логично предположить, что у них должно быть некоторое значение по умолчанию - например, для ситуации, когда прибор только что изготовлен и включается в первый раз. Само собой, инициализацию EEPROM можно выполнить с помощью программатора - при прошивке микроконтроллера прошивается не только память программ FLASH, но и память EEPROM. Это довольно неудобно, потому что нужно выполнить как минимум 2 операции по записи и 2 операции проверки. Гораздо удобнее, когда программа микроконтроллера сама позаботится об инициализации своих данных EEPROM, и загрузит в них установки по умолчанию. С помощью контрольной суммы довольно просто определить необходимость такой инициализации - когда не совпала контрольная сумма защищенных данных EEPROM. Для целей инициализации в памяти программ FLASH может храниться копия массива данных по умолчанию в виде удобной для использования структуры, либо эти данные можно просто инициализировать нужными значениями в специальной процедуре восстановления значений EEPROM. При этом отпадает необходимость инициализировать программатором данные EEPROM - программа микроконтроллера позаботится об этом самостоятельно.

В этом примере будет рассмотрен исходный код инициализации и защиты данных EEPROM, примененный в проекте [3]. В файле types.h определена структура для хранения данных в EEPROM:

#ifndef __TYPES__
#define __TYPES__
 
#include < inttypes.h >
 
#define u8 unsigned char
#define u16 unsigned int
#define u32 uint32_t
#define false 0
#define true 1
 
typedef struct _eeparam
{
   u8 seedAA;
   u8 ledstate;   //состояние светодиода
   // .. тут можно добавить свои параметры
   u16 crc;
}Teeparam;
 
#endif //__TYPES__

Здесь для целей демонстрации применена простейшая структура, в которой кроме CRC находится всего лишь одно полезное поле данных ledstate (поле seedAA необходимо только для корректной инициализации алгоритма подсчета контрольной суммы для того случая, когда все поля полезных полей структуры окажутся нулевыми).

Все подпрограммы для работы с EEPROM размещены в модуле eepromutil.c, это 3 подпрограммы: Save(), Load() и RestoreEEPROM().

#include < avr/eeprom.h > 
#include < string.h > 
#include "types.h"
#include "crc16.h"
#include "vars.h"
#include "strval.h"
 
void Save (void)
{
   u16 crc;
   u8* eeadr;
 
   crc = 0;
   eeadr = 0;
   //записываем все данные целиком, кроме CRC
   eeprom_write_block(&eeprm, eeadr, sizeof(Teeparam)-sizeof(u16));
   AddCRC16(&eeprm, sizeof(Teeparam)-sizeof(u16), &crc);
   eeadr += sizeof(Teeparam)-sizeof(u16);
   //записываем CRC
   eeprom_write_block(&crc, eeadr, sizeof(crc));
}
 
u8 Load (void)
{
   u16 crccalc;
   u8* eeadr;
 
   crccalc = 0;
   eeadr = 0;
   //читаем все данные целиком
   eeprom_read_block(&eeprm, eeadr, sizeof(Teeparam));
   //посчитаем CRC
   AddCRC16(&eeprm, sizeof(Teeparam)-sizeof(u16), &crccalc);
   return (eeprm.crc == crccalc);
}
 
void RestoreEEPROM (void)
{
   eeprm.seedAA = 0xAA;
   eeprm.ledstate = 0;
   Save();
}

Назначение подпрограмм Save(), Load() и RestoreEEPROM() понятно из их названий. Все они работают с глобальной переменной eeprm типа Teeparam. Это структура данных, где хранится копия данных EEPROM. Save() сохраняет переменную eeprm в память EEPROM, записывая при этом корректную контрольную сумму данных. Load() загружает переменную eeprm из памяти EEPROM, делая при этом проверку - если контрольная сумма совпала, то Load() вернет true. RestoreEEPROM() восстанавливает содержимое EEPROM записывая туда значения по умолчанию. Все устроено логично и просто. Вот как используются подпрограммы в основном коде приложения (приведен кусок функции main из проекта [3]):

...
int main(void)
{
   u8 loaded = false;     //флаг успешной загрузки
 
   //настраиваем ножки 
   portsInit();
 
   //грузим настройки
   while (!loaded)
   {
      loaded = Load();
      if (!loaded)
      {
         //инициализируем EEPROM
         ON_RED();
         RestoreEEPROM();
         OFF_RED();
      }
   }
   eeprm.ledstate?ON_RED():OFF_RED();
 
   //включаем ватчдог
   wdt_enable(WDTO_1S);
...

[Ссылки]

1. AVR100: Accessing the EEPROM.
2. 141228avr100.zip - исходный код к даташиту AVR100, документация.
3. USB консоль для управления радиолюбительскими приборами.
4AVR103: режимы программирования EEPROM.
5. AVR104: буферизированная запись в EEPROM с управлением по прерыванию.