История развития программного обеспечения

Язык программирования PL/M-80 и его потомки

PL/M-80 – процедурный машинно-ориентированный язык программирования для систем на базе микропроцессоров Intel 8080 и 8085, а также совместимого с ними снизу вверх Z -80 фирмы Zilog. Аббревиатура расшифровывается как Programming Language for Microcomputers (язык программирования для микрокомпьютеров), а 80 – указывает тип микропроцессора, для которого реализована та или иная версия языка. PL/M был разработан в 1972 г. фирмой МАА (Microcomputers Applications Associates) по заказу корпорации Intel. Первый компилятор для этого языка написал Гарри Килдол (Gary Kildall)1, будущий автор операционной системы СР/М-80.

Компиляторы с PL/M были сделаны для следующих микропроцессоров: Intel 4004, 8008, 8080/85, 8051, 80196, 8086/8088, 80188/80186, 286 и 386. Мы видим, что номер в названии языка соответствует типу микропроцессора, для которого он реализован. Соответственно PL/M для процессора 8051 называется PL/M-51. Для каждого микропроцессора язык имеет свои, иногда весьма существенные, особенности.

Часто утверждается, что он основан на усечённом языке PL/1, но это совершенно не так, поскольку он заимствовал черты нескольких языков, Алгола, XPL и в том числе PL/1. Язык PL/M-80 исходно разрабатывался фирмой MAA (позднее она стала Digital Research ) в 1972 г. заказу корпорации Intel как самостоятельный язык [1], ориентированный на архитектуру микропроцессора i8080, и при более детальном знакомстве с ним вы увидите, что схожесть с PL/1 создаёт только ключевое слово DCL, служащее для объявления переменных. Язык процедурный, со строгой типизацией данных, имеет блочную структуру и правила видимости имён. Достоинство языка – его компактность. Интеловский компилятор с PL/M-80 создавал при трансляции очень компактный объектный код с коэффициентом расширения 6, то есть один оператор языка заменялся при трансляции в среднем шестью ассемблерными командами. Аналогично, объ ё мный коэффициент объектного кода по сравнению с эквивалентным ассемблерным лежал в пределах 1,2—1,3. То есть полученный после трансляции объектный код занимал в 1,2 раза больше памяти, нежели код программы, написанной на ассемблере i8080.

Первоначально интеловский транслятор с PL/M -80 работал под операционной системой ISIS-II на системах разработки Intellec-800, Intellec Series II, Intellec Series III и замечательной двухпроцессорной портативной системе iPDS. Затем компиляторы для него появились под ОС CP/M-80, а с появлением PC – появились системы поддержки разработок на PL/M и на них, в частности, в виде эмуляторов CP/M-80 на PC. Компиляторы с PL/M были сделаны для следующих микропроцессоров: Intel 4004, 8008, 8080, 8051, 80196, 8086/8088, 80188/80186, 286 и 386. Режим программирования для микропроцессора i486 поддерживался в PL/M -386. В настоящее время Intel этот язык не поддерживает, однако существуют продукты других фирм, в том числе несколько конвертеров исходных текстов программ на PL/M в Си.  

Ниже дан краткий справочник по языку PL/M-80, а затем приведены различия между PL/M-86 и PL/M-80, PL/M-286 и PL/M-86, между PL/M-386 и PL/M-286. Подробное описание языка PL/M-51 вы можете найти в помещённом в Музее авторском курсе А.В. Микушина по микропроцессорам.

К раткий справочник по языку PL/M-80

Итак, программа на PL/M-80 состоит из предложений. Каждое предложение заканчивается знаком “;” (точка с запятой). Общий вид программы:

имя_программы: DO;
обявления и определения
;
[исполняемые предложения;]
END имя_программы;    

1. Алфавит PL/M-80

Буквы : A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

Числа: 0 1 2 3 4 5 6 7 8 9

Спецсимволы: $ = . / ( ) + - ' * , < > : ; и пробел  

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

Наименьшей значимой единицей языка в предложении PL/M является токен. Каждый токен принадлежит к одному из следующих классов: переменные, зарезервированные слова, простые разделители (это все специальные символы за исключением знака доллара), составные разделители (<>, <=, >=, :=, /*, */), числовые константы, строковые константы.

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

2. Идентификаторы

Идентификаторы задают имена переменных, процедур, констант и предложений. В последнем случае они именуются также метками. Идентификаторы могут быть длинной от 1 до 31 символа, причём первым символом должна быть буква, а остальные – б у квы, цифры и знак доллара, который добавляется для улучшения читабельности текста, но игнорируется компилятором. Так что идентификатор TEST$$$EXAMPLE и TESTEXAMPLE для него одинаковы. Кроме того, знак доллара не может быть первым в имени идентификатора. Идентификаторы должны отличаться от ключевых слов.

Переменные могут быть скалярами, массивами или структурами.

3. Зарезервированные (ключевые) слова

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

ADDRESS
AND
BASED
BY
BYTE
CALL
CASE
DATA
DECLARE
DISABLE
DO
ELSE
ENABLE
END
EOF
GOTO
GO
HALT
IF
INITIAL
INTERRUPT
LABEL
LITERALLY
MINUS
MOD
NOT
PLUS
PROCEDURE
RETURN
OR
THEN
TO
WHILE
XOR

4. Типы данных

В языке всего два типа данных. Тип данных BYTE ссылается на 8-битовые данные, а тип ADDRESS – на 16-битовые. Переменная типа BYTE занимает соответственно один байт памяти и принимает беззнаковые целые значения от 0 до 255, а переменная типа ADDRESS занимает два байта памяти и принимает беззнаковые целые значения от 0 до 65535 (0FFFFh). Можно создавать массивы и указатели любого из этих типов.  

4.1. Константы

Константа – это значение, которое не изменяется в ходе исполнения программы. Они бывают числовыми и строковыми. Числовые константы в PL/M-80 целочисленные, 8- и 16-битовые. Они могут быть записаны как двоичные (суфикс B), восьмеричные (суфиксы O и Q), десятичные (D) и шестнадцатеричные (H) числа. Для обозначения системы счисления используется соответствующий суфикс числа, если он опущен, то число считается десятичным.

Шестнадцатеричные константы длжны начинаться с цифры, чтобы их не путать с переменными, для этого впереди её пишется ноль, например 0Ah, и заканчиваться суффиксом h. Это нужно делать обязательно, так как число типа 09D без суффикса h будет воспринято как десятичное. Как и с идентификаторами, знак доллара может встраиваться в числовую константу. Это иногда удобно для записи двоичных чисел. Строковые константы – последовательность символов, заключённых в кавычки (апострофы). Разумеется, чтобы включить в состав строки саму кавычку, её следует при записи удвоить. Строки символов представлены 7-битным кодом ASCII. Сторок длинной в 1 символ рассматривается как значение типа BYTE; длинной 2 – как ADDRESS. Строки длиннее двух символов могут использоваться только с помощью dot-оператора. (На самом деле он представляет собой обычный указатель :). Максимальная длина строки – 255 знаков.

5. Выражения и структуры

5.1 Арифметические операторы

В PL/M – семь арифметических операторов, которые могут выполнять действия над данными типа BYTE или ADDRESS. Таким образом, арифметика в языке только целочисленная, поскольку в системных программах для этого процессора плавующая арифметика не требуется. Однако она может быть реализована с помощью специальной библиотеки.

Список операторов:

+ двоичное сложение.
- двоичное вычитание или унарная операция отрицания.
PLUS двоичное сложение с учётом переноса.
MINUS двоичное вычитание с учётом переноса.
* двоичное умножение.
/ двоичное деление.
MOD остаток от двоичного деления.  

Операции умножения и деления всегда дают результат типа ADDRESS. Другие операции дают в результат типа BYTE, если оба операнда имеют тип BYTE, и тип ADDRESS, в других случаях.  

5.2 Логические операторы

В PL/M реализовано четыре логических оператора, выполняющих булевские операции над 8- или 16-битовыми величинами.  

NOT отрицание (дополнение).
AND двоичная (поразрядная) конъюнкция (умножение).
OR двоичная (поразрядная) дизъюнкция (сложение).
XOR исключающее ИЛИ.  

Результат операции имеет тип BYTE, если оба операнда типа BYTE. Во всех других случаях – результат имеет тип ADDRESS, причём, если необходимо, то старшие разряды результата дополняются нулями.  

5.3 Операторы отношений

В языке имеется шесть операторов отношений. Все они возвращают результат 0FFH – представляющий "true" или 00H – означающий "false".

< двоичный оператор “меньше чем”.
<= двоичный оператор “меньше чем или равно”.
= двоичный оператор “ равно”.
>= двоичный оператор “больше чем или равно”.
> двоичный оператор “больше чем ”.
<> двоичный оператор “не равно”.    

5.4. dot-оператор

В PL/M имеется оператор, именуемый dot-оператором (dot — точка). Это унарный оператор, возвращающий адрес памяти, по которому расположен его операнд. Операндом может быть константа, переменная, строка (байтовый массив) и т.д.

Формат:
.variable$name
.constant
.(constant)
.(constant, ...)

Последняя форма записи широко используется для передачи подпрограмме адреса сообщения, например:

.('Message', 0DH, 0AH, 0 H )  

5.5. Старшинство операций

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

*
/
MOD
+
-
PLUS
MINUS
<
<=
=
=>
>
<>
NOT
AND
OR     XOR

5.6. Комментарии

В PL/M комментарий представляет собой последовательность символов ограниченных слева разделителем /*, а справа – парными ему знаками */. Комментарий может появиться в тексте программы везде, где может появиться пробел.  

/* текст комментария игнорируется компилятором */  

5.7. Исполняемые предложения

Присваивание.

variable = expression;

или

variable, variable, ... = expression;

Встроенное присваивание может использоваться внутри выражения. (Правда, я ни разу не встречал в текстах, чтобы кто-то им пользовался :).

(variable := expression)

Пример из [3]:

ALT + (CORR := TCORR + PCORR) - (ELEV := HT/SCALE);
даст точно такой же результат как и
ALT + (TCORR + PCORR) - (HT/SCALE);

Но при этом двум переменным будут попутно присвоены промежуточные значения.  

Простой блок DO-END.

DO;
statement; ...;
END;

Блоки могут быть вложенными. Глубина уровней вложенности определяется реализацией компилятора PL/M.

С блоками связаны границы видимости имён. 1. Имя, если оно не объявлено как PUBLIC, видимо только внутри блока, где оно объявлено. 2. Если внутри вложенного блока объявлено переменная с таким же именем, как у переменой во внешнем блоке, то действует имя внутренней переменной.

Цикл DO - WHILE.

Исполняется пока выражение истинно.

DO WHILE expression;
statement; ...;
END;  

Итеративный цикл DO.

DO variable = expression1 TO expression2 [BY expression3];
statement; ...;
END;

Например:
DO J=0 TO 8;
VECTOR(J) =0;
END;

Предложение DO - CASE.

Исполняется n-е предложение, где n – результат вычисления выражения, причём предложения нумеруются с нуля.

DO CASE expression;
statement0;
statement1;
...;
END;

Условный оператор имеет несколько форм:

IF - THEN.

IF expression THEN statement;

IF - THEN - ELSE.

IF expression THEN statement; ELSE statement;

Обратите внимание, что там, где указан один оператор, он может быть заменён на простой блок DO, например:

IF TMP>5 THEN
  DO
    INCR = INCR +3;
    COUNT = COUNT – INCR;
  END;
ELSE COUNT = 0;

Оператор безусловного перехода GO TO.

В PL/M GO TO и GOTO -- синонимы.

GO TO label;
или
GO TO number;
или
GO TO variable;

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

6. Объявление переменных

Переменные объявляются с помощью предложения DECLARE. Это неисполняемое предложение, которое связывает объект с его именем, статически распределяет память и позволяет присваивать переменным начальные значения.

Объявление одиночной (скалярной) переменной:

DECLARE identifier type;
DECLARE (identifier_list) type;

например,
DECLARE (OLD,NEW) BYTE;
DECLARE POINT ADDRESS, VAL BYTE;

Поскольку язык поддерживает модульное программирование, то программа может быть разбита на большое количество модулей, которые потом собираются вместе с помощью компоновщика. Соответственно, могут быть внешние переменные, т. е. объявленные в другом модуле и переменные, которые мы можем объявить доступными для других модулей. Делается это с помощью атрибутов EXTERNAL – для переменных, объявленных в других модулях, и PUBLIC – для переменных, которые мы делаем видимыми из других модулей.

DECLARE identifier type EXTERNAL;
DECLARE identifier type PUBLIC;

Ещё более интересная возможность – задать физический адрес для размещения переменной.
Делается это с помощью атрибута AT (address).

Например:
DECLARE io $ reg BYTE AT (0FFE0h);

Объявление группы переменных одного типа:
DECLARE (identifier, ...) type;

Объявление группы переменных разных типов:
DECLARE identifier type, identifier type, ...;

Переменная может быть базированной, т.е. её адрес определяется содержимым другой переменной (в Си это называется pointer). Для этого при её объявлении применяется атрибут BASED, например:

DECLARE MY $ PTR ADDRESS,
    A BASED MY$PTR BYTE;  

Объявление массива:
DECLARE identifier ( constant ) type;

Индексы массива начинаются с нуля. Поэтому объявление
DECLARE В(10) BYTE;
Объявляет массив с элементами В(0),...,B(9).

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

Например:

DECLARE В(5) BYTE INITIAL(1,2,3,4,5);

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

Атрибут DATA применяется для объявления констант, при этом не указывается их тип или размер массива, для массива подразумевается тип BYTE, а размер массива определяется неявно из значений, записанных в качестве аргумента этого атрибута.

Значения идентификаторов, объявленных с атрибутом DATA не должны изменяться во время исполнения программы.  

DECLARE HELLO DATA ('Hello, world.');  

Язык PL/M также поддерживает примитивные макросредства.2

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

DECLARE FOREVER LITERALLY 'WHILE TRUE';

DO FOREVER ;

Чтобы не писать длинное ключевое слово LITERALLY, первым делом обычно переопределяют само это слово, например:

DECLARE AS LITERALLY 'LITERALLY';
DECLARE CRLF AS '0Dh,0Ah';  

Метки объявляются с типом LABEL до их использования в программе. Это не всегда необходимо, потому что как только метка встретилась в программе перед оператором, компилятор сам делает такое неявное объявление:

DECLARE метка LABEL;

Однако, если она встретилась в предложении GOTO и не была ранее объявлена, то это компилятору не понравится.  

7. Объявление процедур

Процедура должна быть объявлена до того, как она используется.

Формат объявления:

имя_процедуры: PROCEDURE (arg _ list) [type] [атрибуты];
DECLARE объявления параметров;
DECLARE объявления переменных;
statement;
...;
END имя_процедуры;

Метка “имя_процедуры” задаёт имя процедуры, по которому затем осуществляется вызов процедуры. Если процедура не возвращает заначения (т.е. не является функцией), то type может быть опущен.

Выход из процедуры осуществляется после последнего её оператора. Он может происходить с возвратом результата или без него. Выход из процедуры может быть указан явно с помощью предложения RETURN.

Как и переменные процедуры могут быть объявлены с атрибутами EXTERNAL и PUBLIC. Процедура может быть объявлена реентерабельной (повторновходимой) с помощью атрибута REENTRANT.

RETURN;
/* выход из процедуры без возвращаемого значения */

RETURN expression;
/* выход из процедуры с возвращаемым значением */  

Вызов процедуры осуществляется с помощью предложения

CALL имя_процедуры(список_аргументов);
или  
имя_процедуры(список_аргументов);

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

В процессоре 8080, как известно, имеется 8 уровней прерываний, пронумерованных от 0 до 7 и команда RST n, вызывающая программное прерывание уровня n. Для того, чтобы написать процедуру обработки прерывания, необходимо после слова PROCEDURE задать ключевое слово INTERRUPT и уровень прерывания. Таким образом, заголовок процедуры для обработки прерывания 2-го уровня выглядит так:  

LEVEL3INT$HANDLER: PROCEDURE INTERRUPT 1;
DISABLE;    % запрет прерываний  
ENABLE;     % разрешение прерываний
END LEVEL3INT$HANDLER;  

При входе в обработчик прерывания запрещаются, а при выходе из процедуры прерывания снова разрешаются. Отмечу, что маскированием прерываний программист может управлять и сам – для прохождения критической секции используются операторы DISABLE и ENABLE .  

7.2. Встроенные процедуры и функции  

Функция  
INPUT (number) Возвращает значение типа BYTE считанное из порта ввода-вывода, номер которого задан аргументом number.
OUTPUT (number)=expression; Посылает значение выражения типа BYTE в порт, номер которого задан аргументом number.
LENGTH (array _ name) Возвращает число элементов в массиве, имя которого задано аргументом array _ name. Возвращаемое значение имеет тип ADDRESS.  
LAST (array _ name) Возвращает наибольший индекс массив, заданного аргументом array _ name. Возвращаемое значение имеет тип ADDRESS.  
Легко заметить, что LAST(array_name) = LENGTH(array_name) - 1.
LOW (expression) Возвращает младший байт выражения expression.
HIGH (expression) Возвращает старший байт выражения expression.
DOUBLE (expression) Возвращает значение типа ADDRESS , эквивалентное expression. Если выражение было типа BYTE, то старший байт заполняется нулями.    

Функции сдвигов.

Циклические сдвиги.
Сдвиг влево:

ROL (выр1, выр2)
Сдвиг вправо:
ROR (выр1, выр2)

Эти функции возвращают значение “выр1”, циклически сдвинутое вправо или влево на число битов, заданное "выр2". Оба выражения должны быть типа BYTE. Значение “выр2” не должно равняться нулю.  
Циклический сдвиг с флагом переноса:  
SCL (выр1, выр2) /*влево*/
SCR (выр1, выр2) /*вправо*/
Эти функции возвращают значение “выр1” циклически сдвинутое вправо или влево на число битов, заданное “выр2”. Флаг переноса участвует в сдвиге. “выр2” должно быть типа BYTE, а “выр1” может быть типа BYTE или типа ADDRESS. Тип результата совпадает с типом “выр1”. Значение “выр2” не должно равняться нулю.  
Логические сдвиги.
SHL (выр1, выр2) /*влево*/
SHR (выр1, выр2) /* вправо */
Эти функции возвращают значение “выр1” циклически сдвинутое вправо или влево на число битов, заданное “выр2”. Последний бит сдвигается через флаг переноса. “выр2” должно быть типа BYTE, а “выр1” может быть типа BYTE или типа ADDRESS. Тип результата совпадает с типом “выр1”. Значение “выр2” не должно равняться нулю.  
Задержка на заданное время.
CALL TIME (expression)
Выражение вычисляется как величина типа BYTE. Процедура TIME обеспечивает задержку на время равное 100 микросекунд умноженное на результат вычисления выражения. (Расчёт тайминга основан на времени исполнения команд на стандартном процессоре i 8080.)

7.3. Предопределённые переменные  

Переменные флагов:
CARRY, ZERO, SIGN, PARITY
Значения этих переменных соответствуют текущему значению соответствующих флагов состояния процессора 8080/8085: CF, ZF, SF и PF.
Переменная MEMORY. Эта переменная содержит первой ячейки памяти, следующей за PL/M-программой, т.е. адрес начала свободной памяти. Позволяет также вычислить длину программы.  
Переменная STACKPTR Содержит текущее значение регистра SP (указатель стека). Присвоение ей нового значения изменит содержимое этого регистра.  

8. Ввод-вывод

Исходно PL/M не имеет стандартных процедур ввода-вывода, как, например Паскаль, Си или Бейсик – просто в большинстве случаев они не нужны для системного ПО или очень просто реализуются через драйверы программного монитора микрокомпьютера. Наконец, ввод-вывод может быть реализован в виде внешней библиотеки процедур.    

Примечания

1.Почти детективную историю его жизни можно прочесть в книге Ю.Л. Полунова “От абака до компьютера: судьбы людей и машин”, – М.: из-во “Русская Редакция”, 2005.

2.Подробно они описаны в моей статье “Использование макроопределений в языке PL/M”  

Литература

  1. Р.Фаулджер. Программирование встроенных микропроцессоров. М .: Мир . 1985. – 275 с .
  2. Intel Corporation 1976. PL/M-80 Programming Manual . Doc # 98-268B.
  3. PL/M 386 Programmer's Guide, RadiSys Corporation, 07-0710-01, December 1999. – 422 pp.
  4. http://www.unix4fun.org/z80pack/doc_cpm_plm.html.
  5. PL/M-86 Programming Manual for 8080/85-Based Development Systems. Intel. 1980. Manual Order Number 4800466-03 Rev. C.

Следующая статья цикла

Статьи "Язык программирования PL/M-80 и его потомки"
Статья помещена в музей 06.06.2007 года.