Калькулятор ZX Spectrum
Наткнулся на описание того как работает библиотека калькулятора в ZX Spectrum (как и многие книжки той эпохи она просто кишит опечатками и ошибками). Текста там много, так что тут вкратце опишу как оно всё работало, ибо это действительно забавно.
Итак, в ПЗУ ZX Spectrum был прошит интерпретатор Basic и как любой приличный бейсик он мог работать с числами с плавающей точкой.
Центральный 8–битный процессор Z80 не обладал поддержкой вещественных чисел, поэтому солидный блок ПЗУ содержал программную эмуляцию работы с ними.
Сами вещественные числа в ZX были 5–байтными. В первом байте содержалась увеличенная на 128 экспонента, а в следующих 4–х байтах — мантисса.
Но вот вызов процедур по работе с вещественными числами был оформлен довольно необычно (имхо).
Точкой входа в процедуры являлась инструкция процессора RST 28, то есть однобайтовое сокращение CALL $0028. Однако данная точка входа не ожидала от вызывающего кода передачи параметров в регистрах, как это часто бывает, совсем даже нет. Вместо этого она начинала анализировать последовательность байт по адресу на вершине стека (то есть с адреса куда надо возвращаться из процедуры) и интерпретировать их как однобайтовые коды команд стекового калькулятора!
Стек калькулятора находится в конце занятой программой и переменными бейсика памяти и растёт не сверху–вниз, а снизу–вверх, по сути — навстречу классическому стеку процессора. Стек калькулятора содержит 5–байтные вещественные числа, которые, правда, еще могут восприниматься строки (тогда в двух байтах хранится адрес начала строки, а еще в двух — её длина) или целые числа (если вкратце, то тогда экспонента равняется нулю, а само число хранится в двух средних байтах). Кроме того логические операции трактуют числовой 0 как FALSE, а всё кроме нуля — как TRUE.
В полном соответствии со стековыми вычислителями команды калькулятора могут помещать или убирать число на вершину стека, дублировать вершину, совершать с двумя числами на вершине арифметическую операцию замещая их одним числом результата и т.д., и т.п.
Полную таблицу кодов команд калькулятора можно посмотреть тут
Я опишу немного самых актуальных вещей (код команды дан в шестнадцатеричном виде):
После встречи инструкции «38» процедура обработки калькулятора возвращала управление машинному коду располагающемуся сразу за байтом с этой инструкцией. То есть получалось, что машинный код как бы перемежался с интерпретируемыми инструкциями калькулятора, при этом при возврате из калькулятора в регистре HL сохранялся адрес первого байта числа на вершине стека.
Инструкции калькулятора покрывают всё множество функций бейсика и кроме того содержат множество вспомогательных вещей для промежуточных вычислений разнообразных математических функций.
Рассмотрим следующий ассемблерный код на ZX Spectrum:
Эта последовательность инструкций приведёт к тому, что в HL окажется указатель на вершину стека с вещественным числом 1.5.
Но обратите внимание, что в этих инструкциях есть даже условные переходы, то есть на данных кодах калькулятора можно реализовывать произвольные алгоритмы, а не только считать синусы с косинусами. Причём из калькуляторного кода можно вызывать машинный код функцией USR бейсика.
Эта естественность с которой инструкции калькулятору можно впрячь в машинный код невольно подталкивает к очевидной мысли — теоретически можно сделать математический сопроцессор для ZX Spectrum реализующий эти команды в железе! Более того — существующий код того же встроенного интерпретатора Basic сможет получить буст в скорости совершенно без внесения каких либо изменений!
Это не только забавно, но и мысль сия действительно приходила в голову людям и ранее, я на такие вещи натыкался в интернете, но чем они закончились и вообще закончились ли сейчас не в курсе.
Итак, в ПЗУ 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 комментариев
+ еще загрузить корректный указатель стека кроме HL
как-то не особо «просто» уже всё это
Но вообще надо аккуратно исследовать вопрос — ведь калькулятор это в сути своей пресолиднейший кусок ROM, т.к. он и есть все функции бейсика включая USR которая должна обратно вывалиться в режим процессора.