Уже добрый десяток лет на рынке представлено множество микроконтроллеров на ядре ARM7TDMI. Это довольно мощное ядро для однокристальных решений. Оно имеет разрядность 32 бита и частоту работы до 100МГц, мало того, ядро однотактовое, т.е. некоторые инструкции исполняются за 1 такт (преимущественно операции с регистрами, без обращений к внешним шинам процессора). Ядро ARM7TDMI на голову превосходит по вычислительным возможностям все 8-ми и 16-ти битные чипы (AVR, MSC-51, PIC12/PIC16/PIC18/PIC24, MSP430, etc).
Однако, относительно недавно, компания 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-битным полным набором инструкций, а 2ой с упрощенным набором 16-битных инструкций. На самом деле ядро всегда исполняло ARM-код, однако в Thumb-режиме подключался некий дешифратор, который налету «мапил» 16 битные инструкции в их 32-битные аналоги.
В Cortex-M3 отказались от 32-битного кода как класса. В семействе Cortex присутствуют еще несколько ядер (Cortex-M0,M1,A0-A3). M3 располагается посередине. M0,M1 — еще сильнее упрощены, а вот A-серия наоборот предназначена для тяжелых и высокопроизводительных приложений, и нее возможность исполнения ARM-кода убирать не стали.
Массивность и низкая плотность кода — большая проблема ARM-ядер, 32 бита на любую операцию дают о себе знать, плюс невозможно закодировать в инструкции константу более 1 байта. Именно из-за этого и введен дополнительный набор инструкций Thumb. Он обеспечивает большую плотность кода (в среднем выигрыш 20-30%), хоть и принося в жертву 5-10% производительности.
В Cortex идея Thumb кода была развита. Набор 16-битных инструкций Thumb был расширен, набор инструкций обозвали Thumb-2. При компиляции в него падение производительности (по сравнению с чистым ARM-кодом) составляет лишь единицы процентов, а вот экономия по объему все те же 20-30%.
Отдельного внимания в наборе Thumb-2 заслуживают такие высокоуровневые инструкции как IT (конструкция с ее применением представлена ниже), вообще, система команд просто напичкана «фичами», призванными повысить оптимизацию при компиляции Си-кода. Итак, конструкция на Thumb-2:
Нечто похожее, можно сделать и в наборе инструкций ARM:
А в чистом Thumb придется несколько «извратиться»:
Хотя если посчитать объемы, то получим, что в случае Thumb конструкция займет 2*5 = 10 байт, на Thumb-2 объем будет 2*4 = 8 байт, на ARM целых 4*3 = 12 байт (хоть и имеет всего 3 инструкции).
Однако, компилятору Keil RealView MDK именно эта хваленая инструкция IT, видимо, неизвестна, так как при изучении генерируемых листингов найдена не была, да и визуально ассемблерный код на выходе из компилятора все-таки больше похож на обычный Thumb. Толи сами исходники специфичные, то ли компилятор на самом деле пока «не допилили» под новое ядро и систему команд. На счет других компиляторов информацией, к сожалению, не обладаю, хотя не плохо было бы посмотреть что генерирует GCC.
Вообще, рекламируется просто бешеная оптимизация кода, якобы итоговый размер будет на 30-50% меньше чем у того же самого исходника, скомпилированного под 8 и даже 16-битный микроконтроллер (к примеру, в документе представленном по первой ссылке в конце статьи). Скажу сразу: это несколько подтасованные результат, он верен только для 32-битного кода, т.е. кода на Си с обилием операций с переменными int, long, а так же большим количеством вычислений (под данные требования хорошо подходит, к примеру, знаменитый тест Dhrystone). Если же переносить код предварительно писавшийся и оптимизированный под 8 бит, то при переносе на 32-битный процессор будет наоборот увеличение размера бинарного кода, по моему опыту код увеличивается по объему чуть ли не в 1.5-2 раза.
Еще одним существенным нововведением в Cortex-M3 явилось добавление команды деления. ARM-ядра с древних времен имеют в своем составе операции умножения (с 64 битным результатом) и умножения с накоплением (так же 64 битный результат). Теперь же к ним добавилась инструкция деления. Конечно, тактов она скорее всего отжирает немало, однако, все-равно это намного быстрее чем отдельная подпрограмма. Как бы это не казалось парадоксальным высокоуровникам и людям далеким от микроконтроллеров: аппаратное деление до сих пор редкость в однокристальных системах (про различные наборы инструкций плавающей арифметики и прочие сопроцессоры и вовсе говорить не приходится, они доступны только в самых тяжелых монстрах, заточенных под мультимедиа).
В отличие от ARM7TDMI у Cortex Гарвардская архитектура памяти (раздельные шины команд и данных). В том же AVR это доставляет определенные неудобства и при программировании следует пользоваться некоторыми макросами компилятора и специфичными функциями, чтобы const-переменные не попадали в ОЗУ. Здесь же (так собственно было во всех ARM после ARMv4, таких как ARM9, ARM11) при программировании отдельные шины не ощущаются, внутри кристалла они все-равно объединяются в единое адресное пространство. Все чипы ARM имеют 32-битное линейное адресное пространство размером в 4Гб (для программистов x86, это соответствует модели памяти flat), в нем размешаются все адреса периферии, ПЗУ и ОЗУ.
Примечание (1): Несмотря на все преимущества, именно огромное адресное пространство является существенной бедой при оптимизации кода: мы имеет 32-битную адресацию, в инструкциях ARM/Thumb и даже Thumb-2 нельзя непосредственно закодировать полный адрес некоего объекта, поэтому адрес кладется в виде данных внутрь кода, а затем достается отдельной инструкцией. Это так же отрицательно сказывается на объеме кода. К примеру, в MSC-51 для чтения переменной из ОЗУ может хватить 2х байт, в ARM же придется хранить минимум 2 байта самой инструкции и 4 байта непосредственно задействовать под хранение адреса.
Примечание (2): всегда хотел попробовать разместить в периферийном регистре код (к примеру, инструкцию возврата) и передать ему управление, пронаблюдав за реакцией ядра. На ARM7TDMI этот трюк может прокатить из-за Фон-Неймановской организации памяти, а вот Cortex с его Гарвардом почти наверняка пошлет в далекие края, свалившись в один из абортов.
Следующее существенное отличие: один стек. Если в ARM7TDMI для разных режимов ядра (речь не о ARM/Thumb, а о режимах в которые процессор переключается при входе в прерывания и для обработки исключений) выделялись отдельные стеки, то здесь имеется только один стек. Не знаю как к этому относиться, в теории это менее гибко, но на практике чертовски удобно. Экономится ОЗУ так как не надо резервировать кучу стеков, упрощается логика вложенных прерываний и реализации системных вызовов (попробуйте на ARM7TDMI сделать системный вызов через программное прерывание SWI с числом параметров более 4х, такой огород потребуется, тут тоже огород, но проще). К тому же, за счет этого были уменьшены задержки входа и выхода из прерываний, а так же переключения между прерываниями.
Второе изменение, позволившее ускорить обработку прерываний — это отказ от VIC. Да, нет больше монстра под названием VIC (векторный контроллер прерываний). Да, это опять шаг от гибкости к простоте, но в микроконтроллерной системе случай, когда надо налету переназначать обработчики прерываний редкость, проще написать свой велосипед для этого, чем в каждом проекте заниматься настройкой VIC-а. Тем более есть возможность разместить таблицу прерываний в ОЗУ и уже в нем спокойно менять адреса обработчиков.
Вместо VIC теперь имеем NVIC и кучу векторов прерываний в начале FLASH. Если в ARM7TDMI вектора прерываний занимали 32 байта в начале, то здесь несколько сотен байт отведено на прерывания от различных устройств. Мало того, теперь это не инструкции прыжков, а реальные вектора с адресами. Т.е. ядро не передает управление по адресу в таблицу прерываний, а делает выборку адреса по нужному смещению и передает по нему управление, с позиции программиста это удобнее, красивее и прозрачнее.
Но основной сюрприз — это первые 2 вектора прерываний. Думаете Reset и еще что-то? НЕТ! По 0-му адресу лежит… значение стека, оно аппаратно заносится ядром в регистр стека при сбросе. А по смещению 4 — адрес точки входа. Что же это дает? А вот что: мы можем сразу начать исполнение программы с Си-кода без предварительных инициализаций. Конечно, в этом случае придется вручную скопировать в ОЗУ секцию RW и обнулить ZI (если совсем отказаться от помощи компилятора).
Такая явная Си-ориентированность заметна и по примерам проектов для Cortex. Все инициализации перенесены из ассемблера в Си. Из-за отказа от множества стеков стало ненужным инициализировать их в самом начале. Заодно и прочие инициализации перекочевали в Си-код.
Еще интересны отличия в системе команд: добавлены высокоуровневые инструкции WFI (wait for interrupt), WFE (wait for event) и прочие, упрощающие создание многопоточных приложений и операционных систем. В наборе представлены инструкции, предназначенные для многопроцессорных систем, это наводит на мысль, что скоро может произойти выход в свет многоядерных однокристальных решений.
Примечание: Хоть многоядерные микроконтроллеры и существуют в виде того же Parallax Propeller (он имеет аж 8 32-битных ядер), но полноценным и годным в коммерческом применении (а не для любительских поделок), его назвать нельзя.
Так же в описании ядра Cortex-M3 добавлен 1 таймер. Таймер простой, умеет генерировать прерывание с определенной периодичностью, однако, к примеру, для ядра операционной системы большего и не требуется.
Примечание: таймер в описании ядра — очень полезная и важная вещь. Так как он описан в документации на ядро и фактически является частью лицензируемого ядра, все производители будут вносить его в свои чипы, а главное у всех он будет иметь одинаковую реализацию. Это очень полезно для совместимости кода: не требуется писать модули поддержки для кучи реализаций таймеров у различных производителей (как это обстоит с ARM7TDMI). Однако, дополнительные таймеры, каждый производитель все-равно будет реализовывать на свой лад, но уже бы один стандартный — яляется хорошим шагом в сторону универсальности.
В заключение стоит сказать, что в документации на ядро так же описан модуль MPU (Memory Protection Unit). Очень полезная вещь в сложных устройствах, когда работает несколько потоков и очень не хочется нарушения работы всей микропрограммы из-за сбоя в каком-либо отдельном потоке. Однако, данный модуль является опциональным и производители чипов встраивать его не спешат. Даже в старшем семействе NXP LPC1700 он отсутствует. У прочих производителей так же замечен не был. Все-таки защита памяти, не говоря о виртуальной памяти, пока остается уделом дорогих и больших монстров.
Ссылки по теме:
Однако, относительно недавно, компания 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-битным полным набором инструкций, а 2ой с упрощенным набором 16-битных инструкций. На самом деле ядро всегда исполняло ARM-код, однако в Thumb-режиме подключался некий дешифратор, который налету «мапил» 16 битные инструкции в их 32-битные аналоги.
В Cortex-M3 отказались от 32-битного кода как класса. В семействе Cortex присутствуют еще несколько ядер (Cortex-M0,M1,A0-A3). M3 располагается посередине. M0,M1 — еще сильнее упрощены, а вот A-серия наоборот предназначена для тяжелых и высокопроизводительных приложений, и нее возможность исполнения ARM-кода убирать не стали.
Массивность и низкая плотность кода — большая проблема ARM-ядер, 32 бита на любую операцию дают о себе знать, плюс невозможно закодировать в инструкции константу более 1 байта. Именно из-за этого и введен дополнительный набор инструкций Thumb. Он обеспечивает большую плотность кода (в среднем выигрыш 20-30%), хоть и принося в жертву 5-10% производительности.
В Cortex идея Thumb кода была развита. Набор 16-битных инструкций Thumb был расширен, набор инструкций обозвали Thumb-2. При компиляции в него падение производительности (по сравнению с чистым ARM-кодом) составляет лишь единицы процентов, а вот экономия по объему все те же 20-30%.
Отдельного внимания в наборе Thumb-2 заслуживают такие высокоуровневые инструкции как IT (конструкция с ее применением представлена ниже), вообще, система команд просто напичкана «фичами», призванными повысить оптимизацию при компиляции Си-кода. Итак, конструкция на 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 именно эта хваленая инструкция IT, видимо, неизвестна, так как при изучении генерируемых листингов найдена не была, да и визуально ассемблерный код на выходе из компилятора все-таки больше похож на обычный Thumb. Толи сами исходники специфичные, то ли компилятор на самом деле пока «не допилили» под новое ядро и систему команд. На счет других компиляторов информацией, к сожалению, не обладаю, хотя не плохо было бы посмотреть что генерирует GCC.
Вообще, рекламируется просто бешеная оптимизация кода, якобы итоговый размер будет на 30-50% меньше чем у того же самого исходника, скомпилированного под 8 и даже 16-битный микроконтроллер (к примеру, в документе представленном по первой ссылке в конце статьи). Скажу сразу: это несколько подтасованные результат, он верен только для 32-битного кода, т.е. кода на Си с обилием операций с переменными int, long, а так же большим количеством вычислений (под данные требования хорошо подходит, к примеру, знаменитый тест Dhrystone). Если же переносить код предварительно писавшийся и оптимизированный под 8 бит, то при переносе на 32-битный процессор будет наоборот увеличение размера бинарного кода, по моему опыту код увеличивается по объему чуть ли не в 1.5-2 раза.
Еще одним существенным нововведением в Cortex-M3 явилось добавление команды деления. ARM-ядра с древних времен имеют в своем составе операции умножения (с 64 битным результатом) и умножения с накоплением (так же 64 битный результат). Теперь же к ним добавилась инструкция деления. Конечно, тактов она скорее всего отжирает немало, однако, все-равно это намного быстрее чем отдельная подпрограмма. Как бы это не казалось парадоксальным высокоуровникам и людям далеким от микроконтроллеров: аппаратное деление до сих пор редкость в однокристальных системах (про различные наборы инструкций плавающей арифметики и прочие сопроцессоры и вовсе говорить не приходится, они доступны только в самых тяжелых монстрах, заточенных под мультимедиа).
В отличие от ARM7TDMI у Cortex Гарвардская архитектура памяти (раздельные шины команд и данных). В том же AVR это доставляет определенные неудобства и при программировании следует пользоваться некоторыми макросами компилятора и специфичными функциями, чтобы const-переменные не попадали в ОЗУ. Здесь же (так собственно было во всех ARM после ARMv4, таких как ARM9, ARM11) при программировании отдельные шины не ощущаются, внутри кристалла они все-равно объединяются в единое адресное пространство. Все чипы ARM имеют 32-битное линейное адресное пространство размером в 4Гб (для программистов x86, это соответствует модели памяти flat), в нем размешаются все адреса периферии, ПЗУ и ОЗУ.
Примечание (1): Несмотря на все преимущества, именно огромное адресное пространство является существенной бедой при оптимизации кода: мы имеет 32-битную адресацию, в инструкциях ARM/Thumb и даже Thumb-2 нельзя непосредственно закодировать полный адрес некоего объекта, поэтому адрес кладется в виде данных внутрь кода, а затем достается отдельной инструкцией. Это так же отрицательно сказывается на объеме кода. К примеру, в MSC-51 для чтения переменной из ОЗУ может хватить 2х байт, в ARM же придется хранить минимум 2 байта самой инструкции и 4 байта непосредственно задействовать под хранение адреса.
Примечание (2): всегда хотел попробовать разместить в периферийном регистре код (к примеру, инструкцию возврата) и передать ему управление, пронаблюдав за реакцией ядра. На ARM7TDMI этот трюк может прокатить из-за Фон-Неймановской организации памяти, а вот Cortex с его Гарвардом почти наверняка пошлет в далекие края, свалившись в один из абортов.
Следующее существенное отличие: один стек. Если в ARM7TDMI для разных режимов ядра (речь не о ARM/Thumb, а о режимах в которые процессор переключается при входе в прерывания и для обработки исключений) выделялись отдельные стеки, то здесь имеется только один стек. Не знаю как к этому относиться, в теории это менее гибко, но на практике чертовски удобно. Экономится ОЗУ так как не надо резервировать кучу стеков, упрощается логика вложенных прерываний и реализации системных вызовов (попробуйте на ARM7TDMI сделать системный вызов через программное прерывание SWI с числом параметров более 4х, такой огород потребуется, тут тоже огород, но проще). К тому же, за счет этого были уменьшены задержки входа и выхода из прерываний, а так же переключения между прерываниями.
Второе изменение, позволившее ускорить обработку прерываний — это отказ от VIC. Да, нет больше монстра под названием VIC (векторный контроллер прерываний). Да, это опять шаг от гибкости к простоте, но в микроконтроллерной системе случай, когда надо налету переназначать обработчики прерываний редкость, проще написать свой велосипед для этого, чем в каждом проекте заниматься настройкой VIC-а. Тем более есть возможность разместить таблицу прерываний в ОЗУ и уже в нем спокойно менять адреса обработчиков.
Вместо VIC теперь имеем NVIC и кучу векторов прерываний в начале FLASH. Если в ARM7TDMI вектора прерываний занимали 32 байта в начале, то здесь несколько сотен байт отведено на прерывания от различных устройств. Мало того, теперь это не инструкции прыжков, а реальные вектора с адресами. Т.е. ядро не передает управление по адресу в таблицу прерываний, а делает выборку адреса по нужному смещению и передает по нему управление, с позиции программиста это удобнее, красивее и прозрачнее.
Но основной сюрприз — это первые 2 вектора прерываний. Думаете Reset и еще что-то? НЕТ! По 0-му адресу лежит… значение стека, оно аппаратно заносится ядром в регистр стека при сбросе. А по смещению 4 — адрес точки входа. Что же это дает? А вот что: мы можем сразу начать исполнение программы с Си-кода без предварительных инициализаций. Конечно, в этом случае придется вручную скопировать в ОЗУ секцию RW и обнулить ZI (если совсем отказаться от помощи компилятора).
Такая явная Си-ориентированность заметна и по примерам проектов для Cortex. Все инициализации перенесены из ассемблера в Си. Из-за отказа от множества стеков стало ненужным инициализировать их в самом начале. Заодно и прочие инициализации перекочевали в Си-код.
Еще интересны отличия в системе команд: добавлены высокоуровневые инструкции WFI (wait for interrupt), WFE (wait for event) и прочие, упрощающие создание многопоточных приложений и операционных систем. В наборе представлены инструкции, предназначенные для многопроцессорных систем, это наводит на мысль, что скоро может произойти выход в свет многоядерных однокристальных решений.
Примечание: Хоть многоядерные микроконтроллеры и существуют в виде того же Parallax Propeller (он имеет аж 8 32-битных ядер), но полноценным и годным в коммерческом применении (а не для любительских поделок), его назвать нельзя.
Так же в описании ядра Cortex-M3 добавлен 1 таймер. Таймер простой, умеет генерировать прерывание с определенной периодичностью, однако, к примеру, для ядра операционной системы большего и не требуется.
Примечание: таймер в описании ядра — очень полезная и важная вещь. Так как он описан в документации на ядро и фактически является частью лицензируемого ядра, все производители будут вносить его в свои чипы, а главное у всех он будет иметь одинаковую реализацию. Это очень полезно для совместимости кода: не требуется писать модули поддержки для кучи реализаций таймеров у различных производителей (как это обстоит с ARM7TDMI). Однако, дополнительные таймеры, каждый производитель все-равно будет реализовывать на свой лад, но уже бы один стандартный — яляется хорошим шагом в сторону универсальности.
В заключение стоит сказать, что в документации на ядро так же описан модуль MPU (Memory Protection Unit). Очень полезная вещь в сложных устройствах, когда работает несколько потоков и очень не хочется нарушения работы всей микропрограммы из-за сбоя в каком-либо отдельном потоке. Однако, данный модуль является опциональным и производители чипов встраивать его не спешат. Даже в старшем семействе NXP LPC1700 он отсутствует. У прочих производителей так же замечен не был. Все-таки защита памяти, не говоря о виртуальной памяти, пока остается уделом дорогих и больших монстров.
Ссылки по теме: