Уже не редкость видеть темы, где разработчики спрашивают о возможности исполнения программ из ОЗУ, внешней памяти и т.п. Этот вопрос вас не должен особо волновать, так как центральному процессору не имеет значения откуда принимать команды для исполнения.
В большинстве своем современные микроконтроллеры основаны на гарвардской архитектуре.
Главной для нас особенностью будет физическое разделение памяти команд и данных в архитектуре. Это несомненно повышает производительность в частных случаях, так как ЦП не нужно тратить время на переключение шин.
Микроконтроллер
Все эксперименты я буду проводить на ARM контроллере ATSAM4C и среде разработки Atmel Studio. Системы этой серии имеют как раз две раздельные шины в блоке.
Естественно, по шине System bus подключается оперативная память, а также периферия. Если мы запустим код по этой шине, то его выполнение будет замедленно в ~2 раза как раз из-за невозможности одновременного исполнения команд и их обработки. Однако в этом случае нам может помочь кэш.
Матрица
К некоторым устройствам подходит и System bus и Code bus. В документации это обозначено в разделе Bus Matrix.
Выгодно использовать лишь параллельный интерфейс EBI для целей расширения памяти, а остальные только в частных случаях.
Студия
Для того, чтобы программа работала из нужного места, необходимо дать соответствующие инструкции компилятору. Это делается с помощью надстроек Linker-а, скрипт xxx_flash.ld. Есть еще xxx_sram.ld, но он обычно отключен.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") OUTPUT_ARCH(arm) SEARCH_DIR(.) /* Распределение адресного пространства */ MEMORY { rom (rx) : ORIGIN = 0x01000000, LENGTH = 0x00080000 //512кб выделено во flash-памяти для программы, флаг: чтение и исполнение ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00026000 //152кб выделено в регионе SRAM0 для данных, флаг: чтение, запись, исполнение. } /* Размер стека для программы. */ STACK_SIZE = DEFINED(STACK_SIZE) ? STACK_SIZE : 0x3000; INCLUDE sam4c_flash.ld |
RAM и ROM — это параметры обозначающие адресное пространство памяти программ и памяти данных соответственно. Мы в праве изменять их как хотим, все адреса описаны в документации.
Можно объединить эти регионы в один, сменив скрипт. Toolchain-/Linker-/Miscellaneous- в верхнем окошке меняем часть названия файла на xxx_sram.ld.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") OUTPUT_ARCH(arm) SEARCH_DIR(.) /* Распределение адресного пространства */ MEMORY { rom (rx) : ORIGIN = 0x01000000, LENGTH = 0x00080000 //Не имеет значения. ram (rwx) : ORIGIN = 0x20003000, LENGTH = 0x00023000 //Вся программа здесь. Выделено 12кб для памяти "возможного загрузчика", остальное для программы и ее данных. } /* Размер стека для программы. */ STACK_SIZE = DEFINED(STACK_SIZE) ? STACK_SIZE : 0x1500; INCLUDE sam4c_sram.ld |
Это то же самое, но теперь вся программа будет в регионе RAM.
Исполнение отдельных функций из ОЗУ
В данной задаче нам Linker вообще не понадобится, разработчики уже позаботились об этой возможности.
1 2 3 | __attribute__( ( long_call, section(".ramfunc") ) ) void ram_foobar (void) { //Smth... } |
При запуске контролера специальная подпрограмма скопирует эту функцию в ОЗУ. От вас ничего не потребуется.
Извлечение кода
Скомпилируем простую программу с настройками Linker-а.
1 2 3 4 5 6 7 8 9 | #include "sam4.h" int main(void) { PIOB->PIO_OER = PIO_OER_P21; //PB21 - выход while(1) { PIOB->PIO_CODR = PIO_CODR_P21; //Очень быстрое моргание светодиодом PIOB->PIO_SODR = PIO_SODR_P21; } } |
Однако, разместив программу в нужном регионе, вы не сможете ее просто загрузить программатором. Кто ее запустит с нужного адреса? Для начала — получим бинарный образ программы!
Всегда проявляется в папке проекта, его мы можем открыть с помощью какого-нибудь HEX-редактора и сохранить в массив, например.
1 2 3 4 5 | const uint8_t store[] = { 0x50, 0x49, 0x02, 0x20, 0x9D, 0x01, 0x00, 0x14, 0x99, 0x01, 0x00, 0x14, 0x99, 0x01, 0x00, 0x14, 0x99, 0x01, 0x00, 0x14, 0x99, 0x01, 0x00, 0x14, 0x99, 0x01, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x99, 0x01, 0x00, 0x14, 0x99, 0x01, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x99, 0x01, 0x00, 0x14, 0x99, 0x01, 0x00, |
Следующий вопрос: как ее запустить? Адрес для запуска я определяю из .lss файла, в котором с комментариями мы можем все увидеть.
1 2 3 4 5 6 7 8 9 10 11 12 | 140002b8 <main>: #include "sam4.h" int main(void) { PIOB->PIO_OER = PIO_OER_P21; //PB21 - выход 140002b8: f44f 5280 mov.w r2, #4096 ; 0x1000 140002bc: f2c4 020e movt r2, #16398 ; 0x400e 140002c0: f44f 1300 mov.w r3, #2097152 ; 0x200000 140002c4: 6113 str r3, [r2, #16] while(1) { PIOB->PIO_CODR = PIO_CODR_P21; //Очень быстрое моргание светодиодом 140002c6: 6353 str r3, [r2, #52] ; 0x34 |
Соответственно, адрес для запуска будет начинаться с функции main() или с 0x140002b8. Запоминаем это значение.
Загрузчик
Задача загрузчика проста — передать управление функции по адресу. А также скопировать программу в нужный регион, если ее там нет.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #include "sam.h" #include <stdint.h> #include <stdlib.h> #include <string.h> #include "store.h" //Образ нашей программы typedef int program_t(void); program_t* fp = (program_t*)(0x140002b8 + 0x1); //Адрес функции main() нашей программы, заметьте, что тип - int: мы можем передавать значения uint8_t *addr = (uint8_t*)0x14000000; //Адрес того региона, куда мы будем загружать. Его вы указали в Linker-е int main() { SystemInit(); PMC->PMC_WPMR = 0x504D43; //Очень черная магия. (на всякий случай) PMC->PMC_PCER0 = PMC_PCER0_PID12 | PMC_PCER0_PID11; //Запускаем PIOA и PIOB memcpy(addr, store, sizeof(store)); //Копируем программу для исполнения в нужный регион. (*fp)(); //Запускаем while(1); //Останавливаем. "" } |
Заметьте, что копирование производится по адресу 0x14000000, указанного в linker-е, сам запуск идет уже с 0x140002b8+1 — последнее — признак Thumb®-2 инструкций микроконтроллера. Кроме того, вы можете расширить возможности загрузчика, например передавать параметры вашей программе, адреса функций, вообще — создать программное ядро.
Эксперимент: стандартный запуск из ПЗУ
Для сравнения проверим нашу программу без всяких настроек и загрузчиков.
Код находится во FLASH по Code Bus, а данные в SRAM0 по System Bus.
48ns — хорошо.
Эксперимент: запуск всего из ОЗУ
Здесь подменой скрипта Linker-а мы запускаем код из встроенного ОЗУ.
Код — SRAM0 по System Bus, данные — SRAM0 по System Bus.
У нас уже 85ns — почти в два раза ниже.
Эксперимент: запуск всего из внешней памяти
Подключаем внешнюю память по EBI интерфейсу. Скорость чтения составляет ~32Мб/сек, скорость записи ~64Мб/сек. Но контроллер запущен на более низкой частоте.
Код — EBI по System Bus, данные — EBI по System Bus.
Результат — 436ns, оно и не удивительно.
Эксперимент: запуск программы из внешней памяти
Мы также используем внешнюю микросхему памяти по EBI интерфейсу, но с интересной конфигурацией.
Код — EBI по Code Bus, данные — SRAM0 по System Bus.
Результат — 374ns. Это самое правильное подключение, разница в скорости сейчас не так заметна из-за простоты программы: память данных почти не используется. С большими приложениями разница будет огромной.
Эксперимент: запуск программы из внешней памяти + КЭШ
Та же конфигурация, но мы добрались до кэш-а микроконтроллера.
Код — EBI (Cached) по Code Bus, данные — SRAM0 по System Bus.
Это прорыв — 39ns! Чтобы использовать кэш, необходимо лишь запускать с немного другого адреса, указанного в документации со скобками «Cached».
Заметки
Обе шины, и системная, и кодовая находятся в общем адресном пространстве. Адресацию вы можете почитать в разделе документации «7. Product Mapping and Peripheral Access «.
Прошу обратить внимание на то, что таблица векторов исполняемой программы не используется. Вам необходимо скопировать ее куда-нибудь в память данных и назначить адрес в регистре.
1 | SCB->VTOR = (адрес & SCB_VTOR_TBLOFF_Msk); |
Без этой таблицы все прерывания и обработчики в исполняемой программе не будут вызываться.