Pull to refresh

Как загружается ARM

Reading time6 min
Views19K
Прошлый мой топик был полностью теоретическим, этот же будет практическим. Практика будет довольно хардкорной (я сам занялся этим вопросом только через год работы с ARMами) — инициализация процессора и памяти. Иными словами: что нужно сделать с процессором, чтобы попасть в функцию main(). Первая часть статьи посвящена инструментам сборки и отладки. Вторая — обработке векторов исключений, третья — инициализации стеков и памяти.
Но сначала хочу сделать одно уточнение. Многие почему-то считают, что ARM — это обязательно монстр со внешней памятью, кучей обвязки, работающий на частоте не менее 600Mhz, и т.д. Это правда лишь отчасти (если говорить об ARM9 и более поздних семействах). Тот чип, с которым я обычно работаю (AT91SAM7X512), не намного сложнее знакомых многим AVR. Ему для работы нужны только кварц и питание (можно и без кварца, но тогда будет совсем грустно). Всё. Но возможностей у него, конечно, больше, много больше, чем у AVR. Но об этом позже. Сегодняшняя статья никак не будет привязана к конкретному железу.

Компиляторы, линковщики, дебаггеры


Вопрос, который волнует очень многих. Есть платные (IAR, Keil MDK, CrossWorks) и бесплатные (gcc-arm). Я в примерах буду использовать gcc-arm. Для винды есть сборки WinARM (кажется, умершая), YAGARTO. В принципе, можно собрать и свою. Есть ещё такая веселая штука, как coLinux, но это совсем другая история. Под Linux кросскомпилятор обычно собирается штатными средствами дистрибутива. Читайте доки, в общем :)
Ещё существует такая полезная вещь, как стандартная библиотека. Та самая, которая реализует функции наподобие printf, mktime, malloc и всего прочего, к чему привыкли программисты на C. Использовать glibc не получится, ибо она слишком большая. Вместо этого обычно используют бесплатную newlib. Она входит в состав WinARM/YAGARTO, а вот пользователям линукса придется собирать её вручную. Опять же — читайте документацию :)
С дебаггерами немного сложнее. Можно использовать эмуляторы, но они довольно глючные, когда дело доходит до периферии. Тут у меня опыта нет. Можно использовать отладочные сообщения в COM-порт. Я так делаю всю жизнь. Мне хватает в 99% случаев.
Но самая классная штука — это JTAG. Устройство, которое подключается к процессору и позволяет дебажить код прямо в камне (ставить брекпойнты, трасировать, просматривать/изменять память ну и т.д.). Правда, стоит денег, с одной стороны, с другой — на плате надо будет разводить под него ножки.

Обработчики исключений


Ладно, будем считать, что компилятор поставили и настроили. Давайте теперь что-нибудь запустим. Начнем с самого начала: что происходит, когда процессор сбрасывается (например, после того, как включили питание и напряжение устаканилось). Тут всё просто: процессор начинает исполнять программу с адреса 0x0. Казалось бы — можно разместить с этого адреса код инициализации и работать себе. Но не всё так просто. Дело в том, что в начальных адресах хранятся вектора обработчиков исключений.
Например, если возникнет прерывание, то обработку его процессор начнет с адреса 0x18, а исключение «неизвестная инструкция» будет обрабатываться с адреса 0x04. В общем, первые 28 байт отведены для таблицы обработчиков исключительных ситуаций (reset — это тоже исключительная ситуация).
arm exception vectors
На рисунке это видно более наглядно. С рисунка же видно, что на каждый обработчик отведено 4 байта, или одна команда процессора. (В режиме ARM. Все обработчики вызываются в этом режиме инструкций.)
Соответсвенно, первым, что мы должны сделать, — это написать обработчики исключений и правильно их разместить. Этим и займемся:
ldr pc, ResetHandlerAddr
ldr pc, UndefHandlerAddr
ldr pc, SWIHandlerAddr
ldr pc, PrefetchAbtHandlerAddr
ldr pc, DataAbtHandlerAddr
nop
ldr pc, IRQHandlerAddr
ldr pc, FIQHandlerAddr

Что делает этот код? Это команды загрузки в регистр pc адресов настоящих обработчиков. Такой себе безусловный переход. Дальше по коду идут переменные, хранящие эти самые адреса:

ResetHandlerAddr: .word ResetHandler
UndefHandlerAddr: .word UndefHandler
SWIHandlerAddr: .word SWIHandler
PrefetchAbtHandlerAddr: .word PrefetchAbtHandler
DataAbtHandlerAddr: .word DataAbtHandler
IRQHandlerAddr: .word IRQHandler
FIQHandlerAddr: .word FIQHandler

Тут можно было применить несколько фокусов, ускоряющих обработку прерываний. Например, как видно, обработчик FIQ находится самым последним в списке, так что обработку этого прерывания можно было начать прямо на месте.
Также можно было использовать регистры AIC (advanced interrupt controller) для прямого перехода на обработчик возникшего прерывания. Но пока не будем усложнять себе жизнь. Пока нам важна только обработка Reset'а.
Так что давайте напишем сами обработчики максимально простыми. Они будут вешать процессор (бесконечно выполняя команду безусловного перехода на самих себя). Всё равно мы не знаем пока, как обрабатывать исключения, поэтому повисший процессор — вполне допустимо.
UndefHandler: B UndefHandler
SWIHandler: B SWIHandler
PrefetchAbtHandler: B PrefetchAbtHandler
DataAbtHandler: B DataAbtHandler
IRQHandler: B IRQHandler
FIQHandler: B FIQHandler

B — это команда безусловного перехода (Branch)
Следующее, что нам нужно сделать, — это настроить указатели стека sp для каждого из режимов работы. Таким образом, если возникнут исключения, — у обработчика уже будет свой стек. Только вначале опишем размеры всех стеков.
.EQU IRQ_STACK_SIZE, 0x100
.EQU FIQ_STACK_SIZE, 0x100
.EQU ABT_STACK_SIZE, 0x100
.EQU UND_STACK_SIZE, 0x100
.EQU SVC_STACK_SIZE, 0x100

Чтобы не мучаться долго, выделим по 256 байт на стек для каждого режима. На самом деле для большинства из этих режимов — это много. Хотя всё зависит от обработчиков. Как видно, тут описаны размеры для 5 из 6 режимов. Остальная память будет использоваться совместно кучей и стеком шестого (user mode) режима.
Теперь опишем константы для облегчения перехода в разные режимы. За текущий режим отвечает регистр CPSR. Он же выполняет и роль статусного регистра.
.EQU ARM_MODE_FIQ, 0x11
.EQU ARM_MODE_IRQ, 0x12
.EQU ARM_MODE_SVC, 0x13
.EQU ARM_MODE_ABT, 0x17
.EQU ARM_MODE_UND, 0x1B
.EQU ARM_MODE_USR, 0x10

.EQU I_BIT, 0x80
.EQU F_BIT, 0x40

Константы I_BIT и F_BIT — это биты, которые запрещают простые и быстрые прерывания, соответственно. Теперь у нас всё готово для инициализации стеков. Делается это просто: загружаем в регистр r0 указатель на вершину стека, потом переходим в нужный режим, записываем в sp значение r0, затем уменьшаем r0 на размер стека и повторяем.
.RAM_TOP:
.word __TOP_STACK
ResetHandler:
ldr sp, .RAM_TOP

msr CPSR_c, #ARM_MODE_FIQ | I_BIT | F_BIT
mov sp, r0
sub r0, r0, #FIQ_STACK_SIZE

msr CPSR_c, #ARM_MODE_IRQ | I_BIT | F_BIT
mov sp, r0
sub r0, r0, #IRQ_STACK_SIZE

msr CPSR_c, #ARM_MODE_SVC | I_BIT | F_BIT
mov sp, r0
sub r0, r0, #SVC_STACK_SIZE

msr CPSR_c, #ARM_MODE_ABT | I_BIT | F_BIT
mov sp, r0
sub r0, r0, #ABT_STACK_SIZE

msr CPSR_c, #ARM_MODE_UND | I_BIT | F_BIT
mov sp, r0
sub r0, r0, #UND_STACK_SIZE

msr CPSR_c, #ARM_MODE_USR

Инициализация памяти


Теперь мы находимся в непривилегилированном режиме со включенными прерываниями и настроенным стеком. Кстати, выйти из этого режима просто так нельзя. Только вызвав исключение. Но об этом в следующей статье.
До перехода в функцию main() осталось совсем чуть-чуть. Надо только перенести кое-какие данные в RAM и обнулить память, которая находится в сегменте .BSS. Это та память, где хранятся глобальные переменные. Дело в том, что стандарт языка C обещает, что глобальные переменные будут обнулены в начале работы, а ARM нам этого не гарантирует. Поэтому обнулим сегмент вручную:

               MOV     R0, #0
               LDR     R1, =__bss_start__
               LDR     R2, =__bss_end__
LoopZI:
               CMP     R1, R2
               STRLO   R0, [R1], #4
               BLO     LoopZI

Константы __bss_end__ & __bss_start__ любезно предоставлены нам линковщиком.
Кстати, тут вы можете наблюдать использование условных инструкций (с суффиксом O). Они будут исполняться, пока R1!=R2.
Также надо перенести из ROM в RAM предварительно инициализированные переменные (те, которые int x=42).
               LDR     R1, =_etext
               LDR     R2, =_data
               LDR     R3, =_edata
LoopRel: 
               CMP     R2, R3
               LDRLO   R0, [R1], #4
               STRLO   R0, [R2], #4
               BLO     LoopRel

Если будем писать на C++, то нужно ещё вызвать конструкторы глобальных объектов:
               LDR     r0, =__ctors_start__
               LDR     r1, =__ctors_end__
ctor_loop:
               CMP     r0, r1
               BEQ     ctor_end
               LDR     r2, [r0], #4
               STMFD   sp!, {r0-r1}
               MOV     lr, pc
               BX r2
               LDMFD   sp!, {r0-r1}
               B       ctor_loop
ctor_end:


Ну в общем ивсё. Вызываем main():
               ldr     r0,=main
               bx      r0


Поздравляю, теперь мы в сишной функции void main(void). Можно заняться инициализацией периферии. Дело в том, что до этого мы инициализировали только програмную среду. Поэтому процессор сейчас работает на самой низкой частоте из всех возможных, вся периферия отключена. Тут не разгуляешься :)
Но инициализация периферии — это штука, которая зависит от конкретной железяки, а цель этой статьи — рассказать, как запускать абстрактный ARM.
И ещё несколько нюансов: этот код напрямую скомпилировать и запустить не получится, потому что здесь не описаны секции, где он располагается. Также я не приводил скрипты линковщика (эти скрипты описывают размещение секций кода и данных в памяти и в образе прошивки).
Но в интернете полно готовых примеров для запуска той или иной железки. Со скриптами, мейкфайлами и всем-всем-всем. Ищите на сайтах производителей :)

Следующая статья, судя по всему, будет опять посвящена теории, на этот раз — описанию режимов процессора и исключительных ситуаций.
Tags:
Hubs:
Total votes 62: ↑60 and ↓2+58
Comments35

Articles