SimpX - моя виртуальная 16-битная машина на виртуальном процессоре Simpleton 4

Тема в разделе "WASM.PROJECTS", создана пользователем aa_dav, 24 ноя 2023.

  1. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    447
    Давно уже хотел сделать свою 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 пикселей.
     
    Последнее редактирование: 24 ноя 2023
    comrade, mantissa, Intro и 4 другим нравится это.
  2. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    447
    P.S.
    Кстати, забыл еще обозначить, что последний компилируемый пример test08-wolf3d.asm это не моих рук творение, а проект человека с ником Total Vacuum на другом ресурсе, который имеет свой компилятор Forth и адаптировал его под SimpX.
    Поэтому асмокод там сразу же выглядит весьма криптовато - по факту это то ли шитый код Forth-машины, то ли что-то в этом духе. Поэтому он на самом деле компилировал свои программы уже написанные на другие платформы просто создав реализацию самого компилятора под SimpX.
    Сам движок а-ля Wolf-3D откликается на кнопки AWD.
     
  3. Intro

    Intro Active Member

    Публикаций:
    0
    Регистрация:
    29 авг 2009
    Сообщения:
    563
    Кстати, я тут в UASM столкнулся с проблемой, компилятору сложно отличить где код, а где просто выражения макропеременной. Лучше сделать r1 := r2 + r3, ну как в паскале, и чтобы сразу было видно где код, а где макровыражение.
    ЗЫ
    И ещё, про умножение и деления. Тут лучше взять пара регистров, типа так: r0:r1 = r2 * r3. Под номер регистров результата выдаётся 2 бита, а 3-й можно использовать как флаг знаковой/беззнаковой операции. Либо даже 1 бит под номер пары, а ещё один бит ещё под что-то, ну скажем умножение/деление, тогда в опкоде достаточно выделить одну команду.
     
    Последнее редактирование: 25 ноя 2023
  4. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    447
    Макросы я вообще еще не делал и пока не вижу потребности даже их делать, всех элементарных конструкций хватает чтобы писать софт, так что лучше писать софт, чем тратить время на никому ненужные удобства, но и нету времени сейчас и это делать.

    У меня их нет в системе команд. :) По духу это больше 8-битный процессор.
    Код (Text):
    1.  
    2. ; r0_mul_r1 - умножить r0 на r1 (числа без знака)
    3. ; вход:   r0, r1 - множители
    4. ; выход:   r0 - результат
    5. r0_mul_r1     [ sp ] <- r1   ; сохраняем регистры в стек
    6.      [ sp ] <- r2
    7.      r2 <- r0     ; перекладываем множитель в r2
    8.      r0 = r0 - r0   ; обнуляем r0 - он будет аккумулировать результат
    9. .loop     r1 <= r1 >> 1   ; прокручиваем r1 на бит вправо (циклически)
    10.      jnc .skip     ; если бит нулевой - пропускаем накопление результата
    11.      r0 = r0 + r2   ; аккумулируем результат в r0
    12. .skip     r2 = r2 + r2   ; сдвигаем r2 влево (умножаем на 2)
    13.      r1 = r1 & $7FFF   ; очищаем "провёрнутый" бит в r1 заодно тестируя на 0
    14.      jnz .loop     ; если не 0 - повторяем цикл
    15.      r2 <- [ sp ]   ; восстанавливаем регистры из стека
    16.      r1 <- [ sp ]
    17.      ret     ; выход из процедуры
    18.  
    Если нужна двойная точность результата, то надо делать add/adc регистровых пар.
     
  5. Intro

    Intro Active Member

    Публикаций:
    0
    Регистрация:
    29 авг 2009
    Сообщения:
    563
    Ну всё равно, можно и с некоторыми другими командами так делать, там же осталось незадействованные опкоды 0Eh, 0Fh.
    Хотя на счёт макросистемы, можно попробовать упрощённый Lua прикрутить, отключить не нужное и настроить как надо, чтобы со строками никаких проблем, и луашные таблицы были. Я слишком привык к сахару, .if, .while, .for, .switch и так далее. Вон посмотрите на мой вариант 8080 для UASM, это же работает не надо запоминать кучу опкодов, код больше похож на Си, создавать сложные программы намного легче. Скажем есть в коде деление на константу, а умная макрофункция может переделать код делением через умножение на константу. Ну и прочие, я например на голом ассме не кодирую, сложно и затратно.
     
    Последнее редактирование: 25 ноя 2023
  6. Intro

    Intro Active Member

    Публикаций:
    0
    Регистрация:
    29 авг 2009
    Сообщения:
    563
    А сложно сделать физическое воплощение этого ЦПУ? Некоторые делаю дома микросхемы, можно найти ролики на ютубе. Думаю не сложней 8080 должно получиться. Ну или на ПЛИС сварганить.
     
  7. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    447
    Имхо тут более уместно Сишечку реализовывать, чем асм улучшать до каких то небес. Причём в стиле gcc чтобы сишечка генерировала асм, а он уже собирался в бинарник - так что просто будет миксовать код из ЯВУ с асмом. Но наколенная реализация сишечки однозначно будет хромать без мощных оптимизаций которые сейчас есть в gcc/clang и скорость кода ожидаемо должна упасть в несколько раз. Вообще этот процессор по 8-битному совсем не совсем дружит с такими абстракциями как стековые переменные - ведь полностью отсутствует адресация со смещениями, в том числе относительно SP. Поэтому чтобы нацелиться на переменную в стеке надо выполнять
    Код (Text):
    1.  
    2.     r0 = sp +s var_offset ; получили в регистре указатель на переменную (+s это сложение без обновления флагов)
    3.  
    Теперь по r0 можно читать/писать или даже использовать в командах. Например a = b + c; может быть в моменте реализовано как:
    Код (Text):
    1.  
    2.     r0 = sp +s a_offset
    3.     r1 = sp +s b_offset
    4.     r2 = sp +s c_offset
    5.     [ r0 ] = [ r1 ] + [ r2 ]
    6.  
    Вот это вот предварительное загрязнение регистра указателем на переменную в стеке конечно будет немного медленее машин где стековые переменные обслуживаются как родные. С другой стороны это всё-равно получше будет, чем реалии i8080 и даже Z80 - по байтам если считать в них будет даже хуже получаться из-за неортогональности регистров. Тот случай когда простота лучше изощрённости в системе команд. Недаром RISC зарулил в своё время.
    Ну и влобная неоптимизированная реализация Сишечки понятно что будет постоянно гонять данные из памяти в регистры и обратно как оно и было в 80-е годы.

    Ну как бы одной из целей что я ставил была как раз и простая физическая реализация чтобы даже поверхностная логика работы была как можно более простой. Так что я тоже думаю, что должно по сложности быть на уровне 8-биток. Даже возможно по числу транзисторов будет +- то же самое. И реально есть мечта до которой пока очень далеко реализовать на каком нибудь MisterFPGA всю машину целиком - SimpX - вместе с портами ввода-вывода, видеосистемой и т.п.
    --- Сообщение объединено, 26 ноя 2023 ---
    Кстати, видеорежим там не простой. Если спозиционироваться на примере test06-bitmap-noise.asm и заменить исходник на следующее:
    Код (Text):
    1.  
    2. ; ******************************************************
    3. ; * test06.asm - заполнение графического битмапа шумом *
    4. ; ******************************************************
    5.  
    6. #include "simple_lib.inc"
    7.  
    8. start     call initGraphicMode   ; инициализируем графический режим
    9.  
    10.      ; заполняем экранную область возрастающими числами
    11.      r0 <- bitmapBase   ; r0 - начало экрана
    12.      r1 <- bitmapSize   ; r1 - размер экрана
    13.      r2 <- 0     ; зануляем r2 (могло бы быть r2 = r2 - r2)
    14. .loop1     [ r0 ] <- r2   ; записываем текущий r2
    15.      r0 <- r0 + 1   ; инкремент r0
    16.      r2 <- r2 + 1   ; инкремент r2
    17.      r1 <= r1 - 1   ; декремент r1 с влияением на флаги
    18.      jnz .loop1   ; если не ноль - повторяем
    19.  
    20.      ;
    21. .repeat     r0 <- bitmapBase   ; r0 - начало экрана
    22.      r1 <- bitmapSize   ; r1 - размер экрана
    23.      r2 <- 0     ; зануляем r2
    24.      ; прибавляем к полоске пикселей 1x4 из битмапа r2
    25. .loop     [ r0 ] = [ r0 ] + r2
    26.      r0 <- r0 + 1   ; инкрементируем r0
    27.      r2 = r2 + $1   ; увеличиваем r2 на 1
    28.      void = r2 - 25   ; проверяем не стал ли r2 равен 25
    29.      jnz .next     ; если нет, то идём на .next
    30.      r2 = r2 - r2   ; иначе обнуляем r2
    31. .next     r1 <= r1 - 1   ; декрементируем r1 с проверкой на 0
    32.      jnz .loop     ; если не 0, то идём на новую итерацию
    33.      ; ждём четыре кадра
    34.      psw = psw | CPU_HALT
    35.      psw = psw | CPU_HALT
    36.      psw = psw | CPU_HALT
    37.      psw = psw | CPU_HALT
    38.      ; скроллируем
    39.      [ vidScrollX ] <- [ vidScrollX ] + 1
    40.      [ vidScrollY ] <- [ vidScrollY ] + 1
    41.      pc <- .repeat   ; повторяем процедуру с самого начала
    42.  
    Где есть только одна добавочка крохотная в конце:
    Код (Text):
    1.  
    2.      ; скроллируем
    3.      [ vidScrollX ] <- [ vidScrollX ] + 1
    4.      [ vidScrollY ] <- [ vidScrollY ] + 1
    5.  
    То есть мы читаем из портов ввода-вывода управляющих аппаратным скроллингом экрана и инкрементируем их. Заметьте как просто и естественно обращаться к глобальным переменным тут - здесь уже реализация Сишечки может выдавливать соки из архитектуры очень качественно - просто берём и в одну инструкцию делаем 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 так что битмап отображается "как есть", как прямоугольный битмап линейной раскладки)