Flash память STM32

Те кто знаком с восьмибитными контроллерами типа AVR и PIC, наверняка хорошо знают о такой нужной вещи как встроенная EEPROM память. Она позволяет сохранять в нее некоторые данные и потом считывать их после выключения/включения контроллера. Энергонезависимая память одним словом. Перейдя на контроллеры STM32 я с удивлением обнаружил, что такой памяти у них просто нет! Но как потом оказалось, существует аж целых две альтернативы такой памяти. Первая - использовать backup домен. Это не совсем энергонезависимая память, чтоб информация не разрушалась после выключения основного питания, к определенному выводу контроллера должна быть подключена батарейка (вроде тех, которые стоят в материнских платах). Об этом я подробнее расскажу в следующей статье. А сейчас мы рассмотрим второй способ энергонезависимого хранения пользовательских данных — flash память контроллера.
Да-да, та самая память в которой хранится написанная вами прошивка. Флеш памяти у STM32 полно, и зачастую остаются неиспользованные пару  килобайт, так почему бы не заюзать их. Если верить даташиту на STM32F100RBT6B, то производитель гарантирует как минимум 10000 циклов перезаписи. На мой взгляд, если требуется запись данных не чаще пары раз в день, то можно смело использовать. Для записи во флеш используется FPEC (Flash program and erase controller) или по-нашему контроллер записи и стирания флеш памяти. Flash память делится на два блока - Main memory и Information block. Первый блок это собественно сама память в которую записывается прошивка и с которой мы будем сегодня работать. А что касается Information block, то он содержит в себе два раздела: System memory и Option Bytes. В первом зашит загрузчик который позволяет прошивать контроллер через UART, удалить или как-то модифицировать его нельзя. Раздел Option Bytes - хранит информацию о защите основной памяти, можно включить защиту от чтения и/или записи. Сам же блок основной памяти поделен на страницы: одна страница - 1 килобайт. Соответственно если у моего STM32F100RBT6B 128 кБайт памяти, то имеем 128 страниц.  Все вышесказанное видно на рисунке ниже:

 flash addresses
Как известно, у STM32 единое адресное пространство (от 0x00000000 до 0xFFFFFFFF) вообще для всего, тут будут и регистры, и оперативная памяти и флеш память (адрес которой начинается с 0x08000000). Прочитать флеш память программно можно при помощи вызова функции показанной ниже:
uint32_t flash_read(uint32_t address) {
  return (*(__IO uint32_t*) address);
}
Что же касается записи, то тут все чуть сложнее. Нельзя просто так взять и записать что-то в флеш, нужно сначала разрешить это в специальном регистре FPEC. Запись может производится по любому адресу флеш памяти по 4 байта, причем сначала пишутся два младших байта потом два страших. Так же имеется один нюанс об который я по началу сломал голову - перед записью память должна быть стёрта! Показателем стёртости считается наличие всех битов установленных в единицу ибо когда что-то записывается в память, то во время записи биты могут быть только сброшены, но не установлены (см пример ниже):

Операция

Результат

Стирание памяти. Все биты установлены в 1

11111111 11111111 11111111 11111111

Занулим младший байт

11111111 11111111 11111111 00000000

Занулим старший байт, а младший снова устновим в 1

00000000 11111111 11111111 00000000

 
Стирается память не побайтно, а целыми страницами и это большое неудобство. Ради модификации одного байта из 50 хранимых, нам придётся читать все 50 байт, вносить изменения, стирать страницу и обратно записывать измененные данные. А если во время всей этой процедуры пропадет питание то будет не очень хорошо - потеря или искажение данных. Этого конечно можно избежать при помощи некоторых хитростей, но сейчас речь не о них. Как уже было сказано выше, прежде чем что-то писать в память или стирать её - нужно снять блокировку, а после окончания записи/стирания установить её обратно (рекомендуется). Для этого нужно последовательно записать в регистр FLASH_KEYR два числа: 0x45670123 и 0xCDEF89AB. если записать другие - то блокировку будет невозможно снять до перезагрузки контроллера.  Для того чтоб дать понять контроллеру записи/стирания чего мы от него хотим, используется регистр FLASH_CR:
 
FLASH CR
Все остальные биты кроме тех, что не отмечены зеленым предназначены для настройки прерываний и управлением записью/стиранием области Option bytes и мной не использовались. Назначение выделенных бит следующее:
PG - пока этот бит установлен нам разрешено писать во флеш
PER - Бит стирания страницы. Чтоб определить какую страницу мы будем стирать - используется регистр FLASH_AR.  Достаточно записать в него любой адрес из диапазона принадлежащего нужной странице. 
MER - Бит стирания ВСЕХ страниц. Самоуничтожение прошивки, работает.
STRT - Запуск выбранной операции (стирание страницы, всех страниц или любая выбранная другими битами этого регистра). 
LOCK - Записывая сюда единицу мы блокируем доступ на запись во флеш память.
Последний регистр который нам потребуется это FLASH_SR:

FLASH SR
В этом регистре нас интересует всего один бит - BSY. Если он единичка, то с памятью сейчас выполняются какие-то действия (запись или стирание) и начинать новую операцию записи пока нельзя. Что касается остальных битов, то они устанавливаются при ошибки записи и при её завершении. Приступим к практике, и начнем с чего попроще - с разблокировки. Функция предельно проста: 
#define FLASH_KEY1 ((uint32_t)0x45670123)
#define FLASH_KEY2 ((uint32_t)0xCDEF89AB)
void flash_unlock(void) {
  FLASH->KEYR = FLASH_KEY1;
  FLASH->KEYR = FLASH_KEY2;
}
Значения FLASH_KEY1 и FLASH_KEY2 взяты из вот этого мануала от ST, рекомендую ознакомится с ним. Думаю что тут комментарии особо не нужны. Код функции которая устанавливает блокировку выглядит так:
void flash_lock() {
  FLASH->CR |= FLASH_CR_LOCK;
}
Следующие функции предназначены для стирания флеш памяти. Разумеется перед их вызовом нужно сначала разблокировать её при помощи вышеописанной функции:
//Функция возврщает true когда можно стирать или писать память.
uint8_t flash_ready(void) {
  return !(FLASH->SR & FLASH_SR_BSY);
}
 
//Функция стирает ВСЕ страницы. При её вызове прошивка самоуничтожается 
void flash_erase_all_pages(void) {
  FLASH->CR |= FLASH_CR_MER; //Устанавливаем бит стирания ВСЕХ страниц
  FLASH->CR |= FLASH_CR_STRT; //Начать стирание
  while(!flash_ready()) // Ожидание готовности.. Хотя оно уже наверное ни к чему здесь...
    ;
  FLASH->CR &= FLASH_CR_MER;
}
 
//Функция стирает одну страницу. В качестве адреса можно использовать любой
//принадлежащий диапазону адресов той странице которую нужно очистить.
void flash_erase_page(uint32_t address) {
  FLASH->CR|= FLASH_CR_PER; //Устанавливаем бит стирания одной страницы
  FLASH->AR = address; // Задаем её адрес
  FLASH->CR|= FLASH_CR_STRT; // Запускаем стирание 
  while(!flash_ready())
    ;  //Ждем пока страница сотрется. 
  FLASH->CR&= ~FLASH_CR_PER; //Сбрасываем бит обратно
}
Ну и наконец функция записи во флеш, записываются 4 байта данных по заданному адресу:
void flash_write(uint32_t address,uint32_t data) {
  FLASH->CR |= FLASH_CR_PG; //Разрешаем программирование флеша
  while(!flash_ready()) //Ожидаем готовности флеша к записи
    ;
  *(__IO uint16_t*)address = (uint16_t)data; //Пишем младшие 2 бата
  while(!flash_ready())
    ;
  address+=2;
  data>>=16;
  *(__IO uint16_t*)address = (uint16_t)data; //Пишем старшие 2 байта
  while(!flash_ready())
    ;
  FLASH->CR &= ~(FLASH_CR_PG); //Запрещаем программирование флеша
}
Не лишним будет сказать, что использовать запись надо с осторожностью, я бы порекомендовал писать в последнюю страницу памяти, уж там-то наверняка ничего важного нет. При условии, что у вас не очень большая прошивка, конечно же. Для того чтоб проверить и наглядно показать как работают эти функции, я написал небольшую демонстрационную программу. Она может прочитать страницу и вывести её содержимое через UART, может записать в страницу тестовые данные или полностью стереть содержимое страницы. Выбор режима так же осуществляется через UART. Короче интуитивно понятный интерфейс :) Просто подключите USB-UART преобразователь к STM32VL Discovery, а конкретнее к UART1 (PA10 - RxD, PA9 - TxD). Терминал нужно настроить на скорость 9600, 1 стоп бит, 8 бит данных, без проверки четности. Вот например я пробую прочитать нулевую страницу флеш памяти:

page 0

А теперь попробуем прочитать ту же самую страницу, но уже при помощи утилиты ST-Link. Как видно, результат один и тот же: 

flash st-link

Это дает нам основание полагать, что все считывается правильно. Теперь попробуем записать в 127-ю страницу некоторые тестовые данные и потом прочитать их:

writed

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