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 можно выразить в языке Си такой строчкой:
где знак? — это операция наподобие Y += X или Y <<= X в том числе и просто Y = X, а X и Y — это регистры или ячейки памяти.
например:
Когда в инструкцию нужно запаковать и два аргумента и код операции и код условия, то одного байта становится решительно мало. Бит в инструкции вообще решительно не хватало, поэтому чтобы уместить всё самое необходимое я наступил себе на горло и превозмог все 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» с английского — типа «недалёкий»)
Все шины и данные — 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) тоже для возможности будущих расширений и префиксов.
Какие у Simpleton 3.x есть необычности или подводные камни:
1. Если простой переход (jump) это:
а относительный переход как:
то с вызовом процедуры (call) эта «однокомандная» система в одну команду не пролазит.
тут нужно делать примерно таким образом:
и тут вот какая неприятность — адресы тут являются immediate-данными и таким образом эти две инструкции будут занимать целых четыре слова. не очень хорошо.
т.к. чаще всего мы хотим вернуться на код следующий за этими инструкциями, то в [SP] надо занести значение PC+2 (на момент выполнения первой инструкции PC указывает на следующую, а надо еще пропустить immediate-данное). И вот чтобы это стало возможным нужна двухоперандная инструкция не только inc, но и inc2. Тогда да — в две инструкции:
мы делаем вызов процедуры
А вот совершить возврат из неё очень просто:
тут уже всё делается в одно действие.
конечно в ассемблере call можно будет сделать макросом.
2. Как разрешить/запретить прерывания не затрагивая арифметико-логические флаги в FLAGS?
Да очень просто:
Т.е. не просто перезаписью, а через логические операции с операндом-константой.
3. Что именно делают прерывания?
Как и в классических процессорах помещают в стек сперва PC, потом FLAGS и совершается переход по заранее определенному адресу.
Таким образом возврат из прерывания должен совершаться так:
Тему эту в вялотекущем режиме я уже обсуждал тут: 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]
50 комментариев
Касательно восьми бит, это свойство конкретно ранних микропроцессоров, возникшее из ограничений технологии того времени. До них были архитектуры с самым разным количеством бит в слове, и вовсе не обязательно это была степень двойки или чётное число. Примеры: en.wikipedia.org/wiki/Word_(computer_architecture)#Table_of_word_sizes
Т.е. любая команда это взятие операнда X или операндов X и Y, заталкивание их в ALU и записывание результата обратно во операнд Y. С аскетичностью и эзотеричностью даже One Instruction Set Computer это имеет мало чего общего. Здесь скорее преследуется цель сделать всё как можно более прямолинейным и простым для программирования — как можно более полная ортогональность (за исключением регистра SP) и никаких особых случаев и команд которые надо специально помнить.
Насчёт старых компьютеров у меня есть статья так что я в курсе. :)
[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 (только одно чтение меняется на запись), только вот у последнего команда может делать гораздо больше действий.
Отказ полный от 8 бит может выглядеть восхитительно удачным только сегодня. Для 80-х все шины ширины 16 (и все вспомогательные микрухи!) в домашней машине дороговато, плюс доступную восьмибитную периферию трудно или невозможно использовать. И по мне, тогда уж лучше Форт-процессор на них сажать. Это уже был бы компик моей мечты))
Т.к. регистров действительно немного, а в самом обобщённом случае Link Register таки нужно пихать в стек, то делать так всегда имхо проще для системы команд. В ней есть только одна операция X ?= Y с лёгкими вариациями. Swap уже не пролазит и не знаю как его протащить без вреда для внутренней красоты. :D
Только до тех лет когда начали делать 128Кб-ные машины.
Далее получилось что истинным 16-биткам уже не хватало и этого и в них перекочевали банкинги и свитчинги в лучшем случае реализованные как сегментные регистры. Байты, правда, конечно, уже к тому времени стали вещью безальтернативной.
Да ну, не вижу ни трудностей ни тем более невозможностей.
Несомненно раз уж есть мощная 8-битная предыстория, то разумно предусмотреть команды типа обмена/сдвига/зануления в слове байт, но уж куда подвести проводки от 8-битной периферии к 16-битной шине данных — по моему дело просто вкуса, но никак не трудностей.
Вот можно было бы и послушать, если есть конкретные образы и идеи. :)
то есть load [PC],[PC] это X=[PC++]; Y=[PC++]; X=[X]; [Y]=X
что эквивалентно load [imm1],[imm2]
вооот, в том числе еще поэтому нужен swap, а у тебя и регистров меньше, и свопа нет
только частных случаев очень много и эффект заметный от ускорения
плюс содержимое линк-регистра может служить указателем на данные процедуры
(после обработки данных с инкрементом получается корректный адрес возврата)
а для «компа мечты» должно быть эффективней и удобнее, а не «проще»
ну, уродливый inc2 протащил же :)
с чего такой вывод? это просто посадить на ту же шину больше микросхем
а не разрабатывать весь микропроцессорный комплект, а не только проц
например, девайс подкидывает байты высоким темпом, нужно успевать выгребать и складывать их, а складывать ты можешь только словами; будешь выделять блок вдвое больше, а потом корячиться с утаптыванием? + удорожание в любом случае
пока некогда :)
Swap регистров обычно нужен когда они существенно неортогональны и ради аккумулятора надо перестановки делать. Тут такой острой необходимости нет.
Регистров на самом деле хватает чтобы сделать memmove/memcopy полностью на регистрах, а это я считаю эталонным кодом для проверки на нехватку регистров.
; вызов процедуры
R3 =+2 PC; адрес возврата (inc2) (команда без immediate)
PC = proc_addr; вызов процедуры
; возврат из процедуры
PC = R3
Как видно никакого SWAP и стека в таких хвостовых функциях можно реально не использовать.
Теоретически можно было бы использовать какую нибудь из запрещенных комбинаций регистров в операндах или псевдо-nop-ы как расширители команд, но повторюсь — мне здесь нравится именно простота.
В том то и дело, что я нахожу такую систему команд довольно эффективной и удобной. Тут само мышление прямолинейно как стрела — всё есть пересылка данных с опциональной операцией с высокой степенью ортогональности. Не нужно задумываться над перекладыванием данных по регистрам как в Z80 или тем какие режимы адресации есть а каких нет и в каких операциях как в 6502.
Чем же он уродлив? inc2 (реализовать можно как ADD с загрузкой константы 2 в Y на первых фазах выполнения команды) часто нужен, потому и сделан — экономит код программы хорошо. Он полностью укладывается в парадигму X ?= Y и уродливым в ней быть не может — inc1 разве чем то уродлив? Тоже часто нужен, потому и полезен — абсолютно та же фигня и даже в тот же профиль.
В эти годы уже просто для поддержки CP/M могли целый процессор Z80 засадить в какой нибудь Commodore XXL (не помню точно модели) — какая уж тут гонка за примитивизмом?
Я уже говорил — команда типа SWAP BYTES внутри регистра (опять таки с логикой Y = swap bytes of X) делает такой сценарий маловероятным. Ну и вообще соединять комп с устройством которое плюётся данными быстрее чем или даже соизмеримо чем скорость с которой процессор отрабатывает memfill — это сомнительно и вряд ли такое вообще есть/было на практике.
Commodore 128
load imm = load [pc++]
load [imm] = load [[pc++]]
второй случай можно закодировать через номер регистра флагов
(потому что, ну кому и зачем мб нужен косвенный доступ через флаги))
например, обмен координат в однонаправленном алгоритме
сортировка пузырьком с условным обменом
(ты же хочешь операции с памятью?)
а я считаю это разновидностью подхода «и так сойдёт» :P
нет! это совершенно не то же самое! например:
load R3,adr1
(вычисления)
(условный переход)
load R3,adr2
(вычисления)
swap PC,R3
тем, что повторяет функцию универсального сложения с любой константой
собс-но, даже инкремент на 1 — пережиток неортогональных восьмибитных процов
(но он хотя бы нужен бывает часто, и его наличие мб оправданным)
у обмена же гораздо шире функционал
засадить могли, а в нормальную производительность z80 не шмогли
и это тормозное в cp/m режиме угрёбище справедливо было непопулярно
тем более, что и стоило вдвое дороже c64 того же года
на практике поддерживали 8 и 16 бит девайсы одновременно очень недешёвые песюки
причём в них не было полного отказа от 8 бит
Логично!
Тогда действительно ничего не мешает сказать, что 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, так и тут — ну пробросим через регистр, несущественно и нужно очень редко чтобы из-за этого огород целый городить.
Это довольно эзотерично чтобы мне захотелось таким пользоваться в жизни. ;D
Как и inc1 это экономия на imm в ряде случаев когда это часто нужно.
Так это и есть по духу 8-битный проц, но такой чтобы как можно более приятнее. inc2 лично мне приятно, экономит код, данные и скорость в массе случаев. Но, например, у этого проца очень слабая поддержка локальных переменных на стеке, а это вполне в духе 8-битных процессоров — в этом я даже нахожу какое то очарование, что система команд тяготеет к тому чтобы побольше всего вытаскивать в глобалки и минимизировать по настоящему комплексные адресации. Потому что по духу он и должен быть таким вот «восьмибитником».
почему «ломать»? обобщить!
что крайне бесит, так же как отсутствие циклических сдвигов
это инкремент на 2 несущественно, а нерациональный расход регистра — существенно
это в том числе вызов по таблице, рядовой случай
в какой массе? перечисли несколько хотя бы разумных случаев? вот как раз на 8-битках иногда могло еще быть полезно, потому что основные типы данных разных размеров, но здесь-то одинаковые они! притом польза, даже если где-то она и будет, не «в два раза», а всего-то навсего в (N+1)/N от всех циклов доступа в итерации
переменные нормально надо располагать, и не понадобятся лишние инкременты :D
(а еще лучше адресацию [R+imm], но она не лезет в такую схему)
ИТЕРИРОВАНИЕ подразумевает использование каждого элемента, то есть последовательный доступ ко всем его частям, то есть одинарный инкремент :P
также не совсем понимаю смысл необходимости флага TI — почему бы всегда не производить копирование в X,Y?
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-ы и вроде как там генерация прерываний не сохраняет никак флаги, типа это должно быть первой инструкцией обработчика прерываний… В общем возможны варианты.
Да, нифига не лезет. Да и норм. На деле дотянуться до произвольной переменной в стеке не так уж и трудно:
R5 = offset
R5 += SP
и вот тут уже мы можем совершать с параметром действия типа R0 += [R5] — мы получили на него указатель двумя командами и тремя словами.
Ну зависит от задачи. Например, если первый элемент нам подходит — переходим к его хвосту, а если нет — пропускаем до следующего элемента.
если не понадобится Y, так не понадобится
почему ломается? считай просто, что схема общая у тебя такая:
1) может быть, записать PC в стек
2) DST ?= SRC
и нет проблем :D
это только если по первому слову ясно (кстати, у тебя какой порядок слов-байтов? little/big endian?)
но если нужно элемент проверить целиком и условно пропустить следующий, то inc1+inc2 ничем не лучше add3
А тут это исключительно как захочет программист. Ячейка памяти 16-битная, регистры — 16-битные, если нужно компоновать 32-битные, то это пользовательский код целиком.
Я сильно над этим не задумывался еще, но т.к. есть 16 однооперандных инструкций (и они никак не обязаны быть теми же что и 16 двухоперандных), то возможно там будет и dec/inc-1/2/3/4 и возможно что-то еще.
Вообще однооперандные инструкции это такие в которых 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) выдаёт на выходе
и так далее.
и второй раз в операциях типа [DST=SRC],[SRC=DST] — ну так там уже прочитано в первый раз
лично я между любым кол-вом дополнительных инкрементов и обменом всегда выберу обмен
уже только ради одного обмена со стековым указателем, даже если больше не понадобится никак 8)
во всех случаях «существенного отличия» экономнее использовать другой instr
А в чём проблема то?
у тебя сейчас возможно лишь 16 различных instr, а могло быть 32-(x<16)
Появился большой искус еще 1 бит поля INSTR откусить под «режим адресации immediate», когда поле SRC есть immediate-данное от 0 до 7, а TI — его знак. Тогда не нужны будут отдельные inc/dec 2/3/4/5/6/7, но надо подумать.
ну, а здесь-то почему не судьба применить отдельную ОПЕРАЦИЮ инкремента на число, закодированнное в опкоде??
вместо того, чтобы сокращать количество доступных операций еще в два раза))))))
Всё это было написано — для всех и всях. Для программиста этот проц удобен тем что в нём довольно широкие команды, не надо ворочаться с аккумулятором, можно напрямую работать с ячейками памяти не загрязняя регистры, команды сразу совмещают и вычисление в АЛУ и пересылку данных откуда и куда угодно. Для аппаратчика — простая система команд когда есть одна команда в глобальном смысле — пересылка данных через АЛУ с лёгкими вариациями не влияющими на общую схему работы.
но сравнительно с 6809 (а тем более 6309) уже не всё так однозначно
из-за этого двухстековые схемы и шитый код намного реже применимы, чем могло быть
Сейчас довёл его до уже нормально исполняющего простые инструкции ассемблера, так вот такой код на C++:
генерирует и исполняет следующий очищенный от C++ код на ассемблере Simpleton 3.x:
Как видно синтаксис этого ассемблера строго подчиняется С-подобному обозначенному в статье синтаксису.
Код сей собирается, успешно исполняется и даёт после выполнения всех команд такую карту регистров и памяти:
Здесь видно что 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
Но тем не менее в мнемониках кодировать весьма удобно становится. :)
Когда еще будет время реализую условия и попробую делать циклы.
Теперь поддерживаются ключевые слова =, org и dw с нюансами.
Программа теперь может выглядеть так:
Основные моменты — регистрозависимость всех идентификаторов и ключевых слов.
Имена регистров: R0-R4, R5 (он же SP), R6 (он же PC), R7 (он же FLAGS).
Машинные команды имеют вид
Где SRC это один из регистров, константа/символ или адрес задаваемый как регистр или константа/символ в квадратных скобках.
OP это операторы в стиле Си:
= присваивание
=+1 инкремент
=-1 декремент
<?> сравнение
+=
-=
+c= то же что и += с учетом флага переноса
-c= то же что и -= с учетом флага переноса
DEST может быть всем тем же что и SRC кроме константы/символа (не в квадратных скобках)
Числовые константы/литералы или десятичные или начинаются с $ и тогда являются шестнадцатеричными.
Если строка начинается не с пробельного символа, то создаётся символ.
Если он предшествует машинной инструкции или dw, то в него записывается её адрес.
Если он предшествует знаку =, то в него записывается константа или значение символа по правую часть от знака. Формульная математика пока не поддерживается вообще.
Если он предшествует ключевому слову org, то он будет равнятся адресу куда переводит компиляцию этот org.
org переводит запись генерируемых инструкций на указанный адрес (origin)
dw прописывает в текущую ячейку данное — оно может быть или константой или символом.
В стиле ассемблера Zilog 80 (и не в стиле ассемблера Intel) имя символа в чистом виде означает адрес ячейки памяти если это метка, а не значение в этой памяти. Чтобы адресовать ячейку надо использовать квадратные скобки.
Т.е.
Причесал код и внедрил условия и сделал все ключевые слова в нижнем регистре для консистентности. Ссылка на исходник потому изменилась: yadi.sk/d/fTZqZ1n12dD72A
Условия пишутся в любом месте инструкции:
@@ — всегда
@z или @nz — ноль/не ноль
@c @nc — перенос
@gt @gte — больше и больше-или-равно (пока не реализовано)
теперь работает такое:
Программа теперь может выглядеть так:
и выводит она следующее (включая дамп памяти и регистров после выполнения):
Единственный пока порт ввода-вывода замаплен на адрес $FFFF (и вообще все порты ввода-вывода будут замаплены на последние ячейки памяти) и при записи в себя выводит символ в консоль.
Ключевое слово dw теперь может принимать строки в кавычках и много данных в одной строке программмы — они даже не разделяются запятыми, а только пробельными символами, так парсер даже проще.
Заодно демонстрация того как CALL имитируется двумя инструкциями — сперва в стек пишется адрес возврата через инструкцию inc_by_two и уже потом совершается переход.
RET в программе условный.
для наглядности взял бы ты сырцы покороче от какого-нибудь спековского эффекта и переписал на своём
Добавил поддержку локальных меток — начинаются с точки и по факту разворачиваются внутри парсера в lastGlobalLabel.thisLocalLabel таким образом можно обратится к метке из любой точки программы по полному имени, но в пределах одной процедуры можно обращаться по короткому имени. При этом создание символов через = не засчитывается как глобальная метка после которой локальные будут соединятся с ней — только прямые объявления меток.
Добавил ключевое слово ds x [ y ] которое создаёт массив размером x слов заполненных значением y (если не указано — 0).
Для краткости и понятности вызова процедур ввёл 4 псевдоинструкции:
В силу того как парсером обрабатываются коды условий типа @nz @z — их можно присовокуплять к этим инструкциям точно так же как к обычным. Однако надо помнить, что если адрес процедуры есть не прямая метка (addr16), а содержимое регистра, то call (как и qcall) неприменима, т.к. первой инструкцией в ней должна быть [ sp ] =+1 pc, поэтому косвенные переходы по крайней мере пока надо расписывать полностью.
Так же PORT_CONSOLE теперь еще работает на ввод возвращая или 0 или символ последней нажатой клавиши (пока по сути обёртка над kbhit/getch без учёта какой то виртуальной архитектуры).
Так же еще кучу багов вымел как в виртуальной машине так и в ассемблере.
В общем теперь возможно написать такую программу:
Программа выведет приглашение ввести с клавиатуры текст в буфер ограниченный десятью символами и выведет потом введённый текст в консоль же.
В принципе это уже приближается к реальному машинописанию на реальном ассемблере, можно писать достаточно сложные программы и почувствовать отклик от них.
И ощущения от архитектуры двоякие.
С одной стороны сам ассемблерный код несмотря на сильно упрощенный синтаксис и крайнюю схожесть с человекочитаемыми операторами из сишечки всё равно выглядит как стена ассемблерного и плохосчитываемого кода. :D Какой то революции человекочитаемого ассемблера не случилось.
С другой стороны мозг реально разгружен когда _пишешь_ на этом ассемблере по сравнению с классикой — не нужно как в Z80 на том же спектруме постоянно задумываться над тем как и куда перекинуть результаты из аккумулятора или HL, во что развернуть проверку регистровой пары на достижение нуля, какие там есть двухбайтовые инструкции на которых можно сэкономить и т.п.
8<=============
А виртуальной машины пока еще нет чтобы эффектами меряться. Да и много чего нет — инструкции в АЛУ даже вводятся по мере того как появляются в них потребности. Это в свою очередь интересно тем, что как только видно что какая то инструкция часто нужна, то берешь и вводишь её — например move with flags update которая пишется в этом синтаксисе как =? и перемещает данное обновляя флаги S и Z обычно не встречается, но тут сразу попросилась разгружать циклы для asciiz-строк.
Единственный формат инструкции Simpleton (2.0) содержит трёхбитовое поле условности инструкции COND:
И сама инструкция в процессе середины выполнения его в схемотехнических кишках должна отвлечься на его проверку и откинуть результат если условие не сработало.
Но как мы знаем побеждают всё-таки архитектуры где не каждая инструкция может быть условной, а всё-таки небольшое подмножество JMP, а всё остальное как правило работает плотнее если условности нет.
Но идеологическим примативом Simpleton является единственный формат инструкции «взять SRC (и возможно DST), поместить их в АЛУ с кодом операции INSTR и результат записать в DST. Это единственное что этот процессор умеет по своей идеологии и делать отдельный формат инструкций для него ну прям то не ради чего он придумывался.
И вот тут и рождается идея — а что если условность выполнения перенести в АЛУ?
Сделать код инструкции который делает следующее — воспринимает SRC как составную величину — 3 бита код условия и 13 бит знакового данного с расширением до 16 бит. И если код условия срабатывает, то складывает SRC с DST и выдаёт результат наружу — иначе наружу выдаётся неизменённый DST.
Тогда код такой операции „conditional 13-bit addition“ в случае если в качестве DST будет подставлен PC сработает как „relative conditional jump“!
Итого за счёт введения новой такой инструкции мы полностью освобождаем 3 бита COND в инструкции и можем, например, увеличить количество регистров с 8 до 16! И всё равно еще остаётся 1 бит. И при этом главное кредо Simpleton остаётся неизменным — правда АЛУ набухает и набухает, т.к. вся сложность и вариативность заметается в него. :)
Но это уже будет Simleton 3.0 пожалуй, ибо я всё еще в раздумьях как поэффективнее воспользоваться возникшими тремя битами (16 регистров, имхо, процессору в духе 8 бит только вредят).
Что интересно — не встречал ранее такого нигде — что код условия в операции становится частью операнда где хранится смещение условного перехода.
Это довольно необычно для меня и даже странновато. Но получается, что можно, например извлекая смещения переходов из таблицы динамически формировать и условность перехода где то в ключевой точке типа switch. Это даже что-то новенькое для меня и явно чувствуется, что может послужить источником каких то интересных полухаков. :)