Необходимые материалы для лекции asm+hex.zip, Logisim-10.zip и Verilog-10.zip.
Несмотря на то, что современные RISC процессоры строятся таким образом, что в формате команды заложен код операции и адреса операндов, но может потребоваться дополнительная расшифровка этих самых операций. Для этого потребуется дешифратор команд. Дешифратор команд является конечным автоматом. При этом входным символом является код операции или opcode, а выходным символом будет являться результирующая строка, представляющая собой развёрнутую информацию о команде.
Рассмотрим построение дешифратора команд на примере сложения непосредственным операндом: addi x1, x2, 5 , где x1- регистр результата, x2 - слагаемое, 5 - непосредственное слагаемое. Если разложить данную инструкцию на микрокоманды управления, то их последовательность будет иметь вид:
В однотактном процессоре данная операция должна быть совершена за один такт. Задача дешифратора максимально детализировать сигналы, применяемые для активации соответствующих блоков. При этом в период дешифрации инструкций могут быть получены дополнительные признаки: знаковая / беззнаковая операция, наличие переходов.
Графически операция сложения с непосредственным операндом выглядит как показано на рисунке 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;
"Внешний вид" - праворукий;
К выходу 6-0 подключим элемент "Не подключайтесь" в разделе проводка (задать разрядность 7). Полученный разветвитель приведен на рисунке 10.4.
(рис 10.4)
Вывод 31 разветвителя подключить к элементу расширитель битов со свойствами:
Для сборки разделенных линий в шину будем также использовать разветвители. На рисунке 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 формата.
Для этого продлим данную шину и подключим к разветвителю со следующими параметрами:
К выводам 0-6 и 31 подключить элементы "Не подключайтесь" соответствующей разрядности.
К выводам 7 и 11-8 подключить разветвитель со свойствами:
К выходу данного разветвителя подключим выход с меткой rd. К выводам 14-12 подключим выход с меткой funct3. К выводам 19-15 подключим выход с меткой rs1. К выводам 20 и 24-21 подключим разветвитель, аналогичный для порта rd. Имя метки зададим rs2.
К выводам 25-30 подключим разветвитель со свойствами:
К выводам 0-4 следует подключить элемент "Не подключайтесь". К выводу 5 подключить выход с именем метки funct7. На рисунке 10.12 приведена спроектированного сплиттера. На рисунке 10.13 приведена полная функциональная схема, проектируемого блока процессора.
(рис 10.12)
(рис 10.13)
Используя ресурс http://riscvasm.lucasteske.dev/#, можно выполнить тестирование работы блока. При вводе инструкций на ассемблере в поле ввода будет сформирован машинный код. Используя машинный код, выполним проверку работы спроектированного блока. Для этого необходимо вводить код в поле Instruct и проверять сигналы и данные на соответствующих выходах.
Примеры тестовых инструкций и соответствующий машинный код:
Самостоятельно:
Провести тестирование с максимально возможным количеством инструкций для различных форматов инструкций.
Убедившись в работоспособности блока, реализуем его условно-графическое обозначение. При начертании блока необходимо перегруппировать выходы для упрощения дальнейшего проектирования. Условно-графическое обозначение приведено на рисунке 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 произведём коммутацию выходов компараторов. Для этого добавим в схему мультиплексор со следующими параметрами:
Выход мультиплексора подключим к выходу с меткой "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)
Необходимые материалы для лекции asm+hex.zip, Logisim-10.zip и Verilog-10.zip.
Несмотря на то, что современные RISC процессоры строятся таким образом, что в формате команды заложен код операции и адреса операндов, но может потребоваться дополнительная расшифровка этих самых операций. Для этого потребуется дешифратор команд. Дешифратор команд является конечным автоматом. При этом входным символом является код операции или opcode, а выходным символом будет являться результирующая строка, представляющая собой развёрнутую информацию о команде.
Рассмотрим построение дешифратора команд на примере сложения непосредственным операндом: addi x1, x2, 5 , где x1- регистр результата, x2 - слагаемое, 5 - непосредственное слагаемое. Если разложить данную инструкцию на микрокоманды управления, то их последовательность будет иметь вид:
В однотактном процессоре данная операция должна быть совершена за один такт. Задача дешифратора максимально детализировать сигналы, применяемые для активации соответствующих блоков. При этом в период дешифрации инструкций могут быть получены дополнительные признаки: знаковая / беззнаковая операция, наличие переходов.
Графически операция сложения с непосредственным операндом выглядит как показано на рисунке 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;
"Внешний вид" - праворукий;
К выходу 6-0 подключим элемент "Не подключайтесь" в разделе проводка (задать разрядность 7). Полученный разветвитель приведен на рисунке 10.4.
(рис 10.4)
Вывод 31 разветвителя подключить к элементу расширитель битов со свойствами:
Для сборки разделенных линий в шину будем также использовать разветвители. На рисунке 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 формата.
Для этого продлим данную шину и подключим к разветвителю со следующими параметрами:
К выводам 0-6 и 31 подключить элементы "Не подключайтесь" соответствующей разрядности.
К выводам 7 и 11-8 подключить разветвитель со свойствами:
К выходу данного разветвителя подключим выход с меткой rd. К выводам 14-12 подключим выход с меткой funct3. К выводам 19-15 подключим выход с меткой rs1. К выводам 20 и 24-21 подключим разветвитель, аналогичный для порта rd. Имя метки зададим rs2.
К выводам 25-30 подключим разветвитель со свойствами:
К выводам 0-4 следует подключить элемент "Не подключайтесь". К выводу 5 подключить выход с именем метки funct7. На рисунке 10.12 приведена спроектированного сплиттера. На рисунке 10.13 приведена полная функциональная схема, проектируемого блока процессора.
(рис 10.12)
(рис 10.13)
Используя ресурс http://riscvasm.lucasteske.dev/#, можно выполнить тестирование работы блока. При вводе инструкций на ассемблере в поле ввода будет сформирован машинный код. Используя машинный код, выполним проверку работы спроектированного блока. Для этого необходимо вводить код в поле Instruct и проверять сигналы и данные на соответствующих выходах.
Примеры тестовых инструкций и соответствующий машинный код:
Самостоятельно:
Провести тестирование с максимально возможным количеством инструкций для различных форматов инструкций.
Убедившись в работоспособности блока, реализуем его условно-графическое обозначение. При начертании блока необходимо перегруппировать выходы для упрощения дальнейшего проектирования. Условно-графическое обозначение приведено на рисунке 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 произведём коммутацию выходов компараторов. Для этого добавим в схему мультиплексор со следующими параметрами:
Выход мультиплексора подключим к выходу с меткой "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)
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.