Лабораторная работа №5 "Декодер инструкций"

Устройство управления (УУ) – один из базовых блоков процессора, функцией которого является декодирование инструкций и выдача управляющих сигналов для всех блоков процессора. Роль УУ в данном курсе (с некоторыми оговорками) будет играть декодер инструкций.

Цель

Описать на языке SystemVerilog блок декодера инструкций для однотактного процессора с архитектурой RISC-V.

Материалы для подготовки к лабораторной работе

Ход работы

  1. Изучить микроархитектуру реализуемого процессорного ядра.
    1. Разобраться с логикой формирования управляющих сигналов для всех типов инструкций.
  2. Изучить описание сигналов декодера инструкций.
  3. Изучить набор поддерживаемых инструкций RISC-V и способы их кодирования
  4. Изучить конструкции SystemVerilog, с помощью которых будет описан декодер (#инструменты)
  5. Реализовать на языке SystemVerilog декодер инструкций (#задание)
  6. Проверить с помощью верификационного окружения корректность его работы.

Предлагаемая микроархитектура процессора RISC-V

На рис. 1 приводится микроархитектура реализуемого ядра процессора RISC-V.

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

../../.pic/Labs/lab_10_irq/fig_03.drawio.svg

Рисунок 1. Микроархитектура будущего процессорного ядра.

Предложенная микроархитектура похожа на микроархитектуру процессора CYBERcobra 3000 Pro 2.0 из ЛР№4, но с некоторыми изменениями.

В первую очередь изменились входы и выходы процессора:

  • память инструкций вынесена наружу, таким образом, у процессора появляются входы и выходы: instr_addr_o и instr_i;
  • также у процессора появились сигналы интерфейса памяти данных:
    • mem_addr_o — адрес внешней памяти;
    • mem_req_o — запрос на обращение во внешнюю память;
    • mem_size_o — размер данных при обращении в память;
    • mem_we_o — сигнал разрешения записи во внешнюю память;
    • mem_wd_o — данные для записи во внешнюю память;
    • mem_rd_i — считанные из внешней памяти данные; Эти сигналы используются при выполнении инструкций загрузки (сохранения) информации из (в) памяти данных.
  • еще у процессора появился вход stall_i, приостанавливающий обновление программного счётчика.

Кроме того, появилось два новых модуля: Interrupt Controller и Control Status Registers. Эти модули будут обеспечивать поддержку прерываний в процессорной системе.

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

Изменились и источники записи в регистровый файл, теперь это:

  • результат операции на АЛУ;
  • данные, считанные с внешней памяти;
  • данные из модуля регистров контроля и статуса.

Для того, чтобы управлять усложнившимся набором мультиплексоров, интерфейсом памяти данных и появившимися модулями нужно специальное устройство — Устройство управления (УУ). В данной микроархитектуре логика устройства управления не вынесена в отдельный модуль, лишь выделена на схеме синим цветом. По большей части, в предложенной микроархитектуре роль устройства управления выполняет декодер инструкций.

Описание сигналов декодера инструкций

Список портов декодера инструкций и их назначение представлен в таблице 1.

Название сигналаПояснение
fetched_instr_iИнструкция, подлежащая декодированию
a_sel_oУправляющий сигнал мультиплексора для выбора первого операнда АЛУ
b_sel_oУправляющий сигнал мультиплексора для выбора второго операнда АЛУ
alu_op_oОперация АЛУ
csr_op_oОперация модуля CSR
csr_we_oРазрешение на запись в CSR
mem_req_oЗапрос на доступ к памяти (часть интерфейса памяти)
mem_we_oСигнал разрешения записи в память, «write enable» (при равенстве нулю происходит чтение)
mem_size_oУправляющий сигнал для выбора размера слова при чтении-записи в память (часть интерфейса памяти)
wb_sel_oУправляющий сигнал мультиплексора для выбора данных, записываемых в регистровый файл
gpr_we_oСигнал разрешения записи в регистровый файл
branch_oСигнал об инструкции условного перехода
jal_oСигнал об инструкции безусловного перехода jal
jalr_oСигнал об инструкции безусловного перехода jalr
mret_oСигнал об инструкции возврата из прерывания/исключения mret
illegal_instr_oСигнал о некорректной инструкции

Таблица 1. Описание портов декодера инструкций.

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

Сигналы кода операции

В данный класс будут входить сигналы, которые сообщают отдельному функциональному блоку о том, какую из операций он должен выполнить. Таких блока два: АЛУ и модуль регистров контроля и статуса. АЛУ может выполнять одну из 16 операций, представленных в ЛР№2, для выбора которой и нужен подобный сигнал. Вы ещё не знакомы с появившимся в микроархитектуре модулем регистров контроля и статуса, однако на текущий момент нужно лишь понимать, что он тоже может выполнять одну из нескольких операций и что для этого ему нужен специальный сигнал.

Таким образом, в класс сигналов кода операции входят:

  • alu_op_o,
  • csr_op_o.

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

Управляющие сигналы мультиплексоров стадии выполнения и записи результата

В данный класс входят сигналы, управляющие мультиплексорами, размещенными в правой части схемы:

  • a_sel_o,
  • b_sel_o,
  • wb_sel_o.

Сигналы a_sel_o и b_sel_o определяют откуда пойдут данные на операнды АЛУ a_i, b_i соответственно. К примеру, если мы хотим, чтобы оба операнда брались из регистрового файла, нам необходимо подать значение 0 на оба соответствующих мультиплексора.

Сигнал wb_sel_o определяет источник данных для записи в регистровый файл: это либо результат операции на АЛУ, считанные данные из памяти данных, либо же данные, полученные из модуля регистров контроля и статуса.

Интерфейс памяти

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

Для взаимодействия с подсистемой памяти данных декодер инструкций будет использовать следующие сигналы:

  • mem_req_o — этот сигнал должен быть выставлен в 1 каждый раз, когда необходимо обратиться к памяти (считать или записать данные);
  • mem_we_o — этот сигнал должен быть выставлен в 1, если необходимо записать данные в память, (0 при чтении);
  • mem_size_o— этот сигнал указывает размер порции данных для передачи (возможные значения этого сигнала указаны в Таблице 2). Для удобства использования, данные значения определены в виде параметров в пакете decoder_pkg.
ПараметрЗначение mem_size_oПояснение
LDST_B3'd0Знаковое 8-битное значение
LDST_H3'd1Знаковое 16-битное значение
LDST_W3'd232-битное значение
LDST_BU3'd4Беззнаковое 8-битное значение
LDST_HU3'd5Беззнаковое 16-битное значение

Таблица 2. Значения сигнала mem_size_o при передаче различных порций данных.

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

Сигналы разрешения записи

В данную категорию входят два однобитных сигнала:

  • gpr_we_o — сигнал разрешения записи в регистровый файл (General Purpose Registers, GPR);
  • csr_we_o — сигнал разрешения записи в модуле регистров контроля и статуса.

Сигналы управления программным счетчиком

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

  • branch_o — сигнал об инструкции условного перехода;
  • jal_o — сигнал об инструкции безусловного перехода jal;
  • jalr_o — сигнал об инструкции безусловного перехода jalr;
  • mret_o — сигнал об инструкции возврата из прерывания/исключения mret.

Сигнал нелегальной инструкции

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

Это не единственное, что должен сделать декодер в подобной ситуации. Давайте разберем подробней, что должно происходить по приходу нелегальной инструкции.

Обработка нелегальной инструкции

Существует множество причин, почему процессору может прийти на исполнение неподдерживаемая инструкция, в том числе:

  • ошибка компиляции: либо баг в самом компиляторе, либо компиляция с неверными параметрами;
  • ошибка в аппаратуре (например сбой в работе памяти);
  • намеренная вставка неподдерживаемой инструкции (например для эксплуатации какой-нибудь уязвимости);
  • инструкция, которая на самом деле поддерживается процессором, но требует большего уровня привилегий и потому не может быть выполнена.

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

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

  • mem_req_o,
  • mem_we_o,
  • gpr_we_o,
  • csr_we_o,
  • branch_o,
  • jal_o,
  • jalr_o,
  • mret_o,

то есть, должны быть запрещены все запросы на запись, обращения в память и любые "прыжки" программного счетчика.

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

Набор поддерживаемых инструкций RISC-V и способы их кодирования

Все инструкции архитектуры RISC-V можно условно разделить на три категории.

  • Вычислительные инструкции (операции выполняются на АЛУ, с записью результата в регистровый файл). В основном это инструкции:
    • использующие в качестве операндов два регистра;
    • использующие в качестве операндов регистр и непосредственный операнд из инструкции (константу).
  • Инструкции для доступа к памяти:
    • загрузки из основной памяти в регистровый файл;
    • сохранения данных из регистрового файла в основную память;
  • Инструкции управления:
    • Условные переходы
    • Безусловные переходы
    • Системные инструкции
      • обращение к регистрам контроля и статуса;
      • системные вызовы и возврат из обработчика прерываний

В Таблице 3 приводится фрагмент из спецификации RISC-V. В верхней её части приводится 6 форматов кодирования инструкций: R, I, S, B, U и J (описание типов представлено в таблице 4). Затем список всех инструкций с конкретными значениями полей, соответствующих формату кодирования инструкции данного типа.

Под rd подразумевается 5-битный адрес регистра назначения (register destination), rs1 и rs2 — 5-битные адреса регистров источников (register source), imm — непосредственный (immediate, задающийся прямиком в инструкции) операнд (константа), расположение и порядок битов которого указывается в квадратных скобках. Обратите внимание, что в разных форматах кодирования константы имеют различную разрядность, а их биты расположены по-разному. Для знаковых операций константу предварительно знаково расширяют до 32 бит. Для беззнаковых расширяют нулями до 32 бит.

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

Таблица 3. Базовый набор инструкций из спецификации RISC-V[1, стр. 130], Стандартное расширение Zicsr[1, стр. 131], а также привилегированная инструкция mret[2, стр. 138].

КодированиеОписание
R-типАрифметические и логические операции над двумя регистрами с записью результата в третий (регистр назначения может совпадать с одним из регистров-источников)
I-типИнструкции с 12-битным непосредственным операндом
S-типИнструкции записи в память (инструкции store)
B-типИнструкции ветвления
U-типИнструкции с 20-битным «длинным» непосредственным операндом, сдвинутым влево на 12
J-типЕдинственная инструкция jal, осуществляющая безусловный переход по адресу относительно текущего счетчика команд

Таблица 4. Описание типов форматов кодирования инструкций ISA RISC-V.

Декодирование инструкций RISC-V

Как уже описывалось в дополнительных материалах, декодирование инструкции начинается с поля opcode (operation code, опкод). По этому полю определяется группа инструкций одного типа. Далее (для большинства типов кодирования) инструкция доопределяется через поля func3 и func7 (при наличии). Обратите внимание, что расположение этих полей одинаково для всех типов инструкций (см. верхнюю часть таблицы 3).

Поля rs1/rs2/imm и rd декодеру не нужны и используются напрямую для адресации в регистровом файле / использования непосредственного операнда в АЛУ.

Существуют особые инструкции, не имеющие никаких переменных полей (к примеру инструкция ECALL в таблице 3). Такие инструкции необходимо проверять целиком (нужно убедиться, что инструкция совпадает вплоть бита).

В Таблице 5 представлены все опкоды реализуемых нами инструкций. Представленные в ней коды операций 5-битные потому, что 2 младших бита полноценного 7-битного кода операции в реализуемых нами инструкциях должны всегда быть равны 11. Если это не так, то вся инструкция уже запрещенная и не нуждается в дальнейшем декодировании.

Для удобства значения кодов операций определены в виде параметров в пакете decoder_pkg.

ПараметрOpcodeОписание группы операцийКраткая запись
OP01100Записать в rd результат вычисления АЛУ над rs1 и rs2rd = alu_op(rs1, rs2)
OP_IMM00100Записать в rd результат вычисления АЛУ над rs1 и immrd = alu_op(rs1, imm)
LUI01101Записать в rd значение непосредственного операнда U-типа imm_urd = imm << 12
LOAD00000Записать в rd данные из памяти по адресу rs1+immrd = Mem[rs1 + imm]
STORE01000Записать в память по адресу rs1+imm данные из rs2Mem[rs1 + imm] = rs2
BRANCH11000Увеличить счетчик команд на значение imm, если верен результат сравнения rs1 и rs2if cmp_op(rs1, rs2) then PC += imm
JAL11011Записать в rd следующий адрес счетчика команд, увеличить счетчик команд на значение immrd = PC + 4; PC += imm
JALR11001Записать в rd следующий адрес счетчика команд, в счетчик команд записать rs1+immrd = PC + 4; PC = rs1+imm
AUIPC00101Записать в rd результат сложения непосредственного операнда U-типа imm_u и счетчика командrd = PC + (imm << 12)
MISC-MEM00011Не производить операцию-
SYSTEM11100Записать в rd значение csr. Обновить значение csr с помощью rs1. (либо mret/ecall/ebreak)csr = csr_op(rs1); rd = csr

Таблица 5. Описание кодов операций.

SYSTEM-инструкции

SYSTEM-инструкции используются для доступа к системным функциям и могут требовать привилегированный доступ. Данные инструкции могут быть разделены на два класса:

  • Обращение к регистрам статуса и контроля (Control and Status Registers, CSR)
  • Все остальные инструкции (возможно из набора привилегированных инструкций)

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

Обращение к регистрам контроля и статуса осуществляется шестью инструкциями стандартного расширения Zicsr. Каждая из этих инструкций (если у нее легальные поля) осуществляет запись в CSR и регистровый файл (блоки Control Status Registers и Register File на рис. 1 соответственно).

Кроме того, для возврата управления основному потоку инструкций, нужна дополнительная SYSTEM-инструкция привилегированного набора команд MRET.

Перечисленные выше инструкции являются "дополнительными" — их намеренно их добавили сверх стандартного набора инструкций, чтобы обеспечить требуемый нашей системе функционал. Однако осталось ещё две SYSTEM-инструкции, которые мы должны уметь декодировать, поскольку они есть в стандартном наборе инструкций.

Инструкции ECALL и EBREAK вызывают исключение. Подробнее исключения и прерывания будут разобраны в ЛР№10.

MISC-MEM инструкция

В базовом наборе инструкций RISC-V к MISC-MEM-операции относится инструкция FENCE. В реализуемом процессорном ядре эта инструкция не должны приводить ни к каким изменениям. Инструкция FENCE в RISC-V необходима при работе с несколькими аппаратными потоками, или "хартами" (hart – «hardware thread»). В RISC-V используется расслабленная модель памяти (relaxed memory model): потоки «видят» все инструкции чтения и записи, которые исполняются другими потоками, однако видимый порядок этих инструкций может отличаться от реального. Инструкция FENCE, использованная между двумя инструкциями чтения и/или записи гарантирует, что остальные потоки увидят первую инструкцию перед второй. Реализация FENCE является опциональной в RISC-V и в данном случае в ней нет необходимости, так как в системе не предполагается наличия нескольких аппаратных потоков. Данная инструкция должна быть реализована как NOP (no operation).

В таблице 6 представлены инструкции из таблицы 3 с приведением их типов, значениями полей opcode, func3, func7, функциональным описанием и примерами использования.

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

Таблица 6. Расширенное описание инструкций RV32IZicsr.

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

Также обратите внимание на инструкции ecall, ebreak и mret. Все эти инструкции I-типа имеют поле func3, равное нулю. С точки зрения декодирования инструкции I-типа, это одна и та же инструкция с разными значениями поля imm. Однако конкретно в данном случае (SYSTEM_OPCODE и func3 == 0) эти инструкции должны рассматриваться как совокупность всех 32-бит сразу (см. таблицу 3).

Формирование управляющих сигналов

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

Пример: для выполнения инструкции записи 32-бит данных из регистрового файла во внешнюю память sw, дешифратор должен направить в АЛУ два операнда (базовый адрес и смещение) вместе с кодом операции АЛУ (сложения) для вычисления адреса. Базовый адрес берется из регистрового файла, а смещение является непосредственным операндом инструкции S-типа. Таким образом для вычисления адреса записи декодер должен выставить следующие значения на выходах:

  • a_sel_o = 2'd0,
  • b_sel_o = 3'd1,
  • alu_op_o= ALU_ADD.

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

  • mem_req_o = 1'b1,
  • mem_size_o = LDST_W (см. таблицу 2),
  • mem_we_o = 1'b1.

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

Поскольку операция sw не является операцией перехода, сигналы jal_o, jalr_o и branch_o и mret должны быть равны нулю (иначе процессор совершит переход, а инструкция sw этого не подразумевает). Точно так же, поскольку во время записи во внешнюю память, в регистровый файл и регистры контроля и статуса ничего не должно быть записано, сигналы gpr_we_o и csr_we_o также должны быть равны нулю.

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

А вот сигнал wb_sel может принять любое значение (поскольку сигнал разрешения записи в регистровый файл равен нулю, не важно, каким будет источник данных для записи в регистровый файл, т.к. в него все равно ничего не будет записано).

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

В таблице 7 определен список выходных сигналов декодера инструкций и групп инструкций, при которых эти выходы могут принимать ненулевое значение.

Название сигналаПояснениеНа каких опкодах может принять ненулевое значение (см. таблицу 6)
a_sel_oУправляющий сигнал мультиплексора для выбора первого операнда АЛУНа всех кроме MISC_MEM и SYSTEM
b_sel_oУправляющий сигнал мультиплексора для выбора второго операнда АЛУНа всех кроме MISC_MEM и SYSTEM
alu_op_oОперация АЛУНа всех кроме MISC_MEM и SYSTEM
csr_op_oОперация модуля CSRТолько на SYSTEM
csr_we_oРазрешение на запись в CSRТолько на SYSTEM
mem_req_oЗапрос на доступ к памяти (часть интерфейса памяти)На LOAD и STORE
mem_we_oСигнал разрешения записи в память, «write enable» (при равенстве нулю происходит чтение)Только на STORE
mem_size_oУправляющий сигнал для выбора размера слова при чтении-записи в память (часть интерфейса памяти)На LOAD и STORE
gpr_we_oСигнал разрешения записи в регистровый файлНа всех кроме STORE, BRANCH, MISC_MEM
wb_sel_oУправляющий сигнал мультиплексора для выбора данных, записываемых в регистровый файлНа всех кроме STORE, BRANCH, MISC_MEM
illegal_instr_oСигнал о некорректной инструкции (на схеме не отмечен)На всех кроме JAL, LUI, AUIPC
branch_oСигнал об инструкции условного переходаТолько на BRANCH
jal_oСигнал об инструкции безусловного перехода jalТолько на JAL
jalr_oСигнал об инструкции безусловного перехода jalrТолько на JALR
mret_oСигнал об инструкции возврата из прерывания/исключения mretТолько на SYSTEM

Таблица 7. Описание портов дешифратора команд.

Дешифратор должен выдать единицу на выходе illegal_instr_o в случае:

  • неравенства двух младших битов opcode значению 11;
  • если значение поля opcode не совпадает ни с одним из известных и следовательно операция не определена.
  • некорректного значения полей func3 или func7 для данного опкода.

Кроме того, поскольку представленная на рис. 1 микроархитектура поддерживает только одно исключение (исключение через сигнал illegal_instr_o), этот сигнал должен быть равен единице и в случае:

  • если это инструкция ECALL / EBREAK.

Инструменты

В первую очередь язык описания аппаратуры SystemVerilog – это язык. С помощью этого языка человек объясняет либо синтезатору какое он хочет получить устройство, либо симулятору – как он хочет это устройство проверить. Синтезатор – это программа, которая создает из логических элементов цифровое устройство по описанию, предоставляемому человеком. Синтезатору внутри Vivado нужно объяснить, что от него нужно. Например, чтобы спросить дорогу у испанца, придется делать это на испанском языке, иначе он ничем не сможет помочь. А если вы знаете испанский, то скорее всего сможете это сделать еще и разными способами. В SystemVerilog точно также – одно и то же устройство можно описать разным кодом, но результат синтеза будет одним и тем же. Однако, часто два разных кода одинаковые по смыслу могут синтезироваться в разную аппаратуру, хотя функционально они будут идентичны, но могут отличаться, например, скоростью работы. Или одни и те же специальные языковые конструкции могут применяться для синтезирования разных цифровых элементов.

Декодер – комбинационная схема. Это значит, что каждый раз подавая на вход одни и те же значения, вы будете получать на выходе один и тот же результат, потому что комбинационные схемы не содержат элементов памяти.

Можно по-разному описывать комбинационные схемы. Например — через конструкцию assign. Для описания декодера отлично подойдет конструкция case, которая превратится не в мультиплексор, а в комбинационную схему с оптимальными параметрами критического пути. В доверилоговую эпоху разработчикам пришлось бы строить гигантские таблицы истинности и карты Карно, искать оптимальные схемы реализации. Сегодня эту задачу решает синтезатор, по описанию устройства сам находит наиболее эффективное решение.

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

Рассмотрим пример ниже. Внутри конструкции always_comb, перед конструкцией case указываются значения по умолчанию. Благодаря этому пропадает необходимость указывать все сигналы внутри каждого обработчика case, достаточно указать только те, что имеют значение отличное от значения по умолчанию. Представленный пример реализует комбинационную схему, которая при control_signal== 4'b1100 будет выставлять сигнал c == 1'b0, то есть отличное, от значения по умолчанию. Сигнал a никак не меняется, поэтому он не указан в соответствующем обработчике. Если сигнал size == 1'b0, то b будет равен 1, а d равен 0. Если сигнал size == 1'b1, то наоборот – b будет равен 0, а d равен 1.

module example (
  input  logic [3:0] control_signal,
  input  logic       sub_signal,
  output logic       a, b, c, d
);
  parameter logic [3:0] SOME_PARAM = 4'b1100;
  always_comb begin
    a = 1'b0;             // значения по умолчанию
    b = 1'b0;             // обратите внимание, что в блоке
    c = 1'b1;             // always_comb используется оператор
    d = 1'b0;             // блокирующего присваивания
    case(control_signal)
      // ...                 какие-то еще комбинации
      SOME_PARAM: begin   // если на control_signal значение SOME_PARAM
        c = 1'b0;
        case (sub_signal)
          1'b0: b = 1'b1; // если на sub_signal значение 1'b0
          1'b1: d = 1'b1; // если на sub_signal значение 1'b1
        endcase
      end
      // ...                 какие-то еще обработчики
      default: begin      // так как описаны не все значения
        a = 1'b0;         // control_signal, то чтобы результатом
        b = 1'b0;         // case не было защелки (latch),
        c = 1'b1;         // на выходе нужно обязательно добавлять
        d = 1'b0;         // default
      end
    endcase
  end

endmodule

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

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

Задание

Необходимо реализовать на языке SystemVerilog модуль декодера инструкций однотактного процессора RISC-V в соответствии с предложенной микроархитектурой. Далее приводится прототип разрабатываемого модуля.

module decoder (
  input  logic [31:0]  fetched_instr_i,
  output logic [1:0]   a_sel_o,
  output logic [2:0]   b_sel_o,
  output logic [4:0]   alu_op_o,
  output logic [2:0]   csr_op_o,
  output logic         csr_we_o,
  output logic         mem_req_o,
  output logic         mem_we_o,
  output logic [2:0]   mem_size_o,
  output logic         gpr_we_o,
  output logic [1:0]   wb_sel_o,
  output logic         illegal_instr_o,
  output logic         branch_o,
  output logic         jal_o,
  output logic         jalr_o,
  output logic         mret_o
);
  import decoder_pkg::*;

endmodule

В зависимости от стиля оформления, модуль может занимать больше сотни строк кода, но это не делает его реализацию сложной. По сути, дешифратор — это просто большой case с описанием того, в каком случае, какие сигналы и чему должны быть равны. Работа требует внимательности, немного усидчивости и понимания выполняемых действий. С огромной вероятностью в коде будут ошибки и их нужно будет исправлять. Ошибки — это нормально (не ошибается тот, кто ничего не делает), а исправление ошибок дает бесценный опыт разработки. Возможно, реализация этого модуля в какой-то момент покажется рутинной, но по окончании следующей лабораторной работы удовольствие от результата покажет, что оно того стоило.

Порядок выполнения задания

  1. Внимательно ознакомьтесь с выходными сигналами декодера инструкций и тем, как они управляют функциональными блоками процессорного ядра, представленного на рис. 1, а также типами команд. В случае возникновения вопросов, проконсультируйтесь с преподавателем.
  2. Добавьте в Design Sources проекта файл alu_opcodes_pkg.sv (если тот ещё не был добавлен в ходе выполнения ЛР№2), а также файлы csr_pkg.sv и decoder_pkg.sv. Эти файлы содержат параметры, которые будет удобно использовать при описании декодера.
  3. Опишите модуль декодера инструкций с таким же именем и портами, как указано в задании.
    1. Для удобства дальнейшего описания модуля, рекомендуется сперва создать сигналы opcode, func3, func7 и присвоить им соответствующие биты входного сигнала инструкции.
    2. Модуль может быть описан множеством способов: каждый выходной сигнал может быть описан через собственную комбинационную логику в отдельном блоке case, однако проще всего будет описать все сигналы через вложенные case внутри одного блока always_comb.
    3. Внутри блока always_comb до начала блока case можно указать базовые значения для всех выходных сигналов. Это не то же самое, что вариант default в блоке case. Здесь вы можете описать состояния, которые будут использованы чаще всего, и в этом случае, присваивание сигналу будет выполняться только в том месте, где появится инструкция, требующая значение этого сигнала, отличное от базового.
    4. Далее вы можете описать базовый блок case, где будет определен тип операции по ее коду.
    5. Определив тип операции, вы сможете определить какая конкретно операция по полям func3 и func7 (если данный тип имеет такие поля).
    6. Не забывайте, что в случае, если на каком-то из этапов (определения типа, или определения конкретной операции) вам приходит непредусмотренное ISA значение какого-либо поля, необходимо выставить сигнал illegal_instr_o.
    7. В случае некорректной инструкции, вы должны гарантировать, что не произойдет условный/безусловный переход, а во внешнюю память, регистровый файл, а также регистры контроля и статуса ничего не запишется. Не важно, что будет выполняться на АЛУ, не важно какие данные будут выбраны на мультиплексоре источника записи. Важно чтобы не произошел сам факт записи в любое из устройств (подумайте какие значения для каких сигналов необходимо для этого выставить).
  4. Проверьте модуль с помощью верификационного окружения, представленного в файле lab_05.tb_decoder.sv. Вполне возможно, что после первого запуска вы столкнётесь с сообщениями о множестве ошибок. Вам необходимо исследовать эти ошибки на временной диаграмме и исправить их в вашем модуле.
    1. Перед запуском моделирования, убедитесь, что у вас выбран корректный модуль верхнего уровня в Simulation Sources.
  5. Данная лабораторная работа не предполагает проверки в ПЛИС

Список источников

  1. The RISC-V Instruction Set Manual Volume I: Unprivileged ISA
  2. The RISC-V Instruction Set Manual Volume II: Privileged Architecture