Что такое FSM
Конечный автомат - это контроллер, который:
- Держит одно из фиксированного множества именованных состояний в любой момент.
- Переходит между состояниями в зависимости от входов (и, возможно, текущего состояния).
- Производит выходы, зависящие от текущего состояния (и, может быть, от текущих входов).
Это покрывает поразительное количество цифрового проектирования: контроллеры светофоров, UART-передатчики, контроллеры памяти, обработчики сетевых протоколов, дешифраторы инструкций - всё, что имеет дискретные режимы работы.
То, что делает FSM ощутимо отличным от datapath-логики - это состояния, не значения. Счётчик тикает через 1, 2, 3, 4. FSM тикает через IDLE, FETCHING, BUSY, DONE - та же форма, но у состояний есть имена и переходы имеют смысл.
Two-process-паттерн
Стандартный FSM Verilog использует два always-блока:
- Тактируемый блок, захватывающий state-регистр на каждом фронте clock. Использует non-blocking. Крошечный - обычно три строки.
- Комбинационный блок, использующий
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-регистр - бит для состояний. Максимум 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. Две причины:
- Безопасность: если state-регистр каким-то образом принял невалидное значение (повреждение, баг, частичный reset),
defaultвозвращает его в известное состояние. - Подсказка синтезатору: когда явные ветки исчерпывающие (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 (соседние состояния отличаются на один бит - минимизирует глитчи). Синтезаторы обычно сами выбирают кодировку; фиксировать её редко нужно.