Menu

Hardware vs Software: чем Verilog мыслит иначе, чем C или Python

Почему Verilog кажется дезориентирующим после софтовых языков: параллельность по умолчанию, время как первоклассное понятие и операторы, которые выполняются не по порядку.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Время в софте vs время в железе

В программе время продвигается на одну инструкцию за раз. CPU доделывает строку 1, затем выполняет строку 2. Если хочешь, чтобы что-то происходило одновременно - берёшь потоки, async или вторую машину.

В железе всё происходит одновременно. Схема не ходит по очереди. Сумматор всегда складывает, мультиплексор всегда выбирает, flip-flop всегда смотрит на clock. Никакого program counter. Никакой "текущей строки".

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

Два уровня параллельности

Когда читаешь модуль на Verilog, ты одновременно смотришь на две разные вещи:

  1. Статическое описание схемы. Wires, регистры, instance вентилей, instance подмодулей, непрерывные присваивания. Всё это существует одновременно. Порядок в файле значения не имеет.
  2. Процедурные блоки - 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.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ