imageДобрый день! Сегодня я хочу рассказать вам как написать минимальную программу, которая запустится на ARM Cortex-M3 и при этом напечатает “Hello, World!”. Постараемся разобрать по шагам необходимый минимум, который нам для этого потребуется. Запускать будем на эмуляторе QEMU. Поэтому любой желающий может воспроизвести, даже если у него нет под рукой железки.

Итак, поехали!

Эмулятор QEMU поддерживает ядро Cortex-M3 и эмулирует на его базе платформу Stellaris LM3S811 от Texas Instruments. Будем запускаться на этой платформе. Нам понадобится тулчейн arm-none-eabi- (скачать можно здесь). Далее нам потребуется написать основную логику нашей программы, стартовый код, который передаст управление в программу, и линкер скрипт.

На хабре уже достаточно неплохих статей про то как помигать диодом на железке с нуля. Поэтому здесь я не буду углубляться в то что и как работает, а приведу лишь минимальный набор необходимых знаний нужных для старта.

Наш hello world в файле test.c:

static volatile unsigned int * const UART_DR = (unsigned int *)0x4000c000;

static void uart_print(const char *s) {
	while (*s != '\0') {
		*UART_DR = *s;
		s++;
	}
}

void c_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). Приведу линкер скрипт test.ld:

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 в Санкт-Петербурге. У нас на стенде будут различные железки, и на демо зоне мы постараемся рассказать как их программировать.

Комментарии (29)


  1. lieff
    31.08.2018 01:33

    Для упрощения еще можно воспользоваться семихостингом. Для arm-none-eabi-gcc это будет что-то типа
    arm-none-eabi-gcc -mthumb -mlittle-endian -march=armv7-m -mcpu=cortex-m3 test.c -o test --specs=rdimon.specs
    Заработает стандартный printf и file IO, которые будут транслироваться на хост.

    Для linaro тулзов arm-linux-gnueabi-gcc/arm-linux-gnueabihf-gcc еще проще:
    arm-linux-gnueabi-gcc -static -mthumb -mlittle-endian -march=armv7-m -mcpu=cortex-m3 test.c -o test

    В обоих случаях получаем бинарь, который можно запустить на QEMU и ввод-вывод будет редиректиться на хост.
    В убунте эти тулзы можно установить из репозитария, а полученный бинарь автоматом будет запускаться через QEMU — очень удобно.


    1. alexkalmuk Автор
      31.08.2018 08:29

      Спасибо, интересная альтернатива. Правда полученный бинарник на железке не запустить без пересборки с настоящими не-хостовыми функциями (как я понял, компилятор навставляет в этих местах bkpt в образе), но любопытно. Я такое не использовал, посмотрю.


      1. bugdesigner
        31.08.2018 08:45

        На реальной железке этот проект не заработает хотя бы потому, что UART нужно настроить — заполнить конфигурационные регистры, включить тактирование итд. Кроме того в эмуляторе UART работает на скорости = бесконечность, а в рельном железе из "Hello world" пройдет лишь "H", ведь статус-регистр не проверяется на предмет окончания передачи. То, что заработало в эмуляторе, не факт, что будет работать на реальном железе.


        1. alexkalmuk Автор
          31.08.2018 08:54

          Конечно может не заработать, я же явно указал в статье, что для упрощения не будем заниматься настройками уарта. Ниже в статье ссылка на эту платформу в Embox, там уже по-настоящему настраивается уарт, прерывания, таймеры.


          1. bugdesigner
            31.08.2018 09:02

            Смысл статьи понятен, я лишь хотел сказать, что полагаться на эмулятор не стоит. В эмуляторе можно проверить лишь "математику" алгоритма. Лучше отлаживать на реальном железе через jtag/swd


            1. alexkalmuk Автор
              31.08.2018 09:18

              Я согласен с Вами. Отлаживать нужно на реальном железе, «работа на эмуляторе» != «работа на железе». Например, именно на железе можно настраивать клоки, управление питанием. Но на эмуляторе можно отладить не только математику, но и большинство драйверов периферии (опять же, какие-то специфические штуки могут проявиться только на железе, но основная логика драйвера при этом сохранится). И если я вижу что какая-то платформа есть в эмуляторе, я сначала пробую ее, а уже потом иду за железкой.


              1. lieff
                31.08.2018 11:39

                А не знаете еще симулятора (кроме платных армовых тулзов), который может количество клоктиков просимулировать и профайл показать. На худой конец инструкции но без танцев с бубном, для QEMU я костыль сделал github.com/lieff/qemu-prof. Не знаю есть ли способ проще.


                1. alexkalmuk Автор
                  31.08.2018 13:20

                  Не смогу подсказать. Мне кажется потактовый симулятор с выводом такого профайла — штука сложная, учитывая что нужно же и кэши процессорные симулировать тогда, если по-хорошему… Ну и еще, там нужно следить за сопроцессорами, и симулировать их в том же цикле что и ЦПУ, типа на одну инструкцию ЦПУ всегда исполняем одну инструкцию сопроцессора и тд (я видел такое в одном симуляторе для MIPS). Поэтому тот же куему ограничивается лишь генерацией Translation блоков с ассеблерными инструкциями. И вся периферия в куему делает ввод-вывод прямо в главном цикле с ЦПУ. Т.е. любые общения с периферией (тот же дма) будут сразу же «ухудшать» частоту ЦПУ. Так что в куему тогда нужно гигантскую архитектурну переделку производить, чтобы такое реализовать. Про другие открытые и более-менее поддерживаемы симуляторы для ARM я, если честно, не в курсе.


        1. Jef239
          31.08.2018 15:20

          Да не, 16 байт (размер FIFO) пройдет, если FIFO по умолчанию включен. Может и 17 — FIFO+сдвиговый регистр.

          Но сам код — жуть.


          1. alexkalmuk Автор
            31.08.2018 15:22

            А что не так с кодом?


            1. Jef239
              31.08.2018 15:34
              -1

              Где включение питания UART? Где включение его тактирования? Где установка делителей?

              Ну и самый смак — отправка в DR без ожидания готовности.

              Ну вот один из работающих вариантов для 16550
                     int cnt = ((LSR & 0x20) != 0) ? OUT_FIFO_16550_SIZE : 0;
                     if ((LSR & 0x40) != 0) /* TEMP - пустота сдвигового регистра */
                        cnt++;
                     for (int i = 0; i < cnt; i++) {
                         uint8_t btOut;
                         if (xQueueReceiveFromISR (port->data->transmit_queue, &btOut, &woken)) {
                            port->regs->TFR = btOut;
                            sent++;
                         } else
                            break;
                     };
              


              1. alexkalmuk Автор
                31.08.2018 15:38
                -1

                Вы статью читали?

                Вот этот самый адрес 0x4000c000 берется из документации, там лежит регистр DR нулевого уарта. Мы не будем заниматься настройкой, а попробуем сразу напрямую положить в него символ


                Я тут не пытаюсь учить уарт программировать. Тут объяснялись базовые вещи по загрузке образа. И ниже говорится

                Если вам нужен более содержательный функционал, стоит воспользоваться готовыми вещами. Мы добавили поддержку данной платформы в Embox. Называется этот темплейт platform/stellaris/lm3s811evb. Поэтому если кто-то хочет попробовать запустить чуть более серьезную вещь (консоль, таймер, прерывания)


                1. Jef239
                  31.08.2018 15:53

                  Вы хотя бы на имитаторе проверили работоспособность? Просто у меня есть сомнения, что этот код даже на имитаторе будет работать. И уж точно — не пойдет на железе.

                  Смените тогда заголовок, укажите, что пишите для имитатора, а не для железа.


                  1. alexkalmuk Автор
                    31.08.2018 15:59
                    -1

                    Конечно я проверил на эмуляторе QEMU. В этом весь смысл.
                    О том что запускать будем на QEMU говорится в самом первом абзаце, еще до ката. Смотрите, я не хочу усложнять код. В идеале, можно было вообще уарт не трогать, а просто сделать бесконечный цикл, и на этом все. Проще некуда. Но хотелось чуть большей информативности, но без объяснения как работать с периферией.


                    1. Jef239
                      31.08.2018 16:10

                      Тогда причем тут Cortex-M3??? Тем более что порты у него бывают самые разные. Вы написали, как вывести hello world на эмуляторе.

                      Но я не понимаю, зачем нужен эмулятор. Математику проще отладить под windows или linux, а специфическое общение с железом на эмуляторе не отладить.

                      Такое впечатление, что статья писалась ради упоминания Embox.

                      P.S. Эмулятор сильно плох тем, что на нем сложно сделать регрессионные тесты. Портировав код под винду мы заменяем устройства на файлы и имеем стабильное прохождение тестов. На эмуляторе это сложно. Особенности железа он тоже адекватно не эмулирует.


                      1. alexkalmuk Автор
                        31.08.2018 16:28

                        Но я не понимаю, зачем нужен эмулятор

                        На эмуляторе все нормально эмулируется с точность до клоков. Да, клоки придется настраивать на железе. И да, могут и другие проблемы возникнуть — сталкивались не раз. Но уарт, контроллер прерываний, системный таймер, сетевуха — все это обычно имеется. Плюс можно тестировать много архитектур. Плюс можно поднять много эмуляторов и тестировать какую-то распределенную логику на них. В общем штука безмерно удобная.


                        1. Jef239
                          31.08.2018 17:15

                          Ну по вашему примеру видно, насколько «точно» имитируется UART. На реальном UART вы таким образом выведите максимум 17 байт (размер FIFO = сдвиговый регистр). Иными словами, то, что работает на имитаторе — не факт, что будет работать на железе.

                          При написании ОС — возможно, что имитатор удобней железа. Но в реальных (не учебных) проектах всегда есть железо и JTAG/SWD.

                          А если железо такое новое, что ещё и семплов нету — ну так его и в эмуляторе не будет.


                          1. alexkalmuk Автор
                            31.08.2018 23:44

                            Ладно, давайте не будем спорить :) Я же не спорю, что jtag вещь полезная, и даже необходимая. Но вот есть немало людей, которые пользуются эмуляторами, вот можно полазить форумы почитать. Там, вероятно, 80% процентов их проектов «учебные». А под железо разве все прям промышленные? Хорошо, давайте считать, что эмулятор нужен только при написании ОС.


                      1. alexkalmuk Автор
                        31.08.2018 16:35

                        Тогда причем тут Cortex-M3??? Тем более что порты у него бывают самые разные.

                        Безусловно, порты разные, но как именно устроен vector table, thumb mode и тд — общее. Суть статьи была в этом. А не в драйвере уарта.


                        1. Jef239
                          31.08.2018 17:19

                          А какая разница, как они устроены? Тот, кто пишет порт RTOS — тот в этом разбирается. А все остальные — просто используют готовый порт.

                          У меня полсотни экземпляров на разных ARM работают под FreeRTOS. Но в тонкостях vector table я не разбираюсь. Это прерогатива ОС, а не моя. Даже для написания драйвера мне это не нужно — дергается готовая процедура контроллера прерываний и всё.


                          1. alexkalmuk Автор
                            31.08.2018 23:41

                            Ну вот вам это не нужно, а кому-то интересно узнать. Не про приложеньки под FreeRTOS же рассказывать в самом деле)) А давайте дальше пойдем. Зачем сегодня в чем-то разбираться — можно ведь прийти в магазин и купить айфон с полки. Смотрите как удобно — ни эмулятор, ни jtag не нужен, ляпота!


                            1. alexkalmuk Автор
                              31.08.2018 23:47

                              Никого не хочу этим обидеть. Просто выражаю мысль о том, что есть разные темы, и не все они отдельно взятому человеку кажутся нужными и интересными. Но я такое просто игнорирую и не читаю в таком случае.


                            1. Jef239
                              31.08.2018 23:59

                              Ну вот как раз для написания приложений под айфон (а ещё пуще под Андроид) эмулятор явно нужен. Не собирать же весь зоопарк совместимых устройств и вариантов IOS?

                              Но вряд ли у кого из новичков будет задача написать приложение, работающее на всех кортексах. Тут, скорее наоборот — ищется SoC под задачу, потом под задачу делается плата.

                              Не про приложеньки под FreeRTOS же рассказывать в самом деле))
                              А почему бы не рассказать про приложеньки под Embox? Сколько труда нужно, чтобы поднять на ней сетевой стек на какой-нибудь K1879ВЯ1Я? А сколько — чтобы портировать её туда?

                              Вот человек про MBed рассказал. И код простой, и компилится прямо на сайте.

                              А про FreeRTOS рассказано уже очень много


                              1. alexkalmuk Автор
                                01.09.2018 00:27

                                А почему бы не рассказать про приложеньки под Embox? Сколько труда нужно, чтобы поднять на ней сетевой стек на какой-нибудь K1879ВЯ1Я? А сколько — чтобы портировать её туда?

                                Так в нашем блоге хватает статей. И про портирование в том числе тоже есть. А приложеньки? Ну так у нас это POSIX. Погодите, а зачем вам тогда перенос сетевого стека и порт на плату, это же другие люди должны делать. Вы же сами выше писали ровно это.

                                А про MBed — ну привел человек какое-то очередное API, что-то на нем написал. Какая-то онлайн-среда разработки. И что дальше? Зачем все это в реальных проектах? Вы же как раз про реальные проекты говорите. Я что буду что-то через сайт mbed.com программировать??.. Пффф. По-моему опять начали спорить о вкусах.


                                1. Jef239
                                  01.09.2018 01:00

                                  Погодите, а зачем вам тогда перенос сетевого стека и порт на плату, это же другие люди должны делать.

                                  А это разница между «делать» и «планировать». Я должен понимать при выборе процессора и ОС, сколько труда уйдет у коллег на адаптацию. Особенно это важно когда речь об импортозамещении, то есть о мелкосерийных российских SoC. На тот же К1879ВЯ1Я даже FreeRTOS пришлось самим портировать. Ну и коллегам надо понимать, сколько у меня займет времени адаптация к API.

                                  А про MBed — ну привел человек какое-то очередное API, что-то на нем написал.
                                  MBed — это разработка компании ARM. То есть туда вложено не просто много денег, а очень много. Вот список поддерживаемых «из коробки» плат.

                                  С одной стороны — это только COortex-M. Ни MIPS, ни ARM1176, ни даже Cortex-A. С другой стороны, если ограничится миром Cortex-M — похоже, что реально удобно.

                                  Какая-то онлайн-среда разработки. И что дальше? Зачем все это в реальных проектах?
                                  А все просто. Подключил плату — быстро опробовал работу устройств. Без установки тулкита, без мучений… Просто собрал на сайте и залил на вирутальную флэшку. А оно — заработало.

                                  Ну как почти реальный пример.
                                  У меня одна из медленных операций — это обращение матриц. Если SoC справляется за 1 миллисекунду — SoC годится. Если нет — то нет. Пожалуй что довольно дешевый способ — это купить готовую плату и залить туда тест, сделанный при помощи MBox. Это сильно дешевле, чем делать такой же проект на FreeRTOS + подъем синхронизации и питания своими руками.


        1. Jenix
          31.08.2018 16:57

          А вроде есть режим 16550, где сбрасывать статус не обязательно, а проц пуляет в дата-регистр когда захочет, даже если не закончилась передача. Например может проверять считыванием готовность к передаче. или наоборот — пулять данные в дата-регистр заведомо гораздо позже, когда они полностью выходят из UART.


  1. x893
    31.08.2018 02:17

    1. alexkalmuk Автор
      31.08.2018 07:50

      Да, это похожая штука для более больших армов, они на arm926ej-s запускают.


      1. x893
        31.08.2018 13:22

        Запускали 8 лет назад.