Был такой процессор в 80х - Intel iAPX 432. Он разрабатывался в качестве преемника 8080 и изначально даже имел кодовое обозначение 8800. Intel заложила в этот процессор очень много всего - абсолютно новая архитектура, совершенно не похожая на предшественников, и даже некоторые концепции ОС, реализованные прямо в кремнии - поддержка объектно-ориентированного программирования, сборщик мусора, планировщик процессов, асинхронные коммуникации, несколько уровней отказоустойчивости и многое другое.

Однако из-за своей сложности архитектура провалилась. Существует несколько post-mortem’ов с описанием проблем и причин провала, но если вкратце, то технологии того времени сильно ограничивали сложность физического чипа. Intel пошла на несколько компромиссов, которые сильно повлияли на производительность. Центральный процессор пришлось разбить на две микросхемы, поскольку не получилось уместить всю логику в один чип. При этом даже этого было недостаточно, чтобы включить все нужные фичи, даже такие полезные как регистровый файл.
Да-да, у iAPX 432 был только один косвенно доступный регистр общего назначения (16-битный top-of-stack), а все остальные обращения к переменным шли через память. Причём данная система от Intel позиционировалась как основанная на полномочиях (возможно, термин capability-based звучит более знакомо), а значит доступ к данным был куда более сложным, чем просто считать или записать значение по конкретному адресу в памяти. К этому я ещё вернусь, но данное решение усугубило проблемы архитектуры.
Было ещё несколько спорных моментов, часть из которых поменяли в следующей ревизии. Изменения были весьма глобальными и частично исправили проблемы iAPX 432, но поезд уже ушёл и рынок похоронил инновационное детище Intel.
К счастью, у компании был план Б, и пока лучшие умы концентрировались на разработке прорывной системы, другая команда работала над временным решением - 8086, который должен был закрыть сиюминутные потребности общественности. В итоге, “временная” архитектура x86 стала доминировать в течение нескольких десятилетий, а iAPX 432 остался в памяти только у компьютерных энтузиастов. Да, так бывает.
Мне интересно ковыряться со старыми и странными процессорами, поэтому не мог пройти мимо возможности запустить что-нибудь на такой диковинке. Дополнительный интерес вызывало то, что, насколько мне известно, за последние пару десятков лет, никто не прикасался к работающей 432 системе.
Hardware
Процессор (он же GDP, general data processor) мне достался в комплекте платы iSBC 432/100. Это single board computer, который имел Multibus интерфейс, для того, чтобы его можно было использовать с Intel Intellec MDS. Но, само собой, мне хотелось иметь куда больший контроль над сигналами процессора и более дружелюбный интерфейс взаимодействия. Поэтому опять решил спроектировать простенькую плату с FPGA и SRAM на борту, которые покрывали бы все нужды процессора.

Кроме питания, необходимо было согласовать уровни сигналов (в то время многие микросхемы работали на 5v, а FPGA на 3v3). И, в принципе, это всё - плата весьма проста и отрассировалась на 2х слоях.
Из нюансов бы отметил 2 момента: я поставил TPS63002, чтобы сконвертировать плавающее напряжение с USB-коннектора в конкретные 5v, но он прожил не очень долго. То ли не рассчитан на такое применение, то ли у меня где-то ошибка.
Вторая особенность платы заключается в использовании одного коннектора для прошивки SPI флешки с FPGA-битстримом и для UART’a с хостом. Обычно я ставлю UART-USB мост, и кроме питания, USB ещё обеспечивает канал связи. Но в данном случае я перестраховался - мой ПК не может выдать достойную силу тока через USB 2.0, а по спецификации iAPX 432 может быть весьма прожорливым, и из-за этого USB кабель подключен к блоку питания. В то же время не хотелось иметь пучок проводов, и поэтому объединил 2 функции в одном коннекторе.
Чтобы вернуть ft232h в режим UART после того, как он использовался для прошивки флешки через iceprog, достаточно перезапустить модуль ядра:
sudo modprobe -r ftdi_sio
sudo modprobe ftdi_sioGateware
В качестве FPGA я взял Lattice iCE40HX. В первую очередь из-за наличия открытого стека для синтеза bitstream’a. Конкретную микросхему выбирал в паяемом корпусе и с достаточным количеством ножек.
Для памяти выбрал синхронную параллельную SRAM, работающую на 250МГц (время доступа было заявлено 2.6нс). Здесь я не смог достичь максимальной частоты работы (хотя в другом проекте та же связка работала на 250МГц), но 125МГц оказалось вполне достаточно для того, чтобы отвечать процессору за 1 такт (и не вводить дополнительные такты ожидания ответа от памяти), так что я не стал тратить время на поиск нужных таймингов для достижения более высокой частоты.

FPGA в моём дизайне выполняла роль контроллера памяти (ведомый на шине), генератора тактовых сигналов для iAPX 432 (их нужно 3) и взаимодействовала с управляющим софтом, запущенным на ПК. Для отладки мне хотелось иметь лог обращений к памяти со стороны GDP, чтобы исследовать логику его работы.
Если говорить о Verilog’е, то единственная проблема возникала с попытками заставить работать SRAM на 250МГц. Yosys (инструмент для синтеза) постоянно вставлял SB_DFFE элементы (D-триггеры с дополнительным входом Clock Enable), которые абсолютно не вписывались во временной бюджет (а для 250МГц он не сильно большой). В конце концов я спроектировал аккуратный модуль, который успешно синтезировался и даже работал (на более низких частотах), но увы не на 250МГц в существующей топологии.

Шина имеет весьма несложный интерфейс. Можно разве что упомянуть то, что почти в любой момент может прийти сигнал того, что кто-то инициировал посылку IPC сообщения (inter-processor communication).
Сам пакет запроса от ведущего устройства (GDP) содержит 32 бита - 24 бита адреса и 8 бит описания того, что хочет получить процессор.

Самое очевидное поле - тип операции. Хотим ли мы записать или прочитать значение из памяти. С длиной тоже всё ясно. Модификаторы несут больше информационную роль, они никак не влияют на поведение моего контроллера памяти. Access бит чуть более интересный - 432 работает с двумя адресными пространствами. Обычная память устройства и внешние регистры для межпроцессорного взаимодействия. К примеру, там может храниться идентификатор устройства. Так как у нас в системе нет Interface Processor’a (еще один процессор из семейства 432, который является мостом между 432 и обычной средой), то по большей части будет использоваться только пространство с обычной памятью.
Последний флаг (RMW) тоже весьма занятный. Он обеспечивает транзакционность на уровне шины. Расшифровывается как Read-Modify-Write. Чтение с установленным RMW флагом блокирует память по этому адресу - пока не придёт команда на запись (или не истечёт таймаут), все остальные операции чтения по этому адресу будут висеть без ответа. В моей упрощённой системе с одним GDP и пассивным ведомым контроллером памяти каких-то конкурирующих запросов к памяти не планируется, поэтому также можем не заводить логику под поддержку этой фичи.
Подготовка процессора к старту
Ранее я не упоминал, но невозможно построить функциональную систему только на процессорах семейства 432. Из-за своей объектно-ориентированной природы, iAPX 432 ожидает, что кто-то уже подготовит множество структур в памяти. Всегда должен быть какой-то attached processor (а-ля 8080 или даже 8086), который проведёт инициализацию памяти и даст сигнал о том, что можно стартовать основные блоки распределённой системы. Прежде чем начать выполнять пользовательский код, 432 GDP совершает множество телодвижений по чтению системных объектов - таблицы размещения сегментов, информация о процессоре (регистры в том самом межпроцессорном пространстве и структуры в обычной памяти), текущем процессе, сегментах кода и данных, и т.д.
Поэтому нужно построить снимок памяти и загрузить его в SRAM до того, как стартовать процессор.
После подачи сигнала INIT/, GDP начинает спамить запросами на чтение межпроцессорного регистра 0x02. Этот регистр содержит информацию о состоянии IPC - есть ли какое-то внешнее сообщение, которое нужно обработать. Если пришло IPC, то процессор начинает полноценный процесс пробуждения. В теории, память может использоваться несколькими устройствами, поэтому нельзя полагаться на заранее заданные абсолютные адреса для чтения разнообразных системных структур. Только один адрес зашит в микрокоде - адрес глобального объектного каталога. И процессор использует свой идентификатор в системе (получая его через запрос регистра 0x00) в качестве индекса в этой глобальной таблице для чтения объекта типа Processor.
И уже из него начинает получать адреса объектов, специфичных для конкретного процессора. Прежде чем исполнить первую пользовательскую инструкцию кода, GDP совершает около 150 операций с памятью. И нам нужно уметь корректно отвечать на эти запросы.
Пример лога запросов на шине со стороны GDP
[+] Connected to SBC
[+] SBC is online
[~] Building image...
[+] ROM image has been written to SBC, size = 1110 bytes
[+] GDP has been started
[~] Read access log after 2s of execution.
[+] Access log (skipped 0 entries):
[000] GDP initialization
[001] spec: <RD 2b, 'Other/interconnect register'> addr: 0x0002
[002] spec: <RD 2b, 'Other/interconnect register'> addr: 0x0000
[003] spec: <RD 4b, 'Memory/other'> addr: objectTableDirectory/objectTableProcessor (0x0018)
[004] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableDirectory/objectTableProcessor (0x0018)
[005] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableDirectory/objectTableProcessor (0x0018) <58d7>
[006] spec: <RD 2b, 'Memory/other'> addr: objectTableDirectory+0x18 (0x0020)
[007] spec: <RD 2b, 'Memory/other'> addr: objectTableDirectory+0x14 (0x001c)
[008] spec: <RD 10b, 'Memory/other'> addr: objectTableProcessor/processorAccess (0x0068)
[009] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableProcessor/processorAccess (0x0068)
[010] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableProcessor/processorAccess (0x0068) <78df>
[011] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x10 (0x0088)
[012] spec: <RD 4b, 'Memory/other'> addr: objectTableDirectory/objectTableDirectory (0x0038)
[013] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableDirectory/objectTableDirectory (0x0038)
[014] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableDirectory/objectTableDirectory (0x0038) <08d7>
[015] spec: <RD 2b, 'Memory/other'> addr: objectTableDirectory+0x38 (0x0040)
[016] spec: <RD 2b, 'Memory/other'> addr: objectTableDirectory+0x34 (0x003c)
[017] spec: <RD 10b, 'Memory/other'> addr: objectTableDirectory/objectTableDirectory (0x0038)
[018] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x00 (0x0078)
[019] spec: <RD 4b, 'Memory/other'> addr: objectTableDirectory/objectTableMain (0x0048)
[020] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableDirectory/objectTableMain (0x0048)
[021] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableDirectory/objectTableMain (0x0048) <d8d7>
[022] spec: <RD 2b, 'Memory/other'> addr: objectTableDirectory+0x48 (0x0050)
[023] spec: <RD 2b, 'Memory/other'> addr: objectTableDirectory+0x44 (0x004c)
[024] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processorData (0x00e8)
[025] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableMain/processorData (0x00e8)
[026] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableMain/processorData (0x00e8) <e8d7>
[027] spec: <RD 2b, 'Memory/other', RMW> addr: processorData+0x00 (0x01e8)
[028] spec: <WR 2b, 'Memory/other', RMW> addr: processorData+0x00 (0x01e8) <0005>
[029] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x00 (0x0078)
[030] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processorData (0x00e8)
[031] spec: <WR 2b, 'Memory/other'> addr: processorData+0x02 (0x01ea) <0102>
[032] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x08 (0x0080)
[033] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processorLocalComms (0x00f8)
[034] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableMain/processorLocalComms (0x00f8)
[035] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableMain/processorLocalComms (0x00f8) <78d7>
[036] spec: <RD 2b, 'Memory/other', RMW> addr: processorLocalComms+0x00 (0x0278)
[037] spec: <WR 2b, 'Memory/other', RMW> addr: processorLocalComms+0x00 (0x0278) <0005>
[038] spec: <RD 4b, 'Memory/other'> addr: processorLocalComms+0x02 (0x027a)
[039] spec: <WR 2b, 'Memory/other'> addr: processorLocalComms+0x04 (0x027c) <0000>
[040] spec: <RD 2b, 'Memory/other', RMW> addr: processorLocalComms+0x00 (0x0278)
[041] spec: <WR 2b, 'Memory/other', RMW> addr: processorLocalComms+0x00 (0x0278) <0000>
[042] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x00 (0x0078)
[043] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processorData (0x00e8)
[044] spec: <WR 2b, 'Memory/other'> addr: processorData+0x02 (0x01ea) <0102>
[045] spec: <RD 8b, 'Memory/other'> addr: processorAccess+0x18 (0x0090)
[046] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/delayCarrierAccess (0x0138)
[047] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableMain/delayCarrierAccess (0x0138)
[048] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableMain/delayCarrierAccess (0x0138) <bedf>
[049] spec: <WR 1b, 'Memory/other'> addr: objectTableMain+0x49 (0x0121) <0001>
[050] spec: <WR 4b, 'Memory/other'> addr: processorAccess+0x24 (0x009c) <004f 004f>
[051] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/delayPortAccess (0x0118)
[052] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableMain/delayPortAccess (0x0118)
[053] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableMain/delayPortAccess (0x0118) <9adf>
[054] spec: <RD 4b, 'Memory/other'> addr: delayPortAccess+0x00 (0x029a)
[055] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/delayPortData (0x0108)
[056] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableMain/delayPortData (0x0108)
[057] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableMain/delayPortData (0x0108) <82d7>
[058] spec: <RD 2b, 'Memory/other', RMW> addr: delayPortData+0x00 (0x0282)
[059] spec: <WR 2b, 'Memory/other', RMW> addr: delayPortData+0x00 (0x0282) <0005>
[060] spec: <RD 6b, 'Memory/other'> addr: delayPortData+0x06 (0x0288)
[061] spec: <RD 4b, 'Memory/other'> addr: delayPortData+0x00 (0x0282)
[062] spec: <RD 2b, 'Memory/other', RMW> addr: delayPortData+0x00 (0x0282)
[063] spec: <WR 2b, 'Memory/other', RMW> addr: delayPortData+0x00 (0x0282) <0000>
[064] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x14 (0x008c)
[065] spec: <WR 1b, 'Memory/other'> addr: objectTableMain+0x89 (0x0161) <0001>
[066] spec: <WR 4b, 'Memory/other'> addr: processorAccess+0x28 (0x00a0) <008f 004f>
[067] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/normalCarrierAccess (0x0158)
[068] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableMain/normalCarrierAccess (0x0158)
[069] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableMain/normalCarrierAccess (0x0158) <f2df>
[070] spec: <RD 4b, 'Memory/other'> addr: normalCarrierAccess+0x00 (0x02f2)
[071] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/normalCarrierData (0x0148)
[072] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableMain/normalCarrierData (0x0148)
[073] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableMain/normalCarrierData (0x0148) <e2d7>
[074] spec: <RD 2b, 'Memory/other'> addr: normalCarrierData+0x02 (0x02e4)
[075] spec: <WR 2b, 'Memory/other'> addr: normalCarrierData+0x02 (0x02e4) <0008>
[076] spec: <RD 4b, 'Memory/other'> addr: normalCarrierAccess+0x1c (0x030e)
[077] spec: <WR 1b, 'Memory/other'> addr: objectTableMain+0xa9 (0x0181) <0001>
[078] spec: <WR 4b, 'Memory/other'> addr: processorAccess+0x04 (0x007c) <00af 004f>
[079] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x00 (0x0078)
[080] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processorData (0x00e8)
[081] spec: <WR 2b, 'Memory/other'> addr: processorData+0x02 (0x01ea) <0103>
[082] spec: <WR 1b, 'Memory/other'> addr: objectTableMain+0xa9 (0x0181) <0001>
[083] spec: <WR 4b, 'Memory/other'> addr: processorAccess+0x28 (0x00a0) <00af 004f>
[084] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processCarrierAccess (0x0178)
[085] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableMain/processCarrierAccess (0x0178)
[086] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableMain/processCarrierAccess (0x0178) <26df>
[087] spec: <RD 4b, 'Memory/other'> addr: processCarrierAccess+0x00 (0x0326)
[088] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processCarrierData (0x0168)
[089] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableMain/processCarrierData (0x0168)
[090] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableMain/processCarrierData (0x0168) <16d7>
[091] spec: <RD 2b, 'Memory/other'> addr: processCarrierData+0x04 (0x031a)
[092] spec: <RD 2b, 'Memory/other', RMW> addr: processCarrierData+0x00 (0x0316)
[093] spec: <WR 2b, 'Memory/other', RMW> addr: processCarrierData+0x00 (0x0316) <0005>
[094] spec: <RD 4b, 'Memory/other'> addr: processCarrierAccess+0x20 (0x0346)
[095] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processAccess (0x0198)
[096] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableMain/processAccess (0x0198)
[097] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableMain/processAccess (0x0198) <dadf>
[098] spec: <RD 4b, 'Memory/other'> addr: processAccess+0x00 (0x03da)
[099] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processData (0x0188)
[100] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableMain/processData (0x0188)
[101] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableMain/processData (0x0188) <4ad7>
[102] spec: <RD 2b, 'Memory/other', RMW> addr: processData+0x00 (0x034a)
[103] spec: <WR 2b, 'Memory/other', RMW> addr: processData+0x00 (0x034a) <0005>
[104] spec: <RD 6b, 'Memory/other'> addr: processData+0x20 (0x036a)
[105] spec: <RD 4b, 'Memory/other'> addr: processAccess+0x14 (0x03ee)
[106] spec: <WR 4b, 'Memory/other'> addr: processCarrierAccess+0x0c (0x0332) <0000 0000>
[107] spec: <RD 4b, 'Memory/other'> addr: processAccess+0x04 (0x03de)
[108] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processContext0Access (0x01a8)
[109] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableMain/processContext0Access (0x01a8)
[110] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableMain/processContext0Access (0x01a8) <0adf>
[111] spec: <RD 4b, 'Memory/other'> addr: processContext0Access+0x00 (0x040a)
[112] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processContext0Data (0x01b8)
[113] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableMain/processContext0Data (0x01b8)
[114] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableMain/processContext0Data (0x01b8) <32d7>
[115] spec: <RD 4b, 'Memory/other'> addr: processContext0Access+0x14 (0x041e)
[116] spec: <WR 2b, 'Memory/other'> addr: processData+0x32 (0x037c) <ffff>
[117] spec: <WR 4b, 'Memory/other'> addr: processContext0Access+0x14 (0x041e) <0000 0000>
[118] spec: <RD 4b, 'Memory/other'> addr: processContext0Access+0x18 (0x0422)
[119] spec: <WR 2b, 'Memory/other'> addr: processData+0x34 (0x037e) <ffff>
[120] spec: <WR 4b, 'Memory/other'> addr: processContext0Access+0x18 (0x0422) <0000 0000>
[121] spec: <RD 4b, 'Memory/other'> addr: processContext0Access+0x1c (0x0426)
[122] spec: <WR 2b, 'Memory/other'> addr: processData+0x36 (0x0380) <ffff>
[123] spec: <WR 4b, 'Memory/other'> addr: processContext0Access+0x1c (0x0426) <0000 0000>
[124] spec: <RD 4b, 'Memory/other'> addr: processContext0Access+0x24 (0x042e)
[125] spec: <RD 8b, 'Memory/context'> addr: processContext0Data+0x00 (0x0432)
[126] spec: <RD 4b, 'Memory/other'> addr: processContext0Access+0x20 (0x042a)
[127] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processContext0Domain (0x01c8)
[128] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableMain/processContext0Domain (0x01c8)
[129] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableMain/processContext0Domain (0x01c8) <409f>
[130] spec: <RD 4b, 'Memory/other'> addr: processContext0Domain+0x00 (0x0440)
[131] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processContext0Instruction0 (0x01d8)
[132] spec: <RD 2b, 'Memory/other', RMW> addr: objectTableMain/processContext0Instruction0 (0x01d8)
[133] spec: <WR 2b, 'Memory/other', RMW> addr: objectTableMain/processContext0Instruction0 (0x01d8) <4497>
[134] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x00 (0x0078)
[135] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processorData (0x00e8)
[136] spec: <WR 2b, 'Memory/other'> addr: processorData+0x02 (0x01ea) <0104>
[137] spec: <RD 4b, 'Memory/instruction'> addr: processContext0Instruction0+0x0e (0x0452)
[138] spec: <RD 4b, 'Memory/other'> addr: processContext0Access+0x08 (0x0412)
[139] spec: <RD 2b, 'Memory/other'> addr: processAccess+0x0c (0x03e6)
[140] spec: <RD 4b, 'Memory/other'> addr: objectTableDirectory:Header (0x0008)
[141] spec: <WR 10b, 'Memory/other'> addr: processData+0x7c (0x03c6) <0000 0000 0000 0000 7fff>
[142] spec: <WR 10b, 'Memory/other'> addr: processData+0x86 (0x03d0) <0004 010f 010f 0000 7fff>
[143] spec: <WR 8b, 'Memory/other'> addr: processData+0x74 (0x03be) <00cd 7417 000c 0000>
[144] spec: <WR 8b, 'Memory/other'> addr: processData+0x68 (0x03b2) <0000 0076 0070 0000>
[145] spec: <WR 4b, 'Memory/other'> addr: processData+0x70 (0x03ba) <0000 0001>
[146] spec: <RD 4b, 'Memory/other'> addr: processAccess+0x10 (0x03ea)
[147] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x00 (0x0078)
[148] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processorData (0x00e8)
[149] spec: <WR 10b, 'Memory/other'> addr: processorData+0x54 (0x023c) <0000 0001 0001 0000 7fff>
[150] spec: <WR 10b, 'Memory/other'> addr: processorData+0x5e (0x0246) <0000 0000 0000 0000 7fff>
[151] spec: <WR 8b, 'Memory/other'> addr: processorData+0x4c (0x0234) <00cd 7a00 000c 0000>
[152] spec: <WR 8b, 'Memory/other'> addr: processorData+0x40 (0x0228) <0000 0070 0070 0000>
[153] spec: <WR 4b, 'Memory/other'> addr: processorData+0x48 (0x0230) <0000 0001>
[154] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x14 (0x008c)
[155] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/normalCarrierAccess (0x0158)
[156] spec: <RD 4b, 'Memory/other'> addr: normalCarrierAccess+0x00 (0x02f2)
[157] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/normalCarrierData (0x0148)
[158] spec: <RD 2b, 'Memory/other'> addr: normalCarrierData+0x02 (0x02e4)
[159] spec: <WR 2b, 'Memory/other'> addr: normalCarrierData+0x02 (0x02e4) <000c>
[160] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x00 (0x0078)
[161] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processorData (0x00e8)
[162] spec: <WR 2b, 'Memory/other'> addr: processorData+0x02 (0x01ea) <0105>
[163] spec: <WR 8b, 'Memory/context'> addr: processContext0Data+0x00 (0x0432) <0000 0000 0000 0070>
[164] spec: <RD 8b, 'Memory/other'> addr: processData+0x22 (0x036c)
[165] spec: <WR 6b, 'Memory/other'> addr: processData+0x24 (0x036e) <0043 0000 0000>
[166] spec: <RD 2b, 'Memory/other', RMW> addr: processData+0x00 (0x034a)
[167] spec: <WR 2b, 'Memory/other', RMW> addr: processData+0x00 (0x034a) <0000>
[168] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x00 (0x0078)
[169] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processorData (0x00e8)
[170] spec: <WR 2b, 'Memory/other'> addr: processorData+0x02 (0x01ea) <0105>
[171] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x04 (0x007c)
[172] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processCarrierAccess (0x0178)
[173] spec: <RD 4b, 'Memory/other'> addr: processCarrierAccess+0x00 (0x0326)
[174] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processCarrierData (0x0168)
[175] spec: <RD 2b, 'Memory/other', RMW> addr: processCarrierData+0x00 (0x0316)
[176] spec: <WR 2b, 'Memory/other', RMW> addr: processCarrierData+0x00 (0x0316) <0001 0000 0000 0000 0000>
[177] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x00 (0x0078)
[178] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processorData (0x00e8)
[179] spec: <WR 2b, 'Memory/other'> addr: processorData+0x02 (0x01ea) <0135>
[180] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x4c (0x00c4)
[181] spec: <WR 4b, 'Memory/other'> addr: processorAccess+0x14 (0x008c) <0000 0000>
[182] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x00 (0x0078)
[183] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/processorData (0x00e8)
[184] spec: <WR 2b, 'Memory/other'> addr: processorData+0x02 (0x01ea) <0132>
[185] spec: <RD 8b, 'Memory/other'> addr: processorAccess+0x18 (0x0090)
[186] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/delayCarrierAccess (0x0138)
[187] spec: <WR 1b, 'Memory/other'> addr: objectTableMain+0x49 (0x0121) <0001>
[188] spec: <WR 4b, 'Memory/other'> addr: processorAccess+0x24 (0x009c) <004f 004f>
[189] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/delayPortAccess (0x0118)
[190] spec: <RD 4b, 'Memory/other'> addr: delayPortAccess+0x00 (0x029a)
[191] spec: <RD 10b, 'Memory/other'> addr: objectTableMain/delayPortData (0x0108)
[192] spec: <RD 2b, 'Memory/other', RMW> addr: delayPortData+0x00 (0x0282)
[193] spec: <WR 2b, 'Memory/other', RMW> addr: delayPortData+0x00 (0x0282) <0005>
[194] spec: <RD 6b, 'Memory/other'> addr: delayPortData+0x06 (0x0288)
[195] spec: <RD 4b, 'Memory/other'> addr: delayPortData+0x00 (0x0282)
[196] spec: <RD 2b, 'Memory/other', RMW> addr: delayPortData+0x00 (0x0282)
[197] spec: <WR 2b, 'Memory/other', RMW> addr: delayPortData+0x00 (0x0282) <0000>
[198] spec: <RD 4b, 'Memory/other'> addr: processorAccess+0x14 (0x008c)
[199] spec: <WR 4b, 'Memory/other'> addr: processorAccess+0x28 (0x00a0) <0000 0000>
[200] Fatal signal is raised by GDPМожно как-то так визуализировать иерархию объектов, которые формируют минимально возможный набор для запуска нашего кода.

Ну или в программном виде:
const processorObjectTable = new ObjectTable('objectTableProcessor');
// empty, would not be used
const tempDirObjectTable = new ObjectTable('objectTableTemp');
const mainObjectTable = new ObjectTable('objectTableMain');
const directoryObjectTable = new ObjectTable('objectTableDirectory');
const objectDirectory = new ObjectTableDirectory(directoryObjectTable);
objectDirectory.addObjectTable(processorObjectTable);
objectDirectory.addObjectTable(tempDirObjectTable);
objectDirectory.addObjectTable(directoryObjectTable);
objectDirectory.addObjectTable(mainObjectTable);
// processors object table contains only processor access segments
processorObjectTable.addObject(new ProcessorAccessSegment('processorAccess', { directoryObjectTable }));
// interconnect segment for UART output
mainObjectTable.addInterconnectSegment('uartInterconnect', 0x1000, 0x10);
// here is all objects, except processor access segments
mainObjectTable.addObject(new ProcessorDataSegment('processorData'));
mainObjectTable.addObject(new LocalCommunicationSegment('processorLocalComms'));
// delay port
mainObjectTable.addObject(new PortDataSegment('delayPortData', { messageQueueSize: 1, portType: PORT_TYPE.DELAY }));
mainObjectTable.addObject(new PortAccessSegment('delayPortAccess', { directoryObjectTable, messageQueueSize: 1 }));
mainObjectTable.addObject(new CarrierDataSegment('delayCarrierData', { carrierType: CARRIER_TYPE.PROCESSOR }));
mainObjectTable.addObject(new CarrierAccessSegment('delayCarrierAccess', { directoryObjectTable }));
// actual process objects
mainObjectTable.addObject(new CarrierDataSegment('normalCarrierData', { carrierType: CARRIER_TYPE.PROCESSOR, hasMessage: true }));
mainObjectTable.addObject(new CarrierAccessSegment('normalCarrierAccess', { directoryObjectTable, messageRef: 'processCarrierAccess' }));
mainObjectTable.addObject(new CarrierDataSegment('processCarrierData', { carrierType: CARRIER_TYPE.PROCESSOR, hasMessage: true }));
mainObjectTable.addObject(new CarrierAccessSegment('processCarrierAccess', { directoryObjectTable, carriedObjectRef: 'processAccess' }));
mainObjectTable.addObject(new ProcessDataSegment('processData'));
mainObjectTable.addObject(new ProcessAccessSegment('processAccess', { directoryObjectTable }));
mainObjectTable.addObject(new ContextAccessSegment('processContext0Access', { directoryObjectTable, objectsRefs: ['uartInterconnect', 'processContext0Vars'] }));
mainObjectTable.addObject(new ContextDataSegment('processContext0Data', { sp: 0 })); // stack grows upward, push increments SP, pop - decrements
mainObjectTable.addObject(new GenericDataSegment('processContext0Stack', { size: stack.size, data: stack.data, type: SEGMENT_TYPE.OPERAND_STACK_DATA }));
mainObjectTable.addObject(new GenericDataSegment('processContext0Vars', { data: varsData, type: SEGMENT_TYPE.GENERIC_DATA }));
mainObjectTable.addObject(new DomainSegment('processContext0Domain', { directoryObjectTable, instructionsRefs: ['processContext0Instruction0'] }));
mainObjectTable.addObject(new InstructionSegment('processContext0Instruction0', { directoryObjectTable, instructions: bytecode, contextIdx: 0 }));
Не буду останавливаться на каждом запросе к памяти, а пройдусь концептуально - опишу интересные моменты и особенности iAPX 432.
Во-первых, нужно упомянуть, как работает трансляция указателей на объекты в физические адреса. Каждый указатель состоит из двух частей - индекс таблицы объектов и индекс самого объекта в таблице. То есть чтобы вычислить физический адрес, процессору нужно сначала найти адрес таблицы (через чтение центрального каталога, который и содержит адреса конкретных таблиц), а затем уже из этой таблицы прочитать дескриптор, в котором записан физический адрес.

Зачем такие сложности? А потому что нужно было реализовать несколько фич:
Каждый указатель кроме адреса содержит права, которыми обладает этот указатель. Можно ли его использовать для чтения/записи/… Причём возможный набор прав зависит от типа объекта. Скажем, если у нас есть указатель на объект типа Processor, то можно разрешить его использование для конкретных высокоуровневых операций, таких как отправить сообщение процессору, или запросить его счётчик тактов (в памяти этой информации нет). Для объекта типа Process будет свой набор прав. А значит нужно проверять тип объекта (соответствующее поле в дескрипторе).
Сборка мусора. Это всё же 80е, поэтому многие концепции сборки мусора на тот момент были наивны. Да и какие-то сложные алгоритмы запихать в крайне ограниченный объём микрокода было непросто. Поэтому управление памятью не сильно усложнено. Объекту может быть назначен уровень вложенности. Когда “функция” (конечно, в терминах 432 это называется совершенно по-другому, но я упрощаю) заканчивает исполнение и происходит возврат к вызывающему коду, процессор освобождает все объекты, которые соответствуют уровню вложенности этой функции. Также дескриптор содержит флаг того, используется ли объект кем-либо.
Кроме того, в дескриптор записывается разная служебная информация (не знаю, используется ли она в логике самого GDP или предназначена для внешних систем): отметки о том, был ли запрос на доступ к объекту, была ли запись в объект, содержит ли объект какие-то данные, или там все нули, и т.д. Если она не нужна процессору, то это дополнительный источник просадок по производительности - ведь эти поля синхронизируются по мере работы, и это лишние запросы к памяти.
Кстати, из кода можно заметить, что у некоторых объектов есть два сегмента - Data и Access. К примеру, ProcessorDataSegment / ProcessorAccessSegment. Что это значит? А это одно из весьма спорных решений Intel (которое поменяли в третьей ревизии iAPX 432). Объект разделён на 2 части - access сегмент содержит только указатели, а data сегмент всё остальное. То есть, работая с каким-то системным объектом, процессор зачастую обращается к 2м физическим сегментам. Редко какая операция требует доступ только к указателям или только к скалярным полям. При этом у нас удваивается количество обращений к памяти, ведь для чтения данных из сегмента нужно пройти двухэтапный путь получения адреса таблицы объектов и доступа к записям в этой таблице. Плюс ещё возможно потребуется записать какую-нибудь служебную информацию. Почему же у Intel получился такой непроизводительный процессор…
Не все, но многие объекты могут быть захвачены (залочены) либо железом, либо программно (через инструкцию LOCK OBJECT). Реализуется весьма просто - в data-сегменте объекта есть поле, которое содержит мета-информацию о блокировке: тип блокировки и идентификатор того, кто захватывает объект. Тоже крайне интересный концепт в 50-летнем процессоре, но что вы скажете об аппаратной поддержке очередей сообщений с поддержкой приоритетов, TTL, неблокирующих операций и ряда других плюшек?
Эта механика используется не только для пересылки сообщений между разными процессами, запущенными на GDP, но и самим планировщиком. Есть набор процессоров и есть набор процессов. Планировщик отвечает за то, чтобы найти процесс для исполнения на конкретном процессоре. Для этого используются очереди сообщений. В качестве потребителей выступают процессоры, в качестве сообщений - процессы, а публиковать сообщения в очередь могут разные источники, как внешние, так и внутренние.

При этом таких очередей несколько (прямо как в современном Linux’е, но в железе) - для обычных задач, задач реконфигурации, срочных процессов и диагностических. Есть даже очередь для спящих процессов, которые должны будут ожить в определённый момент (планировщик просто переместит его в очередь для обычных процессов). Причём очередь может быть как FIFO, так и основанная на приоритетах. Скажу честно, что я не особо экспериментировал в этой области - у меня всего один процесс для одного процессора, поэтому многие вещи я знаю только из теории, а не из практики. Разве что столкнулся с тем, что по истечении кванта времени, отведённого на процесс, планировщик вдруг захотел включиться в работу, а мне это было не нужно. Пришлось обмануть и занизить тактовую частоту счётчика тиков процессора - для iAPX 43202 нужно предоставить отдельный тактовый сигнал для внутренних нужд, он может быть произвольным и используется чисто в планировщике.
Если вкратце, то для запуска пользовательской программы необходимо подготовить начальный каталог, который содержит адреса таблиц объектов, сами эти таблицы и описать ряд основных объектов - процессор (фактически состоит из нескольких связанных сегментов), процесс, очереди, сообщения в этих очередях и сегменты кода/данных. Немало, но используя лог обращений к памяти, можно понять, какие именно поля являются критичными для процессора, и заполнить только их, игнорируя часть архитектурной сложности системы.
Организация пользовательского кода и данных
Исполняемый код процесса распределён по нескольким объектам типа Context. Упрощая, можно воспринимать это как функции или скорее процедуры. Контекст формируется из сегментов кода (объекты типа Instruction) и 4х списков с сегментами данных (списки называются entry access segments, EAS). Код может обращаться только к переменным, которые лежат в сегментах, определённых в EAS’ах. Так достигается некая изоляция - чтобы получить доступ к данным другого контекста, нужно вызвать соответствующую команду, которая импортирует EAS из другого контекста (конечно, если права доступа позволят).

Вызов процедуры (контекста) - очень накладная операция. iAPX 432 необходимо подготовить пачку объектов, описывающих контекст (не только соответствующие data и access сегменты, но и, к примеру, сегмент стека), что требует около 20–30 обращений к памяти. Это, кстати, одно из бутылочных горлышек программ на Ada, написанных для данной системы. Компилятор весьма неоптимально распределял код - в ISA есть инструкции для передачи управления на код, который располагается внутри контекста, так что не всегда было резонно создавать новый контекст и платить большую цену за вызов подпрограммы.
В моём низкоуровневом коде я использую только один контекст и один сегмент с кодом (больших программ я не писал, всё легко уместилось в 64Кб). Также было достаточно одного сегмента с данными (плюс стек), хотя EAS может содержать 16384 ссылок, что позволяет адресовать 4 x 16k x 64kb = 4Гб. Неплохо для проца из 80х.
Так как мне хотелось исследовать чистую производительность системы, я планировал писать код на “ассемблере”, а не пытаться найти трюки для Ada компилятора, чтобы выжать из него хоть что-то достойное. А значит нужно разобраться в программной архитектуре 432, прежде чем писать свои программы и транслятор, чтобы скомпилировать их в машинный код.
Кое-что я уже упоминал - отсутствие доступных регистров, за исключением одного 16-битного top-of-stack. И наличие стека (который, кстати, растёт вверх, а не вниз как в ARM или x86). Правда нет привычных инструкций PUSH/POP, но GDP сам модифицирует указатель на стек, если инструкция ссылается на данные оттуда.
Процессор поддерживает различные типы операндов - как обычные целочисленные (разрядностью до 32х бит!), так и числа с плавающей точкой (iAPX 432 одним из первых начал поддерживать числа в формате, который позже будет закреплён в спецификации IEEE 754). И, конечно же, есть инструкции для работы с разными объектами: системными (типа вышеуказанной “LOCK OBJECT”) и пользовательскими.
Формат машинного кода не подразумевает кодирования непосредственных значений, а значит большинство операндов - это ссылки на значения в памяти. Как же устроена адресация переменных? Увы, непросто - каждая ссылка состоит из двух частей: селектор сегмента данных, который содержит переменную, и смещение в данном сегменте. В простейшей форме селектор кодируется как индекс EAS’a в контексте (один из 4х) и индекс указателя на целевой сегмент. То есть даже самое тривиальное использование константы в коде превращается в несколько обращений к памяти. Э - эффективность. Есть ещё и косвенная адресация сегмента - это когда мы по селектору из машинного кода вычисляем адрес ячейки памяти, которая содержит ещё один селектор, который уже задаёт сегмент с нашей переменной.

Смещение переменной в сегменте данных тоже может быть более сложным, чем просто число. Базовый пример - обращение к элементу массива по индексу, который сам является переменной. Это возможно.
Как видите, ничего экстраординарного, можно приступать к написанию простого компилятора. Но я изначально задумывался о производительности и заранее предпринял несколько шагов по оптимизации. В первую очередь нужно по максимуму использовать единственный регистр, а если не получается, то выбирать простейшие режимы адресации с минимальным числом чтений из памяти.
Кроме того, важный нюанс при написании транслятора заключается в том, что машинный код имеет формат инструкций переменной длины, причём они даже не выровнены по границам байт! То есть инструкция может кодироваться как шестью (6!) битами, так и двумястами (200!) битами. С точки зрения программирования это важно, потому что более короткие инструкции означают более компактный код и меньше обращений к памяти со стороны процессора, чтобы прочитать этот код.
Программы для iAPX 432
Как всегда, начнём с Hello world. Но как же нам что-то вывести на экран? У нас есть FPGA на шине с GDP, но как распознать команду процессора на отправку текста в консоль? Стандартный путь - это инициировать IPC через инструкции BROADCAST TO PROCESSORS или SEND TO PROCESSOR, но тогда нам придётся эмулировать ещё один процессор в системе, которому GDP посылает это IPC сообщение. К счастью, есть путь проще - команда MOVE TO INTERCONNECT просто записывает значение в межпроцессорный регистр, не требуя каких-то сложных подготовленных структур в памяти. Фактически это просто одна транзакция на шине, и FPGA может отловить запись по определённому адресу (GDP использует только 0x0 и 0x02, все остальные адреса в нашем распоряжении) и переслать данные через UART нашему ПК.
.stack {
size = 0x10
data = []
}
.data {
msgIdx = { size = 2, data = [0x06, 0x00] }
# reversed, because we start sending data from the end
msg = { size = 12, data = [0x21, 0x64, 0x6c, 0x72, 0x6f, 0x57, 0x20, 0x6f, 0x6c, 0x6c, 0x65, 0x48] }
# variables for sending data via UART
interconnectRegUart = { size = 2, data = [0x02, 0x00] }
interconnectSegmentSelector = { size = 2, data = [0x28, 0x00] }
}
sendTwoChars:
MOVE_TO_INTERCONNECT interconnectSegmentSelector interconnectRegUart $data[msgIdx]
# array is iterated in range [msgLen ... 1], because we want to reference uart payload from base 0
# and need to skip element at index 0 (it's reserved for uart payload length)
DEC_2U msgIdx msgIdx
EQUAL_ZERO_2U msgIdx $st0
BRANCH_FALSE $st0 sendTwoChars
RETURN_FROM_CONTEXT
Код тривиален даже для людей, незнакомых с iAPX 432. Разве что поясню пару моментов.
Я ввёл псевдо-переменную $data как раз с целью оптимизации размера кода. Если мы адресуем переменную от начала сегмента, то это позволяет избежать кодирования смещения начала массива. Использовав msg[msgIdx], получим дополнительные 7 бит для кодирования 0x02 (смещение переменной msg в сегменте данных). В данном случае это экономия на спичках, но хотелось показать наглядный пример.
Можете обратить внимание на то, что условный переход выполняется за 2 инструкции - сначала сравниваем переменную с нулём и результат сравнения записываем в другую переменную (в данном случае - стек, а точнее top-of-stack регистр), а затем в зависимости от значения результата сравнения выполняем прыжок.

BRANCH_TRUE msgIdx sendTwoChars не работает, потому что BRANCH_TRUE работает с 8-битными значениями, а msgIdx - 16 бит. И дело не в том, что iAPX 432 проверяет разрядность, а просто в банальной арифметике - при касте 16-битного значения в 8-битное мы теряем часть информации и логика нарушается. Ранний возврат при msgIdx = 0x100 нам не нужен :)
Наконец-то переходим к тому, ради чего всё затевалось - написание бенчмарка. Как обычно, для этого я выбрал программу получения цифр числа Pi. А конкретно - кранинковый алгоритм (spigot). Он крайне простой для реализации, но позволяет сравнить производительность ALU.
.stack {
size = 0x20
data = []
}
.data {
idx = { size = 2 }
arr = { size = 55000 } # iteration in range [1 .. (LEN - 1)],
# array length for 8192 digits is 27307, size should be 54614
### global variables
toPrint = { size = 2, data = [0x00, 0x20] } # amount of digits to print
# toPrint = { size = 2, data = [0x00, 0x08] }
# toPrint = { size = 2, data = [0x00, 0x01] }
# toPrint = { size = 2, data = [0x0A, 0x00] }
LEN = { size = 2, data = [0x00, 0x00] } # length of array - 1
nineCount = { size = 2, data = [0x00, 0x00] } # count of consecutive 9s
previousDigit = { size = 2, data = [0x02, 0x00] } # previous digit
### local variables for inner loops
carry = { size = 4 }
denominator = { size = 4 }
numerator = { size = 4 }
digitFromCarry = { size = 2 }
nextDigit = { size = 2 }
### constants
c10 = { size = 4, data = [0x0A, 0x00, 0x00, 0x00] } # constant 10
c3 = { size = 4, data = [0x03, 0x00, 0x00, 0x00] } # constant 3
c2 = { size = 2, data = [0x02, 0x00] } # constant 2
c9 = { size = 4, data = [0x09, 0x00, 0x00, 0x00] }
### variables for sending data via UART
interconnectRegTiming = { size = 2, data = [0x00, 0x00] }
interconnectRegUart = { size = 2, data = [0x02, 0x00] }
interconnectSegmentSelector = { size = 2, data = [0x28, 0x00] }
}
MOVE_TO_INTERCONNECT interconnectSegmentSelector interconnectRegTiming c2
### initialization
MUL_4U toPrint c10 $st0 # stk[0] = toPrint * 10, sp = 4 (toPrint is 2b, so LEN would be used as high part for operation)
DIV_4U c3 $st0 $st0 # stk[0] = stk[0] / 3
SAVE_4U LEN # LEN = stk[0] (LEN is 2b, so high part, which is 0x0000, would be saved to nineCount)
MOVE_4U $st0 idx # idx = stk[0], sp = 0 (idx is 2b, so high part would be saved as first element for an array)
MOVE_2U c2 $st0 # stk[0] = 2, sp = 2
init_array:
SAVE_2U $data[idx] # arr[idx] = stk[0]
DEC_2U idx idx # idx--
EQUAL_ZERO_2U idx $st0 # stk[2] = (idx == 0), sp = 4
BRANCH_FALSE $st0 init_array # if (stk[2] === false) goto init_array, sp = 2
# XXX: only way to pop value from stack without extra access to memory
BRANCH_TRUE $st0 main_loop # sp = 0
main_loop:
ZERO_4U carry # carry = 0
### computation loop
MOVE_2U LEN denominator # denominator = LEN
ADD_2U denominator denominator $st0 # stk[0] = denominator + denominator, sp = 2
INC_2U $st0 numerator # numerator = stk[0] + 1, sp = 0
update_loop:
CONVERT_2U_4U $data[denominator] $st0 # stk[0] = arr[denominator], sp = 4
MUL_4U $st0 c10 $st0 # stk[0] = stk[0] * 10
ADD_4U $st0 carry $st0 # stk[0] = stk[0] + carry
SAVE_4U $st0 # stk[4] = stk[0], sp = 8
REMINDER_4U numerator $st0 $st0 # stk[4] = stk[4] % numerator
CONVERT_4U_2U $st0 $data[denominator] # arr[denominator] = stk[4], sp = 4
DIV_4U numerator $st0 $st0 # stk[0] = stk[0] / numerator
MUL_4U denominator $st0 carry # carry = denominator * stk[0], sp = 0
DEC_2U numerator $st0 # stk[0] = numerator - 1, sp = 2
DEC_2U $st0 numerator # numerator = stk[0] - 1 (numerator -= 2), sp = 0
DEC_2U denominator denominator # denominator--
EQUAL_ZERO_2U denominator $st0 # stk[0] = (denominator === 0), sp = 2
BRANCH_FALSE $st0 update_loop # if (stk[0] === false) goto update_loop, sp = 0
### output digits
MOVE_2U carry $st0 # stk[0] = carry, sp = 2
SAVE_2U $st0 # stk[1] = stk[0], sp = 4
GREATER_THAN_2U c9 $st0 $st0 # stk[1] = stk[1] > 9
SAVE_2U digitFromCarry # digitFromCarry = stk[1]
BRANCH_FALSE $st0 nextDigit_computed # if (stk[1] === 0) skip decrement, sp = 2
SUB_2U c10 $st0 $st0 # stk[0] = stk[0] - 10
nextDigit_computed:
SAVE_2U nextDigit # nextDigit = stk[0]
EQUAL_2U $st0 c9 $st0 # stk[0] = stk[0] === 9
BRANCH_FALSE $st0 print_digits
INC_2U nineCount nineCount
BRANCH main_loop
print_digits:
ADD_2U previousDigit digitFromCarry $st0
MOVE_TO_INTERCONNECT interconnectSegmentSelector interconnectRegUart $st0
DEC_2U toPrint toPrint
MOVE_2U nextDigit previousDigit
EQUAL_ZERO_2U nineCount $st0
BRANCH_TRUE $st0 check_done
print_nines_loop:
# either output 0x0009, or 0x0000, based on digitFromCarry
MOVE_TO_INTERCONNECT interconnectSegmentSelector interconnectRegUart c9[digitFromCarry]
DEC_2U toPrint toPrint
DEC_2U nineCount nineCount
EQUAL_ZERO_2U nineCount $st0
BRANCH_FALSE $st0 print_nines_loop
check_done:
EQUAL_ZERO_2U toPrint $st0
BRANCH_FALSE $st0 main_loop
### end of program
MOVE_TO_INTERCONNECT interconnectSegmentSelector interconnectRegTiming c2
RETURN_FROM_CONTEXT
Опять же, листинг не должен вызвать много вопросов, особенно с учётом ручного трассирования стека. Но отмечу несколько вещей:
Здесь наглядно видно, что iAPX 432 абсолютно не контролирует, как используются скалярные значения. Несмотря на всю свою объектно-ориентированность, GDP позволяет обращаться к int’ам как к short’ам. Да даже использовать их как массивы (взгляните на
c9[digitFromCarry]).Я нашёл только один способ для уменьшения указателя стека без дополнительного обращения к памяти:
BRANCH_TRUE $st0 main_loop.Ну и напоследок - деление и получение остатка это две разных операции, в отличие от других архитектур.
Замеры и выводы

На данный момент, iAPX 432 занял первое место в моём чарте по производительности. Оказался даже быстрее чем Intel 8080, который вычислял Pi по куда более продвинутому алгоритму Чудновского.
Но это не совсем честно - сравнивать процессоры разных поколений. Как насчёт современника - 8086? К сожалению, моя система на 8086 пока в нерабочем состоянии, но я воспользовался достаточно точным эмулятором (заявлена точность до такта) - 86Box. И оказалось, что iAPX 432 в среднем в 2.5 раза быстрее!
Как такое возможно? Этому результату способствовал ряд факторов - я преднамеренно избежал многих ловушек производительности, индуцированных компилятором Ada. Также, хоть ALU в iAPX 432 и 16-битное, производительность его на 16-битных и 32-битных операциях всё равно выше, чем у 8086. Ещё возможно повлияло то, что у меня нет дополнительных пропусков тактов при работе с памятью - она достаточно быстра, однако в эмуляторе я тоже постарался выбрать систему с похожими характеристиками, так что скорее всего данный фактор можно игнорировать.
Не скажу, что программировать под iAPX 432 мне понравилось, но вот сам путь к запуску своего кода на этой машине доставил мне удовольствие.
Все материалы (схема платы, gerber'ы, код для FPGA, код управляющей программы на ПК) можете найти в соответствующем репозитарии - https://github.com/quasiengineer/iapx432-sbc
Я записал несколько видео, в которых более подробно (по сравнению с текстовым форматом данной статьи) рассказываю о технических нюансах iAPX 432 и сопровождаю рассказ иллюстрациями и вырезками из различных доков. Если тема iAPX 432 интересна и не отторгает характерный славянский акцент в английской речи, то можете глянуть:

















