Quantcast
Channel: Hard-Soft News » soft
Viewing all articles
Browse latest Browse all 10

Разработка программ для микроконтроллеров ARM для работы на голом железе

$
0
0

Поскольку в некоторых (правда не очень широких) кругах я считаюсь специалистом по процессором ARM и всем, что с ними связано, еще недавно мне часто задавали вопрос как установить Linux на ту или иную железку ARM. Сейчас после появления Android такие вопросы задают гораздо реже и это еще одна польза от Android (по крайней мере для меня). Главная проблема с установкой Linux ARM заключается в том, что не существует стандартный платформы ARM, подобной стандартный платформе PC. Современные системы ARM, это системы выполненные на одном кристалле, на котором помимо процессора расположены различные контроллеры. Проблема в том, что в разных моделях и архитектура контроллеры и адреса регистров, и даже значение, которые записываются в эти адреса, могут развиться даже в рамках одного модельного ряда. Это значит, что для установки Линукса на любое железо ARM необходим свой уникальный набор драйверов. Этот факт очень важно понять не только тем кто пытается поставить Линукс на ARM, но и тем, кто просто пишет программы для ARM-систем, например для микроконтроллеров. Далее мы рассмотрим три типа современных систем на основе ARM и пример программы, предназначенной для работы на голом железе микроконтроллера ARM.

Устройства, основанные на ARM можно разделить на три группы.

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

Ко второй группе относятся устройства которые уже могут выполнять специализированные операционные системы, предназначенные для микроконтроллеров, но они все еще достаточно просты для того чтобы для них можно было писать программы, работающие на голом железе.

Наконец, третья категория, — это устройства, достаточно мощные для того чтобы работать под операционными системами общего назначения, предназначенными для обильных устройств (например Android). Неудивительно, что в последнее время именно устройств этой категории становится все больше и больше.

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

Труднее всего, конечно, описывать программирование на голом железе, потому что любая ОС создает некую среду, сохраняющую единообразие на всех платформах. Но мы начнем именно с самых простых устройств, которые оказываются самыми сложными в программировании. Даже самая простая программа, предназначенная для работы на голом железе, должна выполнить ряд неочевидных для обычного (привыкшего к программированию под ОС) программиста действий. Чтобы понять механику работы программы на голом железе, давайте вспомним, что делает операционная система с момента загрузки и до момента запуска прикладной программы. Прежде всего, любая ОС подготавливает компьютер к работе: настраивает средства управления различными элементами машины, в том числе, например, устанавливает скорость доступа к шинам и памяти. Важный этап в работе любой ОС – настройка таблицы векторов прерываний.

Когда операционная система загружает прикладную программу, она, помимо прочего, создает различные элементы среды окружения для этой программы, и располагает различные блоки программы в оперативной памяти так, чтобы они могли найти друг друга. Все, что я пишу здесь про работу ОС, представляет собой довольно упрощенную картину. Это тот необходимый минимум, который нужно знать, чтобы понять, как работает программа на голом железе. Одной из задач, которые такая программа должна решить самостоятельно, является заполнение таблицы векторов прерываний. Вот как выглядит таблица векторов прерываний минимальной программы для микроконтроллера ARM (пример позаимствован из замечательного блога ):

 B Reset_Handler /* Reset */
 B . /* Undefined */
B . /* SWI */
B . /* Prefetch Abort */
B . /* Data Abort */
B . /* reserved */
B . /* IRQ */
B . /* FIQ */

Таблица векторов прерываний состоит из команд безусловного перехода (B). Таблица векторов может содержать и другие команды, но смысл их должен быть тот же – переход по некоторому адресу. Команда

В.

(B с точкой) это переход на тот же адрес, по которому расположена инструкция, то есть бесконечный цикл. Таким образом, мы видим, что в приведенной таблице прерываний определен только обработчик прерывания Reset. Прерывание Reset может быть генерировано, например, кнажатием кнопки Reset. В любом случае это единственное прерывание, которое программа обязана  обрабатывать. Нетрудно видеть, что любое другое прерывание, буде фоно возникнет, приведет к зависанию программы. Обработчик прерывания Reset выглядит следующим образом:

Reset_Handler:
 LDR sp, =stack_top
 BL c_entry
 B .

Первая инструкция загружает в регистр SP адрес верхушки стека. Следующая инструкция – это вызов процедуры c_entry, с которой начинается, собственно, работа нашей программы. Фактически вся наша программа умещается в обработчике прерывания Reset. Вам может показаться, что это не самое элегантное решение, но многие серьезные программы для микроконтроллеров делают точно так же.

Далее следует уже знакомая нам «B с точкой». После выхода из функции c_entry программа снова зацикливается (если бы программа работала под управлением операционной системы, она должна была бы передать управление системе, но программе, которая работает сама по себе, после выхода из основной функции просто нечего делать. Коротко говоря, прерывание Reset заставляет систему запустить программу на выполнение. Все вместе это собирается в ассемблерный файл crt.s:

.section INTERRUPT_VECTOR, "x"
.global _Reset
_Reset:
B Reset_Handler /* Reset */
B . /* Undefined */
B . /* SWI */
B . /* Prefetch Abort */
B . /* Data Abort */
B . /* reserved */
B . /* IRQ */
B . /* FIQ */

Reset_Handler:
  LDR sp, =stack_top
  BL c_entry
  B .

Первая строка  ассемблерного файла  сообщает компоновщику, что файл содержит раздел INTERRUPT_VECTOR, который содержит исполнимый код «x». Вторая строчка объявляет глобально видимое имя «_Reset». Оно тоже понадобится компоновщику. Все, что следует дальше, мы уже описали.

Теперь нам нужна собственно функция c_entry. В нашем случае она  будет выглядеть так:

int c_entry()
{
return 0;
}

Эта функция не делает ничего полезного, но именно по этой причине она будет выполняться на любом микроконтроллере (назовем файл, содержащий этот код, test.c).

Дальше нам нужно написать еще один скрипт, с которым прикладные программисты обычно не имеют дела. Речь идет о скрипте компоновщика, который указывает, как скомпоновать программу. Когда программа собирается для операционной системы, компоновщик, рассчитанный на эту ОС, сам знает, что ему делать. Но в случае программирования на голом железе мы должны рассказать ему об этом. Наш скрипт предназначен для компоновщика GNU (ld), так в дальнейшем для сборки программ мы будем пользоваться именно инструментами GNU.

ENTRY(_Reset)
SECTIONS
{
 . = 0x0;
 .text : {
 startup.o (INTERRUPT_VECTOR)
 *(.text)
 }
 .data : { *(.data) }
 .bss : { *(.bss) }
 . = . + 0x1000; /* 4kB of stack memory */
 stack_top = .;
}

Первая строка скрипта компоновки указывает точку входа в программу. Таким образом выполнении нашей программы начнется с перехода на обработчик прерывания Reset, который вызовет функцию c_entry. Дальше мы сообщаем компоновщику, что секция INTERRUPT_VECTOR находится в файле starup.o и должна быть расположена, начиная с адреса 0×0. Затем следует еще описание нескольких секций, которые мы сейчас пропустим. Интересно для нас то, как заполняется переменная stack_top, которую использует процедура инициализации. Вы уже наверное догадались, что точка означает в этом скрипте то же, что и в ассемблерном файле – текущий адрес. Таким образом, выражение

 . = . + 0x1000;

Означает «прибавить к текущему адресу значение 4096». То есть верхушка стека оказывается смещена на 4096 байтов относительно завершающего адреса последней секции программы. Для программ на микроконтроллерах 4096 байтов стека – совсем неплохо (а для нашей конкретной программы даже слишком много).

И так теперь мы знаем, из каких основных элементов должна состоять программа, предназначенная для работы на голом железе. Во-первых, это инициализирующая часть, написанная обычно на ассемблере (хотя может использовать и C), во-вторых, это собственно программа, которую нужно выполнить, в третьих это сценарий компоновщика.

Разделение программ на инициализирующую часть и основной код удобно тем, что можно использовать один и от же код инициализации для многих разных программ.

Вот и все на сегодня. В заключение ссылки на очень полезную книгу. Она доступна в сети бесплатно (хотя, к сожалению, не переведена на русский):

Miro Samek, Building Bare Metal ARM Systems with GNU – как указывает название, эта книга как раз про то, о чем мы сегодня говорили, хотя изложение в ней начинается сразу с вещей гораздо более сложных.

В следующих статьях этого цикла я расскажу как собирать программы для ARM-микроконтроллеров, загружать их в устройства и запускать на исполнение.


Viewing all articles
Browse latest Browse all 10

Trending Articles