§ Провода и регистры (wire/reg)
Сразу скажу, что есть отличный учебник по верилогу. Но мы пойдем другим путем.Логические схемы обычно имеют 3 главных составляющих:
- Комбинаторная логика для вычислений
- Регистры для хранения данных
- Провода для соединения комбинаторной логики и регистров
Для того, чтобы объявить существование провода, надо написать так:
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(<порты>); <описание порты> .... endmoduleMyModuleName - это имя модуля, которое можно потом использовать далее.
Для примера, объявлю простой модуль:
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Теперь же можно создать небольшой файл тестбенча (беру как шаблон):module tb; reg clock; always #0.5 clock = ~clock; initial begin clock = 0; $dumpfile("tb.vcd"); $dumpvars(0, tb); #2000 $finish; end .... endmodule`timescale 10ns / 1ns Чтобы объявить здесь новый подмодуль, его можно записать в таком виде:
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; // Внутренние переносыИ вот теперь самое интересное. Прицепляем провода к портам первого полного сумматора:
// К порту cin - провод cin .a (a[0]), // К порту a - провод a[0] .b (b[0]), .c (c[0]), .cout (w[0]) // К порту cout (выход) - провод w[0] );fadd u0( .cin (cin), Второй сумматор будет прицеплен уже так:
0]), // К порту cin - перенос из модуля u0 .a (a[1]), .b (b[1]), .c (c[1]), .cout (w[1]) );fadd u1( .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) ); // Здесь уже на выход coutfadd u2( .cin(w[Приведу полный текст модуля tb.v:
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`timescale 10ns / 1ns Теперь проверим, как работает в симуляторе:
§ Встроенные сумматоры
В довершении темы я могу сказать кое-что, что необязательно каждый раз писать такую огромную схему сумматора. В верилоге можно использовать обычное сложение и вычитание, а также умножение и целочисленное деление, и после синтеза будет адаптировано конкретно под тот чип, который используется. В некоторых чипах есть DSP-блоки, которые содержат в себя быстрые блоки умножения, и потому можно не городить огород и просто умножать. В почти всех ПЛИС-ах также есть специальные возможности для сумматоров и вычитателей, потому лучше писать + или -, а не делать череду из сложных модулей.В данном случае, можно было бы просто сделать вот так:
wire [4:0] c = a + b; wire cout = c[4];Последний cout даже лишний, потому что результат переноса будет находится в регистре c[4].