§ Провода и регистры (wire/reg)

Сразу скажу, что есть отличный учебник по верилогу. Но мы пойдем другим путем.
Логические схемы обычно имеют 3 главных составляющих:
  • Комбинаторная логика для вычислений
  • Регистры для хранения данных
  • Провода для соединения комбинаторной логики и регистров
Комбинаторная логика это сочетание элементов И, ИЛИ, НЕ, ИЛИ-НЕ и так далее. Таблиц истинности для двухвходовых элементов всего 16. Регистры нужны для того, чтобы хранить промежуточные вычисления, а провода, ясное дело, чтобы соединять входы, выходы и соединения между собой.
Для того, чтобы объявить существование провода, надо написать так:
1wire a;
Здесь a - это имя провода, может быть любым, но надо понимать, что имя переменной состоит из латинских букв, цифр и знаков подчеркивания, не должно начинаться с цифры и регистр символов имеет значение.
Вот таким макаром объявляется пучок проводов (или еще это называют шиной):
1wire [3:0] b;
Перед именем переменной находится [3:0], которое означает, что самый старший провод в пучке будет третьим (3), а самый младший нулевым (0). Записывать надо именно так, от старшего к младшему. Не знаю, зачем так сделали, но так сделали уж.
Любому проводу можно назначить какое-то значение. Для одного провода это значение будет однобитовым. Назначение выглядит так:
1assign a = 1'b0;
Здесь проводу 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
Символ подчеркивания никак не играет роли в формировании конечного значения, он нужен для наглядности.
1wire [2:0] d = 6;
Есть интересный нюанс. На самом деле, это число 32'd6, обрезано до 3-х младших бит. То есть, если число задать в десятичном виде, то тогда оно будет именно 32-х битным.
Чтобы можно было обратиться к любому проводу в пучке проводов, то пишется это так b[2]:
1wire       a;     // Объявляем провод a
2wire [3:0] b;     // Объявляем 4-х битный провод b
3assign a = b[2];  // Провод a будет иметь то же самое значение, что и бит 2 в шине b
Также можно не только обращаться к одному лишь проводу, а делать целые срезы, например b[2:1], который извлечет значения из 2-го и 1-го бита и получит новую 2-х битную шину:
1wire [3:0] b;
2wire [1:0] a = b[2:1];
В этом примере я вообще обошелся без assign, потому что в верилоге можно сразу назначить проводу какое-то определенное значение. Как видим, шина a двухбитная, и значение этой шины равно срезу [2:1] из шины b.
А вообще assign полезен для того, чтобы назначать выходы в модулях, а так можно пользоваться таким способом назначения.
В качестве номера провода можно использовать и значение с другого провода или регистра:
1wire [1:0] a = 2'b01; // a=1
2wire [3:0] b = 4'hB;  // b=1101 в двоичной системе
3wire       c = b[a];  // c=b[1]=0, потому что бит 1 в b равен 0
Как можно догадаться, такой способ выборки будет представлять из себя, физически, однобитный мультиплексор.
Еще несколько проводов и шин можно объединять в пучки с помощью фигурных скобок:
1wire [1:0] a;
2wire [2:0] b;
3wire [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]
Тут главное не запутаться и читать справа налево. Справа будет старший бит, слева - младший.
Еще можно объединить пучки проводов (шины) в массив:
1wire [1:0] a[16];   // Можно так
2wire [1:0] b[0:16]; // Можно и так
3wire [1:0] c[2:16]; // И даже так
Как обращаться к массиву? Как и обычно это делается, например a[2][1:0] выбирает биты 1 и 0 из массива по индексу 2 (второй элемент массива). Или можно просто выбрать элемент a[2]. Только стоит заметить, что в случае массива выберется именно элемент 2, а не провод 2 в шине.
Массивы a и b идентичны, просто в другой, более расширенной записи находятся, а вот массив c - нет, у него недоступны элементы 0 и 1, то есть, массив начинается с 2.
Что такое регистры? Это ячейки памяти. Регистры фактически идентичны проводам, но есть некоторые отличия. В регистры записывается какая-то определенная информация и она потом не меняется, а в проводах изменение идет сразу же. В верилоге регистры объявляются так:
1reg       a;        // Однобитный регистр, хранит либо 0, либо 1
2reg [1:0] b;        // Двухбитный, хранит 2^2=4 значения от 00 до 11
3reg [3:1] c;        // Трехбитный, но начинается с разряда 1 и заканчивается 3
4reg [2:0] d[4];     // Массив из 4-х трехбитных регистров (память)
5reg [2:0] e = 3'h7; // Регистр, который заранее проиницилизирован значением 7
Также регистру нельзя присвоить через assign или как-то еще значение, как проводу. Присваивание регистру нового значения происходит другим методом, об этом позже.

§ Модули

В верилоге имеется возможность группировать схемы в модули (например в микросхемы). Это делается с помощью объявления модуля и его входов-выходов:
1module MyModuleName(<порты>);
2<описание порты>
3....
4endmodule
MyModuleName - это имя модуля, которое можно потом использовать далее.
Для примера, объявлю простой модуль:
1module abmov(a, c);
2
3  input  wire [1:0] a;
4  output wire [1:0] c;
5
6  assign c = a;
7
8endmodule
Этот модуль имеет 2 порта. Один порт объявлен как входной, а второй как выходной. Присвоить значение выходному порту можно только через assign, если этот порт является проводом/шиной (wire). Модуль пока что ничем не занимается, кроме как копирует из порта a в порт c значение.
Модуль еще можно задать иначе:
1module nand
2(
3  input  wire a,
4  input  wire b,
5  output wire c
6);
7
8assign c = ~(a & b);
9
10endmodule
Это эквивалентные способы записи, и мне второй нравится больше и я им всегда пользуюсь. Этот модуль работает как элемент NAND, поскольку выходу c присваивается значение ~(a & b).
Модули можно включать друг в друга, как матрешки. Но давайте разберемся, как вообще использовать готовые модули? Разберу простой модуль. Допустим, объявляем модуль где-то в файле nand.v
1module nand(input a, intput b, output c);
2assign c = ~(a & b);
3endmodule
Обращу внимание, что вместо input/output wire можно просто писать input/output, компилятор допишет wire сам. Также, чтобы модуль заработал, надо для icarus verilog добавить в компилятор этот самый модуль. Я обычно делаю в makefile, добавляю имя модуля в конце строки компиляции:
1iverilog -g2005-sv -DICARUS=1 -o tb.qqq tb.v nand.v
Теперь же можно создать небольшой файл тестбенча (беру как шаблон):
1`timescale 10ns / 1ns
2module tb;
3
4  reg           clock;
5  always  #0.5  clock = ~clock;
6  initial begin clock = 0; $dumpfile("tb.vcd"); $dumpvars(0, tb); #2000 $finish; end
7
8  ....
9
10endmodule
Чтобы объявить здесь новый подмодуль, его можно записать в таком виде:
1wire d;
2nand ModuleName1  // Объявляем модуль (имя модуля ModuleName1) типа nand
3(
4  .a  (1'b0),     // На вход `a` модуля подается 0 (1 бит)
5  .b  (1'b1),     // На вход `b` модуля подается 1 (1 бит)
6  .c  (d)         // С выхода `c` принимается итоговое значение и назначается проводу `d`
7);
Получается, объявляется некоторый модуль с неким именем нужного типа и проставляются ему входы-выходы. Там где имя с точкой - это имя порта внутри модуля, а там где значение в скобках - это либо входящий, либо исходящий провод уже непосредственно из модуля. Это можно сравнить с ножками на микросхеме (там где в скобках), а там где с точками - это контактами внутри микросхемы.

§ Схема сумматора

Разработаем и проверим схему сумматора. Первым делом, рассмотрим схему полного сумматора.

Итак, входами будут a,b,cin, а выходами c,cout. А теперь переведу схему в код на верилоге.
1module fadd(
2  input  a, input  b, input cin,
3  output c, output cout
4);
5
6wire w1 = a ^ b;    // XOR над элементами a, b -> провод w1
7wire w2 = a & b;    // AND над a, b -> на провод w2
8wire w3 = w1 & cin; // AND над w1 и cin [ (a ^ b) & cin ] -> на провод w3
9
10assign c    = w1 ^ cin; // XOR над w1, cin [ a ^ b ^ cin ] -> на выход
11assign cout = w2 | w3;  // OR над w2 и w3 [ (a & b) | ((a ^ b) & cin) ] -> на выход
12
13endmodule
Здесь провода w1, w2, w3 - внутренние, но провода c, cout - это выходы, присваивать значения выходному проводу можно только через assign.
В целом, если ограничиться однобитным сумматором, то этого достаточно. Но что если сделать 4-х битный?

Объявляем 5 основных проводов и 3 вспомогательных:
1wire       cin = 1'b1;  // Перенос из предыдущих разрядов (если они есть)
2wire [3:0] a = 4'd3;    // Первое слагаемое
3wire [3:0] b = 4'd6;    // Второе слагаемое
4wire [3:0] c;           // Результат сложения
5wire       cout;        // Перенос
6wire [2:0] w;           // Внутренние переносы
И вот теперь самое интересное. Прицепляем провода к портам первого полного сумматора:
1fadd u0(
2   .cin  (cin),  // К порту cin - провод cin
3   .a    (a[0]), // К порту a - провод a[0]
4   .b    (b[0]),
5   .c    (c[0]),
6   .cout (w[0])  // К порту cout (выход) - провод w[0]
7);
Второй сумматор будет прицеплен уже так:
1fadd u1(
2   .cin  (w[0]), // К порту cin - перенос из модуля u0
3   .a    (a[1]),
4   .b    (b[1]),
5   .c    (c[1]),
6   .cout (w[1])
7);
Аналогично следующие:
1fadd u2( .cin(w[1]), .a(a[2]), .b(b[2]), .c(c[2]), .cout(w[2]) );
2fadd u3( .cin(w[2]), .a(a[3]), .b(b[3]), .c(c[3]), .cout(cout) ); // Здесь уже на выход cout
Приведу полный текст модуля tb.v:
1`timescale 10ns / 1ns
2module tb;
3
4reg           clock;
5always  #0.5  clock = ~clock;
6initial begin clock = 0; $dumpfile("tb.vcd"); $dumpvars(0, tb); #2000 $finish; end
7
8// Объявление входов-выходов
9wire       cin = 1'b1;  // Перенос из предыдущих разрядов (если они есть)
10wire [3:0] a = 4'd3;    // Первое слагаемое
11wire [3:0] b = 4'd6;    // Второе слагаемое
12wire [3:0] c;           // Результат сложения
13wire       cout;        // Перенос
14wire [2:0] w;           // Внутренний перенос
15
16// Серия полных сумматоров
17fadd u0( .cin(cin),  .a(a[0]), .b(b[0]), .c(c[0]), .cout(w[0]) );
18fadd u1( .cin(w[0]), .a(a[1]), .b(b[1]), .c(c[1]), .cout(w[1]) );
19fadd u2( .cin(w[1]), .a(a[2]), .b(b[2]), .c(c[2]), .cout(w[2]) );
20fadd u3( .cin(w[2]), .a(a[3]), .b(b[3]), .c(c[3]), .cout(cout) ); // Здесь уже на выход cout
21
22endmodule
Теперь проверим, как работает в симуляторе:

§ Встроенные сумматоры

В довершении темы я могу сказать кое-что, что необязательно каждый раз писать такую огромную схему сумматора. В верилоге можно использовать обычное сложение и вычитание, а также умножение и целочисленное деление, и после синтеза будет адаптировано конкретно под тот чип, который используется. В некоторых чипах есть DSP-блоки, которые содержат в себя быстрые блоки умножения, и потому можно не городить огород и просто умножать. В почти всех ПЛИС-ах также есть специальные возможности для сумматоров и вычитателей, потому лучше писать + или -, а не делать череду из сложных модулей.
В данном случае, можно было бы просто сделать вот так:
1wire [4:0] c    = a + b;
2wire       cout = c[4];
Последний cout даже лишний, потому что результат переноса будет находится в регистре c[4].