Оглавление
§ О процессоре
Этот процессор уникален. Не буду говорить о том, сколько лет я пытался просто подумать о том чтобы его сделать, но... много. В любом случае, процессор RISC-V вот почему мне стал сейчас интересен.
- Набор инструкции невероятно простой
- Процессор полностью 32-х битный
- Для него есть компилятор GCC (и можно даже скомпилировать там DOOM)
- У него нет флагов состояния (перенос или переполнение) – это, на самом деле, багофича
- И он абсолютно свободен и не покрыт патентами с ног до головы, можно делать что угодно с этим процессором
У процессора и другие преимущества есть, но я выделил главное. Да, есть такая проблема что набор инструкции 32-х битные, но кто-то не считает что это проблема, а кто-то считает.
Я долго пытался подобраться к нему и наконец, пришло время разобраться и поставить все, полностью все точки на i.
§ Регистры
Количество регистров у процессора не меньше, чем у AVR, а именно 32. Но они, в отличии от AVR, все строго 32х битные. Занимают регистры 1024 бит памяти в ячейках либо BRAM, либо же в самих LE / ALM в ПЛИС.
Называются они от x0 до x31. Регистр x0 всегда равен 0. Но обычно у регистров RISC-V есть определенные, закрепленные за ними названия и я приведу таблицу. Как утверждают сами разработчики спецификации, делайте с этими регистрами чего хотите, нам всё равно. Но так тоже не хорошо и потому некоторым регистрам всё-таки названия, но дали.
РЕГИСТР | ИМЯ | ЗНАЧЕНИЕ
========+========+===================================
x0 | zero | Всегда 0
x1 | ra | Адрес возврата
x2 | sp | Указатель стека
x3 | gp | Глобальный указатель
x4 | tp | Указатель потока
x5-x7 | t0-t2 | Временные
x8 | s0/fp | Сохраненные данные / Фрейм
x9 | s1 | Сохраненные данные
x10-x11 | a0-a1 | Аргументы / Данные возврата
x12-x17 | a2-a7 | Аргументы
x18-x27 | s2-s11 | Сохраненные данные
x28-x31 | t3-t6 | Временные
Все регистры, кроме 0, распределены условно, но имеют определенное значение.
RA – при вызове процедуры, сюда записывается адрес возврата (LINK). Для процедур единичной вложенности это удобно, и это работает как и в ARM
SP – собственно, это и есть указатель стека
GP – указывает на глобальные переменные (опционально)
TP – указатель локальных переменных текущего потока (тоже опционально) в многопроцессорной системе, к примеру
A0-A7 – аргументы функции при вызове процедуры, до 8 регистров. Но это необязательно. Можно хоть все 31 использовать. Никаких особенных привязок нет. Обычно, при большом количестве аргументов (более 8), они уже начинают укладываться в стеке
T0-T6 – внутри процедуры можно затирать как угодно. Эти регистры временные и вне процедур никак не используются. Но надо помнить, что при передаче управления другой процедуре эти переменные уже затрутся ей, так что нужно сохранять при необходимости, а лучше их не использовать между вызовами
FP – указатель текущего фрейма, это аналог BP, для того чтобы знать на каком месте в стеке выделена область локальных переменных
S0-S11 – регистры, которые обязаны быть сохраненными в стеке перед вызовов процедуры
§ Схема инструкции
В отличии от CISC инструкции, все инструкции в RISC-V крайне просты по своей структуре (ортогональны) и помещаются в базовый формат.
+==============+=========+=========+=========+=============+========+=====+
| 31 25 | 24 20 | 19 15 | 14 12 | 11 7 | 6 0 | ТИП |
+==============+=========+=========+=========+=============+========+=====+
| funct7 | rs2 | rs1 | funct3 | rd | opcode | R |
| imm[11:0] | rs1 | funct3 | rd | opcode | I |
| imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode | S |
| imm[12,10:5] | rs2 | rs1 | funct3 | imm[4:1,11] | opcode | SB |
| imm[31:12] | rd | opcode | U |
| imm[20,10:1,11,19:12] | rd | opcode | UJ |
+==============+=========+=========+=========+=============+========+=====+
Смотря на некоторые значения в imm – вычисления непосредственного значения, приходит удивление со словами "а где это просто?", поскольку данные собираются буквально "по битам" со всех сторон. Да, есть некоторые сложные вещи тут, но только для человеческого восприятия, но вовсе не для машинного зрения. Я, конечно, сам не понимаю, зачем сделали так сложно для UJ-типа, но ладно.
Приведу пояснения к обозначениям.
opcode – собственно, номер операции
rd – регистр-назначение (dest)
rs1, rs2 – регистры-источники
funct3 и funct7 – 3-битное и 7-битное дополнение к опкоду (при необходимости)
imm – константа, вшитая в инструкцию
Несмотря ни на что, инструкции собираются весьма просто. Опкод размером 7 бит (от 00 до 7F), а также funct3 и funct7 для его дополнения потенциально может кодировать огромное количество инструкции.
На таблице можно отметить что существуют несколько типов инструкции. Все они 32-х битные, однако каждый тип обладает собственным форматированием полей.
Краткое описание типов:
R – работа только с регистрами, например, сложение, вычитание регистров rs1, rs2 и запись результата в rd
I – операции с регистром и непосредственным значением до 12 бит, причем, число, записанное в непосредственном значении, могут знакорасширяться до 32 бит
S,SB – эти инструкции кодируют обычно работу с памятью, считывание из памяти, запись
U – для загрузки и операции с 20-битными константами
UJ – для реализации функции переходов
§ Перемешанные Immediate
В некоторых типах, таких как SB или UJ биты очень сильно перемешаны. Если, например, понятно что imm[11:0] для I-типа будет брать из диапазона инструкции [31:20] и записываться в диапазон imm[11:0], что в Си-коде достаточно сделать так I >> 20, и для U-типа так I & 0xFFFFF000, то SB-тип (условные переходы) уже сложнее.
# SB-тип Условные переходы
31..25 => imm[12,10:5]
11..7 => imm[4:1,11]
Если представить в виде таблицы маппинга битов, то получится следующее:
31 | 30 29 28 27 26 25 | 11 10 9 8 | 7 | ИНСТРУКЦИЯ
12 | 10 9 8 7 6 5 | 4 3 2 1 | 11 | КОНСТАНТА
Сверху представлены исходные биты, ниже – биты, куда будут они поставлены в итоговое значение константы. Здесь видна интересная особенность – нет 0-го бита, и это объясняется тем что для совершения перехода всё выравнивается на 2 байта минимально. То есть, нет смысла хранить младший бит для этого.
- Сначала смещаем 31-й бит в самое начало, а потом устанавливаем его на правильную позицию:
(I >> 31) << 12
- Смещаются биты от 25 до 30, ограничивается через AND
0x3F и сдвиг к позиции 5: ((I >> 25) & 0x3F) << 5
- Аналогично для следующей группы: смещение на 8, ограничиваем AND
0x0F и установка на 1: ((I >> 8) & 0x0F) << 1
- И последний бит: смещение на 7, ограничение AND
0x01 и установка на 11: ((I >> 7) & 0x01) << 11
Далее надо все компоненты собрать воедино.
# S-тип Указатели в память
Здесь немного попроще.
31..25 => imm[11:5]
11..7 => imm[4:0]
Достаточно лишь два действия сделать: (I >> 25) << 5 и (I >> 7) & 0x1F, беззнаковое смещение сначала на 25 бит, а потом установка на позицию 5, и далее сместить на 7, ограничить AND 0x1F и... всё, то есть, двигать больше никуда не надо, так как уже этот диапазон на позиции 0 установлен.
# UJ-тип Безусловный переход
Используется в инструкции JAL (Jump & Link) и почти такой же запутанный, как и SB-тип.
31 | 30 29 28 27 26 25 24 23 22 21 | 20 | 19 18 17 16 15 14 13 12 | ИНСТРУКЦИЯ
20 | 10 9 8 7 6 5 4 3 2 1 | 11 | 19 18 17 16 15 14 13 12 | КОНСТАНТА
- Бит 31 сдвигается на 20-ю позицию:
(I >> 31) << 20
- Биты 21..30 передвигаются на 1-ю позицию, ограничены AND
0x3FF: ((I >> 21) & 0x3FF) << 1
- Бит 20 двигается на 11-ю позицию:
((I >> 20) & 1) << 11
- А вот биты с 12 по 19 (восемь бит) вообще остаются на месте:
I & 0xFF000
§ Compressed инструкции
Есть еще интересная особенность процессора: он может работать в двух режимах – как обычном, когда инструкции размером 32 бита, так и в C-режиме, с 16-битными инструкциями.
Для обычных инструкции опкод всегда заканчивается на 11, то есть, первые два младших бита равны единицам. Если инструкция представляет из себя 16-битную "сжатую" инструкцию, то у нее как минимум в двух младших битах не 11, а например, 01 или другие значения. Когда процессор исполняет очередную 16-битную инструкцию, то он прибавляет к PC+2, а не PC+4, как обычно.
Конечно, для считывания из памяти это не очень удобно, потому я пока что буду реализовывать именно стандартный вариант.