
В программировании микроконтроллеров обычно код исполняется из 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/ |
Компоновщик |
|
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 файле?
Что происходит с микроконтроллером, когда мы вызываем Си-функцию?
unreal_undead2
Так и не понял, как передать управлению коду в RAM именно на таком процессоре (разные физические шины), скажем AVR.
rukhi7
Никак! Вот это:
звучит, конечно, не однозначно. Как будто можно аппаратную архитектуру поменять программно, но нет! Если у вас в железе Гарвардская архитектура, реализовать Принстонскую ни как не получится, без замены процессора (микросхемы).
N1X
Это похоже был мягкий намек автору на кривость формулировки. Потому что архитектура процессора она в принципе "в железе", и скрипт линкера ее "реализовать" не поможет.