Глава 6 Подпрограммы и модули
Хорошо написанное программное обеспечение должно представлять собой совокупность взаимодействующих модулей, а не одну большую программу, выполняющую все задачи от начала и до конца. У модульного принципа программирования имеется множество достоинств, без использования которых практически нельзя обойтись, когда размер кода становится больше нескольких сот строк или когда проект разрабатывается командой программистов.
Что же должны представлять собой эти модули? Чтобы ответить на этот вопрос, рассмотрим применение программных структур, которые предназначены для упрощения использования такого модульного подхода, и команды процессора, связанные с этими структурами.
Прочитав эту главу, вы:
• Убедитесь в необходимости применения модульного принципа программирования.
• Поймете структуру стека и его использование в механизме вызова подпрограмм и возврата из них.•
• Поймете термин «вложенная подпрограмма».
• Узнаете, как можно передать параметры в подпрограмму и возвратить результат в вызывающую программу.
• Сможете писать подпрограммы, оказывающие минимальное влияние на свое окружение.
• Сможете создавать программный стек для открытия и закрытия кадра в памяти данных, служащего для передачи параметров и обеспечения временной рабочей области.
Давайте заглянем внутрь вашего персонального компьютера. Скорее всего он будет похож на тот, фотография которого приведена на Рис. 6.1. На материнской плате типичного компьютера размещаются микропроцессор, различного рода память и другие сопутствующие ИС, а также некоторое количество слотов расширения. К этим слотам можно подключить плату дискового контроллера и видеокарту. Существуют и другие платы, например звуковая плата, модем, сетевой контроллер. Каждая из этих плат выполняет собственные и совершенно независимые задачи, но все они взаимодействуют через сервисы, предоставляемые главной платой — материнской.
Рис. 6.1. Модульная конструкция на примере ПК
Преимущества такой модульной конструкции очевидны:
Гибкость, т. е. относительная легкость модернизации или изменения конфигурации путем добавления или смены карт расширения.
• Возможность повторного использования компонентов от предыдущей системы.
• Возможность покупки стандартных плат или разработки собственных специализированных плат.
• Легкость обслуживания.
Разумеется, у такого подхода есть и некоторые недостатки. Материнская плата, в которую интегрированы все устройства, имеет меньший размер и, теоретически, меньшую стоимость, чем эквивалентная совокупность простой материнской платы и карт расширения. Скорее всего она даже будет более надежной, поскольку входные и выходные сигналы не проходят через разъемы. Однако, если в такой плате возникнут какие-либо неполадки, их, как правило, гораздо труднее отследить и исправить.
В модульном программировании используется тот же самый принцип построения «программных узлов», т. е. программ. Вот формальное определение принципа модульного программирования, которое дается в научно-технической литературе:
Способ разработки программ, при котором программа разбивается на логически завершенные единицы — модули. При этом каждый модуль может разрабатываться, программироваться, транслироваться и тестироваться независимо от других[89].
Таким образом, для написания программы в соответствии с принципом модульного программирования нам необходимо разбить общую задачу на несколько отдельных процедур, каждая из которых будет выполнять четко очерченную задачу. Такого рода модуль должен быть достаточно маленького размера, хорошо документирован и легок для понимания, причем не только для написавшего его программиста.
Преимущества модульного программирования аналогичны преимуществам модульного проектирования, но еще более убедительны:
• Модули можно тестировать, отлаживать и поддерживать по отдельности; это обеспечивает общую надежность.
• Можно повторно использовать модули из других проектов или купить их у сторонних производителей.
• Легче модернизировать программу (простой заменой модулей).
Решение о том, каким образом следует разбить программу на отдельные независимые задачи, принимается на основе опыта. Собственно кодирование этих задач в виде подпрограмм ничем не отличается от написания примеров, которые мы рассматривали в предыдущих главах (см., например, Программу 5.9 на стр. 162). Для реализации указанных подпрограмм имеется несколько дополнительных команд, которые перечислены в Табл. 6.1. Далее в настоящей главе мы рассмотрим эти команды, а также некоторые методики, применяемые при разработке программного обеспечения.
Вход в программный модуль можно выполнить посредством вызова этого модуля из другой части программы или по некоторому аппаратному событию, внешнему по отношению к ЦПУ. Этим событием может быть напряжение заданного уровня на одном из выводов процессора или же сигнал от внутреннего периферийного устройства, например признак переполнения в модуле таймера. В первом случае программный модуль называется подпрограммой, как и во многих языках высокого уровня, таких как FORTRAN и BASIC[90]. Во втором случае речь идет о подпрограмме обработки прерывания, или просто обработчике прерывания. Принципы написания этих модулей, а также процедурьгвхода и выхода из них настолько отличаются от обычных подпрограмм, что им посвящена отдельная глава (глава
Пока же мы займемся подпрограммами.
Аналогом подпрограмм в аппаратуре являются платы расширения. Предположим, что нам необходимо реализовать задержку длительностью 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-битных регистров, которые используются исключительно для хранения адресов возврата из подпрограмм[91]. Структура, показанная на Рис. 6.3, называется также аппаратным стеком. Этот стек расположен вне адресного пространства памяти микроконтроллера, поэтому его содержимое не может быть изменено программно[92].
С данным стеком связан 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[93] похожа на обычную команду 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-мс задержки[94], то значение K = 0 будет интерпретироваться как К = 256. Таким образом, диапазон задержек, формируемых подпрограммой, составит 0.1…25.6 с. Проверка перед циклом[95] даст нам диапазон задержек 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[96].
При использовании этой подпрограммы вызывающая программа копирует множимое в регистр 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 среднего уровня в явном виде программный стек не поддерживает[97]. Однако такую структуру можно эмулировать, используя косвенную адресацию на базе регистров 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) и которая возвращает соответствующее значение температуры по шкале Фаренгейта.