Последнее обновление: крупный апгрейд онлайн-эмулятора: https://gamedev.ru/flame/forum/?id=249067&page=22&m=5402801#m317
Последний гитхаб: https://github.com/aa-dav/SimpX#readme
Онлайн-эмулятор: https://aa-dav.github.io/
>8------------------------------------
Родилась, имхо, более интересная архитектура (впрочем наследующая многое из нижесказанного прям тут): Simpleton 4.
Пора уже выделить в отдельную ветку из 8-битного "компьютера мечты" мою задумку 16-битного процессора под кодовым названием "Simpleton" (что с английского переводится как "недалёкий").
Здесь всё вкратце и по делу, хотя вся предыстория вопроса с рассуждениями вокруг да около рассредоточена в упомянутой теме.
Под впечатлением от некоторых идей Gigatron TTL мне захотелось описать достаточно простой архитектурно микропроцессор для некоего виртуального (пока) "8-битного компьютера мечты", который однако для пущей простоты будет 16-битным.
16-битность и ячейки памяти и регистров процессора сразу снимает многие сложные для реализации и практики вопросы - ведь 16-битное адресное пространство 8-биток было весьма неудобным при истинной 8-битности АЛУ.
Пакет исходников эмулятора Simpleton и ассемблера для него пока размещаю по следующей ссылке: https://yadi.sk/d/fTZqZ1n12dD72A
Как нибудь как будет свободное время перееду на github и расширю там суть содержимого.
Архитектура
И ячейки памяти и регистры - всё 16-битное в 16-битном адресном пространстве (таким образом общий объём памяти - 128Кб или 64кибислова).
Восемь 16-битных регистров общего назначения r0-r7. Три последних имеют особое поведение и имеют псевдонимы:
sp (r5) - регистр стека
pc (r6) - счётчик инструкций
flags (r7) - флаги
Простота архитектуры заключается в том, что все инструкции имеют одинаковый формат и смысл "взять 1 или 2 аргумента, провести над ними операцию в арифметико-логическом устройстве (АЛУ) и записать результат во второй аргумент".
Формат инструкции (которая как и ячейки памяти 16-битная) следующий:
Инструкция таким образом состоит из семи полей:
SRC - 3-битный код регистра-источника для первого входного данного в АЛУ (r0-r7)
SI - (source indirect, косвенность источника) однобитный флаг, что грузить данное SRC надо не из регистра, а из ячейки памяти адрес которой находится в этом регистре
DST - 3-битный код регистра-источника для второго входного данного в АЛУ и так же код регистра-приёмника для результата операции
DI - (destination indirect, косвенность приёмника) флаг, что грузить и сохранять данное из/в DST надо не в регистр, а по адресу памяти который в нём хранится
COND - 3-битный код условия - в зависимости от содержимого флагов текущая инструкция может быть пропущена
TO - (two operand instruction) - флаг двухоперандной инструкции - если этот бит 0, то АЛУ не нуждается во втором аргументе и его не надо грузить (тратить на это время)
CMD - 4-битный код операции которую будет над аргументом или аргументами выполнять АЛУ - при этом однооперандные и двухоперандные инструкции вещи разные поэтому спектр возможных операций вместе с битом TO составляет 32 штуки. Для однооперандных это может быть просто пересылка данных (отсутствие операции над SRC с сохранением результата в DST), инкременты/декременты, прокрутки, а двухоперандные - арифметика, битовые операции и т.п.
Таким образом инструкции в общем реализуют как бы операторы языка Си вида Y ?= X, где ?= - это оператор типа A = B, C += D или E &= F.
При этом X и Y могут быть:
- регистрами: r0 - r7
- ячейками памяти на которые регистры ссылаются (пишутся в квадратных скобках): [ r0 ] - [ r7 ]
- ячейками памяти конкретных адресов (immediate addresses): [ $0000 ] - [ $FFFF ]
- кроме того X (источник) может быть непосредственным данным (immediate): $0000 - $FFFF
Если первые два пункта очевидны из формата инструкций, то с непосредственными адресами и данными нужно пояснить.
Для этого надо понять особое поведение трёх выделенных регистров:
Регистр sp при косвенном чтении пост-инкрементируется, а при косвенной записи пре-декрементируется. Таким образом он становится регистром стека.
Регистр pc являясь счётчиком инструкции автоматически пост-инкрементируется при косвенном чтении. Поэтому на момент исполнения инструкции он указывает уже на следующую (т.к. через косвенное чтение из него текущая инструкция и считывается, что логично).
Так вот, если для поля SRC указать режим чтения из pc+indirect, то считанное как SRC значение продвинув pc поведёт себя полностью как непосредственное immediate-данное. Для DST такой трюк запрещён и код pc+indirect для DST зарезервирован для будущих применений.
Регистр flags содержит все флаги выполнения процессора (включая бит запрещения прерываний), поэтому косвенные чтения/запись из/в него совершенно лишены смысла.
В силу этого код flags+indirect (т.е. зажжены все биты соответствующих полей инструкции) зарезервирован под особый смысл - чтение за командой (из pc с автоматическим инкрементом) непосредственного данного адреса той ячейки памяти с которой и будет производится работа. Этот особый случай справедлив и для SRC и для DST и реализует косвенные обращения с конкретными ячейками памяти.
За счёт этого можно вообще не загрязнять регистры операциями вида:
[ $00A0 ] = 10 [ $00A0 ] += [ $00A1 ]
и т.п.
Конечно регистры полезны для промежуточных вычислений.
Таким образом одна инструкция Simpleton может занимать от одного до трёх слов и содержать от 0 до 2 непосредственных данных или адресов памяти.
Ассемблер
Программы на ассемблере Simpleton выглядят так:
PORT_CONSOLE = $FFFF sp = $FF00 pc = start mark dw 0 ; 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 = $0001 r1 = $1000 r0 &= r1 [ mark ] = r0 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
По привычным для ассемблеров правилам идентификатор не отделённый от начала строки пробелом создаёт метку за которой уже находятся ключевые слова или инструкции.
Парсер ассемблера почти не делит символы на особые и остальные, поэтому все ключевые слова и даже квадратные скобки должны быть отделены друг от друга пробелами!
; начинает комментарий в любом месте строки.
Оператор = для метки превращает её в идентификатор со значением справа от знака равно - таким образом первая строка определяет константу для порта ввода-вывода.
Ключевое слово org перемещает текущий адрес компиляции в указанный const-адрес.
Ключевое слово dw заполняет текущую ячейку памяти указанными далее через пробел словами (можно использовать адреса меток даже тех что будут объявлены позднее). Здесь можно указать строковой литерал заключенный между двойных кавычек как строки в Си - но отличие будет в том, что никакого нулевого терминатора сам ассемблер автоматически не вставляет - если надо он указывается явно после строки.
Ключевое слово ds создаёт массив слов размером указанного далее const-параметра и если далее указать еще один const-параметр, то заполнит их не нулями, а этим значением.
Поддерживаются локальные метки - если начинать метку с символа точки, то она получает имя последней нормальной метки + себя, т.е. global.local при этом употребление такой локальной метки в любом месте после имени глобальной автоматически дополнит её до полного имени, но если нужно будет обратится из другого глобального блока, то придётся писать имя локальной метки полностью.
Почти все машинные команды как уже было сказано выше имеют вид Y ?= X, где оператор может быть:
Однооперадные операторы реализованные сейчас
= простая пересылка данных
=? пересылка данных с обновлением флагов Zero и Carry
=+1 инкремент
=+2 инкремент на 2
=-1 декремент
=-2 декремент на 2
Двухоперандные операторы реализованные сейчас
+= - сложение
+c= - сложение с учётом флага переноса
-= - вычитание
-c= - вычитание с учётом флага переноса
&= - двоичное и
|= - двоичное или
^= - двоичный XOR
<?> - сравнение (не портит DST т.е. пропускает его через АЛУ без изменений откидывая SRC)
Для краткости и понятности вызова процедур ввёл 4 псевдоинструкции:
call arg ; эквивалентно следующему: [ sp ] =+2 pc pc = arg
ret ; эквивалентно pc = [ sp ]
а так же для быстрых вызовов:
qcall arg ; эквивалентно r4 =+2 pc pc = arg
и
qret ; эквивалентно pc = r4
Здесь заодно видно как предполагается вызов и возврат из процедур и что вообще переходы в этом процессоре суть есть просто запись в регистр счётчика инструкций pc.
Коды условий сейчас могут находится в инструкции буквально в любом месте и имеют вид @условие, в частности:
@@ - всегда (по умолчанию)
@z или @= - флаг нуля
@nz или @!= - не флаг нуля
@c - флаг переноса
@nc - не флаг переноса
@> - больше (для данных со знаком)
@>= - больше или равно (для данных со знаком)
т.е. некоторые элементы взяты с архитектуры команд процессоров PDP-11, MSP430?
А при команде переноса как (или откуда первый/последний бит переносимого результата) формируется флаг переноса?
KPG
> А при команде переноса как (или откуда первый/последний бит переносимого
> результата) формируется флаг переноса?
"С переносом" значит что перенос учитывается как третье слагаемое, т.е. ADC у Intel и прочих.
Вообще состав всех вещей которые делает АЛУ еще не зафиксирован, поэтому многое пока не описано или вообще отсутствует. Всё рамках свободного редкого времени на это маленькое хобби. Надеюсь, что на пенсии доделаю в железе на FPGA. :D
KPG
> т.е. некоторые элементы взяты с архитектуры команд процессоров PDP-11, MSP430?
Ну, у всех процессоров есть что-то общее. :) В первую очередь ориентировка шла на Gigatron TTL, а то что повышенная ортогональность регистров и бит индирекции - это довольно частые гости много где.
reserved
reserved
Реализовав ассемблер начал задумываться над бОльшим - над ЯВУ для Simpleton. :)
Пока выкристаллизовались следующие требования:
- компилятор со статической типизацией сильно похожий на Си (в связи с нижеследующими пунктами), но ни в коем случае не конкурент Си, ибо нефиг
- максимальная простота синтаксиса для того чтобы можно было реализовать компилятор на самом Simpleton. компилятор должен быть возможен однопроходный с минимумом расходом памяти для текущего состояния компилятора в процессе прохода.
- как можно большая эффективность для 16-битного процессора без сложных схем адресации
- для простоты реализации функции можно помечать как нерееэнтерабельные - такие функции не полагаются на дорогую стековую адресацию своих аргументов и параметров - и то и другое располагается в глобальной памяти. такие функции могут вызывать только нереэнтерабельные же функции либо не вызывать других функций вообще.
- STDLIBless - ядро языка не предполагает вообще что существуют какие то скрытые под капотом функции ядра незримо выполняющиеся - весь код пишет сам программист или содержится во внешних библиотеках
в Си первых ревизий еще от Кернигана и Ричи есть немало здравых идей в пользу 8-битного минимализма - например любая функция возвращает параметр укладывающийся в слово в регистре r0, просто если не возвращает, то в нём мусор который вызывающий код отбрасывает. это забавно. но с другой стороны есть немало в Си такого чего не хочется тащить сюда - например отдельный неймспейс для структур.
Наверное, лучше сразу отсюда начать плясать - https://llvm.org/docs/WritingAnLLVMBackend.html А IR хоть подмножеством JavaScript генерировать
0iStalker
LLVM не отвечает одному из основных требований - компиляции на самом Simpleton.
=A=L=X=
Так если настроить бэкэнд на симплетона, то можно будет и сам llvm скомпилировать на симплетон, и будет ништяк.
Delfigamer
> и сам llvm скомпилировать на симплетон
:) Vulkan если еще портировать, то воще зажить можно будет!
=A=L=X=
Берёшь Вулкан и портируешь. Но сначала всё равно понадобится рабочий компилятор мейнстрима.
Delfigamer
> можно будет и сам llvm скомпилировать на симплетон, и будет ништяк.
Вряд ли он уложится в 64кб памяти. ну и портирование стдлибы с файлами, тредами, сигналами, плавающей точкой и прочим весельем тоже не выглядит простым занятием.
Пока наверное тема главным образом превратится в рассуждения про очень простой язык в духе Си максимально простой для 8/16-биток.
Пока дам ему рабочее название - SimpL.
Сперва про типы - что мне не нравится в Си что типы данных закручиваются вокруг имени переменной которую они объявляют и слева и справа.
В шарпе даже сделали более логичные int[] array;
В SimpL объявление типа будет линейным - оно начинается с какого то примитивного типа, структуры или определённого пользователем типа (ключевое слово type - аналог typedef в Си) и далее каждый новый слой типизации добавляется к нему справа применяясь ко всему что написано слева до него целиком.
Слои типизации - указатель, массив и функция.
int - целый тип.
int* - указатель на целое.
int*[10] - массив из 10 указателей на целое
int*[10]* - указатель на массив из 10 указателей на целое.
int*[10]*(word) - указатель на функцию от одного аргумента типа word возвращающую указатель на массив из 10 указателей на целое.
int*[10]*(word)[20]*(int,word) - указатель на функцию от двух аргументов типа int и word возвращающую указатель на массив из 20 указателей на функции принимающие аргумент типа word и возвращающие указатель на массив из 10 указателей на int.
Тут надо заметить, что в объявлении типов скобки разворачиваются сразу в указатель на функцию, которая возвращает в качестве результата тип объявленный левее, т.е. не надо для функций (в отличие от массивов) вставлять явный * для декларирования - ибо оно тут лишено смысла изначально всегда было.
Такие вот деклараторы типов которые используются как то так:
var a b c: int*[10]*(word)[20]*( int,word); type uberT: int*[10]*( word)[20]*( int,word); var x y z: uberT;
Пока вырисовывается что-то типа такого...