PIC-микроконтроллеры. Все, что вам необходимо знать

Катцен Сид

Часть II

ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ

 

 

В 1-й части мы познакомились с понятием гарвардской архитектуры и разработали собственный примитивный компьютер BASIC. Хотя этот компьютер существует только в нашем воображении, он был разработан с оглядкой на реальные микроконтроллеры, являющиеся предметом обсуждения данной книги.

Вторая часть книги посвящена главным образом программным аспектам микроконтроллеров, выбранных нами для изучения, — микроконтроллеров PIC® среднего уровня компании Microchip. В этой части мы рассмотрим следующие вопросы:

• Внутренняя структура микроконтроллеров PIC среднего уровня.

• Набор команд.

• Способы адресации команд.

• Разработка программ с использованием интегрированной среды разработки MPLAB®.

• Трансляция с языка ассемблера.

• Подпрограммы и модульный принцип разработки программ.

• Обработка прерываний.

• Язык высокого уровня Си и компиляция написанных на нем программ.

 

Глава 4

Микроконтроллер

PIC16F84

В том же году, когда Microchip приобрела у компании General Instrument интеллектуальные права на микросхему периферийного интерфейсного контроллера (PIC), было разработано первое семейство 8-битных микроконтроллеров с гарвардской архитектурой. Это начальное (или базовое) семейство PIC16C5XX, как и более современные семейства того же уровня PIC10FXXX и 12СХХХ, имело 33 команды, 12-битную память программ, параллельные порты ввода/вывода и один 8-битный таймер/счетчик. Как и во всех последующих семействах микроконтроллеров PIC, исполнительный блок обрабатывал данные побайтно, что соответствует 8-битной организации памяти данных.

К 1992 году на свет появилось среднее семейство PIC16CXXX. Микроконтроллеры этого семейства имели уже 14-битную память программ, что облегчало доступ к памяти данных больших объемов. По сравнению с младшими семействами появились две новые команды. Был значительно расширен базовый набор периферии — добавились такие устройства, как 16-битные таймеры, модуль АЦП, последовательные порты. Также была добавлена поддержка прерываний.

В оставшейся части книги, за исключением главы 16, мы будем рассматривать микроконтроллеры именно этого семейства. И только в 16-й главе мы коснемся старшего семейства PIC18XXXX, появившегося на рынке в 1999 году. Микроконтроллеры данного семейства получили 16-битное ядро и 42 дополнительные команды, большинство из которых направлено на поддержку компиляторов с языков высокого уровня.

В этой главе мы познакомимся с ядром микроконтроллеров семейства среднего уровня с точки зрения его архитектуры. Прочитав эту главу, вы:

• Познакомитесь с внутренней структурой микроконтроллеров PIC среднего уровня с гарвардской архитектурой.

• Разберетесь в назначении, структуре и распределении памяти программ и памяти данных.

• Поймете идею деления памяти данных на банки и их взаимосвязь с состоянием управляющего бита RP0 регистра STATUS.

• Сможете интерпретировать биты регистра STATUS, которые управляют страницами памяти, а также содержат флаги С, DC и Z.

• Поймете, как можно манипулировать содержимым счетчика команд и какую роль в этом играет регистр специального назначения PCLATH.

• Узнаете зависимость между фазами тактового сигнала и внутренней последовательностью микроопераций.

• Познакомитесь с основными периферийными модулями на примере модели PIC16F84.

С точки зрения программирования все устройства с одним и тем же ядром полностью идентичны. Более того, все ядра, используемые в микроконтроллерах PIC, имеют очень много общего. С аппаратной точки зрения блоки выборки и исполнения незначительно отличаются, в частности, поддерживаемым объемом памяти и построением схем сброса.

Отдельные представители семейства имеют схожий базовый набор портов и модулей периферийных устройств, однако отличаются в части дополнительных возможностей ввода/вывода. Например, в микроконтроллере PIC16F73 имеется 5-канальный 8-битный аналого-цифровой преобразователь, в PIC16F874/7 — уже 8-канальный 10-битный аналого-цифровой преобразователь, а в PIC16F627/628 — аналоговый компаратор и 16-битный таймер/счетчик. Однако, несмотря на эти отличия, существует множество идентичных модулей, использующихся как в моделях среднего уровня, так и в моделях с расширенным ядром. К рассмотрению этих модулей мы вернемся в 3-й части книги.

В данной главе мы в основном будем рассматривать ядро микроконтроллеров семейства среднего уровня. Одним из первых представителей этого семейства (1994) был 18-выводной микроконтроллер PIC16C83/4. Он быстро завоевал популярность, поскольку был первым PIC-микроконтроллером, в котором память программ была реализована в виде электрически стираемого ППЗУ (см. Рис. 2.13 на стр. 42). То есть для стирания кристалла не требовался источник ультрафиолетового излучения, и весь процесс перепрограммирования занимал всего несколько секунд. Кроме того, эти модели имели блок энергонезависимой памяти данных объемом 16 байт для длительного хранения данных. В течение нескольких лет эта модель оставалась уникальной среди всей линейки микроконтроллеров PIC.

В 1996 году на смену PIC16C83/4 пришла модель PIC16F83/4, которая имела FLASH-память программ и лучшие параметры, например более высокую рабочую тактовую частоту.

Несмотря на то что для новых разработок рекомендуется использовать микроконтроллер PIC16F627/8, появившийся в 2002 году, основное его отличие от предшественника заключается в расширенном наборе периферийных модулей, к рассмотрению которых мы приступим в третьей части книги. Поэтому для простоты изложения большинство материалов данной части будет базироваться на модели PIC16F84. Однако при необходимости мы будем ссылаться и на другие модели семейства.

В упрощенном виде архитектура микроконтроллера PIC16F84 показана на Рис. 4.1. Модель PIC16F83 отличается только уменьшенным объемом памяти программ (512 команд). На первый взгляд эти микроконтроллеры имеют очень сложную архитектуру, однако в действительности она ненамного сложнее архитектуры нашего компьютера BASIC (см. Рис. 3.4 на стр. 64). Основное отличие заключается в том, что к внутренней шине данных памяти данных, помимо ЦПУ, подключены периферийные модули. Чтобы лучше понять материал этой главы, рекомендую еще раз просмотреть материал, касающийся указанного компьютера. Вообще говоря, в микроконтроллерах PIC реализована гарвардская архитектура с ее раздельными модулями памяти программ и данных, в которой периферийные устройства отображены на адресное пространство памяти данных. То есть с точки зрения программы все эти устройства расположены в памяти данных. То же касается и различных служебных регистров, таких как регистры статуса и управления, а также счетчика команд.

Рис. 4.1. Архитектура микроконтроллера PIC16F84

Блок выборки

Блок выборки, показанный отдельно на Рис. 4.2, предназначен для выборки команд из памяти программ и загрузки их в конвейер. Адрес каждой извлекаемой команды задается счетчиком команд. В свою очередь код операции, выделенный из кода команды, подается на вход дешифратора команд, который активирует в требуемой последовательности соответствующие узлы исполнительного блока.

Рис. 4.2. Блок выборки микроконтроллера PIC16F84

Память программ

Основным элементом блока выборки является память программ. Программное обеспечение во встраиваемых системах фиксировано, поскольку после включения питания микроконтроллер должен сразу же приступить к выполнению своих задач, не тратя время на загрузку программы. Поэтому память программ, как правило, реализуется в виде ПЗУ какого-либо типа. В большинстве микроконтроллеров PIC используется многократно-программируемое ПЗУ; если в обозначении модели присутствует буква «F», то это FLASH-память. В памяти программ микроконтроллера PIC16F84 может храниться до 1024 команд, размер каждой из которых составляет 14 бит (см. Рис. 3.5 на стр. 68). В новых микроконтроллерах семейства среднего уровня размер памяти программ колеблется от 1024 (например, PIC16F627) до 8192 (PIC16F876/7) команд.

Счетчик команд

Счетчик команд (Program Counter — PC) используется для адресации, или указания, считываемой из памяти программ команды. Этот 13-битный регистр обычно инкрементируется после каждой выборки, функционируя как двоичный счетчик. Однако, как мы с вами увидим в следующей главе, существует ряд команд, таких как команда goto, выполнение которых вызывает переход к другому месту в памяти программ. Соответственно, нормальное функционирование счетчика команд нарушается. Кроме того, программист может напрямую обращаться к счетчику команд через память данных (см. Рис. 4.8).

Несмотря на то что показанный на Рис. 4.3 счетчик команд является 13-битным регистром, способным адресовать 213 = 8192 команды, в микроконтроллере PIC16F84 задействованы только младшие 10 бит (210 = 1024). Если программа попытается перейти по адресу, находящемуся вне этого диапазона, то адрес «свернется» по границе памяти программ. В любом случае счетчик команд очищается при сбросе микроконтроллера, т. е. первая выполняемая команда всегда располагается по адресу h’000’. Этот адрес называется вектором сброса.

Рис. 4.3. Память программ микроконтроллера PIC16F84

Конвейер

Конвейер реализован в виде двух 14-битных регистров. В первом регистре хранится команда, только что считанная из памяти программ по адресу, определяемому значением счетчика команд. Второй регистр подключен к дешифратору команд и соответственно содержит исполняемую в данный момент команду. Такое решение позволяет осуществлять выборку команды одновременно с исполнением команды, выбранной в предыдущем цикле. Естественно, при этом предполагается, что последовательность выполнения команд имеет линейный характер. Для команд, выполняющих переход к другому месту памяти программ, команда, находящаяся на вершине конвейера, должна быть заменена на другую. Этот процесс называется сбросом конвейера и увеличивает время исполнения команды на один цикл. Как видно из Рис. 4.4, обычно команда выполняется за один машинный цикл.

Рис. 4.4. Фазы машинного кода

Дешифратор команд

Дешифратор команд представляет собой логическую схему, которая декодирует каждое поле 14-битного слова команды и управляет передачей соответствующих адресов и данных на требуемые входы исполнительного блока, а также конфигурирует АЛУ.

Все модели микроконтроллеров PIC имеют встроенный генератор, задающий последовательность выполнения внутренних микроопераций в соответствии с сигналами, поступающими от дешифратора команд. В качестве времязадающего элемента обычно используется внешний кварцевый резонатор, подключаемый к выводам OSC1 и OSC2 микроконтроллера (Рис. 4.2). Именно этот резонатор определяет тактовую частоту микроконтроллера fOSC. Более подробно о генераторе мы поговорим в главе 10. Максимальная тактовая частота большинства моделей среднего уровня составляет 20 МГц, однако в некоторых ранних моделях частота не могла превышать 10, а то и 4 МГц. Минимальная тактовая частота ничем не ограничена.

Частота тактового генератора делится на четыре с помощью схемы, показанной на Рис. 2.25 (стр. 54), чтобы получить четыре не перекрывающихся тактовых сигнала. Каждый из этих сигналов используется схемой дешифратора для управления внутренними узлами процессора в требуемой последовательности. Соответственно длительность одного машинного цикла равна четырем периодам внешнего сигнала fOSC (см. Рис. 4.4). Таким образом, при использовании резонатора с частотой 4 МГц частота машинных циклов составляет fOSC/4, или один миллион циклов в секунду, что соответствует периоду машинного цикла, равному 1 мкс.

В зависимости от текущей фазы машинного цикла блок выборки выполняет следующие операции:

Q1: Инкрементирование счетчика команд и выставление его содержимого на шину адреса памяти программ.

Q4: Считывание кода команды с шины данных памяти программ в регистр команд IR1 и одновременно с этим пересылка предыдущей команды по конвейеру в регистр команд IR2, подключенный к дешифратору команд.

Стек

Стек представляет собой отдельный блок из восьми 13-битных регистров, который подключен к счетчику команд. В 6-й главе мы увидим, что стек используется для хранения предыдущего значения счетчика команд, т. е. «запоминает» точку возврата при вызове подпрограммы.

Исполнительный блок

Исполнительный блок (Рис. 4.5) осуществляет извлечение данных из памяти данных или непосредственно из кода команды и выполняет обработку этого значения, используя АЛУ. Режим работы АЛУ определяется сигналами, формируемыми дешифратором команд. Результат помещается либо в рабочий регистр W, либо обратно в память данных, перезаписывая исходное значение.

Арифметико-логическое устройство (АЛУ)

Основным элементом исполнительного блока является АЛУ (см. Рис. 2.10 на стр. 39), которое осуществляет обработку данных, поступающих из двух источников. Одним из этих источников является 8-битный рабочий регистр W. В качестве другого источника может выступать:

• Регистр данных, указанный в команде. Например, команда addwf h’20’,f прибавляет содержимое рабочего регистра к содержимому регистра h’20’.

• Константа, являющаяся частью слова команды (см. Рис. 3.6 на стр. 69). К примеру, команда addlw 5 прибавляет число 5 к содержимому рабочего регистра.

В первом случае результат может быть помешен либо обратно в память данных, если бит адресата равен 0 (см. Рис. 3.5 на стр. 68), либо в рабочий регистр, если этот бит равен 1, например в случае команд типа addwf h'20',w

Рис. 4.5. Исполнительный блок микроконтроллера PIC16F84

Регистр STATUS

Этот регистр, тесно связанный с АЛУ, содержит три бита-флага, которые используются для информирования программы о результате выполнения команды. В частности, не было ли переноса при выполнении операции сложения.

Флаг переноса

Бит 0 регистра STATUS используется в качестве флага переноса С (от англ. carry — перенос). Основное его назначение — хранение бита переноса предыдущей операции сложения. При операциях вычитания значение этого бита соответствует инвертированному биту заема (см. Пример 4.2). Например, 24–12 = 12В ¯1, а 12–24 = 88B ¯0. Флаг С также используется в операциях сдвига, как показано на Рис. 5.13 (стр. 148). Пометка «R/W?» на Рис. 4.6 отражает тот факт, что этот бит доступен как для чтения (R), так и для записи (W), а после сброса по питанию его состояние не определено (при любом другом сбросе состояние этого бита не изменяется).

Рис. 4.6. Формат регистра STATUS микроконтроллера PIC16F84

Флаг десятичного переноса

Бит 1 регистра STATUS используется в качестве флага десятичного переноса DC (от англ. digit carry). Этот флаг функционирует точно так же, как и обычный флаг С, только содержит бит переноса из младшего полубайта в старший, т. е. из 3-го бита в 4-й. Аналогично флагу С, флаг DC содержит дополнение бита заема из 3-го бита в 4-й.

Информация о наличии или отсутствии переноса между полубайтами полезна при работе с данными, представленными в двоично-десятичном коде (посмотрите хотя бы на Пример 4.5). При использовании этого кода в каждом полубайте хранится 4-битное представление десятичного числа от 0 до 9 (см. стр. 20), и флаг десятичного переноса указывает на возникновение переноса между десятичными разрядами.

Флаг нуля

Бит 2 регистра STATUS используется в качестве флага нуля Z (от англ. zero — ноль). Этот бит устанавливается, если результат выполнения команды равен нулю, и сбрасывается в противном случае.

В отличие от многих других микроконтроллеров, в микроконтроллерах PIC отсутствуют команды, специально предназначенные для сброса или установки флагов, подобные команде sec, имеющейся в микроконтроллерах семейств 6800/5/11 компании Motorola.

Тем не менее, как видно из Рис. 4.7, к регистру STATUS можно обращаться как к регистру данных с адресом h’03’. Поэтому любая команда, которая изменяет содержимое регистра данных, может в принципе использоваться для изменения состояния флагов. Однако существует определенная проблема, заключающаяся в том, что многие из этих команд сами по себе влияют на состояние одного или нескольких флагов (см., к примеру, Табл. 5.1 на стр. 129) и таким образом переопределяют значение, являющееся результатом выполнения команды. Если мы, к примеру, попробуем сбросить все флаги с помощью команды очистки регистра сlrf 3 (см. Табл. 5.2 на стр. 131), то в результате флаг Z окажется установленным в 1, сообщая о том, что результат операции равен нулю! Для изменения отдельных битов регистра STATUS рекомендуется использовать команды сброса/установки бита регистра данных bcf и bsf (см. Табл. 5.2), поскольку сами по себе эти команды не влияют на состояние флагов. Например, команда bsf 3,0 (установить 0-й бит регистра h’03’) эквивалентна команде sec, а команда bcf 3,2 (сбросить 2-й бит регистра h’03’) сбросит флаг Z.

Назначение более специализированных флагов, размещающихся в 3-м и 4-м битах, будет описано в последующих главах. В общих чертах флаг  (Power Down) сбрасывается при выполнении команды sleep, которая используется для выключения тактового генератора и перевода микроконтроллера в режим ожидания с малым потреблением (< 1 мкА). Флаг  (Time Out) сбрасывается при наступлении тайм-аута сторожевого таймера. Оба этих флага доступны только для чтения — их состояние не может быть изменено программно. После сброса указанные флаги устанавливаются в 1.

Все эти биты называются флагами или, иногда, семафорами, поскольку они сигнализируют о том или ином результате выполнения команды. Бит 5 этого регистра несколько отличается от остальных, так как на его состояние не влияют происходящие события. Наоборот, флаг RP0 используется программистом для изменения состояния процессора. Чтобы проиллюстрировать назначение этого бита-переключателя, нам потребуется более подробно изучить структуру памяти данных микроконтроллера PIC16F84.

На Рис. 4.7 приведена упрощенная модель памяти данных микроконтроллера PIC16F84. Эту память данных можно представить в виде картотечного шкафа, который в данном случае имеет два отделения (банка). Внутри каждого отделения имеется некоторое количество папок, или файлов, каждый из которых содержит восемь битов данных. Наравне с этим термином также используется термин регистр.

Рис. 4.7. Упрощенная структура памяти данных микроконтроллера PICI6F84

Вообще говоря, в нашей картотеке присутствуют файлы двух типов. Некоторые из этих файлов имеют названия и выполняют специальные, заранее заданные функции. Такие файлы называются регистрами специального назначения (РСН). РСН используются для управления и отслеживания состояния микроконтроллера и его различных периферийных устройств. В частности, как мы уже видели на Рис. 4.6, регистр с адресом h’03’ является регистром STATUS, а регистр с адресом h’06’ представляет собой регистр данных параллельного порта В, связанного с выводами RB7…RB0 (обычно обозначаемыми как RB[7:0]) микроконтроллера, как показано на Рис. 4.1.

Остальные файлы, выделенные на рисунке серым цветом, программист может называть по своему усмотрению и использовать для хранения пользовательских данных общего назначения. В микроконтроллере PIC16F84 имеется 68 таких регистров общего назначения (РОН), адреса которых лежат в диапазоне h’0C’…h’4F’. Во всех микроконтроллерах PIC среднего уровня регистры с младшими адресами используются в качестве РСН, а регистры со старшими адресами — в качестве РОН. Однако в более новых представителях семейства, таких как PIC16F628, требуется больше РСН в связи с большим числом периферийных устройств. При этом под РСН резервируется память с адресами до h’1F’. С учетом сказанного, во всех программах, приведенных в настоящей книге, предполагается, что для хранения переменных доступна область памяти, начиная с адреса h’20’.

Во многих моделях объем памяти для хранения пользовательских данных больше, чем в микроконтроллере PIC16F84. И тем не менее даже в самой развитой модели среднего уровня объем памяти не превышает 368 байт. Это не так уж и много, поэтому программы должны использовать эту ограниченную емкость очень эффективно.

Возвращаясь к Рис. 3.5 (стр. 68), можно заметить, что из 14 бит, составляющих код команды, семь зарезервировано под адрес операнда в памяти данных. Семь битов дают нам всего 27 = 128 адресов, т. е. страницу или банк, вмещающий в себя 128 регистров. Для преодоления этого ограничения необходимо где-то взять дополнительные биты, чтобы расширить диапазон адресов. В микроконтроллере PIC16F84 для этой цели используется 5-й бит регистра STATUS, который называется RP0 (Register Page 0). Этот бит выполняет роль 8-го бита адреса, позволяя использовать память данных, содержащую до 256 регистров. При RP0 = 0 (после сброса по питанию) обращения производятся к 0-му банку памяти данных (регистры h’00’…h’7F’). При RP0 = 1 разрешается доступ к 1-му банку, т. е. к регистрам h’80’…h’FF’.

Большинство моделей среднего уровня имеют 4 банка ОЗУ и используют уже два бита регистра STATUS — RP1:RP0 (6-й и 5-й биты соответственно); см. Рис. 5.5 на стр. 122. В результате формируется 9-битный адрес памяти данных. Более подробно этот вопрос будет рассмотрен в следующей главе.

Несмотря на то что использование банков памяти является довольно эффективным вариантом преодоления 7-битного ограничения на размер адреса, их использование может вызвать трудности у неопытных программистов. В качестве примера рассмотрим фрагмент кода, в котором осуществляется запись числа b’00001111’ в регистр h’86’. Для выполнения этой операции мы воспользуемся командой movlw, загружающей 8-битную константу в рабочий регистр:

movlw b’000011111 ; Загружаем константу h’0F’ в W

movwf h’86’ ; и копируем его в регистр с адресом h’86’

В коде ошибочно указан адрес h’86’, или b’10000110’, значение которого слишком велико для имеющегося размера поля. Большинство ассемблеров просто обрежут биты, выходящие за границы поля, в результате чего мы получим адрес h’06’, или Ь’0000110’. Таким образом, с точки зрения ассемблера адреса h’86’ и h’06’ являются одинаковыми, хотя большинство ассемблеров при этом выдадут программисту предупредительное сообщение. В частности, фирменный ассемблер Microchip (который мы будем рассматривать в главе 8) выдаст следующее сообщение:

Message[302] Register in operand not in bank 0.

Ensure that bank bits are correct.

Другими словами, именно программист должен позаботиться о корректной установке битов RPx перед обращением к таким адресам.

Чтобы понять, как это можно сделать, нам придется забежать немного вперед и рассмотреть команды манипуляций с битами, приведенные в Табл. 5.2 на стр. 131. Во всех микроконтроллерах требуется иметь возможность управления состоянием отдельных битов регистра как для задания опций в РСН, так и для «дрыгания» ножками портов ввода/вывода. В микроконтроллерах PIC для этих целей используются следующие команды.

∙ bcf

Команда сброса бита регистра данных (Bit Clear File) позволяет программисту сбросить любой бит в любом регистре. К примеру, команда bcf h’20’,7 сбрасывает 7-й бит регистра h’20’. Состояние остальных битов регистра при этом не изменяется.

∙ bsf

Команда установки бита регистра (Bit Set File) позволяет программисту установить любой бит в любом регистре. К примеру, команда bsf h131’,3 устанавливает 3-й бит регистра h’31’. Состояние остальных битов не изменяется.

Возвращаясь к нашему примеру, теперь мы можем написать:

bsf 3,5 ; Установка 5-го бита (RP0) регистра STATUS позволяет обращаться к 1-му банку памяти

movlw b100001111’ ; Загружаем константу h’0F’ в W

movwf h’86’ ; и копируем его в регистр с адресом h’86’

bcf 3,5 ; Сбрасываем RP0 для возврата к 0-му банку

На самом деле использование 1-го банка в микроконтроллере PIC16F84 сведено к минимуму. Все 68 РОН отображены на оба банка памяти, т. е. по адресу h’n’ и по адресу h’n+80’ расположен один и тот же регистр. Например, если программисту необходимо прочитать содержимое регистра h’20’, не имеет значения, какой из банков при этом используется процессором, поскольку в регистре h’A0’ находятся те же самые данные, а не просто их копия! Такое встречается достаточно редко, поскольку микроконтроллеры PIC с памятью данных большего объема «разбрасывают» свои уникальные РОН (и РСН) по всем имеющимся банкам памяти. Тем не менее в более новых моделях, таких как PIC16F628, имеется небольшая группа (обычно 16) РОН, отображенных на все банки памяти. Такое решение позволяет максимально быстро сохранять и восстанавливать критические данные независимо от того, с каким из банков процессор работает в данный момент времени (см. стр. 218).

Большинство наиболее часто используемых РСН также отображаются на все банки памяти. Типичным примером может служить регистр STATUS, который одновременно расположен и по адресу h’03’, и по адресу h’83’. Это сделано из-за того, что к флагам и битам регистра STATUS приходится обращаться очень часто, и постоянное переключение банков было бы неэффективным. В самом деле, когда в коде, приведенном выше, мы сбрасываем бит RP0 для перехода от 1-го банка к 0-му, мы предполагаем, что регистр STATUS присутствует в обоих банках. В противном случае мы никогда бы не смогли переключить бит RP0 и сменить банк памяти.

Большинство РСН микроконтроллера PIC16F84 встречаются во всех представителях семейств среднего уровня, более того, они, как правило, и размещаются по тем же самым адресам. Так, регистр STATUS расположен по адресам h’03’/h’83’.

Формально мы не будем в этой главе рассматривать никакие РСН, за исключением регистра STATUS и регистров, имеющих отношение к счетчику команд. Однако будет удобнее, если все эти регистры будут кратко описаны в одном месте. Поэтому мы просто перечислим их здесь, а подробно об их назначении поговорим в соответствующих главах книги.

Косвенная адресация

При непосредственной адресации адрес операнда содержится в коде команды. В микроконтроллерах среднего уровня для этого зарезервировано 7-битное поле, показанное на Рис. 3.5 (стр. 68). В сфере встраиваемых устройств, когда коды команд хранятся в ПЗУ какого-либо типа, такие адреса являются фиксированными и, соответственно, не могут модифицироваться.

Альтернативным способом, используемым в том или ином виде всеми вычислительными устройствами, является хранение адреса операнда в каком-либо регистре. В случае PIC этот адрес содержится в регистре FSR (File Select Register), располагающемся по адресу h’04’ памяти данных. Для переключения в режим косвенной адресации внутренняя логика отслеживает обращение по нулевому адресу памяти данных. Когда в команде указывается этот нулевой адрес, на шину адреса памяти данных выставляется содержимое регистра FSR, как показано на Рис. 5.6 (стр. 124).

При косвенной адресации местоположение операндов является не константой в памяти программ, а переменной в регистре FSR. То есть положение операнда может изменяться в процессе выполнения программы. В качестве примера можно взглянуть на Программу 5.2, приведенную на стр. 125.

К режиму косвенной адресации имеют отношение следующие регистры:

∙ INDF (h’00’)

Нулевой адрес памяти данных обозначается как INDF (INDirect File). Поскольку этот регистр используется исключительно для включения режима косвенной адресации, то его не стали реализовывать как настоящий регистр. То есть вы не можете хранить данные в регистре INDF, поскольку он физически не существует!

∙ FSR (h’04’)

В индексном регистре FSR содержится 8-битный адрес, который используется при обращении команды по нулевому адресу (к регистру INDF).

Таймер

Большинство микроконтроллеров имеют возможность измерения временных интервалов и/или формирования прямоугольных импульсов заданной длительности. Как правило, для этого используется один или более счетчиков, инкрементирование которых происходит либо по внешним импульсам, либо по внутреннему тактовому сигналу Например, если в автоматической упаковочной машине необходимо осуществлять подсчет консервных банок, движущихся по конвейеру, то в качестве входного сигнала таймера может использоваться сигнал от фотоэлектрического датчика. Если в одну коробку помещается 24 банки, то во внутренний 8-битный счетчик необходимо загрузить значение h’E8’ (-24). При переполнении счетчика с h’FF’ до h’00’ будет сгенерировано прерывание (см. главу 7) и микроконтроллер начнет выполнять соответствующие действия.

Во всех микроконтроллерах PIC имеется, по крайней мере, один базовый таймер/счетчик Таймер 0. Счетный регистр таймера TMR0 (h’01’), доступный для чтения и записи, может тактироваться внешним сигналом, подаваемым на вход микроконтроллера TOCKI (Timer0 ClocK In), который совмещен с линией RA4 порта А. Кроме того, инкрементирование счетчика может происходить по внутреннему тактовому сигналу Q4 (Рис. 4.4), частота которого составляет 1/4 частоты кварцевого резонатора. Частота обоих сигналов (и внешнего, и внутреннего) может быть снижена при помощи внутреннего 8-битного предделителя. Коэффициент деления предделителя задается тремя младшими битами регистра OPTION_REG, расположенного по адресу h’81’ (см. Рис. 13.2 на стр. 452), которые называются PS2:PS1:PS0. Соответственно, коэффициент деления будет равен 2PS+1. Например, если PS[2:0] = 111, то счетный регистр таймера будет инкрементироваться с частотой f/256, где f — частота источника тактового сигнала.

Предделитель может быть отключен от таймера установкой бита PSA (OPTION_REG[3]New Scientist , vol.59, no. 2141, 4 July 1998, p.139.
) в 1. При этом импульсы будут поступать непосредственно на счетчик. Кроме того, при записи в счетный регистр таймера регистр предделителя также сбрасывается (к примеру, последовательность команд movlw h’E8’, movwf 1 вызовет сброс предделителя), позволяя отсчитывать временной интервал от точно заданного момента.

Когда бит PSA регистра OPTION_REG установлен в 1, то предделитель используется в качестве постделителя сторожевого таймера (см. Рис. 13.1 на стр. 451). Сторожевой таймер предназначен для сброса микроконтроллера в случае, если он не будет периодически переустанавливаться командой сброса сторожевого таймера clrwdt (Clear WatchDog Timer). Это гарантирует сброс микроконтроллера в случае его некорректной работы, вызванной внешними помехами или ошибкой в программе, возможно, из-за перехода к незапрограммированной области памяти программ. При этом сторожевой таймер перестанет периодически переустанавливаться. Если предделитель подключен к Таймеру 0 (PSA = 0), то период тайм-аута сторожевого таймера будет примерно равен 18 мс. При установленном бите PSA период гарантированного сброса процессора будет составлять (2PS х 18) мс. Таким образом, для предотвращения сброса микроконтроллера интервал между выполнением команд clrwdt должен быть меньше указанного периода. Кроме того, эта команда сбрасывает счетчик предделителя. При наступлении тайм-аута бит  регистра STATUS сбрасывается. Если это необходимо, сторожевой таймер можно отключить при программировании микроконтроллера (во время занесения кода программы в память программ). Различные конфигурационные биты (fuses) располагаются в ячейке памяти программ по адресу h’2007’ (см. Рис. 10.6 на стр. 312), которая недоступна, когда микроконтроллер работает в нормальном режиме. Все эти детали обычно скрыты от оператора программным обеспечением программатора.

С Таймером 0 связаны следующие регистры:

∙ TMR0 (h’01’)

Этот 8-битный суммирующий счетчик, иногда называемый таймером/счетчиком реального времени, осуществляет счет импульсов, поступающих на вход таймера. Данный регистр в любой момент времени доступен как для чтения, так и для записи. При переполнении счетчика (при смене значения с h’FF’ на h’00’) он устанавливает бит T0IF (Timer 0 Interrupt Flag) регистра управления прерываниями INTCON (см. Рис. 7.3 на стр. 213). Этот флаг может использоваться для генерации прерывания.

∙ OPTION_REG (h’81’)

Для управления Таймером 0 используются шесть битов этого регистра, расположенного по адресу h’81’ (см. Рис. 13.2 на стр. 452):

• PS2, PS1, PS0 (биты 2, 1 и 0 соответственно) определяют коэффициент деления предделителя (2PS+1) Таймера 0 или постделителя (2PS) сторожевого таймера.

• T0SE (бит 4) позволяет программисту задать фронт импульсов на входе T0CKI, по которому будет осуществляться инкрементирование счетчика:

0 — нарастающий фронт, 1 — спадающий фронт.

T0CS (бит 5) используется для выбора источника тактового сигнала таймера: 0 — системный тактовый сигнал, 1 — импульсы со входа T0CKI.

• Остальные два бита регистра используются для выбора активного фронта внешнего прерывания и конфигурирования входов порта В.

Счетчик команд

Мы уже говорили (см. Рис. 4.2), что микроконтроллеры PIC среднего уровня имеют 13-битный счетчик команд (Program Counter — PC), выполняющий функцию указателя на команды в пределах 8 Кбайт. Сколько именно битов счетчика используется в каждой конкретной модели, зависит от имеющегося объема памяти программ. Так, в микроконтроллере PIC16F84 используется 10 бит (210 = 1 Кбайт), в PIC16F628 — 11 бит (211 = 2 Кбайт), в PIC16F874 — 12 бит (212 = 4 Кбайт), а в PIC16F877 задействованы все 13 бит.

Иногда может потребоваться изменить состояние счетчика команд из программы. Для этого младший байт PC напрямую доступен через регистр специального назначения PCL (Program Counter Low). А для изменения всех 13 бит требуется дополнительный регистр. Регистр-защелка старшего байта PCLATH не является в действительности старшим байтом PC, а служит в качестве буфера. Изменение содержимого регистра PCLATH не влияет на старший байт счетчика команд, однако одновременно с записью в регистр PCL новое значение PCLATH загружается в старший байт 13-битного счетчика команд. Таким образом, как показано на Рис. 4.8, все 13 бит счетчика команд обновляются одновременно. Запомните эту особенность, она потребуется вам при ответе на Вопрос для самопроверки 4.2.

Рис. 4.8. Изменение всех 13 битов счетчика команд при записи в регистр PCL

Для манипулирования счетчиком команд используются следующие регистры:

∙ PCL (h’02’)

Регистр PCL физически является младшим байтом счетчика команд. Этот регистр доступен как для чтения, так и для записи.

∙ PCLATH (h’0A’)

Регистр PCLATH является регистром-защелкой для хранения данных, которые должны быть загружены в старший байт счетчика команд. Загрузка старшего байта происходит при записи в регистр PCL, что обеспечивает одновременное обновление всех 13 бит счетчика.

Не забывайте, что регистр PCLATH используется только в качестве буфера, поэтому значение, считанное из него, не будет соответствовать текущему состоянию старшего байта счетчика команд.

Параллельные порты ввода/вывода

Способность одновременно изменять или контролировать состояние нескольких цифровых линий представляет собой воистину универсальную возможность систем на базе микроконтроллеров. В зависимости от типа корпуса микроконтроллеры среднего уровня имеют от 4 до 52 таких линий ввода/вывода. К примеру, в 40-выводном микроконтроллере PIC16F877 имеется в общей сложности 33 линии ввода/вывода.

Микроконтроллер PIC16F84 имеет 13 линий ввода/вывода, разделенных на два порта. ПортА имеет 5 линий ввода/вывода, отображенных на адресное пространство памяти данных по адресу h’05’. Остальные 8 линий относятся к порту В, размещенному по адресу h’06’. Эти порты можно считать своеобразными «окнами» в памяти данных, поскольку значения, записываемые в регистры с адресами h’05’ и h’06’, появляются на выводах микроконтроллера RA4…RA0 и RB7…RB0 соответственно (см. Рис. 10.2 на стр. 304). Однако физически и логически эти порты гораздо сложнее, чем обычные внутренние регистры. Мы еще вернемся к этому вопросу в главе 11, пока же скажем только, что линия порта может быть сконфигурирована как выход (при этом ЦПУ может управлять состоянием соответствующего вывода) или как вход (при этом ЦПУ может считывать состояние данного вывода). Для этого предназначены регистры направления данных TRISA и TRISB (для порта А и В соответственно), расположенные по адресам h’85’ и h’86’. Название TRIS образовано от слова TRIState (см. Рис. 11.3 на стр. 333). Эти регистры находятся в 1-м банке, поскольку они обычно конфигурируются в начале программы и впоследствии не изменяются.

В качестве примера рассмотрим следующую ситуацию. Предположим, что нам необходимо сделать выводы RB[6:0] порта В входами, а вывод RB7 — выходом. Тогда код для конфигурирования порта будет выглядеть следующим образом:

bsf 5,3 ; Переходим к 1-му банку

movlw h’7F’ ; Двоичному числу 0111 1111 соответствует:

movwf h ’86’ ; RB7 — выход, RB6…0 — входы

bcf 5,3 ; Возвращаемся к 0-му банку

Несмотря на то что этот код совершенно корректен и благодаря комментариям его назначение в общем-то понятно, он все же не очень удобен для чтения. Второй вариант того же куска кода имеет куда более дружественный вид, но с точки зрения ассемблера совершенно идентичен первому (см. стр. 70).

STATUS equ 03 ; Регистр STATUS расположен по адресу h’03’

RP0 equ 05 ; Бит переключения банков — 5-й

TRISB equ h’86’ ; Регистр направления расположен по адресу h’86’

PORTB equ 06 ; Регистр данных порта расположен по адресу h’06

         bsf STATUS,RP0 ; Переходим к 1-му банку

         movlw b’ 01111111’ ; Двоичному числу 0111 1111 соответствует:

         movwf TRISB  ; RB7 — выход, RB6…0 — входы

         bcf STATUS,RP0 ; Возвращаемся к 0-му банку

Очевидно, что последний вариант более предпочтителен. Может показаться, что сделанные нами изменения носят исключительно косметический характер. Однако более ясный текст снижает вероятность возникновения ошибок, а также облегчает отладку и внесение последующих изменений в программу. В реальных программах, в отличие от приведенного фрагмента кода, используется множество разнообразных переменных и битов регистров, поэтому они должны быть ясными и понятными.

Четыре первые строки приведенного фрагмента показывают один из способов, посредством которого программист может сообщить транслятору с языка ассемблера о необходимости подстановки вместо символьного имени числового значения. В частности, строка

STATUS equ 03

говорит о том, что при использовании в качестве операнда имени STATUS оно должно заменяться числом 3 (т. е. регистр h’03’). Директива equ является сокращением от «EQUivalent to». Директивой называется псевдокоманда, которая, как правило, не генерирует реальный машинный код, а используется для передачи информации транслятору. Начиная с этого момента, мы будем для ясности присваивать нашим регистрам и битам имена.

В качестве примера напишем код, который формирует на выводе RB7 положительный импульс (предполагается, что ЦПУ работает с 0-м банком):

bsf PORTB,7; Выставляем на RB7 ВЫСОКИЙ уровень (устанавливаем 7-й бит)

bcf PORTB,7; Выставляем на RB7 НИЗКИЙ уровень (сбрасываем 7-й бит)

С параллельными портами ввода/вывода связаны следующие регистры:

∙ PORTA (h’05’)

В этом регистре задействовано только 5 младших битов, подключенных к выводам RA4…RA0 микроконтроллера. Вывод RA4 используется также модулем Таймера 0. Фантомные три старших бита читаются как 0. В некоторых моделях семейства, например в PIC16F628, могут быть реализованы все 8 линий порта А.

∙ TRISA (h’85’)

Этот регистр предназначен для конфигурирования линий порта А в качестве входов или выходов. Установка бита TRISA[n] в 1 делает вывод RA[n] входом, а сброс в 0 — выходом. При любом сбросе все биты регистра TRISA устанавливаются в 1, и все выводы порта соответственно становятся входами.

∙ PORTB (h’06’)

Двунаправленный 8-битный порт ввода/вывода, подключенный к выводам RB0…RB7 микроконтроллера. Вывод RB0 может использоваться также в качестве входа аппаратного прерывания.

∙ TRISB (h’86’)

Этот регистр используется для конфигурирования линий порта В в качестве входов или выходов. Более подробно — см. описание регистра TRISA.

EEPROM-память данных

В большинстве моделей среднего и старшего семейства имеется блок памяти объемом до 256 (в PIC16F84 — 64) байт, для хранения содержимого которого не требуется питания. Эта энергонезависимая память не является частью (энергозависимой) памяти данных, а обращения к ней производятся посредством определенных РСН, как к обычному периферийному устройству. Любой байт этой памяти можно считать или записать посредством регистра EEDATA. Адрес байта задается регистром EEADR, а управление процессом чтения/записи осуществляется с помощью регистров EECON1 и EECON2. Срок службы большинства модулей EEPROM составляет не менее 10 млн циклов перезаписи, а период сохранности данных — не менее 40 лет. Типичными примерами использования энергонезависимой памяти является хранение количества страниц, отпечатанных лазерным принтером, или суммарный путь, пройденный автомобилем.

Подробно процессы чтения и записи EEPROM будут рассмотрены в главе 15, здесь же мы просто приведем последовательность действий для выполнения операций чтения/записи.

Чтение

1. Поместить адрес (h’00….h,FF’) в EEADR.

2. Установить бит RD (0-й бит регистра EECON1) в 1 для переключения в режим чтения.

3. Считать адресованные данные из EEDATA.

Запись

1. Поместить адрес в EEADR.

2. Поместить данные в EEDATA.

3. Установить бит WREN (2-й бит регистра EECON1) в 1 для переключения в режим записи.

4. Записать число h’55’ в EECON2.

5. Записать число h’AA’ в EECON2.

6. Начать цикл записи установкой бита WR (1-й бит регистра EECON1) в 1. Операция записи, которая, как правило, является достаточно редким событием, специально сделана такой запутанной, чтобы исключить случайное изменение EEPROM. На самом деле регистра EECON2 не существует, однако последовательная запись по его адресу значений Ь’55’и h’AA’ необходима для разблокирования EEPROM. Прерывания могут нарушить эту последовательность, поэтому, если они используются, их следует запретить. Длительность операции записи составляет около 50 мс, после ее завершения устанавливается 4-й бит регистра EECON1 (флаг EEIF), который может использоваться для прерывания работы процессора. Флаг WRERR (3-й бит регистра EECON1) устанавливается, если цикл записи был прерван, скажем, в результате внешнего сброса.

К EEPROM-памяти данных относятся следующие регистры (адреса указаны для модели PIC16F84):

∙ EEDATA (h’08’)

Этот регистр содержит данные адресованной ячейки EEPROM после операции чтения или же данные, которые будут записаны в EEPROM во время операции записи.

∙ EEADR (h’09’)

Этот регистр содержит адрес байта, к которому производится обращение, загружаемый перед началом операции чтения или записи.

∙ EECON1 (h’88’)

Этот регистр содержит следующие биты управления и состояния:

• Бит запуска операции чтения EEPROM.

• Бит разрешения операции записи.

• Бит запуска операции записи в EEPROM.

• Бит признака преждевременного завершения цикла записи.

• Бит признака нормального завершения цикла записи.

Более полную информацию можно получить из Рис. 15.2, приведенного на стр. 545.

∙ EECON2 (h’89’)

Этот управляющий регистр физически не существует, и при его чтении всегда возвращается нулевое значение. Указанный адрес используется для загрузки последовательности, разрешающей цикл записи. Последовательность состоит из двух чисел h’55’ и h’AA’, записываемых друг за другом.

Прерывания

Регистр управления прерываниями INTCON, расположенный по адресу h’0B’, содержит биты маски и статуса, управляющие реакцией микроконтроллера на прерывания. Использование этого регистра описывается в главе 7. Большинство периферийных устройств имеют собственные биты, относящиеся к прерываниям и расположенные в других управляющих регистрах (см., например, Рис. 7.5 на стр. 223).

Примеры

Пример 4.1

Объясните, каким образом внедрение в схему блока выборки команд конвейера увеличивает производительность микроконтроллеров PIC. Предвидите ли вы какие-либо проблемы, связанные с поддержкой команд перехода (таких как goto), относительно структуры конвейера?

Решение

Наличие конвейера является обязательным условием для организации параллельной работы блока выборки и исполнительного блока. То есть для того, чтобы иметь возможность исполнять команду и одновременно с выборкой из памяти программ команды n + 1, требуется внутренний элемент с памятью, который передавал бы код команды в дешифратор команд. Поскольку все команды имеют одинаковый размер (14 бит), структуру регистров конвейера и управление ими можно значительно упростить. Большинство традиционных CISC-процессоров имеют команды, длина которых может быть различной. К примеру, размер команд микроконтроллера 68НС11 колеблется от 1 до 4 байт, т. е. длительность фазы выборки составляет от 1 до 4 транзакций на шине. Некоторые более развитые процессоры имеют многоступенчатый конвейер, каждый этап которого связан с определенной частью исполнительного блока. За счет этого можно реализовать несколько одновременных потоков исполняемых команд.

Проблема, связанная с конвейером, вытекает из предположения, что команды программы будут исполняться последовательно, т. е. так, как они расположены в памяти программ. При этом команды, не удовлетворяющие этому условию и изменяющие содержимое счетчика команд, требуют очистки конвейера, с тем чтобы код адресованной команды оказался на вершине конвейера. Например, если командой к является команда goto n , то к тому моменту, когда процессор узнает, что в действительности на следующем шаге необходимо будет выполнить команду n, команда k + 1 будет уже загружена в конвейер. Поэтому необходимо выполнить холостой цикл, во время которого код команды n будет напрямую загружен в конвейер (разумеется, команда k + 1, код которой находится на вершине конвейера, не выполняется). Иногда эту операцию называют очисткой конвейера. Соответственно, такие команды, как goto, выполняются за два машинных цикла. Команды условного пропуска (см. главу 5) выполняются за два цикла в случае пропуска и за один цикл в противном случае. Все остальные команды выполняются за один машинный цикл.

Пример 4.2

Можете ли вы определить, почему после операции вычитания или сложения с отрицательным числом значение флага С является дополнением к биту заема?

Подсказка: вспомните правила двоичной арифметики в дополнительных кодах (стр. 22).

Решение

Операция вычитания во всех микроконтроллерах PIC реализована одинаково: байт данных переводится в дополнительный код, а затем выполняется сложение, как показано на Рис. 2.9 на стр. 39. В этой ситуации итоговый бит переноса равен 0, если результат сложения получается отрицательным, и 1, если положительным. Например:

1. 06 — 0А —> 00000110 + 11110110 = (0) 11111100 или -4 (нет переноса).

2. 0А — 06 —> 00001010 + 11111010 = (1) 00000100 или +4 (есть перенос).

В обоих случаях флаг переноса соответствует инвертированному биту заема. Такое поведение соответствует философии RISC PIC-микроконтроллеров — процессор должен быть максимально простым и понятным.

Точно такая же инверсия происходит при использовании отрицательного операнда в командах сложения, например в команде addlw -6. Это выражение будет преобразовано транслятором в addlw h’FC’, где h’FC’, конечно же, представляет собой дополнительный код числа 6.

Пример 4.3

Один умник решил скопировать содержимое регистра STATUS в регистр h’40’, с тем чтобы использовать его в дальнейшем. Однако бит 2 регистра STATUS оказался сброшен в 0. Почему?

Решение

Из текста на стр. 67 мы узнали, что команда movf устанавливает флаг Z, если содержимое регистра-адресата равно нулю, в противном случае флаг Z сбрасывается. Так что следующий фрагмент программы

movf STATUS,w ; Скопировать содержимое регистра h’03’(STATUS) в W,

movwf h’40’ ; а потом в регистр h’40’

действительно скопирует содержимое регистра h’03’ в регистр h’40’. Но до тех пор, пока все биты регистра STATUS не будут равны нулю, флаг Z будет постоянно сбрасываться. При нормальной работе флаги  и  равны 1, поэтому итоговым значением флага Z всегда будет 0, независимо от его исходного состояния.

Разумеется, это ограничение можно обойти. Одно из таких решений показано на стр. 217.

Пример 4.4

Как бы вы задали следующую конфигурацию некоторых РСН из 1-го банка:

• OPTION_REG b’10101111’

• TRISA b’00011110’

• TRISB b’11111111’

Решение

Поскольку все три регистра находятся в 1-м банке, нам необходимо будет переключить банки перед записью данных и переключиться на 0-й банк после конфигурирования регистров.

STATUS equ 3 ; Регистр STATUS расположен по адресу h’03’

RP0 equ 5 ; Бит RP0 — 5-й

OPTION_REG equ h’81’ ; Регистр OPTION_REG расположен по адресу h’81’

TRISA equ h’85’ ; Регистр направления порта А

TRISB equ h’86’ ; Регистр направления порта В

       bsf STATUS,RP0 ; Переходим к 1-му банку

       movlw b’10101111’ ; Первую константу

       movwf OPTION_REG ; в регистр OPTION_REG

       movlw b’00011110’ ; Вторую константу

       movwf TRISA ; в TRISA

       movlw b’11111111’ ; Третью константу

       movwf TRISB ; в TRISB

       bcf STATUS,RPO; Возвращаемся к 0-му банку

Пример 4.5

Напишите программу для инкрементирования упакованного BCD-числа, находящегося в памяти данных по адресу h’20’.

Решение

Два двоично-десятичных (BCD) разряда можно упаковать в один байт, т. е. он может использоваться для хранения чисел от 0 до 99. Например, значение 01001001h’20’ соответствует числу 49. Инкрементирование числа, хранящегося в таком хитром виде, с использованием обычных правил двоичного сложения может привести к некорректному результату. Например, Ь’01001001 + 1’(49 + 1) даст нам b’01001010’ (h’4A’), тогда как нам необходимо получить число Ь’01010000’ (h’50’). Аналогично, Ь’ 10011001 + 1’ (99 + 1) даст нам Ь’10011010’ (h’9A’) вместо Ь’00000000’ + Ь’1’ (h’1 00’).

Из приведенных примеров можно увидеть, что если после инкрементирования какого-либо разряда BCD-числа он равен 10, то необходимо его обнулить, а к старшему разряду прибавить единицу. Воспользовавшись этим рассуждением, сформулируем перечень задач, которые должна выполнять наша программа:

1. Инкрементировать BCD-число по правилам обычной двоичной арифметики.

2. Если младший полубайт результата равен 10, прибавить к результату число 6.

3. Если старший полубайт результата равен 10, прибавить к нему число 6.

В Программе 4.1 приведена эффективная реализация описанного алгоритма. После инкрементирования по правилам обычной двоичной арифметики к результату прибавляется число 6 и проверяется состояние флага DC. Этот флаг установится только в том случае, если исходное значение полубайта было равно десяти (h’0A’ + h’06’ = h’10’). В этом случае сумма сохраняется как необходимая коррекция, иначе производится вычитание для возврата к исходному значению. Старший полубайт (BCD-разряд) проверяется и корректируется аналогичным образом, только при этом используется уже флаг полного переноса С. Если он установлен, то результат сложения с числом h’60’ сохраняется, в противном случае это число вычитается. При необходимости флаг переноса может использоваться для установки разряда сотен, чтобы показать переполнения с 99 до 100.

Программа 4.1. Инкрементирование упакованного BCD-числа

; ****************************

; * ФУНКЦИЯ: Инкрементирует число в BCD-формате *

; * ВХОД: BCD в регистре h’20’ *

; * ВЫХОД: BCD+1 в регистре h’20’ *

; * ПРИМЕР: 10011000 (98) + 1 = 10011001 (99) *

; ****************************

STATUS equ 3 ; Регистр STATUS

C equ 0; Флаг переноса — бит 0

DC equ 1 ; Флаг десятичного переноса — бит 1

BCD equ h’20’ ; Исходное BCD-число — в регистре h’20’

; -----------------------------

BCD_INC

         incf BCD, w ; Инкрементируем число и помещаем в W

         addlw 6 ; Прибавляем шесть

         btfss STATUS,DC ; Это было нужно, ЕСЛИ был десятичный перенос,

         addlw -6 ; ИНАЧЕ не нужно

;Теперь проверим старший разряд, прибавляя к нему 6 и проверяя флаг переноса

         addlw h’60’ ; Прибавим h’60’ (т. е. шесть к старшему разряду)

         btfss STATUS,С ; Это было нужно, ЕСЛИ был перенос,

         addlw — h’60’ ; ИНАЧЕ отменяем коррекцию

; Инкрементированное и скорректированное BCD-число теперь в W

         movwf BCD ; Помещаем его в память

В качестве альтернативного варианта можно было бы вычитать перед инкрементированием число девять и, если в результате инкрементирования флаг Z окажется установленным, инкрементировать старший разряд, в противном случае прибавить 10. Ту же операцию следует повторить для старшего разряда.

Вопросы для самопроверки

4.1. Когда микропроцессор используется в вычислительном устройстве общего назначения, программа обычно загружается в ОЗУ, доступное как для чтения, так и для записи, и выполняется уже оттуда. Это означает, что в один момент времени в системе может выполняться текстовый редактор, а в другой — программа работы с электронными таблицами. Разумеется, это неприменимо к встраиваемым приложениям, в которых программа хранится в энергонезависимом ПЗУ какого-либо типа. Объясните, для чего так сделано, и укажите преимущества различных вариантов исполнения энергонезависимой памяти — ПЗУ (ROM), СППЗУ (EPROM) и ЭСППЗУ (EEPROM).

4.2. Микроконтроллер среднего уровня PIC16F877 имеет память программ объемом 8 Кбайт, в которой может храниться до 8192 14-битных команд, расположенных в диапазоне адресов h’0000’…h’1FFF’. Как, не прибегая к помощи команды goto, которая имеет определенные ограничения (см. Рис. 5.1 на стр. 117), можно выполнить переход к команде, расположенной в памяти программ по адресу h’1234’, из любого места программы?

4.3. Учитывая, что команда movf воздействует на флаг Z (см. Пример 4.3), как можно использовать эту команду для проверки на ноль содержимого любого регистра данных?

4.4. Из Табл. 1.1, приведенной на стр. 18, можно увидеть, что коды заглавных букв A…Z отличаются от кодов соответствующих строчных букв только значением 5-го бита, который равен 0 в случае заглавных и 1 — в случае строчных букв. Можете ли вы, используя команды, которые фактически были представлены в этой главе, написать процедуру перевода символа ASCII, находящегося в регистре h’20’, из нижнего регистра в верхний?

4.5. Используя конфигурационные значения из Примера 4.4, напишите программу, формирующую положительный импульс на выводе RA0 длительностью 4 мкс. Предполагается, что тактовая частота равна 4 МГц.

4.6. Можете ли вы написать последовательность команд, которая выдаст на вывод RA1 ВЫСОКИЙ уровень, затем сформирует на выводе RA0 четыре импульса и в завершение выставит на вывод RA1 НИЗКИЙ уровень? Не забудьте сконфигурировать регистр TRISA.

4.7. В большинстве электронных часов используется кварцевый резонатор частотой 32.768 кГц, часто называемый «часовым». Из-за больших объемов выпуска эти резонаторы имеют низкую стоимость. Хотя использование такого резонатора и снизит скорость выполнения программы, из Рис. 10.3 на стр. 306 можно увидеть, что мощность, рассеиваемая микроконтроллером, прямо пропорциональна тактовой частоте. Поэтому «часовой» резонатор является достаточно привлекательным выбором для многих экономичных приложений.

Можете ли вы вычислить длительность машинного цикла при использовании такого резонатора? Какой смысл имеет значение 32 768 для времязадающих узлов?

 

Глава 5

Набор команд

Написание программы в чем-то сродни постройке дома. Имея в наличии определенные стройматериалы, строитель просто укладывает их вместе в нужном порядке. Разумеется, для этого он должен иметь соответствующие знания и навыки, ведь нарушение технологии может привести к тому, что крыша дома будет протекать, а сам дом будет продуваться всеми ветрами и даже может развалиться!

Можно проектировать дом одновременно с его постройкой. В принципе, если вы строите простую хижину, это вполне допустимо. Однако очевидно, что при таком подходе к строительству полученный в результате дом не сможет очень долго защищать владельца от дождя, а также не будет экономичным, ремонтопригодным, эргономичным, да и просто красивым. Гораздо лучше нанять архитектора, чтобы он спроектировал здание до того, как начнется строительство. Разумеется, этот проект будет в достаточной степени абстрактным, хотя лучше, если проектировщик имеет представление о технических характеристиках и стоимости имеющихся строительных материалов.

К сожалению, очень многие программируют «на бегу», практически не задумываясь о составлении сколько-нибудь подробного проекта. В области программного обеспечения термин «проектирование» означает написание алгоритма и разработку необходимых структур данных. И опять же лучше, если разработчик алгоритма будет учитывать «кирпичики», из которых будет построена программа. В нашем случае такими кирпичиками являются машинные команды.

В примерах, приведенных в данной главе, большей частью затрагиваются именно вопросы кодирования (строительства). В последующих главах мы познакомимся с более развитыми структурами, которые облегчат нам жизнь на этом этапе разработки программы. Кроме того, мы сможем попрактиковаться в разработке алгоритмов и структур данных.

Можно провести параллель между написанием программы и приготовлением кулинарного блюда. Для любой плиты, будь то микроволновая печь или электроплита, имеется свой набор операций. Эти операции — к примеру, выпаривание, обжаривание, кипячение — по своей сути аналогичны набору команд, которые могут быть выполнены центральным процессором. Различные ингредиенты, которые могут использоваться во время этих операций, являются данными команд. Эти данные могут находиться как во внутренних регистрах процессора, так и во внешней памяти. Причем исполнительный адрес операнда может задаваться несколькими различными способами. Эти способы называются режимами адресации.

В соответствии с RISC-подобной философией микроконтроллеров PIC ядро среднего уровня имеет всего 33 команды плюс две устаревшие команды, доставшиеся в наследство от младшего семейства, которые мы не будем рассматривать. Каждая команда представляет собой 14-битное слово, в котором содержится собственно код операции (КОП), адрес или значение операнда, а также бит адресата результата операции. Некоторые из этих команд и режимов адресации мы уже рассмотрели в главе 3 при обсуждении нашего компьютера BASIC. Теперь же пришла пора полностью разобраться с этим материалом. Так что в этой главе мы подробно рассмотрим различные режимы адресации и все имеющиеся команды.

Прочитав эту главу, вы:

• Узнаете, что режим адресации предназначен для точного указания местонахождения данных команды.

• Поймете, каким образом адресуется операнд команды при различных режимах адресации.

• Поймете, каким образом формат слова команды влияет на использование этих команд.

• Разберетесь, каким образом бит IRP регистра STATUS позволяет процессору обращаться ко всей памяти данных с использованием косвенной адресации.

• Узнаете, что наиболее часто используемыми командами являются команды пересылки данных, предназначенные для копирования данных между рабочим регистром и памятью данных.

• Узнаете, что процессор может выполнять базовые арифметические операции, такие как сложение, вычитание, инкрементирование, декрементирование и изменение битов.

• Научитесь выполнять сравнение данных и их проверку на определенное значение с выполнением требуемых действий в зависимости от результата.

• Узнаете, что данные в памяти данных можно циклически сдвигать через флаг переноса С.

• Научитесь использовать четыре базовых логических операции для того, чтобы выполнять инвертирование, установку, сброс, смену значения, проверку состояния битов и сличение данных.

• Поймете, как можно управлять ходом программы в зависимости от состояния любого бита или нулевого значения в регистре данных.

Подавляющее большинство команд оперирует данными, находящимися либо во внутренних регистрах ЦПУ, либо вне его (в памяти данных или памяти программ). Так что в 14-битном слове команды должно быть поле, информирующее дешифратор команд о том, где расположены эти данные. Исключение из указанного правила составляют несколько команд с адресацией кодом самой команды, такие как nop (нет операции) и return (возврат из подпрограммы). Прежде чем перейти к изучению собственно команд, мы рассмотрим различные методы, используемые для указания местоположения любых операндов.

В общем случае все команды записываются следующим образом:

мнемоника < операнд А >,< операнд В >

где операнд-А — исходные данные или их местоположение, а операнд В — адресат команды. Например, команда movf h’20’,w (копировать регистр данных) копирует содержимое источника (регистр h’20’) в приемник (рабочий регистр).

Существует несколько вариантов такой записи. Наиболее часто встречаются команды с 2.5 операндами. Например, команда addwf [регистр], d прибавляет содержимое рабочего регистра W к содержимому указанного регистра данных и помещает результат либо в W, либо обратно в регистр данных. Так, команда addwf h’20’, w означает «прибавить содержимое W к содержимому регистра h’20’ и записать результат в регистр h’20’». Коротко это можно записать как [f20] <- W + [f20], где квадратные скобки означают «содержимое», а стрелка — «становится». Такой тип нотации называется языком регистровых передач (Register Transfer Language — RTL).

Разумеется, эта команда не является трехоперандной в полном смысле этого слова, поскольку в качестве адресата может использоваться только один из источников, т. е. либо рабочий регистр, либо регистр данных. В некоторых командах указывается только один операнд-адресат, например clrf h’20’, a y команд с самоадресацией вообще нет явных операндов.

Все команды можно разделить по используемому способу адресации.

Адресация кодом команды #_01.jpg_3

Такие команды, как clrwdt (сброс сторожевого таймера), retfie (возврат из прерывания), nop (нет операции), return (возврат из подпрограммы) и sleep (переход в «спящий» режим), не используют операнды из памяти. У всех этих команд в старших семи битах слова команды присутствуют нули. Например, команда clrwdt имеет машинный код Ь’0000000 0000100’.

Адресация константы #_02.jpg_3

В командах, работающих с константами, младшие 8 бит кода команды используются для указания операнда-источника, являющегося в данном случае константой, а не байтом в регистре данных. Например, команда addlw 06 кодируется как b’11 111000000110 ’. Операндом-адресатом в командах такого типа всегда является рабочий регистр, что и отражено в мнемоническом обозначении. Так, в нашем примере сумма W + 6 копируется обратно в рабочий регистр W. На языке регистровых передач эта операция выражается как W <- W + #6, где символ «#» (диез, или решетка) указывает, что стоящее после него число является константой, а не адресом регистра данных.

В некоторых процессорах такая разновидность адресации называется непосредственной, поскольку значение данных доступно непосредственно, без обращения к памяти.

Абсолютная адресация памяти программ #_03.jpg_2

В микроконтроллерах PIC предусмотрены две команды, которые позволяют программе перейти к другой команде, находящейся в любом месте памяти программ. Этими командами являются команды goto и call (вызов подпрограммы, см. главу 6). В микроконтроллерах с 14-битным ядром под этот адрес в коде команды выделяется 11 бит. Так что машинный код команды goto h’400’ будет равен Ь’10 110000000000 ’. Аналогично call h’530’ — b’10 010100110000 ’.

Используя этот 11-битный адрес, можно непосредственно адресовать любую команду в памяти программ объемом до 211 = 2 Кбайт. Однако в микроконтроллерах среднего уровня реализован 13-битный счетчик, который может адресовать память данных объемом до 8 Кбайт (память такого объема имеется, например, в модели PIC16F877). Для разрешения этой ситуации при выполнении команд goto и call абсолютный 11-битный адрес объединяется с битами 4:3 регистра защелки PCLATH, формируя таким образом полный 13-битный адрес, загружаемый в счетчик команд. Этот процесс показан на Рис. 5.1 (см. также Рис. 4.8 на стр. 103).

Рис. 5.1. Формирование 13-битного адреса памяти программ из 11-битного абсолютного адреса, передаваемого при вызове команд goto и call

При сбросе по включению питания регистр PCLATH сбрасывается, так что непосредственная область действия команды goto составляет h’000’…h’7FF’. Это соответствует диапазону адресов памяти программ объемом 2 Кбайт, имеющейся, например, в микроконтроллере PIC16F628. В моделях с большим объемом памяти программ необходимо использовать дальние переходы и вызовы (т. е. за пределы h’7FF’), используя биты PCLATH[4:3]. Например, в микроконтроллере PIC16F877 переход к адресу h’F00’ должен быть реализован следующим образом:

bsf PCALTH,3 ; Запишем в PCALTH[4:3] =11

bsf PCLATH,4 ;

goto h’F00’ ; Перейдем к требуемому адресу!

Обратите внимание, что после изменения этих битов они остаются в данном состоянии до следующего изменения, поэтому необходимо быть внимательным при переносе такого кода на процессор с памятью программ больше 2 Кбайт.

Прямая адресация памяти данных #_05.jpg_2

Большинство данных, используемых программой, размещаются в памяти данных. Соответственно, этот режим адресации используют команды, в которых источник и/или адресат находятся в регистрах памяти данных. Адрес регистра содержится в семи младших битах кода команды (обозначенных как fffffff). Например, код команды addwf h’26’,f (сложить содержимое рабочего регистра с регистром данных h’26’ и поместить результат обратно в регистр данных, или, коротко, [f] <- [f] + [W]) выглядит как Ь’00 011110100110 ’.

Большинство команд, использующих прямую адресацию, могут пересылать результат либо в рабочий регистр, либо обратно в регистр данных. Бит 7 кода команды, помеченный как d (см. также Рис. 3.5 на стр. 68), используется для указания адресата, как в следующем примере:

addwf h’26’,w ; Код команды — 00 0111 0 0100110

addwf h’26’,f  ; Код команды — 00 0111 1 0100110

В обоих случаях содержимое регистра с адресом h’26’ прибавляется к содержимому рабочего регистра. В первом случае, приведенном на Рис. 5.2, а, результат помещается в рабочий регистр, оставляя содержимое регистра данных неизменным (d = 0), тогда как во втором случае, показанном на Рис. 5.2, б, исходное содержимое регистра данных замещается (d = 1) итоговой суммой.

Как мы увидим далее (см., например, Табл. 5.2), большинство команд используют непосредственную адресацию. Однако у этого метода адресации есть два ограничения, которые программист должен иметь в виду.

Всего 7 бит

Под адрес регистра данных в коде команды среднего семейства отведено всего семь битов, соответственно, используя прямую адресацию, можно обращаться только к регистрам из диапазона h’00’…h’7F’. Из Рис. 4.7 (стр. 97), а также Рис. 5.3 видно, что для обхода этого ограничения в микроконтроллере PIC16F84 в качестве суррогатного старшего бита адреса используется 5-й бит (RP0) регистра STATUS. В результате память разбивается на два банка регистров, каждый объемом до 27 = 128 регистров. Для переключения между 0-м (RP0 = 0) и 1-м (RPO = 1) банками этот бит управления страницами, расположенный в 5-м бите регистра STATUS (Рис. 4.6 на стр. 95), можно менять точно так же, как и любой другой бит регистра данных.

Рис. 5.2. Выбор операнда-результата в команде addwf h’26’

Особенность модели PIC16F84 заключается в том, что в ней имеется всего два банка памяти. Большинство микроконтроллеров среднего уровня имеют 4 банка памяти. В качестве примера можно назвать микроконтроллер PIC16F627/8 (усовершенствованный PIC16F84), структура памяти данных которого приведена на Рис. 5.4. Чтобы иметь возможность переключаться на любой банк памяти, требуется уже два бита управления страницами, показанных на Рис. 5.5. Эти два бита RP1:RP0, выделенные на рисунке серым цветом, обнуляются при сбросе любого типа, т. е. после сброса мы всегда работаем с 0-м банком памяти. Поэтому программист должен соответствующим образом изменить эти биты, если он хочет обратиться к регистру, находящемуся в другом банке. Например, если необходимо скопировать содержимое регистра h’120’, расположенного во втором банке, в рабочий регистр и переключиться обратно на 0-й банк, мы должны написать:

bcf STATUS,6 ; Устанавливаем RP1 (6-й бит) в 1

bcf STATUS,5 ; Сбрасываем RP0 (5-й бит) в 0

movf h’120’,w ; Копируем содержимое регистра h’120’ в W

bcf STATUS,6 ; Сбрасываем RP1 (возвращаемся к 0-му банку)

Примером такой интенсивной работы с банками памяти может служить Программа 15.4, приведенная на стр. 552.

Если программист забудет изменить биты RP1:0 перед выполнением команды movf h’120’, то в рабочий регистр будет скопировано содержимое из регистра данных h’020’ (полагая, что процессор находится в нулевом банке), поскольку в коде команды будет записано только семь младших битов адреса Ь’(01)0100000’ (h’120’)! Ассемблер, однако, выдаст предупреждение, вид которого показан на стр. 99.

Чтобы избежать слишком частого переключения банков памяти, все регистры общего назначения (РОН) микроконтроллера PIC16F84 отображены на оба банка, как показано на Рис. 5.3.

Рис. 5.3. Память данных микроконтроллера PIC16F84

Подобное зеркалирование всех регистров встречается достаточно редко — чаще отображают небольшую группу регистров. Например, в моделях PIC16F627/8 предусмотрена общая область из 16 РОН, отображенных на все четыре банка (Рис. 5.4). Например, регистры данных с адресами h’070’, h’0F0’, h’170’ и h’1F0’ являются одним и тем же регистром. Переменные, которые могут потребоваться при работе с различными банками, по возможности следует размещать в этом общем пуле регистров. В общей же сложности в данных моделях имеется 224 уникальных РОН.

Рис. 5.4. Память данных микроконтроллера PIC16F627/8

Некоторые из наиболее часто используемых регистров специального назначения (РСН) тоже отображены на все банки, например регистр STATUS. Поэтому в приведенном выше примере мы могли изменять биты RP1:0 и возвращаться в 0-й банк, даже находясь во 2-м банке.

Рис. 5.5. Обобщенный формат регистра STATUS микроконтроллеров с 14-битным ядром

Фиксированные адреса

Будучи составной частью кода команды, 7-битный адрес операнда является фиксированным и поэтому не может быть изменен во время выполнения программы. Хотя явное задание этих адресов может показаться очевидным способом для указания местоположения объекта в памяти данных, существует ряд ситуаций, в которых такое ограничение слишком неудобно.

В качестве примера, иллюстрирующего эту недостаточную гибкость, предположим, что мы хотим очистить содержимое всех регистров данных 0-го банка модели PIC16F627/8, т. е. регистров h’20’…h’7F’. Очевидным решением этой задачи будет многократное (96 раз) использование команды clrf (очистка регистра), как показано в Программе 5.1.

Программа 5.1. Очистка группы регистров с использованием прямой адресации

CLEAR_ARRAY

        clrf h’20’ ; Очищаем регистр 32

        clrf h’21’ ; и 33

        clrf h’21’ ; Каждая команда clrf

        clrf h’23’ ; занимает одну ячейку

        clrf h’24’ ; в памяти программ

        clrf h’25’ ; Очищаем регистр 37

        clrf h’26’ ; и так далее

        ... ...

        clrf h’7E’ ; Очищаем регистр 126; еще чуть

        clrf h’7F’ ; Очищаем регистр 127; уф-ф!

Несмотря на то что этот код вполне работоспособен, он чрезвычайно неэффективен. Каждая из 96 команд выполняет одну и ту же операцию, хотя и для другого адреса. Если нам потребуется очистить все 244 РОН, то придется выполнить 224 команды clrf, и все для того, чтобы выполнить эту простейшую задачу. Поскольку в памяти программ микроконтроллера PIC16F627 имеется всего 1024 ячейки, такое решение займет более 20 % памяти.

Должен быть лучший способ!

Косвенная адресация памяти данных #_10.jpg_2

В любом процессоре имеется одна из разновидностей косвенной адресации, при которой один или более внутренних регистров используются для хранения адреса операнда в памяти данных. Такие адресные или индексные регистры используются в качестве указателя на данные. Основное отличие от прямой адресации заключается в том, что содержимое регистра-указателя может изменяться в процессе выполнения программы. То есть искомый адрес уже не зафиксирован в виде двоичного кода в памяти программ (обычно ПЗУ), а является переменной величиной. Например, для очистки массива данных из Программы 5.1 можно использовать цикл, инкрементируя в каждом проходе цикла регистр, указывающий на очищаемый регистр.

В микроконтроллерах PIC реализован достаточно простой вариант такого типа адресации — в полном соответствии с их философией. В младшем и среднем семействах имеется отдельный элемент ИЛИ-HE, который детектирует обращение по прямому 7-битному адресу Ь’0000000’ и, как показано на Рис. 5.6, просто выставляет на шину адреса памяти данных содержимое регистра h’04’, называемого индексным регистром (FSR). Это происходит, если в качестве адресата команды используется нулевой адрес, по которому располагается регистр косвенной адресации INDF. Этот регистр является виртуальным, т. е. физически не существует. Он используется исключительно для выставления содержимого регистра FSR в качестве адреса операнда. Хотя такая реализация косвенной адресации может показаться довольно ущербной, она использует очень простую дополнительную логику и не требует для работы дополнительных тактов, в отличие от альтернативных способов, реализованных в других процессорах и микроконтроллерах.

Рис. 5.6. Механизм косвенной адресации

В качестве простого примера предположим, что содержимое регистра FSR равно h’86’.Тогда команда clrf 0 (или clrf INDF) очистит регистр, расположенный по адресу h’86’, а не по адресу h’00’! Разумеется, содержимое регистра FSR можно изменить в любой момент времени, например, его можно инкрементировать в каждом проходе цикла, как в Программе 5.2.

Давайте в качестве примера перепишем Программу 5.1, заменив линейную структуру циклом, как показано на Рис. 5.7.

Рис. 5.7. Использование цикла для очистки массива данных

Теперь наша программа будет работать по следующему алгоритму, представляющему собой перечень задач:

1. Установить указатель FSR на начало массива.

2. Очистить адресуемый регистр данных, указав в качестве адресата регистр данных h’00’.

3. Инкрементировать указатель FSR.

4. Проверить, не достигли указатель конца массива, в нашем случае — адреса h’80’. Если нет, то перейти к пункту 2.

5. Продолжить выполнение программы.

Визуально этот процесс представлен на Рис. 5.8.

Рис. 5.8. Проход массива

Код, соответствующий этому алгоритму, приведен в Программе 5.2. Линейная структура предыдущей программы была преобразована в цикл, тело которого выделено серым цветом. Очистку регистров по-прежнему выполняет команда clrf, которая «проходит» по массиву, начинающемуся с адреса h’20’. При каждом проходе цикла указатель в регистре данных h’04’ инкрементируется. В конце концов содержимое регистра FSR выйдет за границу заданного диапазона, в результате чего программа выйдет из цикла и продолжит выполнение следующей секции кода.

Программа 5.2 . Очистка группы регистров с использованием косвенной адресации

В Программе 5.2 имеется много других особенностей, так что нам еще придется вернуться к рассмотрению набора команд.

Задача 1

Регистр FSR инициализируется адресом первого очищаемого регистра данных путем записи константы h’20’ в рабочий регистр W (movlw h’20’) с последующим копированием W в регистр h’04’ (movwf FSR). Как видно, в наборе команд отсутствует отдельная команда непосредственного копирования константы в регистр данных. Практически все циклы требуют инициализации перед входом в них.

Задача 2

Основная команда очистки регистра использует косвенную адресацию, указывая в качестве адресата фантомный регистр h’00’ (INDF) — clrf INDF. Эта строка помечена меткой СLOOP. Ассемблер понимает, что это именно метка, а не команда, поскольку она начинается с самой левой позиции строки исходного файла. Строки без меток должны начинаться с отступа хотя бы в один пробел.

Задача 3

При каждом проходе цикла указатель увеличивается на единицу. Эта операция осуществляется командой incf FSR,f. Обратите внимание, что в качестве адресата указан сам регистр памяти данных, а не рабочий регистр W.

Задача 4

Если вы не собираетесь крутиться в этом цикле бесконечно, то вам потребуется механизм для выхода из него. В нашем случае для этого используется сравнение содержимого регистра FSR с константой h’80’, т. е. адресом первого регистра, находящегося вне заданного диапазона. Сравнение осуществляется копированием содержимого регистра FSR в W (movf FSR,w) и последующим вычитанием рабочего регистра из константы h’80’ с использованием команды addlw — h'80' (прибавление отрицательного числа). Если эти числа равны, то флаг Z будет установлен, в результате чего команда btfss STATUS, Z (см. стр. 133) пропустит следующую за ней команду goto CLOOP. До наступления этого события команда goto будет передавать управление на начало цикла, и процесс будет повторяться с FSR, указывающим наследующий сбрасываемый регистр данных.

В итоге вариант программы с циклом состоит из 8 команд против 96 в линейном варианте, т. е. размер программы уменьшился в 12 раз. Однако наша новая программа выполняется в 7 раз дольше из-за наличия различных команд, необходимых для организации цикла и выполняющихся 96 раз! Обычно затраты на накладные расходы не так велики, как в данном примере.

* * *

Наличие регистра FSR, хранящего адрес операнда, означает, что у нас теперь есть 8-битный изменяемый адрес для обращения к памяти данных вместо фиксированного 7-битного. В свою очередь, из этого следует, что при работе с памятью данных, имеющей два банка (аналогичной приведенной на Рис. 5.3), к любому регистру можно обратиться откуда угодно. Например, если мы хотим записать число b’01111111 в регистр данных h’86’ (регистр специального назначения TRISB, расположенный в 1-м банке), то вместо кода, приведенного на стр. 105, мы можем написать:

movlw h’86’ ; Настроим FSR для работы

movwf FSR ; с регистром h’86’(TRISB)

movlw b01111111 ; Маска

movwf 0 ; Записываем ее в указываемый регистр

При этом нам не придется возиться с битом переключения страниц RP0. Если необходимо часто обращаться к какому-либо из регистров первого банка, то можно записать в FSR адрес этого регистра и больше регистр FSR не трогать. Разумеется, предполагается, что он не требуется для других целей.

В моделях с четырьмя банками памяти требуется дополнительный бит для образования 9-битного адреса. Бит IRP регистра STATUS, формат которого показан на Рис. 5.5, позволяет косвенно адресовать банки 0/1 (IRP = 0, состояние по умолчанию) и банки 2/3 (IRP = 1). Например, ранее написанный код для копирования содержимого регистра h’120’ банка 2 (PIC16F627/8) в рабочий регистр W, приведенный на стр. 119, можно переписать следующим образом:

bsf STATUS,7 ; Установим бит IRP (банки 2/3)

movlw h'120' ; Инициализируем указатель в FSR

movwf FSR ;

rnovf 0,w ; Копируем содержимое регистра, указываемого FSR, в W

bcf STATUS,7 ; Сбрасываем IRP (банки 0/1)

Поскольку в регистре W могут находиться только 8-битные значения, старший бит адреса при выполнении команды movlw h’120’ будет отброшен, т. е. в регистр W будет записано число h’20’. Роль отсутствующего девятого бита выполняет бит IRP, установленный в 1, поэтому обращение произойдет к регистру h’120’, что и требовалось. Ассемблер, возможно, выдаст предупреждение, что в регистр W записывается слишком большое значение. Это предупреждение можно игнорировать.

Битовая адресация #_03.jpg_3

Четыре команды (на что указывают два бита, помеченные знаками «??») предназначены либо для изменения, либо для проверки состояния отдельных битов в регистре данных. В этом случае в коде команды имеется 3-битное поле NNN, предназначенное для хранения позиции бита (0…7), тогда как адрес регистра кодируется обычным образом. Так, машинный код команды bcf h’20’,7 (сбросить бит 7 в регистре h’20’) выглядит как Ь’01 00111 010000’. Остальными командами этой группы являются команда bsf (установить бит регистра данных, код 01), btfsc (проверить состояние бита и пропустить следующую команду, если он сброшен, код 10) и btfss (проверить состояние бита и пропустить следующую команду, если он установлен, код 11). Последнюю из перечисленных команд мы уже использовали в Программе 5.2 для проверки 2-го бита регистра h’03’ (т. е. флага Z регистра STATUS) и выходили из цикла, если условие было истинно.

Пока что мы классифицировали команды по способу, которым они определяют местоположение своих операндов. Однако чаще используется деление команд по выполняемым функциям. С этой точки зрения все 33 команды микроконтроллеров PIC с 14-битным ядром можно разбить на 6 групп, четыре из которых будут рассмотрены в этой главе. Команды, относящиеся к подпрограммам и прерываниям, будут описаны в 6-й и 7-й главах, а управляющим командам, связанным с функционированием микроконтроллера, посвящена глава 10.

В таблицах команд, приводимых далее, в левом столбце приводятся мнемонические обозначения команд. Затем указывается влияние данной команды на три флага регистра STATUS, причем символ «» соответствует отсутствию какого-либо изменения, а символ «√» — нормальному воздействию. В последнем столбце приводится краткое описание операций, выполняемых командой. Полностью набор команд приведен в Приложении Г. Если вам потребуется более подробное описание, его можно найти в документации на любой микроконтроллер PIC соответствующего семейства (см. сайт, посвященный оригинальному изданию данной книги). Однако, в связи с тем что микроконтроллеры PIC имеют RISC-архитектуру, команд достаточно мало, и они простые.

Команды пересылки данных

Почти треть всех команд в любой компьютерной программе, независимо от оборудования, на котором она выполняется, используются для простой пересылки данных между памятью и внутренними регистрами. С учетом этого в Табл. 5.1 приведены наиболее часто используемые команды PIC.

Все три команды пересылки используются либо для простого копирования однобайтного значения между рабочим регистром и указанным регистром данных, либо для загрузки константы в рабочий регистр. При этом исходные данные не изменяются, они просто копируются в регистр-адресат. Команда swap тоже копирует содержимое регистра данных в W, однако при этом меняет местами младший и старший полубайты.

∙ movlw

Эта команда заносит указанную 8-битную константу в рабочий регистр W. Например, команда movlw h’80’ инициализирует W значением b’10000000’.

Заметьте, адресатом в этой команде всегда является рабочий регистр, поэтому для инициализации регистра данных какой-либо константой требуется дополнительная операция — см. ниже.

∙ movwf

Эта команда предназначена для копирования (сохранения) содержимого рабочего регистра в регистре данных. Например, команда movwf h’23’ скопирует байт из W в регистр h’23’.

Таким образом, для инициализации регистра h’23’, скажем, числом Ь’10000000’, необходимо выполнить следующие операции:

movlw h’80’ ; Заносим в W число Ь’10000000’

movwf h’23’ ; и копируем его в регистр h’23’

∙ movf

Эта команда предназначена для копирования (загрузки) содержимого любого регистра данных в рабочий регистр W. Например, команда movf h’22’, w загрузит в W содержимое регистра h’22’.

Вообще говоря, в качестве адресата данной команды можно указать сам регистр данных, в результате чего мы выполним, как может показаться, бессмысленную операцию. В нашем случае это будет команда movf h’22’,f, которая скопирует содержимое регистра h’22’ в него же! Однако команда movf воздействует на флаг нуля Z (это единственная команда из Табл. 5.1, воздействующая хоть на какой-то флаг регистра состояния), который установится, если содержимое регистра равно нулю. Команда movf [File],f не изменяет содержимое указанного регистра, поэтому ее можно использовать для проверки регистра на нулевое значение, т. е. в качестве отсутствующей команды tstf [File],f, имеющейся во многих других микроконтроллерах и микропроцессорах. Таким образом, мы можем проверить содержимое любого регистра данных с помощью одной-единственной команды. Вариант проверки рабочего регистра на нулевое значение приведен на стр. 141.

Принимая во внимание тот факт, что большинство команд, оперирующих регистрами в памяти данных, позволяют задавать в качестве адресата либо тот же регистр, либо рабочий регистр W, операцию пересылки можно рассматривать как неявного члена этой группы команд. Например, в зависимости от ситуации операция инкрементирования регистра данных с последующим копированием нового значения в рабочий регистр W может быть записана следующим образом:

incf h’22’,f ; Инкрементируем содержимое регистра h’22’

movf h’22’,w ; и копируем его в W

либо так:

incf h’22’,w ; Копируем инкрементированное содержимое регистра h’22’ в W

Разумеется, в последнем случае содержимое регистра данных не изменяется.

∙ swapf

Команда swapf переставляет местами старший и младший полубайты содержимого регистра данных и помешает результат либо в тот же регистр данных, либо в рабочий регистр. Например, команда swapf h’22’,w выполнит операцию:

Команда swapf полезна в тех случаях, когда полубайты регистра используются для хранения BCD-чисел, однако может использоваться и для копирования содержимого регистра данных в W. В отличие от более понятной команды movf [File],w состояние флага Z при этом не изменяется. Недостатком, конечно же, будет перестановка полубайтов местами при копировании.

В Программе 7.2 на стр. 226 команда swapf используется именно с этой целью.

Команды арифметических операций

Процессоры микроконтроллеров PIC младшего и среднего уровня помимо сложения и вычитания могут выполнять и другие арифметические операции. В качестве примера можно отметить операции сброса, инкрементирования и декрементирования. В Табл. 5.2 также перечислены команды, устанавливающие или сбрасывающие заданный бит в указанном регистре данных.

Сложение и вычитание

В микроконтроллерах PIC имеется две команды сложения.

∙ addlw

Эта команда позволяет прибавить 8-битную константу к рабочему регистру W. Например, команда addlw b’10101010’:

∙ addwf

Эта команда прибавляет переменную из памяти данных к содержимому рабочего регистра W. В отличие от команды addlw, в качестве адресата может использоваться как W, так и исходный регистр данных. Например, addwf h’26’,f:

Очистка

Как рабочий регистр, так и любой регистр данных могут быть сброшены. В обоих случаях при выполнении этой операции будет установлен флаг Z, индицирующий нулевой результат операции.

∙ clrw

Эта команда очищает рабочий регистр. По своему действию она эквивалентна команде movlw 0.

∙ clrf

С помощью этой команды можно очистить содержимое любого регистра данных. Например, clrf h’26’:

Обе команды сложения, да и вообще все команды, оперируют 8-битными операндами. Тем не менее можно обрабатывать операнды любой длины, если делать это побайтно. В случае сложения, например, нам потребуется складывать попарно соответствующие байты операндов (от младшего байта до старшего) с добавлением бита переноса, полученного при сложении n-х байтов, к (n + 1) — й сумме. Входной перенос при сложении младшего байта равен нулю, а перенос из старшего байта становится старшим битом результата. Например, h’FFFF’ + h’FF = h’100FE’ (65 535 + 255 = 65 790).

Чтобы проиллюстрировать этот процесс, напишем программу, складывающую 8-битное число с 16-битным и получающую в результате 17-битную сумму. Первое слагаемое, как показано на Рис. 5.9, размещается в двух ячейках памяти данных с адресами h’20’ (старший байт) и h’21’ (младший байт). Сумма сохраняется в трех ячейках с адресами h’30’ (старший байт), h’31’ (средний байт) и h’32’ (младший).

Pис. 5.9. Сложение 16-битного числа с 8-битным

Составим перечень задач (алгоритм), учитывая, что нам нужно реализовать этот процесс в виде последовательности операций, выполняемых 1-байтными командами:

1. Прибавить младший байт первого слагаемого к младшему байту второго слагаемого — получим младший байт суммы и бит переноса С1 (Рис. 5.10, а).

2. Прибавить бит переноса С1 к старшему как байту первого слагаемого — получим средний байт суммы и новый бит переноса С2 (Рис. 5.10, б).

3. Старшим байтом суммы является последний бит переноса С2 — 0 или 1 (Рис. 5.10, в).

Поскольку это будет первая программа главы, все операции подробно показаны на Рис. 5.10. Во многих случаях подобная детализация совершенно бесполезна, а алгоритм в виде списка задач может быть дополнен более абстрактной блок-схемой.

Рис. 5.10. Визуализация процесса сложения

Прежде чем перейти к написанию программы, нам необходимо познакомиться с двумя командами (подробно мы их рассмотрим чуть позже). Команда incf позволяет нам непосредственно прибавлять единицу к содержимому любого регистра данных, а команда btfsc проверяет состояние конкретного бита заданного регистра данных и, если этот бит сброшен, выполняет пропуск следующей команды (см. Табл. 5.4). В нашем случае таким регистром является регистр h’03’ (регистр STATUS), а проверяемым битом — бит 0 (флаг переноса), т. е. команда будет записана как btfsc 5,0 или, более понятно, как btfsc STATUS,С. Мы уже использовали аналогичную команду btfss для проверки флага Z в Программе 5.2.

Все три указанные задачи помечены в листинге соответствующими комментариями.

Вводная часть

Всем регистрам с данными присвоены символические имена с помощью директивы equ. Как уже говорилось на стр. 105, использование осмысленных имен вместо голых адресов регистров дает в итоге более удобочитаемую программу. При этом уменьшается вероятность возникновения ошибок и облегчается отладка программы.

Задача 1

Младший байт 1-го слагаемого загружается в W, складывается со 2-м слагаемым, и результат сохраняется в памяти в качестве младшего байта суммы. При этом команда addwf изменяет соответствующим образом состояние флага С. К счастью, на его состояние не влияют последующие команды пересылки.

Задача 2

Старший байт 1-го слагаемого загружается в W. Если бит переноса С1 из предыдущей задачи равен 0, то команда прибавления единицы (addlw 1) пропускается, в противном случае производится инкрементирование содержимого W. Затем результат копируется в средний байт суммы.

Задача 3

Если бит переноса С2 из предыдущей задачи равен 1, то предварительно сброшенный старший байт суммы увеличивается до h’01’. Обратите внимание, что команда clrf SUM_U не воздействует на флаг переноса. Если С2 равен 0, то команда incf SUM_1,f пропускается и старший байт суммы остается нулевым.

Программа 5.3. Выполнение сложения с двойной точностью

AUGEND_H equ h’20’ ; Два регистра 1-го слагаемого

AUGEND_L equ h’21’

ADDEND_L equ h’22’ ; Второе слагаемое

SUM_U equ h’30’ ; Три регистра суммы

SUM_H equ h’31’

SUM_L equ h’32’

STATUS equ 3 ; Регистр STATUS расположен по адресу h’03’

С equ 0 ; Флаг переноса — 0-й бит регистра STATUS

; Задача 1 ---------------

DP_ADD

        movf AUGEND_L,W ; Берем младший байт 1-го слагаемого

        addwf ADDEND_L,w ; Прибавляем 2-е слагаемое, результат — в W

        movwf SUM_L ; Помечаем результат в младший байт суммы

; Задача 2 ---------------

        movf AUGEiCD_H,w ; Берем старший байт 1-го слагаемого

        btfsc STATUS, С ; Был ли перенос при предыдущем сложении?

           addlw 1 ; ЕСЛИ да, ТО прибавляем единицу

        movwf SUM_H ; Помечаем в средний байт суммы

; Задача 3 ---------------

        clrf SUM_U ; Обнуляем старший байт суммы {не влияя ка флаг С)

        btfsc STATUS, С ; Был ли перенос при предыдущем сложении?

          incf SUM_U,f ; ЕСЛИ да, ТО старший байт суммы равен 01

         ...  ...

В Программе 5.3 следует обратить внимание на два момента:

1. Ни одна из команд программы, за исключением команд сложения, не влияет на состояние флага С. Благодаря этому флаг С можно проверить с помощью команды btfsc даже через две команды после выполнения операции сложения.

2. Команды, следующие после каждой команды btfsc, имеют отступ на один пробел больше, чем остальные. Увеличенный отступ просто подчеркивает, что выполнение этого блока необязательно, т. е. он может быть пропущен. Ассемблер все эти украшательства игнорирует!

Команды инкрементирования и декрементирования

Содержимое любого регистра данных можно увеличить или уменьшить на единицу.

∙ incf

Эта команда увеличивает на единицу содержимое заданного регистра данных, помещая результат либо обратно в исходный регистр, либо в рабочий регистр W.

∙ decf

Эта команда уменьшает на единицу содержимое заданного регистра данных, помещая результат либо обратно в исходный регистр, либо в рабочий регистр W. Например, если в регистре h’26’ было записано число h’64’, то после выполнения команды decf h’26’,f в нем окажется число h’63’.

Если в качестве адресата указать рабочий регистр (decf h’26’, w), то содержимое регистра h’26’ останется равным h’64’, а содержимое рабочего регистра станет равным h’63’.

* * *

Хочу обратить ваше внимание на то, что обе эти команды не влияют на флаг переноса С в отличие от эквивалентных команд прибавления или вычитания единицы. В частности, это означает, что если вы собираетесь инкрементировать 3-байтное число, хранящееся в формате , то просто инкрементировать младший байт и проконтролировать переносы в старшие байты не получится. В следующем фрагменте программы используется команда btfss, которая пропускает команду, если 2-й бит регистра STATUS (флаг нуля Z) установлен в 1.

   incf LOWER,f; Прибавим единицу

   btfss STATUS,Z; Результат равен нулю?

      goto NEXT ; ЕСЛИ нет (Z == 0), TO выходим

    incf MIDDLE,f; ИНАЧЕ инкрементируем следующий байт

      goto NEXT ; ЕСЛИ нет (Z == 0), ТО выходим

     incf UPPER,f ; ИНАЧЕ инкрементируем следующий байт

NEXT

... ...; Прочий код

В приведенном фрагменте инкрементируется самый младший байт, и если он становится равным нулю (h’FF’ —> h’00’), то инкрементируется следующий байт и так далее для всех байтов. Эта последовательность прерывается, если при инкрементировании регистра получается ненулевое значение, например

h’06 FF FE’ h’06 FF FF —> h’07 00 00’.

Бит-ориентированные команды

Возможность сброса либо установки отдельного бита в любом регистре памяти данных очень важна, особенно при изменении различных РСН, управляющих процессором и его периферийными устройствами. Изменяемый регистр можно указать с помощью как прямой, так и косвенной адресации.

∙ bcf

Эта команда позволяет программисту сбросить в 0 любой из восьми битов указанного регистра памяти данных.

∙ bsf

Эта команда аналогична bcf, только указанный бит не сбрасывается, а устанавливается в 1. Например, при установке 5-го бита регистра h’26’ мы имеем

Одним из назначений данных команд является управление различными флагами и переключателями регистра STATUS. В коде, приведенном на стр. 119, мы уже использовали эти две команды для изменения состояния битов RPx (для переключения банков памяти данных). Ни одна из этих команд не воздействует на биты регистра STATUS. Однако важно понимать, что все команды, непосредственно изменяющие содержимое регистров данных, на самом деле считывают этот байт во временный регистр, выполняют соответствующую операцию (incf, bcf и т. д.), используя АЛУ, после чего помещают результат обратно в память данных. Такое поведение называется принципом чтение — модификация — запись и выполняется за один машинный цикл. Иногда такое функционирование может привести к неожиданным побочным эффектам (см., например, стр. 335).

Вычитание

В системе команд имеется две команды вычитания, операнды которых аналогичны командам сложения.

∙ subwf

Эта команда вычитает содержимое рабочего регистра из переменной, хранящейся в памяти данных. Как обычно, результат операции может быть помещен либо в рабочий регистр, либо обратно в исходный регистр данных. Например, при выполнении команды subwf h’26’,f происходит следующее:

Как мы уже обсуждали на стр. 95 и в Примере 4.2, приведенном на стр. 109, состояние флага переноса С равно дополнению к биту заема, возникающего при выполнении команд вычитания. Упущение из виду этого факта является одним из основных источников ошибок при написании программы!

∙ sublw

Команда sublw представляет собой еще один из источников ошибок, поскольку она вычитает содержимое рабочего регистра W из константы, а не наоборот, как можно подумать. Например, если в регистре W было, скажем, h’64’ (d’100’), то в результате выполнения команды sublw 1 вместо вычитания единицы получим 1 — h’64’ = h’9D’, что равно десятичному 157 (вообще говоря, это число — h’63’ в дополнительном коде). Лично я считаю, что из-за такой перевернутой реализации использование этой команды является неоправданным риском. В качестве альтернативы давайте посмотрим на команду addlw h’FF’. В нашем случае мы получим h’64’ + h’FF’ = h’(1)63’ (десятичное 99). Если игнорировать перенос, то получается, что 8-битный результат в W на единицу меньше исходного значения. Конечно же, зная о том, что h’FF’ является числом —1 в дополнительном коде, можно записать команду как addlw - 1, что будет более понятно. Более того, флаг С в данном случае равен 1 и, интерпретируя его как дополнение к биту заема, получаем, что заема не было.

В дальнейшем мы будем игнорировать команду sublw и использовать вместо нее addlw. Вообще-то мы уже так делали в Программе 5.2, где нам было нужно вычесть константу h’80’ из W. Ассемблер просто преобразует отрицательное число в его эквивалент в дополнительном коде, например, вместо addlw — 6 будет addlw h’FA’.

* * *

Одной из наиболее важных операций является операция сравнения двух чисел. С математической точки зрения это можно сделать при помощи вычитания байта (обозначаемого ниже как [f] и для регистра данных, и для константы) из содержимого рабочего регистра [W]. Результат [W] — [f] представляет реальную разность величин операндов. Однако в большинстве случаев достаточно определить отношение между величинами, т. е. узнать, не больше ли W, чем байт данных? Для этого необходимо контролировать состояния флагов С и Z регистра STATUS.

Рабочий регистр больше, чем байт данных… нет заема, не ноль

Рабочий регистр равен байту данных… ноль

Рабочий регистр меньше, чем байт данных… заем, не ноль

В нашем процессоре флаг С является дополнением к биту переноса, а флаг Z устанавливается при нулевом результате. Таким образом:

[W] больше, чем или равно [f]: [W] — [f] дает отсутствие заема (С = 1).

[W] равно [f]: [W] — [f] дает ноль (Z = 1).

[W] меньше, чем [f]: [W] — [f] дает заем (С = 0).

Эти варианты приведены на Рис. 5.11, где показано сравнение значения, находящегося в W, с содержимым регистра h’36’. Команда subwf h’36’,w формирует разность и изменяет флаги Z и С, как показано на рисунке. Собственно, разность двух чисел, находящаяся в W, нас не интересует, однако она перезаписывает исходное содержимое, которое может потребоваться сохранить перед сравнением.

Рис. 5.11. Сравнение содержимого W и регистра данных командой subwf h’26’

Рассмотрим следующий пример. Имеется топливная цистерна объемом 255 л, на дне которой установлен датчик, показывающий оставшееся количество топлива как линейную функцию от давления. Предположим, что значение выходного сигнала датчика представляется в виде байта, считываемого с порта В (см. стр. 105), который мы назовем FUEL. Нам нужно написать процедуру, которая будет включать световой сигнал «Пусто» (бит 0 порта А), если в цистерне осталось меньше 20 л, и включать звуковой излучатель (бит 1 порта А), если осталось меньше 5 л (см. Рис. 5.12). Активный уровень на обоих выходах — ВЫСОКИЙ. Эта задача может быть реализована следующим образом:

STATUS ecu 3; Регистр STATUS расположен по адресу h’03’

С equ 0 ; Флаг переноса — 0-й бит

Z equ 2 ; Флаг нуля — 2-й бит

FUEL equ 6 ; Уровень топлива можно считать из регистра h’06’ (порт В)

DISPLAY equ 5 ; Порт А — регистр h’05’

LAMP equ 0 ; Сигнальная лампочка управляется 0-м битом

BUZZ equ 1 ; Звуковой излучатель управляется 1-м битом

ALARM

        bcf DISPLAY,BUZZ ; Выключим пищалку

        bcf DISPLAY,LAMP ; Выключим лампочку

        movf FUEL, w ; Считываем значение уровня топлива в W

        addlw -5 ; FUEL — 5. ЕСЛИ БОЛЬШЕ ИЛИ РАВНО,

        btfss STATUS,С ; ТО заема не будет (С == 1), так что пропускаем

           bsf DISPLAY,BUZZ ; ИНАЧЕ включаем пищалку

        movf FUEL,W ; Снова считываем значение уровня топлива в W

        addlw — d’20’ ; FUEL — 20. ЕСЛИ БОЛЬШЕ ИЛИ РАВНО,

        btfss STATUS,С ; ТО заема не будет <С == 1), так что пропускаем

           bsf DISPLAY.LAMP ; ИНАЧЕ включаем лампочку

NEXT:

... ...;

Рис. 5.12. Операции сравнения в системе контроля уровня топлива

После каждого вычитания флаг переноса будет равен 1 (т. е. нет заема), если число в рабочем регистре (количество топлива) больше или равно значению константы, с которым оно сравнивается посредством вычитания. Обратите внимание на использование команды bsf для установки соответствующих битов порта А (предполагается, что эти линии уже сконфигурированы как выходы). Точно так же команда bcf используется для выключения светового сигнала и звукового излучателя в самом начале процедуры.

К операциям сравнения можно отнести и операцию проверки, во время которой байт данных проверяется на равенство нулю. Мы уже видели (см. стр. 67), что содержимое любого регистра данных можно проверить на нулевое значение простым копированием его в себя самого, например movf h’36’,f. Если в регистре находится нулевое значение, то флаг Z установится в 1. Аналогичная проверка рабочего регистра может быть выполнена прибавлением к нему нуля, т. е. addlw 0. Эта команда установит флаг Z при нулевом значении в рабочем регистре, не изменяя его содержимого.

Команды логических операций и операций сдвига

Микроконтроллеры PIC могут выполнять все четыре базовые логические операции — НЕ, И, ИЛИ и Исключающее ИЛИ, как показано в Табл. 5.3.

Операция НЕ

Логическая функция НЕ, показанная на Рис. 1.1 (стр. 26), инвертирует (формирует обратный код) логическое состояние входа.

∙ comf

С помошью этой команды можно инвертировать содержимое любого заданного регистра данных. Так, команда comf h’26’,f вычисляет обратный код содержимого регистра h’26’:

Как обычно, результат может быть помещен либо в исходный регистр данных, либо в W (в последнем случае исходное содержимое остается неизменным), например:

В микроконтроллерах PIC отсутствует команда типа comw для инвертирования содержимого рабочего регистра, однако эту операцию можно выполнить за один машинный цикл посредством вычитания W из числа b’11111111’, что дает в результате тот самый обратный код, т. е. sublw h’FF’. Например (см. также стр. 147):

Операция И

Из Рис. 1.2 (стр. 27) можно увидеть следующие соотношения:

• Логическое И любого бита и 0 всегда дает в результате 0.

• Логическое И любого бита и 1 дает в результате исходный бит.

Используя эти свойства, мы можем обнулять группы битов в байте данных посредством логического умножения этого байта на соответствующую битовую маску.

Операция И между байтом данных и тестовым шаблоном, используемая для сброса всех ненужных битов, может также применяться для проверки на ноль заданной группы битов. Если эти биты равны нулю, то общий результат тоже будет равен нулю, и флаг Z установится в 1.

∙ andwf

Команда andwf выполняет операцию побитового И между содержимым рабочего регистра W и любого регистра данных, помещая результат либо в исходный регистр данных, либо в W. Например, при логическом умножении каждого бита W на соответствующий бит регистра h’26’ и помещении результата обратно в h’26’, имеем

К примеру, если нам нужно сбросить шесть старших битов регистра h’26’, то мы можем написать следующее:

movlw b’00000011’ ; Маска

andwf h’26’,f; Логически умножается на содержимое регистра h’26’

Эту же операцию можно было бы выполнить, повторив шесть раз команду bcf.

Чтобы разобраться, как можно использовать функцию И для проверки на ноль группы битов, представим себе контроллер стиральной машины, который считывает состояние восьми переключателей передней панели через порт В, т. е. через регистр h’06’. Нам нужно, чтобы при нулевом значении битов 7 и 6 (одновременно нажаты кнопки «СТАРТ» и «БЫСТРО») включалась программа быстрой стирки. Вот как можно это сделать:

movlw b’11000000’ ; Маска

andwf h’06’,w ; Операция И с регистром PORTB

btfss STATUS,Z; Пропуск, если Z == 0 (т. е. результат не равен 0)

   goto FAST_WASH;  ИНАЧЕ перейти к процедуре FAST_WASH

... ...; Следующая проверка

В результате операции логическое И между содержимым регистра h’06’ и константой h’11000000’ младшие 6 битов сбрасываются. Результат будет равен нулю, если оба бита 6 и 7 порта В были сброшены перед выполнением команды. При этом будет установлен флаг Z, в результате чего программа перейдет к команде, помеченной меткой FAST_WASH. Не забудьте, что для проверки нулевого значения одного бита регистра данных можно использовать команду btfsc.

∙ andlw

Эта команда выполняет операцию побитового И между содержимым рабочего регистра и однобайтной константой. Например:

В результате операции, показанной на рисунке, старший полубайт содержимого W обнуляется, а младший — остается неизменным.

Операция ИЛИ

Из Рис. 1.3 (стр. 28) можно увидеть следующие соотношения:

• Логическое ИЛИ любого бита с 0 всегда дает в результате исходный бит.

• Логическое ИЛИ любого бита с 1 всегда дает в результате 1.

Используя эти свойства, мы можем устанавливать группы битов в байте данных, выполняя логическое сложение с соответствующей битовой маской.

∙ iorwf

По аналогии с командой andwf, эта команда выполняет операцию побитового ИЛИ между любым регистром данных и содержимым рабочего регистра W. Так, при логическом сложении каждого бита W с соответствующим битом регистра h’26’ и помещении результата обратно в h’26’ имеем

Например, для установки в 1 старших семи битов регистра данных h’36’ мы можем написать:

movlw b’11111110’ ; Маска

iorwf h’36’,f ; Устанавливаем старшие 7 битов, младший бит не изменяется

∙ iorlw

Эта команда выполняет операцию побитового ИЛИ содержимого W с однобайтной константой. Например, для установки младших двух битов рабочего регистра в 1:

Операция Исключающее ИЛИ

Из Рис. 1.4 на стр. 28 можно увидеть следующее:

• В результате операции Исключающее ИЛИ между битом и нулем возвращается исходный бит.

• В результате операции Исключающее ИЛИ между битом и 1 возвращается инвертированное значение исходного бита.

Другим полезным свойством оператора XOR является его использование в качестве логического дифференциатора. При более внимательном рассмотрении таблицы истинности можно заметить, что на выходе элемента Исключающее ИЛИ будет 1, если на его входах присутствуют различные логические уровни, и 0, если эти уровни одинаковы. Соответственно, в результате побитовой операции Исключающее ИЛИ между двумя байтами мы получим байт с 0 в тех позициях, где биты входных переменных были одинаковыми, и с 1 в тех позициях, где они были различными.

∙ xorwf

Эта команда выполняет побитовую операцию Исключающее ИЛИ между любым регистром данных и содержимым рабочего регистра W. Так, при выполнении операции Исключающее ИЛИ между каждым битом W и соответствующим битом регистра h’26’ и записи результата обратно в h’26’ имеем

Например, для переключения состояния старшего бита регистра h’36’ мы можем написать:

movlw Ь'10000000’ ; Маска

xorwf h'36',f ; Переключаем только старший бит регистра

В качестве примера продемонстрируем использование операции Исключающее ИЛИ для определения отличий между двумя группами битов. Рассмотрим процедуру, непрерывно опрашивающую состояние порта В микроконтроллера, к которому подключены восемь переключателей с передней панели стиральной машины. Процедура ожидает изменения состояния переключателей:

START

         movf PORTB,w; Считываем начальное состояние переключателей

         movwf h’20’ ; Сохраняем его в регистре h’20’

S_LOOP

          movf PORTB,w ; Считываем текущее состояние переключателей

          xorwf h’20’,w ; Ищем отличия от исходного состояния

          btfsc STATUS,Z ; Пропускаем, если результат проверки не равен нулю

              goto S_LOOP ; ИНАЧЕ проверяем снова

При этом возможны два варианта:

Результат, получаемый в рабочем регистре, отражает любые изменения состояния передней панели. В первом случае между исходным состоянием переключателей, сохраненным в регистре h’20’, и текущим нет никаких отличий. Во втором случае 4-й переключатель был переключен из 1 в 0. Чтобы определить, какой именно бит изменился, можно сдвигать результат вправо с подсчетом количества сдвигов до тех пор, пока оставшееся значение не будет равно 0 (см. Рис. 5.14). А характер изменения (0 —> 1 или 1 —> 0) можно определить посредством логического умножения итогового байта на байт исходного состояния переключателей, находящийся в регистре h’20’, т. е. с помощью команды andwf h’20’,w. Если 4-й бит результата равен нулю, то исходное значение тоже было равным 0 и, соответственно, состояние бита изменилось с 0 на 1 и наоборот.

∙ xorlw

Эта команда выполняет побитовую операцию Исключающее ИЛИ содержимого W с однобайтной константой. Например, для инвертирования всех битов в регистре W, т. е. для вычисления обратного кода:

Операции сдвига

Сдвиг данных влево или вправо является базовой операцией, реализованной во всех цифровых системах. Мы уже видели на Рис. 2.22 (стр. 51), как это можно сделать аппаратно. АЛУ всех без исключения микроконтроллеров и микропроцессоров позволяют реализовать различные комбинации команд сдвига вправо и влево.

Во всех микроконтроллерах PIC имеется две команды для циклического сдвига содержимого любого регистра данных, по одной команде для каждого направления.

∙ rrf

Эта команда сдвигает содержимое указанного регистра данных на один бит вправо, при этом вдвигаемый бит считывается из флага С, значение которого затем устанавливается в соответствии с выдвинутым битом. Эта операция показана на Рис. 5.13.

Рис. 5.13. Циклический сдвиг содержимого регистра данных на один бит вправо

Учитывая эту особенность команды, программист может выполнить нормальный сдвиг вправо с загрузкой в старший бит нуля (как на Рис. 2.22), если он сбросит бит С перед выполнением команды сдвига.

bcf STATUS,С ; Обнуляем бит переноса в регистре STATUS

rrf h’30’,f; Сдвигаем регистр вправо

Одним из использований операций сдвига является побитовая проверка данных. Предположим, для примера, что состояние 8 кнопок мобильного телефона было сохранено в регистре данных h’26’. Вам требуется определить самую левую разомкнутую кнопку, при этом считаем, что разомкнутой кнопке соответствует 1, а замкнутой — 0. Так, если были считаны следующие состояния:

то в W должно получиться число 6 (Ь’00000110’).

Рабочий регистр в Программе 5.4 используется в качестве счетчика. Поскольку флаг переноса сбрасывается перед каждым сдвигом, вдвигается всегда лог. 0. В какой-то момент остаток становится равным нулю, и процесс завершается. Так, 00010111 (1) —> 00001011 (2) —> 00000101 (3) —> 00000010 (4) —> 00000001 (5) —> 00000000 (6).

Список действий, необходимых для решения поставленной задачи, показанный также в виде блок-схемы на Рис. 5.14, будет следующим:

1, Обнулить KEY_COUNT.

2. ПОКА SWITCH_PATTERN не равно нулю, ВЫПОЛНЯТЬ:

а) ЕСЛИ остаток равен нулю, ТО выйти из цикла.

б) Сдвинуть SHIFT_PATTERN на один бит влево.

в) Инкрементировать KEY_COUNT.

3. Значение в KEY_COUNT равно позиции самой левой разомкнутой

Рис. 5.14. Процедура определения позиции самого левого установленного бита

При сдвиге вправо во флаг переноса выдвигается самый правый (младший) бит. Заменив команду btfsc STATUS,Z командой btfsc STATUS,С, мы сможем определить позицию самого правого бита. Во многих случаях циклическое выдвигание бита во флаг переноса может использоваться для побитовой проверки данных. Например, мы можем модифицировать свою программу таким образом, чтобы она подсчитывала число установленных битов в байте (см. Программу 5.6).

Программа 5.4. Поиск самого старшего единичного бита в регистре

SWITCH_PATTERN equ h’26’ ; Состояние кнопок в регистре h’26’

STATUS equ 3 ; Регистр STATUS расположен по адресу h’03’

С equ 0 ; Бит 0 — флаг переноса

Z equ 2 ; Бит 2 — флаг нуля

; Задача 1 -------------------

HIGH_BIT

          clrw ; Обнуляем счетчик

; Задача 2: Сдвигаем вправо и инкрементируем счетчик до тех пор, пока проверяемый байт не равен нулю

; Задача 2а -------------------

LOOP

        movf h’26’,f ; Остаток равен нулю?

        btfsc STATUS,Z ; ЕСЛИ нет, TO пропускаем команду

        goto FINI ; ИНАЧЕ выходим из цикла

; Задача 2б -------------------

         bcf STATUS,С ; Сбрасываем флаг перекоса

         rrf SWITCH_PATTERN,f ; Сдвигаем регистр вправо

; Задача 2в -------------------

          addlw 1 ; Увеличиваем счетчик на 1

          goto LOOP ; и выполняем следующий сдвиг

; Задача 3 ---------------------

FINI

... ...; KEYJTOUNT в W

Заметьте, если все кнопки были замкнуты, то в Программе 5.4 возвращается ноль. Поскольку после сдвига производится проверка на ноль, то нельзя будет различить ситуацию «нет разомкнутых кнопок» и «разомкнута только 1-я кнопка». При разработке программ необходимо уделять особое внимание вопросу их «живучести» при возникновении ограничивающих условий, подобных указанным.

∙ rlf

Команда rlf похожа на команду rrf, только она, как показано на Рис. 5.15, выполняет сдвиг влево.

Рис. 5.15. Циклический сдвиг содержимого регистра данных на один бит влево

В качестве примера использования команды rlf вспомним (см. стр. 25), что сдвиг влево можно использовать для умножения числа на степень двойки. Например:

00000110 (6) <<

00001100 (12) <<

00011000 (24) <<

00110000 (48) <<

и т. д.

где оператор языка Си «<<» используется для обозначения сдвига влево.

Чтобы проиллюстрировать этот процесс, предположим, что у нас имеется 16-битное число b’00000111 11010000’ (равное десятичному 1024 + 512 + 256 + 128 + + 64 + 16 = 2000), хранящееся в двух регистрах данных, например:

После сдвига на один бит влево получим число-:

равное десятичному числу 4000 (2048 + 1024 + 512 + 256 + 128 + 32 = 4000).

Проблема в том, что команда rlf может сдвигать только один бит. Поэтому нам необходимо разбить эту операцию на три этапа, как показано на Рис. 5.16:

1. Сбросить флаг переноса, чтобы при сдвиге вдвигался 0.

2. Сдвинуть влево младший байт, в результате чего значение флага переноса станет равным значению бита by.

3. Сдвинуть влево старший байт, при этом на месте младшего бита окажется бит, загруженный во флаг переноса при предыдущей операции.

Рис. 5.16. Сдвиг 2-байтного числа на один бит влево для умножения на 2

Из рисунка видно, что этот процесс совершенно прозрачен — выходной бит переноса первого регистра данных становится входным для второго. Фрагмент кода, выполняющий эту операцию, выглядит следующим образом:

bcf STATUS,С ; Сбрасываем флаг С, в котором содержится вдвигаемый бит

rlf h’31’,f ; Сдвигаем младший байт, MSB оказывается во флаге С

rlf h’30’,f ; Сдвигаем старший бит

Команды передачи управления

Все команды, перечисленные в Табл. 5.4, тем или иным образом модифицируют состояние счетчика команд PC.

∙ nop

Команда «нет операции» не изменяет состояние системы, однако при ее выполнении инкрементируется PC, поскольку при этом производится выборка следующей команды из памяти программ. Таким образом, единственным результатом выполнения команды пор будет изменение значения счетчика команд.

Эта команда выполняется за один машинный цикл, так что ее основное назначение — реализация коротких задержек (с дискретностью 1 мкс при частоте тактового сигнала 4 МГц). Например, чтобы выдать на 0-й вывод порта А отрицательный импульс длительностью 2 мкс, мы можем написать:

bcf PORTA,0 ; Выставляем на RA0 НИЗКИЙ уровень

nop ; Ждем 2 мкс

nop ;

bsf PORTA,0 ; Выставляем на RA0 ВЫСОКИЙ уровень

предполагая, что 0-й бит порта А сконфигурирован как выход (см. стр. 105) и перед выполнением указанных команд на этом выходе был ВЫСОКИЙ уровень.

∙ goto

Эта команда позволяет выполнить переход к любой требуемой команде в пределах всей памяти программ.

В примере, показанном на Рис. 5.17, команда goto h’3F9’ размещена в памяти программ по адресу h’005’. В процессе выполнения программы счетчик команд инкрементируется до h’006’, а команда, расположенная по этому адресу, извлекается в конвейер для исполнения в следующем цикле. Однако при выполнении команды goto h’3F9’ в счетчик команд помещается адрес h’3F9’. То есть следующей исполняемой командой будет команда, расположенная по указанному адресу. Для этого команда № 1018 должна быть загружена в конвейер, поверх ненужного уже кода 7-й команды. Этот процесс называется сбросом конвейера, и для него требуется дополнительный машинный цикл. Поэтому команда goto выполняется за два машинных цикла.

На рисунке ячейка с адресом h’3F9’ помечена меткой FRED (завершающее двоеточие необязательно). Настоятельно рекомендуется использовать метки, а не абсолютные адреса (см. также стр. 106), поскольку программисту не так-то легко узнать, по какому адресу будет размещаться та или иная команда, и в любом случае ее положение может измениться в процессе разработки программы.

∙ btfsc

Команды btfsc и btfss играют очень важную роль при программировании микроконтроллеров PIC — это видно хотя бы из того, что они встречались практически в каждой программе данной главы. Эти команды использовались для организации операции выбора на основе состояний различных флагов регистра STATUS, обозначаемого фразами «ЕСЛИ…ТО» в текстовых описаниях алгоритмов или символом  на блок-схемах. В частности, в Программе 5.4 команда btfsc STATUS,z (или, что менее понятно, команда btfsc 3,2) позволяет реализовать цикл, выполняющийся до тех пор, пока байт данных не станет равным нулю, пропуская команду выхода из цикла goto при Z = 0.

На самом деле, команда btfsc может использоваться не только для проверки флагов регистра STATUS. Проверить можно любой бит в любом регистре данных и пропустить следующую команду, если этот бит сброшен (см. стр. 128). На Рис. 5.18 шестой командой является команда btfsc h’20’,7. Она проверяет состояние 7-го бита регистра h’20’ и, в зависимости от его состояния, выполняет одно из двух действий:

1. ЕСЛИ 7-й бит равен 0, ТО команда 7 пропускается и выполняется команда 8.

2. ЕСЛИ 7-й бит равен 1, то выполняется команда 7.

Рис. 5.18. «Перескакивание» через команду при сброшенном бите регистра h’20’

Часто в качестве такой 7-й команды используется команда goto, что позволяет программе реагировать на изменение состояния любого бита в памяти данных переходом к соответствующему блоку

При пропуске команды необходимо очищать конвейер, так же как и в случае команды goto, поскольку нарушается линейный характер выполнения программы. Это означает, что команда btfsc выполняется за один машинный цикл, если пропуск не осуществляется, и за два цикла — в противном случае.

∙ btfss

Данная команда выполняет пропуск следующей команды, если указанный бит равен 1. За исключением этого ее функционирование полностью аналогично команде btfsc.

∙ decfsz

Команда decfsz представляет альтернативный вариант реализации операции выбора. Подобно комбинации команды decf и следующей за ней команды btfss STATUS,Z, эта команда позволяет декрементировать содержимое любого регистра данных и в случае равенства его нулю пропускать следующую команду.

Типичным примером использования этой команды является подсчет числа проходов цикла. Например, предположим, что нам требуется сформировать на выводе RA0 20 импульсов, длительность каждого из которых будет не менее 2 мкс. Ниже приведен фрагмент программы, выполняющий указанные действия, а его блок-схема — на Рис. 5.19 (предполагается, что частота используемого резонатора равна 4 МГц).

       movlw d’20’ ; Запишем 20 в W

       movwf h’3F’ ; и скопируем его в регистр h’3F’ — счетчик цикла

; ----------------

LOOP

        bcf PORTA,0 ; Выставим на RAO НИЗКИЙ уровень

        nop ; Ждем один машинный цикл

        bcf PORTA,0 ; Выставим на RA0 ВЫСОКИЙ уровень

; -----------------

        decfsz h’3F’ ; Считаем в обратном направлении

            goto LOOP ; Повторить тело цикла, если не ноль

... ...; ИНАЧЕ выйти из цикла

Первоначальный код, обрамленный комментариями в виде пунктирной линии, завершается командой декрементирования с проверкой, которая обеспечивает выход из цикла при достижении регистром h’3F’ нулевого значения. Обратите внимание на запись d’20’ — так в ассемблере явно указывается десятичное число (см. стр. 267). Эта запись эквивалентна записи h’14’, однако гораздо понятнее программисту. В теле цикла используется только одна команда nop, поскольку дополнительная задержка длительностью в один машинный цикл формируется в результате выполнения команд bcf и bsf.

∙ incfsz

Команда инкрементирования регистра данных и пропуска следующей команды при нулевом результате инкрементирует, а не декрементирует содержимое указанного регистра данных. При переходе содержимого через ноль, т. е. в ситуации h’FC’ —> h’FD’ —> h’FE’ —> h’FF’ —> h’00’, будет пропущена следующая команда. Вернемся к нашему примеру, блок-схема которого показана на Рис. 5.19. Если мы предварительно загрузим в регистр h’3F’ число -20 (h’FC’) и заменим команду decfsz h’3F’,f командой incfsz h’3F’,f, то получим тот же самый результат. Только в этом случае счет будет осуществляться в прямом направлении, а не в обратном.

Рис. 5.19. Формирование 20 импульсов на выводе RA0

Примеры

Пример 5.1

Напишите программу для декрементирования 2-байтной переменной, расположенной в памяти данных по адресам h’26’ (старший байт) и h’27’ (младший байт). Помните, что команда decf не влияет на состояние флага переноса/заема.

Решение

Сначала напишем алгоритм:

1. ЕСЛИ младший байт в регистре h’27’ равен нулю, то декрементировать старший байт.

2. ВСЕГДА декрементировать младший байт.

Одна из возможных реализаций этого алгоритма приведена в Программе 5.5. При увеличении разрядности исходного значения до n байт оно будет обрабатываться точно так же — от младшего байта к старшему, с декрементированием как (n + 1) — го, так и n-го байта при равенстве нулю n-го байта.

Программа 5.5. Декрементирование 2-байтного числа

STATUS ecu 3 ; Регистр STATUS

Z equ 2 ; Бит 2 — флаг нуля

MSB equ h’26’ ; Старший байт

LSB equ h’27’ ; Младший байт

       movf LSB,f ; Младший байт равен кулю?

       btfcs STATUS,Z ; ЕСЛИ нет, ТО пропускаем декрементирование старшего байта

       decf MSB,f ; ИНАЧЕ декрементируем старший байт

       decf LSB,f ; Всегда декрементируем младший байт

Пример 5.2

В некоторых ранних моделях компьютеров для представления двоично-десятичных чисел использовался сдвоенный пятизначный код (bi-quinary). Этот код представляет собой 7-битный код, в котором при любой комбинации битов будут установлены только два из них:

Хотя такое представление чрезвычайно неэффективно (используется только 10 из 128 возможных комбинаций), его преимуществом является чрезвычайная простота обнаружения ошибок. Напишите программу для проверки корректности числа, представленного в сдвоенном пятизначном коде и находящегося в регистре h’20’ (полагаем, что старший бит равен нулю). В случае ошибки в рабочий регистр необходимо записать h’FF’, иначе — h’00’.

Решение

Все, что нам нужно сделать, — это распознать ситуацию, когда число установленных битов будет больше или меньше двух. Исходя из этого, составим перечень задач:

1. Подсчитать количество единичных битов в числе.

2. Обнулить W.

3. Если полученное число не равно двум, загрузить h’FF’ в W для индицирования ошибки.

Одна из возможных реализаций этого алгоритма приведена в Программе 5.6. В программе исходный байт сдвигается влево до тех пор, пока остаток не станет равным нулю. Если в результате сдвига устанавливается флаг переноса, то инкрементируется счетчик единичных битов. При выходе из счетчика битов вычитается двойка. Если результат вычитания равен нулю, процедура завершается с нулевым значением в W, индицирующим корректность числа. В противном случае в W загружается число h’FF’ для индикации ошибки. Это значение соответствует числу — 1 и традиционно используется для сообщения об ошибочных ситуациях. Существует всего 20 комбинаций с двумя установленными битами, из которых только 10 являются корректными. Можете ли вы доработать программу таким образом, чтобы исключить из рассмотрения эти дополнительные комбинации?

Программа 5.6. Обнаружение ошибок в сдвоенном пятизначном коде

STATUS equ 3 ; Регистр STATUS расположен по адресу h’03’

С equ 0 ; Бит 0 — флаг переноса

Z equ 2 ; Бит 2 — флаг нуля

BI_QUIN equ 20h ; Проверяемый байт

COUNT equ 21h ; Счетчик битов

BI_QUINARY clrf COUNT ; Обнуляем счетчик битов

; Задача 1

LOOP bcf STATUS,С ; Сбрасываем флаг переноса

         rlf BI_QUIN,f ; Сдвигаем байт влево

         btfsc STATUS,С ; ЕСЛИ нет переноса, ТО пропускаем команду

         incf COUNT,f ; Инкрементируем счетчик

         movf BI_QUIN,f ; Проверяем остаток

         btfss STATUS,Z ; ЕСЛИ ноль, ТО выходим из цикла

         goto LOOP ; ИНАЧЕ повторяем цикл

; Задачи 2 и 3

         movf COUNT,w ; Берем подсчитанное значение

         sublw 2 ; Сравниваем его с двумя

         btfss STATUS,Z ; ЕСЛИ ноль, завершаем программу (W = 0)

         movlw h’FF’ ; ИНАЧЕ помещаем h’FF’ (-1) в W

... ...; и выходим

Пример 5.3

Микроконтроллеры PIC младшего и среднего уровней не имеют команд для непосредственного умножения или деления. Однако для реализации этих важных арифметических операций можно использовать сложение и вычитание.

Например, для деления числа на 10 можно подсчитать, сколько раз можно вычесть из исходного числа десять без формирования бита заема. Подсчитанное таким образом значение будет частным, а оставшееся после вычитаний значение — остатком отделения. Используя этот способ, напишите программу для преобразования двоичного числа, меньшего или равного h’63’ (десятичное 99), находящегося в регистре h’20’, в два BCD-числа, помещаемые в регистры Ь’21’ (десятки) и h’22’ (единицы); см. стр. 20.

Решение

При делении числа на 10 формируется частное от 0 до 9 (напоминаю, что максимальное значение по условиям задачи равно 99) и остаток. Частное представляет собой число десятков, а остаток — число единиц.

Самым простым решением этой задачи, блок-схема которого изображена на Рис. 5.20, является циклическое вычитание десяти (addlw — d’10’ или addlw — h’0А’). В регистре TENS будет подсчитываться количество операций вычитания, выполненных до момента генерации заема, — искомое число десятков на единицу меньше подсчитанного значения. Прибавив к оставшемуся значению число 10, получим остаток отделения, т. е. число единиц.

Рис. 5.20. Преобразование десятичного числа (0…99) в BCD-число

Программа 5.7. Преобразование двоичного числа в двоично-десятичное

STATUS equ 3 ; Регистр STATUS расположен по адресу h’03’

С equ 0 ; Флаг переноса — бит 0

BINARY equ h’20’ ; Исходное число

TENS equ h’21’ ; Частное (число десятков)

UNITS equ h’22’ ; Остаток (число единиц)

; Сначала делим на 10

BIN_2_BCD clrf TENS ; Обнуляем счетчик цикла

                 movf BINARY,w ; Копируем исходный байт в W

; Вычитаем 10 и считаем кол-во вычитаний до генерации заема

LOOP incf TENS,f ; Запомнили очередную операцию

         addlw — d’10’ ; Вычли десять

         btfsc STATUS,С ; ЕСЛИ заем (С == 0), ТО выходим из цикла

           goto LOOP ; ИНАЧЕ вычитаем еще раз

; Корректируем лишнее вычитание и определяем число единиц

         decf TENS,f ; Последняя операция вычитания — лишняя

         addlw d’10’ ; Прибавляем 10 к оставшемуся значению

         movwf UNITS ; Получаем остаток от деления (число единиц)

... ...; Следующая процедура

Пример 5.4

Другим подходом к делению является представление делителя в виде суммы чисел, являющихся дробными степенями двойки. К примеру, дробь 1/3 можно приближенно выразить следующим образом:

На основе этого ряда напишите программу, которая будет делить число N, находящееся в рабочем регистре, на три, помещая частное в тот же регистр.

В качестве временных переменных для хранения частного и количества сдвигов можно использовать соответственно регистры h’20’ и h’21’.

Решение

Сначала в Программе 5.8 обнуляется байт частного, а число из W копируется в регистр Ь’21’. После этого исходное число сдвигается вправо для получения различных дробей, которые либо прибавляются, либо вычитаются из регистра h’20’, постепенно формируя искомое частное.

При последнем члене ряда, равном 1/129, результат равен 0.3359375, т. е. отклонение от точного значения составляет 0.78 %. При работе с 8-битными числами включать в ряд остальные члены не имеет смысла.

Если же необходима большая точность, то исходное значение следует расширить до 16 бит, добавив младший нулевой байт. Используя при этом 2-байтные арифметические операции и операции сдвига, можно будет увеличить число членов ряда и получить точность вплоть до 1/32768.

Программа 5.8. Процедура деления на три

QUOTIENT equ h’20’ ; Временная переменная для хранения частного

TEMP equ h’21’ ; Временная переменная для операций сдвига

STATUS equ 3 ; Регистр STATUS

С equ 0 ; Бит 0 — флаг перекоса

DIV_3 clrf QUOTIENT ; Обнуляем результат

         movwf TEMP ; Помещаем N во временный регистр

         bcf STATUS,С ; Сбрасываем флаг переноса

         rrf TEMP,f ; Сдвигаем вправо, получаем N/2

         irtovf TEMP,w ; Копируем в W

         movwf QUOTIENT ; и в QUOTIENT, получаем Q = N/2

         bcf STATUS,С ; Сбрасываем флаг переноса

         rrf TEMP,f ; Сдвигаем вправо, получаем N/4

         roovf TEMP,w ; Копируем в W

         subwf QUOTIENT,f; Вычитаем, получаем Q = N*(1/2 — 1/4)

         bcf STATUS,С ; Сбрасываем флаг переноса

         rrf TEMP,f ; Сдвигаем вправо, получаем N/8

         roovf TEMP,w ; Копируем в W

         addwf QUOTIENT,f ; Складываем, получаем Q = N*(1/2 — 1/4 + 1/8)

         bcf STATUS,С ; Сбрасываем флаг переноса

         rrf TEMP,f ; Сдвигаем вправо, получаем N/16

         movf TEMP,w ; Копируем в W

         subwf QUOTIENT,f ; Вычитаем, получаем Q = N*(1/2 — 1/4 + 1/8 — 1/16)

          bcf STATUS,С ; Сбрасываем флаг переноса

          rrf TEMP,f ; Сдвигаем вправо, получаем N/32

          movf TEMP,w ; Копируем в W

          addwf QUOTIENT,f ; Складываем, получаем Q = N*(1/2 — 1/4 + 1/8 — 1/16 + 1/32)

          bcf STATUS,С ; Сбрасываем флаг переноса

          rrf TEMP,f ; Сдвигаем вправо, получаем N/64

          movf TEMP,w ; Копируем в W

          subwf QUOTIENT,f ; Вычитаем, получаем Q = N*(1/2 — 1/4 + 1/8 — 1/16 + 1/32 — 1/64)

          bcf STATUS,С ; Сбрасываем флаг переноса

          rrf TEMP,f ; Сдвигаем вправо, получаем N/128

          movf TEMP,w ; Копируем в W

          addwf QUOTIENT,w ; Складываем, получаем N*(1/2 — 1/4 + 1/8 — 1/16 + 1/32 — 1/64 + 1/128)

Пример 5.5

Одной из операций, выполняемой процедурой перевода температуры из шкалы Цельсия в шкалу Фаренгейта, является умножение числа, находящегося в регистре h’22’, на девять. Итоговое 16-битное произведение должно находиться в регистрах h’21’ (старший байт) и h’22’ (младший байт).

Решение

Задачу умножения числа на девять можно разбить на две подзадачи: умножение исходного числа на восемь и прибавление к полученному произведению исходного числа. Соответственно, в Программе 5.9 реализован следующий алгоритм:

1. Умножить число на восемь (сдвинуть 3 раза влево).

2. Добавить исходное число к частичному 16-битному произведению.

Однобайтный множитель копируется в младший байт будущего произведения. Расширение до 16 бит производится обнулением старшего байта произведения. Сбросив флаг переноса и выполнив 3 раза операцию сдвига, получаем частичное произведение исходного числа на 8. И наконец, прибавив однобайтный множитель к двухбайтному частичному произведению, получаем окончательный результат.

Принцип «сдвиг и сложение» (см. стр. 25) может использоваться для реализации умножения любых чисел. Например, умножение на 10 можно реализовать как х8 + х2. Эту операцию запрограммировать немного сложнее, поскольку необходимо оперировать 2-байтными временными переменными. Однако это все равно гораздо быстрее, нежели простое сложение в цикле.

Программа 5.9. Процедура умножения на девять

STATUS equ 3 ; Регистр STATUS расположен по адресу h'03'

MULTIPLICAND equ h’22’ ; Множимое

PRODUCT_H equ h’23’ ; Старший байт произведения

PRODUCT_L equ h’24’ ; Младший байт произведения

С equ 0 ; Флаг переноса — 0-й бит регистра STATUS

; Задача 1: Умножить множимое на восемь

MUL_9 movf MULTIPLICAND,w ; Берем множимое, которое

           movwf PRODUCT_L ; становится младшим байтом произведения,

           clrf PRODUCT_H ; расширенным до 16 бит

           bcf STATUS,С ; Сбрасываем флаг переноса

           rlf PRODUCT_L,f ; Теперь сдвигаем 16-битное значение на три разряда влево

           rlf PRODUCT_H,f

           rlf PRODUCT_L,f

           rlf PRODUCT_H,f

           rlf PRODUCT_L,f

           rlf PRODUCT_H,f

; Задача 2: Сложить Х8 и X1

           addwf PRODUCT_L,f ;Прибавим множимое (еще в W!) к младшему байту произведения

           btfsc STATUS,С ;ЕСЛИ нет переноса, ТО пропускаем команду

             incf PRODUCT_H,f ; ИНАЧЕ увеличиваем старший байт произведения на 1

... ...; Следующая процедура

Пример 5.6

Некий температурный регистратор считывает значение температуры каждый час, и к концу дня в памяти данных накапливается 24 значения, расположенные по адресам h’30’…h’47’. Напишите программу, просматривающую этот массив и вычисляющую среднесуточную температуру.

Решение

Для вычисления среднего значения необходимо просмотреть весь массив, аналогично тому, как это было показано на Рис. 5.8, добавляя каждый его элемент к 2-байтной сумме. После прохода массива эта сумма делится на 24 для вычисления среднего значения:

Исходя из сказанного, составим перечень задач:

1. Обнулить среднее.

2. Установить указатель на Temp[0] (i = 0).

3. ВЫПОЛНЯТЬ:

а) Прибавить Temp[i] к общей 2-байтной сумме.

б) Инкрементировать i.

в) Повторять, ПОКА i < 24.

4. Разделить на 24.

Этот алгоритм реализован в Программе 5.10. Сумма элементов массива накапливается в регистрах h’48’:h’47’, которые перед входом в цикл сбрасываются. Деление реализовано циклическим вычитанием числа 24 из общей суммы. Это похоже на процедуру деления на 10, реализованную в Программе 5.7, только в данном случае однобайтная константа вычитается из двухбайтного значения. Число успешных вычитаний представляет собой частное, т. е. в нашем случае усеченное среднее значение. Разумеется, более правильно было бы округлять результат до ближайшего большего целого, если остаток больше половины делителя.

Программа 5.10. Вычисление среднесуточной температуры

INDF equ 0 ; Регистр косвенной адресации

STATUS equ 3 ; Регистр STATUS

FSR equ 4 ; Индексный регистр

TEMP_0 equ h’30’ ; Начальный элемент массива

SUM equ h’48’ ; Общая сумма накапливается в регистрах h’48’:h’49’

AVERAGE equ h’4A’ ; Среднее

Z equ 2 ; Флаг нуля — 2-й бит регистра STATUS

С equ 0 ; Флаг переноса — 0-й бит регистра STATUS

; Задача 1: Обнулить общую сумму и среднее

AV_DAILY clrf SUM ; Обнуляем старший байт суммы

                clrf SUM+1 ; Обнуляем младший байт суммы

; Задача 2: Установить указатель на Temp[0]

                movlw ТЕМР_0 ; Помещаем адрес первого элемента массива

                movwf FSR ; в регистр указателя

; Задача 3: Основной цикл

; Задача 3,а: Прибавить Temp[i] к 2-байтной сумме

LOOP1 movf INDF,w; Считываем Temp[i]

           addwf SUM+1,f ; Добавляем к младшему байту суммы

           btfsc STATUS,С ; ЕСЛИ нет переноса, ТО не инкрементируем старший байт

           incf SUM,f ; ИНАЧЕ учитываем перенос

; Задача 3,б: Инкрементирование i

NEXT incf FSR,f ; i++

;Задача 3,в: Повторять вычисления, пока i < 24

           movf FSR,w ; Считываем значение указателя

           sublw TEMP_0+h’18’ ; Вычитаем адрес конечного элемента массива (Теmр[24])

           btfss STATUS,Z ; ЕСЛИ равно, то выходим из цикла

              goto LOOP1 ; ИНАЧЕ повторяем

; Задача 4: Разделить на 24 для получения среднего

            clrf AVERAGE ; Обнуляем регистр среднего

; Вычитаем 24 и накапливаем количество вычитаний до формирования бита заема

LOOP2 movlw d’24’ ; Заносим константу 24 в W

           incf AVERAGE,f ; Запоминаем очередную операцию вычитания

           subwf SUM+1,f ;Вычитаем 24 из младшего байта суммы

           btfsc STATUS,C ; ЕСЛИ заем, ТО переходим к старшему байту

             goto LOOP2 ; ИНАЧЕ повторяем вычитание

           movlw 1 ; Вычитаем единицу из старшего байта

           subwf SUM,f

           btfsc STATUS,С ; ЕСЛИ заем (С==0), ТО выходим из цикла

             goto LOOP2 ; ИНАЧЕ повторяем вычитание

           decf AVERAGE,f ; Компенсируем лишнюю операцию вычитания

... ...; Следующая процедура

Вопросы для самопроверки

5.1. Можете ли вы сказать, какую операцию выполняют следующие команды над байтом данных, находящимся в рабочем регистре W?

addwf FILE,w

subwf FILE,w

5.2. Как можно проще всего с помощью одной команды поменять значение 0-го бита любого регистра данных? Допускается затрагивать и другие биты.

5.3. Напишите программу, которая складывает два 16-битных числа, получая 17-битную сумму. Первое слагаемое размещается в регистрах памяти данных h’20’ (старший байт) и h’21’ (младший байт). Второе слагаемое размещается в регистрах h’22’ (старший байт) и h’23’ (младший байт). Сумма запоминается в трех регистрах: h’24’ (старший байт), h’25’ (средний байт) и h’26’ (младший байт).

5.4. Напишите программу для вычитания двухбайтного числа NUM_2, находящегося в регистрах h’22’:h’23’, из числа NUM_1, находящегося в регистрах h’20’:h’21’. Двухбайтная разность должна запоминаться в регистрах h’24’:h’25’. Не забудьте, что в случае возникновения заема при вычитании младших байтов, необходимо при вычитании старших байтов дополнительно вычесть единицу из NUM_1. Считается, что NUM_2 меньше или равно NUM_1. Как можно после завершения программы определить, что это условие не было выполнено?

5.5. Как можно доработать программу из Примера 5.3, чтобы результат ее выполнения представлял собой однобайтное значение в формате TENS: UNITS, сохраняемое в регистре h’21’? Такое представление числа называется упакованным двоично-десятичным форматом, при котором в каждом байте хранится значение двух декад (по одной на каждый полубайт). Подсказка: подумайте об использовании команды swapf.

5.6. Доработайте программу из Примера 5.3 для получения трехразрядного BCD-числа, удалив ограничение на максимальный размер исходного десятичного числа. Результат должен сохраняться в регистрах h’21’, h’22’ и h’23’ (сотни, десятки и единицы соответственно).

5.7. В качестве фрагмента процедуры тестирования памяти данных в каждый регистр диапазона h’20’…h’4F’ необходимо записать значение Ь’01010101’ (h’55’). Используя в качестве заготовки Программу 5.2, напишите эту процедуру.

5.8. Доработайте программу из Примера 5.1 таким образом, чтобы она могла декрементировать 32-битное число, расположенное в регистрах h’26’…h’29’ (первым расположен старший байт).

5.9. Данные из массива, расположенного в памяти данных по адресам h’30’…h’4F’, необходимо передать побайтно в удаленный компьютер по сети Интернет. Чтобы приемник мог проверить корректность принимаемых данных, предлагается добавлять один байт, представляющий собой дополнительный код 8-битной суммы всех переданных байтов данных. Если сложить все принятые байты данных и эту контрольную сумму, то при отсутствии ошибок сумма должна быть равна нулю. Напишите процедуру, просматривающую весь массив данных и помещающую эту контрольную сумму в регистр h’20’.

5.10. Взяв за основу программу регистратора из Примера 5.6, напишите программу, вычисляющую максимальную суточную температуру. При выходе из процедуры это значение должно находиться в регистре h’48’.

5.11. В Примере 5.6 среднее значение массива отсчетов температуры вычисляется путем суммирования всех байтов и последовательного вычитания из суммы числа 24 до тех пор, пока частное не станет меньше нуля. Доработайте программу таким образом, чтобы среднее значение округлялось до ближайшего целого, т. е. при остатке, большем 12, округление производилось бы в большую сторону.

5.12. Напишите процедуру умножения однобайтного числа, находящегося в регистре h’23’, на 13. Двухбайтное произведение следует поместить в регистры h’23’:h’24’. Распределение памяти данных для этой процедуры выглядит следующим образом:

(регистр h’21’ служит для расширения однобайтного множимого до 16 байт).

Обратите внимание, что для выполнения задачи потребуется три операции сдвига и сложения.

5.13. Одним из простейших методов шифрования данных является изменение порядка битов. Например, Ь’10111100’ — > Ь’00111101’. Напишите процедуру, выполняющую эту операцию над числом, находящимся в регистре h’20’. Зашифрованное значение должно остаться в рабочем регистре. Вы можете использовать регистр h’21’ в качестве временной переменной и W — в качестве счетчика цикла. Подсказка: используйте 8 раз команду сдвига влево и вправо.

5.14. Простейший цифровой фильтр нижних частот может быть реализован с использованием алгоритма:

где Sn — n-й отсчет 8-битного АЦП, подключенного к порту В.

Напишите процедуру, реализующую такой фильтр, при условии, что три отсчета Sn-2, Sn-1 и Sn хранятся в памяти данных по адресам h’20’, h’21’ и h’22’ соответственно. Итоговое значение Array[i] должно сохраняться в регистре h’48’.

5.15. Некое 3-байтное число размещено в памяти данных следующим образом: . Напишите процедуру, подсчитывающую количество единичных битов в этом числе.

5.16. В телевизионном шоу имеется 8 участников, разделенных на две команды: А и В. У каждого участника есть кнопка, формирующая при нажатии сигнал лог. 1. Состояние всех этих кнопок можно одновременно считать с порта В микроконтроллера. Кнопки команды А подключены к младшим четырем линиям порта.

Напишите процедуру, которая будет:

• Определять момент ответа на вопрос (нажата любая из кнопок).

• Определять ответившую команду (если команда А, то регистр h’20’ обнуляется, если команда В, то в него записывается ненулевое значение).

• Определять, кто из членов команды нажал на кнопку (номер участника помещается в регистр h’21’).

5.17. Контроль четности является простейшим методом защиты цифровых данных от помех. При проверке на нечетность (odd parity) к биту данных добавляется такой дополнительный бит, чтобы итоговое количество единичных битов получилось нечетным. Напишите процедуру, которая считывает 8-битное число, находящееся в регистре h’20’, и изменяет его старший бит в соответствии с описанным принципом. Можно допустить, что перед входом в процедуру 7-й бит исходного байта всегда сброшен. Подсказка: подсчитайте количество единичных битов как в Примере 5.2, а затем проверьте младший бит полученного значения. Любая степень двойки — четная, кроме нулевой (20 = 1). Соответственно, если 0-й бит равен 1, то число нечетное.

 

Глава 6

Подпрограммы и модули

Хорошо написанное программное обеспечение должно представлять собой совокупность взаимодействующих модулей, а не одну большую программу, выполняющую все задачи от начала и до конца. У модульного принципа программирования имеется множество достоинств, без использования которых практически нельзя обойтись, когда размер кода становится больше нескольких сот строк или когда проект разрабатывается командой программистов.

Что же должны представлять собой эти модули? Чтобы ответить на этот вопрос, рассмотрим применение программных структур, которые предназначены для упрощения использования такого модульного подхода, и команды процессора, связанные с этими структурами.

Прочитав эту главу, вы:

• Убедитесь в необходимости применения модульного принципа программирования.

• Поймете структуру стека и его использование в механизме вызова подпрограмм и возврата из них.•

• Поймете термин «вложенная подпрограмма».

• Узнаете, как можно передать параметры в подпрограмму и возвратить результат в вызывающую программу.

• Сможете писать подпрограммы, оказывающие минимальное влияние на свое окружение.

• Сможете создавать программный стек для открытия и закрытия кадра в памяти данных, служащего для передачи параметров и обеспечения временной рабочей области.

Давайте заглянем внутрь вашего персонального компьютера. Скорее всего он будет похож на тот, фотография которого приведена на Рис. 6.1. На материнской плате типичного компьютера размещаются микропроцессор, различного рода память и другие сопутствующие ИС, а также некоторое количество слотов расширения. К этим слотам можно подключить плату дискового контроллера и видеокарту. Существуют и другие платы, например звуковая плата, модем, сетевой контроллер. Каждая из этих плат выполняет собственные и совершенно независимые задачи, но все они взаимодействуют через сервисы, предоставляемые главной платой — материнской.

Рис. 6.1. Модульная конструкция на примере ПК

Преимущества такой модульной конструкции очевидны:

Гибкость, т. е. относительная легкость модернизации или изменения конфигурации путем добавления или смены карт расширения.

• Возможность повторного использования компонентов от предыдущей системы.

• Возможность покупки стандартных плат или разработки собственных специализированных плат.

• Легкость обслуживания.

Разумеется, у такого подхода есть и некоторые недостатки. Материнская плата, в которую интегрированы все устройства, имеет меньший размер и, теоретически, меньшую стоимость, чем эквивалентная совокупность простой материнской платы и карт расширения. Скорее всего она даже будет более надежной, поскольку входные и выходные сигналы не проходят через разъемы. Однако, если в такой плате возникнут какие-либо неполадки, их, как правило, гораздо труднее отследить и исправить.

В модульном программировании используется тот же самый принцип построения «программных узлов», т. е. программ. Вот формальное определение принципа модульного программирования, которое дается в научно-технической литературе:

Способ разработки программ, при котором программа разбивается на логически завершенные единицы — модули. При этом каждый модуль может разрабатываться, программироваться, транслироваться и тестироваться независимо от других.

Таким образом, для написания программы в соответствии с принципом модульного программирования нам необходимо разбить общую задачу на несколько отдельных процедур, каждая из которых будет выполнять четко очерченную задачу. Такого рода модуль должен быть достаточно маленького размера, хорошо документирован и легок для понимания, причем не только для написавшего его программиста.

Преимущества модульного программирования аналогичны преимуществам модульного проектирования, но еще более убедительны:

• Модули можно тестировать, отлаживать и поддерживать по отдельности; это обеспечивает общую надежность.

• Можно повторно использовать модули из других проектов или купить их у сторонних производителей.

• Легче модернизировать программу (простой заменой модулей).

Решение о том, каким образом следует разбить программу на отдельные независимые задачи, принимается на основе опыта. Собственно кодирование этих задач в виде подпрограмм ничем не отличается от написания примеров, которые мы рассматривали в предыдущих главах (см., например, Программу 5.9 на стр. 162). Для реализации указанных подпрограмм имеется несколько дополнительных команд, которые перечислены в Табл. 6.1. Далее в настоящей главе мы рассмотрим эти команды, а также некоторые методики, применяемые при разработке программного обеспечения.

Вход в программный модуль можно выполнить посредством вызова этого модуля из другой части программы или по некоторому аппаратному событию, внешнему по отношению к ЦПУ. Этим событием может быть напряжение заданного уровня на одном из выводов процессора или же сигнал от внутреннего периферийного устройства, например признак переполнения в модуле таймера. В первом случае программный модуль называется подпрограммой, как и во многих языках высокого уровня, таких как FORTRAN и BASIC. Во втором случае речь идет о подпрограмме обработки прерывания, или просто обработчике прерывания. Принципы написания этих модулей, а также процедурьгвхода и выхода из них настолько отличаются от обычных подпрограмм, что им посвящена отдельная глава (глава

Пока же мы займемся подпрограммами.

Аналогом подпрограмм в аппаратуре являются платы расширения. Предположим, что нам необходимо реализовать задержку длительностью 1 мс. Эта задержка может потребоваться при генерации тонального сигнала частотой 500 Гц, чтобы пилот самолета обратил внимание на предупреждающие сигналы панели управления, например о низком уровне топлива или перегреве двигателей. В модульной программе эта задержка может быть реализована отдельной подпрограммой, которая будет вызываться из основной программы по мере необходимости, скажем, для периодического переключения состояния вывода порта с ВЫСОКОГО на НИЗКОЕ на время длительностью 1 мс. Эта ситуация показана на Рис. 6.2.

Рис. 6.2. Вызов подпрограммы

Вообще говоря, операция вызова подпрограммы заключается в простой записи адреса первой команды подпрограммы в счетчик команд (РС), как это делалось и в команде goto. Так, если наша подпрограмма расположена, начиная с адреса h’400’, то может показаться, что команда goto h’400’ выполнит требуемое действие. Если предположить, что точка входа в подпрограмму обозначена меткой DELAY_1MS, как в Программе 6.1, мы получим команду goto DELAY_1MS.

Теперь перед нами встает проблема — как вернуться обратно? Каким-то образом микроконтроллер должен запомнить то место в программе, откуда он перешел к подпрограмме, чтобы вернуться к следующей команде вызывающей программы. Эта ситуация показана на Рис. 6.2, причем вызов подпрограммы может осуществляться из любого места основной программы или даже из другой подпрограммы (см. Рис. 6.4).

Один из вариантов решения указанной проблемы заключается в запоминании этого адреса возврата в специальном регистре или ячейке памяти данных перед переходом к подпрограмме. А для возврата это значение может быть загружено обратно в счетчик команд при завершении подпрограммы. Указанный способ перестает работать в случае вызова одной подпрограммы из другой. В результате вторая подпрограмма перезапишет адрес, сохраненный первой подпрограммой, и возврата в основную программу никогда не произойдет. Этого можно избежать, если задействовать под стек адресов возврата более одного регистра или ячейки памяти. Структура такого стека типа LIFO (последним вошел — первым вышел) показана на Рис. 6.3, а.

Рис. 6.3. Использование аппаратного стека для хранения адресов возврата

В микроконтроллерах PIC с 14-битным ядром стек реализован в виде восьми 13-битных регистров, которые используются исключительно для хранения адресов возврата из подпрограмм. Структура, показанная на Рис. 6.3, называется также аппаратным стеком. Этот стек расположен вне адресного пространства памяти микроконтроллера, поэтому его содержимое не может быть изменено программно.

С данным стеком связан 3-битный счетчик, который указывает на следующий свободный регистр в стеке. Этот регистр указателя стека (SP) не может быть явным образом изменен с помощью какой-либо команды, а автоматически инкрементируется при каждом исполнении команды call. Эта команда выполняется аналогично команде goto и, кроме того, перед записью заданного адреса в счетчик команд заносит его текущее значение в стек.

Это значение является адресом команды, следующей за командой call, поскольку РС уже был инкрементирован, и эта следующая команда была загружена в конвейер одновременно с выполнением команды call (см. Рис. 4.4 на стр. 92).

На Рис. 6.3, б показано состояние, возникающее после вызова подпрограммы, обозначенной меткой DELAY_1MS. Процесс исполнения этой команды call DELAY_1MS выглядит следующим образом:

1. Содержимое счетчика команд загружается в ячейку стека, на которую указывает указатель стека SP. Это сохраненное значение является адресом команды, следующей за командой call.

2. Инкрементируется указатель стека.

3. Адрес назначения DELAY_1MS, представляющий собой адрес точки входа в подпрограмму, перезаписывает исходное содержимое РС. Это приводит к передаче управления в подпрограмму

За исключением операции записи адреса возврата (стадии 1 и 2), команда call функционирует точно также, как и команда goto. Соответственно, она тоже выполняется за два машинных цикла в связи с необходимостью сброса стека для удаления команды, расположенной вслед за командой call и уже загруженной на вершину конвейера. Схожи эти команды и тем, что абсолютный 11-битный адрес в коде команды call расширяется в 13-битный адрес памяти программ с помощью 3-го и 4-го битов регистра PCLATH, как показано на Рис. 5.17 (стр. 153). Дальние вызовы подпрограмм, расположенных в диапазоне адресов h’07FF’…h’1FFF’, требуются только в тех микроконтроллерах с 14-битным ядром, размер памяти программ которых составляет более 2048 слов, например в PIC16F877.

Командой, завершающей подпрограмму, должна быть команда return. Эта команда извлекает адрес возврата из стека и помещает его в счетчик команд, как показано на Рис. 6.3, в. Исполнение команды return происходит следующим образом:

1. Декрементируется указатель стека.

2. 13-битный адрес, адресуемый указателем стека, копируется из стека в счетчик команд.

Таким образом, независимо от того, откуда была вызвана подпрограмма, сразу же после ее завершения выполнение вернется к команде, следующей за командой call.

Команда retlw похожа на обычную команду return, за исключением того, что помещает заданное число в рабочий регистр. Так, чтобы после возврата из подпрограммы в W оказалось число h’FF’ (-1), скажем, для индикации ошибки, можно использовать команду retlw -1. Обе команды возврата сбрасывают конвейер и соответственно выполняются за два машинных цикла.

Прелесть стекового механизма в том, что он поддерживает вложенные подпрограммы. Рассмотрим ситуацию, показанную на Рис. 6.4, где основная программа вызывает подпрограмму первого уровня SR1, которая, в свою очередь, вызывает подпрограмму второго уровня SR2. Чтобы в конечном счете вернуться обратно в основную программу, последовательность действий при возврате должна в точности соответствовать последовательности действий при входе. Это обеспечивается LIFO-структурой стека, который автоматически поддерживает произвольные вложенные последовательности, причем глубина вложенности ограничена только размером стека. То есть в микроконтроллерах среднего семейства число уровней вложенности равно восьми. Стек может даже обрабатывать ситуацию, когда подпрограмма вызывает саму себя! Такая подпрограмма называется рекурсивной. Как мы увидим в главе 7, стековый механизм также используется и для обработки прерываний. Поэтому в системах, использующих как подпрограммы, так и прерывания, глубина вложенности будет немного меньше. Этот способ настолько удобен, что практически все микропроцессоры и микроконтроллеры осуществляют поддержку подпрограмм подобным образом.

Рис. 6.4. Вложенные подпрограммы

Поскольку стек совместно со своим указателем является частью «железа» микроконтроллера и не требует инициализации, программист должен учитывать только следующие моменты:

• Вызов подпрограмм должен осуществляться с помощью команды call.

• Точка входа в подпрограмму должна быть помечена (эта метка станет именем подпрограммы).

• Последней командой в подпрограмме должна быть команда return или retlw, причем последняя используется для загрузки в рабочий регистр заданной константы при возврате из подпрограммы (см. Программу 6.6).

В качестве упражнения давайте напишем подпрограмму формирования задержки длительностью 1 мс, которая указывалась на Рис. 6.2. Программное формирование задержки заключается в простом «ничегонеделании» в течение требуемого времени. Обычно это реализуется с помощью цикла, в котором заданная константа декрементируется до нуля, как показано на Рис. 6.5. Выбирая соответствующее значение константы, можно сформировать задержку требуемой длительности. Понятно, что эта задержка будет зависеть от частоты тактового сигнала микроконтроллера. В примерах данной главы предполагается, что тактовая частота равна 4 МГц, что соответствует длительности машинного цикла 1 мкс (см. также Программу 12.8 на стр. 401).

Рис. 6.5. Формирование задержки при помощи цикла

Рассмотрим подпрограмму, блок-схема которой приведена на Рис. 6.5. В данной подпрограмме в рабочий регистр помещается константа N, и это число инкрементируется до достижения нулевого значения в цикле, тело которого состоит из трех команд. После завершения цикла осуществляется выход из подпрограммы с использованием команды return.

Программа 6.1 . Подпрограмма формирования 1-мс задержки

;*************************

; * ФУНКЦИЯ: Формирует задержку длительностью 1 мс *

; * при частоте резонатора 4 МГц *

; * ВХОД: Нет *

; * ВЫХОД: Изменяются флаги и W *

;**********************

N equ d’249’ ; Параметр задержки, см. текст

DELAY_1MS

          movlw N ; Инициализируем цикл 1~

; ЦИКЛ -----------------

D_LOOP

          addlw -1 ; Декрементируем счетчик N-

          btfss STATUS,Z ; Проверяем: равен нулю? N+1~

            goto D_LOOP ; ЕСЛИ нет, ТО повторяем 2*(N—1)~

; -------------------------

return

Чтобы вычислить общее число машинных циклов, которое тратится на выполнение подпрограммы, и, таким образом, определить величину N, нужно оценить, сколько времени выполняется та или иная команда подпрограммы:

1. Команда call DELAY_1MS, используемая для перехода к подпрограмме, выполняется за 2 машинных цикла.

2. Команда movlw, предшествующая входу в цикл, выполняется за один машинный цикл.

3. Команды addlw, декрементирующие содержимое рабочего регистра, затрачивают в общей сложности N циклов (N проходов цикла).

4. Команда btfsc STATUS,Z, проверяющая состояние флага Z (не стал ли W равен нулю после предыдущего декрементирования?), также выполняется N-раз. Однако при последнем проходе происходит выход из цикла за счет пропуска команды перехода, что добавляет один цикл из-за сброса конвейера. Таким образом, общая задержка, вносимая этой командой, составляет N + 1 циклов.

5. Поскольку выход из цикла происходит за счет пропуска команды goto, она выполняется только N — 1 раз; каждое ее выполнение занимает 2 цикла. Ее вклад в общую задержку составляет, таким образом, (N — 1) х 2.

6. Заключительная команда return выполняется за 2 цикла.

Таким образом, общее число циклов равно

2(call) + 1(movlw) + N(addlw) + (N + 1)(btfss) + 2 x (N — 1)(goto) + 2(return)

Приравняв это выражение числу 1000, получим

2 + 1 + N + (N + 1) + 2 х (N — 1) + 2 = 1000

4 + (4 х N) = 1000

4 x N = 996

N = 249

Наша подпрограмма задержки в значительной степени ограничена тем, что рабочий регистр, как и все регистры данных микроконтроллеров PIC, является 8-битным, т. е. максимальное значение N равно b’11111111’, или десятичному 255. На самом деле значение N = 0 в нашей подпрограмме даст наибольшую задержку! Это происходит потому, что W декрементируется до проверки на ноль, т. е. его содержимое будет изменяться следующим образом: h’00’ —> h’FF’ —> h’FE’ — >… -> h’01’ —> h’00’. To есть запись нуля аналогична записи числа d’256’. Таким образом, максимальная задержка, формируемая нашей подпрограммой, составляет 4 + (4 х 256) = 1028 циклов, или 1.028 мс при частоте резонатора 4 МГц.

Задержку можно немного увеличить, добавляя в тело цикла команды пор (нет операции). Каждая команда пор добавляет один машинный цикл, не влияя при этом на флаги регистра STATUS. Таким образом, вставка после команды addlw -1 четырех команд пор, как показано в Программе 6.2, даст суммарную задержку длительностью 4 + 8 х N машинных циклов. Для N = 249 мы теперь получим 4 + 1992 = 1996 циклов, или примерно 2 мс при длительности машинного цикла 1 мкс. Подумайте, как можно использовать дополнительные команды пор для достижения точного значения в 2000 циклов?

Программа 6.2. Подпрограмма формирования 2-мс задержки

; ***********************

; * ФУНКЦИЯ: Формирует задержку длительностью 2 мс *

; * при частоте резонатора 4 МГц… *

; * ВХОД: Нет *

; * ВЫХОД: Изменяются флаги и W *

; ***********************

N equ d’249’ ; Параметр задержки, см. текст

DELAY_2MS

           movlw N ; Инициализируем цикл 1~

; ЦИКЛ ----------------

D_LOOP

            addlw -1 ; Декрементируем счетчик N~

            nop ; Добавляем четыре дополнительных N~

            nор ; цикла с помощью команд N~

            nор ; «нет операции» N~

            nop ; N~

            btfss STATUS,Z ; Проверяем: равен нулю? N+1~

               goto D_LOOP ; ЕСЛИ нет, ТО повторяем 2*(N-1)~

; ------------------------

            return

Добавляя подобным образом команды пор, можно создавать подпрограммы задержки, работающие при различных тактовых частотах. Например, при частоте кварцевого резонатора 8 МГц подпрограмма из Программы 6.2 сформирует задержку длительностью 1 мс. Так что вставка соответствующего количества команд пор позволит программисту «подстроить» нашу подпрограмму для использования совместно с резонаторами частотой от 4 до 20 МГц (см. также Программу 12.8 на стр. 401). Подумайте, сколько потребуется команд пор для получения 1-мс задержки при резонаторе на частоту 20 МГц?

Этот метод не очень подходит в тех случаях, когда необходимы достаточно большие задержки. Для их реализации можно использовать дополнительный цикл, в теле которого будет выполняться наш базовый цикл, формирующий 1-мс задержку (выделено на Рис. 6.6 серым цветом). Если этот базовый цикл будет выполнен 100 раз, то мы получим 100-мс задержку.

Рис. 6.6. Формирование задержки с использованием вложенных циклов

Код подпрограммы, реализующей 100-мс задержку, приведен в Программе 6.3. При входе в подпрограмму регистр, называемый COUNT1, инициализируется значением d’100’. Затем выполняется внутренний цикл формирования 1-мс задержки. Когда W становится равным нулю и внутренний цикл завершается, регистр COUNT1 декрементируется при помощи команды decfsz COUNT1,f. Выход из внешнего цикла произойдет только при достижении нуля в счетном регистре, т. е. после выполнения 100 внутренних циклов. Пока содержимое этого счетного регистра не равно нулю, внутренний цикл выполняется вновь и вновь.

Программа 6.3 . Подпрограмма формирования 100-мс задержки

;************************

; * ФУНКЦИЯ: Формирует задержку длительностью 100 мс *

; * при частоте резонатора 4 МГц *

; * ВХОД: Нет *

; * ВЫХОД: Изменяются флаги и W. Регистр h’30’ обнуляется *

;*************************

COUNT1 equ h’30’ ; Регистр h’30’ — счетчик цикла

N equ d’249’ ; — Параметр задержки, см. текст

DELAY_100MS

          movlw d’100’ ; Инициализируем счетчик внешнего цикла

          movwf COUNT1;

; Внешний цикл ---------------

DELAY_1MS

           movlw N ; Инициализируем внутренний цикл

; Внутренний цикл ------------

D_LOOP

           addlw -1 ; Декрементируем счетчик внутреннего цикла

           btfss STATUS,Z ; Проверяем: равен нулю?

              goto D_LOOP ; ЕСЛИ нет, ТО повторяем

; ----------------------------------

            decfsz COUNT1,f ; Декрементируем счетчик внешнего цикла

            goto DELAY_1MS ; и повторяем до достижения им нуля

; ----------------------------------

return

Разумеется, отсчет заданного времени в Программе 6.3 осуществляется не очень точно, поскольку мы игнорируем время, которое занимают команды внешнего цикла, такие как decfsz. Отчасти это компенсируется тем, что количество машинных циклов, затрачиваемых при одном проходе внутреннего цикла, уменьшилось до 4 х N, давая в общей сложности 100 х 4 цикла, поскольку команды goto и return теперь относятся к внешнему циклу. Реальная задержка, формируемая нашей подпрограммой, будет равна 99.905 мс, т. е. всего на 95 мкс меньше требуемого значения, что соответствует точности не хуже 0.1 %. Добавив одну команду пор во внешний цикл, мы получим задержку длительностью 100.005 мс, т. е. на каждые 100 000 мкс погрешность составит 5 мкс.

Максимальная задержка, формируемая этой подпрограммой, составляет 256 000 машинных циклов, что соответствует длительности 100 мс при использовании резонатора 10 МГц или 256 мс при использовании резонатора 4 МГц. Для формирования задержек большей длительности нам потребуется три вложенных цикла, что позволит получать задержки более одной минуты (см. Пример 6.3).

Наша процедура формирования 100-мс задержки является примером подпрограммы, у которой отсутствуют входные параметры (аппаратным аналогом которых являются входные сигналы карты расширения) и которая ничего не возвращает. Эта подпрограмма просто выполняет свою задачу, заключающуюся в формировании задержки (а также изменяет регистры данных, рабочий регистр и некоторые флаги регистра STATUS). Однако большинство подпрограмм используют данные, передаваемые им при вызове, а также предоставляют некоторые данные при возврате.

В качестве простого примера доработаем Программу 6.3 таким образом, чтобы она формировала задержки длительностью К x 100 мс, где К — однобайтный параметр, «передаваемый» вызывающей программой. Системное представление такой функции приведено на Рис. 6.7. Здесь имеется один входной сигнал диапазона 1…256 и полностью отсутствуют выходные сигналы. Также на этом рисунке отмечено размещение всех локальных переменных, используемых внутри подпрограммы. Последнее полезно для контроля многократного использования регистра данных различными подпрограммами и вызывающими функциями. Обратите внимание на двойные вертикальные границы прямоугольника — так на блок-схемах обычно обозначаются модули или подпрограммы.

Рис. 6.7. Системное представление подпрограммы формирования задержки длительностью К х 100 мс

Поскольку в данном случае имеется всего один однобайтный параметр, наиболее удобным местом для размещения в вызывающей программе значения К является рабочий регистр. Таким образом, для формирования 5-с задержки, в вызывающей программе можно написать:

movlw d’50’ ; 50 х 0.1 с даст нам 5-секундную задержку

call DELAY_K100MS ; Сформируем ее!

Сама подпрограмма, код которой приведен в Программе 6.4, реализует следующий алгоритм:

1. ВЫПОЛНЯТЬ, ПОКА K > 0:

а) Сформировать задержку 100 мс.

б) Декрементировать К.

2. Конец.

Программа 6.4. Подпрограмма формирования задержки длительностью К х 100 мс

; *******************

; * ФУНКЦИЯ: Формирует задержку длительностью около К х 100 мс *

; * при частоте резонатора 4 МГц *

; * ПРИМЕР: К = 100, задержка 10 с *

; * ВХОД: К в W, от 1 до 256 *

; * ВЫХОД: Изменяются флаги и W. *

; * Регистры h’30’ и h’31’обнуляются *

; ********************

COUNT1 equ h’30’ ; Счетчик 100-мс цикла

К equ h’31’ ; Временная переменная для К

N equ d’249’ ; Параметр задержки

DELAY_K100MS

          movwf К ; Сохраняем К в регистре

; ФОРМИРУЕМ 100-мс задержку -------------

DELAY_100MS

          movlw d’100’ ; Инициализируем счетчик 100-мс цикла

          movwf COUNT1

DELAY_1MS

           movlw N ; Инициализируем внутренний цикл

D_LOOP

           addlw -1; Декрементируем счетчик

           bcfss STATUS,Z; Проверяем: равен нулю?

              goto D_LOOP ; ЕСЛИ нет, ТО повторяем

           decfsz COUNT1,f ; Декрементируем счетчик 100-мс цикла

              goto DELAY_1MS ; и повторяем, пока он не будет равен 0

; Декрементируем К -------------------

            decfsz K,f

; ПОКА К > 0 -----------------------------

               goto DELAY_100MS ; Повторяем 100-мс задержку, ПОКА К > 0

FINI

            return

Программа просто копирует значение параметра из W в регистр h’31’, прежде чем приступить к выполнению уже знакомого нам участка кода (он выделен комментариями в виде пунктирной линии), который идентичен коду Программы 6.3 и предназначен для формирования одной задержки длительностью 100 мс. После формирования указанной задержки регистр, содержащий значение К, декрементируется, этот блок выполняется снова, и так до тех пор, пока К не станет равно нулю. Таким образом, код, формирующий 100-мс задержку, будет выполнен К раз.

Поскольку проверка К на ноль производится после формирования 100-мс задержки, то значение K = 0 будет интерпретироваться как К = 256. Таким образом, диапазон задержек, формируемых подпрограммой, составит 0.1…25.6 с. Проверка перед циклом даст нам диапазон задержек 0…25.5 с. И опять же время задержки вычисляется приближенно, поскольку мы игнорируем время, которое затрачивается на выполнение команд внешних циклов.

Поскольку рабочий регистр требуется для инициализации регистра COUNT1 и организации внутреннего 1-мс цикла, мы не можем использовать его для хранения величины К во время выполнения подпрограммы. Вообще говоря, если бы вызывающая программа знала, что регистр h’31’ используется подпрограммой для хранения значения К, то она могла бы передать это значение, записав его непосредственно в данный регистр. Однако, чем меньше вызывающая программа знает о «внутренностях» вызываемой подпрограммы, тем лучше, поскольку подпрограмма должна как можно меньше затрагивать свое окружение. В этом отношении подпрограмма DELAY_K100MS не слишком хороша, поскольку использует два регистра памяти данных и изменяет содержимое рабочего регистра.

В качестве примера рассмотрим Программу 6.5, в которой реализован тот же самый алгоритм, только блок формирования 100-мс задержки вызывается как существующая подпрограмма (код которой приведен в Программе 6.3), т. е. является вложенной подпрограммой. Предположим, что для хранения параметра К был выбран регистр h’30’, который также используется подпрограммой DELAY_100MS в качестве счетчика цикла. В результате после возврата из подпрограммы DELAY_100MS переменная К всегда была бы равна нулю, а последующее декрементирование всегда бы давало ненулевой результат. Таким образом, задержка окажется бесконечной и система зависнет! Эта проблема решается простым изменением строки «К equ h’30’» на строку «К equ h’31’». Однако если программист, отвечающий за разработку подпрограммы DELAY_100MS, изменит ее внутреннее распределение памяти, не уведомив об этом остальных членов команды, то может произойти настоящая катастрофа! Так что, даже если все подпрограммы прошли тестирование, определенная комбинация их вызовов может вызвать сбой. Мы еще вернемся к этой проблеме.

Программа 6.5 . Альтернативный вариант подпрограммы формирования задержки длительностью К х 100 мс

; ************************

; * ФУНКЦИЯ: Формирует задержку длительностью около К х 100 мс *

; * при частоте резонатора 4 МГц *

; * ПРИМЕР: К = 100, задержка 10 с *

; * ВХОД: К в W, от 1 до 256 *

; * ВЫХОД: Изменяются флаги и W. *

; * Регистры h’30’ и h’31’ обнуляются *

; ************************

К equ h’31’ ; Временная переменная для К

DELAY_K100MS

          movwf К ; Сохраняем К в регистре

; Задача 1: ФОРМИРУЕМ 100-мс задержку ------------

DK_LOOP

          call DELAY_100MS

; Задача 2: Декрементируем К -------------------------

           decfsz K,f; Декрементируем К

; Задача 3: ПОКА К > 0 -----------------------------------

           goto DK_LOOP ; ПОВТОРЯЕМ, ПОКА К > 0

           return

Подпрограмма, код которой приведен в Программе 6.4, все еще имеет тип void, т. е. не возвращает никаких значений в вызвавшую программу. В качестве следующего примера мы напишем подпрограмму, результатом работы которой будет однобайтное значение. Эта подпрограмма будет использоваться совместно с цифровым индикатором. Большинство таких индикаторов работают по принципу выборочного включения требуемых сегментов, как показано на Рис. 6.8. Обычно эти сегменты представляют собой светодиоды (см. Рис. 11.15 на стр. 361) или электроды элемента на жидких кристаллах.

Рис. 6.8. 7-сегментный индикатор

Системное представление нашей подпрограммы приведено на Рис. 6.8, а. Входным сигналом в данном случае является 4-битный двоичный код, находящийся в рабочем регистре. Этот код представляет собой десять десятичных цифр в виде Ь’0000’…Ь’1001’. Выходным значением, также возвращаемым в W, является соответствующий 7-сегментный код, необходимый для отображения соответствующей цифры (см. Табл. 6.2). Причем предполагается, что включение сегмента происходит при подаче на него 1, а выключение — соответственно при подаче 0. При необходимости можно реализовать и обратную полярность.

В большинстве микроконтроллеров и микропроцессоров таблицы преобразования реализуются в виде набора кодов, хранящихся в памяти программ, а результатом отображающей функции f(N) является N-й байт таблицы. В микроконтроллерах PIC с 12- и 14-битным ядром гарвардская архитектура делает невозможным использование значений памяти программ в виде данных (исключения — см. Программу 15.5 на стр. 553). Вместо этого таблицы преобразования реализуются в виде наборов команд retlw, каждая из которых возвращает однобайтную константу. Такая структура показана в Табл. 6.2. Поскольку каждая команда retlw помещает в W 8-битное значение, я сбросил неиспользуемый 7-й бит в 0.

При использовании таких таблиц извлечение k-го элемента таблицы заключается в выполнении N-й команды. При этом константа, находящаяся в коде команды, будет помещена в рабочий регистр, после чего произойдет нормальный возврат в вызывающую программу. В следующем примере k = 6, поэтому выполнится 6-я команда retlw; возвращающая в W код Ь’01111000’ для символа .

Подпрограмма, код которой приведен в Программе 6.6, осуществляет выборку элемента таблицы, прибавляя число N, передаваемое через рабочий регистр, к младшему байту счетчика команд (регистр PCL, расположенный по адресу h’02’). Поскольку PC уже указывает на 1-ю команду retlw, то после прибавления N он будет указывать на N-ю команду, что нам и требуется.

Программа 6.6. Программная реализация дешифратора 7-сегментного индикатора

; ******************

; * ФУНКЦИЯ: Возвращает N-й элемент таблицы, *

; *:где N — содержимое W *

; * ПРИМЕР: При W = 6 возвращается код b’01111101’ *

; * ВХОД: N (в диапазоне 0…9) в W *

; * ВЫХОД: N-й элемент таблицы в W *

; *******************

PCL equ 2 ; Младший байт РС — в регистре h’02’

SVN_SEG

       addwf PCL,f ; Прибавим W к PCL, получая РС + N

;                   xgfedcba

       retlw b’00111111’ ; Код для 0; Возвращается при N = 0

       retlw b’00000110’ ; Код для 1; Возвращается при N = 1

       retlw b’01011011’ ; Код для 2; Возвращается при N = 2

       retlw b’01001111’ ; Код для 3; Возвращается при N = 3

       retlw b’01100110’ ; Код для 4; Возвращается при N = 4

       retlw b’01101101’ ; Код для 5; Возвращается при N = 5

       retlw b’01111101’ ; Код для 6; Возвращается при N = 6

       retlw b’00000111’ ; Код для 7; Возвращается при N = 7

       retlw b’01111111’ ; Код для 8; Возвращается при N = 8

       retlw b’01101111’ ; Код для 9; Возвращается при N = 9

В Программе 6.6 не учитывается возможность того, что входное значение в W может быть больше h’09’. Разумеется, такого быть не должно, однако надежный код должен предусматривать все непредвиденные ситуации, даже если они ошибочны с точки зрения программы. Это особенно справедливо в том случае, если модуль предполагается повторно использовать в других приложениях. Что же случится, если такое произойдет, и как можно усовершенствовать программу для возврата в этом случае кода ошибки, скажем —1?

Кажущаяся простота метода прибавления байта, находящегося в W, к младшему байту счетчика команд (PCL) для выбора одной из N команд возврата обманчива. Несмотря на то что этот способ работает в большинстве случаев, когда размер таблицы невелик, у неопытного программиста он может привести к краху системы в самой, казалось бы, безобидной ситуации.

Проблема возникает из-за того, что изменение регистра PCL командой addwf PCL,f затрагивает только 8 младших битов 13-битного счетчика команд. Если при сложении произойдет переполнение, то в итоге счетчик команд изменится в обратном направлении! Например, если подпрограмма из Программы 6.6 будет расположена по адресу h’1F8’ (т. е. метка SVN_SEG будет соответствовать константе h’1F8’) и если в регистре W будет записано число h’08’, то в результате выполнения команды addwf PCL,f в счетчике команд вместо значения h’200’ окажется значение h’(1)F8’ + h’08’ = h’(1)00’. Весьма сомнительно, чтобы команда, расположенная по адресу h’100’, оказалась командой возврата из подпрограммы, поэтому выход из подпрограммы будет произведен некорректно и состояние стека останется несбалансированным. Точное положение подпрограммы в памяти программ предсказать нелегко, поскольку вряд ли программист может заранее сказать, в каком месте памяти программ будет расположена подпрограмма, т. е. какое значение будет в РС при входе в подпрограмму. Даже если он узнает значение SVN_SEG, просмотрев ассемблерный листинг (см. Листинг 8.2 на стр. 247), оно может впоследствии измениться в результате корректировки других частей программы. Немного усложнив программу, ее можно сделать нечувствительной к пересечению этой 256-байтной границы (см. Программу 6.7).

Хранение данных с использованием последовательности команд retlw довольно неэффективно, поскольку 14-битное слово используется для хранения 8-битного значения. В микроконтроллерах линейки PIC16F87X реализована возможность чтения 14-битных данных непосредственно из памяти программ, правда, достаточно «криво» (см. Программу 15.5 на стр. 553). Микроконтроллеры старшего семейства имеют специальные команды, такие как tblrd, которые позволяют обращаться к отдельному байту любого 16-битного слова памяти программ (см. Табл. 16.1 на стр. 585).

Использование W для передачи данных в/из подпрограмм ограничено одним байтом в каждом направлении. Если необходимо передать несколько однобайтных значений или значение большей разрядности, то для этой цели придется задействовать регистры данных. В качестве примера рассмотрим подпрограмму, код которой приведен в Программе 6.7. Эта подпрограмма выполняет перемножение двух однобайтных значений, обозначенных как MULTIPLICAND и MULTIPLIER, и возвращает 16-битное значение PRODUCT_L: PRODUCT_H (Рис. 6.9).

Рис. 6.9. Системное представление подпрограммы умножения однобайтных чисел

Алгоритм умножения, реализованный в Программе 6.7, представляет собой обобщенный вариант алгоритма, использованный нами в предыдущих процедурах умножения, например в Программе 5.9, приведенной на стр. 163. В указанном примере значение множителя, равное 9, представлялось в виде суммы (1 + 8). Аналогичным образом, умножение на 10 можно выполнить путем однократного (х2) и троекратного (х8) сдвига исходного значения влево с последующим сложением полученных частичных произведений. В общем случае множимое циклически сдвигается влево и значение, полученное в результате n-го сдвига, прибавляется к произведению, если n-й бит множителя равен 1. Выполнив эту операцию 8 раз, получим

где символы «<<» обозначают операцию сдвига влево.

Таким образом, в Программе 6.7 реализован следующий алгоритм:

1. Обнулить 2-байтное произведение.

2. Расширить множимое до 16 бит.

3. ВЫПОЛНЯТЬ, ПОКА множитель не станет равным нулю:

а) Сдвинуть множитель вправо.

б) Если есть перенос, то прибавить число, полученное в результате сдвига множимого к 2-байтному частичному произведению.

в) Сдвинуть множимое вправо.

4. Вернуть 16-битное произведение.

Программа 6.7. Подпрограмма умножения 8-битных чисел

; Глобальные объявления

STATUS equ 3 ; Регистр STATUS

С equ 0 ; Флаг переноса — бит 0

z equ 2 ; Флаг нуля — бит 2

MULTIPLIER equ h’20’ ; Множитель

MULTIPLICAND equ h’21’ ; Множимое

PRODUCT_L equ h’2E’ ; Произведение, младший байт

PRODUCT_H equ h’2F’ ; Произведение, старший байт

; Подпрограмма MUL

; ************************

; * ФУНКЦИЯ: Перемножает два байта и возвращает 2-байтное произведение *

; * ПРИМЕР: MULTIPLICAND = h’10’, MULTIPLIER = h’FF’ *

; *              : PRODUCT_H: PRODUCT_L = h’0FF0’ (d’16 x 255 = 4080’) *

; * ВХОД: MULTIPLIER = per. h’20’, MULTIPLICAND = per. h’21’ *

; * ВЫХОД: PRODUCT_H = per. h’2E’, PRODUCT_L = per. h’2F’ *

; *              : MULTIPLIER, MULTIPLICAND изменяются *

; *              : W, STATUS и MULTIPLICANDS = per. h’30’ изменяются*

; **************

;Локальные объявления

MULTIPLICANDS equ h’30’ ; Байт для расширения множимого

; Задача 1: Обнулить произведение

MUL clrf PRODUCT_L

        clrf PRODUCT_H

; Задача 2: Расширить множимое до 16-битного числа

         clrf MULTIPLICANDS

; Задача 3: ВЫПОЛНЯТЬ

       ; Задача За: Сдвинуть множитель на один бит вправо

MUL_LOOP bcf STATUS,С ; Сбрасываем флаг переноса

                  rrf MULTIPLIER,f

       ; Задача 3б: ЕСЛИ С == 1, ТО прибавить множимое к произведению

                   btfss STATUS,С ;ЕСЛИ С == 1, TO складываем

                      goto MUL_CONT ; ИНАЧЕ пропускаем эту задачу

                    movf MULTIPLICAND,w ; Выполняем сложение

                    addwf PRODUCTS,f ; Сначала младшие байты

                    btfsc STATUS,С ; ЕСЛИ нет переноса, ТО переходим к старшим

                        incf PRODUCTS,f ; ИНАЧЕ учитываем перенос

                    movf MULTI PLICANDS,w ; Теперь старшие байты

                    addwf PRODUCTS, f

        ; Задача 3в: Сдвинуть множимое на один бит влево (х2)

MUL_CONT bcf STATUS,С ; Обнуляем бит переноса

                  rlf MULTIPLICAND,f

                  rlf MULTIPLICANDS_H,f

        ; ПОКА множитель не станет равным нулю

                  movf MULTIPLIERS,f ; Проверяем множитель на ноль

                  btfss STATUS,Z

                     goto MUL_LOOP ; ЕСЛИ не ноль, ТО повторяем

                   return; ИНАЧЕ выходим из подпрограммы

В самом начале Программы 6.7 объявлены переменные, которые передаются в/из подпрограммы. Размещение всех этих глобальных объявлений в одной части программы и использование отдельного регистра для каждой из этих переменных снижает вероятность их переопределения, но за счет довольно неэкономного использования скудных ресурсов, каковыми является память данных. Временные локальные переменные объявляются в каждой подпрограмме, поскольку их необходимо будет «уничтожать» после завершения подпрограммы. Однако это все же не исключает переопределения локальных переменных при использовании вложенных подпрограмм.

Код программы в точности соответствует приведенному алгоритму. Принятие решения о том, прибавлять или нет сдвинутое влево 2-байтное множимое к частичному произведению, основывается на состоянии флага переноса после сдвига множителя вправо. Таким образом, реализуется условное сложение

Произведение = произведение + (множимое << n ) х бит n.

Чтобы не выполнять эту операцию 8 раз, суммирование завершается, когда множитель становится равным нулю. Отсюда следует, что время выполнения подпрограммы является переменной величиной, зависящей от значения множителя. Наихудшему случаю соответствует значение множителя, равное 255 (b’11111111’) — При этом выполнение подпрограммы занимает 142 машинных цикла, включая и 2 машинных цикла, затрачиваемых на исполнение команды call.

При использовании этой подпрограммы вызывающая программа копирует множимое в регистр h’20’, а множитель — в регистр h’21’. При возврате из подпрограммы 16-битное произведение можно прочитать из регистров h’2E’:h’2F’. Предположим, для примера, что нам необходимо перемножить байты, находящиеся в регистрах h’42’ и h’46’.

movf h’4’2,w ; Берем 1-е число

movwf h’20’ ; и копируем в MULTIPLIER

movf h’46’,w ; Берем 2-е число

movwf h’21’ ; и копируем в MULTIPLICAND

call MUL ;Перемножаем! После возврата результат — в регистрах h’2Е’:h’2F1

Большинство микроконтроллеров и микропроцессоров имеют программный стек, который, помимо сохранения адресов возврата из подпрограмм, позволяет программисту помещать и извлекать данные в/из памяти для передачи информации между вызывающей программой и подпрограммой. Поскольку стек является динамическим объектом, увеличивающимся в соответствии с размером передаваемых и временных переменных и уменьшающимся после завершения подпрограммы, он представляет собой очень эффективный метод распределения памяти. Более того, при каждом последующем вложенном вызове формируется новый стековый фрейм в дополнение к уже существующим. При этом вероятность перекрытия переменных при использовании вложенных подпрограмм практически исключается.

Языки высокого уровня, такие как Си (см. главу 9), обычно реализуют именно такую модель стека. При этом объем создаваемых и передаваемых переменных ограничивается только объемом памяти данных, которая может быть выделена под этот стек.

Обратной стороной такого решения является необходимость использования дополнительных ресурсов ЦПУ для создания стека и управления им. Обычно используется один или более отдельных регистров адреса или указателей стека, а для эффективной работы со стеком необходимы режимы адресации, облегчающие доступ к переменным в стеке. И даже в этом случае результат обычно медленнее, а размер кода больше, чем в моделях, использующих фиксированное распределение памяти.

Ядро микроконтроллеров PIC среднего уровня в явном виде программный стек не поддерживает. Однако такую структуру можно эмулировать, используя косвенную адресацию на базе регистров FSR и INDF (см. стр. 123). Поскольку регистр указателя стека, как таковой, отсутствует, в приведенном ниже коде для этих целей мы задействовали регистр данных h’40’, назвав его PSP.

Программист должен также зарезервировать участок памяти данных для хранения различных стековых фреймов. Мы решили, что вершина стека (Top Of Stack — TOS) будет располагаться по адресу h’50’. Если не использовать регистры из диапазона адресов h’50’…h’70’, то для нашего стека будет доступно 48 байт. В микроконтроллерах PIC16F62X этот блок памяти отображен на все банки. Поскольку адреса возврата из подпрограмм сохраняются в аппаратном стеке, наш программный стек может целиком использоваться для передачи параметров и хранения локальных переменных подпрограмм. Инициализация стека осуществляется записью константы h’50’, названной TOS, в регистр указателя PSP.

В качестве примера разберем вариант подпрограммы умножения, ориентированный на использование стека (Программа 6.7). Структура программного стека для данного случая показана на Рис. 6.10. В соответствии с рисунком, для вызова этой подпрограммы необходимо выполнить следующие действия:

1. Поместить множимое и множитель в стековый фрейм и вызвать подпрограмму.

2. Обнулить следующий байт фрейма, который будет использоваться в качестве дополнительного байта множимого.

3. Обнулить два следующих байта для инициализации будущего 2-байтного произведения.

В приведенном ниже (на следующей странице) фрагменте кода показана реализация 1-го пункта:

а) Передать содержимое регистра PSP в FSR. В результате FSR будет указывать на вершину нового стекового фрейма. Если это подпрограмма первого уровня (т. е. не вложенная в другую подпрограмму), то в этом регистре в нашем случае будет значение h’50’.

б) Скопировать множимое из памяти (предполагаем, что, как и в предыдущем примере, оно находится в регистре h’46’) в W, а затем во фрейм, используя в качестве указателя регистр FSR. Операция занесения множимого в стек завершается декрементированием регистра FSR.

в) Аналогичным образом поместить в стек множитель.

г) Вызвать подпрограмму.

Рис. 6.10. Стековый фрейм при работе с подпрограммой MUL_S

Код подпрограммы MUL_S приведен в Программе 6.8. В этой программе реализованы этапы 2…4, показанные на Рис. 6.10. Вначале обнуляется переменная MULTIPLICAND_H, используемая для расширения множимого, после чего обнуляются следующие две ячейки фрейма для инициализации произведения. Затем в регистр PSP заносится адрес следующей свободной ячейки, расположенной после фрейма. Таким образом, если из подпрограммы будет вызвана другая подпрограмма, то для следующего уровня вложенности будет задействован свой фрейм, вершина которого будет располагаться сразу же после старого фрейма. В нашем случае эти две команды можно опустить, поскольку вложенные вызовы отсутствуют, правда, при этом потребуется изменить код, реализующий в программе этап 3 в. Именно так и сделано в Примере 6.6 (см. далее).

; Глобальные объявления

PSP equ h’40’ ; Указатель псевдостека

TOS equ h’50’ ; Исходная вершина стека

INDF equ 0 ; Регистр косвенной адресации

FSR equ 04 ; Индексный регистр

STATUS equ 3 ; Регистр STATUS

С equ 0 ; Флаг переноса — бит 0

Z equ 2 ; Флаг нуля — бит 2

MULTIPLIER equ h’46’ ; Множитель

MULTIPLICAND equ h’42’ ; Множимое

MAIN

; Сначала инициализируем вершину стека,

movlw TOS

movwf PSP ; адрес которой равен h’40’

;

;Несколько позже, когда необходимо вызвать подпрограмму

; (а)

movf PSP,w ; Заносим текущий адрес вершины стека

movwf FSR ; в индексный регистр

; (б)

movf MULTIPLIСAND,w ;Помещаем множимое в стек,

movf INDF ;копируя содержимое регистра

decf FSR, f ;и декрементируя FSR

; (в)

movf MULTIPLIER,w ; Помещаем множитель в стек,

movwf INDF ; копируя содержимое регистра

decf FSR,f; и декрементируя FSR

; (г)

call MUL_S ; Вызываем подпрограмму

Основная часть подпрограммы, т. е. реализация 3-го пункта, аналогична Программе 6.7, за исключением того, что для доступа к различным элементам стека необходимо манипулировать содержимым регистра FSR. Единственное место, где использование FSR может быть не очевидным, — реализация этапа 3 в. Поскольку переход к этому блоку может осуществляться различным образом, в зависимости от того, прибавлялось ли множимое к произведению или нет, то содержимое регистра FSR при входе в этот блок не определено. Однако его можно повторно инициализировать значением из регистра PSP, который на данном этапе выполнения программы указывает на регистр, расположенный сразу же после фрейма. Если мы увеличим это значение PSP на 5, то получим адрес параметра MULTIPLICAND.

И в завершение подпрограмма «очищает» стек, записывая в регистр PSP его предыдущее значение. В данном случае для этого оно увеличивается на 5, а в общем случае — прибавляется размер фрейма n.

Для реализации Программы 6.8 нам потребовалось 45 команд в отличие от 20 команд Программы 6.7. В наихудшем случае на ее выполнение будет затрачено 274 машинных цикла, что также является гораздо худшим результатом по сравнению со 142 циклами Программы 6.7. Таким образом, с какой стороны ни посмотреть, такая стековая модель явно хуже, если не принимать во внимание возможность повторного использования кода и его надежность. Использование стековой модели будет более оправданно в случае написания больших программ при ограниченных ресурсах памяти. Однако программы, выполняющиеся на микроконтроллерах PIC младшего и среднего уровней, как правило, не очень сложны. Более того, маленький объем памяти программ может наложить дополнительные ограничения на использование такого довольно экстравагантного решения. Если время выполнения программы критично, то дополнительные накладные расходы на поддержание стека не стоят полученных результатов.

Программа 6.8. Подпрограмма умножения 1-байтных чисел, использующая стековую модель

; *************************************

; * ФУНКЦИЯ: Перемножает два байта и возвращает *

; *                  2-байтное произведение *

; * ПРИМЕР: MULTIPLICAND = h’10’, MULTIPLIER = h’FF’ *

; *               PRODUCT_H: PRODUCT_L = h’0FF0’ (d’16 x 255 = 4080’) *

; * ВХОД: MULTIPLICAND = PSP, MULTIPLIER = PSP-1 *

; *:           FSR указывает на следующий после MULTIPLIER регистр *

; * ВЫХОД: PRODUCT_H = PSP-3, PRODUCT_L = PSP-4 *

; * ВЫХОД: Изменяются W и STATUS *

; **************************************

; При вызове FSR ---> MULTIPLICAND_H (старший байт множимого)

; Задачи 1 и 2: Расширить множимое и обнулить произведение

MUL_S clrf INDF

           decf FSR,f ; FSR ---> PRODUCT_L

           clrf INDF

           decf FSR,f ; FSR ---> PRODUCT_H

           clrf INDF

           decf FSR,w ; Теперь устанавливаем указатель

           movwf PSP ; на нижнюю границу фрейма

; Задача 3: ВЫПОЛНЯТЬ

       ; Задача 3а: Сдвинуть множитель на один бит вправо

            incf FSR,f;

            incf FSR,f;

            incf FSR,f ; FSR ---> MULTIPLICANDS

MUL_LOOP bcf STATUS,С ; Сбрасываем флаг переноса

            rrf INDF,f

      ; Задача 3б: ЕСЛИ С == 1, TO прибавить множимое к произведению

            btfss STATUS,С ; ЕСЛИ С == 1, ТО выполняем сложение

               goto MUL_COMT ; ИНАЧЕ пропускаем эту операцию

             incf FSR,f ; JSR ---> MULTIPLICAND

             movf INDF,w ; Выполняем сложение

             decf F5R,f

             decf FSR,f

             decf FSR,f ; FSR ---> PRODUCT_L

             addwf INDF,f : Сначала младшие байты

             decf FSR,f; FSR ---> PRODUCT_H

             bcfsc STATUS,С ; ЕСЛИ нет переноса, переходим к старшим байтам

             incf INDF,f; ИНАЧЕ учитываем перенос

             incf FSR,f

             incf FSR,f; FSR ---> MULTIPLICANDS

             movf INDF,w ; Теперь старшие байты

             decf FSR,f

            decf FSR,f; FSR ---> PRODUCT_H

            addwf INDF,f

       ; Задача 3в: Сдвинуть множимое на один бит влево

MUL_CONT movf PSP,w ; Устанавливаем FSR на нижнюю границу фрейма

             addlw 5

             movwf FSR ; FSR ---> MULTIPLICAND

             bcf STATUS,С ; Сбрасываем бит переноса

             rlf INDF,f

             decf FSR,f

             decf FSR,f ; FSR ---> MULTIPLICANDS

             rlf INDF,f

        ; ПОКА множитель не равен нулю

             incf FSR,f ; FSR ---> MULTIPLIER

             movf INDF,f; Проверяем множитель на равенство нулю

             btfss STATUS,Z

                goto MUL_LOOP ; ЕСЛИ не ноль, TO повторяем вычисления

; Задача 4: Очистка стека

             movlw 5 ; Устанавливаем FSR на верхнюю границу фрейма,

             addwf PSP,f ; прибавляя 5 к указателю PSP

             return; Выходим из подпрограммы

Примеры

Пример 6.1

Напишите подпрограмму, формирующую фиксированную задержку длительностью 208 мкс. Частота тактового сигнала процессора составляет 4 МГц.

Решение

Для коротких временных интервалов, сравнимых с заданным, наилучшим решением будет код, приведенный в Программе 6.1.

При частоте 4 МГц длительность машинного цикла равна 1 мкс, соответственно нам потребуется 208 машинных циклов. Воспользовавшись формулой со стр. 176, получим

4 + 4 х N = 208 циклов

      4 x N = 204 цикла

           N = 51

Чему будет равно N в случае использования 20-МГц резонатора?

Программа 6.9. Подпрограмма формирования задержки длительностью 208 мкс

N equ d’51’ ; Параметр задержки

DELAY_208 movlw N ; Берем параметр задержки, 1~

D_LOOP addlw -1 ; Декрементируем счетчик N~

             btfss -1; STATUS,Z ; Пропускаем, ЕСЛИ ноль, N +1~

                goto; D_LOOP ; ИНАЧЕ повторяем, 2*(N-1)~

             return ; Выходим, 2~

Пример 6.2

В Программе 6.3 мы познакомились с подпрограммой, формирующей задержку номинальной длительностью 100 мс. Причем длительность этой задержки была подсчитана довольно приблизительно, так как мы просто умножили величину задержки, формируемую основным блоком (1 мс), на число проходов внешнего цикла, равного 100. Вычислите точную задержку при использовании 4-МГц резонатора и определите величину ошибки (в процентах).

Решение

Просматривая приведенный ниже текст подпрограммы, мы можем вычислить общее количество машинных циклов, основываясь на времени выполнения каждой команды и ее положении относительно тела цикла.

; Два цикла на переход к подпрограмме 2~

DELAY_100MS

         movlw d’100’ ; 1~

         movwf COUNT1 ; 1~

; Внешний цикл --------------

DELAY_1MS

          movlw 249 ; Эта команда выполняется 100 раз, 100*1~

; Внутренний цикл -----------

D LOOP

          addlw -1 ; 249 раз по 100 249*100~

          btfss STATUS,Z ; плюс один раз при пропуске 250*100~

             goto D_LOOP ; 248 раз по 2- и по 100 раз 248*2*100~

; ---------------------------------

          decfsz COUNT1,f ; 10 плюс один при пропуске 100+1~

              goto DELAY_1MS ; 99 раз 2*99~

; ---------------------------------

return ; 2~

В результате мы получим 99 905 циклов. Это на 95 меньше требуемого. Таким образом, ошибка составляет — (95/100000) x 100 = -0.95%

Одна команда пор, размещенная перед командой decfsz COUNT1,f, даст нам дополнительные 100 циклов. В результате длительность задержки будет равна 100.05 мкс, что соответствует ошибке +0.005 %.

Пример 6.3

Для полноты картины необходимо написать подпрограмму, формирующую минутную задержку.

Решение

Шестидесятисекундную задержку можно реализовать как 240 х 255 мс. Наше решение, код которого приведен в Программе 6.10, будет иметь точно такую же структуру, как и подпрограмма формирования задержки длительностью K х 100 мс (Программа 6.4). Максимальное значение К равно 255, что дает нам всего 25.5 с, однако мы можем увеличить время выполнения прохода среднего цикла до 250 мс, получая в результате дискретность задания задержки, равную

25 с. Задав теперь количество повторений внешнего цикла, равное 240, мы получим требуемые 60 с задержки.

Программа 6.10. Подпрограмма формирования задержки длительностью 1 мин

COUNT1 equ h’30’ ; Счетчики в регистре h’30’

COUNT2 equ h’31’ ; и h’31’

; *********************************

; * ФУНКЦИЯ: Формирует задержку длительностью ~ 1 мин при частоте резонатора 4 МГц *

; * ВХОД: Нет *

; * ВЫХОД: W и STATUS изменяются *

; * Регистры h’34:35:36’ обнуляются *

; **********************************

DELAY_1_MIN

             movlw d’240’ ; Инициализируем внешний цикл 1~

             movwf COUNT2 ; 1~

DELAY_250MS

             movlw d’250’ ; Инициализируем средний цикл 1~

             movwf COUNT1 ; для задержки 250 мс 1~

;Внутренний цикл (1 мс)

DELAY_1MS

             movlw d’249’ ; 250*240~

D_LOOP addlw -1 ; 249*250*240~

             btfss STATUS,Z ; (249+1)*250*240~

                goto D_LOOP ; (2*(249+1)*250*240)~

             decfsz COUNT1,f ; (250+1)*240~

                 goto DELAY_1MS ; 2*(250-1)*240~

              decfsz COUNT2,f ; 240+1~

                 goto DELAY_250MS ; 2*(240-1)~

              return ; 2~

Из комментариев, приведенных в листинге, можно понять логику формирования задержки, которая составляет 59.821088 с, обеспечивая точность около 0.3 %. И опять же подпрограмму можно дополнить командами nор. Каждая команда пор, помещенная после первого decfsz, добавляет 250 х 240 = 60 000 циклов, так что, вставив три таких команды, мы вместо недостачи получим перебор на 1088 циклов, что даст нам точность не хуже +0.02 %.

Пример 6.4

Напишите подпрограмму для преобразования однобайтного значения, передаваемого через рабочий регистр, в BCD-число, разряды которого будут находиться в регистрах HUNDRED (h’30’), TENS (h’31’) и UNITS (h’32’).

Решение

Мы уже встречались с процедурой преобразования двоичных чисел в двоично-десятичные в Примере 5.3, приведенном на стр. 159. Однако эта подпрограмма могла преобразовывать только числа из диапазона 0…99, т. е. имеющие два разряда. Тем не менее мы можем воспользоваться примененной в той подпрограмме методикой, вычитая сначала сотни и подсчитывая их число. После этой операции остаток будет меньше 100, и остальная часть подпрограммы будет в точности соответствовать исходной. Полный текст новой подпрограммы, приведенный в Программе 6.11, реализует следующий алгоритм:

1. Разделить на 100; остаток — число сотен.

2. Разделить частное на 10; остаток — число десятков.

3. Частное — число единиц.

Программа 6.11 . Подпрограмма преобразования двоичного числа в 3-разрядное BCD-число

; ************************************

; * ФУНКЦИЯ: Преобразовывает число из W в три BCD-разряда *

; * ПРИМЕР: Вход = h’FF’ (d’255’), HUNDREDS = h’02’ *

; *               TENS = h’02’, UNITS = h’05’ *

; * ВХОД: W — исходное число *

; * ВЫХОД: HUNDREDS = число сотен, TENS = число десятков *

; * UNITS = число единиц. W — также число единиц *

; *************************************

; Сначала делим на 100

BIN_2_BCD clrf HUNDREDS ; Обнуляем счетчик сотен

LOOP100 incf HUNDREDS,f ; Запоминаем очередное вычитание

              addlw -d’100’ ; Вычитаем сотню

              btfsc STATUS,С ; ЕСЛИ заем (С == 0), ТО выходим из цикла

                goto LOOP100 ; ИНАЧЕ вычитаем дальше

              decf HUNDREDS,f ; Корректируем лишнее вычитание,

              addlw d’100’ ; прибавляя 100 к остатку

;Затем делим на 10

               clrf TENS ; Обнуляем счетчик десятков

LOOP10 incf TENS,f ;Запоминаем очередное вычитание

               addlw -d’10’ ;Вычитаем 10

               btfsc STATUS,С ;ЕСЛИ заем (С == 0), ТО выходим из цикла

                 goto LOOP10 ; ИНАЧЕ вычитаем дальше

; Берем остаток — число единиц

               decf TENS,f ; Корректируем лишнее вычитание,

               addlw d’10’ ; прибавляя 10 к остатку

               movwf UNITS ; Получая в результате число единиц,

               return ; выходим из подпрограммы

Пример 6.5

Напишите подпрограмму для вычисления квадратного корня из 16-битного целого числа, размещенного в регистрах h’26’:h’27’. Результат должен возвращаться в рабочем регистре.

Решение

Самый примитивный способ решения этой задачи будет заключаться в простом переборе всех целых чисел k, вычислении k2 посредством умножения и проверке, что результат не превышает заданного значения. Эквивалентный, но немного более замысловатый способ основывается на вычитании последовательности чисел 1, 3, 5, 7, 9, 11…. из исходного числа до возникновения заема. Число вычитаний и будет искомым ближайшим значением квадратного корня. Эту последовательность можно записать в следующем виде:

Таким образом, возможная структура нашей подпрограммы будет следующей:

1. Сбросить счетчик цикла.

2. Задать переменную I (магическое число) равной 1.

3. ВЫПОЛНЯТЬ бесконечно:

а) Вычесть I из Number.

в) ИНАЧЕ инкрементировать счетчик цикла.

г) Прибавить 2 к I .

Вернуть значение счетчика цикла, равное √Number.

На Рис. 6.11, а показано вычисление значения √65 описанным способом. Блок-схема этого алгоритма показана на Рис. 6.11, б, а код подпрограммы приведен в Программе 6.12. Максимальное значение счетчика цикла равно h’FF’, поскольку √(65535)~=255. Поэтому под данную локальную переменную резервируется всего один регистр h’35’. Аналогично, максимально возможное значение магического числа I равно 511 (h’1FF’), поэтому под эту локальную переменную резервируется уже два регистра h’36’:h’37’. Отсюда следует, что на этапе За выполняется двухбайтное вычитание. При возникновении заема из младшего байта, к копии старшего байта I (I_Н) перед вычитанием добавляется 1. Поскольку I_Н никогда не будет больше h’01’, указанная операция никогда не вызовет переполнения. Если заем генерируется при вычитании из этого старшего байта, ЭТО означает, что результат стал меньше нуля и цикл завершается. В противном случае COUNT инкрементируется, а I умножается на 2. На самом деле значение счетчика цикла всегда равно I/2 — 1, так что переменная COUNT не нужна. Вместо этого при возврате из подпрограммы можно просто сдвинуть 16-битное значение / на один разряд вправо. При этом произойдет деление на 2, а вычитание единицы производится посредством отбрасывания бита, выдвинутого в флаг переноса (I всегда нечетное, поэтому младший бит этого числа всегда равен 1). Попробуйте реализовать этот альтернативный алгоритм.

Рис. 6.11. Нахождение корня квадратного из целого числа

Программа 6.12. Подпрограмма вычисления квадратного корня

; Глобальные объявления

STATUS equ 3 ; Регистр STATUS

equ 0 ; Флаг переноса — бит 0

NUM_H equ h’26’ ; Исходное значение, старший байт

NUM_L equ h’27’ ; Исходное значение, младший байт

; ****************

; * ФУНКЦИЯ: Вычисляет корень квадратный из 16-битного целого *

; * ПРИМЕР: Число = h’FFFF’ (65,535), Корень = h’FF’ (d’255’)*

; * ВХОД: Число в регистрах h’26’:h’27’ *

; * ВЫХОД: Корень в W. Регистры h’26’:h’27’ и h’35’:h’36’:h’37’ изменяются *

; *****************

; Локальные объявления

COUNT equ h’35’ ; Счетчик цикла

I_Н equ h’36’ ; Магическое число, старший байт

I_L equ h’37’ ; Магическое число, младший байт

; Задача 1: Обнулить счетчик цикла

SQR_ROOT clrf COUNT

; Задача 2: Инициализация магического числа единицей

                clrf I_L

                clrf I_H

                incf I_L,f

; Задача 3: ВЫПОЛНИТЬ

; Задача 3а: Number — I

SQR_LOOP movf I_L,w; Берем младший байт магического числа

                 subwf NUM_L,f; Вычитаем из младшего байта исходного числа

                 movf I_H,W ; Берем старой байт магического числа

                 btfss STATUS,С ; ЕСЛИ не было заема (С==1), ТО пропускаем

                    addlw 1 ; Учитываем заем

                 subwf NUM_H,f ; Вычитаем старшие байты

; Задача 3б: ЕСЛИ потеря значимости, ТО выйти

                 btfss STATUS,С; ЕСЛИ нет заема (С==1), ТО продолжаем

                    goto SQR_END ; ИНАЧЕ вычисление завершено

; Задача Зв: ИНАЧЕ инкрементировать счетчик цикла

                  incf COUNT,f

; Задача 3г: Увеличить магическое число на 2

                  movf I_L,w

                  addlw 2

                  btfsc STATUS,С ; Если нет переноса, ТО пропускаем

                    incf I_H,f; ИНАЧЕ корректируем старший байт

                      movwf I_L

                      goto SQR_LOOP

; Задача 4: Вернуть счетчик цикла в качестве значения корня

SQR_END movf COUNT,w ; Копируем результат в W

                      return

Пример 6.6

Напишите программу умножения содержимого регистра h’46’ на десять (х2 + х8). Для хранения данных и передачи параметров воспользуйтесь программным стеком.

Решение

Объявления глобальных переменных для подпрограммы, код которой приведен в Программе 6.13, и вызывающей процедуры следующие:

PSP equ h’40’ ; Указатель псевдостека

TOS equ h’50’ ; Исходная вершина стека

INDF equ 0 ; Регистр косвенной адресации

FSR equ 04 ; Индексный регистр

XCAND equ h’46’ ; Множимое STATUS

equ 3 ; Регистр STATUS

С equ 0 ; Флаг переноса — бит 0

; Основная процедура инициализирует указатель стека PSP

MAIN movlw TOS ; Устанавливаем PSP

         movwf PSP ; на исходную вершину стека

; .................... т. д.

; Подготовка к вызову подпрограммы X10

         movf PSP,w ; Устанавливаем FSR на текущую

         movwf FSR ; позицию в стеке

; Теперь заносим множимое в стек

          movf XCAND,w; Копируем множимое в W,

          movwf INDF ; а затем помещаем его в стек

         call X10 ; Теперь вызываем подпрограмму

; При возврате из подпрограммы PSP возвращается в исходную позицию; а произведение располагается по адресу PSP+3:PSP+2

NEXT_MAIN ... ... ; Продолжение основной программы

В Программе 6.13 сначала производится сдвиг множимого влево на один бит (умножение на два), а затем еще на два бита (умножение на 8). Два получившихся 16-битных числа затем складываются, образуя искомое произведение. Точно так же, как и в Программе 6.8, производится манипулирование регистром FSR для доступа к соответствующим данным. Двухбайтное произведение может быть считано вызывающей программой по смещению относительно указателя псевдостека. В отличие от Программы 6.8, в данном случае PSP не затрагивается ни когда в стек заносится множимое, ни в самой подпрограмме. Так сделано потому, что эта подпрограмма не вызывает других подпрограмм, т. е. не требуется формирования нового стекового фрейма.

Программа 6.13. Использование программного стека для передачи параметров и организации рабочей области

; *****************

; * ФУНКЦИЯ: Умножает 1-байтное число на 10 *

; * ПРИМЕР: h’64 х 0А = ЗЕ8’ (d’100 х 10 = 1000’) *

; * ВХОД: Множимое помещается в стек по адресу PSP *

; * ВЫХОД: Произведение по адресу PSP-3:PSP-2 в формате (старший байт:младший байт) *

; *****************

Х10 movf PSP,w ; Устанавливаем FSR на

       movwf FSR ; текущую позицию стека

       decf FSR,f ; Указываем на байт расширения XCAND

       clrf INDF ; Обнуляем его

;Теперь умножим на 2, сдвинув XCAND на один бит влево

       bcf STATUS,С ; Сбрасываем бит переноса

       incf FSR,f ;Указываем на младший байт XCAND

       rlf INDF,f ;Сдвигаем влево младший байт

       decf FSR,f ;Указываем на старший байт

       rlf INDF,f ;Сдвигаем влево старший байт

; Прибавляем к 16-битному частичному произведению

       incf FSR,f ; Указываем на младший байт XCANDx2

       movf INDF,w ; Считываем его

       decf FSR,f ; Указываем на младший байт произведения

       decf FSR,f

       movwf INDF ; Копируем туда младший байт XCANDx2

       incf FSR,f ; Указываем на старший байт XCANDx2

       movf INDF,w ; Считываем его

       decf FSR,f ; Указываем на старший байт произведения

       decf FSR,f

       movwf INDF ; Копируем туда старший байт XCANDx2

; Теперь надо сдвинуть еще на два бита, чтобы умножить на 8

       incf FSR,f ; Указываем на младший байт XCANDx2

       incf FSR,f

       incf FSR,f

       bcf STATUS,С ; Сбрасываем бит переноса

       rlf INDF,f ; Сдвигаем влево младший байт

       decf FSR, f ; Указываем на старший байт XCANDx2

       rlf INDF,f ; Сдвигаем влево старший байт

       incf FSR,f

       rlf INDF,f ; Сдвигаем влево младший байт

       decf FSR,f ; Указываем на старший байт

       rlf INDF,f ; Сдвигаем влево старший байт

; Прибавляем к 16-битному частичному произведению

       incf FSR,f ; Указываем на младший байт XCANDx8

       movf INDF,w ; Считываем его

       decf FSR,f ; Указываем на младший байт произведения

       decf FSR,f

       addwf INDF,f ; Прибавляем младший байт

       incf FSR,f ; Указываем на старший байт XCANDx8

       btfsc STATUS,С ; ЕСЛИ перенос, ТО инкрементируем старший байт

       incf INDF,f

       movf INDF,w ; ИНАЧЕ просто считываем его

       decf FSR,f ; Указываем на старший байт произведения

       decf FSR,f

       addwf INDF,f ; Прибавляем старший байт

       return

Пример 6.7

Для гарантии того, что в подпрограмме дешифратора 7-сегментного кода (Программа 6.6) не возникнет переполнения регистра PCL при прибавлении к нему смещения, программист воспользовался директивой org (ORiGin; см. стр. 244), которая указывает ассемблеру разместить подпрограмму по некоторому абсолютному адресу (h’700’ в Программе 6.14). При тестировании подпрограммы посредством вызова ее из другой части программы по адресу, меньшему h’700’, система «падает» и ее поведение становится непредсказуемым. Что было сделано неправильно?

Решение

Система сходит с ума из-за того, что при сбросе регистр PCLATH обнуляется. При вызове подпрограммы командой call h’700’ счетчик команд становится равным h’700’, однако содержимое регистра PCLATH не меняется. Позже, при выполнении команды addwf PCL,f, все содержимое 13-битного счетчика команд обновляется, причем младшие восемь битов берутся из регистра PCL, а старшие пять — из регистра PCLATH, как показано на Рис. 4.8 (стр. 103). В результате вместо перехода к одной из команд retlw происходит переход к произвольному адресу памяти программ в диапазоне h’0000’….h’00FF’! Это произойдет даже при отсутствии переполнения во время добавления к регистру PCL смещения из рабочего регистра.

Программа 6.14. Доработанный программный дешифратор 7-сегментного индикатора

org h’700’; Подпрограмма начинается с адреса h’700’

SVN_SEG

        addwf PCL,f ; Прибавим W к PCL, получая PC + N

     retlw b’00111111’ ; Код для 0; Возвращается при N = 0

     retlw b’00000110’ ; Код для 1; Возвращается при N = 1

     retlw b’01011011’ ; Код для 2; Возвращается при N = 2

     retlw b’01001111’ ; Код для 3; Возвращается при N = 3

     retlw b’01100110’ ; Код для 4; Возвращается при N = 4

     retlw b’01101101’ ; Код для 5; Возвращается при N = 5

     retlw b’01111101’ ; Код для 6; Возвращается при N = 6

     retlw b’00000111’ ; Код для 7; Возвращается при N = 7

     retlw b’01111111’ ; Код для 8; Возвращается при N = 8

     retlw b’01101111’ ; Код для 9; Возвращается при N = 9

Этой ошибки можно избежать, записав в регистр PCLATH число h’07’ (7-я страница) перед вызовом подпрограммы. В результате содержимое счетчика команд вместо h’OONN’ изменится на h’07NN’, что и требовалось.

movlw h’07’; Подготавливаем PCL

movwf PCLATH; к работе с 7-й страницей памяти программ

movf NN,w; Заносим десятичное число NN в W

call SVN_SEG; Вызываем подпрограмму

Но даже при наличии такой заплатки размер таблицы ограничен 255 элементами (это максимальное значение, добавление которого к регистру PCL не вызовет переполнения, приводящего к неверному функционированию программы). В любом случае в программировании считается дурным тоном задавать абсолютное положение секций кода программы, поскольку при этом можно перезаписать код, автоматически размещаемый самим ассемблером. В случае больших программ попытки определения и отслеживания положений несметного числа модулей чреваты ошибками. В качестве одного из вариантов решения проблемы больших таблиц, одновременно гарантирующего правильную установку регистра PCLATH, можно назвать вычисление смещения, которое необходимо прибавить к адресу начала подпрограммы, непосредственно в программе и помещение старшего байта суммы в регистр PCLATH. Разумеется, микроконтроллеры PIC поддерживают только 8-битную арифметику, поэтому нам придется отдельно вычислить значения старшего и младшего байтов адреса начала подпрограммы. К счастью, в ассемблере Microchip имеется две директивы, high и low, которые можно использовать для разбиения 13-битного адреса на 8-битные составляющие.

movlw high SVN_SEG ; Берем старший байт адреса начала таблицы,

movwf PCLATH ; который является номером страницы памяти программ

movlw low SVN,SEG+1 ; Берем младший байт адреса начала таблицы

addwf NN,w ; Прибавляем к нему смещение из регистра NN

btfsc STATUS,С ; Есть перенос?

incf PCLATH,f ; Если да, значит, перешли границу страницы

movf NN, w ; Берем смещение

call SVN_SEG ; Вызываем подпрограмму

В приведенном выше фрагменте кода используется адрес начала таблицы (SVN_SEG+1), поскольку именно это значение будет в счетчике команд после выборки команды addwf PC,f. Разумеется, в этом случае можно обойтись без инструкции org h’700’, использованной нами в Программе 6.14.

Данный фрагмент кода можно легко доработать для вычисления 2-байтного смещения, выполняя при обновлении PCL 2-байтное сложение. Как и прежде, в подпрограмму будет передаваться только младший байт смещения. Используя эту методику, можно реализовать таблицы любого размера, располагающиеся в любом месте памяти программ (эти параметры ограничены только размером памяти программ). Более подробно обо всем этом можно прочитать в фирменном руководстве по применению AN556 «Implementing a Table Read».

Вопросы для самопроверки

6.1. Один студент написал подпрограмму формирования 1-мс задержки следующим образом:

DELAY_1MS movlw d’249’ ; Инициализируем счетчик цикла

D_LOOP addlw -1 ; Декрементируем счетчик

             btfss STATUS,Z ; Проверяем: равен нулю?

                goto D_LOOP ; ЕСЛИ нет, ТО повторяем

             return

Что получится в результате?

6.2. Напишите подпрограмму, которая будет считывать значение порта В каждый час. Вы можете воспользоваться модифицированным для 60-минутного интервала вариантом Программы 6.10. Подумайте, почему данное решение является не слишком хорошим примером использования ресурсов микроконтроллера.

6.3.Напишите подпрограмму по следующим исходным данным:

• Разделить 2-байтное число на 1 — байтное.

• Делимое передается в подпрограмму в регистрах h’2E’:h’2F’

(DIVIDEND_H:DIVIDEND_L).

• Делитель передается в подпрограмму в рабочем регистре.

• Частное от деления возвращается в регистрах h’29’:h’2A’

(QUOTIENT_H:QUOTIENT_L).

• Остаток отделения возвращается в рабочем регистре.

Реализуйте деление методом вычитания до возникновения потери значимости (underflow). Похожую задачу выполняет Программа 5.10 на стр. 164. Прокомментируйте проблему, возникающую при выполнении деления указанным способом.

6.4. Доработайте Программу 6.6 таким образом, чтобы она могла отображать символы ’A’…’F’. Необходимо предусмотреть обработку как заглавных, так и строчных букв. Также ваша программа должна быть надежной.

6.5. Программа 6.15 предназначена для формирования 30-секундной задержки. Подсчитайте время выполнения подпрограммы и, таким образом, реальную длительность формируемой задержки.

Программа 6.15. Подпрограмма формирования задержки длительностью 30 с

; *********************************

; * ФУНКЦИЯ: Формирует задержку длительностью 1 мин при частоте резонатора 4 МГц *

; * ВХОД: Нет *

; * ВЫХОД: W и STATUS изменяются *

; * Регистры h’34:35:36’ обнуляются *

; **********************************

;Локальные объявления

COUNT0 equ h’34’ ; 3-байтный счетчик в регистрах h’34’

COUNT1 equ h’35’ ; и h’35’

COUNT2 equ h’36’ ; и h’36’

H equ d’153’ ; Параметр задержки

DELAY_30S

          movlw H ; Заносим 153 в старший байт счетчика

          movwf COUNT2;

          clrf COUNT1;

          clrf COUNT0;

D_LOOP

          decfsz COUNT0,f ; Декрементируем младший байт

             goto D_LOOP ; до нуля

          decfsz COUNT1,f ; Затем декрементируем средний байт

             goto D_LOOP ; до нуля и повторяем

          decfsz COUNT2,f ; Затем декрементируем старший байт

             goto D_LOOP ; до нуля и повторяем

          return

6.6. Результат считывания состояния механического переключателя может быть неверным, поскольку при замыкании контактов происходит их «дребезг» в течение нескольких миллисекунд, проявляющийся в формировании последовательности нулей и единиц. Аналогично ведут себя и некоторые электронные устройства, например фототранзистор при попадании в зону с пониженной освещенностью и выходе из нее. Хотя данная проблема может быть решена аппаратно, более экономичным решением будет использование программных методов.

Напишите подпрограмму, которая будет возвращать в 7-м бите рабочего регистра установившееся состояние переключателя, подключенного к выводу RB7 порта В. Состояние будет считаться установившимся, если при 5000 (h’1388’) последовательных операциях считывания возвращается одно и то же значение. Состояние остальных битов рабочего регистра при возврате из подпрограммы не имеет значения.

6.7. К порту В подключен аналого-цифровой преобразователь. Напишите программу, аналогичную программе из предыдущего вопроса, только на этот раз решение о стабильности считываемого значения будет приниматься в результате 1000 одинаковых считываний, а в рабочем регистре будет возвращаться код, соответствующий аналоговому напряжению.

6.8. В подпрограмме, являющейся ответом на предыдущий вопрос, возвращается стабильное значение зашумленного оцифрованного сигнала после считывания 1000 одинаковых значений. Используя эту подпрограмму, напишите основную процедуру, которая будет определять, насколько текущий результат отличается от предыдущего, и записывать этот признак в регистр h’40’. В позиции каждого отличающегося бита должна быть записана 1. Номер самого правого изменившегося бита следует поместить в регистр h’41’.

6.9. Подпрограмма, написанная в качестве ответа на вопрос 6.7, не вернет никакого значения, если в аналоговом сигнале будет присутствовать относительно высокочастотный шум, поскольку в результате «дрожания» сигнала появление 1000 одинаковых отсчетов будет весьма маловероятным событием. Для снижения шума можно воспользоваться усреднением множества отсчетов. Если шум является случайным, то считывание n значений приведет к снижению шума в √n раз. Напишите подпрограмму, которая будет 256 раз считывать значение порта В и возвращать 8-битное среднее значение В рабочем регистре W (при этом отношение сигнал/шум увеличится в 16 раз).

6.10. Схема, приведенная на Рис. 6.12, представляет собой 7-битный генератор псевдослучайных чисел, построенный на базе сдвигового регистра с элементом Исключающее ИЛИ в цепи обратной связи. Напишите подпрограмму, последовательно выдающую в порт В 127 таких двоичных случайных чисел. Подпрограмма должна инициализироваться любым ненулевым значением. Например, если начальное значение будет равно 01, то первые 32 числа будут следующими:

02 04 08 10 20 41 83 06 0C 18 30 61 С2 85 0А 14

28 51 АЗ 47 8F 1E ЗС 79 F2 Е4 С8 91 22 45 8В 16…

Последовательность повторится после формирования 127 значений.

Что произойдет, если в качестве начального значения будет взят ноль?

Рис. 6.12. Генератор псевдослучайных 7-битных чисел

6.11. Преобразование значения температуры из шкалы Цельсия в шкалу Фаренгейта осуществляется по формуле

F = C∙(9/5) + 32.

Напишите подпрограмму, в которую передается значение температуры по шкале Цельсия (от 0 до 100 °C) и которая возвращает соответствующее значение температуры по шкале Фаренгейта.

 

Глава 7

Обработка прерываний

Подпрограммы, которые мы с вами обсуждали в главе 6, можно назвать «предсказуемыми» событиями, поскольку они вызываются в соответствии с логикой программы. Ситуации же реального времени, наступающие в результате взаимодействия процессора с внешними физическими воздействиями, далеко не так просты. Очень часто вне ядра ЦПУ происходят различные события, требующие немедленной реакции процессора. Подавляющее большинство контроллеров способны реагировать на самые разнообразные события такого рода, нарушающие их нормальное функционирование. Что же касается микроконтроллеров, то запросы на обслуживание могут исходить как от встроенных периферийных устройств, например при переполнении таймера, так и извне от источника, совершенно не связанного с микроконтроллером. По меньшей мере, по сигналу внешнего сброса (одно из внешних событий) микроконтроллер должен перейти к первой команде программы. Аналогичным образом, в качестве реакции на внешний запрос на обслуживание, или прерывание, микроконтроллер должен перейти к специальной подпрограмме, называемой подпрограммой обработки прерывания.

Несмотря на то что представители семейства среднего уровня имеют различные наборы встроенных периферийных устройств, таких как аналоговые устройства, последовательные порты, таймеры, все они реагируют на прерывания одинаковым образом. В этой главе мы рассмотрим основные источники таких запросов реального времени, общие для всех моделей микроконтроллеров, и, частично, внешние прерывания. Также мы коснемся вопроса взаимодействия встроенных периферийных устройств с системой прерываний (прерывания, соответствующие конкретным устройствам, будут рассмотрены в соответствующих главах третьей части книги).

Прочитав эту главу, вы:

• Осознаете необходимость обработки прерываний.

• Разберетесь в концепции таблицы векторов как отправной точки при реакции на события сброса и прерываний.

• Ознакомитесь с последовательностью событий, происходящих при обнаружении PIC-микроконтроллером запроса на прерывание.

• Поймете причины возникновения задержки.

• Разберетесь в назначении бита глобального разрешения прерываний.

• Поймете назначение попарно объединенных битов локальной маски прерывания и локального флага прерываний, соответствующих различным источникам прерываний.

• Сможете писать простые обработчики прерываний, реализующие:

— Переключение контекста.

— Определение источника прерывания.

— Возврат по команде rеtfiе.

Простой пример, демонстрирующий необходимость быстрой реакции на событие, показан на Рис. 7.1. В данном случае нам необходимо измерить время между точками R сигнала электрокардиограммы (ЭКГ), который, по определению, является внешним событием реального времени. Временное разрешение должно быть не менее 0.1 мс, а наибольший интервал между максимальными значениями сигнала скорее всего не превысит 1.5 с. Для измерения этого интервала с заданными параметрами можно было бы использовать независимый 16-битный счетчик, работающий на частоте 10 кГц. Как мы увидим в главе 13, все микроконтроллеры среднего уровня имеют 8-битный счетчик, счетный регистр которого расположен по адресу h’01’. На Рис. 7.1 показано, как можно с помощью регистра h’3F’ организовать 16-битный счетчик. Этой конфигурации соответствует Программа 13.2, приведенная на стр. 462. Пока же предположим, что состояние счетчика можно считать из двух указанных регистров в любой момент времени. Если состояние счетчика, соответствующее последней точке R, было сохранено в двух временных регистрах, то, вычитая состояние счетчика, соответствующее текущей точке R, получим требуемую длительность.

Рис. 7.1. Обнаружение и обработка внешнего события

Следующей задачей является обнаружение максимального уровня сигнала, поскольку сердце пациента, по определению, не синхронизировано с микроконтроллером! Один из возможных способов определения точки R заключается в непрерывном считывании этого сигнала и обработки его по алгоритму выделения максимума. В данном случае для обеспечения заданного временного разрешения применение метода последовательного опроса (polling) потребует проведения измерений 10 000 раз в секунду. Учитывая, что частота сердцебиения обычно составляет около 60 ударов в минуту, 99.99 % времени будет затрачено впустую. Более того, это означает, что бóльшая часть вычислительной мощности процессора будет затрачена на обнаружение одного события из 10 000.

В качестве альтернативы можно воспользоваться внешним устройством, задачей которого будет обнаружение максимального уровня сигнала. Это устройство может быть как полностью аналоговым, так и построенным на базе микроконтроллера с аналого-цифровым преобразователем (см. Пример 14.2 на стр. 529). Независимо от реализации, этот блок будет посылать сигнал основному процессору при обнаружении точки R. Этот сигнал используется для прерывания работы микроконтроллера, который должен приостановить выполнение текущей задачи и изменить состояние счетчика не позже чем через 100 мкс.

В таких ситуациях, когда внешние процессы происходят сами по себе и никоим образом не синхронизированы с работой процессора, необходимо найти способ, посредством которого определенные события смогли бы прерывать выполнение программы и направлять процессор на выполнение требуемых действий. Опрос внешних событий применим при достаточно редком их возникновении и/или малом количестве отслеживаемых параметров и небольшом объеме вычислений. Вероятность пропуска какого-либо важного события можно уменьшить, увеличивая частоту опроса, однако в конце концов наступает такой момент, когда процессор уже не сможет ничего делать, кроме как читать данные от периферийных устройств. Вопрос недостаточности ресурсов особенно остро встает при необходимости опроса большого количества сигналов за короткий промежуток времени.

Обратной стороной мониторинга сигналов в реальном времени с использованием прерываний является усложнение аппаратных средств и аппаратно-программного интерфейса. Если вы совсем запутались, вообразите себе телефонную сеть. Можно построить такую сеть, при которой абонент снимал бы трубку, скажем, каждые 5 мин и спрашивал: «Эй! Есть тут кто-нибудь?» Не говоря уже о неудобствах (накладные расходы), связанных с выполнением этой операции, звонящему может просто надоесть ждать, и он повесит трубку. Разумеется, можно снизить вероятность такого события, увеличивая частоту опроса до, скажем, одного раза в минуту. Однако в этом случае вам придется проводить все свое время у телефона, принимая при этом всего несколько звонков в день. То есть 99 % ваших усилий будет затрачено впустую.

Данный пример достаточно нелеп, и на практике используется метод, основанный на прерываниях, когда вы поднимаете трубку, только услышав сигнал вызова. Такое решение гораздо эффективнее, но эта эффективность достигается за счет усложнения аппаратуры для телефонной компании. Кроме того, существует и другая проблема, заключающаяся в том, что вы (сравните с процессором) не имеете никакого понятия о том, когда зазвонит телефон. Ну а он, по закону подлости, зазвонит в самое неподходящее время. Так что вам (если только вы не железный) придется оторваться от текущих дел. К примеру, если вы в этот момент решали какую-либо задачу, вам придется потратить некоторое время на сохранение промежуточных результатов, с тем чтобы после разговора вы могли вернуться к ней.

Микроконтроллеры могут реагировать на запросы прерывания от самых разных источников, находящихся вне микроконтроллера, либо от различных портов и периферийных устройств, имеющихся в составе конкретного представителя семейства. Например, микроконтроллеры PIC16F874/7 поддерживают до 13 различных прерываний от этих периферийных устройств, а также одно внешнее прерывание, подаваемое через вывод INT (вывод 6 на Рис. 4.1, стр. 89). Вход внешнего прерывания использует ту же ножку микроконтроллера, к которой подключена 0-я линия порта В, т. е. вывод RB0. Программист может в индивидуальном порядке запретить или разрешить прерывания от этих источников, а также полностью запретить работу всей системы прерываний. Поскольку процесс реакции на прерывание практически не зависит от его источника, в этой главе мы главным образом будем вести речь именно об этом внешнем прерывании.

С учетом случайного характера внешних событий отклик процессора на запрос прерывания будет выглядеть следующим образом:

1. Завершение исполнения текущей команды.

2. Автоматическое сохранение, по меньшей мере, состояния счетчика команд (PC) — это необходимо для возврата из обработчика прерывания. Некоторые процессоры (такие как микроконтроллеры PIC старшего семейства) могут также автоматически сохранять содержимое регистра STATUS и других внутренних регистров.

3. Переход к соответствующей процедуре обработки прерывания.

4. Выполнение требуемых действий.

5. Восстановление состояния процессора и возврат к тому месту основной программы, в котором произошло прерывание.

Короче говоря, возникновение сигнала прерывания приводит к тому, что микроконтроллер прекращает выполнение текущей задачи, сохраняет свое состояние в прерываемой фоновой программе в стеке и переходит к выполнению специальной подпрограммы, называемой процедурой обработки прерывания (Interrupt Service Routine — ISR). Эта высокоприоритетная процедура представляет собой обычную подпрограмму, которая выполняется при наступлении заданного события.

Конкретные детали отклика на запрос прерывания в некоторой степени отличаются от процессора к процессору. В микроконтроллерах семейства среднего уровня реакция на прерывание осуществляется следующим образом (см. Рис. 7.2):

Рис. 7.2. Отклик на запрос прерывания

1. При выполнении каждой команды процессор проверяет наличие запроса прерывания от разрешенного источника. Независимо от наличия такого запроса он дожидается завершения исполнения команды, т. е. выполнение команды не прерывается даже в случае команд, выполняющихся за 2 машинных цикла.

2. Если такой запрос отсутствует, микроконтроллер просто переходит к выполнению следующей команды, и описанный процесс повторяется.

3. При наличии запроса следующие три машинных цикла затрачиваются на передачу управления процедуре обработки прерывания. Из этих циклов первый является холостым, а во время двух оставшихся производится сброс конвейера. Эта задержка длительностью от 3 до 4 машинных циклов между подачей внешнего сигнала на вывод INT и моментом выполнения 1-й команды обработчика называется задержкой обработки прерывания (interrupt latency). Бóльшую точность получить нельзя в связи со случайной природой сигнала внешнего прерывания, который может появиться на любом этапе машинного цикла.

4. Во время этой задержки микроконтроллеры PIC выполняют следующие операции:

а) Запрещается вся система прерываний, что гарантирует блокирование всех прерываний на время обработки текущего. Это осуществляется сбросом 7-го бита регистра управления прерываниями INTCON, который на Рис. 7.3 помечен как флаг общего разрешения прерываний (GIE). Указанный бит является маской прерывания, поскольку он используется для маскирования активности прерываний. При сбросе микроконтроллера бит GIE всегда сбрасывается, так что по умолчанию прерывания запрещены.

б) Состояние 13-битного счетчика команд заносится в стек точно так же, как и при выполнении команды call (см. Рис. 6.3 на стр. 172). Как и в случае подпрограмм, эта операция позволяет процессору после выхода из процедуры обработки прерывания вернуться к выполнению прерванной фоновой программы. Поскольку в PIC-микроконтроллерах среднего уровня реализован 8-уровневый аппаратный стек, из обработчика прерывания можно вызывать до семи вложенных друг в друга подпрограмм.

в) Первая команда обработчика прерывания всегда размещается по адресу h’004’ памяти программ. Так что завершающий этап рассматриваемой последовательности состоит в занесении в PC указанного адреса, называемого вектором прерывания. Разумеется, если код обработчика прерывания находится в каком-либо другом месте памяти программ, то первой командой будет команда goto, как показано в Программе 7.1.

5. Как и все подпрограммы, процедура обработки прерывания должна завершаться командой возврата. Однако при прерывании необходимо не только извлечь из стека сохраненное значение PC, но и установить бит GIE регистра INTCON для разрешения последующих прерываний. Напоминаю, что указанный бит был сброшен на этапе 4а при переходе к обработчику прерывания. Для этого используется команда возврата из прерывания retfie (см. Табл. 6.1 на стр. 170). Таким образом, после возврата в фоновую программу можно будет обработать все отложенные или будущие прерывания.

Однако отличие процедуры обработки прерывания от подпрограмм заключается не только в использовании команды retfie. Одни отличия связаны с логикой работы системы прерывания, а другие — с псевдослучайным характером прерываний. Сначала поговорим о первой ситуации, для чего рассмотрим логические узлы, относящиеся к системе прерываний.

Хотя большинство представителей микроконтроллеров PIC среднего уровня поддерживают прерывания от различных источников, три из этих источников во всех без исключения устройствах связаны с регистром INTCON, как показано на Рис. 7.3.

Рис. 7.3. Логика системы прерываний микроконтроллера PIC16F84

Этими основными источниками являются:

• Внешний сигнал, подаваемый на вывод INT. Это внешнее прерывание может генерироваться либо по нарастающему , либо по спадающему  фронту входного сигнала, что определяется состоянием бита INTEDG регистра OPTION_REG. Этот сигнал проходит через вентиль Исключающее ИЛИ, выполняющий в данном случае роль программируемого инвертора, как было описано на стр. 28.

• Изменение состояния любого из четырех старших выводов порта В (регистр h’06’) с момента последнего чтения из этого порта.

• Переполнение счетного регистра таймера/счетчика TMR0 (регистр h’01’) с h’FF’ до h’00’.

Формат регистра INTCON микроконтроллера PIC16F84 приведен на Рис. 7.3. С каждым из четырех источников прерываний связан соответствующий бит флага прерывания. Например, при появлении на 6-м выводе микроконтроллера отрицательного перепада сигнала  будет установлен флаг INTF (бит 1). Это произойдет независимо от того, разрешена работа системы прерываний или нет.

В тех случаях, когда разрешены прерывания более чем от одного источника, состояние этих флагов можно контролировать программно для определения конкретного источника; см. листинг на стр. 220. Вы можете опрашивать эти биты даже при выключенной системе прерываний. Несмотря на то что флаг устанавливается внешним (по отношению к ЦПУ) событием, сбрасываться он должен программно. При обработке прерывания жизненно важно сбрасывать этот флаг в процедуре обработки прерывания перед возвратом из обработчика, т. е. следует выполнить команду bcf INTCON, INTF.

Каждый флаг прерывания имеет соответствующий бит разрешения прерывания. Так, флагу INTF соответствует бит INTE. Это позволяет программисту произвольным образом маскировать источники прерывания в любом сочетании.

На самом деле каждый из флагов прерывания логически умножается (AND) на соответствующий бит маски прерывания. На Рис. 7.3 2-й элемент И показывает этот механизм для внешнего прерывания. Например, если требуется разрешить прерывания как от вывода INT, так и от Таймера 0, то придется установить биты 7, 5 и 4, т. е. выполнить команды

movlw b’10110000’

movwf INTCON

Биты разрешения прерывания можно изменять точно так же, как и обычные биты регистров. При сбросе микроконтроллера все маскирующие биты обнуляются, тем самым запрещая прерывания от соответствующих источников.

Что же касается именно PIC16F84, то в этой модели прерывание может генерироваться также при завершении цикла записи во внутреннюю EEPROM-память данных. В регистре INTCON для флага EEIF не хватило места, поэтому этот флаг разместили в 4-м бите регистра управления EEPROM — регистре EECON1. Чуть позже в этой главе (Рис. 7.5) мы с вами увидим, каким образом осуществляется поддержка дополнительных прерываний в более развитых представителях семейства.

Поскольку имеется четыре источника прерываний, выходы всех элементов И, на вход каждого из которых подаются сигналы флага и маски, необходимо объединить по ИЛИ для получения итогового сигнала запроса прерывания, который, собственно, и инициирует реакцию ЦПУ на прерывание. Из Рис. 7.3 видно, что сигнал с выхода этого элемента ИЛИ управляется (с помощью еще одного элемента И) битом общего разрешения прерываний GIE, который расположен в 7-м бите регистра INTCON. Однако для «пробуждения» процессора, если он находится в режиме пониженного потребления, используется именно исходный сигнал с выхода элемента ИЛИ. Как мы увидим далее в главе 10, при останове программы и переводе микроконтроллера в режим пониженного потребления можно значительно снизить ток, потребляемый устройством (до значения не более 1 мкА). Например, при контроле температуры на дне озера в течение года с интервалом в 1 час с помощью регистратора данных с батарейным питанием собственно вычисления занимают ничтожную долю от общего времени работы. Перевод микроконтроллера в режим Power Down после считывания и сохранения каждого отсчета уменьшает емкость батареи, необходимую для обеспечения работы прибора в течение столь длительного времени. Перевод в этот режим осуществляется командой sleep. Для «пробуждения» микроконтроллера используется прерывание от внешнего источника, в данном случае от экономичного генератора с периодом сигнала, равным одному часу. Как говорилось выше, процесс вывода микроконтроллера из «спящего» режима не зависит от состояния бита GIE.

Чтобы проиллюстрировать программные аспекты обработки прерываний, рассмотрим задачу подсчета числа посетителей в небольшом магазине. Одним из возможных решений было бы использование пары маломощный лазер — фотоэлемент, расположенных по бокам от входной двери. При пересечении луча покупателем на управляющий микроконтроллер поступит сигнал запроса в виде прямоугольного импульса  как показано на Рис. 7.4. В этот момент микроконтроллер может быть занят выполнением своей основной задачи, например организацией обмена данными между торговым терминалом и основным компьютером склада.

Рис. 7.4. Контроль числа посетителей магазина

Программа 7.1 . Подсчет числа посетителей

STATUS equ 3 ; Регистр STATUS

INTCON equ h’0B’ ; Регистр управления прерываниями

INTF equ 1 ; Флаг внешнего прерывания — бит 1

INTE equ 4 ; Бит маски внешнего прерывания — бит 4

GIE equ 7 ; Бит глобального разрешения прерываний — бит 7

_status equ h’4F’ ; Ячейка для сохранения регистра STATUS

EVENT equ h’20’ ; Счетчик общего числа посетителей

; Вектор сброса --------------

           org 000 ; При сбросе в PC заносится число h’000’

           goto MAIN; Переходим к началу фоновой программы

; Вектор прерывания --------

           org 004 ; При прерывании PС переходит к адресу h’004’

           goto PERS_COUNT ; Переходим к обработчику прерывания

; Фоновая программа начинается с инициализации

MAIN   bsf INTCON,INTE ; Разрешаем внешнее прерывание

           bsf INTCON,GIE ; Разрешаем работу системы прерываний

           clrf EVENT ; Обнуляем счетчик посетителей

; Бесконечный цикл основной программы —

M_LOOP ; Выполняем то

            ; Выполняем это

            ; Выполняем еще что-нибудь

           goto M_LOOP

; ****************

; * ФУНКЦИЯ: Обработчик инкрементирует счетчик EVENT *

; ****************

PERS_COUNT

          movwf _work ; Сохраняем W в памяти данных

          swapf STATUS,w ; Считываем регистр STATUS, не меняя флагов

          movwf _status ; Сохраняем его в памяти данных

; -----------------

          bcf INTCOM,TNTF ; Сбрасываем флаг прерывания

          incf EVENT,f ; Регистрируем событие

; -----------------

          swapf _status,w ; Восстанавливаем исходное состояние

          movwf STATUS ; регистра STATUS

          swapf _work,f ; Теперь восстанавливаем исходное состояние W,

          swapf _work,w ; не воздействуя на флаги

          retfie ; и возвращаемся в фоновую программу

В самом начале Программы 7.1 указано два вектора. По адресу h’000’, который является вектором сброса, размещена команда goto MAIN. Аналогично, по адресу h’004’ размещена команда перехода к обработчику прерывания goto PERS_COUNT. Размещение команд по конкретным адресам осуществляется с помощью директивы org (см. стр. 244). Таким образом, при реагировании микроконтроллера на прерывание мы получаем следующую последовательность: прерывание —> h’004’ —> PERS_COUNT.

В основной программе просто устанавливаются биты маски INTE и GIE для разрешения внешних прерываний, а также обнуляется счетчик посетителей EVENT. Расположенный далее бесконечный цикл представляет собой фоновые задачи процессора.

С точки зрения программы прерывания генерируются произвольно, и, соответственно, если они не запрещены, то они могут возникнуть при выполнении любого участка фонового цикла, в том числе и во время выполнения какой-либо подпрограммы. Процедура обработки прерывания использует внутренние регистры процессора точно так же, как и все остальные программные модули, что может привести к конфликтам при доступе к ресурсам микроконтроллера. Например, может случиться так, что прерывание возникнет в тот момент, когда фоновая программа приступит к проверке содержимого какого-либо регистра. Выполнение следующей за операцией проверки команды пропуска может зависеть, скажем, от состояния флага нуля в регистре STATUS. Однако в процедуре обработки прерывания состояние флага Z скорее всего изменится, в результате чего при возврате в фоновую программу будет выполнен пропуск команды, т. е. управление будет передано совсем не туда, куда нужно. Любое изменение флага Z может привести к неправильной передаче управления в фоновой программе. Отследить возникновение такой ситуации практически невозможно, поскольку эффект от такого прерывания проявляется от случая к случаю, так как возникновение ошибки зависит от прерывания, возникающего в неправильное время и в неправильном месте (иногда это может происходить всего раз в неделю), и поэтому ее трудно воспроизвести.

Повреждение содержимого регистра STATUS в таких случаях может иметь гораздо более серьезные последствия, например, в цикле опроса (см. листинг на стр. 219). В данном случае при входе в обработчик прерывания был установлен бит RP0 регистра STATUS (см. Рис. 4.7 на стр. 97) для обращения к регистрам, расположенным в 1-м банке памяти. Эта операция была необходима, поскольку регистры управления EEPROM имеются только в данном банке, тогда как РСН STATUS и INTCON отражены на оба банка. При выходе из подпрограммы бит RP0 сбрасывается для возврата к 0-му банку памяти, т. е. предполагается, что в момент возникновения прерывания фоновая программа работала с 0-м банком. Очевидно, что если прерывание возникнет во время работы с банком 1, то дальнейшая программа будет работать неправильно.

Отсюда становится ясно, что независимо от сложности обработчика прерывания нам необходимо сохранить, по меньшей мере, содержимое рабочего регистра и регистра STATUS. Для работы в качестве временного хранилища резервируют несколько регистров данных, которые больше ни для чего не используются. Обычно названия этих переменных начинаются с символа подчеркивания, показывающего, что эти регистры используются для системных нужд и не должны использоваться прикладной программой. В соответствии с данным соглашением в Программе 7.1 регистр h’4E’ обозначен как _work, а регистр h’4F’ — как _status.

Учитывая вышесказанное, любую процедуру обработки прерывания можно условно разделить на три отдельные части.

Сохранение контекста

Сначала копия рабочего регистра сохраняется в регистре _work. Напоминаю, что команда movwf не влияет на биты регистра STATUS. Затем регистр STATUS сохраняется в регистре данных _status (h’4F’). Казалось бы, что может быть проще — скопировать регистр STATUS в W, а затем сохранить рабочий регистр в регистре _status. Однако команда movf изменяет состояние флага Z. Поэтому для копирования данных в рабочий регистр мы вместо команды movf воспользуемся командой swapf. Команда swapf не изменяет состояние флагов, однако переставляет местами старший и младший полубайты. Но мы можем восстановить их нормальное положение при восстановлении регистра.

Процесс сохранения и восстановления состояния внутренних регистров (это внутреннее состояние называется контекстом программы) при входе и выходе из обработчика прерывания называется переключением контекста. Разумеется, необходимо следить за тем, чтобы эти ячейки не использовались в обработчике для других целей.

Основной код

Сбрасывается флаг прерывания INTF регистра INTCON, чтобы избежать повторного перехода (т. е. по сути дела бесконечного возврата) к обработке прерывания после возврата в основную программу. При наличии нескольких источников прерываний на этом этапе обработчика производится проверка соответствующих флагов прерываний (см. стр. 219), каждый из которых впоследствии может сбрасываться в начале соответствующей процедуры обработки.

В рабочей секции основного кода просто инкрементируется содержимое регистра EVENT. Естественно, это основная задача процедуры обработки прерывания.

Восстановление контекста

При завершении обработчика первым делом в рабочий регистр с помощью команды swapf заносится исходное состояние регистра STATUS, которое затем копируется оттуда в STATUS.

Исходное значение W восстанавливается из временной переменной _work с использованием двух последовательных команд swapf. При этом состояние регистра STATUS не изменяется.

И наконец, выполняется команда возврата retfie, которая также не влияет на состояние флагов регистра STATUS.

Хотя в нашем примере мы сохраняли только рабочий регистр и регистр STATUS, в других случаях может потребоваться сохранение и других РСН. Так, в Примере 7.3 сохраняется регистр FSR, поскольку он используется как в основной программе, так и в обработчике прерывания. Одним словом, если в процедуре обработки прерывания изменяются какие-либо РСН, то при выходе из обработчика должно быть восстановлено их исходное состояние. В любом случае первым необходимо сохранить содержимое рабочего регистра, поскольку он будет использоваться в качестве промежуточного хранилища при сохранении остальных регистров. Соответственно восстанавливаться рабочий регистр должен в самую последнюю очередь.

По мере возможности регистры, в которых сохраняется контекст программы, следует выбирать таким образом, чтобы они не зависели от банка памяти, используемого процессором в момент-прерывания. В микроконтроллере PIC16F84 все РОН отображены на оба банка, поэтому можно выбирать любые. Тем не менее это не слишком типичная ситуация, особенно если в модели используется большее число банков для поддержки большого количества уникальных РСН. В более новых моделях часто предусмотрена небольшая область памяти, отображенная на все банки, например, старшие 16 байт в модели PIC16F627/8, как показано на Рис. 5.4 (стр. 121). Более старые модели, такие как PIC16C74, вообще не имеют общих РОН. В этих случаях программист должен либо гарантировать, что прерывания не возникнут в те моменты, когда процессор работает с банком, отличным от используемого для сохранения, либо проверять состояние битов RP при входе в обработчик прерывания и переключаться на системный банк (обычно банк 0) перед сохранением регистра STATUS. Затем состояние битов RP в _status изменяется таким образом, чтобы оно соответствовало их исходному значению.

В нашем, намеренно упрощенном примере предполагается, что разрешено обслуживание только внешнего прерывания. В большинстве же случаев могут быть разрешены прерывания от нескольких источников. А поскольку в микроконтроллерах PIC имеется только один вектор прерывания (h’004’), то одной из первых задач обработчика будет проверка, какое из периферийных устройств вызвало прерывание. Все флаги прерываний доступны для чтения, поэтому их можно поочередно проверять, пока не найдется флаг, который установлен. В самом сложном случае, когда активны все четыре источника прерываний микроконтроллера PIC16F84, и учитывая, что регистр EECON1 находится в 1-м банке, код для проверки флагов будет выглядеть следующим образом:

bsf STATUS,RP0 ; Переключаемся на 1-й банк

btfsc INTCON,1 ; Проверяем флаг внешнего прерывания

   goto EXTERNAL ; ЕСЛИ установлен, переходим к соотв. обработчику

btfsc INTCON,2 ; Проверяем флаг прерывания от Таймера 0

   goto TIMER0 ; ЕСЛИ установлен, переходим к соотв. обработчику

btfsc INTCON,0 ; Проверяем флаг прерывания по изменению порта В

   goto CHANGE_B ; ЕСЛИ установлен, переходим к соотв. обработчику

bcfsc EECON1,4 ; Проверяем флаг прерывания от EEPROM

   goto EEPROM_WR ; ЕСЛИ установлен, переходим к соотв. обработчику

... ...

IRQ_EXIT

    bcf STATUS,RP0 ; Возвращаемся в 0-й банк

    retfie ; и выходим из обработчика

Порядок опроса определяет уровень приоритета прерывания для случая одновременной генерации нескольких прерываний. Так что, если активно и внешнее прерывание, и прерывание от Таймера 0, то сначала будет обработано первое. В этом случае наличие отложенного запроса прерывания от Таймера 0 при возврате в основную программу приведет к повторному вызову процедуры обработки прерывания, где оно будет обработано (при условии, что в этот промежуток времени не возникло новых запросов прерывания с более высоким приоритетом). В любом случае соответствующий флаг должен сбрасываться, иначе прерывание будет генерироваться бесконечно! Сброс флага производится в соответствующей секции обработчика.

При сброшенном бите маски аналогичная методика опроса может применяться для контроля событий без использования прерываний. Например, при записи байта в EEPROM (см. Программу 15.2 на стр. 547) программа обычно ожидает установки флага EEIF (4-й бит регистра EECON1), после чего сбрасывает его и продолжает выполнение.

W_LOOP

        btfss EECON1,EEIF; Проверяем состояние флага EEIF

          goto W_LOOP ; ЕСЛИ сброшен, проверяем снова

; ИНАЧЕ продолжаем выполнение программы после сброса флага EEIF

        bcf EECON1,EEIF

Вообще говоря, во всех системах, управляемых прерываниями, необходимо предпринимать некоторые меры предосторожности в случае обработки прерываний от нескольких источников. Для примера рассмотрим некоторую систему, получающую запрос прерывания от таймера, скажем, 1000 раз в секунду, а также запрос внешнего прерывания с вывода INT с нерегулярной частотой. Если обработчик внешнего прерывания выполняется, например, за 4 мс, то к моменту выхода из него будут потеряны три запроса прерывания от таймера! В некоторых процессорах имеется схема назначения приоритетов прерываний, благодаря которой запросы с более высоким приоритетом (в данном случае прерывание от таймера) могут прерывать процессы с более низким приоритетом (обработчик внешнего прерывания). В нашем же случае единственным вариантом будет введение ограничения времени выполнения обработчика внешнего прерывания — не более 1 мс. При наличии прерываний от нескольких источников необходимо рассчитать время выполнения (включая задержки) и частоту возникновения прерываний для наихудшего случая. Поскольку некоторые из этих параметров связаны с внешними событиями, не контролируемыми процессором, это может оказаться достаточно нетривиальной задачей.

Другая часто возникающая проблема связана с обработкой таких событий, при которых многобайтные значения контролируются и изменяются как в фоновой программе, так и в обработчике прерывания. Возьмем, к примеру, часы реального времени (RTC), в которых обновляется четыре регистра (HOURS, MINUTES, SECONDS и JIFFY), содержащих время в формате «часы: минуты: секунды: десятые доли секунд» (см. Пример 7.3). Предположим, что внешний генератор с частотой 10 Гц прерывает работу микроконтроллера 10 раз в секунду и обработчик прерывания обновляет указанные регистры.

Теперь предположим, что эти RTC являются составной частью системы центрального отопления. Фоновая программа должна переключать насос из включенного состояния в выключенное в 09:00:00:00. И в указанное время это произошло. Но вот наступило время 09:59:59:09 того же дня. Фоновая программа, основная задача которой состоит в отслеживании текущего времени, считывает значение часов, равное 09. После этого она собирается считывать значение минут, когда происходит прерывание от генератора. Работа фоновой программы прерывается, и состояние RTC изменяется на 10:00:00:00. При возврате в фоновую программу соответственно считываются значения 00:00:00. Считая, что сейчас 09:00:00:00, программа переключает насос, вследствие чего периоды, когда насос включен и выключен, меняются местами неограниченное число раз!

Разумеется, использовать в программе именно переключение состояния является плохим стилем программирования; то есть в 9 утра насос следовало именно включить, а не просто переключить его состояние. По крайней мере, в последнем случае система будет работать неверно лишь в течение ограниченного времени. Вообще говоря, в случае обработки подобных многобайтных данных в фоновой программе, необходимо на время запрещать обработчик прерывания сбросом соответствующего бита маски. Все прерывания, возникающие в этот период, будут обработаны позже, после установки этого бита маски. Хотя если интервал времени, в течение которого прерывание было замаскировано, окажется слишком большим, некоторые события могут быть пропущены.

Итак, подведем итог. Процедуры обработки прерываний аналогичны обычным подпрограммам, однако необходимо помнить следующее:

• Обработчик прерывания должен завершаться командой retfie вместо return.

• Рабочий регистр, а также все РСН, изменяемые в обработчике прерывания, должны быть сохранены при входе в обработчик и восстановлены при выходе из него, если они также используются в фоновой программе.

• Параметры не могут передаваться в/из обработчика прерывания через рабочий регистр. Вместо этого, при необходимости, следует использовать глобальные переменные (данные, расположенные в памяти по известному адресу).

• Обработчики прерывания должны быть как можно короче и должны выполнять минимальный набор операций. Это полезно при отладке, а также позволяет гарантировать, что ни одно из других событий не будет пропущено.

• Если в обработчике прерывания обрабатываются многобайтные данные, то при любом обращении фоновой программы к этим переменным необходимо запрещать прерывания (сбросом бита GIE).

Регистр INTCON, формат которого показан на Рис. 7.3, содержит только бит глобального разрешения прерываний, а также используется для управления тремя основными источниками прерываний, а именно внешним прерыванием, прерыванием по переполнению Таймера 0, а также прерыванием по изменению состояния выводов порта В. Последний оставшийся бит EEIE (INTCON[6]В данной книге для отделения целой части числа от дробной используется точка, а не запятая. — Примеч. ред.
В данной книге для отделения целой части числа от дробной используется точка, а не запятая. — Примеч. ред.
) в модели PIC16F84 используется для хранения бита маски прерывания модуля EEPROM. По причине нехватки места соответствующий ему бит флага прерывания EEIF размещен в другом регистре, а именно в регистре EEC0N1[4]Здесь имеется в виду не размер данных, которыми оперирует микроконтроллер, а число битов, использующихся для записи слова команды. — Примеч. пер.
(4-й бит). В 18-выводных моделях среднего уровня того же поколения часто используется такой подход. Например, бит маски прерывания от модуля АЦП ADIE в модели PIC16C71 располагается в INTCON [6]В данной книге для отделения целой части числа от дробной используется точка, а не запятая. — Примеч. ред.
В данной книге для отделения целой части числа от дробной используется точка, а не запятая. — Примеч. ред.
, а соответствующий флаг прерывания ADIF находится в регистре управления АЦП ADCONO[1]Этот сайт посвящен оригинальному изданию книги на английском языке, и все перечисленные ниже материалы представлены также на английском. — Примеч. ред.
.

Однако в общем случае система прерываний должна быть способна работать со множеством периферийных модулей, каждый из которых, в свою очередь, может иметь одно или несколько прерываний. Одним из вариантов решения такой задачи было бы перемещение битов флагов и масок прерываний в локальные регистры управления и состояния самого модуля, как это было сделано для модуля EEPROM модели PIC16F84. Более логичным, однако, было бы считать систему прерываний самостоятельным модулем и ввести группу дополнительных регистров, содержащих биты масок и флагов всех прерываний.

На Рис. 7.5 показана логика системы прерываний в моделях PIC16F627/8. Эти модели имеют помимо основных еще пять периферийных модулей, формирующих в общей сложности семь отдельных запросов на прерывание. Правая часть схемы, изображенной на рисунке, практически идентична приведенной на Рис. 7.3. Единственное отличие заключается в том, что в данном случае 6-й бит регистра INTCON называется PEIE (разрешение прерывания от периферийных устройств). Серым цветом в левой части Рис. 7.5 выделены семь дополнительных источников прерываний и логические элементы, используемые для разрешения прерываний от этих источников. Выходы указанных элементов объединены по ИЛИ для формирования одного-единственного сигнала, который, в свою очередь, управляется битом маски PEIE. Так что в этих моделях 6-й бит регистра INTCON выполняет функцию разрешения прерываний от всех дополнительных периферийных модулей.

Рис. 7.5. Логика системы прерываний микроконтроллеров PIC16F627/8

Устройства среднего уровня имеют различные наборы периферийных устройств, однако все они используют бит PEIE в качестве дополнительного бита маски для разрешения/запрещения прерываний от этих модулей. Если данный бит установлен, то любое из этих периферийных устройств может быть использовано для вывода микроконтроллера из спящего режима независимо от состояния бита глобального разрешения прерываний GIE.

На Рис. 7.6 изображены два дополнительных регистра.

Рис. 7.6. Регистры системы прерываний микроконтроллеров PIC16F627/8.

ЕЕ — Запись в EEPROM: СМ — Аналоговый компаратор: RC — Прием по USART: ТХ — Передача по USART: SSP — Синхронный последовательный порт: CCP1 — Захват/сравнение 1: TMR2 — Таймер 2: TMR1 — Таймер 1

Регистр флагов прерываний от периферийных устройств PIR1, расположенный в 0-м банке, который содержит семь флагов прерываний, и регистр разрешения прерываний от периферийных устройств PIE1, содержащий соответствующие биты маскирования прерываний. Для примера на рисунке также показано формирование сигнала прерывания, генерируемого при приеме символа со входа последовательного порта (см. Рис. 12.20 на стр. 419), который устанавливает флаг прерывания по приему символа RCIF, расположенный в 5-м бите регистра PIR1. Для разрешения генерации прерывания по этому событию и перехода процессора к обработчику прерывания нам необходимо установить три бита маски.

bsf STATUS,RP0 ; Переключаемся на 1-й банк, чтобы

bsf STATUS,RP1 ; обратиться к регистру PIE1

bsf PIE1,RCIE ; Разрешаем прерывание по приему символа

bsf INTCON,PEIE ; Разрешаем прерывания от периферийных устройств

bsf INTCON,GIE ; Разрешаем работу системы прерываний

bcf STATUS,RP0 ; Возвращаемся в 0-й банк

Необходимо сделать несколько замечаний по поводу приведенного фрагмента программы. Во-первых, как и большинство микроконтроллеров PIC среднего уровня, PIC16F627/8 имеют четыре банка регистров, выбираемых при помощи битов RP1 и RP0 регистра STATUS (см. Рис. 5.4 на стр. 121). После сброса микроконтроллера оба бита обнуляются, т. е. используется 0-й банк памяти. Поэтому в приведенном коде мы не стали дополнительно сбрасывать бит RP1. Регистр PIE1 всегда располагается в 1-м банке, так как после конфигурирования дальнейшее его изменение, как правило, не требуется. В то же время из соображений удобства регистр PIR1 размещается в 0-м банке, поскольку он часто опрашивается и изменяется. Регистр INTCON отображен на все четыре банка.

В некоторых процессорах, таких как PIC16F87X, имеется очень много источников прерывания, слишком много даже для описанной схемы. В таких устройствах вводится дополнительная пара регистров PIR2 и PIE2, располагающихся в тех же банках, что и аналогичные регистры первой пары. В этих регистрах находятся соответственно биты маски и флаги прерываний дополнительных источников.

Примеры

Пример 7.1

Возьмем конвейерную линию по упаковке консервированного горошка. Одним из элементов автоматического упаковщика является фотоэлемент, формирующий одиночный короткий импульс при пересечении луча банкой, аналогично схеме на Рис. 7.4. После прохода 24 банок на 0-м выводе порта A (RA0) необходимо сформировать импульс длительностью 1 мс , переключающий электронику упаковочного механизма. Предположим, что PIC 16F84 тактируется от 4-МГц резонатора.

Решение

Код программы приведен в Программе 7.2. По адресу вектора сброса (h’000’) расположена команда перехода к основной фоновой программе (MAIN), а по адресу вектора прерывания (h’004) расположена команда перехода к процедуре обработки прерывания, названной CAN_COUNT.

Поскольку после сброса прерывания автоматически запрещаются, различные регистры и порты обычно конфигурируются в самом начале фоновой программы до разрешения прерываний. Это исключает вероятность появления прерываний до завершения инициализационного кода. Инициализация же заключается в следующем:

1. Сброс 0-го бита порта А, что гарантирует наличие НИЗКОГО уровня на выводе RA0 после сброса.

2. Все линии параллельных портов ввода/вывода при сбросе микроконтроллера переключаются на вход. Для переключения 0-й линии порта А на выход, необходимо сбросить соответствующий бит регистра TRISA. Поскольку этот регистр располагается в 1-м банке, необходимо переключить банки памяти, изменив бит RP1 регистра STATUS (см. стр. 99). Более подробно о работе с портами ввода/вывода можно прочитать в главе 11.

3. Сбрасываются регистры EVENT, в котором подсчитывается количество импульсов от фотодетектора, и BATCH, в который заносится ненулевое значение в обработчике прерывания после прохода 24 банок.

4. Сброс всех битов регистра INTCON сбрасывает все флаги прерываний, которые могли бы установиться с момента сброса. Это очень важно, поскольку указанные флаги могут устанавливаться независимо от состояния соответствующих битов маски. Последующая установка бита глобального разрешения прерываний разрешает работу системы прерываний, а установка бита INTE разрешает внешние прерывания с вывода INT.

Основной задачей фоновой программы является периодическая проверка значения регистра BATCH. При старте программы он равен нулю, однако обработчик прерывания записывает в него ненулевое значение после прохода группы из 24 банок. При обнаружении ненулевого значения регистр обнуляется, на выходе RA0 устанавливается ВЫСОКИЙ уровень и вызывается подпрограмма 1-мс задержки, названная DELAY (см. Программу 6.1 на стр. 175). После этого цикл повторяется. Вообще говоря, фоновая программа во встраиваемых системах представляет собой именно такой бесконечный цикл, однако, как правило, в нем выполняется намного больше задач, чем в этом простом примере. Например, можно управлять многоразрядным семисегментным дисплеем аналогично тому, как это показано на Рис. 11.16 (стр. 362), отображая, скажем, общее число банок с момента запуска конвейера.

Программа 7.2 . Программа автоматического упаковщика

              include "pl6f84.inc"

_work equ h’4E’ ; Для сохранения W при входе в обработчик

_status equ h’4F’; Для сохранения STATUS при входе в обработчик

EVENT equ h’20’ ; Счетчик количества банок

BATCH equ h’21’ ; Флаг прохода 24 банок

; ---------------------

            org 000 ; Вектор сброса

            goto MAIN;  Переходим к началу фоновой программы

; ---------------------

            org 004; Вектор прерывания

            goto CAN_COUNT;  Переходим к началу обработчика прерывания

; ---------------------

; Фоновая программа начинается с секции инициализации

MAIN bcf PORTA,0; Гарантируем наличие 0 на выводе RA0

         bsf STATUS,RP0 ; Переключаемся в 1-й банк

         bcf TRISA,0 ; Переключаем вывод RA0 на выход

; Примечание. При использовании модели с модулем АЦП, например PIC16F877,

; вывод PortA[0] должен быть сконфигурирован как цифровой вход!!!

         bcf STATUS,RP0 ; Переключаемся обратно в 0-й банк

         clrf BATCH ; Обнуляем флаг группы

         clrf EVENT ; и счетчик банок

         clrf INTCON ; Сбрасываем все флаги прерывания

         bsf INTCON,GIE ; Разрешаем все прерывания

         bsf INTCON,INTE ; Разрешаем внешнее прерывание

; ----------------------

ПОКА флаг группы равен нулю, ничего не делаем

LOOP movf BATCH,f ; Проверяем BATCH == 0?

         btfsc STATUS,Z ; Пропускаем, если нет

            goto M_LOOP ; В противном случае проверяем снова

; 24 банки прошло, Формируем импульс —

         clrf BATCH ; Обнуляем флаг

         bsf PORTA,0 ; Выставляем на RA0 ВЫСОКИЙ уровень

         call DELAY ; Ждем 1 мс

         bcf PORTA,0 ;Выставляем на RA0 НИЗКИЙ уровень

         goto M_LOOP ;Возвращаемся к началу

; ----------------------

; ***********************

; Это процедура обработки прерывания

CAN_COUNT

         movwf _work ; Сохраняем W в памяти данных

         swapf STATUS,w ; Считываем текущее состояние STATUS

         movwf _status ; и сохраняем его в памяти данных

; -----------------------        

         dcf INTCON,INF ; Сбрасываем флаг внешнего прерывания

         incf EVENT,f ; Регистрируем очередное событие

         movf EVENT,w ; Читаем значение счетчика

         addlw -d’24’ ; Сравниваем с 24 (EVENT — 24)

         btfss STATUS,С ; ЕСЛИ EVENT больше или равно, ТО пропускаем (нет заема)

            goto CAN_EXIT ; ИНАЧЕ выходим

         clrf EVENT ; Обнуляем счетчик банок и сообщаем

         incf BАТСН, f ; в фоновую программу, что прошло 24 банки

; ---------------------

CAN_EXIT

         swapf _status,w ; Восстанавливаем исходное состояние STATUS

         movwf STATUS ; из памяти данных

         swapf _work,f ; Теперь восстанавливаем исходное состояние

         swapf _work,w ; рабочего регистра, не воздействуя на флаги,

         retfie ; и возвращаемся в фоновую программу

При возникновении прерывания (при пересечении банкой луча фотодетектора) управление будет передано в процедуру обработки прерывания, т. е. произойдет следующая последовательность переходов: прерывание — > h’004’ —> CAN_COUNT. Как обычно, эта процедура состоит из трех секций.

Сохранение контекста

Рабочий регистр и регистр STATUS сохраняются в памяти программ, как это было описано на стр. 217.

Основной код

Сбрасывается флаг внешнего прерывания INTF (INTCON[l]), чтобы избежать обработки ложного прерывания при возврате в фоновую программу. При наличии нескольких источников прерывания на данном этапе должен был бы производиться опрос различных флагов прерываний (см. стр. 219).

Основная задача обработчика прерываний заключается в инкрементировании регистра EVENT. Вычитая число 24 из копии этой переменной и контролируя заем (т. е. равенство С единице), программа определяет момент, когда число EVENT становится равно или даже больше 24. При этом инкрементируется переменная BATCH для передачи в фоновую программу информации о том, что прошли очередные 24 банки, a EVENT обнуляется. То есть регистр EVENT играет роль счетчика по модулю 24.

Восстановление контекста

При возврате из обработчика прерывания восстанавливаются исходные значения регистров W и STATUS, как было описано на стр. 218. Заключительная команда retfie не изменяет состояние флагов регистра STATUS.

Пример 7.2

На фабрике по производству пищевых продуктов банки с тушеной фасолью проходят по конвейеру через туннельную печь, как показано в верхней части Рис. 7.7, где их содержимое стерилизуется. Фотодетекторы используются для подсчета банок, вошедших в печь и вышедших из нее. При пересечении луча на выходе соответствующего фотодетектора устанавливается ВЫСОКИЙ уровень.

Вам требуется разработать интерфейс данной системы, управляемый прерываниями, объединив два сигнала для активизации одного входа INT микроконтроллера. Звуковой излучатель, подключенный к 0-му выводу порта В, должен подать звуковой сигнал, сигнализируя о заторе, если количество банок в печи станет больше четырех.

Решение

С аппаратной точки зрения в данном примере имеется две задачи. Первая — как определить, какой из детекторов, входной или выходной, сформировал запрос прерывания. Из Рис. 7.7 видно, что при перекрывании луча оба фотоэлемента формируют тактовый импульс, подаваемый на тактовый вход соответствующего D-триггера. Поскольку вход триггера подтянут к лог. 1, подача тактового импульса вызывает переключение триггера в состояние лог. 1. За счет объединения выходных сигналов обоих триггеров по ИЛИ при перекрытии любого луча на выводе INT формируется нарастающий фронт сигнала.

Рис. 7.7. Система контроля печи

Состояние обоих внешних флагов IN и OUT можно считать соответственно с входов RA0 и RA1 порта А, что позволяет различать эти два события (вход банки в печь и ее выход) в обработчике прерывания. Соответствующий флаг затем можно сбросить, подав управляющий сигнал на вход сброса соответствующего триггера. Под эти сигналы задействованы еще две линии порта — RA2 и RA3 (CanceMN и Cancel_OUT соответственно).

Чтобы проиллюстрировать работу этого узла, предположим, что банка только что пересекла луч выходного детектора, как показано на Рис. 7.7. При этом произойдут следующие события:

1. Импульс, сформированный детектором, подается на триггер выходного датчика OUT.

2. Триггер переключается, что, в свою очередь, приводит к появлению ВЫСОКОГО уровня на выводе RA1, а также на входе INT/RB0 (через элемент ИЛИ). Последний сигнал является запросом внешнего прерывания.

3. Когда микроконтроллер передает управление на обработчик прерывания, тот проверяет состояние обоих триггеров, считывая биты RA1 и RA2 порта. В данном случае на выводе RA1 будет присутствовать ВЫСОКИЙ уровень, соответственно обработчик выдаст отрицательный импульс на вывод RA3.

4. Этот импульс сбросит триггер выходного датчика (OUT) и таким образом прекратит генерацию запроса прерывания от данного источника.

Осталась одна проблема: если событие наступит до того, как программа сбросит соответствующий внешний триггер, то это событие будет пропущено, поскольку с выхода элемента ИЛИ на вход INT будет подаваться сигнал НИЗКОГО уровня. В данной ситуации последующие фронты не будут сформированы, и система прерываний надолго окажется заблокированной! Эту ситуацию можно обойти программным способом, опрашивая оба внешних флага перед выходом из обработчика прерывания и выполняя соответствующие действия, если состояние обоих битов порта отлично от нуля.

Процедура обработки прерывания для данного случая приведена в Программе 7.3. Контекст программы сохраняется при входе в обработчик и восстанавливается при выходе из него так, как уже было описано на стр. 217.

Программа 7.3. Обработчик прерывания системы контроля печи

OVEN  movwf _work ; Сохраняем W в памяти данных

           swapf STATUS,w ; Считываем текущее состояние STATUS

           movf _status ; и сохраняем его в памяти данных

; --------------------

CHECK bcf INTCON,INTF ; Сбрасываем флаг внешнего прерывания

           btfsc PORTA,0 ; Сигнал IN?

              goto IN ; ЕСЛИ не ноль, банка только что вошла в печь

           btfsc PORTA,1 ;Сигнал OUT?

              goto OUT ;ЕСЛИ не ноль, банка только что вышла из печи

; --------------------

; Точка выхода

           swapf _status,w ; Восстанавливаем исходное состояние STATUS

           movwf STATUS ; из памяти данных

           swapf _work,f ; Теперь восстанавливаем исходное состояние

           swapf _work,w ; рабочего регистра, не воздействуя на флаги,

           retfie  ; и возвращаемся в фоновую программу

; --------------------

; Основное тело процедуры обработки прерывания

IN    incf EVENT,f ; Регистрируем вхождение банки в печь

       bcf PORTA,2 ; Сбрасываем внешний триггер IN,

       bsf PORTA,2 ; формируя импульс его сброса,

         goto ALARM ; и проверяем наличие аварийной ситуации

OUT decf EVENT,f ; Регистрируем выход банки из печи

        bcf PORTA,3 ; Сбрасываем внешний триггер OUT,

        bsf PORTA,3 ; формируя импульс его сброса

ALARM movf EVENT,w ; Берем количество банок

        addlw -5 ; Вычитаем 5

        btfss STATUS,С ; ЕСЛИ нет заема, пищим

          goto BUZ_OFF ; ИНАЧЕ все в порядке, выключаем звук

        bcf PORTB,7 ; Включаем звуковой излучатель

          goto CHECK ; и снова опрашиваем внешние триггеры

BUZ_OFF

        bsf PORTB,7 ; Выключаем звук

           goto CHECK ; и снова опрашиваем внешние триггеры

Основная часть кода просто сбрасывает внутренний флаг прерывания INTF и по очереди проверяет состояние внешних триггеров. В зависимости от их состояния выполняется одна из трех секций обработчика:

1. Если на выводе RA0 ВЫСОКИЙ уровень, значит, банка пересекла луч входного детектора. Соответственно к счетчику (регистру EVENT) прибавляется единица и триггер входного детектора сбрасывается. Если значение счетчика больше четырех, то путем подачи на выход RB0 НИЗКОГО уровня включается звуковой сигнализатор, в противном случае он выключается. Выполняется повторная проверка триггеров.

2. Если на выводе RA1 ВЫСОКИЙ уровень, значит, банка пересекла луч выходного детектора. Соответственно из счетчика EVENT вычитается единица и триггер выходного детектора сбрасывается. Счетчик проверяется на равенство четырем, и звуковой излучатель переключается в соответствующее состояние. Выполняется повторная проверка триггеров.

3. Если ни один из триггеров не установлен, выполняется выход из обработчика.

Данная последовательность повторяется до завершения секций 1 или 2. Это позволяет гарантировать корректную обработку ситуации, когда оба луча перекрываются одновременно или в пределах небольшого временного окна.

Фоновая программа на листинге не показана. Она аналогична фоновой программе из Программы 7.2, т. е. в ней будут сконфигурированы различные порты, регистр-счетчик событий при старте программы будет сброшен, а прерывания разрешены. Очевидно, что данная фоновая программа будет отвечать за формирование сигнала излучателя, а также за выполнение других задач, обработку которых не следует возлагать на обработчик прерывания, чтобы максимально уменьшить размер кода обработчика. В реальной системе фоновая программа могла бы управлять цифровым индикатором, показывающим общее количество банок в печи (четыре — это смешное значение, взятое только для примера). Кроме того, следует реализовать сброс на ненулевое значение после затора, а также предусмотреть какой-нибудь знак, индицирующий (ошибочно вычисленное) отрицательное значение количества банок.

Пример 7.3

На стр. 220 мы говорили о часах реального времени системы центрального отопления. Напишите процедуру обработки прерывания, которая при каждом прерывании, генерирующемся с периодом 0.1 с, увеличивала бы на единицу значение времени, хранящееся в четырех регистрах данных. Это значение представлено в 24-часовом формате. В каждом байте хранится два BCD-разряда, к примеру, BCD-число 40 в регистре MINUTES представляется как Ь’0100 0000’. Этот формат называется упакованным BCD-форматом.

Решение

При каждом вызове процедуры обработки прерывания необходимо добавлять 1 к четырехбайтному числу, хранящемуся в регистрах HOURS: MINUTES: SECONDS: JIFFY. Причем регистр JIFFY используется как счетчик по модулю 10, SECONDS и MINUTES — по модулю 60, a HOURS — по модулю 24. С учетом этого составим перечень задач:

1. Прибавить 1 kJIFFY.

2. Если JIFFY = 10, то обнулить JIFFY и прибавить единицу к SECONDS. В противном случае перейти к п. 6.

3. Если SECONDS = 60, то обнулить SECONDS и прибавить единицу к MINUTES. В противном случае перейти к п. 6.

4. Если MINUTES = 60, то обнулить MINUTES и прибавить единицу к HOURS. В противном случае перейти к п. 6.

5. Если HOURS = 24, обнулить HOURS.

6. Выйти из обработчика.

Код, реализующий описанный алгоритм, приведен в Программе 7.4. Сохранение и восстановление контекста реализовано обычным образом. Однако, поскольку в обработчике используется регистр FSR, он тоже сохраняется в регистре _fsr и восстанавливается при выходе из обработчика.

Программа 7.4. Обработчик прерывания часов реального времени

_work equ h’4D’ ; Копия W

_status egu h’4E’ ; Копия STATUS

_fsr equ h’4F’ ; Копия FSR

HOURS equ h’20’ ; Часы 2 разряда

MINUTES equ h’21’ ; Минуты (2 разряда)

SECONDS equ h’22’ ; Секунды (2 разряда)

JIFFY equ h’23’ ; Доли секунды (2 разряда)

; Сначала сохраним контекст

RTC movwf _work ; Сохраняем W

       swapf STATUS,w ; и регистр STATUS,

       movwf _status

       movf FSR,w ; а также регистр FSR

       movwf _fsr

       bcf INTCON,INTF ; Сбрасываем флаг внешнего прерывания

; Основной код --------------

; Задача 1

       incf JIFFY,f ; Увеличим Jiffy на единицу

       movlw d’10’ ; Сравним десятью

       btfss STATUS,Z ; ECЛИ равно, TO продолжаем

          goto EXIT ; ИНАЧЕ выходим из обработчика

; Задача 2

       clrf JIFFY ; ИНАЧЕ обнуляем Jiffy

       movlw SECONDS ; Устанавливаем FSR на Seconds

       movwf FSR

       call BCD_INC ; и инкрементируем BCD-число

       movlw h’60’ ; Сравниваем с 0110 0000 (60 BCD)

       subwf SECONDS,w

       btfss STATUS,Z ; ЕСЛИ разно, TO продолжаем

          goto EXIT ; ИНАЧЕ выходим из обработчика

; Задача 3

       clrf SECONDS ; ИНАЧЕ обнуляем Seconds

       decf FSR,f ; Устанавливаем FSR на Minutes

       call BCD_INC ; и инкрементируем BCD-число

       movlw ’60’ ; Сравниваем с 0110 0000 {60 BCD)

       subwf MINUTES,w

       btfss STATUS,Z ; ЕСЛИ разно, TO продолжаем

          goto EXIT ; ИНАЧЕ выходим из обработчика

; Задача 4

       clrf MINUTES ; ИНАЧЕ обнуляем Minutes

       decf FSR,f ; Устанавливаем FSR ка Hours

       call BCD_INC ; и инкрементируем BCD-число

       movlw h’24’ ; Сравниваем с 0010 0100 (24 BCD)

       subwf HOURS,w

       btfsc STATUS,Z ; ЕСЛИ не равно, TO продолжаем

       clrf HOURS ; ИНАЧЕ обнуляем Hours

; --------------------------

; Задача 5

EXIT reovf _fsr,w ; Восстанавливаем FSR

        movwf FSR

        swapf _status,w ; Восстанавливаем STATUS

        movwf STATUS

        swapf _work,f ; Восстанавливаем W,

        swapf _work,w ; не воздействуя на флаги,

        retfie ; и выходим из обработчика

Основное тело обработчика прерывания разбито на секции в соответствии с составленным алгоритмом. После каждого инкрементирования из нового значения вычитается константа, равная основанию счета. Если эти значения равны, регистр обнуляется и инкрементируется следующий байт числа. Вместо проверки флага нуля можно было бы контролировать значение флага переноса, проверяя, чтобы полученное значение было равно или больше значения основания, btfss STATUS,С.

В примере предполагается, что данные хранятся в упакованном BCD-формате. То есть число 59 хранится в виде h’0101 1001’ или h’59’. Это означает, что операция инкрементирования должна соответствовать формату BCD. Эту коррекцию можно осуществить после обычного инкрементирования, проверяя, чтобы младший полубайт не стал больше девяти. В противном случае к числу прибавляется шесть. Поскольку число не может принимать значения более 59, нам не нужно выполнять аналогичную проверку для старшего полубайта. С алгоритмом полного инкрементирования упакованного BCD-числа можно познакомиться в Примере 4.5 на стр. 111.

Поскольку эту операцию надо выполнить 3 раза (для всех байтов, исключая JIFFY, который никогда не становится больше девяти), лучше всего оформить ее в виде подпрограммы. Код такой подпрограммы приведен в Программе 7.5. В данном случае регистр FSR указывает на регистр, который содержит подлежащее инкрементированию упакованное BCD-число. Это значение просто инкрементируется непосредственно в регистре с использованием косвенной адресации. После этого оно корректируется описанным выше способом. Предполагается, что данные, на которые указывает FSR, уже находятся в BCD-формате, т. е. в подпрограмме отсутствует преобразование натурального двоичного числа в двоично-десятичное.

Программа 7.5. Подпрограмма инкрементирования упакованного BCD-числа

; *************

; * ФУНКЦИЯ: Прибавляет 1 к упакованному BCD-числу (98 макс) *

; * ВХОД: FSR указывает на регистр с числом *

; * ВЫХОД: BCD-число инкрементируется *

; * W и STATUS изменяются *

; *************

BCD_INC incf INDF,f ; Прибавляем 1 к адресованному байту

              movf INDF,w; Считываем его

              addlw 6; Прибавляем шесть

              btfss STATUS,DC ; Проверяем десятичный перенос

                 goto BCD_EXIT; ЕСЛИ нет, ТО выходим

              movwf INDF; ИНАЧЕ возвращаем скорректированное значение

BCD_EXIT return

Пример 7.4

В торговом автомате монеты разных номиналов проходят через один из шести микропереключателей, подключенных к порту В. При прохождении монеты переключатель замыкается, что вызывает появление на соответствующем выводе сигнала НИЗКОГО уровня, как показано на Рис. 7.8.

Напишите процедуру обработки прерывания, которая будет накапливать значение суммы в регистре MONEY. Предполагается, что в фоновой программе регистр INTCON будет сконфигурирован таким образом, чтобы разрешить внешнее прерывание с вывода RB0/INT.

Рис. 7.8. Монетоприемник торгового автомата

Решение

Как показано в Программе 7.6, после сохранения контекста и сброса флага INTF производится последовательная проверка всех переключателей. НИЗКОМУ уровню на каком-либо выводе порта соответствует лог. О в соответствующем бите регистра PORTB. В соответствии с логикой работы механизма монетоприемника одновременно может быть замкнут только один переключатель, поэтому нет необходимости выходить из процедуры после неудачного поиска.

Программа 7.6. Обработчик прерывания монетоприемника торгового автомата

VEND  movwf _work; Сохраняем W в памяти данных

          swapf STATUS,w; Читаем STATUS, не изменяя флагов,

          movwf _status; и сохраняем его в памяти данных

; -------------------------

CHECK bcf INTCON,INTF ; Сбрасываем флаг внешнего прерывания

           movf MONEY,w ; Берем текущее значение MONEY

           btfss PORTB,7 ; Проверяем $2

             addlw d’200’ ; ЕСЛИ 0, ТО прибавляем 200

           btfss PORTB,6 ; Проверяем $1

             addlw d’100’ ; ЕСЛИ 0, ТО прибавляем 100

           btfss PORTB,5 ; Проверяем 25с

             addlw d’25’ ; ЕСЛИ 0, ТО прибавляем 25

           btfss PORTB,4 ; Проверяем 10с

             addlw d’10’ ; ЕСЛИ 0, ТО прибавляем 10

           btfss PORTB,3 ; Проверяем 5с

             addlw 5 ; ЕСЛИ 0 ТО прибавляем 5

           btfss PORTB,2 ; Проверяем 1с

             addlw 1 ; ЕСЛИ 0, ТО прибавляем 1

           movwf MONEY ; Сохраняем новую сумму

; ---------------------------

; Точка выхода

           swapf _status,w ; Восстанавливаем исходное значение

           movwf STATUS ; регистра STATUS

           swapf _work,f ; Восстанавливаем исходное значение W,

           swapf _work,w ; не воздействуя на флаги,

           retfie ; и возвращаемся в фоновую программу

Вопросы для самопроверки

7.1. Перепишите Программу 7.2 так, чтобы она подсчитывала число банок, равное одному гроссу (144). Это значение следует хранить в упакованном BCD-формате (СОТНИ и ДЕСЯТКИ: ЕДИНИЦЫ), и оно может использоваться фоновой подпрограммой для отображения общего числа банок.

7.2. Какие изменения следует внести в Программу 7.2, чтобы максимальное число банок в печи могло быть равным 1000?

7.3. Взяв в качестве образца Рис. 7.1, напишите процедуру обработки прерывания, выполняющую следующие операции:

• Копирование 16-битного числа в два регистра общего назначения — ТЕМР_Н и TEMP_L.

• Вычитание его из предыдущего значения, хранящегося в регистрах LAST_COUNT_H и LAST_COUNT_L, и запись разницы в регистры DIFFERENCE Н и DIFFERNCE L.

• Замещение предыдущего значения новым.

• Запись в РОН с именем NEW ненулевого значения для передачи в фоновую программу информации о том, что доступно новое значение. Фоновая процедура обнулит регистр NEW после обработки данных.

7.4. Скорость вращения вала можно измерить с использованием кодирующего диска, который генерирует импульс при повороте вала на каждые 10°. Этот импульс может использоваться в качестве сигнала внешнего прерывания микроконтроллера. Учитывая, что максимальная скорость вращения составляет 20 000 оборотов в минуту, какое наибольшее время выполнения может иметь процедура обработки прерывания, позволяющее избежать пропуска импульсов? Предполагается, что частота кварцевого резонатора равна 4 МГц.

7.5. Электронная рулетка определяет расстояние путем излучения ультразвуковых импульсов и контролируя время прихода отраженного сигнала. Схема такого ультразвукового дальномера приведена на Рис. 7.9 (за его основу взята схема с Рис. 7.7).

Наибольшее измеряемое расстояние составляет 2.5 м при разрешении 1 см. Скорость звука в воздухе при температуре 20 °C равна 344 м/с, т. е. время, за которое сигнал пройдет расстояние 1 см и вернется обратно, равно 58 мкс.

Рис. 7.9. Аппаратная часть ультразвукового дальномера

Используя в качестве задающего генератор с частотой 17.2 кГц, получим одно прерывание каждые 58 мкс.

Учитывая схему, программа должна выполнять следующие операции:

• Фоновая программа

1) Обнулить счетчик JIFFY и флаг NEW

2) Подать импульс на излучатель.

3) Ждать установления ненулевого значения флага NEW

4) Отобразить подсчитанное значение.

5) Перейти к п. 1.

• Процедура обработки прерывания

1) При каждом импульсе генератора инкрементировать счетчик JIFFY.

2) При обнаружении сигнала от приемника записать в флаг NEW ненулевое значение, извещающее фоновую программу о том, что в переменной JIFFY находится конечное число.

3) Повторять, пока активен хотя бы один сигнал.

4) Выйти из прерывания.

Напишите код процедуры обработки прерывания, которая использует регистр NEW для извещения фоновой программы о приходе отраженного сигнала. За основу можно взять Программу 7.3.

7.6. Предполагается увеличить диапазон цифрового ультразвукового дальномера до 10 м и разрешение до 1 мм. Какие изменения необходимо внести в аппаратную и программную части?

7.7. Ультразвуковой дальномер из предыдущего вопроса был собран и протестирован. Однако обнаружилось, что с течением времени показания медленно изменяются. Сначала грешили на дрейф, но генератор оказался стабильным. Немного подумав, один студент предположил, что скорость звука зависит от условий окружающей среды. После небольшого исследования он вывел следующую зависимость скорости звука от температуры:

Vt = V0∙√(1 + (Δt/273)),

где V0 — скорость распространения при 20 °C, a Vt — скорость при температуре t. Какое изменение температуры приведет к появлению ошибки в 1 мм при работе дальномера на максимальной дистанции?

 

Глава 8

Инструментальные средства для работы с языком ассемблера

Начиная С главы 3, мы написали уже достаточно программ. Для доходчивости эти программы были написаны таким образом, чтобы их легче было понимать человеку. То есть команды представлялись короткими мнемониками, например return вместо Ь’00000000001000’. Аналогично, регистры имели имена, такие как INTCON, а строки имели метки и комментарии. Однако такое символьное представление годится только для человека. Микроконтроллер и знать ничего не знает, кроме двоичных кодов (см. стр. 64), составляющих исполнимый код и данные.

Воспользовавшись описанием набора команд (см. Приложение Г), можно вручную транслировать программу из подобного удобочитаемого формата в машиночитаемый двоичный код. В принципе для таких устройств, как микроконтроллеры PIC, имеющих сокращенный набор команд (RISC) и небольшое число режимов адресации, это не так уж и сложно. Однако это довольно долгий и утомительный процесс, особенно если программа достаточно большая. Кроме того, при таком подходе велика вероятность возникновения ошибок и усложняется внесение изменений в программу.

Знакомые вам компьютеры незаменимы там, где необходимо быстро и аккуратно выполнять однообразные операции. Очевидно, что задача перевода программы из символьного представления в машинный код полностью подходит под эту категорию. В данной главе мы вкратце рассмотрим различные программные средства, помогающие осуществлять этот процесс трансляции. В следующей главе мы также познакомимся с аналогичными средствами для языка высокого уровня.

Прочитав эту главу, вы:

• Узнаете, что такое язык ассемблера и как он соотносится с машинным кодом.

• Поймете преимущества символьного представления перед машинным кодом.

• Разберетесь в назначении ассемблера.

• Поймете разницу между абсолютным и перемещаемым кодом.

• Поймете назначение компоновщика.

• Ознакомитесь с процессом трансляции ассемблерной программы в абсолютный машинный код.

• Узнаете структуру файла машинных кодов и назначение программы-загрузчика.

• Научитесь использовать интегрированную среду разработки для автоматизации взаимодействия между различными программными средствами, необходимыми для превращения исходного кода в запрограммированный микроконтроллер.

Суть процесса преобразования ассемблерной программы показана на Рис. 8.1. Программа была набрана в символьном виде человеком, преобразована компьютером и выдана в машиночитаемом виде. Разумеется, за этой простотой скрываются гораздо более сложные процессы, которые мы разберем довольно подробно, что поможет вам в написании программ (в степени, достаточной, чтобы вы могли свободно писать программы).

Рис. 8.1. Преобразование исходного кода на языке ассемблера в машинный код

Вообще говоря, различные трансляторы и утилиты выпускаются и продаются огромным числом компаний, занимающихся разработкой программного обеспечения, поэтому конкретные детали и операции в различных коммерческих продуктах несколько отличаются. Что же касается конкретно микроконтроллеров PIC, то их производитель, компания Microchip Technology Inc, избрала стратегию бесплатного предоставления программных средств для работы с языком ассемблера. Именно это сыграло большую роль в популярности данных микроконтроллеров. Поэтому коммерческое ПО для работы на таком низком уровне встречается достаточно редко, и, более того, используемый синтаксис обычно совпадает с синтаксисом, принятым в программах самой Microchip. По этой причине в данной главе мы будем рассматривать пакет инструментальных средств Microchip.

Использование компьютеров для трансляции кода, представленного в удобном для человека символьном виде (исходный код), в понятный микроконтроллеру двоичный код (объектный или машинный код) и загрузки его в память устройств началось в конце 1940-х кодов. Помимо всего прочего, применение компьютеров позволило использовать системы счисления высших порядков, например шестнадцатеричную. В шестнадцатеричной системе фрагмент кода, представленного на Рис. 8.1, будет выглядеть следующим образом:

0AA0 0820 3Е06 1905 00A0 0008

Шестнадцатеричный загрузчик транслирует этот код в двоичный и поместит его в память по заданному адресу. Данный загрузчик может входить в состав программного обеспечения вашего программатора PIC-EPROM. Написание программ в шестнадцатеричных кодах мало кого прельщает. Несмотря на некоторое уменьшение числа нажатия на клавиши, при таком способе ввода программы очень легко наделать кучу ошибок.

Для серьезного занятия программированием требуется, как минимум, символьный транслятор, или ассемблер. Его применение позволяет программисту использовать для команд и внутренних регистров мнемонические обозначения, а также присваивать имена константам, переменным и адресам. Символьный язык, на котором пишется исходный код, называется языком ассемблера. В отличие от языков высокого уровня, таких как Си или Паскаль, язык ассемблера обеспечивает полное (один к одному) соответствие с генерируемым машинным кодом, т. е. из одной строки исходного кода получается одна команда. В качестве примера рассмотрим Программу 8.1, представляющую собой слегка модифицированную версию Программы 6.12 со стр. 199. Эта подпрограмма вычисляет значение квадратного корня из 16-битной переменной NUM, которая размещается в двух байтах памяти данных, и возвращает в рабочем регистре 8-битное целое значение квадратного корня.

Присвоение имен различным адресам и константам особенно необходимо в случае больших программ, состоящих из нескольких тысяч строк кода. В совокупности с комментариями это облегчает отладку, разработку и поддержку кода. К примеру, в большинстве наших программ до настоящего момента мы использовали следующие строки:

STATUS equ 3; Регистр STATUS расположен по адресу h’03’

С equ 0 ; Флаг переноса — 0-й бит этого регистра

Псевдокоманда equ представляет собой образец директивы ассемблера. Директива не генерирует код как команда, а используется для передачи ассемблеру информации, касающейся его работы. В данном случае написанное означает, что при обнаружении в поле операндов команды имени STATUS оно должно быть заменено на число 3, а имя С должно быть заменено на число 0.

Директива equ лучше всего подходит для указания имен РСН и их битов. Поскольку для каждой модели микроконтроллера эти значения фиксированы и, соответственно, не являются уникальными для какой-либо конкретной программы, компания Microchip предоставляет для каждого устройства файлы с расширением. inc. Эти файлы могут быть включены в пользовательскую программу в качестве заголовочных файлов. Для примера в Листинге 8.1 приведена начальная часть файла p16f84a.inc.

Листинг 8.1. Фрагмент файла p16f84a.inc, предоставляемого компанией Microchip

; This header file defines configurations, registers, and other

; useful bits of information for the PIC16F84 microcontroller.

; These names are taken to match the data sheets as closely as possible.

; --- Register Files ---

INDF EQU H’0000’

TMR0 EQU H’0001’

PCL EQU H’0002’

STATUS EQU H’0003’

FSR EQU H’0004’

PORTA EQU H’0005’

PORTB EQU H’0006’

EEDATA EQU H’0008’

EEADR EQU H’0009’

PCLATH EQU H’000A’

INTCON EQU H’000B’

OPTION_REG EQU H’0081’

TRISA EQU H’0085’

TRISB EQU H’0086’

EECON1 EQU H’0088’

EECON2 EQU H'0089’

; --- STATUS Bits ---

IRP EQU H’0007’

RP1 EQU H’0006’

RP0 EQU H’0005’

NOT_TO EQU H’0004’

NOT_PD EQU H’0003’

Z EQU H’0002’

DC EQU H’0001’

C EQU H’0000’

; --- INTCON Bits ---

GIE EQU H’0007’

EEIE EQU H’0006’

TOIE EQU H’0005’

INTE EQU H’0004’

RBIE EQU H’0003’

TOIF EQU H’0002’

INTF EQU H’0001’

RBIF EQU H’0000’

Примечание.

Перевод комментариев в заголовке файла: «В этом заголовочном файле определяются биты конфигурации, регистров и другие полезные константы для микроконтроллера PIC16F84. Символические имена выбраны такими, чтобы максимально соответствовать справочному листку на микроконтроллер.» ( Примеч. пер .)

В Программе 8.1 директива include используется для вставки в программу имен регистров специального назначения. Помимо того, что использование этой директивы освобождает программиста от необходимости набирать множество директив equ, любой последующий переход на другой процессор, скажем, с PIC16F84A на PIC16F627, можно будет осуществить простой заменой заголовочного файла. Начиная с этого момента, мы будем активно использовать эту возможность ассемблера. Хотя мы и применяем директиву include для вставки заголовочного файла, она может использоваться для вставки любого файла подходящего формата, например, содержащего код подпрограммы; в качестве примера обратите внимание на Программу 12.8, приведенную на стр. 401.

Программа 8.1. Неперемещаемая программа, использующая нашу подпрограмму вычисления квадратного корня

; Глобальные объявления

       include "p16f84a.inc" ; Заголовочный файл

       cblock h’26’; Начало блока переменных (с регистра h’26’)

          NUM:2; Старший байт (NUM), младший байт (NUM+1)

       endc; Конец блока

; Основной цикл -------------

MAIN call SQR_ROOT; Фиктивный основной цикл

         sleep; Останавливаемся

; --------------------------------

; ********************************

; * ФУНКЦИЯ: Вычисляет корень квадратный из 16-битного целого *

; * ПРИМЕР: Число = h’FFFF’ (65,535), Корень = h’FF’ (d’255’)*

; * ВХОД: Число в регистрах NUM: NUM+1 *

; * ВЫХОД: Корень в W. Регистры NUM: NUM+1 и I:I+1, COUNT изменяются *

; *********************************

; Локальные объявления

         cblock;

            I:2, COUNT:1 ; Магическое число и счетчик цикла

          endc

          org h’200’; Код размещается в памяти программ начиная с h’200’

SQR_ROOT clrf COUNT ; Задача 1: Обнулить счетчик цикла

           clrf I ; Задача 2: Инициализация магического числа единицей

           clrf I+1;

           incf I+1,f;

; Задача 3: ВЫПОЛНЯТЬ

SQR_LOOP movf I+1,w ; Задача 3а: Number — I

                 subwf NUM+1,f ; Вычитаем из младшего байта исходного числа

                 movf I,w ; Берем старший байт магического числа

                 btfss STATUS,С ; ЕСЛИ не было заема (С==1), ТО пропускаем

                 addlw 1 ; Учитываем заем

                 subwf NUM,f ; Вычитаем старшие байты

; Задача 3б: ЕСЛИ потеря значимости, ТО выйти

                 btf ss STATUS,С ; ЕСЛИ нет заема (C==1), TO продолжаем

                     goto SQR_END ; ИНАЧЕ вычисление завершено

                 incf COUNT,f ; Задача 3в: ИНАЧЕ инкрементируем счетчик цикла

                 movf 1 + 1,w ; Задача 3 г: Увеличиваем магическое число на 2

                 addlw 2

                 btfsc STATUS,С ; Если нет переноса, ТО пропускаем

                    incf I,f ; ИНАЧЕ корректируем старший байт

                 movwf I+1

                 goto SQR_LOOP

SQR_END   movf COUNT,w ; Задача 4: Возвращаем счетчик цикла в качестве значения корня

                return

                end

Директива equ используется также для присваивания имен переменным, хранимым в РОН. Так, в Программе 6.12 на стр. 199 имеются следующие строки:

NUM_H equ h’26’ ; Исходное значение, старший байт

NUM_L equ h’27’ ; Исходное значение, младший байт

Эти имена и адреса должны быть, разумеется, уникальными для данной программы, а не для какого-то конкретного устройства. В Программе 8.1 используется пара директив cblock — endc, выполняющих схожие функции. Эти директивы сообщают ассемблеру о том, что переменные должны быть размещены в указанных регистрах. В строках, заключенных между данными директивами, перечисляются имена переменных и количество байтов, занимаемых каждой переменной. Возвращаясь к нашему примеру:

cblock h’26’ ; Начало блока переменных (с регистра h ’ 26’)

NUM:2 ; Резервируем два байта под NUM

endc ; Конец блока

где число, записанное через двоеточие после имени переменной, определяет количество байтов, зарезервированных подданную переменную. Отдельные байты, входящие в состав переменной, можно адресовать с использованием арифметического оператора «+»; например, в 3-байтной переменной SUM:3 1-й байт обозначается как SUM, 2-й байт — как SUM+1 и 3-й байт — как SUM+2.

При описании первого блока переменных в Программе 8.1 явно указывается, что он начинается с регистра h’26’. Во всех последующих директивах cblock указание адреса можно опустить, если новые переменные должны располагаться сразу же после уже описанных. Таким образом, переменная I:2 размещается в регистрах h’27’:h’28’, a COUNT:1 — в регистре h’29’. Такой подход обеспечивает гораздо большую гибкость по сравнению с ручным распределением регистров самим программистом, так как при изменении какого-либо модуля или добавлении новых элементов распределение памяти изменится автоматически. Кроме того, изменение начального адреса какого-либо блока, скажем с регистра h’26’ на регистр h’20’, автоматически размещает все переменные программы по новым адресам.

Также для именования (присваивания имен) различных объектов программы можно использовать директиву #define. Например, строка вида

#define 6,7 BUZZER

позволяет нам писать bsf BUZZER вместо bsf 6,7, например для включения звукового излучателя, подключенного к выводу 7 порта В (регистр h’06’).

Чтобы проиллюстрировать еще одну возможность ассемблера, подпрограмма из Программы 8.1 размещается, начиная с адреса h’200’ памяти программ. Это осуществляется использованием директивы org (см. также Программу 7.1 на стр. 215). В результате данной операции метке программы SQR_ROOT было присвоено значение h’200’.

В последней строке Программы 8.1 размещается директива end. Эта директива указывает ассемблеру игнорировать весь текст, располагающийся ниже ее, т. е. прекратить трансляцию.

Разумеется, для работы символьного транслятора требуется больше вычислительных ресурсов, нежели для простого шестнадцатеричного загрузчика, особенно это касается памяти и устройств резервного хранения. До появления в конце 1970-х персональных компьютеров для выполнения ассемблирования требовались мэйнфреймы, мини-компьютеры или же специальные системы разработки микропроцессоров/микроконтроллеров. Естественно, эти решения были весьма дорогостоящими, и по причине невозможности использования указанных вычислительных средств повсеместно использовалось ручное кодирование.

Программы-трансляторы, вообще говоря, выполняют две задачи:

1. Преобразование различных мнемоник команд и меток в их эквивалентные значения в машинном коде.

2. Размещение команд и данных по заданным адресам.

Для большинства программ, выполняющихся на микроконтроллерах PIC младшего и среднего уровней, более чем достаточно возможностей абсолютного ассемблера. Чтобы облегчить понимание процесса трансляции, изображенного на Рис. 8.2, мы рассмотрим все этапы преобразования нашей программы, начиная с создания файла с исходным кодом и заканчивая итоговым файлом с абсолютным машинным кодом. Перемещаемый ассемблер мы затронем чуть позже.

Рис. 8.2. Абсолютная трансляция с языка ассемблера

Редактирование

Прежде всего исходный файл необходимо создать. Для этого используется текстовый редактор. Текстовый редактор отличается от текстового процессора тем, что он не вставляет в свой текст никаких управляющих символов, разметки и другой подобной информации. Например, в нем отсутствует перенос строк, поэтому если вы хотите перейти на новую строку, вы должны сами нажать клавишу ввода. В составе большинства операционных систем поставляется простой текстовый редактор, в частности в Microsoft Windows это программа notepad. Также имеется множество программ сторонних разработчиков, кроме того, большинство текстовых процессоров имеют текстовый режим, в котором их можно использовать как обычный текстовый редактор. Файлы с исходным кодом на языке ассемблера имеют расширение. asm.

Типичная строка файла с исходным кодом имеет следующий формат:

За исключением строк, в которых содержится только комментарий, все строки должны содержать инструкцию (либо исполняемую микроконтроллером команду, либо директиву) и соответствующий операнд или операнды. Все метки должны начинаться с 1-й позиции строки; если метка отсутствует, то первым символом строки должен быть пробел или символ табуляции, индицирующие этот факт. Метка может состоять из 32 алфавитно-цифровых символов, символов подчеркивания или символов вопроса. При этом первым символом метки обязательно должна быть буква или символ подчеркивания. Обычно метки нечувствительны к регистру символов. Метка строки соответствует адресу памяти программ первой следующей за ней исполняемой команды. Метка должна отделяться от последующей команды или директивы пробелом, двоеточием или даже символом новой строки.

Необязательный комментарий обозначается символом точки с запятой, при этом допускается вводить строки, состоящие только из комментария (см. строки 9…16 Программы 8.1). Ассемблер игнорирует комментарии, т. е. они служат исключительно для документирования текста программы. Комментариев должно быть много, и они должны объяснять, что программа делает, а не просто дублировать команду. Например, строка:

movf I,w; Скопировать I в w

является примером напрасной траты времени, тогда как строка

movf I,w; Считать старший байт магического числа

гораздо более полезна. Исходный код, не содержащий комментариев или содержащий их очень мало, очень часто неработоспособен. Плохо документированную программу трудно отлаживать, а впоследствии изменять или дополнять. Последние действия иногда называются сопровождением программы.

Команда должна отделяться от своих операндов символами пробела или табуляции. При наличии в команде двух операндов они отделяются друг от друга запятой. В командах, у которых в качестве операнда-адресата может выступать как рабочий регистр, так и регистр данных, в поле операнда-адресата следует писать символы w или f или числа 0 или 1 соответственно. При отсутствии явного указания на операнд-адресат ассемблер по умолчанию задаст регистр данных, но при этом выдаст предупреждение программисту.

Ассемблирование

Программа ассемблера просматривает файл с исходным кодом, проверяя его на наличие синтаксических ошибок. При отсутствии последних она приступает к трансляции текста программы в абсолютный объектный код, представляющий, по большому счету, обычный машинный код, дополненный информацией, касающейся адресов памяти программ, по которым он должен быть расположен. К синтаксическим ошибкам относится, в частности, ссылка на несуществующую метку или неизвестная команда. В результате работы программы формируется файл сообщений об ошибках, содержащий все подобные «проступки». Если синтаксические ошибки отсутствуют, то генерируется файл листинга и файл с машинным кодом.

Возвращаясь к нашему примеру, процесс трансляции запускается вводом строки

mpasmwin /aINHX8M /е+ /l+ /с+ /rhex /p16f84a root.asm

где mpaswin.exe — программа ассемблера, a root.asm — наш исходный файл. Флаги задаются в виде /<опция> и могут сопровождаться знаком «+» или «-» соответственно для разрешения или запрещения данной опции. Так, ключ /е+ включает генерацию файла ошибки, /l+ — то же для файла листинга, /с+ делает метки чувствительными к регистру символов, /rhex задает основание счисления по умолчанию (шестнадцатеричное). Флаг /pl6f84а указывает ассемблеру, что исходный код предназначен для модели PIC16F84A. Программа mpaswin может транслировать код для всех микроконтроллеров PIC (с 12-, 14- или 16-битными ядрами).

Листинг

Файл листинга (см. Листинг 8.2) воспроизводит оригинальный исходный код, добавляя к нему шестнадцатеричное значение адреса каждой команды и ее код. В файл включается также таблица символов, перечисляющая все символы/метки, определенные в программе, например NUM указан как регистр h’26’. Карта использования памяти показывает использование памяти программ в графическом виде. Любые предупреждающие сообщения (warning) вставляются в файл листинга в том месте, к которому они относятся. Например, если пропущен признак операнда-адресата (w или f), то ассемблер по умолчанию поставит последний и выведет в этом месте листинга предупреждающее сообщение.

Этот файл используется только для документирования и не исполняется процессором.

Листинг 8.2. Содержимое файла root_abs. 1st

MPASM 4.02 Released   ROOT_ABS.ASM   9-23-2005   17:15:57   PAGE 1

LOC OBJECT CODE LINE SOURCE TEXT

     VALUE

               00001 ; Глобальные объявления

               00002 include "p16f84a.inc"; Заголовочный файл

               00001 LIST

               00002 ; P16F84A.INC Standard Header File, V2.00 Microchip Tech, Inc.

               00003

               00004  cblock h’26’ ; Качало блока переменных (с регистра h’26’)

00000026 00005 NUK:2 ; Старший байт (NUM), младший байт (NUM+1)

               00006  endc ; Конец блока

               00007 ; Основной цикл —

0000 2200 00008 MAIN call SQR_ROOT ; Фиктивный основной цикл

0001 0063 00009          sleep ; Останавливаемся

                00010 ; ---------------------------------

                00011

                00012 ; *********************************

                00013 ; * ФУНКЦИЯ: Вычисляет корень квадратный *

                           * из 16-битного целого *

                00014 ; * ПРИМЕР: Number = h’FFFF’; (65,535), *

                            * Root = h’FF’ (255) *

                 00015; * ВХОД: Number а регистре NUM: NUM+1 *

                 00016;  * ВЫХОД: Корень в W. NUM: NUM+1;  I:I+1 и COUNT измен. *

                 00017 ; **********************************

                 00018

                 00019 ; Локальные объявления

                 00020 cblock

00000028  00021 I:2, COUNT:1 ; Магическое число и счетчик цикла

                 00022 endc

                 00023

0200          00024 org h’200’; Код размещается в памяти программ начиная с h’200’

0200 01АА 00025 SQR_ROOT clrf COUNT ; Задача 1: Обнулить счетчик цикла

                 00026

0201 01А8 00027 clrf I; Задача 2: Инициализация магического числа единицей

0202 01А9 00028 clrf I+1;

0203 0АА9 00029 incf I+1,f;

                 00030

00031 ; Задача 3: ВЫПОЛНЯТЬ

0204 0829 00032 SQR_LOOP movf I+,w ; Задача За: Number — I

0205 02A7 00033 subwf NUM+1,f; Вычитаем из мл. байта исходного числа

0206 0828 00034 movf I,w ; Берем старший байт магического числа

0207 1С03 00035 btfss STATUS,С; ЕСЛИ не было заема (С==1), ТО пропускаем

0208 3Е01 00036 addlw 1 ; Учитываем заем

0209 02А6 00037 subwf NUM,f; Вычитаем старше байты

                 00038

                 00039 ; Задача 3б: ЕСЛИ потеря значимости, ТО выйти

020A 1С03 00040 btfss STATUS,С ; ЕСЛИ нет заема (С==1), ТО продолжаем

020В 2Ф13 00041 goto SQR_END ; ИНАЧЕ вычисление завершено

                 00042

020D 0829 00045 movf I+1,w; Задача 3 г: Увеличиваем магическое число на 2

020Е ЗЕ02 00046 addlw 2

020F 1803 00047 btfsc STATUS,С ; Если нет переноса, ТО пропускаем

0210 0АА8 00048 incf I,f ; ИНАЧЕ корректируем старший байт

0211 00А9 00049 movwf I+1

0212 2А04 00050 goto SQR_LOOP

0213 082А 00052 SQR_END movf COUNT,w ; Задача 4: Возвращаем счетчик цикла в качестве значения корня

0214 0008 0005З return

                00054 end

SYMBOL TABLE

       LABEL          VALUE

С                         00000000

COUNT                0000002А

I                          00000028

MAIN                   00000000

NUM                    00000026

SQR_END             00000213

SQR_LOOP           00000204

SQR_ROOT           00000200

STATUS               00000003

__16F84A            00000001

MEMORY USAGE MAP ('X' = Used, '-' = Unused)

0000: XX ---------------------- ------------ ------------ ------------

0200: ХХХХХХХХХХХХХХХХ ХХХХХ--- ------------ ------------

All ocher memory blocks unused.

Program Memory Words Used: 23

Program Memory Words Free: 1001

Errors: 0

Warnings: 0 reported, 0 suppressed

Messages: 0 reported, 0 suppressed

Исполняемый код

Конечным результатом любого процесса трансляции является объектный файл, иногда называемый также файлом в машинных кодах. После размещения указанного кода в памяти программ микроконтроллера он может запускаться как исполнимая программа.

Как видно из Листинга 8.3, этот файл состоит из строк шестнадцатеричных чисел, представляющих двоичный машинный код, каждая из которых начинается с адреса размещения первого байта строки. Этот файл может использоваться программатором для записи кода в ПЗУ программ по корректным адресам. Поскольку в файле явно указано местоположение каждого байта, такой тип файлов называется файлом с абсолютным объектным кодом. Часть ПО программатора микроконтроллеров PIC (см. Рис. 17.4 на стр. 616), которая считывает, декодирует и помещает этот код в память программ устройства, иногда называют абсолютным загрузчиком.

Листинг 8.3. Содержимое файла с абсолютным машинным кодом root_abs. hex

: 020000040000FA

: 040000000022630077

: 10040000АА01А801А901А90А2908А702280803С12

: 10041000013EA602031C132AAA0A290802ЗЕ031859

: 0A042000A80AA900042A2A0808000F

: 00000001FF

В мире микроконтроллеров/микропроцессоров используется много разнообразных форматов. Хотя большая часть этих стандартов де-факто присущи какому-либо конкретному производителю, они в большинстве своем могут использоваться совместно с любыми марками микроконтроллеров. Формат файла с машинным кодом, используемый нами, известен как «8-bit Intel hex» и был задан с помощью флага /aINHEX8M.

Давайте попристальнее взглянем на одну из строк файла root_abs. hex.

Загрузчик распознает запись по символу двоеточия. Двоеточие сопровождается двухразрядным шестнадцатеричным числом, указывающим количество байтов машинного кода в этой записи; в данном случае оно равно h’10’ = d’16’. Следующие четыре шестнадцатеричных разряда являются начальным адресом данных. Поскольку память программ в микроконтроллерах PIC адресуется пословно, адрес команды h’200’ транслируется в адрес байта h’400’. Следующее 2-разрядное число является признаком записи: h’00’ — для нормальной записи и h’01’ — для записи конца файла (см. последнюю строку в Листинге 8.3).

Основным содержимым записи является машинный код, в котором каждая команда записывается двумя 2-разрядными шестнадцатеричными числами в порядке младший байт: старший байт. Сначала загрузчик считывает младший байт (например, h’AA’), а затем добавляет к нему старший байт (например, h’01’), формируя 12-, 14- или 16-битное слово в зависимости от модели целевого микроконтроллера. Например, код команды clrf h’2А’ для 14-битного ядра будет равен h’01AA’.

Последний байт записи называется контрольной суммой. Контрольная сумма вычисляется как дополнительный код суммы всех предыдущих байтов записи, т. е. отрицательное значение суммы, игнорируя любые переполнения. Для контроля корректности передачи загрузчик складывает все принятые байты записи, включая байт контрольной суммы. Если при передаче не было ошибок, полученное число должно быть равно нулю.

Ассемблеры очень привередливы к синтаксису исходного кода. При наличии в исходном коде синтаксических ошибок будет сгенерирован файл сообщений об ошибках. Например, если бы при наборе 50-й строки была допущена ошибка:

got SQRLOOP

то был бы сгенерирован файл сообщений об ошибках, текст которого приведен в Листинге 8.4.

Листинг 8.4. Содержимое файла сообщений об ошибках.

Warning[207] ROOT_ABS.ASM 50: Found label after column 1. (got)

Error[122] ROOT_ABS.ASM 50: Illegal opcode (SQRLOOP)

Ассемблер не смог распознать слово got как команду или директиву и ошибочно предположил, что это метка, начинающаяся в 1-й позиции строки. Исходя из этого предположения, он решил, что слово SQRLOOP — это мнемоническое обозначение команды/директивы, и опять же не смог его распознать.

* * *

Большинство ассемблеров позволяет программисту определять последовательность команд процессора в виде макрокоманд. Такие макрокоманды могут в дальнейшем использоваться точно так же, как и обычные команды. Например, в приведенном ниже коде определяется макрокоманда Delay_1ms, которая реализует задержку длительностью 1 мс при использовании 4-МГц резонатора. Последовательность «родных» команд заключена между парой директив macro — endm. Впоследствии эти команды будут подставлены ассемблером в текст программы вместо мнемоники Delay_1ms. Заметьте, это просто встраиваемый код, а не вызов подпрограммы.

Delay_1ms macro

                 local LOOP

                 movlw d’250’ ; Считаем от 250

LOOP         addlw -1 ; Декрементируем

                 btfss STATUS,Z ; до нуля

                   goto LOOP ;

                  endm

При использовании в теле макрокоманды меток они должны быть объявлены в ней с помощью директивы local. Эта директива применяется для разрешения конфликта совпадения имен меток при многократном использовании макрокоманды в теле программы.

Данный пример достаточно необычен, поскольку созданная нами «команда» не имеет операндов. Как и «родные» команды, макрокоманды могут иметь один и более операндов. Чтобы посмотреть, как это делается, напишем макрокоманду Вnе (переход, если не равно нулю). Таким образом, команда Bne NEXT приведет к передаче управления на указанную метку, если флаг Z равен нулю, в противном случае выполнение программы продолжится со следующей команды. Макрокоманда Вnе определяется следующим образом:

Bne macro destination

      btfss STATUS,Z

         goto destination

      endm

Обратите внимание, что имя макрокоманды не должно совпадать с именем реальной команды даже из другого семейства микроконтроллеров.

Макрокоманды могут быть любой сложности и иметь любое количество операндов, разделяемых запятыми. К примеру, компания Microchip предоставляет большое количество макрокоманд, реализующих различные арифметические операции, такие как умножение 16-битных и 32-битных чисел. Однако интенсивное использование макрокоманд может усложнить отладку программы, особенно в тех случаях, когда простая с виду макрокоманда имеет побочные эффекты в виде изменения содержимого регистров и состояния флагов. Частым источником ошибок является использование перед макрокомандой команд пропуска с целью обойти ее при некотором событии. Поскольку макрокоманда в реальности состоит из нескольких команд, выполнение команды пропуска приведет к переходу внутрь макрокоманды, причем с тяжелыми последствиями.

Макроопределения, как приобретенные, так и написанные самостоятельно, можно собрать вместе в один файл и включать в пользовательскую программу директивой include. Так, если ваш файл называется mymacro.mac, то наличие в начале программы строки

include "mymacro.mac"

позволит программисту использовать все макроопределения, описанные в этом файле. Любой макрос, определенный во включаемом файле, но не использующийся в программе, никоим образом не влияет на итоговый машинный код.

Описанный выше процесс называется абсолютным ассемблированием. В этом случае исходный код располагается в единственном файле (ну, может быть, еще в нескольких включаемых файлах), и ассемблер помешает итоговый машинный код по известным (т. е. абсолютным) адресам памяти программ. Когда же программа состоит из нескольких модулей, очень часто написанных разными людьми или/и полученных из внешних источников и коммерческих библиотек,

Процесс создания программы, используемый в таких случаях, изображен на Рис. 8.3. Ключевую роль здесь играет программа компоновщика (или, иначе, редактора связей), которая осуществляет поддержку таких перекрестных ссылок между модулями. Перед компоновкой файл с исходным кодом каждого модуля должен быть транслирован в перемещаемый объектный файл. Термин «перемещаемый» означает, что окончательное местоположение модуля и различные адреса внешних меток еще не определены. Такая трансляция выполняется перемещаемым ассемблером. В отличие от абсолютного ассемблирования, в данном случае область памяти, в которую будет помещен машинный код, определяется компоновщиком, а не программистом, хотя абсолютные адреса, скажем, регистров портов ввода/вывода, все равно могут задаваться.

Рис. 8.3. Перемещаемая трансляция с языка ассемблера

Если рассматривать эту программу как разновидность компоновщика задач, то ее основными функциями будет следующее:

• Объединение кода и данных различных входных модулей.

• Присваивание числовых значений символьным меткам, которым не были заданы фиксированные значения самим программистом, с использованием директив equ и аналогичных.

• Генерация файла с абсолютным машинным кодом, а также сопутствующих файлов символов, листинга и ошибок этапа компоновки.

Чтобы компоновщик мог выполнить свою работу, он должен иметь представление об архитектуре памяти целевого процессора, т. е. он должен знать, где начинается и где заканчивается массив регистров общего назначения, где в памяти программ размещаются вектора, а также по каким адресам может располагаться код программы. Вся эта информация находится в так называемом командном файле компоновщика.

Простой пример такого командного файла для модели PIC16F627 приведен в Листинге 8.5.

Листинг 8.5. Содержимое командного файла компоновщика rms.1kr

// File: rms.1kr

// Simple linker command file for PIC16F627 Created 23/11/2003

CODEPAGE NAME=vectors START=0x0 END=0x4

CODEPAGE NAME=program START=0x5 END=0x3FF

DATABANK NAME=gprs START=0x20 END=0x4F

DATABANK NAME=auto START=0x50 END=0x6F

SECTION NAME=STARTUP ROM=vectors // Reset and int vectors

SECTION NAME=TEXT ROM=program // ROM code space

SECTION NAME=BANK0 RAM=gprs // Bank0 static storage

SECTION NAME=TEMP RAM=auto // Temporary auto storage

В этом файле использованы три директивы.

∙ codepage

Директива codepage используется для описания памяти программ. В данном случае директива используется для задания двух областей памяти — области векторов сброса и прерывания vectors, расположенной по адресам h’000’…h’004’, а также области program, расположенной в диапазоне адресов h’005’…h’3FF’ и используемой для размещения исполнимого кода. Думаю, вы уже догадались, что префикс 0х используется для указания шестнадцатеричных значений. Такая нотация используется в языке Си.

∙ databank

Эта директива похожа по своему назначению на директиву codepage, но используется для данных, размещаемых в ОЗУ. В данном случае группа регистров с адресами h’20’…h’4F’ названа gpr0, а группа регистров с адресами h’50’…h’6F’ — auto. Первая группа регистров используется в качестве области памяти данных общего назначения 0-го банка, а вторая группа определяет область памяти, которую программист может использовать для локальных переменных подпрограмм и которая освобождается после возврата из них.

∙ section

Эта директива компоновщика определяет две секции кода в памяти программ. Первая из них, названная STARTUP, будет использоваться программистом для размещения двух команд goto, расположенных по адресам имеющихся векторов, тогда как вторая, TEXT, используется для хранения основного кода программы. Директива ассемблера code с соответствующей меткой, помещаемая в файл с исходным кодом, сообщает компоновщику, в каком из двух блоков должен быть размещен следующий за ней код (в качестве примера см. Программу 8.2). Таким образом, можно задать сколь угодно много секций кода. Например, все подпрограммы можно разместить в заданной области памяти программ, изменив командный файл компоновщика следующим образом:

SECTION NAME=TEXT ROM=program // ROM code space

SECTION NAME=SUBROUTINES ROM=program // ROM subroutine stream

Кроме того, на секции можно разбить области памяти, заданные директивой DATABANK, заменив атрибут RAM директивы CODEPAGE на атрибут ROM. В нашем случае определено две секции. Одна из них, названная BANK0, предназначена для хранения данных, существующих на протяжении всего времени выполнения программы, а другая, названная TEMP, предназначена для хранения данных, которые можно перезаписывать после завершения подпрограммы. Директива ассемблера udata (Uninitialized DATA — неинициализированные данные) позволяет зарезервировать пространство для меток в области регистров общего назначения. Директива udata_ovr (Uninitialized DATA OVeRlay — перегружаемые неинициализированные данные) сообщает ассемблеру о том, что данные регистры можно использовать между вызовами подпрограмм (см. Программу 8.4).

Для иллюстрации принципов компоновки напишем программу, реализующую функцию вычисления среднеквадратичного значения √(NUM_12+ NUM_22).

Предположим, что над этой задачей работает три коллектива программистов. Задачи были распределены между ними руководителем проекта (четвертым человеком?) следующим образом:

1. Написание основной функции, выполняющей следующие действия:

а) Возведение NUM_1 в квадрат.

б) Возведение NUM_2 в квадрат.

в) Сложение NUM_12 и NUM_22.

г) Вычисление квадратного корня суммы (в).

2. Написание подпрограммы возведения в квадрат однобайтного числа, находящегося в рабочем регистре, которая возвращает двухбайтное значение в двух РОН.

3. Написание подпрограммы, вычисляющей значение корня квадратного двухбайтного числа и возвращающей результат в W.

В графическом виде процесс разработки, основанный на такой декомпозиции задачи, приведен на Рис. 8.4.

Рис. 8.4. Компоновка трех файлов с исходным кодом для реализации программы вычисления квадратного корня

Текст основной функции приведен в Программе 8.2. Программа начинается с команды goto, расположенной по адресу вектора сброса и размещенной в секции STARTUP. А, начиная с метки MAIN, код располагается в секции TEXT за счет использования директивы TEXT code. Из map-файла (генерируется компоновщиком), содержимое которого приведено в Листинге 8.6, видно, что метке MAIN соответствует адрес h’005’.

Программа 8.2. Основной перемещаемый исходный файл main. asm

           include "p16f627.inc"

           extern SQR_ROOT, SQR, SQUARE

; ---------------------------------

BANKO udata ; Статические данные

NUM_1 res 1 ; Первое число

NUM_2 res 1 ; Второе число

SUM     res 2 ; Два байта суммы

RMS     res 1 ; Один байт результата

; ---------------------------------

STARTUP code

               goto MAIN ; Вектор сброса

TEXT code

MAIN movf NUM_1,w ; Берем 1-е число

         call SQR; Возводим его в квадрат

         movf SQUARE+1,w ; Берем младший байт

         movwf SUM+1 ; Он становится младшим байтом суммы

         movf SQUARE,w ; Берем старший байт

         movwf SUM ; Он становится старшим байтом суммы

         movf NUM_2,w ; Теперь берем 2-е число

         call SQR ; Возводим его в квадрат

         movf SQUARE+1,w ; Берем младший байт

         addwf SUM+1,f ; Прибавляем к младшему байту суммы

         btfsc STATUS,С ; Проверяем перенос

           incf SUM, f ; Учитываем перенос

         movf SQUARE,w ; Берем старший байт

         addwf SUM,f ; Прибавляем к старшему байту суммы

         call SQR_ROOT ; Вычисляем корень квадратный

         movwf RMS ; Получаем среднеквадратичное значение

         sleep  ; Прекращаем вычисления

         global SUM

         end

В основной процедуре используется четыре переменных, расположенных в секции данных BANK0. Эта секция размещается в инициализированной области ОЗУ с помощью директив udata и res (REServe). Под каждую из входных переменных NUM_1 и NUM_2 зарезервировано по одному регистру. Под переменную SUM, в которой сохраняется значение суммы NUM_12 + NUM_22, зарезервировано два байта памяти данных. Поскольку эта переменная является входной для подпрограммы SQR_ROOT, она объявлена в конце файла как глобальная с помощью директивы global. Это означает, что ее расположение общеизвестно, так что дополнительные файлы, которые компонуются вместе, могут использовать имя SUM, объявляя его как extern, т. е. внешнее по отношению к файлу. Переменные, не объявленные таким образом, «скрыты» от внешнего мира, т. е. являются локальными переменными. Таким образом, директива extern в заголовке Программы 8.2 позволяет основной процедуре вызывать подпрограммы SQR_ROOT и SQR, еще не зная, где они будут расположены. Точно таким же образом переменная SQUARE используется подпрограммой SQR для возврата квадрата байта, переданного ей в регистре W. Место под эту переменную резервируется в области РОН в подпрограмме SQR, и ее точное положение в памяти данных файлу main.asm неизвестно, оно будет распределено позже компоновщиком. Из шар-файла, текст которого приведен в Листинге 8.6, видно, что в конечном счете эта переменная была размещена в регистрах h’25’:h’26’ (старший: младший байты).

Основная часть кода выполняет перечисленные выше задачи. Значение NUM_12 помещается в регистры SUM;SUM+1, к которым впоследствии будет прибавлено значение NUM_22. Затем результат передается в подпрограмму SQR_ROOT, возвращающую в рабочем регистре значение квадратного корня. В заключение это значение сохраняется в регистре RMS, под который был зарезервирован один байт в секции данных BANK0.

Подпрограмма sqr. asm, текст которой приведен в Программе 8.3, базируется на подпрограмме из Программы 6.7 (стр. 186), выполняющей перемножение двух-байтных значений. В нашем случае при входе в подпрограмму содержимое рабочего регистра копируется в регистр с именем X, а в регистрах X_COPY_H:X_COPY_L формируется 16-битная копия этого значения. Используя алгоритм сдвига и сложения, вычисляется значение X х X = X2. Эти три регистра размещаются в секции TEMP с помощью директивы udata_ovr, которая сообщает компоновщику о том, что эти регистры могут повторно использоваться другими модулями. Из шар-файла можно увидеть, что переменная X была размещена в регистре h’50’, как и переменная I, использующаяся в подпрограмме SQR_ROOT (см. Программу 8.3). За счет этого достигается гораздо более эффективное использование памяти данных. Переменные, существующие только в пределах той подпрограммы, в которой они определены, в языке Си называются автоматическими (automatic), поскольку занимаемая ими память автоматически перераспределяется по мере необходимости. Если же память под переменную выделяется фиксированно, то такая переменная называется статической (static). Глобальные переменные, такие как SQUARE, всегда объявляются как статические. В нашем случае переменная SQUARE создается резервированием двух байтов данных в секции BANK0 с использованием директивы udata. Она также публикуется с использованием директивы global, поскольку является именем подпрограммы.

Программа 8.3. Перемещаемый исходный файл sqr. asm

           include ”p16f627.inc"

; Подпрограмма SQR

; **********************

; * ФУНКЦИЯ: Возводит в квадрат 1-байтное число и возвращает 2-байтный результат *

; * ПРИМЕР: X = 10h (16), SQUARE = 0100h (256) *

; * ВХОД: X в W *

; * ВЫХОД: SQUARE:2 в области неинициализированных данных *

; **********************

BANK 0 udata ; Статические данные

SQUARE res 2 ; Старший: младший байты квадрата

; -----------------------

TEMP udata_ovr ; Автоматические данные

X res 1 ; X

X_COPY_L res 1 ; Копия X

X_COPY_H res 1 ; Старший байт X

; -----------------------

TEXT code

; Задача 1: Обнуляем 2-байтное значение квадрата

SQR clrf SQUARE

       clrf SQUARE+1

; Задача 2: Копируем и расширяем X до 16 битов

       movwf X ; Сохраняем X в памяти данных

       movwf X_COPY _L; Создаем копию X

       clrf X_COPY_H ; и расширяем до двух байтов

; Задача 3: ВЫПОЛНЯТЬ

     ; Задача 3а: Сдвигаем X на один бит вправо

SQR_LOOP bcf STATUS,С ; Сбрасываем бит перекоса

                 rrf X,f ; Сдвигаем

     ; Задача 3б: ЕСЛИ С == 1, ТО прибавить сдвинутое значение X к квадрату

                 btfss STATUS,С ; ЕСЛИ С == 1, TO складываем

                    goto SQR_CONT ; ИНАЧЕ пропускаем эту задачу

                 movf X_COPY_L,w ; ВЫПОЛНЯЕМ сложение

                 addwf SQUARE+1,f ; Сначала младшие байты

                   btfsc STATUS,С ; ЕСЛИ нет переноса, ТО переходим к старшим байтам

                   incf SQUARE, f ; ИНАЧЕ учитываем перенос

                 movf X_COPY_H,w ; Теперь старшие байты

                 addwf SQUARE,f

       ; Задача 3 г: Сдвигаем 1б-битную копи» X на один бит вправо

SQR_CONT bcf STATUS,С ; Сбрасываем бит переноса

                 X_COPY_L,f

                 X_COPY_H,f

        ; ПОКА X не равен нулю

                 movf X,f ; Проверяем множитель на ноль

                 btfss STATUS,Z

                   goto SQR_LOOP ; ЕСЛИ не ноль, TO повторяем вычисления

FINI            return ; ИНАЧЕ ВЫХОДИМ

                 global SQUARE, SQR

                 end

Код последней подпрограммы приведен в Программе 8.4. Эта программа практически идентична Программе 8.1. Отличие между ними заключается в замене директивы org на TEXT code и cblock на TEMP udata_ovr для распределения автоматических локальных переменных. Данные передаются в подпрограмму посредством 2-байтной глобальной переменной SQR_ROOT, которая объявлена как внешняя (место под эту переменную было выделено в файле main.asm). Имя подпрограммы SQR_ROOT опубликовано как глобальное, чтобы ее было видно из файла main.asm.

Программа 8.4. Перемещаемый исходный файл root. asm

               include "p16f627.inc"

               extern SUM ; 2-байтное число (старший: младший)

TEMP udata_ovr ; Автоматические переменные

I res 2 ; Магическое число (старший: младший)

COUNT res 1 ; Счетчик цикла

; ------------------------------

TEXT code

SQR_ROOT clrf COUNT ; Задача 1: Обнулить счетчик цикла

                 clrf I; Задача 2: Записать 1 в магическое число

                 clrf I+1 incf I+1,f

SQR_LOOP movf I+1,w ; Задача 3а: Number — I

                 subwf SUM+1,f ; Вычитаем мл. байт I из мл. байта Num

                 movf I,w ; Берем старший байт магического числа

                 btfss STATUS,С; Пропускаем, ЕСЛИ не было заёма

                   addlw 1 ; Корректируем заём

                 subwf SUM,f ; Вычитаем старшие байты

                 btfss STATUS,С; ЕСЛИ нет заёма, ТО продолжаем

                    goto SQR_END; ИНАЧЕ процесс завершен

                 incf COUNT,f; Задача 3б: ИНАЧЕ инкрементируем счетчик цикла

                  movf I+1,w ; Задача 3в: Увеличиваем магическое число на 2

                    addlw 2

                  btfsc STATUS,С ; ЕСЛИ нет переноса, ТО продолжаем

                    incf I,f; ИНАЧЕ прибавляем перенос к старшему байту

                  movwf I+1

                  goto SQR_LOOP

SQR_END     movf COUNT,w; Задача 4: Возвращаем счетчик цикла в качестве корня

                   return

                   global SQR_ROOT

                   end

Как и во всех исходных файлах, в файле root.asm используются различные регистры специального назначения, такие как STATUS. Поэтому заголовочный файл pic 16f627.inc включается в каждый из исходных файлов. Поскольку содержимое данного файла представляет собой набор директив equ, имена, определяемые в этом файле, публикуются как абсолютные и не затрагиваются компоновщиком. По этой причине в шар-файле (Листинг 8.6) эти фиксированные идентификаторы не указываются. Однако они выводятся в файл листинга, генерируемый компоновщиком.

Чтобы связать вместе эти три исходных файла, в командной строке при запуске компоновщика перечисляются имена входных объектных файлов, имя командного файла компоновщика и имена шар-файла и файла с машинным кодом. В нашем случае эта строка будет следующей:

mplink.exe rms.lkr main.о sqr.о root.о /m rms.map /о rms.hex

Понятно, что сгенерированный шар-файл будет называться rms.mар, а файл с абсолютным машинным кодом — rms. hex.

Для документирования проекта компоновщик генерирует составной файл листинга, похожий (но более полный) на файл, текст которого приведен в Листинге 8.2, и опциональный map-файл. Как видно из Листинга 8.6, этот файл состоит из двух списков. В первом из них приводится информация по каждой секции. Список включает имя секции, тип, начальный адрес, местоположение секции (в памяти программ или памяти данных) и ее размер в байтах. Из таблицы использования памяти программ (Program Memory Usage) видно, что было использовано 63 ячейки памяти программ, включая два байта вектора сброса команды goto — или примерно 6 % от имеющегося объема.

Листинг 8.6. Содержимое map-файла rms.map, генерируемого компоновщиком

MPLINK 3.80, Linker

Linker Map File — Created Sat Jan 08 23:09:26 2005

Section Info

Section Type Address Location Size(Bytes

STARTUP code 0x000000 program 0x000002

cinit romdata 0x000001 program 0x000004

TEXT code 0x000005 program 0x000078

BANK0 udata 0x000020 data 0x000007

TEMP udata 0x000050 data 0x000003

Program Memory Usage

Start End

0x000000 0x000002

0x000005 0x000040

63 out of 1024 program addresses used, program memory utilization is 6%

Symbols — Sorted by Name

Name       Address   Location     Storage File

FINI           0x00002b program static sqr.asm

MAIN         0x000005 program static main.asm

SQR           0x000016 program extern sqr.asm

SQR_CONT 0x000025 program static sqr.asm

SQR_END    0x00003f program static root.asm

SQR_LOOP 0x00001b program static sqr.asm

SQR_LOOP 0x000030 program static root.asm

SQR_ROOT 0x00002c program extern root.asm

COUNT       0x000052 data static root.asm

I                0x000050 data static root.asm

NUM_1        0x000020 data static main.asm

NUM_2        0x000021 data static main.asm

RMS            0x000024 data static main.asm

SQUARE      0x000025 data extern sqr.asm

SUM            0x000022 data extern main.asm

X                0x000050 data static sqr.asm

X_COPY_H   0x000052 data static sqr.asm

X_COPY_L   0x000051 data static sqr.asm

Во второй таблице выводится информация об идентификаторах, используемых в итоговой программе. Приводится информация о месте расположения каждого идентификатора в памяти программ или данных, а также имя исходного файла, в котором он объявлен. Глобальные идентификаторы помечаются словом extern, а идентификаторы локальных переменных помечаются словом static (к ним относятся и автоматические переменные, такие как COUNT и X_COPY_H, которые располагаются в регистре h’52’).

Итоговый файл, приведенный в Листинге 8.7, представляет собой обычный исполнимый файл в машинных кодах, который можно загрузить в память программ и запустить обычным образом.

Листинг 8.7 . Содержимое итогового абсолютного объектного файла rms. hex

: 020000000528D1

: 040002000034003492

: 06000А0020081620260864

: 10001000А3002508А200210816202608А30703181С

: 10002000А20А2508А2072С20А4006300А501А601АЕ

: 10003000D000D100D2010310D00C031C2528510898

: 10004000A6070318A50A5208A5070310D10DD20D63

: 10005000D008031D1B280800D201D001D101D10A0C

: 100060005108A3025008031C013EA202031C3F28B2

: 10007000D2 0A5108023E0318D0 0ADl003028520893

: 02008000080076

: 00000001FF

Разработка, тестирование и отладка программного обеспечения требуют большого числа различных программных средств. С некоторыми из них мы уже познакомились — это редактор, ассемблер и компоновщик. На самом деле существует много других пакетов программ, таких как компиляторы языков высокого уровня (см. главу 9), симуляторы и программаторы EEPROM. Все эти пакеты условно показаны на Рис. 8.5. Настройка данных программных средств и обеспечение взаимодействия между ними в индивидуальном порядке может представлять собой достаточно сложную задачу, особенно при использовании продукции разных производителей. В последнем случае обеспечение совместимости между различными форматами промежуточных файлов может превратиться в сущий кошмар.

Многие компании, занимающиеся разработкой инструментальных средств, предлагают графические среды, делающие процесс разработки программ простым и интуитивно понятным. Что касается микроконтроллеров PIC, то компания Microchip Technology предоставляет интегрированную среду разработки (ИСР) MPLAB®, которая объединяет полностью совместимые средства разработки программ под одной «крышей». Как и все программные продукты компании (за исключением компилятора языка Си), ИСР MPLAB распространяется свободно.

Рис. 8.5. Инструментальные средства создания и отладки программного обеспечения

Среда MPLAB осуществляет интеграцию Microchip-совместимых программных средств с целью создания законченной среды для разработки ПО. В частности, в состав MPLAB входят следующие программы:

• Менеджер проектов, который группирует заданные файлы, относящиеся к данному проекту; например, файлы с исходным кодом, объектные файлы, файлы симуляции, файлы листингов и hex-файлы.

• Редактор для написания исходных файлов и командных файлов компоновщика.

• Ассемблер, компоновщик и библиотекарь для трансляции исходного кода и создания библиотечных модулей, которые могут использоваться компоновщиком.

• Симулятор для моделирования процесса исполнения команд и ввода/вывода на персональном компьютере (см. Рис. 8.7).

• Загрузчик, который используется совместно с программатором, подключаемым к компьютеру через последовательный порт или USB (см. Рис. 17.4 на стр. 616).

• Программное обеспечение для эмуляции микроконтроллеров PIC в режиме реального времени на целевой аппаратуре. При этом вместо целевого процессора к плате устройства подключается внутрисхемный эмулятор (ICE) или отладчик, управляемый через последовательный порт или USB.

В фирменном руководстве пользователя MPLAB IDE User’s Guide содержится учебник и подробная информация по ИСР MPLAB, рассмотрение которой выходит за рамки данной книги. Тем не менее, исключительно для иллюстрации, приведу два скриншота, полученных во время разработки предыдущего примера, в котором используются файлы main.asm, sqr.asm и root.asm, как изображено на Рис. 8.6 и Рис. 8.7.

На Рис. 8.6 показано окно, отображающее содержимое проекта (файл rms.mcp), сформированного после работы начального «мастера». Проект включает три исходных файла, созданных ранее при помощи редактора. Кроме того, в проекте присутствует командный файл компоновщика rms.lkr, который также был создан и сохранен ранее. Итоговый файл с машинным кодом будет называться rms.hex.

Рис. 8.6. Окно проекта ИСР MPLAB версий 6.x, отображающее имена файлов, используемых для ассемблирования, компоновки и симуляции Программы 8.2

После того как проект создан, можно приступать к выполнению следующих операций:

1. Ассемблирование файла main.asm для получения объектного файла main.o.

2. Ассемблирование файла sqr.asm для получения объектного файла sqr.o.

3. Ассемблирование файла root.asm для получения объектного файла root.о.

4. Компоновка объектных файлов, полученных на этапах 1…3, в соответствии с командным файлом rms.lkr.

5. При отсутствии синтаксических ошибок создание абсолютного исполняемого файла, содержимое которого приведено в Листинге 8.7.

Для этого необходимо выбрать в меню P roject (четвертый пункт слева на Рис. 8.7) команду Make Project. При обнаружении синтаксических ошибок на экране появится окно ошибок со списком. Двойной щелчок на любой ошибке вызовет переход к соответствующему окну с исходным кодом и установке курсора на строку, в которой эта ошибка была обнаружена.

Рис. 8.7. Снимок экрана при работе ИСР MPLAB версий 6.x во время симуляции проекта, приведенного на Рис. 8.6

После успешного создания программы можно выполнить ее симуляцию. При этом ПК моделирует поведение микроконтроллера PIC, т. е. выполнение его команд и функционирование периферийных модулей. Пользователь может в любой момент сбросить симулируемый микроконтроллер, установить точки останова, выполнять программу в пошаговом или нормальном режиме. Во время симуляции можно наблюдать за содержимым заданных регистров или даже всей памяти данных. Разумеется, скорость выполнения программы при симуляции будет на несколько порядков ниже, чем при использовании реального микроконтроллера.

Симуляцию можно запустить из меню Debugger. Пункты этого меню вынесены на отдельную панель инструментов Debugger (справа вверху на Рис. 8.7). В режиме симуляции оператор может:

• Сбросить виртуальный процессор, нажав на кнопку .

• Запустить  симуляцию на максимальной скорости и приостановить  ее.

• Автоматически выполнять программу  со скоростью несколько шагов в секунду.

• Выполнять программу пошагово в трех различных режимах (по одной строке при каждом щелчке по соответствующей кнопке):

— Шаг с заходом  — проходит по всей программе, включая подпрограммы.

— Шаг без захода  — обходятся подпрограммы (они выполняются с максимальной скоростью).

— Шаг с выходом  — код подпрограммы выполняется за один шаг.

На Рис. 8.7 показан конечный результат симуляции нашей программы вычисления среднеквадратичного значения. Как и все три окна с исходными файлами, окно просмотра переменных (Watch) было открыто с помощью меню V iew . В это окно оператор может добавлять любые именованные регистры общего назначения, такие как NUM_1, значение которого может отображаться в двоичном, десятичном и шестнадцатеричном виде с различной разрядностью (побитово, один байт, два байта, три байта). Эти значения обновляются после каждого выполнения симулятором одиночного или автоматического шага. При работе на максимальной скорости обновление окна Watch производится при приостановке симуляции или остановке выполнения программы в точке останова.

Кроме того, на Рис. 8.7 показано содержимое окна Stop-watch. Из данных этого окна следует, что для выполнения программы потребовалось 292 машинных цикла при начальных значениях NUM_1 и NUM_2 соответственно 0x05 и 0x08. Поскольку симулировалась работа с кварцевым резонатором частотой 8 МГц, время выполнения программы составило 146 мкс.

В процессе симуляции выполняемая в данный момент команда помечается символом  в левом поле соответствующего окна с исходным кодом. На Рис. 8.7 этот символ указывает на последнюю команду sleep и нарисован поверх символа точки останова . Точки останова можно устанавливать или сбрасывать, выполняя щелчок правой кнопкой мыши на соответствующей команде. При нажатии на кнопку  программа будет выполняться с максимальной скоростью до тех пор, пока не встретится следующая точка останова.

Симуляция не позволяет выявить все проблемы, особенно те, которые связаны со сложным взаимодействием программной части и аппаратных средств. Однако более 95 % всех проблем вызываются исключительно ошибками при написании программы, и симуляция представляет собой хороший инструмент для тестирования и отладки подобного кода.

Например, наша программа не будет работать, если результат операции NUM_12 + NUM_22 > 65535, поскольку размер переменной SUM составляет два байта (см. Вопрос для самопроверки 8.5). При отладке всегда необходимо первым делом проверить функционирование программы при максимально и минимально возможных значениях переменных. Тем не менее такая проверка никоим образом не гарантирует корректную работу программы при всех возможных сочетаниях значений входных переменных.

* * *

В заключение приведем общую информацию, специфичную для Microchip-совместимых ассемблеров, которая может вам понадобиться при чтении программ из оставшейся части книги:

• Представление чисел.

— Шестнадцатеричные: начинаются с символа «h», после которого следует шестнадцатеричное число, заключенное в кавычки, например h’41’. Также может использоваться завершающий символ «h» (например, 41h) или префикс «0х» (например, 0x41). Как правило, в ассемблере это основание используется по умолчанию, поэтому в некоторых программах указатели шестнадцатеричной системы могут быть опущены. Однако лучше на это не полагаться.

— Двоичные: начинаются с символа «Ь», после которого следует двоичное число, заключенное в кавычки, например Ь’01000001’.

— Десятичные: начинаются с символа «d», после которого следует десятичное число, заключенное в кавычки, например d’65’. Также могут обозначаться префиксом в виде точки (.65 в нашем случае).

— Символы ASCII: заключаются в одинарные кавычки, например ’А’.

• Арифметические операции с метками.

— Текущее положение в программе: обозначается $, например goto $+2.

— Сложение: +, например, goto LOOP+6.

— Вычитание: - например, goto LOOP-8.

— Умножение: *, например, subwf LAST*2.

— Деление: /, например, subwf LAST/2.

• Директивы.

— org: помещает последующий код в память программ, начиная с указанного адреса, например org h’100’. Если директива org не используется, то код размещается, начиная с адреса вектора сброса, т. е. h’000’. Может использоваться только при абсолютном ассемблировании.

— code: аналог директивы org для перемещаемого ассемблирования. Реальный адрес секции кода определяется в командном файле компоновщика. В данном файле может быть определено более одной секции кода, в этом случае их имена записываются в поле меток, например SUBROUTINES code.

— equ: связывает числовое значение с символьным именем, например PORTB equ 06. Вместо директивы equ можно использовать директиву #define (заимствованную из языка Си): #define PORTB 06.

— cblock…endc: используется при абсолютном ассемблировании для размещения переменных программы в памяти данных, например:

cblock h’20’

      FRED; Один байт по адресу h’20’ для переменной FRED

      JIM:2 ; Два байта по адресам h’21’:h’22’ для переменной JIM

      ARRAY:10 ; Десять байтов по адресам h’23’…h’2C’ для переменной ARRAY

endc

После первого использования директивы cblock указывать адрес необязательно.

— udata: аналог cblock для перемещаемого ассемблирования. Начальный адрес блока в памяти данных указывается в командном файле компоновщика. В этом файле может быть определено более одной секции данных, в таком случае их имена записываются в поле меток, например:

SCRATCHPAD udata ; Секция неинициализированных данных

                      FRED ; Резервируется один байт для переменной FRED

                      JIM:2 ; Резервируется два байта для переменной JIM

                      ARRAY:10 ; Резервируется десять байтов для переменной ARRAY

— udata_ovr: эта директива аналогична udata, за исключением того, что компоновщик пытается повторно использовать регистры, выделенные под объявленные таким образом переменные.

— res: используется совместно с директивой udata для резервирования одного или более байтов под переменную в секции данных.

— extern: публикует именованные переменные как определенные вне текущего файла. Впоследствии эти переменные связываются компоновщиком.

— global: публикует именованные переменные, которые были определены (т. е. под них было зарезервировано место в памяти) в данном файле, и таким образом делает их видимыми для компоновщика.

— macro…endm: используется для замены последовательности команд процессора, помещенных между указанными директивами, одной макрокомандой, например макрокоманда:

Addf macro N,datum

        movf datum,w

        addlw N

        movwf datum

        endm

прибавляет константу N к заданному регистру datum. Соответственно для прибавления 5 к регистру h’20’ программист может использовать вызов Addf 5,h’20’.

— include: используется для включения содержимого указанного файла в точке использования директивы, например include "myfile. asm". Вместо нее можно использовать аналогичную директиву #include.

— end: обычно размещается в последней строке исходного ассемблерного файла. Сообщает ассемблеру о том, что содержимое файла, расположенное после нее, следует игнорировать.

Примеры

Пример 8.1

Следующая последовательность команд позволяет выполнить обмен содержимого рабочего регистра и регистра F, не используя при этом никаких дополнительных регистров.

xorwf F,f ;  [F] <- W^F

xorwf F,w ; W <- W^(W^F) = 0^F = F

xorwf F,f ;  [F] <- F^W^F = 0^W = W

Символ «А» означает операцию Исключающее ИЛИ.

Создайте из этой последовательности макрокоманду Exgwf F, в которой F является заданным регистром, например Exgwf h’20’.

Решение

Обрамив код соответствующими директивами, получим макрокоманду

Exgwf macro FILE

            xorwf FILE,f

            xorwf FILE(w

            xorwf FILE,f

           endm

Обратите внимание, что эта макрокоманда не влияет на состояние флага С, а флаг Z устанавливается в соответствии с содержимым рабочего регистра, которое было в нем при вызове макрокоманды.

Пример 8.2

Линейка микроконтроллеров PIC18XXXX имеет команду bnc (перейти, если не было переноса), которая выполняет переход по указанному адресу при нулевом значении флага переноса С. Напишите макрокоманду, выполняющую те же действия, для микроконтроллеров с 12- и 14-битным ядром.

Решение

Для написания этого кода мы возьмем макрокоманду bne, текст которой приведен на стр. 225, и заменим флаг Z флагом С. Назовем полученную макрокоманду Всс (Branch if Carry Clear), поскольку имя Bcn в ассемблерах версий 3+ является зарезервированным словом, т. е. мнемоническим обозначением команды микроконтроллеров PIC18XXXX.

3cc macro destination

         btfss STATUS,С

            goto destination

         endm

Пример 8.3

Напишите макрокоманду, формирующую задержку длительностью n машинных циклов, где n является целым числом не более 1024. Так, например, написав в программе строку Delay_cycles d’400’, мы должны будем получить задержку длительностью 400 машинных циклов.

Решение

В макрокоманде, приведенной ниже, один проход цикла выполняется за 4 машинных цикла, поэтому операнд макроса делится на четыре для получения начального значения счетчика цикла. В указанном примере операнд 400 загружается в рабочий регистр как число d’100’.

Delay_cycles

           macro cycles

           local LOOP

             movlw cycles/4 ; Один проход — 4 маш. цикла

LOOP     addlw -1 ; Декрементируем

             btfss STATUS,Z ; Ноль?

               goto LOOP

             endm

Метка, используемая в макрокоманде, объявлена при помощи директивы local, чтобы гарантировать, что при каждом использовании макрокоманды имя LOOP не будет добавляться в таблицу идентификаторов транслятора. Если бы мы не сделали этого, то при повторном использовании макрокоманды возникла бы ошибка «Address label duplicated» (дублирование метки).

Пример 8.4

Макрокоманды могут быть вложенными, т. е. при написании одной макрокоманды можно использовать другие. В качестве примера напишем макрокоманду в которой РОН инициализируется заданным значением, а затем декрементируется до нуля. Предполагая, что макрос Movlf уже определен:

Movlf macro literal,destination

           movlw literal ; Загружаем константу в W

           movwf destination ; и пересылаем ее в заданный регистр

          endm

напишите код требуемой макрокоманды.

Решение

Возможное решение выглядит следующим образом:

Countdown macro literal/destination

                  local C_LOOP ; Метка макрокоманды

                    Movlf literal,counter ; Инициализируем счетчик

CLOOP          decfsz counter,f ; Декрементируем

                      goto C_LOOP ; Повторяем, пока не равно нулю

                    endm

Заданный регистр, обозначенный именем counter, сначала инициализируется константой с помощью макрокоманды Movlf. Операция обратного счета реализована с помощью команды decfsz, которая как декрементирует содержимое регистра, так и осуществляет выход из цикла при достижении нуля. Таким образом, вставка строки Countdown d’100’,h’40’ проинициализирует регистр h’40’ десятичным числом 100 и декрементирует его до нуля. Этот процесс займет (3 х count) + 1 циклов задержки, т. е. 301 цикл в нашем случае.

Заметьте, что при выполнении этой макрокоманды помимо заданного РОН изменяется также содержимое рабочего регистра и регистра STATUS. Такие побочные эффекты очень опасны при использовании макрокоманд, особенно если такая макрокоманда была написана кем-то другим и ее код скрыт от нас во включаемом файле. На всякий случай всегда считайте, что регистры W и STATUS изменились, пока не доказано обратное. Смена банков памяти внутри макроопределений также несет в себе потенциальную опасность.

Пример 8.5

Модель PIC16F84 имеет одну особенность, а именно: в ней все РОН отображены на оба банка памяти (см. Рис. 4.7 на стр. 97). Обычно в каждом банке памяти располагаются уникальные РОН. Например, модели PIC16F627/8 имеют 80 уникальных РОН в 0-м банке, 80 уникальных РОН в 1-м банке, 48 уникальных РОН во 2-м банке, а также 16 общих РОН, отображенных на все четыре банка памяти (см. карту памяти, приведенную на Рис. 5.4, стр. 121).

Чтобы выбрать регистр в 1-м банке, необходимо соответствующим образом изменить биты RP0:RP1 регистра STATUS. Так, для копирования содержимого рабочего регистра в регистр h’E0’ требуется следующее:

bsf STATUS,RP0 ; Переключаемся на 1-й банк

bcf STATUS,RP1

movwf h’E0 ’; Копируем W в регистр h’E0’

bcf STATUS,RP0; Переключаемся обратно на 0-й банк

При использовании перемещаемого ассемблера программист не всегда знает, в какой из банков компоновщик поместил переменную. Более того, при изменении набора исходных файлов и их содержимого номер этого банка может произвольно изменяться на различных этапах проекта!

Чтобы обойти эту проблему, в ассемблере имеется директива выбора банка banksel. Эта директива автоматически отслеживает местоположение именованной переменой и вставляет в программу соответствующий код, учитывающий изменения. Покажите, как следует использовать эту директиву при сохранении десятичных констант 1,10,100 в трех РОН, названных var_0, var_1 и var_2 соответственно.

Решение

Возможная последовательность команд приведена ниже. Директива вставляет в код перед выполнением следующей команды соответствующую комбинацию команд bsf STATUS,RPx и bcf STATUS,RPx.

movlw 1 ; Первая константа

banksel var_0 ; Переключаемся на соответствующий банк

movwf var_0 ; Сохраняем

movlw d’10’; Вторая константа

banksel var_1 ; Переключаемся на соответствующий банк

movwf var_1 ; Сохраняем

movlw d’100’ ; Третья константа

banksel var_2 ; Переключаемся на соответствующий банк

movwf var_2 ; Сохраняем

При использовании косвенной адресации в моделях с 4 банками памяти, необходимо соответствующим образом изменять бит IRP регистра STATUS (см. стр. 127). Для этого предназначена директива bankisel, использующаяся аналогично директиве banksel.

Вопросы для самопроверки

8.1. Напишите макрокоманды, аналогичные командам условного перехода Ьс (переход при переносе) и bz (переход при нуле), имеющимся в моделях PIC18XXXX.

8.2. Напишите макрокоманду, которая реализует функцию PRODUCT:2 = VAR1 х VAR2 (формат вызова макрокоманды — Mul XPLIER, XCAND, PRODUCT). Подсказка : обратите внимание на Программу 6.7, приведенную на стр. 186. Как вы думаете, какие есть преимущества и недостатки использования макрокоманд вместо подпрограмм при большом объеме составляющего их кода, как в данном случае?

8.3. В кодах команд goto и call используется 11-битный адрес, позволяющий выполнять переход в пределах 2 Кбайт памяти программ (см. Рис. 5.17 на стр. 153). Как видно из этого рисунка, содержимое счетчика команд замещается 11-битным адресом, содержащимся в коде команды совместно с битами 4:3 регистра PCLATH (h’0A’) для формирования полного 13-битного адреса. Некоторые микроконтроллеры среднего уровня имеют память программ объемом 4 или 8 Кбайт (скажем, PIC16F74 и PIC16F876 соответственно). Для формирования 13-битного значения счетчика команд при использовании команд goto и call в этих моделях тоже используются биты PCLATH[4:3] (см. стр. 117), разбивая память программ по сути дела на две или четыре страницы. Программист должен самостоятельно устанавливать эти биты для выбора страницы перед вызовом команд goto или call. Например, в модели PIC16F876 для вызова подпрограммы FRED, начинающейся с адреса h’0B00’ (т. е. на 1-й странице), мы имеем

bct PCLATH,3 ; Переходам на 1-ю страницу памяти программ

bsf PCLATH,4

call FRED ; Вызываем подпрограмму

В перемещаемой программе адреса меток, таких как FRED, не определены, и в моделях с несколькими страницами памяти программ эти метки могут быть помещены компоновщиком на любую-страницу. Чтобы ассемблер мог изменить биты PCLATH[4:3] соответствующим образом, в нем предусмотрена директива pagesel, которая должна использоваться перед любой командой goto или call аналогично директиве banksel, использованной нами в Примере 8.5. Покажите, как можно использовать данную директиву для поддержки последовательности вызовов подпрограмм, названных SUB_0, SUB_1, SUB_2.

8.4. Недостаток использования директивы banksel для выбора банка памяти заключается в том, что дополнительные команды вставляются в код даже в том случае, если микроконтроллер уже работает с требуемым банком. Подумайте над тем, как можно избежать этого при написании подпрограмм, критичных к размеру кода или ко времени выполнения.

8.5. Определите максимальное значение переменных NUM_1 и NUM_2 из нашей программы вычисления среднеквадратичного значения двух переменных, при котором программа будет работать корректно.

8.6. Перепишите код основной процедуры main.asm из Программы 8.2 и подпрограммы root.asm из Программы 8.4 таким образом, чтобы программа могла работать с любыми значениями переменных NUM_1 и NUM_2. Для этого потребуется использовать подпрограммы сложения и вычисления квадратного корня, оперирующие 3-байтными значениями.

8.7. В следующем фрагменте используется макрокоманда Movlf из Примера 8.4. Эти строки не работают так, как требуется. По всей видимости, переменная COUNT меняется произвольным образом, причем никак не связанным с требуемой константой 32. Почему?

movf COUNT,f; Проверяем COUNT на ноль

btfsc STATUS,Z ; ЕСЛИ не ноль, ТО пропускаем

Movlf d’32’,COUNT ; ИНАЧЕ реинициализируем

8.8. Программист, имеющий опыт работы с микроконтроллером 68НС05 компании Motorola, перешел к микроконтроллерам семейства PIC и собирается написать макросы, симулирующие, помимо всего прочего, приведенные ниже команды 68НС05. Заметьте, что регистр аккумулятора в семействе 68НС05 эквивалентен по назначению рабочему регистру в РIС-микроконтроллерах.

Ida memory

Загрузить в аккумулятор (LoaD Accumulator) байт из памяти данных.

Ida #data

Загрузить в аккумулятор (LoaD Accumulator) константу.

sta memory

Сохранить содержимое аккумулятора (STore Accumulator) в памяти данных.

tst memory

Проверить (TeST) байт памяти данных на нулевое значение.

tsta

Проверить аккумулятор (TeST Accumulator) на нулевое значение.

Напишите соответствующие макроопределения. Как вы думаете, почему такой подход является не слишком хорошей идеей?

 

Глава 9

Язык высокого уровня

Для написания всех программ в последних шести главах мы с вами использовали язык ассемблера. Хотя ассемблерные программы достаточно сильно отличаются от чистого машинного кода (см. стр. 239), тем не менее между любой машинной инструкцией и соответствующей командой ассемблера сохраняется однозначное соотношение. То есть программист вынужден мыслить объектами внутренней структуры микроконтроллера — регистров и памяти, а не объектами реализуемого алгоритма. Несмотря на то что большинство ассемблеров поддерживают макрорасширения, благодаря которым несколько машинных команд могут быть сгруппированы в виде псевдокоманды высокого уровня, это не более чем попытка обойти недостаток машинно-ориентированного языка. В чем же выражается этот недостаток? А в том, что для улучшения эффективности, качества и увеличения степени повторного использования программ кодирующий язык должен быть максимально независим от архитектуры процессора и должен иметь синтаксис, более ориентированный на решение задач.

Разумеется, не стоит и пытаться выучить какой-либо язык высокого уровня в одной короткой главе. Тем не менее, прочитав эту главу, вы:

• Поймете необходимость использования языка высокого уровня.

• Оцените преимущества, предоставляемые языком высокого уровня.

• Поймете, какие проблемы связаны с использованием языка высокого уровня для встраиваемых приложений на базе микроконтроллеров.

• Научитесь писать коротенькие программы на Си.

Уже в первые годы после появления коммерческих программных систем люди осознали, что писать большие программы на родном для компьютера языке очень сложно. Дело в том, что помимо всего прочего компьютеры начали периодически устаревать, и для каждой новой модели программы приходилось переписывать. Большие же программы даже в то время состояли из многих тысяч строк кода. Программисты встречались так же редко, как зубы у курицы, и ценились на вес золота. Поэтому, чтобы компьютеры были коммерчески выгодными, следовало найти средство, позволяющее сохранить инвестиции в дефицитное время программистов. При разработке универсального языка, независимого от аппаратной платформы, основное внимание было уделено тому, чтобы программист мог записывать код более естественным образом, в терминах, соответствующих решаемой задаче, а не на уровне памяти, регистров и флагов.

Разумеется, существует множество различных классов задач, требующих программирования, поэтому с тех пор было разработано большое количество языков программирования. Одними из первых языков были 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. Преобразование исходного кода, написанного на языке высокого уровня, в машинный код

При работе со встраиваемыми системами очень важно правильно выбрать язык высокого уровня. Причем основным критерием в данном случае будет объем машинного кода, генерируемого компилятором с языка высокого уровня, по сравнению с эквивалентным кодом, написанным на ассемблере. Ведь большинство встраиваемых микроконтроллерных устройств являются малогабаритными, имеют невысокую вычислительную мощность, а также ограниченные ресурсы памяти и малую стоимость — взять, к примеру, контроллер пульта дистанционного управления телевизора. В большинстве недорогих микроконтроллеров используется процессор с невысокой производительностью, имеющий, в лучшем случае, несколько сот байт ОЗУ и несколько килобайт ПЗУ Так что язык высокого уровня должен быть таким, чтобы его компилятор мог генерировать код, который если и не будет таким же эффективным, каким он мог бы быть при использовании ассемблера, то, по крайней мере, сравнимым с ним.

Наиболее часто для написания программ встраиваемых микропроцессорных и микроконтроллерных систем (Рис. 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 декрементируется в результате выполнения оператора декремента —. Записанное выражение эквивалентно выражению 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 видно, что на выходе компилятора получается ассемблерный код, который впоследствии может быть ассемблирован и скомпонован с другими модулями обычным образом. Чтобы проиллюстрировать это, в Листинге 9.1а приведен ассемблерный код, получившийся в результате компиляции Программы 9.1 кросс-компилятором компании Custom Computer Services (CCS). Это недорогой Си-компилятор (~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

видны окна с исходным текстом на языке Си и сгенерированным ассемблерным кодом. Несмотря на то что симуляция осуществляется на уровне ассемблера, в окне с кодом на языке Си всегда выделяется строка, соответствующая симулируемой (и выделенной) в данный момент команде ассемблера. В окне 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). Вот как это можно записать на стандартном языке Си (префикс Ох используется в языке Си для обозначения шестнадцатеричной системы):

#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.

   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. Таким образом, оператор 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-й строке как число с плавающей точкой. Это необходимо для поддержки сложных математических вычислений с дробными числами, выполняющихся в 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]И десять пальцев на ногах, однако система счисления по основанию 20 используется очень редко (но все-таки она существует).
будут размещены в десяти последовательно расположенных регистрах. Большинство микроконтроллеров 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.