11. Память, компоновка, загрузка
Между подачей питания и первой строкой main происходит довольно много.
Пока всё работает, знать это необязательно — но ровно до первого переполнения
стека или «переменная сама поменялась». Этот раздел о том, где что лежит
и кто это туда положил.
Как это называется в документации
Оригинал Перевод linker script скрипт компоновщика section секция startup code стартовый код stack pointer указатель стека heap куча vector table таблица векторов option bytes байты конфигурации read protection защита от чтения boot mode режим загрузки stack overflow переполнение стека log-structured storage журнал записей erased state стёртое состояние: все биты — единицы power-loss safe устойчивый к пропаданию питания
11.1 Где что лежит
Сборка раскладывает программу по секциям, и у каждой своё место:
| Секция | Что в ней | Где живёт |
|---|---|---|
.text |
код | Flash |
.rodata |
константы, const-таблицы, строковые литералы |
Flash |
.data |
переменные с начальным значением | ОЗУ, образ во Flash |
.bss |
переменные без начального значения | ОЗУ |
| стек | локальные переменные, адреса возврата | ОЗУ, с верхнего края |
| куча | malloc |
ОЗУ, после .bss |
Отсюда следствие, которое экономит память. Глобальная переменная
с начальным значением занимает место дважды: во Flash под образ начального
значения и в ОЗУ под саму себя. Большая таблица, объявленная без const,
съедает килобайты дефицитного ОЗУ:
uint8_t font[2048] = { /* ... */ }; /* 2 КБ Flash + 2 КБ ОЗУ */
const uint8_t font[2048] = { /* ... */ }; /* только 2 КБ Flash */
На камне с 20 КБ ОЗУ разница решающая.
Переменная без инициализации попадает в .bss и стоит только места в ОЗУ:
образа во Flash у неё нет, потому что стартовый код просто заполняет
эту область нулями.
11.2 Что происходит до main
- Аппаратный сброс. Ядро читает из таблицы векторов два первых слова:
начальное значение указателя стека и адрес
Reset_Handler. SystemInit. Базовая настройка тактирования и, при необходимости, перенос таблицы векторов. В стартовом файле Keil это первый вызовReset_Handler, ещё до библиотечной__main.- Копирование
.data. Стартовый код переносит образ начальных значений из Flash в ОЗУ. - Обнуление
.bss. Область заполняется нулями — вот почему неинициализированные глобальные переменные гарантированно нулевые, а локальные — нет. У Keil оба шага делает__main. - Инициализация библиотеки и вызов
main.
⚠️ В
SystemInitглобальные переменные ещё не готовы. У KeilSystemInitвызывается до__main: записанное в глобальную переменную потом перетрёт образ.dataили нули.bss. В стартовых файлах ST для GCC порядок зависит от семейства: у F0, F3 и F4SystemInitстоит после копирования, у G0, G4, L4, H7 и U5 — до него. Надёжнее вSystemInitглобальных переменных не трогать вовсе.
Всё это лежит в стартовом файле, который добавляется в проект вместе с пакетом устройства. Обычно его не трогают, но полезно знать, что он есть: именно там объявлены слабые заглушки обработчиков прерываний, которые замещаются вашими функциями (раздел 7).
⚠️ Локальные переменные не обнуляются. Глобальные — да, стартовым кодом. Локальная переменная получает то, что осталось в этом месте стека от предыдущих вызовов. Классическая ошибка — рассчитывать на нулевое начальное значение локального счётчика.
11.3 Скрипт компоновщика
Файл, который говорит, куда какую секцию положить. Обычно генерируется средой, и править его приходится нечасто — но понимать полезно.
Ключевых частей две. Описание областей памяти:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
И размещение секций по этим областям — где .text идёт во FLASH,
а .data объявляется в RAM, но со ссылкой на образ во FLASH.
Когда скрипт приходится править:
- разместить буфер в определённой области (например, увести данные из CCM, недоступной для DMA — раздел 9);
- оставить в конце Flash место под собственное хранилище настроек, чтобы компоновщик его не занял кодом;
- сдвинуть начало программы, если впереди стоит собственный загрузчик;
- задать размеры стека и кучи.
Разместить переменную в конкретной секции можно атрибутом:
uint8_t dma_buffer[512] __attribute__((section(".sram2")));
Атрибут работает только вместе со скриптом: в MEMORY заводят область SRAM2
(у F407 — 16 КБ с 0x2001C000, а RAM укорачивают до 112 КБ), в SECTIONS —
.sram2 (NOLOAD) : { *(.sram2) } > SRAM2. Без этого GNU ld положит секцию
куда придётся. Стартовый код такую секцию не обнуляет — начальное значение
задают в программе.
11.4 Стек и куча
Обе области живут в одном ОЗУ и растут навстречу друг другу: стек — от верхней
границы в сторону младших адресов, куча — от конца .bss в сторону старших.
Между ними свободное место, и если они встретятся, программа испортит
собственные данные.
Сколько стека нужно. Каждый вызов функции кладёт туда адрес возврата, регистры и локальные переменные. Прерывание добавляет свой кадр поверх. Глубокая рекурсия или большой локальный массив съедают стек мгновенно:
void parse(void)
{
uint8_t buffer[4096]; /* 4 КБ на стеке при 20 КБ ОЗУ — почти наверняка крах */
}
Такие буферы делают статическими или глобальными.
⚠️ Переполнение стека не даёт ошибки — на Cortex-M3/M4 без MPU. Стек просто заезжает на
.bssи портит переменные. Проявляется это как «данные меняются сами по себе», причём в месте, никак не связанном с настоящей причиной. Один из самых трудноуловимых классов отказов — приёмы отладки в разделе 35. Поймать переполнение можно сторожем MPU (раздел 11а), а у Cortex-M33 — регистрами предела стекаMSPLIMиPSPLIM.
Про malloc на микроконтроллере. Динамическая память здесь редко уместна:
куча фрагментируется, а отказ в выделении обрабатывать обычно негде.
Стандартный подход — заранее выделенные статические буферы фиксированного
размера. Если динамика всё же нужна, лучше брать её у ОСРВ (раздел 25),
где хотя бы предсказуемо поведение при нехватке.
11.5 🔧 Запись во встроенную Flash
Микроконтроллер умеет писать в собственную Flash — так хранят настройки, калибровки, счётчики наработки.
Порядок: разблокировать, стереть страницу, записать, заблокировать.
static void flash_unlock(void)
{
if (FLASH->CR & FLASH_CR_LOCK) {
FLASH->KEYR = FLASH_KEY1; /* 0x4567 0123 */
FLASH->KEYR = FLASH_KEY2; /* 0xCDEF 89AB */
}
}
uint8_t flash_erase_page(uint32_t addr)
{
flash_unlock();
flash_wait();
FLASH->SR = FLASH_SR_PGERR | FLASH_SR_WRPRTERR | FLASH_SR_EOP; /* флаги сбрасываются единицей */
FLASH->CR |= FLASH_CR_PER; /* режим стирания страницы */
FLASH->AR = addr; /* любой адрес внутри страницы */
FLASH->CR |= FLASH_CR_STRT; /* пуск */
flash_wait();
FLASH->CR &= ~FLASH_CR_PER; /* обязательно снять режим */
flash_lock();
return (FLASH->SR & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) ? 1u : 0u;
}
uint8_t flash_write(uint32_t addr, const uint16_t *data, uint32_t count)
{
if (addr & 1u) {
return 1u; /* адрес обязан быть чётным */
}
flash_unlock();
flash_wait();
FLASH->SR = FLASH_SR_PGERR | FLASH_SR_WRPRTERR | FLASH_SR_EOP;
FLASH->CR |= FLASH_CR_PG;
for (uint32_t i = 0; i < count; i++) {
*(volatile uint16_t *)(addr + i * 2u) = data[i];
flash_wait();
if (FLASH->SR & FLASH_SR_PGERR) {
FLASH->CR &= ~FLASH_CR_PG;
flash_lock();
return 1u; /* писали в нестёртую память */
}
}
FLASH->CR &= ~FLASH_CR_PG;
flash_lock();
return 0u;
}
⚠️ У F1 записывать можно только полусловами. Не байтами и не словами — ровно по 16 бит, по чётным адресам. Байтовая или словная запись при поднятом
PG— ошибка шины и HardFault. ФлагPGERRозначает другое: ячейка по адресу не стёрта. У F4 разрядность выбирается полемPSIZE, но зависит от напряжения питания: 32 бита доступны не всегда.⚠️ Ключи разблокировки идут строго по порядку. Записали неверный — блок программирования запирается до сброса, и дальнейшие попытки бесполезны.
Чтение никаких церемоний не требует: Flash отображена в память, читайте как обычную переменную по адресу.
Разница между семействами: у F1 единица стирания — страница 1 КБ (2 КБ у старших), у F4 — сектор, причём разного размера: первые четыре по 16 КБ, потом один 64 КБ, дальше по 128 КБ. Отвести «последнюю страницу под настройки» на F4 означает пожертвовать сразу 128 КБ.
Как хранить настройки, чтобы страница не износилась, а пропадание питания ничего не испортило, — в 11.8.
11.6 Байты конфигурации и защита
Отдельная небольшая область, которая настраивает поведение самого кристалла:
| Параметр | Что делает |
|---|---|
| RDP | защита Flash от чтения через отладчик |
| WRP | защита отдельных страниц от записи |
nRST_STOP, nRST_STDBY |
сбрасывать ли камень при входе в режимы сна |
WDG_SW |
сторожевой таймер аппаратный или программный |
⚠️ Защита от чтения снимается только вместе со стиранием всей Flash. Это не побочный эффект, а замысел: иначе защита не имела бы смысла. Включив её на этапе отладки, вы не «залочите плату навсегда» — но каждую перепрошивку будете начинать с полного стирания. А вот второй уровень защиты у некоторых семейств необратим: отладочный интерфейс отключается насовсем.
Уровень защиты задаётся значением байта RDP:
| Уровень | F1 | F4 | Что значит |
|---|---|---|---|
| 0 | 0xA5 |
0xAA |
защиты нет |
| 1 | любое другое | любое, кроме 0xAA и 0xCC |
отладчик Flash не читает; снятие — со стиранием всей Flash |
| 2 | нет | 0xCC |
навсегда: отладка и загрузка из системной памяти отключены, байты конфигурации больше не меняются |
У обеих серий «без защиты» — одно особое значение, а все остальные защищают.
Поэтому стёртые байты конфигурации (0xFF) дают защищённый камень, а не открытый.
11.7 Как камень выбирает, откуда стартовать
Это уже разбиралось в разделе 4 применительно к прошивке, здесь — со стороны памяти:
| BOOT1 | BOOT0 | Откуда стартует |
|---|---|---|
| × | 0 | из Flash — обычная работа |
| 0 | 1 | из системной памяти — заводской загрузчик |
| 1 | 1 | из ОЗУ |
При обычной загрузке Flash дублируется с адреса 0x0000 0000, поэтому
таблица векторов оказывается там, где её ждёт ядро. При старте из системной
памяти на нулевой адрес отображается заводской загрузчик.
Свой загрузчик строится на этом же механизме: программа занимает начало Flash, проверяет условие (нажата кнопка, пришла команда), при необходимости принимает новую прошивку и записывает её дальше по адресам, а затем передаёт управление основному приложению. Основному приложению при этом нужно сдвинуть таблицу векторов:
SCB->VTOR = 0x08008000u; /* если приложение начинается с 32 КБ */
У F1 SystemInit и сам пишет SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET —
вместо строки можно задать там VECT_TAB_OFFSET = 0x8000. А переход
из загрузчика в приложение — не одна строка: запретить прерывания,
остановить SysTick, загрузить MSP из первого слова таблицы приложения
и перейти по адресу из второго (раздел 48).
Забытая строка даёт самый сбивающий с толку эффект: программа работает, пока не придёт первое прерывание, — а потом уходит в обработчик загрузчика.
11.8 🔧 Настройки во Flash: журнал записей
Запись из 11.5 работает, но если при каждом сохранении стирать страницу и писать настройки заново, возникают три беды.
- Износ. Гарантированный ресурс страницы у F1 и F4 — 10 000 стираний. Кнопка «сохранить», которую нажимают 100 раз в день, израсходует его за 100 дней.
- Пропадание питания. Между стиранием и записью настроек нет нигде: выключили питание в этот момент — после включения пусто.
- Новая прошивка. Добавили в структуру поле — старые байты читаются по новой раскладке и превращаются в мусор.
Решение — журнал на двух страницах. Запись — 32 байта: настройки, номер сохранения и CRC-32. Новые записи ложатся подряд на свободное место; страница стирается, только когда место кончилось, — причём не текущая, а другая, где лежат лишь устаревшие записи. При старте берётся годная запись с наибольшим номером.
typedef struct {
uint16_t version; /* раскладка структуры */
uint16_t brightness;
int16_t offset_mv;
uint16_t flags;
uint32_t baud;
char name[12];
} cfg_t;
typedef struct {
cfg_t cfg;
uint32_t seq; /* номер сохранения */
uint32_t crc; /* CRC-32 от cfg и seq */
} rec_t;
/* годная запись с наибольшим номером на обеих страницах */
static const rec_t *newest(uint32_t *page_of)
{
const rec_t *best = NULL;
for (uint32_t p = CFG_BASE; p < CFG_BASE + 2u * PAGE; p += PAGE)
for (uint32_t i = 0; i < SLOTS; i++) {
const rec_t *r = slot(p, i);
if (r->crc == crc32(r, offsetof(rec_t, crc)) && (best == NULL || r->seq > best->seq)) {
best = r;
*page_of = p;
}
}
return best;
}
int cfg_save(const cfg_t *in)
{
uint32_t page = CFG_BASE, i;
const rec_t *old = newest(&page);
rec_t rec;
uint16_t buf[sizeof(rec_t) / 2];
for (i = 0; i < SLOTS && !is_free(slot(page, i)); i++) { }
if (i == SLOTS) { /* страница кончилась: стираем другую — */
page = (page == CFG_BASE) ? CFG_BASE + PAGE : CFG_BASE;
if (flash_erase_page(page)) /* там только устаревшие записи */
return 1;
i = 0;
}
memcpy(&rec.cfg, in, sizeof rec.cfg);
rec.seq = old ? old->seq + 1u : 1u;
rec.crc = crc32(&rec, offsetof(rec_t, crc));
memcpy(buf, &rec, sizeof buf); /* запись — полусловами */
return flash_write(page + i * sizeof(rec_t), buf, sizeof buf / 2u);
}
/* при старте: сохранённое, а если его нет или раскладка другая — умолчания */
void cfg_get(cfg_t *out)
{
if (!cfg_load(out) || out->version != CFG_VERSION)
*out = cfg_defaults;
}
Журнал занимает две последние страницы F103C8 — 0x0800 F800 и 0x0800 FC00;
стирание и запись полусловами — функции из 11.5. crc32 — обычная побитовая
CRC-32 с многочленом 0xEDB88320, та же, что у архиваторов.
Износ. На странице 32 записи, страницы чередуются, и каждая стирается раз в 64 сохранения: 10 000 × 64 = 640 000 сохранений. При 100 сохранениях в день это 6400 дней, 17,5 года вместо 100 дней. Модель на тысяче сохранений насчитала 15 + 16 = 31 стирание вместо 1000.
Пропадание питания. Стёртая Flash — все единицы, поэтому свободное место видно без отдельных пометок. Годной записью оно не притворится: CRC-32 от 28 байт 0xFF равна 0x80E3231D, а в поле CRC стоит 0xFFFFFFFF. Недописанная запись и не свободна, и не годна — её пропускают, следующая ложится за ней.
Модель обрывала питание в 17 точках 40-го сохранения: до первого из 16 полуслов и после каждого из них. 16 раз из 17 после включения читалось 39-е сохранение, в последнем случае, когда записаны все 16 полуслов, — 40-е; мусора не было ни разу, и следующее сохранение каждый раз проходило. Обрыв посреди стирания тоже не страшен: стирается страница без последней записи, та лежит на другой, — после такого обрыва модель прочитала 64-е сохранение.
Версия. Поле version отличает запись прошлой прошивки с другой
раскладкой: cfg_get вместо неё отдаёт умолчания. В модели сохранённая
яркость 77 с версией 0 при старте дала умолчание 500.
На F4 то же самое, только стирается сектор. Под журнал отводят либо два сектора по 16 КБ сразу за таблицей векторов — 16 384 / 32 = 512 записей в каждом, но образ программы тогда обходит их в скрипте компоновщика (11.3), — либо два последних сектора по 128 КБ: просто, но дорого.
⚠️ Пока идёт стирание, код из Flash не выполняется. У F1 один банк, и выборка команд ждёт конца стирания страницы — это десятки миллисекунд. Прерывания всё это время запаздывают. Сохраняйте, когда это не мешает, либо держите срочный обработчик в ОЗУ. У F407 стирание дольше: сектор 16 КБ — 0,25 с типично и до 0,5 с, сектор 128 КБ — 1 с и до 2 с (при записи словом; байтами — ещё дольше). Таймаут сторожа (12.2) берут с запасом на стирание.
⚠️ F1 пишет и стирает Flash только при включённом HSI. Кто выключил внутренний генератор ради экономии после перехода на кварц, получит Flash, которая не пишется.
⚠️ Блок больше страницы. У F103C8 страница 1 КБ. Структура, записанная блоком в 2 КБ, залезает на соседнюю страницу, которую никто не стирал, и запись обрывается ошибкой программирования на середине.
11.9 F0 и G0: полуслова у F0, двойные слова у G0
F0 пишет во Flash как F1: только полусловами, страница стирается
по любому адресу внутри неё через FLASH->AR, ключи разблокировки те же.
Рецепт 11.5 переносится без правок; отличается лишь размер страницы —
у F072 2 КБ (у F03x–F05x — 1 КБ), 64 страницы на 128 КБ:
FLASH->CR |= FLASH_CR_PER;
FLASH->AR = addr; /* любой адрес внутри страницы */
FLASH->CR |= FLASH_CR_STRT;
*(volatile uint16_t *)(addr + i * 2u) = data[i]; /* полусловами, как у F1 */
G0 — другой контроллер, общий с L4, G4 и H7-семейством: регистры
сдвинуты (KEYR на 0x08, CR на 0x14), регистра AR нет — страницу
стирают по номеру в поле PNB регистра CR, а пишут двойными
словами: адрес кратен 8, два 32-битных слова подряд. Байт или полуслово
контроллер отвергнет ошибкой размера SIZERR, а два слова не из одного
двойного слова — ошибкой выравнивания PGAERR. Ключи в заголовке
камня не объявлены — они в HAL, значения те же 0x45670123 и 0xCDEF89AB.
Занятость — не BSY, а BSY1 и CFGBSY; ошибки — SIZERR, PGAERR,
PGSERR, PROGERR, WRPERR. Перед новой операцией их сбрасывают записью
единиц в FLASH->SR: с невычищенным флагом контроллер поставит PGSERR,
и запись не начнётся. Код F1/F0 на G0 не собирается: no member named 'AR' in
'FLASH_TypeDef'.
FLASH->CR = (FLASH->CR & ~FLASH_CR_PNB) | (page << FLASH_CR_PNB_Pos) | FLASH_CR_PER; /* номер, не адрес */
FLASH->CR |= FLASH_CR_STRT;
*(volatile uint32_t *)(addr + i * 8u) = (uint32_t)data[i];
__ISB();
*(volatile uint32_t *)(addr + i * 8u + 4u) = (uint32_t)(data[i] >> 32);
flash_wait();
Для журнала настроек из 11.8 это значит: запись у G0 идёт порциями по 8 байт, и размер записи журнала стоит сделать кратным восьми, а не двум. Страница у G071 — те же 2 КБ, 64 страницы на 128 КБ.
11.10 Грабли
Локальный счётчик начинается не с нуля. Локальные переменные стартовым кодом не обнуляются (11.2).
Переменные меняются «сами». Стек переполнился и заехал на .bss; ошибки при этом нет (11.4).
HardFault при записи во Flash F1. Писали байтом или словом, а можно только полусловами по чётным адресам (11.5).
Запись во Flash F1 поднимает PGERR. Писали в нестёртую ячейку: перед записью страницу стирают (11.5).
Блок программирования заперся до сброса. Ключи разблокировки записаны не в том порядке (11.5).
Снять защиту от чтения можно только со стиранием всей Flash, а второй уровень защиты у некоторых семейств необратим (11.6).
Прерывания опаздывают на десятки миллисекунд при сохранении настроек. Пока идёт стирание, код из Flash не выполняется (11.8).
Flash F1 не пишется после перехода на кварц. Запись и стирание требуют включённого HSI (11.8).
Запись обрывается на середине блока. Блок больше страницы (у F103C8 — 1 КБ) залез на нестёртую соседнюю (11.8).
11.11 Итоги
- Переменная с начальным значением занимает место дважды: во Flash под образ
и в ОЗУ под себя.
constдля больших таблиц оставляет их только во Flash. - Глобальные переменные обнуляются стартовым кодом, локальные — нет.
- Стек и куча растут навстречу; переполнение стека не даёт ошибки, а тихо портит соседние данные.
- Большие буферы делайте статическими, а не локальными.
- У F1 Flash программируется только полусловами по чётным адресам.
- Ключи разблокировки Flash идут строго по порядку; ошибка запирает блок до сброса.
- Единица стирания: у F1 страница 1 КБ, у F4 сектор от 16 до 128 КБ.
- Защита от чтения снимается только полным стиранием; второй уровень
у некоторых семейств необратим. Без защиты — только
RDP= 0xA5 (F1) или 0xAA (F4); у F4 значение 0xCC отключает отладку навсегда. - Настройки хранят журналом на двух страницах: записи подряд, стирается только страница с устаревшими записями, у записи номер и CRC. Ресурс вырастает в 64 раза, пропадание питания не портит ничего, поле версии защищает от смены раскладки.
- Приложение за загрузчиком обязано сдвинуть
SCB->VTOR, иначе прерывания уйдут в чужие обработчики.
Полные исходники
flash_f0.c — запись во Flash STM32F072 полусловами.
/* Раздел 11.9: запись во Flash STM32F072 — как у F1: полусловами, стирание по адресу. */
#include <stddef.h>
#include "stm32f0xx.h"
_Static_assert(offsetof(FLASH_TypeDef, KEYR) == 0x04u && offsetof(FLASH_TypeDef, AR) == 0x14u, "KEYR и AR как у F1");
_Static_assert(FLASH_BANK1_END == 0x0801FFFFu, "128 КБ Flash у F072");
#define PAGE_SIZE 2048u /* у F072 страница 2 КБ (у F03x/F04x/F05x — 1 КБ) */
_Static_assert((FLASH_BANK1_END + 1u - FLASH_BASE) / PAGE_SIZE == 64u, "64 страницы");
static void flash_unlock(void)
{
if (FLASH->CR & FLASH_CR_LOCK) {
FLASH->KEYR = FLASH_KEY1;
FLASH->KEYR = FLASH_KEY2;
}
}
static void flash_wait(void)
{
while (FLASH->SR & FLASH_SR_BSY) {
}
if (FLASH->SR & FLASH_SR_EOP) {
FLASH->SR = FLASH_SR_EOP;
}
}
uint8_t flash_erase_page_f0(uint32_t addr)
{
flash_unlock();
flash_wait();
FLASH->CR |= FLASH_CR_PER;
FLASH->AR = addr; /* любой адрес внутри страницы */
FLASH->CR |= FLASH_CR_STRT;
flash_wait();
FLASH->CR &= ~FLASH_CR_PER;
FLASH->CR |= FLASH_CR_LOCK;
return (FLASH->SR & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) ? 1u : 0u;
}
uint8_t flash_write_f0(uint32_t addr, const uint16_t *data, uint32_t count)
{
if (addr & 1u) {
return 1u;
}
flash_unlock();
flash_wait();
FLASH->CR |= FLASH_CR_PG;
for (uint32_t i = 0; i < count; i++) {
*(volatile uint16_t *)(addr + i * 2u) = data[i]; /* полусловами, как у F1 */
flash_wait();
}
FLASH->CR &= ~FLASH_CR_PG;
FLASH->CR |= FLASH_CR_LOCK;
return (FLASH->SR & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) ? 1u : 0u;
}
flash_g0.c — запись во Flash STM32G071 двойными словами.
/* Раздел 11.9: запись во Flash STM32G071 — двойными словами, стирание по номеру страницы. */
#include <stddef.h>
#include "stm32g0xx.h"
_Static_assert(offsetof(FLASH_TypeDef, KEYR) == 0x08u && offsetof(FLASH_TypeDef, CR) == 0x14u, "KEYR на 0x08, CR на 0x14");
#define PAGE_SIZE 2048u /* страница 2 КБ */
#define FLASH_KB 128u /* G071xB */
_Static_assert(FLASH_KB * 1024u / PAGE_SIZE == 64u, "64 страницы");
_Static_assert(FLASH_CR_PNB_Pos == 3u, "номер страницы — поле PNB в CR");
/* ключи в заголовке камня G0 не объявлены — они в HAL (stm32g0xx_hal_flash.h); значения те же, что у F1 и F0 */
#define FLASH_KEY1 0x45670123u
#define FLASH_KEY2 0xCDEF89ABu
static void flash_unlock(void)
{
if (FLASH->CR & FLASH_CR_LOCK) {
FLASH->KEYR = FLASH_KEY1;
FLASH->KEYR = FLASH_KEY2;
}
}
static void flash_wait(void)
{
while (FLASH->SR & (FLASH_SR_BSY1 | FLASH_SR_CFGBSY)) { /* BSY1 и CFGBSY, не BSY */
}
if (FLASH->SR & FLASH_SR_EOP) {
FLASH->SR = FLASH_SR_EOP;
}
}
uint8_t flash_erase_page_g0(uint32_t page)
{
flash_unlock();
flash_wait();
FLASH->CR = (FLASH->CR & ~FLASH_CR_PNB) | (page << FLASH_CR_PNB_Pos) | FLASH_CR_PER; /* номер, не адрес */
FLASH->CR |= FLASH_CR_STRT;
flash_wait();
FLASH->CR &= ~FLASH_CR_PER;
FLASH->CR |= FLASH_CR_LOCK;
return (FLASH->SR & (FLASH_SR_PGAERR | FLASH_SR_PGSERR | FLASH_SR_WRPERR)) ? 1u : 0u;
}
/* Запись двойными словами: адрес кратен 8, каждая запись — два слова подряд. */
uint8_t flash_write_g0(uint32_t addr, const uint64_t *data, uint32_t count)
{
if (addr & 7u) {
return 1u;
}
flash_unlock();
flash_wait();
FLASH->CR |= FLASH_CR_PG;
for (uint32_t i = 0; i < count; i++) {
*(volatile uint32_t *)(addr + i * 8u) = (uint32_t)data[i];
__ISB();
*(volatile uint32_t *)(addr + i * 8u + 4u) = (uint32_t)(data[i] >> 32);
flash_wait();
}
FLASH->CR &= ~FLASH_CR_PG;
FLASH->CR |= FLASH_CR_LOCK;
return (FLASH->SR & (FLASH_SR_PGAERR | FLASH_SR_PGSERR | FLASH_SR_WRPERR)) ? 1u : 0u;
}
Дальше: раздел 11а «MPU: защита памяти от своих же ошибок».