§ Провода и регистры (wire/reg)
Сразу скажу, что есть отличный учебник по верилогу. Но мы пойдем другим путем.Логические схемы обычно имеют 3 главных составляющих:
- Комбинаторная логика для вычислений
- Регистры для хранения данных
- Провода для соединения комбинаторной логики и регистров
Для того, чтобы объявить существование провода, надо написать так:
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.... 4endmoduleMyModuleName - это имя модуля, которое можно потом использовать далее.
Для примера, объявлю простой модуль:
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].