Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР 115, ИЮЛЬ 2008 г.

Программируем железные руки

Андрей «Dron_Gus» Гусаков (dron_gus@mail.ru)

Хакер, номер #115, стр. 115-113-1

Осваиваем контроллеры архитектуры ARM

Нет, мы не будем имплантировать в твое бренное тело электроды и посылать атрофированным мышцам киловольты, мы займемся привычным делом – программингом. Только вот от банальной архитектуры х86 откажемся в пользу менее известной, но более пригодной для создания фрикерских девайсов архитектуры ARM. Переводится это как Advanced RISC Machines. При умелом использовании контроллеры на этой архитектуре заменят тебе руки… а, возможно, и мозги.

Экскурс в историю

Архитектура ARM зародилась в Acorn Computers в 1983-85 годах. Основной фичей этой архитектуры по сравнению с множеством других RISC’ов были инструкции с условным выполнением. Так, на классической архитектуре х86 выражение в стиле «If (условие) оператор1 else оператор2» после компиляции генерирует код, как минимум с двумя переходами. Тебе, конечно, известно, что любые переходы требуют перезагрузки всего конвейера, отчего и теряется драгоценная производительность конвейерных процов. Для каждой ARM-инструкции существует 4-битный префикс условия – любая инструкция может быть выполнена или нет в зависимости от установленных флагов. Таким образом, ту же конструкцию if для АРМа можно выразить в виде линейного кода.

Преимущества условного выполнения

If (условие) оператор1 else оператор2

Классическая архитектура (например, x86):

  1. Проверить условие
  2. Если не выполнено – идти к 5
  3. Оператор1
  4. Идти к 6
  5. Оператор2

Архитектура ARM:

  1. На основании условия взвести или нет флаги
  2. Оператор1 при условии взведения флага
  3. Оператор2 при условии невзведения флага

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

Другой фичей является выполнение нескольких простых операций в одной инструкции. Тем самым обеспечивается еще большая плотность кода и производительность. За счет таких операций в некоторых случаях можно отказаться от хранения результатов промежуточных вычислений в регистрах.

Первый процессор ARM1 выпустили в 1985-м, а через год появился и коммерческий вариант – ARM2. На тот момент он был настоящим прорывом: насчитывая вчетверо меньше транзисторов, чем 286 процессор, ARM2, тем не менее, обгонял его по производительности и к тому же был 32-разрядным. Позже появился процессор ARM3 с еще большей производительностью и апгрейдом в виде 4 Кб кэш-памяти.

В 1990 году результатом совместной работы с Apple стало ядро ARM6 и проц на его основе – ARM610. Кстати, именно этот процессор был использован в одном из первых КПК Apple Newton. Но в 90-х тягаться с более производительными и монструозными конкурентами было сложно, и ARM стала позиционировать свои процессоры, как «встраиваемые». Любой желающий мог «воткнуть» ARM-ядро в свой специализированный процессор. Эта стратегия оказалась удачной, так что скоро архитектура получила широчайшее распространение. Ядро ARM7DTMI – основа огромного количества процов для сотиков. Именно оно и будет предметом дальнейшего разговора. Сегодня ARM – 75% от всех выпускаемых интегрируемых процессоров. Их ставят в сотики и КПК, контроллеры HDD и маршрутизаторы. Для КПК, кстати, есть отдельная более производительная ветка – StrongARM. Intel тоже отхватила себе кусочек StrongARM и теперь развивает их под именем XScale.

Разобравшись с этой архитектурой и научившись хорошо программировать, ты сможешь поднимать неплохие деньги и выбирать себе направление работы по вкусу, начиная с программирования сотовых телефонов и mp3-плееров и заканчивая WiFi-роутерами и шлюзами. Хочешь программируй «голое» железо, хочешь – ставь linux, qnx или даже windows ce/mobile.

Выбираем проц и плату

Перейдем от истории ко дню сегодняшнему и посмотрим, чем может быть полезен ARM кул-хацкеру. Ныне АРМы выпускают все кому не лень, от мелких фирм, даже не имеющих своих производств, до гигантов типа Philips и Atmel. Продукты этих двух производителей наиболее доступны в магазинах (в разумных количествах, а не партиями от 10000 шт.). Еще нас пока мало интересуют ядра ARM9 и выше: сотни мегагерц, кучи интерфейсов – это конечно вкусно, но корпуса BGA и цены делают эти процессоры сложными в применении для обычного радиолюбителя.

Я предлагаю начать с продукции фирмы Atmel (про контроллеры ATmega ты мог неоднократно читать в ][). Меня их товар привлекает еще со времен знакомства с контроллерами архитектуры 8051. Отличная, понятная документация, обилие примеров и множество приверженцев будут хорошим подспорьем при изучении нового для тебя направления. Фирма Atmel производит достаточно широкую линейку процессоров на ядре ARM7DTMI. Предлагаю остановиться на AT91SAM7S256. Ресурсов даже такого мелкого процессора хватит для декодирования mp3 в реальном времени. Проверенно! Кстати, немного о самом контроллере:

  • ядро ARM7DTMI до 55 МГц (при желании можно разогнать)
  • 256 кило флешки
  • 64 кило ОЗУ (Винды мы на него ставить не будем, так что этого нам хватит)
  • 32 ноги под различные интерфейсы и самостоятельное дерганье
  • USB device full speed (до 12 Мбит/сек для общения с ПК)
  • куча различных вкусностей – таймеры, часы реального времени, контроллер прерываний, DMA и т.д.

Единственный минус контроллера – это корпус. К сожалению, процессоры такого уровня уже давно не выпускаются в корпусах DIP (когда ноги продеваются в отверстия платы). Есть вариант корпуса LQFP (ноги на все 4 стороны с шагом 0.5). Для самостоятельной пайки вариант не очень, а для самостоятельного изготовления платы – тем паче. Но не стоит отчаиваться! Во-первых, можно купить платку-переходник. Во-вторых, купить отладочную плату с этим процом, забыть про пайку и сразу же погрузиться в программирование. Я себе купил платку OLIMEX SAM7-P256 (впрочем, есть и другие варианты).

Что ты получишь, приобретя эту плату:

  • заботливо распаянные разъемы USB, 2 x RS232, MMC/SD, JTAG
  • пару кнопок
  • датчик температуры
  • все сигналы процессора, выведенные на линейку контактов
  • небольшую зону для макетирования
  • стабилизатор питания и кнопку reset
  • избавление от геморроя с пайкой (это главное).

Если помнишь, в статье «Шпионим за тетей Клавой» у Dlinyj и Serg2x2 возникла проблема с хранением длинного лога нажатых кнопок. С этим контролером ты можешь забыть о таких траблах. Вряд ли тебе удастся забить всю флеш. Остаток можно перепрограммировать прямо из программы и кидать туда логи. В списке вкусностей платы от OLIMEX упомянут разъем MMC/SD – значит, к процу легко можно прицепить флешку. Фантазия работает? Да, этого процессора за глаза хватит для обработки файловой системы! У него еще и USB есть… Можно даже mass storage изобразить. Так что, если твой подопытный перед вводом пароля захочет протереть клаву или, что еще хуже, пересказать «Войну и Мир» ты не потеряешь ни одного драгоценного символа. Они будут заботливо сложены на много-гигабайтную флешку. Принеся такой логгер домой, ты сможешь его запросто воткнуть в УСБ и без особых извращений просмотреть все добытое в виде обычных текстовых файлов на обычном съемном диске.

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

Средства разработки

Перечислять все компиляторы, а тем более среды разработки не имеет смысла. Погуглив минут пять, любой желающий без труда найдет несколько вариантов. Я предлагаю начать с IAR Embedded Workbench IDE. Эта среда требует минимум настроек перед работой, под нее имеется множество примеров и т.д. Писать программу можно как на чистом C, так и на C++. Можно писать и на ассемблере, но для таких процессоров это редко практикуется. Для джедаев могу предложить попробовать поставить себе GCC с какой-нибудь средой вроде Eclipse, но я эту связку настроить так и не сумел. Если тебе удастся – черкани мне пару строк.

Предположим, программу ты написал, откомпилировал и получил прошивку. Возникает резонный вопрос: как ее залить в камень? Существуют различные внутрисхемные отладчики/программаторы для ARM-процессоров, такие как: Wriggler, J-Link, U-link, MT-Link, JetLink и т.д. Пока ты плотно не занялся этими процессорами, покупать отладчик не имеет смысла (стоить он будет не так уж мало). Тут Atmel позаботилась о нас. В процессоре есть зашитый на заводе загрузчик. После определенной манипуляции с внешними сигналами управление передается ему. Далее, по USB или RS232, с помощью специального софта мы можем послать новую прошивку. Проц зальет ее во флеш или в ОЗУ и начнет выполнять. Это не самый удобный вариант, так как все время приходится перекидывать джамперы, но зато он экономит твои деньги. Софтина называется SAM-PROG и идет в наборе утилит от Atmel – AT91 In-system Programmer (ISP).

Первые шаги

Предположим, я убедил тебя в целесообразности покупки платки от Olimex. Что дальше? Лезем на сайт Atmel, находим там свой процессор. Качаем на него даташит (этот документ станет для тебя Библией на все время работы) и программу AT91 In-system Programmer (ISP). Ставим программу. Ничего сложного, комментировать не буду.

Теперь берем плату в руки и ищем на ней джампер TEST. Если при сбросе процессор видит, что на этой ноге лог 1, то он сам себя прошивает загрузчиком SAM-BA. Загрузчик получает управление после сброса и ждет «указаний». Итак, замыкаем джампер и втыкаем USB-шнурок. На плате должен загореться красный светодиод – это значит, что на плату поступает питание. Считаем до 20 (ждем пока SAM-BA перешьется во флеш), вынимаем УСБ, снимаем джампер TEST и снова втыкаем шнурок. Твой Windows должен увидеть новое устройство (что-то вроде «atm6124.sys ATMEL AT91xxxxx Test Board»). После установки всех драйверов можно запустить программу SAM-PROG и убедиться, что она видит подключенный процессор. К сожалению, под некоторыми Виндами софт от Atmel работает не совсем стабильно и приходится танцевать с бубном. Если SAM-PROG вылетает с ошибкой, попробуй сначала запустить софтину, а потом уже подключать шнурок к плате.

Лезем на сайт OLIMEX, ищем свою платку и качаем bin-файл примера Blinking LED project. Как следует из названия – это обычная мигалка светодиодами. Кормим файл SAM-PROG и жмем Writer Flash. Передергиваем USB и видим на отладочной плате два мигающих светодиода. Если нет – проверь, стоят ли у тебя на плате джамперы LED1 и LED2 (они должны быть замкнуты).

Как же это работает?

Теперь давай попробуем разобраться, как это работает. С того же сайта качаем исходный проект и открываем его IAR Embedded Workbench IDE. Если ты еще не успел установить себе эту среду разработки, можешь открыть *.c файлы обычным блокнотом (для понимания принципа этого будет достаточно). Весь проект состоит из двух исходников: system.c и main.c.

AT91PS_PIO m_pPio

= AT91C_BASE_PIOA;

int main()

{

//Init frequency

InitFrec();

//Init leds

InitPeriphery();

// loop forever

while(1)

{

m_pPio->PIO_CODR

= BIT18; //set reg to 0 (led2 on)

m_pPio->PIO_SODR

= BIT17; //set reg to 1 (led1 off)

Delay(800000); //simple delay

m_pPio->PIO_CODR

= BIT17; //set reg to 0 (led1 on)

m_pPio->PIO_SODR

= BIT18; //set reg to 1 (led2 off)

Delay(800000);

//simple delay

}

}

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

Установка частоты тактирования процессора – вызов InitFrec(). Код самой функции находится в файле system.c. Детально разбирать его пока не имеет смысла. Надо понять, как работает блок тактирования контроллера. Блок достаточно большой и приводить (или даже переводить) его описание из даташита не будем. Вот краткое описание того, что делает эта функция:

  • отключает watchdog. Это такой блок, который следит за тем, чтобы процессор не завис. Приложение должно не реже определенного интервала сбрасывать watchdog, иначе это расценивается как зависание или зацикливание процессора и подается сигнал сброса. В нашем примере watchdog не используется и поэтому отключается. Но считается хорошим тоном использовать все заложенные возможности для повышения надежности.
  • запуск кварцевого генератора (сам кварц ты можешь найти на плате – металлическая «лодочка» с надписью 18.432). Кварц генерирует на частоте 18.432 МГц. Это далеко не предельная частота функционирования процессора.
  • запуск PLL (ФАПЧ). Не вдаваясь в подробности, скажу, что на выходе PLL мы можем получить частоту кварцевого генератора, умноженную и поделенную на любые целые коэффициенты (Fpll = Focs * M / D). Конкретно в этой программе числа подобраны так, чтобы получить на выходе PLL-частоту примерно 96 МГц (почему – я объясню, когда мы начнем знакомиться с блоком USB)
  • после установления частоты PLL ядро переключается на тактирование от PLL и включается деление на 2 (ядро теперь работает на частоте 48 МГц). Кстати, сразу после сброса ядро работает от встроенного в процессор генератора с частотой 22-42 КГц.

Далее идет настройка выводов процессора. Функция InitPeriphery()

AT91PS_PIO p_pPio

= AT91C_BASE_PIOA;

AT91PS_PMC p_pPMC

= AT91C_BASE_PMC;

void InitPeriphery(void) {

/**** LED BUTTONS ****/

//enable the clock of the PIO

p_pPMC->PMC_PCER

= 1 << AT91C_ID_PIOA;

//LED 1

//configure the PIO Lines

.. corresponding to LED1

p_pPio->PIO_PER |= BIT17;

//Enable PA17

p_pPio->PIO_OER |= BIT17;

//Configure in Output

p_pPio->PIO_SODR |= BIT17;

//set reg to 1

//LED 2

//configure the PIO Lines

corresponding to LED2

p_pPio->PIO_PER |= BIT18;

//Enable PA18

p_pPio->PIO_OER |= BIT18;

//Configure in Output

p_pPio->PIO_SODR |= BIT18;

//set reg to 1

}

Ты можешь спросить: «Что это за указатели на непонятные структуры?» Так Atmel предлагает нам работать с периферией. Для каждого узла контроллера заведена структура, описывающая все регистры, которые относятся к этому узлу. Такой подход позволяет достаточно легко использовать один и тот же код с однотипными узлами процессора (например, с любым из трех каналов таймера или одним из двух блоков USART). Также это позволяет с минимум исправлений перетаскивать код с одного на другой контроллер. Все эти структуры и дефайны для работы с ними описаны в файлах AT91SAM7Sхх.h в зависимости от конкретного контроллера. Контроллеры at91sam7s64, at91sam7s128, at91sam7s256 и at91sam7s512 отличаются только объемами памяти, поэтому и файлы описания периферии совпадают.

Первая строчка (p_pPMC->PMC_PCER = 1 << AT91C_ID_PIOA) включает тактирование модуля ввода вывода. Именно он отвечает за все 32 ножки общего назначения. Контроллер очень гибко настраивается с точки зрения потребления, так почти любой неиспользуемый узел можно отключить. Изначально они все отключены (не забудь включить модуль PIO перед использованием).

Далее идет по три однотипных строчки для каждой из двух ног, на которых «висят» светодиоды. Первая строчка разрешает контроль ножки модулем в/в. Все ноги имеют дополнительные функции и могут быть «подключены» к другим модулям (таким как SPI, USART и т.д.). Поэтому, если мы хотим управлять ножкой через модуль PIO (так сказать «вручную»), то мы предварительно должны это разрешить записью в регистр PIO_PER (PIO Enable Register). Затем стоит назначить эту ножку, как выход, записью в регистр PIO_OER (Output Enable Register). Ну и, наконец, установить на выходе лог 1 с помощью регистра PIO_SODR (Set Output Data Register).

Те, кто уже успел скачать с сайта OLIMEX схему отладочной платы, обратили внимание, как подключен светодиод: анодом к источнику +3.3В, катодом через резистор к соответствующей ножке контроллера. Сразу же после настройки выводов контроллера, когда на них лог 1 (напряжение близкое к +3.3В), ток через светодиод не идет, и он не горит. Чтобы он загорелся, необходимо вывести на ногу контроллера лог 0 (напряжение близко к 0 В).

В основном цикле как раз и происходит попеременная установка – то лог 1, то лог 0 на выходах контроллера. Между установкой и сбросом введены небольшие паузы Delay(800000). Без них ты бы не увидел мигания светодиодов, так бы быстро это происходило. Функция задержки реализована достаточно просто и прямолинейно, обычным чиклом while с переменным числом итераций. Такой метод полностью занимает процессор на время задержки. Это бесполезное расходование процессорного времени, поэтому в следующей статье я покажу, как сделать «мигалку» вначале на прерываниях от таймеров, а потом и посредством простенькой операционной системы.

Теперь ты можешь попытаться изменить время задержек и откомпилировать проект заново. К сожалению, на выходе ты не получишь готового *.bin файла. Тебе придется сделать его самому при помощи утилиты hex2bin. Но предварительно нужно получить hex файл. Для этого лезем в настройки проекта (Project -> Options или <Alt+F7>). Там, в разделе Linker на закладке Extra Output, ставим галочку Generate Extra Output, Output format – Intel-extended, галочку Override default и вводим имя с расширением hex. Внимание: версия hex2bin, которая стоит у меня, плохо переваривает имена длиннее 8.3.

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

Если ты уже заглянул в файл с описаниями структур, то мог заметить, что многие регистры имеют по три «отражения» Enable (Set), Disable (Clear) и Status. При этом первые два регистра – только для записи, последний – для чтения. Для чего это сделано? Рассмотрим на примере регистров управления портом в/в PIO_SODR, PIO_CODR и PIO_ODSR. При записи в регистр PIO_SODR (Set Output Data Register) числа 3 ножки 0 и 1 примут состояние лог 1. Остальные ножки не изменят своего состояния. Каждый бит этого регистра отвечает за свою ножку. Если мы пишем в этот бит 1, то соответствующая ножка принимает состояние лог 1, если 0 – не меняет своего состояния. Если после этого записать число 2 в регистр PIO_CODR (Clear Output Data Register), то ножка 1 примет состояние лог 0, а ножка 0 останется в прежнем состоянии. Во время всех этих действий регистр PIO_ODSR (Output Data Status Register) отражает текущее состояние порта в/в.

На первый взгляд, это кажется излишним, ведь можно использовать один регистр и конструкции вроде PIO |= 3; и PIO &= ~2. Но такие операции не являются атомарными. Это значит, что, например, операция PIO |= 3 состоит из трех машинных команд: загрузить значение из регистра периферии в регистр общего назначения; логическая операция ИЛИ; загрузка значения обратно в регистр периферии. Если в процессе выполнения этих трех операций произойдет прерывание или переключение контекста в ОС, и участок кода, получивший управление, тоже захочет установить/сбросить биты регистра PIO, то после возврата управления, ранее записанные значения могут быть затерты последней машинной командой загрузки в регистр PIO. Именно поэтому для многих регистров введены такие вот «двойники». Обращаться к ним следует обычной операцией присвоения. Это гарантирует, что в какой бы момент не произошло прерывание или смена контекста, ни одно «воздействие» на регистр не будет пропущено.

Содержание
ttfb: 6.2470436096191 ms