Изучаю Nibbler
Пока занимаюсь другими делами, параллельно немного ковыряю архитектуру Nibbler’а. Хотелось бы перенести на него какой-нибудь однобитный движок, покруче обычной одноканальной пищалки, которая есть в примерах. Как оказалось, это крайне нетривиальная задача.
Дело в том, что у Nibbler’а нет не только стека, но и косвенной адресации. Совсем, полностью. Первое означает невозможность подпрограмм с возвратом. Второе означает невозможность доступа к данным по указателю. Ни к каким, ни в ПЗУ, ни в ОЗУ.
Это особенность именно Nibbler’а, значительно упрощающая его устройство, и она лежит в основе схемы: как-нибудь прикрутить эти возможности нельзя. В промышленных 4-битных процессорах типа SM510 и TMS1000 рудиментарные реализации стека (один вызов) и косвенной адресации (регистр-указатель) есть.
Подпрограммы на нормальных процессорах обычно реализуют через механизм call/return: call запоминает текущий адрес исполнения на стеке и переходит на новый адрес, а return забирает адрес со стека и переходит туда. Глубина вложения определяется размером стека. Например, у 6502 это может быть аж 128 уровней, хотя в реальных программах это всего один-два десятка.
У ранних процессоров нет стека, а только регистр возврата: call запоминает один-единственный адрес в этом регистре, а return совершает обратный переход. Если нужна вложенность вызовов, нужно изобретать собственную альтернативу стеку. Это может быть пересохранение адреса возврата в фиксированные для каждой подпрограммы ячейки ОЗУ (без возможности самовызова), или реальный многоуровневый стек. Но он требует косвенной адресации.
В Nibbler’е механизма вызова подпрограмм просто нет. Их реализуют jump-переходом. Перед вызовом в ячейках ОЗУ сохраняют номер (не адрес, это очень важно!) возврата. Для возврата выполняется диспетчер выхода: если индекс такой-то, jump на метку. У каждой точки возврата есть своя метка, и в диспетчере вручную, длинной колбасой сравнений, выясняется, на какую метку нужно перейти. Потому что команды перехода по указателю, то есть косвенной адресации, в архитектуре нет.
Также в Nibbler’е нет указателей в принципе. Хуже того, он имеет Гарвадскую архитектуру: код в одной памяти, ОЗУ в другой, в раздельных пространствах. Единственный способ загрузки данных из памяти кода (ПЗУ) — непосредственная загрузка, типа lit #N (4-битное значение в аккумулятор). Для памяти есть только ld ADDR и st ADDR, загрузка и сохранение аккумулятора в абсолютный адрес ОЗУ. Никакой альтернативы ld a,(hl) на Z80 нет.
Из этого следует одна вещь: писать для Nibbler’а очень сложно. Тем удивительнее, что ИИ без проблем справился и написал рабочий вариант динозаврика. Но это всё-таки достаточно простая игра.
Для целей же проигрывателя музыки нужно иметь доступ к данным музыки. Обычно через указатель: он указывает на блок данных одной ноты, оттуда считываются данные, нота звучит, далее указатель продвигается вперёд, считываются данные следующей ноты. Логика обычного индексированного массива. На Nibbler’е она недоступна.
Эту проблему можно решить следующим образом: вместо массива данных нот сделать код с кучей lit #N : st ADDR, который установит параметры следующей ноты, и сделает вызов подпрограммы проигрывания ноты. Но это порождает две новые проблемы.
Во-первых, код загрузки данных для одной ноты будет занимать в десятки раз больше памяти, чем обычный массив, а у Nibbler’а всего 4096 байт ПЗУ (именно байт, он читает команды байтами: 4-битная команда и 4-битный операнд).
Во-вторых, а как мы будем возвращаться из подпрограммы, если нет ни стека, ни регистра возврата, ни косвенной адресации? Только диспетчером: перед вызовом передаём через ОЗУ номер текущей ноты, а на выходе проверяем номер ноты и идём на адрес возврата. Допустим, у нас 512 нот (если бы столько влезло в ПЗУ). Значит, 512 адресов возврата, 512 меток, и switch/case из 512 значений. Который сам не влезет в память.
В таких дичайших ограничениях есть два варианта. Или смириться и не делать ничего сложного, что и было проделано автором Nibbler’а 13 лет назад, который показал примерно максимум возможного в своих примерах. Или же дорабатывать железо и архитектуру. Как минимум, расширением ПЗУ, чтобы влезло упомянутое выше огромное количества кода, либо ещё одним пространством памяти с аппаратным индексным регистром. Про это напишу в другой раз.
рабочее
nibbler
кряхтение