Введение
Если вы читали предыдущий материал, то знаете как объявлять, читать и записывать данные в EEPROM. Но давайте разберемся, как же на самом деле происходят эти операции и что от нас скрывает компилятор. Это позволит лучше понимать работу микроконтроллера, и при желании написать свои специфические функции для работы с EEPROM.
Регистры для работы с EEPROM
Для работы с EEPROM используются три регистра ввода/вывода: регистр адреса, регистр данных и регистр управления.
Регистр адреса EEAR (EEPROM Address Register) предназначен для адресации однобайтной ячейки EEPROM памяти, к которой будет производиться обращение.
Для полной адресации хотя бы минимального объема EEPROM памяти (512-ти байт) требуется 9 разрядов (2 в 9-ой степени = 512), поэтому регистр EEAR является 16-ти разрядным и физически расположен в двух регистрах ввода/вывода – EEARH и EEARL. Регистр EEARL полностью доступен для записи/чтения. А в регистре EEARH для записи/чтения доступны только младшие разряды, используемые для адресации. Остальные разряды доступны только для чтения и содержат «0».
Для чтения/записи используется один и тот же регистр данных – EEDR (EEPROM Data Register). Если выполняется процедура записи, мы должны поместить в EEDR байт данных, если выполняется процедура чтения, прочитать байт данных из EEDR.
Регистр управления EECR (EEPROM Control Register) предназначен для управления доступом к EEPROM.
Бит EERIE (EEPROM Ready Interrupt Enable) – разрешение/запрещение прерывания по событию готовности EEPROM. Если бит EERIE установлен в 1, установлен флаг глобального разрешения прерываний (бит I регистра SREG) и бит EEWE очищен, то микроконтроллер будет генерировать прерывание “EEPROM Ready Interrupt”.
Бит EEMWE (EEPROM Master Wirte Enable) - главное разрешение записи. Если в течении 4-ех тактов после установки этого бита, устанавливается бит EEWE, то микроконтроллер выполняет запись в EEPROM. Бит EEMWE аппаратно сбрасывается в ноль после 4-ех периодов тактовой частоты.
Бит EEWE (EEPROM Write Enable) - разрешение записи. Этот бит выполняет роль стартового сигнала записи в EEPROM. Когда установлен адрес, данные и бит EEMWE, установка бита EEWE инициирует запись в EEPROM. Бит EEWE должен быть установлен в течении 4-ех тактов после установки EEMWE. Если это произойдет позже, то запись в EEPROM не будет произведена. Чтобы избежать возможных проблем, рекомендуется запрещать прерывания на время выполнения записи в EEPROM.
Бит EEWE аппаратно сбрасывается, после завершения операции записи. Поэтому перед каждой операцией записи в EEPROM нужно проверять состояние этого разряда.
Бит EERE (EEPROM Read Enable) - разрешение чтения. Установка бита EERE инициирует процесс чтения из EEPROM. Перед каждым циклом чтения нужно проверять состояние разряда EEWE, если выполняется операция записи, то чтение из EEPROM не даст результата.
Низкоуровневая запись в EEPROM на Си
Разберем вариант низкоуровневой работы с EEPROM, то есть без использования встроенных макросов/функций компиляторов. Допустим, мы объявили в EEPROM переменную, как нам изменить ее значение из своего приложения?
Процедура записи в EEPROM состоит из следующих шагов:
1. Ожидаем готовности EEPROM, опрашивая бит EEWE регистра EECR.
2. Устанавливаем адрес в регистре EEAR.
3. Записываем байт данных в регистр EEDR.
4. Устанавливаем основной флаг разрешения записи EEMWE регистра EECE
5. Устанавливаем флаг разрешения записи EEWE регистра EECE
Тут есть два нюанса.
Запись в EEPROM не может производиться одновременно с записью во флэш память. Если ваше приложение в какие то моменты пишет во флэш, то перед записью EEPROM`а, проверьте флаг SPMEN регистра SPMCR. Он должен быть сброшен в ноль. Более подробную информацию смотрите в даташите.
Второй нюанс связан с флагом EEWE. Он должен быть установлен в течении 4 циклов после флага EEMWE. Если в проекте используются прерывания, то на каком-то из этапов этой последовательности их нужно запрещать. Это можно сделать или в самом начале или перед установкой флага EEMWE.
В Си коде описанная последовательность будет выглядеть так:
while (EECR & (1<<EEWE));
EEAR = adr;
EEDR = value;
EECR |= (1<<EEMWE);
EECR |= (1<<EEWE);
где adr- это адрес байта в EEPROM, value - данные для записи, EEAR - 16-ти разрядный регистр адреса.
Использовать абсолютный адрес байтов EEPROM не удобно и чаще всего адрес берется у переменной, объявленной в EEPROM. Код в этом случае будет таким:
while (EECR & (1<<EEWE));
EEAR = (uint16_t) &data;
EEDR = value;
EECR |= (1<<EEMWE);
EECR |= (1<<EEWE);
где data - переменная объявленная в EEPROM, а & - оператор взятия адреса.
Этот код будет одинаковым для IAR AVR, CodeVison и Atmel Studio, а если оформить его в виде функции, то возникнут отличия.
Для IAR AVR и CodeVision AVR:
void EEPROM_WriteByte(uint8_t __eeprom*adr, uint8_t value)
{
while (EECR & (1<<EEWE));
EEAR = (uint16_t)adr;
EEDR =value;
EECR |= (1<<EEMWE);
EECR |= (1<<EEWE);
}
Пример использования:
__eeprom uint8_t data;
...
EEPROM_WriteByte(&data, 125);
Для Atmel Studio:
void EEPROM_WritByte(uint8_t *adr, uint8_t value)
{
while (EECR & (1<<EEWE));
EEAR = (uint16_t)adr;
EEDR =value;
EECR |= (1<<EEMWE);
EECR |= (1<<EEWE);
}
Пример использования:
uint8_t data EEMEM;
...
EEPROM_WriteByte(&data, 125);
Вариант функции записи с запрещением прерываний будет выглядеть следующим образом.
Для IAR AVR и CodeVision AVR:
#define I_FLAG 7
void EEPROM_WriteByte(uint8_t __eeprom*adr, uint8_t value)
{
uint8_t save;
while (EECR & (1<<EEWE));
EEAR = (uint16_t)adr;
EEDR =value;
save = SREG;
SREG &= ~(1 << I_FLAG);
EECR |= (1<<EEMWE);
EECR |= (1<<EEWE);
SREG = save;
}
Низкоуровневое чтение из EEPROM на Си
Процедура чтения EEPROM состоит из следующих шагов:
1. Ожидаем готовность EEPROM, опрашивая бит EEWE регистра EECR.
2. Устанавливаем адрес в регистре EEAR.
4. Устанавливаем флаг разрешения чтения EERE регистра EECR
5. Считываем содержимое регистра данных EEDR
На Си описанная последовательность будет выглядеть так:
while (EECR & (1<<EEWE));
EEAR = adr;
EECR |= (1<<EERE);
value = EEDR;
Доступ к объявленной в EEPROM переменной:
while (EECR & (1<<EEWE));
EEAR = (uint16_t)&data;
EECR |= (1<<EERE);
value = EEDR;
Код оформленный в виде функции будет выглядеть следующим образом.
Для IAR AVR и CodeVision AVR:
uint8_t EEPROM_ReadByte(uint8_t __eeprom*adr)
{
while (EECR & (1<<EEWE));
EEAR = (uint16_t)adr;
EECR |= (1<<EERE);
return EEDR;
}
Пример использования:
__eeprom uint8_t data;
uint8_t tmp;
...
tmp = EEPROM_ReadByte(&data);
Для Atmel Studio:
uint8_t EEPROM_ReadByte(uint8_t *adr)
{
while (EECR & (1<<EEWE));
EEAR = (uint16_t)adr;
EECR |= (1<<EERE);
return EEDR;
}
Пример использования:
uint8_t data EEMEM;
uint8_t tmp;
...
tmp = EEPROM_ReadByte(&data);
Продолжение следует...
Comments
Есть такая проблема, особенно для WDT актуальна.
Quoting Peter:
Можно по-другому - запрещать для файла или функции Cross call optimizaion (ключ --no_cross_call ). Тогда компилятор не будет искать одинаковые куски кода.
Не нужно читать или писать в EEPROM из основной программы и прерываний.
Если программа написано с использованием RTOS, то для доступа к EEPROM надо использовать мьютексы.
Quoting Peter:
Тип вполне допустим, хотя бы для объявления переменных такого вида.
1. Кто сказал что нельзя читать? Почему нельзя ЧИТАТЬ-ТО? Это не писать же! Это быстро же! Не согласен.
2. Про РТОС откуда появилось? Не будем размазываться.
3. (и самое неприятное): Если переменная объявлена с квалификатором __eeprom - она немедленно затеряется среди других обычных переменных проекта, потому как по виду ничем не отличается от них. Поэтому лучший вариант - это создание и применение специальной функции.
Безопасно. Наглядно. Сердито. Уж нечаянно не применишь.
Макросы? Ну, возможно, макросы кто их любит.
Я не говорил, что нельзя читать или писать. Это просто нежелательно, так как может вызвать проблемы-сбои в работе программы.
Quoting Peter:
Быстро - это не аргумент.
Представьте ситуацию: в основной программе запущена запись байта в EEPROM, и в это же время прерывание выполняет чтение из другой ячейки.
Quoting Peter:
Ситуация с RTOS аналогична предыдущему случаю, только потоков где выполняется работа с EEPROM может быть больше.
Quoting Peter:
Почему затеряется?
Она отличается спецификатором __eeprom. Он заметен.
Quoting Peter:
Я тоже использую самописные функции чтения-записи, но переменные объявляю со спецификатором __eeprom. Так удобней, имхо.
Code:
\ ?Subroutine2:
\ 00000000 9AE2 SBI 0x1C, 0x02
\ 00000002 9AE1 SBI 0x1C, 0x01
Поэтому однозначно: оптимизацию давить.
Code:
CLI
SBI EECR,EEMWE
SBI EECR,EEWE
OUT SREG,T0
2. __eeprom без ручного запрета прерываний использовать нельзя, потому как НЕ ЗАЩИЩАЕТ EEAR и EEDR. Что толку защищать 4 такта, когда в обработчике прерывании адрес EEPROM может сбиться и запись произойдет не туда куда надо. Лучше писать свои.
//////
void EEPROM_WriteByt e(unsigned int adr, unsigned char value)
{
while (EECR & (1 < < EEWE));
EEAR = adr;
EEDR =value;
EECR |= (1 < < EEMWE);
EECR |= (1 < < EEWE);
}
void EEPROM_WriteByt eIRQ(unsigned int adr, unsigned char value)
{
while (EECR & (1 < < EEWE));
EEAR = adr;
EEDR =value;
EECR |= ( (1 < < EEMWE) | (1 < < EERIE));
EECR |= (1 < < EEWE);
}
//////// Вот во что превращается запись для EEPROM_WriteByteIRQ:
IN R16, 0x1C
ORI R16, 0x0C
OUT 0x1C, R16
RJMP ?Subroutine0 ; !!!!!!!!
2. Чтобы защитить eear и eedr нужно их в прерывании сохранять, а перед выходом восстанавливать .
1. Случаи с авариями самолётов и ракет из-за сбоев ПО - тоже редкие события. Редкие но меткие)) Хотите нечто подобного в своих разработках? Это дело времени - закон подлости работает всегда))
2. Квалификатор __eeprom мало того что все равно требует геморроя с сохранением / восстановлением двух IO-регистров, так еще и не совместим со студией. Не лучше ли иметь свою надёжную версию?
#...
#include
#define EEPROM_SECTION __attribute__ ((section (".eeprom")))
EEMEM uint8_t Mem1=0;
EEMEM uint8_t Mem2=0;
EEMEM uint8_t Mem3=0;
EEMEM uint8_t Mem4=0;
unsigned char x1;
unsigned char x2;
unsigned char x3;
unsigned char x4;
void Memory()
{
eeprom_update_byte(&Mem1,x1);
eeprom_update_byte(&Mem2,x2);
eeprom_update_byte(&Mem3,x3);
eeprom_update_byte(&Mem4,x4);
}
int main()
{
x1=eeprom_read_byte(&Mem1);
y1=eeprom_read_byte(&Mem2);
x2=eeprom_read_byte(&Mem3);
x4=eeprom_read_byte(&Mem4);
.....;
while(1)
{
.....;
}
}
1. Борьба с известной детской болезнью АВР, когда при изменении напряжения питания, контроллер EEPROM может (не обязатеьно даже при программном доступе к нему, а и просто ни с того ни с сего) произвести несанкционирова нную запись. Записывает, естественно, в ячейку, на которую указывает EEAR. Лечение(скорее не лечение, а паллиатив): самом конце функции (макроса) доступа к EEPROM писать строку EEAR=0 и не использовать эту ячейку в своей работе. Пусть себе портится нулевая ячейка. И пусть себе отчаянные головы используют квалификатор переменных __eeprom, который этого действия, конечно, не делает.
2. Установка BODEN/BODLEVEL, как у Вас. Тут какие грабли? Только потребление немного возрастёт... Ну, и конечно устанавливать уровень срабатывания BODLEVEL таким, чтобы соответствовал тактовой частоте процессора. То есть не ставить BODLEVEL на уровень срабатывания 1.8 Вольт, если у Вас кварц 16МГц. :)
Ни разу не видел, чтобы запись сама стартовала. Правда я всегда BOD разрешаю.
Quoting Peter:
__eeprom делает только одно действие - указывает компилятору в какой области памяти размещена переменная. Большего от него не требуется.
Quoting Peter:
Тут согласен.
Однако, если Вы не слышали про эту особенность - почитайте http://www.avrfreaks.net/index.php?name=PNphpBB2&file=printview&t=75638 или просто погуглите на тему: AVR EEPROM портится EEAR и т.п.
И еще: не понял смысла объявления __eeprom, если к ней обращаетесь самодельными функциями. Для меня - нет и не должно быть переменных __eeprom, потому как они не являются переменными, которые можно просто a=eeval; eeval=b; b=&eeval; Это слишком легкомысленно на мой взгляд. Для меня "переменная EEPROM" - это группа мажорированных ячеек EEPROM (с определенными адресами, размещаемые неблизко друг от друга), доступ к которым осуществляется специальными функциями.
Тут не обсуждать надо, а обеспечить чтобы процессор работал в тех условиях, где его работоспособность гарантируется.
Quoting Peter:
Не попадались такие случаи, программа с ошибками может портить - было у меня пару раз такое, но чтобы само - как-то не верится.
Quoting Peter:
Смысл простой - каждая eeprom-переменная будет расположена по уникальному адресу, не надо самому об этом думать. Компилятор обеспечит.
Quoting Peter:
Зачем же так строго?
Quoting Peter:
В мажорировании иногда есть смысл, но применять его для каждого байта на мой взгляд как-то слишком. Но это уже от задачи зависит и от характера самих данных.
Если в ячейке хранится какая-нибудь настройка, которую изменяют раз в несколько лет, то как показывает практика, ничего с ней не случится. Тут мажорирование не нужно.
Но это зависит от условий использования аппаратуры - может у вас приборы работают в условиях радиации или сильных эл.-магнитных помех?
К сожалению в кристалл Attiny13 в 1Кб Flash такие функции не записать, просто места нет. В него и так вместил программу, которую фактически все делают на 2Kб Flash. А так как ГОСТ стандарт подтвердил работаспособнос ть приборов сигнализации на маленьком кристалле. То пришлось лечить эту уязвимость EEPROM, только выставлением нужных Fuse. Приборы сигнализации, с таким лечением работают без сбоев уже 2 года. Но одну вещь я так и не осмыслил, за счёт чего портились данные EEPROM, (с точки зрения электрических процессов в самом кристалле). Ведь и в первом случаее до выставления BOODLEVEL и во втором случае после выставления BOODLEVEL, процессор отключается при падении напряжения до определённого критического уровня. Почему стабильность работы EEPROM стала зависить от этого BOODLEVEL, когда поднял я режим работоспособнос ти кристалла до 4,7V?
Тогда не будет Вас поджидать северный пушной зверек в самом интересном месте...
Зачем? Что даст сохранение EEAR и EEDR? От какого сбоя спасёт?
По сути EEAR и EEDR это такой-же глобальный ресурс как CREG и поступать при его изменении желательно так-же
Обычно в прерываниях и одновременно в основной программе eeprom не читают. У вас не так? Как тогда организуете этот процесс?
Quoting ArtemKAD:
Допустим основная программа запустила запись ячейки eeprom и сразу после этого возникло прерывание, которое должно прочитать другую ячейку eeprom. Как это работать будет? Чем поможет сохранение EEAR и EEDR?
Это возможно только если Вы вообще в прерывании EEPROM не читаете. Иначе "вместе" возможно.
Quoting _Артём_:
Если уже запустила - т.е. выполнила команду записи в регистр EECR программа проболтается в прерывании до завершения записи eeprom на цикле
while (EECR & (1
Или вы ещё и пишете в eeeprom в прерывании?
Событие редкое, почему и не повисеть? Если читать в ОЗУ, то надо постоянно синхронизировать переменные. А смысл в такой работе?
Quoting _Артём_:
Бывало и такое. Если вся обработка радиоканала происходит в прерываниях, не вижу причин почему там-же не сохранять новые данные по ключам и результатам.
Что же это за прерывание на несколько миллисекунд? Как-то неправильно это.
Quoting ArtemKAD:
Смысл - синхронизировать значения. Но это можно делать нечасто.
Quoting ArtemKAD:
Как-то это идеологически неправильно. имхо.
Там где использовал - было правильно. Правда надо добавить, что в том прерывании прежде чем заниматься столь длительными процедурами(и криптография и сохранение) остальные прерывания таки разрешались.
PS. Сейчас конечно сделал бы иначе, но что было то было...
Code:
while (EECR & (1<<EEWE));
А если попало в прерывание на границе записи EEAR или EEDR, сперва прочитает ячейку, а потом корректно продолжит процедуру записи. Если бы второй вариант был без сохранения этих регистров, могло после возврата из прерывания записать не то и не туда
Уже перешёл на xmega. Чем там отличаются приёмы работы с eeprom? Что такого приритетный контроллер прерываний вносит нового? Всё также делаю - чтение/запись в eeprom только в основной программе.
Если по вашему делать (ждать конца записи в прерывании), то получается блокировка уровня (или нескольких) на время записи. Не годится такое.
Самое любопытное, что в связи с некоторым глюком первых ревизий xMega в Atmel-овской процедуре записи EEPROM примерно так и делают. Готовят данные для записи, запрещают все прерывания кроме наивысшего уровня и усыпляют камень до завершения записи (собственного прерывания EEPROM).
ФКурсе, но то дела минувших дней...и тд. Сейчас не актуально.
Но как несколько уровней приоритета прерываний влияют на работу с eeprom вы так и не уточнили.
По разному: кто делает, а кто и нет.
Было пару сотен xmeg A3 rev.B. Не делали такой процедуры - такая eeprom неработоспособн а. Поставили AT25 на 1 КБ. На 10-20 центов дороже, но зато правильней и быстрей работает ещё к тому же.
Да так-же если на нескольких уровнях читать содержимое EEPROM надо или сохранять/восст анавливать регистры или делать "критическую секцию" на каждом уровне или вообще не использовать EEPROM в прерываниях...
1.Все переменные потенциально нуждающиеся в сохранении в EEPROM, во время основного цикла хранятся в SRAM.
2.Используется внешний монитор питания который в случае снижения питающего напряжения подает сигнал на пин INT0 обладающий высшим приоритетом прерывания после RESET. В обработчик помещаются все необходимыемеро приятия для сохранения вышеупомянутых переменных хоть по две, хоть по три копии из SRAM в EEPROM. И будет всем счастье, причем не одно. Во первых не будут расходоваться лишний раз цыклы перезаписи. Во вторых во время работы основного цикла перепысывай хоть по сто раз свои данные без всяких флагов и т.п. переживаний. И в третьих, если питание пропало или падает мне уже без разницы сколько я буду занят в этом самом прерывании, устройство не работает.
Теперь как это сделать. Ставится кондер большой емкости. После команды внешнего монитора питания вся схема отключается, проц питается от кондера (какое то время, необходимо расчитать емкость) выполняет приоритетную процедуру по внешнему прерыванию и сохраняет все необходимые данные. Затем как по учебнику нужно чтобы монитор инициировал "твердый" сброс и ПАВЕР ОФФ.
вуаля.
Оказывается такая тема используется вдоль и поперек. Кому интересно почитайте по супервизорам.
RSS feed for comments to this post