mirror of
https://github.com/MPSU/APS.git
synced 2025-09-16 01:30:10 +00:00
Переработка лабораторных работ, связанных с памятью (#89)
* Переработка лабораторных работ, связанных с памятью Существенно переработаны ЛР3 и ЛР7: Из ЛР3 убрано задание реализовать память данных. Эта память использовалась только студентами ИВТ и только в рамках одной лабы. В итоге использовалась готовая память, и ничего не мешает использовать ее с самого начала. Задание по реализации памяти инструкций также претерпело изменения. Теперь код памяти инструкций предоставляется сразу. Это объясняется тем, что код модуля состоит в общем-то из 4х строк, на которые тратится слишком много времени (с учетом добавления тестбенча и проверок). Кроме того, использование готового кода позволяет дать модуль чуть посложнее (с параметризацией размера). По итогу правок, третья лабораторная работа превращается чисто в лабораторную по написанию регистрового файла, что должно положительно сказаться на кривой сложности лаб. После второй лабы происходит слишком резкий скачок в объемах работы. Соответственно, в связи с тем, что память данных больше не делается на третьей лабе, дополнительная ЛР по памяти данных с byte enable необходимо перенести до реализации тракта данных. * ЛР3, 5, 6. Правки из ревью * ЛР7. Добавление иллюстраций
This commit is contained in:
committed by
GitHub
parent
06dc07c03f
commit
85883858ac
@@ -1,4 +1,4 @@
|
||||
# Лабораторная работа 3 "Регистровый файл и внешняя память"
|
||||
# Лабораторная работа 3 "Регистровый файл и память инструкций"
|
||||
|
||||
Процессор — это программно-управляемое устройство, выполняющее обработку информации и управление этим процессом. Очевидно, программа, которая управляет процессором, должна где-то храниться. Данные, с которыми процессор работает, тоже должны быть в доступном месте. Нужна память!
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
|
||||
Описать на языке SystemVerilog элементы памяти для будущего процессора:
|
||||
|
||||
- память команд (Instruction Memory);
|
||||
- память данных (Data Memory);
|
||||
- память инструкций (Instruction Memory);
|
||||
- регистровый файл (Register File).
|
||||
|
||||
## Допуск к лабораторной работе
|
||||
## Материалы для подготовки к лабораторной работе
|
||||
|
||||
Для успешного выполнения лабораторной работы, вам необходимо освоить:
|
||||
|
||||
@@ -21,7 +20,7 @@
|
||||
|
||||
1. Изучить способы организации памяти (раздел [#теория про память](#теория-про-память)).
|
||||
2. Изучить конструкции SystemVerilog для реализации запоминающих элементов (раздел [#инструменты](#инструменты-для-реализации-памяти)).
|
||||
3. В проекте с прошлой лабораторной реализовать модули: Instruction Memory, Data Memory и Register File ([#задание](#задание-по-реализации-памяти)).
|
||||
3. В проекте с прошлой лабораторной реализовать модули: Instruction Memory и Register File ([#задание](#задание-по-реализации-памяти)).
|
||||
4. Проверить с помощью тестового окружения корректность их работы.
|
||||
5. Проверить работу регистрового файла в ПЛИС.
|
||||
|
||||
@@ -66,7 +65,7 @@ _Рисунок 1. Примеры блоков ПЗУ и ОЗУ._
|
||||
|
||||
_Рисунок 2. Структурная схема логического блока в ПЛИС[[1]](https://en.wikipedia.org/wiki/Field-programmable_gate_array)._
|
||||
|
||||
В логическом блоке есть **таблицы подстановки** (Look Up Table, LUT), которые представляют собой не что иное как память, которая переконфигурируется под нужды хранения, а не реализацию логики. Таким образом, трехвходовой LUT может выступать в роли восьмиразрядной памяти.
|
||||
В логическом блоке есть **таблицы подстановки** (Look Up Table, LUT), которые представляют собой не что иное как память, которая переконфигурируется под нужды хранения, а не реализацию логики. Таким образом, трехвходовой LUT может выступать в роли 8-битной памяти.
|
||||
|
||||
Однако LUT будет сложно приспособить под многопортовую память: посмотрим на схему еще раз: три входа LUT формируют адрес одной из восьми ячеек. Это означает, что среди этих восьми ячеек нельзя обратиться к двум из них одновременно.
|
||||
|
||||
@@ -116,6 +115,12 @@ logic [19:0] memory3 [1:16]; // А вот memory3 хоть и совпадае
|
||||
|
||||
Для обращения к конкретной ячейке памяти используются квадратные скобки с указанием нужного адреса `memory[addr]`. Грубо говоря, то, что указывается в квадратных скобках будет подключено ко входу адреса памяти `memory`.
|
||||
|
||||
Чтение из памяти может быть сделано двумя способами: синхронно и асинхронно.
|
||||
|
||||
Синхронное чтение подразумевает ожидание следующего тактового синхроимпульса для выдачи данных после получения адреса. Иными словами, данные будут установлены на выходе не в тот же такт, когда был выставлен адрес на вход памяти данных, а на следующий. Не смотря на то, что в таком случае на каждой операции чтения "теряется" один такт, память с синхронным чтением имеет значительно меньший критический путь, чем положительно сказывается на временных характеристиках итоговой схемы.
|
||||
|
||||
Память с асинхронным чтением выдает данные в том же такте, что и получает адрес (т.е. ведет себя как комбинационная схема). Несмотря на то, что такой подход кажется быстрее, память с асинхронным чтением обладает длинным критическим путем, причем чем большего объема будет память, тем длиннее будет критический путь.
|
||||
|
||||
Реализация асинхронного подключения к выходу памяти осуществляется оператором `assign`. А, если требуется создать память с синхронным чтением, то присваивание выходу требуется описать внутри блока`always_ff`.
|
||||
|
||||
Так как запись в память является синхронным событием, то описывается она в конструкции `always_ff`. При этом, как и при описании регистра, можно реализовать управляющий сигнал разрешения на запись через блок вида `if(write_enable)`.
|
||||
@@ -186,7 +191,7 @@ module rom16_8 (
|
||||
logic [7:0] ROM [0:15]; // создать память с 16-ю 8-битными ячейками
|
||||
|
||||
initial begin
|
||||
$readmemh("rom_data.mem", ROM); // поместить в память RAM содержимое
|
||||
$readmemh("rom_data.mem", ROM); // поместить в память ROM содержимое
|
||||
end // файла rom_data.mem
|
||||
|
||||
|
||||
@@ -210,11 +215,10 @@ endmodule
|
||||
|
||||
## Задание по реализации памяти
|
||||
|
||||
Необходимо описать на языке SystemVerilog три вида памяти:
|
||||
Необходимо описать на языке SystemVerilog два вида памяти:
|
||||
|
||||
1. память инструкций;
|
||||
2. память данных;
|
||||
3. регистровый файл.
|
||||
2. регистровый файл.
|
||||
|
||||
### 1. Память инструкций
|
||||
|
||||
@@ -234,44 +238,53 @@ mоdulе instr_mеm(
|
||||
|
||||
При этом по спецификации процессор RISC-V использует память с побайтовой адресацией. Байтовая адресация означает, что процессор способен обращаться к отдельным байтам в памяти (за каждым байтом памяти закреплен свой индивидуальный адрес).
|
||||
|
||||
Однако, если у памяти будут 32-рязрядные ячейки, доступ к конкретному байту будет осложнен, ведь каждая ячейка — это 4 байта. Как получить данные третьего байта памяти? Если обратиться к третьей ячейке в массиве — придут данные 12-15-ых байт (поскольку каждая ячейка содержит по 4 байта). Чтобы получить данные третьего байта, необходимо **разделить значение пришедшего адреса на 4** (отбросив остаток от деления). `3 / 4 = 0` — и действительно, если обратиться к нулевой ячейке памяти — будут получены данные 3-го, 2-го, 1-го и 0-го байт. То, что помимо значения третьего байта есть еще данные других байт нас в данный момент не интересует, важна только сама возможность указать адрес конкретного байта.
|
||||
Однако, если у памяти будут 32-битные ячейки, доступ к конкретному байту будет осложнен, ведь каждая ячейка — это 4 байта. Как получить данные третьего байта памяти? Если обратиться к третьей ячейке в массиве — придут данные 12-15-ых байт (поскольку каждая ячейка содержит по 4 байта). Чтобы получить данные третьего байта, необходимо **разделить значение пришедшего адреса на 4** (отбросив остаток от деления). `3 / 4 = 0` — и действительно, если обратиться к нулевой ячейке памяти — будут получены данные 3-го, 2-го, 1-го и 0-го байт. То, что помимо значения третьего байта есть еще данные других байт нас в данный момент не интересует, важна только сама возможность указать адрес конкретного байта.
|
||||
|
||||
Деление на 2<sup>n</sup> можно осуществить, отбросив `n` младших бит числа. Учитывая то, что для адресации 1024 ячеек памяти мы будем использовать 10 бит адреса, память инструкций должна выдавать на выход данные, расположенные по адресу `addr_i[11:2]`.
|
||||
|
||||
### 2. Память данных
|
||||
Не смотря на заданный размер памяти инструкций в 1024 32-битных ячейки, на практике удобно параметризовать это значение, чтобы в ситуациях, когда требуется меньше или больше памяти можно было получить обновленное значение, не переписывая код во множестве мест. Подобное новшество вы сможете оценить на практике, получив возможность существенно сокращать время синтеза процессора, уменьшая размер памяти до необходимого минимума путем изменения значения одного лишь параметра.
|
||||
|
||||
У данного модуля будет шесть входных/выходных сигналов:
|
||||
Для этого можно например создать параметр: `INSTR_MEM_SIZE_BYTES`, показывающий размер памяти инструкций в байтах. Однако, поскольку у данной памяти 32-битные ячейки, нам было бы удобно иметь и параметр `INSTR_MEM_SIZE_WORDS`, который говорит сколько в памяти 32-битных ячеек.
|
||||
При этом `INSTR_MEM_SIZE_WORDS = INSTR_MEM_SIZE_BYTES / 4` (т.е. в 32-битном слове 4 байта).
|
||||
|
||||
- вход тактового синхроимпульса
|
||||
- вход запроса на работу с памятью
|
||||
- вход сигнала разрешения записи
|
||||
- 32-битный вход адреса
|
||||
- 32-битный вход данных записи
|
||||
- 32-битный выход данных синхронного чтения
|
||||
В случае подобной параметризации, необходимо иметь возможность подстраивать количество используемых бит адреса. Для 1024 ячеек памяти мы использовали 10 бит адреса, для 512 ячеек нам потребуется уже 9 бит. Нетрудно заметить, что нам нужно такое число бит данных, возведя в степень которого `2`, мы получим размер нашей памяти (либо число, превышающее этот размер в случае, если размер памяти не является степенью двойки). Иными словами, нам нужен логарифм по основанию 2 от размера памяти, с округлением до целого вверх. И неудивительно, что в SystemVerilog есть специальная конструкция, которая позволяет считать подобные числа. Эта конструкция называется `$clog2` (`с` означает "ceil" — операцию округления вверх).
|
||||
|
||||
Поскольку реализация памяти состоит буквально из нескольких строчек, но при этом использование параметров может вызвать некоторые затруднения, код памяти инструкций предоставляется в готовом виде:
|
||||
|
||||
```SystemVerilog
|
||||
mоdulе data_mеm(
|
||||
inрut logic clk_i,
|
||||
input logic mem_req_i,
|
||||
inрut logic write_enable_i,
|
||||
mоdulе instr_mеm
|
||||
import memory_pkg::INSTR_MEM_SIZE_BYTES;
|
||||
import memory_pkg::INSTR_MEM_SIZE_WORDS;
|
||||
(
|
||||
inрut logic [31:0] addr_i,
|
||||
inрut logic [31:0] write_data_i,
|
||||
оutрut logic [31:0] rеаd_dаtа_o
|
||||
);
|
||||
|
||||
logic [31:0] ROM [INSTR_MEM_SIZE_WORDS]; // создать память с
|
||||
// <INSTR_MEM_SIZE_WORDS>
|
||||
// 32-битных ячеек
|
||||
|
||||
initial begin
|
||||
$readmemh("program.mem", ROM); // поместить в память ROM содержимое
|
||||
end // файла program.mem
|
||||
|
||||
// Реализация асинхронного порта на чтение, где на выход идет ячейка памяти
|
||||
// инструкций, расположенная по адресу addr_i, в котором обнулены два младших
|
||||
// бита, а также биты, двоичный вес которых превышает размер памяти данных
|
||||
// в байтах.
|
||||
// Два младших бита обнулены, чтобы обеспечить выровненный доступ к памяти,
|
||||
// в то время как старшие биты обнулены, чтобы не дать обращаться в память
|
||||
// по адресам несуществующих ячеек (вместо этого будут выданы данные ячеек,
|
||||
// расположенных по младшим адресам).
|
||||
assign rеаd_dаtа_o = R0M[addr_i[$clog2(INSTR_MEM_SIZE_BYTES)-1:2]];
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
Как и память инструкций, память данных будет состоять из 32-разрядных ячеек. Только теперь их будет 4096, а значит при обращении к ячейкам памяти нужно использовать не 10 бит адреса, а 12. При этом по-прежнему необходимо разделить пришедший адрес на 4, т.е. нужно отбросить два младших бита. Таким образом, обращение к ячейкам памяти (для записи и чтения) должно осуществляться по адресу `addr_i[13:2]`.
|
||||
|
||||
Однако в отличие от памяти инструкций, в память данных добавлено два управляющих сигнала (`mem_req_i`и `write_enable_i`). Сигнал `mem_req_i` является сигналом запроса на работу с памятью. Без этого сигнала память не должна выполнять операции чтения/записи (вне зависимости от сигнала `write_enable`, определяющего происходит сейчас запись или чтение). Как сделать так, чтобы не происходило чтение без запроса? Например, не обновлять значение, считанное во время предыдущей операции чтения.
|
||||
|
||||
Если `mem_req_i == 1` и `write_enable_i == 1`, то происходит запрос на запись в память. В этом случае, необходимо записать значение `write_data_i` в ячейку по адресу `addr_i[13:2]`. Во всех других случаях (любой из сигналов `mem_req_i`, `write_enable_i` равен нулю), запись в память не производится.
|
||||
|
||||
Если `mem_req_i == 1` и `write_enable_i == 0`, то происходит запрос на чтение из памяти. В этом случае, необходимо записать в выходной регистр `read_data_o` значение из ячейки по адресу `addr_i[13:2]`. Во всех других случаях чтение из памяти не производится (`read_data_o` сохраняет предыдущее значение).
|
||||
|
||||
|
||||
### 3. Регистровый файл
|
||||
|
||||
На языке SystemVerilog необходимо реализовать модуль регистрового файла (`rf_r𝚒sсv`) для процессора с архитектурой RISC-V, представляющего собой трехпортовое ОЗУ с двумя портами на чтение и одним портом на запись и состоящей из 32-х 32-битных регистров, объединенных в массив с именем `rf_mem`.
|
||||
|
||||
У данного модуля будет восемь входных/выходных сигналов:
|
||||
|
||||
- вход тактового синхроимпульса
|
||||
@@ -299,46 +312,24 @@ mоdulе rf_r𝚒sсv(
|
||||
|
||||
```
|
||||
|
||||
На языке SystemVerilog необходимо реализовать модуль регистрового файла (`rf_r𝚒sсv`) для процессора с архитектурой RISC-V, представляющего собой трехпортовую ОЗУ с двумя портами на чтение и одним портом на запись и состоящей из 32-х 32-битных регистров с именем `rf_mem`.
|
||||
По адресу `0` должно всегда считываться значение `0` вне зависимости от того, какое значение в этой ячейке памяти, и есть ли она вообще. Такая особенность обусловлена тем, что при выполнении операций очень часто используется ноль (сравнение с нулем, инициализация переменных нулевым значением, копирование значения одного регистра в другой посредством сложения с нулем и записи результата и т.п.). Эту особенность регистрового файла можно реализовать несколькими способами:
|
||||
|
||||
При этом по адресу `0` должно всегда считываться значение `0` вне зависимости от того, какое значение в этой ячейке памяти, и есть ли она вообще. Такая особенность обусловлена тем, что при выполнении операций очень часто используется ноль (сравнение с нулем, инициализация переменных нулевым значением, копирование значения одного регистра в другой посредством сложения с нулем и записи результата и т.п.).
|
||||
- можно решить эту задачу с помощью мультиплексора, управляющим сигналом которого является сигнал сравнения адреса на чтение с нулем;
|
||||
- либо же можно проинициализировать нулевую ячейку памяти нулем с запретом записи в неё каких-либо значений. В этом случае в ячейке всегда будет ноль, а значит и считываться с нулевого адреса будет только он.
|
||||
|
||||
Как и в случае реализации [памяти инструкций](#1-память-инструкций), вы можете решить эту задачу с помощью мультиплексора, управляющим сигналом которого является сигнал сравнения адреса на чтение с нулем.
|
||||
|
||||
Либо же можно проинициализировать нулевую ячейку памяти нулем с запретом записи в неё каких-либо значений. В этом случае в ячейке всегда будет ноль, а значит и считываться с нулевого адреса будет только он.
|
||||
Инициализация ячейки памяти может быть осуществлена (только при проектировании под ПЛИС) с помощью присваивания в блоке `initial`.
|
||||
|
||||
## Порядок выполнения работы
|
||||
|
||||
1. Внимательно ознакомьтесь с заданием. В случае возникновения вопросов, проконсультируйтесь с преподавателем.
|
||||
2. Реализуйте память инструкций. Для этого:
|
||||
2. Добавьте в проект файл [`memory_pkg.sv`](memory_pkg.sv). Этот файл содержит объявление пакета `memory_pkg`, в котором прописаны размеры памяти инструкций и памяти данных (реализуется позднее).
|
||||
3. Реализуйте память инструкций. Для этого:
|
||||
1. В `Design Sources` проекта с предыдущих лаб, создайте `SystemVerilog`-файл `instr_mem.sv`.
|
||||
2. Опишите в нем модуль памяти инструкций с таким же именем и портами, как указано в задании.
|
||||
1. Сперва необходимо создать память (массив регистров). Как это сделать, сказано в разделе [описание памяти на языке SystemVerilog](#описание-памяти-на-языке-systemverilog). Разрядность ячеек памяти должна быть 32 бита, количество ячеек — 1024.
|
||||
2. Добавить в `Design Sources` проекта [`файл с содержимым памяти инструкций`](program.mem). Данный файл будет использоваться при вызове системной функции `$readmemh` в описании памяти инструкций.
|
||||
3. К созданной памяти необходимо подключить выход модуля `read_data_o`. При подключении должен быть использован вход модуля `addr_i`, значение которого должно быть уменьшено в 4 раза (побайтовая адресация).
|
||||
4. При реализации выхода `read_data_o` помните, что обращаясь к ячейке памяти, вам необходимо использовать `[11:2]` биты адреса.
|
||||
5. Реализуемый порт на чтение памяти инструкций должен быть **асинхронным**.
|
||||
3. После описания памяти инструкций, её необходимо проверить с помощью тестового окружения.
|
||||
1. Тестовое окружение находится [`здесь`](tb_instr_mem.sv).
|
||||
2. Для запуска симуляции воспользуйтесь [`этой инструкцией`](../../Vivado%20Basics/Run%20Simulation.md).
|
||||
3. Перед запуском симуляции убедитесь, что в качестве top-level модуля выбран корректный (`tb_instr_mem`).
|
||||
4. **Во время симуляции, вы должны прожать "Run All" и убедиться, что в логе есть сообщение о завершении теста!**
|
||||
3. Реализуйте память данных. Для этого:
|
||||
1. В `Design Sources` проекта создайте `SystemVerilog`-файл `data_mem.sv`.
|
||||
2. Опишите в нем модуль памяти данных с таким же именем и портами, как указано в задании.
|
||||
1. Описание модуля будет схожим с описанием модуля памяти инструкций, однако порт чтения в этот раз будет **синхронным** (запись в него будет происходить в блоке `always_ff`). Количество ячеек в памяти данных — 4096. Кроме того, необходимо будет описать логику записи данных в память.
|
||||
2. Запись в ячейки памяти описывается подобно записи данных в [регистры](../../Basic%20Verilog%20structures/Registers.md), только при этом, происходит доступ к конкретной ячейке памяти с помощью входа `addr_i` (как осуществляется доступ к ячейкам памяти сказано в разделе [описание памяти на языке SystemVerilog](#описание-памяти-на-языке-systemverilog)).
|
||||
3. Доступ к ячейкам (на запись и чтение) осуществляется по адресу `addr_i[13:2]`.
|
||||
4. Обратите внимание что работа с памятью должна осуществляться только когда сигнал `mem_req_i == 1`, в противном случае запись не должна производиться, а на шине `read_data_o` должен оставаться результат предыдущего чтения.
|
||||
3. После описания памяти данных, её необходимо проверить с помощью тестового окружения.
|
||||
1. Тестовое окружение находится [`здесь`](tb_data_mem.sv).
|
||||
2. Для запуска симуляции воспользуйтесь [`этой инструкцией`](../../Vivado%20Basics/Run%20Simulation.md).
|
||||
3. Перед запуском симуляции убедитесь, что в качестве top-level модуля выбран корректный (`tb_data_mem`).
|
||||
4. **Во время симуляции, вы должны прожать "Run All" и убедиться, что в логе есть сообщение о завершении теста!**
|
||||
2. Опишите в нем модуль памяти инструкций по предоставленному коду.
|
||||
4. Реализуйте регистровый файл. Для этого:
|
||||
1. В `Design Sources` проекта создайте `SystemVerilog`-файл `rf_riscv.sv`.
|
||||
2. Опишите в нем модуль регистрового файла с таким же именем и портами, как указано в задании.
|
||||
1. Обратите внимание, что имя памяти (не название модуля, а имя объекта памяти внутри модуля) должно быть `rf_mem`. Такое имя необходимо для корректной работы верификационного окружения.
|
||||
1. Обратите внимание, что имя памяти (не название модуля, а имя массива регистров внутри модуля) должно быть `rf_mem`. Такое имя необходимо для корректной работы верификационного окружения.
|
||||
2. Как и у памяти инструкций, порты чтения регистрового файла должны быть **асинхронными**.
|
||||
3. Не забывайте, что у вас 2 порта на чтение и 1 порт на запись, при этом каждый порт не зависит от остальных (в модуле 3 независимых входа адреса).
|
||||
4. Чтение из нулевого регистра (чтение по адресу 0) всегда должно возвращать нулевое значение. Этого можно добиться двумя путями:
|
||||
|
Reference in New Issue
Block a user