Обратился ко мне на днях знакомый с просьбой помочь освоить микроконтроллеры. Сам он электронщик, но понемногу и программы простенькие пишет, для себя.
Как известно, самый лучший микроконтроллер для новичка тот, с которым работает более опытный товарищ, который всегда подскажет и поможет. Ну а мне нравятся микроконтроллеры PIC от Microchip. Или я просто привык к ним.
Приводить массу примеров других, "самых лучших" микроконтроллеров, не нужно. Как и восклицать "только не это!", не стоит. Кому что нравится.
Я дал ему пару PIC12F629 в корпусе DIP8, которые оказались под рукой. Это простые микроконтроллеры линейки Mid-Range и с ними просто работать, что как раз подходит для начала. Светодиодом помигать, с кнопкой поработать, что нибудь простенькое, желательно полезное, сделать. Словом, хорошо подходит для типичных задач новичков. Да и корпус удобный для установки на беспаечную макетную плату. Кроме того, эти микросхемы оказались единственными в таком корпусе, даже не помню, откуда у меня взялись. Предпочитаю с SMD работать.
Программатор у него был, и эти микроконтроллеры он поддерживал. Причем даже в режиме внутрисхемного программирования. Правда с MPLAB X IDE этот программатор не работал, но это не страшно, будет родной программой пользоваться.
К ассемблеру товарищ интереса не проявил, что не удивительно, захотелось ему С. Да еще IDE что бы, да с отладчиком. Ну с IDE то просто, MPLAB X вполне подойдет. А вот компилятор XC8 уже не очень. В бесплатном варианте он генерирует очень грязный код. Microchip говорит, что это из-за слабой оптимизации в бесплатном варианте. Некоторые злые языки говорят, что наоборот, он специально мусорит в коде, что бы платную версию покупали. Истина где то между этими вариантами, скорее всего.
Какие еще существуют компиляторы для PIC я говорил в статье "Автоматизация. "День открытых дверей" для начинающих. Программирование". Хотел предложить CC5X, но у товарища Linux, поэтому для запуска требовался wine, что его не устраивало. Да и в CC5X нужно вручную банки для размещения переменных указывать и страницы про процедур. Кроме того, там нестандартные расширения языка используются. Да и к MPLAB X он под Linux по человечески не подключается.
И тут я вспомнил про SDCC, который умеет компилировать для PIC с длиной команды 14 и 16 бит. С этим компилятором я никогда раньше не работал, но знал, что там поддержка PIC не самая хорошая. Зато бесплатный и под Linux работает. Кроме того, для MPLAB X есть плагин позволяющий его подключить к IDE.
Однако, документация о PIC честно предупреждает
This port is not yet mature and still lacks many features. However, it can work for simple code
При этом сам порт для PIC практически прекратил развитие, причем уже давно. Так что, прежде, чем советовать этот компилятор новичку, нужно самому посмотреть на него и попробовать в работе. Скачал, подключил к IDE. Дополнительно нужны gputils, так как он работает с gpasm и gplink. Простенький пример программы откомпилировал без проблем. Неужели я зря много лет не обращал внимания на этот прекрасный компилятор?
Увы, такая красота и простота были недолгими. Отладчик наотрез отказался загружать результат компиляции. Проблема оказалась в формате файлов COD и COF, которые генерирует компилятор. Это плохо... Но ведь у SDCC есть и свой отладчик, пусть и простенький и похожий на DDD.
Перекомпилирую пример из командной строки с включением отладки. Опять засада... Должно создаться два дополнительных файла для отладчика - adb и cdb. Информация об именах и адресах переменных хранится в cdb, который как раз компилятор и не хочет создавать.
Ладно, фокус не удался. Придется товарищу или с XC8 помучиться, что вполне возможно для простеньких программ, или CC5X все таки осваивать. А там посмотрим...
Но мне стало интересно, а какой же код генерирует SDCC. Ведь компилятор то хвалят, не зря наверное. Взял совсем простой пример
Я уже говорил не раз, почему тексты программ приходится приводить в виде рисунков. В Zen нет возможности выложить текст программы в читабельном виде, просто не предусмотрено такого форматирования. Да и текст в виде картинки при загрузке проверяется, и картинка будет отвергнута, если на ней просто текст.
Немного прокомментирую этот пример. Первая строка запрещает компилятору выносить имена полей структур и объединений в глобальное пространство имен, что приводит к ошибкам компиляции. А в сообщениях об ошибках компилятор не многословен и понять причину ошибки не просто.
Включение stdint.h позволяет использовать тип uint8_t. Эта привычка у меня осталась с давних времен, еще с ЕС и СМ ЭВМ, так как стандарт С не указывает точную разрядность типов переменных.
Собственно, изначально в программе просто устанавливался в единицу вывод GP3 порта ввода-вывода. Это делается в строке 22. Как раз будет нужно для мигания светодиодом. Переменная wGPIO необходима, что бы избежать засад с "чтение-модификация-запись" при работе с отдельными разрядами порта.
Дело в том, что при выполнении строки 22 (GPIObits.GPIO3=1) сначала будет считано состояние выводов порта, причем физически с выводов, а не из регистра данных порта. Затем будет установлен нужный разряд, после чего все запишется снова в порт. В более современных микроконтроллерах у портов есть еще регистры LAT, но тут приходится создавать временную переменную, что бы избежать доступа RMW.
Как эта переменная используется показано в строках 23 и 24. Мы устанавливаем нужный разряд в переменной, а потом записываем ее значение в порт. Это позволяет избежать "порчи разрядов порта".
Объединение нужно для того, что бы работать с портом как с байтом, так и с отдельными его разрядами без использования логических операций и масок. Аналогично описаны порты ввода-вывода в компиляторе XC8.
Множество строк GPIO=wGPIO.byte нужны для того, что бы исключить удаление "неиспользуемого кода" оптимизатором компилятор. Как оказалось впоследствии, он и не собирался ничего выбрасывать, хотя в документации об этом говорилось.
Ну а остальные строки я добавил потом, когда увидел результат компиляции... Просто, что бы показать "всю красоту" работы SDCC для PIC.
Давайте сначала посмотрим, во что превращаются строки с 22 по 26 нашего примера
Строка 149 результата (BANKSEL) для данного микроконтроллера не нужна. Но простим это компилятору, так как ассемблер на следующем шаге просто выдаст предупреждение и не будет генерировать для нее код.
Строка 22 превратилась в строку 150. Здесь все абсолютно корректно, команда BSF установит 3 разряд порта в 1. Все хорошо и с установкой бита 2 в нашей временной переменной и с последующей записью этой переменной в порт(строки 152-155 результата). _GPIO и _GPIObits имеют один и тот же адрес, это просто разные имена для разных представлений порта. Это синонимы, и ассемблер их превратит в один и тот же адрес.
А вот дальше пошли чудеса! Обратите внимание на строку 157 результата. Здесь снова устанавливается 2 бит wGPIO, хотя исходная строка подразумевает запись в нее числа 7! Строка 27 исходного файла (wGPIO=7) должна была превратиться в пару машинных команд: MOVLW 7 и MOVWF _wGPIO. А это уже ошибка компилятора.
Дальше не легче... В примере у нас логический сдвиг переменной на 3 разряда влево. По непонятной причине компилятор копирует нашу переменную в регистр WREG, что совершенно не требуется. При этом он создал две рабочие переменные r0x1002 и r0x1003, которые тоже не нужны. Далее компилятор помещает в эти рабочие переменные копию нашей переменной и начинает сдвиги. Сброс бита 0 в регистре STATUS необходим, это флаг С, который участвует в переносе.
Компилятор сдвигает влево рабочую переменную r0x1003 и помещает результат сдвига в WREG. Только для того, что бы тут же перезаписать ранее сохранную в r0x1002 копию wGPIO. После этого он продолжает сдвиги уже r0x1002. Тут криминала нет. Но потом он выполняет сдвиг вправо и помещает результат в WREG.
Однако, результат сдвига обратно в wGPIO так и не попадает! Зато компилятор, видимо совсем запутавшись, сбрасывает или устанавливает в wGPIO свой любимый бит 2, в зависимости от флага переноса. И это снова ошибка компилятора! Причем результат компиляции будет точно таким же если строку 27 записать в виде wGPIO=wGPIO << 3. Что вполне естественно.
Хорошо, а что будет со сдвигом переменной, которая не является частью объединения? Компилятор остается верен себе и копирует переменную var, через WREG, в рабочую переменную r0x1002. После этого выполняется сдвиг рабочей переменной с помещением результата в WREG и последующим его копированием обратно в var. А затем уже два раза сдвигается непосредственно var. Сколько лишних шагов... Но тут хоть ошибки нет!
И после этого можно говорить, что XC8 замусоривает код? Примерно одного поля ягоды по части мусора... Зато в XC8 хоть ошибок таких не делается.
Но может проблема в том, что у нас в том, что структура битовое поле внутри объединения анонимная? Стандарт это допускает. Для SDCC можно указать, какому стандарту С следует компилятор. Однако, переключение стандартов никак не повлияло на результат трансляции.
Остается исключить влияние анонимности элемента объединения. Немного изменим наш пример программы
И снова откомпилируем. Я покажу только часть результата
Ура! У нас стало немного лучше. Во первых, строка 25 программы теперь корректно компилируется в MOVLW 7 с последующей записью в wGPIO и порт. С одной ошибкой справились...
А вот дальше все не так радужно. Сдвиг нашего объединения все так же выполняется с привлечением двух рабочих переменных, что совершенно излишне. Но хоть результат правильный, ошибка пропала.
При этом сдвиг переменной являющейся частью объединения и простой переменной выполняется по прежнему по разному. Хотя обе они занимают ровно один байт в памяти. Более того, у них одинаковый тип uint8_t. Почему такая разница? Это тайна покрытая мраком...
Заключение
SDCC позволяет компилировать под разные аппаратные платформы. Хотя изначально создавался для MCS-51. Лексический и синтаксический анализаторы одни и те же для всех платформ, а вот кодогенераторы разные. Однако, такие проблемы есть и для PIC14 (PIC10 (10F322), PIC12, PIC16), и для для PIC16 (PIC18).
Поэтому говорить, что компилятор плохой, не буду. Однако, для для PIC его использовать явно не стоит. А что делать с товарищем подумаю...
У этого примера есть еще один побочный эффект. Я не раз говорил, что нужно хорошо знать используемый микроконтроллер, в мельчайших деталях. Включая архитектуру процессора и памяти, машинные команды. Без этих знаний подобные проблемы с компилятором выявить и понять не получится. И новичок скорее всего просто разуверится в своих силах и решит, что микроконтроллеры не для него. И будет не прав.
Эта статья родилась спонтанно и не входит в цикл "Микроконтроллеры для начинающих". Но данный случай хорошо показывает, почему я начал этот цикл статей. И почему так много внимания уделяю подробностям, которые многим кажутся излишними в наш ввек языков высокого уровня.
Вы думаете, что подобного не бывает с другими компиляторами и другими микроконтроллерами? Ошибаетесь. За свою жизнь я не раз сталкивался с проблемами компиляторов и библиотек. Для самых разных ЭВМ (включая большие, занимающие просторные залы) и микроконтроллеров.
Так одна из версий компилятора Zortech C отказывалась отключать оптимизацию операции побитового И с константой 0xFFFF, что не позволяло работать с памятью видеоадаптера EGA в графических режимах напрямую (пришлось писать процедуру на ассемблере). Так что бдительность терять не стоит. Засада может поджидать в самых неожиданных местах в самое не подходящее время.
И нельзя опускать руки при возникновении проблем! Как там у Владислава Крапивина в "Тень каравеллы"? Если препятствия множатся, значит победа близка!