Уже добрый десяток лет на рынке представлено множество микроконтроллеров на базе ядра ARM7TDMI. Это достаточно мощное ядро для однокристальных решений.
Он имеет разрядность 32 бита и рабочую частоту до 100 МГц; причём ядро однотактовое, т.е.
некоторые инструкции выполняются за 1 такт (в основном операции с регистрами, без обращения к внешним шинам процессора).
Ядро ARM7TDMI на голову превосходит все 8- и 16-битные чипы (AVR, MSC-51, PIC12/PIC16/PIC18/PIC24, MSP430 и т.д.) по вычислительным возможностям.
Однако сравнительно недавно ARM представила новое семейство ядер Cortex; нас будет интересовать его разновидность Cortex-M3, которая призвана именно заменить ARM7TDMI в нише однокристальных решений.
Мне посчастливилось работать с чипами NXP LPC1300, а точнее LPC1343, основанными на ядре Cortex-M3, сразу после их официального выпуска.
Сейчас им уже передана пара проектов.
И скажу вам, как бывалому ARM-программисту: они мне очень понравились, хотя у них есть свои особенности в архитектуре.
Итак, Cortex-M3 призван заменить ARM7TDMI. При его разработке компания ARM Ltd. ставила целью увеличить функциональность и добавить полезные инструкции без существенного усложнения логики схем процессора, тем самым увеличив плотность кода и производительность.
Из-за этого нам пришлось пойти на беспрецедентный шаг: ядро ARM впервые бинарно несовместимо с предыдущими семействами.
Собственно, это произошло по той причине, что Cortex-M3 не может выполнять 32-битный ARM-код. Все предыдущие ядра имели 2 режима работы и в каждом из них был свой набор команд. Эти режимы назывались ARM и Thumb. Первый работал с полным 32-битным набором команд, а второй — с упрощенным 16-битным набором команд. Фактически ядро всегда выполняло ARM-код, но в режиме Thumb был подключен некий декодер, который «на лету» «сопоставлял» 16-битные инструкции в их 32-битные аналоги.
Cortex-M3 отказался от 32-битного кода как класса.
В семействе Cortex есть еще несколько ядер (Cortex-M0, M1, A0-A3).
М3 расположен посередине.
М0, М1 еще более упрощены, а вот А-серия, наоборот, предназначена для тяжелых и высокопроизводительных приложений, и возможность выполнения ARM-кода там не убрали.
Массивность и низкая плотность кода — большая проблема ядер ARM; 32 бита на любую операцию дают о себе знать, плюс невозможно закодировать в инструкции константу больше 1 байта.
Именно из-за этого был введен дополнительный набор инструкций Thumb. Он обеспечивает большую плотность кода (средний выигрыш 20–30%), хотя и жертвует 5–10% производительности.
В Cortex была развита идея Thumb-кода.
Набор 16-битных команд Thumb был расширен, набор команд получил название Thumb-2. При компиляции в него падение производительности (по сравнению с чистым ARM-кодом) составляет всего несколько процентов, но экономия в объеме все та же 20-30%.
Особого внимания в наборе Thumb-2 заслуживают инструкции высокого уровня, такие как IT (дизайн с его использованием представлен ниже); в общем, система команд просто напичкана «фичами», призванными повысить оптимизацию при компиляции кода на языке C. Итак, конструкция на Thumb-2: CMP r0, r1
ITE EQ ; if (r0 == r1)
MOVEQ r0, r2 ; then r0 = r2;
MOVNE r0, r3 ; else r0 = r3;
Нечто подобное можно сделать и в наборе инструкций ARM: CMP r0, r1 ; if (r0 == r1)
MOVEQ r0, r2 ; then r0 = r2;
MOVNE r0, r3 ; else r0 = r3;
А вот в чистом Thumb придется немного «извратиться»: CMP r0, r1 ; if (r0 == r1)
BNE .
else MOV r0, r2 ; then r0 = r2; B .
endif .
else: MOV r0, r3 ; else r0 = r3; .
endif
Хотя если посчитать объёмы, то получим, что в случае с Thumb конструкция будет занимать 2*5=10 байт, на Thumb-2 объём будет 2*4=8 байт, на ARM целых 4*3= 12 байт (хотя в нем всего 3 инструкции).
Однако эта хваленая ИТ-инструкция компилятору Keil RealView MDK, видимо, неизвестна, так как при изучении сгенерированных листингов она не обнаружена, а визуально вывод ассемблерного кода компилятором все же больше похож на обычный Thumb. Либо сам исходный код специфичен, либо компилятор еще не «доработан» под новое ядро и систему команд. К сожалению, у меня нет информации о других компиляторах, хотя было бы неплохо посмотреть, что генерирует GCC. А вообще рекламируется просто сумасшедшая оптимизация кода, якобы конечный размер будет на 30-50% меньше, чем у того же исходника, скомпилированного под 8 и даже 16-битный микроконтроллер (например, в документе, представленном по первой ссылке по адресу конец статьи).
Скажу сразу: это несколько подтасованный результат, он справедлив только для 32-битного кода, т.е.
C-кода с обилием операций с переменными int и long, а также большим количеством вычислений (например, знаменитый тест Драйстоуна хорошо соответствует этим требованиям).
Если перенести ранее написанный и оптимизированный под 8 бит код, то при переносе на 32-битный процессор произойдет, наоборот, увеличение размера двоичного кода; по моему опыту код увеличивается в размерах почти в 1,5-2 раза.
Еще одним важным нововведением в Cortex-M3 стало добавление команды дивизии.
В ядрах ARM с древних времен предусмотрены операции умножения (с 64-битным результатом) и умножения с накоплением (также 64-битного результата).
Теперь к ним добавилась инструкция деления.
Конечно, она, скорее всего, потребляет много тактов, однако все равно намного быстрее, чем отдельная подпрограмма.
Как бы парадоксально это ни казалось профессионалам высокого уровня и людям, далеким от микроконтроллеров: аппаратное деление в однокристальных системах все еще редкость (о различных наборах инструкций плавающей арифметики и других сопроцессорах говорить не приходится, они доступны только в самых тяжелых монстрах, предназначенных для мультимедиа).
В отличие от ARM7TDMI, Cortex имеет гарвардскую архитектуру памяти (отдельные шины команд и данных).
В AVR, например, это вызывает определенные неудобства, и при программировании следует использовать некоторые макросы компилятора и специальные функции, чтобы константные переменные не попадали в оперативную память.
Здесь (собственно так было во всех ARM после ARMv4, таких как ARM9, ARM11) при программировании отдельные шины не ощущаются; внутри чипа они по-прежнему объединены в единое адресное пространство.
Все чипы ARM имеют 32-битное линейное адресное пространство размером 4 ГБ (для x86-программистов это соответствует модели плоской памяти), которое содержит все адреса периферийных устройств, ПЗУ и ОЗУ.
Примечание(1): Несмотря на все преимущества, именно огромное адресное пространство является существенной проблемой при оптимизации кода: у нас 32-битная адресация, в инструкциях ARM/Thumb и даже Thumb-2 невозможно напрямую закодировать полный адрес определенного объекта.
, поэтому адрес помещается как данные внутри кода, а затем получает его с помощью отдельных инструкций.
Это также отрицательно влияет на объем кода.
Например, в MSC-51 для чтения переменной из ОЗУ может хватить 2 байта, но в ARM придется хранить как минимум 2 байта самой инструкции и 4 байта будут использоваться непосредственно для хранения адреса.
Заметка 2): Мне всегда хотелось попробовать поместить код (например, инструкцию возврата) в регистр периферии и передать ему управление, наблюдая за реакцией ядра.
На ARM7TDMI этот трюк может сработать из-за фон-неймановской организации памяти, но Кортекс со своим Гарвардом почти наверняка отправит его за тридевять земель, попав в один из абортов.
Следующее существенное отличие: один стек.
Если в ARM7TDMI для разных режимов ядра были выделены отдельные стеки (речь идет не о ARM/Thumb, а о режимах, в которые процессор переключается при входе в прерывания и при обработке исключений), то здесь стек только один.
Не знаю, как к этому относиться, в теории это менее гибко, но на практике чертовски удобно.
Экономится оперативная память, так как нет необходимости резервировать кучу стеков, упрощается логика вложенных прерываний и реализация системных вызовов (попробуйте на ARM7TDMI сделать системный вызов через программное прерывание SWI с более чем 4 параметрами, например потребуется сад, это тоже сад, но попроще).
Кроме того, за счет этого были уменьшены задержки входа и выхода из прерываний, а также переключения между прерываниями.
Вторым изменением, позволившим ускорить обработку прерываний, стал отказ от VIC. Да, монстра под названием VIC (Vector Interrupt Controller) больше нет. Да, это снова шаг от гибкости к простоте, но в микроконтроллерной системе случай, когда нужно на лету переназначать обработчики прерываний, встречается редко; для этого проще написать свой велосипед, чем возиться с настройкой ВИКа в каждом проекте.
Более того, можно разместить таблицу прерываний в оперативной памяти и легко менять в ней адреса обработчиков.
Вместо VIC у нас теперь NVIC и куча векторов прерываний в начале FLASH. Если в ARM7TDMI векторы прерываний вначале занимали 32 байта, то здесь под прерывания от различных устройств выделяется несколько сотен байт. Более того, теперь это не инструкции перехода, а настоящие векторы с адресами.
Те.
Ядро не передает управление по адресу таблице прерываний, а выбирает адрес по нужному смещению и передает управление по нему; с точки зрения программиста это удобнее, красивее и прозрачнее.
Но главный сюрприз — это первые 2 вектора прерывания.
Думаете Ресет и что-то еще? НЕТ! По адресу 0 лежит. значение стека, оно вводится ядром в регистр стека аппаратно при сбросе.
А по смещению 4 — адрес точки входа.
Что это дает? А вот что: мы можем сразу приступить к выполнению программы из Си-кода без предварительной инициализации.
Конечно, в этом случае вам придется вручную копировать раздел RW в ОЗУ и сбрасывать ZI (если вы полностью откажетесь от помощи компилятора).
Эта четкая С-ориентация заметна и на примерах проектов для Cortex. Все инициализации перенесены с ассемблера на C. Из-за исключения многих стеков стало ненужно инициализировать их в самом начале.
В то же время другие инициализации перешли в код C. Еще одно интересное отличие - в системе команд: добавлены высокоуровневые инструкции WFI (ожидание прерывания), WFE (ожидание события) и другие, упрощающие создание многопоточных приложений и операционных систем.
В наборе содержатся инструкции, предназначенные для многопроцессорных систем, что позволяет предположить, что в ближайшее время могут появиться многоядерные однокристальные решения.
Примечание: Хотя многоядерные микроконтроллеры существуют в виде того же Parallax Propeller (он имеет целых 8 32-битных ядер), его нельзя назвать полноценным и пригодным для коммерческого использования (а не для любительских поделок).
Также в описание ядра Cortex-M3 добавлен 1 таймер.
Таймер простой, он может генерировать прерывание с определенной частотой, однако, например, для ядра операционной системы больше ничего и не требуется.
Примечание: Таймер в описании ядра — очень полезная и важная вещь.
Поскольку он описан в документации ядра и фактически является частью лицензионного ядра, все производители будут включать его в свои чипы и, что самое главное, у всех будет одинаковая реализация.
Это очень полезно для совместимости кода: не нужно писать модули поддержки кучи реализаций таймеров от разных производителей (как в случае с ARM7TDMI).
Однако каждый производитель все равно реализует дополнительные таймеры по-своему, но даже один стандартный — хороший шаг к универсальности.
В заключение стоит сказать, что в документации ядра описан и модуль MPU (Memory Protection Unit).
Очень полезная вещь в сложных устройствах, когда запущено несколько потоков и очень не хочется, чтобы вся прошивка сорвалась из-за сбоя в каком-то отдельном потоке.
Однако этот модуль является необязательным и производители чипов не спешат его интегрировать.
Его нет даже у старшего семейства NXP LPC1700. У других производителей подобного не замечено.
Тем не менее, защита памяти, не говоря уже о виртуальной памяти, по-прежнему остается уделом дорогих и больших монстров.
Ссылки по теме:
- Сравнение и преимущество Cortex перед 8/16-битными чипами
- Официальная документация ядра Cortex-M3
- Официальная документация ядра ARM7TDMI-S.
- Общая статья о ARM в Wiki
- Разработка программного обеспечения процессора ARM Cortex-M3 для программистов процессоров ARM7TDMI
-
Любопытство С Высоты Птичьего Полета
19 Oct, 24 -
Ip-Телефоны Akuvox. Обзор Бюджетных Моделей
19 Oct, 24