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)
TaF
13.12.2019 08:35Более полезен виртуальный тренажер реально существующих процессоров.
Подобные игры ума я считаю просто вредными. Ни в каком реальном учебном процессе эти знания не пригодятся.poslannikD
13.12.2019 12:02Не подскажите какие есть на данный момент виртуальные тренажеры реально существующих процессоров ???
amartology
13.12.2019 13:51+1Для того, чтобы хорошо понимать, как работают реально существующие процессоры, и мочь оценить, чем один из них лучше или хуже другого, надо понимать общие принципы работы. Вот эта штуковина — хороший тренажер для понимания общих принципов
yakov-bakhmatov
13.12.2019 16:45Немного покритикую.
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
13.12.2019 18:252. Не понятно, зачем нужен стек на 16 слов, если есть 19 неиспользуемых регистров.
стек, имхо, гораздо удобнее чем разные регистры, когда у вас есть только одна операция mov
… гораздо более интересно зачем там рандом и как он устроен и работает?
а нужен он для изучения принципов работы в образовательных целях
isqwonder
13.12.2019 19:50В реальном мире существует странный микропроцессор MAXQ2000
(сейчас принадлежит Maxim Integrated). У него тоже всё через пересылки в разные регистры реализовано. Мнемоники они, конечно, назначили привычные, но под ними только move.
Лет 15 назад мы под него toolchain разрабатывали.
Griboks
Объясните, пожалуйста, как запускаются операция сложения?
Автор использует 41 бит: 40 бит на число + 1 грязный бит? Ведь регистр всегда содержит два числа, в любом случае. Даже если его обнулять после очередного сложения, там всё-равно останутся 0 и 0, что должно бесконечно рекурсивно запускать сложение и вызвать stack overflow.
Politura
Там же схема есть «Plus register example».
На схеме последняя операция plus gpr0 и комментарий: значение 60 из регистра plus перемещается в регистр gpr0, а регистр plus при этом обнуляется.
Думаю, там нет никакого стека, просто стек придумали для облегчения понимания.
Скорее всего, все что кладем в plus, прибавляется к его текущему значению, а когда из plus что-то забираем, то plus обнуляется.
Griboks
А как он обнуляется? Как различается нулевое и обнулённое значение? Там стоит счётчик записи? Существует область памяти с пометками занятости всех адресов? У каждого числа есть флаг обнуления?
Politura
Зачем все эти сложности? Ладно, попробую написать псевдокодом, надеюсь будет понятнее.
Представьте, что у регистра plus есть два метода get и set:
private uint _plusValue;
public uint Get()
{
uint result = _plusValue;
_plusValue = 0;
return result;
}
public void Set(uint value)
{
_plusValue += value;
}
Никак. Зачем их различать?
Griboks
Но ведь есть всего одна инструкция: move. А вы написали два метода. Получается, что Get() не может быть вызван.
Habivax
Не получается.
Он же образно выражался.
Move имеет два операнда, куда и откуда.
Move туда — это Set, Move обратно — это Get.
Habivax
А зачем регистр обнулять? Обнуляется (устанавливается в исходное положение) автомат сложения. Первая запись в регистр заносит число в стек (адрес 0) и двигает указатель вверх, вторая запись заносит число в стек (адрес 1), двигает указатель вниз и запускает сумматор. Результат в стек (адрес 0) и автомат в исходное положение (обнулить).
Можно вариант с бесконечным суммированием, каждая запись прибавляется к предыдущей, реального стека нет. При первой записи в ячейке памяти с суммой был ноль, а всё, что пишется в регистр, к ней прибавляется. Чтение выдает сумму и сбрасывает ячейку памяти-сумматор в ноль. Так второй раз прочитать сумму нельзя. Но если добавить флаг «было чтение», то можно, сумма обнулится при следующей записи со сбросом флага.
Примечание: чтение и запись в один регистр могут адресоваться к разным адресам памяти. Например запись к реальной ячейке памяти, содержимое которой приплюсуется к сумме, а чтение к ячейке памяти в которой эта сумма хранится.