Добрый день! Сегодня я хочу рассказать вам как написать минимальную программу, которая запустится на ARM Cortex-M3 и при этом напечатает “Hello, World!”. Постараемся разобрать по шагам необходимый минимум, который нам для этого потребуется. Запускать будем на эмуляторе QEMU. Поэтому любой желающий может воспроизвести, даже если у него нет под рукой железки.
Итак, поехали!
Эмулятор QEMU поддерживает ядро Cortex-M3 и эмулирует на его базе платформу Stellaris LM3S811 от Texas Instruments. Будем запускаться на этой платформе. Нам понадобится тулчейн arm-none-eabi- (скачать можно здесь). Далее нам потребуется написать основную логику нашей программы, стартовый код, который передаст управление в программу, и линкер скрипт.
На хабре уже достаточно неплохих статей про то как помигать диодом на железке с нуля. Поэтому здесь я не буду углубляться в то что и как работает, а приведу лишь минимальный набор необходимых знаний нужных для старта.
Наш hello world в файле test.c:
Вот этот самый адрес 0x4000c000 берется из документации, там лежит регистр DR нулевого уарта. Мы не будем заниматься настройкой уарта (на железе это нужно будет проделать), а попробуем сразу напрямую положить в него символы.
Теперь, нам нужно как-то передать управление в нашу функцию с_entry в файле test.c. Для этого создадим код следующего содержания (файл startup.S), и потом положим его в итоговый образ ELF в начало.
Первым словом по адресу 0x0 должен лежать указатель на вершину стека (SP). По адресу 0x4 находится PC, который как и SP загружается в регистры. Отметим, что start объявлен именно как функция, а не как метка из-за того что код на Cortex-M исполняется в режиме Thumb (это такой упрощенный набор команд ARM), и требуется чтобы адреса функций в векторе прерываний были в виде (address | 0x1) — т.е. последний бит адреса должен быть равен 1.
Далее функция start просто загружает адрес нашей функции c_entry() из файла test.c и передает туда управление через “bx r1”.
Остается лишь успешно слинковать нашу программу. Для этого требуется задать карту памяти нашего микроконтроллера. В документации можно найти адреса и размеры флеш памяти (ROM) и ОЗУ (RAM). Приведу линкер скрипт test.ld:
Здесь важно обратить внимание на адреса. “.” в линкер скрипте обозначает текущую позицию. Мы укладываем в начало ROM (адрес 0x0) секцию .text соблюдая очередность — первым идет startup.o(.text). Далее переходим к RAM (. = 0x20000000;) и укладываем туда data (инициализированные глобальные данные) и bss (неинициализированные глобальные данные). Ниже видим ALIGN(8) — ARM требует выравнивание SP (Stack Pointer) на 8. Так как стек растет вниз, то аллокация места под стек это всего лишь навсего прибавление ”. =. + 0x1000”. Нашу программу мы хорошо знаем, поэтому 4кБ стека хватит с большим запасом.
Вот и все, остается все это собрать вместе. Привожу build.sh:
Тут все более-менее должно быть понятно, за исключением может быть флага -ffreestanding. В данном случае добавлять его необязательно (можете проверить), но так как мы готовим бареметальный образ с нуля, то лучше сказать компилятору, чтобы он не обращал внимания на такие функции как main().
В итоге у нас получился ELF файл test.elf. Запускаем его на QEMU:
Работает.
Конечно, это учебный пример предназначенный для понимания происходящего. Если вам нужен более содержательный функционал, стоит воспользоваться готовыми вещами. Мы добавили поддержку данной платформы в Embox. Называется этот темплейт platform/stellaris/lm3s811evb. Поэтому если кто-то хочет попробовать запустить чуть более серьезную вещь (консоль, таймер, прерывания), то можете собрать и попробовать. При этом, повторюсь, вам не нужно иметь аппаратную плату.
А тех кому все-таки мало эмулятора, или кто хочет задать нам вопросы и поиграться с железками мы будем ждать в эту субботу и в воскресенье на IT фестивале techtrain.ru в Санкт-Петербурге. У нас на стенде будут различные железки, и на демо зоне мы постараемся рассказать как их программировать.
Итак, поехали!
Эмулятор 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 в Санкт-Петербурге. У нас на стенде будут различные железки, и на демо зоне мы постараемся рассказать как их программировать.
lieff
Для упрощения еще можно воспользоваться семихостингом. Для 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 — очень удобно.
alexkalmuk Автор
Спасибо, интересная альтернатива. Правда полученный бинарник на железке не запустить без пересборки с настоящими не-хостовыми функциями (как я понял, компилятор навставляет в этих местах bkpt в образе), но любопытно. Я такое не использовал, посмотрю.
bugdesigner
На реальной железке этот проект не заработает хотя бы потому, что UART нужно настроить — заполнить конфигурационные регистры, включить тактирование итд. Кроме того в эмуляторе UART работает на скорости = бесконечность, а в рельном железе из "Hello world" пройдет лишь "H", ведь статус-регистр не проверяется на предмет окончания передачи. То, что заработало в эмуляторе, не факт, что будет работать на реальном железе.
alexkalmuk Автор
Конечно может не заработать, я же явно указал в статье, что для упрощения не будем заниматься настройками уарта. Ниже в статье ссылка на эту платформу в Embox, там уже по-настоящему настраивается уарт, прерывания, таймеры.
bugdesigner
Смысл статьи понятен, я лишь хотел сказать, что полагаться на эмулятор не стоит. В эмуляторе можно проверить лишь "математику" алгоритма. Лучше отлаживать на реальном железе через jtag/swd
alexkalmuk Автор
Я согласен с Вами. Отлаживать нужно на реальном железе, «работа на эмуляторе» != «работа на железе». Например, именно на железе можно настраивать клоки, управление питанием. Но на эмуляторе можно отладить не только математику, но и большинство драйверов периферии (опять же, какие-то специфические штуки могут проявиться только на железе, но основная логика драйвера при этом сохранится). И если я вижу что какая-то платформа есть в эмуляторе, я сначала пробую ее, а уже потом иду за железкой.
lieff
А не знаете еще симулятора (кроме платных армовых тулзов), который может количество клоктиков просимулировать и профайл показать. На худой конец инструкции но без танцев с бубном, для QEMU я костыль сделал github.com/lieff/qemu-prof. Не знаю есть ли способ проще.
alexkalmuk Автор
Не смогу подсказать. Мне кажется потактовый симулятор с выводом такого профайла — штука сложная, учитывая что нужно же и кэши процессорные симулировать тогда, если по-хорошему… Ну и еще, там нужно следить за сопроцессорами, и симулировать их в том же цикле что и ЦПУ, типа на одну инструкцию ЦПУ всегда исполняем одну инструкцию сопроцессора и тд (я видел такое в одном симуляторе для MIPS). Поэтому тот же куему ограничивается лишь генерацией Translation блоков с ассеблерными инструкциями. И вся периферия в куему делает ввод-вывод прямо в главном цикле с ЦПУ. Т.е. любые общения с периферией (тот же дма) будут сразу же «ухудшать» частоту ЦПУ. Так что в куему тогда нужно гигантскую архитектурну переделку производить, чтобы такое реализовать. Про другие открытые и более-менее поддерживаемы симуляторы для ARM я, если честно, не в курсе.
Jef239
Да не, 16 байт (размер FIFO) пройдет, если FIFO по умолчанию включен. Может и 17 — FIFO+сдвиговый регистр.
Но сам код — жуть.
alexkalmuk Автор
А что не так с кодом?
Jef239
Где включение питания UART? Где включение его тактирования? Где установка делителей?
Ну и самый смак — отправка в DR без ожидания готовности.
alexkalmuk Автор
Вы статью читали?
Я тут не пытаюсь учить уарт программировать. Тут объяснялись базовые вещи по загрузке образа. И ниже говорится
Jef239
Вы хотя бы на имитаторе проверили работоспособность? Просто у меня есть сомнения, что этот код даже на имитаторе будет работать. И уж точно — не пойдет на железе.
Смените тогда заголовок, укажите, что пишите для имитатора, а не для железа.
alexkalmuk Автор
Конечно я проверил на эмуляторе QEMU. В этом весь смысл.
О том что запускать будем на QEMU говорится в самом первом абзаце, еще до ката. Смотрите, я не хочу усложнять код. В идеале, можно было вообще уарт не трогать, а просто сделать бесконечный цикл, и на этом все. Проще некуда. Но хотелось чуть большей информативности, но без объяснения как работать с периферией.
Jef239
Тогда причем тут Cortex-M3??? Тем более что порты у него бывают самые разные. Вы написали, как вывести hello world на эмуляторе.
Но я не понимаю, зачем нужен эмулятор. Математику проще отладить под windows или linux, а специфическое общение с железом на эмуляторе не отладить.
Такое впечатление, что статья писалась ради упоминания Embox.
P.S. Эмулятор сильно плох тем, что на нем сложно сделать регрессионные тесты. Портировав код под винду мы заменяем устройства на файлы и имеем стабильное прохождение тестов. На эмуляторе это сложно. Особенности железа он тоже адекватно не эмулирует.
alexkalmuk Автор
На эмуляторе все нормально эмулируется с точность до клоков. Да, клоки придется настраивать на железе. И да, могут и другие проблемы возникнуть — сталкивались не раз. Но уарт, контроллер прерываний, системный таймер, сетевуха — все это обычно имеется. Плюс можно тестировать много архитектур. Плюс можно поднять много эмуляторов и тестировать какую-то распределенную логику на них. В общем штука безмерно удобная.
Jef239
Ну по вашему примеру видно, насколько «точно» имитируется UART. На реальном UART вы таким образом выведите максимум 17 байт (размер FIFO = сдвиговый регистр). Иными словами, то, что работает на имитаторе — не факт, что будет работать на железе.
При написании ОС — возможно, что имитатор удобней железа. Но в реальных (не учебных) проектах всегда есть железо и JTAG/SWD.
А если железо такое новое, что ещё и семплов нету — ну так его и в эмуляторе не будет.
alexkalmuk Автор
Ладно, давайте не будем спорить :) Я же не спорю, что jtag вещь полезная, и даже необходимая. Но вот есть немало людей, которые пользуются эмуляторами, вот можно полазить форумы почитать. Там, вероятно, 80% процентов их проектов «учебные». А под железо разве все прям промышленные? Хорошо, давайте считать, что эмулятор нужен только при написании ОС.
alexkalmuk Автор
Безусловно, порты разные, но как именно устроен vector table, thumb mode и тд — общее. Суть статьи была в этом. А не в драйвере уарта.
Jef239
А какая разница, как они устроены? Тот, кто пишет порт RTOS — тот в этом разбирается. А все остальные — просто используют готовый порт.
У меня полсотни экземпляров на разных ARM работают под FreeRTOS. Но в тонкостях vector table я не разбираюсь. Это прерогатива ОС, а не моя. Даже для написания драйвера мне это не нужно — дергается готовая процедура контроллера прерываний и всё.
alexkalmuk Автор
Ну вот вам это не нужно, а кому-то интересно узнать. Не про приложеньки под FreeRTOS же рассказывать в самом деле)) А давайте дальше пойдем. Зачем сегодня в чем-то разбираться — можно ведь прийти в магазин и купить айфон с полки. Смотрите как удобно — ни эмулятор, ни jtag не нужен, ляпота!
alexkalmuk Автор
Никого не хочу этим обидеть. Просто выражаю мысль о том, что есть разные темы, и не все они отдельно взятому человеку кажутся нужными и интересными. Но я такое просто игнорирую и не читаю в таком случае.
Jef239
Ну вот как раз для написания приложений под айфон (а ещё пуще под Андроид) эмулятор явно нужен. Не собирать же весь зоопарк совместимых устройств и вариантов IOS?
А почему бы не рассказать про приложеньки под Embox? Сколько труда нужно, чтобы поднять на ней сетевой стек на какой-нибудь K1879ВЯ1Я? А сколько — чтобы портировать её туда?Но вряд ли у кого из новичков будет задача написать приложение, работающее на всех кортексах. Тут, скорее наоборот — ищется SoC под задачу, потом под задачу делается плата.
Вот человек про MBed рассказал. И код простой, и компилится прямо на сайте.
А про FreeRTOS рассказано уже очень много
alexkalmuk Автор
Так в нашем блоге хватает статей. И про портирование в том числе тоже есть. А приложеньки? Ну так у нас это POSIX. Погодите, а зачем вам тогда перенос сетевого стека и порт на плату, это же другие люди должны делать. Вы же сами выше писали ровно это.
А про MBed — ну привел человек какое-то очередное API, что-то на нем написал. Какая-то онлайн-среда разработки. И что дальше? Зачем все это в реальных проектах? Вы же как раз про реальные проекты говорите. Я что буду что-то через сайт mbed.com программировать??.. Пффф. По-моему опять начали спорить о вкусах.
Jef239
А это разница между «делать» и «планировать». Я должен понимать при выборе процессора и ОС, сколько труда уйдет у коллег на адаптацию. Особенно это важно когда речь об импортозамещении, то есть о мелкосерийных российских SoC. На тот же К1879ВЯ1Я даже FreeRTOS пришлось самим портировать. Ну и коллегам надо понимать, сколько у меня займет времени адаптация к API.
MBed — это разработка компании ARM. То есть туда вложено не просто много денег, а очень много. Вот список поддерживаемых «из коробки» плат.
С одной стороны — это только COortex-M. Ни MIPS, ни ARM1176, ни даже Cortex-A. С другой стороны, если ограничится миром Cortex-M — похоже, что реально удобно.
А все просто. Подключил плату — быстро опробовал работу устройств. Без установки тулкита, без мучений… Просто собрал на сайте и залил на вирутальную флэшку. А оно — заработало.
Jenix
А вроде есть режим 16550, где сбрасывать статус не обязательно, а проц пуляет в дата-регистр когда захочет, даже если не закончилась передача. Например может проверять считыванием готовность к передаче. или наоборот — пулять данные в дата-регистр заведомо гораздо позже, когда они полностью выходят из UART.
x893
Прямо вспомнилась юность
balau82.wordpress.com/2010/02/28/hello-world-for-bare-metal-arm-using-qemu
alexkalmuk Автор
Да, это похожая штука для более больших армов, они на arm926ej-s запускают.
x893
Запускали 8 лет назад.