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

Кроме, собственно, встроенной flash-памяти, я также немного расскажу и о оперативной памяти. Как обычно, основным источником для статьи является родное руководство от компании STMicroelectronics (Reference Manual), а также обобщенная информация из различных форумов и статей в интернете, ну и на основе своего опыта.

 

Предисловие

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

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

 

Почему FLASH?

Во многих небольших проектах возникает необходимость сохранять какие-либо параметры работы устройства в независимой от электричества памяти (EEPROM). Но в STMicroelectronics решили, что использование этой памяти в большинстве микроконтроллеров не целесообразно, исключение составляет только серия LP (Low Power), в которой EEPROM присутствует.

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

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

В первую очередь flash-память предназначена для хранения инструкций для микроконтроллера, другими словами - программного кода. Но если Ваш программный код занимает не весь объем памяти, то почему бы не выделить ее часть под хранение наших настроек? 

Плюсы очевидны:

  1. Не требуется использование внешней памяти, соответственно сокращается время на разработку платы и происходит удешевление продукта;
  2. Меньше программного кода, следовательно меньшее время затрачивается на разработку.

Есть конечно и свои ограничения: 

  1. Запись во Flash требует некоторого времени, что влияет на производительность МК в момент записи или очистки памяти;
  2. Чтобы внести изменения в уже существующие данные, нужно стереть всю страницу или записывать в "чистый" блок памяти;
  3. Количество циклов перезаписи гарантировано в районе 100 тысяч операций - вроде бы много, но перезаписывая данные в страницу раз в секунду, МК выработает ресурс flash чуть более, чем за сутки. Поэтому очень не рекомендую постоянно писать во flash, рекомендуется производить операции очистки / записи лишь для сохранения данных в "энергонезависимой" памяти. В остальных случаях работаем с оперативной памятью;
  4. Минимум Вы можете использовать 1 страницу памяти (даже для одного байта), а размер одной страницы составляет от одного до двух килобайт, в зависимости от модели микроконтроллера. Такова селяви устройства flash-памяти. 

Но не стоит забывать что в первую очередь flash-память предназначена для хранения программного кода и, соответственно, сама структура интерфейса Flash в микроконтроллере устроена таким образом, чтобы оптимизировать загрузку инструкций для МК и организовать их (инструкций) кэширование, пока идет выполнение уже загруженных.

 

Структура

Как нам сообщает Reference Manual, модуль flash-памяти является высокопроизводительным и имеет следующие ключевые особенности:

  • Для устройств серии XL-density: объем памяти составляет до 1Mb, разбитый на два банка памяти:

- 1-й банк памяти: 512KB;
- 2-й банк памяти: до 512KB, в зависимости от модели микроконтроллера.

  • Для других устройств: объем памяти составляет до 512KB, состоящий из одного банка памяти размером до 512KB.
  • Сама по себе Flash-память состоит из трех блоков: основной блок памяти, информационный блок и блока регистров управления Flash. Структура блоков памяти и их характеристики приведены на рисунке №1.

 

Рис. 1. Организация структуры flash-памяти в зависимости от модели


Интерфейс flash-памяти (FLITF (Flash Memory Interface)) позволяет выполнять следующие операции:

  • Чтение из памяти с возможностью буферизации (2 слова по 64 бита);
  • Операции записи и очистки памяти;
  • Организация защиты flash от чтения и/или записи.

 

Основная flash-память разбита на страницы размером в 1 или 2 килобайта, это зависит от линейки микроконтроллеров STM. Использовать под свои нужды Вы можете только целое количество страниц, соответственно, если Ваша прошивка занимает все страницы памяти МК, то использование flash-памяти под свои нужды будет проблематично.

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

 

Операции с flash-памятью

Операций работы с flash-памятью не так уж и много: можем прочитать, записать и очистить память. Причем запись и очистка памяти, как правило, осуществляется вместе. Также еще можно заблокировать память для чтения и/или для записи. Но перед началом работы необходимо произвести инициализацию модуля Flash.

 

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

Операции чтения flash-памяти и доступ к данным выполняются через шину AHB. Буферизация чтения, для выполнения команд, выполняется через шину ICode. Арбитраж выполнения осуществляется самой flash-памятью, а приоритет отдается доступу к данным на шине DCode. Другими словами, пока микроконтроллер выполняет текущую команду, интерфейс Flash-памяти передает МК данные по следующей.

Чтение данных из flash-памяти может быть сконфигурировано следующим образом:

• Задержка (Latency): количество состояний ожидания для операции чтения, устанавливается «на лету», не требуется инициализация МК;

• Буферизация (2 слова по 64 бита) (Prefetch buffer): активируется во время инициализации МК. Весь блок памяти может быть заменен за один цикл чтения из памяти, так как размер блока соответствует размеру полосы пропускания шины flash-памяти. Благодаря буферизации, возможна более быстрая работа МК, поскольку МК извлекает из памяти по одному "слову" за раз одновременно со следующим "словом", которое помещается в предварительный буфер.

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

Эти параметры следует использовать в соответствии с временем доступа к flash-памяти. Значение периода ожидания представляет собой отношение периода SYSCLK (системных часов) к времени доступа к flash-памяти и устанавливается в зависимости от следующих параметров:

  • Уровень "0", если 0 <SYSCLK ≤ 24 MHz;
  • Уровень "1", если 24 MHz < SYSCLK ≤ 48 MHz;
  • Уровень "2", если 48 MHz < SYSCLK ≤ 72 MHz;


Конфигурация с половинным циклом недоступна в сочетании с предделителем на AHB. Системные часы (SYSCLK) должны быть равны часам HCLK. Поэтому эту функцию можно использовать только с низкочастотными тактовыми частотами 8MHz или менее. Он может быть создан из HSI или HSE, но не из PLL.

Буфер предварительной выборки должен храниться при использовании пределителя, отличного от 1 на часах AHB.

Буфер предварительной выборки должен быть включен / выключен только тогда, когда SYSCLK ниже 24MHz и на часах AHB не применяется предварительный делитель (SYSCLK должен быть равен HCLK). Буфер предварительной выборки обычно включается / выключается во время процедуры инициализации, а микроконтроллер работает на встроенном генераторе 8MHz (HSI).

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

Для инициализации Flash предназначен регистр FLASH_ACR, который используется для включения / выключения буферизации и полного цикла чтения, а также для управления задержкой времени доступа к flash-памяти в зависимости от настроенной частоты работы процессора. В приведенной ниже таблице приведены битовые карты и описания бит для этого регистра. 

 

Рис. 2. Flash Access Control register

 

Address offset: 0x00
Reset value: 0x0000 0030

Биты Название Описание
 31:6 Зарезервировано   
5 PRFTBS - Prefetch buffer status Состояние буферизации.
0: отключена; 1: включена. Бит доступен только для чтения.
PRFTBE - Prefetch buffer enable Включение буферизации.
0: отключена; 1: включена. 
HLFCYA - Flash half cycle access enable Доступ к полному циклу Flash.
0: отключен; 1: включен. 
2:0  LATENCY - Latency Управление задержкой.
Эти биты представляют отношение периода SYSCLK (системных часов) к времени доступа Flash.
000 Уровень "0", если 0 <SYSCLK≤ 24 МГц
001 Уровень "1", если 24 МГц <SYSCLK ≤ 48 МГц
010 Уровень "2", если 48 МГц <SYSCLK ≤ 72 МГц
Таб. 1. Описание регистра FLASH_ACR (Flash access control register)

Теперь посмотрим, как это выглядит в программном коде: 

Листинг №1. Процедура настройки flash-памяти
void FLASH_Init(void) {
    FLASH_HalfCycleAccessCmd(FLASH_HalfCycleAccess_Disable);    // Отключаем половинный цикл доступа к памяти, используем полный
FLASH_PrefetchBufferCmd( FLASH_PrefetchBuffer_Enable); // Включаем кэширование FLASH_SetLatency(FLASH_Latency_2); // Настраиваем задержку с учетом частоты работы МК } 

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

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

 

Чтение данных из flash-памяти.

Чтение flash не представляет никаких сложностей для понимания, достаточно обратится к необходимой ячейки памяти: 

Листинг №2. Чтение данных из flash-памяти
value = *(__IO uint32_t*) address;

Вот и все, для чтения нам больше ничего не нужно!  Единственной о чем нужно помнить, это то, что чтение происходит блоками по 32 бита.

Чтобы сделать код более наглядным, чтение из flash-памяти я вынес в отдельную функцию

Листинг №3. Функция чтения данных из flash-памяти
uint32_t flash_read(uint32_t address) {
	return (*(__IO uint32_t*) address);
}

 

Запись данных во flash-память.

Запись данных во Flash устроена немного сложнее, чем чтение. Основным отличием Flash от оперативной памяти является то, что перед тем, как произвести запись значения в ячейку, ее необходимо очистить. Нельзя вот просто так записать в память сначала одно значение, затем другое - так не получится. Но и очистить ячейку памяти просто так нельзя! Можно очистить только страницу целиком или сразу всю память МК.

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

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

Обычно под свои нужды программисты используют одну-две последних страницы памяти МК. Это обусловлено тем, что код программы может модифицироваться и занимать все больше объема flash. А так как, запись инструкций для МК производится, как правило, с первой страницы, то может произойти ситуация, что запись новой прошивки просто перетрем пользовательские данные. Исключение составляют ситуации, когда, к примеру, в микроконтроллер "заливается" автозагрузчик (bootloader) и основная программа начинается не с первой страницы, а, к примеру, с десятой. Но это тема отдельной статьи и здесь упоминание будет только вскользь. 

А теперь, когда мы ознакомились с теорией, разберем уже практические способы реализации очистки и перезаписи flash памяти.

Листинг №4. Процедура записи данных во flash-память
#define FIRMWARE_PAGE_OFFSET 	0x0C00

void WriteToFlash(uint32_t Value)
{
	uint8_t i;
	uint32_t pageAdr;

	pageAdr = NVIC_VectTab_FLASH | FIRMWARE_PAGE_OFFSET;                    // Адрес страницы памяти

	FLASH_Unlock();                                                         // Разблокируем память для записи
	FLASH_ErasePage(pageAdr);                                               // Очистим страницу памяти 

	FLASH_ProgramWord((uint32_t)(pageAdr), (uint32_t)Value);                // Запишем новое значение памяти
	FLASH_Lock();
}

Как Вы наверно заметили, запись в память происходит между двумя командами "FLASH_Unlock()" и "FLASH_Lock()". Нетрудно догадаться, что это связано с разблокировкой памяти для возможности производить запись во Flash. Соответственно, после записи, Flash нужно закрыть для изменения.

NVIC_VectTab_FLASH и PARAMS_PAGE_OFFSET - это определения адресов памяти и смещения, относительно начального адреса.

Чтобы было понятнее - разберем пример: 

Размер прошивки автозагрузчика составляет менее 3Kb, соответственно нам нужно начать запись основной прошивки в третью страницу или выше с учетом того, чтобы прошивка уместилась в оставшуюся память. Не забывайте, что страницы flash-памяти начинаются с нуля, соответственно четвертая по счету страница называется "Page3". 

NVIC_VectTab_FLASH  - обычно равен 0x08000000, он определен в системе (в файле misc.h). 

FIRMWARE_PAGE_OFFSET- это наше определение и формируется по следующей формуле: НомерСтраницыПамяти * РазмерСтраницыПамяти. Номер страницы - мы определяем самостоятельно, размер страницы памяти для большинства МК серии STM32F103 составляет 1Kb, за исключением микроконтроллеров линейки HD и CL (Connectivity Line), в которых она равна двум килобайтам. 

В нашем случае получается: (4-1) * 0x0400 = 0x0C00 - это смещение относительно начального адреса. Соответственно писать данные мы будем начинать с адреса 0x08000C00. 

Можно конечно и сразу указать адрес, куда будем писать (0x08000C00), но лучше сразу привыкать писать "правильно" и с уклоном на будущее, чтобы Ваш код без особой переделки  мог мигрировать на другие МК.

 

Чтобы стереть страницу памяти, достаточно в команду FLASH_ErasePage в качестве параметра передать любой адрес из диапазона адресов страницы. Как правило передают адрес первого блока нужной страницы. Не ошибитесь с определением страницы памяти, так как можно стереть и саму прошивку ))).

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

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

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

 

Использование структур данных.

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

Само определение структуры выглядит примерно так:

Листинг №5. Определение структуры данных параметров
// Определим структуру
typedef struct {
	uint8_t Param1;         // 1 byte
	uint8_t Param2;         // 1 byte
	uint16_t Param3;        // 2 byte

	uint8_t Param4;         // 1 byte
	uint8_t Param5;         // 1 byte
	uint16_t Param6;        // 2 byte

	uint16_t Param7;        // 2 byte
	uint16_t Param8;        // 2 byte

	uint32_t Param9;        // 4 byte
} Params_def;

// Теперь переменная
static Params_def params;

// Опеределим, сколько блоков памяти у нас определено под параметры
// Это необходимо для определения количества циклов чтения/записи параметров
#define PARAMS_WORD_CNT 	sizeof(params) / sizeof(uint32_t) // Расчитывается исходя из размера структуры в памяти МК деленного на размер блока (4 байта)

Как видите - ничего сложного нет. 

Когда Вы определяете переменную, под нее резервируется место в оперативной памяти в размере, который Вы определили. Адреса памяти резервируются подряд, в блоках по четыре байта. Соответственно, если вы определяете подряд три переменных по байту, а затем одну, размером в два байта, то в памяти они займут в общей сложности 8 байт: 3 байта в первом блоке и два во втором. В силу специфики работы с оперативной памятью, занимать память частями не получится!

На рисунке №3 я отобразил, как будет распределяться память при определении переменных.

 

Рис. 3. Размещение переменных в памяти

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

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

В данном примере, правильный порядок определения переменных это требование, а не рекомендация, так как нам нужно знать точное количество блоков памяти, в которых разместится переменная с нашим определением структуры. Второй пример с ошибкой на рисунке 3 показывает, что если мы не так определим порядок переменных, то объем занимаемой памяти в структуре будет занимать не один блок, как мы думали, а два. Соответственно при сохранении данных во Flash, мы можем потерять часть информации.

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

Переменная с нашей структурой хранится в оперативной памяти, при ее определении в оперативной памяти выделяется определенный объем байт. Мы можем записывать во Flash не только каждый параметр в отдельности, а целиком всю структуру, для этого мы определяем адрес ячейки оперативной памяти, с которого начинается наша структура, и целиком копируем его во flash. Чтение данных происходит в обратном порядке.

Ну а теперь примеры:

Листинг №6. Запись структуры данных во Flash
void FLASH_WriteSettings(void) {

	uint8_t i;
	uint32_t pageAdr;

	// Эти параметры могут заполняться в любом месте программы
	params.Counter1 = 0xc1;
	params.Counter2 = 0xc2;
	params.Counter3 = 0xc3;
	params.Counter4 = 0xc4;
	params.Nothing1 = 0xF1F1;
	params.Nothing2 = 0xF2F2;

	pageAdr = NVIC_VectTab_FLASH + PARAMS_PAGE_OFFSET;                              // Адрес страницы памяти

	uint32_t *source_adr = (void *)&params;

	FLASH_Unlock();                                                                 // Разблокируем память для записи
	FLASH_ErasePage(pageAdr);                                                       // Очистим страницу памяти параметров №0

	for (i = 0; i < PARAMS_WORD_CNT; ++i) {
                FLASH_ProgramWord((uint32_t)(pageAdr + i*4), *(source_adr + i));        // Запишем новое значение памяти
        }

	FLASH_Lock();                                                                   // Заблокируем Flash

}

Хочется обратить Ваше внимание на строчку, в которой происходит запись во flash-память:

FLASH_ProgramWord((uint32_t)(pageAdr + i*4), *(source_adr + i));

В одном случае адрес увеличивается на "i*4", а в другом просто на "i" - Это не опечатка: в первом случае у нас переменная определена как "uint32_t", а в другом как ссылка на ячейку памяти размером "uint32_t". Соответственно в первом случае мы изменяем число на четыре байта, а во втором - изменяем на единицу ссылку на ячейку памяти размеров в 4 байта.

Теперь посмотрим на то, как выглядит заполнение нашей структуры данных из Flash:

Листинг №7. Чтение структуры данных из Flash
void Flash_ReadParams(void) {

	uint32_t *source_adr = (uint32_t *)(NVIC_VectTab_FLASH + PARAMS_PAGE_OFFSET);   // Определяем адрес, откуда будем читать
	uint32_t *dest_adr = (void *)&params;                                           // Определяем адрес, куда будем писать

	for (uint16_t i=0; i < PARAMS_WORD_CNT; ++i) {                                  // В цикле производим чтение
		*(dest_adr + i) = *(__IO uint32_t*)(source_adr + i);                    // Само чтение
	}
}

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

 

Блокировка чтения / записи Flash

Подошло время ознакомится с защитой данных от чтения и/ или записи.

В какой-то момент времени у разработчиков наступает момент, когда хочется спрятать какой-либо код или данные от посторонних глаз или защитить от изменения свою прошивку. Выполнить это можно несколькими способами, рассмотрим два из них: программно или через стандартную  утилиту "STM32 ST-LINK Utility".

Сразу оговорюсь, защита от чтения, это не панацея - в сети полно сайтов, которые предлагают прочитать прошивку не повредив данные. Каким то образом, неизвестным мне (пока )))), они снимают бит защиты от чтения и блокируют стирание flash. Поэтому в "крутых" прошивках, защищенных от чтения, присутствует еще и дополнительная защита, позволяющая избежать клонирования и декомпиляции кода.

Установка защиты чтения и записи данных устанавливается с помощью "Options Byte", находящихся в информационном блоке Flash. В этой статье подробно расписывать работу не буду, остановлюсь лишь на ключевых моментах.

Защита от чтения реализована почти гениально: устанавливается бит запрета чтения flash (RDP - Read Protection). Если бит снимается, то все содержимое Flash стирается. Установить бит и снять его можно как программно, так и с помощью сторонних утилит.

Защита от записи реализовано постранично, блоками по 4 килобайта: для микроконтроллеров с размером страницы равным одному килобайту, блокируется сразу по четыре страницы памяти, для МК с размером памяти равным двум килобайтом - блокируется по две страницы. Защита осуществляется в ячейках памяти "Options Byte" с помощью изменения битов  WRPn (Write Protection).

 

Программное исполнение.

Самый простой способ защитить прошивку от чтения, это воспользоваться стандартной библиотекой от STM. Код будет выглядеть приблизительно так:

Листинг №8. Защита Flash от чтения
#ifndef DEBUGMODE                                                       // DEBUGMODE - Наше определение, чтобы можно было тестировать прошивку
        if (FLASH_GetReadOutProtectionStatus() == RESET)                // Проверяем, установлена ли защита
        {                                                               // Если нет:
                FLASH_Unlock();                                         // Разблокируем память Flash
                FLASH_ReadOutProtection(ENABLE);                        // Устанавливаем защиту от чтения
                FLASH_Lock();                                           // Блокируем память Flash
        }
#endif  

Как видите, ничего сложного нет.

Дефайн "DEBUGMODE" определяем в том случае, если нам надо отладить прошивку, иначе, при включенной защите от чтения, пройтись отладчиком по прошивке не получится.

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

Кстати некоторое наблюдение, защиту от чтения можно не только установить, но и снять через программный код. Зачем это нужно - решать Вам ))).

С чтением все понятно, для чего оно нужно - тоже, а вот зачем нужна защита от записи? Первое, наверное, что приходит в голову, это наш автозагрузчик, чтобы случайно при прошивке микроконтроллера, мы не затерли свой Bootloader. 

Вот как выглядит процедура установки защиты от записи на первые 8 килобайт памяти:

Листинг №9. Функция защиты от записи
#ifndef DEBUGMODE                                                                               // DEBUGMODE - Наше определение, чтобы можно было тестировать прошивку и перепрошивать ее
void Lock_Bootloader(void){

        unsigned int WRPR_Value;                                                                // Список закрытых для записи страниц
        unsigned int ProtectedPages;                                                            // Список страниц, которые необходимо закрыть

        FLASH_Unlock();                                                                         // Разблокируем память Flash
        WRPR_Value = FLASH_GetWriteProtectionOptionByte();                                      // Получим список уже заблокированных страниц

        ProtectedPages = WRPR_Value & (FLASH_WRProt_Pages0to3 | FLASH_WRProt_Pages4to7);        // Установим только те страницы, которые нам необходимо защитить

        if(ProtectedPages != 0x00)                                                              // Если есть что защищать, то
        {
                FLASH_EraseOptionBytes();                                                       // Очистим Option Byte (Это необходимо сделать до установки записи)
                FLASH_EnableWriteProtection(ProtectedPages);                                    // Установим защиту памяти от записи
        }

        FLASH_Lock();                                                                           // Блокируем память Flash
}
#endif  

Данные о защищенных страницах памяти хранятся в битах Operation Byte. Защищенными считаются те страницы, бит которых сброшен. Если бит установлен, то страница доступна для записи! Принцип работы с Option Byte такой же, как и со всей Flash - перед установкой значения, его необходимо очистить, при этом все биты устанавливаются в 1.

Хочу обратить Ваше внимание на проверку условия  "if(ProtectedPages != 0x00)" - условие верное, мы сравниваем ProtectedPages, а не WRPR_Value. В ProtectedPages установлены биты только тех блоков памяти, которые нам нужны и они не защищены от записи. А в WRPR_Value установлены биты защищенных блоков памяти.

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

Кстати, не пытайтесь менять значение Option Byte "на лету". Чтение этого блока происходит при перезагрузке микроконтроллера и сохраняется в регистрах FLASH_OBR и FLASH_WRPR, поэтому изменять значение в процессе выполнения кода без перезагрузки МК бессмысленно - сохранено будет последнее установленное Вами значение.

 

Модифицированная функция установки/снятия защиты от записи Flash.

По просьбе Serg, привожу пример функции установки или сброса защиты от записи. Почему-то разработчики от STM постеснялись добавить эту функцию в библиотеку для линейки МК STM32F1xx, хотя для других линеек МК такой (похожий) функционал присутствует. Основное ее отличие от родной функции в том, что перед записью в регистр Operation Byte необходимо производить его очистку, соответственно при использовании родной функции происходит сброс всех настроек защиты, а в моей функции реализовано изменение отдельных бит, что позволяет более гибко управлять защитой, хотя и пагубно влияет на МК, так как количество циклов перезаписи для Flash гарантировано в пределах 100000 циклов. (Даже на тестовых МК я еще ни разу не столкнулся с тем, что у меня количество циклов перезаписи перевалило за сотню тысяч )))).

Само тело функции желательно разместить в модуле "stm32f10x_flash.c" после функции "FLASH_EnableWriteProtection()":

Листинг №10-а. Функция установки/снятия защиты от записи
/**
  * @brief  Включение / отключение защиты от записи Flash памяти
  * @note   Переработанная и дополненная процедура установки защиты от записи (оригинал FLASH_EnableWriteProtection)
  * @note   Эта функция может быть использована на всех устройствах серии STM32F10x.
  * @param  FLASH_Pages: перечень блоков страниц памяти, для которых необходимо установить новое значение.
  *   This parameter can be:
  *     @arg For @b STM32_Low-density_devices: value between FLASH_WRProt_Pages0to3 and FLASH_WRProt_Pages28to31
  *     @arg For @b STM32_Medium-density_devices: value between FLASH_WRProt_Pages0to3
  *       and FLASH_WRProt_Pages124to127
  *     @arg For @b STM32_High-density_devices: value between FLASH_WRProt_Pages0to1 and
  *       FLASH_WRProt_Pages60to61 or FLASH_WRProt_Pages62to255
  *     @arg For @b STM32_Connectivity_line_devices: value between FLASH_WRProt_Pages0to1 and
  *       FLASH_WRProt_Pages60to61 or FLASH_WRProt_Pages62to127
  *     @arg For @b STM32_XL-density_devices: value between FLASH_WRProt_Pages0to1 and
  *       FLASH_WRProt_Pages60to61 or FLASH_WRProt_Pages62to511
  *     @arg FLASH_WRProt_AllPages
  * @param  NewState: новое состояние, которое необходимо установить для указанных блоков памяти.
  *   Этот параметр может принимать значение:
  *     @arg ENABLE - установка блокировки записи
  *     @arg DISABLE - выключение блокировки записи
  * @retval FLASH Status: Возвращаемое значение может быть: FLASH_ERROR_PG,
  *         FLASH_ERROR_WRP, FLASH_COMPLETE or FLASH_TIMEOUT.
  */
FLASH_Status FLASH_SetWriteProtection(uint32_t FLASH_Pages, FunctionalState NewState)
{
  uint16_t WRP0_Data = 0xFFFF, WRP1_Data = 0xFFFF, WRP2_Data = 0xFFFF, WRP3_Data = 0xFFFF;
  uint16_t oldData0, oldData1, oldData2, oldData3;
  uint16_t newData0, newData1, newData2, newData3;

  FLASH_Status status = FLASH_COMPLETE;

  /* Check the parameters */
  assert_param(IS_FLASH_WRPROT_PAGE(FLASH_Pages));

  FLASH_Pages = (uint32_t)(~FLASH_Pages);
  WRP0_Data = (uint16_t)(FLASH_Pages & WRP0_Mask);
  WRP1_Data = (uint16_t)((FLASH_Pages & WRP1_Mask) >> 8);
  WRP2_Data = (uint16_t)((FLASH_Pages & WRP2_Mask) >> 16);
  WRP3_Data = (uint16_t)((FLASH_Pages & WRP3_Mask) >> 24);

  /* Wait for last operation to be completed */
  status = FLASH_WaitForLastOperation(ProgramTimeout);

  if(status == FLASH_COMPLETE)
  {
	// Скопируем текущие настройки защиты, потому что мы потом их сотрем
	oldData0 = OB->WRP0 & 0xFF;
	oldData1 = OB->WRP1 & 0xFF;
	oldData2 = OB->WRP2 & 0xFF;
	oldData3 = OB->WRP3 & 0xFF;

	// Очистим Option Byte (Это необходимо сделать до установки записи, иначе ничего не сможем изменить)
	FLASH_EraseOptionBytes();

	/* Wait for last operation to be completed */
	status = FLASH_WaitForLastOperation(ProgramTimeout);

	/* Authorizes the small information block programming */
    FLASH->OPTKEYR = FLASH_KEY1;
    FLASH->OPTKEYR = FLASH_KEY2;
    FLASH->CR |= CR_OPTPG_Set;

    // Рассчитаем новые значения
	if (NewState==ENABLE) {
		newData0 = oldData0 & WRP0_Data;
		newData1 = oldData1 & WRP1_Data;
		newData2 = oldData2 & WRP2_Data;
		newData3 = oldData3 & WRP3_Data;
	} else {
		newData0 = ~(~oldData0 & WRP0_Data);
		newData1 = ~(~oldData1 & WRP1_Data);
		newData2 = ~(~oldData2 & WRP2_Data);
		newData3 = ~(~oldData3 & WRP3_Data);
	}

    if(status == FLASH_COMPLETE)
    {
    	OB->WRP0 = newData0;

      /* Wait for last operation to be completed */
      status = FLASH_WaitForLastOperation(ProgramTimeout);
    }

    if(status == FLASH_COMPLETE)
    {
    	OB->WRP1 = newData1;

      /* Wait for last operation to be completed */
      status = FLASH_WaitForLastOperation(ProgramTimeout);
    }

    if(status == FLASH_COMPLETE)
    {
    	OB->WRP2 = newData2;

      /* Wait for last operation to be completed */
      status = FLASH_WaitForLastOperation(ProgramTimeout);
    }

    if(status == FLASH_COMPLETE)
    {
    	OB->WRP3 = newData3;

      /* Wait for last operation to be completed */
      status = FLASH_WaitForLastOperation(ProgramTimeout);
    }

    if(status != FLASH_TIMEOUT)
    {
      /* if the program operation is completed, disable the OPTPG Bit */
      FLASH->CR &= CR_OPTPG_Reset;
    }
  }
  /* Return the write protection operation Status */
  return status;
}

И необходимо добавить определение функции в модуле "stm32f10x_flash.h" также после функции "FLASH_EnableWriteProtection()", иначе Вы не сможете вызвать ее из свей программы:

Листинг №10-b. Заголовок функции установки/снятия защиты от записи
FLASH_Status FLASH_SetWriteProtection(uint32_t FLASH_Pages, FunctionalState NewState);

Сама функция является переработанной функцией от STM "FLASH_EnableWriteProtection()": 

  • добавлен параметр "NewState", который отвечает за установку или снятия флага защиты;
  • в теле функции добавлена проверка на значение этого параметра и, в зависимости от него, устанавливаются или сбрасываются соответствующие биты.
  • сброс OperationByte производится не перед вызов функции, а внутри нее. Это связано с особенностью записи во Flash, поэтому сначала запоминаются предыдущие значения OperationByte, производится очистка OperationByte, а затем уже записываются новые значения с учетом предыдущих.

Таким образом можно заменить по своему коду вызовы "FLASH_EnableWriteProtection(ProtectedPages)" на "FLASH_SetWriteProtection(ProtectedPages, ENABLE)" для включения защиты, или "FLASH_SetWriteProtection(ProtectedPages, DISABLE)" для ее отключения. Не забудьте в этом случае убрать очистку OperationByte с помощью функции "FLASH_EraseOptionBytes()", так как она сбросит все раннее установленные флаги защиты от записи.

 

Использование утилиты STM32 ST-LINK Utility.

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

Из всех возможностей утилиты, мы обратимся только к работе с областью данных микроконтроллера, известной как "Option Byte".

Попасть в этот функционал программы можно либо комбинацией кнопок "Ctrl+B", либо через меню "Target" / "Option Byte". Перед Вами откроется похожее окно:

Рис. 4. Настройка Option Byte (STM32  ST-LINK Utility).

 

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

Не знаю почему, но если Вы установили защиту от записи, то после снятие этой защиты через утилиту, автоматически выставляется флаг защиты от чтения. а снятие флага защиты от чтения полностью стирает Flash.

 

Заключение

В этой статье я постарался кратко рассказать как можно использовать Flash  в своих целях, как сделать свой автозагрузчик (Bootloader) и как защитить своий код. Все примеры в статье протестированы и готовы к использованию. На этот раз вложение добавлять не буду - весь материал содержит небольшой объем кода и легко воспринимается в тексте.

Если найдете ошибки или будут вопросы - welcome в комментарии ))).

 

 

 

Комментарии  

#1 Егор 22.04.2018 08:23
В листинге №7 в цикле не изменяется адрес откуда читаем и куда записываем.
Цитировать
#2 Админ 22.04.2018 09:54
Цитирую Егор:
В листинге №7 в цикле не изменяется адрес откуда читаем и куда записываем.

Спасибо, исправил :-)
Цитировать
#3 serg 02.05.2018 15:38
1) листинг 9
if(ProtectedPages != 0x00)
может != 0xFF?
а то как-то не соответствует " Защищенными считаются те страницы, бит которых сброшен. Если бит установлен, то страница доступна для записи! "
а если будут все страницы защищены, что соответствует 0x00, мы не войдем в условие?
2) листинг 7
интересно услышать ваше понимание, зачем тут void*
uint32_t *dest_adr = (void *)¶ms;
3) тоже, хотелось бы услышать зачем тут volatile
*(dest_adr + i) = *(__IO uint32_t*)(source_adr + i);
4) да, кстати, лучше сделать в листинге 6 как в 7-мом.
uint32_t *source_adr = (uint32_t *)(NVIC_VectTab_FLASH + PARAMS_PAGE_OFFSET);
и не нужно, тогда объяснять нафига нам разные инкременты с i.
5) пишите что "Защита от записи реализовано постранично, блоками по 4 килобайта:", хотя ST-Link дает возможность по 1К делать защиту. несоответствие.
Цитировать
#4 serg 02.05.2018 15:43
вообще больше смахивает на переводную статью. признайтесь. особенно "на часах AHB" - может "тактовой частоте AHB".
зашел сюда, чтобы прочитать про особенности работы с 2-х банковой памятью, а тут для начинающих.
Цитировать
#5 Админ 02.05.2018 16:07
Цитирую serg:
вообще больше смахивает на переводную статью. признайтесь. особенно "на часах AHB" - может "тактовой частоте AHB".
зашел сюда, чтобы прочитать про особенности работы с 2-х банковой памятью, а тут для начинающих.

Добрый день!
Вы ошибаетесь, статья полностью в моем авторстве. Более того, все примеры приведены из реального рабочего кода и проверены.
Я не исключаю наличия ошибок, все мы Человеки, поэтому здесь включена возможность комментирования статьи и по мере необходимости в её текст вносятся правки.
У меня нет профильного ИТ-образования, я самоучка, поэтому в некоторых моментах могу называть термины немного по другому, могу допускать ошибки, оговорки, да и просто опечатки. Выпускающего редактора у меня нет.
Ваши примеры приму к сведению и внесу правки, а на предыдущий Ваш пост отвечу скорее всего завтра, сегодня нет возможности сделать это в полной мере.
И да, эти статьи рассчитаны на новичков :lol:
Цитировать
#6 Админ 03.05.2018 10:17
Цитирую serg:
1) листинг 9
if(ProtectedPages != 0x00)
может != 0xFF?
а то как-то не соответствует " Защищенными считаются те страницы, бит которых сброшен. Если бит установлен, то страница доступна для записи! "
а если будут все страницы защищены, что соответствует 0x00, мы не войдем в условие?
2) листинг 7
интересно услышать ваше понимание, зачем тут void*
uint32_t *dest_adr = (void *)¶ms;
3) тоже, хотелось бы услышать зачем тут volatile
*(dest_adr + i) = *(__IO uint32_t*)(source_adr + i);
4) да, кстати, лучше сделать в листинге 6 как в 7-мом.
uint32_t *source_adr = (uint32_t *)(NVIC_VectTab_FLASH + PARAMS_PAGE_OFFSET);
и не нужно, тогда объяснять нафига нам разные инкременты с i.
5) пишите что "Защита от записи реализовано постранично, блоками по 4 килобайта:", хотя ST-Link дает возможность по 1К делать защиту. несоответствие.

Ну что ж, пришло время для ответа...
1. По коду:
WRPR_Value = FLASH_GetWriteProtectionOptionByte(); // Получим список уже заблокированных страниц
ProtectedPages = WRPR_Value & (FLASH_WRProt_Pages0to3 | FLASH_WRProt_Pages4to7);
Если WRPR_Value = 0, то заблокированы все страницы, если нет, то мы сравниваем WRPR_Value с нужными нам блоками памяти, и если биты для этих блоков

установлены, то (они не защищены для записи) в ProtectedPages будут только те страницы, которые нам нужны и они не защищены от записи, а не весь список возможных страниц.
Поэтому "(ProtectedPages != 0x00)" это проверка на наличие страниц, с отключенной защитой от записи. А вот если бы условие было "if(WRPR_Value != 0x00)", тогда да,

было бы ошибкой и несоответствием описанию;

2. (void *) - указатель на объект неопределённого типа, если его не указать, то компилятор (по крайней мере мой) будет предупреждать о несоответствии типов

переменных (warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]);

3. Да, моя ошибка с копипастом, volatile здесь не нужен, исправлю;

4. Можно было бы, но в "родной" функции от STM используется значение переменной с типом uint32, а не указатель на ячейку памяти. Можно поиграться с преобразованием, но в рамках данной статьи я думаю это будет не уместно;

5. Насколько я помню этот момент, то в STLink при включении/выключении флажка для одной страницы, включался весь блок в четыре страницы, что соответствует 4-м килобайтам.
В самом регистре WRPR 1 бит равен 4-м страницам (для мк STM32F103C).
Так что возможно, что у Вас получилось это сделать, но не уверен что для мк STM32F103C8.
Цитировать
#7 SERG 03.05.2018 13:15
1. да, был не прав. У Вас сейчас всё верно написано.
Полез разбираться в их примеры, у них написано сложнее, из-за того, что они еще и показывают
как защиту снять в одном куске кода. Неплохо бы показать еще как снимать у Вас,
условие проверки станет интереснее.
2. (void *) - всё правильно, это указатель на неопределенный тип.
Только его используют по другому. Чтобы компилятор не ругался, нужно просто
привести к типу слева (явное приведение к типу) или (uint32_t *). Всё равно,
компилятор неявно приведет (void *) к (uint32_t *). например (void) можно использовать
как подавление предупреждения о неиспользуемом аргументе (используется в HAL STM).
Цитировать
#8 SERG 03.05.2018 13:16
4. Да библиотеки эти (STL) написаны с ошибками бывает, люди находят грабли сами.
опять же, посмотрите как написано в примере для установки защиты, у Вас получилось намного проще.
5. да по документации так. для
low-density devices - 4 pages of 1 Kbyte
For medium-density devices - 4 pages of 1 Kbyte
For high-density devices - 2 pages of 2 Kbytes
For connectivity line devices - 2 pages of 2 Kbytes
Я вообще это писал к тому, что на скриншоте можно ставить по 1стр защиту, а не по 4, получается. Странно.
Цитировать
#9 Админ 03.05.2018 13:39
По 1-му: Спасибо, со временем добавлю в статью этот момент.
По 2-му пункту - Си для меня "не родной" язык, я до сих пор бывает путаюсь с указателями и преобразованиями ))), а по сути, он просто ругается, а мне не нравится, когда компилятор ругается: как показывает практика, это "Жжжжж" не спроста и всплывет где-нибудь боком потом. Тем более вопросы распределения памяти в МК очень сложно решать без отладчика. Примеры того, каким образом занимаются ячейки памяти я в этой статье уже приводил.
по 5-му: я когда начал комментировать вопрос, сомневался в своей памяти, а потом вспомнил что галочки выставляются блоками. Скорее всего это связано с тем, что у других линеек МК может быть реализована постраничная защита, к примеру там где страниц памяти мало, а свободных битов в регистре много :-), например STM8, который тоже цепляется к STLink
А в целом, спасибо за коммент. :-)
Цитировать
#10 Админ 03.05.2018 13:53
Я заканчиваю статью про BootLoader, просто как всегда не хватает времени на хобби (((.
Есть тестовая прошивка, которую приложу к новой статье, а в этом материале все примеры взяты как раз из нее.
Эта прошивка состоит из двух блоков:
1. Загрузчик - мигает три раза диодом, читает значение из Flash, увеличивает на единицу и записывает обратно, а затем передает управление основному блоку программы;
2. Основной блок, читает значение из Flash, а затем мигает с интервалами столько раз, сколько установлено в прочитанном из Flash значении.

Счетчик увеличивается только при сбросе питания и сбрасывается в ноль при достижении десяти.

Фактически это и есть пример загрузчика, будет только доработана функциональность по загрузке прошивки по CAN-шине.
Цитировать
#11 SERG 03.05.2018 14:13
Помучил компилятор со структурой, уже самому интересно стало, как он там выравнивает. получил такой результат
aa 00 bb bb cc ee dd 00 78 56 34 12 ff ff 00 00 78 56 34 12 99 99
при такой структуре
typedef struct {
uint8_t Param1; // 1 byte
uint16_t Param2; // 2 byte
uint8_t Param3; // 1 byte

uint8_t Param4; // 1 byte
uint8_t Param5; // 1 byte
uint32_t Param6; // 4 byte

uint16_t Param7; // 2 byte
uint32_t Param8; // 4 byte

uint16_t Param9; // 2 byte

} Params_def;
params.Param1 = 0xaa;
params.Param2 = 0xbbbb;
params.Param3 = 0xcc;
params.Param4 = 0xee;
params.Param5 = 0xdd;
params.Param6 = 0x12345678;
params.Param7 = 0xffff;
params.Param8 = 0x12345678;
params.Param9 = 0x9999;

немного не сходится с картинкой нижней-правой.
Цитировать
#12 Админ 03.05.2018 14:20
Цитирую SERG:
Помучил компилятор со структурой,... немного не сходится с картинкой нижней-правой.

AA 00 BB BB ..... 0xAA 0xBBBB
CC EE DD 00 ..... 0xCC 0xEE 0xDD
78 56 34 12 ..... 0x12345678
FF FF 00 00 ..... 0xFFFF
78 56 34 12 ..... 0x12345678
99 99 00 00 ..... 0x9999

Все верно, я так и описывал - размещает в памяти поблочно в соответствии с моим рисунком.
А значение 0x12345678 пишется в обратном порядке, такова философия (и она разумна) и ее надо учитывать.
Возвращаемое значение при чтении в коде соответствует первоначальному - не зеркальное!
Цитировать
#13 SERG 03.05.2018 14:26
к чему вообще это пишу. вообще плохо самому выставлять размер структуры, нельзя думать за компилятор. Лучше брать sizeof(params)/sizeof(uint32_t) и будет результат количества блоков, неважно как он там их выравнивал. Если нужно сделать структуру компактнее, то опять же не нужно самому думать, а как она там ляжет в памяти, лучше воспользоваться запаковкой структур, она всё сожмет.

сам сейчас сел за бутлоадеры, только через ethernet, пока разбираюсь, что там под капотом и обсуждая с Вами статью, заодно нескучно. благо что у ST примеров навалом по бутлоадерам. А так, я уже давно использую их эмулятор eeprom, работает стабильно.
Цитировать
#14 Админ 03.05.2018 14:28
А пардон, вижу отличие по первому блоку
Возможно, сейчас проверю у себя
Цитировать
#15 Админ 03.05.2018 14:31
Цитирую SERG:
к чему вообще это пишу. вообще плохо самому выставлять размер структуры, нельзя думать за компилятор. Лучше брать sizeof(params)/sizeof(uint32_t) и будет результат количества блоков, неважно как он там их выравнивал. Если нужно сделать структуру компактнее, то опять же не нужно самому думать, а как она там ляжет в памяти, лучше воспользоваться запаковкой структур, она всё сожмет.
....


Согласен, но в данном случае хотелось именно объяснить как это все работает и предупредить тех, кто изучает работу с Flash, о том, с какими приколами им придется столкнуться
Цитировать
#16 SERG 03.05.2018 14:58
Цитирую Админ:
[quote name="SERG"]
Все верно, я так и описывал - размещает в памяти поблочно в соответствии с моим рисунком.
А значение 0x12345678 пишется в обратном порядке, такова философия (и она разумна) и ее надо учитывать.
Возвращаемое значение при чтении в коде соответствует первоначальному - не зеркальное!

это дамп из IAR. он там показывает в little endian как мы и пишем 0хХХХХХ. STM32 использует little endian, в отличии от STM8 - у него
Big Endian
Цитировать
#17 Админ 03.05.2018 15:02
У меня получилось при прочих равных следующее:
BBBB00AA 00DDEECC 12345678 0000FFFF 12345678 00009999
то есть получается, что выравнивание идет до слова, но в целом картина не меняется. (Рисунок исправил)
Значение SizeOF() показывает 24 байта.
В принципе добавлю второй вариант в статью с его использованием, будет полезно ))).
Цитировать
#18 Админ 03.05.2018 15:46
Изменил одну строчку в коде в листинге №5:
Значение PARAMS_WORD_CNT теперь не константа, а рассчитывается по формуле "sizeof(params) / sizeof(uint32_t)", где sizeof(params) - размер структуры параметров в памяти МК в байтах, а sizeof(uint32_t) - размер блока памяти.
Цитировать
#19 Админ 04.05.2018 11:03
Цитирую SERG:
Полез разбираться в их примеры, у них написано сложнее, из-за того, что они еще и показывают как защиту снять в одном куске кода. Неплохо бы показать еще как снимать у Вас

Добавил, пользуйтесь )))
Цитировать
#20 Слава 28.03.2022 09:17
Спасибо большое за рассказ, применил на практике но обнаружил интересное поведение, прошу помочь с пониманием причины.

Язык : чистый Си, CMSIS, KEIL,проц stm32f030k6t6.
Если структура в которую копирую из флеша описана как глобальная и с префиксом static то при почти любых ее размерах проц виснет при копировании из флеша в нее.

Если эту же переменную описать локально в процедуре main, то ве работает.

Почему так?
Цитировать
#21 Хулио 18.01.2024 18:14
Подскажите, а если я захочу поставить защиту от записи через стлинк на процессор, в котором уже стоит защита от чтения, firmware удалится автоматом?
Цитировать
#22 S D 11.04.2024 10:11
Цитирую SERG:
Лучше брать sizeof(params)/sizeof(uint32_t) и будет результат количества блоков, неважно как он там их выравнивал.

#define PARAMS_WORD_CNT sizeof(params) / sizeof(uint32_t)

Имхо, всё это вредные советы. Сейчас проверил на 103 железке. При размере params в 10 байт я таки получил в памяти sizeof(params) равным 10. И соответственно PARAMS_WORD_CNT получился 2 вместо предполагаемых трех.
Цитировать