Время в софте vs время в железе
В программе время продвигается на одну инструкцию за раз. CPU доделывает строку 1, затем выполняет строку 2. Если хочешь, чтобы что-то происходило одновременно - берёшь потоки, async или вторую машину.
В железе всё происходит одновременно. Схема не ходит по очереди. Сумматор всегда складывает, мультиплексор всегда выбирает, flip-flop всегда смотрит на clock. Никакого program counter. Никакой "текущей строки".
Verilog должен описывать этот мир текстом. То, как он это делает, и есть источник любой путаницы в этой главе.
Два уровня параллельности
Когда читаешь модуль на Verilog, ты одновременно смотришь на две разные вещи:
- Статическое описание схемы. Wires, регистры, instance вентилей, instance подмодулей, непрерывные присваивания. Всё это существует одновременно. Порядок в файле значения не имеет.
- Процедурные блоки -
initialиalways- которые выглядят как маленькие программы, по которым симулятор шагает. Внутри одного из таких блоков операторы действительно выполняются в каком-то порядке. Но несколькоalwaysмогут быть активны одновременно, каждый в своём крошечном потоке симулированного времени.
module example(input wire a, input wire b, output wire y, output wire z);
assign y = a & b; // существует всегда
assign z = a | b; // тоже существует всегда, параллельно
always @(posedge a) begin
// отдельный "always-on" реактор, который просыпается
// на каждом нарастающем фронте `a`
end
endmodule
Две строки с assign - это не последовательность. Они описывают два куска комбинационной логики, которые синтезатор может разместить бок о бок. Блок always - это третья штука, происходящая параллельно.
Что значит "сигнал"
В софте переменная держит значение, пока ты его не поменяешь. В Verilog сигнал держит значение непрерывно - и в каждый момент оно зависит от того, что бы его ни приводило в действие.
wire приводится в действие снаружи (через assign, через выходной port подмодуля, через inout-соединение). reg приводится в действие изнутри процедурного блока. У обоих всегда есть значение. Понятия "неинициализированный" в смысле C тут нет - сигналы либо приведены к определённому значению, либо имеют специальное unknown x, либо высокоимпедансное z. Последние два разберём в X and Z Values.
Clock меняет всё
Как только появляется clock, время начинает иметь значение. Flip-flop - это крошечный кусок железа, который захватывает значение своего входа на нарастающем (или ниспадающем) фронте clock и держит его до следующего фронта. Именно clock позволяет строить счётчики, конечные автоматы, pipelines - всё, что обладает памятью.
Заметь q <= d, а не q = d. Это non-blocking присваивание - рабочая лошадка тактируемой логики. Оно говорит "на следующем фронте clock запланируй, чтобы q стал тем, чем сейчас является d". Правила копнём в Blocking vs Non-blocking; пока просто запомни: это присваивание не притворяется софтовым оператором.
RTL: ментальная модель register transfer
Большая часть синтезируемого Verilog пишется в стиле под названием Register Transfer Level, или RTL. Идея проста:
- Реши, какое состояние нужно твоей схеме (регистры).
- Для каждого регистра опиши две вещи: что его сбрасывает, и какая комбинационная логика вычисляет его следующее значение.
- Подключи выходы комбинационной логики к входам регистров - и у тебя рабочая схема.
always @(posedge clk) begin
if (reset) state <= IDLE;
else state <= next_state;
end
always @(*) begin
case (state)
IDLE: next_state = start ? RUNNING : IDLE;
RUNNING: next_state = done ? IDLE : RUNNING;
default: next_state = IDLE;
endcase
end
Это автомат на два состояния. Первый always - тактируемый, это flip-flop. Второй - чисто комбинационный, это просто уравнение. Почти каждый автомат, счётчик и pipeline, который ты напишешь, имеет такую форму.
Привычки, на которых застреваешь
Если идёшь из софта, вот короткий список привычек, от которых стоит отойти:
- "Переменные обновляются, когда я им что-то присваиваю." На фронте clock - нет: non-blocking присваивание планирует обновление на конец временного шага.
- "Операторы выполняются сверху вниз." Вне процедурных блоков - нет. Внутри тактируемого блока - вроде бы да, но blocking vs non-blocking меняет, что вообще значит "по порядку".
- "Аллоцирую, когда понадобится." Железо ничего не аллоцирует. Каждый регистр и вентиль должен существовать на момент синтеза. Размер каждого вектора фиксирован.
- "Этот цикл быстрый - это же одна операция."
forв синтезируемом Verilog разворачивается в параллельное железо. Цикл на 64 итерации становится 64 копиями тела, а не инструкцией CPU, которая выполняется 64 раза.
Ты не учишься писать программу. Ты учишься описывать схему. Инстинкт читать сверху вниз - ровно неправильный, и переучиваться придётся какое-то время.
Что дальше
Следующие документы проведут через получение локального тулчейна (опционально - в браузере и так работает), затем написание твоего первого module с нуля. Мы будем возвращаться к контрасту hardware-vs-software каждый раз, когда что-то будет казаться странным - потому что большая часть "странного" имеет один и тот же корень: это не софт.
Часто задаваемые вопросы
В чём разница между hardware и software в терминах Verilog?
Software - это последовательность инструкций, которые CPU выполняет одну за другой. Hardware - то, что описывает Verilog - это сеть вентилей и проводов, которые все одновременно несут сигналы. Файл Verilog описывает эту сеть. Симулятор имитирует параллельное поведение; инструмент синтеза превращает его в реальный кремний.
Код Verilog выполняется сверху вниз, как в C?
Нет - и считать его таким - самая частая ошибка новичка. Операторы верхнего уровня (assigns, instance модулей, always-блоки) все 'существуют' одновременно. Только внутри процедурных блоков - initial и always - происходит что-то, отдалённо напоминающее последовательное выполнение, и даже там non-blocking присваивания ломают эту иллюзию.
Что значит 'concurrent' в Verilog?
Это значит, что несколько операторов описывают части схемы, которые работают одновременно. Два assign в одном модуле - это не 'строка 1, потом строка 2', а два куска железа, работающих параллельно, оба непрерывно реагируют на свои входы.
Что такое RTL design?
RTL - это Register Transfer Level. Это стиль написания Verilog, в котором ты описываешь схему как набор регистров (flip-flop) и комбинационную логику, вычисляющую их следующие значения. Большая часть синтезируемого Verilog - это RTL. Уровень выше - поведенческий; уровень ниже - gate-level.