MOVE.B ADC0, D0
где ADC0 представляет длинный абсолютный адрес (в нашем случае $80000), требует 28 тактов (3,5 мкс в нашем процессоре), в то время как команда
MOVE.B (A3), D0
использующая косвенную адресацию через A3, выполняется всего за 12 тактов. Эта разница обусловлена исключительно процессами на магистрали, где для пересылки каждого байта требуются (в МП 68008) 4 такта. В процессе выполнения первой команды ЦП извлекает из памяти двухбайтовый код операции, четырехбайтовое расширение (длинного) адреса и, наконец, запрошенный байт данных, т. е. всего 7 байт, на что расходуется 28 тактов. Вторая команда требует извлечения двухбайтового кода операции и запрошенного байта данных, т. е. всего 3 байт (12 тактов). Вообще системы с узкими шинами (вроде нашего МП 68008, у которого внутренняя 32-разрядная архитектура должна себя чувствовать как в смирительной рубашке, общаясь с внешним миром через 8-разрядную шину) особенно неэффективны в условиях интенсивных передач данных.
Наконец, началась программа! Первые 8 байт ПЗУ хранят важнейший стартовый вектор: указатель стека и входную точку программы. Входная точка находится в «истинном» ПЗУ (по адресу $40008), поэтому мы можем немедленно очистить бит BOOT, что приводит к замещению временного образа ПЗУ, используемого при начальной загрузке, оперативной памятью. Теперь мы можем загружать векторы прерываний в начало ОЗУ, в конкретные ячейки, определяемые архитектурой МП 68008 (вся область векторов приведена в табл. 11.5): $68 (INT2), $74 (INT5) и $7С (NMI = INT7). Мы использовали только INT5 (от 100 мкс — таймера в микросхеме параллельного порта); в этот вектор мы загружаем адрес нашего обработчика прерываний. В зависимости от конкретного состояния прибора (ожидание пуска или внешнего сигнала запуска, начало новой развертки, процесс развертки) обработчик прерываний должен выполнять различные функции; поэтому мы написали один грандиозный обработчик со многими точками входа, соответствующими его функциям. На данном этапе мы еще не готовы принимать данные, поэтому в вектор INT5 мы загружаем входную точку idle__int (прерывание простоя). Очень полезно загрузить на всякий случай все неиспользуемые векторы прерываний адресом bad__int (ложное прерывание) (вдруг произойдет деление на нуль, ложное прерывание и т. д.); мы загружаем в них адрес программы, которая зажигает ЭЛД определенным образом (далее будет видно, каким именно).
Теперь наступает утомительный, но существенный этап инициализации портов. БИС периферийных устройств, как, например, 8536, обладают изумительной гибкостью, но за нее приходится платить тщательным планированием. Вы должны продумать, какие управляющие байты следует послать, в какие регистры и в каком порядке, чтобы получить требуемый результат. Для простых параллельных портов в процессе планирования следует выбрать направление, полярность, режим и прерывания, а для таймеров — основание счета, каскадирование, режим запуска, прерывания и проч. В программе 11.3 приведен полный текст инициализации параллельного порта/таймера. Разрешаются параллельные порты А, В и С, причем биты 4–6 порта В назначаются выходными, а остальные - входными (см. рис. 11.15). Таймер-0 настраивается на деление его тактовой частоты 4 МГц на 400 и на непрерывный перезапуск с генерацией прерывания (по INT5) каждые 100 мкс. Заметьте, что все установочные входы мы сделали инверсными, поэтому при замыкании контакта (на который изначально подано +5 В) на землю с него считывается 1, а не 0. На входе, к которому подключена кнопка СТОП, мы использовали опцию «запоминания 1», так что мгновенное нажатие фиксируется, а отрабатывается оно только в конце развертки.
Наконец, мы очищаем массивы в ОЗУ (отметьте использование подпрограммы), инициализируем регистры, разрешаем прерывания и переходим на выполнение «главного» цикла.
Главная программа: главный цикл. Завершив инициализацию, мы входим в бесконечный главный цикл main__loop. Фактически он состоит из двух циклов: цикла ожидания нажатия кнопки ПУСК и цикла непрерывного обновления памяти изображения, на фоне которого осуществляется сбор данных в режиме прерываний. Программа обработки прерываний, завершив последнюю развертку, устанавливает программный «флаг останова» stop__flag, который непрерывно проверяется вторым главным циклом. Обнаружив установленный флаг, главная программа возвращается в первый цикл ожидания нового пуска. Давайте сопоставим структурную схему и программные строки.
Главный цикл (рис. 11.19) начинается с установки на ЭЛД состояния «ожидание». Затем программа ждет нажатия кнопки ПУСК, т. е. ее перехода из разомкнутого в замкнутое состояние. Это сложнее, чем кажется, потому что кнопка не содержит цепей подавления дребезга, в результате чего вы имеете несколько десятков близко расположенных перепадов между уровнями «замкнуто» и «разомкнуто», возникающих на протяжении, возможно, 25 мс. Этого времени может хватить на завершение самого короткого цикла измерений (если вы выбрали 1 развертку и интервал дискретизации 100 мкс), после чего измерения будут ошибочно продолжены, поскольку контакт кнопки все еще колеблется между состояниями «разомкнуто» и «замкнуто». Поэтому мы написали простенькую программу подавления дребезга, которая фиксирует, что кнопка была непрерывно разомкнута в течение приблизительно 50 мс (тем временем многократно выполняется подпрограмма обновления update), а затем переходит в состояние «замкнуто». Наконец мы получили приказ на выступление!
Программа сбрасывает выходной сигнал КОНЕЦ, считывает состояние управляющей панели и использует соответствующим образом полученные значения (устанавливая программные флаги типа auto__loop и параметры вроде dwell__per__bin и num__sweeps). Обратите внимание на использование таблицы decode__tbl (и косвенной адресации с индексацией) для получения значений, соответствующих положениям переключателей.
Далее программа очищает массивы DATA и NORM, инициализирует некоторые регистры (адресов и данных) и сбрасывает флаг останова. Последний шаг заключается в изменении содержимого вектора INT5 (который пока указывает на метку idle__int в обработчике прерываний) на адрес wait__trig или sweep__start в зависимости от того, какой режим установлен на управляющей панели: внешнего запуска или автозапуска.
Наконец, главная программа входит в «рабочий» цикл, в котором многократно выполняются два действия: вызов подпрограммы update (обновления массива DISPLAY в соответствии с содержимым массива DATA) и проверка флага останова stop__flag. На фоне этого унылого цикла прерывания тайком выполняют все то, ради чего был сделан наш прибор.
Главная программа: подпрограммы. Перед тем как взяться за наиболее сложную программу нашего комплекса - обработчик прерываний, рассмотрим две подпрограммы, вызываемые главной программой (рис. 11.20).
Рис. 11.20. Структурные схемы подпрограмм.
Подпрограмма clear__arrays заполняет нулями оба массива DATA и NORM; массив DISPLAY очищать нет необходимости, потому что программа update сразу же скопирует нули из DATA в DISPLAY. Эта программа обновляет за раз одно значение из массива DISPLAY, используя для этого текущие параметры изображения с управляющей панели и входные данные из массивов DATA и NORM; она также обновляет состояние порта ЭЛД, копируя байт памяти led__store.
Рассмотрим сначала простую подпрограмму clear__arrays из программы 11.3. Регистры А0 и А1 используются, как указатели двух массивов, и все 32 разряда D0 заполняются нулями. Счетчик D1 инициализируется величиной, равной размеру массива минус один; сейчас станет понятно, зачем это нужно. В цикле слово или длинное слово нулей пересылается в массивы с помощью косвенной адресации (с постинкрементом); вспомните, что постинкрементная адресация — штука интеллигентная, она инкрементирует адресный регистр правильным образом, прибавляя в нашем случае 2 в операции со словом и 4 в операции с длинным словом. Команда DBF заслуживает особого объяснения. Она представляет собой один из вариантов команды DBcc, для которого код условия ее = «ложь» (False). Любая команда (в общем виде) DBcc Dn, метка фактически проверяет два условия.