ЛР3,7,12. Убрано использование магических констант при работе с памятью

Данная логика не используется в реальных проектах и добавляет большие
мультиплексоры, отрицательно сказывающиеся на таймингах схемы.
This commit is contained in:
Andrei Solodovnikov
2023-11-15 17:59:41 +03:00
parent 1da4ed0173
commit a47e4d441c
10 changed files with 73 additions and 127 deletions

View File

@@ -226,19 +226,13 @@ mоdulе instr_mеm(
);
```
Не смотря на разрядность адреса, на практике, внутри данного модуля вы должны будете реализовать память с 1024-мя 32-битными ячейками (в ПЛИС попросту не хватит ресурсов на реализации памяти с 2<sup>32</sup> ячеек).
Не смотря на разрядность адреса, на практике, внутри данного модуля вы должны будете реализовать память с 1024-мя 32-битными ячейками (в ПЛИС попросту не хватит ресурсов на реализации памяти с 2<sup>32</sup> ячеек). Таким образом, реально будет использоваться только 10 бит адреса.
При этом по спецификации процессор RISC-V использует память с побайтовой адресацией. Байтовая адресация означает, что процессор способен обращаться к отдельным байтам в памяти (за каждым байтом памяти закреплен свой индивидуальный адрес).
Однако, если у памяти будут 32-рязрядные ячейки, доступ к конкретному байту будет осложнен, ведь каждая ячейка — это 4 байта. Как получить данные третьего байта памяти? Если обратиться к третьей ячейке в массиве — придут данные 12-15-ых байт (поскольку каждая ячейка содержит по 4 байта). Чтобы получить данные третьего байта, необходимо **разделить значение пришедшего адреса на 4** (отбросив остаток от деления). `3/4=0` — и действительно, если обратиться к нулевой ячейке памяти — будут получены данные 3-го, 2-го, 1-го и 0-го байт. То что помимо значения третьего байта есть еще данные других байт нас в данный момент не интересует, важна только сама возможность указать адрес конкретного байта.
Деление на 2<sup>n</sup> можно осуществить отбросив `n` младших бит числа. Таким образом на выход память инструкций должна выдавать данные, расположенные по адресу addr_i[31:2];
Обращение в память по адресам, превышающим `4095` должно выдавать значение `32'd0`. Почему именно `4095`? `4095/4=1023` — индекс последней ячейки памяти.
Как реализовать подобный функционал? Разумеется, с помощью [мультиплексора](../../Basic%20Verilog%20structures/Multiplexors.md).
В данном случае, вы можете либо воспользоваться конструкцией `if-else` в блоке `always_comb`, либо **тернарным условным оператором** во время реализации **непрерывного присваивания**. В любом из случаев, вам нужно будет реализовать условие того, что пришедшее значение на шине адреса попадает в диапазон допустимых адресов. Если это так, вернуть склейку данных из памяти, начинающихся с этого адреса, в противном случае — вернуть 0.
Деление на 2<sup>n</sup> можно осуществить отбросив `n` младших бит числа. Учитывая то, что для адресации 1024 ячеек памяти мы будем использовать 10 бит адреса, память инструкций должна выдавать на выход данные, расположенные по адресу `addr_i[11:2]`.
### 2. Память данных
@@ -263,23 +257,11 @@ mоdulе data_mеm(
```
Как и память инструкций, память данных будет состоять из 32-разрядных ячееколько теперь их будет 4096), и при обращении к этим ячейкам будет необходимо делить адрес на 4.
Как и память инструкций, память данных будет состоять из 32-разрядных ячеек. Только теперь их будет 4096, а значит при обращении к ячейкам памяти нужно использовать не 10 бит адреса, а 12. При этом по-прежнему необходимо разделить пришедший адрес на 4, т.е. нужно отбросить два младших бита. Таким образом, обращение к ячейкам памяти (для записи и чтения) должно осуществляться по адресу `addr_i[13:2]`.
Однако в отличие от памяти инструкций, в память данных добавлено два управляющих сигнала (`mem_req_i`и `write_enable_i`). Сигнал `mem_req_i` является сигналом запроса на работу с памятью. Без этого сигнала память не должна выполнять операции чтения/записи (вне зависимости от сигнала `write_enable`, определяющего происходит сейчас запись или чтение). Как сделать так, чтобы не происходило чтение без запроса? Например возвращать на шину чтения специальное "магическое число".
Однако в отличие от памяти инструкций, в память данных добавлено два управляющих сигнала (`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:16383]` (4096*4-1), на выходе `read_data_o` должно оказаться значение ячейки по адресу в 4 раза меньше пришедшего.
- В случае, если `mem_req_i == 1` и значение `addr_i` **не попадает** в диапазон `[0:16383]`, на выходе `read_data_o` должно оказаться магическое число `32'hdead_beef`.
Данные условия должны проверяться **строго** в указанном порядке (с указанным **приоритетом**) с помощью цепочки `if-else-if-else`, причем обязательно с блоками `else`.
Зачем нужны эти магические числа `32'hfa11_1eaf` и `32'hdead_beef`? У этих чисел легко узнаваемая сигнатура, позволяющая обратить на них внимание. В случае, если при чтении из памяти в регистровый файл попадут эти значения, увидев их вы сможете почувствовать что "что-то не то", и проверить: а было ли в памяти по указанному адресу действительно такое значение (в отличие от значения `32'h0000_0000`, которое не вызовет у вас никаких вопросов). Вероятность того, что такие числа возникнут в естественном ходе работы программы достаточно мала, а значит скорее всего если вы встретите эти числа — это сигнал того, что что-то в вашем процессоре работает неправильно (например, произошло обращение за пределы памяти, или неправильно формируется сигнал `mem_req_i`).
Если `mem_req_i == 1` и `write_enable_i == 1` происходит запрос на запись в память. В этом случае, необходимо по положительному фронту `clk_i` записать в значение `write_data_i` в ячейку по адресу в 4 раза меньшему `addr_i`. Во всех других случаях (любой из сигналов `mem_req_i`, `write_enable_i` равен нулю), запись в память не производится.
Поскольку мы описываем память с синхронным чтением, было бы неплохо, чтобы в результате мы получили блочную память (см. [теорию про память](#теория-про-память)). Однако блочная память — это заранее созданный аппаратный блок памяти, в котором нет места придуманным нами магическим числам, поэтому описывая порт на чтение, сперва лучше описать регистр, в который по запросу на работу с памятью всегда будет записываться значение из соответствующей ячейки. А уже после можно описать выход `rеаd_dаtа_o` перед которым будет стоять мультиплексор с тремя входами: константами `32'hfa11_1eaf`, `32'hdead_beaf` и значением с выхода описанного вами регистра:
![../../.pic/Labs/lab_03_memory/fig_03.drawio.png](../../.pic/Labs/lab_03_memory/fig_03.drawio.png)
Если `mem_req_i == 1` и `write_enable_i == 1`, то происходит запрос на запись в память. В этом случае, необходимо записать значение `write_data_i` в ячейку по адресу `addr_i[13:2]`. Во всех других случаях (любой из сигналов `mem_req_i`, `write_enable_i` равен нулю), запись в память не производится.
### 3. Регистровый файл
@@ -327,7 +309,7 @@ mоdulе rf_r𝚒sсv(
1. Сперва необходимо создать память (массив регистров). Как это сделать, сказано в разделе [описание памяти на языке SystemVerilog](#описание-памяти-на-языке-systemverilog). Разрядность ячеек памяти должна быть 32 бита, количество ячеек — 1024.
2. Добавить в проект [`файл с содержимым памяти инструкций`](program.txt)([`как добавить файл, инициализирующий память`](../../Vivado%20Basics/How%20to%20add%20a%20mem-file.md)). Данный файл будет использоваться при вызове системной функции `$readmemh` в описании памяти инструкций.
3. К созданной памяти необходимо подключить выход модуля `read_data_o`. При подключении должен быть использован вход модуля `addr_i`, значение которого должно быть уменьшено в 4 раза (побайтовая адресация).
4. При подключении выхода `read_data_o` помните, что чтение по адресам, превышающим `4095` должно возвращать `0`.
4. При реализации выхода `read_data_o` помните, что обращаясь к ячейке памяти, вам необходимо использовать `[11:2]` биты адреса.
3. После описания памяти инструкций, её необходимо проверить с помощью тестового окружения.
1. Тестовое окружение находится [`здесь`](tb_instr_mem.sv).
2. Для запуска симуляции воспользуйтесь [`этой инструкцией`](../../Vivado%20Basics/Run%20Simulation.md).
@@ -338,10 +320,8 @@ mоdulе rf_r𝚒sсv(
2. Опишите в нем модуль памяти данных с таким же именем и портами, как указано в задании.
1. Описание модуля будет схожим с описанием модуля памяти инструкций, однако порт чтения в этот раз будет **синхронным** (запись в него будет происходить в блоке `always_ff`). Кроме того необходимо будет описать логику записи данных в память.
2. Запись в ячейки памяти описывается подобно записи данных в [регистры](../../Basic%20Verilog%20structures/Registers.md), только при этом, происходит доступ к конкретной ячейке памяти с помощью входа `addr_i` (как осуществляется доступ к ячейкам памяти сказано в разделе [описание памяти на языке SystemVerilog](#описание-памяти-на-языке-systemverilog)).
3. Необходимо помнить, что запись будет вестись в ячейку с индексом в 4 раза меньшим пришедшего адреса.
4. Обратите внимание что работа с памятью должна осуществляться только когда сигнал `mem_req_i == 1`, в противном случае запись не должна производиться, а на шину `read_data_o` должно возвращаться магическое число `32'hfall_leaf`.
5. При чтении по адресам вне допустимого диапазона (старше адреса `16383`), на шине `read_data_o` должно выставляться значение `32'hdead_beaf`.
6. Проверка условий перед записью значения в `read_data_o` должно идти в указанном в задании порядке: сперва проверяются значения сигналов `mem_req_i` и `write_enable_i`, и только после этого значение адреса. Реализация логики приоритетов осуществляется в цепочке блоков `if-else-if-else`.
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).

View File

@@ -63,32 +63,17 @@ parameter STEP = 8;
end
end
for (i = 0; i < (ADDR_SIZE+STEP); i = i + 1 + $urandom() % STEP) begin
if (i < (ADDR_SIZE)) begin
REQ = |($urandom %10);
WE = 0;
#TIME_OPERATION;
RDa = RD;
WD = $urandom;
#TIME_OPERATION;
WE = $urandom % 2;
#TIME_OPERATION;
if ((WE && REQ || !REQ) && RD !== 32'd4195425967) begin
$error("When writing (write_enable_i = %h) read_data_o should be equal to fa11_1eaf, your data: %h_%h, time: %t", WE, RD[31:16],RD[15:0], $time);
err_count = err_count + 1;
end
if ((!WE && REQ) && RD !== RDa) begin
$error("When reading (write_enable_i = %h), the data %h is overwritten with data %h at address %h, time: %t", WE, RDa, RD, A, $time);
err_count = err_count + 1;
end
end
else begin
WE = 0;
REQ = 1;
#TIME_OPERATION;
if (RD !== 32'd3735928559) begin
$error("When reading (write_enable_i = %h) at address greater than 16383 (current addr = %d), it should return dead_beef, but your data: %h_%h, time: %t", WE, A, RD[31:16],RD[15:0], $time);
err_count = err_count + 1;
end
REQ = |($urandom %10);
WE = 0;
#TIME_OPERATION;
RDa = RD;
WD = $urandom;
#TIME_OPERATION;
WE = $urandom % 2;
#TIME_OPERATION;
if ((!WE && REQ) && RD !== RDa) begin
$error("When reading (write_enable_i = %h), the data %h is overwritten with data %h at address %h, time: %t", WE, RDa, RD, A, $time);
err_count = err_count + 1;
end
#TIME_OPERATION;
end
@@ -96,9 +81,9 @@ parameter STEP = 8;
REQ = 1;
WE = 0;
#TIME_OPERATION;
for (i = 0; i < 4; i = i + 1) begin
if(i==0) begin
repeat(2)@(posedge CLK);
for (i = 0; i < 4; i = i + 1) begin
if(i==0) begin
repeat(2)@(posedge CLK);
#1; RDa = RD;
end else
if(RD !== RDa) begin
@@ -118,12 +103,6 @@ parameter STEP = 8;
err_count = err_count + 1;
end
@(posedge CLK);
i = {14{1'b1}};
repeat(2) @(posedge CLK);
if (RD === 'd3735928559) begin
$error("incorrect reading from address = %d, data = %h", A, RD);
err_count = err_count + 1;
end
$display("Number of errors: %d", err_count);
if( !err_count ) $display("\ndata_mem SUCCESS!!!\n");
$finish();