Учебный курс. Пишем счетчик на Verilog.

В предыдущей статье мы познакомились с устройством и принципом работы двоичного счетчика. Теперь попробуем описать его на языке Verilog и убедимся в том, насколько языки описания аппаратуры экономят время разработчика и делают его работу эффективнее.

Для написания Verilog-кода потребуется какой-нибудь текстовый редактор, конечно лучше, если он будет с подсветкой синтаксиса.

Отдельный функциональный блок в Verilog называется модулем, и описание блока начинается с определения имени модуля и его интерфейса (входных и выходных сигналов — портов). Рекомендуется в одном файле описывать только один модуль и давать одинаковые названия модулю и файлу. Назовем наш файл counter.v и сделаем заготовку для нашего модуля.

1
2
3
4
5
6
7
8
9
10
11
/*************************************
*  4-разрядный синхронный счетчик    *
*************************************/
module counter (
    clk,			// тактовый сигнал
    rst_n,			// сброс счетчика
    down,			// направление счета (0 - вперед, 1 - назад)
    out				// значение счетчика
);
// тело модуля
endmodule

Первые три строки — многострочный комментарий, который начинается с «/*» и заканчиваются «*/», однострочные комментарии идут после двойного слэша «//». Далее идет ключевое слово «module» и название модуля. После названия в скобочках перечисляются все порты модуля через запятую, здесь мы должны дать имена всем входным и выходным сигналам счетчика. Только эти сигналы будут видны снаружи и к ним можно будет присоединять другие модули. Можно было записать это одной строкой, однако я всегда описываю каждый сигнал новой строкой и поясняю назначение выводов в комментариях сразу в описании модуля. Отступы и выравнивания позволяют сделать код более удобочитаемым. Если активный уровень управляющего сигнала — 0 (низкий уровень), то я добавляю к его имени суффикс _n, и не нужно будет вспоминать при чтении кода какой уровень является активным для того или иного сигнала, особенно если переменных в проекте очень много. Наш счетчик сбрасывается нулем, поэтому и сигнал сброса я назвал rst_n. После описания всех портов следует закрывающая скобка и точка с запятой. Как и в языке Си, каждое выражение в Verilog должно оканчиваться точкой с запятой. Далее идет непосредственно описание функционирования блока, которое мы рассмотрим далее по тексту. И наконец, конец модуля помечается ключевым словом «endmodule».

Имена портов мы определили, однако не ясно какие из них являются входами, а какие выходами. Укажем это:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*************************************
*  4-разрядный синхронный счетчик    *
*************************************/
module counter (
    clk,			// тактовый сигнал
    rst_n,			// сброс счетчика
    down,			// направление счета (0 - вперед, 1 - назад)
    out				// значение счетчика
);
 
input  clk;
input  rst_n;
input  down;
output out;
 
// тело модуля
 
endmodule

Кстати, порт может быть и двунаправленным, тогда он обозначается как inout. Кроме направления нужно определить тип этих портов — wire или reg. Реальный аналог типа wire — проводник в схеме. Сигналом типа wire, можно соединять отдельные модули и элементы между собой, однако ему нельзя будет присвоить какое-либо значение (хотя можно присоединить другие сигналы при помощи ключевого слова assign). Выходному порту out мы будем присваивать то или иное значение, в зависимости от состояния входных портов, поэтому для него используем тип reg. Входные порты могут быть только wire, выходные как wire, так и reg. Поскольку выход у нас 4-разрядный, то нам нужно четыре провода для него. Чтобы описать шину (многоразрядный сигнал), необходимо после указания типа в квадратных скобках определить ширину шины, у нас это [3:0] (4 провода — от 3 до 0). Обратиться к определенному сигналу в шине можно указав ее имя и в скобках номер разряда, например, out[2]. С шиной в Verilog можно работать как с переменной, единым сигналом, другими словами, не нужно присваивать значения каждому проводнику в шине по-отдельности.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*************************************
*  4-разрядный синхронный счетчик    *
*************************************/
module counter (
    clk,			// тактовый сигнал
    rst_n,			// сброс счетчика
    down,			// направление счета (0 - вперед, 1 - назад)
    out				// значение счетчика
);
 
input  clk;
input  rst_n;
input  down;
output out;
 
wire       clk;
wire       rst_n;
wire       down;
reg  [3:0] out;
 
// тело модуля
 
endmodule

Если не указывать тип порта, то он назначается по умолчанию (как правило, wire). Кстати, можно записать все это более компактно, определив необходимое прямо в интерфейсе модуля:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*************************************
*  4-разрядный синхронный счетчик    *
*************************************/
module counter (
    input  wire      clk,	// тактовый сигнал
    input  wire      rst_n,	// сброс счетчика
    input  wire      down,	// направление счета (0 - вперед, 1 - назад)
    output reg [3:0] out	// значение счетчика
);
 
// тело модуля
 
endmodule

В теле модуля должен быть описан хотя бы один процесс. Процесс описывает логику работу модуля, действия, которые он предпринимает при изменении входных сигналов. Процесс описывается ключевым словом always, далее может идти список сигналов, при изменении которых выполняются описанные в процессе действия. Наш счетчик реагирует (изменяет свое значение на выходе) по фронту сигнала clk и по низкому уровню rst_n.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*************************************
*  4-разрядный синхронный счетчик    *
*************************************/
module counter (
    input  wire      clk,	// тактовый сигнал
    input  wire      rst_n,	// сброс счетчика
    input  wire      down,	// направление счета (0 - вперед, 1 - назад)
    output reg [3:0] out	// значение счетчика
);
 
always @(posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
        out <= 4'b0000;
    else
        if (down == 1'b0)
            out <= out + 4'b0001;
        else
            out <= out - 4'b0001;
end
 
endmodule

Все блоки always в модуле выполняются одновременно (параллельно). Символ «@» указывает на то, что необходимо ожидать появления некоторого события. Событием может быть изменение уровня сигнала, тогда эти сигналы перечисляются в скобках. Если необходимо выполнить некоторые действия по фронту сигнала, то перед именем сигнала необходимо указать ключевое слово posedge (по положительному фронту, нарастанию) или negedge (по отрицательному фронту, спаду).

Значение счетчика должно изменяться по положительному фронту тактового сигнала clk или когда сигнал сброса переключился в ноль. Ключевые слова beginend обрамляют тело блока always, там описываются все действия, которые необходимо выполнить по появлению указанного события.

Итак, на вход счетчика пришел положительный фронт сигнала clk или же сигнал сброса установился в 0. Мы не знаем, какое из этих событий произошло, поэтому необходимо проверить уровень сигнала сброса. Если он равен нулю, то необходимо выставить все нули на выходе, иначе проверяем состояние сигнала направления счета (down) и изменяем состояние выхода счетчика (плюсуем или минусуем единицу в зависимости от уровня на входе down). В записи констант первым числом определяется разрядность этой константы, потом идет апостроф, после которого указывается форма записи (b — двоичный вид, h — шестнадцатеричный, o — восьмеричный, d — десятичный) и само значение константы. Поскольку у нас разрядность выхода out равна 4, то мы и отразили это в записи константы при присваивании.

Пару слов нужно сказать и о типах присваивания в Verilog, они бывают блокирующими (=) и неблокирующими (<=). В чем же их отличие? Если в блоке always определено несколько блокирующих присваиваний, следующее не выполняется, пока не сделано предыдущее, причем значение вычисляется непосредственно в момент присваивания. Приведем пример:

1
2
3
4
5
6
7
reg [7:0] x = 2;
reg [7:0] y = 0;
 
always @(sig) begin
    x = 8'd3;
    y = x + 8'd1;
end

В момент определения переменной x, ей присваивается значение 2. При изменении сигнала sig сначала переменной x присваивается значение 3 (y в этот момент равен 0), значение y не присваивается пока x не станет равен 3 («=» блокирует следующую операцию). Как только присваивание нового значения прошло, то выполняется следующее присваивание, и y становится равным 4. В итоге, после первого выполнения блока always x=3, y=4. Теперь рассмотрим неблокирующее присваивание:

1
2
3
4
5
6
7
reg [7:0] x = 2;
reg [7:0] y = 0;
 
always @(posedge sig) begin
    x <= 8'd3;
    y <= x + 8'd1;
end

Как только появился фронт сигнала sig, первым делом рассчитываются все значения, которые стоят с правой стороны от оператора присваивания. С тройкой все понятно, а вот чему равно выражение x+1? При первом выполнении блока always оно будет равно 3, так как x=2 в этот момент (равно первоначальному значению). И уже после вычисления этих выражений производится одновременное присваивание всем переменным в блоке. Таким образом, переменные x и y становятся равными 3.

Блокирующие присваивание, как правило, используется при описании комбинационной логики, когда событие происходит по уровню сигнала. Неблокирующее присваивание применяется при описании синхронной логики, когда действие выполняется по фронту сигнала (с ключевыми словами posedge или negedge).

Итак, мы вкратце познакомились с языком Verilog и написали простенький счетчик. Как вы видите, в этом нет ничего сложного. Я думаю все согласятся с тем, что написать код счетчика на Verilog куда проще (как и понять логику работы блока), чем делать все это на уровне вентилей или транзисторов. В следующей статье мы промоделируем этот счетчик в среде ModelSim, чтобы убедиться в правильности его работы.

1 комментарий

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *