RV32I - Стандартный набор целочисленных инструкций RISC-V

Разделы статьи:

Большая часть данного документа в той или иной степени является переводом спецификации RISC-V[1], распространяемой по лицензии CC-BY-4.0 .

Краткая справка по RISC-V и RV32I

RISC-V — открытая и свободная система набора команд (ISA) на основе концепции RISC. Чтобы понять архитектуру любого компьютера, нужно в первую очередь выучить его язык, понять, что он умеет делать. Слова в языке компьютера называются «инструкциями», или «командами», а словарный запас компьютера — «системой команд»[2, стр. 355].

В архитектуре RISC-V имеется обязательный для реализации минимальный список команд — набор инструкций I (Integer). В этот набор входят различные логические и арифметические операции с целыми числами, работа с памятью, и команды управления. Этого достаточно для обеспечения поддержки компиляторов, ассемблеров, компоновщиков и операционных систем (с дополнительными привилегированными инструкциями). Плюс, таким образом обеспечивается удобный "скелет" ISA и программного инструментария, вокруг которого могут быть построены более специализированные ISA процессоров путем добавления дополнительных инструкций.

Строго говоря RISC-V — это семейство родственных ISA, из которых в настоящее время существует четыре базовые ISA. Каждый базовый целочисленный набор инструкций характеризуется шириной целочисленных регистров и соответствующим размером адресного пространства, а также количеством целочисленных регистров. Существует два основных базовых целочисленных варианта, RV32I и RV64I, которые, соответственно, обеспечивают 32- или 64-битное адресное пространство и соответствующие размеры регистров регистрового файла. На основе базового набора инструкций RV32I существует вариант подмножества RV32E, который был добавлен для поддержки небольших микроконтроллеров и имеет вдвое меньшее количество целочисленных регистров — 16, вместо 32. Разрабатывается вариант RV128I базового целочисленного набора инструкций, поддерживающий плоское 128-битное адресное пространство. Также, стоит подчеркнуть, что размеры регистров и адресного пространства, во всех перечисленных стандартных наборах инструкций, не влияют на размер инструкций — во всех случаях они кодируются 32-битными числами. То есть, и для RV32I, и для RV64I одна инструкция будет кодироваться 32 битами. Базовые целочисленные наборы команд используют представление знаковых целых чисел в дополнительном коде.

В рамках дисциплины АПС затрагивается только RV32I, то есть стандартный набор целочисленных инструкций, предусматривающий в процессоре регистровый файл из 32-х 32-битных регистров, и использующий 32-битное адресное пространство памяти.

Кроме обязательного подмножества целочисленных инструкций, RISC-V предусматривает несколько стандартных опциональных расширений. Вот некоторые из них:

  • M — Целочисленное умножение и деление (Integer Multiplication and Division)
  • A — Атомарные операции (Atomic Instructions), инструкции для атомарного чтения-изменения-записи в память для межпроцессорной синхронизации
  • F — Стандартное расширение для арифметических операций с плавающей точкой одинарной точности (Single-Precision Floating-Point) добавляет регистры с плавающей точкой, инструкции вычислений с одинарной точностью, а также инструкции загрузки и сохранения в регистровый файл для чисел с плавающей точкой
  • D — Стандартное расширение с плавающей точкой двойной точности (Double-Precision Floating-Point) расширяет регистры с плавающей точкой до 64 бит и добавляет инструкции вычислений с двойной точностью, загрузку и сохранение
  • C — Набор сжатых инструкций (Compressed Instructions), позволяющий кодировать инструкции 16-битными словами, что позволяет уплотнить программный код (если одну и ту же программу можно писать 16-битными словами вместо 32-битных, значит её размер сократится в 2 раза). Разумеется, у такого уплотнения есть своя цена, иначе инструкции просто кодировали бы 16-ю битами вместо 32. У сжатых инструкций меньший диапазон адресов и констант.
  • Zicsr — Инструкции для работы с контрольными и статусными регистрами (Control and Status Register (CSR) Instructions). Используется, например, при работе с прерываниями/исключениями и виртуальной памятью
  • Zifencei — Инструкции синхронизации потоков команд и данных (Instruction-Fetch Fence)

Поддерживаемые процессором команды отражаются в названии набора инструкций. Например, RV64IMC это архитектура RISC-V с 64-битными регистрами и 64-битным адресным пространством, поддерживающая кроме стандартных целочисленных операций умножение и деление M, и может выполнять сжатые инструкции C.

Одной из целей проекта RISC-V является его использование в качестве стабильного объекта для разработки программного обеспечения. Для этого ее разработчики определили комбинацию базового ISA (RV32I или RV64I) и некоторых стандартных расширений (IMAFD + Zicsr + Zifencei) как "general-purpose" ISA (набор инструкций общего назначения), а для комбинации расширений набора команд IMAFDZicsrZifencei стали использовать аббревиатуру G. То есть RV32G это тоже самое, что и RV32IMAFDZicsrZifencei.

Чтобы устройство управления понимало, когда оно имеет дело с набором сжатых команд C, то есть с 16-битными инструкциями, а когда с другими наборами команд, то есть с инструкциями длиной 32 бита, каждая 32-битная инструкция в младших битах имеет 11. Если в двух младших битах что-то отличное от 11, значит это 16-битная инструкция!

На рисунке ниже показана видимая пользователю структура для основного подмножества команд для целочисленных вычислений RV32I. Она содержит регистровый файл, состоящий из 31 регистра общего назначения x1x31, каждый из которых может содержать целочисленное значение, и регистра x0, жестко привязанного к константе 0. В случае RV32, регистры xN, и вообще все регистры, имеют длину в 32 бита. Также есть АЛУ, выполняющее операции над данными в регистровом файле (концепция RISC - load&store), и память с побайтовой адресацией и шириной адреса 32 бита.

Также существует еще один дополнительный видимый пользователю регистр: счетчик команд — pc (program counter), который содержит адрес текущей инструкции. pc изменяется либо автоматически, указывая на следующую инструкцию, либо в результате использования инструкций управления (операции условного и безусловного переходов).

../.pic/Labs/rv_model.png

RISC-V является load&store архитектурой (все операции с числами выполняются над данными только в регистровом файле), поэтому глядя на рисунок выше можно легко заключить, что функционально все инструкции сводятся к трём типам:

  • Операции на АЛУ над числами в регистровом файле
  • Операции обмена данными между регистровым файлом и памятью
  • Манипуляции с pc (другими словами — управление программой)

Как было сказано ранее, память имеет 32-битную шину адреса и имеет побайтовую адресацию. Это значит, что каждый из 232 байт памяти имеет свой уникальный адрес, по которому к нему можно обратиться, чтобы считать из него или записать в него новую информацию. Однако, инструкции кодируются 32-битными числами, а один байт это всего 8 бит, значит одна инструкция занимает сразу 4 адреса в памяти. Подразумевается, что из такой памяти можно читать одновременно из нескольких последовательных адресов, то есть устройство управления процессора сообщает памяти начальный адрес требуемой ячейки, и количество ячеек (одну, две или четыре), которые нужно прочитать или записать.

Одна ячейка называется байт - 8 бит. Две последовательные 8-битные ячейки называются полуслово - 16 бит. Четыре последовательные 8-битные ячейки называются словом - 32 бита. Например, если процессор собирается выполнить инструкцию, которая занимает четыре байта по адресам 0x00000007 — 0x00000004, то он обращается к памяти, сообщая, что "нужны 4 байта начиная с адреса 0x00000004", взамен процессор получает 32-битное число — инструкцию, которая была слеплена из байт, хранящихся в памяти по адресам: 4, 5, 6 и 7, для данного примера. К памяти также можно обратиться за полусловом или за байтом. Предполагается реализация выровненного доступа к памяти, то есть адреса слов и полуслов должны быть кратны 4 и 2, соответственно.

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

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

В архитектуре RISC-V каждая несжатая инструкция представлена 32-разрядным словом. Микропроцессоры — это цифровые системы, которые читают и выполняют команды машинного языка. Для людей чтение и разработка компьютерных программ на машинном языке представляются нудным и утомительным делом, поэтому мы предпочитаем представлять инструкции в символическом формате, который называется языком ассемблера[2, стр. 356]. Ассемблер позволяет выполнить взаимно однозначный переход от машинного кода к тестовому и обратно.

RV32I

В таблице ниже приводятся 40 команд стандартного набора целочисленных инструкций RV32I: мнемоники языка ассемблера, функции, описания, форматы кодирования и значения соответствующих полей при кодировании. В RISC-V предусмотрено несколько форматов кодирования инструкций (следующий рисунок, еще ниже), то есть договоренность какая информация в каком месте 32-битной инструкции хранится и как она представлена. У всех операций есть поле opcode (operation code - код операции), в котором закодировано "что нужно сделать". По полю opcode устройство управления понимает, что требуется сделать процессору и каким именно способом закодирована инструкция (R, I, S, B, U или J). В 32-битных инструкциях два младших бита всегда равны 11.

Почти все инструкции имеют поле Func3, и некоторые — поле Func7. Их названия определены их разрядностью: 3 и 7 бит, соответственно. В этих полях, если они есть у инструкции, закодировано уточнение операции. Например, код операции 0010011 указывает на то, что будет выполняться некоторая операция на АЛУ между значением из регистрового файла и константой. Поле Func3 уточняет операцию, для данного примера, если оно будет равно 0x0, то АЛУ выполнит операцию сложения между значением из регистра и константой из инструкции. Если Func3 равно 0x6, то будет выполнена операция "логическое ИЛИ".

../.pic/Labs/lab_05_decoder/rv32i_summary.png

Обратите внимание на операции slli, srli и srai (операции сдвига на константную величину). У этих инструкций немного измененный формат кодирования I*. Формат кодирования I предоставляет 12-битную константу. Сдвиг 32-битного числа более, чем на 31 не имеет смысла. Для кодирования числа 31 требуется всего 5 бит. Выходит, что из 12 бит константы используется только 5 бит для операции сдвига, а оставшиеся 7 бит – не используются. А, главное (какое совпадение!), эти 7 бит находятся ровно в том же месте, где у других инструкций находится поле Func7. Поэтому, чтобы у инструкций slli, srli и srai использующих формат I не пропадала эта часть поля, к ней относятся как к полю Func7.

На рисунке ниже приводится фрагмент из оригинальной спецификации RISC-V. Сверху приводятся 6 форматов кодирования инструкций: R, I, S, B, U и J, а ниже приводятся конкретные значения полей внутри инструкции. Под rd подразумевается 5-битный адрес регистра назначения, rs1 и rs2 - 5-битные адреса регистров источников, imm — константа, расположение и порядок битов которой указывается в квадратных скобках. Обратите внимание, что в разных форматах кодирования константы имеют различную разрядность, а их биты упакованы по-разному. Для знаковых операций константу предварительно знаково расширяют до 32 бит. Для беззнаковых расширяют нулями до 32 бит.

../.pic/Labs/lab_05_decoder/rv32i_BIS.png

Ниже, для наглядности, приводится пример кодирования пары инструкций из книги Харриса и Харриса "Цифровая схемотехника и архитектура компьютера" в машинный код[2, стр. 368].

../.pic/Other/rv32i/example_instr_code.png

Примечание: s2, s3, s4, t0, t1, t2 — это синонимы регистров x18,x19,x20,x5,x6,x7 соответственно. Введены соглашением о вызовах (calling convention) для того, чтобы стандартизировать функциональное назначение регистров. Подробнее об этом будет в лабораторной работе по программированию.

Псевдоинструкции

В архитектуре RISC-V размер команд и сложность аппаратного обеспечения минимизированы путем использования лишь небольшого количества команд. Тем не менее RISC-V определяет псевдокоманды, которые на самом деле не являются частью набора команд, но часто используются программистами и компиляторами. При преобразовании в машинный код псевдокоманды транслируются в одну или несколько команд RISC-V[2, стр. 410]. Например, псевдокоманда безусловного перехода j, преобразуется в инструкцию безусловного перехода с возвратом jal с регистром x0 в качестве регистра-назначения, то есть адрес возврата не сохраняется.

../.pic/Other/rv32i/pseudo.png

Основные типы команд

В основе ISA лежит четыре основных типа команд (R/I/S/U), которые изображены на рисунке ниже. Все они имеют фиксированную длину в 32 бита и должны быть выровнены в памяти по четырехбайтовой границе. Если адрес перехода (в случае безусловного перехода, либо успешного условного перехода) не выровнен, генерируется исключение о невыровненном адресе инструкции. Исключение не генерируется в случае невыполненного условного перехода.

../.pic/Other/rv32i/RISU.png

Для упрощения декодирования, архитектура команд RISC-V сохраняет положение адресов регистров-источников (rs1 и rs2) и регистра назначения (rd) между всеми типами инструкций.

За исключением 5-битных непосредственных операндов, используемых в командах CSR, все непосредственные операнды (imm) проходят знаковое расширение. Для уменьшения сложности оборудования, константа размещается в свободные (от полей func3/func7/rs1/rd) биты инструкции, начиная от левого края. В частности, благодаря этому ускоряется схема знакового расширения, поскольку знаковый бит всех непосредственных операндов всегда находится в 31-ом бите инструкции.

Способы кодирования непосредственных операндов

Существует еще два формата кодирования констант в инструкции (B/J-типа), представленные на рисунке ниже.

Единственное различие между форматами S и B заключается в том, что в формате B, 12-битная константа используется для кодирования кратных двум смещений адреса при ветвлении (примечание: кратность двум обеспечивается сдвигом числа на 1 влево). Вместо того, чтобы сдвигать непосредственный операнд относительно всех бит инструкции на 1 влево, средние биты (imm[10:1]) и знаковый бит остаются в прежних местах, а оставшийся младший бит константы формата S (inst[7]) кодирует imm[11] бит константы в формате B.

Аналогично, единственное различие между форматами U и J состоит в том, что в формате U 20-разрядная константа сдвигается влево на 12 бит, в то время как в формате J — на 1. Расположение бит в непосредственных значениях формата U и J выбирались таким образом, чтобы максимально увеличить перекрытие с другими форматами и между собой.

../.pic/Other/rv32i/BJ.png

На рисунке ниже показаны непосредственные значения (константы), создаваемые каждым из основных форматов команд, также они помечены, чтобы показать, какой бит команды (inst[y]) какому биту непосредственного значения соответствует.

../.pic/Other/rv32i/ISBUJ.drawio.svg

Знаковое расширение — одна из самых важных операций над непосредственными значениями (особенно в RV64I). Поэтому в RISC-V знаковый бит всех непосредственных значений всегда содержится в 31-м бите инструкции. Это позволяет выполнять знаковое расширение параллельно с декодированием команды.

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

Меняя местами биты в кодировке непосредственных значений инструкций B и J-типа вместо использования динамических мультиплексоров для умножения константы на 2, мы уменьшили разветвленность сигнала команды и затраты на мультиплексирование примерно в 2 раза. Скремблированное кодирование непосредственных значений добавит незначительную задержку при статической компиляции. Для динамической генерации инструкций есть небольшие дополнительные издержки, однако для наиболее частых коротких ветвлений вперед предусмотрено простое кодирование непосредственных значений.

Команды для целочисленных вычислений

Большинство инструкций целочисленных вычислений работают с 32-битными значениями, хранящимся в регистровом файле. Такие команды либо кодируются как операции константа-регистр, используя формат I-типа, либо как операции регистр-регистр, используя формат R-типа. В обоих случаях результат сохраняется в регистр rd . Ни одна инструкция целочисленных вычислений не вызывает арифметических исключений.

Мы стали добавлять поддержку специального набора команд для проверок на переполнение целочисленных арифметических операций в основной набор команд, поскольку многие проверки на переполнение могут быть достаточно дешево реализованы в RISC-V с использованием инструкций ветвления. Проверка на переполнение для беззнакового сложения требует только одной дополнительной команды перехода после сложения:

add t0, t1, t2
bltu t0, t1, overflow

Для знакового сложения, если известен знак одного операнда, проверка на переполнение требует только одного ветвления после сложения:

addi t0, t1, +imm;
blt t0, t1, overflow.

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

add t0, t1, t2
slti t3, t2, 0
slt t4, t0, t1
bne  t3,  t4, overflow

В RV64 проверки 32-разрядных знаковых сложений могут быть дополнительно оптимизированы путем сравнения результатов выполнения команд ADD и ADDW для каждого из операндов.

Команда типа константа-регистр

../.pic/Other/rv32i/addi_andi.png

ADDI суммирует знакорасширенную 12-битную константу с регистром rs1. Арифметическое переполнение игнорируется, и результатом являются младшие 32 бита результата. Команда ADDI rd, rs1, 0 используется для реализации ассемблерной псевдоинструкции MV rd, rs1.

SLTI (установить, если меньше чем константа) помещает значение 1 в регистр rd, если регистр rs1 меньше, чем расширенное непосредственное значение, когда оба значения обрабатываются как знаковые числа, иначе в rd записывается 0. SLTIU аналогична, но сравнивает значения как беззнаковые числа (то есть непосредственное значение сначала расширяется до 32 бит, а затем обрабатывается как число без знака). Обратите внимание, что команда SLTIU rd, rs1, 1 устанавливает rd в 1, если rs1 равен нулю, в противном случае rd устанавливается в 0 (псевдоинструкция ассемблера SEQZ rd, rs).

Примечание: у студентов часто возникает вопрос: зачем вообще нужны инструкции вида SLT, если есть инструкции вида BLT? Например, они могут использоваться для вычисления сложных условий переходов. Один из примеров таких условий вы видели выше, в примере обработке результата сложения на переполнение. Кроме того, не смотря на ограниченность этих инструкций (все они проверяют только на строго меньше), мы можем добиться операции строго больше поменяв операнды местами, а если результат обоих операций даст 0 — значит операнды равны. Поскольку идея RISC архитектуры в том, чтобы переложить организацию всех этих ухищрений на компилятор, этих инструкций оказывается достаточно.

ANDI, ORI, XORI — это логические операции, которые выполняют побитовое И, ИЛИ и исключающее ИЛИ над регистром rs1 и непосредственным 12-битным значением с знаковым расширением и помещают результат в rd. Обратите внимание, что команда XORI rd, rs, -1 выполняет побитовую логическую инверсию значения регистра rs1 (псевдоинструкция NOT rd, rs).

../.pic/Other/rv32i/slli_srli_srai.png

Сдвиги на константу кодируются как разновидность формата команд I-типа. Операнд, который должен быть сдвинут, находится в rs1, а величина сдвига кодируется в младших 5 битах поля непосредственного значения. Тип сдвига вправо определяется 30-ым битом. SLLI - логический сдвиг влево (нули задвигаются в младшие биты); SRLI - логический сдвиг вправо (нули задвигаются в старшие биты); SRAI - арифметический сдвиг вправо (исходный знаковый бит задвигается в старшие биты).

../.pic/Other/rv32i/lui_auipc.png

LUI (загрузка старшей части непосредственного значения) используется для получения 32-битных констант и использует формат U-типа. LUI помещает непосредственное значение U-типа в старшие 20 бит регистра назначения rd, заполняя младшие 12 бит нулями. AUIPC (прибавить старшую часть непосредственного значения к pc) используется для построения адресов относительно pc, и использует формат U-типа. AUIPC формирует 32-битное смещение из 20-битного непосредственного значения U-типа, заполняя младшие 12 битов нулями, прибавляет это смещение к значению pc, а затем размещает результат в регистре rd.

Команда AUIPC поддерживает последовательности из двух команд для получения произвольных смещений pc как для передачи потока управления, так и для доступа к данным. Комбинация AUIPC и 12-битного непосредственного значения в JALR может передавать управление на любой 32-битный адрес pc, в то время как AUIPC сложенное с 12-битным непосредственным значением смещения в обычных командах загрузки или сохранения позволяет получить доступ к любому 32-битному адресу данных относительно pc. Текущее значение pc можно получить, установив непосредственное значение U-типа в 0. Несмотря на то, что команда JAL+4 также позволяет получить значение pc, она может вызывать остановки конвейера в более простых микроархитектурах или засорять структуры буфера предсказания переходов (BTB) в более сложных микроархитектурах.

Команды типа регистр-регистр

В RV32I определено несколько арифметических операций R-типа. Все операции берут исходные операнды из регистров rs1 и rs2 и записывают результат в регистр rd. Полями funct7 и funct3 задается тип операции.

../.pic/Other/rv32i/add_and_sll_sub.png

ADD и SUB выполняют сложение и вычитание соответственно. Переполнения игнорируются, и младшие 32 бита результатов записываются в место назначения. SLT и SLTU выполняют знаковое и беззнаковое сравнения соответственно, записывая 1 в rd, если rs1 < rs2, или 0 в противном случае. Обратите внимание, что команда SLTU rd, x0, rs2 устанавливает rd в 1, если rs2 не равно нулю, иначе устанавливает rd в ноль (псевдоинструкция ассемблера SNEZ rd, rs). AND, OR и XOR выполняют побитовые логические операции.

SLL, SRL и SRA выполняют соответственно логический сдвиг влево, логический сдвиг вправо и арифметический сдвиг вправо значения в регистре rs1 на величину сдвига, содержащуюся в младших 5 битах регистра rs2.

Команда NOP

../.pic/Other/rv32i/nop.png

Инструкция NOP не изменяет архитектурное состояние процессора, за исключением увеличения pc и опциональных счетчиков производительности. NOP кодируется как ADDI x0, x0, 0.

Команды NOP могут быть использованы для выравнивания сегментов кода по микроархитектурно значимым границам адресов или для резервирования места для модификаций встраиваемого кода. Хотя существует множество возможных способов кодирования NOP, мы использовали каноническое кодирование NOP, чтобы обеспечить возможность микроархитектурной оптимизации, а также для более читаемого вывода при дизассемблировании.

Список использованной литературы

  1. RISC-V Instruction Set Manual
  2. Д.М. Харрис, С.Л. Харрис / Цифровая схемотехника и архитектура компьютера / пер. с англ. Imagination Technologies / М.: ДМК Пресс, 2018.