8/16-битный компьютер мечты (процессор Simpleton)

Американский видеоблоггер 8-bit-guy давненько уже поднял и периодически ворошит такую тему как «какой мог бы быть 8-битный компьютер моей мечты» — со своим видением вопроса. Эти его видео напомнили мне что сама эта идея «а как бы я хотел чтобы было устроено нутро компьютера» сопровождает меня и, я верю, что и вас с самого детства. И так как мы тут рассуждаем в рамках ретро, то вопрос стоит именно как в заголовке.
Тему эту в вялотекущем режиме я уже обсуждал тут: gamedev.ru/flame/forum/?id=242499 (читать совсем необязательно) и раз уж здесь у нас возникло какое то летнее затишье, то решил немного перенести оттуда сюда уже в виде некоторых выжимок для «просто поболтать».


Сердцем «компьютера мечты» является процессор и я тут решил не размениваться по мелочам и прежде всего «изобрести» именно некую систему команд «8-битного процессора мечты» для 8-битного компьютера мечты, чтобы уж совсем рубить под корень. Да ладно — все мы наверняка в своё время и не раз задумывались над «идеальным процессором» так чего тянуть кота за хвост? Мечтать так мечтать!
Сразу обозначу — что в схемотехнике познания мои весьма скромны — на уровне институтского курса про триггеры, счётчики и сумматоры, так что всё это, повторюсь, лишь фантазии для помечтать, пообсудить и, возможно, придумать что-то своё.

Из последнего на меня произвел определенное впечатление Gigatron TTL в котором преследовалась цель простоты и минималистичности в ущерб всему. Но впечатление произвела совсем не эта цель (напротив, она мне тут неинтересна — не о том мечты), а довольно забавная концепция, когда команда LOAD является всего лишь частным случаем обобщенной арифметико-логической команды в которой при пересылке источника в приёмник попутно всегда проводится операция пропускаемого данного с аккумулятором, просто в случае с LOAD эта субоперация — NOP. Тем не менее в Gigatron есть разные форматы команд — STORE и переходы уже не подчиняются той же логике, а вот я подумал — а что если такой совмещенный с опциональными вычислениями «MOV» будет единственной командой в процессоре вообще?
Итак родилась и проэволюционировала из Simpleton 1.0 в Simpleton 3.0 следующая архитектура:
1. Все инструкции имеют один и тот же бинарный формат и декодируются совершенно одинаково
2. Команда берет аргумент1 и (опционально, если инструкция двухоперандная) аргумент2, проводит над ними операцию (возможно NOP) и записывает результат в аргумент2.
3. Все команды условные (включая условие always/всегда)
Т.е. суть одной команды Simpleton 3 можно выразить в языке Си такой строчкой:
if (cond) Y ?= X;

где знак? — это операция наподобие Y += X или Y <<= X в том числе и просто Y = X, а X и Y — это регистры или ячейки памяти.
например:

R0 = data1 ; непосредственное данное
[mem2] += R4 ; запись в память
R1 =+1 [R2] ; косвенная адресация, а =+1 - это операция инкремента аргумента

Когда в инструкцию нужно запаковать и два аргумента и код операции и код условия, то одного байта становится решительно мало. Бит в инструкции вообще решительно не хватало, поэтому чтобы уместить всё самое необходимое я наступил себе на горло и превозмог все 8-битные инстинкты и сделал еще одно ключевое решение:
Архитектура Simpleton 3 полностью 16-битная и не знает понятие 8-битного байта вообще.
Т.е. и регистры и ячейки памяти и адресное пространство Simpleton 3 — всё 16-битное.
И чем больше я об этом думаю тем больше восхищаюсь тем сколько удачна идея отказаться от 8 бит. 8-битные системы всегда стояли нараскоряку между 8-битными данными и 16-битным адресным пространством. Адресное пространство уже требовало 16-битных операций для вычислений адресов, да и сами 16-битные числа в вычислениях были зачастую востребованней — в результате всё это мялось с ноги на ногу какими то полухаками и расширениями, Z80 так вообще имел 16-битные сложения/вычитания с переносом — просто кишмиш какой то. Более того когда созрели 16-битные микропроцессоры уже даже 8-битки успели вылезть за рамки 64Кб и начались метания с банкингом, свитчингом и сегментацией памяти — всё это уродливо, напряжно и прочно ассоциируется с 16 битами.
Так вот нахрен. Нахрен 8-битный байт и всё тут. 16 бит адреса — значит и 16 бит данных. Без исключений. 16-битный процессор пусть будет 16 битным во всём. И но пасаран! Мы просто зашорили свой мозг 8-битным байтом как чем то безусловно обязательным и лежащим в фундаменте всей информации. Но это вообще не так.

Заранее еще напишу какие цели Simpleton 3 НЕ преследует:
— он не является сверхпростым как Gigatron — сложность на уровне Z80 или чуть выше
— он не пытается выполнить команду за такт в четырёхстадийном конвеере
— он не является сверхпроизводительным RISC-ядром с высоким потенциалом к реализации спекулятивного предсказания переходов, внеочередного исполнения команд и трёх уровней кеша для использования в качестве современного и актуального микроконтроллера для обработки изображения в нейронных сетях четвёртого поколения

Итак,

Архитектура Simpleton 3


Девиз: «не такой уж я и тупица!» (кстати, «simpleton» с английского — типа «недалёкий»)

Все шины и данные — 16 битные. Т.е. адресное пространство: 65536 16-битных ячеек. Байтов в системе в принципе нет.
В процессоре 7 регистров общего назначения R0-R6.
При этом R6 является счётчиком инструкций и имеет еще псевдоним PC.
R5 имеет особое поведение при косвенной адресации (автоинкремент при чтении и автодекремент при записи) и имеет псевдоним SP (стек).
R4 содержит флаги процессора и имеет псевдоним FLAGS.
При этом в нём же содержится такая штука как бит запрещения/разрешения прерываний поэтому команд типа EI/DI как в Z80 тоже нет, нужно просто произвести запись во FLAGS.
В FLAGS есть 4 бита арифметико-логических флагов:
— флаг нуля (zero)
— флаг переноса (carry)
— флаг знака (sign)
— флаг переполнения (overflow) (carry для знаковых)
Вполне себе стандартный набор.
Когда инструкция считывается из памяти сперва считывается её код по текущему PC и PC тут же инкрементируется. Т.е. на момент фазы парсинга инструкции PC уже указывает на следующую команду.
Если у инструкции есть непосредственные данные (от 0 до 2), то их считывание происходит в процессе выполнения команды с тем же эффектом — считывается данное по адресу PC и последний тут же инкрементируется.

У всех инструкций один и тот же формат:



Рассмотрим битовые поля в таком порядке в каком их обрабатывает логика микропроцессора после считывания инструкции:
SRC — код регистра-источника (R0-R6) или численное значение 7 для обозначения непосредственного операнда следующего за инструкцией. Первым делом процессор копирует это значение в во внутренний регистр X.
IS — (Indirect Source) признак косвенной адресации источника. Если равен 1, то значение в X замещается значением ячейки памяти по адресу X. При этом, если SRC является SP, то SP инкрементируется (аналог POP).
Флаг TI (two operand instruction) — признак двухоперандной инструкции. Если TI = 1, то данное DST, т.е. то куда будет в конце операции записан результат из АЛУ нужно перед выполнением операции еще поместить во второй промежуточный регистр Y. X и Y пойдут в АЛУ как раз как входы.
ID — (Indirect Destination) признак косвенной адресации приёмника. Если он включен и инструкция предполагает использование двух операндов (взведённый флаг TI), то значение Y вычисленное на предыдущем шаге помещается в регистр STORE_ADDR и замещается значением ячейки памяти с этим адресом. В конце инструкции именно в адрес STORE_ADDR запишется результат.
COND — (condition) — условие. Восемь возможных условий таковы:
Always — всегда
Z/NZ — ноль/не ноль
C/NZ — перенос/не перенос
G/GE — больше/больше или равно (знаковые сравнения)
восьмое пока не придумал куда кинуть
В общем на этом этапе, если условие не выполнилось, то остаток инструкции можно пропустить и перейти к следующей. Вообще по здравому размышлению условие надо проверять первым действием на входе в инструкцию и пропускать сразу всё, но не забывать инкрементировать PC на количество непосредственных данных, но по моему это усложнит логику и поэтому лучше поместить проверку условия технически в это место, где immediate-данные уже считаны и PC на них выправлен.
INSTR — код инструкции. Т.е. получается возможно 16 одноперандных инструкций и 16 двухоперандных.
Однооперандные: mov/neg/inc/dec/incf/decf/…
(incf/decf меняют флаги в то время как inc/dec — нет)
Двухоперандные: add/adc/sub/sbc/and/or/xor/cp/…
Пока лень обдумывать конкретику. Но, например, уже ясно, что инструкция сравнения должна принимая SRC и DST на входе и выставив флаги возвращать из АЛУ именно DST чтобы его не испортить по сравнению, как полагается.
На последнем шаге процессор сохраняет результат на выходе из АЛУ в DST и если это была индирекция через через регистр SP, то декрементирует его.

В общем как я и писал выше общая схема инструкций в этом процессоре это:
Y ?= X
где X может быть одним из 7 регистров R0-R6, непосредственным данным imm16 или ячейкой памяти по адресу любого из ранее перечисленного, т.е. [R0-R6] или [addr16]
тем же самым может быть Y, кроме варианта imm16 который не имеет смысла в операции записи
а знак? — это одна из арифметико-логических операций

Существует одна запрещенная комбинация инструкции когда DST является imm16, а бит индирекции ID не зажжён. Она зарезервирована под гипотетические будущие расширения команд.
Так же запрещено в рабочем коде использовать mov из регистра в него самого же, если это не R0 (т.е. явный NOP) тоже для возможности будущих расширений и префиксов.

Небольшой FAQ

Какие у Simpleton 3.x есть необычности или подводные камни:

1. Если простой переход (jump) это:

PC = addr16

а относительный переход как:

PC += offset16

то с вызовом процедуры (call) эта «однокомандная» система в одну команду не пролазит.
тут нужно делать примерно таким образом:

[SP] = адрес возврата
PC = адрес процедуры

и тут вот какая неприятность — адресы тут являются immediate-данными и таким образом эти две инструкции будут занимать целых четыре слова. не очень хорошо.
т.к. чаще всего мы хотим вернуться на код следующий за этими инструкциями, то в [SP] надо занести значение PC+2 (на момент выполнения первой инструкции PC указывает на следующую, а надо еще пропустить immediate-данное). И вот чтобы это стало возможным нужна двухоперандная инструкция не только inc, но и inc2. Тогда да — в две инструкции:

[SP] =+2 PC
PC = адрес процедуры

мы делаем вызов процедуры
А вот совершить возврат из неё очень просто:

PC = [SP]

тут уже всё делается в одно действие.
конечно в ассемблере call можно будет сделать макросом.

2. Как разрешить/запретить прерывания не затрагивая арифметико-логические флаги в FLAGS?
Да очень просто:

FLAGS &= константа ; для сброса 
FLAGS |= константа ; для взведения

Т.е. не просто перезаписью, а через логические операции с операндом-константой.

3. Что именно делают прерывания?
Как и в классических процессорах помещают в стек сперва PC, потом FLAGS и совершается переход по заранее определенному адресу.
Таким образом возврат из прерывания должен совершаться так:

FLAGS = [SP] ; заодно разрешаеются прерывания, т.к. восстанавливается старый FLAGS
PC = [SP]

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

avatar
Идея процессорных архитектур с единственной операцией существует довольно давно. en.wikipedia.org/wiki/One_instruction_set_computer

Касательно восьми бит, это свойство конкретно ранних микропроцессоров, возникшее из ограничений технологии того времени. До них были архитектуры с самым разным количеством бит в слове, и вовсе не обязательно это была степень двойки или чётное число. Примеры: en.wikipedia.org/wiki/Word_(computer_architecture)#Table_of_word_sizes
avatar
Ну, строго говоря тут не совсем «процессор с одной инструкцией». Если смотреть под немного другим углом — то в нём столько инструкций сколько есть кодов у поля INSTR + бит TI, просто все инструкции соблюдают строго один и тот же принцип — они берут X и возможно Y, проводят над ними операцию и записывают в Y. Ввиду того, что всё состояние процессора находится полностью в его РОН — такая схема достаточна для реализации практически всего что машине как правило надо — а то что прямо в это не укладывается (как CALL) вполне можно реализовать двумя операциями.
Т.е. любая команда это взятие операнда X или операндов X и Y, заталкивание их в ALU и записывание результата обратно во операнд Y. С аскетичностью и эзотеричностью даже One Instruction Set Computer это имеет мало чего общего. Здесь скорее преследуется цель сделать всё как можно более прямолинейным и простым для программирования — как можно более полная ортогональность (за исключением регистра SP) и никаких особых случаев и команд которые надо специально помнить.
Насчёт старых компьютеров у меня есть статья так что я в курсе. :)
avatar
Есть, кстати, у такого процессора еще одна забавная особенность — на нём можно оперировать ячейками памяти практически избегая «загрязнения регистров», т.е. делать команды вида:
[addr1] = [addr2]
или:
[addr1] += [addr2]
вообще минуя регистры общего назначения. за счёт того, что машинный цикл команды допускает сразу и многократные чтения из памяти и запись в память в конце команды — нет крайне часто встречаемого запрета в процессорах на то чтобы оба аргумента инструкции были в памяти.
казалось бы это должно сильно усложнить логику процессора, но как по мне — так нет. Тут как говорится «всё познаётся в сравнении».

Ведь возьмём такой популярный 8-битный процессор как i8080 на котором базирована командная система Z80.
В нём есть такая забавная инструкция как LHLD imm16, которая в синтаксисе Z80 выглядит как LD HL,(addr). Т.е. в регистровую пару HL считывается слово лежащее по адресу указанному в самой инструкции (imm16).
А что это с точки зрения машины состояний процессора? Во первых процессор 8-битный и все считывания 8-битные.
Поэтому сперва считывается код инструкции, потом считываются последовательно два байта imm16, потом считываются последовательно 2 байта лежащие по этому адресу и они уже рассовываются по регистрам H и L. Т.е., на минуточку, кроме считывания опкода в одной инструкции происходит 4 считывания из которых 2 меняют PC. Это уже по нагрузке на схемотехнические разбивки на шаги чтения/записи равно по сложности самому сложному возможному варианту возможных шагов чтения/записи единственной команды у Simpleton (только одно чтение меняется на запись), только вот у последнего команда может делать гораздо больше действий.
avatar
Зря лишил себя одного регистра из-за непосредственного операнда, всё равно его читаешь через PC, вот и кодируй (с индиректом) тоже через PC. И зачем адрес возврата обязательно пихать в память? В рисках, например, он пишется в (обычно специально выделенный) регистр, это дело вызываемой процедуры, сохранять в памяти его или нет. Можно сделать и через РОН, тут тебе пригодилась бы команда обмена, то есть вызов процедуры выглядит так load R,adr + swap PC,R и возврат load/swap PC,R (если обошёлся без стека).

Отказ полный от 8 бит может выглядеть восхитительно удачным только сегодня. Для 80-х все шины ширины 16 (и все вспомогательные микрухи!) в домашней машине дороговато, плюс доступную восьмибитную периферию трудно или невозможно использовать. И по мне, тогда уж лучше Форт-процессор на них сажать. Это уже был бы компик моей мечты))
avatar
Зря лишил себя одного регистра из-за непосредственного операнда, всё равно его читаешь через PC, вот и кодируй (с индиректом) тоже через PC.
Immediate сам может быть тоже indirect и таким образом операции можно проводить непосредственно над памятью не загрязняя регистры. С чтением через PC так уже не получится и поэтому такой лишний регистр, имхо, только усложнит в итоге программы.

И зачем адрес возврата обязательно пихать в память?
Т.к. регистров действительно немного, а в самом обобщённом случае Link Register таки нужно пихать в стек, то делать так всегда имхо проще для системы команд. В ней есть только одна операция X ?= Y с лёгкими вариациями. Swap уже не пролазит и не знаю как его протащить без вреда для внутренней красоты. :D

Для 80-х все шины ширины 16 (и все вспомогательные микрухи!) в домашней машине дороговато
Только до тех лет когда начали делать 128Кб-ные машины.
Далее получилось что истинным 16-биткам уже не хватало и этого и в них перекочевали банкинги и свитчинги в лучшем случае реализованные как сегментные регистры. Байты, правда, конечно, уже к тому времени стали вещью безальтернативной.

плюс доступную восьмибитную периферию трудно или невозможно использовать

Да ну, не вижу ни трудностей ни тем более невозможностей.
Несомненно раз уж есть мощная 8-битная предыстория, то разумно предусмотреть команды типа обмена/сдвига/зануления в слове байт, но уж куда подвести проводки от 8-битной периферии к 16-битной шине данных — по моему дело просто вкуса, но никак не трудностей.

Это уже был бы компик моей мечты))
Вот можно было бы и послушать, если есть конкретные образы и идеи. :)
avatar
Immediate сам может быть тоже indirect и таким образом операции можно проводить непосредственно над памятью не загрязняя регистры. С чтением через PC так уже не получится
это почему еще? сам же пишешь:
значение в X замещается значением ячейки памяти по адресу X
то есть load [PC],[PC] это X=[PC++]; Y=[PC++]; X=[X]; [Y]=X
что эквивалентно load [imm1],[imm2]
Т.к. регистров действительно немного,
вооот, в том числе еще поэтому нужен swap, а у тебя и регистров меньше, и свопа нет
а в самом обобщённом случае Link Register таки нужно пихать в стек,
только частных случаев очень много и эффект заметный от ускорения
плюс содержимое линк-регистра может служить указателем на данные процедуры
(после обработки данных с инкрементом получается корректный адрес возврата)
то делать так всегда имхо проще для системы команд
а для «компа мечты» должно быть эффективней и удобнее, а не «проще»
В ней есть только одна операция X ?= Y с лёгкими вариациями. Swap уже не пролазит и не знаю как его протащить без вреда для внутренней красоты.
ну, уродливый inc2 протащил же :)
Только до тех лет когда начали делать 128Кб-ные машины.
с чего такой вывод? это просто посадить на ту же шину больше микросхем
а не разрабатывать весь микропроцессорный комплект, а не только проц
Да ну, не вижу ни трудностей ни тем более невозможностей.
например, девайс подкидывает байты высоким темпом, нужно успевать выгребать и складывать их, а складывать ты можешь только словами; будешь выделять блок вдвое больше, а потом корячиться с утаптыванием? + удорожание в любом случае
Вот можно было бы и послушать, если есть конкретные образы и идеи. :)
пока некогда :)
avatar
это почему еще?
Когда load [PC] (на одном операнде X рассмотрим) отрабатывает он сперва в X грузит PC, а уже потом из-за бита indirect замещает X на [X]. Это совсем не то же самое что сперва загрузить в X [PC++], а потом заменить X на [X]. Система команд очень прямолинейна.

поэтому нужен swap
Swap регистров обычно нужен когда они существенно неортогональны и ради аккумулятора надо перестановки делать. Тут такой острой необходимости нет.
Регистров на самом деле хватает чтобы сделать memmove/memcopy полностью на регистрах, а это я считаю эталонным кодом для проверки на нехватку регистров.
avatar
P.S. блин, какая то комбинация клавиш сработала на enter и запостила посередине набора, сейчас еще будет продолжение ответа.
avatar
только частных случаев очень много и эффект заметный от ускорения
Такую оптимизацию можно проделать в существующей системе команд и без swap:
; вызов процедуры
R3 =+2 PC; адрес возврата (inc2) (команда без immediate)
PC = proc_addr; вызов процедуры
; возврат из процедуры
PC = R3
Как видно никакого SWAP и стека в таких хвостовых функциях можно реально не использовать.
Теоретически можно было бы использовать какую нибудь из запрещенных комбинаций регистров в операндах или псевдо-nop-ы как расширители команд, но повторюсь — мне здесь нравится именно простота.

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

ну, уродливый inc2 протащил же :)
Чем же он уродлив? inc2 (реализовать можно как ADD с загрузкой константы 2 в Y на первых фазах выполнения команды) часто нужен, потому и сделан — экономит код программы хорошо. Он полностью укладывается в парадигму X ?= Y и уродливым в ней быть не может — inc1 разве чем то уродлив? Тоже часто нужен, потому и полезен — абсолютно та же фигня и даже в тот же профиль.

с чего такой вывод?
В эти годы уже просто для поддержки CP/M могли целый процессор Z80 засадить в какой нибудь Commodore XXL (не помню точно модели) — какая уж тут гонка за примитивизмом?

например, девайс подкидывает байты высоким темпом, нужно успевать выгребать

Я уже говорил — команда типа SWAP BYTES внутри регистра (опять таки с логикой Y = swap bytes of X) делает такой сценарий маловероятным. Ну и вообще соединять комп с устройством которое плюётся данными быстрее чем или даже соизмеримо чем скорость с которой процессор отрабатывает memfill — это сомнительно и вряд ли такое вообще есть/было на практике.
avatar
>Commodore XXL (не помню точно модели)

Commodore 128
avatar
а ну да, собственно 128Кб и есть, точно! :)
avatar
Когда load [PC] (на одном операнде X рассмотрим) отрабатывает он сперва в X грузит PC, а уже потом из-за бита indirect замещает X на [X]. Это совсем не то же самое что сперва загрузить в X [PC++], а потом заменить X на [X].
что совсем не повод так бездарно тратить номер регистра
load imm = load [pc++]
load [imm] = load [[pc++]]
второй случай можно закодировать через номер регистра флагов
(потому что, ну кому и зачем мб нужен косвенный доступ через флаги))

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

Регистров на самом деле хватает чтобы сделать memmove/memcopy полностью на регистрах, а это я считаю эталонным кодом для проверки на нехватку регистров.
а я считаю это разновидностью подхода «и так сойдёт» :P

Такую оптимизацию можно проделать в существующей системе команд и без swap:
; вызов процедуры
R3 =+2 PC; адрес возврата (inc2) (команда без immediate)
PC = proc_addr; вызов процедуры
; возврат из процедуры
PC = R3
нет! это совершенно не то же самое! например:
load R3,adr1
(вычисления)
(условный переход)
load R3,adr2
(вычисления)
swap PC,R3

Чем же он уродлив?
тем, что повторяет функцию универсального сложения с любой константой
собс-но, даже инкремент на 1 — пережиток неортогональных восьмибитных процов
(но он хотя бы нужен бывает часто, и его наличие мб оправданным)
у обмена же гораздо шире функционал

В эти годы уже просто для поддержки CP/M могли целый процессор Z80 засадить в какой нибудь Commodore XXL (не помню точно модели) — какая уж тут гонка за примитивизмом?
засадить могли, а в нормальную производительность z80 не шмогли
и это тормозное в cp/m режиме угрёбище справедливо было непопулярно
тем более, что и стоило вдвое дороже c64 того же года

Я уже говорил — команда типа SWAP BYTES внутри регистра (опять таки с логикой Y = swap bytes of X) делает такой сценарий маловероятным. Ну и вообще соединять комп с устройством которое плюётся данными быстрее чем или даже соизмеримо чем скорость с которой процессор отрабатывает memfill — это сомнительно и вряд ли такое вообще есть/было на практике.
на практике поддерживали 8 и 16 бит девайсы одновременно очень недешёвые песюки
причём в них не было полного отказа от 8 бит
avatar
второй случай можно закодировать через номер регистра флагов
(потому что, ну кому и зачем мб нужен косвенный доступ через флаги))
А вот это очень прикольная идея! Действительно косвенная индексация через FLAGS не просто ненужная операция, но по сути своей — UB в чистом виде. Вообще работать с регистром флагов надо очень осторожно во избежание проблем с будущими совместимостями и главная задача вынесения его в РОН — это упрощение системы команд при соблюдении легкости сохранения через [SP] = FLAGS и восстановления через FLAGS = [SP], а так же манипуляцией бита Enable Interrups через FLAGS ~= mask и FLAGS |= mask. Но схемотехнически и архитектурно как чтение так и запись неиспользуемых его битов — это Undefined Behaviour для возможностей будущих расширений, поэтому ни о какой разумной индирекции в таких условиях речи быть не может.
Логично!
Тогда действительно ничего не мешает сказать, что R7 это и есть наш FLAGS (остальные регистры сохраняют нумерацию описанную в посте) и когда indirect=0, то ведет себя как этот регистр.
Но когда встречаются indirect=1 и R7 (т.е. все биты ответственные за DST и/или SRC единичны), то схема переключается в режим immediate+indirect в точности как описано в посте.
И действительно тогда чистый immediate лучше реализовать как чтение PC+indirect, тогда остаётся только заметить, что любое косвенное чтение через PC приводит всегда к его инкременту — это и логично и схемотехнически резонно, а не какой то «исключительный случай». Т.е. fetch через PC — он и в африке fetch через PC откуда бы действие не происходило.
И внезапно выходит, что действительно, у нас высвободился регистр R4 и неисключительных регистров теперь пять штук — R0-R4. При этом исчезло понятие «запрещенной комбинации DST», но и на этом фоне еще и высвободился регистр.
Очень удачная идея! Спасибо!
Самое интересное, что чисто схемотехнически такое усовершенствование насколько я понимаю вообще не несёт практически никакого пенальти по сравнению с первоначальным вариантом — добавочных линий и логических элементов похоже что вообще не нужно вводить в схему — они уже все и в первом проекте были, просто немного по другому затусованы.

например, обмен координат в однонаправленном алгоритме
сортировка пузырьком с условным обменом
Не стоит того чтобы из-за этого ломать систему команд. Даже в ЯВУ swap как правило записывается проброской данного через tmp, так и тут — ну пробросим через регистр, несущественно и нужно очень редко чтобы из-за этого огород целый городить.

load R3,adr1
(вычисления)
(условный переход)
load R3,adr2
(вычисления)
swap PC,R3
Это довольно эзотерично чтобы мне захотелось таким пользоваться в жизни. ;D

тем, что повторяет функцию универсального сложения с любой константой
Как и inc1 это экономия на imm в ряде случаев когда это часто нужно.

собс-но, даже инкремент на 1 — пережиток неортогональных восьмибитных процов
Так это и есть по духу 8-битный проц, но такой чтобы как можно более приятнее. inc2 лично мне приятно, экономит код, данные и скорость в массе случаев. Но, например, у этого проца очень слабая поддержка локальных переменных на стеке, а это вполне в духе 8-битных процессоров — в этом я даже нахожу какое то очарование, что система команд тяготеет к тому чтобы побольше всего вытаскивать в глобалки и минимизировать по настоящему комплексные адресации. Потому что по духу он и должен быть таким вот «восьмибитником».
avatar
ну, в одном хотя бы смог убедить :) тем не менее…

Не стоит того чтобы из-за этого ломать систему команд.
почему «ломать»? обобщить!

Даже в ЯВУ swap как правило записывается проброской данного через tmp,
что крайне бесит, так же как отсутствие циклических сдвигов

ну пробросим через регистр, несущественно
это инкремент на 2 несущественно, а нерациональный расход регистра — существенно

Это довольно эзотерично чтобы мне захотелось таким пользоваться в жизни. ;D
это в том числе вызов по таблице, рядовой случай

Как и inc1 это экономия на imm в ряде случаев когда это часто нужно.

inc2 лично мне приятно, экономит код, данные и скорость в массе случаев
в какой массе? перечисли несколько хотя бы разумных случаев? вот как раз на 8-битках иногда могло еще быть полезно, потому что основные типы данных разных размеров, но здесь-то одинаковые они! притом польза, даже если где-то она и будет, не «в два раза», а всего-то навсего в (N+1)/N от всех циклов доступа в итерации
avatar
в какой массе? перечисли несколько хотя бы разумных случаев?
Главное откуда оно возникло — это минимизация и ускорение реализации CALL, как я писал, но помимо этого может быть применён для, допустим, быстрой адресации переменных близких к вершине стека (наряду с =+1) и может быть полезен при итерировании по массивам с таким размером ячейки и тому подобное.
avatar
Главное откуда оно возникло — это минимизация и ускорение реализации CALL,
Кстати, лучшим вариантом именно минимизации с ускорением может быть отдать под однокомандный call в принципе небессмысленный, но на практике вряд ли нужный случай «op [PC],src» — то есть dst=PC всегда прямая адресация, а ID=1 в этом случае будет означать «перед выполнением сохранить PC в стек». Подумай, так ли тебе нужно писать что-то в следующий опкод.

помимо этого может быть применён для, допустим, быстрой адресации переменных близких к вершине стека
переменные нормально надо располагать, и не понадобятся лишние инкременты :D
(а еще лучше адресацию [R+imm], но она не лезет в такую схему)

может быть полезен при итерировании по массивам с таким размером ячейки и тому подобное.
ИТЕРИРОВАНИЕ подразумевает использование каждого элемента, то есть последовательный доступ ко всем его частям, то есть одинарный инкремент :P

также не совсем понимаю смысл необходимости флага TI — почему бы всегда не производить копирование в X,Y?
avatar
также не совсем понимаю смысл необходимости флага TI — почему бы всегда не производить копирование в X,Y?
Если TI=0, то общая схема инструкции следующая:
1. SRC загружается в X
2. X пропускается через АЛУ с кодом инструкции и TI=0 (т.е. полный код инструкции это TI+INSTR — оба поля)
3. результат сохраняется в DST
Если TI=1, то общая схема инструкции следующая:
1. SRC загружается в X
2. DST загружается в Y
3. X и Y пропускаются через АЛУ с кодом инструкции и TI=1 (т.е. полный код инструкции это TI+INSTR — оба поля)
3. результат сохраняется в DST
Т.е. TI — это признак двухоперандной инструкции — в таковой нужно больше действий в первой фазе по загрузке второго входного аргумента в Y и поэтому лучше по одному биту сразу понимать надо или не надо этот шаг делать.
Но можно так же воспринимать это так, что TI+INSTR формируют 5-битный код инструкции половина из которых однооперандная, а половина — двухоперандная.

Подумай, так ли тебе нужно писать что-то в следующий опкод.
Так и есть — это пожалуй единственная «бесполезная» комбинация аргументов которая осталась и действительно сделать её каким то особым случаем выглядит привлекательно.
Но лично мне не нравится, что ломается общая схема работы Simpleton-а.
Однако есть еще одно соображение — генерация исключений. Сколько ни думаю, но получается что при генерации исключений реально схемотехника должна быть вот настолько замороченной — нужно уметь и PC и FLAGS сохранить в стек да еще и переход совершить. Надо еще подумать, возможно из-за того что такая схемотехника просто нужна для прерываний, то и команда такая не будет обременением по итогу. Посмотрим, когда будет интерес и время подумать еще над этим помозгую. Немного смотрел в ARM-ы и вроде как там генерация прерываний не сохраняет никак флаги, типа это должно быть первой инструкцией обработчика прерываний… В общем возможны варианты.

(а еще лучше адресацию [R+imm], но она не лезет в такую схему)
Да, нифига не лезет. Да и норм. На деле дотянуться до произвольной переменной в стеке не так уж и трудно:
R5 = offset
R5 += SP
и вот тут уже мы можем совершать с параметром действия типа R0 += [R5] — мы получили на него указатель двумя командами и тремя словами.

ИТЕРИРОВАНИЕ подразумевает использование каждого элемента
Ну зависит от задачи. Например, если первый элемент нам подходит — переходим к его хвосту, а если нет — пропускаем до следующего элемента.
avatar
Если TI=1, то общая схема инструкции следующая:
1. SRC загружается в X
2. DST загружается в Y
это можно делать одновременно, и TI не нужен
если не понадобится Y, так не понадобится

Так и есть — это пожалуй единственная «бесполезная» комбинация аргументов которая осталась и действительно сделать её каким то особым случаем выглядит привлекательно.
Но лично мне не нравится, что ломается общая схема работы Simpleton-а.
почему ломается? считай просто, что схема общая у тебя такая:
1) может быть, записать PC в стек
2) DST ?= SRC
и нет проблем :D

Ну зависит от задачи. Например, если первый элемент нам подходит — переходим к его хвосту, а если нет — пропускаем до следующего элемента.
это только если по первому слову ясно (кстати, у тебя какой порядок слов-байтов? little/big endian?)
но если нужно элемент проверить целиком и условно пропустить следующий, то inc1+inc2 ничем не лучше add3
avatar
это можно делать одновременно, и TI не нужен
если не понадобится Y, так не понадобится
Одновременно считывать из памяти не получится. Заполнение Y реально может быть не просто ненужным, но и времязатратным процессом.
(кстати, у тебя какой порядок слов-байтов? little/big endian?)
А тут это исключительно как захочет программист. Ячейка памяти 16-битная, регистры — 16-битные, если нужно компоновать 32-битные, то это пользовательский код целиком.
то inc1+inc2 ничем не лучше add3
Я сильно над этим не задумывался еще, но т.к. есть 16 однооперандных инструкций (и они никак не обязаны быть теми же что и 16 двухоперандных), то возможно там будет и dec/inc-1/2/3/4 и возможно что-то еще.
avatar
P.S.
Вообще однооперандные инструкции это такие в которых X пропускаясь через АЛУ перед записью в Y никак не зависит от Y.
Т.е. (используется синтаксис операторов похожий на синтаксис Си-подобных языков):
R0 = R1; move
R0 =+1 R1; inc1
R0 =<< R2; сдвиг влево на 1 бит
R0 =+2 R3; inc2
R0 ~= R4; побитовая инверсия
и так далее. т.е. над SRC производится операция и записывается в DST

Двухоперандные инструкции берут и SRC и DST и пропустив их оба через АЛУ записывают результат в DST:

R0 += R1; сумма R0 и R1 записывается в R0
R0 <<= R2; R0 сдвигается влево на R2 бит (существенное отличие с однооперандным аналогом!)
R0 cmp R3; R0 и R3 сравниваются — неизменность R0 достигается за счёт того, что АЛУ именно его (Y) выдаёт на выходе
и так далее.
avatar
Заполнение Y реально может быть не просто ненужным, но и времязатратным процессом.
вроде как [DST] читать не нужно только для load, что определяется по опкоду (лучше нулевому) и без TI
и второй раз в операциях типа [DST=SRC],[SRC=DST] — ну так там уже прочитано в первый раз

Я сильно над этим не задумывался еще, но т.к. есть 16 однооперандных инструкций (и они никак не обязаны быть теми же что и 16 двухоперандных), то возможно там будет и dec/inc-1/2/3/4 и возможно что-то еще.
лично я между любым кол-вом дополнительных инкрементов и обменом всегда выберу обмен
уже только ради одного обмена со стековым указателем, даже если больше не понадобится никак 8)
avatar
вроде как [DST] читать не нужно только для load
Я только что выше подробнее пояснил.
avatar
а я пояснил, что тратить целый бит опкода на то не нужно
во всех случаях «существенного отличия» экономнее использовать другой instr
avatar
а я пояснил, что тратить целый бит опкода на то не нужно
во всех случаях «существенного отличия» экономнее использовать другой instr
Эм… Так и есть. Просто по одному биту кода инструкции можно тут же понять нужен Y на входе или нет.
А в чём проблема то?
avatar
в том, что ты на ровном месте потерял до половины нумерации для инструкций
у тебя сейчас возможно лишь 16 различных instr, а могло быть 32-(x<16)
avatar
Так их и есть 32, бит TI просто для простоты определяет какие из них однооперандные, а какие — двухоперандные. Они не обязаны быть одинаковыми, это просто удобство для декодера.
avatar
P.S.
Появился большой искус еще 1 бит поля INSTR откусить под «режим адресации immediate», когда поле SRC есть immediate-данное от 0 до 7, а TI — его знак. Тогда не нужны будут отдельные inc/dec 2/3/4/5/6/7, но надо подумать.
avatar
рукалицо.жпг

ну, а здесь-то почему не судьба применить отдельную ОПЕРАЦИЮ инкремента на число, закодированнное в опкоде??

вместо того, чтобы сокращать количество доступных операций еще в два раза))))))
avatar
ты уж определись — либо у тебя бит TI определяет что-то отдельно, а само ДЕЙСТВИЕ, производимое в АЛУ с операндами, кодируется битами INSTR и тогда у тебя возможно, повторяю, ЛИШЬ 16 УНИКАЛЬНЫХ ОПЕРАЦИЙ АЛУ; либо у тебя 32 разных действия кодируются 5 битами, но тогда нет фактически никакого отдельного TI, просто нет
avatar
Бит TI это прямой флаг для декордера инструкции нужно подгатавливать для АЛУ аргумент Y или нет. Всего инструкций 32, половина из них с зажжённым битом TI — половина нет. Но действительно бит TI уходит в АЛУ вместе с INSTR выступая как полный 5-битовый код инструкцуии, т.к. двухоперандные и однооперандные инструкции с одинаковым полем INSTR могут делать совершенно разные вещи.
avatar
а зачем вообще в опкод пихать такой флаг? опкод считай просто индекс для декодера, который вытащит из таблицы полную формулу команды/микропрограммы в общем виде со всеми флагами, где что муксить
avatar
да просто для простоты. нет тут никаких микропрограмм — вся логика прямолинейна и однообразна.
avatar
простоты ЧЕГО? «комп мечты» железячника или программиста? их мечты часто вступают в противоречие
avatar
вы что не понимаете, что один заранее готовый бит на блюдечке это гораздо гораздо проще, чем микропрограммы с декодером по таблицам и так далее? что за бред?
avatar
КОМУ проще? Аппаратчику, который будет разводить проц — да, проще; и у производителя, возможно, будет чуть меньше брака. Программисту — нет, ничуть не проще, ему нах не нужно думать про этот бит. Программист вовсе не мечтает забивать голову мусорной инфой про формат опкода и из-за удобства разводки и копеечной разницы в цене потерять в удобстве программирования. Так вот я и спрашиваю поэтому — «комп мечты» КОГО ты хочешь изобрести?
avatar
«Так вот я и спрашиваю поэтому — «комп мечты» КОГО ты хочешь изобрести?»

Всё это было написано — для всех и всях. Для программиста этот проц удобен тем что в нём довольно широкие команды, не надо ворочаться с аккумулятором, можно напрямую работать с ячейками памяти не загрязняя регистры, команды сразу совмещают и вычисление в АЛУ и пересылку данных откуда и куда угодно. Для аппаратчика — простая система команд когда есть одна команда в глобальном смысле — пересылка данных через АЛУ с лёгкими вариациями не влияющими на общую схему работы.
avatar
Да что-то не выходит пока для всех, а для железячника в основном. Программисту наплевать на «одну команду в глобальном смысле» и «не загрязняя регистры», ему важно, чтоб команды были быстрыми и удобными, и свободных регистров (или быстрой памяти) побольше (и насрать, сколько несвободных «грязных» при этом будет), и чтоб специализированных поменьше. Например, быстрая команда R=R+n с кодировкой n=1..16 в опкоде полезнее жалкой пары инкрементов, но в предложенную схему не вписывается (не в одностадийное декодирование, во всяком случае). Например, программисту были бы удобны два и более указателя стека, как в 6809, или хотя бы быстрое переключение между ними, а еще лучше автоинкремент любого адресующего регистра, что решается добавкой операций чтения/записи, но ты ради упрощения декодера сокращаешь количество возможных разных команд или ограничиваешь их тип.
avatar
Имхо такой проц гораздо удобнее и прямолинейнее в программировании чем тот же Z80 при том оставаясь всё-таки на уровне 8-биток своей скупостью команд и отсутствием сложных адресаций. Не забываем как звучит заголовок поста — это не попытка создать идеальный процессор вообще, но именно 8-битный по своим суммарным характеристикам и ощущениям. Взять 16-битный i8086 — это уже совсем другой зверь с огромным количеством удобных для ЯВУ адресаций и схем. Вот для меня это уже не то.
avatar
не такое уж большое достижение, намечтать «удобней z80»
но сравнительно с 6809 (а тем более 6309) уже не всё так однозначно
avatar
У меня есть краткий обзор на «ветку» процессоров Motorola/MOS. Мне, если честно, их архитектура аккумулятор-память в принципе не нравится. Это такое что-то из 50/60-х и вот не моё. От MOS 6502 вообще плакать хочется. 6809 несомненно намного круче и богаче на сложные адресации, откуда и опкоды уже двухбайтные начинают вылазить ну и простым не назовёшь. Но много лучше конечно 6502.
avatar
программисту на опкоды тоже начхать обычно, важен только полный (со всеми дополнительными данными) размер команд, если он у всех одинаков (как в исходном арме) или можно подобрать аналоги нужного размера — тогда это еще можно с толком использовать, если нет — ну не похрен ли, сколько там опкод занимает
avatar
в данном случае я и не говорил про программиста.
avatar
и кстати, на z80 очень огорчает отсутствие операций ex sp,rr
из-за этого двухстековые схемы и шитый код намного реже применимы, чем могло быть
avatar
Чтобы проверить архитектуру на практике, пусть и виртуальной, решил написать то что доступно — эмулятор машины Simpleton 3.x, текущий исходник, если вдруг кому интересно, что конечно вряд ли, можно качнуть тут: yadi.sk/d/-PGx1pEBf_O6kw
Сейчас довёл его до уже нормально исполняющего простые инструкции ассемблера, так вот такой код на C++:

    m.parseStart();
    int line = 0;
    m.parseLine( line++, "start R0 = $FFFF" ); // в R0 грузим константу $FFFF
    m.parseLine( line++, " R1 = $CCCC" ); // в R1 грузим константу $CCCC
    m.parseLine( line++, " R0 -= R1" ); // из R0 вычитаем R1 и заносим результат в R0
    m.parseLine( line++, " [ first ] = R0" ); // в ячейку памяти по адресу first заносим R0
    m.parseLine( line++, " [ second ] =+1 [ first ]" ); // в ячейку памяти по адресу second заносим инкремент ячейки first
    m.parseLine( line++, " R0 = R0" ); // NOP и ноль - эмулятор останавливается на команде NOP
    m.parseLine( line++, "first R0 = R0" ); // метка first ячейки с данными 0
    m.parseLine( line++, "second R0 = R0" ); // метка second ячейки с данными тоже 0 (DW пока не делал)
    m.parseEnd();

    m.show();

    while ( m.mem[ m.reg[ REG_PC ] ] != 0 )  // nop as stop
      m.step();
    m.show();

генерирует и исполняет следующий очищенный от C++ код на ассемблере Simpleton 3.x:

start  R0         =   $FFFF      // в R0 грузим константу $FFFF
       R1         =   $CCCC      // в R1 грузим константу $CCCC
       R0         -=  R1         // из R0 вычитаем R1 и заносим результат в R0
       [ first ]  =   R0         // в ячейку памяти по адресу first заносим R0
       [ second ] =+1 [ first ]  // в ячейку памяти по адресу second заносим инкремент ячейки first
       R0         =   R0         // NOP и ноль - эмулятор останавливается на команде NOP
first  R0         =   R0         // метка first ячейки с данными 0
second R0         =   R0         // метка second ячейки с данными тоже 0 (DW пока не делал)

Как видно синтаксис этого ассемблера строго подчиняется С-подобному обозначенному в статье синтаксису.
Код сей собирается, успешно исполняется и даёт после выполнения всех команд такую карту регистров и памяти:

R0:3333  R1:CCCC  R2:0000  R3:0000  R4:0000  SP:0000  PC:000A  FL:0000
0000:000E  0010:0000  0020:0000  0030:0000  0040:0000  0050:0000  0060:0000  0070:0000
0001:FFFF  0011:0000  0021:0000  0031:0000  0041:0000  0051:0000  0061:0000  0071:0000
0002:001E  0012:0000  0022:0000  0032:0000  0042:0000  0052:0000  0062:0000  0072:0000
0003:CCCC  0013:0000  0023:0000  0033:0000  0043:0000  0053:0000  0063:0000  0073:0000
0004:3801  0014:0000  0024:0000  0034:0000  0044:0000  0054:0000  0064:0000  0074:0000
0005:00F0  0015:0000  0025:0000  0035:0000  0045:0000  0055:0000  0065:0000  0075:0000
0006:000B  0016:0000  0026:0000  0036:0000  0046:0000  0056:0000  0066:0000  0076:0000
0007:10FF  0017:0000  0027:0000  0037:0000  0047:0000  0057:0000  0067:0000  0077:0000
0008:000B  0018:0000  0028:0000  0038:0000  0048:0000  0058:0000  0068:0000  0078:0000
0009:000C  0019:0000  0029:0000  0039:0000  0049:0000  0059:0000  0069:0000  0079:0000
000A:0000  001A:0000  002A:0000  003A:0000  004A:0000  005A:0000  006A:0000  007A:0000
000B:3333  001B:0000  002B:0000  003B:0000  004B:0000  005B:0000  006B:0000  007B:0000
000C:3334  001C:0000  002C:0000  003C:0000  004C:0000  005C:0000  006C:0000  007C:0000
000D:0000  001D:0000  002D:0000  003D:0000  004D:0000  005D:0000  006D:0000  007D:0000
000E:0000  001E:0000  002E:0000  003E:0000  004E:0000  005E:0000  006E:0000  007E:0000
000F:0000  001F:0000  002F:0000  003F:0000  004F:0000  005F:0000  006F:0000  007F:0000

Здесь видно что PC дошёл до 000A и остановился — это где первый искуственный NOP (R0 = R0) — check.
В R0 разница между FFFF и CCCC = 3333 — check.
В R1 — CCCC — check.
По адресу 000B хранится 3333 — это метка first — check.
По следующему адресу — метке second хранится увеличенное на 1 значение в first — 3334 — check!
Можно посмотреть в коды инструкций — адреса (а это между прочим forward reference для которых надо было запоминать адреса которые надо поправить после конца парсинга) 000B и 000C явно видно в ячейках с инструкциями по адресам 0006 и 0008-0009.
Забавное ощущение когда свой ассемблер делаешь виртуальной несуществующей машины. :D
Парсер и генератор кода конечно примитивный — лишь бы откровенных ошибок с подстановкой совсем уж неверных типов лексем не на свои места не было. Например косвенная адресация просто как флаг взводится и сбрасывается при встрече символов [ и ] поэтому такой код будет валидным: [ R0 = R1 ] и эквивалентен [ R0 ] = [ R1 ] (строго говоря валидно и [ R0 = R1
Но тем не менее в мнемониках кодировать весьма удобно становится. :)
Когда еще будет время реализую условия и попробую делать циклы.
avatar
Проапгрейдил ассемблер Simpleton еще дальше, теперь программа берет внешний файл source.asm, собирает и исполняет его с адреса 0 пока не наткнётся на инструкцию с кодом 0 (R0 = R0).
Теперь поддерживаются ключевые слова =, org и dw с нюансами.
Программа теперь может выглядеть так:

       R0         =     $FFFF
       R1         =     $CCCC
       R0         -=    R1
       [ first ]  =     R0
       [ second ] =+1   [ first ]
       [ third ]  +=    16
       dw         0     // DEF WORD 0 помещает в текущую ячейку компиляции константу
       org        $0020 // компилируем теперь начиная с адреса $0020
first  dw         0     // first и second теперь заданы
second dw         0     // как два нулевых слова в памяти
       org        $0030 // смещаем адрес компиляции в $0030
third  dw         forth // пример что в качестве DEF WORD слова можно указать символ, причём forward
forth  =          $1000 // через = значение символа задаётся напрямую без записи данных в память
       dw         1     // эта единица в памяти программы следует сразу за $1000 (third)

Основные моменты — регистрозависимость всех идентификаторов и ключевых слов.
Имена регистров: R0-R4, R5 (он же SP), R6 (он же PC), R7 (он же FLAGS).
Машинные команды имеют вид

DEST OP SRC

Где SRC это один из регистров, константа/символ или адрес задаваемый как регистр или константа/символ в квадратных скобках.
OP это операторы в стиле Си:
= присваивание
=+1 инкремент
=-1 декремент
<?> сравнение
+=
-=
+c= то же что и += с учетом флага переноса
-c= то же что и -= с учетом флага переноса
DEST может быть всем тем же что и SRC кроме константы/символа (не в квадратных скобках)
Числовые константы/литералы или десятичные или начинаются с $ и тогда являются шестнадцатеричными.

Если строка начинается не с пробельного символа, то создаётся символ.
Если он предшествует машинной инструкции или dw, то в него записывается её адрес.
Если он предшествует знаку =, то в него записывается константа или значение символа по правую часть от знака. Формульная математика пока не поддерживается вообще.
Если он предшествует ключевому слову org, то он будет равнятся адресу куда переводит компиляцию этот org.
org переводит запись генерируемых инструкций на указанный адрес (origin)
dw прописывает в текущую ячейку данное — оно может быть или константой или символом.
В стиле ассемблера Zilog 80 (и не в стиле ассемблера Intel) имя символа в чистом виде означает адрес ячейки памяти если это метка, а не значение в этой памяти. Чтобы адресовать ячейку надо использовать квадратные скобки.
Т.е.

some_addr = $1000
R0 = some_addr // в R0 запишется $1000
R0 = [ some_addr ] // в R0 запишется значение в ячейке с адресом $1000
some_addr = R0 // такое вообще запрещено, т.к. в констансту нельзя писать, надо:
[ some_addr ] = R0 // а вот это запишет R0 в ячейку памяти
avatar
P.S.
Причесал код и внедрил условия и сделал все ключевые слова в нижнем регистре для консистентности. Ссылка на исходник потому изменилась: yadi.sk/d/fTZqZ1n12dD72A
Условия пишутся в любом месте инструкции:
@@ — всегда
@z или @nz — ноль/не ноль
@c @nc — перенос
@gt @gte — больше и больше-или-равно (пока не реализовано)
теперь работает такое:

        r0 = $8
loop    [ counter ] =-1 [ counter ]
        r0 =-1 r0
        pc = loop @nz // эта инструкция перейдёт на loop если не ноль
        dw 0

        org $0030
counter	dw $0010
avatar
Сильно переработал код виртуальной машины и ассемблера — разделил собственно классы на Машину и Ассемблер и кроме того теперь виртуальная машина умеет выводить символы в консоль, а ассемблер поддерживает строки и множество данных в одном ключевом слове dw.
Программа теперь может выглядеть так:

PORT_CONSOLE    =         $FFFF   ; символ для порта ввода-вывода консоли
                sp        = $0050 ; настроим стек

                r0        = str1
                [ sp ]    =+2 pc  ; запоминаем в стеке pc для возврата
                pc        = print ; вместе с предыдущей инструкцией - CALL

                r0        = str2
                [ sp ]    =+2 pc
                pc        = print ; т.е. два раза вызвали процедуру print разных строк

exit            dw        0        ; STOP полный останов программы
        
                ; процедура print, на входе r0 - указатель на ASCIIZ-строку
print           r1        =? [ r0 ]    ; MOV с обновлением FLAGS (carry и zero)
                pc        = [ sp ] @z  ; если флаг нуля, то совершаем выход
                [ PORT_CONSOLE ] = r1  ; в порт консоли выводим очередной символ 
                r0        =+1 r0       ; увеличиваем указатель на строку
                pc = print             ; цикл на начало процедуры

                org $0050              ; начало данных для вывода
str1            dw "Hello, world!" 13 10 0
str2            dw "That's it." 13 10 0

и выводит она следующее (включая дамп памяти и регистров после выполнения):

Hello, world!
That's it.
R0:006C  R1:0000  R2:0000  R3:0000  R4:0000  SP:0050  PC:000C  FL:0001
0000:005E  0010:FFFF  0020:0000  0030:0000  0040:0000  0050:0048  0060:0054  0070:0000
0001:0050  0011:2000  0021:0000  0031:0000  0041:0000  0051:0065  0061:0068  0071:0000
0002:000E  0012:006E  0022:0000  0032:0000  0042:0000  0052:006C  0062:0061  0072:0000
0003:0050  0013:000D  0023:0000  0033:0000  0043:0000  0053:006C  0063:0074  0073:0000
0004:40D6  0014:0000  0024:0000  0034:0000  0044:0000  0054:006F  0064:0027  0074:0000
0005:006E  0015:0000  0025:0000  0035:0000  0045:0000  0055:002C  0065:0073  0075:0000
0006:000D  0016:0000  0026:0000  0036:0000  0046:0000  0056:0020  0066:0020  0076:0000
0007:000E  0017:0000  0027:0000  0037:0000  0047:0000  0057:0077  0067:0069  0077:0000
0008:0060  0018:0000  0028:0000  0038:0000  0048:0000  0058:006F  0068:0074  0078:0000
0009:40D6  0019:0000  0029:0000  0039:0000  0049:0000  0059:0072  0069:002E  0079:0000
000A:006E  001A:0000  002A:0000  003A:0000  004A:0000  005A:006C  006A:000D  007A:0000
000B:000D  001B:0000  002B:0000  003B:0000  004B:0000  005B:0064  006B:000A  007B:0000
000C:0000  001C:0000  002C:0000  003C:0000  004C:0000  005C:0021  006C:0000  007C:0000
000D:1018  001D:0000  002D:0000  003D:0000  004D:0000  005D:000D  006D:0000  007D:0000
000E:026D  001E:0000  002E:0000  003E:0000  004E:0000  005E:000A  006E:0000  007E:0000
000F:00F1  001F:0000  002F:0000  003F:0000  004F:000C  005F:0000  006F:0000  007F:0000

Единственный пока порт ввода-вывода замаплен на адрес $FFFF (и вообще все порты ввода-вывода будут замаплены на последние ячейки памяти) и при записи в себя выводит символ в консоль.
Ключевое слово dw теперь может принимать строки в кавычках и много данных в одной строке программмы — они даже не разделяются запятыми, а только пробельными символами, так парсер даже проще.
Заодно демонстрация того как CALL имитируется двумя инструкциями — сперва в стек пишется адрес возврата через инструкцию inc_by_two и уже потом совершается переход.
RET в программе условный.
avatar
укочал, но времени смотреть пока нет

для наглядности взял бы ты сырцы покороче от какого-нибудь спековского эффекта и переписал на своём
avatar
Тогда еще добавка из последнего:
Добавил поддержку локальных меток — начинаются с точки и по факту разворачиваются внутри парсера в lastGlobalLabel.thisLocalLabel таким образом можно обратится к метке из любой точки программы по полному имени, но в пределах одной процедуры можно обращаться по короткому имени. При этом создание символов через = не засчитывается как глобальная метка после которой локальные будут соединятся с ней — только прямые объявления меток.
Добавил ключевое слово ds x [ y ] которое создаёт массив размером x слов заполненных значением y (если не указано — 0).
Для краткости и понятности вызова процедур ввёл 4 псевдоинструкции:

call arg
; эквивалентно следующему:
[ sp ] =+2 pc
pc = arg

ret
; эквивалентно
pc = [ sp ]

; а так же для быстрых вызовов:

qcall arg
; эквивалентно
r4 =+2 pc
pc = arg

qret
; эквивалентно
pc = r4

В силу того как парсером обрабатываются коды условий типа @nz @z — их можно присовокуплять к этим инструкциям точно так же как к обычным. Однако надо помнить, что если адрес процедуры есть не прямая метка (addr16), а содержимое регистра, то call (как и qcall) неприменима, т.к. первой инструкцией в ней должна быть [ sp ] =+1 pc, поэтому косвенные переходы по крайней мере пока надо расписывать полностью.
Так же PORT_CONSOLE теперь еще работает на ввод возвращая или 0 или символ последней нажатой клавиши (пока по сути обёртка над kbhit/getch без учёта какой то виртуальной архитектуры).
Так же еще кучу багов вымел как в виртуальной машине так и в ассемблере.
В общем теперь возможно написать такую программу:

PORT_CONSOLE    = $FFFF
    sp  = $FF00

    pc  = start

; string_input
; in: r0 - string buffer
;     r1 - max buffer size
; out: 
string_input  r3  = r0    ; remember beginning
.loop    r2  =? [ PORT_CONSOLE ]
    pc  = .loop @z
    r2  <?> 13
    pc  = .end @z  ; if CR
    r2  <?> 8
    pc  = .backsp @z  ; if BS
    r1  =? r1
    pc  = .overfl @z  ; if buffer overflow
    ; accept symbol
    [ PORT_CONSOLE ] = r2
    r1  =-1 r1
    [ r0 ]  = r2
    r0  =+1 r0
    pc  = .loop    ; continue input
    ; backspace
.backsp    r0  <?> r3
    pc  = .loop @z  ; ignore del at start of line
    [ PORT_CONSOLE ] = r2
    [ PORT_CONSOLE ] = 32  ; erase prev symbol at (windows) console...
    [ PORT_CONSOLE ] = r2
    r1  =+1 r1
    r0  =-1 r0
    pc  = .loop
    ; overflow
.overfl    pc  = .loop ; just continue
    ; end
.end    [ r0 ]  = 0
    ret

; string_print
; in: r0 - string buffer
string_print  r1  =? [ r0 ]
    ret @z
    r0  =+1 r0
    [ PORT_CONSOLE ] = r1
    pc  = string_print

; string_len  
; in:  r0 - string buffer
; out:  r0 - length of the string
string_len  r1  = 0
.loop    r2  = [ r0 ]
    pc  = .end @z
    r0  =+1 r0
    r1  =+1 r1
    pc  = .loop
.end    r0  = r1
    ret

start    
    r0  = msg1
    call  string_print
    r0  = buf
    r1  = 10
    call  string_input
    [ PORT_CONSOLE ] = 10

    r0  = msg2
    call   string_print
    r0  = buf
    call  string_print
    r0  = CrLf
    call  string_print

    dw  0
buf    ds  12 $AAAA
msg1    dw  "Enter command: " 0
msg2    dw  "You entered this text: " 0
CrLf    dw  13 10 0

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

И ощущения от архитектуры двоякие.
С одной стороны сам ассемблерный код несмотря на сильно упрощенный синтаксис и крайнюю схожесть с человекочитаемыми операторами из сишечки всё равно выглядит как стена ассемблерного и плохосчитываемого кода. :D Какой то революции человекочитаемого ассемблера не случилось.
С другой стороны мозг реально разгружен когда _пишешь_ на этом ассемблере по сравнению с классикой — не нужно как в Z80 на том же спектруме постоянно задумываться над тем как и куда перекинуть результаты из аккумулятора или HL, во что развернуть проверку регистровой пары на достижение нуля, какие там есть двухбайтовые инструкции на которых можно сэкономить и т.п.

8<=============

А виртуальной машины пока еще нет чтобы эффектами меряться. Да и много чего нет — инструкции в АЛУ даже вводятся по мере того как появляются в них потребности. Это в свою очередь интересно тем, что как только видно что какая то инструкция часто нужна, то берешь и вводишь её — например move with flags update которая пишется в этом синтаксисе как =? и перемещает данное обновляя флаги S и Z обычно не встречается, но тут сразу попросилась разгружать циклы для asciiz-строк.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.