Я достаточно много пишу о микроконтроллерах, и о схемотехнике, и о программировании. Но вот вопросам средств разработки уделяю очень мало внимания. Да, указывается, например, какой компилятор использовался, но не объясняется, почему именно он. И не говорится, что он еще умеет, какие у него плюсы и минусы. Пришло время начать исправлять это упущение.
Итак, сегодня речь пойдет о компиляторах, а по факту, об одном компиляторе, но в двух вариантах, языка C для 8-битных микроконтроллеров Microchip PIC. Они поддерживают все 8-битные семейства, включая PIC18. Речь пойдет о компиляторах CC5X и CC8E разработанных "B Knudsen Data", микроскопической норвежской компанией.
Сразу хочу сказать, что данная статья ни в коей мере не является рекламной, а я не имею никакого отношения к этой компании и ее продуктам. В статье речь будет идти исключительно о технических подробностях использования компиляторов.
Эти компиляторы не самые известные и, по этой причине, не самые популярные. Но они выделяются на общем фоне качеством генерируемого кода и своей ориентированностью именно на разработку программ для микроконтроллеров. Они не стремятся соответствовать последним версия стандартов универсального языка С, более того, имеют множество ограничений, Но при этом имеют и множество удобных расширений. И не стремятся оградить программиста от всех аппаратных и архитектурных тонкостей (и сложностей) микроконтроллеров.
Да, я знаю, что микроконтроллеры PIC не пользуются особой любовью среди любителей, да и далеко не все профессионалы от них в восторге. Да и особенности CC5X и CC8E понравятся далеко не всем программистам. Но мне нравятся и PIC и эти два компилятора.
Сколько все это стоит и где взять?
Оба компилятора имеют бесплатные версии которые, как это часто бывает, называют "ознакомительными". Эти версии имеют некоторые ограничения в функциональности, о которых речь пойдет чуть позже, но не имеют ограничений по использованию для разработки коммерческих программ. Вот прямая цитата с сайта разработчика
Restrictions: The free edition can be used to generate code for all prototype, commercial and non-commercial systems without restrictions. Permission is required to distribute the FREE edition. Making any changes to the compiler is strongly prohibited.
CC5X, компилятор для PIC10, PIC12, PIC16
Максимальный размер кода 32 к слов (по 12 или 14 бит). Этого достаточно, что бы в подавляющем большинстве случаев вообще не замечать ограничения. Во всяком случае, для любительского применения.
Целые переменные могут быть знаковыми и беззнаковыми, но разрядностью только 1, 8 или 16 бит. Коммерческая версия поддерживает еще 24 и 32 разрядные переменные. Переменные с плавающей запятой поддерживаются, но только размерностью 24 бита.
Естественно, не поддерживается полная оптимизация. Но и без нее код получается весьма компактным, меньшим, чем генерируют другие компиляторы. По большому счету, отсутствует лишь оптимизация переключения банков и страниц памяти.
Компилятор работает под Windows, но прекрасно чувствует себя под Wine в Linux. Не имеет IDE, но может подключаться к MPLAB X IDE от Microchip. Правда в Linux такое подключение нормально не работает, но кого в Linux может напугать командная строка?
Скачать компилятор и документацию можно с сайта разработчика CC5X Free Edition. Какой либо разницы между "профессиональными пользователями" и "студентами и непрофессионалами" нет, можете выбирать, что вам больше нравится. Скорее всего, это используется лишь для сбора статистики интересующихся.
CC8E, компилятор для PIC18
Максимальный размер кода 128 кБ. Этого достаточно, что бы в подавляющем большинстве случаев вообще не замечать ограничения. Во всяком случае, для любительского применения.
Все остальные ограничения точно такие же, как и для CC5X. Поэтому я не буду их повторять.
Скачать компилятор и документацию можно с сайта разработчика CC8E Free Edition.
IDE нет, а что тогда вообще есть?
Да, IDE нет. Даже в коммерческой версии. Но к MPLAB X IDE этот компилятор подключается, но всяком случае, под Windows. Да и "комплекта поставки", как такового, нет. Вы скачиваете собственно компилятор (или в виде архива, или в виде программы установки) и документацию. И на этом все.
Нет никаких высокоуровневых библиотек. Минимальный набор заголовочных файлов. Но есть поддержка математических функций, в виде подключаемого файла. Для целых переменных эта поддержка ограничивается умножением и делением. А вот для плавающей запятой реализованы квадратный корень, логарифмы, экспоненты, синусы, косинусы.
Разумеется, есть довольно богатый набор заголовочных файлов с описанием различных моделей микроконтроллеров и скрпитов компоновки (память в PIC может иметь весьма нетривиальную структуру).
Весьма аскетичный набор... Но в нем есть все необходимое.
Наиболее интересные и значимые расширения языка
Поскольку компиляторы во многом схожи, я не буду рассматривать их по отдельности. Конкретные особенности вы всегда сможете найти в документации.
Прямой доступ к отдельным битам переменных
В стандартном С можно описать переменную как объединение (union), например, так
Это даст возможность работать с переменной как единым целым
ODR.reg=0x15;
или с отдельными битами, описанными как битовые поля
ODR.bit2=0;
В различных статья я постоянно использую именно такой метод описания регистров оборудования. Однако, компиляторы CC5X и CC8E предлагают интересную альтернативу. Предположим, что у нас есть такое определение беззнаковой переменной разрядностью 16 бит
uns16 var;
Тогда, без каких либо дополнительных описание мы можем работать с отдельными битами
var.2=0; var.12=1;
То есть, просто указывая номер бита. Причем присваивать можно не обязательно константу, но и переменную. Более того, можно получить доступ к различным частям переменной.
var.low8=0x15; var.high8=18;
Присвоит младшему байту нашей переменной 0x15, а старшему 18. И при работе с отдельными битами, и при работе с отдельными частями переменной, компилятор автоматически сформирует все необходимы команды сдвигов и маскирований. Что, впрочем, совершенно естественно.
Возможность обратиться к любому биту доступна для любых переменных, а не только для целых.
Нельзя сказать, что эта возможность является жизненно необходимой, но она удобна в программах активно работающих с регистрами оборудования. И в других компиляторах не встречается.
Битовые переменные
Программы активно работающие напрямую с регистрами оборудования, да и вообще низкоуровневые программы, часто используют различные флаги. К сожалению, в C с такими переменными есть проблемы. Даже логический тип данных появился далеко не сразу, да и его размерность значительно превышает 1 бит.
CC5X и CC8E поддерживают тип данных размерностью 1 бит. Не в виде битового поля структуры, а как самостоятельную переменную.
bit bit_var, bit_var1;
uns8 var;
var=0;
if (bit_var1) var.4=bit_var1;
Компилятор сам упакует все битовые переменные с набор байт в памяти данных и вставит необходимые команды для работы с ними. Это позволяет значительно экономить память данных, объем которой зачастую довольно мал.
Правда у битовых переменных есть одно, но существенное ограничение. Нельзя получить их адрес.
Указание адреса размещения переменной
С одной стороны, возможность в явном виде указать адрес размещения переменной есть во многих компиляторах. Но с другой, CC5X и CC8E идут несколько дальше.
uns8 PORTA @ 0x80;
bit some_var @ PORTA.3;
bit tmp_var;
PORTA=0xAA;
tmp_var=some_var;
. . .
some_var=tmp_var;
Как видно, мы можем не только указать адрес размещения переменной, как это сделано для PORTA, но и задать размещение битовой переменной как бита по заданному адресу. Да, это можно сделать и с помощью #define. Но согласитесь, что предлагаемый метод очень элегантен и удобен.
Двоичные константы (литералы) с разделителями
Двоичные константы не всегда были частью языка С, но поддерживались многими компиляторами.
uns8 var;
var=0b00011100;
Двоичные константы бывают довольно длинными, что приводит к проблемам с наглядностью. CC5X и CC8E позволяют использовать точку в качестве разделителя отдельных частей константы (литерала). Эта точка может использоваться для наглядности, компилятор ее просто игнорирует.
uns16 var;
var=0b000.11101.00.110.110;
Согласитесь, такая запись гораздо нагляднее, да и ошибиться труднее.
Специфические низкоуровневые особенности
CC5X и CC8E не пытаются скрыть от программиста низкоуровневые архитектурные особенности микроконтроллеров. Это может показаться непривычным и неудобным тем, кто раньше писал на С только чисто прикладные программы. Но это позволяет программисту, даже не позволяет, а требует, внимательно относиться с размещению кода и данных, например.
Структура памяти PIC подробно описана мной в статье
Организация памяти 8 битных микроконтроллеров Microchip PIC
здесь я не буду углубляться в эти тонкости.
Банковая организация памяти данных
Любая переменная, которую программист определяет в программе, по умолчанию размещается компиляторами в нулевом банке или разделяемой области памяти. Разумеется, размер этих областей ограничен. Поэтому во многих случаях требуется явное указание банка памяти для переменной с помощью специальных модификаторов.
bank0 uns8 var1;
bank3 uns16 var2;
shrbank uns8 var3;
shrbank соответствует разделяемой между различными банками области памяти, если таковая есть в микроконтроллере.
Так же, можно использовать директиву #pragma rambank, после которой все переменные без явного указания банка памяти данных помещаются в заданный банк. Причем это касается и локальных переменных, и параметров процедур, так как аппаратный стек данных в PIC отстутствует.
Указывать банк памяти нужно только при описании переменной, при ее использовании в выражениях ничего указывать уже не требуется. Команды для переключения банков памяти компилятор добавляет автоматически.
Использование банков памяти компилятор показывает вот в таком виде
Страничная организация памяти программ
Память программ PIC поделена на страницы. Любая функция, включая main, описанная программистом размещается на одной из страниц. По умолчанию на нулевой. Причем функция не может пересекать границы страницы.
Естественно, существуют специальные модификаторы, которые программист может (иногда должен) использовать для управления размещением
page2 void func() {}
Размещает func на второй странице памяти программ. Кроме модификаторов предусмотрены две директивы #pragma codepage и #pragma location.
Кроме того, можно указать и адрес размещения кода с помощью директивы #pragma origin. Эта директива примерно соответствует директиве ORG ассемблера.
Использование страниц памяти программ компилятор показывает вот в таком виде
Здесь же видно, как компилятор показывает выгоду от приобретения коммерческой версии (Estimated CODE SIZE of full optimization). В данном случае такой большой процент вызван тем, что на третьей странице размещены константы в памяти программ, к которым активно обращается процедура на нулевой странице.
Я специально отключил ручное управление переключением страниц, что бы показать, насколько важным может быть управление размещением. То, что наглядно показывают CC5X и CC8E другими компиляторами просто скрывается, но из сгенерированного кода никуда не исчезает.
Указатели
В Гарвардской архитектуре области памяти данных и программ разделены. И доступ к ним в PIC осуществляется по разному. Однако, CC5X и CC8E позволяют создавать "универсальные" указатели. С помощью универсальных указателей можно получать доступ и к переменным (памяти данных) и к константам (память программ). Однако, управление переключением банков и страниц при этом уже осуществляется программистом вручную.
Константы в памяти программ
Константы, определяемые модификатором const, размещаются в памяти программ, а не памяти данных. Доступ к таким константам осуществляется или специальными командами, если микроконтроллер их поддерживает, или через команду retlw.
В целом, так проступают почти все компиляторы для микроконтроллеров. Но в CC5X и CC8E есть дополнительные возможности управления размещением. В частности, можно указать страницу, на которой будут размещены константы. Кроме того, определен и специальный тип данных DataInW, который очень полезен для некоторых микроконтроллеров.
Кроме того, компилятор автоматически объединяет константы для исключения дублирования информации. Причем это касается не только символьных строк.
Ограничения, иногда серьезные
За высокую эффективность генерируемого кода приходится платить. В частности, не редко оказывается, что корректный, с точки зрения стандарта С, код отвергается компилятором. Наиболее показателен пример с использованием элементов массива с обеих сторон оператора присваивания.
uns8 arr1[10], arr2[10];
extern uns8 i,j;
arr1[i]=arr2[j];
Приведет к ошибке компиляции и "чистосердечному" признанию компилятора, что с точки зрения С код корректен, но вот компилятор, увы, имеет ограничения. Поэтому просит изменить, упростить, код. Решением, в данном случае, будет введение дополнительной переменной
uns8 arr1[10], arr2[10];
extern uns8 i,j;
uns8 tmp;
tmp=arr2[j];
arr1[i]=tmp;
будет откомпилировано без ошибок. На самом деле, здесь просто компилятор сталкивается с архитектурным ограничением, так как регистр INDF зачастую всего один. И есть два пути, или сгенерировать код, возможно, не оптимальный, но работающий, или предложить программисту самому подумать над тем, что именно он хочет сделать, над оптимизацией. Компилятор выбирает именно второй путь.
Другим, не всегда очевидным примером, может быть доступ к специфическим регистрам. Например,
TRISIO =& 0x1F;
приведет к ошибке при компиляции для микроконтроллера семейства BasуLine, но для других семейств ошибки не будет. А все дело в том, что в BaseLine регистр TRISIO не имеет адреса, доступ к нему осуществляется специальной командой TRIS. А в других семействах регистры TRIS имеют полноценные адреса. И компилятор просто предлагает программисту учитывать специфику микроконтроллера, а не громоздит скрытый код создавая иллюзию легкости.
Переменные не инициализируются
Компилятор не генерирует никакого дополнительного кода, только то, что написал программист. Даже отдельный пролог для main не формируется. Поэтому не удивительно, что объявленные переменные не инициализируются. И это может сыграть злую шутку с теми, кто привык, что переменные, инициализация которых явно не указана, обнуляются. Да, это не соответствует стандарту, но многие компиляторы так делают, обнуляют переменные (иногда только глобальные и статические, иногда и локальные в стеке).
Но в случае CC5X и CC8E даже попытка описать в явном виде инициализацию при объявлении приведет к ошибке компиляции
// Так нельзя! Будет ошибка компиляции
uns8 var=15;
// Так можно, ошибки нет
uns8 var;
var=15;
Инициализация при объявлении требует создания пролога, который будет выполняться перед передаче управления коду функции. Но компилятор не формирует пролог. И не создает у программиста иллюзию его формирования. В коде будет только то, что программист явно написал.
Многофайловую компиляци лучше не использовать
С этим и раньше были проблемы, но все таки можно было откомпилировать исходные файлы и потом собрать их компоновщиком. Кроме того, можно было сформировать из С-файлов файлы на ассемблере, и потом их отдельно ассемблировать и собрать компоновщиком.
Но дело в том, что в качестве ассемблера подразумевался MPASM, а в качестве компоновщика MPLINK. А Microchip отказалась от поддержки 32-разрядных систем, что привело к исключению из состава MPLAB X IDE этих программ.
Вы можете попробовать использовать gpasm и gplink, но результат, как говорится, не гарантирован.
В документации на компиляторы об этом написано так
IMPORTANT NOTE: Support for MPLINK has been terminated. MPLAB X 5.35 is the last version that includes mpasmx/mplink. However, devices supported by mplink can be used with newer MPLAB X versions if a copy of the mpasmx package is stored on the computer.
Поэтому наилучшим способом будет включение всех отдельных С-файлов в главный, содержащий main, с помощью директивы #include. Это позволит препроцессору собрать все ваши файлы в один большой, который и будет компилироваться. Да, make использовать не получится, а время компиляции и сборки будет больше. Но программы для 8-битных микроконтроллеров бывают не столь сложными и большими (особенно у любителей), что бы привести к значительному увеличению времени сборки проекта.
Это не приведет к изменениям генерируемого HEX файла. Более того, из-за специфики архитектуры микроконтроллера (стек, в частности) компиляция единого файла позволяет лучше отработать оптимизатору. Так что и раньше использование #include сборки всех С-файлов в единый компилируемый файл было рекомендованным способом.
Оптимизация
Я немного лукавил, когда говорил, что только написанное программистом попадет в код. Разумеется, код будет добавлен и от имени компилятора, например, для операторов switch или доступа к константам. Но иногда и написанное программистом подвергается изменению, или вовсе в код не попадает. Это "шалости" оптимизатора.
Замена вызова на переход
Если процедура вызывается всего один раз, то компилятор заменит команды вызова и возврата на команду перехода. На первый взгляд это дает "копеечную" экономию. Но представьте, что процедура вызывается из одного места, но это место располагается внутри цикла. Итоговая экономия времени может оказаться весьма заметной. Хотя экономия памяти будет весьма малой.
Исключение не вызываемых функций (не исполняющегося кода)
А вот это уже весьма полезно. Так как многофайловая компиляция и сборка недоступна, нет и возможности использовать библиотеки, что бы включать в HEX файл только нужные процедуры. И без такой оптимизации у нас все описанные в "библиотечных" С-файлах процедуры попадали бы в HEX-файл.
Но мы можем немного поуправлять оптимизатором с помощью директивы #pragma library. В результате, из всех описанных в файле процедур в HEX-файл попадут только те, что вызываются в явном виде. И мы можем без опаски включать С-файл с множеством описанных функций без опаски, что все они окажутся в памяти микроконтроллера. Туда попадут только те, что действительно вызываются.
Заключение
Можно еще долго рассказывать о компиляторах CC5X и CC8E, приводить примеры сгенерированного кода и влияние ключей командной строки, описывать тонкости формирования таблиц перехода оператора switch, говорить о ручном управлении памятью и размещением, и т.д. Но лучше просто скачать компилятор с документацией и посмотреть все самостоятельно.
Да, этот не самый тривиальный компилятор, и с ним бывает непросто. Но он дает возможность очень тонкого управления сгенерированным кодом, что важно при работе с микроконтроллерами. Он не занимается самодеятельностью и не создает иллюзию легкости разработки программ, он в точности выполняет то, что описывает программист.
В какой то степени, этот компилятор можно назвать низкоуровневым С-компилятором, или даже высокоуровневым ассемблером. Он прекрасно работает даже в бесплатном варианте, что важно для любителей, и ограничения бесплатной версии редко бывают заметны. И, в конечном итоге, всегда можно приобрести коммерческую версию, если ограничения бесплатной мешают. Стоимость этих компиляторов гораздо ниже, чем того же XC8 от Microchip. Но для подавляющего большинства любителей бесплатной версии окажется достаточно.