Эмулятор троичной ЭВМ «Я Сетунь 70» на Rust
Доводилось ли вам слышать о линейке советских троичных компьютеров «Сетунь»? А доводилось ли заглядывать в Политехнический музей, чтобы воочию увидеть эти уникальные машины? Нет? Или, может быть, да? В любом случае, сегодня у вас есть отличный шанс познакомиться с эмулятором «Сетуни 70», разобраться во всех её тонкостях и даже написать собственное программное обеспечение.
Введение
Существующие материалы о семействе троичных ЭВМ «Сетунь» часто грешат поверхностностью и неточностью, создавая лишь самое туманное представление об этих машинах без погружения в технические детали. И это выглядит довольно странно, ведь как в советскую эпоху, так и в наши дни разработчики, инженеры и пользователи «Сетуни» выпустили немало статей и около десятка монографий. Существовали даже специализированные учебные пособия по программированию и практическому применению этих ЭВМ — от научных расчетов и обработки данных до создания интерпретаторов и целых образовательных сред.
Встречаются поистине увлекательные материалы — например, об адаптации системы команд «Сетунь 70» под архитектуру ее предшественницы 1959 года выпуска. Представьте себе задачу: на машине, располагающей всего 81 словом оперативной памяти (каждое длиной по 18 разрядов), не имеющей ни ПЗУ, ни полноценной операционной системы, вам нужно реализовать эмулятор гораздо более сложной и развитой архитектуры, чей объем памяти почти в 20 раз больше. И ведь справлялись! Причем обходились без гигабайтных прошивок, громоздких сред разработки и хитроумных оптимизирующих компиляторов.
Краткая предыстория появления эмулятора
Разработка эмулятора «Сетуни 70» ведется уже давно: старт был положен еще в 2001 году. Проект поочередно переписывался на разные языки программирования, сталкиваясь с трудностями из-за нехватки знаний об архитектуре целевой машины, а также из-за ограничений и особенностей синтаксиса самих языков. Некоторые преграды удавалось преодолевать с легкостью, другие требовали серьезных усилий. К примеру, подавляющее большинство языков не поддерживает массивы с отрицательными индексами, из-за чего приходилось прибегать к искусственным смещениям и дополнительным проверкам. Это порождало баги, съедало львиную долю производительности и вынуждало постоянно заниматься ручным переводом адресов.
Зачем вообще понадобились отрицательные индексы? Дело в том, что в машине используется сбалансированная (симметричная) троичная система счисления, оперирующая тритами со значениями {-1, 0, +1}. Каждое понятие и параметр в этой архитектуре подчинены данной логике. Так, нумерация страниц памяти и ячеек внутри них охватывает диапазон от -13 до +13, а короткие ячейки распределены от -40 до +40. Значение в 6-тритном трайте (так называется слог 18-разрядного троичного слова) варьируется от -384 до +384. Диапазон же значений полноценного 18-разрядного троичного слова вы можете вычислить самостоятельно.
Эволюция реализации и список использованных языков выглядели следующим образом:
-
Java,
-
Pascal,
-
C,
-
C++,
-
Ассемблер,
-
Haskell,
-
Elm,
-
Idris,
-
снова C++,
-
и, наконец, Rust в тандеме с искусственным интеллектом.
Изначально ставилась амбициозная цель — уложиться в 2 000 строк кода, однако этот рубеж так ни разу и не был покорен. Выбор конкретного языка в разное время диктовался стремлением испытать его возможности в столь специфической сфере. Самыми удобными на практике неожиданно оказались Haskell и Elm, а самым лаконичным и наглядным традиционно выступил Ассемблер.
Конечно, не стоит думать, что все эти годы проект отнимал всё мое время. Отнюдь нет: параллельно велись другие дела, а к данной задаче я возвращался урывками, делая перерывы на месяцы и даже годы по мере накопления знаний об архитектуре.
Трудности эмуляции недвоичных машин
Помимо синтаксических нюансов, работы с индексами и производительности, существует коварная когнитивная ловушка, в которую попадают многие — «эффективность представления». Это специфическое искажение свойственно тем, кто воспринимает двоичную систему как нечто абсолютное и эталонное, пытаясь через неё выразить любые альтернативные концепции без малейшей попытки взглянуть на проблему шире.
Тот, кто углублялся в вопрос представления чисел в уравновешенной троичной системе, наверняка сталкивался с двумя базовыми подходами: «пара битов на один трит» и «знаковые слои» (что по сути сводится к тем же двум битам, но при ином структурировании). Рано или поздно к ним приходят все исследователи. В этот момент легко поддаться обманчивому выводу: «Но ведь это на 50% менее эффективно, чем прямая двоичная запись!» Кажется, что любые альтернативы заведомо проигрывают. Однако это верно лишь до тех пор, пока вы не начнете учитывать специфику отрицательных чисел (требующих дополнительных разрядов), потенциальную устойчивость к сбоям, особенности округления и прочие факторы.
Если же поразмыслить глубже, выясняется, что большее число битов на троичный разряд — это вовсе не недостаток, а логичное и неизбежное решение. Приведу несколько ключевых соображений, а итоговый вариант, удовлетворивший все требования, мы рассмотрим чуть ниже.
Подсказка 1. Фокусироваться следует не на том, как физически сохранить конкретный разряд, а на том, зачем именно вам это нужно. Анализ работы любых программ показывает, что они занимаются лишь тем, что считывают, модифицируют и записывают (перезаписывая или дополняя) числа из заранее очерченного диапазона. При этом вы никогда не сможете выйти за рамки диапазона и способа представления, заданных архитектурой конкретной машины.
Подсказка 2. Сколько бы раз вы ни складывали 2 и 2, в любой целочисленной системе вы неизменно получите 4. Каждый раз. Единственная реальная польза от беспрерывного повторения этой операции — шанс вовремя зафиксировать аппаратный сбой в памяти, процессоре или шине. Но стоит ли оно того?
Подсказка 3. Считав из памяти некий байт и выполнив над ним арифметическую операцию, вы можете получить результат, превышающий емкость 8 двоичных разрядов. Вернуть его на место одного исходного байта уже не получится: либо часть информации будет утрачена, либо придется выделять дополнительное пространство. Засунуть полтора байта в ячейку размером с один не удастся — она не обладает способностью динамически сжиматься или растягиваться.
Подсказка 4. Современное цифровое пространство насквозь двоично, поэтому в нашем случае неизбежно придется постоянно конвертировать двоичные данные в троичные и обратно.
Подсказка 5. Если вы наивно полагаете, что объявленная в коде переменная типа «байт» после компиляции так и останется одиночным байтом в памяти работающего приложения, вы заблуждаетесь: на практике она может развернуться в десятки, а то и сотни байт.
Подсказка 6. Любой конечный результат работы программы в конечном итоге предназначен для восприятия человеком — будь то в виде цифр или символов. Цель любого вычисления — быть прочитанным человеком, пусть даже этот финал скрывается за длинной цепочкой промежуточных преобразований. Об этом нельзя забывать.
Краткое описание архитектуры машины
ЭВМ «Сетунь 70» представляет собой изящную и предельно простую по своей концепции стековую машину, обладающую при этом весьма нестандартной архитектурой. По крайней мере, мне не доводилось встречать аналогичные принципы организации памяти.
Помимо того, что все данные здесь хранятся и обрабатываются в сбалансированном троичном коде, стоит выделить следующие ключевые особенности.
Память первого уровня
-
Как оперативная, так и постоянная память машины весьма скромны по объему — всего 27 страниц, каждая из которых содержит двадцать семь 18-разрядных слов. В сумме это дает лишь 729 слов.
-
Из этих 27 страниц под оперативную память отведено лишь девять. Остальное пространство отдано под ПЗУ, где хранятся диагностические тесты, служебные подпрограммы и реализация макрокоманд. Иными словами, программно мы можем изменять только 243 слова из 729.
-
Любопытно, что страницы ОЗУ имеют нумерацию от -4 до +4, то есть они «сконцентрированы» вокруг нулевой отметки, будучи сверху и снизу зажаты массивами ПЗУ.
-
Одна из страниц оперативной памяти полностью закреплена за аппаратным стеком процессора.
И это весь доступный объем. Не стоит впадать в отчаяние: воспринимайте это устройство скорее как аналог кэш-памяти в современных процессорах, коим по сути оно и является.
Исходная архитектура «Сетуни 70» уже закладывала возможность масштабирования адресного пространства без необходимости переписывать старый код. Набор регистров включает так называемые «регистры приписки», управляющие выборкой данных из страниц памяти в стек. С увеличением их разрядности пропорционально росло бы и количество доступных страниц.
Память второго уровня
В качестве дополнения к оперативной памяти к машине могли подключаться два магнитных барабана емкостью около 19,5 мегатрайт каждый (причем приставка «мега» здесь используется в своем честном десятичном значении). Именно это хранилище фактически выполняло роль привычной нам оперативной памяти. В системе даже предусмотрены специальные команды для блочного обмена целыми страницами первого уровня с магнитным барабаном — вполне стандартный подход для машин той эпохи.
Система прерываний
Архитектура включает механизм отслеживания исчерпания и пересохранения страниц памяти. При наступлении подобных событий текущий контекст фиксируется, после чего из ПЗУ вызывается соответствующая подпрограмма-обработчик. Причем в зависимости от режима задействуются разные алгоритмы. Например, если при линейном выполнении пользовательского кода программа доходит до границы страницы и пытается обратиться к несуществующему адресу, срабатывает прерывание и управление переходит к обработчику из ПЗУ. Аналогичным образом прерывание возникает при попытке вложенного вызова макроопераций.
Система команд
Командный набор включает в себя так называемый слог-ссылку и еще 81 команду. Двадцать семь из них являются макрокомандами, которые реализованы программно и хранятся в постоянной памяти.
Слог-ссылка
Эта операция отвечает за копирование содержимого слова либо его части (6-тритного слога) из памяти в стек данных. Прямой аналог современной инструкции Push.
Базовые команды
Пул из 27 инструкций для манипуляций с данными и стеком, включая арифметику, циклические сдвиги и пересылки.
Системные команды
Включают 27 инструкций для загрузки значений в управляющие регистры, взаимодействия с периферией и т.д.
Макрооперации (макрокоманды)
Также в количестве 27 штук. Их логика была прошита в сменных страницах ПЗУ и исполнялась процессором в специальном макрорежиме. По задумке конструкторов, заменяя физические платы с соответствующими страницами памяти, можно было на лету переконфигурировать машину под узкоспециализированные задачи.
Как же всё это функционировало?
Судя по всему, любая сколько-нибудь сложная программа брала на себя роль диспетчера, управляющего подгрузкой кода и данных с магнитного барабана, а также вызовом системных сервисов (да-да, операционная система под собственным именем там тоже присутствовала).
Тем не менее, на «Сетуни 70» работали весьма солидные программные комплексы: среды разработки, текстовые редакторы, ассемблеры, комплексы поддержки психофизиологических исследований, а также автоматизированная обучающая система «Наставник», которая гоняла студентов МГУ по иностранным языкам и программированию, принимая у них зачёты и экзамены. И всё это функционировало стабильно!
Разумеется, никто не предполагал написание прикладного софта в чистых машинных кодах с прямым доступом к «железу». Для этого задействовались интерпретаторы, системные команды ОС или компиляторы языков высокого уровня. Впрочем, для энтузиастов существовало сразу несколько вариантов ассемблера.
Периферийные устройства
Помимо магнитных барабанов, к «Сетуни 70» подключались устройства ввода-вывода на перфоленте (считыватель и перфоратор), а также электрифицированная пишущая машинка с программным управлением «Консул 254», заменяющая одновременно клавиатуру и принтер. Через специальный адаптер «Сетунь 72» можно было подключить до 27 выносных клавиатурных терминалов для приема экзаменов у студентов. В некоторых источниках также упоминается интеграция цветного телевизора для проведения психофизиологических опытов.
И, конечно же, нельзя не упомянуть главную консоль управления с массивом индикаторов и встроенными часами. С её помощью оператор мог в любой момент остановить выполнение любого алгоритма, скорректировать значения ячеек памяти и продолжить работу дальше.
Особенности реализации архитектуры «Сетуни 70»
Вернемся к нашему эмулятору. Программа разработана для окружения Ubuntu и воспроизводит вторую модификацию семейства — «Сетунь 70». Как уже отмечалось, она значительно сложнее, гибче и производительнее ранних версий.
Важно подчеркнуть, что под одним и тем же названием скрывалось два существенно различающихся варианта машины:
-
ранняя, базовая версия 1970 года (подарившая название всей линейке), оснащенная единственным стеком данных;
-
глубоко переработанная модификация примерно 1975 года выпуска, ориентированная на концепцию структурного программирования.
Вторая ревизия отличалась скорректированными режимами работы, появлением второго стека для хранения адресов возврата и переработанным набором инструкций. Всё это делало процесс написания кода значительно более комфортным.
Эту деталь необходимо держать в уме, поскольку ПО, написанное под одну архитектуру, скорее всего, откажется запускаться на другой (за исключением разве что примитивных изолированных программ, умещающихся в пределах одной страницы памяти).
Текущая версия моего эмулятора воспроизводит именно раннюю модификацию как более простую для понимания и реализации, однако в перспективе планируется поддержка и позднего варианта.
Библиотека «Аналемма»
Упомянутая ранее библиотека «Аналемма» — это то самое универсальное решение, позволившее обойти описанные выше архитектурные и производительные ограничения при работе с троичными данными.
Откуда такое название? Всё просто: при определенных условиях аналемма принимает дискретную и симметричную форму.
Этот инструмент позволяет хранить целые сбалансированные троичные числа в наиболее практичном виде, выполняя над ними арифметику и конвертации практически без потерь времени. На данный момент реализована поддержка 3-, 6-, 9- и 18-разрядных величин.
Суть подхода предельно прозрачна. Вместо того чтобы на лету вычислять, искать, выравнивать и преобразовывать битовые представления в массивах, мы поступаем следующим образом:
Для каждого числа заданной разрядности (например, 729 вариантов для 6 разрядов) создается универсальный дескриптор. Каждый такой объект содержит готовые наборы представлений, востребованных при отладке и тестировании эмулятора:
-
классическое двоичное число (для связи внешнего мира с троичной логикой),
-
двоично-кодированное троичное представление (те самые «два бита на трит»),
-
массив индивидуальных троичных разрядов,
-
индекс первого ненулевого трита (его знак определяет знак всего числа, а значение -1 соответствует нулю),
-
флаг четности,
-
уравновешенное девятеричное представление в алфавите {W, X, Y, Z, 0, 1, 2, 3, 4},
-
строковый троичный эквивалент в алфавите {N, Z, P},
-
девятеричную строку,
-
десятичное строковое представление,
-
шестнадцатеричную строку.
Таблица генерируется единожды на этапе сборки проекта. В самой памяти или регистрах вместо сложных вычислений хранится лишь компактная ссылка на конкретный экземпляр числа, а компилятор берет на себя заботу об управлении этими ссылками.
Да, каждое подобное число требует определенного объема памяти, но выделяется он лишь один раз, в то время как в коде мы оперируем легкими двоичными адресами.
Более того, отпадает необходимость тратить процессорное время на арифметические расчеты: для 3-, 6- и 9-разрядных трайтов все результаты предварительно просчитаны и просто выбираются из готовых таблиц. Для 18-разрядных величин часть операций вычисляется динамически — иначе вместо скромных 4 мегабайт ссылок нам потребовалось бы задействовать гигантскую таблицу размером в 17 гигабайт.
На текущем этапе библиотека спроектирована по принципу «максимальной прозрачности», но при необходимости её можно оптимизировать ради большей компактности или внедрить на аппаратном уровне в качестве специализированного сопроцессора.
В дальнейших планах — реализовать гибридное представление 3-, 6- и 9-разрядных элементов с возможностью их гибкой комбинации в массивах, а также ввести поддержку динамической разрядности (например, 12 и 15 бит вместо фиксированных блоков).
Конфигурационный слой архитектуры
Чтобы заложить фундамент для будущих модификаций, я предусмотрел гибкий конфигурационный слой. В нём задаются системы команд, мнемоники и правила, расширяющие базовые возможности «Аналеммы». Кроме того, здесь настраивается таблица символов и управляющих кодов для симуляторного терминала («Консул 254»). В дальнейшем этот слой позволит переключаться между базовой и расширенной версиями архитектуры «Сетуни».
Ограничения эмулятора
Представленный релиз является одной из первых работоспособных итераций, функционирующих пока в тепличных условиях. Функционал намеренно сокращен, однако в целом он соответствует имеющимся спецификациям.
В текущей реализации имеются определенные допущения, вызванные лаконичностью исторических описаний периферийных интерфейсов, а также отсутствием оригинальных загрузочных прошивок и диагностических тестов.
Графическая фронтальная панель с её индикаторами и органами ручного управления еще не эмулируется.
Можно сказать, что это лишь первый шаг. Но шаг весьма многообещающий.
Где взять эмулятор?
Если теория вас утомила, перейдем к практике. Обратите внимание: поскольку эмулятор опирается на внешнюю библиотеку, оба репозитория должны располагаться в соседних директориях на одном уровне.
1. Библиотека «Аналемма»
https://github.com/Barmaleikin/analemma-core.git
2. Эмулятор
https://github.com/Barmaleikin/Setun-70.git
Соберите и проверьте «Аналемму» в среде Ubuntu с помощью команд:
cargo build --release && cargo run tests
Убедившись, что сборка прошла успешно, проделайте то же самое для эмулятора. Если в процессе возникнет ошибка об отсутствии «Аналеммы», просто скопируйте папку с проектом в соседнюю директорию и повторите процедуру.
Запуск эмулятора
При успешной сборке выполните команду:
cargo run --release
В результате на экране появится приветствие:
Я СЕТУНЬ 70
После этого работа программы завершится. Чтобы задействовать машину в интерактивном режиме, запустите её с параметром:
cargo run --release -- echo
Теперь вы можете «пообщаться» с машиной: она принимает символы с клавиатуры, отфильтровывая всё, чего не было в её оригинальной кодировке (например, поддерживаются исключительно заглавные буквы латиницы и кириллицы), и возвращает результат обратно в терминал.
Можете передать на вход строку прямо при запуске и оценить результат:
cargo run --release -- echo "УРА! ЗАРАБОТАЛО! заведут мотор и через 42 к-а-к... ЭТО ТОЧНО."
Что дальше?
На этом пока всё. Исходный код открыт для изучения и экспериментов. Работа над проектом продолжается: локальные наработки уже ушли вперед от опубликованной версии — в частности, активно добавляется поддержка перфоленты и периферии «Консул 254» (модуль последней уже выложен в открытый доступ, но пока функционирует автономно).
Если вы хотите оставить комментарий и рассчитываете на мой ответ, начните своё сообщение со слов: «Примите меня в игру:». Так я пойму, что вы осилили этот текст до самого конца.
Искусственный интеллект разгадывает советские загадки на внимательность
Код как борьба. Кратчайшая история IT. Часть 3. Клод Шеннон
Забытые ретро-технологии, которые потерпели фиаско, часть 3
Мужской депрессии не существует
Звуковой щит: как физики воссоздали парадокс чёрных дыр в ультрахолодном газе
Путь Теренса Тао: от австралийского вундеркинда до гениального математика
Как объединить документы, диаграммы и знания: бережный подход к сложному
Производство антивещества в промышленных масштабах: сколько это в граммах