diff --git a/.pic/Labs/lab_03_memory/fig_03.drawio.png b/.pic/Labs/lab_03_memory/fig_03.drawio.png index 03fccc4..7090b81 100644 Binary files a/.pic/Labs/lab_03_memory/fig_03.drawio.png and b/.pic/Labs/lab_03_memory/fig_03.drawio.png differ diff --git a/.pic/Labs/lab_03_memory/fig_04.drawio.png b/.pic/Labs/lab_03_memory/fig_04.drawio.png deleted file mode 100644 index d67f2bc..0000000 Binary files a/.pic/Labs/lab_03_memory/fig_04.drawio.png and /dev/null differ diff --git a/Labs/03. Register file and memory/README.md b/Labs/03. Register file and memory/README.md index c6bf158..219e1e6 100644 --- a/Labs/03. Register file and memory/README.md +++ b/Labs/03. Register file and memory/README.md @@ -224,27 +224,15 @@ mоdulе instr_mеm( ``` -Из [теории про память](#теория-про-память) вы могли догадаться, что такой модуль описывает память ёмкостью `4 GiB`. Однако в реальности, наша память будет куда меньше (в ПЛИС попросту не хватит ресурсов на реализацию памяти подобного объёма). +Из [теории про память](#теория-про-память) вы могли догадаться, что такой модуль описывает память ёмкостью `4 GiB`. Однако в реальности, наша память будет куда меньше (в ПЛИС попросту не хватит ресурсов на реализацию памяти подобного объёма). На практике, внутри данного модуля вы должны будете реализовать память с 1024-мя 32-битными ячейками. -На практике, внутри данного модуля вы должны будете реализовать память с 1024-мя 8-битными ячейками. +При этом по спецификации процессор RISC-V использует память с побайтовой адресацией. Байтовая адресация означает, что процессор способен обращаться к отдельным байтам в памяти (за каждым байтом памяти закреплен свой индивидуальный адрес). -Как так вышло, что разрядность ячеек отличается от разрядности выходного сигнала? -Дело в том, что по спецификации процессор RISC-V должен работать с памятью с побайтовой адресацией. Байтовая адресация означает, что процессор способен обращаться к отдельным байтам в памяти (за каждым байтом памяти закреплен свой индивидуальный адрес). +Однако, если у памяти будут 32-рязрядные ячейки, доступ к конкретному байту будет осложнен, ведь каждая ячейка — это 4 байта. Как получить данные третьего байта памяти? Если обратиться к третьей ячейке в массиве — придут данные 12-15-ых байт байт (поскольку каждая ячейка содержит по 4 байта). Чтобы получить данные третьего байта, необходимо разделить пришедший адрес на 4 (отбросив остаток от деления). `3 / 4 = 0` — и действительно, если обратиться к нулевой ячейке памяти — будут получены данные 3-го, 2-го, 1-го и 0-го байт. То что помимо значения третьего байта есть еще данные других байт нас в данный момент не интересует, важна только сама возможность указать адрес конкретного байта. -Именно поэтому, ячейки памяти должны быть восьмибитными. +Деление на `2n` можно осуществить отбросив `n` младших бит числа. Таким образом на выход память инструкций должна выдавать данные, расположенные по адресу addr_i[31:2]; -Однако в то же время, инструкции процессора будут 32-битными (состоящими из 4 подряд идущих байт), поэтому выходной сигнал данной памяти будет 32-разрядным. -Как сделать так, чтобы подавать на выход 4 подряд идущих байта? С помощью [конкатенации](../../Basic%20Verilog%20structures/Concatenation.md). - -На вход реализуемого вами модуля приходит адрес младшего байта инструкции, которую вы должны вернуть (этот адрес всегда будет кратен четырем, поскольку в наших лабах инструкции всегда будут состоять из 4 байт). - -На выход необходимо подать 32-разрядную склейку: - -![../../.pic/Labs/lab_03_memory/fig_03.drawio.png](../../.pic/Labs/lab_03_memory/fig_03.drawio.png) - -*Рисунок 3. Пример склейки для чтения* - -Обращение в эту память по адресам, превышающим `1020` должны выдавать значение `32'd0`. Почему именно `1020`? Если обратиться по адресу `1021` (чего произойти не может, т.к. адрес всегда будет кратен четырем, но предположим что такое обращение все-таки произошло), то вы должны будете вернуть данные из ячеек памяти по адресам: `1021`, `1022`, `1023` и `1024`. При этом ячейки по адресу 1024 уже не будет (ведь 1024 ячейки расположатся по адресам `[0:1023]`). +Обращение в память по адресам, превышающим `4095` должно выдавать значение `32'd0`. Почему именно `4095`? `4095 / 4 = 1023` — индекс последней ячейки памяти. Как реализовать подобный функционал? Разумеется, с помощью [мультиплексора](../../Basic%20Verilog%20structures/Multiplexors.md). @@ -252,7 +240,7 @@ mоdulе instr_mеm( ### 2. Память данных -У данной памяти будет 6 портов: +У данной памяти будет 8 портов: - вход тактового синхроимпульса - вход запроса на работу с памятью @@ -273,21 +261,21 @@ mоdulе data_mеm( ``` -Идея этой памяти повторяет идею памяти инструкций: несмотря на разрядность адреса и данных, внутри будет память, состоящая из 4096-и 8-битных ячеек. +Как и память инструкций, память данных будет состоять из 32-разрядных ячеек (только теперь их будет 4096), и при обращении к этим ячейкам будет необходимо делить адрес на 4. Однако в отличие от памяти инструкций, в память данных добавлено два управляющих сигнала (`mem_req_i`и `write_enable_i`). Сигнал `mem_req_i` является сигналом запроса на работу с памятью. Без этого сигнала память не должна выполнять операции чтения/записи (вне зависимости от сигнала `write_enable`, определяющего происходит сейчас запись или чтение). Как сделать так, чтобы не происходило чтение без запроса? Например возвращать на шину чтения специальное "магическое число". - В случае `mem_req_i == 0` или `write_enable_i == 1` (т.е. когда не выполняется операция чтения), на выходе `read_data_o` должно оказаться значение `32'hfa11_1eaf` (поскольку `1` схожа с латинским символом `l`, это выражение можно прочесть как `fall_leaf`). -- В случае, если `mem_req_i == 1` и значение `addr_i` **попадает** в диапазон [0:4095], на выходе `read_data_o` должна оказаться склейка из 4 значений ячеек памяти, начиная ячейки, размещенной по пришедшему адресу (обратите внимание, что в отличие от памяти инструкций, мы **не уменьшаем** диапазон допустимых значений до `[0:4092]`, поскольку архитектура RISC-V допускает обращение в одну отдельную ячейку памяти). -- В случае, если `mem_req_i == 1` и значение `addr_i` **не попадает** в диапазон [0:4095], на выходе `read_data_o` должно оказаться магическое число `32'hdead_beef`. +- В случае, если `mem_req_i == 1` и значение `addr_i` **попадает** в диапазон `[0:16383]` (4096*4-1), на выходе `read_data_o` должно оказаться значение ячейки по адресу в 4 раза меньше пришедшего. +- В случае, если `mem_req_i == 1` и значение `addr_i` **не попадает** в диапазон `[0:16383]`, на выходе `read_data_o` должно оказаться магическое число `32'hdead_beef`. Зачем нужны эти магические числа `32'hfa11_1eaf` и `32'hdead_beef`? У этих чисел легко узнаваемая сигнатура, позволяющая обратить на них внимание. В случае, если при чтении из памяти в регистровый файл попадут эти значения, увидев их вы сможете почувствовать что "что-то не то", и проверить: а было ли в памяти по указанному адресу действительно такое значение (в отличие от значения `32'h0000_0000`, которое не вызовет у вас никаких вопросов). Вероятность того, что такие числа возникнут в естественном ходе работы программы достаточно мала, а значит скорее всего если вы встретите эти числа — это сигнал того, что что-то в вашем процессоре работает неправильно (например, произошло обращение за пределы памяти, или неправильно формируется сигнал `mem_req_i`). -Если `mem_req_i == 1` и `write_enable_i == 1` происходит запрос на запись в память. В этом случае, необходимо по положительному фронту `clk_i` записать в четыре подряд идущие ячейки, начиная с той, что расположена по адресу `addr_i` соответствующие байты с шины `write_data_i`. Во всех других случаях (любой из сигналов `mem_req_i`, `write_enable_i` равен нулю), запись в память не производится. +Если `mem_req_i == 1` и `write_enable_i == 1` происходит запрос на запись в память. В этом случае, необходимо по положительному фронту `clk_i` записать в значение `write_data_i` в ячейку по адресу в 4 раза меньшему `addr_i`. Во всех других случаях (любой из сигналов `mem_req_i`, `write_enable_i` равен нулю), запись в память не производится. -![../../.pic/Labs/lab_03_memory/fig_04.drawio.png](../../.pic/Labs/lab_03_memory/fig_04.drawio.png) +Поскольку мы описываем память с синхронным чтением, было бы неплохо, чтобы в результате мы получили блочную память (см. [теорию про память](#теория-про-память)). Однако блочная память — это заранее созданный аппаратный блок памяти, в котором нет места придуманным нами магическим числам, поэтому описывая порт на чтение, сперва лучше описать регистр, в который по запросу на работу с памятью всегда будет записываться значение из соответствующей ячейки. А уже после можно описать выход `rеаd_dаtа_o` перед которым будет стоять мультиплексор с тремя входами: константами `32'hfa11_1eaf`, `32'hdead_beaf` и значением с выхода описанного вами регистра: -*Рисунок 4. Пример склейки для записи* +![../../.pic/Labs/lab_03_memory/fig_03.drawio.png](../../.pic/Labs/lab_03_memory/fig_03.drawio.png) ### 3. Регистровый файл @@ -326,22 +314,16 @@ mоdulе rf_r𝚒sсv( Либо же можно проинициализировать нулевую ячейку памяти нулем с запретом записи в неё каких-либо значений. В этом случае в ячейке всегда будет ноль, а значит и считываться с нулевого адреса будет только он. -### 4. Проверка в ПЛИС - -Последним этапом будет проверка работоспособности вашего регистрового файла в ПЛИС (файлы для прошивки как обычно расположены в папке [board files](board%20files)). - -Разработанные блоки будут использованы при реализации процессора `CYBERcobra 3000 Pro 2.0` и последующих лабораторных для реализации системы с процессором с архитектурой RISC-V. - ## Порядок выполнения работы 1. Внимательно ознакомьтесь с заданием. В случае возникновения вопросов, проконсультируйтесь с преподавателем. 2. Реализуйте память инструкций. Для этого: 1. В `Design Sources` проекта с предыдущих лаб, создайте `SystemVerilog`-файл `instr_mem.sv`. 2. Опишите в нем модуль памяти инструкций с таким же именем и портами, как указано в задании. - 1. Сперва необходимо создать память (массив регистров). Как это сделать, сказано в разделе [описание памяти на языке SystemVerilog](#описание-памяти-на-языке-systemverilog). Разрядность ячеек памяти должна быть 8 бит, количество ячеек — 1024. + 1. Сперва необходимо создать память (массив регистров). Как это сделать, сказано в разделе [описание памяти на языке SystemVerilog](#описание-памяти-на-языке-systemverilog). Разрядность ячеек памяти должна быть 32 бита, количество ячеек — 1024. 2. Добавить в проект [`файл с содержимым памяти инструкций`](program.txt)([`как добавить файл, инициализирующий память`](../../Vivado%20Basics/How%20to%20add%20a%20mem-file.md)). Данный файл будет использоваться при вызове системной функции `$readmemh` в описании памяти инструкций. - 3. К созданной памяти необходимо подключить выход модуля `read_data_o`. При подключении должен быть использован вход модуля `addr_i` и оператор [конкатенации](../../Basic%20Verilog%20structures/Concatenation.md). - 4. При подключении выхода `read_data_o` помните, что чтение по адресам, превышающим `1020` должно возвращать `0`. + 3. К созданной памяти необходимо подключить выход модуля `read_data_o`. При подключении должен быть использован вход модуля `addr_i`, значение которого должно быть уменьшено в 4 раза (побайтовая адресация). + 4. При подключении выхода `read_data_o` помните, что чтение по адресам, превышающим `4095` должно возвращать `0`. 3. После описания памяти инструкций, её необходимо проверить с помощью тестового окружения. 1. Тестовое окружение находится [`здесь`](tb_instr_mem.sv). 2. Для запуска симуляции воспользуйтесь [`этой инструкцией`](../../Vivado%20Basics/Run%20Simulation.md). @@ -352,9 +334,9 @@ mоdulе rf_r𝚒sсv( 2. Опишите в нем модуль памяти данных с таким же именем и портами, как указано в задании. 1. Описание модуля будет схожим с описанием модуля памяти инструкций, однако порт чтения в этот раз будет **синхронным** (запись в него будет происходить в блоке `always_ff`). Кроме того необходимо будет описать логику записи данных в память. 2. Запись в ячейки памяти описывается подобно записи данных в [регистры](../../Basic%20Verilog%20structures/Registers.md), только при этом, происходит доступ к конкретной ячейке памяти с помощью входа `addr_i` (как осуществляется доступ к ячейкам памяти сказано в разделе [описание памяти на языке SystemVerilog](#описание-памяти-на-языке-systemverilog)). - 3. Необходимо помнить, что запись будет вестись в 4 ячейки памяти одновременно: ту, на которую указывает адрес и следующие за ней три ячейки. + 3. Необходимо помнить, что запись будет вестись в ячейку с индексом в 4 раза меньшим пришедшего адреса. 4. Обратите внимание что работа с памятью должна осуществляться только когда сигнал `mem_req_i == 1`, в противном случае запись не должна производиться, а на шину `read_data_o` должно возвращаться магическое число `32'hfall_leaf`. - 5. Как и в памяти инструкций, при чтении по адресам вне допустимого диапазона (только в этот раз старше адреса `1023`), на шине `read_data_o` должно выставляться значение `32'hdead_beaf`. + 5. Как и в памяти инструкций, при чтении по адресам вне допустимого диапазона (только в этот раз старше адреса `16383`), на шине `read_data_o` должно выставляться значение `32'hdead_beaf`. 3. После описания памяти данных, её необходимо проверить с помощью тестового окружения. 1. Тестовое окружение находится [`здесь`](tb_data_mem.sv). 2. Для запуска симуляции воспользуйтесь [`этой инструкцией`](../../Vivado%20Basics/Run%20Simulation.md). @@ -363,7 +345,7 @@ mоdulе rf_r𝚒sсv( 4. Реализуйте регистровый файл. Для этого: 1. В `Design Sources` проекта создайте `SystemVerilog`-файл `rf_riscv.sv`. 2. Опишите в нем модуль регистрового файла с таким же именем и портами, как указано в задании. - 1. Обратите внимание, что имя памяти (не название модуля, а имя объекта памяти внутри модуля) должно быть `rf_mem`. Такое имя необходимо для корректной работы верификационного окружения. + 1. Обратите внимание, что имя памяти (не название модуля, а имя объекта памяти внутри модуля) должно быть `rf_mem`. Такое имя необходимо для корректной работы верификационного окружения. 2. В отличии от памяти инструкций и данных, ячейки памяти регистрового файла должны быть 32-битными (а на 8-битными). Это означает, что реализация портов чтения и записи будет проще. 3. Не забывайте, что у вас 2 порта на чтение и 1 порт на запись, при этом каждый порт не зависит от остальных (в модуле 3 независимых входа адреса). 4. Чтение из нулевого регистра (чтение по адресу 0) всегда должно возвращать нулевое значение. Этого можно добиться двумя путями: