mirror of
https://github.com/MPSU/APS.git
synced 2025-09-16 01:30:10 +00:00
WIP: APS cumulative update (#98)
* WIP: APS cumulative update * Update How FPGA works.md * Перенос раздела "Последовательностная логика" в отдельный док * Исправление картинки * Исправление оформления индексов * Переработка раздела Vivado Basics * Добавление картинки в руководство по созданию проекта * Исправление ссылок в анализе rtl * Обновление изображения в sequential logic * Исправление ссылок в bug hunting * Исправление ссылок * Рефактор руководства по прошивке ПЛИС * Mass update * Update fig_10 * Restore fig_02
This commit is contained in:
committed by
GitHub
parent
78bb01ef95
commit
a28002e681
@@ -1,4 +1,4 @@
|
||||
# Лабораторная работа 3 "Регистровый файл и память инструкций"
|
||||
# Лабораторная работа №3 "Регистровый файл и память инструкций"
|
||||
|
||||
Процессор — это программно-управляемое устройство, выполняющее обработку информации и управление этим процессом. Очевидно, программа, которая управляет процессором, должна где-то храниться. Данные, с которыми процессор работает, тоже должны быть в доступном месте. Нужна память!
|
||||
|
||||
@@ -6,46 +6,45 @@
|
||||
|
||||
Описать на языке SystemVerilog элементы памяти для будущего процессора:
|
||||
|
||||
- память инструкций (Instruction Memory);
|
||||
- регистровый файл (Register File).
|
||||
- память инструкций;
|
||||
- регистровый файл.
|
||||
|
||||
## Материалы для подготовки к лабораторной работе
|
||||
|
||||
Для успешного выполнения лабораторной работы, вам необходимо освоить:
|
||||
В дополнение к [материалам](../../Basic%20Verilog%20structures/), изученным в ходе предыдущих работ, вам рекомендуется ознакомиться с:
|
||||
|
||||
- приведенные способы описания [мультиплексоров](../../Basic%20Verilog%20structures/Multiplexors.md)
|
||||
- способы описания [регистров](../../Basic%20Verilog%20structures/Registers.md)
|
||||
- способами описания [регистров](../../Basic%20Verilog%20structures/Registers.md) на языке SystemVerilog.
|
||||
|
||||
## Ход работы
|
||||
|
||||
1. Изучить способы организации памяти (раздел [#теория про память](#теория-про-память)).
|
||||
2. Изучить конструкции SystemVerilog для реализации запоминающих элементов (раздел [#инструменты](#инструменты-для-реализации-памяти)).
|
||||
3. В проекте с прошлой лабораторной реализовать модули: Instruction Memory и Register File ([#задание](#задание-по-реализации-памяти)).
|
||||
4. Проверить с помощью тестового окружения корректность их работы.
|
||||
3. Реализовать модули памяти инструкции и регистрового файла.
|
||||
4. Проверить с помощью верификационного окружения корректность их работы.
|
||||
5. Проверить работу регистрового файла в ПЛИС.
|
||||
|
||||
## Теория про память
|
||||
|
||||
Память — это устройство для упорядоченного хранения и выдачи информации. Различные запоминающие устройства отличаются способом и организацией хранения данных. Базовыми характеристиками памяти являются:
|
||||
|
||||
- V — объем (количество бит данных, которые единовременно может хранить память);
|
||||
- V — объём (количество бит данных, которые единовременно может хранить память);
|
||||
- a — разрядность адреса (ширина шины адреса, определяет адресное пространство — количество адресов отдельных ячеек памяти);
|
||||
- d — разрядность хранимых данных (разрядность ячейки памяти, как правило совпадает с разрядностью входных/выходных данных).
|
||||
|
||||
В общем случае `V = 2^a * d`.
|
||||
|
||||
Для объема памяти в 1 KiB ([кибибайт](https://ru.wikipedia.org/wiki/%D0%9A%D0%B8%D0%B1%D0%B8%D0%B1%D0%B0%D0%B9%D1%82), 1024 байта или 8192 бита) разрядность адреса может быть, например, 10 бит (что покрывает 2^10 = 1024 адреса), тогда разрядность хранимых данных должна быть 8 бит. 1024 * 8 = 8192, то есть 1 кибибайт. Если разрядность адреса, например, 8 бит (что покрывает 2^8 = 256 адресов), то разрядность данных `d = V / 2^a` это 8192 / 256 = 32 бита.
|
||||
Для объема памяти в 1 KiB ([кибибайт](https://ru.wikipedia.org/wiki/%D0%9A%D0%B8%D0%B1%D0%B8%D0%B1%D0%B0%D0%B9%D1%82), 1024 байта или 8192 бита) разрядность адреса может быть, например, 10 бит (что покрывает 2^10 = 1024 адреса), тогда разрядность хранимых данных должна быть 8 бит. 1024 * 8 = 8192, то есть 1 кибибайт. Если разрядность адреса составляет 8 бит (что покрывает 2^8 = 256 адресов), то разрядность данных `d = V / 2^a` это 8192 / 256 = 32 бита.
|
||||
|
||||
Однако, может быть такое, что не все ячейки памяти реально реализованы на кристалле микросхемы, то есть некоторые адреса существуют, но по ним не имеет смысла обращаться, а объем памяти, соответственно, не равен `V ≠ 2^a * d` — он меньше. Подобные случаи будут рассмотрены отдельно.
|
||||
Однако, может быть такое, что не все ячейки памяти реализованы на кристалле микросхемы, то есть некоторые адреса существуют, но по ним не имеет смысла обращаться, а объем памяти, соответственно, не равен `V ≠ 2^a * d` — он меньше.
|
||||
|
||||
Память можно разделить на категории: ПЗУ (постоянное запоминающее устройство) и ОЗУ (оперативное запоминающее устройство). Из ПЗУ можно только считывать информацию, которая попадает в ПЗУ до начала использования памяти и не может изменяться в процессе работы. Из ОЗУ можно считывать и записывать информацию. В самом простом случае ПЗУ имеет один вход адреса `addr` и один выход считываемых данных `read_data`. На вход `addr` подается адрес требуемой ячейки памяти, на выходе `read_data` появляются данные, которые хранятся по этому адресу.
|
||||
Память можно разделить на категории: ПЗУ (постоянное запоминающее устройство) и ОЗУ (оперативное запоминающее устройство). Из ПЗУ можно только считывать информацию, которая попадает в него до начала использования памяти и не может изменяться в процессе работы. Из ОЗУ можно считывать и записывать информацию. В самом простом случае ПЗУ имеет один вход адреса `addr` и один выход считываемых данных `read_data`. На вход `addr` подается адрес требуемой ячейки памяти, на выходе `read_data` появляются данные, которые хранятся по этому адресу.
|
||||
|
||||
Для ОЗУ требуется больше сигналов. Кроме входного `addr` и выходного `read_data` добавляются: входные данные для записи `write_data`, сигнал синхронизации `clk`, который определяет момент записи данных и сигнал разрешения на запись `write_enable`, который контролирует нужно ли записывать данные или только считывать. Для того, чтобы записать информацию в такую память необходимо:
|
||||
|
||||
- выставить адрес `addr` в который планируется запись данных,
|
||||
- выставить сами данные для записи на вход `write_data`,
|
||||
- установить сигнал `write_enable` в состояние разрешения записи (как правило это 1) и
|
||||
- дождаться нужного фронта `clk` — в этот момент данные будут записаны по указанному адресу. При этом, на выходе `read_data` будут старые данные, хранящиеся по адресу `addr`. На одном такте происходит одновременное считывание информации и запись новой.
|
||||
- дождаться нужного (положительного, либо отрицательного) фронта `clk` — в этот момент данные будут записаны по указанному адресу.
|
||||
|
||||
Так же возможна реализация, в которой вход `write_data` и выход `read_data` объединены в единый вход/выход `data`. В этом случае операции чтения и записи разделены во времени и используют для этого один единый порт ввода-вывода (`inout`, двунаправленный порт) `data`.
|
||||
|
||||
@@ -53,9 +52,9 @@
|
||||
|
||||
_Рисунок 1. Примеры блоков ПЗУ и ОЗУ._
|
||||
|
||||
Кроме того, различают память с **синхронным** и **асинхронным** чтением. В первом случае, перед выходным сигналом шины данных ставится дополнительный регистр, в который по тактовому синхроимпульсу записываются запрашиваемые данные. Такой способ может очень сильно сократить **критический путь** цифровой схемы, но требует дополнительный такт на доступ в память. В свою очередь, асинхронное чтение позволяет получить данные, не дожидаясь очередного синхроимпульса, но такой способ увеличивает критический путь.
|
||||
Кроме того, различают память с **синхронным** и **асинхронным** чтением. В первом случае, перед выходным сигналом шины данных ставится дополнительный регистр, в который по тактовому синхроимпульсу записываются запрашиваемые данные. Такой способ может значительно сократить **критический путь** цифровой схемы, но требует дополнительный такт на доступ в память. В свою очередь, асинхронное чтение позволяет получить данные, не дожидаясь очередного синхроимпульса, но такой способ увеличивает критический путь.
|
||||
|
||||
Еще одной характеристикой памяти является количество доступных портов. Количество портов определяет к скольким ячейкам памяти можно обратиться одновременно. Проще говоря, сколько входов адреса существует. Все примеры памяти рассмотренные выше являются **однопортовыми**, то есть у них один порт. Например, если у памяти 2 входа адреса `addr1` и `addr2` — это **двухпортовая память**. При этом не важно, можно ли по этим адресам только читать/писать или выполнять обе операции.
|
||||
Еще одной характеристикой памяти является количество доступных портов чтения или записи (не путайте с портами модуля, которые являются любыми его входными/выходными сигналами). Количество портов определяет к скольким ячейкам памяти можно обратиться одновременно. Проще говоря, сколько входов адреса существует. Все примеры памяти рассмотренные выше являются **однопортовыми**, то есть у них один порт. Например, если у памяти 2 входа адреса `addr1` и `addr2` — это **двухпортовая память**. При этом не важно, можно ли по этим адресам только читать/писать или выполнять обе операции.
|
||||
|
||||
Регистровый файл, который будет реализован в рамках данной работы, является **трехпортовым**, и имеет 2 порта на чтение и 1 порт на запись.
|
||||
|
||||
@@ -65,7 +64,7 @@ _Рисунок 1. Примеры блоков ПЗУ и ОЗУ._
|
||||
|
||||
_Рисунок 2. Структурная схема логического блока в ПЛИС[[1]](https://en.wikipedia.org/wiki/Field-programmable_gate_array)._
|
||||
|
||||
В логическом блоке есть **таблицы подстановки** (Look Up Table, LUT), которые представляют собой не что иное как память, которая переконфигурируется под нужды хранения, а не реализацию логики. Таким образом, трехвходовой LUT может выступать в роли 8-битной памяти.
|
||||
В логическом блоке есть **таблицы подстановки** (Look Up Table, LUT), которые представляют собой не что иное как память, которая конфигурируется под нужды хранения, а не реализацию логики. Таким образом, трехвходовой LUT может выступать в роли 8-битной памяти.
|
||||
|
||||
Однако LUT будет сложно приспособить под многопортовую память: посмотрим на схему еще раз: три входа LUT формируют адрес одной из восьми ячеек. Это означает, что среди этих восьми ячеек нельзя обратиться к двум из них одновременно.
|
||||
|
||||
@@ -75,9 +74,9 @@ _Рисунок 2. Структурная схема логического бл
|
||||
|
||||
Минусом является ограниченность в реализации многопортовой памяти.
|
||||
|
||||
Сравним блочную память с распределенной/регистровой: поскольку большой объем памяти съест много логических блоков при реализации распределенной/регистровой памяти, такую память лучше делать в виде блочной.
|
||||
Сравним блочную память с распределенной/регистровой: поскольку большой объем памяти "съест" много логических блоков при реализации распределенной/регистровой памяти, такую память лучше делать в виде блочной.
|
||||
|
||||
В то же время, к плюсам распределенной/регистровой памяти можно отнести возможность синтезировать память с асинхронным портом на чтение, чем мы и воспользуемся при реализации однотактного процессора (если бы порт чтения памяти был синхронным, нам потребовалось ждать один такт, чтобы получить инструкцию из памяти инструкций или данные из регистрового файла, что затруднило бы реализацию однотактного процессора, где каждая инструкция должна выполняться ровно за один такт).
|
||||
В то же время, к плюсам распределенной/регистровой памяти относится возможность синтезировать память с асинхронным портом на чтение, чем мы и воспользуемся при реализации однотактного процессора (если бы порт чтения памяти был синхронным, нам потребовалось ждать один такт, чтобы получить инструкцию из памяти инструкций или данные из регистрового файла, что затруднило бы реализацию однотактного процессора, где каждая инструкция должна выполняться ровно за один такт).
|
||||
|
||||
Обычно синтезатор сам понимает, какой вид памяти подходит под описанную схему на языке SystemVerilog.
|
||||
|
||||
@@ -89,13 +88,13 @@ _Рисунок 2. Структурная схема логического бл
|
||||
|
||||
Память на языке SystemVerilog объявляется [подобно регистрам](../../Basic%20Verilog%20structures/Registers.md), используя ключевое слово `logic`. Но, кроме разрядности (разрядности ячеек памяти, в данном случае) после имени регистра (памяти, в данном случае) указывается количество создаваемых ячеек либо в виде натурального числа, либо в виде диапазона адресов этих ячеек.:
|
||||
|
||||
```SystemVerilog
|
||||
```Verilog
|
||||
logic [19:0] memory1 [16]; // memory1 и memory2 являются полностью
|
||||
logic [19:0] memory2 [0:15]; // идентичными памятями.
|
||||
|
||||
logic [19:0] memory3 [15:0]; // memory3 будет такой же памятью, что и
|
||||
// предыдущие, но на временной диаграмме
|
||||
// Vivado при ее отображении сперва будут
|
||||
// Vivado при её отображении сперва будут
|
||||
// идти ячейки, начинающиеся со старших
|
||||
// адресов (что в рамках данного курса
|
||||
// лабораторных работ будет скорее минусом).
|
||||
@@ -113,13 +112,13 @@ logic [19:0] memory3 [1:16]; // А вот memory3 хоть и совпадае
|
||||
|
||||
_Листинг 1. Пример создания массива ячеек._
|
||||
|
||||
В приведенном листинге `logic [19:0] memory1 [16];` создается память с шестнадцатью (от 0-го до 15-го адреса) 20-битными ячейками памяти. В таком случае говорят, что ширина памяти 20 бит, а глубина 16. Для адресации такой памяти потребуется адрес с разрядностью ceil(log2(16)) = 4 бита (`ceil` — операция округления вверх). Это однопортовая память.
|
||||
В приведенном листинге `logic [19:0] memory1 [16];` создается память с шестнадцатью (от 0-го до 15-го адреса) 20-битными ячейками памяти. В таком случае говорят, что ширина памяти 20 бит, а глубина 16. Для адресации такой памяти потребуется адрес с разрядностью ceil(log2(16)) = 4 бита (`ceil` — операция округления вверх).
|
||||
|
||||
Для обращения к конкретной ячейке памяти используются квадратные скобки с указанием нужного адреса `memory[addr]`. Грубо говоря, то, что указывается в квадратных скобках будет подключено ко входу адреса памяти `memory`.
|
||||
|
||||
Чтение из памяти может быть сделано двумя способами: синхронно и асинхронно.
|
||||
Как уже говорилось, чтение из памяти может быть сделано двумя способами: синхронно и асинхронно.
|
||||
|
||||
Синхронное чтение подразумевает ожидание следующего тактового синхроимпульса для выдачи данных после получения адреса. Иными словами, данные будут установлены на выходе не в тот же такт, когда был выставлен адрес на вход памяти данных, а на следующий. Не смотря на то, что в таком случае на каждой операции чтения "теряется" один такт, память с синхронным чтением имеет значительно меньший критический путь, чем положительно сказывается на временных характеристиках итоговой схемы.
|
||||
Синхронное чтение подразумевает ожидание следующего тактового синхроимпульса для выдачи данных после получения адреса. Иными словами, данные будут установлены на выходе не в тот же такт, когда был выставлен адрес на вход памяти данных, а на следующий. Несмотря на то, что в таком случае на каждой операции чтения "теряется" один такт, память с синхронным чтением имеет значительно меньший критический путь, чем положительно сказывается на временных характеристиках итоговой схемы.
|
||||
|
||||
Память с асинхронным чтением выдает данные в том же такте, что и получает адрес (т.е. ведет себя как комбинационная схема). Несмотря на то, что такой подход кажется быстрее, память с асинхронным чтением обладает длинным критическим путем, причем чем большего объема будет память, тем длиннее будет критический путь.
|
||||
|
||||
@@ -127,14 +126,14 @@ _Листинг 1. Пример создания массива ячеек._
|
||||
|
||||
Так как запись в память является синхронным событием, то описывается она в конструкции `always_ff`. При этом, как и при описании регистра, можно реализовать управляющий сигнал разрешения на запись через блок вида `if(write_enable)`.
|
||||
|
||||
```SystemVerilog
|
||||
```Verilog
|
||||
module mem16_20 ( // создать блок с именем mem16_20
|
||||
input logic clk, // вход синхронизации
|
||||
input logic [3:0] addr, // адресный вход
|
||||
input logic [19:0] write_data, // вход данных для записи
|
||||
input logic write_enable, // сигнал разрешения на запись
|
||||
output logic [19:0] async_read_data// асинхронный выход считанных данных
|
||||
output logic [19:0] sync_read_data // синхронный выход считанных данных
|
||||
input logic clk, // вход синхронизации
|
||||
input logic [3:0] addr, // адресный вход
|
||||
input logic [19:0] write_data, // вход данных для записи
|
||||
input logic write_enable, // сигнал разрешения на запись
|
||||
output logic [19:0] async_read_data,// асинхронный выход считанных данных
|
||||
output logic [19:0] sync_read_data // синхронный выход считанных данных
|
||||
);
|
||||
|
||||
logic [19:0] memory [0:15]; // создать память с 16-ю
|
||||
@@ -162,7 +161,7 @@ endmodule
|
||||
|
||||
_Листинг 2. Пример описания портов памяти._
|
||||
|
||||
В случае реализации ПЗУ нет необходимости в описании входов для записи, поэтому описание памяти занимает всего пару строк. Чтобы проинициализировать такую память (то есть поместить в нее начальные значения, которые можно было бы из нее читать), требуемое содержимое нужно добавить к прошивке, вместе с которой данные попадут в ПЛИС. Для этого в проект добавляется текстовый файл формата `.mem` с содержимым памяти. Для того, чтобы отметить данный файл в качестве инициализирующего память, можно использовать системную функцию `$readmemh`.
|
||||
В случае реализации ПЗУ нет необходимости в описании входов для записи, поэтому описание памяти занимает всего пару строк. Чтобы проинициализировать такую память (то есть поместить в неё начальные значения, которые можно было бы считать), требуемое содержимое нужно добавить к прошивке, вместе с которой данные попадут в ПЛИС. Для этого в проект добавляется текстовый файл формата `.mem` с содержимым памяти. Для того, чтобы отметить данный файл в качестве инициализирующего память, можно использовать системную функцию `$readmemh`.
|
||||
|
||||
У данной функции есть два обязательных аргумента:
|
||||
|
||||
@@ -171,20 +170,24 @@ _Листинг 2. Пример описания портов памяти._
|
||||
|
||||
и два опциональных:
|
||||
|
||||
- стартовый адрес, начиная с которого память будет проинициализирована данным файлом (по-умолчанию равен нулю)
|
||||
- стартовый адрес, начиная с которого память будет проинициализирована данным файлом (по умолчанию равен нулю)
|
||||
- конечный адрес, на котором инициализация закончится (даже если в файле были ещё какие-то данные).
|
||||
|
||||
Пример полного вызова выглядит так:
|
||||
|
||||
`$readmemh("<data file name>",<memory name>,<start address>,<end address>);`
|
||||
```Verilog
|
||||
$readmemh("<data file name>",<memory name>,<start address>,<end address>);
|
||||
```
|
||||
|
||||
Однако на деле обычно используются только обязательные аргументы:
|
||||
|
||||
`$readmemh("<data file name>",<memory name>);`
|
||||
```Verilog
|
||||
$readmemh("<data file name>",<memory name>);
|
||||
```
|
||||
|
||||
Пример описанной выше памяти:
|
||||
|
||||
```SystemVerilog
|
||||
```Verilog
|
||||
module rom16_8 (
|
||||
input logic [3:0] addr1, // первый 4-битный адресный вход
|
||||
input logic [3:0] addr2, // второй 4-битный адресный вход
|
||||
@@ -207,7 +210,7 @@ endmodule
|
||||
|
||||
_Листинг 3. Пример использования инициализирующей функции $readmemh._
|
||||
|
||||
Содержимое файла `rom_data.mem`, к примеру может быть таким (каждая строка соответствует значению отдельной ячейки памяти, начиная со стартового адреса):
|
||||
Содержимое файла `rom_data.mem`, к примеру, может быть таким (каждая строка соответствует значению отдельной ячейки памяти, начиная со стартового адреса):
|
||||
|
||||
```hex
|
||||
FA
|
||||
@@ -233,16 +236,16 @@ _Листинг 3. Пример использования инициализи
|
||||
- 32-битный вход адреса
|
||||
- 32-битный выход данных (асинхронное чтение)
|
||||
|
||||
```SystemVerilog
|
||||
mоdulе instr_mеm(
|
||||
inрut logic [31:0] addr_i,
|
||||
оutрut logic [31:0] rеаd_dаtа_o
|
||||
```Verilog
|
||||
module instr_mem(
|
||||
input logic [31:0] read_addr_i,
|
||||
output logic [31:0] read_data_o
|
||||
);
|
||||
```
|
||||
|
||||
Не смотря на разрядность адреса, на практике, внутри данного модуля вы должны будете реализовать память с 1024-мя 32-битными ячейками (в ПЛИС попросту не хватит ресурсов на реализации памяти с 2<sup>32</sup> ячеек). Таким образом, реально будет использоваться только 10 бит адреса.
|
||||
Несмотря на разрядность адреса, на практике, внутри данного модуля вы должны будете реализовать память с 512-ю 32-битными ячейками (в ПЛИС попросту не хватит ресурсов на реализации памяти с 2<sup>32</sup> ячеек). Таким образом, реально будет использоваться только 9 бит адреса.
|
||||
|
||||
При этом по спецификации процессор RISC-V использует память с побайтовой адресацией. Байтовая адресация означает, что процессор способен обращаться к отдельным байтам в памяти (за каждым байтом памяти закреплен свой индивидуальный адрес).
|
||||
При этом по спецификации процессор RISC-V использует память с побайтовой адресацией [[2](https://github.com/riscv/riscv-isa-manual/releases/download/20240411/unpriv-isa-asciidoc.pdf), стр. 15]. Байтовая адресация означает, что процессор способен обращаться к отдельным байтам в памяти (за каждым байтом памяти закреплен свой индивидуальный адрес).
|
||||
|
||||
Однако, если у памяти будут 32-битные ячейки, доступ к конкретному байту будет осложнен, ведь каждая ячейка — это 4 байта. Как получить данные третьего байта памяти? Если обратиться к третьей ячейке в массиве — придут данные 12-15-ых байт (поскольку каждая ячейка содержит по 4 байта). Чтобы получить данные третьего байта, необходимо **разделить значение пришедшего адреса на 4** (отбросив остаток от деления). `3 / 4 = 0` — и действительно, если обратиться к нулевой ячейке памяти — будут получены данные 3-го, 2-го, 1-го и 0-го байт. То, что помимо значения третьего байта есть еще данные других байт нас в данный момент не интересует, важна только сама возможность указать адрес конкретного байта.
|
||||
|
||||
@@ -250,18 +253,18 @@ mоdulе instr_mеm(
|
||||
|
||||
_Рисунок 3. Связь адреса байта и индекса слова в массиве ячеек памяти._
|
||||
|
||||
Деление на 2<sup>n</sup> можно осуществить, отбросив `n` младших бит числа. Учитывая то, что для адресации 1024 ячеек памяти мы будем использовать 10 бит адреса, память инструкций должна выдавать на выход данные, расположенные по адресу `addr_i[11:2]`.
|
||||
Деление на 2<sup>n</sup> можно осуществить, отбросив `n` младших бит числа. Учитывая то, что для адресации 512 ячеек памяти мы будем использовать 9 бит адреса, память инструкций должна выдавать на выход данные, расположенные по адресу `addr_i[10:2]`.
|
||||
|
||||
Не смотря на заданный размер памяти инструкций в 1024 32-битных ячейки, на практике удобно параметризовать это значение, чтобы в ситуациях, когда требуется меньше или больше памяти можно было получить обновленное значение, не переписывая код во множестве мест. Подобное новшество вы сможете оценить на практике, получив возможность существенно сокращать время синтеза процессора, уменьшая размер памяти до необходимого минимума путем изменения значения одного лишь параметра.
|
||||
Несмотря на зафиксированный заданием размер памяти инструкций в 512 32-битных ячейки, на практике удобно параметризовать это значение, чтобы в ситуациях, когда требуется меньше или больше памяти можно было получить обновленный модуль, не переписывая код во множестве мест. Подобное новшество вы сможете оценить на практике, получив возможность существенно сокращать время синтеза процессора, уменьшая размер памяти до необходимого минимума путем изменения значения одного лишь параметра.
|
||||
|
||||
Для этого можно например создать параметр: `INSTR_MEM_SIZE_BYTES`, показывающий размер памяти инструкций в байтах. Однако, поскольку у данной памяти 32-битные ячейки, нам было бы удобно иметь и параметр `INSTR_MEM_SIZE_WORDS`, который говорит сколько в памяти 32-битных ячеек.
|
||||
Для этого можно, например, создать параметр: `INSTR_MEM_SIZE_BYTES`, показывающий размер памяти инструкций в байтах. Однако, поскольку у данной памяти 32-битные ячейки, нам было бы удобно иметь и параметр `INSTR_MEM_SIZE_WORDS`, который говорит сколько в памяти 32-битных ячеек.
|
||||
При этом `INSTR_MEM_SIZE_WORDS = INSTR_MEM_SIZE_BYTES / 4` (т.е. в 32-битном слове 4 байта).
|
||||
|
||||
В случае подобной параметризации, необходимо иметь возможность подстраивать количество используемых бит адреса. Для 1024 ячеек памяти мы использовали 10 бит адреса, для 512 ячеек нам потребуется уже 9 бит. Нетрудно заметить, что нам нужно такое число бит данных, возведя в степень которого `2`, мы получим размер нашей памяти (либо число, превышающее этот размер в случае, если размер памяти не является степенью двойки). Иными словами, нам нужен логарифм по основанию 2 от размера памяти, с округлением до целого вверх. И неудивительно, что в SystemVerilog есть специальная конструкция, которая позволяет считать подобные числа. Эта конструкция называется `$clog2` (`с` означает "ceil" — операцию округления вверх).
|
||||
В случае подобной параметризации, необходимо иметь возможность подстраивать количество используемых бит адреса. Для 512 ячеек памяти мы использовали 9 бит адреса, для 1024 ячеек нам потребуется уже 10 бит. Нетрудно заметить, что нам нужно такое число бит данных, возведя в степень которого `2`, мы получим размер нашей памяти (либо число, превышающее этот размер в случае, если размер памяти не является степенью двойки). Иными словами, нам нужен логарифм по основанию 2 от размера памяти, с округлением до целого вверх. И неудивительно, что в SystemVerilog есть специальная конструкция, которая позволяет считать подобные числа. Эта конструкция называется `$clog2` (`с` означает "ceil" — операцию округления вверх).
|
||||
|
||||
Поскольку реализация памяти состоит буквально из нескольких строчек, но при этом использование параметров может вызвать некоторые затруднения, код памяти инструкций предоставляется в готовом виде:
|
||||
|
||||
```SystemVerilog
|
||||
```Verilog
|
||||
module instr_mem
|
||||
import memory_pkg::INSTR_MEM_SIZE_BYTES;
|
||||
import memory_pkg::INSTR_MEM_SIZE_WORDS;
|
||||
@@ -278,10 +281,10 @@ import memory_pkg::INSTR_MEM_SIZE_WORDS;
|
||||
$readmemh("program.mem", ROM); // поместить в память ROM содержимое
|
||||
end // файла program.mem
|
||||
|
||||
// Реализация асинхронного порта на чтение, где на выход идет ячейка памяти
|
||||
// Реализация асинхронного порта на чтение, где на выход идёт ячейка памяти
|
||||
// инструкций, расположенная по адресу read_addr_i, в котором обнулены два
|
||||
// младших бита, а также биты, двоичный вес которых превышает размер памяти
|
||||
// данных в байтах.
|
||||
// данных в байтах.
|
||||
// Два младших бита обнулены, чтобы обеспечить выровненный доступ к памяти,
|
||||
// в то время как старшие биты обнулены, чтобы не дать обращаться в память
|
||||
// по адресам несуществующих ячеек (вместо этого будут выданы данные ячеек,
|
||||
@@ -295,7 +298,7 @@ _Листинг 4. SystemVerilog-описание памяти инструкц
|
||||
|
||||
### 3. Регистровый файл
|
||||
|
||||
На языке SystemVerilog необходимо реализовать модуль регистрового файла (`rf_r𝚒sсv`) для процессора с архитектурой RISC-V, представляющего собой трехпортовое ОЗУ с двумя портами на чтение и одним портом на запись и состоящей из 32-х 32-битных регистров, объединенных в массив с именем `rf_mem`.
|
||||
На языке SystemVerilog необходимо реализовать модуль регистрового файла для процессора с архитектурой RISC-V, представляющего собой трехпортовое ОЗУ с двумя портами на чтение и одним портом на запись и состоящей из 32-х 32-битных регистров, объединенных в массив с именем `rf_mem`.
|
||||
|
||||
У данного модуля будет восемь входных/выходных сигналов:
|
||||
|
||||
@@ -308,18 +311,18 @@ _Листинг 4. SystemVerilog-описание памяти инструкц
|
||||
- 32-битный выход данных асинхронного чтения по первому адресу
|
||||
- 32-битный выход данных асинхронного чтения по второму адресу
|
||||
|
||||
```SystemVerilog
|
||||
mоdulе rf_r𝚒sсv(
|
||||
inрut logic сlk_i,
|
||||
inрut logic write_enable_i,
|
||||
```Verilog
|
||||
module register_file(
|
||||
input logic clk_i,
|
||||
input logic write_enable_i,
|
||||
|
||||
inрut logic [ 4:0] write_addr_i,
|
||||
inрut logic [ 4:0] read_addr1_i,
|
||||
inрut logic [ 4:0] read_addr2_i,
|
||||
input logic [ 4:0] write_addr_i,
|
||||
input logic [ 4:0] read_addr1_i,
|
||||
input logic [ 4:0] read_addr2_i,
|
||||
|
||||
inрut logic [31:0] write_data_i,
|
||||
оutрut logic [31:0] read_data1_o,
|
||||
оutрut logic [31:0] read_data2_o
|
||||
input logic [31:0] write_data_i,
|
||||
output logic [31:0] read_data1_o,
|
||||
output logic [31:0] read_data2_o
|
||||
);
|
||||
|
||||
```
|
||||
@@ -333,34 +336,21 @@ mоdulе rf_r𝚒sсv(
|
||||
|
||||
## Порядок выполнения работы
|
||||
|
||||
1. Внимательно ознакомьтесь с заданием. В случае возникновения вопросов, проконсультируйтесь с преподавателем.
|
||||
2. Добавьте в проект файл [`memory_pkg.sv`](memory_pkg.sv). Этот файл содержит объявление пакета `memory_pkg`, в котором прописаны размеры памяти инструкций и памяти данных (реализуется позднее).
|
||||
3. Реализуйте память инструкций. Для этого:
|
||||
1. В `Design Sources` проекта с предыдущих лаб, создайте `SystemVerilog`-файл `instr_mem.sv`.
|
||||
2. Опишите в нем модуль памяти инструкций по предоставленному в _листинге 4_ коду.
|
||||
4. Реализуйте регистровый файл. Для этого:
|
||||
1. В `Design Sources` проекта создайте `SystemVerilog`-файл `rf_riscv.sv`.
|
||||
2. Опишите в нем модуль регистрового файла с таким же именем и портами, как указано в задании.
|
||||
1. Обратите внимание, что имя памяти (не название модуля, а имя массива регистров внутри модуля) должно быть `rf_mem`. Такое имя необходимо для корректной работы верификационного окружения.
|
||||
2. Как и у памяти инструкций, порты чтения регистрового файла должны быть **асинхронными**.
|
||||
3. Не забывайте, что у вас 2 порта на чтение и 1 порт на запись, при этом каждый порт не зависит от остальных (в модуле 3 независимых входа адреса).
|
||||
4. Чтение из нулевого регистра (чтение по адресу 0) всегда должно возвращать нулевое значение. Этого можно добиться двумя путями:
|
||||
1. Путем добавления мультиплексора перед выходным сигналом чтения (мультиплексор будет определять, пойдут ли на выход данные из ячейки регистрового файла, либо, в случае если адрес равен нулю, на выход пойдет константа ноль).
|
||||
2. Путем инициализации нулевого регистра нулевым значением и запретом записи в этот регистр (при записи и проверки write_enable добавить дополнительную проверку на адрес).
|
||||
3. Каким образом будет реализована эта особенность регистрового файла не важно, выберите сами.
|
||||
3. После описания регистрового файла его необходимо проверить с помощью [`тестового окружения`](../../Basic%20Verilog%20structures/Testbench.md).
|
||||
1. Тестовое окружение находится [`здесь`](tb_rf_riscv.sv).
|
||||
2. Для запуска симуляции воспользуйтесь [`этой инструкцией`](../../Vivado%20Basics/Run%20Simulation.md).
|
||||
3. Перед запуском симуляции убедитесь, что в качестве top-level модуля выбран корректный (`tb_rf_riscv`).
|
||||
4. **Во время симуляции, вы должны прожать "Run All" и убедиться, что в логе есть сообщение о завершении теста!**
|
||||
5. Проверьте работоспособность вашей цифровой схемы в ПЛИС. Для этого:
|
||||
1. Добавьте файлы из папки [`board files`](https://github.com/MPSU/APS/tree/master/Labs/03.%20Register%20file%20and%20memory/board%20files) в проект.
|
||||
1. Файл [nexys_rf_riscv.sv](https://github.com/MPSU/APS/tree/master/Labs/03.%20Register%20file%20and%20memory/board%20files/nexys_rf_riscv.sv) необходимо добавить в `Design Sources` проекта.
|
||||
2. Файл [nexys_a7_100t.xdc](https://github.com/MPSU/APS/tree/master/Labs/03.%20Register%20file%20and%20memory/board%20files/nexys_a7_100t.xdc) необходимо добавить в `Constraints` проекта. В случае, если вы уже добавляли одноименный файл в рамках предыдущих лабораторных работ, его содержимое необходимо заменить содержимым нового файла.
|
||||
2. Выберите `nexys_rf_riscv` в качестве модуля верхнего уровня (`top-level`).
|
||||
3. Выполните генерацию битстрима и сконфигурируйте ПЛИС. Для этого воспользуйтесь [следующей инструкцией](../../Vivado%20Basics/How%20to%20program%20an%20fpga%20board.md).
|
||||
4. Описание логики работы модуля верхнего уровня и связи периферии ПЛИС с реализованным модулем находится в папке [`board files`](https://github.com/MPSU/APS/tree/master/Labs/03.%20Register%20file%20and%20memory/board%20files).
|
||||
1. Добавьте в проект файл [`memory_pkg.sv`](memory_pkg.sv). Этот файл содержит объявление пакета `memory_pkg`, в котором прописаны размеры памяти инструкций и памяти данных (реализуется позднее).
|
||||
2. Реализуйте память инструкций посредством описания, представленного в _листинге 4_.
|
||||
3. Опишите регистровый файл с таким же именем и портами, как указано в задании.
|
||||
1. Обратите внимание, что имя памяти (не название модуля, а имя массива регистров внутри модуля) должно быть `rf_mem`. Такое имя необходимо для корректной работы верификационного окружения.
|
||||
2. Как и у памяти инструкций, порты чтения регистрового файла должны быть **асинхронными**.
|
||||
3. Не забывайте, что у вас 2 порта на чтение и 1 порт на запись, при этом каждый порт не зависит от остальных (в модуле 3 независимых входа адреса).
|
||||
4. Чтение из нулевого регистра (чтение по адресу 0) всегда должно возвращать нулевое значение. Этого можно добиться двумя путями:
|
||||
1. Путем добавления мультиплексора перед выходным сигналом чтения (мультиплексор будет определять, пойдут ли на выход данные из ячейки регистрового файла, либо, в случае если адрес равен нулю, на выход пойдет константа ноль).
|
||||
2. Путем инициализации нулевого регистра нулевым значением и запретом записи в этот регистр (при записи и проверки write_enable добавить дополнительную проверку на адрес).
|
||||
3. Каким образом будет реализована эта особенность регистрового файла не важно, выберите сами.
|
||||
4. Проверьте модуль с помощью верификационного окружения, представленного в файле [`lab_03.tb_register_file.sv`](lab_03.tb_register_file.sv). В случае, если в TCL-консоли появились сообщения об ошибках, вам необходимо [найти](../../Vivado%20Basics/05.%20Bug%20hunting.md) и исправить их.
|
||||
1. Перед запуском моделирования, убедитесь, что у вас выбран корректный модуль верхнего уровня в `Simulation Sources`.
|
||||
5. Проверьте работоспособность вашей цифровой схемы в ПЛИС.
|
||||
|
||||
## Источники
|
||||
|
||||
1. [Field-programmable gate array](https://en.wikipedia.org/wiki/Field-programmable_gate_array)
|
||||
2. [The RISC-V Instruction Set Manual Volume I: Unprivileged ISA, Document Version 20240411, Editors Andrew Waterman and Krste Asanović, RISC-V Foundation, April 2024](https://github.com/riscv/riscv-isa-manual/releases/download/20240411/unpriv-isa-asciidoc.pdf)
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
После того, как вы проверили на моделировании регистровый файл, вам необходимо проверить его работу на прототипе в ПЛИС.
|
||||
|
||||
Инструкция по реализации прототипа описана [здесь](../../../Vivado%20Basics/How%20to%20program%20an%20fpga%20board.md).
|
||||
Инструкция по реализации прототипа описана [здесь](../../../Vivado%20Basics/07.%20Program%20and%20debug.md).
|
||||
|
||||
На _рис. 1_ представлена схема прототипа в ПЛИС.
|
||||
|
||||
|
@@ -8,7 +8,7 @@
|
||||
See https://github.com/MPSU/APS/blob/master/LICENSE file for licensing details.
|
||||
* ------------------------------------------------------------------------------
|
||||
*/
|
||||
module tb_rf_riscv();
|
||||
module lab_03_tb_register_file();
|
||||
|
||||
logic CLK;
|
||||
logic [ 4:0] RA1;
|
||||
@@ -22,7 +22,7 @@ module tb_rf_riscv();
|
||||
logic [31:0] RD1ref;
|
||||
logic [31:0] RD2ref;
|
||||
|
||||
rf_riscv DUT(
|
||||
register_file DUT(
|
||||
.clk_i (CLK),
|
||||
.read_addr1_i (RA1),
|
||||
.read_addr2_i (RA2),
|
||||
@@ -33,7 +33,7 @@ module tb_rf_riscv();
|
||||
.read_data2_o (RD2)
|
||||
);
|
||||
|
||||
rf_riscv_ref DUTref(
|
||||
register_file_ref DUTref(
|
||||
.clk_i (CLK ),
|
||||
.read_addr1_i (RA1 ),
|
||||
.read_addr2_i (RA2 ),
|
||||
@@ -61,7 +61,7 @@ module tb_rf_riscv();
|
||||
|
||||
initial begin
|
||||
$timeformat (-9, 2, "ns");
|
||||
$display( "\nStart test: \n\n==========================\nCLICK THE BUTTON 'Run All'\n==========================\n"); $stop();
|
||||
$display("Test has been started");
|
||||
RA1 = 'b1;
|
||||
@(posedge CLK);
|
||||
if (32'hx !== RD1) begin
|
||||
@@ -153,7 +153,7 @@ module tb_rf_riscv();
|
||||
end
|
||||
endmodule
|
||||
|
||||
module rf_riscv_ref(
|
||||
module register_file_ref(
|
||||
input logic clk_i,
|
||||
input logic write_enable_i,
|
||||
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user