Добрый день! Сегодня я хочу рассказать вам как написать минимальную программу, которая запустится на ARM Cortex-M3 и при этом напечатает “Hello, World!”. Постараемся разобрать по шагам необходимый минимум, который нам для этого потребуется. Запускать будем на эмуляторе QEMU. Поэтому любой желающий может воспроизвести, даже если у него нет под рукой железки.
Итак, поехали!
Эмулятор QEMU поддерживает ядро Cortex-M3 и эмулирует на его базе платформу Stellaris LM3S811 от Texas Instruments. Будем запускаться на этой платформе. Нам понадобится тулчейн arm-none-eabi- (скачать можно здесь developer.arm.com/open-source/gnu-toolchain/gnu-rm/downloads). Далее нам потребуется написать основную логику нашей программы, стартовый код, который передаст управление в программу, и линкер скрипт.
На хабре уже достаточно неплохих статей про то как помигать диодом на железке с нуля. Поэтому здесь я не буду углубляться в то что и как работает, а приведу лишь минимальный набор необходимых знаний нужных для старта.
Наш hello world в файле test.c:
volatile unsigned int * const UART_DR = (unsigned int *)0x4000c000;
static void uart_print(const char *s) {
while (*s != '') {
*UART_DR = *s;
s++;
}
}
void с_entry(void) {
uart_print("Hello, World!n");
while (1)
;
}
Вот этот самый адрес 0x4000c000 берется из документации, там лежит регистр DR нулевого уарта. Мы не будем заниматься настройкой, а попробуем сразу напрямую положить в него символы.
Теперь, нам нужно как-то передать управление в нашу функцию с_entry в файле test.c. Для этого создадим код следующего содержания (файл startup.S), и потом положим его в итоговый образ ELF в начало.
.type start, %function
.word stack_top /* Вот это вершина стека */
.word start /* А здесь инициализируем PC */
.global start
start:
ldr r1, =c_entry
bx r1
Первым словом по адресу 0x0 должен лежать указатель на вершину стека (SP). По адресу 0x4 находится PC, который как и SP загружается в регистры. Отметим, что start объявлен именно как функция, а не как метка из-за того что код на Cortex-M исполняется в режиме Thumb (это такой упрощенный набор команд ARM), и требуется чтобы адреса функций в векторе прерываний были в виде (address | 0x1) — т.е. последний бит адреса должен быть равен 1.
Далее функция start просто загружает адрес нашей функции c_entry() из файла test.c и передает туда управление через “bx r1”.
Остается лишь успешно слинковать нашу программу. Для этого требуется задать карту памяти нашего микроконтроллера. В документации можно найти адреса и размеры флеш памяти (ROM) и ОЗУ (RAM). Приведу линкер скрипт:
ENTRY(start)
SECTIONS
{
. = 0x0; /* Это флэшка (ROM) */
.text : {
startup.o(.text)
test.o(.text)
}
. = 0x20000000; /* С этого адреса начинается RAM */
.data : { *(.data) }
.bss : { *(.bss) }
. = ALIGN(8);
. = . + 0x1000; /* Отдаем под стек 4кБ */
stack_top = .;
}
Здесь важно обратить внимание на адреса. “.” в линкер скрипте обозначает текущую позицию. Мы укладываем в начало ROM (адрес 0x0) секцию .text соблюдая очередность — первым идет startup.o(.text). Далее переходим к RAM (. = 0x20000000;) и укладываем туда data (инициализированные глобальные данные) и bss (неинициализированные глобальные данные). Ниже видим ALIGN(8) — ARM требует выравнивание SP (Stack Pointer) на 8. Так как стек растет вниз, то аллокация места под стек это всего лишь навсего прибавление ”. =. + 0x1000”. Нашу программу мы хорошо знаем, поэтому 4кБ стека хватит с большим запасом.
Вот и все, остается все это собрать вместе. Привожу build.sh:
#!/bin/sh
arm-none-eabi-as -c -mthumb -mlittle-endian -march=armv7-m -mcpu=cortex-m3 startup.S -o startup.o
arm-none-eabi-gcc -c -mthumb -ffreestanding -mlittle-endian -march=armv7-m -mcpu=cortex-m3 test.c -o test.o
arm-none-eabi-ld -T test.ld test.o startup.o -o test.elf
Тут все более-менее должно быть понятно, за исключением может быть флага -ffreestanding. В данном случае добавлять его необязательно (можете проверить), но так как мы готовим бареметальный образ с нуля, то лучше сказать компилятору, чтобы он не обращал внимания на такие функции как main().
В итоге у нас получился ELF файл test.elf. Запускаем его на QEMU:
$ qemu-system-arm -M lm3s811evb -kernel test.elf -nographic
Hello, World!
Работает.
Конечно, это учебный пример предназначенный для понимания происходящего. Если вам нужен более содержательный функционал, стоит воспользоваться готовыми вещами. Мы добавили поддержку данной платформы в Embox. Называется этот темплейт platform/stellaris/lm3s811evb. Поэтому если кто-то хочет попробовать запустить чуть более серьезную вещь (консоль, таймер, прерывания), то можете собрать и попробовать. При этом, повторюсь, вам не нужно иметь аппаратную плату.
А тех кому все-таки мало эмулятора, или кто хочет задать нам вопросы и поиграться с железками мы будем ждать в эту субботу и в воскресенье на IT фестивале techtrain.ru в Санкт-Петербурге. У нас на стенде будут различные железки, и на демо зоне мы постараемся рассказать как их программировать.
Автор: alexkalmuk