Хабрахабр

[Из песочницы] Написание простого процессора и окружения для него

В этой статье я расскажу какие шаги нужно пройти для создания простого процессора и окружения для него. Здравствуйте!

Важны такие параметры как: Для начала нужно определиться с тем, каким будет процессор.

Архитектуры процессоров можно разделить по размеру инструкций на 2 вида (на самом деле их больше, но другие варианты менее популярны):

Их инструкции простые и выполняются сравнительно быстро, тогда как CISC процессоры могут иметь разный размер инструкций, некоторые из которых могут выполняться достаточно продолжительное время. Основное их отличие в том, что RISC процессоры имеют одинаковый размер инструкций.

Я решил сделать RISC процессор во многом похожий на MIPS.

Я это сделал по целому ряду причин:

  • Довольно просто создать прототип такого процессора.
  • Вся сложность такого вида процессоров перекладывается на такие программы как ассемблер и/или компилятор.

Вот основные характеристики моего процессора:

  • Машинное слово и размер регистров — 32 бита
  • 64 регистра (включая счетчик команд)
  • 2 типа инструкций

Регистровый тип) выглядит вот так: Register type(досл.

rtype

Особенность таких инструкций заключается в том, что они оперируют с тремя регистрами.

Немедленный тип): Immediate type(досл.

itype

Инструкции этого типа оперируют с двумя регистрами и числом.

OP — это номер инструкции, которую нужно выполнить (или же для указания, что эта инструкция Register type).

R0, R1, R2 — это номера регистров, которые служат операндами для инструкции.

Func — это дополнительное поле, которое служит для указания вида Register type инструкций.

Imm — это поле куда записывается то значение, которое мы хотим явно предоставить инструкции в качестве операнда.

  • Всего 28 инструкций

Полный список инструкций можно посмотреть в github репозитории.

Вот лишь пару из них:

nor r0, r1, r2

NOR это Register type инструкция, которая делает логическое ИЛИ НЕ на регистрах r1 и r2, после записывает результат в регистр r0.

Для того, чтобы использовать эту инструкцию нужно изменить поле OP на 0000 и поле Func на 0000000111 в двоичной системе счисления.

lw r0, n(r1)

LW это Immediate type инструкция, которая загружает значение памяти по адресу r1 + n в регистр r0.

Для того, чтобы использовать эту инструкцию в свою очередь нужно изменить поле OP на 0111, а в поле IMM записать число n.

После создания ISA можно приступить к написанию процессора.

Вот некоторые из них: Для этого нам нужно знание какого нибудь языка описания оборудования.

  • Verilog
  • VHDL (не путать с предыдущим!)

программирование на нем было частью моего учебного курса в университете. Я выбрал Verilog, т.к.

Для написания процессора нужно понимать логику его работы:

  1. Получение инструкции по адресу Счетчика команд (PC)
  2. Декодирование инструкции
  3. Выполнение инструкции
  4. Прибавление к Cчетчику команды размера выполненной инструкции

И так до бесконечности.

Получается нужно создать несколько модулей:

Разберем по отдельности каждый модуль.

Регистровый файл

С его помощью нужно получать значения каких то регистров, или изменять их. Регистровый файл предоставляет доступ к регистрам.

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

Декодер

Он указывает какие операции нужно выполнить АЛУ и другим блокам. Декодер это тот блок, который отвечает за декодирование инструкций.

Например, инструкция addi должна сложить значение регистра $zero(Он всегда хранит 0) и 20 и положить результат в регистр $t0.

addi $t0, $zero, 20

На этом этапе декодер определяет, что эта инструкция:

  • Immediate type
  • Должна записать результат в регистр

И передает эти сведения следующим блокам.

АЛУ

В нем обычно выполняются все математические, логические операции, а также операции сравнения чисел. После управление переходит в АЛУ.

То есть, если рассмотреть ту же инструкцию addi, то на этом этапе происходит сложение 0 и 20.

Другие

По мимо вышеперечисленных блоков, процессор должен уметь:

  • Получать и изменять значения в памяти
  • Выполнять условные переходы

Тут и там можно увидеть как это выглядит в коде.

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

Я решил реализовать его на языке программирования Си.

Так как мой процессор имеет RISC архитектуру, то для того, чтобы упростить себе жизнь, я решил спроектировать ассемблер так, чтобы в него можно было легко добавлять свои псевдоинструкции(комбинации из нескольких элементарных инструкций или из других псевдоинструкций).

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

Обычная программа начинается с объявления сегмента.

Для нас достаточно двух сегментов .text — в котором будет храниться исходный код наших программ — и .data — в котором будет хранится наши данные и константы.

Инструкция может выглядеть вот так:

.text jie $zero, $zero, $zero # Ветвление addi $t1, $zero, 2 # $t1 = $zero + 2 lw $t1, 5($t2) # $t1 = *($t2 + 5) syscall 0, $zero, $zero # syscall(0, 0, 0) la $t1, label# $t1 = label

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

В .data же указываются объявления данных.

.data .byte 23 # Константа размером 1 байт .half 1337 # Константа размером 2 байта .word 69000, 25000 # Константы размером 4 байта .asciiz "Hello World!" # Константная нуль терминируемая строка (Си строка) .ascii "12312009" # Константная строка (без терминатора) .space 45 # Пропуск 45 байтов

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

Удобно парсить (сканировать) ассемблер файл в таком виде:

  1. Сначала сканируем сегмент
  2. Если это .data сегмент, то мы парсим разные типы данных или .text сегмент
  3. Если это .text сегмент, то мы парсим команды или .data сегмент

В первый раз он считает по каким смещениям находятся ссылки (они служат для), они обычно выглядят вот так: Для работы ассемблеру нужно проходить исходный файл 2 раза.

la $s4, loop # Загружаем адрес loop в s4 loop: # Ссылка! mul $s2, $s2, $s1 # s2 = s2 * s1 addi $s1, $s1, -1 # s1 = s1 - 1 jil $s3, $s1, $s4 # если s3 < s1 то перейди на метку

А во второй проход можно уже и генерировать файл.

В дальнейшем, можно запускать выходной файл из ассемблера на нашем процессоре и оценивать результат.

Но это уже позже. Также готовый ассемблер можно использовать в Си компиляторе.

Ссылки:

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть