Menu

FSM в Verilog: стандартный паттерн конечного автомата

Как писать Verilog FSM так, как делают профи: тактируемый state-регистр, комбинационный блок next-state и чистое разделение, которое легко читать и синтезировать.

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

Что такое FSM

Конечный автомат - это контроллер, который:

  • Держит одно из фиксированного множества именованных состояний в любой момент.
  • Переходит между состояниями в зависимости от входов (и, возможно, текущего состояния).
  • Производит выходы, зависящие от текущего состояния (и, может быть, от текущих входов).

Это покрывает поразительное количество цифрового проектирования: контроллеры светофоров, UART-передатчики, контроллеры памяти, обработчики сетевых протоколов, дешифраторы инструкций - всё, что имеет дискретные режимы работы.

То, что делает FSM ощутимо отличным от datapath-логики - это состояния, не значения. Счётчик тикает через 1, 2, 3, 4. FSM тикает через IDLE, FETCHING, BUSY, DONE - та же форма, но у состояний есть имена и переходы имеют смысл.

Two-process-паттерн

Стандартный FSM Verilog использует два always-блока:

  1. Тактируемый блок, захватывающий state-регистр на каждом фронте clock. Использует non-blocking. Крошечный - обычно три строки.
  2. Комбинационный блок, использующий case по текущему состоянию для вычисления next-state и выходов. Использует blocking. "Интересный" код живёт тут.

Это разделение даёт три выгоды: заставляет думать о состоянии явно, выдаёт железо, чисто отображаемое в "один регистр плюс один комбинационный блок", и это стандарт - каждый, кто прочтёт твой Verilog, узнает его моментально.

Разобранный пример: детектор последовательности

Классическая обучающая FSM: детектируй битовую последовательность 1011 на serial-входе. Выдай однотактовый импульс при завершении последовательности.

Двухблочная структура - bullet point выше. Чистящие детали стоит обозначить:

  • Дефолты в начале комбинационного блока. next_state = state (оставайся, где есть) и detected = 1'b0 (без импульса) - это присваивания "ничего не делать". Каждая ветка case затем только меняет то, что отличается. Это делает невозможным вывод latch.
  • localparam для имён состояний. Любой, кто читает module, думает в S0, S1, S2, S3, а не в 3'd0, 3'd1. Синтезатор делает подстановку.
  • Никаких выходов из тактируемого блока. Вся "что делает это состояние"-логика живёт в комбинационном блоке. Тактируемый блок отвечает ни за что, кроме удержания текущего состояния.

Moore vs Mealy

Moore: выход зависит только от текущего состояния. Mealy: выход зависит от текущего состояния и текущих входов.

В примере выше detected устанавливается внутри ветки S3 только когда in соответствует одному из ожидаемых паттернов завершения. Это Mealy-выход - он зависит от in в дополнение к state. Версия Moore имела бы отдельное состояние "только что детектировано" и ставила бы detected = 1, когда это состояние текущее; выход пульсировал бы тактом позже, но никогда бы не реагировал на глитч на in.

Оба стиля валидны. Moore - дефолт в учебниках, потому что выходы гарантированно не глитчат при изменении входов посреди такта. Mealy быстрее (нет задержки регистра для input-driven-выходов) и даёт меньшее железо во многих случаях. Выбирай по протоколу, который реализуешь.

Кодировка состояний: binary, one-hot, gray

Битовый паттерн, который ты назначаешь каждому состоянию, важен для площади и скорости:

  • Binary (S0 = 3'd0, S1 = 3'd1, ...): наименьший state-регистр - log2N\lceil \log_2 N \rceil бит для NN состояний. Максимум decode-логики.
  • One-hot (S0 = 4'b0001, S1 = 4'b0010, ...): N бит для N состояний. Decode-логика тривиальна (каждое состояние - один wire); переходы быстрые. FPGA часто дефолтят на это.
  • Gray code: соседние состояния отличаются на один бит. Полезно, когда биты состояния пересекают clock-домены.

Большинство современных синтезаторов выбирают кодировку сами (Vivado, Quartus, Design Compiler - у всех есть автоматический режим, пробующий каждую и выбирающий лучшую). Указывать редко нужно. Указывать когда нужно: большинство тулов принимают атрибут или прагму (* fsm_encoding = "one_hot" *).

Three-block-вариант

Иногда увидишь FSM, разбитый на три блока: один тактируемый для state, один комбинационный для next-state, один комбинационный для выходов. Это просто two-block-паттерн с вычислением выходов, вынесенным в свой блок:

// State-регистр
always @(posedge clk) ...

// Логика next-state
always @(*) ...

// Логика выходов
always @(*) begin
    case (state)
        ...
    endcase
end

Split-output-стиль полезен, когда выходов много, и next-state-логика выглядела бы заваленной, будь они в одном блоке. Для маленьких FSM это overkill.

Что делает default в FSM

У case-оператора каждой FSM должна быть ветка default. Две причины:

  1. Безопасность: если state-регистр каким-то образом принял невалидное значение (повреждение, баг, частичный reset), default возвращает его в известное состояние.
  2. Подсказка синтезатору: когда явные ветки исчерпывающие (2-битное состояние со всеми 4 значениями), default: next_state = 'x; говорит синтезатору "обещаю, default недостижим, оптимизируй свободно". Если недостижимый путь всё-таки задействован в симуляции, получающийся x распространится и выявит баг немедленно.
default: begin
    next_state = S0;   // безопасное восстановление
    // или
    next_state = 'x;   // недостижимо, оптимизируй свободно
end

Выбирай по тому, доказал ли ты, что default действительно недостижим.

За чем следить

Забывают дефолты в начале комбинационного блока. Без next_state = state и дефолтов для выходов ветка, не присваивающая всего, протекает latch.

Кладут выходы в тактируемый блок. Если detected <= 1 живёт в блоке always @(posedge clk), выход регистрирован - появляется тактом позже. Это может быть намеренно ("registered Mealy"-выход), но это частая случайная ошибка дизайна, когда спека требует мгновенный импульс.

Смешивают blocking и non-blocking. Тактируемый блок: <=. Комбинационный: =. Смешивание двух в одном блоке - race condition.

Комбинационный always-блок, который ссылается на next_state и присваивает state. Это строит петлю обратной связи, которую симулятор не может разрешить. Тактируемый блок владеет state; комбинационный владеет next_state; никогда не давай ни одному из них трогать переменную другого.

Что дальше

Теперь умеешь строить любой контроллер, который можешь описать. Следующая глава делает шаг назад от синтезируемого дизайна и разбирает testbench, который его дёргает - как подавать стимулы, наблюдать выходы, дампить waveform и валидировать, что твои модули реально делают то, что ты думаешь.

Часто задаваемые вопросы

Что такое finite state machine в Verilog?

FSM - это контроллер, который держит одно из небольшого множества именованных состояний и переключается между ними в зависимости от входов. В Verilog стандартная реализация имеет два блока: тактируемый always, обновляющий state-регистр на каждом фронте clock, и комбинационный always, вычисляющий next-state и выходы по текущему состоянию и входам.

Что такое стандартный паттерн FSM в Verilog?

Two-process FSM: один тактируемый блок always @(posedge clk) держит state-регистр и использует non-blocking присваивание, а один комбинационный блок always @(*) использует case по текущему состоянию для вычисления next-state и выходов. Это разделение делает код легко читаемым, lint-овым и чисто синтезируемым.

В чём разница между Mealy и Moore FSM?

В Moore FSM выходы зависят только от текущего состояния. В Mealy FSM выходы зависят и от текущего состояния, и от текущих входов. Mealy реагирует на такт быстрее (нет задержки регистра для input-dependent-выходов), но может давать глитчи, если входы меняются посреди такта. Moore медленнее на такт, но предсказуемее - выбор по умолчанию, если не нужна скорость.

Как кодировать состояния в Verilog?

Используй localparam-константы внутри module: localparam IDLE = 3'd0; и т.д. Три частые кодировки: binary (состояния 0, 1, 2, ... - наименьший state-регистр), one-hot (один бит на состояние, меньше уровней логики на переход) и gray code (соседние состояния отличаются на один бит - минимизирует глитчи). Синтезаторы обычно сами выбирают кодировку; фиксировать её редко нужно.

Coddy programming languages illustration

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

НАЧАТЬ