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

iAPX 432
iAPX 432

Однако из-за своей сложности архитектура провалилась. Существует несколько 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 на борту, которые покрывали бы все нужды процессора.

Спроектированная платка для запуска iAPX 432
Спроектированная платка для запуска iAPX 432

Кроме питания, необходимо было согласовать уровни сигналов (в то время многие микросхемы работали на 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_sio

Gateware

В качестве 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

Можно как-то так визуализировать иерархию объектов, которые формируют минимально возможный набор для запуска нашего кода.

Граф объектов, нужных для запуска GDP
Граф объектов, нужных для запуска 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.

Во-первых, нужно упомянуть, как работает трансляция указателей на объекты в физические адреса. Каждый указатель состоит из двух частей - индекс таблицы объектов и индекс самого объекта в таблице. То есть чтобы вычислить физический адрес, процессору нужно сначала найти адрес таблицы (через чтение центрального каталога, который и содержит адреса конкретных таблиц), а затем уже из этой таблицы прочитать дескриптор, в котором записан физический адрес.

уот так уот
уот так уот

Зачем такие сложности? А потому что нужно было реализовать несколько фич:

  1. Каждый указатель кроме адреса содержит права, которыми обладает этот указатель. Можно ли его использовать для чтения/записи/… Причём возможный набор прав зависит от типа объекта. Скажем, если у нас есть указатель на объект типа Processor, то можно разрешить его использование для конкретных высокоуровневых операций, таких как отправить сообщение процессору, или запросить его счётчик тактов (в памяти этой информации нет). Для объекта типа Process будет свой набор прав. А значит нужно проверять тип объекта (соответствующее поле в дескрипторе).

  2. Сборка мусора. Это всё же 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 регистр), а затем в зависимости от значения результата сравнения выполняем прыжок.

Hello world успешно получен
Hello world успешно получен

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 интересна и не отторгает характерный славянский акцент в английской речи, то можете глянуть:

Комментарии (16)


  1. unreal_undead2
    27.05.2026 07:04

    Подозреваю, что на чисто счётном бенчмарке на скалярах практически не работает логика по проверке прав доступа, влияющая на производительность. Вопрос, что будет в более реальном случае, когда обрабатываются массивы разных структур.


    1. mark_ablov Автор
      27.05.2026 07:04

      Вопрос, что будет в более реальном случае, когда обрабатываются массивы разных структур.

      Массивы структур это те же скаляры. ISA легко позволяет работать со структурами и массивами, используя простую арифметику под капотом.

      Другое дело кастомные типы (можно назвать их классами в терминах ООП) и объекты этих классов с полноценной системой доступа. Вот на них будет просадка.

      Но многие источники хаяли iAPX 432 как раз в задачах вычислительного толка, а не исследовали сценарии, для которых и была разработана данная система.


      1. unreal_undead2
        27.05.2026 07:04

        Я имею в виду "правильную" работу со структурами как объектами.

        Если смотреть чисто на вычисления - возможно, оно побыстрее 8086, но по честному надо сравниваться с процессором с таким же числом транзистором на той же тактовой частоте с другой архитектурой.


        1. DGG
          27.05.2026 07:04

          Насколько я понимаю, он должен был конкурировать не с PC на 8086 , а с нишами мейнфреймов и UNIX-рабочих станций.

          И вот по сравнению с тамошними конкурентами он был медленный.

          Интересно, кстати, 8087 - стековый, это глядя на iAPX432 сделали?


          1. unreal_undead2
            27.05.2026 07:04

            должен был конкурировать не с PC на 8086 , а с нишами мейнфреймов и UNIX-рабочих станций.

            Типа того - но из-за epic fail с ним пришлось развивать дальше линейку x86 (хотя в конце 80х Интел и в RISC поигрался, там вроде не так плохо получилось, но тоже не взлетело).

            8087 примерно в то же время делался, возможно команды общались.


  1. Javian
    27.05.2026 07:04

    "Гладко было на бумаге". Видимо там есть какая-то предыстория об этой архитектуре. Возможно в научных кругах в теоретических работах она обрела популярность.


    1. unreal_undead2
      27.05.2026 07:04

      У нас издавали книжку Органика, можно почитать на досуге.


  1. checkpoint
    27.05.2026 07:04

    Очень интересно,спасибо. Да, об этом процессоре написано во всех учебниках как о об одном и самых провальных технических решений и как демонстрация проблемы "второй системы" (по Бруксу).

    Не совсем понятно зачем Вам потребовалась частота работы с памятью в 250 МГц если у iAPX432 тактовая всего 5 МГц ?


    1. mark_ablov Автор
      27.05.2026 07:04

      250Мгц - это частота тактирования. Максимальная скорость работы памяти достигается в определенных условиях - скажем, если используется burst режим. В среднем нужно 4-5 тактов + всякие CDC и получаем уже числа порядка десятков мегагерц, а не сотен.

      Для чипов 432, которые у меня есть в наличии, этого достаточно (хотя есть и 8МГц версии), но, как всегда, я стараюсь использовать решения, проверенные в других проектах. И в одном из таких проектов, мне нужна была очень быстрая память, поэтому использовал эту связку, которая перешла и в дизайн для 432.


  1. Strijar
    27.05.2026 07:04

    Использовал в одном из своих проектов TPS63002 - тоже дохли как мухи


  1. cdriper
    27.05.2026 07:04

    перемудрили с тем, что надо было реализовывать на апаратном уровне, а что отдать на откуп софту


    1. unreal_undead2
      27.05.2026 07:04

      Я это рассматриваю как пример того, когда из одних посылок разные люди делают противоположные выводы. К концу 70х было общее понимание, что большая часть софта пишется на языках высокого уровня и архитектуру надо делать в основном для этого сценария, а не для пишущих вручную на ассемблере. В Интеле решили, что для этого железо должно само поддерживать высокоуровневые конструкции - и появился монстр 432 c аппаратной поддержкой работы с объектами и т.д. Более практичные люди посмотрели, как компилятор использует существующий набор команд и увидели, что и там и так много лишнего и лучше, наоборот, всё упростить - и появилась идеология RISC, живая до сих пор.


      1. cdriper
        27.05.2026 07:04

        у меня немного другие мысли, больше про защиту памяти и ее виртуализацию


        1. unreal_undead2
          27.05.2026 07:04

          К тому времени и то и другое уже давно использовалось (хотя бы на IBM 370).


          1. cdriper
            27.05.2026 07:04

            в массовом сегменте этого не было, IBM 370 это совершенно другой уровень


            1. unreal_undead2
              27.05.2026 07:04

              Перетащить технологию с большой машины на микрокомпьютер по мере роста возможностей последнего - это уже не инновация, а механическая инженерная работа. Да и то не справились - на том же IBM 370 изначально виртуальная машина давала весь функционал физической, включая запуск вложенных виртуалок.