Составить таблицу основных команд процессоров типа arm
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
ЛАБОРАТОРНАЯ РАБОТА 3
Цель: Научиться работать с файлом листинга; изучить дополнительные приёмы компоновки и использования директив объявления данных; научиться программировать ветвления в ассемблерной программе и вести простой диалог через устройство ввода вывода.
Краткие теоретические сведения
Листинг – это один из выходных файлов, создаваемых транслятором. Он имеет текстовый вид и нужен при отладке программы, т.к. кроме строк самой программы содержит дополнительную информацию.
Обычно as создает в результате ассемблирования только объектный файл. Получить файл листинга можно, указав ключ -a и задав имя файла листинга в командной строке. Например:
Строки в первой части листинга имеют следующую структуру:
Все ошибки и предупреждения, обнаруженные при ассемблировании, транслятор выводит на экран, и файл листинга не создается.
Номер строки представляет собой номер строки файла листинга. Номера строк особенно полезны при работе с перекрестными ссылками.
Важно понимать, что номера строк в поле “номер строки” — это не номера строк исходного модуля. Например, при расширении макрокоманды или включении файла отсчет строк продолжается, хотя текущая строка в исходном файле остается той же. Чтобы перевести номер строки (сгенерированный, например, при создании перекрестных ссылок), вы должны найти соответствующую строку в листинге, а затем (по номеру или на глаз) найти ее в исходном файле.
Адрес — это смещение машинного кода от начала текущего сегмента.
Машинный код представляет собой действительную последовательность шестнадцатеричного значения байт и слов, которые ассемблируются из соответствующей исходной строки программы. Информация справа от данной инструкции — это машинный код, в который ассемблируется инструкция.
Исходный текст программы — это просто строка исходной программы вместе с комментариями. Некоторые строки на языке ассемблера (например, строки, содержащие только комментарии) не генерируют никакого машинного кода, и поля “смещение” и “исходный текст программы” в таких строках отсутствуют. Тем не менее номер строки им присваивается.
Подробнее о содержимом и об опциях создания файла листинга можно прочесть, выполнив команду .
Описание инструкций. Команды условного перехода
Команда перехода по адресу, имеющаяся в наборе инструкций процессоров ARM , выглядит следующим образом: b , где аргумент – метка, поставленная выше или ниже в коде программы – указывает, по какому именно адресу должен быть выполнен переход.
Как и многие другие команды ARM , команда перехода может быть снабжена кодом условия, и будет выполняться, только если это условие истинно. Добавив к команде b код условия, мы получаем целое семейство команд условного перехода.
Команда условного перехода выполняет или не выполняет переход по заданному адресу в зависимости от флагов состояния процессора, хранящихся в специальном регистре cpsr . Флаги – это биты специального регистра, отражающие состояние процессора в текущий момент времени. Наиболее часто используются следующие: флаг отрицательного результата N (установлен в единицу, если реузльтат последней арифметической операции был отрицательным), флаг нуля Z (единица, если в результатом последней операции был ноль), флаг переноса С (устанавливается, если в результате последней операции случился перенос бита из старшего разряда) и флаг переполнения V (в результате последней операции произошло переполнение разрядной сетки).
Чаще всего программисты формируют флаги, проверяя отношение между двумя операндами op1 op2 , для чего выполняется команда вычитания или команда сравнения. Команда сравнения имеет мнемонический код операции cmp и такой же формат, как и команда вычитания:
Она и выполняется точно так же, как команда вычитания за исключением того, что разность не записывается на место первого операнда. Таким образом, единственным результатом команды сравнения является формирование флагов, которые устанавливаются так же, как и при выполнении команды вычитания. Команды этой группы выполняют условный переход в зависимости от состояния флагов регистра cpsr . Синтаксис этих команд в ассемблере приведен в таблице.
В результате, ветвления в программе организуются в два этапа:
- проверка условия командой cmp или формирование флагов каким-то другим способом, например, арифметической инструкцией;
- использование инструкции условного перехода, т.е. одной из разновидностей команды b , приведенной в таблице.
Как упоминалось ранее, объединение и размещение секций выполняет компоновцик. С помощью специального скрипта компоновщика программист может управлять тем, как именно объединяются секции и в какой области памяти они размещаются. Ниже приведён пример очень простого скрипта.
- Команда SECTIONS определяет, как будут объединены секции и куда они должны быть помещены.
- В блоке, следующем после команды SECTIONS , приводится численное значение — счётчик размещений. Размещение всегда инициализируется значением 0x0 . Его можно проинициализировать каким-либо другим значением. В данном случае установка нами значения в ноль избыточное действие.
- и 4. Эта часть скрипта определяет, что секции .text из исходных файлов abc.o и def.o должны перейти в секцию .text выходного файла.
Скрипт компоновщика можно упростить указанием символа * вместо имён файлов:
Если программа содержит обе секции ( .text и .data ), то объединение и размещение секции .data можно выполнить следующим образом:
Здесь секция .text помещается по адресу 0x0 , а секция .data по адресу 0x400 . Если же счётчику размещений не были присвоены конкретные значения, то секции помещаются в соседних областях памяти.
Пример скрипта компоновщика
Используем последний пример скрипта для управления расположением программных секций .text и .data . Для этой цели воспользуемся слегка модифицированной версией программы для вычисления суммы элементов массива:
Единственное отличие — то, что массив теперь находится в секции .data . Также стоит отметить, что в этой программе не нужна инструкция для перепрыгивания через данные, т. к. скрипт корректно размещает секции .text и .data . В результате объявление данных может быть расположено в программе в любом удобном месте, а скрипт компоновщика позаботится о правильном размещении секций а памяти.
Когда программа компонуется, скрипт передаётся в качестве входных данных компоновщику, как показано в следующих командах:
Опция -T определяет, что файл sum-data.lds должен быть использован в качестве скрипта компоновщика. Сброс таблицы символов даст понимание того, как секции помещаются в памяти
Из таблицы символов становится очевидным, что секция .text размещается с адреса 0x0 , а секция .data с 0x400 .
Больше об ассемблерных директивах
Рассмотрим еще несколько часто используемых директив на примере двух программ:
- Программа суммы элементов массива
- Программа, считающая длину строки
Следующий код суммирует массив байт и сохраняет результат в r3 :
В коде представлены две новые ассемблерные директивы .byte и .align . Эти директивы описаны ниже.
Аргументы директивы .byte имеют размер 1 байт и собраны в последовательность байт в памяти. Существуют аналогичные директивы .2byte и .4byte для хранения 16- и 32-битных значений соответственно. Общий синтаксис приведён ниже
Аргумент может быть простым целым числом представленным в двоичной, восьмеричной, десятичной или шестнадцатеричной формах. Целые числа могут также быть представлены как символьные константы (обозначены одинарными кавычками), в этом случае будет использоваться ASCII значение символа.
Аргументом могут также служить выражения составленные их букв и других символов. Например:
Процессору ARM требуется, чтобы инструкции были представлены в 32-битных ячейках памяти. Адрес первого из четырёх байт должен быть кратным 4. Чтобы придерживаться этого, директива .align используется для заполнения недостающими битами до тех пор, пока адрес не будет кратным 4. Это требуется только когда данные байт или полуслов вставлены в код.
Следующий код считает длину строки и сохраняет результат в регистр r1:
В коде представлены две новые ассемблерные директивы .asciz и .equ .
Директива .asciz в качестве аргументов принимает строковые литералы. Строковые литералы представляют собой последовательность символов в двойных кавычках. Строковые литералы собраны в ячейки памяти последовательно. Ассемблер автоматически вставляет нули после каждой строки.
Директива .ascii аналогична .asciz , но ассемблер не вставляет нули после каждой строки.
Ассемблер поддерживает так называемые таблицы символов. В них содержатся имена меток с адресами. Всякий раз, когда ассемблер встречает определение метки, он делает запись в таблицу символов. Всякий раз, когда ассемблер встречает ссылку на метку, он заменяет метку соответствующим адресом из таблицы символов.
Использование директивы .equ также возможно для ручной вставки записей в таблицу символов для сопоставления имён и значений, которые не обязательно являются адресами. Эти имена и имена меток вместе называются символьными именами.
Общий синтаксис этой директивы представлен ниже.
Имя является символьным именем и имеет те же ограничения, что и имя метки. Выражение может быть простым литералом или выражением, описанным в директиве .byte .
Организация обмена информации
Для простейшего обмена информацией программы с окружающим миром мы воспользуемся последовательной консолью. Для использования ее в эмуляторе QEMU необходимо выполнить проброс порта UART . UART (универсальный асинхронный приемопередатчик) – устройство и одноимённый протокол, обеспечивающие передачу данных по последовательному интерфейсу. Наиболее широко распространенным примером является последовательный порт персонального компьютера, однако последовательный интерфейс широко используется и в микроконтроллерных устройствах. UART эмулируется системой qemu-system-arm, и мы соединим его c консолью хоста – то есть выполним проброс из хост-системы (снаружи эмулятора) в гостевую систему (внутрь).
Чтобы было удобно вести диалог с ассемблерной программой, мы соединим последовательную консоль эмулятора с одним из графических окон терминала, запущенных на хост-системе. Доступ к терминалу на хост-системе будет осуществляться через виртуальный файл устройства /dev/pts/? , где знак вопроса заменяется номером консоли. Узнать, какой именно файл устройства соответствует запущенному треминалу можно, выполнив в нём команду tty . Так, например, для использования первого терминала запуск эмулятора будет иметь вид:
Таким образом, информация будет отправляться и приниматься с двух сторон: из окружающей среды, в роли котороый выступает ОС GNU /Linux на хост-системе, и изнутри эмулятора QEMU . В эмулируемой qemu-system-arm модели connex в качестве последовательного порта с нулевым номером выступает порт по адресу 0x40100000 . Поэтому для того чтобы почесть или передать байт, нужно почесть или записать байт по адресу 0x101f1000 :
Чтобы иметь возможность читать через UART то, что отвечает пользователь, может понадобиться отключить перехват текстового ввода (по умолчанию ввод с клавиатуры, выполненный в окне терминала, получает запущенная в нём программа-облочка командной строки). Самый простой способ это сделать – поставить запущенную в терминале оболочку на большую паузу, например “усыпить” на целый день командой sleep 1d .
Также заметим, что для корректной работы этой программы необходимо использовать скрипт компоновщика, размещающий по отдельным адресам секции кода и данных, как было показано в примере выше.
Последнее замечание будет касаться точности эмуляции UART . Реализованная модель в QEMU не воспроизводит в точности передачу байтов с точки зрения синхронизации и задержек, существующих в реальном устройстве: данные в эмулируемом UART просто мгновенно “появляются” по соответствующему адресу. В реальном устройстве его использование несколько сложнее (например, требуется проверка флага “Transmit FIFO Full” по дополнительному порту, прежде чем выполнять вывод очередного байта, и т.д.).
В данном разделе приводится описание наборов инструкций процессора ARM7TDMI.
4.1 Краткое описание формата
В данном разделе представлено краткое описание наборов инструкций ARM и Thumb.
Ключ к таблицам наборов инструкций представлен в таблице 1.1.
Процессор ARM7TDMI выполнен на основе архитектуры ARMv4T. Более полное описание обоих наборов инструкций представлено в "ARM Architecture Reference Manual".
Таблица 1.1. Ключ к таблицам
Форматы набора инструкций ARM показаны на рисунке 1.5.
Более детальная информация относительно форматов набора инструкций ARM приведена в "ARM Architectural Reference Manual".
Рисунок 1.5. Форматы набора инструкций ARM
Некоторые коды инструкций не определены, но они не вызывают поиска неопределенных инструкций, например, инструкция умножения с битом 6 измененным к 1. Запрещается использовать такие инструкции, т.к. в будущем их действие может быть изменено. Результат выполнения данных кодов инструкций в составе процессора ARM7TDMI непредсказуем.
4.2 Краткое описание инструкций ARM
Набор инструкций ARM представлен в таблице 1.2.
Таблица 1.2. Краткое преставление инструкций ARM
Подробно ознакомиться с системой команд в режиме ARM можно здесь.
Режимы адресации - процедуры, которые используются различными инструкциями для генерации значений, используемых инструкциями. Процессор ARM7TDMI поддерживает 5 режимов адресации:
- Режим 1 - Сдвиговые операнды для инструкций обработки данных.
- Режим 2 - Чтение и запись слова или беззнакового байта.
- Режим 3 - Чтение и запись полуслова или загрузка знакового байта.
- Режим 4 - Множественные чтение и запись.
- Режим 5 - Чтение и запись сопроцессора.
Режимы адресации с указанием их типов и мнемонических кодов представлены в таблице 1.3.
Таблица 1.3. Режимы адресации
Операнд является частью инструкции, которая ссылается на данные или периферийное устройство. Операнды 2 представлены в таблице 1.4.
Таблица 1.4. Операнд 2
Поля представлены в таблице 1.5.
Таблица 1.5. Поля
Тип | Суффикс | Установки | Бит |
Поле | _c | Бит-маска поля управления | 3 |
_f | Бит-маска поля флагов | 0 | |
_s | Бит-маска поля статуса | 1 | |
_x | Бит-маска поля расширения | 2 |
Поля условий представлены в таблице 1.6.
Таблица 1.6. Поля условий
Тип поля | Суффикс | Описание | Условие |
Условие | EQ | Равно | Z=1 |
NE | Неравно | Z=0 | |
CS | Беззнаковое больше или равно | C=1 | |
CC | Беззнаковое меньше | C=0 | |
MI | Отрицательное | N=1 | |
PL | Положительное или ноль | N=0 | |
VS | Переполнение | V=1 | |
VC | Нет переполнения | V=0 | |
HI | Беззнаковое больше | C=1, Z=0 | |
LS | Беззнаковое меньше или равно | C=0, Z=1 | |
GE | Больше или равно | N=V (N=V=1 или N=V=0) | |
LT | Меньше | N<>V (N=1 и V=0) или (N=0 и V=1) | |
GT | Больше | Z=0, N=V (N=V=1 или N=V=0) | |
LE | Меньше или равно | Z=0 или N<>V (N=1 и V=0) или (N=0 и V=1) | |
AL | Всегда истинный | флаги игнорируются |
4.3 Краткое описание набора инструкций Thumb
Форматы набора инструкций Thumb показаны на рисунке 1.6. Более подробная информация по форматам наборов инструкций ARM приведена "ARM Architectural Reference Manual".
Привет всем!
По роду деятельности я программист на Java. Последние месяцы работы заставили меня познакомиться с разработкой под Android NDK и соответственно написание нативных приложений на С. Тут я столкнулся с проблемой оптимизации Linux библиотек. Многие оказались абсолютно не оптимизированы под ARM и сильно нагружали процессор. Ранее я практически не программировал на ассемблере, поэтому сначала было сложно начать изучать этот язык, но все же я решил попробовать. Эта статья написана, так сказать, от новичка для новичков. Я постараюсь описать те основы, которые уже изучил, надеюсь кого-то это заинтересует. Кроме того, буду рад конструктивной критике со стороны профессионалов.
Введение
Итак, для начала разберёмся что же такое ARM. Википедия дает такое определение:
Архитектура ARM (Advanced RISC Machine, Acorn RISC Machine, усовершенствованная RISC-машина) — семейство лицензируемых 32-битных и 64-битных микропроцессорных ядер разработки компании ARM Limited. Компания занимается исключительно разработкой ядер и инструментов для них (компиляторы, средства отладки и т. п.), зарабатывая на лицензировании архитектуры сторонним производителям.
Если кто не знает, сейчас большая часть мобильных устройств, планшетов разработаны именно на этой архитектуре процессоров. Основным преимуществом данного семейства является низкое энергопотребление, благодаря чему он часто используется в различных встроенных системах. Архитектура развивалась с течением времени, и начиная с ARMv7 были определены 3 профиля: ‘A’(application) — приложения, ‘R’(real time) — в реальном времени,’M’(microcontroller) — микроконтроллер. Историю разработки этой технологии и другие интересный данные вы можете прочитать в Википедии или погуглив в интернете. ARM поддерживает разные режимы работы (Thumb и ARM, кроме того в последние время появился Thumb-2, являющийся смесью ARM и Thumb). В данной статье рассмотрим собственно режим ARM, в котором исполняется 32-битный набор команд.
- 37 регистров (из которых видимых при разработке только 17)
- Арифметико-логи́ческое устройство (АЛУ) — выполняет арифметические и логические задачи
- Barrel shifter — устройство, созданное для перемещения блоков данных на определенное количество бит
- The CP15 — специальная система, контроллирующая ARM сопроцессоры
- Декодер инструкций — занимается преобразованием инструкции в последовательность микроопераций
Конвейерное исполнение (Pipeline execution)
В ARM процессорах используется 3-стадийный конвейер (начиная с ARM8 был реализова 5-стадийный конвейер). Рассмотрим простой конвейер на примере процессора ARM7TDMI. Исполнение каждой инструкции состоит из трёх ступеней:
1. Этап выборки (F)
На этом этапе инструкции поступают из ОЗУ в конвейер процессора.
2. Этап декодирования (D)
Инструкции декодируются и распознаётся их тип.
3. Этап исполнения (E)
Данные поступают в ALU и исполняются и полученное значение записывается в заданный регистр.
Но при разработке надо учитывать, что, есть инструкции, которые используют несколько циклов исполнения, например, load(LDR) или store. В таком случае этап исполнения (E) разделяется на этапы (E1, E2, E3. ).
Условное выполнение
Одна из важнейших функций ARM ассемблера — условное выполнение. Каждая инструкция может исполняться условно и для этого используются суффиксы. Если суффикс добавляется к названию инструкции, то прежде чем выполнить ее, происходит проверка параметров. Если параметры не соответствуют условию, то инструкция не выполняется. Суффиксы:
MI — отрицательное число
PL — положительное или ноль
AL — выполнять инструкцию всегда
Суффиксов условного выполнения намного больше. Остальные суффиксы и примеры прочитать в официальной документации: ARM документация
А теперь пришло время рассмотреть…
Основы синтаксиса ARM ассемблера
Тем, кто раньше работал с ассемблером этот пункт можно фактически пропустить. Для всех остальных опишу основы работы с этим языком. Итак, каждая программа на ассемблере состоит из инструкций. Инструкция создаётся таким образом:
<инструкция|операнды>
Метка — необязательный параметр. Инструкция — непосредственно мнемоника инструкции процессору. Основные инструкции и их использование будет разобрано далее. Операнды — константы, адреса регистров, адреса в оперативной памяти. Комментарий — необязательный параметр, который не влияет на исполнение программы.
Имена регистров
Разрешены следующие имена регистров:
1.r0-r15
3.v1-v8 (переменные регистры, с r4 по r11)
4.sb and SB (статический регистр, r9)
10.pc and PC (программный счетчик, r15).
Переменные и костанты
- Числовые
- Логические
- Строковые
Примеры инструкций ARM ассемблера
Чтобы закрепить использование основных инструкций давайте напишем несколько простых примеров, но сначала нам понадобится arm toolchain. Я работаю в Linux поэтому выбрал: frank.harvard.edu/~coldwell/toolchain (arm-unknown-linux-gnu toolchain). Ставится он проще простого, как и любая другая программа на Linux. В моем случае (Russian Fedora) понадобилось только установить rpm пакеты с сайта.
Теперь пришло время написать простейший пример. Программа будет абсолютно бесполезной, но главное, что будет работать:) Вот код, который я вам предлагаю:
Компилируем программу до получения .bin файла:
(код в файле arm.s, а toolchain в моем случае лежит в директории /usr/arm/bin/)
Если все прошло успешно, у вас будет 3 файла: arm.s (собственно код), arm.o, arm.elf, arm.bin (собственно исполняемая программа). Для того, чтобы проверить работу программы не обязательно иметь собственное arm устройство. Достаточно установить QEMU. Для справки:
QEMU — свободная программа с открытым исходным кодом для эмуляции аппаратного обеспечения различных платформ.
Включает в себя эмуляцию процессоров Intel x86 и устройств ввода-вывода. Может эмулировать 80386, 80486, Pentium, Pentium Pro, AMD64 и другие x86-совместимые процессоры; PowerPC, ARM, MIPS, SPARC, SPARC64, m68k — лишь частично.
Работает на Syllable, FreeBSD, FreeDOS, Linux, Windows 9x, Windows 2000, Mac OS X, QNX, Android и др.
Итак, для эмуляции arm понадобится qemu-system-arm. Этот пакет есть в yum, так что тем, у кого Fedora, можно не заморачиваться и просто выполнить комманду:
yum install qemu-system-arm
Далее надо запустить эмулятор ARM, так, чтобы он выполнил нашу программу arm.bin. Для этого создадим файл flash.bin, который будет флэш памятью для QEMU. Сделать это очень просто:
Теперь грузим QEMU с полученой flash памятью:
На выходе вы получите что-то вроде этого:
[anton@localhost ~]$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
QEMU 0.15.1 monitor — type 'help' for more information
(qemu)
Наша программа arm.bin должна была изменить значения четырех регистров, следовательно для проверки правильности работы давайте посмотрим на эти самые регистры. Делается это очень простой коммандой: info registers
На выходе вы увидите все 15 ARM регистров, при чем у четырех из них будут измененные значения. Проверьте:) Значения регистров совпадают с теми, которые можно ожидать после исполнения программы:
P.S. В этой статье я постарался описать основы программирования на ARM ассемблер. Надеюсь вам понравилось! Этого хватит для того, чтобы далее углубляться в дебри этого языка и писать на нем программы. Если все получится, буду писать дальше о том, что узнаю сам. Если есть ошибки, прошу не пинать, так как я новичок в ассемблере.
Пару дней назад я опубликовал и потом внезапно убрал в черновики статью о плане написать про создание своей ОС для архитектуры ARM. Я сделал это, потому что получил много интересных отзывов как на Хабре, так и в G+.
Сегодня я попробую подойти к вопросу с другой стороны, я буду рассказывать о том, как программировать микроконтроллеры ARM на нарастающих по сложности примерах, пока мы не напишем свою ОС или пока мне не надоест. А может, мы перепрыгнем на ковыряние в Contiki, TinyOS, ChibiOS или FreeRTOS, кто знает, их там столько много разных и интересных (а у TinyOS еще и свой язык программирования!).
Небольшой экскурс в архитектуру
ARM продвигает замечательную архитектуру, которую успешно лицензирует, мне на самом деле сложно представить, в каком устройстве нет никакого присутствия продуктов этой компании. В вашем смартфоне гарантированно есть несколько ядер на базе архитектуры ARM. Еще парочка найдется в современном ноутбуке (и это даже не CPU, а так, сопутствующий контроллер какой-либо периферии), еще несколько – в автомобиле. Есть они и в других бытовых вещах: микроволновках и телевизорах.
Такая гибкость достигается тем, что в самом базовом варианте ядро ARM очень простое. Сейчас существуют три разновидности этой архитектуры. Application применяется в устройствах «общего назначения» – как основной процессор в смартфоне или нетбуке. Этот профиль самый навороченный функционально, тут есть и полноценный MMU (модуль управления памятью), возможность аппаратно выполнять инструкции Java bytecode и даже поддержка DRM-схем. Microcontroller – это полная противоположность профилю application, применяемая (внезапно!) для использования в микроконтроллерах. Тут актуально минимальное энергопотребление и детерминистическое поведение. И, наконец, real-time используется как эволюция профиля microcontroller для задач, где критично иметь гарантированное время отклика. Все эти профили получили реализацию в одном или нескольких ядрах Cortex, так, например, Cortex-A9 основан на профиле application и является частью процессора в iPhone 4S, а Cortex-M0 основан на профиле microcontroller.
Железки!
В качестве целевой платформы мы будем рассматривать работу с Cortex-M, так как она самая простая, соответственно, надо вникать в меньшее количество вопросов. В качестве тестовых устройств я предлагаю вам LPC1114 – MCU производства NXP, схему на котором можно собрать буквально на коленке (нет, правда, вам нужен только сам MCU, FTDI-кабель на 3,3 В, несколько светодиодов и резисторов). LPC1114 построен на базе Cortex-M0, так что это будет самый урезанный вариант платформы.
В качестве альтернативного варианта мы будем работать с платформой mbed, а конкретно, с моделью на базе LPC1768 (а значит, внутри там Cortex-M3, несколько более навороченный). Вариант уже не настолько бюджетный, но процесс заливки бинарников на чип и отладки упрощен максимально. Да и можно поиграться с самой платформой mbed (вкратце: это онлайн-IDE и библиотека, с помощью которой можно программить на уровне ардуины).
Приступим
Интересной особенностью современных ARM-ов является то, что их вполне реально программировать целиком на С, без применения ассемблерных вставок (хотя ассемблер не так уж и сложен, у Cortex-M0 всего 56 команд). Хотя некоторые команды в принципе не доступны из С, эту проблему решает CMSIS – Cortex Microcontroller Software Interface Standard. Это драйвер для процессора, который решает все основные задачи управления им.
Как же загружается процессор? Типична ситуация, когда он просто начинает выполнять команды с адреса 0x00000000. В нашем случае процессор несколько более умный, и рассчитывает на специально определенный формат данных в начале памяти, а именно – таблицу векторов прерываний:
Старт выполнения программы происходит следующим образом: процессор читает значение по адресу 0x00000000 и записывает его в SP (SP – регистр, который указывает на вершину стека), после чего читает значение по адресу 0x00000004 и записывает его в PC (PC – регистр, который указывает на текущую инструкцию + 4 байта). Таким образом начинает выполняться какой-то код пользователя, при этом у нас уже есть стек, указывающий куда-то в память (т.е., все условия для выполнения программы на С).
В качестве тестового упражнения мы будем мигать светодиодом. На mbed у нас их целых четыре, в схему с LPC1114 (далее — «доска») мы устанавливаем светодиод вручную.
Перед тем как непосредственно писать код, нам надо выяснить еще одну вещь, а именно – что где должно располагаться в памяти. Поскольку мы не работаем с какой-то «стандартной» ОС, то компилятор (вернее, компоновщик) не может узнать, где у него должен быть стек, где сам код, а где — куча. К счастью для нас, у семейства ядер Cortex стандартизированная карта памяти, что позволяет относительно просто портировать приложения между разными процессорами этой архитектуры. Работа с периферией, конечно, остается процессорозависимой.
Карта памяти для Cortex-M0 выглядит вот так:
У Cortex-M3 она, по сути, такая же, но несколько более детальна. Проблема тут в том, что у NXP есть свой, отдельный взгляд на этот вопрос, так что проверяем карту памяти в документации на процессор:
На самом деле, SRAM у нас начинается с 0x10000000! Вот так, одни стандарты, другие стандарты, а все равно надо тома документации листать.
Вооружившись этими знаниями, идем писать код. Для начала – таблица прерываний:
Сохраним эту таблицу в boot.s . Тут, фактически, только одна ассемблерная вставка – функция hang, которая устраивает процессору бесконечный цикл. Все прерывания, кроме reset, указывают на нее, так что в случае непредвиденной ситуации процессор просто зависнет, а не пойдет выполнять непонятный участок кода.
Сама таблица должна бы быть длиннее, но на самом деле мы могли бы закончить ее еще после вектора Reset, остальные у нас не сработали бы в этом примере. Но, на всякий случай, мы заполнили таблицу почти целиком (кроме пользовательских прерываний).
Теперь напишем реализацию функции main:
У mbed первый светодиод подключен к порту GPIO 1.18, на доске мы подключили светодиод к GPIO 1.8. Одни и те же пины могут выполнять разные функции, эти по умолчанию работают именно как GPIO (General Purpose I/O – линии ввода/вывода общего назначения).
Код относительно прямолинеен, если держать под рукой LPC-шный User manual (один и второй). Для начала мы указываем режим работы GPIO через регистр GPIO_DIR_REG (у наших процессоров они в разных местах, да и вообще LPC1768 может работать с GPIO более эффективно), где 1 – вывод, 0 – ввод. Потом мы запускаем бесконечный цикл, в котором пишем в порт попеременно значения 0 и 1 (0 В и 3,3 В соответственно).
Функция для «паузы» у нас работает наугад, просто прокручивая относительно долгий цикл ( volatile int не дает компилятору выоптимизировать этот цикл целиком).
Наконец, все это нужно правильно скомпоновать:
Сценарий компоновщика объясняет ему, где у нас флеш, где оперативная память, какие у них размеры (тут используются размеры для LPC1114, так как у LPC1768 всего больше, сдвиги, к счастью, идентичны). После определения карты памяти мы указываем, какие сегменты куда копировать, .text (код программы) попадает в флеш, .bss (статические переменные, которых у нас пока нет) – в память. Помимо этого мы задаем два символа, которые использовали в boot.s: _stack_base – указывает на вершину стека и _boot_checksum (спасибо Zuy за уточнение!) – записывает чексумму загрузчика. Чексумма рассчитывается по формуле: дополнительный код (2's compliment) от суммы полей выше (т.е. адреса стека, и всех прерываний до непосредственно чексуммы). Хотя утилиты для прошивки (см. далее) сами исправили бы чексумму на правильную, если бы мы прошивали бы код из самого приложения, то загрузиться снова мы бы уже не смогли.
Теперь у нас есть три файла: boot.s, main.c, mem.ld, пора это все скомпилировать и, наконец, запустить. В качестве тулчейна мы будем использовать GCC, позже, возможно, я покажу как делать то же с LLVM. Пользователям OS X я советую взять тулчейн у Linaro – в самом конце списка: Bare-Metal GCC ARM Embedded. Пользователям других ОС я советую взять тулчейн там же :-) (разве что гентушникам будет проще сэмержить crossdev и скомпилить GCC).
Интересный момент тут — это отключение использования всех стандартных библиотек у GCC. Действительно, весь код, который попадет в итоговый бинарник – это код, который написали мы сами.
Вопрос: как компоновщик знает, куда надо засунуть таблицу прерываний? А он и не знает, там не написано :-). Он просто линкует подряд, начиная с нулевого адреса, так что порядок файлов (boot.o, потом main-c0.o) очень важен! Попробуйте слинковать наоборот или слинковать boot.o два раза и сравните вывод в lst-файле.
Хорошая идея – посмотреть на итоговый листинг (файл lst) или закинуть бинарник в дизассемблер. Даже если вы не говорите на ARM UAL, то чисто визуально можно проверить, что хотя бы таблица прерываний находится на своем месте:
Еще можно обратить внимание на забавный момент – GCC при компиляции под Cortex-M3 генерирует функцию wait() больше, чем в варианте под Cortex-M0. Правда, если включить оптимизацию то она вправит ему мозги.
Мигаем!
Все что нам осталось – залить бинарники на наши тестовые платформы. С mbed тут все максимально просто, просто скопируйте blink-c3.bin на виртуальную флешку и нажмите reset (на mbed). С доской все немного сложнее. Во-первых, для того, чтобы попасть в загрузчик, нам нужен резистор между GND и GPIO 0.1. Во-вторых, необходима программа для непосредственно прошивки. Можно использовать Flash Magic (Win, OS X), можно использовать консольную утилиту – lpc21isp:
- ставим резистор между j5 и j7 (10 кОм подойдет);
- нажимаем reset;
- запускаем lpc21isp;
- снимаем резистор;
- нажимаем reset еще раз – запускается приложение.
Если у вас есть возможность запустить примеры на разных устройствах, вы заметите, что скорость мигания на них не идентична. Это связанно с тем, что у разных устройств разная частота ядра, соответственно, wait() они выполняют за разное время. В следующей части мы изучим вопросы осцилляции детальнее и сделаем четкий отсчет времени.
P.S. Отдельное спасибо хабраюзеру pfactum за то, что тратит время на исправление моих ошибок в тексте :-).
P.P.S. Просьба тем, у кого есть тестовая платформа на базе ARM – пишите в комментариях – какая. Я могу пересмотреть аппаратную базу для дальнейших статей.
В последнее время я часто встречаю о самых разных устройствах, работающих на процессорах с архитектурой ARM. В этой статье я хочу начать рассказ о архитектуре процессоров ARM7TDMI (не путать с ARMv7). ARM7TMI — это довольно таки устаревшее семейство, но оно довольно таки широко используется в разных embedded устройствах. Так как моя работа очень плотно связанна с разработкой таких устройств — то я довольно неплохо ориентируюсь именно в этом семействе. Но если кому-то будет интересно — могу рассказать и о более новых семействах ARM.
Общее описание
Надо сказать, что ARM — это просто архитектура, на основании которой построено множество разных процессоров. У них может быть совершенно разная периферия, разные методы взаимодействия с периферией, разная частота и энергопотребление, но объединяет их одно — процессорное ядро ARM.
ARM7 с одной стороны довольно таки прост (особенно по сравнению с x86), с другой стороны — имеет большую производительность и меньшее энергопотребление. Но меньший набор команд и тот факт, что длинна команды фиксирована приводит к увеличению объема программ.
Отличия от x86
Надеюсь, многие из читающих эту статью хотя бы в общих чертах знают архитектуру x86 :)
Чем же ARM7 отличается от x86?
Регистры
ARM имеет 32 регистра длинной 32 бита. На самом деле одновременно доступно только 16 из них. Остальные регистры переключается вместе с режимами процессора. Все регистры совершенно одноправны (сравните с x86, где даже регистры общего назначения имеют разные свойства). Правда один из регистров, r15, используется как счетчик инструкций (program counter), у него даже есть псевдоним — pc. Так что очевидно, что его не стоит использовать в качестве регистра общего назначения :)
Другой регистр, r14 используется как указатель стека (stack pointer) и имеет псевдоним sp. Но его никто не мешает использовать как обычный рабочий регистр, если вам вдруг совершено не нужен стек. Хотя и это и не рекомендуется.
Третий регистр, r13 по соглашению хранит адрес возврата из текущей функции. Точно и как и предыдущий регистр его можно использовать как рабочий.
Остальные 13 регистров программист (правда, чаще — компилятор) может использовать как хочет.
Плюс, есть ещё 2 выделенных регистра состояния процессора. На самом деле один и тот же регистр, просто одна его ипостать содержит сохраненные данные после переключения режима.
Режимы работы
Процессор может работать в 7 разных режимах: User, FIQ, IRQ, Supervisor, Abort, System, Undefined. 4 из этих режимов (FIQ, IRQ, Undefined, Abort) слушат для обработки исключительных операций — обработка быстрого прерывания, обработка обычного прерывания, попытка выполнить неизвестную инструкцию, попытка обратиться к несуществующей области памяти (или по невыровненому адресу). Режимы Abort и Undefined позволяют сделать эмуляцию инструкций сопроцессора и добавить поддержку виртуальной памяти соответственно.
Остальные три режим служат для защиты операционной системы от прикладных программ.
Любопытно что все режимы (кроме System) имеют свои регистры r13 и r14. Таким образом при переключении режимов нет надобности сохранять значения вершины стека и адрес возврата.
Набор команд
- все операции над данными происходят только в регистрах. Нельзя модифицировать данные в памяти
- с памятью работают только команды пересылки данных. Возможна передача данных регистр-память, память-регистр и регистр-регистр
- каждая команда имеет модификаторы условного исполнения. Т.е. можно делать не только условный переход, но и например условную пересылку или сложение.
Прочее
Есть ещё много отличий, например таких как отсутствие команд для работы с числами с плавающей точкой, команд для работы с десятично -двоичными числами, запрет на обращения к невыравненым адресам в памяти, расширения DSP и Jazelle, отсутствие портов ввода-вывода (вся периферия мапится в память), только линейная адресация (хотя при наличии MMU можно переключать страницы), хитрые модификаторы команд пересылки для использования битовых сдвигов.
В общем, если кому-то будет интересно — могу рассказать подробнее об АРМах, конкретно о процессоре AT91SAM7x, о embedded разработке вообще и в частности.
Читайте также: