Periwinkle: процессор с одной инструкцией +30



Хочу рассказать о процессоре, который я разработал в 2016 году. Он реализован на C как виртуальная машина. Мой друг Бьёрн написал для него ассемблер на F#.

Periwinkle представляет собой процессор OISC (one instruction set computer), в отличие от RISC и CISC. У него нет никакой конвейеризации. По сути, производительность не является главной задачей проекта, он создан скорее для удовольствия и в учебных целях.

Моя подруга Алёна придумала ему название Periwinkle, то есть барвинок (этот удивительно живучий цветок считается символом жизненной силы — прим. пер.)

Есть множество типов инструкций для OISC. Но в Periwinkle это инструкция move. Просто перемещаете литерал в регистр или значение из одного регистра в другой. Логические и арифметические операции, ветвление и т.д. выполняются с помощью регистров.

Длина инструкции Periwinkle стабильно 40 бит. Шина данных 32 бита.



У Periwinkle в общей сложности 64 регистра.



Вот описание некоторых:

Программный счётчик (PC)

  • Счётчик до 32 бит
  • Размер памяти программы составляет 255 40-битных слов (зависит от реализации)
  • Перемещение -1 в регистр PC приводит к явной остановке счётчика

Стек общего назначения (STK)
  • Стек для чего угодно (16 уровней в глубину)
  • Нет сигнала переполнения и неполного стека
  • Чтение пустого стека возвращает 0
  • Перемещение сюда значения — это операция push
  • Перемещение отсюда — операция pop

Генератор случайных чисел (RNG)

  • При вызове генерирует случайное 32-битное число
  • Перемещение сюда значения кажется бессмысленным

Skip If Zero (SIZ)
  • Пропускает следующую инструкцию, если в него переместить нуль

Skip If Non-zero (SINZ)
  • Пропускает следующую инструкцию, если в него переместить ненулевое значение

Reference (REF)

  • Используется для указания на адрес на основе перемещённого значения
  • Большие значения усекаются до 6-битных чисел

Dereference (DEF)
  • Разыменование после REF

Зарезервированные регистры (RSV)
  • Перемещение сюда значения не имеет эффекта. Регистр по-прежнему будет содержать ноль.
  • Можно применить для какой-нибудь задачи при переносе виртуальной машины на микроконтроллер или чего-то еще
  • Для будущих/дополнительных регистров
  • Можно использовать для удаления элемента из стека общего назначения и операционных регистров, переместив элемент сюда (не рекомендуется)
  • При чтении возвращает 0

Регистры общего назначения (GPR0-GPR31)

  • Могут содержать 32-битные числа

Нулевой регистр

  • Можно использовать для удаления элемента из стека общего назначения и операционных регистров, переместив этот элемент сюда
  • Возвращает 0 при чтении

Регистр состояния:

  • 0000 0000 0000 0000 0000 0000 000P ZVNC
  • Содержит пять флагов (C, N, V, Z, P)
  • Carry
  • Negative
  • Overflow
  • Zero
  • Регистр PLUS влияет на флаги C, N, V, Z, P
  • Регистры AND, OR, XOR влияют на флаги N, Z, P
  • Последняя запущенная операция повлияет на регистр состояния
  • Перемещение значения здесь кажется бессмысленным.

Но как работают регистры PLUS, AND, OR, XOR? В этих четырёх регистрах есть своеобразный стек, который является на самом деле вычислительным стеком. Когда в стеке вычислений два числа, операция запускается.



Вот пример для регистра PLUS. Схема работает аналогично для остальных трёх регистров.

Но как производить арифметические действия всего с четырьмя операторами? Как я уже говорил, цель этого проекта — развлечение и образование. Поэтому можете самостоятельно синтезировать недостающие действия.

Вычитание производится путём сложения с дополнительным кодом, который представляет отрицательные числа. Дополнительный код образуется инверсией битов, то есть запушив на число 0xFFFFFFFF (2??-1) через XOR. И прибавив единицу через регистр PLUS. Впрочем, ассемблер поддерживает отрицательные числа, так что не обязательно всё это делать.

Умножение — просто многократное сложение.

Деление — это сколько раз одно число помещается в число. Это можно посчитать через счётчик последовательных вычитаний.

Битовый сдвиг делается умножением на 2 или делением на 2.

Сами подумайте, как синтезировать другие операции.

Если интересно, вот репозиторий github некоторых программ на ассемблере, которые я написал для Periwinkle. Инструкция move работает слева направо:

#50 gpr0 //переместить литерал 50(base-10) в gpr0
gpr0 gpr1 //переместить значение gpr0 в gpr1

Кроме того, попытаюсь выложить исполняемый файл виртуальной машины Periwinkle VM. Для какой платформы делать виртуальную машину? (Windows (x86? x86-64?), Linux (x86? x86-64? ARM?, ARM64? и т.д.?) и т.д.?) Поскольку ассемблер написан на F#, наверное, он может работать везде, нужен только .NET framework, можете заглянуть в Mono.

Если вы знаете архитектуру PIC16, то могли заметить у Periwinkle некоторое сходство с ней (STATUS, SIZ, SINZ, REF, DEF). Действительно, она вдохновила меня на работу как первая архитектура, с которой я начал программировать на ассемблере.




Комментарии (16):

  1. Griboks
    /#20999988

    Объясните, пожалуйста, как запускаются операция сложения?

    Когда в стеке вычислений два числа, операция запускается.

    Автор использует 41 бит: 40 бит на число + 1 грязный бит? Ведь регистр всегда содержит два числа, в любом случае. Даже если его обнулять после очередного сложения, там всё-равно останутся 0 и 0, что должно бесконечно рекурсивно запускать сложение и вызвать stack overflow.

    • Politura
      /#21000172

      Там же схема есть «Plus register example».
      На схеме последняя операция plus gpr0 и комментарий: значение 60 из регистра plus перемещается в регистр gpr0, а регистр plus при этом обнуляется.
      Думаю, там нет никакого стека, просто стек придумали для облегчения понимания.
      Скорее всего, все что кладем в plus, прибавляется к его текущему значению, а когда из plus что-то забираем, то plus обнуляется.

      • Griboks
        /#21001640

        А как он обнуляется? Как различается нулевое и обнулённое значение? Там стоит счётчик записи? Существует область памяти с пометками занятости всех адресов? У каждого числа есть флаг обнуления?

        • Politura
          /#21006904

          Зачем все эти сложности? Ладно, попробую написать псевдокодом, надеюсь будет понятнее.
          Представьте, что у регистра plus есть два метода get и set:
          private uint _plusValue;
          public uint Get()
          {
          uint result = _plusValue;
          _plusValue = 0;
          return result;
          }
          public void Set(uint value)
          {
          _plusValue += value;
          }


          Как различается нулевое и обнулённое значение?

          Никак. Зачем их различать?

          • Griboks
            /#21006994

            Но ведь есть всего одна инструкция: move. А вы написали два метода. Получается, что Get() не может быть вызван.

            • Habivax
              /#21007118

              Не получается.
              Он же образно выражался.
              Move имеет два операнда, куда и откуда.
              Move туда — это Set, Move обратно — это Get.

        • Habivax
          /#21007044

          А зачем регистр обнулять? Обнуляется (устанавливается в исходное положение) автомат сложения. Первая запись в регистр заносит число в стек (адрес 0) и двигает указатель вверх, вторая запись заносит число в стек (адрес 1), двигает указатель вниз и запускает сумматор. Результат в стек (адрес 0) и автомат в исходное положение (обнулить).

          Можно вариант с бесконечным суммированием, каждая запись прибавляется к предыдущей, реального стека нет. При первой записи в ячейке памяти с суммой был ноль, а всё, что пишется в регистр, к ней прибавляется. Чтение выдает сумму и сбрасывает ячейку памяти-сумматор в ноль. Так второй раз прочитать сумму нельзя. Но если добавить флаг «было чтение», то можно, сумма обнулится при следующей записи со сбросом флага.

          Примечание: чтение и запись в один регистр могут адресоваться к разным адресам памяти. Например запись к реальной ячейке памяти, содержимое которой приплюсуется к сумме, а чтение к ячейке памяти в которой эта сумма хранится.

  2. neco
    /#21000970

    а какой пятый флаг? там четыре преведены…

  3. TaF
    /#21001172

    Более полезен виртуальный тренажер реально существующих процессоров.
    Подобные игры ума я считаю просто вредными. Ни в каком реальном учебном процессе эти знания не пригодятся.

    • poslannikD
      /#21002476

      Не подскажите какие есть на данный момент виртуальные тренажеры реально существующих процессоров ???

    • amartology
      /#21003358 / +1

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

  4. yakov-bakhmatov
    /#21004794

    Немного покритикую.

    1. Насколько я понял, этот процессор не умеет работать с памятью (RAM).
    2. Не понятно, зачем нужен стек на 16 слов, если есть 19 неиспользуемых регистров.
    3. За регистрами PLUS, AND, OR, XOR спрятано неплохое такое АЛУ (Арифметико-логическое устройство). И ещё аппаратный генератор случайных чисел. Получается как-то не очень: давайте сделаем АЛУ с четырьмя операциями, но номального доступа к ним не дадим. Тут должна была быть шутка про сложные хирургические операции через не подходящие для этого отверстия.

    Критикуя — предлагай!

    Давным-давно в далёкой-далёкой галактике на форуме WASM.RU пользователь Black_mirror опубликовал такую тему:

    Однокомандный dzen-процессор ;)

    Исходное сообщение
    Число регистров - 2^k (k должно быть не менее 3, наверное :)
        (к ним также относится счётчик команд ip)
    Архитектура - трехадресная
    Число команд - одна единственная (условное вычитание)
    Число флагов - один (f - флаг переполнения)
    Разрядность машинного слова - 2+3*k
    Максимально адресуемое число ячеек памяти - 2^(2+3*k)
     
    Все обрабатываемые числа считаются положительными,
    -1=11...1111 - максимальное число
     
    Формат команды:
     
    тип
    00 R1 R2 R3 - csub R1,R2,R3 if f=0 then R1:=R2-R3,f:=R3>R2 else f=0
    01 R1 R2 R3 - csub R1,R2,[R3]   if f=0 then R1:=R2-[R3],f:=[R3]>R2 else f=0
    10 R1 R2 R3 - csub R1,[R2],R3   if f=0 then R1:=[R2]-R3,f:=R3>[R2] else f=0
    11 R1 R2 R3 - csub [R1],R2,R3   if f=0 then [R1]:=R2-R3,f:=R3>R2 else f=0
     
    R1,R2,R3 - поля размером k бит, вся команда занимает одну ячейку памяти
     
    Основной цикл:
    Проверить флаг
        если f=0
            1) считать команду из памяти
            2) если тип команды 01 или 10 - считать операнд из памяти
            3) вычислить разность и установить флаг при переполнении
            4) записать результат в приёмник и увеличить счетчик команд
            если он(ip) не является приёмником данной команды       
        иначе
            сбросить флаг
        перейти к началу
     
    При включении питания или поступлении сигнала сброса все регистры и флаг
    переполнения обнуляются, и начинает выполняться команда расположеная по
    нулевому адресу.
     
    Примеры(предполагается что в начале флаг переполнения сброшен)
     
    1) обнуление регистра
     
    csub r,r,r; ;r=0
     
    2) загрузка в регистр единицы
     
    csub r,r,r  ;r=0
    csub r,ip,r ;r=$(адрес этой команды)
    csub r,ip,r     ;r=1
     
    3) сложение регистров a и b
     
    csub r,r,r      ;r=0
    csub b,r,b  ;b=-b
    csub r,r,r  ;при b>0 эта команда не выполяется
    csub a,a,b  ;a=a-(-b)
    csub r,r,r  ;при -b>a эта команда не выполяется
     
    4) проверка числа на 0
     
    csub z,z,z
    csub z,z,r
    csub <- эта команда будет выполнена если r=0
     
    5) загрузка из памяти
     
    csub z,z,z
    csub r,[m],z
     
    6) сохранение в памяти
     
    csub z,z,z
    csub [m],r,z
     
    7) деление числа в регистре a на число в регистре b, с записью частного в
    регистр d и остатка в регистр a
     
    #1 csub z,z,z   ;z=0
    #2 csub m,ip,z  ;m=$
    #3 csub m,m,ip  ;m=-1
    #4 csub z,z,z   ;никогда не исполняется
    #5 csub d,d,d   ;d=0
    #6 csub l,ip,m  ;l=адрес следующей команды
    ;начало цикла
    #7 csub d,d,m   ;d=d-(-1) не исполняется при первом проходе цикла
    #8 сsub z,z,z  ;исполняется только при первом прохорде
    #9 csub a,a,b   ;a=a-b
    #10 csub ip,l,z ;ip=l-z, то есть переход в начало цикла если в предыдущей
            ;команде не произошло переполнение
    ;далее довычисление остатка
    #11 csub z,z,b  ;z=z-b
    #12 csub z,z,z  ;на результат предыдущей команды не влияет
    #13 csub a,a,b  ;формирование остатка
     
     
    Работа алгоритма:
     
    пусть a=7 и b=3
    #1 z=0 f=0
    #2 m=2 f=0
    #3 m=-1 f=1 -1=11...1111
    #5 d=0 f=0
    #6 l=7 f=1
     
    #8 z=0 f=0
    #9 a=4 f=0
    #10 ip=7 f=0
     
    #7 d=1 f=1
    #9 a=1 f=0
    #10 ip=7 f=0
     
    #7 d=2 f=1  частное уже вычислено
    #9 a=-2 f=1     -2=11...1110
     
    #11 z=-3 f=1    -3=11...1101
    #13 a=1 f=0 остаток тоже вычислен
    
    

    • neco
      /#21005650

      2. Не понятно, зачем нужен стек на 16 слов, если есть 19 неиспользуемых регистров.

      стек, имхо, гораздо удобнее чем разные регистры, когда у вас есть только одна операция mov

      … гораздо более интересно зачем там рандом и как он устроен и работает?

      а нужен он для изучения принципов работы в образовательных целях

  5. isqwonder
    /#21006232

    В реальном мире существует странный микропроцессор MAXQ2000
    (сейчас принадлежит Maxim Integrated). У него тоже всё через пересылки в разные регистры реализовано. Мнемоники они, конечно, назначили привычные, но под ними только move.
    Лет 15 назад мы под него toolchain разрабатывали.