Программирование МПС на языке ассемблер
Программирование микропроцессорных систем (МПС) на языке простого ассемблера не является процедурой принципиально сложной в силу того, что осуществляется практически на языке самого микропроцессора и не требует специфических знаний о соглашениях, глобальных и локальных переменных и других особенностях языка высокого уровня. С другой стороны, малая информационная емкость команд ассемблера и необходимость в полной технической информации о программируемой системе делает этот процесс в значительной мере трудоёмким.
Рассмотрим основные моменты практического программирования простроенной нами МПС на примере задачи по вводу в память системы внешнего импульсного сигнала амплитуды ТТЛ с последующим воспроизведением этого сигнала. Идея заключается в следующем (рис. 1.)
:Рис. 1.
Через равные промежутки времени будем опрашивать порт ввода, выделяя бит, по которому поступает внешний импульсный сигнал, и записывать полученные байты последовательно в доступный нам блок оперативного запоминающего устройства (ОЗУ) МПС. Частота опроса порта подбирается экспериментально исходя из соображений теоремы Котельникова
: минимальный интервал изменения сигнала должен быть опрошен программой хотя бы два раза для того чтобы это изменение было нами зафиксировано.Обратимся к техническим характеристикам нашей МПС:
Карта памяти была разделена нами посредством дешифратора на участки по 2048 байт (
07FFh байт или 2Кбайт) с целью применения микросхем ROM и RAM соответствующей емкости. С нулевого адреса (0000h) мы поместили микросхему ПЗУ (ROM BIOS), содержащую подпрограммы начальной инициализации и системные вызовы. В пространстве адресов ПЗУ находится вектор (адрес) для обслуживания единственного предусмотренного нами прерывания - 0038h, что соответствует программному вызову RST7 (код 0FFh или 1111.1111b). Предполагается, что это прерывание будет использовано в нашей системе для обслуживания матрицы клавиш 8х3 и регенерации семисегментного 8-разрядного индикатора, выполняющего функции алфавитно-цифрового дисплея. Диапазон адресов 0800h ÷ 37FFh отведен в МПС под ОЗУ, причем ячейки в интервале 0800h ÷ 08FFh (256 байт) будут выполнять роль системного ОЗУ, где мы разместим изменяемые вектора системных вызовов, буфер клавиатуры и экрана и служебные ячейки BIOS. Ячейки в интервале 3700h ÷ 37FFh (256 байт) отведем под стек, указав при старте вершину стека по адресу 37FFh (LXI SP, 37FFH). Диапазон адресов 3800h ÷ 3FFFh отведен нами для устройств ввода-вывода, подключаемых в данной конфигурации как ячейки памяти, и физически представляющих два параллельных 8-битных регистра, один из которых работает в этом диапазоне на вывод, а второй - на ввод. Пространство устройств ввода-вывода для схемотехнической простоты не было пока разбито на более мелкие интервалы, что позволяет обратиться к нашим портам и командами ввода вывода (к примеру OUT 38H и IN 38H).Таким образом, наша программа и вводимые данные должны располагаться в области пользовательского ОЗУ, занимающего интервал памяти
3800h ÷ 3FFFh, ячейки ОЗУ в диапазоне 4000h ÷ 0FFFFh системой не используется.Таблица 1. Карта памяти учебной МПС.
0FFFFh
|
Неиспользуемая область |
4000h |
3FFFh Устройства ввода-вывода 3800h |
3700h Область стека 37FFh |
36FFh
5 RAM - ОЗУ пользователя
1000h |
0FFFh 1 RAM - ОЗУ пользователя 0900h |
0800h Служебное ОЗУ 08FFh |
07FFh ROM BIOS 0000h |
Наряду с картой памяти нас также интересует информация о структуре портов ввода-вывода и о назначении их отдельных битов. Биты
3 ÷ 0 порта вывода, доступного для записи по адресу 3800h, обслуживают дешифратор типа "код-код", управляющий включением сегментов a, b, c, d, e, f, g индикатора. Биты 6 ÷ 4 этого же порта подключены ко входам дешифратора типа "код-позиция", управляющего подсветкой разрядов индикатора активным уровнем "0" на выходах и одновременно осуществляющего сканирование столбцов матрицы клавиатуры 8х3, "бегущим нулём". Бит 7 порта отвечает за последовательную передачу данных и подключен ко входу операционного усилителя, трансформирующего выходные сигналы амплитуды ТТЛ в двуполярные сигналы амплитудой ±5 В для согласования с внешними устройствами (к примеру - интерфейсом RS-232).Бит 7 порта, доступного для чтения по адресу 3800h подключен к выходу компаратора, осуществляющего ввод от внешних устройств последовательных сигналов как цифрового, так и аналогового характера, и будет использован нами для ввода цифровой последовательности. Разряды 3 ÷ 0 этого же порта подключены через резисторы 10 кОм к линии питания +5 В и осуществляют опрос столбцов матрицы клавиатуры 8х3, причем бит 3 ответственен за отслеживание нажатия клавиши "Shift". Эта клавиша будет осуществлять управление в нашей программе. Биты 6 ÷ 4 порта для информативного ввода не используются, но могут быть использованы в дальнейшем.
Задача поставлена. Она содержит работу с внешними устройствами в реальном времени, программирование периферии и портов ввода-вывода. Оформим рассматриваемую нами программу ввода данных в МПС в виде ассемблерной программы с элементами псевдокода. Строки псевдокода становятся комментариями при переводе алгоритма на язык ассемблера. Повторяющиеся участки кода оформим в виде подпрограмм.
;------------------------------------------------------------;
;
Ввод данных импульсной последовательности в МПС;
через бит 7 параллельного порта ввода 3800H;------------------------------------------------------------;
ORG 0900H; -
начало ОЗУ пользователяSTART: ;-------------
Цикл ввода ----------------------------;______
Начальная инициализацияCALL INIT;
загрузим регистровые парыMVI A,00H;
байт управления консолью: 0000.0000bSTA PORT;
включаем O, в позиции 0 и столбец 0CALL KEYB;
опросим клавиши на нажатие "Shift"MET1: LDA PORT;
читаем порт вводаANI 80H;
выделяем бит 7 (1000.0000b)MOV M,A;
заносим байт в ОЗУ по адресу [HL]INX H;
укажем следующую ячейку ОЗУLXI B,01FFH;
загрузим в [BC] длительность задержкиCALL LOOP;
выполним задержкуCALL DEND;
уменьшим счетчик длины и проверим конецJNZ MET1;
если не конец - повторим ввод байта;------------------
Цикл вывода ----------------------------;______
Начальная инициализацияCALL INIT;
загрузим регистровые парыCALL KEYB;
опросим клавиши на нажатие "Shift"MET2: MOV A,M;
читаем байт из ОЗУ по адресу [HL]ANI 80H;
выделяем бит 7 для симметрии выводаSTA PORT;
выводим байт в порт выводаINX H;
укажем следующую ячейку ОЗУLXI B,01FFH;
загрузим в [BC] длительность задержкиCALL LOOP;
выполним задержкуCALL DEND;
уменьшим счетчик длины и проверим конецJNZ MET2;
если не конец - повторим вывод байтаJMP START;
переход на цикл вводаINIT:
LXI Н,0B00H; укажем начало области ОЗУ для записиLXI D,2D00H;
укажем размер области ОЗУ для записиRET; (3800h-0B00H = 2D00H)
LOOP: DCR C;
уменьшим младший байт счетчика задержкиJNZ LOOP;
повтор, если не 0DCR B;
уменьшим старший байт счетчика задержкиJNZ LOOP;
повтор, если не 0RET;
возврат из задержкиDEND: DCR E;
уменьшим младший байт счетчика длиныRNZ;
возврат, если не 0DCR D;
уменьшим старший байт счетчика длиныRET;
возврат с признаком (Z или NZ)KEYB: LDA PORT;
читаем порт вводаANI 04H;
выделяем бит 3 (0000.1000b)JNZ KEYB;
повторяем, если бит 3 не = 0RET;
выходим - нажат "Shift";------ Внешние метки -------------------------------------
SYST:
EQU 0000H;___Холодный старт системы_______________PORT: EQU 380
0H;___Регистры ввода-вывода________________END;_____________ укажем транслятору конец программы
Естественно, что программа не сразу приобретёт законченный компактный вид. Процесс оптимизации включает в себя выделение повторяющихся участков в подпрограммы, если это удобно, и придание циклам ввода и вывода симметричного по времени характера, чтобы байты выводились точно в такие же моменты времени, как и вводились. В этом случае последовательность выводимых битов будет точно повторять введенную в память последовательность.
Оценить длительность задержки можно, просчитав по тактам цикл ввода, и, зная длительность такта
(500 нс при рабочей частоте процессора 2 МГц), определить суммарную задержку. Но существует менее трудоемкий путь: выполняя следующую программу,ORG 0900H; -
начало ОЗУ пользователяSTART: ;-------------
Оптимизация задержки -------------------------XRA A;
обнулим значение аккумулятораLXI B,01FFH;
загрузим в [BC] длительность задержкиLOOP: DCR C;
уменьшим младший байт счетчика задержкиJNZ LOOP;
повтор, если не 0DCR B;
уменьшим старший байт счетчика задержкиJNZ LOOP;
повтор, если не 0STA PORT;
выводим байт в порт выводаCMA;
инвертируем все биты аккумулятораJMP LOOP;
повторим задержку и выводнаблюдать осциллографом сигнал на выходе порта, и по длительностям импульсов определить оптимальное значение константы задержки.
Таким образом, мы формируем временн
ые интервалы программным путем. Существует альтернативное решение: использовать для задания временных интервалов подходящий внешний генератор и систему прерываний микропроцессора, где мы обслуживаем единственный вектор 0038h, который мы предполагали использовать для обслуживания индикатора и клавиатуры. В этом случае в системной ПЗУ мы должны иметь следующий код, устанавливающий стек системы и расставляющий начальные вектора прерываний.;------------------------------------------------------------;
;
Программа системной ПЗУ;------------------------------------------------------------;
ORG 0000H; -
начало системной ПЗУBEGIN: ;--------
Холодный старт системы -----------------------LXI SP,37FFH;
укажем стек системыJMP INIT;
переход к программе инициализацииORG 0008H;
другие RST0-6. . . ;
ORG 0038H;
адрес RST7, DI - автоматическиCONS: PUSH H;
сохраним [HL]LHLD VECT7;
загрузим в [HL] адрес обработки прерыванияPCHL;
переход на этот адрес;______
Начальная инициализацияINIT: MVI A,00H;
байт управления консолью: 0000.0000bSTA PORT;
включаем 0, в позиции 0 и столбец 0. . . ;
другие стартовые действия…LXI H,KEYB;
укажем в [HL] адрес обработки консолиSHLD VECT7;
сохраним его как изменяемый векторMVI A,00H;
зададим счетчик прерываний 256STA CNT;
запишем в ячейку счетчикаWAIT: EI;
разрешим прерыванияHLT;
остановим программу для ожидания прерыванияWAI1: ;
этот адрес будет запомнен, как возвратJPM WAIT;
ожидание следующего прерыванияKEYB: POP H;
восстановим [HL]LDA CNT;
читаем счетчик прерыванийDCR A;
уменьшаем егоSTA CNT;
сохраняем счетчикRNZ;
выход из прерывания (не 256-ое)MVI A,00H;
зададим счетчик прерываний 256STA CNT;
восстановим в ячейке счетчикаKEY0: LXI H,BUFF;
укажем на начало буфера строкиMOV A,M;
берем первый символANI 0FH;
выделим младший ниббл. . . ;
другие действия консолиRET;
завершение работы подпрограммыОсновная идея механизма прерываний заключается в том, что какое-либо устройство, требующее мгновенной реакции, выдает сигнал
INT, по которому процессор прекращает текущую программу и ждет по шине данных от одного до трёх байт инструкций прерывания, качестве которых удобно использовать RST0 ÷ RST7 или CALL ADDR. По этим инструкциям процессор запоминает в стеке адрес возврата на следующую ячейку и переходит к выполнению подпрограммы обработки прерывания. Команды RST0 ÷ RST7 удобны тем, что автоматически выполняют инструкцию DI - запрещение прерывания, в других случаях эта инструкция должна быть первой в подпрограмме обработки прерывания. Прерывания разрешаются далее командой EI, если критический участок подпрограммы обработки прерывания пройден и допустимо вложение прерываний, либо перед выходом из подпрограммы.В качестве источника прерывания используем простой внешний генератор по схеме мультивибратора на логических элементах с частотой генерации 12800 Гц (максимальная частота прерываний для нашей системы
~35000 Гц). При старте системы прерывания запрещены. Процессор устанавливает стек и уходит в программу инициализации, где выполняет настройку устройств ввода-вывода и записывает в служебное ОЗУ адреса переходов по прерываниям и счетчик прерываний. После этого прерывания разрешаются и процессор останавливается по HLT. Каждый следующий импульс генератора будет возбуждать прерывания и после каждого 256-го мы будем обслуживать индикатор и клавиатуру (т.е. 50 раз в секунду - что вполне достаточно).Переписав ячейку
VECT7 новым адресом, указывающим на другую подпрограмму мы можем осуществлять опрос порта с частотой 12800, 6400 Гц и т.д. Рассмотрим практическую реализацию такой программы.;------------------------------------------------------------;
;
Ввод данных импульсной последовательности в МПС;
через бит 7 параллельного порта ввода 3800H;
по обработке прерывания RST7 (адрес 0038H);------------------------------------------------------------;
ORG 0900H; -
начало ОЗУ пользователяSTART: ;-------------
Цикл ввода/вывода ----------------------DI;
запретим прерыванияXRA A;
очистим ASTA TRIG;
запишем в ячейку-переключательSWITCH: LDA TRIG
; прочитаем переключательCMA;
инвертируем (переключим)STA TRIG;
запишем в ячейку-переключательORA A;
если переключатель = 0JZ SW1;
укажем прерывание на чтение блокаLXI H, MET1;
укажем адрес прерывания на запись блокаJMP SW2;
перепишем векторSW1: LXI H, MET2;
укажем адрес прерывания на чтение блокаSW2: SHLD VECT7;
перепишем вектор;______
Начальная инициализация регистровCALL INIT;
загрузим регистровые парыMVI A,00H;
байт управления консолью: 0000.0000bSTA PORT;
включаем 0, в позиции 0 и столбец 0CALL KEYB;
опросим клавиши на нажатие "Shift"ST1: EI;
разрешим прерыванияHLT;
ждём прерываниеCALL DEND;
уменьшим счетчик длины и проверим конецJNZ ST1;
если не конец - повторим ввод байтаJMP SWITCH;
MET1: POP H;
восстановим [HL]LDA PORT;
читаем порт вводаANI 80H;
выделяем бит 7 (1000.0000b)MOV M,A;
заносим байт в ОЗУ по адресу [HL]INX H;
укажем следующую ячейку ОЗУRET;
выход из прерыванияMET2: POP H;
восстановим [HL]MOV A,M;
читаем байт из ОЗУ по адресу [HL]ANI 80H;
выделяем бит 7 для симметрии выводаSTA PORT;
выводим байт в порт выводаINX H;
укажем следующую ячейку ОЗУRET;
выход из прерыванияINIT:
LXI Н,0B00H; укажем начало области ОЗУ для записиLXI D,2D00H;
укажем размер области ОЗУ для записиRET; (3800h-0B00H = 2D00H)
DEND: DCR E;
уменьшим младший байт счетчика длиныRNZ;
возврат, если не 0DCR D;
уменьшим старший байт счетчика длиныRET;
возврат с признаком (Z или NZ)KEYB: LDA PORT;
читаем порт вводаANI 04H;
выделяем бит 3 (0000.1000b)JNZ KEYB;
повторяем, если бит 3 не = 0RET;
выходим - нажат "Shift"TRIG: DB 00H;
ячейка-переключатель;------ Внешние метки --------------------------------------
PORT: EQU 380
0H;___Регистры ввода-вывода_________________END;_____________ укажем транслятору конец программы
Набранный в программе-редакторе ассемблерный текст подлежит трансляции в объектный код и дальнейшей отладке. Рассмотрим процессы отладки и практического применения разработанной нами программы.