mirror of
https://github.com/MPSU/APS.git
synced 2025-09-15 09:10:10 +00:00
Initial commit
This commit is contained in:
108
Basic Verilog structures/Concatenation.md
Normal file
108
Basic Verilog structures/Concatenation.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Конкатенация (объединение сигналов)
|
||||
|
||||
Конкатенация позволяет присвоить какому-то многоразрядному сигналу "склейку" из нескольких сигналов меньшей разрядности, либо наоборот: присвоить сигнал большей разрядности группе сигналов меньшей разрядности.
|
||||
|
||||
Оператор конкатенации выглядит следующим образом: `{sig1, sig2, ..., sign}`.
|
||||
|
||||
Предположим, у нас есть следующий набор сигналов:
|
||||
|
||||

|
||||
|
||||
```SystemVerilog
|
||||
|
||||
logic a;
|
||||
logic b;
|
||||
logic [7:0] c;
|
||||
logic [1:0] d;
|
||||
|
||||
logic [5:0] e;
|
||||
```
|
||||
|
||||
И мы хотим, чтобы на провод `e` подавались следующие сигналы:
|
||||
|
||||
- на старший бит сигнала `e` подавался сигнал `a`
|
||||
- на его следующий бит подавался сигнал `b`
|
||||
- на его следующие 2 бита подавались биты `[4:3]` сигнала `c`
|
||||
- на младшие 2 бита подавался сигнал `d`
|
||||
|
||||

|
||||
|
||||
Это можно сделать путем 4 непрерывных присваиваний:
|
||||
|
||||
```SystemVerilog
|
||||
logic a;
|
||||
logic b;
|
||||
logic [7:0] c;
|
||||
logic [1:0] d;
|
||||
|
||||
logic [5:0] e;
|
||||
|
||||
assign e[5] = a;
|
||||
assign e[4] = b;
|
||||
assign e[3:2] = c[4:3];
|
||||
assign e[1:0] = d;
|
||||
```
|
||||
|
||||
либо через одно присваивание, использующее конкатенацию:
|
||||
|
||||
```SystemVerilog
|
||||
logic a;
|
||||
logic b;
|
||||
logic [7:0] c;
|
||||
logic [1:0] d;
|
||||
|
||||
logic [5:0] e;
|
||||
|
||||
assign e = {a, b, c[4:3], d};
|
||||
```
|
||||
|
||||
Кроме того, возможна и обратная ситуация. Предположим, мы хотим подать отдельные биты сигнала `e` на различные провода:
|
||||
|
||||

|
||||
|
||||
```SystemVerilog
|
||||
logic a;
|
||||
logic b;
|
||||
logic [7:0] c;
|
||||
logic [1:0] d;
|
||||
|
||||
logic [5:0] e;
|
||||
|
||||
assign a = e[5];
|
||||
assign b = e[4];
|
||||
assign c[4:3] = e[3:2];
|
||||
assign d = e[1:0];
|
||||
```
|
||||
|
||||
Подобную операцию можно так же выполнить в одно выражение через конкатенацию:
|
||||
|
||||
```SystemVerilog
|
||||
logic a;
|
||||
logic b;
|
||||
logic [7:0] c;
|
||||
logic [1:0] d;
|
||||
|
||||
logic [5:0] e;
|
||||
|
||||
assign {a, b, c[4:3], d} = e;
|
||||
```
|
||||
|
||||
Кроме того, конкатенация может использоваться при **множественном дублировании** сигналов. Дублирование выполняется выражением:
|
||||
|
||||
```SystemVerilog
|
||||
{a, {число_повторений{повторяемый_сигнал}} ,b}
|
||||
```
|
||||
|
||||
Допустим, мы хотим присвоить какому-то сигналу три копии `[4:3]` битов сигнала `c`, после которых идут сигналы `a` и `b`.
|
||||
Это можно сделать выражением:
|
||||
|
||||
```SystemVerilog
|
||||
logic a;
|
||||
logic b;
|
||||
logic [7:0] c;
|
||||
|
||||
logic [7:0] e;
|
||||
|
||||
assign e = { {3{c[4:3]}}, a, b};
|
||||
```
|
||||
|
156
Basic Verilog structures/Controllers.md
Normal file
156
Basic Verilog structures/Controllers.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Пример разработки модуля-контроллера периферийного устройства
|
||||
|
||||
Для того, чтобы лучше понять, что от вас требуется в рамках лабораторной работы по периферийным устройствам, рассмотрим процесс разработки структурной схемы (не SystemVerilog-описания) для контроллера светодиодов.
|
||||
|
||||
В первую очередь, здесь будет продублирована выдержка из спецификации на этот контроллер (общая часть раздела "[Описание контроллеров периферийных устройств](../../Labs/7.%20Peripheral%20units/README.md#описание-контроллеров-периферийных-устройств)", а также подраздел "[Светодиоды](../../Labs/7.%20Peripheral%20units/README.md#светодиоды)"):
|
||||
|
||||
## Спецификация контроллера
|
||||
|
||||
### Общие термины
|
||||
|
||||
1. Под "**запросом на запись** по адресу `0xАДРЕС`" будет пониматься совокупность следующих условий:
|
||||
1. Происходит восходящий фронт `clk_i`.
|
||||
2. На входе `req_i` выставлено значение `1`.
|
||||
3. На входе `write_enable_i` выставлено значение `1`.
|
||||
4. На входе `addr_i` выставлено значение `0xАДРЕС`
|
||||
2. Под "**запросом на чтение** по адресу `0xАДРЕС`" будет пониматься совокупность следующих условий:
|
||||
1. На входе `req_i` выставлено значение `1`.
|
||||
2. На входе `write_enable_i` выставлено значение `0`.
|
||||
3. На входе `addr_i` выставлено значение `0xАДРЕС`
|
||||
|
||||
Обратите внимание на то, что **запрос на чтение** должен обрабатываться **синхронно** (выходные данные должны выдаваться по положительному фронту `clk_i`).
|
||||
|
||||
При описании поддерживаемых режимов доступа по данному адресу используется интуитивно понятное обозначение:
|
||||
|
||||
* R — доступ **только на чтение**;
|
||||
* W — доступ **только на запись**;
|
||||
* RW — доступ на **чтение и запись**.
|
||||
|
||||
В случае отсутствия **запроса на чтения**, на выход `read_data_o` должно подаваться значение `32'hfa11_1eaf`. Это никак не повлияет на работу процессора, но будет удобно в процессе отладки на временной диаграмме (тоже самое было сделано в процессе разработки памяти данных).
|
||||
|
||||
Если пришел **запрос на запись** или **чтение**, это еще не значит, что контроллер должен его выполнить. В случае, если запрос происходит по адресу, не поддерживающему этот запрос (например **запрос на запись** по адресу поддерживающему только чтение или наоборот), данный запрос должен игнорироваться, а на выходе `read_data_o` должно появиться значение `32'hdead_beef`.
|
||||
|
||||
К примеру, в случае запроса на чтение по адресу `0x0100004` (четвертый байт в адресном пространстве периферийного устройства "переключатели"), на выходе `read_data_o` должно оказаться значение `32'hdead_beef`. В случае отсутствия запроса на чтение (`req_i == 0` или `write_enable_i == 1`), на выходе `read_data_o` контроллера переключателей должно оказаться значение `32'hfa11_1eaf`.
|
||||
|
||||
В случае осуществления записи по принятому запросу, необходимо записать данные с сигнала `write_data_i` в регистр, ассоциированный с адресом `addr_i` (если разрядность регистра меньше разрядности сигнала `write_data_i`, старшие биты записываемых данных отбрасываются).
|
||||
|
||||
В случае осуществления чтения по принятому запросу, необходимо по положительному фронту `clk_i` выставить данные с сигнала, ассоциированного с адресом `addr_i` на выходной сигнал `read_data_o` (если разрядность сигнала меньше разрядности выходного сигнала `read_data_o`, возвращаемые данные должны дополниться нулями в старших битах).
|
||||
|
||||
### Светодиоды
|
||||
|
||||
Светодиоды являются простейшим устройством вывода. Поэтому, чтобы задание было интересней, для их управления был добавлен регистр, управляющий режимом вывода данных на светодиоды.
|
||||
Рассмотрим прототип модуля, который вам необходимо реализовать:
|
||||
|
||||
```SystemVerilog
|
||||
module led_sb_ctrl(
|
||||
/*
|
||||
Часть интерфейса модуля, отвечающая за подключение к системной шине
|
||||
*/
|
||||
input logic clk_i,
|
||||
input logic rst_i
|
||||
input logic req_i,
|
||||
input logic write_enable_i,
|
||||
input logic [31:0] addr_i,
|
||||
input logic [31:0] write_data_i,
|
||||
output logic [31:0] read_data_o,
|
||||
|
||||
/*
|
||||
Часть интерфейса модуля, отвечающая за подключение к периферии
|
||||
*/
|
||||
output logic [15:0] led_o
|
||||
);
|
||||
|
||||
logic [15:0] led_val;
|
||||
logic led_mode;
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
Данный модуль должен выводить на выходной сигнал `led_o` данные с регистра `led_val`. Запись и чтение регистра `led_val` осуществляется по адресу `0x00`. Запись любого значения, превышающего `2¹⁶-1` должна игнорироваться.
|
||||
|
||||
Регистр `led_mode` отвечает за режим вывода данных на светодиоды. Когда этот регистр равен единице, светодиоды должны "моргать" выводимым значением. Под морганием подразумевается вывод значения из регистра `led_val` на выход `led_o` на одну секунду (загорится часть светодиодов, соответствующие которым биты шины `led_o` равны единице), после чего на одну секунду выход `led_o` необходимо подать нули. Запись и чтение регистра `led_mode` осуществляется по адресу `0x04`. Запись любого значения, отличного от `0` и `1` должна игнорироваться.
|
||||
|
||||
Отсчет времени можно реализовать простейшим счетчиком, каждый такт увеличивающимся на 1 и сбрасывающимся по достижении определенного значения, чтобы продолжить считать с нуля. Зная тактовую частоту, нетрудно определить до скольки должен считать счетчик. При тактовой частоте в 10 МГц происходит 10 миллионов тактов в секунду. Это означает, что при такой тактовой частоте через секунду счетчик будет равен `10⁷-1` (счет идет с нуля).
|
||||
|
||||
Обратите внимание на то, что адрес `0x24` является адресом сброса. В случае записи по этому адресу единицы вы должны сбросить регистры `led_val`, `led_mode` и все вспомогательные регистры, которые вы создали. Для реализации сброса вы можете как создать отдельный регистр `led_rst`, в который будет происходить запись, а сам сброс будет происходить по появлению единицы в этом регистре (в этом случае необходимо не забыть сбрасывать и этот регистр), так и создать обычный провод, формирующий единицу в случае выполнения всех указанных условий (условий запроса на запись, адреса сброса и значения записываемых данных равному единице).
|
||||
|
||||
Адресное пространство контроллера:
|
||||
|
||||
|Адрес|Режим доступа|Допустимые значения| Функциональное назначение |
|
||||
|-----|-------------|-------------------|-----------------------------------------------------------------------------------|
|
||||
|0x00 | RW | [0:65535] | Чтение и запись в регистр `led_val` отвечающий за вывод данных на светодиоды |
|
||||
|0x04 | RW | [0:1] | Чтение и запись в регистр `led_mode`, отвечающий за режим "моргания" светодиодами |
|
||||
|0x24 | W | 1 | Запись сигнала сброса
|
||||
|
||||
## Реализация схемы контроллера
|
||||
|
||||
Для начала, добавим на структурную схему входы и выходы модуля:
|
||||
|
||||

|
||||
|
||||
В первую очередь, спецификация вводит понятия **запрос на чтение** и **запрос на запись**. Создадим вспомогательные провода, которые будут сигнализировать о том, что произошел **запрос на чтение** или **запрос на запись**:
|
||||
|
||||

|
||||
|
||||
Далее, спецификация накладывает ограничение на допустимые адреса и значения. Поэтому создадим вспомогательные сигналы, сигнализирующие о том, что текущий адрес соответствует одному из регистров контроллера, а данные для записи соответствуют диапазону допустимых значений этих регистров:
|
||||
|
||||

|
||||
|
||||
Теперь, когда подготовительные работы выполнены, начнем с реализации сброса этого контроллера. Сброс может произойти в двух случаях: когда `rst_i == 1` либо же в случае **запроса на запись** единицы по адресу `0x24`. Создадим вспомогательный провод `rst`, который будет равен единице в случае, если произойдет любое из этих событий. Этот сигнал будет сбрасывать все созданные в данном модуле регистры.
|
||||
|
||||

|
||||
|
||||
Продолжим описание контроллера, создав первый из **архитектурных регистров** — `led_val`. Запись в этот регистр возможна только в случае выполнения трех условий:
|
||||
|
||||
* произошел **запрос на запись**;
|
||||
* `addr_i == 0x00`;
|
||||
* `write_data_i` находится в диапазоне [0:65535].
|
||||
|
||||
Создадим вспомогательный сигнал `val_en`, который будет равен единице только в случае выполнения этих трех условий:
|
||||
|
||||

|
||||
|
||||
Теперь реализация регистра `lev_val` становится совершенно тривиальной задачей, ведь у нас есть:
|
||||
|
||||
* сигнал сброса регистра `rst`;
|
||||
* сигнал разрешения записи в регистр `val_en`;
|
||||
* сигнал данных для записи в регистр `write_data_i`(из которого мы будем брать только младшие 16 бит данных).
|
||||
|
||||

|
||||
|
||||
Аналогичным образом реализуем еще один **архитектурный регистр** `led_mode`:
|
||||
|
||||

|
||||
|
||||
Два этих регистра должны управлять поведением выходного сигнала `led_o` следующим образом:
|
||||
|
||||
1. В случае `led_mode == 0` на выходе `led_o` должно оказаться значение `led_val`;
|
||||
2. В случае `led_mode == 1` на выходе `led_o` должно циклически меняться значение c `led_val` на `16'd0` и обратно с периодом в одну секунду.
|
||||
|
||||
Для реализации счета времени нам потребуется вспомогательный **неархитектурный регистр** `cntr`, который станет простейшим счетчиком со сбросом. Мы знаем, что тактовый сигнал нашей схемы будет работать с периодом в 10 МГц. Если каждый такт инкрементировать счетчик на единицу, то за одну секунду счетчик досчитает до 10 миллионов. Первой мыслью может показаться, что нам нужно, чтобы счетчик считал до 10 миллионов, дойдя до которых он бы сбрасывался в ноль, однако в этом случае у нас будут сложности при дальнейшей реализации. Будет куда удобней, если вместо этого счетчик будет считать до 20 миллионов (полного периода смены значения с `led_val` на `16'd0` и обратно). В этом случае, нам останется всего лишь добавить условие вывода значения на мультиплексор:
|
||||
|
||||
* пока значение счетчика меньше 10 миллионов, на выходе `led_o` будет значение `led_val`
|
||||
* в противном случае, на выходе `led_o` будет значение `16'd0`.
|
||||
|
||||
Таким образом, поведение счетчика описывается следующим образом:
|
||||
|
||||
* счетчик сбрасывается, в следующих случаях:
|
||||
* произошел сброс (`rst == 1`);
|
||||
* произошло отключение "моргания" светодиодов (`led_mode == 0`);
|
||||
* счетчик досчитал до 20 миллионов (`cntr >= 32'd20_000_000`);
|
||||
* в остальных ситуациях, счетчик инкрементирует свое значение.
|
||||
|
||||

|
||||
|
||||
Последним этапом описания контроллера будет добавление логики управления выходным сигналом `read_data_o`.
|
||||
|
||||
На управление этим сигналом наложены следующие требования:
|
||||
|
||||
* изменения этого сигнала должны быть **синхронными** (значит перед выходным сигналом должен стоять регистр);
|
||||
* в случае отсутствия **запроса на чтение**, данный сигнал должен принять значение `32'hfa11_1eaf`;
|
||||
* в случае **запроса на чтение** по неподдерживаемому адресу, данный сигнал должен принять значение `32'hdead_beef`;
|
||||
* в случае **запроса на чтение** по поддерживаемому адресу, данный сигнал должен принять значение ассоциированного с этим адресом регистра (дополнив это значение нулями в старших разрядах).
|
||||
|
||||
Таким образом, итоговая схема примет вид:
|
||||
|
||||

|
296
Basic Verilog structures/Modules.md
Normal file
296
Basic Verilog structures/Modules.md
Normal file
@@ -0,0 +1,296 @@
|
||||
# Описание модулей в SystemVerilog
|
||||
|
||||
Основой цифровых схем в SystemVerilog является модуль. Модуль — это блок SystemVerilog-кода описывающий цифровую схему какого-то устройства, например пульта телевизора:
|
||||
|
||||

|
||||
|
||||
У пульта есть входные сигналы: кнопки, нажатие на которые сообщает о нашем намерении изменить громкость или переключить канал. Кроме того, есть выходной сигнал ИК-светодиода, по которому пульт отправляет информацию телевизору.
|
||||
|
||||
Для создания модуля в языке SystemVerilog используются ключевые слова `module` и `endmodule`, которые определяют начало и конец модуля, обрамляя его. Можно сказать, что эти ключевые слова являются корпусом нашего устройства, отделяют его содержимое от внешнего мира.
|
||||
|
||||
Определим наш модуль:
|
||||
|
||||

|
||||
|
||||
```SystemVerilog
|
||||
module
|
||||
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
У всякого модуля должно быть название. Назовём его `box`. В круглых скобках пишутся имена портов, их направление и типы. Если модуль не имеет ни входов, ни выходов, внутри скобок ничего не пишется. После них всегда ставится точка с запятой.
|
||||
|
||||

|
||||
|
||||
```SystemVerilog
|
||||
module box();
|
||||
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
Модуль без входов и выходов (портов) — это просто коробка, которая никак не взаимодействует с внешним миром. Подключим к нему два входных сигнала `a, b` и один выходной `q`. Для объявления портов, необходимо указать направление порта (вход это или выход), и тип используемого сигнала. В рамках данного курса лабораторных работ в качестве типа и входов и выходов будет использоваться тип `logic`, о котором будет рассказано чуть позже.
|
||||
|
||||

|
||||
|
||||
```SystemVerilog
|
||||
module box(
|
||||
input logic a,
|
||||
input logic b,
|
||||
output logic q
|
||||
);
|
||||
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
Внутри модуля могут быть объявления сигналов, параметров, констант и т.п., о которых другой модуль не узнает. Объявим внутри модуля `box` провод `c`.
|
||||
|
||||

|
||||
|
||||
```SystemVerilog
|
||||
module box(
|
||||
input logic a,
|
||||
input logic b,
|
||||
|
||||
output logic q
|
||||
);
|
||||
|
||||
logic c;
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
Для объявления провода `c` использовалось ключевое слово (тип) `logic`. Этот тип, подобно стволовым клеткам, может быть в конечном итоге привести к созданию как ячеек памяти (регистров), так и проводов, в зависимости от того, как было описано присваивание объекту этого типа. Поэтому в примере выше говорить о том, что был создан провод не совсем корректно, объект схемы `c` станет проводом, когда будет произведено подключение к этому объекту, соответствующее подключению провода.
|
||||
|
||||
Подключим провод `c` ко входу `a`. Для этого используется конструкция `assign c = a;`. Такая конструкция называется **непрерывным присваиванием**. Если очень сильно упростить, то непрерывное присваивание схоже со спайкой двух проводов. После подобного присваивания, провод `c` всегда будет иметь то же значение, что и `a` — как только входной сигнал `a` изменит свое значение, внутренний провод `c` также изменит свое значение (проводу `c` будет **непрерывно присваиваться** значение входа `a`).
|
||||
|
||||

|
||||
|
||||
```SystemVerilog
|
||||
module box(
|
||||
input logic a,
|
||||
input logic b,
|
||||
|
||||
output logic q
|
||||
);
|
||||
|
||||
logic c;
|
||||
|
||||
assign c = a;
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
Стоит однако заметить, что аналогия со спайкой проводов имеет свои недостатки: после неё некоторые студенты начинают думать, что расположение "спаиваемых" сигналов относительно знака равно не имеет значения, однако это не так.
|
||||
|
||||
В непрерывном присваивании участвует две компоненты: выражение-приемник сигнала и выражение-источник сигнала. Обычно, выражением-приемником является провод (либо группа проводов). Выражение-источник сигнала может быть совершенно различным. В примере приведенном выше выражением-источником так же был провод, но вместо него мог использоваться и регистр и выражение, построенное из цепочки арифметических или логических вентилей.
|
||||
|
||||
Важно понять, что при непрерывном присваивании слева от знака равно указывается то **чему мы будем присваивать**, а справа от знака равно указывается то **что мы будем присваивать**.
|
||||
|
||||
К примеру, мы можем присвоить проводу `с` значение выхода логического вентиля. Пусть нам нужно, чтобы к сигналу `c` был подключен результат операции `a ИЛИ b`.
|
||||
|
||||

|
||||
|
||||
Такую схему можно реализовать следующим описанием:
|
||||
|
||||
```SystemVerilog
|
||||
module box(
|
||||
input logic a,
|
||||
input logic b,
|
||||
|
||||
output logic q
|
||||
);
|
||||
|
||||
logic c;
|
||||
|
||||
assign c = a | b;
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
Пусть в схеме имеется ещё один логический вентиль - Исключающее ИЛИ. На него подаётся результат операции `a ИЛИ b`, то есть `c`, а также входной сигнал `b`. Результат операции `c ИСКЛЮЧАЮЩЕЕ ИЛИ b` подаётся на выход `q` нашего модуля.
|
||||
|
||||

|
||||
|
||||
```SystemVerilog
|
||||
module box(
|
||||
input logic a,
|
||||
input logic b,
|
||||
|
||||
output logic q
|
||||
);
|
||||
|
||||
logic c;
|
||||
|
||||
assign c = a | b;
|
||||
assign q = c ^ b;
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
Отлично! Мы научились создавать простейшее описание модуля.
|
||||
|
||||
Для завершения базового представления о модулях осталось разобраться с таким понятием как **вектор**.
|
||||
|
||||
## Векторы
|
||||
|
||||
В SystemVerilog **вектором** называют группу проводов или регистров, объединенных общим именем, которая может использоваться как для передачи многоразрядных чисел, так и нескольких сигналов, выполняющих общую задачу.
|
||||
|
||||
Синтаксис объявления вектора представлен ниже:
|
||||
|
||||
<pre>
|
||||
<тип> [<старший индекс>:<младший индекс>] <i>имя_вектора</i>
|
||||
</pre>
|
||||
|
||||
Несмотря на то, что может использоваться любой диапазон индексов (даже отрицательный), на практике стараются начинать младший индекс с нуля.
|
||||
|
||||
Пример:
|
||||
|
||||
<pre>
|
||||
<b>logic</b> [7:0] <i>sum</i>; // Объявляется 8-битный вектор с именем sum типа logic.
|
||||
// Старший индекс равен 7, младший — 0.
|
||||
</pre>
|
||||
|
||||
Используя индекс, можно обратиться к отдельным битам вектора. С помощью диапазона индексов можно получить доступ к диапазону соответствующих битов.
|
||||
|
||||
|фрагмент кода|описание|
|
||||
|-------------|--------|
|
||||
|sum[0]; | Обращение к младшему биту вектора sum, объявленного выше|
|
||||
|sum[7:4]; | Обращение к старшим четырем битам 8-битного вектора sum, объявленного выше|
|
||||
|
||||
Важно понимать, что векторы могут быть использованы и при описании портов модуля:
|
||||
|
||||
```SystemVerilog
|
||||
module vector_ex(
|
||||
input logic [3:0] a, // У данного модуля четырехразрядный вход 'a'
|
||||
output logic [7:0] b // и восьмиразрядный выход 'b'.
|
||||
);
|
||||
|
||||
assign b[7:4] = a; // К старшим четырем битам выхода b подключен вход a
|
||||
assign b[3:1] = a[2:0]; // К битам с третьего по первый выхода b подключены
|
||||
// биты со второго по нулевой входа a
|
||||
assign b[0] = a[3]; // к младшему биту b подключен старший бит a;
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
## Иерархия модулей
|
||||
|
||||
Модули могут содержать другие модули. Реализуя модуль "Пульт ДУ" можно использовать такие цифровые схемы как "Передатчик ИК-сигнала" и "Контроллер нажатия клавиш". Обе эти цифровые схемы могут быть независимыми модулями, которые объединяются в модуле верхнего уровня.
|
||||
|
||||
Допустим, у нас есть модуль `inv`, который подает на выход инверсию входа и мы хотим реализовать модуль `top`, который хочет использовать функционал модуля `inv` следующим образом:
|
||||
|
||||

|
||||
|
||||
Опишем `inv`:
|
||||
|
||||
```SystemVerilog
|
||||
module inv(
|
||||
input logic a,
|
||||
output logic d
|
||||
);
|
||||
|
||||
assign d = ~a;
|
||||
endmodule
|
||||
```
|
||||
|
||||
Опишем `top`:
|
||||
|
||||
```SystemVerilog
|
||||
module top(
|
||||
input logic a,
|
||||
input logic b,
|
||||
output logic q
|
||||
);
|
||||
// подключение модуля
|
||||
inv invertor_1( // подключаем модуль inv и
|
||||
// даём экземпляру этого модуля
|
||||
// имя invertor_1
|
||||
|
||||
.a(a), // вход а модуля inv подключаем ко
|
||||
//входу a модуля top
|
||||
|
||||
.d(c) // выход d модуля inv подключаем к
|
||||
// проводу с модуля top
|
||||
);
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
Обратите внимание на то, как подключаются сигналы к вложенному модулю: при подключении после `.` пишется имя сигнала подключаемого модуля, затем в скобках пишется имя сигнала подключающего модуля. Для лучшего понимания, посмотрите внимательно на схеме на провод `c` и выход `d` модуля `inv`, а так же на SystemVerilog-описание этой схемы.
|
||||
|
||||
Мы можем подключить сколько угодно экземпляров одного модуля, поэтому у каждого из экземпляра должно быть свое уникальное имя. Пусть `c` подаётся на логический вентиль И вместе со входом `b`. Результат операции И тоже пойдет на инвертор, а затем на выход `q` модуля top.
|
||||
|
||||

|
||||
|
||||
Тогда в нашем описании добавится подключение второго модуля `inv` и провод `c`.
|
||||
|
||||
```SystemVerilog
|
||||
module inv(
|
||||
input logic a,
|
||||
output logic d
|
||||
);
|
||||
|
||||
assign d = ~a;
|
||||
endmodule
|
||||
```
|
||||
|
||||
```SystemVerilog
|
||||
module top(
|
||||
input logic a,
|
||||
input logic b,
|
||||
output logic q
|
||||
);
|
||||
|
||||
logic c;
|
||||
|
||||
// подключение модуля 1
|
||||
inv invertor_1( // подключаем модуль inv и даём ему
|
||||
// имя invertor_1
|
||||
|
||||
.a(a), // подключаем вход 'а' модуля inv ко
|
||||
// входу 'a' модуля top
|
||||
|
||||
.d(c) // подключаем выход 'd' модуля inv к
|
||||
// проводу 'с' модуля top
|
||||
);
|
||||
|
||||
// подключение модуля 2
|
||||
inv invertor_2( // подключаем модуль inv и даём ему
|
||||
// имя invertor_2
|
||||
|
||||
.a(c & b), // на вход 'а' модуля inv подаём
|
||||
// результат логической операции
|
||||
// "с И b"
|
||||
|
||||
.d(q) // подключаем выход 'd' модуля inv
|
||||
// к выходу q модуля top
|
||||
);
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
___
|
||||
|
||||
## Итоги
|
||||
|
||||
1. Ключевым блоком в иерархии цифровой схемы, описанной на языке SystemVerilog является **модуль**. Модули позволяют выносить части сложной цифровой схемы в отдельные блоки, из которых потом и будет составлена итоговая схема, что сильно упрощает разработку.
|
||||
2. Условно, модуль можно разделить на следующие части:
|
||||
1. Объявление модуля:
|
||||
1. Ключевые слова `module` / `endmodule` определяющие границы описания модуля.
|
||||
2. Название модуля, следующее за ключевым словом `module`. Описанный модуль представляет собой отдельный тип, имя которого совпадает с названием модуля.
|
||||
3. Указание входов и выходов (портов) модуля, идущих в круглых скобках после названия модуля. Для указания направления порта модуля используются ключевые слова `input` и `output`. После указание направления порта следует указать тип порта (в рамках данного курса типом портов всегда будет logic), его разрядность, а затем имя.
|
||||
2. Функциональное описание модуля:
|
||||
1. Объявление внутренних сигналов модуля (будь то проводов или регистров) с помощью ключевого слова `logic`.
|
||||
2. Создание при необходимости объектов других модулей.
|
||||
3. Описание функциональной связи между различными сигналами и объектами внутри описываемого модуля.
|
||||
|
||||
## Проверь себя
|
||||
|
||||
Как по-вашему описать нижеприведенную схему на языке описания аппаратуры SystemVerilog?
|
||||
|
||||
Обратите внимание, что вход `a` модуля `top` является двухразрядным: нулевой его бит идет на вход `a` модуля `or`, первый бит идет на вход `b` модуля `or`.
|
||||
|
||||

|
240
Basic Verilog structures/Multiplexors.md
Normal file
240
Basic Verilog structures/Multiplexors.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# Описание мультиплексора на языке SystemVerilog
|
||||
|
||||
**Мультипле́ксор** — устройство, имеющее **несколько сигнальных входов**, **один или более управляющих входов** и **один выход**. Мультиплексор позволяет передавать сигнал **с одного из входов на выход**; при этом выбор желаемого входа осуществляется подачей соответствующей комбинации управляющих сигналов.
|
||||
|
||||
Иными словами, мультиплексор — это переключатель (коммутатор), соединяющий выход с одним из множества входов.
|
||||
|
||||

|
||||
|
||||
Для начала создадим простой двухвходовой мультиплексор. Предположим, на `Y` нам необходимо передать один из сигналов — `D0` или `D1` в зависимости от значения управляющего сигнала `S`: когда `S==0`, на `Y` подается сигнал `D0`, в противном случае — `D1`.
|
||||
|
||||

|
||||
|
||||
На языке SystemVerilog это можно описать несколькими способами. Первый — с помощью **[тернарного условного оператора](https://ru.wikipedia.org/wiki/Тернарная_условная_операция)**:
|
||||
|
||||
## Тернарный условный оператор
|
||||
|
||||
<details>
|
||||
|
||||
<summary>О тернарном условном операторе</summary>
|
||||
|
||||
Операторы бывают различной **[арности](https://ru.wikipedia.org/wiki/Арность)**(количества аргументов оператора[операндов]):
|
||||
|
||||
- унарный (с одним операндом), пример: `-a`;
|
||||
- бинарный (с двумя операндами), пример: `a+b`;
|
||||
- тернарный (с тремя операндами), пример: `cond ? if_true : false`;
|
||||
- и др.
|
||||
|
||||
Несмотря на то, что тернарным оператором может быть любой оператор, принимающий три операнда, обычно под ним подразумевается **тернарный условный оператор**, работающий следующим образом:
|
||||
|
||||
```text
|
||||
<условие> ? <значение_если_условие_истинно> : <значение_если_условие_ложно>
|
||||
```
|
||||
|
||||
Первым операндом идет некоторое условие (любое выражение, которое может быть сведено к 1 или 0). Далее ставится знак вопроса (часть тернарного оператора, отделяющая выражение первого операнда от выражения второго операнда). Далее пишется выражение, которое будет результатом тернарного условного оператора в случае, если условие оказалось истинным. После чего ставится двоеточие (часть тернарного условного оператора, отделяющая выражение второго операнда от выражения третьего операнда). Затем пишется выражение, которое будет результатом тернарного условного оператора в случае, если условие оказалось ложным.
|
||||
|
||||
Пример для языка C++:
|
||||
|
||||
```c++
|
||||
a = b+c >= 5 ? b+c : b+d;
|
||||
```
|
||||
|
||||
Сперва вычисляется первый операнд (выражение `b+c >= 5`). Если это выражение оказалось истинным (равно единице), то переменной `a` будет присвоено значение второго операнда (выражения `b+c`), в противном случае переменной `a` будет присвоено значение третьего операнда (выражения `b+d`).
|
||||
</details>
|
||||
|
||||
```SystemVerilog
|
||||
logic Y;
|
||||
assign Y = S==1 ? D1 : D0;
|
||||
```
|
||||
|
||||
Данное выражение говорит нам, что если `S==1`, то `Y` присваивается значение `D1`, в противном случае — значение `D0`.
|
||||
|
||||

|
||||
|
||||
Также мультиплексор можно описать через конструкцию `if-else` в блоке `always`.
|
||||
|
||||
## Блок if-else
|
||||
|
||||
> Далее будет ключевой параграф сложного для понимания текста, очень важно запомнить что там написано и разобрать приведенные листинги.
|
||||
|
||||
<br><br>
|
||||
|
||||
---
|
||||
|
||||
### Блок always
|
||||
|
||||
Блок `always` — это специальный блок, который позволяет описывать комбинационные и последовательностные схемы, используя более сложные конструкции, такие как `if-else`, `case`. На самом деле, в языке SystemVeriog помимо общего блока `always`, которым можно описать любой вид логики, существует множество специализированных блоков, предназначенных для описания отдельно комбинационной, синхронной и последовательностной асинхронной логики соответственно:
|
||||
|
||||
- always_comb
|
||||
- always_ff
|
||||
- always_latch
|
||||
|
||||
Мультиплексор можно описать в любом из этих блоков, разница будет лишь в том, к чему именно будет подключен выход мультиплексора: к проводу, регистру, или защелке.
|
||||
|
||||
При присваивании внутри блоков `always` используйте специальный оператор **неблокирующего присваивания** `<=`. Бывает еще оператор **блокирующего присваивания** `=`, объяснение различий в этих операторах требует отдельного документа, поэтому на текущий момент, во избежание проблем в будущем просто запомните: **внутри любого блока always необходимо использовать только оператор неблокирующего присваивания <=**.
|
||||
|
||||
```SystemVerilog
|
||||
logic Y;
|
||||
always_comb begin // 1) Используется always_comb, т.к. мы хотим подключить
|
||||
// выход мультиплексора к проводу
|
||||
if(S) begin // 2) if-else может находиться только внутри блока always.
|
||||
Y <= D1; // 3) Используется оператор неблокирующего присваивания.
|
||||
end else begin
|
||||
Y <= D0;
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Кроме того, важно запомнить, что присваивание сигналу допускается **только в одном** блоке always.
|
||||
|
||||
Неправильно:
|
||||
|
||||
```SystemVerilog
|
||||
logic Y;
|
||||
always_comb begin
|
||||
if(S==1) begin
|
||||
Y <= D1;
|
||||
end
|
||||
end
|
||||
|
||||
always_comb begin
|
||||
if(S==0) begin // Нельзя выполнять операцию присваивания
|
||||
Y <= D0; // для одного сигнала (Y) в нескольких
|
||||
end // блоках always!
|
||||
end
|
||||
```
|
||||
|
||||
Если нарушить это правило то в будущем (возможно не сразу, но в любом случае — обязательно), возникнет ошибка, которая так или иначе будет связана с **multiple drivers**.
|
||||
|
||||
---
|
||||
|
||||
> Остановитесь на выделенном выше фрагменте документа, пока полностью не разберете его. Без освоения всех описанных выше особенностей языка SystemVerilog вы столкнетесь в будущем с множеством ошибок.
|
||||
|
||||
<br><br>
|
||||
|
||||
## case-блок
|
||||
|
||||
Мультиплексор также можно описать с использованием **конструкции case**. Блок `case` лучше подходит для описания мультиплексора, когда у того более двух входов (ведь в случае конструкции `if-else` пришлось бы делать вложенное ветвление).
|
||||
|
||||
Конструкция `case` представляет собой инструмент множественного ветвления, который сравнивает значение заданного выражения с множеством вариантов, и, в случае первого совпадения, использует соответствующую ветвь. На случай, если ни один из вариантов не совпадет с заданным выражением, конструкция `case` поддерживает вариант `default`. Данная конструкция визуально похожа на оператор `switch-case` в Си, однако вы должны понимать, что используется она не для написания программы, а описания аппаратуры, в частности **мультиплексоров**/**демультиплексоров** и **дешифраторов**.
|
||||
|
||||
**Конструкция `case`, наряду с `if-else`, может быть описана только в блоке `always`**.
|
||||
|
||||
Реализация двухвходового мультиплексора с помощью `case` может выглядеть так:
|
||||
|
||||
```SystemVerilog
|
||||
logic Y;
|
||||
always @(*) begin
|
||||
case(S) // Описываем блок case, где значение сигнала S
|
||||
// будет сравниваться с различными возможными его значениями
|
||||
1'b0: Y <= D0; // Если S==0, то Y = D0
|
||||
1'b1: Y <= D1;
|
||||
endcase // Каждый case должен заканчиваться endcase
|
||||
end // (так же как каждый begin должен оканчиваться end)
|
||||
```
|
||||
|
||||
Рассмотрим вариант посложнее и опишем следующую схему:
|
||||
|
||||

|
||||
|
||||
Здесь уже используется мультиплексор 4в1. Управляющий сигнал `S` в данном случае двухбитный. В блоке `case` мы перечисляем всевозможные варианты значений `S` и описываем выход мультиплексора.
|
||||
|
||||
```SystemVerilog
|
||||
module case_mux_ex(
|
||||
input logic A,
|
||||
input logic B,
|
||||
input logic C,
|
||||
input logic D,
|
||||
input logic [2:0] S,
|
||||
|
||||
output logic Y
|
||||
|
||||
);
|
||||
always_comb begin
|
||||
case(S)
|
||||
3'b00: Y <= A;
|
||||
3'b01: Y <= C | B; // в блоке case можно мультиплексировать
|
||||
// не только провода, но и логические выражения
|
||||
3'b10: Y <= (C|B) & D;
|
||||
/*
|
||||
Обратите внимание, что разрядность сигнала S — 3 бита.
|
||||
Это означает, что есть 8 комбинаций его разрядов.
|
||||
Выше было описано только 3 комбинации из 8.
|
||||
Если для всех остальных комбинаций на выходе мультиплексора должно
|
||||
быть какое-то одно значение "по умолчанию", используется специальная
|
||||
комбинация "default":
|
||||
*/
|
||||
default: Y <= D;
|
||||
endcase
|
||||
end
|
||||
endmodule
|
||||
```
|
||||
|
||||
### Защелка
|
||||
|
||||
Очень важно при описании блока `case` описывать оставшиеся комбинации управляющего сигнала с помощью `default` — в противном случае в вашей схеме может появиться [защелка](https://www.build-electronic-circuits.com/d-latch/) (даже несмотря на то, что для описания защелок в SytemVerilog есть отдельный блок `always`: `always_latch`).
|
||||
|
||||
Защелка позволяет хранить данные, причем данные в нее записываются не по тактовому синхроимпульсу, а на протяжении относительно длинного промежутка времени, когда управляющий сигнал "открывает" защелку. Из-за этого она не является ни комбинационной, ни синхронной схемой. Обычно появление защелки в цифровой схеме говорит об ошибке разработки: в случае, если планировалась комбинационная логика, добавление защелки приведет к непредвиденному удержанию предыдущих значений (поскольку защелка сохраняет предыдущее значение до прихода очередной комбинации управляющего сигнала, описанной в блоке `case`). В случае синхронной логики, будет непредсказуемое поведение данных, т.к. информация в защелку будет записываться не синхронно, как это ожидается, а на протяжении длительного промежутка времени, пока защелка "открыта".
|
||||
|
||||
Пример:
|
||||
|
||||
```SystemVerilog
|
||||
module unexpected_d_latch_ex(
|
||||
input logic [1:0] S,
|
||||
input logic D0,
|
||||
input logic D1,
|
||||
output logic R
|
||||
);
|
||||
|
||||
always_comb begin
|
||||
case(S)
|
||||
2'b00: R <= D0;
|
||||
2'b01: R <= D1;
|
||||
// Поскольку сигнал S двухразрядный, осталось еще две комбинации:
|
||||
// S == 2'b10
|
||||
// S == 2'b11
|
||||
endcase
|
||||
end
|
||||
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||

|
||||
|
||||
На данной схеме различные её части обозначены следующим образом:
|
||||
|
||||
1. Мультиплексор, который мы хотели описать
|
||||
2. Защелка
|
||||
3. Мультиплексор, который был добавлен чтобы генерировать сигнал, "открывающий" защелку
|
||||
4. Константная единица (питание)
|
||||
5. Константный ноль (земля).
|
||||
|
||||
В случае, если `S == 0` или `S == 1`, на выход мультиплексора 3 будет подана единица, которая переведет защелку в "прозрачный" режим (данные с выхода мультиплексора 1 будут проходить сквозь защелку).
|
||||
|
||||
В случае, если `S > 1`, на выход мультиплексора 3 будет подан ноль, который переведет защелку в "непрозрачный" режим (данные с выхода мультиплексора 1 не будут идти сквозь защелку, вместо этого на выходе защелки останутся последние данные, которые шли через нее, пока она была "открыта").
|
||||
|
||||

|
||||
|
||||
Таким образом, во избежание появление защелки, необходимо описывать все возможные комбинации в блоке `case` (при необходимости покрывая множество оставшихся комбинаций с помощью `default`).
|
||||
|
||||
## Итоги
|
||||
|
||||
1. Мультиплексор — это **комбинационный** блок, подающий на выход один из нескольких входных сигналов.
|
||||
2. Мультиплексор можно описать множеством способов, среди них:
|
||||
1. Использование [тернарного условного оператора](#тернарный-условный-оператор) через непрерывное присваивание;
|
||||
2. Использование конструкции [`if-else`](#блок-if-else) внутри блока `always`;
|
||||
3. Использование конструкции [`case`](#case-блок) внутри блока always (при этом стоит помнить, что через `case` можно описывать не только мультиплексоры).
|
||||
1. При использовании `case`, во избежание появления [защелок](#защелка), необходимо убедиться в том, что описаны все возможные комбинации управляющего сигнала (при необходимости, множество оставшихся комбинаций можно покрыть с помощью комбинации `default`)
|
||||
3. Конструкции `if-else` и `case` в рамках данных лабораторных работ можно описывать только внутри блока [`always`](#блок-always). При работе с этим блоком необходимо помнить следующие особенности:
|
||||
1. Существует несколько типов блока `always`: `always_comb`, `always_ff`, `always_latch`, определяющих то, к чему будет подключена описанная в этом блоке логика: проводу, регистру или защелке соответственно.
|
||||
2. Внутри блока always следует использовать оператор неблокирующего присваивания `<=`.
|
||||
3. Присваивание для любого сигнала возможно только внутри **одного** блока always. Два разных сигнала могут присваиваться как в одном блоке always, так каждый в отдельном, но операция присваивания одному и тому же сигналу в двух разных блоках always — нет.
|
||||
|
||||
---
|
||||
|
||||
## Проверь себя
|
||||
|
||||
Как, по-вашему, описать на языке SystemVerilog схему, приведённую ниже?
|
||||
|
||||

|
12
Basic Verilog structures/README.md
Normal file
12
Basic Verilog structures/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Базовые конструкции языка Verilog
|
||||
|
||||
Данные файлы содержат информацию, овладев которой вы сможете без труда выполнить первые лабораторные работы.
|
||||
Порядок изучения следующий:
|
||||
|
||||
1. Для первой лабораторной работы необходимо разобраться как описывается базовый модуль и [комбинационная логика](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BC%D0%B1%D0%B8%D0%BD%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D0%B0%D1%8F_%D0%BB%D0%BE%D0%B3%D0%B8%D0%BA%D0%B0), построенная на непрерывном присваивании. Этому посвящен документ [Modules.md](Modules.md).
|
||||
2. Для выполнения второй лабораторной работы необходимо уметь писать базовый модуль (см. пункт 1) и описывать такой комбинационный блок, как [мультиплексор](https://ru.wikipedia.org/wiki/%D0%9C%D1%83%D0%BB%D1%8C%D1%82%D0%B8%D0%BF%D0%BB%D0%B5%D0%BA%D1%81%D0%BE%D1%80_%28%D1%8D%D0%BB%D0%B5%D0%BA%D1%82%D1%80%D0%BE%D0%BD%D0%B8%D0%BA%D0%B0%29). Этому посвящен документ [Multiplexors.md](./Multiplexors.md).
|
||||
3. Для выполнения третьей лабораторной работы в дополнение к предыдущим добавляется знание по описанию базовой ячейки памяти — регистру, и способу группировки сигналов (конкатенации). Этому посвящены документы [Registers.md](./Registers.md) и [Concatenation.md](./Concatenation.md) соответственно.
|
||||
|
||||
Для выполнения всех последующих лаб необходимы знания по всем этим документам.
|
||||
|
||||
Желаю успехов при подготовке к лабораторным работам!
|
217
Basic Verilog structures/Registers.md
Normal file
217
Basic Verilog structures/Registers.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# Описание регистра на языке SystemVerilog
|
||||
|
||||
Перед тем, как описывать память, необходимо научиться описывать отдельные регистры. [Регистр](https://ru.wikipedia.org/wiki/Регистр_(цифровая_техника)) — это базовая ячейка памяти, позволяющая хранить состояние, пока на схему подается питание. В современной электронике, регистр чаще всего строится на D-триггерах. В лабораторной работе по АЛУ уже вскользь упоминалось, что как для описания проводов, так и для описания регистров, используется тип `logic`.
|
||||
|
||||
```SystemVerilog
|
||||
logic reg_name;
|
||||
```
|
||||
|
||||

|
||||
|
||||
У регистра может быть несколько входов и один выход. Основных входов, без которых не может существовать регистр два: вход данных и вход тактирующего синхроимпульса. На рисунке они обозначены как `D` и `clk`. Опциональный вход сигнала сброса (`rst`) позволяет обнулять содержимое регистра вне зависимости от входных данных и может работать как с тактовым синхроимпульсом (синхронный сброс), так и без него (асинхронный сброс).
|
||||
|
||||
Помимо прочего у регистра также может быть входной сигнал разрешения записи (`enable`), который определяет будут ли записаны данные с входного сигнала данных в регистр или нет, опциональный вход установки (`set`), позволяющий принудительно выставить значение регистра в единицу.
|
||||
|
||||
Выход у регистра один. На рисунке выше он обозначен как `Q`.
|
||||
|
||||
Важно понимать, что названия приведенных портов не являются чем-то высеченным на камне, они просто описывают функциональное назначение. В процессе описания работы регистра вы будете оперировать только над именем регистра, и сигналами, которые подводите к нему.
|
||||
|
||||
Поскольку все сигналы в цифровой схеме передаются по цепям, удобно представлять, что к выходу регистра всегда неявно подключен провод, с именем, совпадающим с именем регистра, поэтому вы можете использовать имя регистра в дальнейшей цифровой логике:
|
||||
|
||||

|
||||
|
||||
Итак, мы добавили регистр на холст схемы, но как соединить его с какой-то логикой? Предположим, у нас есть сигнал тактового синхроимпульса и данные, которые мы хотим записать:
|
||||
|
||||

|
||||
|
||||
Данной схеме соответствует код:
|
||||
|
||||
```SystemVerilog
|
||||
modulе rеg_ехаmрlе(
|
||||
inрut logic clk,
|
||||
inрut logic dаtа,
|
||||
оutрut logic rеg_dаtа
|
||||
);
|
||||
|
||||
logic rеg_nаmе;
|
||||
|
||||
еndmоdulе
|
||||
```
|
||||
|
||||
Очевидно, мы хотим подключить сигнал `clk` ко входу тактирующего сигнала регистра, вход `data` ко входу данных, а выход регистра к выходу `reg_data`:
|
||||
|
||||

|
||||
|
||||
Запись в регистр возможна только по фронту тактирующего синхроимпульса. **Фронт** — это переход сигнала из нуля в единицу (**положительный фронт**), либо из единицы в ноль (**отрицательный фронт**).
|
||||
|
||||
Описание регистра, а так же указание фронта и тактирующего сигнала происходит в конструкции `always_ff`:
|
||||
|
||||
```SystemVerilog
|
||||
аlwауs_ff @(pоsеdgе clk)
|
||||
```
|
||||
|
||||
Далее, внутри данной конструкции необходимо указать, что происходит с содержимым регистра. В нашем случае, происходит запись с входного сигнала `data`
|
||||
|
||||
```SystemVerilog
|
||||
аlwауs_ff @(pоsеdgе clk)
|
||||
rеg_nаmе <= dаtа;
|
||||
еnd
|
||||
```
|
||||
|
||||
Обратите внимание на оператор `<=`. В данном случае, это не знак "меньше либо равно", а оператор **неблокирующего присваивания**. Существует оператор **блокирующего присваивания** (`=`), который меняет способ построения схемы для такого же выражения справа от оператора, однако в данный момент этот оператор останется за рамками курса. Хоть это и плохая практика в обучении, но пока вам надо просто запомнить, что **при описании записи в регистр всегда используйте оператор неблокирующего присваивания `<=`**.
|
||||
|
||||
Помимо прочего, нам необходимо связать выход схемы с выходом регистра. Это можно сделать уже известным вам оператором **непрерывного присваивания** `assign`.
|
||||
|
||||
Таким образом, итоговый код описания данной схемы примет вид:
|
||||
|
||||
```SystemVerilog
|
||||
modulе rеg_ехаmрlе(
|
||||
inрut logic сlk,
|
||||
inрut logic dаtа,
|
||||
оutрut logic rеg_dаtа
|
||||
);
|
||||
|
||||
logic rеg_nаmе;
|
||||
|
||||
аlwауs_ff @(pоsеdgе clk) bеgin
|
||||
rеg_nаmе <= dаtа;
|
||||
еnd
|
||||
|
||||
аssign reg_data = reg_name;
|
||||
|
||||
еndmоdulе
|
||||
```
|
||||
|
||||
Предположим, мы хотим добавить управление записью в регистр через сигналы `enable` и `reset`. Это, например, можно сделать следующим образом:
|
||||
|
||||
```SystemVerilog
|
||||
modulе rеg_ехаmрlе(
|
||||
inрut logic сlk,
|
||||
inрut logic dаtа,
|
||||
inрut logic reset,
|
||||
inрut logic enable,
|
||||
оutрut logic rеg_dаtа
|
||||
);
|
||||
|
||||
logic rеg_nаmе;
|
||||
|
||||
аlwауs_ff @(pоsеdgе clk) bеgin
|
||||
if(rеsеt) bеgin
|
||||
rеg_nаmе <= 1'b0;
|
||||
еnd
|
||||
еlse if(enable) bеgin
|
||||
rеg_nаmе <= dаtа;
|
||||
еnd
|
||||
еnd
|
||||
|
||||
аssign rеg_dаtа = rеg_nаmе;
|
||||
|
||||
еndmоdulе
|
||||
```
|
||||
|
||||
Обратите внимание на очередность условий. В первую очередь, мы проверяем условие **сброса**, и только после этого условие **разрешения на запись**.
|
||||
Если сперва проверить разрешение на запись, а затем в блоке `else` описать логику сброса, то регистр не будет сбрасываться в случае, если `enable` будет равен `1` (запись в регистр будет приоритетней его сброса). Если сброс описать не в блоке `else`, а в отдельном блоке `if`, то может возникнуть неопределенное состояние: нельзя однозначно сказать в какой момент придет сигнал `reset` относительно сигнала `enable` и что в итоге запишется в регистр. Поэтому при наличии сигнала сброса, остальная логика по записи в регистр должна размещаться в блоке `else`.
|
||||
|
||||
Кроме того, САПР-ы смотрят на паттерн описания элемента схемы, и когда распознают его, реализуют элемент так как задумывал разработчик. Поэтому при описании регистра всегда сперва описывается сигнал сброса (если он используется) и только затем в блоке `else` описывается вся остальная часть логики записи.
|
||||
|
||||
Итоговая схема регистра со сбросом и сигналом разрешения записи:
|
||||
|
||||

|
||||
|
||||
Помимо прочего есть еще одно важное правило, которое необходимо знать при описании регистра:
|
||||
|
||||
**Присваивание регистру может выполняться только в одном блоке `always`**
|
||||
|
||||
Даже если вдруг, САПР не выдаст сразу сообщение об ошибке, в конечном итоге, на этапе синтеза схемы она рано или поздно появится в виде сообщения связанного с **"multiple drivers"**.
|
||||
|
||||
В блоке присваивания регистру можно описывать и комбинационную логику, стоящую перед ним, например схему:
|
||||
|
||||

|
||||
|
||||
можно описать как
|
||||
|
||||
```SystemVerilog
|
||||
modulе rеg_ехаmрlе(
|
||||
inрut logic сlk,
|
||||
inрut logic dаtа,
|
||||
input logic A,
|
||||
input logic B,
|
||||
оutрut logic rеg_dаtа
|
||||
);
|
||||
|
||||
logic rеg_nаmе;
|
||||
|
||||
аlwауs_ff @(pоsеdgе clk) bеgin
|
||||
rеg_nаmе <= А & В;
|
||||
еnd
|
||||
|
||||
аssign reg_data = reg_name;
|
||||
|
||||
еndmоdulе
|
||||
```
|
||||
|
||||
Однако это всего лишь упрощение. Если вы умеете описывать регистр с подключением к нему всего одного провода на входе данных, вы все равно сможете описать эту схему:
|
||||
|
||||
```SystemVerilog
|
||||
modulе rеg_ехаmрlе(
|
||||
inрut logic сlk,
|
||||
inрut logic А,
|
||||
inрut logic В,
|
||||
оutрut logic rеg_dаtа
|
||||
);
|
||||
|
||||
logic rеg_nаmе; // Обратите внимание, что несмотря на то, что
|
||||
logic аb; // и reg_name и ab объявлены типом logic,
|
||||
// ab станет проводом, а reg_name — регистром
|
||||
// (из-за непрерывного присваивания на ab, и блока
|
||||
// always_ff для reg_name)
|
||||
аssign аb = А & В;
|
||||
|
||||
аlwауs_ff @(pоsеdgе clk) bеgin
|
||||
rеg_nаmе <= аb;
|
||||
еnd
|
||||
|
||||
аssign reg_data = reg_name;
|
||||
|
||||
еndmоdulе
|
||||
```
|
||||
|
||||
Поэтому так важно разобраться в базовом способе описания регистра.
|
||||
|
||||
Более того, с точки зрения синтезатора данное описание проще для синтеза, т.к. ему не разделять из одного `always` блока комбинационную и синхронные части.
|
||||
|
||||
Вообще говоря регистр в общем смысле этого слова представляет собой многоразрядную конструкцию (в рассмотренном ранее примере, однобитный регистр мог представлять из себя простой D-триггер).
|
||||
Создание многоразрядного регистра мало отличается от создания многоразрядного провода, а описание логики записи в многоразрядный регистр ничем не отличается от логики записи в одноразрядный регистр:
|
||||
|
||||
```SystemVerilog
|
||||
modulе rеg_ехаmрlе(
|
||||
inрut logic сlk,
|
||||
inрut logic [7:0] dаtа,
|
||||
оutрut logic [7:0] rеg_dаtа
|
||||
);
|
||||
|
||||
logic [7:0] rеg_nаmе;
|
||||
|
||||
аlwауs_ff @(pоsеdgе clk) bеgin
|
||||
rеg_nаmе <= dаtа;
|
||||
еnd
|
||||
|
||||
аssign reg_data = reg_name;
|
||||
|
||||
еndmоdulе
|
||||
```
|
||||
|
||||
## Итоги
|
||||
|
||||
1. [Регистр](https://ru.wikipedia.org/wiki/Регистр_(цифровая_техника)) — это базовая ячейка памяти, позволяющая хранить состояние, пока на схему подается питание.
|
||||
2. Для объявления регистра используется тип `logic`, при необходимости после типа указывается разрядность будущего регистра.
|
||||
3. Для описания логики записи в регистр используется блок `always_ff`, в круглых скобках которого указывается тактирующий сигнал и фронт, по которому будет вестись запись, а так же (в случае асинхронного сброса), сигнал сброса.
|
||||
4. Регистр может иметь различные управляющие сигналы: установки/сброса/разрешения на запись. Логика этих управляющих сигналов является частью логики записи в этот регистр и так же описывается в блоке `always_ff`.
|
||||
5. При описании логики записи в регистр, необходимо пользоваться оператором **неблокирующего присваивания** `<=`.
|
||||
6. Нельзя описывать логику записи в регистр более чем в одном блоке `always` (иными словами, операция присваивания для каждого регистра может находиться только в одном блоке always).
|
||||
|
||||
## Проверь себя
|
||||
|
||||
Как, по-вашему, описать на языке SystemVerilog схему, приведённую ниже?
|
||||
|
||||

|
90
Basic Verilog structures/Testbench.md
Normal file
90
Basic Verilog structures/Testbench.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Тестовое окружение (Testbench)
|
||||
|
||||
Для проверки правильного функционирования цифровых устройств необходимо разработать тестовое окружение. Тестовое окружение (testbench) – это блок, который окружает проверяемое устройство, формирует для него тестовые сигналы и автоматически проверяет, что сигналы на выходе проверяемого устройства соответствуют заложенным функциям. Тестовое окружение не является реальным аппаратным блоком (он только симулируется), поэтому в нем возможно использовать традиционные конструкции программирования. Например, то, что описывается в блоках `initial` в тестовом окружении, выполняется как программа в классическом программировании – строчка за строчкой.
|
||||
|
||||

|
||||
|
||||
Для того, чтобы создать тестовое окружение (testbench) необходимо создать новый файл симуляции в проекте. Для этого нажмите на `Add source`, после чего нужно выбрать `Add or create simulation sources` (на картинке ниже) → `Create File…` и так далее.
|
||||
|
||||

|
||||
|
||||
Так как для проверки разных модулей придется создавать различные тестовые окружения, то Vivado придется сообщать в явном виде какой из файлов симуляции вы сейчас хотите запустить. Для этого надо щелкнуть на нужном файле правой кнопкой и выбрать пункт `Set as Top` (продемонстрировано на картинке далее).
|
||||
|
||||

|
||||
|
||||
Название файла, для которого будет запускаться симуляция, отображается жирным шрифтом в окне `Sources` в папке `Simulation Sources`. После того, как выбран нужный файл симуляции, его можно запустить через панель `PROJECT MANAGER`, нажав на `Run Simulation`, а затем, в самом простом случае, можно запустить поведенческое моделирование `Run Behavioral Simulation`, оно позволяет увидеть поведение устройства в ответ на воздействия testbench’а. Временные задержки на прохождение сигналов через цифровые блоки при этом не учитываются. Так же можно посмотреть на реакцию устройства после синтеза `Post-Synthesis` или после имплементации `Post-Implementation`. Функциональная симуляция не учитывает временные задержки, временная – учитывает. Наиболее приближенная к реальности симуляция `Post-Implementation Timing Simulation`. Она учитывает временные задержки конкретных компонентов, в конкретной ПЛИС, с конкретными задержками распространения сигнала по каждому из проводов.
|
||||
|
||||

|
||||
|
||||
Пример тестового окружение для сумматора
|
||||
|
||||
Ниже приводится пример тестового окружения, в котором:
|
||||
|
||||
❶ – создаются провода и регистры для подключения к тестируемому модулю,
|
||||
|
||||
❷ – подключается проверяемый модуль,
|
||||
|
||||
❸ – описывается задача `task`, которую, подобно функции или подпрограмме, можно вызывать с различными параметрами,
|
||||
|
||||
❹ – в блоке `initial` последовательно два раза вызывается задача `add_op`, после чего симуляция останавливается `$stop`,
|
||||
|
||||
❺ – пример генерации тактового сигнала для подачи на вход проверяемого устройства.
|
||||
|
||||
|
||||
``` verilog
|
||||
`timescale 1ns / 1ps // Первое число указывает в каких величинах задержка
|
||||
// например, использование #10 это 10 наносекунд
|
||||
// если бы параметр был 10ns, то #10 означало бы 100ns
|
||||
// Второе число указывает точность симуляции
|
||||
// тут симуляция происходит для каждой пикосекунды
|
||||
|
||||
module my_testbench (); // объявляем модуль тестового окружения
|
||||
// внешних сигналов нет, поэтому скобки пустые
|
||||
reg [31:0] A, B; //❶ объявляем регистры для управления входами сумматора
|
||||
wire [31:0] S; // объявляем провод для подключения к выходу суммы
|
||||
reg Cin; // объявляем регистр для управления входом Cin
|
||||
wire Cout; // объявляем провод для подключения к выходу Cout
|
||||
|
||||
adder dut ( //❷ подключаем тестируемый модуль
|
||||
.a(A), // dut (device under test) – классическое название тестируемого модуля,
|
||||
.b(B), // при желании можно использовать любое другое имя
|
||||
.cin(Cin),
|
||||
.s(S),
|
||||
.cout(Cout));
|
||||
|
||||
initial begin // блок последовательного исполнения, начинает работу с момента времени 0
|
||||
add_op(6, 3); //❹ запустить задачу task add_op с параметрами 6 и 3
|
||||
add_op(2, 7); // когда закончиться предыдущая задача запустить новую
|
||||
$stop; // остановить симуляцию
|
||||
end
|
||||
|
||||
task add_op; //❸ объявляем задачу add_op
|
||||
input [31:0] a_op, b_op; // task получает на вход два параметра
|
||||
begin
|
||||
A = a_op; // подать на вход A сумматора новое значение a_op
|
||||
B = b_op; // подать на вход B сумматора новое значение b_op
|
||||
Cin = 0; // подать на вход Cin ноль
|
||||
#100; // выждать 100 ns чтобы сигнальчики разбежались и сумматор успел посчитать
|
||||
if (S == (a_op + b_op)) // если реальность (S) и ожидание (a_op+b_op) совпадают, то
|
||||
$display("GOOD %d + %d = %d", A, B, S); // вывести в терминал сообщение good
|
||||
else // в противном случае
|
||||
$display("BAD %d + %d = %d", A, B, S); // вывести в терминал другое сообщение
|
||||
end
|
||||
endtask
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
``` verilog
|
||||
reg clk; //❺ это вообще не относится к описанному выше testbench’у
|
||||
always #10 clk = ~clk; // каждые 10ns менять clk на противоположное значение
|
||||
```
|
||||
|
||||
Блоков initial в тестовом окружении может быть сколько угодно. Все эти блоки запускаются на исполнение параллельно. Блоков task так же может быть сколько угодно много и каждый из них может выполнять разные проверки. Так же поддерживаются множество стандартных языковых конструкции, например цикл for. Параметры для $display передаются так же, как у printf в языке C (на википедии есть вся информация).
|
||||
|
||||
Данный пример проверяет две суммы (6+3) и (2+7). Тест необходимо дополнить большим количеством проверок: несколько с очень большими числами, несколько с отрицательными, несколько с отрицательными и положительными, несколько со входным переносом Cin = 1, несколько операций с числами вызывающим переполнение (чтобы проверить формирование Cout).
|
||||
|
||||
По аналогии с этим примером необходимо реализовать модули проверки для:
|
||||
- АЛУ (по несколько проверок на каждую операцию)
|
||||
- Регистрового файла (последовательно записать какие-нибудь данные в разные адреса, а потом считать, убедившись, что все правильно, при этом считывание по адресу 0 должно всегда показывать 0)
|
||||
- Памяти инструкций (считать содержимое из нескольких ячеек, чтобы убедиться, что память проинициализирована)
|
262
Basic Verilog structures/Verilog syntax.md
Normal file
262
Basic Verilog structures/Verilog syntax.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# Конструкции языка Verilog
|
||||
|
||||
Verilog чувствителен к регистру. Это значит что "ЗаписЬ" и "запись" Verilog будет воспринимать как два разных слова. Все ключевые слова Verilog записываются строчными буквами.
|
||||
Каждое утверждение оканчивается точкой с запятой (`;`). Таким образом, символ переноса строки не прерывает выражение и может быть использован для повышения читаемости кода.
|
||||
В Verilog Си-подобные комментарии. Вы можете закомментировать одну строку или окончание строки с помощью двух косых черт (`//`). Кроме того, вы можете использовать многострочные комментарии — всё что находится между парами символов `/*` и `*/` считается комментарием.
|
||||
Любое имя, определяемое пользователем (имя переменной/модуля и т.п.) должно начинаться с латинской буквы, а не с числа. Определяемые пользователем имена не должны совпадать с ключевыми словами Verilog.
|
||||
|
||||
В тексте будет много определений синтаксиса языка Verilog. При появлении новых конструкций будет использована следующая нотация:
|
||||
|
||||
|| |
|
||||
|-------------|-------------|
|
||||
| **жирным** | будут выделены ключевые слова Verilog, такие слова необходимо использовать в точности как они написаны |
|
||||
|_курсивом_ | будут выделены имена, определяемые пользователем |
|
||||
|<> | в скобках будут указаны требуемые характеристики, такие как тип, направление input/output размерность и т.п.|
|
||||
|
||||
По прочтению этой главы вы узнаете:
|
||||
|
||||
* Типы данных, представленных в языке Verilog;
|
||||
* Как описывать базовую конструкцию модуля Verilog.
|
||||
|
||||
## Типы данных
|
||||
|
||||
В Verilog, каждому сигналу, константе, переменной или функции должен быть присвоен _тип данных_. Какие-то типы являются синтезируемыми, а какие-то используются только для моделирования абстрактного поведения.
|
||||
|
||||
Verilog поддерживает четыре базовых состояния, которые может принять сигнал: 0, 1, X и Z. Ниже представлено описание каждого из этих состояний:
|
||||
|
||||
|Значение|Описание|
|
||||
|--------|--------|
|
||||
|0| Логический ноль / ЛОЖЬ|
|
||||
|1| Логическая единица / ИСТИНА|
|
||||
|X| Неопределенное (неизвестное) или непроинициализированное состояние|
|
||||
|Z| [Высокоимпедансное состояние](https://ru.wikipedia.org/wiki/%D0%92%D1%8B%D1%81%D0%BE%D0%BA%D0%BE%D0%B8%D0%BC%D0%BF%D0%B5%D0%B4%D0%B0%D0%BD%D1%81%D0%BD%D0%BE%D0%B5_%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B5)|
|
||||
|
||||
<!-- В Verilog к этим значениям привязывается так называемая _сила сигнала_. Силы используются для определения значения сигнала, управляемого несколькими источниками. Сигнал принимает значение источника с большей силой сигнала. Если у источников силы сигнала равны, значение сигнала неопределено в случае, если источники подают разные сигналы, и равно сигналу источников, если они подают одно и тоже значение.
|
||||
В университетских курсах по Verilog вы не будете использовать силы сигнала, о них написано выше лишь для полноты картины. -->
|
||||
|
||||
### Класс данных "Цепь"
|
||||
|
||||
Как говорилось выше, каждому сигналу в Verilog должен быть назначен тип данных. Класс данных под названием "цепь" моделирует соединение (электрическую цепь) между компонентами и может принимать значения 0, 1, X, Z. Сигнал цепи постоянно управляется источником сигнала и меняет своё значение каждый раз, когда меняется значение на источнике (цепь не может хранить значение).
|
||||
|
||||

|
||||
|
||||
Рассмотрим рисунок выше. Красным цветом выделены сигналы класса "цепь". Сигнал _net\_11_ используется для соединения выхода логического элемента И со входом данных D-триггера _data\_0_. Выход логического элемента И является источником сигнала для цепи _net\_11_. Выход _data\_0_ является источником сигнала _net\_2_.
|
||||
|
||||
В Verilog множество типов класса цепь, но самым распространенным является тип **wire**(провод). Вы можете представить подобный сигнал, как провод, подключенный к некому источнику. Значение сигнала в любой части провода одинаково и совпадает со значением подключенного к нему источника сигнала. Если провод не подключен ни к одному источнику, он находится в высокоимпедансном состоянии (принимает значение Z).
|
||||
|
||||
Есть еще один тип, полностью идентичный типу **wire**, который называется **tri**. Эти типы взаимозаменяемы и разделены только для повышения читаемости кода. Тип **tri** используется, когда сигналом управляют несколько источников.
|
||||
|
||||

|
||||
|
||||
Пример сигнала, управляемого несколькими источниками.
|
||||
|
||||
### Класс данных "Переменная"
|
||||
|
||||
Verilog также поддерживает типы данных, которые моделируют ячейки памяти. Сигналы класса "переменная" могут принимать значения 0, 1, X, Z<!-- , но с ними не связана сила сигнала -->. Типы класса "переменная" могут хранить присвоенное им значение до очередного присваивания. Ниже представлено описание переменных типов:
|
||||
|
||||
|Тип |Описание|
|
||||
|------------|--------|
|
||||
|**reg** | Переменная, моделирующая базовую ячейку памяти. Может принимать значения 0, 1, X, Z|
|
||||
|**integer** | 32-битная переменная, представленная в [дополнительном коде](<https://ru.wikipedia.org/wiki/%D0%94%D0%BE%D0%BF%D0%BE%D0%BB%D0%BD%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%BA%D0%BE%D0%B4>), представляющая диапазон чисел [‑2 147 483 648 : 2 147 483 647]|
|
||||
|**real** | 64-битная переменная, представляющая числа с плавающей точкой в диапазоне [2.2×10<sup>‑308</sup> : 2.2×10<sup>308</sup>]|
|
||||
|**time** | Беззнаковая 64-битная переменная, принимающая значения в диапазоне [0 : 9.2×10<sup>18</sup>] |
|
||||
|**realtime**| Синоним типа **real**. Используется для повышения читаемости|
|
||||
|
||||
### Векторы
|
||||
|
||||
В Verilog вектором называют одномерный массив элементов. Все типы класса "цепь", а также тип **reg** могут использоваться для формирования вектора. Синтаксис объявления вектора представлен ниже:
|
||||
|
||||
<pre>
|
||||
<тип> [<старший индекс>:<младший индекс>] <i>имя_вектора</i>
|
||||
</pre>
|
||||
|
||||
Несмотря на то, что может использоваться любой диапазон индексов (даже отрицательный), на практике стараются начинать младший индекс с нуля.
|
||||
Пример:
|
||||
|
||||
<pre>
|
||||
<b>wire</b> [7:0] <i>sum</i>; // Объявляется 8-битный вектор с именем sum типа wire.
|
||||
// Старший индекс равен 7, младший — 0.
|
||||
// Векторы типа wire обычно называют "шиной"(bus).
|
||||
|
||||
<b>reg</b> [15:0] <i>Q</i>; // В данной строке объявлен 16-битный вектор типа reg.
|
||||
</pre>
|
||||
|
||||
Используя индекс, можно обратиться к отдельным битам вектора. С помощью диапазона индексов, можно получить доступ к диапазону соответствующих битов.
|
||||
|
||||
|фрагмент кода|описание|
|
||||
|-------------|--------|
|
||||
|sum[0]; | Обращение к младшему биту вектора sum, объявленного выше|
|
||||
|Q[15:8]; | Обращение к старшим восьми битам 16-битного вектора Q, объявленного выше|
|
||||
|
||||
</pre>
|
||||
|
||||
Обратите внимание, что нельзя объявить вектор типа **integer**, **time**, **real**, **realtime**. Однако, типы **integer** и **time** являются векторами сами по себе. Иными словами, вы не можете объявить следующий вектор:
|
||||
|
||||
<pre>
|
||||
<b>integer</b> [7:0] dunno_what_size_of_this_object;
|
||||
</pre>
|
||||
|
||||
но можете обращаться к отдельным битам объявленного объекта как если бы он был вектором:
|
||||
|
||||
|фрагмент кода|описание|
|
||||
|-------------|--------|
|
||||
|<b>integer</b> int_object;<br>int_object[31];|<br>Обращение к старшему биту вектора|
|
||||
|
||||
Таким образом, вы можете считать **integer** синонимом `reg signed [31:0]` (о значении **signed** будет чуть позже), а **time** синонимом `reg [63:0]`
|
||||
|
||||
Поскольку **real** и **realtime** представляют числа с плавающей точкой, они не считаются векторами, и вы не можете обратиться к их отдельным битам.
|
||||
|
||||
### Множества (Arrays)
|
||||
|
||||
Множество (array) — это многомерный массив элементов. Множество можно назвать вектором векторов. Векторы внутри множества (элементы множества) обладают одинаковой размерностью. Для объявления множества, необходимо указать тип и размерность элементов множества, а также имя и размерность самого множества. При указании размерности множества, обычно сперва указывают начальный индекс, а после конечный.
|
||||
Синтаксис объявления множества:
|
||||
|
||||
<pre>
|
||||
<тип элемента> [<старший индекс элемента> : <младший индекс элемента>] <i>
|
||||
имя_множества</i> [начальный индекс множества : конечный индекс множества]
|
||||
</pre>
|
||||
|
||||
Пример:
|
||||
|
||||
<pre>
|
||||
<b>reg</b> [7:0] <i>mem</i> [0:4095]; // Объявляется множество 8-битных векторов типа
|
||||
// reg, размером 4096.
|
||||
// Множество, элементами которого являются вектора
|
||||
// типа reg (например то, что объявлено здесь)
|
||||
// называют памятью.
|
||||
// Элементы памяти называют "словами".
|
||||
|
||||
<b>integer</b> <i>A</i> [1:100]; // Объявляется множество со 100 элементами типа
|
||||
// integer.
|
||||
</pre>
|
||||
|
||||
К элементу множества можно обратиться, используя его индекс. Кроме того, если элементом множества является вектор, можно так же обратиться к отдельному биту этого вектора, добавив и его индекс.
|
||||
Примеры:
|
||||
|
||||
|фрагмент кода|описание|
|
||||
|-------------|--------|
|
||||
|mem[2]; | Обращение к третьему элементу множества mem (третьему, поскольку нумерация начинается с нуля).<br> Подобное обращение вернет 8-битный вектор типа reg.|
|
||||
|mem[2][7]; | Обращение к старшему биту третьего элемента множества mem.|
|
||||
|A[2]; | Обращение ко второму элементу множества A (в этом случае, нумерация начинается с единицы, поскольку так было указано при объявлении множества A)|
|
||||
|
||||
|
||||
### Целочисленные константы
|
||||
|
||||
В Verilog есть два способа выразить целое число. Первый способ — это обычная запись десятичного числа, формируемая последовательностью цифр от 0 до 9, с опциональным указанием знака перед ней. Второй способ — это запись целого числа с указанием основания системы исчисления. Данная форма записи состоит из трех частей:
|
||||
|
||||
1. опциональной константы размера
|
||||
2. апострофа (`'`), за которым указывается формат основания системы исчисления
|
||||
3. набора цифр, представляющих значение числа
|
||||
|
||||
<pre>
|
||||
<размер>'[s]<основание><значение>
|
||||
</pre>
|
||||
|
||||
Между каждой из частей может стоять пробел, но не внутри любой из них.
|
||||
|
||||
Первая часть (константа размера) указывает точное число бит целочисленной константы. Для указания размера используется ненулевое положительное десятичное число. Например, для указания двух шестнадцатиричных цифр потребуется 8 бит, поскольку каждая шестнадцатиричная цифра занимает 4 бита.
|
||||
Вторая часть, формат основания системы исчисления состоит из апострофа и нечувствительной к регистру букве, обозначающей основание системы исчисления, перед которой допустимо указать символ s(или S) для указания того, что число знаковое.
|
||||
|
||||
|Синтаксис|Описание |
|
||||
|---------|-------------------------------|
|
||||
|**'b** | Беззнаковое двоичное |
|
||||
|**'o** | Беззнаковое восьмеричное |
|
||||
|**'d** | Беззнаковое десятичное |
|
||||
|**'h** | Беззнаковое шестнадцатиричное |
|
||||
|**'sb** | Знаковое двоичное |
|
||||
|**'so** | Знаковое восьмеричное |
|
||||
|**'sd** | Знаковое десятичное |
|
||||
|**'sh** | Знаковое шестнадцатиричное |
|
||||
|
||||
Третья часть содержит беззнаковое число, записанное цифрами, разрешенными для данной системы исчисления. Шестнадцатиричные цифры a-f не чувствительны к регистру. Кроме того, в качестве цифры могут использоваться символы `x`, `z` и `?`(аналогичен `z`) для обозначения неопределенного/высокоимпендансного состояния соответствующих им бит.
|
||||
Если размер беззнакового числа в третьей части меньше размера, указанного в первой части, это число дополняется слева нулями за исключением случаев, когда в старшем бите числа записано `x` или `z`, в этом случае, число дополняется слева `x` или `z` соответственно.
|
||||
Если размер беззнакового числа в третьей части больше размера, указанного в первой части, это число обрезается слева.
|
||||
|
||||
Константы, для которых не указан размер будут иметь размер минимум 32 бита. Отрицательные числа представляются в дополнительном коде.
|
||||
|
||||
При записи числа в любом его месте кроме начала может использоваться символ нижнего подчеркивания (`_`). Этот символ будет игнорироваться и используется как разделитель для повышения читаемости.
|
||||
|
||||
Пример 1 — Беззнаковые константы:
|
||||
|
||||
|фрагмент кода|описание|
|
||||
|-------------|--------|
|
||||
|659 | знаковое десятичное число|
|
||||
|'h 837FF | беззнаковое шестнадцатиричное число|
|
||||
|'o7460 | беззнаковое восьмеричное число|
|
||||
|4af | запрещенная запись (шестнадцатиричная запись возможна лишь с 'h)|
|
||||
<br>
|
||||
Пример 2 — Знаковые константы:
|
||||
|
||||
|фрагмент кода|описание|
|
||||
|-------------|--------|
|
||||
|4'b1001 | 4-битное двоичное беззнаковое число|
|
||||
|5 'D 3 | 5-битное десятичное беззнаковое число|
|
||||
|3'b01x | 3-битное беззнаковое двоичное число, у которого неизвестен младший бит|
|
||||
|12'hx | 12-битное неизвестное число|
|
||||
|16'hz | 16-битное высокоимпеднансное число|
|
||||
<br>
|
||||
Пример 3 — Использование знака с константами:
|
||||
|
||||
|фрагмент кода|описание|
|
||||
|-------------|--------|
|
||||
|8 'd -6 | Запрещено, константа в третьей части записывается в виде беззнакового числа|
|
||||
|-8 'd 6 | Здесь записано -6 в дополнительном коде (1111_1010), хранящееся в 8-ми битах.|
|
||||
|4 'shf | Здесь записано 4-битное число с двоичной записью: '1111', которое интерпретируется как '-1' в десятичной записи. <br> Это эквивалент записи -4'h 1|
|
||||
|-4 'sd15 | Это эквивалент записи -(-4'd 1), или двоичного '0001'|
|
||||
|16'sd? | То же, что и 16'hz|
|
||||
<br>
|
||||
Пример 4 — Выравнивание размера:
|
||||
|
||||
|фрагмент кода|описание|
|
||||
|-------------|--------|
|
||||
|8 'h F0AA |Константа превышает указаный размер и будет обрезана до AA|
|
||||
|8'sb1001 |Константа меньше указанного размера и дополняется слева нулями до "00001001"|
|
||||
|13'h1z1 |Константа меньше указанного размера и дополняется слева Z до "zzzzzzzzz0001"|
|
||||
<br>
|
||||
Пример 5 — Использование нижнего подчеркивания:
|
||||
|
||||
|фрагмент кода |
|
||||
|-----------------------|
|
||||
|27_195_000 |
|
||||
|16'b0011_0101_0001_1111|
|
||||
|32 'h 12ab_f001 |
|
||||
|
||||
### Присваивания между различными типами
|
||||
|
||||
Verilog является так называемым языком со слабой типизацией, что означает, что он допускает присваивания между различными типами. Причиной тому является то, что Verilog относится к любому типу как к группе битов. Присваивая значения одного типа другому, Verilog автоматически обрезает или дополняет необходимым для присваивания количеством старших бит. Ниже приведен пример того, как Verilog справляется с присваиваниями между различными типами (предположим, что мы создали переменную `ABC_TB`, которая объявлена как `reg [2:0]`)
|
||||
|
||||
<pre>
|
||||
ABC_TB = 2'b00; // ABC_TB будет присвоено 3'b000. Автоматически добавлен
|
||||
// старший бит
|
||||
ABC_TB = 5; // ABC_TB будет присвоено 3'b101. Целочисленное число
|
||||
// размером минимум 32 бита обрезано до 3-х.
|
||||
ABC_TB = 8; // ABC_TB будет присвоено 3'b000. Целочисленное число
|
||||
// со значащими битами "1000" будет брезано до 3-х.
|
||||
</pre>
|
||||
|
||||
|
||||
### Проверь себя
|
||||
|
||||
Чаще всего используемыми типами в Verilog являются wire и reg. В чем их фундаментальное различие?
|
||||
|
||||
1. У них нет различий, поскольку они оба могут принимать значения 0, 1, X, Z.
|
||||
2. wire является типом класса "цепь" — это значит, что значением этого сигнала постоянно управляет источник. reg является типом класса "переменная" — это значит, что он будет хранить своё значение после присваивания.
|
||||
3. wire может принимать лишь значения 0 и 1, в то время как reg может принимать значения 0, 1, X, Z.
|
||||
4. Они не могут подаваться на входы друг друга.
|
||||
|
||||
## Создание модуля в Verilog
|
||||
|
||||
Все устройства, описываемые языком Verilog воплощаются в виде модуля (**module**). Модули могут включать в себя создание других модулей с целью создания иерархических проектов. Начало и конец описания подобного устройства обозначаются ключевыми словами **module** и **endmodule** соответственно. На практике считается хорошим тоном размещать каждый модуль в отдельном файле с расширением `.v`.
|
||||
|
||||
Синтаксис объявления модуля:
|
||||
<pre>
|
||||
<b>module</b> <i>module_name</i> (<port_list> and <port_definitions>);
|
||||
// module_items
|
||||
<b>endmodule</b>
|
||||
</pre>
|
||||
|
||||

|
||||
|
||||
### Объявления портов
|
||||
|
||||
Первое, что указывается после имени модуля — это описание его входов и выходов, или так называемых портов. Каждому порту должно быть дано имя, направление и тип.
|
Reference in New Issue
Block a user