Пишем свою простейшую программу для ARM Cortex-M3

от автора

imageДобрый день! Сегодня я хочу рассказать вам как написать минимальную программу, которая запустится на 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 != '\0') { 		*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 в Санкт-Петербурге. У нас на стенде будут различные железки, и на демо зоне мы постараемся рассказать как их программировать.


ссылка на оригинал статьи https://habr.com/post/421833/