Eng. Lab#2

This commit is contained in:
Andrei Solodovnikov
2026-04-13 10:31:30 +03:00
parent ca0ae579f3
commit 047d4e65a6

View File

@@ -1,73 +1,73 @@
# Лабораторная работа №2. Арифметико-логическое устройство
# Lab 2. Arithmetic Logic Unit
Так как основной задачей процессора является обработка цифровых данных, одним из его основных блоков является арифметико-логическое устройство (АЛУ). Задача АЛУ производить над входными данными арифметические и поразрядно логические операции.
Since the primary purpose of a processor is to process digital data, one of its core components is the arithmetic logic unit (ALU). The ALU performs arithmetic and bitwise logical operations on input data.
## Цель
## Goal
Используя навыки по описанию мультиплексоров, описать блок арифметико-логического устройства (АЛУ) на языке SystemVerilog.
Using multiplexer description skills, describe an arithmetic logic unit (ALU) module in SystemVerilog.
## Материалы для подготовки к лабораторной работе
## Preparation Materials
В дополнение к [материалам](../../Basic%20Verilog%20structures/), изученным в ходе предыдущей лабораторной работы, вам рекомендуется ознакомиться с:
In addition to the [materials](../../Basic%20Verilog%20structures/) covered in the previous lab, you are encouraged to review:
- способами описания [мультиплексора](../../Basic%20Verilog%20structures/Multiplexors.md) на языке SystemVerilog.
- how to describe a [multiplexer](../../Basic%20Verilog%20structures/Multiplexors.md) in SystemVerilog.
## Общий ход выполнения работы
## General Workflow
1. Изучить устройство и принцип работы АЛУ (раздел [#теория](#Теория))
2. Изучить языковые конструкции SystemVerilog для реализации АЛУ (раздел [#инструменты](#Инструменты))
3. Внимательно ознакомиться с заданием (раздел [#задание](#Задание))
4. Описать модуль АЛУ, проверить его предоставленным верификационным окружением.
5. Проверить работу АЛУ в ПЛИС.
1. Study the structure and operating principle of an ALU (see [#theory](#theory))
2. Study the SystemVerilog language constructs for implementing an ALU (see [#tools](#Tools))
3. Read the assignment carefully (see [#assignment](#Assignment))
4. Describe the ALU module and verify it using the provided verification environment.
5. Test the ALU on the FPGA.
## Теория
## Theory
**Арифметико-логическое устройство** (**АЛУ**, Arithmetic Logic Unit ALU) это блок процессора, выполняющий арифметические и поразрядно логические операции. Разница между арифметическими и логическими операциями в отсутствии у последних бита переноса, так как логические операции происходят между 1-битными числами и дают 1-битный результат, а в случае АЛУ (в рамках данной лабораторной работы) одновременно между 32-мя 1-битными парами чисел. В логических операциях результаты значений отдельных битов друг с другом никак не связаны.
An **arithmetic logic unit** (**ALU**) is a processor block that performs arithmetic and bitwise logical operations. The difference between arithmetic and logical operations is that logical operations have no carry bit — logical operations are performed between 1-bit numbers and produce a 1-bit result, whereas in the ALU (within the scope of this lab) they operate simultaneously on 32 pairs of 1-bit numbers. In logical operations, the result of individual bits are entirely independent of one another.
Также, кроме результата операций, АЛУ может формировать специальные сигналы-флаги, которые показывают выполняется ли заданное условие. Например, выводить `1`, если один операнд меньше другого, или если в результате суммы произошло переполнение.
In addition to the operation result, the ALU can generate special flag signals that indicate whether a given condition holds. For example, it can output `1` if one operand is less than another, or if an addition resulted in overflow.
Обычно АЛУ представляет собой комбинационную схему (то есть не имеет элементов памяти), на входы которой поступают информационные (операнды) и управляющие (код операции) сигналы, в ответ на что на выходе появляется результат заданной операции. АЛУ может быть реализовано и в виде последовательностной логики, но это скорее исключение.
An ALU is typically a combinational circuit (i.e., it has no memory elements). Its inputs receive data (operands) and control (operation code) signals, and its output produces the result of the specified operation. An ALU can also be implemented as sequential logic, but this is the exception rather than the rule.
![../../.pic/Labs/lab_02_alu/fig_01.drawio.svg](../../.pic/Labs/lab_02_alu/fig_01.drawio.svg)
_Рисунок 1. Структурное обозначение элемента АЛУ[[1, стр.304]](https://reader.lanbook.com/book/241166?lms=1b8d65a957786d4b32b8201bd30e97f3)._
_Figure 1. Block diagram symbol of an ALU [1, p. 247]._
На рис. 1 изображено структурное обозначение АЛУ, используемое в книге "Цифровая схемотехника и архитектура компьютера RISC-V" Харрис и Харрис. На входы `A` и `B` поступают операнды с разрядностью _N_. На 2-битный вход `ALUControl` подается код операции. Например, если туда подать `10`, то на выходе `Y` появится результат операции огическое И_ между битами операндов `A` и `B`. Если же подать `00`, то на выходе появится результат сложения. Это лишь пример, разрядность и коды могут отличаться в зависимости от количества выполняемых операций и архитектуры.
Fig. 1 shows the ALU block diagram symbol used in the book "Digital Design and Computer Architecture: RISC-V Edition" by Harris & Harris. Inputs `A` and `B` receive N-bit operands. The 2-bit `ALUControl` input carries the operation code. For example, supplying `10` causes output `Y` to produce the bitwise AND of operands `A` and `B`. Supplying `00` produces the addition result. This is just an example — the bit widths and codes may vary depending on the number of operations and the architecture.
Существует несколько подходов к реализации АЛУ, отличающиеся внутренней организацией. В лабораторных работах применяется повсеместно используемый подход мультиплексирования операций, то есть подключения нескольких операционных устройств (которые выполняют какие-то операции, например сложения, логического ИЛИ и т.п.) к мультиплексору, который будет передавать результат нужного операционного устройства на выходы АЛУ.
There are several approaches to implementing an ALU, differing in their internal organization. In these labs we use the widely adopted operation-multiplexing approach: multiple functional units (each performing a specific operation such as addition, bitwise OR, etc.) are connected to a multiplexer, which forwards the result of the selected unit to the ALU outputs.
Рассмотрим данный подход на примере всё того же АЛУ из книги Харрисов. На рис. 2, в левой его части, изображена внутренняя организация этого АЛУ, справа таблица соответствия кодов операциям. На выходе схемы (внизу) стоит 4-входовой мультиплексор, управляемый 2-битным сигналом `ALUControl`. Ко входам мультиплексора подключены выходы `N` логических И (побитовое И N-битных операндов), `N` логических ИЛИ, а выход N-битного сумматора подключён дважды, позволяя выбирать его результат для двух кодов операции.
Let us examine this approach using the same ALU from the Harris & Harris book. In Fig. 2, the left side shows the internal organization of this ALU; the right side shows the operation code table. At the output of the circuit (bottom) is a 4-input multiplexer controlled by the 2-bit `ALUControl` signal. The multiplexer inputs are connected to the outputs of `N` AND gates (bitwise AND of N-bit operands), `N` OR gates, and the output of an N-bit adder — connected twice, allowing the adder result to be selected for two different operation codes.
![../../.pic/Labs/lab_02_alu/fig_02.drawio.svg](../../.pic/Labs/lab_02_alu/fig_02.drawio.svg)
_Рисунок 2. Структурная схема АЛУ [[1, стр.305]](https://reader.lanbook.com/book/241166?lms=1b8d65a957786d4b32b8201bd30e97f3)._
_Figure 2. ALU block diagram [1, p. 247]._
Вход `A` подключается напрямую ко входам этих операционных устройств. Вход `B` тоже — за исключением подключения к сумматору. На вход второго операнда сумматора подаётся результат мультиплексирования сигналов `B` и `~B`. Управляющим сигналом этого мультиплексора является младший бит управляющего сигнала `ALUControl`. Кроме того, этот же младший бит подаётся и на сумматор в качестве входного бита переноса. Это означает, что когда `ALUControl[0] = 0`, вычисляется сумма `A + B + 0`, а когда `ALUControl[0] = 1`, вычисляется сумма `A + ~B + 1`, что (с учётом [дополнительного кода](https://ru.wikipedia.org/wiki/Дополнительный_код) ) эквивалентно `A B`.
Input `A` connects directly to the inputs of all functional units. Input `B` does as well — except for the adder connection. The second operand input of the adder receives the result of multiplexing `B` and `~B`. The control signal for this multiplexer is the least significant bit of `ALUControl`. Furthermore, that same least significant bit (LSB) is supplied to the adder as its carry-in. This means that when `ALUControl[0] = 0`, the sum `A + B + 0` is computed, and when `ALUControl[0] = 1`, the sum `A + ~B + 1` is computed — which (due to the properties of [two's complement](https://en.wikipedia.org/wiki/Two%27s_complement)) is equivalent to `A B`.
Преимущество такой организации АЛУ в простоте его модификации, настройке под нужные коды операций, читаемости кода и масштабируемости. Можно легко добавить или убрать требуемые операции. Подумайте, как бы вы обновили данную схему, если бы от вас потребовалось расширить её функционал операциями XOR (Исключающее ИЛИ) и (SGE операция "больше либо равно")?
The advantage of this ALU organization is its ease of modification, flexibility in assigning operation codes, code readability, and scalability. Operations can easily be added or removed. Consider how you would update this circuit if you needed to extend its functionality with XOR (exclusive OR) and SGE (greater-than-or-equal) operations.
## Инструменты
## Tools
Как было сказано выше, АЛУ можно реализовать, [мультиплексируя](../../Basic%20Verilog%20structures/Multiplexors.md) результаты нескольких операционных устройств.
As mentioned above, an ALU can be implemented by [multiplexing](../../Basic%20Verilog%20structures/Multiplexors.md) the results of several functional units.
При описании очередной комбинации управляющего сигнала, выходу мультиплексора можно сразу присваивать необходимое логическое выражение (например результат побитового ИЛИ можно подать на выход сразу в виде выражения: `a | b`, однако в некоторых случаях выражения будут сложнее из-за различных особенностей реализации, о которых будет рассказано в задании).
When describing each combination of the control signal, you can directly assign the required logical expression to the multiplexer output (for example, the result of a bitwise OR can be expressed directly as `a | b`, though some cases require more complex expressions due to implementation specifics discussed in the assignment).
### Параметры
### Parameters
Очень удобным на практике оказывается использование параметров. Параметры добавляют модулю гибкости, позволяя убрать ["магические"](https://ru.wikipedia.org/wiki/Магическое_число_(программирование)#Плохая_практика_программирования) константы из описания модулей, подставляя вместо них выразительное символьное имя. Параметры отдаленно схожи с макросами `#define` в языке Си, однако стоит понимать, что это не одно и то же. Дефайны представляют собой специальные текстовые макросы, которые автоматически заменяются на этапе препроцессора (как если бы вы прошлись по всем файлам своего кода и вручную заменили бы макросы на их значения). Например, с помощью дефайнов можно писать целые куски кода, а не просто одно какое-то число. При этом у дефайнов глобальная область видимости (объявив их в одном месте, этот макрос будет доступен во всем последующем коде). Параметр в свою очередь может хранить только значение какого-то конкретного типа (т.е. в параметр нельзя поместить фрагмент кода) а область видимости параметра ограничена тем модулем, где он был объявлен.
Parameters are very useful in practice. They add flexibility to a module by replacing ["magic"](https://en.wikipedia.org/wiki/Magic_number_(programming)#Numeric_literal) constants in module descriptions with meaningful symbolic names. Parameters are loosely similar to `#define` macros in C, but they are not the same thing. Defines are special text macros that are automatically substituted at the preprocessor stage (as if you had manually replaced every occurrence in your source files). For example, defines can expand into entire code fragments, not just single values. Defines also have global scope (once declared, the macro is available throughout all subsequent code). A parameter, by contrast, can only hold a value of a specific type (you cannot store a code fragment in a parameter), and its scope is limited to the module in which it is declared.
Допустим, ваше устройство должно включить тостер, если на вход ему придет сигнал `32'haf3c5bd0`. Человек, не знакомый с устройством, при прочтении этого кода будет недоумевать, что это за число и почему используется именно оно. Однако, скрыв его за параметром `TOASTER_EN`, читающий поймет, что это код включения тостера. Кроме того, если некоторая константа должна использоваться в нескольких местах кода, то определив её в виде параметра, можно будет менять её в одном месте, и она тут же поменяется везде.
Suppose your device must turn on a toaster when it receives the signal `32'haf3c5bd0`. A reader unfamiliar with the design would be puzzled by that number. However, hiding it behind a parameter named `TOASTER_EN` immediately communicates that it is the toaster enable code. Furthermore, if the same constant is used in multiple places, defining it as a parameter means you only need to change it in one location for the change to propagate everywhere.
Параметры позволяют влиять на структуру модуля. К примеру, описывая сумматор, можно параметризовать его разрядность и использовать этот параметр при описании модуля (например, в качестве диапазона массива модулей). В этом случае вы сможете создавать множество сумматоров различных разрядностей, подставляя при создании нужное вам значение параметра.
Parameters can also influence the structure of a module. For example, when describing an adder, you can parameterize its bit width and use that parameter throughout the module description (e.g., as the range of a module array). This lets you instantiate adders of different widths simply by supplying a different parameter value at instantiation time.
Параметр может быть объявлен в модуле двумя способами:
A parameter can be declared in a module in two ways:
- в прототипе модуля;
- в теле описания модуля.
- in the module prototype;
- in the module body.
В первом случае после имени модуля ставится символ `#`, затем в круглых скобках указывается ключевое слово `parameter`. Далее идет тип параметра (по умолчанию — знаковое 32-битное число), после чего задаются его имя и, при необходимости, значение по умолчанию.
In the first case, a `#` symbol is placed after the module name, followed by the keyword `parameter` in parentheses. Next comes the parameter type (default is a signed 32-bit integer), then the name, and optionally a default value.
Пример:
Example:
```Verilog
module overflow #(parameter WIDTH = 32)(
@@ -83,9 +83,9 @@ module overflow #(parameter WIDTH = 32)(
endmodule
```
_Листинг 1. Пример описания параметра в прототипе модуля._
_Listing 1. Example of declaring a parameter in the module prototype._
В случае, если параметр не влияет на разрядность портов, его можно объявить в теле модуля:
If the parameter does not affect port widths, it can be declared in the module body:
```Verilog
module toaster(
@@ -99,11 +99,11 @@ module toaster(
endmodule
```
_Листинг 2. Пример описания параметра в теле модуля._
_Listing 2. Example of declaring a parameter in the module body._
В случае АЛУ будет удобно использовать параметры для обозначения кодов команд. Во-первых, для того чтобы в `case` не допустить ошибок, а во-вторых чтобы можно было легко менять управляющие коды для повторного использования АЛУ в других проектах.
For the ALU, it is convenient to use parameters to denote operation codes. First, this avoids errors in the `case` statement; second, it makes it easy to change control codes when reusing the ALU in other projects.
Сравните сами _листинги 3 и 4_:
Compare _listings 3 and 4_ yourself:
```Verilog
//parameter ADD = 5'b00000;
@@ -114,11 +114,11 @@ _Листинг 2. Пример описания параметра в теле
always_comb
case(ALUOp)
//...
5'b00011: //... // вообще же ничего не понятно
5'b11000: //... // никуда не годится
5'b00011: //... // completely unclear
5'b11000: //... // unacceptable
```
_Листинг 3. Пример описания модуля, использующего "магические" числа._
_Listing 3. Example of a module description using "magic" numbers._
```Verilog
parameter ADD = 5'b00000;
@@ -129,17 +129,17 @@ parameter SUB = 5'b01000;
always_comb
case(ALUOp)
//...
ADD: //... // очень понятно
SUB: //... // так лаконично и красиво
ADD: //... // very clear
SUB: //... // concise and clean
```
_Листинг 4. Пример описания модуля, использующего параметры._
_Listing 4. Example of a module description using parameters._
С параметрами смотрится гораздо взрослее, серьёзнее и понятнее. Кстати, сразу на заметку: в SystemVerilog можно объединять группу параметров в **пакет** (package), а затем импортировать его внутрь модуля, позволяя переиспользовать параметры без повторного их прописывания для других модулей.
Using parameters looks far more professional, serious, and readable. As a side note: in SystemVerilog you can group parameters into a **package** and then import it into a module, allowing you to reuse parameters without redeclaring them in each module.
Делается это следующим образом.
This is done as follows.
Сперва создается SystemVerilog-файл, который будет содержать пакет (к примеру, содержимое файла может быть таким):
First, create a SystemVerilog file that contains the package (for example, the file might contain):
```Verilog
package riscv_params_pkg;
@@ -148,71 +148,70 @@ package riscv_params_pkg;
endpackage
```
Далее, внутри модуля, которому нужны параметры из этого пакета, необходимо сделать соответствующий импорт этих параметров. Это можно сделать либо для каждого параметра отдельно, либо импортировать все параметры сразу:
Then, inside the module that needs parameters from this package, import them. You can import each parameter individually or import all of them at once:
```Verilog
module riscv_processor
//import riscv_params_pkg::*;
import riscv_params_pkg::ISA_WIDTH; // Если необходимо импортировать
import riscv_params_pkg::ISA_WIDTH; // If you need to import
(
//...Порты
//...Ports
);
import riscv_params_pkg::ANOTHER_EX; // все параметры в пакете, эти две строчки
// могут быть заменены закомментированной
// выше строкой:
import riscv_params_pkg::ANOTHER_EX; // all parameters in the package, these two lines
// can be replaced by the commented-out line above:
endmodule
```
### Битовые сдвиги
### Bit Shifts
Реализуемое в данной лабораторной работе АЛУ использует операции битового сдвига. **Битовый сдвиг** — это операция, при которой все биты числа смещаются на заданное количество позиций. Сдвиг числа на _N_ бит эквивалентен _N_ сдвигам на 1 бит. В архитектуре RISC-V используются два типа сдвигов: **логический** и **арифметический**.
The ALU implemented in this lab uses bit shift operations. A **bit shift** is an operation in which all bits of a number are moved by a specified number of positions. Shifting a number by _N_ bits is equivalent to performing _N_ single-bit shifts. RISC-V uses two types of shifts: **logical** and **arithmetic**.
При **логическом сдвиге** биты сдвигаются влево или вправо, а освободившиеся позиции заполняются нулями. При этом разряды, "вытолкнутые" за пределы разрядной сетки числа, пропадают. Например, если выполнить логический сдвиг двоичного числа _1<ins>0011010</ins>_ на один бит влево, получится _<ins>0011010</ins>0_. Обратите внимание, что старшая единица была вытолкнута за границу и исчезла.
In a **logical shift**, bits are moved left or right and the vacated positions are filled with zeros. Bits that are shifted beyond the bit-width of the number are discarded. For example, a logical left shift of the binary number _1<ins>0011010</ins>_ by one bit yields _<ins>0011010</ins>0_. Note that the leading one was pushed out and lost.
![../../.pic/Labs/lab_02_alu/fig_03.drawio.svg](../../.pic/Labs/lab_02_alu/fig_03.drawio.svg)
_Рисунок 3. Иллюстрация преобразования двоичного числа при логическом сдвиге._
_Figure 3. Illustration of a binary number transformation under a logical shift._
При арифметическом сдвиге заполнение освобождённых битов выполняется так, чтобы сохранился знак числа. В дополнительном коде знак определяется старшим битом, поэтому:
In an **arithmetic shift**, vacated bit positions are filled in a way that preserves the sign of the number. In two's complement, the sign is determined by the most significant bit, therefore:
- при **арифметическом сдвиге вправо** освободившиеся позиции заполняются значением старшего бита исходного числа. Это позволяет сохранить знак. Например, арифметический сдвиг на два бита вправо числа _<ins>100110</ins>10_ даёт _11<ins>100110</ins>_;
- **арифметический сдвиг влево** эквивалентен логическому, так как заполнение младших битов нулями не влияет на знак числа.
- in an **arithmetic right shift**, vacated positions are filled with the value of the original MSB. This preserves the sign. For example, an arithmetic right shift by two bits of _<ins>100110</ins>10_ yields _11<ins>100110</ins>_;
- an **arithmetic left shift** is equivalent to a logical left shift, since filling the lower bits with zeros does not affect the sign of the number.
![../../.pic/Labs/lab_02_alu/fig_04.drawio.svg](../../.pic/Labs/lab_02_alu/fig_04.drawio.svg)
_Рисунок 4. Иллюстрация преобразования двоичного числа при арифметическом сдвиге._
_Figure 4. Illustration of a binary number transformation under an arithmetic shift._
Битовый сдвиг имеет важный арифметический смысл — он соответствует умножению или делению числа на степень двойки:
Bit shifts have an important arithmetic meaning — they correspond to multiplication or division of a number by a power of two:
- сдвиг влево на _N_ бит эквивалентен умножению на _2<sup>N</sup>_,
- сдвиг вправо на _N_ бит эквивалентен целочисленному делению на _2<sup>N</sup>_.
- a left shift by _N_ bits is equivalent to multiplication by _2<sup>N</sup>_,
- a right shift by _N_ bits is equivalent to integer division by _2<sup>N</sup>_.
Этот приём знаком должен быть знаком вам при работе с десятичной системой: умножая число на 10, мы просто дописываем справа ноль. То же самое работает и для деления: если отрезать последний разряд у числа <ins>4</ins>2, получится 4, что соответствует целочисленному делению на 10. В двоичной системе добавление (стирание) нуля справа эквивалентно умножению (делению) на 2.
You may be familiar with this principle from the decimal system: multiplying a number by 10 simply appends a zero on the right. The same applies to division: dropping the last digit of <ins>4</ins>2 gives 4, which is integer division by 10. In binary, appending (or removing) a zero on the right is equivalent to multiplying (or dividing) by 2.
Арифметический сдвиг важен тем, что сохраняет это свойство для знаковых чисел, представленных в дополнительном коде. Логический сдвиг вправо изменяет и знак, и модуль отрицательного числа в дополнительном коде, поэтому не может использоваться для деления знаковых чисел.
Arithmetic shift is important because it preserves this property for signed numbers represented in two's complement. A logical right shift changes both the sign and the magnitude of a negative number in two's complement, so it cannot be used for dividing signed numbers.
Операции умножения и деления — это очень «дорогие» операции как с точки зрения элементов схемы (если эти операции реализуются аппаратно), так и с точки зрения времени их вычисления. Поэтому выполнение сдвигов в качестве замены умножения применяется повсеместно. Например, написав в коде языка C++ выражение `var * 8`, после компиляции вы наверняка получите операцию сдвига влево на 3.
Multiplication and division are very "expensive" operations both in terms of circuit area (when implemented in hardware) and computation time. As a result, using shifts as a substitute for multiplication is widespread. For example, writing `var * 8` in C++ code will almost certainly be compiled to a left shift by 3.
Ещё одно применение сдвигов: установка и очищение флагов управляющих регистров. Дело в том, что обычно процессоры не имеют доступа к отдельным битам многоразрядных регистров — их значения читаются записываются целиком. Вот как можно реализовать битовые операции с помощью сдвигов:
Another application of shifts is setting and clearing bits in control registers. Since processors typically cannot access individual bits of wide registers — their values are read and written as a whole — bit manipulation is achieved using shifts:
```C++
X = X | (1 << N); // Установка N-го бита
X = X & ~(1 << N); // Очистка N-го бита
Y = (X & (1 << N)) != 0; // Чтение N-го бита
X = X | (1 << N); // Set the N-th bit
X = X & ~(1 << N); // Clear the N-th bit
Y = (X & (1 << N)) != 0; // Read the N-th bit
```
#### Особенности реализации сдвига
#### Shift Implementation Notes
> [!IMPORTANT]
> По спецификации RISC-V, для **ВСЕХ** операций сдвига используются только 5 младших бит операнда B[2, стр. 26-27].
> According to the RISC-V specification, **ALL** shift operations use only the 5 least significant bits of operand B [2, pp. 2627].
Сами посмотрите: выполнять операцию сдвига более чем на 31 для 32-битных чисел не имеет смысла, число полностью заполнится нулями (единицами). Т.е. сдвигая на любое число, большее 31, вы получите один и тот же результат. Для того чтобы закодировать 31 требуется минимум 5 бит, отсюда и это требование. Оно обязательно, поскольку старшие биты в дальнейшем будут использоваться по другому назначению и, если вы упустите это, ваш будущий процессор станет работать неправильно.
Consider why: shifting a 32-bit number by more than 31 positions makes no sense — the result is simply all zeros (or all ones). That is, shifting by any value greater than 31 always produces the same result. Encoding 31 requires at least 5 bits, hence this requirement. It is mandatory: the upper bits will be used for other purposes later, and ignoring this will cause your future processor to behave incorrectly.
## Задание
## Assignment
Необходимо на языке SystemVerilog реализовать АЛУ в соответствии со следующим прототипом:
Implement an ALU in SystemVerilog according to the following prototype:
```Verilog
@@ -224,112 +223,112 @@ module alu (
output logic [31:0] result_o
);
import alu_opcodes_pkg::*; // импорт параметров, содержащих
// коды операций для АЛУ
import alu_opcodes_pkg::*; // import of parameters containing
// operation codes for the ALU
endmodule
```
Для стандартного набора целочисленных операций архитектуры RISC-V требуется выполнять 16 различных операций. Для кодирования 16 операций было бы достаточно 4 бит, но в лабораторной работе предлагается использовать 5-битный код, что связано с особенностями кодирования инструкций. Старший бит кода операции указывает на то, является ли операция вычислительной или это операция сравнения.
The standard integer instruction set of RISC-V requires 16 distinct operations. While 4 bits would suffice to encode 16 operations, this lab uses a 5-bit code, which is related to the instruction encoding scheme. The MSB of the operation code indicates whether the operation is a computational operation or a comparison.
Для удобства чтения, список инструкций разбит на две таблицы.
For readability, the instruction list is split into two tables.
В первой таблице перечислены операции, вычисляющие значение сигнала `result_o`. **При получении АЛУ любого кода операции, не входящего в эту таблицу, сигнал `result_o` должен быть равен нулю**.
The first table lists the operations that compute the value of the `result_o` signal. **If the ALU receives any operation code not listed in this table, the `result_o` signal must be zero.**
|Операция|={cmp, mod, opcode}|Выражение | Действие |
|--------|-------------------|-----------------------|-------------------------------------------------------|
| ADD | 0 0 000 |result_o = a_i + b_i | Сложение |
| SUB | 0 1 000 |result_o = a_i b_i | Вычитание |
| SLL | 0 0 001 |result_o = a_i << b_i | Сдвиг влево |
| SLTS | 0 0 010 |result_o = a_i < b_i | **Знаковое** сравнение |
| SLTU | 0 0 011 |result_o = a_i < b_i | **Беззнаковое** сравнение |
| XOR | 0 0 100 |result_o = a_i ^ b_i | Побитовое исключающее **ИЛИ** |
| SRL | 0 0 101 |result_o = a_i >> b_i | Сдвиг вправо |
| SRA | 0 1 101 |result_o = a_i >>> b_i | Арифметический сдвиг вправо (операнд `a_i` — знаковый)|
| OR | 0 0 110 |result_o = a_i \| b_i | Побитовое логическое **ИЛИ** |
| AND | 0 0 111 |result_o = a_i & b_i | Побитовое логическое **И** |
| Operation | ={cmp, mod, opcode} | Expression | Action |
|-----------|---------------------|-------------------------|-------------------------------------------------------|
| ADD | 0 0 000 | result_o = a_i + b_i | Addition |
| SUB | 0 1 000 | result_o = a_i b_i | Subtraction |
| SLL | 0 0 001 | result_o = a_i << b_i | Left shift |
| SLTS | 0 0 010 | result_o = a_i < b_i | **Signed** comparison |
| SLTU | 0 0 011 | result_o = a_i < b_i | **Unsigned** comparison |
| XOR | 0 0 100 | result_o = a_i ^ b_i | Bitwise exclusive **OR** |
| SRL | 0 0 101 | result_o = a_i >> b_i | Right shift |
| SRA | 0 1 101 | result_o = a_i >>> b_i | Arithmetic right shift (`a_i` operand is signed) |
| OR | 0 0 110 | result_o = a_i \| b_i | Bitwise logical **OR** |
| AND | 0 0 111 | result_o = a_i & b_i | Bitwise logical **AND** |
_Таблица 1. Список вычислительных операций._
_Table 1. List of computational operations._
Во второй таблице перечислены операции, вычисляющие значение сигнала `flag_o`. **При получении АЛУ любого кода операции, не входящего в эту таблицу, сигнал `flag_o` должен быть равен нулю**.
The second table lists the operations that compute the value of the `flag_o` signal. **If the ALU receives any operation code not listed in this table, the `flag_o` signal must be zero.**
|Операция|={cmp, mod, opcode}| Выражение | Действие |
|--------|-------------------|----------------------|-----------------------------------|
| EQ | 1 1 000 | flag_o = (a_i == b_i)| Выставить флаг, если **равны** |
| NE | 1 1 001 | flag_o = (a_i != b_i)| Выставить флаг, если **не равны** |
| LTS | 1 1 100 | flag_o = a_i < b_i | **Знаковое** сравнение **<** |
| GES | 1 1 101 | flag_o = a_i ≥ b_i | **Знаковое** сравнение **≥** |
| LTU | 1 1 110 | flag_o = a_i < b_i | **Беззнаковое** сравнение **<** |
| GEU | 1 1 111 | flag_o = a_i ≥ b_i | **Беззнаковое** сравнение **≥** |
| Operation | ={cmp, mod, opcode} | Expression | Action |
|-----------|---------------------|------------------------|--------------------------------------|
| EQ | 1 1 000 | flag_o = (a_i == b_i) | Set flag if operands are **equal** |
| NE | 1 1 001 | flag_o = (a_i != b_i) | Set flag if operands are **unequal** |
| LTS | 1 1 100 | flag_o = a_i < b_i | **Signed** comparison **<** |
| GES | 1 1 101 | flag_o = a_i ≥ b_i | **Signed** comparison **≥** |
| LTU | 1 1 110 | flag_o = a_i < b_i | **Unsigned** comparison **<** |
| GEU | 1 1 111 | flag_o = a_i ≥ b_i | **Unsigned** comparison **≥** |
_Таблица 2. Список операций сравнения._
_Table 2. List of comparison operations._
**Выражения в этих двух таблицах приведены для примера. Не все из них можно просто переписать — часть этих выражений надо дополнить. Чтобы вы не копировали выражения, в них вставлены неподдерживаемые символы.**
**The expressions in these two tables are provided as examples. Not all of them can simply be transcribed — some must be supplemented. To prevent copy-pasting, the expressions contain unsupported characters.**
Несмотря на разделение на вычислительные операции, и операции сравнения, в _Таблице 1_ (вычислительных операций) оказалось две операции: `SLTS` и `SLTU`, которые выполняют сравнения. В итоге у нас есть две похожие пары инструкций:
Despite the separation into computational operations and comparison operations, _Table 1_ (computational operations) includes two operations — `SLTS` and `SLTU` — that perform comparisons. This gives us two similar pairs of instructions:
- `LTS`
- `LTU`
- `SLTS`
- `SLTU`
Первая пара инструкций вычисляет "ветвительный" результат. Результат операции будет подан на выходной сигнал `flag_o` и использован непосредственно при ветвлении.
The first pair computes a "branch" result. The operation result is placed on the `flag_o` output and used directly for branching.
Вторые две инструкции используются для получения "вычислительного" результата. Т.е. результат сравнения будет подан на выходной сигнал `result_o` так же, как подается результат операции `ADD`, и будет использован в неких вычислениях, избегая при этом условного перехода.
The second pair is used to obtain a "computational" result. That is, the comparison result is placed on the `result_o` output in the same way as the result of the `ADD` operation, and is used in further computations — avoiding a conditional branch.
К примеру, нам необходимо пройтись по массиву из миллиона элементов и убедиться, что все они были неотрицательны. Об этом будет сигнализировать переменная `num_of_err`, значение которой должно быть равно числу элементов массива, меньших нуля. Вычислить значение этой переменной можно двумя способами:
For example, suppose we need to iterate over an array of one million elements and check that all of them are non-negative. This is tracked by the variable `num_of_err`, whose value should equal the count of elements less than zero. The variable can be computed in two ways:
1. В каждой итерации цикла сделать ветвление: в одном случае инкрементировать переменную, в другом случае — нет (для ветвления использовать "ветвительную" операцию `LTS`).
2. В каждой итерации цикла складывать текущее значение переменной с результатом "вычислительной" операции `SLTS`.
1. In each loop iteration, perform a branch: in one case increment the variable, in the other do not (use the "branch" operation `LTS` for branching).
2. In each loop iteration, add the result of the "computational" operation `SLTS` to the current value of the variable.
Операции ветвления очень сильно влияют (в худшую сторону) на производительность конвейерного процессора. В первом случае мы получим миллион операций ветвления, во втором — ни одной! Разумеется, потом переменную `num_of_err` скорее всего сравнят с нулем что приведет к ветвлению, но при вычислении значения этой переменной ветвления можно будет избежать.
Branch operations have a strong (negative) impact on the performance of a pipelined processor. In the first case we get one million branch operations; in the second — none! Of course, `num_of_err` will likely be compared to zero at some point, causing a branch, but during the computation of that variable's value the branch can be avoided entirely.
Различие между `SLTS` и `SLTU` (или `LTS` и `LTU`) заключается в том, как мы интерпретируем операнды: как знаковые числа (операции `STLS` и `LTS`) или как беззнаковые (операции `SLTU` и `LTU`).
The difference between `SLTS` and `SLTU` (or `LTS` and `LTU`) lies in how we interpret the operands: as signed numbers (operations `SLTS` and `LTS`) or as unsigned (operations `SLTU` and `LTU`).
Предположим, мы сравниваем два двоичных числа: `1011` и `0100`. Если интерпретировать эти числа как беззнаковые, то это `11` и `4`, результат: `11 > 4`. Однако если интерпретировать эти числа как знаковые, то теперь это числа `-5` и `4` и в этом случае `-5 < 4`.
Suppose we compare two binary numbers: `1011` and `0100`. Interpreted as unsigned, these are `11` and `4`, giving `11 > 4`. Interpreted as signed, they are `-5` and `4`, giving `-5 < 4`.
Как мы видим, результат одной и той же операции над одними и теми же двоичными числами может зависеть от того, каким образом мы интерпретируем эти двоичные числа. Для большинства операций в АЛУ это не важно: например, сложение будет работать одинаково в обоих случаях, благодаря свойствам дополнительного кода, а побитовые операции работают с отдельными битами двоичного числа. А вот для операции арифметического сдвига это важно — **операнд А в арифметическом сдвиге должен интерпретироваться как знаковый**.
As we can see, the result of the same operation on the same binary numbers can depend on how those numbers are interpreted. For most ALU operations this does not matter — for example, addition works identically in both cases due to the properties of two's complement, and bitwise operations work on individual bits. However, for arithmetic right shift it does matter — **the operand A in an arithmetic shift must be interpreted as signed**.
По умолчанию SystemVerilog интерпретирует все сигналы как беззнаковые, если мы хотим изменить это поведение, необходимо воспользоваться конструкцией `$signed`.
By default, SystemVerilog interprets all signals as unsigned. To change this behavior, use the `$signed` construct.
Конструкция `$signed` говорит САПР интерпретировать число, переданное в качестве операнда, как знаковое.
The `$signed` construct tells the synthesis tool to interpret the value passed as an operand as a signed number.
```Verilog
assign Result = $signed(A) / 10;
```
В этом примере некоторому сигналу `Result` присваивают результат деления **знакового** числа `A` на `10`.
In this example, signal `Result` is assigned the result of dividing the **signed** number `A` by `10`.
Так как используются не все возможные комбинации управляющего сигнала АЛУ, то **при описании через `case` не забывайте использовать `default`**. Если описать АЛУ как задумано, то получится что-то похожее на _рис. 5_. Но не обязательно, зависит от вашего описания.
Since not all possible combinations of the ALU control signal are used, **when describing the logic with `case`, do not forget to include a `default` branch**. If the ALU is described as intended, the result will look something like _Fig. 5_ — though it may differ depending on your description.
![../../.pic/Labs/lab_02_alu/fig_05.png](../../.pic/Labs/lab_02_alu/fig_05.png)
_Рисунок 5. Пример схемы, реализующей АЛУ._
_Figure 5. Example circuit implementing the ALU._
Обратите внимание на то, что сумматор на _рис. 5_ отличается от всех остальных блоков. Для того, чтобы спроектированный в ЛР№1 32-разрядный сумматор был создан не зазря, а также для закрепления навыков по созданию экземпляров модулей внутри других модулей, вам предлагается использовать его при реализации АЛУ.
Note that the adder in _Fig. 5_ differs from all other blocks. To make the 32-bit adder designed in Lab 1 worthwhile, and to reinforce module instantiation skills, you are expected to use it when implementing the ALU.
Другие блоки распознаны Vivado на основе представленных в описании АЛУ арифметических или логических выражений и в процессе синтеза будут реализованы через те компоненты ПЛИС, которые позволят лучше всего удовлетворить временным и физическим ограничениям проекта (см. главу "Этапы реализации проекта в ПЛИС"). Сумматор же будет реализован так, как это описали мы, поскольку вместо использования абстрактной операции "+" в описании АЛУ было сказано разместить конкретный модуль, реализующий конкретную схему. Такая реализация сумматора не является эффективной ни в плане временных характеристик, ни в плане количества затраченных на реализацию ресурсов ПЛИС. Но как уже упоминалось в ЛР№1, цель этой реализации — воспроизвести простоту логики рассуждений о том, как спроектировать сумматор.
The other blocks are recognized by Vivado from the arithmetic or logical expressions in the ALU description and will be synthesized using the FPGA primitives that best satisfy the project's timing and physical constraints (see "FPGA Design Flow"). The adder, however, will be implemented exactly as described, because instead of using the abstract `+` operator, the ALU description instantiates a specific module implementing a specific circuit. This adder implementation is not efficient — neither in terms of timing characteristics nor in terms of FPGA resource utilization. But as mentioned in Lab 1, the goal of this implementation is to illustrate the simplicity of the design reasoning behind the adder.
### Порядок выполнения задания
### Assignment Steps
1. Добавьте в `Design Sources` проекта файл [`alu_opcodes_pkg.sv`](alu_opcodes_pkg.sv). Этот файл содержит объявление пакета `alu_opcodes_pkg`, в котором прописаны все опкоды АЛУ.
1. Поскольку данный файл не содержит описания модулей, он не отобразится во вкладке `Hierarchy` окна `Sources` Vivado (исключением может быть ситуация, когда в проекте вообще нет ни одного модуля). Добавленный файл можно будет найти во вкладках `Libraries` и `Compile Order`.
2. Обратите внимание, что имена параметров кодов операций АЛУ, объявленных в добавляемом пакете, имеют префикс `ALU_`, которого не было в _таблицах 1 и 2_.
3. В случае, если вы добавили пакет в проект и импортировали его в модуле АЛУ, однако Vivado выдает ошибку о том, что используемые параметры не объявлены, попробуйте сперва исправить все остальные синтаксические ошибки и сохранить файл. Если и это не помогло, можно перейти на вкладку `Compile Order`, нажать правой кнопкой мыши по файлу `alu_opcodes_pkg.sv` и выбрать `Move to Top`. Таким образом, мы сообщаем Vivado, что при компиляции проекта, этот файл всегда необходимо собирать в первую очередь. Это вариант "последней надежды" и должен использоваться только в самом крайнем случае. Когда в проекте нет никаких проблем, Vivado всегда может самостоятельно определить правильный порядок компиляции файлов. Тот факт, что вам приходится менять этот порядок означает, что в проекте есть какие-то проблемы, не позволяющие Vivado определить правильный порядок самостоятельно.
2. Опишите модуль `alu` с таким же именем и портами, как указано в [задании](#Задание).
1. Поскольку у вас два выходных сигнала, зависящих от сигнала `alu_op_i`, вам потребуется описать два разных [мультиплексора](../../Basic%20Verilog%20structures/Multiplexors.md) (их лучше всего описывать через два отдельных блока `case`). При описании, используйте `default` на оставшиеся комбинации сигнала `alu_op_i`.
2. Следите за разрядностью ваших сигналов.
3. Для реализации АЛУ, руководствуйтесь таблицей с операциями, а не схемой в конце задания, которая приведена в качестве референса. Обратите внимание, в одной половине операций `flag_o` должен быть равен нулю, в другой `result_o` (т.е. всегда либо один, либо другой сигнал должен быть равен нулю). Именно поэтому удобней всего будет описывать АЛУ в двух разных блоках `case`.
4. Вам не нужно переписывать опкоды из таблицы в качестве вариантов для блока `case`. Вместо этого используйте символьные имена с помощью параметров, импортированных из пакета `alu_opcodes_pkg`.
5. Описывая операцию сложения вы **должны** использовать ваш 32-битный сумматор из первой лабораторной. При описании вычитания сумматор использовать не надо, можно использовать оператор `-`.
1. При подключении сумматора, на входной бит переноса необходимо подать значение `1'b0`. Если не подать значение на входной бит переноса, результат суммы будет не определен (т.к. не определено одно из слагаемых).
2. Выходной бит переноса при подключении сумматора можно не указывать, т.к. он использоваться не будет.
6. При реализации операций сдвига, руководствуйтесь [особенностями реализации сдвигов](#Особенности-реализации-сдвига).
3. Проверьте модуль с помощью верификационного окружения, представленного в файле [`lab_02.tb_alu.sv`](lab_02.tb_alu.sv). В случае, если в TCL-консоли появились сообщения об ошибках, вам необходимо [найти](../../Vivado%20Basics/05.%20Bug%20hunting.md) и исправить их.
1. Перед запуском моделирования убедитесь, что у вас выбран корректный модуль верхнего уровня в `Simulation Sources`.
4. [Проверьте](./board%20files) работоспособность вашей цифровой схемы в ПЛИС.
1. Add the file [`alu_opcodes_pkg.sv`](alu_opcodes_pkg.sv) to the project's `Design Sources`. This file contains the declaration of the `alu_opcodes_pkg` package, which defines all ALU operation codes.
1. Since this file contains no module declarations, it will not appear in the `Hierarchy` tab of the Vivado `Sources` window (the exception is when the project contains no modules at all). The added file can be found in the `Libraries` and `Compile Order` tabs.
2. Note that the ALU operation code parameter names declared in the added package have the prefix `ALU_`, which was not present in _Tables 1 and 2_.
3. If you have added the package to the project and imported it in the ALU module but Vivado reports an error saying the used parameters are undeclared, first try fixing all other syntax errors and saving the file. If that does not help, go to the `Compile Order` tab, right-click on `alu_opcodes_pkg.sv`, and select `Move to Top`. This tells Vivado to always compile this file first. This is a last-resort option and should only be used as a final measure. When the project has no issues, Vivado is always able to determine the correct compilation order on its own. The fact that you need to change this order indicates there are problems in the project preventing Vivado from determining the correct order automatically.
2. Describe the `alu` module with the same name and ports as specified in the [assignment](#Assignment).
1. Since you have two output signals that depend on `alu_op_i`, you will need to describe two separate [multiplexers](../../Basic%20Verilog%20structures/Multiplexors.md) (best described using two separate `case` blocks). Use `default` to cover the remaining combinations of `alu_op_i`.
2. Pay attention to the bit widths of your signals.
3. When implementing the ALU, follow the operations table rather than the reference schematic at the end of the assignment. Note that in one half of the operations `flag_o` must be zero, and in the other half `result_o` must be zero (i.e., at all times, one or the other signal must be zero). This is why it is most convenient to describe the ALU using two separate `case` blocks.
4. You do not need to copy operation codes from the table as `case` values. Instead, use the symbolic names from the parameters imported from the `alu_opcodes_pkg` package.
5. When describing the addition operation, you **must** use your 32-bit adder from Lab 1. For subtraction, you do not need to use the adder — you may use the `-` operator.
1. When connecting the adder, supply `1'b0` to the carry-in input. If the carry-in is left undriven, the sum result will be undefined (since one of the addends is undefined).
2. The carry-out can be left unconnected, as it will not be used.
6. When implementing shift operations, follow the [shift implementation notes](#Shift-Implementation-Notes).
3. Verify the module using the verification environment provided in the file [`lab_02.tb_alu.sv`](lab_02.tb_alu.sv). If error messages appear in the TCL console, [find](../../Vivado%20Basics/05.%20Bug%20hunting.md) and fix them.
1. Before running the simulation, make sure the correct top-level module is selected in `Simulation Sources`.
4. [Verify](./board%20files) that your digital circuit works correctly on the FPGA.
## Список использованной литературы
## References
1. [Д.М. Харрис, С.Л. Харрис / Цифровая схемотехника и архитектура компьютера: RISC-V / пер. с англ. В. С. Яценков, А. Ю. Романов; под. ред. А. Ю. Романова / М.: ДМК Пресс, 2021](https://e.lanbook.com/book/241166);
1. D.M. Harris, S.L. Harris / Digital Design and Computer Architecture. RISC-V Edition, 2021;
2. [The RISC-V Instruction Set Manual Volume I: Unprivileged ISA, Document Version 20240411, Editors Andrew Waterman and Krste Asanović, RISC-V Foundation, April 2024](https://github.com/riscv/riscv-isa-manual/releases/download/20240411/unpriv-isa-asciidoc.pdf).