
Как вы могли заметить у микроконтроллеров STM32 секторы NOR Flash памяти обладают разным размером: 16kByte(4 шт), 64kByte (1 шт), 128kByte ( 7+ шт.). Это накладывает определенную специфику на программирование микроконтроллеров STM32. Из каких секций обычно состоит Flash память микроконтроллерной программы? В этом тексте я предлагаю решение проблемы разметки памяти для случая работы с микроконтроллерами STM32.
Что обычно приходится помещать во Flash память? А вот что
№ | Сектор памяти |
1 | Таблица векторов прерываний (ISR vector) |
2 | NVRAM (энергонезависимая память) |
3 | Конфигурационные данные |
4 | Калибровочные данные (можно объединить с конфиг. данными или отправить в NVRAM) |
5 | Бинарный код программы (приложение) |
6 | Данные для инициализации глобальных переменных |
7 | Первичный загрузчик (mbr) |
8 | Вторичный загрузчик (bootloader) |
Итак обо всем по порядку....
Определения
Секция памяти - интервал адресов в памяти процесса.
Компоновщик (linker) - консольная программа, которая из множества объектных файлов и скрипта с конфигом склеивает один исполняемый файл.
таблица векторов прерываний - это таблица адресов функций обработчиков прерываний. Первая колонка это номер прерывания, вторая колонка абсолютный адрес обработчика прерываний в on-chip NOR-Flash памяти. Обычно в этой таблице сотни записей. У каждого микроконтроллера она своя. В бинарном файле перечислена вторая колонка как массив uint32_t: индекс массива-номер прерывания, значение - адрес ISR.
Данные калибровки (calibration data)- данные которые будут применены как значения параметров программного обеспечения после построения артефактов в процессе разработки. Например код страны, расположение руля. Калибровочные данные не содержат исполняемый или интерпретируемый код.
загрузчик - это отдельная прошивка, которая загружает другую прошивку.
Таблица векторов прерываний
Начало любой прошивки - это таблица векторов прерываний. Там хранится значения верхушки стека, адрес первой функции (reset handler) и адреса всех обработчиков прерываний. В случае с STM32 надо, чтобы по адресу 0x0800 0000 всегда был валидный reset handler. Без этого прошивка заклинит при подаче питания. Причем адрес верхушки стека не должен выходить за диапазон RAM памяти для микроконтроллера в который вписывается эта прошивка. Иначе прошивка тоже заклинит.
Таблица векторов прерываний жестко завязана на оно конкретное смещение в Flash памяти. Нельзя просто так взять и прописать *.bin файл по произвольному смещению. В этом случае PC прыгнет исполнять код мимо ResetHandler-а и прошивка просто зависнет.
Конфигурационные и калибровочные данные (ConfigurationData и СalibrationData)
« Лучшее программирование — конфигурирование »
Хорошей практикой считается когда код и данные разделены в программе. Сейчас объясню почему. Это нужно для того, чтобы можно было поменять конфиг без необходимости пере собирать всю программу. Это заложено в стандарт программирования ISO-26262. ISO-26262 требует раздельного хранения калибровочных данных, конфигурационных данных и кода внутри прошивки. Принцип уходит прямиком в гарвардскую архитектуру компьютера, где код и данные разделены по разным местам.
Это требование возникло не на пустом месте, а сделано как раз для того, чтобы после выпуска релиза с программой можно было проводить гибкую товарную политику. Для этого даже была разработана культовая утилита TunerPRO.
TunerPRO - это бесплатный бинарный редактор прошивок. Накладывать патчи. Эта программа позволит вам редактировать константы в готовом .bin файле. Минуя стадию повторной пересборки всего проекта прошивки (компиляции и компоновки). Это очень выручает, когда на персональном компьютере, по разным причинам, нет toolchain-а разработчика. Можно сказать, что TunerPRO - хакерская tool-а. Ещё TunerPRO позволяет сравнивать .bin файлы, искать паттерны и многое другое.
Я видел как сервисные центры считывают прошивку из ECU. Далее программой TunerPRO меняют в ней конкретную константу (например калибровки дроссельной заслонки) и записывают модифицированную прошивку обратно в ECU. Поэтому и надо сделать так, чтобы сервисным центрам было проще найти нужную константу для настройки прошивки под конкретные агрегаты.
В скрипте компоновщика надо определить отдельную секцию CONFIGURATION_DATA для конфигурационных данных
/* Specify the memory areas */ MEMORY { .... FLASH1 (rx) : ORIGIN = 0x08000000, LENGTH = 1K CONFIGURATION_DATA (rx) : ORIGIN = 0x08000400, LENGTH = 31K .... } /* Define output sections */ SECTIONS { .... /*We will change content of ConfigurationData with TunerPRO program*/ .ConfigurationData : { __configuration_data_section_start__ = .; KEEP(*(.ConfigurationData)) __configuration_data_section_end__ = .; FILL(0xFF); . = ORIGIN(CONFIGURATION_DATA) + LENGTH(CONFIGURATION_DATA); } >CONFIGURATION_DATA ....
В коде все конфигурационные данные оснащать ключевым словом SECTION_CFG_DATA
#define SECTION_CFG_DATA __attribute__((section (".ConfigurationData"))) ClockConfig_t SECTION_CFG_DATA ClockConfig = { .irq_priority = 7, .hf_source = CLK_HI_FREQ_8MHZ, // CLOCK_HF_EXTERNAL, .lf_source = CLK_LOW_FREQ_32KHZ, // CLOCK_LF_EXTERNAL, .core_source = CLOCK_CORE_SRC_PLL0, .valid = true, .core_clock_hz = 168000000, .pll0 = 168000000, .pll1 = 168000000, };
Отдельная секция для конфигурационных данных сделает так, что от сборки к сборке конфигурационные данные всегда будут попадать в одни и те же абсолютные физические адреса. Поэтому вам не придется каждый раз после пересборки прошивки опять подготавливать новый *.XDF файл для утилиты TunerPRO.
Энергонезависимая память (NVRAM)
«Любая вещь лучше, когда внутри неё есть NVRAM.»
В разработке на микроконтроллерах хорошей практикой считается, когда на устройстве есть число-хранилище для запоминания чисел между пере сбросом питания. Все это называют по-своему: NVRAM, StoreFS, FlashFS, Энергонезависимая Key-Value Map(а), HashMap(ка), NVS, memory-manager, накопитель и прочее и прочее. Наличие NVRAM - это требование протокола UDS.
Проблема в том, что для NVRAM для оптимизации износа памяти надо два сектора. Большие секторы выделять нельзя, так как не хватит памяти для generic приложения. При этом для NVRAM обычно не требуется много памяти. Очевидно, что для NVRAM надо выделить секторы по 16кБайт. В случае c STM проблема в том, что эти удобные для NVRAM секторы размещены в середине по отношению к остальным секторам flash памяти. Получается, что на загрузчик остается 16-32k Byte памяти. Как решить эту проблему будет написано чуть позже.
В коде самой прошивки надо занять диапазон адресов для NVRAM константным массивом и заполнять его сплошняком значением 0xFF (чистая память). Делается это вот так.
#define K_BYTES 1024UL #define NVS_SECTOR_SIZE (16 * K_BYTES) #define NVS_SIZE (2 * NVS_SECTOR_SIZE) #define SECTION_NVRAM __attribute__((section (".NvRam"))) const uint8_t SECTION_NVRAM nvram_memory[NVS_SIZE]= { [0 ... (NVS_SIZE-1)] = 0xFF };
При этом в скрипте компоновщика надо определить секцию NvRam
/* Specify the memory areas */ MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 112K CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K FLASH1 (rx) : ORIGIN = 0x08000000, LENGTH = 1K CONFIGURATION_DATA (rx) : ORIGIN = 0x08000400, LENGTH = 31K NVRAM (rx) : ORIGIN = 0x08008000, LENGTH = 32K FLASH2 (rx) : ORIGIN = 0x08010000, LENGTH = 690K } /* Define output sections */ SECTIONS { ..... /* placing my named section at given address: */ .NvRam : { . = ALIGN(4); _nvram_start = .; KEEP(*(.NvRam)) KEEP(*(.NvRam.*)) FILL(0xFF); . = ORIGIN(NVRAM) + LENGTH(NVRAM); /* Move location counter to end of FLASH */ _nvram_end = .; } > NVRAM ...... }
Код Приложения (App или Generic-прошивка он же секция text)
" Код отдельно, конфиги - отдельно "
Generic-прошивка - это самая большая секция в памяти. В ней хранится код самого приложения. То для чего и была разработана вся прошивка. При этом саму физическую загрузку прошивки должен осуществлять не загрузчик, а, как не парадоксально, само приложение. Дело в том, что в приложении больше памяти и больше драйверов для разнообразных интерфейсов. Поэтому ничего не мешает 10% прошивки получить по WiFi, 20% по BlueTooth, 30% по RS485, еще 30% по CAN. A настройки NVRAM получить от сканера QR кодов в драйвере видеокамеры. У приложений обычно и так есть поддержка очень богатого connectivity. Если бы вы пытались засунуть поддержку всех возможных интерфейсов и протоколов в загрузчик, то загрузчик был бы больше самого приложения! Поэтому закачкой данных должно заниматься именно приложение. Приложение, как губка впитывает прошивку со всех сторон.
Загрузчик (Bootloader)
«Самая лучшая функция прошивки — это возможность обновления прошивки.»
Обычно загрузчик стартует сразу после подачи питания перед запуском приложения. Это чисто системная часть кода. Загрузчик должен быть компактным в плане требуемого ROM. Если загрузчик маленький, то будет больше места для приложения и его бизнес логики, которая и создает основную ценность продукта. Загрузчик должен уметь прошивать как минимум по UART. Когда всё сломается пере прошивка по UART окажется единственным спасением. Нет проводного интерфейса проще UART. В UART нечему ломаться. UART требует меньше всего NOR-Flash(а). Плюс для UART куча дешевых переходников USB-UART и бесплатного софта для serial портов типа Putty и TeraTerm.
Современные загрузчики порой требуют поддержки сложных протоколов, работы с NVRAM и зачастую требуют конфигурации через UART-CLI. Уместить такой загрузчик в 16-32kByte практически невозможно. В связи с этим приходится загрузчик квартировать в секторе размером 126kByte.
Первичный загрузчик (mbr)
Как следует из названия первичный загрузчик запускает вторичный загрузчик. В случае с STM32 первичный загрузчик всегда стартует с адреса начала FLASH памяти: 0x0800_0000. Всё, что по-большому счету требуется от mbr - это прыгнуть на константный адрес. Как правило, на вторичный загрузчик, который у меня обычно прописан в самом последнем секторе flash памяти (0x080E_0000). Причем прыгать надо с осторожностью. Надо сперва проверить, что там лежит валидная таблица векторов прерываний. Иначе зависнем. Если таблица содержит абсурдные значения, то не прыгаем, а просто крутимся в суперцикле и даем на LEDы индикацию об ошибке.
Если бы все секторы FLASH памяти были одного размера (как у всех нормальных производителей микроконтроллеров), например по 8kByte, то не было бы и необходимости в отдельной сборке под названием mbr. Мы бы просто прописали начиная с 0x08000000 полноценный загрузчик любого размера, который бы стартовал при подаче питания и запускал приложение.
Пример простой разметки памяти
Вот пример типичного монолитного приложения для STM32. Это приложение без загрузчика. Тут фактически только приложение NVRAM и сектор с отщепленными конфигурационными данными.
# | Начальный | kByte | ||
сектора | Название | адрес | Размер | Пояснение |
0 | ISR vector | 0x08000000 | 1 | Таблица векторов прерываний |
0 и 1 | CFG_DATA | 0x08000400 | 31 | Конфигурационные данные |
2 | NVRAM | 0x08008000 | 32 | Файловая система |
4 | App | 0x08010000 | 448 | Приложение |
Пример разметки памяти в случае использования загрузчика
Как уже было сказано, при вклинивании NVRAM под загрузчик у вас останется от 16 до 32 килобайт. А этого мало ввиду того, что современные загрузчики требуют поддержки сложных протоколов и зачастую требуют конфигурации через UART-CLI. Уместить такой загрузчик в 16-32kByte практически невозможно. В связи с этим приходится загрузчик квартировать в секторе размером 126kByte. Но вот незадача. При подачи питания MCU начинает исполнять код с адреса 0x08000000 нулевого сектора. Если в нулевом секторе нет загрузчика, то прошивка заклинивает. В связи с этим надо написать еще один загрузчик mbr, чтобы запустить полноценный загрузчик. Совсем простую и тупую прошивку, которая просто прыгает на константный адрес. Получается чтобы работать с микроконтроллером STM32 под каждую плату надо написать три прошивки: mbr, загрузчик и приложение.
№ | Название проекта | Стартовый адрес | Пояснение |
1 | mbr | 0x0800 0000 | Первичный загрузчик |
2 | загрузчик | 0x080e 0000 | Вторичный загрузчик |
3 | приложение | 0x0801 0000 | Приложение |
Это можно метафорично сравнить с трехступенчатой космической ракетой или попросту с коробкой переключения передач. Прошивка, которая содержит первичный загрузчик, вторичный загрузчик, NVRAM и само приложение.

Итог
Как видите, применение микроконтроллеров STM32 имеет свои особенности. Любая разработка на STM32 требует написания, как минимум, трех прошивок: mbr, bootloader и generic.
Ссылки
Название | URL |
Aтрибуты Хорошей Прошивки (Firmware) | |
ARM Cortex-M: Исполнение кода из RAM памяти | |
Размещение глобальных констант по фиксированным адресам | |
ISO 26262-6 разбор документа (или как писать безопасный софт) | |
TunerPro Quick guide | https://oldskulltuning.com/wp-content/uploads/2023/03/Quick-guide-rev1.4.pdf |
Пуск LittleFS (NVRAM с запретом до-записи flash) | |
Атрибуты Хорошего Загрузчика | |
Настройка ToolChain(а) для Win10+GCC+С+Makefile+ARM Cortex-Mx+GDB | |
Обзор утилиты TunerPro (или const volatile) | |
NVRAM для микроконтроллеров |
