Калькулятор ZX Spectrum

Наткнулся на описание того как работает библиотека калькулятора в ZX Spectrum (как и многие книжки той эпохи она просто кишит опечатками и ошибками). Текста там много, так что тут вкратце опишу как оно всё работало, ибо это действительно забавно.

Итак, в ПЗУ ZX Spectrum был прошит интерпретатор Basic и как любой приличный бейсик он мог работать с числами с плавающей точкой.
Центральный 8–битный процессор Z80 не обладал поддержкой вещественных чисел, поэтому солидный блок ПЗУ содержал программную эмуляцию работы с ними.
Сами вещественные числа в ZX были 5–байтными. В первом байте содержалась увеличенная на 128 экспонента, а в следующих 4–х байтах — мантисса.
Но вот вызов процедур по работе с вещественными числами был оформлен довольно необычно (имхо).
Точкой входа в процедуры являлась инструкция процессора RST 28, то есть однобайтовое сокращение CALL $0028. Однако данная точка входа не ожидала от вызывающего кода передачи параметров в регистрах, как это часто бывает, совсем даже нет. Вместо этого она начинала анализировать последовательность байт по адресу на вершине стека (то есть с адреса куда надо возвращаться из процедуры) и интерпретировать их как однобайтовые коды команд стекового калькулятора!
Стек калькулятора находится в конце занятой программой и переменными бейсика памяти и растёт не сверху–вниз, а снизу–вверх, по сути — навстречу классическому стеку процессора. Стек калькулятора содержит 5–байтные вещественные числа, которые, правда, еще могут восприниматься строки (тогда в двух байтах хранится адрес начала строки, а еще в двух — её длина) или целые числа (если вкратце, то тогда экспонента равняется нулю, а само число хранится в двух средних байтах). Кроме того логические операции трактуют числовой 0 как FALSE, а всё кроме нуля — как TRUE.
В полном соответствии со стековыми вычислителями команды калькулятора могут помещать или убирать число на вершину стека, дублировать вершину, совершать с двумя числами на вершине арифметическую операцию замещая их одним числом результата и т.д., и т.п.
Полную таблицу кодов команд калькулятора можно посмотреть тут
Я опишу немного самых актуальных вещей (код команды дан в шестнадцатеричном виде):

01 - обмен двух чисел на вершине стека
02 - отбрасывание вершины стека
31 - дублирование вершины стека
...
0F - сложение двух числе на вершине (результат замещает их)
03 - вычитание
04 - умножение
05 - деление
06 - возведение в степень
1F - синус (результат замещает аргумент на вершине)
20 - косинус
21 - тангенс
...
17 - конкатенация двух строк на вершине
...
07 - X OR Y - логическое "ИЛИ"
...
A0 - втолкнуть на вершину 0
A1 - втолкнуть на вершину 1
A2 - втолкнуть на вершину 0.5
A3 - втолкнуть на вершину Pi/2
A4 - втолкнуть на вершину 10
...
34 - втолкнуть на вершину произвольное число
     (помещается в упакованной форме после кода)
...
C0-C5 - сохранить вершину стека во временную ячейку M0-M5
D0-D5 - втолкнуть на вершину временную ячейку M0-M5
...
2A - abs( x ) - функция бейсика модуля числа
2B - peek( x ) - функция бейсика peek
2D - usr( x ) - функция бейсика USR (вызов машинного кода)
...
3B - execute B - выполнить инструкцию калькулятора с кодом из регистра "B" 
     (берется при входе в калькулятор из одноимённого регистра ЦП)
...
33 - безусловный переход на байт со знаком (хранится после кода команды)
00 - условный переход (как 33) если число на вершине = TRUE
38 - возврат из калькулятора

После встречи инструкции «38» процедура обработки калькулятора возвращала управление машинному коду располагающемуся сразу за байтом с этой инструкцией. То есть получалось, что машинный код как бы перемежался с интерпретируемыми инструкциями калькулятора, при этом при возврате из калькулятора в регистре HL сохранялся адрес первого байта числа на вершине стека.
Инструкции калькулятора покрывают всё множество функций бейсика и кроме того содержат множество вспомогательных вещей для промежуточных вычислений разнообразных математических функций.
Рассмотрим следующий ассемблерный код на ZX Spectrum:

RST 28 ; входим в калькулятор
DB $A1 ; FPUSH 1
DB $A2 ; FPUSH 0.5
DB $0F ; FADD
DB $38 ; FEND

Эта последовательность инструкций приведёт к тому, что в HL окажется указатель на вершину стека с вещественным числом 1.5.
Но обратите внимание, что в этих инструкциях есть даже условные переходы, то есть на данных кодах калькулятора можно реализовывать произвольные алгоритмы, а не только считать синусы с косинусами. Причём из калькуляторного кода можно вызывать машинный код функцией USR бейсика.
Эта естественность с которой инструкции калькулятору можно впрячь в машинный код невольно подталкивает к очевидной мысли — теоретически можно сделать математический сопроцессор для ZX Spectrum реализующий эти команды в железе! Более того — существующий код того же встроенного интерпретатора Basic сможет получить буст в скорости совершенно без внесения каких либо изменений!
Это не только забавно, но и мысль сия действительно приходила в голову людям и ранее, я на такие вещи натыкался в интернете, но чем они закончились и вообще закончились ли сейчас не в курсе.

5 комментариев

avatar
Последнее достаточно забавно, и мне даже нравится. В качестве FPU можно использовать AYX-32 (про который тут когда-нибудь будет статья). Самое простое — код по $0028 поменять на вызовы команд по портам АУ.
  • tsl
  • +1
avatar
Это как? Внешний доступ к памяти возможен, к регистрам — нет, а ведь в них по итогу калькуляторных процедур определённые значения ожидаются. Разве что обманом заставить проц по im0 выполнить команды загрузок, да еще нужные значения им подсунуть. Твой аух сумеет разве такое?
avatar
Калькулятор держит стек в памяти как и ряд переменных в basic vars позволяющих определить где он заканчивается. Поэтому технически можно было бы обставить это дело таким образом, что код в RST 28 каким то образом запускает процесс калькулятора как нечто внешнее (накормив тем же адресом откуда надо выполнять инструкции), а по сигналу завершения берёт HL из известных переменных бейсика как он в общем то и делает скорее всего в оригинале.
avatar
дык, как и говорю, по im0 (либо пзу менять/подставлять озу)
+ еще загрузить корректный указатель стека кроме HL
как-то не особо «просто» уже всё это
avatar
Ну если не лезть внутрь ЦП, то это вполне реальный путь и он всё-равно будет теоретически намного быстрее обычного калькулятора, т.к. процессору останется исполнить ну десяток-два максимум инструкций.
Но вообще надо аккуратно исследовать вопрос — ведь калькулятор это в сути своей пресолиднейший кусок ROM, т.к. он и есть все функции бейсика включая USR которая должна обратно вывалиться в режим процессора.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.