Языки программирования для микроконтроллеров
Программирование для микроконтроллеров, как и программирование для универсальных компьютеров прошло большой путь развития от программирования в машинных кодах до применения современных интегрированных систем написания программ, отладки и программирования микроконтроллеров. В настоящее время исходный текст программы пишется на одном из языков программирования.
Сами языки программирования в свою очередь делятся на две группы:
К языкам программирования «низкого» уровня относятся языки программирования в которых каждому оператору соответствует не более одной машинной команды. Набор машинных команд каждого конкретного процессора обязательно входит в состав такого языка программирования. Языки программирования низкого уровня в настоящее время называются ассемблерами (старое название автокоды). Для каждого процессора существует своя группа ассемблеров. Ассемблеры для одного и того же процессора различаются между собой дополнительными возможностями, облегчающими программирование.
Языки программирования «высокого» уровня позволяют заменять один оператор несколькими машинными командами. Это позволяет увеличивать производительность труда программистов. Кроме того, языки «высокого» уровня позволяют писать программы, которые могут выполняться на различных микропроцессорах. (Естественно, что при этом необходимо использовать программы — трансляторы для соответствующего процессора.) В настоящее время наиболее распространены такие языки программирования высокого уровня как С и PLM
О преимуществах и недостатках языков высокого и низкого уровней говорилось достаточно много. Выбор языка программирования зависит от состава аппаратуры, для которой пишется программа, а также от требующегося быстродействия всего программно-аппаратного комплекса в целом.
В тех случаях, когда объём ОЗУ и ПЗУ мал (в районе нескольких килобайт) альтернативы ассемблеру нет. Именно эти языки программирования позволяют получать самый короткий и самый быстродействующий код программы (при прочих равных условиях, т.к. испортить можно всё!).
Языки программирования высокого уровня позволяют значительно сократить время создания программы, но при этом увеличивается размер программы, поэтому для выбора такого языка программирования для микропроцессорных систем необходимо иметь достаточно большой объём памяти программ (несколько десятков килобайт). Увеличение объёма программы связано с несколькими факторами:
Первый из этих пунктов постепенно утрачивает своё значение с появлением всё более совершенных трансляторов. Третий пункт тоже решается тем же путём при применении различных видов оптимизаторов, входящих в состав компилятора. Однако в большинстве случаев оптимизатор не может определить одинаковые действия, если они отличаются хотя бы одной командой. Кроме того, оптимизатор работает только в пределах одного модуля!
Виды программ-трансляторов
Процесс преобразования операторов исходного языка программирования в машинные коды микропроцессора называется трансляцией исходного текста. В настоящее время ручная трансляция программ практически не используется. Трансляция производится специальными программами-трансляторами. Их классификация приведена на рисунке 1.
Рисунок 1. Классификация программ-трансляторов языков программирования
Существует два больших класса программ-трансляторов: компиляторы и интерпретаторы. При использовании компиляторов весь исходный текст программы преобразуется в машинные коды, и именно эти коды записываются в память микропроцессора. При использовании интерпретатора в память микропроцессора записывается исходный текст программы, а трансляция производится при считывании из памяти программ очередного оператора. Естественно, что быстродействие интерпретаторов намного ниже по сравнению с компиляторами, т.к. при использовании оператора в цикле он транслируется многократно.
Применение интерпретатора может обеспечить выигрыш только в случае его разработки для языка программирования “высокого” уровня. В этом случае может быть сэкономлена внутренняя память программ, а также облегчен процесс отладки программ (при применении языка программирования BASIC) или облегчен перенос программ с одного типа процессора на другой (при применении языка программирования JAVA).
При программировании на языке программирования ASSEMBLER применение интерпретатора приводит к проигрышу по всем параметрам, поэтому для языков программирования низкого уровня применяются только программы–компиляторы.
Для программирования микроконтроллеров как на языке программирования “низкого” уровня, так и на языке программирования “высокого” уровня используются только компиляторы, поэтому рассмотрим подробнее виды этих трансляторов.
Виды компиляторов
Программы-компиляторы бывают оценочные и профессиональные.
Оценочные или учебные компиляторы позволяют написать простейшие программы для конкретного процессора и определить подходит ли процессор для тех задач, которые предстоит решать в процессе разработки устройства. Конечно, если программа очень проста, то можно весь программный продукт написать на оценочном компиляторе. Оценочные компиляторы позволяют транслировать одиночный файл исходного текста программы. Иногда такие компиляторы позволяют включать в процесс трансляции содержимое отдельных файлов специальной директивой. В результате работы оценочного компилятора сразу получается исполняемый или загрузочный модуль программы, поэтому такие компиляторы называются компиляторы с единой трансляцией.
Профессиональные трансляторы позволяют производить трансляцию исходного текста программы по частям. Это позволяет значительно сократить время трансляции исходного текста программы, так как не нужно транслировать весь текст программы, а можно транслировать только ту часть программы, которая менялась после предыдущей трансляции.
Кроме того, каждый программный модуль может писать отдельный программист. Это позволяет сократить время написания программы. Даже в том случае, если программу пишет один человек, время написания программы сокращается за счёт использования готовых отлаженных и оттранслированных программных модулей. В таких компиляторах процесс трансляции программы разбивается на два этапа: трансляция программного модуля и связывание программных модулей в единую программу. Поэтому такие компиляторы называются компиляторами с раздельной трансляцией.
Профессиональные компиляторы разрабатываются и продаются отдельными фирмами. Для микроконтроллеров семейства MCS-51 получили известность продукты таких фирм как FRANCLIN, IAR, KEIL.
В состав современных средств написания и отладки программ для микроконтроллеров обычно входят эмуляторы процессоров или отладочные платы, текстовый редактор, компиляторы языка высокого уровня (чаще всего «C») и ассемблера, редактор связей и загрузчик программы в отладочную плату. Все программы обычно объединены интегрированной средой разработки программного проекта, позволяющую поддерживать один или несколько программных проектов.
Понравился материал? Поделись с друзьями!
Вместе со статьей «Языки программирования для микроконтроллеров» читают:
Можно ли использовать С++ вместо Си для небольших проектов в микроконтроллерах
Существует мнение, что использование С++ при разработке программного обеспечения для микроконтроллеров это как стрельба из пушки по воробьям. Мол код получается большого размера и неповоротливый, а мы привыкли бороться за каждый бит в ОЗУ или ПЗУ. И программное обеспечение для микроконтроллера может быть написано обязательно на Си. Действительно, ведь язык Си был задуман как альтернатива ассемблеру, код должен был быть такой же компактный и быстрый, а читаемость и удобство разработки позволять легко писать довольно большие программы. Но ведь когда-то и разработчики на ассемблере говорили тоже самое про Си, с тех пор утекло много воды и программистов, использующих только ассемблер, можно по пальцам пересчитать. Конечно, ассемблер еще играет важную роль в разработке кода для быстрых параллельных вычислений, написании ОСРВ, но это скорее исключение из правил. Так же как когда-то Си пробивал себе дорогу в качестве стандарта для встроенного ПО, так и язык С++ уже вполне может заменить Си в этой области. С++ стандарта С++14 и современные компиляторы имеют достаточно средств для того чтобы создавать компактный код и не уступать по эффективности коду, созданному на Си, а благодаря нововведениям быть понятнее и надежнее. Ниже приведен код поиска наименьшего числа в массиве из 5 целых чисел на двух языках Си и С++ на компиляторе IAR for ARM 8.20 с отключенной оптимизацией.
Код на Си, занимающий 156 байт
И его ассемблерное представление
И код на С++, занимающий 152 байт
И его ассемблерное представление
Как можно увидеть сгенерированный компилятором код на С++ на 4 байт меньше, а скорость работы на 12 тактов быстрее. Все это достигается за счет новых возможностей С++14. Конечно, можно заметить, что для обоих компиляторов была отключена оптимизация, что это очень синтетический тест который не имеет ничего общего с реальной реализацией, но все же можно сказать, что не все так однозначно.
Нужно учитывать особенности программирования для микроконтроллеров, ведь требования к небольшому объему памяти программ 32,64..512 кБ, еще меньшему объему ОЗУ и низкой частоте микропроцессоров (особенно при использовании для низкопотребляющих датчиков), накладывают свои ограничения. И с уверенностью можно сказать, что не все фишки С++ полезны. Например, использование стандартной библиотки шаблонов может отнять значительное количество ресурсов, а такие важные в большом мире С++ вещи как исключениия можно с уверенностью выкинуть из проектов для небольших микроконтроллеров, поскольку они требуют значительного увеличения размера стека и кода для хранения информации об обработчике исключения и дальнейшем его поиске. Поэтому я попытаюсь рассказать как можно использовать С++ и его новые особенности для небольших проектов и постараюсь показать, что без зазрения совести С++ можно использовать вместо Си.
Первым делом надо определиться с задачей. Она должна быть достаточно простой, но и достаточно показательной, чтобы увидеть как можно, например, полностью отказаться от макросов, по-возможности уйти от указателей, уменьшить риски глупых ошибок и так далее…
Выбор, как обычно пал на светодиды.
Для того, чтобы читатель понимал что мы хотим сделать, я приведу конечный вариант задачи, которую необходимо реализовать на микроконтроллере:
Итак, на нашей плате есть 4 светодиода: LED1, LED2, LED3 и LED4. Они подключены к портам GPIOA.5, GPIOC.5, GPIOC.8, GPIOC.9 соответственно. Пока давайте будем работать с LED1, который находится на GPIOA.5.
Для начала программист Снежинка написал вот такой вот простой код на Си, который будет переключать светодиод. Выглядит это так:
Код работает хорошо и правильно, Снежинка остался доволен своей работой и пошел отдыхать. Но для непосвящённого в тонкости разводки платы и булевые операции пользователя, этот код не совсем понятен, поэтому Снежинке пришлось дописывать комментарии, которые поясняют, что на порте GPIOA.5 находится светодиод и собственно мы хотим его переключить.
Давай подумаем, как должен выглядеть такой код на человеческом языке. Может быть так:
Как мы можем увидеть, здесь уже не нужны комментарии и назначение такого кода интуитивно понятно. Самое замечательно то, что этот псевдокод практически полностью соответствует коду на С++. Посмотрите, единственное отличие — мы должны вначале создать светодиод, указав на каком порту он находится.
Программисты минималисты могут сказать, что да код понятнее, но ведь он избыточен, создается объект, идет вызов конструктора, методов, сколько же ОЗУ и дополнительного кода генерируется. Но если вы взглянете листинг на ассемблере, то приятно удивитесь, размер кода на С++ при включенной опции inline functions для обоих компиляторов, будет такой же как и для Си программы, а из-за особенностей вызова функции main, общий код на С++ даже на одну инструкцию меньше.
Ассемблерный код из Си исходников
Ассемблерный код из С++ исходников
Это еще раз подтверждает тот факт, что современные компиляторы делают свою работу по превращению вашего замечательного и понятного кода на языке С++ в оптимальный ассемблерный код. И совсем не каждый программист на ассемблере может достичь такого уровня оптимизации.
Конечно, при отключенной оптимизации код на С++ не будет таким компактным по размеру стека и быстродействию. Для сравнения приведу неоптимизированный вариант с вызовом конструктора и методов.
Для меня нет дилеммы между держанием в голове множество ненужных деталей чтобы написать микропрограмму датчика (какие элементы на какие порты подключены, в каком текущем состоянии находится сейчас порт или тот или иной модуль и так далее) и простотой и понятностью кода. Ведь в конце концов, нам нужно описать логику работы устройства, интерфейс взаимодействия с пользователем, реализовать расчеты, а не запомнить, что для того чтобы считать данные с АЦП, нужно вначале его выбрать с помощью сигнала CS, находящегося на порту GPIOA.3 и установить его в единицу. Пусть этим занимается разработчик модуля АЦП.
Первоначально может показаться, что необходимо писать много дополнительного кода, но уверяю вас, это с лихвой окупится, когда приложение станет немного сложнее, чем просто моргнуть светодиодом.
Вернемся к нашему заданию. Не успел Снежинка показать результат своей работы заказчику, как заказчик, ощутив прелесть моргания светодиода в ночи, решил, что хорошо бы иметь моргающие в режиме “Елочка” четыре светодиода, тем более что на носу Китайский Новый год и будет много потенциальных покупателей.
Наш программист Снежинка, одновременно выполняющий несколько проектов, решил сэкономить время и сделать все в лоб самым, как он считает надежным и понятным способом:
Код работает, но обратите внимание, на последнюю запись TOGGLE_BIT(GPIOС->ODR, 5U). Светодиоды 1 и 4 находятся на ножке номер 5, но на разных портах. Используя Ctrl С-Ctrl V, Снежинка скопировал первую запись, и забыл поменять порт. Это типичная ошибка, которую допускают программисты, работающие под давлением менеджмента, устанавливающих срок “вчера”. Проблема заключается в том, что для поставленной задачи надо было быстро написать код, и у Снежинки не было времени подумать над дизайном ПО, он просто сел и написал то что надо было, при этом допустив небольшую помарку, которую он конечно же найдет при первой же прошивке в устройство. Однако, нужно понимать, что на это он потратит какое-то время. Кроме того, Снежинка добавил два ужасных макроса, которые по его мнению облегчают ему работу. В предыдущем примере на С++ мы добавили довольно много кода, в том числе для того, чтобы заменить эти макросы на замечательные встроенные функции. Зачем?
Давайте рассмотрим очень популярный макрос установки бита. С помощью него можно устанавливать бит в любом целочисленном типе.
Все выглядит очень красиво, за исключением одного – в данном коде ошибка и нужный бит не установится. С помощью макроса SET_BIT устанавливается 10 бит в переменной value, которая имеет размер 8 бит. Интересно сколько программист будет искать такую ошибку, если объявление переменной будет не так близко к вызову макроса? Единственное преимущество данного подхода – это несомненный факт того, что код будет занимать наименьший размер.
Чтобы избежать потенциальной ошибки, давайте заменим этот макрос на шаблонную функцию
Здесь встроенная функция setBit принимает ссылку на параметр, в котором нужно установить бит и номер бита. Функция может принимать произвольный тип параметра и номера бита. В данном случае для того, чтобы убедиться, что номер бита не превышает размер типа параметра, другими словами, что бит точно можно установить в параметре такого типа, мы делаем проверку с помощью функции assert. Функция assert проверяет условие во время исполнения и если условие соблюдено, то код продолжает исполняться дальше, а вот если условия не соблюдено, то программа завершится с ошибкой. Описание прототипа функции assert лежит в файле cassert, его и нужно подключить. Такая проверка будет полезна во время разработки, если вдруг кто-то решит передать неверный входной параметр, вы заметите это во время работы, когда он сработает. Понятно, что в продуктовом коде нет смысла использовать проверку входных параметров, так как это занимает место, замедляет работу, да к тому же во время разработки вы уже отлавили все потенциальные возможности передачи неверных параметров, поэтому assert можно отключить, определив NDEBUG символ в исходном файле или определив его для всего проекта.
Обратите внимание на ключевое слово inline. Это ключевое слово указывает компилятору, что хотелось бы, чтобы данная функция рассматривалась как встраиваемая. Т.е. мы предполагаем, что компилятор просто заменит вызов функции на её код, однако на практике такого можно добиться только с установками оптимизации у компилятора. В IAR Workbench это установка флажка напротив опции “Function Inlining” в закладке С/С++ Compiler->Optimization. В таком случае наша функция также быстра и занимает столько же места как и макрос.
Вернемся снова к коду Снежинки, как же тут обстоят дела с расширяемостью?
Ведь судя по всему заказчик не остановится на этом и что произойдет, если светодиодов будет не 4, а 40? Размер кода увеличится линейно в 10 раз. Вероятность ошибки возрастет во столько же раз, а поддержка кода в дальнейшем превратится в рутину.
Более мудрый программист на С мог бы написать код так:
Функция main теперь содержит меньше кода и самое главное стала легко расширяемая. При увеличении количества светодиодов, теперь достаточно просто добавить порт к которому подключен светодиод в массив светодиодов pLeds и макрос LEDS_COUNT поменять на количество светодиодов. При этом размер кода вообще не увеличится. Конечно глубина стека при этом вырастет значительно, так как массив светодиодов создается на стеке, а он уже равен 56 байтам.
Между первым решением и вторым всегда есть выбор, что важнее для конкретной вашей реализации: Меньший размер кода, расширяемость, удобочитаемость и лаконичность или меньший размер ОЗУ и скорость. По моему опыту в 90% случаев можно выбрать первое.
Но давайте рассмотрим этот код повнимательнее. Это типичный код на Си с использованием указателей и макросов типа SET_BIT() и TOGGLE_BIT(). И в связи с этим, здесь существуют риски потенциальных проблем, например, функция SwitchOnAllLed(tLed *pLed, int size) принимает указатель и размер массива. Во-первых, нужно понимать, что ничего не запрещает передать в эту функцию нулевой указатель, поэтому нужна проверка, что указатель не равен NULL, а ведь случайно можно вообще передать указатель на другой объект. Во-вторых, в случае, если вдруг программист передаст размер больше чем объявленный размер массива, поведение такой функции будет совершенно непредвиденным. Поэтому конечно, лучше в этой функции проверять размер. Добавление таких проверок приведет к увеличению кода, проверки можно сделать и с использовнием assert, но лучше попробовать написать тоже самое на С++
Да этот код занимает уже значительно больше места. Но мы увидим в дальнейшем, как такой дизайн поможет нам сэкономить время, а размер кода будет практически таким же как и на Си, при усложнении программы.
Здесь используется класс LedsController, приведу его код:
Методу SwitchOnAll() теперь не надо передавать указатель на массив, он использует уже существующий массив, сохраненный внутри объекта класса.
Почему же этот код считается надежнее? Во-первых, мы нигде не используем указатели, мы храним массив объектов на все существующие светодиоды в нашем классе и обращаемся непосредственно к объекту, а не к указателю. Во-вторых, мы используем специальный синтаксис для цикла for, который обходит наш массив без необходимости указывания его размера, за нас это делает компилятор. Этот цикл работает с любыми объектами являющиеся итераторами. Массив в С++ по умолчанию является таким объектом.
Единственное место, где можно ошибиться, это задание размера массива с помощью константы LedsCount. Однако, даже из этого небольшого примера, можно увидеть, что С++ предоставляет намного больше средств для написания надежного кода.
Еще один момент, требующий внимания – это то, что мы можем по ошибке создать несколько объектов класса LedsController, что приведет к увеличению размера используемого ОЗУ (стека) и к интересному поведению программы. Защититься от этого может помочь шаблон Одиночка, но делать это стоит только тогда, когда у вас довольно крупный проект, большая команда разработчиков и существует риск, что кто-то забудет о том, что объект вашего контроллера уже создан и нечаянно создаст еще один такой же. В нашем же случае, это явный переизбыток, функция небольшая, и мы четко помним, что объект класса LedsController у нас один.
Но вернемся к разработке, как обычно бывает, в тот момент, когда программист реализовал задачу (елочку в нашем случае), заказчик тут же просит реализовать еще два режима: моргание в шахматном порядке и моргание всеми светодиодами, а режимы должны меняться по нажатию кнопки. В случае со Снежинкой произойдет практически полный провал и если привести код программы в стиле Снежинка, то он будет настолько громоздким, что не влезет на страницу данной статьи, поэтому приводить здесь я его не буду.
Лучше посмотрим что сможет сделать программист на С. Понимая, что от заказчика могут поступить еще новые предложения, он скорее всего сделает нечто вроде этого:
И хотя, чтобы добавить новый режим, нужно всего лишь добавить новый перечеслитель, добавить установку начального значения для этого режима и обработку работы светодиодов для этого режима, программа все еще требует основательного пояснения и комментариев и уже смотрится громоздкой. Поэтому принимается решение убрать обработку режимов в отдельные методы:
В таком случае основная программ выглядит намного лучше:
Но все же хотелось бы что-то вроде человеческого
Можно попытаться сделать такое на Си, но тогда придется держать несколько переменных вне функций, например, currentLed, Mode. Эти переменные должны быть глобальными, чтобы функции знали про них. А глобальные переменные, как мы знаем это опять потенциальный риск ошибки. Можно нечаянно поменять значение глобальной переменной в каком-то из модулей, так как вы не можете держать в голове все возможные места, где и как она меняется, а через год уже и не вспомните зачем она вообще нужна.
Можно использовать для хранения этих данных структуры и пытаться использовать ООП на Си, но следует понимать, что в данном случае будет много накладных расходов, придется как минимум хранить указатель на функцию, а код будет выглядеть очень похожим на С++.
На каком языке сейчас чаще всего программируют микроконтроллеры?
Простой 1 комментарий
У Java ME есть минимальные системные требования для целевых устройств.
Взглянув на них, становится понятно, что это не для микроконтроллеров в общем случае. Конечно, встраиваемая система встраиваемой системе рознь, но вот микроконтроллеры ещё используют не только для встраиваемых систем, а прямо в железо, например, радио-приемопередающего устройства, спроектированного на работу с протоколом физического уровня. Такие контроллеры могут иметь килобайты памяти всех видов. Зачастую, такие девайсы предлагают не так много ассемблерных инструкций, чтобы имело смысл делать под них компилятор Си. В более универсальных микроконтроллерах компилятор есть, поэтому это вполне себе повод для радости.
Там, где можно развернуть JME, уже есть Linux kernel, поэтому ответ на вопрос о том, почему больше используется Си, чем Java, заключается в том, чем занимается компания, в чем у нее бизнес и какой у нее рынок. Количественно, решений, которым нужно JME просто меньше, относительно тех, в которых не нужна прослойка в виде ОС.
Удачи с размещением виртуальной машины Java + кода самой программы с учетом имеющихся ресурсов.
Удачи с размещением виртуальной машины Java + кода самой программы с учетом имеющихся ресурсов.
Абсолютно некорректное сравнение
вполне корректное. Микроконтроллеры совсем не заканчиваются ATTiny13 и есть вполне «жирные» в плане ресурсов. Так что справедливости ради микроконтроллер на java вполне имеет право на существование. Что и иллюстрируют те же sim-карты.
а есть примеры кода для таких джава карт?
В более мощных контроллерах в ходу уже не конкретный язык, а целые ОС. В основном в прошивках просто зашивают Линукс, а отдельные части по управлению контроллером реализуют на Си как драйвера.
знал, что при помощи JVM язык добивается кроссплатформенности и за счет этого Java стала такой популярной. Т.е. в моем понимании язык тогда как раз таки и подходил для микроконтроллеров
Не знаю с какого потолка вы взяли подобное понимание, но именно из-за JVM JAVA совершенно неприемлем для микроконтроллеров. За кроссплатформенность JAVA приходится расплачиваться большим расходом памяти и низким быстродействием.
PS Некоторые контроллеры Амперки которые на ARM Cortex программируются на JavaScript, но это уже другая история (помоему не очень удачная)





