
В программировании микроконтроллеров обычно код исполняется из on-chip NOR flash памяти. Да... Отдельная шина для кода и отдельная шина для данных (Гарвардская архитектура). Однако иной раз надо разместить Си-функцию в RAM памяти. То есть реализовать элементы принстонской архитектуры компьютера: код и данные в одной памяти на одной шине.
Определения
Секция памяти - интервал адресов в памяти процесса.
Принстонская архитектура компьютера - это кода и код и данные лежат в оперативной памяти
Компоновщик (linker) - консольная программа, которая из множества объектных файлов и скрипта с конфигом склеивает один исполняемый файл.
Причины по которым приходится исполнять код из RAM
Исполнение кода из RAM может потребоваться по целому ряду причин
1--Надо проверить, что MPU в самом деле выдает прерывания при запрете исполнения кода из специфических интервалов памяти. Например из того же SRAM. Поэтому надо для теста специально сконфигурировать пуск из RAM, чтобы увидеть как отработает MPU.
2--Ускорение вычислений. RAM память ближе к ядру процессора. Поэтому код из RAM выполняется быстрее. Некоторые процессоры специально имеют отдельный блок RAM памяти с нулевой latency (CCM, ITCM и т.п.)
3--У некоторых микроконтроллеров в RAM просто больше памяти, чем Flash. Да... Например в K1948BK018 (16kByte) или даже 5023ВС016 (256kByte RAM). Поэтому целесообразно загружать целевую прошивку сразу прямиком в SRAM.
4--Исполнение кода из RAM позволяет обновлять саму Flash память. Поэтому некоторые загрузчики временно переключаются исполнятся из RAM.
Реализация
Пробовать я буду на болгарской учебно-тренировочной электронной плате Olimex-STM32-H407 с микроконтроллером STM32f407ZG внутри. Там как раз есть непрерывный интервал RAM c адресами от 0x2000_0000...0х2001_BFFF размером 112 kByte,
Фаза 1: Определение секции
Первым делом в *.ld скрипте компоновщика следует определить имя секции RamFunc и указать ей раздел физической памяти.
/* Specify the memory areas */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 112K
CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
}
/* Initialized data sections goes into RAM, load LMA copy after code */
.data :
{
. = ALIGN(4);
_sdata = .; /* create a global symbol at data start */
*(.data) /* .data sections */
*(.data*) /* .data* sections */
__RamFunc_start__ = . ;
*(.RamFunc) /* .RamFunc sections */
*(.RamFunc*) /* .RamFunc* sections */
__RamFunc_end__ = . ;
. = ALIGN(4);
_edata = .; /* define a global symbol at data end */
} >RAM AT> FLASH
Тут происходит подмешивание кода функций (секция RamFunc) к начальным данным для глобальных переменных. Надо обратить внимание на две переменные: _sdata и edata. Они пригодятся в процедуре Reset Handler. RamFunc_start и RamFunc_end нужны для отладки.
Фаза 2: Определить функцию
Надо к каждой функции указать ключевое слово (attribute((section(".RamFunc")))) чтобы GCC компоновщик понял, что именно эту функцию и надо прописать в секцию RamFunc
__attribute__((section(".RamFunc")))
static uint32_t sram_function(uint32_t in_value) {
uint32_t out_value = in_value + 1;
LOG_INFO(TEST,"%s(),In:%u,Out:%u",__FUNCTION__,in_value,out_value);
return out_value;
}
Фаза 3. Загрузка бинарного кода функции в RAM память.
При подаче на плату электропитания SRAM память обнуляется. Поэтому надо как-то загрузить в RAM память машинный код с той функцией, которую мы хотим исполнять. В микроконтроллерах это делается внутри процедуры под названием Reset_Handler. Reset_Handler пишут на ассемблере. Перекопированием данных занимается ассемблерная функция LoopCopyDataInit.
Reset_Handler:
ldr sp, =_estack /*Load register with word, set stack pointer */
/* Copy the data segment initializers from flash to SRAM */
movs r1, #0
b LoopCopyDataInit
CopyDataInit:
ldr r3, =_sidata
ldr r3, [r3, r1]
str r3, [r0, r1]
adds r1, r1, #4
LoopCopyDataInit:
ldr r0, =_sdata /* Load register with word*/
ldr r3, =_edata /* Load register with word*/
adds r2, r0, r1 /* ADDS <Rd>,<Rn>,<Rm>*/
cmp r2, r3 /* Compare (immediate) subtracts an immediate value from a register value. It updates the condition flags based on the result, and discards the result. */
bcc CopyDataInit /* branch if carry clear */
ldr r2, =_sbss /* Load register with word*/
b LoopFillZerobss /* Branch to target address*/
/* Zero fill the bss segment. */
FillZerobss:
movs r3, #0
str r3, [r2], #4
LoopFillZerobss:
ldr r3, = _ebss
cmp r2, r3
bcc FillZerobss
/* Call the clock system intitialization function.*/
bl SystemInit
/* Call static constructors */
bl __libc_init_array
/* Call the application's entry point.*/
bl main
bx lr
.size Reset_Handler, .-Reset_Handler
Проверка
Я написал модульный тест для проверки, что функция в самом деле лежит в RAM и делает свою работу
static uint32_t g_out_value =0;
__attribute__((section(".RamFunc")))
static void sram_function_void(const uint32_t in_value)
{
g_out_value = in_value + 1;
return ;
}
bool test_ram_function_void(void){
LOG_INFO(TEST, "%s():", __FUNCTION__);
bool res = true;
ram_code_info();
log_level_get_set(ARRAY,LOG_LEVEL_DEBUG);
LOG_INFO(TEST, "sram_function_void:0x%p", sram_function_void);
res = is_ram_addr((uint32_t)sram_function_void);
ASSERT_TRUE(res);
sram_function_void(1);
ASSERT_EQ(2, g_out_value);
sram_function_void(2);
ASSERT_EQ(3, g_out_value);
log_level_get_set(ARRAY,LOG_LEVEL_INFO);
return res;
}
Как можно заметить, модульный тест проходит.

Второй способ
Это был простой способ. Однако подмешивать код к данным глобальных переменных интуитивно выглядит, как не очень красивая реализация. Может вы захотите отчистить RAM память функций, а затем снова подгрузить функции туда для нового пуска. Поэтому надо сделать отдельную секцию для RAM функций.
/* RamFunctions section
If initialized variables will be placed in this section,
the startup code needs to be modified to copy the init-values.
*/
RamFunctionsBinaryCodeStart = LOADADDR(.RamFunctions);
.RamFunctions :
{
. = ALIGN(4);
RamFuncStart = .; /* create a global symbol at RamFunctions start */
*(.RamFunctions) /* .RamFunctions sections */
*(.RamFunctions*) /* .RamFunctions* sections */
*(.Ram_Fun) /* .Ram_Fun sections */
*(.Ram_Fun*) /* .Ram_Fun* sections */
. = ALIGN(4);
RamFuncEnd = .; /* create a global symbol at RamFunctions end */
} >RAM AT> FLASH
В переменной RamFunctionsBinaryCodeStart окажется Flash адрес начала секции с бинарным кодом функций. Далее в start up коде надо прописать инструкцию bl load_ram_functions_binary
ldr r2, =_sbss /* Load register with word*/
b LoopFillZerobss /* Branch to target address*/
/* Zero fill the bss segment. */
FillZerobss:
movs r3, #0
str r3, [r2], #4
LoopFillZerobss:
ldr r3, = _ebss
cmp r2, r3
bcc FillZerobss
bl load_ram_data_binary
bl load_ram_functions_binary
/* Call the clock system intitialization function.*/
bl SystemInit
/* Call static constructors */
bl __libc_init_array
/* Call the application's entry point.*/
bl main
bx lr
.size Reset_Handler, .-Reset_Handler
В system init реализовать в си коде процедуру копирования бинарного кода функций из flash в SRAM.
/*
copy array of dwords from pSrc to pHead
pSrc->pHead
*/
static void data_copy(uint32_t * pHead,
uint32_t * pTail,
uint32_t * pSrc) {
while (pHead < pTail) {
*pHead = *pSrc;
pHead++;
pSrc++;
}
}
extern void RamFunctionsBinaryCodeStart;
extern void RamFuncStart;
extern void RamFuncEnd;
void load_ram_functions_binary(void) {
data_copy(&RamFuncStart, &RamFuncEnd, &RamFunctionsBinaryCodeStart);
}
В Си коде саму функцию надо пометить ключевым словом Ram_Fun (не RamFunctions, а Ram_Fun )
__attribute__((section(".Ram_Fun")))
static uint32_t pure_ram_function(uint32_t in_value) {
uint32_t out_value = in_value + 1;
LOG_INFO(TEST,"%s(),In:%u,Out:%u",__FUNCTION__, in_value, out_value);
return out_value;
}
Вот так отрабатывает функция из RAM.

Итог
Удалось научиться на микроконтроллерах с процессором ARM Cortex-Mх внутри запускать Си-функции прямо из SRAM памяти. Это открывает дорогу для ускорения работы кода, модульного тестирования MPU, уменьшения энергопотребления и обновления Flash памяти.
Словарь
Акроним |
Расшифровка |
GCC |
GNU Compiler Collection |
GNU |
GNU’s Not UNIX |
RAM |
Random Access Memory (ОЗУ) |
SRAM |
Static Random Access Memory |
CCM |
Core Coupled Memory |
ITCM |
Instruction Tightly-Coupled Memory |
MPU |
Memory protection unit |
MIK32 |
Mikron32 |
Ссылки
Название |
URL |
Выполняем сторонние программы на микроконтроллерах с Гарвардской архитектурой: как загружать программы без знания ABI? @bodyawm |
|
How to place and execute ARM Cortex M code in SRAM memory |
|
Еще немного про Core-Coupled Memory (CCM) на STM32 |
|
Размещение кода функции в RAM |
https://electronix.ru/forum/topic/130521-razmeschenie-koda-funktsii-v-ram/ |
Компоновщик |
|
Генерация перемещаемого кода для процессоров ARM в компиляторе LLVM @EasyLy |
|
Assembler hint for ARM, ARMv7-M |
https://docs.google.com/spreadsheets/d/1PJhyhc2xLXqMWsjBXjnfePOyHGVJKIZBc2WRMgTtgrY/edit?gid=0#gid=0 |
BEKEN: как поместить функцию в RAM |
https://microsin.net/programming/arm-troubleshooting-faq/beken-place-function-to-ram.html |
Язык управления компоновщиком |
Вопросы по тексту:
Какие виды памяти есть в микроконтроллере.
Как в Си языке во время исполнения кода узнать размер функции? Есть же возможность узнавать размер типов данных и структур.
Как из Си-кода узнать размер секции .bss .data .text и пр?
Каким образом кнопочные Siemens/Motorola/Nokia телефоны могли в run-time до устанавливать игры без пере прошивки микроконтроллера внутри?
Почему в ARM-Cortex-Mx процессорах фактический адрес функций на единицу больше, чем то значение адреса функции, что указано в *.map файле?
Что происходит с микроконтроллером, когда мы вызываем Си-функцию?
Комментарии (11)
SpLab
12.08.2025 07:17Тема интересная, но статья похожа на студенческую лабу где студент поработал с источниками но так и не понял сам. Как туториал для конкретного ядра - сойдет. Но название явно не соответствует содержанию. В классической гарвардской архитектуре код из ОЗУ выполнить нельзя. И тут автор схитрил и взял ядро cortex-m (как бы случайно) с "модифицированной гарвардской архитектурой". Но об этом нюансе ни слова. В связи с этим было бы интересно увидеть ответ автора на первый комментарий/вопрос - а что вы скажите про авр или пик?
bodyawm
12.08.2025 07:17На авр есть SPM. Можно зарезервировать область под программу и выполнять ее оттуда, перед этим записывая ее в память. На МК с MMU все проще, там можно сделать mmap в IRAM.
Об ABI, выполнении программ на МК почитайте мои статьи про хакинг телефона.
Поскольку SPM можно использовать только в адресном пространстве загрузчика, есть хак с джампом как в OptiBoot.
SpLab
12.08.2025 07:17Честно сказать не понял как связана инструкция SPM в AVR с возможностью запуска кода из озу. Понял лишь что если это возможно, то это опять хак, свойственный конкретной платформе.
Статья же вроде как прямо с заголовка определяет применимость подхода ко всем мк. И без всяких ремарок и допущений говорите о том что можно чуть ли не изменить архитектуру парой пасов линкера. Назвали бы что то вроде: Запуск кода из озу мк cortex-m в общем то и вопросов бы не возникло (нет они бы конечно возникли в силу минимального пояснения, но это туториал, подробности можно и загуглить, главное задан вектор).
А потом условная Алиса начинает выдавать "молодым специалистам" что мол архитектура это условности, вот вам несколько строк в линкере и одно превращается в другое.
За предложение ознакомится с вашими статьями спасибо, постараюсь найти время, за одно окунуться в ностальгию, все реже появляется необходимость в BareMetal коде.
vitecd
12.08.2025 07:17Статья же вроде как прямо с заголовка определяет применимость подхода ко всем мк
ну, если ВСЕ МК - ARM Cortex M, то да
unreal_undead2
12.08.2025 07:17SPM вроде как в флеш пишет - соглашусь, что это вариант для динамического изменения кода, но достаточно специализированный (использование в своих целях - скорее хак) и небыстрый (если, скажем, для JIT запользовать). И да, Принстон из Гарварда она не делает - код/данные для обычных инструкций всё равно разделены, просто есть лазейка для копирования.
bodyawm
12.08.2025 07:17Да я некорректно истолковал комментарий. Я думал что SpLab хочет посмотреть как можно запускать программы на AVR, поэтому ответил без пояснения)
unreal_undead2
Так и не понял, как передать управлению коду в RAM именно на таком процессоре (разные физические шины), скажем AVR.
rukhi7
Никак! Вот это:
звучит, конечно, не однозначно. Как будто можно аппаратную архитектуру поменять программно, но нет! Если у вас в железе Гарвардская архитектура, реализовать Принстонскую ни как не получится, без замены процессора (микросхемы).
N1X
Это похоже был мягкий намек автору на кривость формулировки. Потому что архитектура процессора она в принципе "в железе", и скрипт линкера ее "реализовать" не поможет.