Глава 9 Язык высокого уровня
Для написания всех программ в последних шести главах мы с вами использовали язык ассемблера. Хотя ассемблерные программы достаточно сильно отличаются от чистого машинного кода (см. стр. 239), тем не менее между любой машинной инструкцией и соответствующей командой ассемблера сохраняется однозначное соотношение. То есть программист вынужден мыслить объектами внутренней структуры микроконтроллера — регистров и памяти, а не объектами реализуемого алгоритма. Несмотря на то что большинство ассемблеров поддерживают макрорасширения, благодаря которым несколько машинных команд могут быть сгруппированы в виде псевдокоманды высокого уровня, это не более чем попытка обойти недостаток машинно-ориентированного языка. В чем же выражается этот недостаток? А в том, что для улучшения эффективности, качества и увеличения степени повторного использования программ кодирующий язык должен быть максимально независим от архитектуры процессора и должен иметь синтаксис, более ориентированный на решение задач.
Разумеется, не стоит и пытаться выучить какой-либо язык высокого уровня в одной короткой главе. Тем не менее, прочитав эту главу, вы:
• Поймете необходимость использования языка высокого уровня.
• Оцените преимущества, предоставляемые языком высокого уровня.
• Поймете, какие проблемы связаны с использованием языка высокого уровня для встраиваемых приложений на базе микроконтроллеров.
• Научитесь писать коротенькие программы на Си.
Уже в первые годы после появления коммерческих программных систем люди осознали, что писать большие программы на родном для компьютера языке очень сложно. Дело в том, что помимо всего прочего компьютеры начали периодически устаревать, и для каждой новой модели программы приходилось переписывать. Большие же программы даже в то время состояли из многих тысяч строк кода. Программисты встречались так же редко, как зубы у курицы, и ценились на вес золота. Поэтому, чтобы компьютеры были коммерчески выгодными, следовало найти средство, позволяющее сохранить инвестиции в дефицитное время программистов. При разработке универсального языка, независимого от аппаратной платформы, основное внимание было уделено тому, чтобы программист мог записывать код более естественным образом, в терминах, соответствующих решаемой задаче, а не на уровне памяти, регистров и флагов.
Разумеется, существует множество различных классов задач, требующих программирования, поэтому с тех пор было разработано большое количество языков программирования[118]. Одними из первых языков были Fortran (FORmula TRANslator) и COBOL (Common Business Oriented Language) в начале 50-х. Первый из указанных языков имел синтаксис, ориентированный на решение научных и инженерных задач, а второй — на решение бизнес-приложений. Несмотря на более чем 40-летний возраст этих языков, многие приложения до сих пор пишутся на них — сказывается инерция многих миллионов строк кода. Другими популярными языками были Algol (ALGOrithmic Language), BASIC, Pascal, Modula, Ada, C, C++ и Java — последние три языка относятся к одному семейству.
Хотя, с точки зрения программиста, написание программ на языке высокого уровня может быть легче и продуктивнее, процесс трансляции с языка высокого уровня в конечный машинный код представляет собой гораздо более сложную задачу по сравнению с процессом ассемблирования, описанным в главе 8. Пакет предназначенных для этого программ называется компилятором, а процесс — соответственно компиляцией.
Сложность компиляторов и их стоимость были приемлемыми при разработке программ для относительно мощных и чрезвычайно дорогих универсальных ЭВМ (мэйнфреймов) того времени. Однако в области микропроцессорных устройств языки высокого уровня практически не использовались вплоть до середины 80-х годов, т. е. до появления достаточно мощных и сравнительно недорогих персональных компьютеров и рабочих станций, на которых могли запускаться компиляторы. Повсеместное распространение таких компьютеров в сочетании с постоянно увеличивающейся вычислительной мощностью микроконтроллерных и микропроцессорных устройств, а также экономической значимостью этого сектора рынка привело к тому, что большинство программ для данных устройств тоже стали писаться на языке высокого уровня.
Если вы собираетесь описать задачу на языке высокого уровня для встраиваемой микроконтроллерной системы, например контроллера стиральной машины, то этот процесс условно можно разбить на следующие этапы:
1. Уточнение постановки задачи и разбиение ее на совокупность модулей, каждый из которых выполняет четко определенные операции с известным набором входных и выходных данных.
2. Продумывание реализации каждого модуля.
3. Создание в редакторе исходного файла в соответствии с синтаксисом используемого языка высокого уровня.
4. Компиляция исходного файла в его эквивалент на языке ассемблера.
5. Ассемблирование и компоновка промежуточного файла для получения файла в машинных кодах.
6. Загрузка итогового машинного кода в память программ конечного устройства.
7. Запуск программы, ее тестирование и отладка.
Этот процесс практически идентичен процессу, изображенному на Рис. 8.3 (стр. 253), просто появился дополнительный пункт — компиляция. Некоторые компиляторы сразу формируют из исходного файла машинный код. Однако при наличии фазы ассемблирования достигается большая гибкость (Рис. 9.1), что особенно важно при разработке программ для встраиваемых устройств на базе микроконтроллеров и микропроцессоров.
Рис. 9.1. Преобразование исходного кода, написанного на языке высокого уровня, в машинный код
При работе со встраиваемыми системами очень важно правильно выбрать язык высокого уровня. Причем основным критерием в данном случае будет объем машинного кода, генерируемого компилятором с языка высокого уровня, по сравнению с эквивалентным кодом, написанным на ассемблере. Ведь большинство встраиваемых микроконтроллерных устройств являются малогабаритными, имеют невысокую вычислительную мощность, а также ограниченные ресурсы памяти и малую стоимость — взять, к примеру, контроллер пульта дистанционного управления телевизора. В большинстве недорогих микроконтроллеров используется процессор с невысокой производительностью, имеющий, в лучшем случае, несколько сот байт ОЗУ и несколько килобайт ПЗУ Так что язык высокого уровня должен быть таким, чтобы его компилятор мог генерировать код, который если и не будет таким же эффективным, каким он мог бы быть при использовании ассемблера, то, по крайней мере, сравнимым с ним[119].
Наиболее часто для написания программ встраиваемых микропроцессорных и микроконтроллерных систем (Рис. 9.2) используется язык Си. Изначально язык Си был разработан как язык для написания операционных систем. На простейшем уровне операционная система (ОС) представляет собой программу, которая делает низкоуровневую работу компьютерных периферийных устройств, таких как клавиатура и дисковые накопители, незаметной для оператора. А раз так, то разработчик ОС должен иметь возможность обращаться к различным регистрам и участкам памяти периферийных устройств и легко интегрироваться с драйверами, пишущимися, как правило, на ассемблере. Поскольку обычные языки высокого уровня и их компиляторы были очень требовательны к вычислительным ресурсам, вплоть до начала 70-х годов невозможно было обойтись без ассемблера, который обеспечивал очень тесное взаимодействие с аппаратурой и позволял генерировать компактный быстрый код. Однако довольно большой конечный размер таких проектов наводит на мысль, что они скорее всего были результатом коллективной разработки со всеми вытекающими отсюда проблемами объединения кода, написанного разными людьми. От участников таких проектов требовалась огромная самодисциплина, а также огромные усилия, затрачиваемые на документирование своей работы. Даже при соблюдении всех этих условий конечный результат нельзя было с легкостью перевести на систему с другим процессором — для этого требовалась практически полная переработка программы.
В начале 70-х один из сотрудников компании Bell Laboratories Кен Томпсон (Ken Thompson) разработал первую версию операционной системы UNIX. Она была написана на языке ассемблера для мини-компьютера DEC PDP-7. В попытке внедрения данной ОС во всей компании была проведена работа по ее переписыванию на языке высокого уровня. К тому времени уже существовал язык CPL (Combined Programming Language — комбинированный язык программирования), разработанный в середине 60-х Лондонским и Кембриджским университетами, который имел некоторые особенности, делавшие его полезным для использования в данной области. Язык BCPL (Basic CPL— базовый CPL) был более простым и в то же время более эффективным языком, разработанным в конце 60-х годов как инструмент для написания компиляторов. Язык В (по первой букве аббревиатуры BCPL) был разработан специально для переноса ОС UNIX на машину DEC PDP-11 и представлял собой, по существу, язык BCPL с измененным синтаксисом.
И BCPL, и В оперировали объектами только одного типа — машинным словом (16 бит для PDP-11). Отсутствие типизации в этих языках вызывало затруднения при работе с отдельными байтами и при реализации вычислений с плавающей точкой. Для решения этих проблем в 1972 году был разработан язык Си (вторая буква аббревиатуры BCPL), который поддерживал различные объекты, как целочисленные, так и с плавающей запятой. Это значительно увеличило его переносимость и гибкость. Весной 1973 года операционная система UNIX была полностью переписана на Си. Объем исходного кода составил около 10 000 строк на языке Си и 1000 строк на языке ассемблера, а итоговый размер получившейся программы увеличился на 30 % по сравнению с оригинальной версией.
Рис. 9.2. Этапы создания исполнимой программы в виде пирамиды
Хотя в момент своего появления язык Си был тесно связан с UNIX, уже через несколько лет появились компиляторы с этого языка, работающие практически под всеми известными ОС. Более того, изначально являясь языком системного программирования, сейчас он используется для написания самых различных прикладных программ, начиная с пакетов автоматизированного проектирования и заканчивая программным обеспечением интеллектуальных яйцеварок!
Через десять лет появилось официальное описание языка (первая редакция), выпущенное создателями языка Брайаном Керниганом (Brian W. Kemighan) и Денисом Ритчи (Dennis К. Ritchi) в виде книги «Язык программирования Си». О мощи и простоте языка свидетельствует тот факт, что за много лет он практически не изменился, избежав разделения на диалекты и новые версии. В 1983 году Национальный Институт Стандартизации США (American National Standards Institute — ANSI), признав возросшее влияние языка Си, основал комитет X3J11 для разработки современного и всестороннего определения этого языка. Итоговый документ, известный как ANSI С, был окончательно утвержден в 1990 году международной организацией по стандартизации (International Organization for Standardization — ISO).
Язык Си (а также его объектно-ориентированные потомки Си++ и Java) не только используется при разработке программного обеспечения для встраиваемых микроконтроллерных и микропроцессорных систем, но также, без сомнения, является наиболее популярным языком программирования общего применения. Завистники даже прозвали его «высокоуровневый ассемблер». Однако именно эта близость языка к ассемблеру вместе с возможностью использования в одной программе ассемблерного и высокоуровневого кода и является, в частности, преимуществом для встраиваемых систем.
Основными преимуществами использования языка высокого уровня для написания программного обеспечения встраиваемых устройств являются:
• Бóльшая продуктивность, в том смысле, что в среднем для написания, проверки и отладки одной строки кода требуется одно и то же время независимо от языка. По определению, одна строка на языке высокого уровня эквивалентна нескольким строкам ассемблерного текста.
• Синтаксис, более ориентированный на решение задач. За счет этого увеличивается производительность труда программиста и точность решения поставленных задач. Кроме того, код становится легче документировать, отлаживать, поддерживать и адаптировать к изменяющимся условиям.
• Лучшая переносимость программ на другие аппаратные платформы, хотя переносимость на все 100 % обеспечивается очень редко. За счет этого увеличивается время жизни программ, которые к тому же становятся относительно независимыми от аппаратной части.
• Более широкий круг пользователей, обусловленный более или менее аппаратной независимостью языка. В результате появляется экономический стимул создания многочисленных библиотек стандартных функций (математические библиотеки, библиотеки поддержки коммуникационных модулей и др.), которые можно повторно использовать во многих проектах.
Разумеется, использование языка высокого уровня не лишено и недостатков, особенно ярко проявляющихся при написании кода, который должен выполняться в системе на базе микроконтроллера или микропроцессора с ограниченными ресурсами:
• Полученный код имеет больший объем и часто выполняется медленнее аналогичной программы, написанной на ассемблере.
• Компилятор стоит намного дороже ассемблера. Стоимость профессиональных пакетов может достигать нескольких тысяч фунтов/долларов.
• Могут возникнуть затруднения при отладке, поскольку на целевом процессоре выполняется сгенерированный ассемблерный код, а не исходный код, написанный на языке высокого уровня. Средства, облегчающие отладку на высоком уровне, могут быть очень дорогими.
В качестве примера посмотрим на Программу 9.1.
Программа 9.1. Простая функция на Си
1: unsigned long summation(unsigned int n)
2: {
3: unsigned long sum = 0;
4: while(n > 0)
5: {
6: sum = sum + n;
7: -- n;
8: }
9: return sum;
10: }
В Программе 9.1 приведен код Си-функции (функции в Си — аналог подпрограмм), вычисляющей следующее соотношение:
Например, если n = 5, то мы получим
sum = 5 + 4 + 3 + 2+ 1.
В нашей реализации n — целое число, передаваемое в функцию, которая вычисляет и возвращает целое значение sum. Поставленная задача реализуется циклическим прибавлением n к предварительно обнуленному значению sum, с одновременным декрементированием n до нуля.
Давайте разберем эту функцию по строкам. Каждая строка помечена номером. Эти номера вставлены исключительно для удобства и не являются частью кода программы.
Строка 1: В этой строке объявляется имя функции (подпрограммы) summation и указывается, что она возвращает целое число типа unsigned long (в компиляторе, используемом нами в данной главе, этому типу соответствует 16-битное целое число без знака), а в качестве параметра n ожидает передачи целого числа типа unsigned int (8-битное целое число без знака).
Строка 2: Открывающая фигурная скобка означает начало блока. Как можно догадаться, у каждого начала должен быть свой конец, который в данном случае обозначается закрывающей фигурной скобкой. Хорошим тоном считается располагать тело блока с некоторым отступом (один символ табуляции) относительно фигурных скобок. Такое форматирование облегчает поиск парных скобок, т. е. начала и конца блока, однако компилятору нет никакого дела до того, какой стиль использует программист. В нашем случае соответствующая закрывающая скобка находится в строке 10. Между строками 2 и 10 заключено тело функции summation ().
Строка 3: В нашей функции используется только одна локальная переменная. В этой строке определяется ее имя (sum) и тип (unsigned long). В языке Си все объекты должны быть определены перед их использованием. Таким образом, компилятору передается информация о свойствах именованной переменной. В данном случае мы сообщаем компилятору о том, что под эту переменную необходимо выделить 16 бит и что она используется для хранения беззнаковых чисел. В этом же объявлении задается начальное значение переменной sum. Все выражение завершается символом точки с запятой, как и любой оператор.
Строка 4: При вычислении sum нам необходимо выполнять одну и ту же операцию до тех пор, пока n не станет равно нулю. В этой строке находится начальная часть оператора цикла while. В общем виде этот цикл выглядит следующим образом:
while(ИСТИНА)
{
делаем это;
делаем то;
делаем что-нибудь еще;
}
Тело цикла, т. е. совокупность операторов, расположенных между фигурными скобками (строки 5 и 8), выполняется до тех пор, пока результат выражения в круглых скобках будет не равен нулю (в языке Си любое значение, не равное нулю, считается истинным). Эта проверка осуществляется перед каждым проходом цикла. В нашем случае вычисляется выражение n > 0. Если это соотношение истинно, то число n прибавляется к sum. После этого n декрементируется, и цикл повторяется. В какой-то момент выражение n > 0 становится ложным, и управление передается на оператор, расположенный после закрывающей фигурной скобки (строка 9).
Строка 5: Открывающая фигурная скобка обозначает начало тела цикла while. В соответствии с принятым стилем операторы, составляющие тело цикла, записываются с отступом.
Строка 6: Вычисляется выражение в правой части оператора присваивания «=» (sum + n), и полученное значение заносится в переменную, расположенную слева от оператора присваивания, т. е. в sum. При прибавлении 8-битной переменной к 16-битной компилятор автоматически расширяет первую до 16 бит (см. Листинг 9.1, команды с адресами h’000E’…h’0011’).
Строка 7: Значение n декрементируется в результате выполнения оператора декремента —[120]. Записанное выражение эквивалентно выражению n = n — 1. Замечу, что большинство Си-программистов вставили бы эту операцию непосредственно в заголовок цикла: while (-n > 0).
Строка 8: Закрывающая скобка тела цикла while. Обратите внимание, что и открывающая (строка 5), и закрывающая скобки имеют одинаковый отступ от начала строки. Компилятор не обращает внимания на все эти изыски, это сделано исключительно для удобочитаемости программы и уменьшения вероятности возникновения ошибок.
Строка 9: Оператор return возвращает одну переменную обратно в вызывающую процедуру. В нашем случае такой переменной является значение sum. Компилятор проверяет, чтобы тип этой переменной соответствовал типу, указанному при объявлении функции, т. е. unsigned long. Возвращаемый параметр является результатом функции, т. е. функция может использоваться в качестве переменной в других выражениях наравне с обычными переменными. Так, если у нас есть функция sqr_root (), возвращающая значение квадратного корня из переданного в нее целого числа (см. Программу 9.2), то в результате выполнения выражения
х = sqr_root(y);
значение, возвращенное функцией sqr_root (у), будет присвоено переменной х.
Строка 10: Закрывающая фигурная скобка тела функции summation ().
Из Рис. 9.1 видно, что на выходе компилятора получается ассемблерный код, который впоследствии может быть ассемблирован и скомпонован с другими модулями[121] обычным образом. Чтобы проиллюстрировать это, в Листинге 9.1а приведен ассемблерный код, получившийся в результате компиляции Программы 9.1 кросс-компилятором компании Custom Computer Services (CCS)[122]. Это недорогой Си-компилятор (~125 долл.), который может быть интегрирован в ИСР MPLAB (см. Рис. 9.3). В файл листинга каждая строка исходного кода на Си выводится как комментарий вместе с соответствующим ей ассемблерным кодом. Для генерации этого демонстрационного листинга в исходный код программы было внесено два незначительных изменения:
• Имя функции было изменено на main (), поскольку любая программа на Си должна, по меньшей мере, содержать хотя бы функцию main (). Эта функция похожа на любую другую Си-функцию, но при ее компиляции компилятор генерирует различные команды инициализации программной среды (см. далее).
• Была добавлена директива #include для включения заголовочного файла, содержащего информацию, касающуюся конкретной модели микроконтроллера PIC16F627.
Листинг 9.1. Результат работы компилятора CCS
а) Ассемблерный листинг, сгенерированный компилятором CCS
CCS PCM С Compiler, Version 3.227, 6513 27-Oct-05 15:04
Filename: SUM.LST
ROM used: 25 words (2 %)
Largest free fragment is 999
RAM used: 8 (5 %) at main() level
8 (5 %) worst case
Stack: 0 locations
0000: MOVLW 00
0001: MOVWF 0A
0002: GOTO 004
0003: NOP
....................... #include <16£627.h>
....................... //////// Standard Header file for the PIC16F627 device
....................... #device PIC16F627
....................... #list
.......................
....................... unsigned long main(unsigned int n)
....................... {
0004: CLRF 04
0005: MOVLW IF
0006: ANDWF 03,F
0007: MOVLW 07
0008: MOVWF IF
........................ unsigned long sum = 0;
0009: CLRF 22
000А: CLRF 23
........................ while(n>0)
........................ {
000B: MOVF 21,F
000C: BTFSC 03.2
000D: GOTO 014
........................ sum = sum + n;
000E: MOVF 21,W
000F: ADDWF 22,F
0010: BTFSC 03.0
0011: INCF 23,F
......................... --n;
0012: DECF 21,F
......................... }
0013: GOTO 00B
......................... return sum;
0014: MOVF 22,W
0015: MOVWF 78
0016: MOVF 23,W
0017: MOVWF 79
.......................... }
..........................
..........................
0018: SLEEP
б) Исполняемый файл в формате Intel HEX
1000000000308A000428000084011F308305073077
100010009F00A201А301A108031914282108A20727
100020000318A30AA1030B282208F8002308F900EB
0200300063006В
00000001FF
;PIC16F627
Давайте посмотрим, как компилятор транслировал нашу программу.
unsigned long main(unsigned int n)
Точка входа в функцию main () всегда располагается по адресу вектора сброса h’000’. Сначала обнуляется регистр PCLATH (h’0A’), поскольку все последующие команды размещаются в младших адресах памяти программ. Далее управление передается по адресу вектора прерывания h’004’. Поскольку в данном случае прерывания не используются, компилятор разместил по этому адресу код функции main (). Функция main () начинается с очистки регистра FSR (h’004’). Затем сбрасываются биты IRP, RP1 и RP0 регистра STATUS, обеспечивая работу с 0-м банком. Наконец, специально для модели PIC16F627 путем установки трех младших битов регистра управления компаратором CMCON (h’1F’) выключается модуль аналогового компаратора (см. Рис. 14.6 на стр. 497).
Наличие этой фазы инициализации является отличительной особенностью функции main (). Благодаря ей выполнение «полезного» кода после сброса будет начинаться с определенного состояния микроконтроллера. Обычно программа на языке Си состоит из множества функций, но только в функции main () производится настройка окружения программы.
unsigned long sum = 0;
Компилятор CCS резервирует два байта под объект типа long. В данном случае младший и старший байты переменной main.sum были размещены в регистрах h’22’ и h’23’ соответственно. Для обнуления этих двух РОН компилятор сгенерировал две команды clrf:
clrf h’22’; Обнуляем младший байт суммы
clrf h’23’; Обнуляем старший байт суммы
while (n > 0) {
Компилятор выделил регистр h’21’ под однобайтный объект main.n. По-хорошему его значение должно задаваться вызывающей функцией. Оператор while реализуется проверкой main.n на ноль и переходом к оператору возврата return в случае, если это условие истинно.
movf h’21’,f; Проверяем на ноль
btfsc STATUS,Z; ЕСЛИ не ноль, ТО пропускаем команду
goto h’014’; ИНАЧЕ переходим к адресу h’014’ (return)
sum = sum + n;
Это выражение реализовано в виде операции прибавления однобайтного числа к двухбайтному следующим образом:
movf h’21’,w; Считываем main.n
addwf h’22’,f; Складываем с младшим байтом суммы
btfsc STATUS,С; Пропускаем команду, ЕСЛИ нет переноса
incf h’23’,f; ИНАЧЕ инкрементируем старший байт суммы
Большинство программистов на Си в этом случае воспользовались бы альтернативным оператором
sum +=n;
результатом которого является переменная sum, увеличенная на n.
--n;
Теперь декрементируем однобайтное число в регистре h’21’:
decf h’21’,f; Декрементируем main.n
В более сложных выражениях результат может зависеть от того, где располагается оператор декремента — (и аналогичный ему оператор инкремента ++) — перед объектом или после него. Когда оператор записывается перед объектом:
number = --n + 4;
то значение n декрементируется перед прибавлением к нему числа 4. В другом случае:
number = n-- + 4;
операция декрементирования выполняется после сложения.
В нашем примере положение оператора декремента не влияет на логику работы программы. Однако в последнем случае компилятор добавит дополнительную команду для перегрузки main.n в рабочий регистр перед его декрементированием, чтобы обеспечить возможность выполнения вычислений с использованием исходного значения main.n, которые могут иметь место.
}
Возврат к началу цикла while осуществляется переходом к командам проверки условия, которые размещаются, начиная с адреса h’00B’.
goto h’00B’
return sum;
В конце функции, возвращающей объект типа unsigned long, компилятор CCS заносит двухбайтное значение в регистры с фиксированными адресами h’78’:h’79’ (младший и старший байты). В нашем случае в эти регистры просто копируется содержимое регистров h’22’:h’23’, т. е. значение main.sum.
movf h’22’,w; Копируем младший байт суммы
movwf h’78’; в младший байт возвращаемого значения
movf h’23’,w; Копируем старший байт суммы
movwf h’79’; в старший байт возвращаемого значения
Обычно функции завершаются командой возврата, однако функция main () завершается командой sleep (см. стр. 308).
Итоговый файл в машинных кодах приведен в Листинге 9.16. Этот файл состоит всего из 24 команд, включая однократно выполняемые команды настройки окружения.
Программы на языке Си можно компилировать и симулировать непосредственно в ИСР MPLAB (см. стр. 264). На скриншоте, показанном на Рис. 9.3,
Рис. 9.3. Симуляция нашего примера в ИСР MPLAB версии 6.x
видны окна с исходным текстом на языке Си и сгенерированным ассемблерным кодом. Несмотря на то что симуляция осуществляется на уровне ассемблера, в окне с кодом на языке Си всегда выделяется строка, соответствующая симулируемой (и выделенной) в данный момент команде ассемблера[123]. В окне Watch выводится состояние двух объектов программы — unsigned int n (соответствует ассемблерному идентификатору main.n из списка идентификаторов) и unsigned long sum (main.sum). Идентификатор _RETURN_ генерируется самим компилятором для именования двух РОН с адресами h’78’:h’79’. Окно Watch можно использовать, как обычно, для контроля состояния объектов программы на Си. На Рис. 9.3 обе указанные переменные выводятся как в шестнадцатеричной, так и в десятичной системе. Как правило, последняя лучше подходит для отображения значений высокоуровневых объектов. Можно выбрать любое основание системы счисления — достаточно щелкнуть правой кнопкой мыши на значении переменной и выбрать пункт Properties контекстного меню. Также значение объекта можно изменить, сделав двойной щелчок на имени переменной (в нашем примере мы задали значение n, равное 100). Снимок экрана был сделан при достижении переменной n значения 71 в процессе декрементирования. После завершения симуляции n становится равным нулю, a sum — десятичному 5050.
Использование языка Си позволяет программисту работать со структурами, операторами и библиотечными функциями, свойственными современному языку высокого уровня. И все же при работе с микроконтроллерами программисту необходимо предоставить возможность легкого доступа к заданным ячейкам памяти данных и к отдельным их битам. Это позволит ему отслеживать состояние, а также изменять содержимое различных регистров специального назначения, таких как параллельные порты ввода/вывода. Благодаря этому процессор сможет взаимодействовать со своими встроенными периферийными устройствами и окружающей средой. Разумеется, эти операции можно выполнить и с помощью стандартных операторов языка Си. Однако во многих компиляторах, предназначенных для микроконтроллеров и микропроцессоров, реализованы нестандартные расширения языка, упрощающие такое «жонглирование» битами. Ну, а поскольку мы решили использовать компилятор CCS, то будем рассматривать именно его расширения.
В качестве примера рассмотрим подпрограмму, которая генерирует импульсы на 0-м выводе порта А (т. е. на выводе RA0) до тех пор, пока на выводе 7 порта В присутствует ВЫСОКИЙ уровень (см. стр. 152). Вот как это можно записать на стандартном языке Си (префикс Ох используется в языке Си для обозначения шестнадцатеричной системы)[124]:
#define PORTA *(unsigned int *)0x05
#define PORTB *(unsigned int *)0x06
while(PORTB & 0x80) /* Выделяем 7-й бит, проверяем, не равен ли он нулю */
{
PORTA = PORTA I 0x01; /* ИЛИ с 00000001; RAO —> ВЫСОКИЙ уровень */
PORTA = PORTA & 0xF7; /* И с 11111110; RAO —> НИЗКИЙ уровень */
}
Обратите внимание на использование парных символов /*…*/ для выделения комментариев.
Особое внимание необходимо уделить использованию указателей для именования абсолютных адресов в памяти данных, причем это касается даже опытных программистов на Си. Например, строка:
определяет имя PORTB в качестве синонима содержимого регистра h’06’. В компиляторе CCS тип unsigned int занимает один байт, однако в других компиляторах для хранения 8-битных данных используется либо unsigned short int, либо unsigned char. В дальнейшем именованный объект может использоваться как обычная глобальная переменная типа int.
В процедуре, приведенной выше, осуществляется логическое умножение (&) содержимого PORTB и константы Ь’10000000’, чтобы определить, установлен 7-й бит регистра или нет; если это так, результат выражения будет отличным от нуля (см. стр. 143). При этом будет выполнен очередной проход цикла while. В теле цикла для установки бита регистра PORTA используется операция ИЛИ «|» (см. стр. 144), а для сброса бита — операция И. Как можно увидеть из приведенного ниже ассемблерного кода, сгенерированного компилятором CSS версии 3, эти выражения были совершенно верно интерпретированы как операции установки и сброса единственного бита. В результате были корректно использованы команды btfss, bcf и bsf.
Если же в программе осуществляется сброс или установка нескольких битов, то используются соответствующие команды ior и and[125].
btfss 6,7; Проверяем 7-й бит регистра PORTB
goto NEXT; ЕСЛИ 0, ТО выходим из цикла
bsf 5,0; Выставляем на RA0 ВЫСОКИЙ уровень
bcf 5,0; Выставляем на RA0 НИЗКИЙ уровень
NEXT ... ...
Этот исполнимый код в точности соответствует тому, который мы написали бы при программировании на ассемблере.
В конкретном случае компилятора CCS для именования содержимого ячейки памяти данных можно было бы использовать нестандартную директиву #byte. Например, строка
#byte INTCON = 0x0В
присваивает регистру с адресом h’06’ имя INTCON. Аналогичным образом в компиляторе CCS можно именовать отдельные биты, используя директиву #bit. Так, строка
#bit INTF = 0х0В.1
присваивает имя 1-му биту регистра h’0B’. Причем если имя INTCON было уже определено, как показано выше, то эту же строку можно было бы записать как
#bit INTF = INTCON.1
Определенные таким образом объекты могут принимать значения только 0 и 1[126]. Таким образом, оператор INTF = 0; сбросит 1-й бит регистра INTCON.
Используя эти директивы компилятора, перепишем наш тестовый фрагмент:
#byte PORTA =5 /* Порт A — регистр h’05’ */
#byte PORTB = 6 /* Порт В — регистр h’06’ */
#bit RA0 = PORTA.0 /* 0-й бит регистра h’05’ — RA0 */
#bit RB7 = PORTB.7 /* 7-й бит регистра h’06’ — RB7 */
while(RB7)
{
RA0 =1; /* На выводе RA0 — ВЫСОКИЙ уровень */
RA0 =0; /* На выводе RA1 — НИЗКИЙ уровень */
}
При компиляции данного фрагмента будет сгенерирован точно такой же исполнимый код, как и раньше. Наличие в компиляторе подобных специальных средств гарантирует, что при их использовании будут сгенерированы эффективные команды манипуляций с битами. Однако все это достигается в ущерб переносимости программы. Начиная с этого момента, мы будем использовать указанную нотацию.
Для удобства все стандартные операторы языка Си приведены в Приложении В.
Примеры
Пример 9.1
Напишите на базе алгоритма, показанного на Рис. 6.11 (стр. 198), функцию, возвращающую квадратный корень из положительного 16-битного целого числа.
Решение
Приведя алгоритм из Примера 6.5 к структуре цикла while, получим следующее:
1. Обнулить счетчик цикла.
2. Присвоить 1 переменной i (магическое число).
3. Пока i меньше или равно заданному числу:
а) Вычесть i из числа.
б) Добавить 2 к i.
в) Инкрементировать счетчик цикла.
4. Вернуть счетчик цикла в качестве значения квадратного корня из числа.
В заголовке функции указывается ее имя (sqr_root) и задаются параметры, передаваемые в функцию, а также возвращаемое ею значение. Строка
unsigned int sqr_root(unsigned long number)
означает, что функция возвращает значение типа unsigned int и ожидает передачи одного объекта типа unsigned long, который внутри функции будет известен под именем number. Собственно код функции приведен в Программе 9.2. Поскольку квадратный корень из 16-битного числа поместится в одном байте, счетчик цикла был объявлен как переменная типа unsigned int. А магическое число i будет в 2 раза больше числа count, поэтому оно объявлено как unsigned long. Одновременно с объявлением локальных переменных можно также задавать их начальные значения.
Программа 9.2. Функция вычисления квадратного корня
unsigned int sqr_root(unsigned long number)
{
unsigned int count = 0;
unsigned long i = 1;
while(number >= i)
{
number = number — i;
i = i + 2;
count++;
}
return count;
}
Цикл while выполняется до тех пор, пока значение числа number не станет меньше i; начиная с этого момента, любая последующая операция вычитания приведет к получению отрицательного результата. Количество проходов цикла является искомым значением квадратного корня и возвращается в вызывающую программу.
При использовании компилятора CS версии 3.18 размер полученной функции составил 29 команд, тогда как исходная реализация этой функции на языке ассемблера (Программа 6.12 на стр. 199) имеет 21 команду. Таким образом, эффективность компилятора составляет 72 %.
Пример 9.2
Термопара К-типа в диапазоне температур 0…1300 °C характеризуется соотношением
t = 7.550162+ 0.0738326∙v) + 2.8121386∙10-7v2,
где t — температура спая в градусах Цельсия, а v — генерируемая ЭДС, находящаяся в диапазоне 0…52.398 мкВ, представленная 14-битным беззнаковым двоичным числом. Напишите функцию, которая будет принимать в качестве входного параметра 14-битное выходное значение аналого-цифрового преобразователя и возвращать измеренное термопарой целочисленное значение температуры в градусах Цельсия.
Решение
Текст нашей функции, названной thermocouple (), приведен в Программе 9.3. Эта функция имеет один параметр emf типа unsigned long (16 бит) и возвращает также 16-битное значение. Локальная переменная temperature определена в 3-й строке как число с плавающей точкой[127]. Это необходимо для поддержки сложных математических вычислений с дробными числами, выполняющихся в 6-й строке. Поскольку мы договорились, что значение имеют только 14 младших битов параметра emf, в 5-й строке выполняется логическое умножение 16-битной переменной и константы h’3FFF’ (0x3FFF) для сброса двух старших битов. И наконец, в 8-й строке переменная temperature типа float приводится к типу unsigned long и возвращается в вызывающую программу.
Программа 9.3. Линеаризация характеристики термопары К-типа
unsigned long thermocouple(unsigned long emf)
{
float temperature; unsigned long outcome;
emf = emf & 0x3FFF; /* Сбрасываем два старших бита */
temperature = 7.550162 + 0.073832605*(unsigned long)emf + 2.8121386e-7*emf*emf;
outcome = (unsigned long)temperature;
return outcome;
}
Итоговый код, скомпилированный для микроконтроллера PIC семейства среднего уровня, занимает 653 слова памяти программ — это около 2/3 всего объема памяти программ модели PIC16F627! По этой причине во встраиваемых микроконтроллерах везде, где только возможно, используется арифметика с фиксированной точкой.
Пример 9.3
На стр. 255 была приведена программа вычисления среднеквадратичного значения — √(NUM_12+ NUM_22). Напишите функцию на языке Си, вычисляющую это выражение и возвращающую 8-битное значение. В функцию должны передаваться две 8-битные переменные — num_1 и num_2.
Решение
В Программе 9.4 для хранения суммы квадратов двух 8-битных переменных используется локальная переменная sum типа unsigned long. Операция возведения в квадрат реализована с помощью оператора умножения «*» вместо использования функции возведения в квадрат, как это было сделано в Программе 8.3 (стр. 258). Однако, чтобы результат арифметических операций соответствовал 16-битной переменной sum, программист должен дать понять компилятору, что необходимо использовать 16-битную арифметику. Для этого каждый из операндов явно приводится к типу unsigned long с помощью конструкции (unsigned long). Функция, текст которой приведен в Программе 9.2, используется для вычисления квадратного корня из 16-битного целого sum и вызывается в 6-й строке функции variance (). Значение, возвращаемое функцией, присваивается локальной переменной rms. При использовании компилятора CCS для реализации этой задачи требуется 94 машинных команды. Ассемблерный вариант этой функции состоит из 62 команд, соответственно эффективность составляет 66 %.
Программа 9.4. Вычисление среднеквадратичного значения двух переменных
unsigned int variance(unsigned int num_1, unsigned int num_2)
{
unsigned long sum;
unsigned int rms;
sum = (unsigned long)num_1*num_1 + (unsigned long)num_2*num_2;
rms = sqr(sum); r
eturn rms;
}
Пример 9.4
Напишите функцию, выполняющую сдвиг содержимого регистра h’20’ справа налево и выставляющую выдвигаемый бит на вывод RA0. При выдаче очередного бита на выход RA0 на выводе RA1 должен формироваться импульс
Решение
В Программе 9.5 для восьмикратного сдвига содержимого регистра h’20’ (названного DATUM) вправо используется оператор цикла for (). Сам сдвиг реализуется с помощью оператора Си «>>» (сдвиг вправо). Перед очередным сдвигом вывод RA0 (названный SER_OUT) устанавливается или сбрасывается в зависимости от значения 0-го бита (LSB) переменной DATUM с использованием условного оператора if-else. В любом случае на выводе RA1 (названном CLOCK) формируется одиночный импульс. Кстати, написанная нами функция реализует простейший последовательный канал синхронной передачи данных (см. главу 12).
Программа 9.5. Простейшая функция передачи по последовательному каналу
#byte DATUM = 0x20 /* Регистр h’20’ */
#bit LSB = DATUM.0 /* 0-й бит регистра h’20’ */
#byte PORTA =5 /* Порт А — регистр h’05’ */
#bit SER_OUT = PORTA.0 /* 0-й бит порта */
#bit CLOCK = PORTA.1 /* 1-й бит порта */
void put_char (void) /* Параметры и возвращаемое значение отсутствуют (void) */
{
int i; /* Счетчик цикла */
for i=0; i<8; i++) /* ВЫПОЛНЯЕМ восемь раз */
{
if (LSB) /* ЕСЛИ 0-й бит равен 1,
SER_OUT = 1; /* выставляем на RA0 ВЫСОКИЙ уровень */
else
SER_OUT = 0; /* ИНАЧЕ выставляем на RA0 НИЗКИЙ уровень */
CLOCK = 1; /* Выдаем на RA1 ВЫСОКИЙ уровень, */
CLOCK = 0; /* а затем НИЗКИЙ */
DATUM = DATUM >> 1; /* Сдвигаем байт данных на один разряд вправо */
}
}
Пример 9.5
Напишите Си-программу для упаковщика консервных банок из Примера 7.1 (стр. 224), рассчитанную на компилятор CCS. В программе должны использоваться прерывания.
Решение
Как и в ассемблерном варианте, в Программе 9.6 имеется две функции. В основной функции main () сначала используется встроенная функция компилятора set_tris_a () для переключения 0-й линии порта А в режим выхода. Затем с помощью другой встроенной функции enable_interrupts () устанавливаются биты маски прерываний INTE и GIE (см. Рис. 7.3 на стр. 213). После этого сбрасывается 0-й бит порта А, гарантируя наличие НИЗКОГО уровня на выводе RA0 при старте программы.
Программа 9.6. Программа автоматического упаковщика банок
#include <16f84.h>
#use delay (clock=8000000) /* Сообщаем компилятору о тактовой частоте (8 МГц) */
#bit RA0 =5.0 /* 0-й бит порта А */
/* Объявляем функцию can_count(), которая не имеет параметров и не возвращает значения */
void can_count(void);
int EVENT, BATCH; /* Две глобальные переменные */
void main(void)
{
set_tris_a(0xFE); /* Конфигурируем RA0 как выход */
enable_interrupts(INT_EXT); /* Устанавливаем бит INTE регистра INTCON */
enable_interrupts(GLOBAL); /* Устанавливаем бит GIE регистра STATUS */
RA0 =0; /* Выставляем на RA0 НИЗКИЙ уровень */
while (1) /* Бесконечный цикл */
{
if (BATCH) /* Если переменная BATCH не равна нулю, */
{
BATCH =0; /* ТО обнуляем ее */
RA0 =1; /* и формируем на выводе RA0 */
delay_ms(1); /* импульс длительностью 1 мс */
RA0 = 0;
}
}
}
/ ******************************************
/* Процедура обработки прерывания */
#int_ext /* Обработчик внешнего прерывания */
void can_count(void)
{
if(++EVENT == 24) /* Инкрементируем счетчик, и ЕСЛИ он равен 24, */
{
EVENT=0; /* ТО обнуляем его */
ВАТСН++; /* и заносим в переменную BATCH ненулевое значение */
}
}
В теле бесконечного цикла непрерывно проверяется значение переменной BATCH. При ненулевом значении (ИСТИНА) переменная сбрасывается и на выводе RA0 формируется положительный импульс длительностью 1 мс. Использование встроенной функции компилятора delay_ms () является самым простым способом генерации точных задержек длительностью до 65 535 мс в этой реализации языка. Чтобы воспользоваться указанной возможностью, программист должен сообщить компилятору значение тактовой частоты микроконтроллера. Для формирования более коротких задержек можно использовать функции delay_us () и delay_cycles (). При работе с компиляторами, не имеющими подобных нестандартных функций, можно использовать собственные подпрограммы задержки, написанные на ассемблере.
Функция can_count () объявлена как процедура обработки внешнего прерывания с помощью директивы #int_ext (). Аналогичные директивы предусмотрены для всех источников прерываний. Компилятор самостоятельно генерирует код для поддержки прерываний от нескольких источников, а также для сохранения и восстановления контекста.
Поскольку функция can_count () является обработчиком прерывания, в нее нельзя обычным образом передать параметры, о чем сигнализирует ключевое слово void. Вместо этого все контролируемые и изменяемые переменные должны быть объявлены глобальными. В нашей программе обе переменные BATCH и EVENT объявлены вне функции и, таким образом, видны всем функциям, как обработчику прерывания, так и фоновой.
В функции can_count () сначала инкрементируется переменная EVENT — оператор ++ записан перед переменной. Если получившееся значение равно 24, то счетчик обнуляется, а переменная BATCH инкрементируется. Таким образом, фоновая программа извещается о том, что упаковка из 24 банок уже заполнена.
По сравнению с 40 командами программы, написанной на ассемблере, при использовании языка высокого уровня размер программы получается равным 94 командам. Однако в последнем случае поддержка прерываний была бы более гибкой при необходимости обработки запросов от нескольких источников прерываний, а функция задержки позволяла бы формировать более длинные интервалы, которые вполне могут возникнуть в реальной жизни.
Пример 9.6
Массив однотипных объектов определяется в Си с помощью конструкции fred[n], где fred — имя массива (в действительности этому идентификатору соответствует адрес первого элемента массива), а n — количество элементов массива. Так, обнаружив в тексте программы строку
unsigned int fred[16]
компилятор зарезервирует в памяти данных 16 регистров, расположенных подряд.
При объявлении массива можно задать для каждого элемента начальное значение. Например, запись
unsigned int svn_seg[10] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};
определяет массив из десяти байтов, содержащих коды управления 7-сегментным индикатором, приведенные на Рис. 6.8 (стр. 183).
Эти десять значений от svn_seg [0] до svn_seg [9] будут размещены в десяти последовательно расположенных регистрах. Большинство микроконтроллеров PIC имеют достаточно ограниченный размер памяти данных, и в таком случае, когда значения не изменяются в дальнейшем, имеет смысл разместить эти десять констант в ПЗУ программ в виде набора команд retlw <константа>, подобно тому, как это было показано в Программе 6.6 на стр. 184. Для этого достаточно к объявлению массива добавить ключевое слово const:
unsigned int const svn_seg[10] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};
Используя описанные способы, напишите программу, реализующую электронный аналог игральной кости с семью светодиодами, подключенными к старшим семи выводам порта В, как показано на Рис. 9.4, а. Основная программа будет просто инкрементировать глобальную переменную целого типа с максимально возможной скоростью. При нажатии на кнопку, подключенную к выводу INT/RB0, программа будет переходить к обработке прерывания (см. Пример 9.5). Процедура обработки прерывания отобразит одну из шести комбинаций светодиодов, а после 10 с выключит светодиоды, чтобы сэкономить энергию батареи. Использование часового кварца частотой 32 768 Гц также уменьшит потребление схемы, как можно увидеть на Рис. 10.3 (стр. 306).
Решение
Коды управления СИД задаются в Программе 9.7 в виде глобального массива из шести констант в соответствии с Рис. 9.4, б. Значения этих кодов сдвинуты на один бит влево по сравнению со значениями, приведенными в таблице истинности, — для их вывода через старшие семь линий порта В.
Рис. 9.4. Коды управления светодиодами электронной игральной кости
Основная программа просто инкрементирует однобайтную переменную throw и сбрасывает ее в ноль, когда она становится больше пяти. Таким образом, реализуется программный счетчик по модулю 6, т. е. 0, 1, 2, 3, 4, 5, 0….
Компилятор CCS версии 3.18 генерирует функцию main(), состоящую из восьми команд, включая несколько команд перехода и пропуска. В результате при указанной тактовой частоте инкрементирование переменной происходит около 1000 раз в секунду. Такая частота обеспечивает практически случайный выбор одного из шести значений по нажатию кнопки, подключенной к выводу INT.
Программа 9.7. Электронная игральная кость
#include <16f84.h>
#use delay (clock=32768)
#byte PORTB = 6
void die(void);
unsigned int const array(6] = (0x7e, 0xec, 0x6c, 0xc8, 0x48, 0x80};
unsigned int throw;
void main(void)
{
set_tris_b(0x01);
enable_interrupts(INT_EXT); /* Устанавливаем INTE в 1 */
enable_interrupts(GLOBAL); */ Устанавливаем GIE в 1 */
while(1) /* Бесконечный цикл */
{
PORTB = 0; /* Выключаем светодиоды */
if(++throw > 5) /* Инкрементируем (по модулю б) */
throw=0;
}
}
#int_ext /* Обработчик внешнего прерывания */
void die(void)
{
PORTB = array[throw]; /* Отображаем n-ю комбинацию точек */
delay_ms(10000); /* в течение 10000 мс */
}
Функция обработчика прерывания die () копирует n-й элемент нашего массива констант в регистр PORTB и, прежде чем вернуться в основную программу, приостанавливает выполнение программы на 10 с. Поскольку в функции main () регистр PORTB постоянно сбрасывается, после возврата из обработчика индикатор будет очищен.
Вопросы для самопроверки
9.1. Для управления светодиодами игральной кости из Примера 9.6 требуется семь линий параллельного порта, а для некоторой электронной игры требуется две такие кости. Посмотрите внимательно на Рис. 9.4 — можно ли уменьшить количество линий, используемых для управления одной костью, до четырех?
9.2. В рамках реализации некоторой электронной игры необходимо написать функцию, возвращающую следующее псевдослучайное число из 127 чисел, сгенерированных генератором, показанным на Рис. 6.12 (стр. 206). В функцию передается текущее, а возвращается следующее число последовательности. Предполагается, что передаваемое в функцию значение отлично от нуля. Как можно доработать функцию, чтобы она выдавала через порт В всю последовательность случайных чисел, начиная с заданного числа?
9.3. Цифровой термометр на базе микроконтроллера PIC показывает температуру от 0 до 100 °C. Чтобы это устройство можно было продавать в США, в термометре следует предусмотреть возможность отображения значения температуры в градусах по шкале Фаренгейта. Напишите для этого термометра функцию, выполняющую перевод целого значения из шкалы Цельсия в шкалу Фаренгейта. Соответственно, передаваемое и возвращаемое значения должны иметь тип unsigned int. Зависимость между шкалами выражается следующим образом:
F = (C x 9)/5 + 32.
Во избежание ошибок переполнения следует использовать 16-битную арифметику.
9.4. Индикатор холодной погоды на приборной панели автомобиля представляет собой три светодиода, подключенных к трем младшим линиям порта А. Ко 2-й линии подключен красный СИД, который включается, если температура снаружи ниже 34°F. К 1-й линии подключен желтый СИД (Температура ниже 40°F), а к 0-й линии подключен зеленый СИД. Предположим, что соответствующие линии порта уже сконфигурированы как выходы и что СИД включается при подаче на вывод НИЗКОГО уровня. Напишите функцию для управления этими светодиодами, в которую передается значение температуры F.