Flip‑flop (в дословном переводе — «шлёпанцы») — так называется элемент, благодаря которому микропроцессор в вашем компьютере, да и вообще любой современный чип, обрёл способность запоминать.
Представьте абстрактную схему, у которой есть вход и выход. Интуитивно понятно: если мы подаём на схему какое‑то входное значение или меняем его, меняется и выходное значение — то есть выход напрямую зависит от входа. Но как получилось, что такую, казалось бы, чисто комбинационную схему (без памяти) заставили запоминать входное значение и использовать его в дальнейшем для вычисления новых выходов?
В этой статье я хочу:
-
разобраться, как мы получили память (flip‑flop);
-
заодно затронуть, откуда берутся проблемы setup/hold time и метастабильности, связанные с памятью.
Начнём, пожалуй, с комбинационных схем. Любую такую схему можно описать с помощью булевой алгебры. Например, операцию сложения трёх бинарных разрядов:
можно описать следующими уравнениями:
Такие схемы — комбинационные: выход зависит исключительно от текущих входных значений. Но мне хочется получить схему, у которой появится ещё и внутреннее состояние. Для этого понадобится ячейка памяти.
Можно замкнуть цепь — соединить вход схемы с её же выходом — и вывести из этой петли один выходной контакт.
Звучит немного безумно, но такая схема действительно обладает свойством сохранять значение (напомню, что я рассматриваю схему только на логическом, цифровом уровне, без учёта конкретного семейства логики (logic family), в котором она в итоге будет реализована). Правда, сохранять она умеет, а вот записывать — нет: если изначальное значение равно 1, схема будет бесконечно «гонять» эту единицу по кругу, и перезаписать её мы не сможем. Значит, нужен способ задавать (и менять) хранимое значение.
Добавим входной контакт и подключим его к схеме через мультиплексор 2-в-1 (MUX), который будет играть роль переключателя.
Селектор мультиплексора назовём clk: когда он равен 0 — режим хранения, MUX пропускает через себя сигнал внутренней петли; когда он равен 1 — режим записи, MUX пропускает новое входное значение. В теории это должно работать точно как D‑триггер‑защёлка (D latch). Но как это выглядит на практике? Попробуем записать несколько значений в такую «защёлку» в симуляции и посмотрим на результат.
`timescale 1us / 1nsmodule custom_d_latch(output wire Q,input clk, D);wire D_and_out, Q_and_out, inv_clk;not (inv_clk, clk);and (D_and_out, D, clk);and (Q_and_out, Q, inv_clk);or (Q, D_and_out, Q_and_out);endmodulemodule stimulus;localparam DELAY=1;reg clk=0, D=1;wire Q;custom_d_latch dl(.clk(clk), .D(D), .Q(Q));initial begin$monitor("Q=%d", Q);set_q(1'b0);set_q(1'b1);endtask set_q(input d);beginD=d;clk=1;#DELAY;clk=0;#DELAY;endendtaskendmodule
output:Q=0Q=1Q=0
Несмотря на то что последнее записанное значение — 1, Q снова возвращается к 0. Это происходит потому, что путь A (схема 3) быстрее, чем путь B (на пути B стоит дополнительный инвертор). При переходе clk из 1 в 0 из‑за разницы в задержках распространения сигнала возникает короткое «окно» с неверным (0) значением. По сути, это гоночное, метастабильное поведение схемы.
Есть два выхода: либо вручную выравнивать задержки каждого пути, либо переработать схему средствами булевой алгебры и проверить все переходы входных сигналов на появление таких «окон» с неправильными значениями. Пойдём по второму пути.
Минимизированная формула нашей схемы с мультиплексором:
По теореме консенсуса мы можем добавить продукт DQ и вывести Q за скобку:
Осталось применить закон де Моргана, чтобы получить формулу, реализуемую целиком на элементах NAND:
Посмотрим, как выглядит схема, реализующая это булево уравнение:
Это и есть D‑триггер‑защёлка (D‑type latch)! Проверим все переходы на предмет метастабильного поведения.
`timescale 1us / 1nsmodule custom_d_latch_nand(output wire Q,input clk, D);not (inv_D, D);nand (D_nand_clk, D, clk);nand (iD_nand_clk, inv_D, clk);nand (Q_nand, iD_nand_clk, Q);nand (Q, Q_nand, D_nand_clk);endmodulemodule stimulus;localparam DELAY=1;reg clk=0, D=1;wire Q;custom_d_latch_nand dl(.clk(clk), .D(D), .Q(Q));initial begin$monitor("Q=%d", Q);set_q(1'b0);#DELAY;set_q(1'b0);#DELAY;set_q(1'b1);#DELAY;set_q(1'b1);#DELAY;set_q(1'b0);#DELAY;endtask set_q(input d);begin$display("setting q = %b\n", d);D=d;clk=1;#DELAY;clk=0;#DELAY;endendtaskendmodule
output:
setting q = 0Q=0setting q = 0setting q = 1Q=1setting q = 1setting q = 0Q=0
На этот раз никаких неожиданных переходов, как в прошлый раз. Если соединить две такие защёлки в конфигурацию master‑slave, получится flip‑flop — но сейчас не об этом. Если разобрать SR‑защёлку — «зерно», лежащее в основе всех подобных схем, — получится примерно такой мультиплексор:
Главное, что нам удалось, — заглянуть внутрь «чёрного ящика» flip‑flop и понять, откуда берётся такая на первый взгляд странная схема. Точно так же можно разобрать и другие типы цифровой памяти (JK‑, T‑триггеры) — все они в конечном счёте базируются на SR‑защёлке.
Взгляд на такие схемы как на замкнутый в петлю мультиплексор даёт интересную перспективу на метастабильность и setup‑violation. При метастабильности ячейка памяти начинает «мигать», постоянно меняя значение, — потому что в замкнутую петлю мультиплексора одновременно попадают противоречивые значения. Представьте стабильную петлю со значением 1, в которую вдруг частично «просочилось» значение 0. Это похоже на трубу с водой, в которую попали пузырьки воздуха: на выходе течёт вода, но время от времени — воздух, то есть поток нестабилен.
Так же увидели, что метастабильность возникает не только из‑за нарушения временных характеристик (setup/hold), но и из‑за неправильно спроектированной схемы самого flip‑flop.
ссылка на оригинал статьи https://habr.com/ru/articles/1057288/