Раньше мы уже познакомились с постоянным назначением сигналов, оно выглядит, например, вот так:
wire a,b,c;
assign c = a & b;
Постоянные назначения весьма полезны, но и они имеют недостатки. Такой код, когда его много, не очень легко читать. Чтобы сделать язык Verilog более выразительным, он имеет так называемые "always" блоки. Они используются при описании системы с помощью поведенческих блоков (behavioral blocks). Использование поведенческих блоков очень похоже на программирование на языке С. Оно позволяет выразить алгоритм так, чтобы он выглядел как последовательность действий (даже если в конечном счете в аппаратуре это будет не так).
Для описания поведенческого блока используется вот такой синтаксис:
always @(<sensitivity_list>) <statements>
<sensitivity_list> – это список всех входных сигналов, к которым чувствителен блок. Это список входных сигналов, изменение которых влияет выходные сигналы этого блока. "Always" переводится как "всегда". Такую запись можно прочитать вот так: "Всегда выполнять выражения <statements> при изменении сигналов, описаных в списке чувствительности <sensitivity list>".
Если указать список чувствительности неверно, то это не должно повлиять на синтез проекта, но может повлиять на его симуляцию. В списке чувствительности имена входных сигналов разделяются ключевым словом "or":
always @(a or b or d) <statements>
Иногда гораздо проще и надежней включать в список чувствительности все сигналы. Это делается вот так:
always @* <statements>
Тогда исправляя выражения в <statements> вам не нужно задумываться об изменении списка чувствительности.
При описании выражений внутри поведенческих блоков комбинаторной логики, с правой стороны от знака равенства, как и раньше, можно использовать типы сигналов wire или reg, а вот с левой стороны теперь используется только тип reg:
reg [3:0] c;
always @(a or b or d)
begin
c = <выражение использующее входные сигналы a,b,d>;
end
Обратите внимание, что регистры, которым идет присвоение в таких поведенческих блоках не будут выполнены в виде D-триггеров после синтеза. Это часто вызывает недоумение у начинающих.
Здесь мы делаем присвоение регистрам с помощью оператора "=", который называется "блокирующим". Для симулятора это означает, что выражение вычисляется, его результат присваивается регистру приемнику и он тут же, немедленно, может быть использован в последующих выражениях.
Таким образом, блокирующие присвоения используются для описания комбинаторной логики в поведенческих блоках. Не блокирующие присвоения будут описаны позднее – они используются для описания синхронной логики и вот уже там регистры reg после синтеза будут представлены с помощью D-триггеров. Не путайте блокирующие и не блокирующие присвоения!
Вы можете написать довольно много выражений связанных между собой, синтезатор переработает их и создаст, возможно, довольно длинные цепи из комбинаторной логики. Например:
wire [3:0] a, b, c, d, e;
reg [3:0] f, g, h, j;
always @(a or b or c or d or e)
begin
f = a + b;
g = f & c;
h = g | d;
j = h - e;
end
То же самое можно сделать по другому, вот так:
always @(a or b or c or d or e)
begin
j = (((a + b) & c) | d) - e;
end
На самом деле, после того, как проект будет откомпилирован, список всех сигналов проекта (netlist) может сильно сократиться. Многие сигналы, описанные программистом, могут исчезнуть – синтезатор выбросит их, создав цепи из оптимизированной комбинаторной логики. В нашем примере сигналы f, g и h могут исчезнуть из списка сигналов проекта после синтезатора, все зависит от того используются ли эти сигналы где-то еще в проекте или нет. Синтезатор даже может выдать предупреждение (warning) о том, что сигналу "f" присвоено значение, но оно нигде не используется – и такое тоже бывает.
Однако вернемся к нашим поведенческим блокам. Теперь с ними мы уже можем делать очень интересные вещи, такие как условные переходы, множественный выбор по условию, циклы и прочее.
Например, мы уже знаем как описать простой мультиплексор с помощью оператора "?", вот так:
reg [3:0] c;
always @(a or b or d)
begin
c = d ? (a & b) : (a + b);
end
А теперь можем написать это же, но по другому:
reg [3:0] c;
always @(a or b or d) begin
if (d) begin
c = a & b;
end else begin
c = a + b;
end
end
Вместо параметра "d" может быть любое выражение. Если значение этого выражения истина (не равно нулю), то будет выполняться первое присвоение "c = a & b". Если значение выражения "d" ложь (равно нулю), то будет выполняться второе присвоение "c = a + b".
Если нужно сделать выбор из многих выриантов (это на языке схемотехники мультиплексор со многими входами), то можно использовать конструкцию case. Конструкции case очень похожи на switch из языка C.
Базовый синтаксис вот такой:
case (selector)
option1: <statement>;
option2: <statement>;
default: <if nothing else statement>; //по желанию, но желательно
endcase
А вот и простой пример:
wire [1:0] option;
wire [7:0] a, b, c, d;
reg [7:0] e;
always @(a or b or c or d or option) begin
case (option)
0: e = a;
1: e = b;
2: e = c;
3: e = d;
endcase
end
Поскольку входы у нас – это 8-ми битные шины, то в результате синтеза получится восемь мультиплексоров четыре-к-одному.
Давайте теперь рассмотрим циклы. Тут нужно сделать замечание, что циклы в Verilog имеют несколько иной смысл, не такой, как в языках C или Pascal. На языке C цикл обозначает, что некоторое действие должно быть выполнено последовательно раз за разом. Чем больше итераций в цикле, тем дольше он будет исполняться процессором. На языке Verilog цикл скорее описывает сколько экземпляров логических функций должно быть реализовано аппаратно. Чтобы синтез прошел успешно, циклы должны иметь заданное фиксированное число итераций – иначе синтезатор просто не сможет ничего сделать.
Рассмотрим простой пример – нужно определить номер самого старшего ненулевого бита вектора (шины):
module find_high_bit(input wire [7:0]in_data, output reg [2:0]high_bit, output reg valid);
integer i;
always @(in_data)
begin
//определим, есть ли в шине единицы
valid = |in_data;
//присвоим хоть что нибудь
high_bit = 0;
for(i=0; i<8; i=i+1)
begin
if(in_data[i])
begin
// запомним номер бита с единицей в шине
high_bit = i;
end
end
end
endmodule
В приведенном примере цикл просматривает все биты "последовательно" от младшего к старшему. Когда будет найден ненулевой бит, то его порядковый номер запоминается в регистре high_bit. Когда цикл закончится, то регистр high_bit будет содержать индекс самого старшего ненулевого бита в шине (если конечно valid тоже не ноль). На самом деле, конечно, нужно представлять себе, что подобная запись цикла будет реализована в аппаратуре довольно длинной цепочкой из мультиплексоров. Вы должны представлять себе, что в этом примере цикл for - это примерно вот такая логическая схема:

Рассмотрим другой пример – нужно найти самый младший не нулевой бит в шине:
module find_low_bit(input wire [7:0]in_data, output reg [2:0]low_bit, output reg valid);
integer i;
always @(in_data)
begin
//определим, есть ли в шине единицы
valid = |in_data;
//присвоим хоть что нибудь
low_bit = 0;
for(i=7; i>=0; i=i-1)
begin
if(in_data[i])
begin
// запомним номер бита с единицей в шине
low_bit = i;
end
end
end
endmodule
Теперь просмотр битов идет от старшего к младшим. Обратите внимание, что счетчик циклов определен как integer - это целое число со знаком. И так нужно, потому что сравнение ">=0" как раз и означает "не отрицательный". Переменная типа reg всегда положительна, значит здесь использоваться не может.
Ну вот пожалуй и все для этого предпоследнего урока! Потерпите пожалуйста, остался один урок!

Подробнее...