Программирование FLASH

Часто необходимо сохранять конфигурационные параметры или еще что либо при отключении питания. В stm32 для этой цели возможно использовать либо backup регистры либо встроенную flash память. 20 регистров backup домена по 2 байта часто недостаточно. Кроме того для поддержания бекап домена нужна отдельная батарейка. Флеш лишена этих недостатков, но имеет свои собственные.
Во-первых — стирать флеш можно только страницами. Во-вторых флеш имеет ограниченный ресурс записи. Поэтому хранить там нужно редко меняющиеся данные.
Для работы с флешем контроллер имеет интерфейс флеш памяти. Переменная FLASH используется для работы с этим интерфейсом из программы С.
Перед началом модификации флеш памяти интерфейс должен быть разблокирован. Это выполняется записью в регистр ключа зарезервированной последовательности:

#define FLASH_KEY1               ((uint32_t)0x45670123)
#define FLASH_KEY2               ((uint32_t)0xCDEF89AB)
  FLASH->KEYR = FLASH_KEY1;
  FLASH->KEYR = FLASH_KEY2;

Если последовательности оказались правильными интерфейс флеш памяти разблокируется, и становится возможной ее модификация. При неправильной последовательности интерфейс блокируется наглухо до перегрузки.
После работы с интерфейсом его рекомендуется заблокировать заново. Блокировка выполняется установкой бита блокировки FLASH_CR_LOCK регистра управления (CR — Control Register). В этом случае нет необходимости следить — заблокирована ли память или нет. Перед началом работы — разблокируем, по окончании — блокируем.
Стирание флеш памяти возможно только страницами. Размер страницы равен 1024 или 2048 байт в зависимости от типа контроллера. Для value line — это всегда 1024 байта. Для стирания необходимо сначала разблокировать интерфейс. Потом в управляющем регистре (CR) установить бит операции очистки страницы (FLASH_CR_PER — Page ERase).

  FLASH->CR |= FLASH_CR_PER;

В регистр адреса (AR — Address Register) нужно записать адрес начала стираемой страницы. Этот регистр имеет тип uint32_t. Если данные храняться в виде структуры, размещённой в начале страницы, то указатель на нее должен быть явно приведен к uint32_t. Возможно можно указать любой адрес из страницы — но я не пробовал.
После указания адреса нужно запустить собственно операцию удаления. Для этого устанавливается бит FLASH_CR_STRT (Start) управляющего регистра.

  FLASH->CR|= FLASH_CR_STRT;

Операция с flash памятью длится достаточно долго — десятки-сотни тактов процессора. Интерфейс не может выполнить следующую операцию до завершения предыдущей. Поэтому необходимо дождаться завершения операции. О выполнении операции сигнализирует бит занятости (FLASH_SR_BSY — BuSY) регистра статуса (SR — Status Register). Я провожу ожидание завершения в обычном цикле

while ((FLASH->SR & FLASH_SR_BSY) != 0 );

Внимание, тонкость! Бит операции (в данном случае FLASH_CR_PER) должен быть явно сброшен по завершении работы с интерфейсом флеш. Если этого не сделать, то при запросе другой операции (например записи) интерфейс отказывается работать!

  FLASH->CR &= ~FLASH_CR_PER;

Ниже приведен полный код стирания 2 страниц.

  /* Authorize the FPEC Access */
  FLASH->KEYR = FLASH_KEY1;
  FLASH->KEYR = FLASH_KEY2;

  /* Clear 2 page */
  FLASH->CR |= FLASH_CR_PER; /* Page erase */
  FLASH->AR = (uint32_t)&SavedDomain; 
  FLASH->CR|= FLASH_CR_STRT; /* Start erase */
  while ((FLASH->SR & FLASH_SR_BSY) != 0 ) /* Wait end of eraze */
    ;
  FLASH->CR |= FLASH_CR_PER;
  FLASH->AR = (uint32_t)((char*)&SavedDomain + 1024); /* The next page */
  FLASH->CR|= FLASH_CR_STRT;
  while ((FLASH->SR & FLASH_SR_BSY) != 0 )
    ;
  FLASH->CR &= ~FLASH_CR_PER; /* Page erase end */

  FLASH->CR |= FLASH_CR_LOCK; /* Lock the flash back */

Предполагается что настройки хранятся в структуре SavedDomain. Они занимают 2 страницы памяти. Поэтому стирается 2 страницы сразу.
По окончании стирания все биты страницы установлены. То есть любое целое число в этой странице станет равным 0xFF, 0xFFFF, 0xFFFFFFFF в зависимости от размера. Это является признаком «чистоты» данного места памяти. Запись возможна только в чистое место страницы.
Запись производится еще проще. Сначала интерфейс разблокируется. В управляющем регистре устанавливается код операции — программирование (FLASH_CR_PG — ProGramming). Операция записи по адресу, расположенному в странице, вызовет программирование флеш памяти записанным значением. Программировать память можно только 16 битными словами (uint16_t). Адрес, куда производится запись, также должен быть выровнен на 2 байта. Старт операции программирования начинается при записи. Для выполнения следующей операции записи нужно дождаться завершения предыдущей. Это выполняется проверкой бита занятости, так же как и при стирании. Содержимое записанного слова нужно сверить с оригиналом, что бы убедиться, что запись прошла успешно. По окончании программирования необходимого количества слов бит операции должен быть сброшен. Вот пример функции, выполняющей программирование:

void WriteFlash(void* Src, void* Dst, int Len)
{
  uint16_t* SrcW = (uint16_t*)Src;
  volatile uint16_t* DstW = (uint16_t*)Dst;

  FLASH->CR |= FLASH_CR_PG; /* Programm the flash */
  while (Len)
  {
    *DstW = *SrcW;
    while ((FLASH->SR & FLASH_SR_BSY) != 0 )
      ;
    if (*DstW != *SrcW )
    {
      goto EndPrg;
    }
    DstW++;
    SrcW++;
    Len = Len - sizeof(uint16_t);
  }
EndPrg:
  FLASH->CR &= ~FLASH_CR_PG; /* Reset the flag back !!!! */
}

В данном коде отсутствует разблокировка и блокировка памяти. Она выполняется так же, как и при стирании.
Я привел описание двух основных функций работы с памятью — стиранием и записью. Более подробно функциональность можно посмотреть в документе PM0042 STM32F10xxx Flash programming.
Ну и напоследок самое интересное. Как я уже говорил я использую флеш память для хранения конфигурации программы. Для этого в программе я объявляю переменную типа структуры, содержащей все необходимые сохраняемые параметры. Эта переменная размещается в флеш памяти.
Размещение в флеш памяти обеспечивается модификатором const. Но кроме того, эта переменная должна иметь начало строго в начале страницы.
Это можно выполнить указанием явного адреса. В uVision это выполняется очень просто. Например так

const SavedDomain_t SavedDomain __attribute__ ((at(FLASH_BASE+1024*14))) = {{1.0,0,1.0,0}};

В gcc с этим все сложнее. Можно не указывать явного адреса. Можно указать только выравнивание на страницу в 1024 байт. Этот вариант самый простой:

const SavedDomain_t SavedDomain __attribute__ ((aligned(1024))) = {{1.0,0,1.0,0}};

А можно разместить переменную в выделенном сегменте данных:

const SavedDomain_t SavedDomain __attribute__ ((section(".eb0rodata"))) = {{1.0,0,1.0,0}};

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

MEMORY^
{^
  RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 4K  /* also change _estack below */^
  FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 14K^
  FLASHB1 (rx) : ORIGIN = 0x00000000, LENGTH = 0^
  EXTMEMB0 (rx) : ORIGIN = 0x8003800, LENGTH = 2K^
  EXTMEMB1 (rx) : ORIGIN = 0x00000000, LENGTH = 0^
  EXTMEMB2 (rx) : ORIGIN = 0x00000000, LENGTH = 0^
  EXTMEMB3 (rx) : ORIGIN = 0x00000000, LENGTH = 0^
}^

Преимуществом методов align и отдельного сегмента является отслеживание компоновщиком выхода за пределы флеш. Например при использовании метода align компоновщик gcc выдал ошибку при переполнении памяти. На keil я не проверял.
Во всех случаях размер структуры должен быть кратен странице. Иначе возможно стирание кода.
Универсальным методом является определение переменной типа указателя на заданный адрес с соответствующим приведением типа. Например вот так:

const SavedDomain_t* SavedDomain = (SavedDomain_t*)(FLASH_BASE+1024*14);

Можно сделать даже еще проще

#define SavedDomain (*(SavedDomain_t*)(FLASH_BASE+1024*14));

Тогда к структуре данных можно обращаться через. При использовании проекта из нескольких файлов этот макрос должен быть объявлен в одном из заголовочных файлов. Этот вариант не отслеживается компоновщиком. Нужно следить по листингу, что бы код программы не наезжал на стираемые сегменты.
В моей программе сохраняемые данные делятся на редко изменяемые (около 200 байт) и сохраняемые при каждом выключении (4байта). При включении прибора я их копирую в оперативную память. Редко изменяемые данные сохраняются только по явной команде в начале страницы флеш. Это должно происходить достаточно редко. Для увеличения жизни флеша для часто изменяемых данных я использую массив элементов по 4 байта каждый. Он занимает остаток страницы и целую следующую. Актуальные данные располагаются в последнем заполненном элементе массива. Определить его можно из того факта, что первый свободный элемент массива содержит все биты 1. Сохранение производится именно в этот элемент массива. Если не осталось свободных элементов массива, то производится стирание обеих страниц, запись редко меняемых данных и запись в первый элемент массива часто меняемых данных. Таким образом износ можно уменьшить примерно в 2000/4 = 500 раз.
При таком подходе существует момент времени, между стиранием данных и их записью назад, когда настройки могут быть потеряны при пропадании питания. Более надежный вариант описан в AN2594. Там используется несколько страниц, стирается только одна, на остальных остаются достоверные данные. Их стирание производится только после копирования в освобождённое место.

Ну и напоследок небольшое отступление. ST разработала достаточно удобную систему обращения к регистрам периферии. Каждому модулю поставлена в соответствие переменная, указывающая на структуру, описывающую так сказать единицу периферии. В нашем случае эта переменная имеет название FLASH. Поля этой структуры соответствуют названию регистров даташита. Поэтому обращение например к управляющему регистру может быть выполнено как обращение к полю структуры по указателю. Биты регистра имеют префикс, состоящий из названия периферии, названия регистра и названия бита. Например бит занятости BSY регистра статуса SR модуля FLASH имеет название FLASH_SR_BSY. Это правило работает почти всегда. Я пока нашел только одно исключение — это каналы прямого доступа в память. Может быть есть и еще. Так что если это правило не проходит — нужно смотреть файл stm32f10x.h. Именно поэтому я привожу полные английские названия регистров и битов. Все таки запоминать наборы букв, когда они превращаются в нечто осмысленное, гораздо легче.
  • +6
  • 27 апреля 2011, 20:15
  • OlegG

Комментарии (48)

RSS свернуть / развернуть
«Поэтому хранить там нуДно редко меняющиеся данные.»
наверное там очепятка и правильное слово «Нужно»
0
А можно подключить внешний EEPROM 24Cxx (i2c) или 93Cxx (microwire). У них ресурс побольше будет, чем у встроенной памяти.
0
никто не спорит, но для этого требуется внешняя микросхема, что есть место и деньги. Цель же этой статьи, на сколько я понял, дать возможность использовать то что есть и так, и что «не просит кушать»
0
Да, и никто не заставляет вас использовать этот код только для настроек. Этот код пожно портировать для самоперепрошивки, что тоже ценно.
0
goto EndPrg;
Руки оторвать!!! break вам на что дан ??
+1
Согласен насчет break. Против садизма. Главное не воспринимать отсутствие goto как догму.
0
Интересная статья на тему Goto в микроконтроллерах:
0
0
Да, надо было мне всё-таки весь блок запостить. Ведь знал же что найдутся возникатели.
0
кстати там упоминается очень интересный проект
Active-HDL
www.aldec.com/activehdl/
Никоим образом не реклама. Пользовался кто? выглядит (по скринам) неплохо.
0
Вариант с сегментами на мой взгляд более правильный, т.к. гарантирует вам что на той же странице не будет расположен код программы, какой бы большой она не стала, и она не будет стерта «случайно» при перезаписи конфига.
Остальные способы имеют право на сужествование, но могут быть ненадежны.
0
Ну можно еще забить структуру полями в духе dummy/reserved до размера странички.
0
Да, тоже вариант. не подумал про это…

А можно ещё остальную часть страницы заливать в оперативку, а после стирания возвращать на место. Лишь бы в остальную часть страницы не помала функция записи :)
0
Лишь бы ресет не произошел между стиранием и записью обратно.
0
Интересная статья, но не совсем понял про ключи. Приведенные в примере последовательности одинаковы для всех чипов или как-то задаются пользователем? Я пока сам не добрался до флэша…
0
Всегда одинаковы
0
специально под это дело STM, выпустила апноты, например для STM32F10x
AN2594 — Application note
EEPROM emulation in STM32F10x microcontrollers

Many applications require EEPROM (electrically erasable programmable read-only
memory) for non-volatile data storage. For low-cost purposes, the STM32F10x devices do
not use EEPROM. Instead, they implement EEPROM emulation using the embedded Flash
memory.
0
  • avatar
  • ZiB
  • 28 апреля 2011, 07:49
0
Апноуты это хорошо, но когда объясняют по русски, то как то роднее, что ли.
0
В AN2594 основное внимание уделено специальной организации данных во флеш для повышения использования ресурса. По теме этого документа более подходящий документ упомянут в статье
0
я сам в подробности не вдавался, дал ссылку на апнот для полноты картины.
0
Еще этот AN о надежности. Для этого заводится несколько страниц, что бы во время стирания не пропали данные.
0
Внимание, тонкость! Бит операции (в данном случае FLASH_CR_PER) должен быть явно сброшен по завершении работы с интерфейсом флеш. Если этого не сделать, то при запросе другой операции (например записи) интерфейс отказывается работать!
 
FLASH->CR &= ~FLASH_CR_PER;
и
EndPrg:
  FLASH->CR &= ~FLASH_CR_PG; /* Reset the flag back !!!! 
Это опытным путем узнали? В PM0063 ничего про это не нашел.
0
Если подумать, то можно вывести и логическим путем. Мы установили бит операции и начали выполнять запись или стирание. Никто кроме программиста не знает сколько он операций будет выполнять. так что он же и должен позаботится о отмене операции (стирании бита). Правда бит мог бы и сбрасываться сам по выполнении 1 операции. Вот это и выявлено экспериментально.
0
Тоже побаловался с несбросом флага записи FLASH_CR_PG. Интересно, но было три разных результата. Немного менял код и, если не сбросить FLASH_CR_PG до включения блокировки, контроллер всегда уходил на HardFault_Handler в разное время:
1. при первой же попытке сбросить флаг программирования FLASH_CR_PG
2. при первой же попытке ввести ключ в FLASH_KEYR
3. при попытке записи во флеш (причем перед этим прокатил сброс FLASH_CR_PG и запись обоих ключей)
Странновато как то ведет он себя. По настроению что ли
0
И еще чисто по С вопрос:
Выражения
void* p;
и
void *p;
одинаковы (да-да-да, меня долго этот вопрос парил и я начитался всякого, типа вот такого accu.org/index.php/journals/1445 )
Тогда в этом месте

void WriteFlash(void* Src, void* Dst, int Len)
{
  uint16_t* SrcW = (uint16_t*)Src;
...

параметры void* Src и void* Dst являются указателями на неопределенный объект и прежде чем присвоить *Src к *SrcW, мы приводим Src к типу uint16_t*. Тогда, получается можно записать и так:
uint16_t* SrcW = (uint16_t)*Src;

Ан нет. Уже не робит. Вот объясните мне в чем подвох?
0
Это несколько не по теме. Задаю наводящий вопрос.
Какую корзинку Вы будете брать, если вам сказали сходить на склад и взять, то что лежит на 3 сталлаже. Там маковое зёрнышко или автомобиль?
0
В приведенном примере выражения имеют совершенно разный смысл.

Выражение (uint16_t*)Src – это приведение типа, указатель на void приводится к указателю на uint16_t.

(uint16_t)*Src – означает «взять значение по указателю (адресу) Src и привести результат к uint16_t». Для типа void операция * («взять значение по указателю») недопустима (не определена).
0
Спасибо. Теперь я еще на сантиметр ближе с Си))
0
я особо не заморачивался с выделением сегментов, работал с адресом Flash напрямую и выровненными на 32бита записями в последней странице. При каждом вызове config_write содержимое config пишется в следующую запись, при переполнении стирается вся страница, и запись начинается с нуля:
#define FLASH_START_ADDR       0x08000000UL
#define FLASH_PAGE_SIZE        1024
#define FLASH_PAGES            16
#define FLASH_WORD_SIZE        4UL
#define FLASH_ERASED_STATE     0xFF
#define CONFIG_START_ADDR      (FLASH_START_ADDR + ((FLASH_PAGES - 1) * FLASH_PAGE_SIZE))
#define CONFIG_WORDS           (CONFIG_RECORD_SIZE / FLASH_WORD_SIZE)
#define CONFIG_RECORD_SIZE     ((sizeof(config) + FLASH_WORD_SIZE - 1) & (~(FLASH_WORD_SIZE - 1)))
#define CONFIG_RECORDS         (FLASH_PAGE_SIZE / CONFIG_RECORD_SIZE)
#define CONFIG_RECORD_ADDR(N)  ((struct config_struct *)(CONFIG_START_ADDR + ((N) * CONFIG_RECORD_SIZE)))

U16 config_record; //current record
#define PARAMS_NUM 6
const U8 param_name[];
struct config_struct {
	U8 params[PARAMS_NUM];
	U16 crc;
} config;
//calculate config CRC
U16 config_calc_crc(void *addr) {
	return crc16_modbus_buf((U8 *)addr, sizeof(config)-sizeof(config.crc), CRC16_MODBUS_INIT);
}

//check config validity
U8 config_check_crc(void *addr) {
	return (((struct config_struct *)addr)->crc == config_calc_crc(addr));
}

//find last used (valid) record. If not found, returns 0xFFFF;
U16 config_last_record(void) {
	struct config_struct *cfg = (void *)CONFIG_START_ADDR;
	for (U16 i=0; i<CONFIG_RECORDS; i++) {
		if (!config_check_crc(cfg)) return i - 1;
		cfg++;
	}
	return CONFIG_RECORDS - 1;
}

//clear configuration (fill with default values)
void config_clear(void) {
	memset(&config, 0, sizeof(config));
}

//read last valid configuration from EEPROM
U8 config_read(void) {
	if (config_record == 0xFFFF) {
		config_clear();
		return 0;
	} else {
		config = *CONFIG_RECORD_ADDR(config_record);
		return 1;
	}
}

//initialize config
void setup_config(void) {
	config_record = config_last_record();
	config_read();
	config1 = config;
}

//write config to next record
void config_write(void) {
	config.crc = config_calc_crc(&config);
	FLASH_Unlock();
	config_record++;
	if (config_record >= CONFIG_RECORDS) {
		config_record = 0;
		FLASH_ErasePage(CONFIG_START_ADDR);
	}
	U32 *source_addr = (void *)&config;
	U32 *dest_addr = (void *)CONFIG_RECORD_ADDR(config_record);
	for (U16 i=0; i<CONFIG_WORDS; i++) {
		FLASH_ProgramWord((U32 )dest_addr, *source_addr);
		source_addr++;
		dest_addr++;
	}
	FLASH_Lock();
}
0
В случае использования только одной страницы если между стиранием сегмента и записью конфигурации произойдет отключение питания, то вся конфигурация потеряется. Если использовать несколько страниц, то при стирании одной, в остальных все равно останутся данные. Если конфигурационные параметры некритичны, то можно все хранить и в одной странице. Если же после стирания конфигурации все перестанет работать, то нужно позаботится об сохранности конфигурации в любых случаях
0
Если объявить массив таким образом:
const SavedDomain_t SavedDomain __attribute__ ((aligned(1024))) = {{1.0,0,1.0,0}};

то он будет находиться во Flash, однако доступен только на чтение, записать туда не даст компилятор
0
Его нужно обмануть явным преобразованием типов.
0
Сделал как вы сказали, заработало! Вполне удобно получается
0
Извиняюсь. А если при программировании в STM32 ST-LINK Utility выставить Read Out Protection = Enable (включить защиту от считывания ), то код:

#define FLASH_KEY1               ((uint32_t)0x45670123)
#define FLASH_KEY2               ((uint32_t)0xCDEF89AB)
  FLASH->KEYR = FLASH_KEY1;
  FLASH->KEYR = FLASH_KEY2;
сможет её временно разблокировать?
0
Неа. Эти ключи не снимают защиту от считывания. Извне всё равно нельзя будет подключиться отладчиком. Однако, запись во FLASH изнутри программы работает независимо от Read Out Protection. Короче, Read Out Protection только отключает отладочные интерфейсы и встроенный загрузчик. Запись-же этих ключей только разрешает запись флеша из программы.
0
Большое спасибо. Все понял.
0
Что-то я не догоняю
если написать так
const SavedDomain_t SavedDomain __attribute__ ((at(FLASH_BASE+1024*14))) = {{1.0,0,1.0,0}};
то при каждом сбросе структура будет специализироваться указанными значениями, и чего толку в них сохранять.
0
специализироваться = инициализироваться
0
Уточнение — при каждой прошивке.
0
хмм. Ясно
0
Не пойму одного момента: если мне нужно записать 1 байт на заполненную в половину страницу, мне нужно всё-равно её перед записью стереть полностью, или стирать нужно только при полном заполнении, когда не осталось «нетронутых» ячеек памяти?
0
2 байта мин порция. Пишу в нетронутую область пока не останется там совсем места, потом стираю всю страницу.
0
День добрый. У меня такой вопрос. Использую функцию HAL_FLASHEx_Erase_IT на процессоре f407. Всё работает, однако когда вызывается данная функция, временно перестают работать все прерывания, даже если их приоритет выше, чем у задачи, из под которой была вызвана функция стирания. В чем тут причина такого странного поведения и как с этим бороться? Мне некоторые процессы нельзя останавливать. Как сделать так, чтобы данные стирались в фоне и этот процесс можно было притормозить другой задачей? Заранее спасибо за ответ
0
Выполнять код из RAM в момент работы с флешем. Ну или использовать МК с разделением флеша на две области (какие-то f4xx такие есть), из одной можно выполнять код пока в другую делается запись/стирание.
0
А можно по подробнее
1. В чем причина того, что работая с флешем (стирая данные), у меня перестают работать прерывания и их приоритеты? Почему нельзя стирать данные и выполнять другие задачи параллельно?
2. Как выполнять код из RAM? Как это делается технически. Если можно, покажите на коде
Заранее спасибо!
0
1. Контроллер флеша однозадачный. Он не может стирать и выбирать данные одновременно. Данные флеша — это код программы.
2. Погуглите
0
Подскажите хотя бы, где это прописывается. Даже не представляю, что конкретно искать и как это называется
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.