Давно уже хотел сделать свою ISA и такую чтобы древностью и мхом пахла - откуда то из 8-битного детства. Но в итоге для максимальной простоты системы команд она получилась 16-битной (и даже ячейки памяти 16-битные). В результате реализовал виртуальный процессор. Наверное система команд более всего похожа на PDP-11, но сходство отдалённое. Во главе всего максимальная простота, но такая, чтобы было равенство и братство в ортогональности регистров и потому кодирование инструкции максимально простое. Можно сказать, что все инструкции делают только одну вещь - берут два аргумента, пропускают их через ALU и записывают результат в назначение. Этого на деле достаточно чтобы введя лёгкие side-effect-ы на системные регистры такие как указатель инструкций или указатель стека сделать практически любую команду привычных ассемблеров в духе RISC. Хотя вот call в одну инструкцию не уложился и является псевдоинструкцией ассемблера на самом деле разлагающуюся на две базовых инструкции. Так же я пропагандирую "математическую нотацию ассемблера", хотя есть и классический режим add r1 r2 r3, а не r1 = r2 + r3. Страничка на гитхабе где всё есть включая исходники: https://github.com/aa-dav/SimpX Описание архитектуры процессора и синтаксиса ассемблера на русском: https://github.com/aa-dav/SimpX/blob/master/README_simpleton_ru.md Онлайн-эмулятор где есть ряд готовых программ на ассемблере и можно вбивать свои и смотреть на результат тут: https://aa-dav.github.io/ В эмуляторе чтобы собрать код и запустить проект надо выбрать файл который будет "корнем компиляции" и нажать меню Compile and run. Есть инклюды воспринимающие имена соседних файлов, но в момент компиляции выбран должен быть в списке тот который будет компилироваться. Так же для аутентичности рекомендую жать меню View->Set 400% чтобы браузер не растягивал всё на полный экран, а выставил правильное соотношение сторон 4x3. Исходники асма обильно откомментированы так что в коде simple_lib.inc можно почерпнуть много сведений какие есть порты ввода-вывода, какая в ней раскладка памяти, какие готовые процедуры и т.п.. Вот как видеопамять устроена - этим прям горжусь, это единственный тоже видеорежим красиво сочетающий и тайлы и графику в одном. В итоге заполнив нужным образом зону пикселей можно в зону атрибутов писать так как будто мы работаем в текстовом видеорежиме и наоброт - заполнив специфическим образом зону атрибутов можно работать с зоной пикселей так как будто бы это линейный пиксельный видеорежим 16 цветов (4 пикселя на слово), но с одной из 16 палитр на знакоместо 8x8 пикселей.
P.S. Кстати, забыл еще обозначить, что последний компилируемый пример test08-wolf3d.asm это не моих рук творение, а проект человека с ником Total Vacuum на другом ресурсе, который имеет свой компилятор Forth и адаптировал его под SimpX. Поэтому асмокод там сразу же выглядит весьма криптовато - по факту это то ли шитый код Forth-машины, то ли что-то в этом духе. Поэтому он на самом деле компилировал свои программы уже написанные на другие платформы просто создав реализацию самого компилятора под SimpX. Сам движок а-ля Wolf-3D откликается на кнопки AWD.
Кстати, я тут в UASM столкнулся с проблемой, компилятору сложно отличить где код, а где просто выражения макропеременной. Лучше сделать r1 := r2 + r3, ну как в паскале, и чтобы сразу было видно где код, а где макровыражение. ЗЫ И ещё, про умножение и деления. Тут лучше взять пара регистров, типа так: r0:r1 = r2 * r3. Под номер регистров результата выдаётся 2 бита, а 3-й можно использовать как флаг знаковой/беззнаковой операции. Либо даже 1 бит под номер пары, а ещё один бит ещё под что-то, ну скажем умножение/деление, тогда в опкоде достаточно выделить одну команду.
Макросы я вообще еще не делал и пока не вижу потребности даже их делать, всех элементарных конструкций хватает чтобы писать софт, так что лучше писать софт, чем тратить время на никому ненужные удобства, но и нету времени сейчас и это делать. У меня их нет в системе команд. По духу это больше 8-битный процессор. Код (Text): ; r0_mul_r1 - умножить r0 на r1 (числа без знака) ; вход: r0, r1 - множители ; выход: r0 - результат r0_mul_r1 [ sp ] <- r1 ; сохраняем регистры в стек [ sp ] <- r2 r2 <- r0 ; перекладываем множитель в r2 r0 = r0 - r0 ; обнуляем r0 - он будет аккумулировать результат .loop r1 <= r1 >> 1 ; прокручиваем r1 на бит вправо (циклически) jnc .skip ; если бит нулевой - пропускаем накопление результата r0 = r0 + r2 ; аккумулируем результат в r0 .skip r2 = r2 + r2 ; сдвигаем r2 влево (умножаем на 2) r1 = r1 & $7FFF ; очищаем "провёрнутый" бит в r1 заодно тестируя на 0 jnz .loop ; если не 0 - повторяем цикл r2 <- [ sp ] ; восстанавливаем регистры из стека r1 <- [ sp ] ret ; выход из процедуры Если нужна двойная точность результата, то надо делать add/adc регистровых пар.
Ну всё равно, можно и с некоторыми другими командами так делать, там же осталось незадействованные опкоды 0Eh, 0Fh. Хотя на счёт макросистемы, можно попробовать упрощённый Lua прикрутить, отключить не нужное и настроить как надо, чтобы со строками никаких проблем, и луашные таблицы были. Я слишком привык к сахару, .if, .while, .for, .switch и так далее. Вон посмотрите на мой вариант 8080 для UASM, это же работает не надо запоминать кучу опкодов, код больше похож на Си, создавать сложные программы намного легче. Скажем есть в коде деление на константу, а умная макрофункция может переделать код делением через умножение на константу. Ну и прочие, я например на голом ассме не кодирую, сложно и затратно.
А сложно сделать физическое воплощение этого ЦПУ? Некоторые делаю дома микросхемы, можно найти ролики на ютубе. Думаю не сложней 8080 должно получиться. Ну или на ПЛИС сварганить.
Имхо тут более уместно Сишечку реализовывать, чем асм улучшать до каких то небес. Причём в стиле gcc чтобы сишечка генерировала асм, а он уже собирался в бинарник - так что просто будет миксовать код из ЯВУ с асмом. Но наколенная реализация сишечки однозначно будет хромать без мощных оптимизаций которые сейчас есть в gcc/clang и скорость кода ожидаемо должна упасть в несколько раз. Вообще этот процессор по 8-битному совсем не совсем дружит с такими абстракциями как стековые переменные - ведь полностью отсутствует адресация со смещениями, в том числе относительно SP. Поэтому чтобы нацелиться на переменную в стеке надо выполнять Код (Text): r0 = sp +s var_offset ; получили в регистре указатель на переменную (+s это сложение без обновления флагов) Теперь по r0 можно читать/писать или даже использовать в командах. Например a = b + c; может быть в моменте реализовано как: Код (Text): r0 = sp +s a_offset r1 = sp +s b_offset r2 = sp +s c_offset [ r0 ] = [ r1 ] + [ r2 ] Вот это вот предварительное загрязнение регистра указателем на переменную в стеке конечно будет немного медленее машин где стековые переменные обслуживаются как родные. С другой стороны это всё-равно получше будет, чем реалии i8080 и даже Z80 - по байтам если считать в них будет даже хуже получаться из-за неортогональности регистров. Тот случай когда простота лучше изощрённости в системе команд. Недаром RISC зарулил в своё время. Ну и влобная неоптимизированная реализация Сишечки понятно что будет постоянно гонять данные из памяти в регистры и обратно как оно и было в 80-е годы. Ну как бы одной из целей что я ставил была как раз и простая физическая реализация чтобы даже поверхностная логика работы была как можно более простой. Так что я тоже думаю, что должно по сложности быть на уровне 8-биток. Даже возможно по числу транзисторов будет +- то же самое. И реально есть мечта до которой пока очень далеко реализовать на каком нибудь MisterFPGA всю машину целиком - SimpX - вместе с портами ввода-вывода, видеосистемой и т.п. --- Сообщение объединено, 26 ноя 2023 --- Кстати, видеорежим там не простой. Если спозиционироваться на примере test06-bitmap-noise.asm и заменить исходник на следующее: Код (Text): ; ****************************************************** ; * test06.asm - заполнение графического битмапа шумом * ; ****************************************************** #include "simple_lib.inc" start call initGraphicMode ; инициализируем графический режим ; заполняем экранную область возрастающими числами r0 <- bitmapBase ; r0 - начало экрана r1 <- bitmapSize ; r1 - размер экрана r2 <- 0 ; зануляем r2 (могло бы быть r2 = r2 - r2) .loop1 [ r0 ] <- r2 ; записываем текущий r2 r0 <- r0 + 1 ; инкремент r0 r2 <- r2 + 1 ; инкремент r2 r1 <= r1 - 1 ; декремент r1 с влияением на флаги jnz .loop1 ; если не ноль - повторяем ; .repeat r0 <- bitmapBase ; r0 - начало экрана r1 <- bitmapSize ; r1 - размер экрана r2 <- 0 ; зануляем r2 ; прибавляем к полоске пикселей 1x4 из битмапа r2 .loop [ r0 ] = [ r0 ] + r2 r0 <- r0 + 1 ; инкрементируем r0 r2 = r2 + $1 ; увеличиваем r2 на 1 void = r2 - 25 ; проверяем не стал ли r2 равен 25 jnz .next ; если нет, то идём на .next r2 = r2 - r2 ; иначе обнуляем r2 .next r1 <= r1 - 1 ; декрементируем r1 с проверкой на 0 jnz .loop ; если не 0, то идём на новую итерацию ; ждём четыре кадра psw = psw | CPU_HALT psw = psw | CPU_HALT psw = psw | CPU_HALT psw = psw | CPU_HALT ; скроллируем [ vidScrollX ] <- [ vidScrollX ] + 1 [ vidScrollY ] <- [ vidScrollY ] + 1 pc <- .repeat ; повторяем процедуру с самого начала Где есть только одна добавочка крохотная в конце: Код (Text): ; скроллируем [ vidScrollX ] <- [ vidScrollX ] + 1 [ vidScrollY ] <- [ vidScrollY ] + 1 То есть мы читаем из портов ввода-вывода управляющих аппаратным скроллингом экрана и инкрементируем их. Заметьте как просто и естественно обращаться к глобальным переменным тут - здесь уже реализация Сишечки может выдавливать соки из архитектуры очень качественно - просто берём и в одну инструкцию делаем a = b + c вообще не заморачиваясь с какими то промежуточными оффсетами. Ляпота. Так вот в таком варианте можно заметить, что экран начнёт плыть по диагонали. Да, есть аппаратный скроллинг, т.к. я планирую чтобы это был игровой комп, а не академическая штука. Поэтому есть собственно и такая штука как обработка прерывания по лучу кинескопа - VBlank - эта штука важную роль играла в Денди, например: https://wasm.in/threads/vblank-i-hblank-na-primere-famicom-nes-dendi.34398/ --- Сообщение объединено, 26 ноя 2023 --- P.S. Видеорежим один - есть страница битмапа 256x256 16-цветных пикселей (4 пикселя в слове), т.е. 16384 слов надо на битмап. На экране видно 256x192 пикселя - где именно окно находится управляется портами ввода-вывода vidScrollX и vidScrollY - при этом изображение "проворачивается" через границы чтобы делать сроллеры. Но на экран пиксели выводятся не напрямую, а через опрос таблицы атрибутов 32x32 (1024 слова). И вот тут можно сказать так, что экранная область это 32x32 тайла и они вот квадратно-гнездовым способом соответствуют 256x256 битмапу так что каждый тайл это плитка 8x8. И верхние 4 бита в таблице атрибутов задают какую палитру (0..15) использует соответствующий тайл. Т.е. максимально цветов на экране может быть 256 из 256-цветной палитры, но не более 16 цветов в одном тайле/знакоместе. Но кроме этого - и это очень важно - нижние биты в таблице атрибутов говорят так же какой именно тайл из битмапа в данном тайле выводится (0..1023) - то есть если заполнить таблицу атрибутов линейно нарастающими числами 0..1023, то битмап будет отображаться на экране как монолитный пиксельный фреймбуфер с нулевой палитрой. Но если заполнить битмап изображением шрифта (причём реально квадратно-гнездовым методом - табличкой квадратной где номера символов 8x8 растут линейно слева-направо/сверху-вниз), то прописывая в таблицу атрибутов номер символа мы получим текстовый видеорежим где хоть весь экран может быть залит одним символов и изменение пикселей этого символа в таблице битмапа сразу поменяет его на всём экране. Поэтому в simple_lib.inc есть функции по выставлению как тестового так и графического режимов - на деле никакие порты ввода-вывода виртуального видеочипа в выставлении видеорежима не задействуются, а просто заливаются соответствующим образом битмап и таблица атрибутов. --- Сообщение объединено, 26 ноя 2023 --- P.P.S (в текстовом видеорежиме в битмап заливается изображение шрифта и таблица атрибутов зануляется, типа всё изначально залито "пробельным/нулевым" символом, а в графическом видеорежиме таблица атрибутов заливается нарастающими числами 0..1023 так что битмап отображается "как есть", как прямоугольный битмап линейной раскладки)