Обзор архитектуры бортового компьютера КК Аполлон

Решил копнуть в историческую историю и рассмотреть архитектуру и систему команд бортового компьютера космического корабля Аполлон.
Последний пока компьютер который летал на Луну вместе с людьми. Сокращённо он называется AGC (Apollo Guidance Computer).
Было два поколения его — Block I и Block II. Второе было существенной доработкой первого и именно оно летало на Луну, поэтому рассматривать буду только его.

Ячейки памяти AGC были 16-битными, но один бит отводился под контроль чётности, что было важно для отказоустойчивости, и программе виден не был.
Таким образом AGC в сущности 15-битный компьютер.

Архитектура классическая для 70-х: аккумулятор-память. Т.е. инструкции содержали код команды и адрес ячейки памяти.
Под код команды отводилось три бита и под адрес оставалось двенадцать. Таким образом адресное пространство AGC было 12-битным и процессор мог адресовать 4096 ячеек памяти.
Однако компьютер имел 2048 слов ОЗУ и 36864 слов ПЗУ, что существенно больше.
Для того чтобы дотягиваться до всего этого изобилия использовался механизм переключения страниц памяти.
Физически ОЗУ было представлено восемью страницами по 256 слов (E0-E7). Они маппились в первые 1024 слова адресного пространства AGC, причём первые три страницы E0-E2 фиксировано лежали друг за друг другом и вот четвёртый блок по адресам из диапазона 768-1023 можно было переключить на любую страницу.
ПЗУ физически было представлено 36 банками по 1024 слов каждый. При этом страницу по адресам 1024-2047 можно было переключить в любой банк, а в конец адресного пространства с адреса 2048 по адрес 4095 были замаплены банки 02 и 03.
Таким образом вырисовывается такая вот табличка (в шестнадцатеричном представлении):

000-0FF - ОЗУ E0 (256)
100-1FF - ОЗУ E1 (256)
200-2FF - ОЗУ E2 (256)
300-3FF - переключаемое ОЗУ (256)
400-7FF - переключаемое ПЗУ (1024)
800-BFF - ПЗУ 02 (1024)
C00-FFF - ПЗУ 03 (1024)

Тут еще хочу заметить, что классически для того времени вся документация использовала восьмеричную систему исчисления и приходится постоянно конвертировать адреса из документации.
Для простоты системы команд так же активно использовался маппинг регистров процессора на память — на первые ячейки ОЗУ.

Первые 20 ячеек памяти были особо важными. Здесь, например, были как раз регистры маппинга памяти.
Но совсем важны были следующие (они даже не были ячейками памяти, первые 8 регистров были частью процессора, записи и чтения их миновали ОЗУ):
Ячейка 5 (Z) — счётчик команд, т.е. адрес следующей инструкции. Следующей потому что увеличивается до того как выполняется текущая инструкция.
Ячейка 0 (A) — аккумулятор. К аккумулятору приложен еще один бит контроля переполнения. AGC использует кодирование чисел в виде «обратного кода (ones' complement)», т.е. возможен как +0 так и -0. Забавно тут заметить, что декремент -16383 «проворачивается» не в максимально возможное положительное число как при привычном ныне кодировании «с дополнением до двух», а до -0. И инкремент 16383 соответственно «проворачивается» в +0. Однако инкремент -0 получит в результате +1 и декремент +0 получит -1.
Ячейка 1 (L) — в Block I хранил часть результата умножения, а в Block II еще использовался в паре с аккумулятором как аккумулятор повышенной разрядности, тогда L хранит нижние биты
Ячейка 2 (Q) — для инструкции деления получает остаток, а вот для инструкции безусловного перехода сюда сохраняется предыдущий Z, т.е. адрес возврата. Таким образом вызов процедур и переход были одной и той же командой — вопрос был только в том будет ли сохранять программа Q для дальнейшего возврата или нет. Аппаратного стека нет, так что процедуры в норме были нереентерабельными как в первых версиях Fortran, что для того времени типично тоже.

Часто еще упоминаемый пример — ряд ячеек из этого же диапазона при записи автоматически делал сдвиги и прокрутки бит влево/вправо и таким образом компьютеру не требовались отдельные инструкции с этими вещами.

Далее следует диапазон ячеек памяти от 20 до 49 — это так называемые «счётчики». Они инкрементировались/декрементировались при поступлении внешних сигналов и могли вызывать при обнулении прерывания.

Как было сказано выше в 15 битах слова инструкции 12 бит отводилось под адрес ячейки памяти с которой мы работаем и всего три бита отводилось под код инструкции. Т.е. инструкций «из коробки» могло быть только восемь.
Поэтому, конечно, был разработан механизм дополнительных опкодов.
В первых в Block I было применено префиксирование инструкцией EXTEND которая имеет кодирование как слово «6» и было бы инструкцией перехода по адресу 6, но аппаратно перехватывалось и вместо перехода взводило внутренний флаг «расширения инструкции» который сбрасывался при исполнении следующей инструкции. Это позволило ввести в систему команд еще восемь инструкций.
Во вторых еще одна магическая форма инструкции из основного набора команд — «INDEX 15» тоже меняла своё поведение полностью и работала как инструкция RESUME, т.е. возврат из прерывания.
В третьих (судя по тому что я понял это появилось начиная с Block II) — ряд инструкций имел смысл только для адресации ОЗУ для чего хватало 10 бит, поэтому опкод можно было расширить до пяти бит.
Ну и в четвёртых ряд инструкций работал с портами ввода-вывода чьё адресное пространство было уже всего 9 бит и опкод можно было расширить до шести бит.

Система команд

После названия инструкции в скобках пишется начало её битового представления, т.е. код команды.
Если команда базовая (одна из восьми возможных в изначальной системе кодирования Block I), то это три бита, но в случае расширенных может быть 5 или 6 бит.
В зависимости от числа бит в команде параметром инструкции может быть A12 — полный 12-битный адрес, A10 — адрес в ОЗУ и IO9 — адрес порта ввода-вывода.
Таким образом инструкции могут иметь в битовом представлении следующие форматы:
III AAAA AAAA AAAA
III IIAA AAAA AAAA
III IIIA AAAA AAAA
где I — это код инструкции, а A — адресная часть.
Кроме того инструкции могут предваряться префиксной инструкцией EXTEND (просто число 6).

Инструкции без префикса EXTEND

TC A12 (000) — Transfer Control — передача управления на адрес Addr, в Q записывается адрес возврата
Забавно, что с помощью TC можно легко реализовать косвенные переходы — для этого достаточно загрузить в аккумулятор адрес куда надо перейти и выполнить TC 0. Работает это потому что двоичное представление команды TC это всего лишь 12-битное число адреса куда надо перейти, а аккумулятор маппится в нулевой адрес памяти. Т.е. TC 0 перейдёт в нулевую ячейку памяти, где содержимое аккумулятора снова выполнится как инструкция TC X, где X — 12-битное число в аккумуляторе. Однако осмысленного Q в таком случае не получится, т.е. косвенный вызов процедуры так не сделать.
RETURN — возврат из процедуры — кодируется как TC 2 и делает ничто иное как точно такой же трюк с передачей управления по значению регистра Q который маппится в ячейку номер 2.
CCS A10 (00100) — Count, Compare, and Skip — в аккумулятор загружается число из A10 (ОЗУ) и приводится к положительному числу, которое декрементируется, если оно не +0. После этого совершается один из переходов в зависимости от содержимого ячейки A10 _до изменения_:
1. Если больше +0, то переходим на следующую ячейку памяти (+1)
2. Если +0, то на ячейку +2
3. Если меньше -0, то на +3
4. Если -0, то на +4
TCF A12 (001XX) — Transfer Control Fixed — передача управления на адрес в ПЗУ. Последнее означает, что биты XX в коде операции не могут быть равны 00 (этот опкод занимает предыдущая инструкция CCS). В отличие от TC не записывает в Q адрес возврата.
DAS A10 (01000) — Double Add to Storage — сложение двойной точности. Складывает регистровую пару A:L с ячейками памяти A10:A10+1 и записывает результат в эти же ячейки памяти. Во второй ячейке хранятся нижние биты двойного слова. Сам формат двойной точности замысловатый и второй второе в нём число продолжает трактоваться как число со знаком и возможна ситуация когда знак в вернем слове отличается от знака в нижнем слове — при этом возникает ситуация когда биты итогового числа надо как бы не складывать, а вычитать.
После сложения в L записывается +0, а в аккумулятор +1,+0 или -1 в зависимости от того произошло ли переполнение и какое именно.
LXCH A10 (01001) — Exchange L and A10 — обменивает содержимое регистра L с ячейкой в ОЗУ
INCR A10 (01010) — Increment — увеличивает содержимое ячейки ОЗУ на 1
ADS A10 (01011) — Add to Storage — складывает аккумулятор с ячейкой ОЗУ и записывает результат и в аккумулятор и в эту же ячейку ОЗУ
CA A12 (011) — Clear and Add — загружает в аккумулятор значение из A12.
NOOP — отсутствие операции — псевдокод для CA 0 (т.е. аккумулятор загружается сам собой).
CS A12 (100) — Clear and Substract — загружает в аккумулятор число из A12 с изменённым знаком.
COM — Complement A — псевдокод для CS 0. Меняет знак аккумулятору.
INDEX A10 (10100) — индексация следующей инструкции. Число из ячейки ОЗУ загружается во внутренний регистр и перед выполнением следующей инструкции будет прибавлено к её коду (включая биты инструкции) без модификации памяти.
Конечно прежде всего это индексация массивов, но может быть использовано даже для динамического временного изменения кода инструкции!
Важно заметить три вещи:
Во первых в качестве ячейки у этой формы INDEX нельзя использовать адрес 15 — такая форма инструкции работает как RESUME (см. ниже).
Во вторых есть форма инструкции INDEX A12 кодируемая через EXTEND (см. ниже).
В третьих инструкция INDEX не сбрасывает флаг EXTEND.
Интересно, что с помощью инструкции INDEX можно делать косвенные переходы.
RESUME — псевдокод для INDEX 15 — выходит из обработчика прерывания, что связано с восстановлением регистра Z из регистров обслуживания прерываний.
DXCH A10 (10101) — Double Exchange — обмен регистровой пары A:L с ячейками памяти A10:A10+1.
TS A10 (10110) — Transfer to Storage — сохранить аккумулятор в ячейку A10, однако если при этом детектируется, что в аккумуляторе произошло переполнение, то аккумулятор загружается -1 или +1 в зависимости от типа переполнения и совершается прыжок на послеследующую инструкцию. Другие инструкции сохраняющие аккумулятор в память обычно просто проводят коррекцию переполненного значения и продолжают выполняться как обычно.
XCH A10 (10111) — Echange — обменять аккумулятор с ячейкой памяти.
AD A12 (110) — Add — прибавить ячейку памяти A12 к аккумулятору.
DOUBLE — псевдокод для AD 0 (удвоение аккумулятора методом сложения с собой же).
MASK A12 (111) — выполняет логическое И аккумулятора с ячейкой памяти и записывает результат в аккумулятор.

Инструкции с префиксом EXTEND

Все эти инструкции предваряются инструкцией EXTEND:
READ IO9 (000000) — в аккумулятор считывается данное из канала ввода-вывода с номером IO9
WRITE IO9 (000001) — записывает аккумулятор в канал ввода-вывода с номером IO9
RAND IO9 (000010) — Read and Mask — проводит логическое И аккумулятора с каналом ввода-вывода IO9
WAND IO9 (000011) — Write and Mask — проводит логическое И канала ввода-вывода IO9 с аккумулятором
ROR IO9 (000100) — Read and OR — проводит логическое ИЛИ аккумулятора с каналом ввода-вывода IO9
WOR IO9 (000101) — Write and OR — проводит логическое ИЛИ канала ввода-вывода IO9 с аккумулятором
RXOR IO9 (000110) — Read and XOR — проводит исключающее ИЛИ аккумулятора с каналом ввода-вывода IO9
EDRUPT IO9 (000111) — Ed's interrupt — практически не документированная инструкция введённая по личной просьбе программиста Эда Смолли. По поведению имитирует наступление аппаратного прерывания, но с мелкими особенностями.
DV A10 (00100) — Divide — деление. Регистровая пара A:L делится на содержимое A10, результат сохраняется в A, а остаток от деления в L.
BZF A12 (001XX) — Branch Zero to Fixed — если аккумулятор 0, то перейти на адрес в ПЗУ (биты XX не могут быть нулями иначе получится опкод DV).
MSU A10 (01000) — Modular Substract — вычитает из аккумулятора A10 как будто бы и A и A10 это беззнаковые числа в привычной нам форме дополнения до двух и приводит результат в аккумуляторе к привычной для AGC форме обратного кода.
Инструкция понадобилась т.к. данные от датчиков поворота поступали именно в форме дополнения до двух.
QXCH A10 (01001) — Q Exhange — обменивает значения в регистре Q и ячейке ОЗУ.
AUG A10 (01010) — Augment — инкрементирует ячейку памяти если она положительна и декрементирует если она отрицательна.
DIM A10 (01011) — Diminish — декрементирует ячейку памяти если она положительна и инкрементирует если она отрицательна.
DCA A12 (011) — Double Clear and Add — загружает в регистровую пару A:L значения из ячеек памяти A12:A12+1
DCS A12 (100) — Double Clear and Substract — загружает в регистровую пару A:L отрицательное значение из ячеек памяти A12:A12+1
INDEX A12 (101) — версия INDEX без ограничений на адрес и без особого поведения с адресом 15.
SU A10 (10100) — Substract — вычитает из аккумулятора ячейку ОЗУ
BZMF A12 (101XX) — Branch Zero or Minus to Fixed — переходит на ячейку A12 в ПЗУ (XX не может быть 00) если аккумулятор меньше ноля или ноль.
MP A12 (111) — Multiply — умножает A на A12 с помещением результата в регистровую пару A:L.

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

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.