Оглавление
§ Провода и регистры (wire/reg)
Сразу скажу, что есть отличные учебники по верилогу, где все рассказано лучше в 100 раз, чем тут. Но мы пойдем другим путем.
Логические схемы обычно имеют 3 главных составляющих:
- Комбинаторная логика для вычислений
- Регистры для хранения данных
- Провода для соединения комбинаторной логики и регистров
Комбинаторная логика это сочетание элементов И, ИЛИ, НЕ, ИЛИ-НЕ и так далее. Таблиц истинности для двухвходовых элементов всего 16. Регистры нужны для того, чтобы хранить промежуточные вычисления, а провода, ясное дело, чтобы соединять входы, выходы и соединения между собой.
Для того, чтобы объявить существование провода, надо написать так:
Здесь a – это имя провода, может быть любым, но надо понимать, что имя переменной состоит из латинских букв, цифр и знаков подчеркивания, не должно начинаться с цифры и регистр символов имеет значение.
Вот таким макаром объявляется пучок проводов (или еще это называют шиной):
Перед именем переменной находится [3:0], которое означает, что самый старший провод в пучке будет третьим (3), а самый младший нулевым (0). Записывать надо именно так, от старшего к младшему. Не знаю, зачем так сделали, но так сделали уж.
Любому проводу можно назначить какое-то значение. Для одного провода это значение будет однобитовым. Назначение выглядит так:
Здесь проводу a назначается бит, равный 0. Обращу внимание на запись 1'b0. Сначала пишется количество бит у числа, то есть, сколько разрядов у этого числа. Потом пишется одинарная кавычка и далее то, в какой системе счисления у нас представлено число (b-двоичное, d-десятичное, h-шестнадцатеричное).
1'b1 = 1
2'b10 = 2
4'hA = 10
5'd12 = 12
4'b11_11 = 15
16'hFF_FF = 65535
Символ подчеркивания никак не играет роли в формировании конечного значения, он нужен для наглядности.
Есть интересный нюанс. На самом деле, это число 32'd6, обрезано до 3-х младших бит. То есть, если число задать в десятичном виде, то тогда оно будет именно 32-х битным.
Чтобы можно было обратиться к любому проводу в пучке проводов, то пишется это так b[2]:
wire a;
wire [3:0] b;
assign a = b[2];
Также можно не только обращаться к одному лишь проводу, а делать целые срезы, например b[2:1], который извлечет значения из 2-го и 1-го бита и получит новую 2-х битную шину:
wire [3:0] b;
wire [1:0] a = b[2:1];
В этом примере я вообще обошелся без assign, потому что в верилоге можно сразу назначить проводу какое-то определенное значение. Как видим, шина a двухбитная, и значение этой шины равно срезу [2:1] из шины b.
А вообще assign полезен для того, чтобы назначать выходы в модулях, а так можно пользоваться таким способом назначения.
В качестве номера провода можно использовать и значение с другого провода или регистра:
wire [1:0] a = 2'b01;
wire [3:0] b = 4'hB;
wire c = b[a];
Как можно догадаться, такой способ выборки будет представлять из себя, физически, однобитный мультиплексор.
Еще несколько проводов и шин можно объединять в пучки с помощью фигурных скобок:
wire [1:0] a;
wire [2:0] b;
wire [4:0] c = {b[2], a[0], b[1:0], a[1]};
Разберемся подробнее. Есть a, b и c, которое имеет 5-битную разрядность (5 проводов). Какие значения будут назначены этим проводам?
- Провод 0 (младший бит): a[1] – самый правый будет самый младший
- Провод 1: b[0] – справа налево идет сначала бит 0 у
b
- Провод 2: b[1] – потом идет бит 1
- Провод 3: a[0]
- Провод 4 (старший бит): b[2]
Тут главное не запутаться и читать справа налево. Справа будет старший бит, слева – младший.
Еще можно объединить пучки проводов (шины) в массив:
wire [1:0] a[16];
wire [1:0] b[0:16];
wire [1:0] c[2:16];
Как обращаться к массиву? Как и обычно это делается, например a[2][1:0] выбирает биты 1 и 0 из массива по индексу 2 (второй элемент массива). Или можно просто выбрать элемент a[2]. Только стоит заметить, что в случае массива выберется именно элемент 2, а не провод 2 в шине.
Массивы a и b идентичны, просто в другой, более расширенной записи находятся, а вот массив c – нет, у него недоступны элементы 0 и 1, то есть, массив начинается с 2.
Что такое регистры? Это ячейки памяти. Регистры фактически идентичны проводам, но есть некоторые отличия. В регистры записывается какая-то определенная информация и она потом не меняется, а в проводах изменение идет сразу же. В верилоге регистры объявляются так:
reg a;
reg [1:0] b;
reg [3:1] c;
reg [2:0] d[4];
reg [2:0] e = 3'h7;
Также регистру нельзя присвоить через assign или как-то еще значение, как проводу. Присваивание регистру нового значения происходит другим методом, об этом позже.
§ Модули
В верилоге имеется возможность группировать схемы в модули (например в микросхемы). Это делается с помощью объявления модуля и его входов-выходов:
module MyModuleName(<порты>);
<описание порты>
....
endmodule
MyModuleName – это имя модуля, которое можно потом использовать далее.
Для примера, объявлю простой модуль:
module abmov(a, c);
input wire [1:0] a;
output wire [1:0] c;
assign c = a;
endmodule
Этот модуль имеет 2 порта. Один порт объявлен как входной, а второй как выходной. Присвоить значение выходному порту можно только через assign, если этот порт является проводом/шиной (wire). Модуль пока что ничем не занимается, кроме как копирует из порта a в порт c значение.
Модуль еще можно задать иначе:
module nand
(
input wire a,
input wire b,
output wire c
);
assign c = ~(a & b);
endmodule
Это эквивалентные способы записи, и мне второй нравится больше и я им всегда пользуюсь. Этот модуль работает как элемент NAND, поскольку выходу c присваивается значение ~(a & b).
Модули можно включать друг в друга, как матрешки. Но давайте разберемся, как вообще использовать готовые модули? Разберу простой модуль. Допустим, объявляем модуль где-то в файле nand.v
module nand(input a, intput b, output c);
assign c = ~(a & b);
endmodule
Обращу внимание, что вместо input/output wire можно просто писать input/output, компилятор допишет wire сам. Также, чтобы модуль заработал, надо для icarus verilog добавить в компилятор этот самый модуль. Я обычно делаю в makefile, добавляю имя модуля в конце строки компиляции:
iverilog -g2005-sv -DICARUS=1 -o tb.qqq tb.v nand.v
Теперь же можно создать небольшой файл тестбенча (беру как шаблон):
`timescale 10ns / 1ns
module tb;
reg clock;
always #0.5 clock = ~clock;
initial begin clock = 0; $dumpfile("tb.vcd"); $dumpvars(0, tb); #2000 $finish; end
....
endmodule
Чтобы объявить здесь новый подмодуль, его можно записать в таком виде:
wire d;
nand ModuleName1
(
.a (1'b0),
.b (1'b1),
.c (d)
);
Получается, объявляется некоторый модуль с неким именем нужного типа и проставляются ему входы-выходы. Там где имя с точкой – это имя порта внутри модуля, а там где значение в скобках – это либо входящий, либо исходящий провод уже непосредственно из модуля. Это можно сравнить с ножками на микросхеме (там где в скобках), а там где с точками – это контактами внутри микросхемы.
§ Схема сумматора
Разработаем и проверим схему сумматора. Первым делом, рассмотрим схему полного сумматора.
Итак, входами будут a,b,cin, а выходами c,cout. А теперь переведу схему в код на верилоге.
module fadd(
input a, input b, input cin,
output c, output cout
);
wire w1 = a ^ b;
wire w2 = a & b;
wire w3 = w1 & cin;
assign c = w1 ^ cin;
assign cout = w2 | w3;
endmodule
Здесь провода w1, w2, w3 – внутренние, но провода c, cout – это выходы, присваивать значения выходному проводу можно только через assign.
В целом, если ограничиться однобитным сумматором, то этого достаточно. Но что если сделать 4-х битный?
Объявляем 5 основных проводов и 3 вспомогательных:
wire cin = 1'b1;
wire [3:0] a = 4'd3;
wire [3:0] b = 4'd6;
wire [3:0] c;
wire cout;
wire [2:0] w;
И вот теперь самое интересное. Прицепляем провода к портам первого полного сумматора:
fadd u0(
.cin (cin),
.a (a[0]),
.b (b[0]),
.c (c[0]),
.cout (w[0])
);
Второй сумматор будет прицеплен уже так:
fadd u1(
.cin (w[0]),
.a (a[1]),
.b (b[1]),
.c (c[1]),
.cout (w[1])
);
Аналогично следующие:
fadd u2( .cin(w[1]), .a(a[2]), .b(b[2]), .c(c[2]), .cout(w[2]) );
fadd u3( .cin(w[2]), .a(a[3]), .b(b[3]), .c(c[3]), .cout(cout) );
Приведу полный текст модуля tb.v:
`timescale 10ns / 1ns
module tb;
reg clock;
always #0.5 clock = ~clock;
initial begin clock = 0; $dumpfile("tb.vcd"); $dumpvars(0, tb); #2000 $finish; end
wire cin = 1'b1;
wire [3:0] a = 4'd3;
wire [3:0] b = 4'd6;
wire [3:0] c;
wire cout;
wire [2:0] w;
fadd u0( .cin(cin), .a(a[0]), .b(b[0]), .c(c[0]), .cout(w[0]) );
fadd u1( .cin(w[0]), .a(a[1]), .b(b[1]), .c(c[1]), .cout(w[1]) );
fadd u2( .cin(w[1]), .a(a[2]), .b(b[2]), .c(c[2]), .cout(w[2]) );
fadd u3( .cin(w[2]), .a(a[3]), .b(b[3]), .c(c[3]), .cout(cout) );
endmodule
Теперь проверим, как работает в симуляторе:
§ Встроенные сумматоры
В довершении темы я могу сказать кое-что, что необязательно каждый раз писать такую огромную схему сумматора. В верилоге можно использовать обычное сложение и вычитание, а также умножение и целочисленное деление, и после синтеза будет адаптировано конкретно под тот чип, который используется. В некоторых чипах есть DSP-блоки, которые содержат в себя быстрые блоки умножения, и потому можно не городить огород и просто умножать. В почти всех ПЛИС-ах также есть специальные возможности для сумматоров и вычитателей, потому лучше писать + или -, а не делать череду из сложных модулей.
В данном случае, можно было бы просто сделать вот так:
wire [4:0] c = a + b;
wire cout = c[4];
Последний cout даже лишний, потому что результат переноса будет находится в регистре c[4].