Глава 8 Инструментальные средства для работы с языком ассемблера
Начиная С главы 3, мы написали уже достаточно программ. Для доходчивости эти программы были написаны таким образом, чтобы их легче было понимать человеку. То есть команды представлялись короткими мнемониками, например return вместо Ь’00000000001000’. Аналогично, регистры имели имена, такие как INTCON, а строки имели метки и комментарии. Однако такое символьное представление годится только для человека. Микроконтроллер и знать ничего не знает, кроме двоичных кодов (см. стр. 64), составляющих исполнимый код и данные.
Воспользовавшись описанием набора команд (см. Приложение Г), можно вручную транслировать программу из подобного удобочитаемого формата в машиночитаемый двоичный код. В принципе для таких устройств, как микроконтроллеры PIC, имеющих сокращенный набор команд (RISC) и небольшое число режимов адресации, это не так уж и сложно. Однако это довольно долгий и утомительный процесс, особенно если программа достаточно большая. Кроме того, при таком подходе велика вероятность возникновения ошибок и усложняется внесение изменений в программу.
Знакомые вам компьютеры незаменимы там, где необходимо быстро и аккуратно выполнять однообразные операции. Очевидно, что задача перевода программы из символьного представления в машинный код полностью подходит под эту категорию. В данной главе мы вкратце рассмотрим различные программные средства, помогающие осуществлять этот процесс трансляции. В следующей главе мы также познакомимся с аналогичными средствами для языка высокого уровня.
Прочитав эту главу, вы:
• Узнаете, что такое язык ассемблера и как он соотносится с машинным кодом.
• Поймете преимущества символьного представления перед машинным кодом.
• Разберетесь в назначении ассемблера.
• Поймете разницу между абсолютным и перемещаемым кодом.
• Поймете назначение компоновщика.
• Ознакомитесь с процессом трансляции ассемблерной программы в абсолютный машинный код.
• Узнаете структуру файла машинных кодов и назначение программы-загрузчика.
• Научитесь использовать интегрированную среду разработки для автоматизации взаимодействия между различными программными средствами, необходимыми для превращения исходного кода в запрограммированный микроконтроллер.
Суть процесса преобразования ассемблерной программы показана на Рис. 8.1. Программа была набрана в символьном виде человеком, преобразована компьютером и выдана в машиночитаемом виде. Разумеется, за этой простотой скрываются гораздо более сложные процессы, которые мы разберем довольно подробно, что поможет вам в написании программ (в степени, достаточной, чтобы вы могли свободно писать программы).
Рис. 8.1. Преобразование исходного кода на языке ассемблера в машинный код
Вообще говоря, различные трансляторы и утилиты выпускаются и продаются огромным числом компаний, занимающихся разработкой программного обеспечения, поэтому конкретные детали и операции в различных коммерческих продуктах несколько отличаются. Что же касается конкретно микроконтроллеров PIC, то их производитель, компания Microchip Technology Inc, избрала стратегию бесплатного предоставления программных средств для работы с языком ассемблера. Именно это сыграло большую роль в популярности данных микроконтроллеров. Поэтому коммерческое ПО для работы на таком низком уровне встречается достаточно редко, и, более того, используемый синтаксис обычно совпадает с синтаксисом, принятым в программах самой Microchip. По этой причине в данной главе мы будем рассматривать пакет инструментальных средств Microchip.
Использование компьютеров для трансляции кода, представленного в удобном для человека символьном виде (исходный код), в понятный микроконтроллеру двоичный код (объектный или машинный код) и загрузки его в память устройств началось в конце 1940-х кодов. Помимо всего прочего, применение компьютеров позволило использовать системы счисления высших порядков, например шестнадцатеричную[107]. В шестнадцатеричной системе фрагмент кода, представленного на Рис. 8.1, будет выглядеть следующим образом:
0AA0 0820 3Е06 1905 00A0 0008
Шестнадцатеричный загрузчик транслирует этот код в двоичный и поместит его в память по заданному адресу. Данный загрузчик может входить в состав программного обеспечения вашего программатора PIC-EPROM. Написание программ в шестнадцатеричных кодах мало кого прельщает. Несмотря на некоторое уменьшение числа нажатия на клавиши, при таком способе ввода программы очень легко наделать кучу ошибок.
Для серьезного занятия программированием требуется, как минимум, символьный транслятор, или ассемблер[108]. Его применение позволяет программисту использовать для команд и внутренних регистров мнемонические обозначения, а также присваивать имена константам, переменным и адресам. Символьный язык, на котором пишется исходный код, называется языком ассемблера. В отличие от языков высокого уровня, таких как Си или Паскаль, язык ассемблера обеспечивает полное (один к одному) соответствие с генерируемым машинным кодом, т. е. из одной строки исходного кода получается одна команда. В качестве примера рассмотрим Программу 8.1, представляющую собой слегка модифицированную версию Программы 6.12 со стр. 199. Эта подпрограмма вычисляет значение квадратного корня из 16-битной переменной NUM, которая размещается в двух байтах памяти данных, и возвращает в рабочем регистре 8-битное целое значение квадратного корня.
Присвоение имен различным адресам и константам особенно необходимо в случае больших программ, состоящих из нескольких тысяч строк кода. В совокупности с комментариями это облегчает отладку, разработку и поддержку кода. К примеру, в большинстве наших программ до настоящего момента мы использовали следующие строки:
STATUS equ 3; Регистр STATUS расположен по адресу h’03’
С equ 0; Флаг переноса — 0-й бит этого регистра
Псевдокоманда equ представляет собой образец директивы ассемблера. Директива не генерирует код как команда, а используется для передачи ассемблеру информации, касающейся его работы. В данном случае написанное означает, что при обнаружении в поле операндов команды имени STATUS оно должно быть заменено на число 3, а имя С должно быть заменено на число 0.
Директива equ лучше всего подходит для указания имен РСН и их битов. Поскольку для каждой модели микроконтроллера эти значения фиксированы и, соответственно, не являются уникальными для какой-либо конкретной программы, компания Microchip предоставляет для каждого устройства файлы с расширением. inc. Эти файлы могут быть включены в пользовательскую программу в качестве заголовочных файлов[109]. Для примера в Листинге 8.1 приведена начальная часть файла p16f84a.inc[110].
Листинг 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[111] используется для вставки в программу имен регистров специального назначения. Помимо того, что использование этой директивы освобождает программиста от необходимости набирать множество директив 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’[112].
Последний байт записи называется контрольной суммой. Контрольная сумма вычисляется как дополнительный код суммы всех предыдущих байтов записи, т. е. отрицательное значение суммы, игнорируя любые переполнения. Для контроля корректности передачи загрузчик складывает все принятые байты записи, включая байт контрольной суммы. Если при передаче не было ошибок, полученное число должно быть равно нулю.
Ассемблеры очень привередливы к синтаксису исходного кода. При наличии в исходном коде синтаксических ошибок[113] будет сгенерирован файл сообщений об ошибках. Например, если бы при наборе 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[114], которая реализует задержку длительностью 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е (переход, если не равно нулю)[115]. Таким образом, команда 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
В этом файле использованы три директивы[116].
∙ 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).
Предположим, что над этой задачей работает три коллектива программистов[117]. Задачи были распределены между ними руководителем проекта (четвертым человеком?) следующим образом:
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.
Для этого необходимо выбрать в меню Project (четвертый пункт слева на Рис. 8.7) команду Make Project. При обнаружении синтаксических ошибок на экране появится окно ошибок со списком. Двойной щелчок на любой ошибке вызовет переход к соответствующему окну с исходным кодом и установке курсора на строку, в которой эта ошибка была обнаружена.
Рис. 8.7. Снимок экрана при работе ИСР MPLAB версий 6.x во время симуляции проекта, приведенного на Рис. 8.6
После успешного создания программы можно выполнить ее симуляцию. При этом ПК моделирует поведение микроконтроллера PIC, т. е. выполнение его команд и функционирование периферийных модулей. Пользователь может в любой момент сбросить симулируемый микроконтроллер, установить точки останова, выполнять программу в пошаговом или нормальном режиме. Во время симуляции можно наблюдать за содержимым заданных регистров или даже всей памяти данных. Разумеется, скорость выполнения программы при симуляции будет на несколько порядков ниже, чем при использовании реального микроконтроллера.
Симуляцию можно запустить из меню Debugger. Пункты этого меню вынесены на отдельную панель инструментов Debugger (справа вверху на Рис. 8.7). В режиме симуляции оператор может:
• Сбросить виртуальный процессор, нажав на кнопку .
• Запустить симуляцию на максимальной скорости и приостановить
ее.
• Автоматически выполнять программу со скоростью несколько шагов в секунду.
• Выполнять программу пошагово в трех различных режимах (по одной строке при каждом щелчке по соответствующей кнопке):
— Шаг с заходом — проходит по всей программе, включая подпрограммы.
— Шаг без захода — обходятся подпрограммы (они выполняются с максимальной скоростью).
— Шаг с выходом — код подпрограммы выполняется за один шаг.
На Рис. 8.7 показан конечный результат симуляции нашей программы вычисления среднеквадратичного значения. Как и все три окна с исходными файлами, окно просмотра переменных (Watch) было открыто с помощью меню View. В это окно оператор может добавлять любые именованные регистры общего назначения, такие как 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) на нулевое значение.
Напишите соответствующие макроопределения. Как вы думаете, почему такой подход является не слишком хорошей идеей?
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОК