Предисловие
Давно ни для кого не секрет, что STMicroelectronics производит замечательные 32-битные ARM микроконтроллеры STM32. В последнее время они набирают всё большую популярность, и на то есть веские причины, которые в рамках этой статьи я повторять не намерен. Кому интересно — раз, два и три. Однако у резкого повышения популярности есть и неприятные минусы — довольно часто авторы статей повторяют одни и те-же ошибки. А если ещё и в официальном документе производителя нужный момент описан поверхностно — то тут черт ногу сломит, пока найдёт решение проблемы. Именно о таком моменте я и хочу рассказать. А именно — как правильно использовать возможность записи во встроенный flash нашего МК. Добро пожаловать под кат.
Курим маны, читаем статьи
Подавляющее большинство статей (а если точнее — вообще все, которые я видел) предлагают реализацию алгоритма, рекомендуемого в официальном мануале по программированию флеша. Вот такого: Кодим, компилим, проверяем — всё работает. Хорошо работает. Ровно до тех пор, пока вы не включите оптимизацию. Я использую gcc и проверял с ключами -O1 и -O2. Без оптимизации — работает. С оптимизацией — не работает. Нервно курим, пьём кофе, и тратим день-два на поиск проблемы и размышления о бренности бытия.
Как правильно
Уж не знаю, почему производитель советует использовать неправильно работающий алгоритм. Возможно где-то это объяснено, но за два дня у меня так и не получилось ничего найти. Решение-же оказалось довольно простым — в регистре FLASH->SR для контроля окончания операции надо использовать не бит BSY (искренне не понимаю, почему даже ST рекомендует использовать его), а бит EOP, выставляющийся при окончании текущей операции стирания/записи. Причина проста — по тем или иным причинам на момент проверки бит BSY может быть ещё не выставлен. Однако бит EOP выставляется тогда, и только тогда, когда операция завершена. Сбрасывается этот бит вручную, записью в него единицы. Кодим, проверяем, радуемся жизни.
Сырцы
Для полноты картины необходимо найти и почитать другие статьи на эту тему (ссылка на одну из них приведена выше). Здесь-же прилагаю исходники с кратеньким описанием. Разблокировка работы с flash — эти две строчки необходимо вставить в функцию инициализации МК:
FLASH->KEYR = 0x45670123; FLASH->KEYR = 0xCDEF89AB;
Стирание страницы flash — перед записью необходимо стереть данные по нужным адресам, это особенность флеша:
//pageAddress - любой адрес, принадлежащий стираемой странице void Internal_Flash_Erase(unsigned int pageAddress) { while (FLASH->SR & FLASH_SR_BSY); if (FLASH->SR & FLASH_SR_EOP) { FLASH->SR = FLASH_SR_EOP; } FLASH->CR |= FLASH_CR_PER; FLASH->AR = pageAddress; FLASH->CR |= FLASH_CR_STRT; while (!(FLASH->SR & FLASH_SR_EOP)); FLASH->SR = FLASH_SR_EOP; FLASH->CR &= ~FLASH_CR_PER; }
Запись:
//data - указатель на записываемые данные //address - адрес во flash //count - количество записываемых байт, должно быть кратно 2 void Internal_Flash_Write(unsigned char* data, unsigned int address, unsigned int count) { unsigned int i; while (FLASH->SR & FLASH_SR_BSY); if (FLASH->SR & FLASH_SR_EOP) { FLASH->SR = FLASH_SR_EOP; } FLASH->CR |= FLASH_CR_PG; for (i = 0; i < count; i += 2) { *(volatile unsigned short*)(address + i) = (((unsigned short)data[i + 1]) << 8) + data[i]; while (!(FLASH->SR & FLASH_SR_EOP)); FLASH->SR = FLASH_SR_EOP; } FLASH->CR &= ~(FLASH_CR_PG); }
Вот такие пироги. Приятного всем кодинга и поменьше багов.
Да-да, та самая память в которой хранится написанная вами прошивка. Флеш памяти у 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 страниц. Все вышесказанное видно на рисунке ниже:
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:
Все остальные биты кроме тех, что не отмечены зеленым предназначены для настройки прерываний и управлением записью/стиранием области Option bytes и мной не использовались. Назначение выделенных бит следующее:PG — пока этот бит установлен нам разрешено писать во флешPER — Бит стирания страницы. Чтоб определить какую страницу мы будем стирать — используется регистр FLASH_AR. Достаточно записать в него любой адрес из диапазона принадлежащего нужной странице. MER — Бит стирания ВСЕХ страниц. Самоуничтожение прошивки, работает.STRT — Запуск выбранной операции (стирание страницы, всех страниц или любая выбранная другими битами этого регистра). LOCK — Записывая сюда единицу мы блокируем доступ на запись во флеш память.Последний регистр который нам потребуется это 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; //Сбрасываем бит обратно }
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 бит данных, без проверки четности. Вот например я пробую прочитать нулевую страницу флеш памяти:
Это дает нам основание полагать, что все считывается правильно. Теперь попробуем записать в 127-ю страницу некоторые тестовые данные и потом прочитать их:
Весь мой код реализующий данный функционал можно скачать тут, а комментарии оставить чуть ниже. Спасибо что дочитали до конца 😉
Используемые источники:
- https://habr.com/post/213771/
- https://easystm32.ru/for-beginners/38-flash-stm32/