Files
APS/Labs/02. Arithmetic-logic unit

Лабораторная работа 2. Арифметико-логическое устройство

Так как основной задачей процессора является обработка цифровых данных, одним из его основных блоков является арифметико-логическое устройство (АЛУ). Задача АЛУ производить над входными данным арифметические и поразрядно логические операции.

Цель

Используя навыки по описанию мультиплексоров, писать блок арифметико-логического устройства (АЛУ) на языке SystemVerilog.

Допуск к лабораторной работе

Освоить описание мультиплексора на языке SystemVerilog.

Общий ход выполнения работы

  1. Изучить устройство и принцип работы АЛУ (раздел #теория)
  2. Изучить языковые конструкции SystemVerilog для реализации АЛУ (раздел #инструменты)
  3. Внимательно ознакомиться с заданием (раздел #задание)
  4. Описать модуль АЛУ, проверить его предоставленным верификационным окружением.
  5. Проверить работу АЛУ в ПЛИС.

Теория

Арифметико-логическое устройство (АЛУ, Arithmetic Logic Unit ALU) это блок процессора, выполняющий арифметические и поразрядно логические операции. Разница между арифметическими и логическими операциями в отсутствии у последних бита переноса, так как логические операции происходят между однобитными числами и дают однобитный результат, а в случае АЛУ (в рамках данной лабораторной работы) одновременно между 32-мя однобитными парами чисел. В логических операциях результаты значений отдельных битов друг с другом никак не связаны.

Также, кроме результата операций, АЛУ формирует флаги, которые показывают выполняется ли заданное условие. Например, выведет 1, если один операнд меньше другого.

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

../../.pic/Labs/lab_02_alu/fig_01.png

Рисунок 1. Структурное обозначение элемента АЛУ

На рис. 1 изображен пример АЛУ, используемый в книге "Цифровая схемотехника и архитектура компьютера" Харрис и Харрис. На входы A и B поступают операнды с разрядностью N. На трехбитный вход F подается код операции. Например, если туда подать 000, то на выходе Y появится результат операции логическое И между битами операндов A и B. Если на F подать 010, то на выходе появится результат сложения. Это лишь пример, разрядность и коды могут отличаться в зависимости от количества выполняемых операций и архитектуры.

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

Рассмотрим на примере все того же АЛУ MIPS из книги Харрисов. На рис. 2, в левой его части, изображена внутренняя организация этого АЛУ, справа таблица соответствия кодов операциям. На выходе схемы (внизу) стоит четырехвходовый мультиплексор, управляемый двумя из трех битов F. К его входам подключены N логических И (побитовое И N-разрядных операндов), N логических ИЛИ, N-разрядный сумматор и Zero Extend устройство делающее из однобитного числа N-битное число, дополняя нулями слева.

К одному из входов этих операционных устройств подключен A без изменений, а ко второму подключен выход двухвходового мультиплексора, управляемого оставшимся битом F. То есть F[2] определяет, что будет вторым операндом: B или ~B. Вдобавок F[2] подается на входной перенос сумматора, то есть, когда F[2] == 1 на выходе сумматора появляется результат операции A + ~B + 1, что (с учетом дополнительного кода) эквивалентно  B.

../../.pic/Labs/lab_02_alu/fig_02.png

Рисунок 2. Структурная схема АЛУ MIPS

Посмотрим, что произойдет, если на вход F такого АЛУ подать 111. Будет выполняться операция SLT(сокращение от Set Less Then) выдать 1, если A меньше B, в противном случае — выдать 0. Биты F[1:0] переключат мультиплексор на выход блока Zero Extend. На вход Zero Extend поступает старший бит выхода сумматора, этот бит отвечает за знак результата. Так как F[2] == 1, сумматор вычисляет A + ~B + 1, то есть  B, значит, если A < B, то результат вычитания будет отрицательный, а старший бит Y[N-1] == 1. Если A не меньше B, то разность будет неотрицательна, а Y[N-1] == 0, как и требуется от этой операции.

../../.pic/Labs/lab_02_alu/fig_03.png

Рисунок 3. Пример исполнения операции АЛУ

Преимущество такой организации АЛУ в его простой модификации, настройке под нужные коды операций, читаемости кода и масштабируемости. Можно легко добавить или убрать требуемые операции. Подумайте, как бы вы обновили данную схему, если бы от вас потребовалось расширить её функционал операциями XOR (Исключающее ИЛИ) и (SGE операция "больше либо равно")?

Инструменты

Как было сказано выше, АЛУ можно реализовать, мультиплексируя результаты нескольких операционных устройств.

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

Параметры

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

Допустим, ваше устройство должно включить тостер, если на вход ему придет сигнал 32'haf3c5bd0. Человек, не знакомый с устройством, при прочтении этого кода будет недоумевать, что это за число и почему используется именно оно. Однако, скрыв его за параметром TOASTER_EN, читающий поймет, что это код включения тостера. Кроме того, если некоторая константа должна использоваться в нескольких местах кода, то определив её через в виде параметра, можно будет менять её в одном месте, и она тут же поменяется везде.

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

Параметр может быть объявлен в модуле двумя способами:

  • в прототипе модуля;
  • в теле описания модуля.

В первом случае для указания параметра после имени модуля ставится символ #, за которым в круглых скобках пишется ключевое слово parameter затем указывается тип параметра (по умолчанию знаковое 32-битное число), после чего указывается имя и (опционально) значение по умолчанию.

Пример:

module overflow #(parameter WIDTH = 32)(
  input  logic [WIDTH-1 : 0]  a, b,
  output logic                 overflow
);

logic [WIDТН : 0] sum;

ass𝚒gn sum = a + b;
ass𝚒gn overflow = sum[WIDTH];

endmodule

Только таким способом объявления параметр может влиять на разрядность портов модуля.

В случае, если параметр не влияет на разрядность портов, его можно объявить в теле модуля:

module toaster(
  input  logic [31:0] command,
  output logic        power
)

parameter TOASTER_EN = 32'haf3c5bd0;

assign power = command == TOASTER_EN;

endmodule

В случае АЛУ будет удобно использовать параметры для обозначения кодов команд. Во-первых, для того чтобы в case не допустить ошибок, а во-вторых чтобы можно было легко менять управляющие коды для повторного использования АЛУ в других проектах.

Сравните сами:

//parameter SLT = 5'b00011;
//parameter BEQ = 5'b11000;

//...

always_comb
  case(ALUOp)
  //...
  5'b00011: //...   // вообще же ничего не понятно
  5'b11000: //...   // никуда не годится

и

parameter SLT = 5'b00011;
parameter BEQ = 5'b11000;

//...

аlwауs_comb
  case(ALUOp)
  //...
  SLT: //...   // очень понятно
  BEQ: //...   // так лаконично и красиво

С параметрами гораздо взрослее, серьезнее и понятнее смотрится. Кстати, сразу на заметку: в SystemVerilog можно объединять группу параметров в пакет(package), а затем импортировать его внутрь модуля, позволяя переиспользовать параметры без повторного их прописывания для других модулей.

Делается это следующим образом.

Сперва создается SystemVerilog-файл, который будет содержать пакет (к примеру содержимое файла может быть таким):

package riscv_params_pkg;
  parameter ISA_WIDTH   = 32;
  parameter ANOTHER_EX  = 15;
endpackage

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

module riscv_processor(
  //...Порты
);

import riscv_params_pkg::ISA_WIDTH;   // Если необходимо импортировать
import riscv_params_pkg::ANOTHER_EX;  // все параметры в пакете,эти две строчки
                                      // могут быть заменены закомментированной
                                      // ниже строкой:
//import riscv_params_pkg::*;

endmodule

При реализации АЛУ, вам также потребуется использовать операции сдвига, к которым относятся:

  • << — логический сдвиг влево
  • >> — логический сдвиг вправо
  • >>> — арифметический сдвиг вправо







Особенности реализации сдвига

Для ВСЕХ операций сдвига вы должны брать только 5 младших бит операнда B.

Сами посмотрите: пятью битами можно описать 32 комбинации [0-31], а у операнда А будет использоваться ровно 32 бита. Это обязательное требование, поскольку старшие биты в дальнейшем будут использоваться по другому назначению и если вы упустите это, ваш будущий процессор станет работать неправильно.







Задание

Необходимо на языке SystemVerilog реализовать АЛУ в соответствии со следующим прототипом:


module аlu_r𝚒sсv (
  𝚒nput  logic [31:0]  a_i,
  𝚒nput  logic [31:0]  b_i,
  𝚒nput  logic [4:0]   alu_op_i,
  оutput logic         flag_o,
  оutput logic [31:0]  result_o
);

import alu_opcodes_pkg::*;      // импорт параметров, содержащих
                                // коды операций для АЛУ

endmodule

Для стандартного набора целочисленных операций архитектуры RISC-V требуется выполнять 16 различных операций. Для кодирования 16 операций было бы достаточно 4 бит, но в лабораторной работе предлагается использовать 5-битный код, что связано с особенностями кодирования инструкций (будет рассмотрено в лекциях 6 и 7). Видно, что старший бит кода операции указывает на то, является ли операция вычислительной или это операция сравнения.

ALUOp ={flag, add/sub, aluop} Result Flag Операция
ADD 0 0 000 Result = А + В Flаg = 0 Сложение
SUB 0 1 000 Result = А В Flаg = 0 Вычитание
SLL 0 0 001 Result = А << В Flаg = 0 Сдвиг влево
SLTS 0 0 010 Result = А < В (знаковое сравнение) Flаg = 0 Знаковое сравнение
SLTU 0 0 011 Result = А < В Flаg = 0 Беззнаковое сравнение
XOR 0 0 100 Result = А ^ В Flаg = 0 Побитовое исключающее ИЛИ
SRL 0 0 101 Result = А >> В Flаg = 0 Сдвиг вправо
SRA 0 1 101 Result = А >>> В Flаg = 0 Арифметический сдвиг вправо (операнд А — знаковый)
OR 0 0 110 Result = А | В Flаg = 0 Побитовое логическое ИЛИ
AND 0 0 111 Result = А & В Flаg = 0 Побитовое логическое И
EQ 1 1 000 Result = 0 Flаg = (А == В) Выставить флаг, если равны
NE 1 1 001 Result = 0 Flаg = (А != В) Выставить флаг, если не равны
LTS 1 1 100 Result = 0 Flаg = А < В (знаковое сравнение) Знаковое сравнение <
GES 1 1 101 Result = 0 Flаg = АВ (знаковое сравнение) Знаковое сравнение
LTU 1 1 110 Result = 0 Flаg = А < В Беззнаковое сравнение <
GEU 1 1 111 Result = 0 Flаg = АВ Беззнаковое сравнение

Выражения в этой таблице приведены для примера. Не все из них можно просто переписать — часть этих выражений надо дополнить. Чтобы вы не копировали выражения, в них вставлены неподдерживаемые символы.

Обратите внимание на инструкции сравнения. У нас есть две похожие пары инструкций:

  • LTS
  • LTU
  • SLTS
  • SLTU

Первая пара инструкций вычисляет "ветвительный" результат. Результат операции будет подан на выходной сигнал Flag и использован непосредственно при ветвлении.

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

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

  1. В каждой итерации цикла сделать ветвление: в одном случае инкрементировать переменную, в другом случае — нет (для ветвления использовать "ветвительную" операцию LTS).
  2. В каждой итерации цикла складывать текущее значение переменной с результатом "вычислительной" операции SLTS.

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

Различие между SLTS и SLTU (или LTS и LTU) заключается в том, как мы интерпретируем операнды: как знаковые числа (операции STLS и LTS) или как беззнаковые (операциии SLTU и LTU).

Предположим, мы сравниваем два двоичных числа: 1011 и 0100. Если интерпретировать эти числа как беззнаковые, то это 11 и 4, результат: 11 > 4. Однако если интерпретировать эти числа как знаковые, то теперь это числа -5 и 4 и в этом случае -5 < 4.

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

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

Конструкция $signed говорит САПР интерпретировать число, переданное в качестве операнда, как знаковое.

  аss𝚒gn Rеsult = $s𝚒gnеd(А) >>> В[4:0];

В этом примере некоторому сигналу Result присваивают результат сдвига знакового числа A на значение количества бит получаемых из младших 5 бит сигнала B.

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

../../.pic/Labs/lab_02_alu/fig_04.png

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

  1. Добавьте в проект файл alu_opcodes_pkg.sv. Этот файл содержит объявление пакета alu_opcodes_pkg, в котором прописаны все опкоды АЛУ.
  2. В Design Sources проекта создайте SystemVerilog-файл аlu_r𝚒sсv.sv.
  3. Опишите в нем модуль АЛУ с таким же именем и портами, как указано в задании.
    1. Поскольку у вас два выходных сигнала, зависящих от сигнала alu_op_i, вам потребуется описать два разных мультиплексора (их лучше всего описывать через два отдельных блока case). При описании, используйте default на оставшиеся комбинации сигнала alu_op_i.
    2. Следите за разрядностью ваших сигналов.
    3. Для реализации АЛУ, руководствуйтесь таблицей с операциями, а не схемой в конце задания, которая приведена в качестве референса. Обратите внимание, в одной половине операций flag_o должен быть равен нулю, в другой result_o (т.е. всегда либо один, либо другой сигнал должен быть равен нулю). Именно поэтому удобней всего будет описывать АЛУ в двух разных блоках case.
    4. Вам не нужно переписывать опкоды из таблицы в качестве вариантов для блока case. Вместо этого используйте символьные имена с помощью параметров, импортированных из пакета alu_opcodes_pkg.
    5. При операции сложения вы должны использовать ваш 32-битный сумматор из первой лабораторной (описывая вычитание сумматор использовать не надо, можно использовать -).
      1. При подключении сумматора, на входной бит переноса необходимо подать значение 1'b0. Если не подать значение на входной бит переноса, результат суммы будет не определен (т.к. не определено одно из слагаемых).
      2. Выходной бит переноса при подключении сумматора можно не указывать, т.к. он использоваться не будет.
    6. При реализации операций сдвига, руководствуйтесь особенностями реализации сдвигов.
  4. После реализации модуля АЛУ его нужно будет проверить с помощью тестового окружения.
    1. Добавьте файл tb_alu.sv в Simulation sources.
    2. Для запуска симуляции воспользуйтесь этой инструкцией.
    3. Перед запуском симуляции убедитесь, что в качестве top-level модуля выбран модуль tb_alu.
    4. Убедитесь, что симуляция завершена (об этом будет соответствующее сообщение в консоли). По завершению симуляции, в случае отсутствия ошибок, будет выведено сообщение "SUCCESS", в противном случае будут выведены сообщения об этих ошибках.
    5. В случае, если были найдены ошибки, вы должны найти и исправить их. Для этого руководствуйтесь документом.
  5. Добавьте в проект модуль верхнего уровня (nexys_alu.sv), соединяющий АЛУ с периферией в ПЛИС. Описание модуля находится здесь
  6. Подключите к проекту файл ограничений (nexys_a7_100t.xdc, файл ограничений, добавленный в первой лабораторной содержит другие данные, вам необходимо вставить содержимое файла для текущей лабы).
  7. Проверьте работу АЛУ в ПЛИС.