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

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

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

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

Итак, входами будут a,b,cin, а выходами c,cout. А теперь переведу схему в код на верилоге.
module fadd(
  input  a, input  b, input cin,
  output c, output cout
);

wire w1 = a ^ b;    // XOR над элементами a, b -> провод w1
wire w2 = a & b;    // AND над a, b -> на провод w2
wire w3 = w1 & cin; // AND над w1 и cin [ (a ^ b) & cin ] -> на провод w3

assign c    = w1 ^ cin; // XOR над w1, cin [ a ^ b ^ cin ] -> на выход
assign cout = w2 | w3;  // OR над w2 и w3 [ (a & b) | ((a ^ b) & cin) ] -> на выход

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),  // К порту cin - провод cin
   .a    (a[0]), // К порту a - провод a[0]
   .b    (b[0]),
   .c    (c[0]),
   .cout (w[0])  // К порту cout (выход) - провод w[0]
);
Второй сумматор будет прицеплен уже так:
fadd u1(
   .cin  (w[0]), // К порту cin - перенос из модуля u0
   .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) ); // Здесь уже на выход 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) ); // Здесь уже на выход cout

endmodule
Теперь проверим, как работает в симуляторе:

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

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

15 апр, 2022
© 2007-2022 Сидит галимый север