Решил копнуть в историческую историю и рассмотреть архитектуру и систему команд бортового компьютера космического корабля Аполлон. Последний пока компьютер который летал на Луну вместе с людьми. Сокращённо он называется 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): 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.