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

Тема в разделе "WASM.ARTICLES", создана пользователем aa_dav, 24 янв 2022.

  1. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    441
    Решил копнуть в историческую историю и рассмотреть архитектуру и систему команд бортового компьютера космического корабля Аполлон.
    Последний пока компьютер который летал на Луну вместе с людьми. Сокращённо он называется 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.
    Таким образом вырисовывается такая вот табличка (в шестнадцатеричном представлении):
    Код (Text):
    1.  
    2. 000-0FF - ОЗУ E0 (256)
    3. 100-1FF - ОЗУ E1 (256)
    4. 200-2FF - ОЗУ E2 (256)
    5. 300-3FF - переключаемое ОЗУ (256)
    6. 400-7FF - переключаемое ПЗУ (1024)
    7. 800-BFF - ПЗУ 02 (1024)
    8. C00-FFF - ПЗУ 03 (1024)
    9.  
    Тут еще хочу заметить, что классически для того времени вся документация использовала восьмеричную систему исчисления и приходится постоянно конвертировать адреса из документации.
    Для простоты системы команд так же активно использовался маппинг регистров процессора на память - на первые ячейки ОЗУ.

    Первые 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.
     
    k3rnl, TermoSINteZ и Aiks нравится это.