Проектирование процессоров RISC-V в среде Logisim и на языке Verilog в среде Quartus

Проектирование блока управления

Разбить на страницы
Показывать лекцию целиком

Практическая работа 10. Проектирование дешифратора инструкций. Блока управления. Сборка процессора

Необходимые материалы для лекции asm+hex.zip, Logisim-10.zip и Verilog-10.zip.

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

Рассмотрим построение дешифратора команд на примере сложения непосредственным операндом: addi x1, x2, 5 , где x1- регистр результата, x2 - слагаемое, 5 - непосредственное слагаемое. Если разложить данную инструкцию на микрокоманды управления, то их последовательность будет иметь вид:

  • Выставить адрес x2 на шине адреса операнда rs1 регистрового файла;
  • Коммутировать часть содержащую непосредственные данные на линию второго операнда АЛУ;
  • Коммутировать данные с выхода АЛУ на вход данных регистрового файла;
  • Включить запись в регистровый файл по адресу регистра x1.
  • В однотактном процессоре данная операция должна быть совершена за один такт. Задача дешифратора максимально детализировать сигналы, применяемые для активации соответствующих блоков. При этом в период дешифрации инструкций могут быть получены дополнительные признаки: знаковая / беззнаковая операция, наличие переходов.

    Графически операция сложения с непосредственным операндом выглядит как показано на рисунке 10.1 (IG - блок непосредственных значений).

    (рис 10.1)

    Для проектирования дешифратора необходимо коды инструкций, применяемые в процессоре, собрать в единую таблицу (таблица 10.1). Для проектирования будут использованы столбцы opcode и opcode HEX.

    Формат инструкции Мнемоника инструкции Opcode Opcode HEX Funct3
    R-style add 0110011 0x33 000
    sub 0110011 0x33 000
    sll 0110011 0x33 001
    xor 0110011 0x33 100
    srl 0110011 0x33 101
    sra 0110011 0x33 101
    or 0110011 0x33 110
    and 0110011 0x33 111
    I-type lb 0000011 0x3 000
    lh 0000011 0x3 001
    lw 0000011 0x3 010
    ld 0000011 0x3 011
    lbu 0000011 0x3 100
    lhu 0000011 0x3 101
    lwu 0000011 0x3 110
    addi 0010011 0x13 000
    slli 0010011 0x13 001
    xori 0010011 0x13 100
    srli 0010011 0x13 101
    srai 0010011 0x13 101
    ori 0010011 0x13 110
    andi 0010011 0x13 111
    jalr 1100111 0x67 000
    S-type sb 0100011 0x23 000
    sh 0100011 0x23 001
    sw 0100011 0x23 010
    sd 0100011 0x23 111
    SB-type beq 1100011 0x63 000
    bne 1100011 0x63 001
    blt 1100011 0x63 100
    bge 1100011 0x63 101
    bltu 1100011 0x63 110
    bgeu 1100011 0x63 111
    u-type lui 0110111 0x37 Не определен
    Uj-type jal 1101111 0x6f Не определен

    Так как в данном случае дешифратор должен анализировать входную комбинацию и формировать выходной сигнал, то можно смоделировать его схему по принципу проверки равенства постоянного значения opcode входной комбинации. Для этого используется элемент компаратор. Opcode будут константами. Ширина поля opcode составляет 7 бит.

    Создадим новую схему в проекте cpu.circ. Имя присвоим instdec. Разместим на поле 8 компараторов c разрядностью 7 бит (формат числа - беззнаковый) для форматов инструкций: R,S, I (отдельно для Load), а также выпадающих из форматов инструкций: jalr, jal, lui, auipc, beq.

    Объединим все входы первых операндов в общую шину и подключим к элементу тоннель. Метке тоннеля задать свойство Inst6_0, а его разрядность определить равной 7. Вторые входы компараторов подключим к константам, соответствующим полю opcode: 33,03,23,63,13,67,6f,37,17. Выходы равенства подключим к элементам выход. Меткам выходов присвоить соответствующие значения: R_format, Load, Store, Branch, I_type, Jalr, Jal, Lui, Auipc. Схема функциональная дешифратора приведена на рисунке 10.2.

    (рис 10.2)

    Следующим этапом необходимо обратиться к формату инструкций, так как в них, в зависимости от opcode, могут быть закодированы разные функциональные сигналы в одних и тех же позициях. Поле, начиная с 7 адреса может содержать адреса регистров либо непосредственные значения. Для переопределения данных, поступающих в теле инструкций, применяется блок, именуемый Immediate Generator - блок непосредственных значений. При проектировании данного блока необходимо воспользоваться описанием полей для различного формата команд (рисунок 10.3).

    (рис 10.3)

    Создадим новую схему, присвоим ей наименование IMMGEN. Фактически схема будет представлять собой сплиттер (разветвитель) сигналов шины и сборщик сигналов в шину.

    Разместим на поле вход с разрядностью равной 32. Свойство метки зададим instr. Подключим к нему разветвитель с настройками:

    Веерный выход - 9;

    Разрядность входа - 32;

    "Внешний вид" - праворукий;

  • Бит 0 - 6 задать настройки (0, сверху);
  • Бит 7 - 1;
  • Бит 8-11 - 2;
  • Бит 12-14 - 3;
  • Бит 15-19 - 4;
  • Бит 20 - 5;
  • Бит 21-24 - 6;
  • Бит 25-30 - 7;
  • Бит 31 - 8.
  • К выходу 6-0 подключим элемент "Не подключайтесь" в разделе проводка (задать разрядность 7). Полученный разветвитель приведен на рисунке 10.4.

    (рис 10.4)

    Вывод 31 разветвителя подключить к элементу расширитель битов со свойствами:

  • разрядность входа - 1;
  • разрядность выхода - 21;
  • тип расширителя - знаковый.
  • Для сборки разделенных линий в шину будем также использовать разветвители. На рисунке 10.5 приведена схема корреспонденции линий для получения непосредственных значений I формата инструкций (для каждой инструкции будет показано отдельное включение).

    (рис 10.5)

    На рисунке 10.6 приведена схема корреспонденции для S формата инструкций.

    (рис 10.6)

    На рисунке 10.7 приведена схема корреспонденции для B формата инструкций.

    (рис 10.7)

    На рисунке 10.8 приведена схема корреспонденции для U и J формата инструкций. На рисунке 10.9 приведена схема функциональная собранного блока.

    (рис 10.8) (рис 10.9)

    Далее следует добавить полученный IMMGEN в ранее спроектированный блок дешифратора instdec. Для этого необходимо разместить на поле вход с разрядностью 32. Присвоить метке входа значение Instruct. Соединить шиной (можно тоннелем) со входом inst блока IMMGEN. Также к шине подключить разветвитель разрядностью 7 бит и количеством веерных выходов равным 1. Для битов с 0 по 6 сверху задать 0. Остальным присвоить выходам присвоить значение "нет". К веерному выходу подключить тоннель и присвоить свойству метки значение Inst6_0. Далее добавить на поле четыре двухвходовых мультиплексора с разрядностью 32. Свойству "Положение выбирающего входа" мультиплексоров задать "Сверху/справа".

    Соединить мультиплексоры последовательно каскадом. Управляющий вход первого мультиплексора подключить к тоннелю с меткой S. Управляющий вход второго мультиплексора подключить к тоннелю с меткой B. Управляющий вход третьего мультиплексора подключить к тоннелю с меткой U, а четвертого к тоннелю с меткой J. Выход с последнего мультиплексора каскада подключить к выходу с именем imm. На рисунке 10.10 показан результат построения схемы на данном этапе.

    (рис 10.10)

    Далее необходимо в дешифраторе к линии Store подключить тоннель с меткой S, к линии Branch тоннель B, а к Jal соответственно J. Линии lui и auipc объединить через двухвходовой элемент "ИЛИ", а его выход подключить к тоннелю U (рисунок 10.11).

    (рис 10.11)

    Следующим шагом дополнить шину Instruct сплиттерами для инструкций R, I формата.

    Для этого продлим данную шину и подключим к разветвителю со следующими параметрами:

  • "Веерный выход" - 9;
  • "Биты данных" - 32;
  • "Внешний вид" -праворукий;
  • "Направление" - восток;
  • Бит 0-6 - 0, сверху;
  • Бит 7 - 1;
  • Бит 8 -11 - 2;
  • Бит 12-14 - 3;
  • Бит 15-19 - 4;
  • Бит 20 - 5;
  • Бит 21-24 - 6;
  • Бит 25-30 - 7;
  • Бит 31 - 8.
  • К выводам 0-6 и 31 подключить элементы "Не подключайтесь" соответствующей разрядности.

    К выводам 7 и 11-8 подключить разветвитель со свойствами:

  • "Веерный выход" - 2;
  • "Биты данных" - 5;
  • "Внешний вид" -праворукий;
  • "Направление" - запад;
  • Бит 0 - 0, сверху;
  • Бит 1-4 - 1, снизу.
  • К выходу данного разветвителя подключим выход с меткой rd. К выводам 14-12 подключим выход с меткой funct3. К выводам 19-15 подключим выход с меткой rs1. К выводам 20 и 24-21 подключим разветвитель, аналогичный для порта rd. Имя метки зададим rs2.

    К выводам 25-30 подключим разветвитель со свойствами:

  • "Веерный выход" - 2;
  • "Биты данных" - 6;
  • "Внешний вид" -праворукий;
  • "Направление" - восток;
  • Бит 0-4 - 0, 0,сверху;
  • Бит 5 - 1, снизу.
  • К выводам 0-4 следует подключить элемент "Не подключайтесь". К выводу 5 подключить выход с именем метки funct7. На рисунке 10.12 приведена спроектированного сплиттера. На рисунке 10.13 приведена полная функциональная схема, проектируемого блока процессора.

    (рис 10.12) (рис 10.13)

    Используя ресурс http://riscvasm.lucasteske.dev/#, можно выполнить тестирование работы блока. При вводе инструкций на ассемблере в поле ввода будет сформирован машинный код. Используя машинный код, выполним проверку работы спроектированного блока. Для этого необходимо вводить код в поле Instruct и проверять сигналы и данные на соответствующих выходах.

    Примеры тестовых инструкций и соответствующий машинный код:

  • addi x2,x1,5# машинный код 00508113;
  • add x3, x1, x8# машинный код 008081b3;
  • lui x15,0x6# машинный код 000067b7.
  • Самостоятельно:

    Провести тестирование с максимально возможным количеством инструкций для различных форматов инструкций.

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

    (рис 10.14)

    Полученный блок следует разместить на главной схеме. Выход блока rs1 подключить к r1sel регистрового файла. Выход rs2 подключить к r2sel. Выход rd подключить к RD регистрового файла. Выход imm блока Decode Imm Gen подключить к входу 1 двухвходового 32-х разрядного мультиплексора. Вход 0 мультиплексора подключить к выходу rs2 регистрового файла. Выход мультиплексора подключить к входу dataB АЛУ (рисунок 10.15).

    (рис 10.15)

    Выход rs1 регистрового файла подключить к входу 0 двухвходового 32-х разрядного мультиплексора. Вход 1 мультиплексора подключить к выходу IMEM-addr счетчика инструкций (рисунок 10.16).

    (рис 10.16)

    Далее необходимо разработать схему управления АЛУ. При проектировании блока АЛУ было создано 4-х разрядное поле микроопераций. Если провести сравнение с форматом инструкций для арифметико-логического устройства, то можно определить, что микрооперация определяется как состояние пятого бита поля funct 7 и кода в поле funct3. Из анализа инструкций можно сделать вывод, что funct 7 составная часть поля микрокоманды операций АЛУ - старший бит.

    Создадим новую схему и присвоим имя aludec. На рисунке 10.17 приведена схема функциональная дешифратора инструкций. Следующим шагом произведем начертание УГО элемента как показано на рисунке 10.18

    (рис 10.17) (рис 10.18)

    Добавить созданный элемент на главную схему. Выходы funct3 и funct7 блока дешифратора инструкций соединить с аналогичными входами блока дешифратора АЛУ.

    Выходы Jal и Jalr дешифратора инструкций объединить двухвходовым элементом "ИЛИ". Выход с данного элемента вывести на вход jump блока ALU dec. Вход alu_enable подключить к выходу двухвходового элемента "ИЛИ" входы которого подключить к выходам R_format и I_type дешифратора инструкций. На рисунке 10.19 показана схема включения ALU dec.

    (рис 10.19)

    Вход мультиплексора управления записью в порт dataB АЛУ подключить к выходу 5-ти входового элемента "ИЛИ". Входы данного элемента следует подключить к выходным сигналам дешифратора инструкций: I_type, Load, Store, Jal. Выходы Lui и Auipc дешифратора инструкций объединить двухвходовым элементом "ИЛИ" и подключить к пятому входу "ИЛИ".

    Выход Load дешифратора инструкций подключить к тоннелю с меткой L, также к тоннелю с данной меткой подключить вход OE оперативной памяти. Входы синхронизации блоков процессора подключить к тоннелю с меткой "CLK". Вход памяти WE подключить к линии, идущей к выходу Store дешифратора инструкций.

    Добавить на схему двухвходовой 32-х разрядный мультиплексор. К входу 0 подключить выход rs1 регистрового файла. К входу 1 подключить выход Imem_addr счётчика инструкций PC. Вход управления данным мультиплексором подключить к выходу, предварительно добавленного двухвходового элемента "ИЛИ". Входы данного логического элемента подключить к выходам элемента "ИЛИ", объединяющего выходы Lui и Auipc дешифратора инструкций, и к выходу Jal данного блока.

    Вход управления WE регистрового файла подключить к выходу предварительно добавленного пятивходового элемента "ИЛИ". Входы данного элемента необходимо подключить к выходам дешифратора: R_format, I_type, Load. Также два оставшихся входа подключить к выходам элементов "ИЛИ", объединяющих Lui и Auipc, а также Jal и Jalr. На рисунке 10.20 приведена схема организации управления входом WE регистрового файла.

    (рис 10.20)

    Блок счетчика инструкций дополнить сумматором и двумя мультиплексорами. Схема включения приведена на рисунке 10.21. Выход счетчика PC_4 соединить с входом 0 младшего мультиплексора. Выход данного мультиплексора соединить с входом 0 старшего мультиплексора. Выход старшего мультиплексора подключить к входу PC_in счетчика. Вход 1 младшего мультиплексора подключить к выходу сумматора. Один из входов сумматора подключить к выходу Imm дешифратора инструкций. Второй вход сумматора подключить к выходу счетчика Imem_addr.

    (рис 10.21)

    Вход 1 старшего мультиплексора соединить с шиной, выходящей из мультиплексора оперативной памяти, но его необходимо соединить через разветвитель, где нулевой бит должен быть подключён к константе лог. "0". Остальные разряды берутся из шины (рисунок 10.22).

    (рис 10.22)

    Вход управления старшего мультиплексора необходимо подключить к выходу элемента "ИЛИ", объединяющего выходы Jal и Jalrc дешифратора инструкций. Вход управления младшего мультиплексора следует подключить к предварительно добавленному двухвходовому элементу "И". Данный вход организуется с применением переходов Branch, рассмотренных ниже.

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

    Для выполнения переходов необходимо создать дополнительный блок. Исходя из таблицы 10.1, данный блок активируется при наличии сигнала branch =63h и анализирует состояние поля funct3. Для реализации данного блока создадим новую схему. Наименование ей присвоим Branc_unit. В данную схему добавить два компаратора с разрядностью 32. Один со свойством "Формат числа" - "Дополнительный код". Второй со свойством "Формат числа" - "Беззнаковое". Далее на основании кодовой комбинации funct3, приведенной в таблице 10.2 произведём коммутацию выходов компараторов. Для этого добавим в схему мультиплексор со следующими параметрами:

  • Выбирающие биты -3;
  • Биты данных - 1;
  • Разрешающий вход - нет.
  • Выход мультиплексора подключим к выходу с меткой "Br".

    Команда перехода Вид перехода Код funct3
    BEQ при равенстве двух регистров 000
    BNE если два регистра не равны 001
    BLT если первый регистр меньше второго 100
    BGE если первый регистр больше второго 101
    BLTU если первый регистр меньше второго (беззнаковое сравнение) 110
    BGEU если первый регистр больше второго (беззнаковое сравнение) 111

    На рисунке 10.23 приведена схема функциональная блока условных переходов. На рисунке 10.24 ее графическое обозначение.

    (рис 10.23) (рис 10.24)

    Созданный модуль должен быть объединен с выходом Branch блока управления. На рисунке 10.25 приведена схема управления процессора с использованием Branch_unit,

    (рис 10.25)

    На рисунке 10.26 приведена итоговая функциональная схема процессора.

    (рис 10.26)

    Верификация созданного процессора

    При написании программ для тестирования созданного процессора, потребуется их загрузка в память ROM процессора. Используя средства компиляции, например, приведенный выше сайт онлайн компилятора, подготавливается HEX файл. В любом поддерживаемом формате. Далее необходимо щелкнуть правой кнопкой мыши по ROM памяти. В ниспадающем меню выбрать "загрузить образ".

    В открывшемся диалоговом окне, выбрать файл с расширением .hex. Откроется диалоговое окно настроек загружаемого файла (рисунок 10.27). Необходимо установить активной метку v.3.0 hex и убедится, что в поле Decoded данные соответствуют созданному ранее файлу (можно проверить, выбрав поле Original). В подвале данного диалога выводится сообщение о наличии ошибок в загружаемом файле.

    (рис 10.27)

    Написанию программ на языке ассемблера для процессоров RISC-V, посвящен ряд курсов Альянса RISC-V, также дается описание структуры программ на ассемблере в [1].

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

    Тестирование процессора на выполнение циклов. Для этого будет разработана программа представленная в листинге 10.1.

    .global _boot
    .text 
    _boot:                    
     .global _start
            .data #Секция глобальных данных
            .text #Секция кода
    _start: #Метка начала программы
            li      x5, 1  # Первое слагаемое
            li      x6, 1  # Второе слагаемое
            li      x8, 10 # Максимальное значение в цикле
            li      x9, 1  # Значение уменьшения счетчика цикла
    loop: # Метка цикла
            add     x6, x5, x6   # Складываем два значения и помещаем в x6
            sub     x8, x8, x9    # Уменьшаем счетчик на 1 
            bne     x8, x0, loop   # Если x8 не равен 0 продолжаем цикл
            # Завершение программы 
            li      x17, 10      # В данном случае применяется вызов в операционную систему
            ecall  
    

    HEX файл для ROM памяти приведен ниже:

    0000: 00100293
    0001: 00100313
    0002: 00a00413
    0003: 00100493
    0004: 00628333
    0005: 40940433
    0006: fe041ce3
    0007: 00a00893
    0008: 00000073
    0009: 00000000
    

    Второй тестовой программой для созданного процессора является вычисление чисел Фибоначчи для первых 10 чисел. Предлагается выполнить ее с применением двух видов алгоритмов: линейном и в цикле. В листинге 10.2 приведен код на языке ассемблера для линейного алгоритма.

    .global _boot
    .text
    _boot:                    
        addi x1, x0, 0  # очистка x1
        sw  x1, 1(x1)  # сохранение в памяти cо смещением 1
        addi x1, x0, 1 # запись в х1 1 
        sw  x1, 4(x1)  # сохранение в памяти со смещением 4
        add x2, x0, x1  # запись в x2 лог. "1"
        sw  x2, 8(x1)   # сохранение в памяти со смещением 8 
        add x3, x1, x2  #сложение х1 и х2 и запись в x3 далее аналогично
        sw  x3, 12(x1) 
        add x4, x2, x3   
        sw  x4, 16(x1)
        add x5, x3, x4   
        sw  x5, 20(x1)
        add x6, x4, x5   
        sw  x6, 24(x1)
        add x7, x5, x6  
        sw  x7, 28(x1)
        add x8, x6, x7   
        sw  x8, 32(x1)
        add x9, x7, x8   # в результате данной операции в х9 сохранено число 
        sw  x9, 36(x1) # в результате данной операции в памяти сохранено число 
    end:
        addi x10, x0, 0
        ecall
    

    HEX файл для ROM памяти приведен ниже:

    v3.0 hex words addressed
    0000: 00000093 0010a0a3 00100093 0010a223 00100133 0020a423 002081b3 0030a623
    0008: 00310233 0040a823 004182b3 0050aa23 00520333 0060ac23 006283b3 0070ae23
    0010: 00730433  0280a023 008384b3 0290a223 00000513 00000073 00000000 00000000 
    

    Решение задачи для вычисления чисел Фибоначчи в цикле приведено в листинге 10.3.

    .global _start
            .data
            .text
    _start:
            li      x5, 1               # Первое число Фибоначчи (F(1))
            li      x6, 1               # Второе число Фибоначчи (F(2))
            li      x7, 0x0000010 # Адрес для хранения чисел Фибоначчи
            li      x8, 10  
            li      x9, 2  # Значение уменьшения цикла
    
    loop:
            sw      x5, 0(x7)           # Сохраняем F(n-1) по адресу t2
            sw      x6, 4(x7)           # Сохраняем F(n) по адресу t2 + 4
            add     x7, x5, x6           # x7 = F(n-1) + F(n)
            add     x5, x5, x6           # Обновляем F(n-1) на F(n)
            add     x6, x6, x7           # Обновляем F(n) на F(n-1) + F(n)
            addi    x7, x7, 8            # Увеличиваем адрес для следующей пары чисел
            sub     x8, x8, x9           # Уменьшаем счетчик на 2 (так как сохраняем 2 числа)
            bne     x8, x0, loop        # Если x8 не ноль, продолжаем цикл
    
            # Завершение программы (если требуется, добавить код завершения)
            li      x10, 10               # Код системного вызова для завершения программы для реального #микропроцессор
            ecall
    

    Содержимое файла ROM памяти приведено ниже

    v3.0 hex words addressed
    0000: 00000093 0010a0a3 00100093 0010a223 00100133 0020a423 002081b3 0030a623
    0008: 00310233 0040a823 004182b3 0050aa23 00520333 0060ac23 006283b3 0070ae23
    0010: 00730433 0280a023 008384b3 0290a223 00000513 00000073 00000000 00000000
    

    На рисунке 10.28 приведен итог вычисления числа Фибоначчи. Он должен соответствовать результату вычисления в линейном алгоритме.

    (рис 10.28)

    Для завершения проектирования процессора на языке Verilog HDL необходимо вернуться в среду в ранее созданный проект RISC_V. Реализуем блок переходов BranchUnit. Как видно из рисунка 10.23 в его основе лежат компараторы, выполняющие сравнение двух 32-х битных кодов. В зависимости от кода на шине funct3 коммутирующие результирующий сигнал сравнения на выход. На языке Verilog HDL данная схема может быть реализована так как показано в листинге 10.4 (файл BranchUnit.v)

    module BranchUnit(
        input [31:0] srcA, srcB, //входы данных с регистров
        input [2:0] funct3, // вход funct3
        output reg branchTaken //выход сигнализирующий о наличии перехода
    );
        always @(*) begin
            case (funct3)
                3'b000: branchTaken = (srcA == srcB); // BEQ
                3'b001: branchTaken = (srcA != srcB); // BNE
                3'b100: branchTaken = ($signed(srcA) < $signed(srcB)); // BLT
                3'b101: branchTaken = ($signed(srcA) >= $signed(srcB)); // BGE
                3'b110: branchTaken = (srcA < srcB); // BLTU
                3'b111: branchTaken = (srcA >= srcB); // BGEU
                default: branchTaken = 1'b0; // по умолчанию выход 0 
            endcase
        end
    endmodule
    

    На рисунке 10.29 приведен вид RTL созданного модуля. Развернув его (рисунок 10.30) , можно убедится в эквивалентности схемы, приведенной на рисунке 10.23.

    (рис 10.29) (рис 10.30)

    Реализуем модуля дешифратора операций АЛУ. Для этого создадим файл ALUDecoder.v. В листинге 10.5 приведен код данного модуля. Анализируя поля funct7, funct3 инструкции RISC-V, а также поле aluOp формируемое блоком ControlUnit данный дешифратор осуществляет формирование кода микрооперации в АЛУ.

    module ALUDecoder(
        input [6:0] funct7,
        input [2:0] funct3,
        input [1:0] aluOp,//код операции
        output reg [3:0] aluControl
    );
        always @(*) begin
            case (aluOp)
                2'b00: aluControl = 4'b0000; // операции загрузка/сохранение (ADD)
                2'b01: aluControl = 4'b0001; // операция ветвления (SUB)
                2'b10: begin // R-type инструкции
                    case ({funct7, funct3})
                        10'b0000000000: aluControl = 4'b0000; // ADD
                        10'b0100000000: aluControl = 4'b0001; // SUB
                        10'b0000000111: aluControl = 4'b0010; // AND
                        10'b0000000110: aluControl = 4'b0011; // OR
                        10'b0000000100: aluControl = 4'b0100; // XOR
                        10'b0000000001: aluControl = 4'b0101; // SLL
                        10'b0000000101: aluControl = 4'b0110; // SRL
                        10'b0100000101: aluControl = 4'b0111; // SRA
                        10'b0000000010: aluControl = 4'b1000; // SLT
                        10'b0000000011: aluControl = 4'b1001; // SLTU
                        default: aluControl = 4'b0000;
                    endcase
                end
                default: aluControl = 4'b0000;
            endcase
        end
    endmodule
    

    На рисунке 10.31 приведен вид RTL созданного модуля.

    (рис 10.31)

    Следующим этапом выполним построение "сплиттера" кода инструкции - дешифратора инструкции (схемотехническая реализация разборки кода инструкции рассмотрена нами в начале данного раздела). Создадим в проекте RISC_V новый файл с именем InstructionDecoder.v. Внесем в него код из листинга 10.6.

    module InstructionDecoder(
        input [31:0] instr,
        output [6:0] opcode,
        output [4:0] rd,
        output [2:0] funct3,
        output [4:0] rs1, rs2,
        output [6:0] funct7,
        output [31:0] imm
    );
        assign opcode = instr[6:0];// выделяем 0 по 6 бит под opcode
        assign rd = instr[11:7]; //с 7 по 11 бит адрес регистра назначения
        assign funct3 = instr[14:12]; //с 12 по 14 бит поле funct3
        assign rs1 = instr[19:15];//с 15 по 19 бит адрес регистра источника 1
        assign rs2 = instr[24:20];//с 20 по 24 бит адрес регистра источника 2
        assign funct7 = instr[31:25];
        assign imm = { {21{instr[31]}}, instr[30:20] }; //формируемое поле //непосредственных значений Immediate (I-Type)
    endmodule
    

    На рисунке 10.32 приведен вид RTL созданного модуля.

    (рис 10.32)

    Если раскрыть модуль InstructionDecoder, то можно наблюдать разветвление эквивалентное разветвителю, приведенному на рисунке 10.12 (рисунок 10.33).

    (рис 10.33)

    Последним функциональным модулем процессора выступит модуль ControlUnit. Данный модуль формирует сигналы управления в зависимости от поля opcode инструкции.

    Для описания данного модуля создадим файл ControlUnit.v и введем код из листинга 10.7.

    module ControlUnit(
        input [6:0] opcode,
        output reg [1:0] aluOp,
        output reg memWrite,
        output reg memRead,
        output reg branch,
        output reg regWrite,
        output reg jump // переход операций JAL и JALR
    );
        always @(*) begin
            case (opcode)
                7'b0000011: begin // Load (I-type)
                    aluOp = 2'b00;
                    memWrite = 0;
                    memRead = 1;
                    branch = 0;
                    regWrite = 1;
                    jump = 0;
                end
                7'b0100011: begin // Store (S-type)
                    aluOp = 2'b00;
                    memWrite = 1;
                    memRead = 0;
                    branch = 0;
                    regWrite = 0;
                    jump = 0;
                end
                7'b1100011: begin // Branch (B-type)
                    aluOp = 2'b01;
                    memWrite = 0;
                    memRead = 0;
                    branch = 1;
                    regWrite = 0;
                    jump = 0;
                end
                7'b0110011: begin // R-type
                    aluOp = 2'b10;
                    memWrite = 0;
                    memRead = 0;
                    branch = 0;
                    regWrite = 1;
                    jump = 0;
                end
                7'b1100111: begin // JALR (I-type)
                    aluOp = 2'b00; // Используется для вычисления адреса (PC + imm)
                    memWrite = 0;
                    memRead = 0;
                    branch = 0;
                    regWrite = 1; // Результат записывается в регистр
                    jump = 1; // Указывает на команду перехода
                end
                7'b1101111: begin // JAL (UJ-type)
                    aluOp = 2'b00; 
                    memWrite = 0;
                    memRead = 0;
                    branch = 0;
                    regWrite = 1; // Результат записывается в регистр
                    jump = 1; // Указывает на команду перехода
                end
                7'b0110111: begin // LUI (U-type)
                    aluOp = 2'b11; 
                    memWrite = 0;
                    memRead = 0;
                    branch = 0;
                    regWrite = 1; // Загружаем значение в регистр
                    jump = 0;
                end
                default: begin
                    aluOp = 2'b00;
                    memWrite = 0;
                    memRead = 0;
                    branch = 0;
                    regWrite = 0;
                    jump = 0;
                end
            endcase
        end
    endmodule
    

    На рисунке 10.34 приведен вид RTL созданного модуля.

    (рис 10.34)

    Все модули объединяются файлом верхнего уровня RISC_V.v код которого приведен в листинге 10.8.

    module RISC_V(
        input clk,        // Основной тактовый сигнал
        input reset       // Сигнал сброса для процессора
    );
    
     
        wire [31:0] pc;           // Счетчик команд (текущий адрес инструкции)
        wire [31:0] instr;        // Инструкция, извлеченная из памяти команд
        wire [31:0] readData1;    // Данные из первого регистра
        wire [31:0] readData2;    // Данные из второго регистра
        wire [31:0] aluResult;    // Результат вычислений АЛУ
        wire [31:0] imm;          // непосредственные значения из памяти
        wire memWrite;            // Сигнал записи данных в память данных
        wire memRead;             // Сигнал чтения данных из памяти данных
        wire regWrite;            // Сигнал записи в регистровый файл
        wire branchTaken;         // Флаг, указывающий на необходимость условного перехода
        wire jump;                // Флаг, указывающий на выполнение безусловноного
        wire [3:0] aluControl;    // Сигналы микроопераций для АЛУ
        wire [6:0] opcode;        // opcode извлеченной инструкции
        wire [4:0] rd, rs1, rs2;  // Регистр назначения и регистры-источники
        wire [2:0] funct3;        // Поле funct3
        wire [6:0] funct7;        // Поле funct3
    
     
        
        // Модуль счетчика команд (Program Counter)
        
        PCUnit pc_unit (
            .clk(clk),               // Синхровход
            .reset(reset),           // Сигнал сброса
            .branchTaken(branchTaken), //Указание на ветвление
            .branchAddr(aluResult),  // Адрес перехода при ветвлении
            .pc(pc)                  // Текущий адрес инструкции
        );
    
        // Модуль памяти инструкций (Instruction Memory)
        
        InstructionMemory instr_mem (
            .addr(pc),               // Вход адреса текущей инструкции
            .instr(instr)            // код инструкции
        );
    
        // Дешифратор инструкции (Instruction Decoder)
       
        InstructionDecoder instr_decoder (
            .instr(instr),           // На вход извлеченная инструкция
            .opcode(opcode),         // выход содержащий Opcode
            .rd(rd),                 // Выход, указывающий адрес регистра назначения (Rd)
            .funct3(funct3),         // Выход funct3 
            .rs1(rs1),               // Выход, указывающий адрес исходного регистра 1 (Rs1)
            .rs2(rs2),               // Выход, указывающий адрес исходного регистра 1 (Rs1)
            .funct7(funct7),         // Выход funct7 
            .imm(imm)                // непосредственное (немедленное значение (Immediate))
        );
    
        // Регистровый файл (Register File)
       
        RegisterFile reg_file (
            .clk(clk),               // Синхровход
            .reset(reset),           // Сигнал сброса
            .regWrite(regWrite),     // Сигнал управления записью в регистры
            .rs1(rs1),               // Вход адреса регистра Rs1 для чтения
            .rs2(rs2),               // Вход адреса регистра Rs2 для чтения
            .rd(rd),                 // Вход адреса регистра Rd для записи
            .writeData(aluResult),   // Шина данных для записи в регистр
            .readData1(readData1),   // Шина данных выхода из Rs1
            .readData2(readData2)    // Шина данных выхода из Rs2
        );
    
        // Модуль управления (Control Unit)
        
        ControlUnit control_unit (
            .opcode(opcode),         // Вход для поля opcode из инструкции
            .aluOp(aluOp),           // Выход - код для дешифратора АЛУ
            .memWrite(memWrite),     // Выходной сигнал управления записью в память
            .memRead(memRead),       // Выходной сигнал управления чтением из памяти
            .branch(branch),         // Выходной сигнал указывающий на ветвление
            .regWrite(regWrite),     // Выходной сигнал управления записью в регистры
            .jump(jump)              // Выходной сигнал безусловного перехода
        );
    
        // Дешифратор операций АЛУ (ALU Decoder)
       
        ALUDecoder alu_decoder (
            .funct7(funct7),         // Входное поле Funct7
            .funct3(funct3),         // Входное поле Funct3
            .aluOp(aluOp),           // Вход кода от ControlUnit
            .aluControl(aluControl)  // выход кода микрооперации
        );
    
        // Арифметико-логическое устройство (АЛУ)
       
        ALU alu (
            .srcA(readData1),        // Вход первый операнд (Rs1)
            .srcB(                  // Вход второй операнд
                (opcode == 7'b0000011 || opcode == 7'b0100011) 
                ? readData2 : imm    // Используется Immediate для операций загрузки/сохранения
            ),
            .aluControl(aluControl), // код микроопрации
            .aluResult(aluResult)    // Результат работы АЛУ
        );
    
        // Оперативная память данных (Data Memory)
        
        DataMemory data_memory (
            .clk(clk),               // Синхровход
            .addr(aluResult),        // Адрес памяти (выход результата АЛУ)
            .writeData(readData2),   // Данные для записи в память
            .memWrite(memWrite),     // Сигнал управления записи в память
            .memRead(memRead),       // Сигнал управления чтения из памяти
            .readData(readData)      // Данные, считанные из памяти
        );
    
        // Модуль ветвления (Branch Unit)
            BranchUnit branch_unit (
            .srcA(readData1),        // Первый операнд для сравнения
            .srcB(readData2),        // Второй операнд для сравнения
            .funct3(funct3),         // Тип сравнения (BEQ, BNE и т.д.)
            .branchTaken(branchTaken)// Флаг наличия перехода
        );
    
    endmodule
    

    На рисунке 10.35 представлен RTL вид процессора.

    (рис 10.35)

    Литература

  • С. Харрис, Д. Харрис. Цифровая схемотехника и архитектура компьютера RISC-V/ пер. с англ. В.С. Яценкова, А.Ю. Романова; под. ред. А.Ю. Романова.-М.: ДМК Пресс, 2021. - 810 с.: ил.
  • СКВОЗНОЙ ЛАБОРАТОРНЫЙ ПРАКТИКУМ ПО ТЕХНОЛОГИЯМ RISC-V
  • Курс "Основы программирования на языке Assembler под архитектуру RISC-V"
  • Страницы:

    Практическая работа 10. Проектирование дешифратора инструкций. Блока управления. Сборка процессора

    Необходимые материалы для лекции asm+hex.zip, Logisim-10.zip и Verilog-10.zip.

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

    Рассмотрим построение дешифратора команд на примере сложения непосредственным операндом: addi x1, x2, 5 , где x1- регистр результата, x2 - слагаемое, 5 - непосредственное слагаемое. Если разложить данную инструкцию на микрокоманды управления, то их последовательность будет иметь вид:

  • Выставить адрес x2 на шине адреса операнда rs1 регистрового файла;
  • Коммутировать часть содержащую непосредственные данные на линию второго операнда АЛУ;
  • Коммутировать данные с выхода АЛУ на вход данных регистрового файла;
  • Включить запись в регистровый файл по адресу регистра x1.
  • В однотактном процессоре данная операция должна быть совершена за один такт. Задача дешифратора максимально детализировать сигналы, применяемые для активации соответствующих блоков. При этом в период дешифрации инструкций могут быть получены дополнительные признаки: знаковая / беззнаковая операция, наличие переходов.

    Графически операция сложения с непосредственным операндом выглядит как показано на рисунке 10.1 (IG - блок непосредственных значений).

    (рис 10.1)

    Для проектирования дешифратора необходимо коды инструкций, применяемые в процессоре, собрать в единую таблицу (таблица 10.1). Для проектирования будут использованы столбцы opcode и opcode HEX.

    Формат инструкции Мнемоника инструкции Opcode Opcode HEX Funct3
    R-style add 0110011 0x33 000
    sub 0110011 0x33 000
    sll 0110011 0x33 001
    xor 0110011 0x33 100
    srl 0110011 0x33 101
    sra 0110011 0x33 101
    or 0110011 0x33 110
    and 0110011 0x33 111
    I-type lb 0000011 0x3 000
    lh 0000011 0x3 001
    lw 0000011 0x3 010
    ld 0000011 0x3 011
    lbu 0000011 0x3 100
    lhu 0000011 0x3 101
    lwu 0000011 0x3 110
    addi 0010011 0x13 000
    slli 0010011 0x13 001
    xori 0010011 0x13 100
    srli 0010011 0x13 101
    srai 0010011 0x13 101
    ori 0010011 0x13 110
    andi 0010011 0x13 111
    jalr 1100111 0x67 000
    S-type sb 0100011 0x23 000
    sh 0100011 0x23 001
    sw 0100011 0x23 010
    sd 0100011 0x23 111
    SB-type beq 1100011 0x63 000
    bne 1100011 0x63 001
    blt 1100011 0x63 100
    bge 1100011 0x63 101
    bltu 1100011 0x63 110
    bgeu 1100011 0x63 111
    u-type lui 0110111 0x37 Не определен
    Uj-type jal 1101111 0x6f Не определен

    Так как в данном случае дешифратор должен анализировать входную комбинацию и формировать выходной сигнал, то можно смоделировать его схему по принципу проверки равенства постоянного значения opcode входной комбинации. Для этого используется элемент компаратор. Opcode будут константами. Ширина поля opcode составляет 7 бит.

    Создадим новую схему в проекте cpu.circ. Имя присвоим instdec. Разместим на поле 8 компараторов c разрядностью 7 бит (формат числа - беззнаковый) для форматов инструкций: R,S, I (отдельно для Load), а также выпадающих из форматов инструкций: jalr, jal, lui, auipc, beq.

    Объединим все входы первых операндов в общую шину и подключим к элементу тоннель. Метке тоннеля задать свойство Inst6_0, а его разрядность определить равной 7. Вторые входы компараторов подключим к константам, соответствующим полю opcode: 33,03,23,63,13,67,6f,37,17. Выходы равенства подключим к элементам выход. Меткам выходов присвоить соответствующие значения: R_format, Load, Store, Branch, I_type, Jalr, Jal, Lui, Auipc. Схема функциональная дешифратора приведена на рисунке 10.2.

    (рис 10.2)

    Следующим этапом необходимо обратиться к формату инструкций, так как в них, в зависимости от opcode, могут быть закодированы разные функциональные сигналы в одних и тех же позициях. Поле, начиная с 7 адреса может содержать адреса регистров либо непосредственные значения. Для переопределения данных, поступающих в теле инструкций, применяется блок, именуемый Immediate Generator - блок непосредственных значений. При проектировании данного блока необходимо воспользоваться описанием полей для различного формата команд (рисунок 10.3).

    (рис 10.3)

    Создадим новую схему, присвоим ей наименование IMMGEN. Фактически схема будет представлять собой сплиттер (разветвитель) сигналов шины и сборщик сигналов в шину.

    Разместим на поле вход с разрядностью равной 32. Свойство метки зададим instr. Подключим к нему разветвитель с настройками:

    Веерный выход - 9;

    Разрядность входа - 32;

    "Внешний вид" - праворукий;

  • Бит 0 - 6 задать настройки (0, сверху);
  • Бит 7 - 1;
  • Бит 8-11 - 2;
  • Бит 12-14 - 3;
  • Бит 15-19 - 4;
  • Бит 20 - 5;
  • Бит 21-24 - 6;
  • Бит 25-30 - 7;
  • Бит 31 - 8.
  • К выходу 6-0 подключим элемент "Не подключайтесь" в разделе проводка (задать разрядность 7). Полученный разветвитель приведен на рисунке 10.4.

    (рис 10.4)

    Вывод 31 разветвителя подключить к элементу расширитель битов со свойствами:

  • разрядность входа - 1;
  • разрядность выхода - 21;
  • тип расширителя - знаковый.
  • Для сборки разделенных линий в шину будем также использовать разветвители. На рисунке 10.5 приведена схема корреспонденции линий для получения непосредственных значений I формата инструкций (для каждой инструкции будет показано отдельное включение).

    (рис 10.5)

    На рисунке 10.6 приведена схема корреспонденции для S формата инструкций.

    (рис 10.6)

    На рисунке 10.7 приведена схема корреспонденции для B формата инструкций.

    (рис 10.7)

    На рисунке 10.8 приведена схема корреспонденции для U и J формата инструкций. На рисунке 10.9 приведена схема функциональная собранного блока.

    (рис 10.8) (рис 10.9)

    Далее следует добавить полученный IMMGEN в ранее спроектированный блок дешифратора instdec. Для этого необходимо разместить на поле вход с разрядностью 32. Присвоить метке входа значение Instruct. Соединить шиной (можно тоннелем) со входом inst блока IMMGEN. Также к шине подключить разветвитель разрядностью 7 бит и количеством веерных выходов равным 1. Для битов с 0 по 6 сверху задать 0. Остальным присвоить выходам присвоить значение "нет". К веерному выходу подключить тоннель и присвоить свойству метки значение Inst6_0. Далее добавить на поле четыре двухвходовых мультиплексора с разрядностью 32. Свойству "Положение выбирающего входа" мультиплексоров задать "Сверху/справа".

    Соединить мультиплексоры последовательно каскадом. Управляющий вход первого мультиплексора подключить к тоннелю с меткой S. Управляющий вход второго мультиплексора подключить к тоннелю с меткой B. Управляющий вход третьего мультиплексора подключить к тоннелю с меткой U, а четвертого к тоннелю с меткой J. Выход с последнего мультиплексора каскада подключить к выходу с именем imm. На рисунке 10.10 показан результат построения схемы на данном этапе.

    (рис 10.10)

    Далее необходимо в дешифраторе к линии Store подключить тоннель с меткой S, к линии Branch тоннель B, а к Jal соответственно J. Линии lui и auipc объединить через двухвходовой элемент "ИЛИ", а его выход подключить к тоннелю U (рисунок 10.11).

    (рис 10.11)

    Следующим шагом дополнить шину Instruct сплиттерами для инструкций R, I формата.

    Для этого продлим данную шину и подключим к разветвителю со следующими параметрами:

  • "Веерный выход" - 9;
  • "Биты данных" - 32;
  • "Внешний вид" -праворукий;
  • "Направление" - восток;
  • Бит 0-6 - 0, сверху;
  • Бит 7 - 1;
  • Бит 8 -11 - 2;
  • Бит 12-14 - 3;
  • Бит 15-19 - 4;
  • Бит 20 - 5;
  • Бит 21-24 - 6;
  • Бит 25-30 - 7;
  • Бит 31 - 8.
  • К выводам 0-6 и 31 подключить элементы "Не подключайтесь" соответствующей разрядности.

    К выводам 7 и 11-8 подключить разветвитель со свойствами:

  • "Веерный выход" - 2;
  • "Биты данных" - 5;
  • "Внешний вид" -праворукий;
  • "Направление" - запад;
  • Бит 0 - 0, сверху;
  • Бит 1-4 - 1, снизу.
  • К выходу данного разветвителя подключим выход с меткой rd. К выводам 14-12 подключим выход с меткой funct3. К выводам 19-15 подключим выход с меткой rs1. К выводам 20 и 24-21 подключим разветвитель, аналогичный для порта rd. Имя метки зададим rs2.

    К выводам 25-30 подключим разветвитель со свойствами:

  • "Веерный выход" - 2;
  • "Биты данных" - 6;
  • "Внешний вид" -праворукий;
  • "Направление" - восток;
  • Бит 0-4 - 0, 0,сверху;
  • Бит 5 - 1, снизу.
  • К выводам 0-4 следует подключить элемент "Не подключайтесь". К выводу 5 подключить выход с именем метки funct7. На рисунке 10.12 приведена спроектированного сплиттера. На рисунке 10.13 приведена полная функциональная схема, проектируемого блока процессора.

    (рис 10.12) (рис 10.13)

    Используя ресурс http://riscvasm.lucasteske.dev/#, можно выполнить тестирование работы блока. При вводе инструкций на ассемблере в поле ввода будет сформирован машинный код. Используя машинный код, выполним проверку работы спроектированного блока. Для этого необходимо вводить код в поле Instruct и проверять сигналы и данные на соответствующих выходах.

    Примеры тестовых инструкций и соответствующий машинный код:

  • addi x2,x1,5# машинный код 00508113;
  • add x3, x1, x8# машинный код 008081b3;
  • lui x15,0x6# машинный код 000067b7.
  • Самостоятельно:

    Провести тестирование с максимально возможным количеством инструкций для различных форматов инструкций.

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

    (рис 10.14)

    Полученный блок следует разместить на главной схеме. Выход блока rs1 подключить к r1sel регистрового файла. Выход rs2 подключить к r2sel. Выход rd подключить к RD регистрового файла. Выход imm блока Decode Imm Gen подключить к входу 1 двухвходового 32-х разрядного мультиплексора. Вход 0 мультиплексора подключить к выходу rs2 регистрового файла. Выход мультиплексора подключить к входу dataB АЛУ (рисунок 10.15).

    (рис 10.15)

    Выход rs1 регистрового файла подключить к входу 0 двухвходового 32-х разрядного мультиплексора. Вход 1 мультиплексора подключить к выходу IMEM-addr счетчика инструкций (рисунок 10.16).

    (рис 10.16)

    Далее необходимо разработать схему управления АЛУ. При проектировании блока АЛУ было создано 4-х разрядное поле микроопераций. Если провести сравнение с форматом инструкций для арифметико-логического устройства, то можно определить, что микрооперация определяется как состояние пятого бита поля funct 7 и кода в поле funct3. Из анализа инструкций можно сделать вывод, что funct 7 составная часть поля микрокоманды операций АЛУ - старший бит.

    Создадим новую схему и присвоим имя aludec. На рисунке 10.17 приведена схема функциональная дешифратора инструкций. Следующим шагом произведем начертание УГО элемента как показано на рисунке 10.18

    (рис 10.17) (рис 10.18)

    Добавить созданный элемент на главную схему. Выходы funct3 и funct7 блока дешифратора инструкций соединить с аналогичными входами блока дешифратора АЛУ.

    Выходы Jal и Jalr дешифратора инструкций объединить двухвходовым элементом "ИЛИ". Выход с данного элемента вывести на вход jump блока ALU dec. Вход alu_enable подключить к выходу двухвходового элемента "ИЛИ" входы которого подключить к выходам R_format и I_type дешифратора инструкций. На рисунке 10.19 показана схема включения ALU dec.

    (рис 10.19)

    Вход мультиплексора управления записью в порт dataB АЛУ подключить к выходу 5-ти входового элемента "ИЛИ". Входы данного элемента следует подключить к выходным сигналам дешифратора инструкций: I_type, Load, Store, Jal. Выходы Lui и Auipc дешифратора инструкций объединить двухвходовым элементом "ИЛИ" и подключить к пятому входу "ИЛИ".

    Выход Load дешифратора инструкций подключить к тоннелю с меткой L, также к тоннелю с данной меткой подключить вход OE оперативной памяти. Входы синхронизации блоков процессора подключить к тоннелю с меткой "CLK". Вход памяти WE подключить к линии, идущей к выходу Store дешифратора инструкций.

    Добавить на схему двухвходовой 32-х разрядный мультиплексор. К входу 0 подключить выход rs1 регистрового файла. К входу 1 подключить выход Imem_addr счётчика инструкций PC. Вход управления данным мультиплексором подключить к выходу, предварительно добавленного двухвходового элемента "ИЛИ". Входы данного логического элемента подключить к выходам элемента "ИЛИ", объединяющего выходы Lui и Auipc дешифратора инструкций, и к выходу Jal данного блока.

    Вход управления WE регистрового файла подключить к выходу предварительно добавленного пятивходового элемента "ИЛИ". Входы данного элемента необходимо подключить к выходам дешифратора: R_format, I_type, Load. Также два оставшихся входа подключить к выходам элементов "ИЛИ", объединяющих Lui и Auipc, а также Jal и Jalr. На рисунке 10.20 приведена схема организации управления входом WE регистрового файла.

    (рис 10.20)

    Блок счетчика инструкций дополнить сумматором и двумя мультиплексорами. Схема включения приведена на рисунке 10.21. Выход счетчика PC_4 соединить с входом 0 младшего мультиплексора. Выход данного мультиплексора соединить с входом 0 старшего мультиплексора. Выход старшего мультиплексора подключить к входу PC_in счетчика. Вход 1 младшего мультиплексора подключить к выходу сумматора. Один из входов сумматора подключить к выходу Imm дешифратора инструкций. Второй вход сумматора подключить к выходу счетчика Imem_addr.

    (рис 10.21)

    Вход 1 старшего мультиплексора соединить с шиной, выходящей из мультиплексора оперативной памяти, но его необходимо соединить через разветвитель, где нулевой бит должен быть подключён к константе лог. "0". Остальные разряды берутся из шины (рисунок 10.22).

    (рис 10.22)

    Вход управления старшего мультиплексора необходимо подключить к выходу элемента "ИЛИ", объединяющего выходы Jal и Jalrc дешифратора инструкций. Вход управления младшего мультиплексора следует подключить к предварительно добавленному двухвходовому элементу "И". Данный вход организуется с применением переходов Branch, рассмотренных ниже.

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

    Для выполнения переходов необходимо создать дополнительный блок. Исходя из таблицы 10.1, данный блок активируется при наличии сигнала branch =63h и анализирует состояние поля funct3. Для реализации данного блока создадим новую схему. Наименование ей присвоим Branc_unit. В данную схему добавить два компаратора с разрядностью 32. Один со свойством "Формат числа" - "Дополнительный код". Второй со свойством "Формат числа" - "Беззнаковое". Далее на основании кодовой комбинации funct3, приведенной в таблице 10.2 произведём коммутацию выходов компараторов. Для этого добавим в схему мультиплексор со следующими параметрами:

  • Выбирающие биты -3;
  • Биты данных - 1;
  • Разрешающий вход - нет.
  • Выход мультиплексора подключим к выходу с меткой "Br".

    Команда перехода Вид перехода Код funct3
    BEQ при равенстве двух регистров 000
    BNE если два регистра не равны 001
    BLT если первый регистр меньше второго 100
    BGE если первый регистр больше второго 101
    BLTU если первый регистр меньше второго (беззнаковое сравнение) 110
    BGEU если первый регистр больше второго (беззнаковое сравнение) 111

    На рисунке 10.23 приведена схема функциональная блока условных переходов. На рисунке 10.24 ее графическое обозначение.

    (рис 10.23) (рис 10.24)

    Созданный модуль должен быть объединен с выходом Branch блока управления. На рисунке 10.25 приведена схема управления процессора с использованием Branch_unit,

    (рис 10.25)

    На рисунке 10.26 приведена итоговая функциональная схема процессора.

    (рис 10.26)

    Верификация созданного процессора

    При написании программ для тестирования созданного процессора, потребуется их загрузка в память ROM процессора. Используя средства компиляции, например, приведенный выше сайт онлайн компилятора, подготавливается HEX файл. В любом поддерживаемом формате. Далее необходимо щелкнуть правой кнопкой мыши по ROM памяти. В ниспадающем меню выбрать "загрузить образ".

    В открывшемся диалоговом окне, выбрать файл с расширением .hex. Откроется диалоговое окно настроек загружаемого файла (рисунок 10.27). Необходимо установить активной метку v.3.0 hex и убедится, что в поле Decoded данные соответствуют созданному ранее файлу (можно проверить, выбрав поле Original). В подвале данного диалога выводится сообщение о наличии ошибок в загружаемом файле.

    (рис 10.27)

    Написанию программ на языке ассемблера для процессоров RISC-V, посвящен ряд курсов Альянса RISC-V, также дается описание структуры программ на ассемблере в [1].

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

    Тестирование процессора на выполнение циклов. Для этого будет разработана программа представленная в листинге 10.1.

    .global _boot
    .text 
    _boot:                    
     .global _start
            .data #Секция глобальных данных
            .text #Секция кода
    _start: #Метка начала программы
            li      x5, 1  # Первое слагаемое
            li      x6, 1  # Второе слагаемое
            li      x8, 10 # Максимальное значение в цикле
            li      x9, 1  # Значение уменьшения счетчика цикла
    loop: # Метка цикла
            add     x6, x5, x6   # Складываем два значения и помещаем в x6
            sub     x8, x8, x9    # Уменьшаем счетчик на 1 
            bne     x8, x0, loop   # Если x8 не равен 0 продолжаем цикл
            # Завершение программы 
            li      x17, 10      # В данном случае применяется вызов в операционную систему
            ecall  
    

    HEX файл для ROM памяти приведен ниже:

    0000: 00100293
    0001: 00100313
    0002: 00a00413
    0003: 00100493
    0004: 00628333
    0005: 40940433
    0006: fe041ce3
    0007: 00a00893
    0008: 00000073
    0009: 00000000
    

    Второй тестовой программой для созданного процессора является вычисление чисел Фибоначчи для первых 10 чисел. Предлагается выполнить ее с применением двух видов алгоритмов: линейном и в цикле. В листинге 10.2 приведен код на языке ассемблера для линейного алгоритма.

    .global _boot
    .text
    _boot:                    
        addi x1, x0, 0  # очистка x1
        sw  x1, 1(x1)  # сохранение в памяти cо смещением 1
        addi x1, x0, 1 # запись в х1 1 
        sw  x1, 4(x1)  # сохранение в памяти со смещением 4
        add x2, x0, x1  # запись в x2 лог. "1"
        sw  x2, 8(x1)   # сохранение в памяти со смещением 8 
        add x3, x1, x2  #сложение х1 и х2 и запись в x3 далее аналогично
        sw  x3, 12(x1) 
        add x4, x2, x3   
        sw  x4, 16(x1)
        add x5, x3, x4   
        sw  x5, 20(x1)
        add x6, x4, x5   
        sw  x6, 24(x1)
        add x7, x5, x6  
        sw  x7, 28(x1)
        add x8, x6, x7   
        sw  x8, 32(x1)
        add x9, x7, x8   # в результате данной операции в х9 сохранено число 
        sw  x9, 36(x1) # в результате данной операции в памяти сохранено число 
    end:
        addi x10, x0, 0
        ecall
    

    HEX файл для ROM памяти приведен ниже:

    v3.0 hex words addressed
    0000: 00000093 0010a0a3 00100093 0010a223 00100133 0020a423 002081b3 0030a623
    0008: 00310233 0040a823 004182b3 0050aa23 00520333 0060ac23 006283b3 0070ae23
    0010: 00730433  0280a023 008384b3 0290a223 00000513 00000073 00000000 00000000 
    

    Решение задачи для вычисления чисел Фибоначчи в цикле приведено в листинге 10.3.

    .global _start
            .data
            .text
    _start:
            li      x5, 1               # Первое число Фибоначчи (F(1))
            li      x6, 1               # Второе число Фибоначчи (F(2))
            li      x7, 0x0000010 # Адрес для хранения чисел Фибоначчи
            li      x8, 10  
            li      x9, 2  # Значение уменьшения цикла
    
    loop:
            sw      x5, 0(x7)           # Сохраняем F(n-1) по адресу t2
            sw      x6, 4(x7)           # Сохраняем F(n) по адресу t2 + 4
            add     x7, x5, x6           # x7 = F(n-1) + F(n)
            add     x5, x5, x6           # Обновляем F(n-1) на F(n)
            add     x6, x6, x7           # Обновляем F(n) на F(n-1) + F(n)
            addi    x7, x7, 8            # Увеличиваем адрес для следующей пары чисел
            sub     x8, x8, x9           # Уменьшаем счетчик на 2 (так как сохраняем 2 числа)
            bne     x8, x0, loop        # Если x8 не ноль, продолжаем цикл
    
            # Завершение программы (если требуется, добавить код завершения)
            li      x10, 10               # Код системного вызова для завершения программы для реального #микропроцессор
            ecall
    

    Содержимое файла ROM памяти приведено ниже

    v3.0 hex words addressed
    0000: 00000093 0010a0a3 00100093 0010a223 00100133 0020a423 002081b3 0030a623
    0008: 00310233 0040a823 004182b3 0050aa23 00520333 0060ac23 006283b3 0070ae23
    0010: 00730433 0280a023 008384b3 0290a223 00000513 00000073 00000000 00000000
    

    На рисунке 10.28 приведен итог вычисления числа Фибоначчи. Он должен соответствовать результату вычисления в линейном алгоритме.

    (рис 10.28)

    Для завершения проектирования процессора на языке Verilog HDL необходимо вернуться в среду в ранее созданный проект RISC_V. Реализуем блок переходов BranchUnit. Как видно из рисунка 10.23 в его основе лежат компараторы, выполняющие сравнение двух 32-х битных кодов. В зависимости от кода на шине funct3 коммутирующие результирующий сигнал сравнения на выход. На языке Verilog HDL данная схема может быть реализована так как показано в листинге 10.4 (файл BranchUnit.v)

    module BranchUnit(
        input [31:0] srcA, srcB, //входы данных с регистров
        input [2:0] funct3, // вход funct3
        output reg branchTaken //выход сигнализирующий о наличии перехода
    );
        always @(*) begin
            case (funct3)
                3'b000: branchTaken = (srcA == srcB); // BEQ
                3'b001: branchTaken = (srcA != srcB); // BNE
                3'b100: branchTaken = ($signed(srcA) < $signed(srcB)); // BLT
                3'b101: branchTaken = ($signed(srcA) >= $signed(srcB)); // BGE
                3'b110: branchTaken = (srcA < srcB); // BLTU
                3'b111: branchTaken = (srcA >= srcB); // BGEU
                default: branchTaken = 1'b0; // по умолчанию выход 0 
            endcase
        end
    endmodule
    

    На рисунке 10.29 приведен вид RTL созданного модуля. Развернув его (рисунок 10.30) , можно убедится в эквивалентности схемы, приведенной на рисунке 10.23.

    (рис 10.29) (рис 10.30)

    Реализуем модуля дешифратора операций АЛУ. Для этого создадим файл ALUDecoder.v. В листинге 10.5 приведен код данного модуля. Анализируя поля funct7, funct3 инструкции RISC-V, а также поле aluOp формируемое блоком ControlUnit данный дешифратор осуществляет формирование кода микрооперации в АЛУ.

    module ALUDecoder(
        input [6:0] funct7,
        input [2:0] funct3,
        input [1:0] aluOp,//код операции
        output reg [3:0] aluControl
    );
        always @(*) begin
            case (aluOp)
                2'b00: aluControl = 4'b0000; // операции загрузка/сохранение (ADD)
                2'b01: aluControl = 4'b0001; // операция ветвления (SUB)
                2'b10: begin // R-type инструкции
                    case ({funct7, funct3})
                        10'b0000000000: aluControl = 4'b0000; // ADD
                        10'b0100000000: aluControl = 4'b0001; // SUB
                        10'b0000000111: aluControl = 4'b0010; // AND
                        10'b0000000110: aluControl = 4'b0011; // OR
                        10'b0000000100: aluControl = 4'b0100; // XOR
                        10'b0000000001: aluControl = 4'b0101; // SLL
                        10'b0000000101: aluControl = 4'b0110; // SRL
                        10'b0100000101: aluControl = 4'b0111; // SRA
                        10'b0000000010: aluControl = 4'b1000; // SLT
                        10'b0000000011: aluControl = 4'b1001; // SLTU
                        default: aluControl = 4'b0000;
                    endcase
                end
                default: aluControl = 4'b0000;
            endcase
        end
    endmodule
    

    На рисунке 10.31 приведен вид RTL созданного модуля.

    (рис 10.31)

    Следующим этапом выполним построение "сплиттера" кода инструкции - дешифратора инструкции (схемотехническая реализация разборки кода инструкции рассмотрена нами в начале данного раздела). Создадим в проекте RISC_V новый файл с именем InstructionDecoder.v. Внесем в него код из листинга 10.6.

    module InstructionDecoder(
        input [31:0] instr,
        output [6:0] opcode,
        output [4:0] rd,
        output [2:0] funct3,
        output [4:0] rs1, rs2,
        output [6:0] funct7,
        output [31:0] imm
    );
        assign opcode = instr[6:0];// выделяем 0 по 6 бит под opcode
        assign rd = instr[11:7]; //с 7 по 11 бит адрес регистра назначения
        assign funct3 = instr[14:12]; //с 12 по 14 бит поле funct3
        assign rs1 = instr[19:15];//с 15 по 19 бит адрес регистра источника 1
        assign rs2 = instr[24:20];//с 20 по 24 бит адрес регистра источника 2
        assign funct7 = instr[31:25];
        assign imm = { {21{instr[31]}}, instr[30:20] }; //формируемое поле //непосредственных значений Immediate (I-Type)
    endmodule
    

    На рисунке 10.32 приведен вид RTL созданного модуля.

    (рис 10.32)

    Если раскрыть модуль InstructionDecoder, то можно наблюдать разветвление эквивалентное разветвителю, приведенному на рисунке 10.12 (рисунок 10.33).

    (рис 10.33)

    Последним функциональным модулем процессора выступит модуль ControlUnit. Данный модуль формирует сигналы управления в зависимости от поля opcode инструкции.

    Для описания данного модуля создадим файл ControlUnit.v и введем код из листинга 10.7.

    module ControlUnit(
        input [6:0] opcode,
        output reg [1:0] aluOp,
        output reg memWrite,
        output reg memRead,
        output reg branch,
        output reg regWrite,
        output reg jump // переход операций JAL и JALR
    );
        always @(*) begin
            case (opcode)
                7'b0000011: begin // Load (I-type)
                    aluOp = 2'b00;
                    memWrite = 0;
                    memRead = 1;
                    branch = 0;
                    regWrite = 1;
                    jump = 0;
                end
                7'b0100011: begin // Store (S-type)
                    aluOp = 2'b00;
                    memWrite = 1;
                    memRead = 0;
                    branch = 0;
                    regWrite = 0;
                    jump = 0;
                end
                7'b1100011: begin // Branch (B-type)
                    aluOp = 2'b01;
                    memWrite = 0;
                    memRead = 0;
                    branch = 1;
                    regWrite = 0;
                    jump = 0;
                end
                7'b0110011: begin // R-type
                    aluOp = 2'b10;
                    memWrite = 0;
                    memRead = 0;
                    branch = 0;
                    regWrite = 1;
                    jump = 0;
                end
                7'b1100111: begin // JALR (I-type)
                    aluOp = 2'b00; // Используется для вычисления адреса (PC + imm)
                    memWrite = 0;
                    memRead = 0;
                    branch = 0;
                    regWrite = 1; // Результат записывается в регистр
                    jump = 1; // Указывает на команду перехода
                end
                7'b1101111: begin // JAL (UJ-type)
                    aluOp = 2'b00; 
                    memWrite = 0;
                    memRead = 0;
                    branch = 0;
                    regWrite = 1; // Результат записывается в регистр
                    jump = 1; // Указывает на команду перехода
                end
                7'b0110111: begin // LUI (U-type)
                    aluOp = 2'b11; 
                    memWrite = 0;
                    memRead = 0;
                    branch = 0;
                    regWrite = 1; // Загружаем значение в регистр
                    jump = 0;
                end
                default: begin
                    aluOp = 2'b00;
                    memWrite = 0;
                    memRead = 0;
                    branch = 0;
                    regWrite = 0;
                    jump = 0;
                end
            endcase
        end
    endmodule
    

    На рисунке 10.34 приведен вид RTL созданного модуля.

    (рис 10.34)

    Все модули объединяются файлом верхнего уровня RISC_V.v код которого приведен в листинге 10.8.

    module RISC_V(
        input clk,        // Основной тактовый сигнал
        input reset       // Сигнал сброса для процессора
    );
    
     
        wire [31:0] pc;           // Счетчик команд (текущий адрес инструкции)
        wire [31:0] instr;        // Инструкция, извлеченная из памяти команд
        wire [31:0] readData1;    // Данные из первого регистра
        wire [31:0] readData2;    // Данные из второго регистра
        wire [31:0] aluResult;    // Результат вычислений АЛУ
        wire [31:0] imm;          // непосредственные значения из памяти
        wire memWrite;            // Сигнал записи данных в память данных
        wire memRead;             // Сигнал чтения данных из памяти данных
        wire regWrite;            // Сигнал записи в регистровый файл
        wire branchTaken;         // Флаг, указывающий на необходимость условного перехода
        wire jump;                // Флаг, указывающий на выполнение безусловноного
        wire [3:0] aluControl;    // Сигналы микроопераций для АЛУ
        wire [6:0] opcode;        // opcode извлеченной инструкции
        wire [4:0] rd, rs1, rs2;  // Регистр назначения и регистры-источники
        wire [2:0] funct3;        // Поле funct3
        wire [6:0] funct7;        // Поле funct3
    
     
        
        // Модуль счетчика команд (Program Counter)
        
        PCUnit pc_unit (
            .clk(clk),               // Синхровход
            .reset(reset),           // Сигнал сброса
            .branchTaken(branchTaken), //Указание на ветвление
            .branchAddr(aluResult),  // Адрес перехода при ветвлении
            .pc(pc)                  // Текущий адрес инструкции
        );
    
        // Модуль памяти инструкций (Instruction Memory)
        
        InstructionMemory instr_mem (
            .addr(pc),               // Вход адреса текущей инструкции
            .instr(instr)            // код инструкции
        );
    
        // Дешифратор инструкции (Instruction Decoder)
       
        InstructionDecoder instr_decoder (
            .instr(instr),           // На вход извлеченная инструкция
            .opcode(opcode),         // выход содержащий Opcode
            .rd(rd),                 // Выход, указывающий адрес регистра назначения (Rd)
            .funct3(funct3),         // Выход funct3 
            .rs1(rs1),               // Выход, указывающий адрес исходного регистра 1 (Rs1)
            .rs2(rs2),               // Выход, указывающий адрес исходного регистра 1 (Rs1)
            .funct7(funct7),         // Выход funct7 
            .imm(imm)                // непосредственное (немедленное значение (Immediate))
        );
    
        // Регистровый файл (Register File)
       
        RegisterFile reg_file (
            .clk(clk),               // Синхровход
            .reset(reset),           // Сигнал сброса
            .regWrite(regWrite),     // Сигнал управления записью в регистры
            .rs1(rs1),               // Вход адреса регистра Rs1 для чтения
            .rs2(rs2),               // Вход адреса регистра Rs2 для чтения
            .rd(rd),                 // Вход адреса регистра Rd для записи
            .writeData(aluResult),   // Шина данных для записи в регистр
            .readData1(readData1),   // Шина данных выхода из Rs1
            .readData2(readData2)    // Шина данных выхода из Rs2
        );
    
        // Модуль управления (Control Unit)
        
        ControlUnit control_unit (
            .opcode(opcode),         // Вход для поля opcode из инструкции
            .aluOp(aluOp),           // Выход - код для дешифратора АЛУ
            .memWrite(memWrite),     // Выходной сигнал управления записью в память
            .memRead(memRead),       // Выходной сигнал управления чтением из памяти
            .branch(branch),         // Выходной сигнал указывающий на ветвление
            .regWrite(regWrite),     // Выходной сигнал управления записью в регистры
            .jump(jump)              // Выходной сигнал безусловного перехода
        );
    
        // Дешифратор операций АЛУ (ALU Decoder)
       
        ALUDecoder alu_decoder (
            .funct7(funct7),         // Входное поле Funct7
            .funct3(funct3),         // Входное поле Funct3
            .aluOp(aluOp),           // Вход кода от ControlUnit
            .aluControl(aluControl)  // выход кода микрооперации
        );
    
        // Арифметико-логическое устройство (АЛУ)
       
        ALU alu (
            .srcA(readData1),        // Вход первый операнд (Rs1)
            .srcB(                  // Вход второй операнд
                (opcode == 7'b0000011 || opcode == 7'b0100011) 
                ? readData2 : imm    // Используется Immediate для операций загрузки/сохранения
            ),
            .aluControl(aluControl), // код микроопрации
            .aluResult(aluResult)    // Результат работы АЛУ
        );
    
        // Оперативная память данных (Data Memory)
        
        DataMemory data_memory (
            .clk(clk),               // Синхровход
            .addr(aluResult),        // Адрес памяти (выход результата АЛУ)
            .writeData(readData2),   // Данные для записи в память
            .memWrite(memWrite),     // Сигнал управления записи в память
            .memRead(memRead),       // Сигнал управления чтения из памяти
            .readData(readData)      // Данные, считанные из памяти
        );
    
        // Модуль ветвления (Branch Unit)
            BranchUnit branch_unit (
            .srcA(readData1),        // Первый операнд для сравнения
            .srcB(readData2),        // Второй операнд для сравнения
            .funct3(funct3),         // Тип сравнения (BEQ, BNE и т.д.)
            .branchTaken(branchTaken)// Флаг наличия перехода
        );
    
    endmodule
    

    На рисунке 10.35 представлен RTL вид процессора.

    (рис 10.35)

    Литература

  • С. Харрис, Д. Харрис. Цифровая схемотехника и архитектура компьютера RISC-V/ пер. с англ. В.С. Яценкова, А.Ю. Романова; под. ред. А.Ю. Романова.-М.: ДМК Пресс, 2021. - 810 с.: ил.
  • СКВОЗНОЙ ЛАБОРАТОРНЫЙ ПРАКТИКУМ ПО ТЕХНОЛОГИЯМ RISC-V
  • Курс "Основы программирования на языке Assembler под архитектуру RISC-V"
  • Вернуться к учебному плану