# Лабораторная работа 8 "Блок загрузки и сохранения"

Итогом шестой лабораторной работы стал практически завершенный процессор архитектуры RISC-V. Особенностью той реализации процессора было отсутствие поддержки инструкций `LB`, `LBU`, `SB`, `LH`, `LHU`, `SH`. Тому было две причины:

- в третьей лабораторной работе была реализована память данных, не поддерживавшая возможность обновления отдельных байт;
- необходимо подготовить считанные из памяти полуслова / байты для записи в регистровый файл.

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

Для этих целей используется специальный модуль — **Блок загрузки и сохранения** (**Load and Store Unit**, **LSU**).

## Цель

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

---

## Ход работы

Изучить:

- Функции и задачи блока загрузки/сохранения
- Интерфейс процессора и блока загрузки/сохранения
- Интерфейс блока загрузки/сохранения и памяти

Реализовать и проверить модуль `riscv_lsu`.

---

## Теория

Модуль загрузки и сохранения (**Load/Store Unit** – **LSU**) служит для исполнения инструкций типа `LOAD` и `STORE`: является прослойкой между внешним устройством – памятью, и ядром процессора. **LSU** считывает содержимое из памяти данных или записывает в нее требуемые значения, преобразуя 8- и 16-битные данные в знаковые или беззнаковые 32-битные числа для регистров процессора. В процессорах с **RISC** архитектурой с помощью **LSU** осуществляется обмен данными между регистрами общего назначения и памятью данных.

![../../.pic/Labs/lab_08_lsu/fig_01.drawio.png](../../.pic/Labs/lab_08_lsu/fig_01.drawio.png)

_Рисунок 1. Место LSU в микроархитектуре RISC-процессора_

### Интерфейс процессора и блока загрузки/сохранения

Параграф посвящен описанию сигналов и правил взаимодействия между процессором и блоком загрузки/сохранения **LSU** **(core protocol)**.

На входной порт `core_addr_i` от процессора поступает адрес ячейки памяти, к которой будет произведено обращение. Намеренье процессора обратиться к памяти (и для чтения, и для записи) отражается выставлением сигнала `core_req_i` в единицу. Если процессор собирается записывать в память, то сигнал `core_we_i` выставляется в единицу, а сами данные, которые следует записать, поступают от него на вход `core_wd_i`. Если процессор собирается читать из памяти, то сигнал `core_we_i` находится в нуле, а считанные данные подаются для процессора на выход `core_rd_o`.

Инструкции `LOAD` и `STORE` в **RV32I** поддерживают обмен 8-битными, 16-битными или 32-битными значениями, однако в самом процессоре происходит работа только с 32-битными числами, поэтому загружая байты или полуслова из памяти их необходимо предварительно расширить до 32-битного значения. Для выбора разрядности на вход **LSU** подается сигнал `core_size_i`, принимающий следующие значения:

| Название |Значение|           Пояснение           |
|----------|--------|-------------------------------|
|  LDST_B  |  3'd0  |Знаковое 8-битное значение     |
|  LDST_H  |  3'd1  |Знаковое 16-битное значение    |
|  LDST_W  |  3'd2  |32-битное значение             |
|  LDST_BU |  3'd4  |Беззнаковое 8-битное значение  |
|  LDST_HU |  3'd5  |Беззнаковое 16-битное значение |

Формат представления числа (является оно **знаковым** или **беззнаковым**) имеет значение только для операций типа `LOAD`: если число знаковое, то производится расширение знака до 32 бит, а если беззнаковое – расширение нулями.

Для операций типа `STORE` формат представления чисел не важен, для них `core_size_i` сможет принимать значение только от 0 до 2.

Выходной сигнал `core_stall_o` нужен для остановки программного счетчика. Ранее логика этого сигнала временно находилась в модуле `riscv_unit` — теперь она займет свое законное место в модуле **LSU**.

### Интерфейс блока загрузки/сохранения и памяти

В параграфе описывается организация внешней памяти, и то, как к ней подключается **LSU**.

Память данных имеет 32-битную разрядность ячейки памяти и поддерживает побайтовую адресацию. Это значит, что существует возможность записи значения по одному байту в пределах одного слова (4-байтовой ячейки памяти). Для указания на необходимые байты интерфейс к памяти предусматривает использование 4-битного сигнала `mem_be_o`, подаваемого вместе с адресом слова `mem_addr_o`. Позиции битов 4-битного сигнала соответствуют позициям байтов в слове. Если конкретный бит `mem_be_o` равен 1, то соответствующий ему байт будет записан в память. Данные для записи подаются на выход `mem_wd_o`. На результат чтения из памяти состояние `mem_be_o` не влияет, так как чтение производится всегда по 32-бита.

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

- сигнал `mem_req_o` сообщает памяти о наличии запроса в память (напрямую подключен к `core_req_i`);
- сигнал `mem_we_o` сообщает памяти о типе этого запроса (напрямую подключен к `core_we_i`):
  - `mem_we_o` равен 1, если отправлен запрос на запись,
  - `mem_we_o` равен 0, если отправлен запрос на чтение;
- сигнал `mem_wd_o` содержит данные на запись в память. В зависимости от размера записи, данные этого сигнала будут отличаться от пришедшего сигнала `core_wd_i` и будут является результатом определенных преобразований.
- сигнал `mem_rd_i` содержит считанные из памяти данные. Перед тем, как вернуть считанные данные ядру через выходной сигнал `core_rd_o`, эти данные будет необходимо подготовить.
- сигнал `mem_ready_i` сообщает о готовности памяти завершить транзакцию на текущем такте. Этот сигнал используется для управления выходным сигналом `core_stall_o`.

<!-- |Команды| Byte Offset |              lsu_data_o                         |
|-------|-------------|-------------------------------------------------|
|  lb   |     00      | {{24{data_rd_i[7]}}, data_rd_i[7:0]}      |
|       |     01      | {{24{data_rd_i[15]}}, data_rd_i[15:8]}    |
|       |     10      | {{24{data_rd_i[23]}}, da-ta_rd_i[23:16]}  |
|       |     11      | {{24{data_rd_i[31]}}, da-ta_rd_i[31:24]}  |
|  lh   |     00      | {{16{data_rd_i[15]}}, da-ta_rd_i[15:0]}   |
|       |     10      | {{16{data_rd_i[31]}}, da-ta_rd_i[31:16]}  |
|  lw   |     00      | data_rd_i[31:0]                              |
|  lbu  |     00      | {24’b0, data_rd_i[7:0]}                      |
|       |     01      | {24’b0, data_rd_i[15:8]}                     |
|       |     10      | {24’b0, data_rd_i[23:16]}                    |
|       |     11      | {24’b0, data_rd_i[31:24]}                    |
|  lhu  |     00      | {16’b0, data_rd_i[15:0]}                     |
|       |     10      | {16’b0, data_rd_i[31:16]}                    | -->

<!-- |Команды| Byte Offset |      data_wd_o      |         data_be_o         |
|  sb   |      00     | { 4{lsu_data_i[7:0]} } |           0001            |
|       |      01     |                        |           0010            |
|       |      10     |                        |           0100            |
|       |      11     |                        |           1000            |
|  sh   |      00     | { 2{lsu_data_i[15:0]} }|           0011            |
|       |      10     |                        |           1100            |
|  sw   |      00     | lsu_data_i[31:0]       |           1111            | -->

---

## Практика

> Познай как описать выходные сигналы модуля — и ты познаешь как описать сам модуль. ©Джейсон Стейтем

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

### mem_req_o, mem_we_o, mem_addr_o

Все эти сигналы подключаются напрямую к соответствующим core-сигналам:

- `mem_req_o` к `core_req_i`;
- `mem_we_o` к `core_we_i`;
- `mem_addr_o` к `core_addr_i`.

### mem_be_o

По запросу на запись (`core_req_i == 1`, `core_we_i == 1`), если `core_size_i` соответствует инструкции записи байта (`SB`), то в сигнале `mem_be_o` бит с индексом равным значению двух младших бит адреса `core_addr_i` должен быть равен единице.

Допустим, пришел запрос на запись байта по адресу 18:

- `core_req_i == 1`,
- `core_we_i == 1`,
- `core_size_i == LDST_B`
- `core_addr_i == 32'b10010`

В данном случае, необходимо выставить единицу во втором (считая с нуля) бите сигнала `mem_be_o` (поскольку значение двух младших бит `core_addr_i` равно двум): `mem_be_o == 4'b0100`.

Если пришел запрос на запись полуслова (`SH`, `core_size_i == LDST_H`), то в сигнале `mem_be_o` необходимо выставить в единицу либо два старших, либо два младших бита (в зависимости от `core_addr[1]`)

Если пришел запрос на запись слова (`SW`, `core_size_i == LDST_W`), то в сигнале `mem_be_o` необходимо выставить в единицу все биты.

### mem_wd_o

Сигнал `mem_wd_o` функционально связан с сигналом `mem_be_o`, т.к. они оба выполняют функцию записи конкретных байт в памяти. Допустим процессор хочет записать байт `0xA5` по адресу 18. Для этого он формирует сигналы:

- `core_req_i == 1`,
- `core_we_i == 1`,
- `core_size_i == LDST_B`
- `core_addr_i == 32'b10010`
- `core_wd_i == 32h0000_00A5`

Мы уже знаем, что `mem_be_o` должен быть при этом равен `4'b0100`. Однако если в память придут сигналы:

- `mem_be_o == 4'b0100`,
- `mem_wd_o == 32'h0000_00A5`

то по адресу 18 будет записано значение `0x00` (поскольку второй байт на шине `mem_wd_o` равен нулю).

Для того, чтобы по 18-ому адресу записалось значение `A5`, это значение должно оказаться во втором байте `mem_wd_o`. А в случае 17-го адреса, значение должно оказаться в первом байте и т.п.

Получается, что в случае записи байта, проще всего продублировать записываемый байт во все байты шины `mem_wd_o`, ведь в память запишется только тот, которому будет соответствовать бит `mem_be_o`, равный единице. Дублирование можно осуществить с помощью [конкатенации](../../Basic%20Verilog%20structures/Concatenation.md).

В случае записи полуслова (`core_size_i == LDST_H`) ситуация схожа, только теперь дублировать надо не 1 байт 4 раза, а полслова (16 младших бит шины `core_wd_i`) два раза.

В случае записи слова (`core_size_i == LDST_W`), сигнал `mem_wd_o` будет повторять сигнал `core_wd_i`.

### core_rd_o

Сигнал `core_rd_o` — это сигнал, который будет содержать данные для записи в регистровый файл процессора во время инструкций загрузки из памяти (`LW`, `LH`, `LHU`, `LB`, `LBU`). Для того, чтобы понять как управлять этим сигналом, нужно понять что происходит во время этих инструкций.

Предположим, по адресам `16-19` лежит слово `32'hA55A_1881`. Чтение по любому из адресов 16, 17, 18, 19 вернет это слово на входном сигнале `mem_rd_i`. В случае инструкции `LB` (`core_size_i == LDST_B`) по адресу 19 (чтение байта, который интерпретируется как знаковое число), в регистровый файл должно быть записано значение `32'hFFFF_FFA5`, поскольку по 19-ому адресу лежит байт `A5`, который затем будет знакорасширен. В случае той же самой инструкции, но по адресу 18, в регистровый файл будет записано значение `32'h0000_005A` (знакорасширенный байт `5A`, расположенный по 18ому адресу).

Получить нужный байт можно из входного сигнала `mem_rd_i`, но чтобы понять какие биты этого сигнала нас интересуют, необходимо посмотреть на входные сигналы `core_size_i` и `core_addr_i[1:0]`. `core_size_i` сообщит конкретный тип инструкции (сколько нужно взять байт из считанного слова), а `core_addr_i[1:0]` укажет номер начального байта, который нужно взять из `mem_rd_i`.

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

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

Для инструкций `LW` на выход `core_rd_o` пойдут данные `mem_rd_i` без изменений.

### core_stall_o

Сигнал `core_stall_o` запрещает менять значение программного счетчика на время обращения в память. Этот сигнал должен:

- стать равным единице в тот же такт, когда пришел сигнал `core_req_i`
- удерживать это значение до тех пор, пока не придет сигнал `mem_ready_i`, но не менее 1 такта (т.е. даже если сигнал `mem_ready_i` будет равен единице, `core_req_i` должен подняться хотя бы на 1 такт).

Для реализации подобного функционала вам потребуется вспомогательный регистр `stall_reg`, каждый такт записывающий значение выхода `core_stall_o` и следующая таблица истинности для этого выхода:

![../../.pic/Labs/lab_08_lsu/fig_02.png](../../.pic/Labs/lab_08_lsu/fig_02.png)

_Рисунок 2. Таблица истинности выхода `core_stall_o`_

---

## Задание

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

```SystemVerilog
module riscv_lsu(
  input logic clk_i,
  input logic rst_i,

  // Интерфейс с ядром
  input  logic        core_req_i,
  input  logic        core_we_i,
  input  logic [ 2:0] core_size_i,
  input  logic [31:0] core_addr_i,
  input  logic [31:0] core_wd_i,
  output logic [31:0] core_rd_o,
  output logic        core_stall_o,

  // Интерфейс с памятью
  output logic        mem_req_o,
  output logic        mem_we_o,
  output logic [ 3:0] mem_be_o,
  output logic [31:0] mem_addr_o,
  output logic [31:0] mem_wd_o,
  input  logic [31:0] mem_rd_i,
  input  logic        mem_ready_i
);

```

![../../.pic/Labs/lab_08_lsu/fig_03.drawio.png](../../.pic/Labs/lab_08_lsu/fig_03.drawio.png)

_Рисунок 3. Структурная схема модуля `riscv_lsu`_

---

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

1. Внимательно ознакомьтесь с описанием функционального поведения выходов **LSU**. В случае возникновения вопросов, проконсультируйтесь с преподавателем.
2. Реализуйте модуль `riscv_lsu`. Для этого:
   1. В `Design Sources` проекта с предыдущих лаб, создайте `SystemSystemVerilog`-файл `riscv_lsu.sv`.
   2. Опишите в нем модуль `riscv_lsu` с таким же именем и портами, как указано в [задании](#задание).
      1. При описании обратите внимание на то, что большая часть модуля является чисто комбинационной. В этом плане реализация модуля будет частично похожа на реализацию декодера.
      2. Однако помимо комбинационной части, в модуле будет присутствовать и один регистр.
3. После описания модуля, его необходимо проверить с помощью тестового окружения.
   1. Тестовое окружение находится [здесь](tb_lsu.sv).
   2. Для запуска симуляции воспользуйтесь [`этой инструкцией`](../../Vivado%20Basics/Run%20Simulation.md).
   3. Перед запуском симуляции убедитесь, что в качестве top-level модуля выбран корректный (`tb_lsu`).
   4. Во время симуляции, вы должны прожать "Run All" и убедиться, что в логе есть сообщение о завершении теста!

