Виртуальный процессор Simpleton 4

В позапрошлом уже году я писал тут про выдуманную под впечатлением от Gigatron TTL архитектуру процессора Simpleton 3.
Однако в итоге я пришёл к выводу, что Simpleton 3 как архитектура сложнее того чем оно заслуживает быть.
В ходе обсуждения на другом ресурсе родилась архитектура Simpleton 4 которая, имхо, проще, ёмче и всячески приятственнее для программирования.
Исходники эмулятора и ассемблера можно посмотреть тут: github.com/aa-dav/Simpleton4 (там же можно увидеть описание ISA на английском языке)

Прежде всего — эта ISA точно является субоптимальной по плотности команд.
Главная цель здесь — это сделать формат инструкций как можно более простым и ортогональным сохраняя программирование достаточно гибким и далёким от эзотеричности.
Плотность кода точно можно повысить получив в итоге что-то типа MSP-430, но это не цель и не вариант. :)
Как и в предыдущем поколении всё — и ячейки памяти и регистры 16-битные для простоты.
В отличие от Simpleton 3.x который каждую команду рассматривал как команду Си вида if (cond) Y ?= X, где? — это код операции Simpleton 4 смотрит на вещи еще более просто.
Он рассматривает все инструкции как одну и ту же операцию: R = Y * X, где опять таки * — это код операции. Т.е. безусловная трёхоперандная система.


Все инструкции имеют один и тот же формат:

Теперь совсем чётко формат инструкции распался на нибблы и последние три ниббла — это коды регистров и флаги их индирекции (косвенного чтения). Первый же ниббл — это код операции.
Таким образом всё что умеет делать этот процессор в каждой операции — это считывание аргументов X и Y, запись их в ALU вместе с кодом операции и запись полученного результата из ALU в R.

— Всё (включая ячейки памяти) 16-битное
— 8 регистров (нельзя сказать что «общего назначения», т.к. других тут просто нет) R0-R7
— последние три имеют псевдонимы:
R5 — SP — указатель стека — при косвенном считывании пост-инкрементируется, а при косвенной записи пре-декрементируется
R6 — PC — счётчик инструкций — автоматически инкрементируется при косвенном считывании. Косвенная запись в PC является делом абсурдным, поэтому закреплена (спасибо за идею Lethargeek!) за отменой записи. Т.е. АЛУ проводит операцию и обновляет флаги, но если назначение — [ PC ], то запись результата из АЛУ никуда не производится.
R7 — PSW — processor status word — регистр флагов (включая, например, бит запрета IRQ). Косвенное чтение/запись по этому регистру просто не имеют смысла, поэтому «косвенный PSW» включает особый режим адресации — считывается непосредственное данное из [ PC++ ] и служит адресом ячейки памяти с которой мы работаем. Это режим адресации 'immediate address'.

Непосредственные данные реализуются как косвенное чтение PC, т.к. после считывания опкода PC уже смотрит на следующее слово в памяти и после его косвенного чтения продвинется дальше (как и всегда). Если нужно считывать операнды из памяти при косвенных чтениях, то в машинных циклах это происходит в следующей последовательности: X, Y и затем R.
Некоторые инструкции воспринимают поле X (включая бит индирекции XI) как 4-битное непосредственное данное. Чаще всего со знаком: -8..+7, но иногда и без знака: 0..16. Этот режим считывания операнда X называется 'inplace immediate' и обозначается в ассемблере литерой 'i' в инструкции.

Рассмотрим на примере типовой инструкции add возможные режимы адресации Simpleton 4:

add r0 r1 r2		; сложение регистров r0 = r1 + r2
add [ r2 ] 100 R4		; сумма R4 с непосредственным данным (PC+indirect) 100 записывается в память по адресу R2
add [ R3 ] R4 [ $200 ]		; сумма R4 c [ $200 ] (immediate address) записывается в ячейку памяти по адресу R3
add [ 100 ] [ 20 ] [ 30 ]	; не загрязняющая регистры (кроме флагов) команда работающая непосредственно с тремя ячейками памяти


Обратите внимание, что ассемблер придерживается немного нестандартных соглашений:
— все лексемы (включая даже скобки!) обязательно разделяются пробелами
— поэтому не используются запятые для разделения аргументов
— поэтому сложные compile-time выражения (сейчас они не реализованы) надо окружать круглыми скобками, за исключением когда выражение уже находится в квадратных, т.е.:

add [ label1 + offset2 ] ( label2 + offset3 ) ( $1000 / 2 ) ; три "сложных" аргумента


Всего возможно 16 опкодов для АЛУ. Из них сейчас использовано 14:

00 ADDI — сложить Y с INPLACE данным в XI+X (-8..+7) (и как всегда результат поместить в R)
01 ADDIS — сложить Y с INPLACE данным in XI+X втихую/Silent (без обновления флагов)
02 ADDS — сложить Y с X втихую/Silent (без обновления флагов)
03 ADD — сложение
04 ADC — сложение с переносом
05 SUB — вычитание
06 SBC — вычитание с переносом
07 AND — логическое И
08 OR — логическое ИЛИ
09 XOR — логическое исключающее ИЛИ
0A CADD — условное сложение. Не обновляет флаги. См. ниже.
0B RRCI — циклический сдвиг Y вправо на INPLACE (0..15) число бит. Во флаге переноса копируется последний «прокрученный» бит.
0C RRC — циклический сдвиг Y вправо на на X бит. Во флаге переноса копируется последний «прокрученный» бит.

Пояснения

1. В этой ISA нет отдельного опкода MOVE (но есть такая мнемоника), т.к. он осуществляется через опкод ADDIS с нулём в INPLACE-аргументе X. Этот аргумент в ассемблере можно опускать:

addis r0 r1 1 ; добавляем 1 к r1 и помещаем результат в r0
addis r0 r1 0 ; добавляем 0 к r1 и помещаем результат в r0
move [ r2 ] r1 		; для самоописуемости мнемоника move кодирует addis с нулём в inplace X
move [ r3 ] [ 100 ]	; копируем из ячейки памяти с адресом 100 в ячейку памяти куда указывает регистр r3


2. Запись значения в PC это переход (jump), а adds pc pc offset — это относительный переход:

move pc address		; прыжок
adds pc pc offset	; относительный прыжок


3. Условное сложение главным образом нужно для реализации условных переходов, но в принципе может быть использовано и много для чего еще.
Работает оно следующим образом: аргумент X (16 бит) разделяется на две части: верхние 4 бита воспринимаются как код условия, а нижние 12 бит расширяются со знаком в слагаемое (-2048..+2047).
Т.е. код условия это не часть опкода, но часть данных! Это навскидку уже может послужить для каких то сумрачных хаков и полухаков. :)
Если условие срабатывает, то АЛУ возвращает в качестве результата Y+X, иначе Y.
Таким образом чтобы реализовать условный (и всегда относительный) переход мы делаем:

cadd pc pc код_условия_со_смещением

Ассемблер для простоты имеет псевдокоды «j** метка», где ** — это мнемоника кода условия, например:

jnz label

Если следующие коды условий (довольно классические):

z, nz, c, nc, s, ns, o, no - тест на вкл и выкл соответствующих флагов
a - "above" - беззнаковое "выше"
be - "below or equal" - беззнаковое "ниже или равно"
ge - "greater or equal" - знаковое "больше или равно"
l - "less" - знаковое "меньше"
g - "greater" - знаковое "больше"
le - "less or equal" - знаковое "меньше или равно"

4. Вызов процедуры не может быть реализован в один опкод и требуется две инструкции и три слова:

addis [ sp ] pc 2 	; вычисляем адрес возврата с занесением его в стек
move pc proc_address	; переходим на процедуру (просто jmp)

Для привычности и краткости я посчитал полезным иметь call как псевдоинструкцию в ассемблере:

call proc_address	; раскрывается в код приведённый выше

Однако возврат из процедуры — уже одна инструкция в одно слово:

move pc [ sp ]		; Опять таки для краткости можно записать типичным 'ret'


5. Инструкция ADDI может быть использована (с 0 в immediate X) как «тестирующий move» (псевдоинструкция 'movet'):

StrCpy: 		; R1 - указатель на src, R2 - указатель на dst
  movet [ r2 ] [ r1 ] 	; опкод addi x y 0
  jz Exit
  addis r1 r1 1		; инкремент r1
  addis r2 r2 1		; инкремент r2
  move pc StrCpy	; прыгаем в начало цикла
Exit:
  ret 			; опкод move pc [ sp ]


6. Включение/выключение прерываний, т.е. выставление соответствующего бита в регистре флагов может быть сделано как:

and psw psw маска_флага ; включить
or psw psw ~маска_флага ; выключить (~маска_флага это побитовая инверсия маски флага)


7. Если в качестве результата ® указывать ключевое слово 'void', то это означает запись в [ PC ] которая приводит к тому, что запись результата из АЛУ никуда не производится. Так в Simpleton 4 можно сделать сравнение:

sub void A B ; экивалентно cmp A B во многих других ISA
jnz ...

или побитовые тесты:

and void r0 $0001
jz ...

или сравнение числа с константой в диапазоне -8..+7 за инструкцию в одно слово (inplace immediate):

addi void r0 -3
jz ... ; r0 равен 3

или проверка i-ого бита операнда через занос его в CF операцией RRCI:

rrci void r0 3 ; третий бит r0 попадёт в CF
jc ... ; переход если CF=1


Если «замапить» в эмуляторе на адрес $FFFF порт ввода-вывода консоли (пока — Windows), то следующая программа уже (давно) собирается и работает:

PORT_CONSOLE    = $FFFF

      move sp $70	; Setup stack

      move r0 str0	; shortcut for addis r0 str0 0
      call str_print	; shortcut for addis [ sp ] pc 2 : addis pc str_print 0
      move r0 str0
      call str_print
      dw 0 ; STOP

str_print   movet r1 [ r0 ]
      jz .exit      ; shortcut for cadd pc pc mix_of_offset_and_condition_code
      move [ PORT_CONSOLE ] r1   ; output char to console
      addi r0 r0 1   ; increment r0
      move pc str_print   ; jump to the beginning of procedure
.exit      ret         ; shortcut for move pc [ sp ]


str0      dw "Hello, world!" 10 0


Новый синтаксис ассемблера

Что меня с самого детства отталкивало в ассемблере — это его неинтуитивность и некраткость.
Очень часто ловил себя за комментариями к коду вида 'HL = BC + 16'.
Поэтому в Simpleton 3 я реализовал математическую нотацию где операции писались в стиле Си как 'R0 += [ label ]'.
Simpeton 4 сразу оказался не совсем тривиальным к реализации такого синтаксиса простым переписыванием, поэтому я в нём пошёл на такой шаг как реализацию классического ассемблерного синтаксиса. Все эти addis, move и так далее.
Но решимости сделать мат-нотацию не убавилось и я её реализовал тоже.

В ассемблере инструкциями 'mode classic' и 'mode new' можно переключаться между режимами синтаксиса.
Новый «математический» синтаксис большую часть операций выражает как 'R = Y op X' где 'op' это символ операции.
Если X пропущен, то он становится литералом '0', если же опущен 'op', то он становится символом '+'.
Следующие инструкции попадают под этот шаблон целиком и полностью (приведён пример для R=R0, Y=R1 и X=[ label ]) (новый синтаксис написано после двоеточия):

02 ADDS : r0 = r1 +s [ label ] ; сложение без обновления флагов
03 ADD  : r0 = r1 +  [ label ] ; сложение
04 ADC  : r0 = r1 +c [ lavel ] ; сложение с переносом
05 SUB  : r0 = r1 -  [ label ] ; вычитание
06 SBC  : r0 = r1 -c [ label ] ; вычитание с переносом
07 AND  : r0 = r1 &  [ label ] ; логическое И
08 OR   : r0 = r1 |  [ label ] ; логическое ИЛИ
09 XOR  : r0 = r1 ^  [ label ] ; логическое исключающее ИЛИ
0A CADD : r0 = r1 +? [ label ] ; условное сложение
0C RRC  : r0 = r1 >> [ label ] ; прокрутка вправо

Но три опкода (на сейчас) выпадают из этой схемы и имеют следующий синтаксис:

00 - ADDIS : r0 <- r1 - 1         ; сложение с immediate без обновления флагов
01 - ADDI  : r0 <= r1 + 3         ; сложение с immediate
0B - RRCI  : r0 <= r1 >> 15       ; прокрутка вправо на immediate число бит

Все команды с immediate получают символ '<' справа от равенства, плюс addis не обновляющее флаги использует знак '-' вместо '='.
Заметьте, что 'addis a b -1' (отрицательный immediate) может быть в новом синтаксисе написано как 'a < — b + -1' и как 'a < — b — 1' (как будто это вычитание).
Move в новом синтаксисе пишется просто как 'a < — b', что уже является сокращением от 'a < — b + 0'.

Заметьте так же, что псевдоинструкции введённые в ассемблер обычного синтаксиса для простоты сохраняют свою силу и в новом синтаксисе — jnz/call/ret и т.д.

Таким образом пример выше может быть переписан на новый манер так:


            mode new

PORT_CONSOLE    = $FFFF

            sp <- $70       ; Setup stack

            r0 <- str0      ; shortcut for addis r0 str0 0
            call str_print
            r0 <- str0
            call str_print
            dw 0 ; STOP

str_print   r1 <= [ r0 ]           ; testing move (addi r1 [ r0 ] 0)
            jz .exit           
            [ PORT_CONSOLE ] <- r1 ; output char to console
            r0 <- r0 + 1           ; increment r0
            pc <- str_print        ; jump to the beginning of procedure
.exit       ret                    ; shortcut for move pc [ sp ]

str0        dw "Hello, world!" 10 0

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

avatar
просмотрел очень бегло, но сразу же несколько замечаний и предложений:

1) куча пробелов выглядит хуже пары запятых и занимает больше места в исходнике
2) интересный вопрос, как устанавливаются флаги, когда приёмник — psw (как регистр) :)
3) не хватает прокрутки ЧЕРЕЗ перенос, а также сдвигов — для ускорения арифметики и возможной битовой графики
4) хорошо бы еще один регистр с автоинкрементами — для шитого кода (мб включать автоинкременты для регистров спецфлагами?)
5) зачем тратить опкод на отдельную команду cmp? когда можно принять запись в [psw] за игнор результата, и она появляется «бесплатно», так же как test и прочие логические аналоги (плюс тогда мб даже имеет смысл сделать сохраняющие результат обычные and/or/xor не устанавливающими флаги)
avatar
1) куча пробелов выглядит хуже пары запятых и занимает больше места в исходнике
Тут забавно — изначально я просто «по быстрячку» (а исходники Simpleton-ов всегда развивались таким путём) воспользовался вводом строк из потоков C++, которые ведут себя именно таким образом — просто вводят от текущей позиции до следующих пробельных символов.
И думал что в режиме теста так проковыляю какое то время, а потом заменю на типовую схему. Однако уже и есть функция extraсtLexem, но… мне так понравилось! :)
Во первых я всегда следую (и в данном случае — всем рекомендую) орфографической традиции — после знаков препинания типа точек или запятых ставить пробелы. Это естественно как дышать и видно даже на этом самом тексте. Поэтому запятые только добавляют один символ к разделителю-пробелу в массе случаев. Их настоящая нужда в мейнстриме — это помощь с отделением друг от друга аргументов со сложными выражениями, что встречается относительно нечасто и может быть разрулено явными скобками.
Ну и просто попробовав я как то втянулся. Функцию extractLexem конечно можно переписать и получить более привычное поведение хотя бы со скобками, но мне после такой практики на это совершенно не хочется тратить время. :)

2) интересный вопрос, как устанавливаются флаги, когда приёмник — psw (как регистр) :)
Действительно отличный вопрос! Сейчас в эмуляторе АЛУ сперва обновляет биты арифметико-логических флагов как бы «напрямую» как если имеет в регистр флагов прямой вход через заднюю дверь (что весьма правдоподобно звучит для схемотехники, не зря он находится «с краю» регистрового файла), а уже после этого основное ядро извлекает результат из АЛУ и пишет его в регистровый файл. Т.е. запись в PSW ведёт себя довольно практично — изменение арифметических флагов будет забыто и операция вида «and psw psw маска» изменит только желаемый бит.
Однако возможно что ради скорости в схемотехнике эти операции могут происходить параллельно и результат сохранения в арфметико-логические флаги будет неопределён. Это может иметь некоторые последствия при обработке прерываний, но обдумав я нахожу что реальных проблем не будет. Поэтому на данный момент наверное лучше исходить из такого предположения.

> 3) не хватает прокрутки ЧЕРЕЗ перенос, а также сдвигов — для ускорения арифметики и возможной битовой графики
Я поэтому замер пока на операциях прокрутки и думаю какие лучше инструкции разместить в оставшейся части.
Во первых — по заветам MSP-430 операции ADD и ADC можно использовать как сдвиг и прокрутку влево:
add r0 r0 r0; сложение само с собой это умножение на 2 и сдвиг на 1 бит влево со вновь входящим нулевым битом
adc r0 r0 r0; сложение с переносом это сдвиг на 1 бит влево со входящим Carry и тут же исходящим Carry, т.е. идёт прокрутка влево через Carry, причём _через_ перенос.
Поэтому тот же MSP-430 не вводил аналогичных команд прокрутки и я не собираюсь.
Но вот прокрутка вправо уже должна быть в явном виде и чтобы побороть несколько проблем я пока решил сделать число прокручиваемых бит настраиваемым, а саму прокрутку — циклической с занесением в Carry, но не ЧЕРЕЗ Carry.
Почему так — потому что можно имитировать еще полезную команду SWAP BYTES просто прокрутив на 8 бит. Байты в слове поменялись местами.
Может быть полезно для упаковки и совместимости с типовыми файлами.
С другой стороны прокрутка вправо на 15 бит это же и прокрутка влево на 1 бит. Появляется взаимозаменяемость.
Далее — если кровь из носа нужен сдвиг влево/вправо на программируемое число бит мы можем подстроить счётчик чтобы нужные биты прокрутились куда требуется, а потом обнулением по «AND маска» занулить ненужные биты.
Так что как минимум беззнаковое деление на степени двойки делается. А если предварительно сделать условный переход по биту знака и в этой ветке сделать «OR маска», то и знаковые числа тоже можно будет делить и сдвигать таким образом.
Вот на этих размышлениях я пока подвис и пока еще думаю. Нужна практика наверное реального программирования на Simpleton 4 чтобы почувствовать что нужно остро и кровь из носу, а что достаточно сэмулировать серией базовых команд.
Тот же Gigatron вообще сдвиги делал через лукап-таблицы (!).

4) хорошо бы еще один регистр с автоинкрементами — для шитого кода (мб включать автоинкременты для регистров спецфлагами?)
Регистров в духе 8-биток тут немного, поэтому специализированного регистра точно не будет. А вот сделать в регистре флагов на первые 4 регистра по два флажка — «пост-инкремент» и «пре-декремент» — это интересная мысль, за неё спасибо. Её надо обдумать, хотя есть минусы. Например при входе в прерывания эти флаги надо обнулять и потом восстанавливать. Но! То как я планирую сделать вход в прерывания с этим похоже что прекрасно справляется. Да да да… Интересно…

5) зачем тратить опкод на отдельную команду cmp? когда можно принять запись в [psw] за игнор результата
А вот это супер-идея! Спасибо! Только и косвенное чтение и косвенная запись в PSW в Simpleton 4 уже заняты — это режим адресации immediate address с помощью которого можно вообще не загрязнять регистры:

[ $1000 ] = [ $1001 ] +s [ $1002 ] ; ни один регистр (включая флаги) кроме PC не изменился

Однако! Еще с Simpleton 3 была запрещена косвенная запись в PC! Здесь этот запрет сохранился, но я его не упомянул.
Пытаться писать прямо в поток выполняемых команд — это как минимум странно. :) Поэтому именно за [ PC ] в поле R и логично закрепить новый смысл — «игнорирование записи результата из АЛУ в файловый регистр или память». Гениально! :D
Действительно высвобождается специализированная инструкция CMP с одной стороны, а с другой стороны появляются булевы тесты и, например, такая экзотичка, как с помощью команды из одного слова — RRCI поместить в Carry любой на выбор бит из операнда. xD
Блин, это прикольная идея! Прям спасибо!
avatar
P.S.
Реализовал псевдорегистр 'void' и поведение с косвенной записью в PC. Теперь освободился еще один опкод — CMP.
Теперь сравнение делается так:

sub void A B ; экивалентно cmp A B во многих других ISA
jnz ...


побитовые тесты (любые в OR/XOR/AND):

and void r0 $0001
jz ...

… или сравнение числа с константой в диапазоне -8..+7 за инструкцию в одно слово (inplace immediate):

addi void r0 -3
jz ... ; r0 равен 3

… или проверка i-ого бита операнда через занос его в CF операцией RRCI:

rrci void r0 3 ; третий бит r0 попадёт в CF
jc ... ; переход если CF=1

Ляпота! Еще раз спасибо за идею! :)
avatar
да, [pc] конечно, что-то перемкнуло меня) главное, что ты понял верно

по сдвигам надо бы всё тщательно взвесить, одинарный сдвиг для быстрой арифметики маловато

насчёт асма, на мой взгляд, задятые достаточно заметно отделяют операнды (особенно на шрифтах фиксированной ширины), ну, и если хочется, никто же после них пробелы не запрещает! и второе, нужны ли вообще квадратные скобки? префикс в большинстве случаев лаконичней:

add [ 100 ] [ 20 ] [ 30 ]
add @100 @20 @30

скобки только для сложных выражений:

add @(label1+offset2) @(label2+offset3) @($1000/2)

хотя при наличии запятых необязательны и они, префикc действует на всё выражение:

add @label1+offset2, @label2+offset3, @$1000/2


также, void как по мне длинновато, может, применять какой-то другой значок или даже «пустое выражение»?

()

ну, или просто псевдами выражать (те же cmp, test...)
avatar
Мне сейчас уже синтаксисом заниматься неохота особо — я реально на нём попробовал как он есть и меня реально устроило. Даже как то понравилось когда ассемблерный текст растягивается в ширину с чётко выделенными пробелами всеми абсолютно местами.
Как я понял это дело привычки, поэтому тратить на это время уже не хочу.
Чем буду в свободное от других хобби время далее заниматься — это миграцией под SDL чтобы сделать прообраз виртуального ПК с клавиатурой и дисплеем и на нём реально пробовать что там со сдвигами применительно к скроллингам всяким и парсер сложных compile-time выражений.
Хотя еще наверное преобразую парсер самого ассемблера к варианту с паттерн-матчингом — при нём проще будет новые синтаксисы внедрять, кстати.
avatar
Здравствуй трех адресный PDP-11 :) Неплохо! Очень!

Есть желание перебросить процессор в Verilog, откатать на визуальном симуляторе цепей Digital и потом в FPGA?! Для Digital можно даже IDE с боку прицепить что бы красиво отлаживать с точками останова и пошагово.
  • SAA
  • 0
avatar
Есть желание перебросить процессор в Verilog, откатать на визуальном симуляторе цепей Digital и потом в FPGA?!
Сам я далёк от паяльника и программатора сильно, но отладив в эмуляторе реализовать в железе — весьма завлекательная идея.
Но лично сам — если только много лет спустя на пенсии, ибо сейчас изучение нового мира схемотехники для меня неподъёмный груз в рамках хобби.
Но если кому то опытному будет любопытно реализовать — я только за. В принципе архитектура open source-ная так что хоть форки делать со своими расширениями команд. :)
avatar
Паяльник и не предлагаю, нет-нет! Посмотрите как в Digital приятно симулировать архитектуру, в данном случае к корке 6502 на verilog подцеплена графическая память и на Verilog доделан модуль ускорителя графики. Удобно, интересно и никаких паяльников. Причем как только перестанет хватать скорости симулятора, можно уйти в реальную железку (без какой либо пайки) и продолжить жить в собтвенной архитектуре на огромных скоростях. Например вышеупомянутая корка 6502 живет на 50МГц в довольно слабом Spartan-6.
Работа с Verilog модулями в симуляторе Digital на чатсоте 3KHz
Если Вы уж взялись за проектирование архитектур процессоров, то считай катитесь по стенкам воронки. Да и грех себе отказывать в таком удовольствие до самой пенсии. Может оформите как продолжение статьи? Я бы помог разобраться :)
avatar
Если Вы уж взялись за проектирование архитектур процессоров, то считай катитесь по стенкам воронки.

В любом случае я сперва напишу эмулятор, оформлю архитектуру видеочипа и т.п., а главное — софт разный под это дело напишу — тогда только буду задумываться всерьёз насчёт физических реализаций. У меня в самом деле с детства очень напряжённые отношения с паяльником и сидеть травить платы и подпаивать сотни проводков — не вызывает энтузиазма вообще. Я не железячник, я больше по софту.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.