4. Последовательностная логика. Учим кремний помнить

В предыдущей главе 3. Комбинационная логика. Учим кремний считать мы собрали из базовых логических вентилей сумматор и арифметико-логическое устройство (ALU). Это впечатляющие инженерные конструкции, но у них есть одно фатальное ограничение: полная амнезия.

Комбинационная логика подобна горному водопаду: пока на вход льется вода (подается напряжение), на выходе крутятся лопасти турбины (вычисляется результат). Но стоит перекрыть вентиль — и поток иссякает, не оставляя и следа. На выходе схемы значение мгновенно исчезает, как только исчезли входные сигналы.

Представьте, что вам предложили написать высоконагруженный бэкенд на Go, в котором строго запрещено использовать переменные в памяти — разрешены только чистые математические функции. Вы не можете выполнить элементарный инкремент счетчика запросов:

Go
i++ // i = i + 1

Почему? Потому что операция i++ требует времени и последовательности:

  1. Прочитать текущее значение переменной i.
  2. Подать его в сумматор ALU и вычислить i + 1.
  3. Запомнить полученный результат в той же ячейке i, перезаписав старое значение.

Если у кремния нет памяти, новому результату просто негде ждать следующей инструкции. Чтобы превратить калькулятор в полноценный компьютер, способный выполнять циклы, сохранять состояние сессий пользователей и манипулировать структурами данных, нам необходимо научить кремний главному свойству интеллекта — памяти.

Добро пожаловать в мир последовательностной логики (Sequential Logic), где выходной сигнал зависит не только от того, что подано на вход прямо сейчас, но и от всей предыстории системы.


Обратная связь: Замыкание петли

Как заставить электрическую цепь, состоящую из кремниевых ключей, помнить сигнал, если источник этого сигнала уже отключен?

Ответ на этот вопрос в середине XX века перевернул радиотехнику и теорию управления: нужно направить выход схемы обратно на ее собственный вход. Этот фундаментальный принцип называется обратной связью (Feedback Loop).

Каждый из нас встречался с акустической обратной связью на концертах или конференциях: если поднести включенный микрофон слишком близко к звуковой колонке, звук из колонки попадает в микрофон, усиливается, снова выходит из колонки — и зал оглушает резкий, непрерывный пронзительный свист. Система «зациклилась» и поддерживает колебания сама по себе, даже если в микрофон больше никто не говорит.

В цифровой схемотехнике обратная связь работает аналогично, но вместо звука мы закольцовываем логические уровни напряжения 0 и 1.

flowchart LR
    subgraph Feedback ["Принцип обратной связи"]
        In["Входной сигнал"] --> Gate["Логический вентиль"]
        Gate --> Out["Выходной сигнал (память)"]
        Out -.->|"Петля обратной связи"| Gate
    end

    classDef fb fill:#131d33,stroke:#38bdf8,stroke-width:1px,color:#f8fafc;
    class Feedback fb;
  

Соединив вентили определенным образом, мы получаем так называемую бистабильную ячейку (bistable multivibrator) — схему, обладающую двумя устойчивыми состояниями равновесия. Она может сколь угодно долго оставаться в состоянии «0» или в состоянии «1», пока внешний импульс не заставит ее переключиться.


SR-триггер: Рождение бита

Простейшим практическим воплощением бистабильной ячейки является SR-триггер (Set-Reset Latch). Его можно собрать всего из двух вентилей NOR (ИЛИ-НЕ), соединенных крест-накрест.

Вспомним таблицу истинности вентиля NOR: на выходе появляется 1 тогда и только тогда, когда на обоих входах нули. Если хотя бы на одном входе есть 1, на выходе гарантированно будет 0.

Соединим выход первого вентиля NOR со входом второго, а выход второго — со входом первого:

flowchart LR
    S["Вход S (Set / Установка)"] --> NOR1["Вентиль NOR 1"]
    R["Вход R (Reset / Сброс)"] --> NOR2["Вентиль NOR 2"]

    NOR1 --> NQ["Выход Не Q (инверсный)"]
    NOR2 --> Q["Выход Q"]

    NQ -.->|"Обратная связь"| NOR2
    Q -.->|"Обратная связь"| NOR1

    classDef gate fill:#1e293b,stroke:#6366f1,stroke-width:1px,color:#f8fafc;
    classDef io fill:#0f172a,stroke:#38bdf8,stroke-width:1px,color:#f8fafc;
    class NOR1,NOR2 gate;
    class S,R,Q,NQ io;
  

У SR-триггера есть два входа управления и два взаимодополняющих выхода:

  • Вход S (Set / Установка): приказ запомнить единицу.
  • Вход R (Reset / Сброс): приказ сбросить ячейку в ноль.
  • Выход Q: основное хранимое значение бита.
  • Выход Не Q ($\overline{Q}$): инвертированное значение (если Q = 1, то Не Q = 0, и наоборот).

Как работает сохранение бита по шагам

  1. Запись единицы (Set):
    Подадим на вход $S = 1$, а на $R = 0$.
    Обратите внимание: вентиль NOR 1, у которого есть вход $S$, выдает инверсный выход $\overline{Q}$, а вентиль NOR 2, у которого есть вход $R$, — основной выход $Q$. Давайте проследим:
    Если подать $S = 1$: выход первого вентиля становится 0. Этот 0 по проводу обратной связи летит на вход NOR 2. У NOR 2 на входе $R = 0$ и от обратной связи $0$. Оба входа равны 0, значит его выход становится 1! Это наш рабочий выход Q. Выход Q = 1 по второму проводу возвращается на NOR 1, подтверждая его ноль.
  2. Хранение (Latch):
    Теперь убираем сигнал со входа: $S = 0$, $R = 0$.
    Что происходит? Выход Q по-прежнему равен 1, и этот сигнал непрерывно поступает на вход NOR 1, удерживая его выход в 0. А этот 0 удерживает выход NOR 2 в состоянии 1.
    Схема запомнила бит! Внешнего воздействия больше нет ($S=0, R=0$), но ток циркулирует по замкнутому кольцу, бесконечно освежая собственное состояние.
  3. Сброс в ноль (Reset):
    Подаем $R = 1$ при $S = 0$.
    Выход Q немедленно падает в 0. Обратная связь передает этот 0 на NOR 1. На NOR 1 оба входа теперь нули ($S=0$ и feedback $0$), поэтому выход $\overline{Q}$ становится 1. Эта единица поступает на NOR 2, надежно блокируя Q в нуле.
    Снимаем сигнал сброса ($R=0$). Схема остается в стабильном состоянии Q = 0.
S (Set) R (Reset) Состояние Q Состояние Не Q Режим работы
0 0 $Q_{prev}$ $\overline{Q}_{prev}$ Хранение (Память) — держит предыдущий бит
1 0 1 0 Запись 1 (Set)
0 1 0 1 Запись 0 (Reset)
1 1 0 0 Запрещенное состояние (Неопределенность!)

Тактовый сигнал: Пульс процессора

Асинхронный SR-триггер прекрасен в лаборатории, но в реальном кристалле микропроцессора работают сотни миллионов таких ячеек, соединенных километрами кремниевых дорожек.

Сигналы не распространяются мгновенно. Из-за разной длины проводников, разного количества логических вентилей на пути сигнала и вариаций емкостей транзисторов сигналы приходят в разные блоки с разной задержкой. Во время вычислений в комбинационных блоках возникают временные паразитные всплески (так называемые глюки / glitches). Если бы ячейки памяти реагировали на входы непрерывно и хаотично, процессор моментально захлебнулся бы собственным шумом и испортил регистры.

Чтобы превратить хаос в гармонию, инженерам понадобился дирижер оркестра. Им стал тактовый сигнал (Clock, CLK).

flowchart TD
    subgraph ClockGen ["Генератор тактового сигнала"]
        Crystal["Кварцевый резонатор<br>(Пьезоэлектрический эффект)"] --> PLL["Фазовая автоподстройка (PLL)<br>Умножение частоты до 3-5 ГГц"]
    end

    PLL --> ClockTree["H-Tree распределение такта<br>(Синхронная доставка фронта;<br>у ядер и uncore свои домены частоты)"]
    ClockTree --> Core0["Ядро 0"]
    ClockTree --> Core1["Ядро 1"]
    ClockTree --> Cache["Кэш L3"]

    classDef clk fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc;
    class Crystal,PLL,ClockTree,Core0,Core1,Cache clk;
  

Как устроен тактовый генератор?

На материнской плате установлен крошечный герметичный металлический бочонок — кварцевый резонатор. Под действием электрического напряжения кристалл кварца деформируется и вибрирует с филигранной физической стабильностью (пьезоэлектрический эффект), генерируя базовый опорный пульс (например, $100\text{ МГц}$).

Внутри кристалла процессора специальная схема — PLL (Phase-Locked Loop, фазовая автоподстройка частоты) — умножает эту базовую частоту в 30–50 раз, превращая ее в заветные гигагерцы.

Сигнал Clock представляет собой непрерывный меандр идеальной формы, пульсирующий между нулем и единицей: 0-1-0-1-0-1.
Когда вы видите в характеристиках процессора частоту 4.0 GHz, это означает:
$$f = 4.0 \times 10^9 \text{ колебаний в секунду}$$
Длительность одного такта (Clock Cycle) составляет невообразимо малые 250 пикосекунд (0.25 наносекунды):
$$T = \frac{1}{f} = \frac{1}{4 \times 10^9} = 0.25 \times 10^{-9} \text{ с} = 250 \text{ пс}$$

За эти 250 пикосекунд свет в вакууме успевает пролететь всего $7.5\text{ сантиметров}$, а электрический сигнал по печатной плате (в диэлектрике) — лишь около $3\dots 4\text{ см}$; а на самом кристалле дальность ещё короче, потому что ее ограничивают RC-задержки проводников! Поэтому доставка тактового импульса ко всем миллиардам транзисторов синхронно без перекоса фаз (Clock Skew) — один из сложнейших шедевров наноинженерии (для этого создаются сбалансированные симметричные сетки разводки в форме буквы Н — H-tree clock networks).


D-триггер: Безопасная память

Теперь мы готовы создать главный рабочий кирпичик процессорной памяти — D-триггер (Data Flip-Flop).

Он решает сразу две ключевые проблемы базового SR-триггера:

  1. Исключает запрещенное состояние: вход данных всего один — D (Data). Сигналы Set и Reset формируются автоматически внутри схемы: на одну сторону подается D, на другую — инвертированный NOT D. Подать 1-1 физически невозможно!
  2. Синхронизируется с пульсом CPU: состояние захватывается строго по входу CLK (Clock).
sequenceDiagram
    autonumber
    participant CLK as Тактовый сигнал (Clock)
    participant D as Вход данных (D)
    participant Q as Выход памяти (Q)

    Note over CLK, Q: Режим покоя (CLK = 0). Выход Q удерживает старый бит.
    CLK->>D: 1. Вход D меняется хаотично (0 -> 1 -> 0)
    Note over Q: Выход Q игнорирует скачки на D!
    CLK->>Q: 2. ФРОНТ ТАКТА (Rising Edge: 0 -> 1)
    D-->>Q: 3. Мгновенный снимок: значение со входа D защелкивается в Q
    Note over CLK, Q: Такт продолжается (CLK = 1). Новые изменения D заблокированы.
    CLK->>Q: 4. Спад такта (Falling Edge: 1 -> 0)
    Note over Q: Хранилище закрыто до следующего такта.
  

Принцип работы Edge-Triggered (стробирование по фронту)

Современные D-триггеры срабатывают не просто по высокому уровню тактового сигнала, а по фронту (Edge-Triggered) — в кратчайший миг перехода тактового сигнала из 0 в 1 (Rising Edge / передний фронт).

Такой триггер можно представить как скоростной фотоаппарат с механическим затвором:

  • Пока затвор закрыт ($CLK = 0$), перед объективом ($D$) может происходить всё что угодно — бегать кошки, падать чашки. Матрица ничего не фиксирует.
  • В микросекунду щелчка затвора ($0 \to 1$) делается мгновенный снимок.
  • Затвор закрывается ($CLK = 1$ или $1 \to 0$), и сделанный кадр сохраняется в памяти ($Q$).

Временные параметры: Setup time и Hold time

Чтобы ячейка успела надежно захватить бит, входные данные должны подчиняться строгим правилам:

  • Setup Time ($t_{su}$): данные на входе D обязаны стать стабильными до прихода фронта такта (за несколько десятков пикосекунд).
  • Hold Time ($t_h$): данные на входе D обязаны оставаться неизменными после прихода фронта такта, чтобы триггер успел надежно захлопнуть внутреннюю петлю обратной связи.

Если комбинационная логика не успеет подготовить данные до наступления $t_{su}$ (например, сумматор ALU работал слишком медленно), триггер захватит промежуточный мусор или свалится в метастабильность. Полное условие для периода такта: $T \ge t_{clk\to Q} + t_{logic} + t_{su} + t_{skew}$ (время срабатывания триггера, задержка комбинационной логики, setup и перекос тактового сигнала); оно определяет максимально возможную тактовую частоту схемы. Нарушение hold-времени снижением частоты не лечится: оно не зависит от периода такта.


Программная аналогия

Если комбинационная логика из предыдущей статьи — это чистые математические функции без побочных эффектов, то D-триггер концептуально очень похож на потокобезопасную ячейку в Go, обновляемую только по сигналу «такта» (в нашем примере — вызову Tick):

Go
package main

import (
    "fmt"
    "sync"
)

// DFlipFlop эмулирует поведение аппаратного D-триггера
type DFlipFlop struct {
    mu    sync.RWMutex
    state bool // Хранимое состояние бита (выход Q)
}

// Tick эмулирует передний фронт тактового импульса (Rising Edge)
// В этот момент триггер мгновенно "фотографирует" вход D и защелкивает его в state.
func (d *DFlipFlop) Tick(data bool) {
    d.mu.Lock()
    defer d.mu.Unlock()
    d.state = data
}

// Read эмулирует чтение с выхода Q.
// В кремнии провод Q находится под потенциалом непрерывно, чтение не требует тактов!
func (d *DFlipFlop) Read() bool {
    d.mu.RLock()
    defer d.mu.RUnlock()
    return d.state
}

func main() {
    trigger := &DFlipFlop{}

    // Вход D колеблется
    trigger.Tick(true) // Поступил фронт такта: сохранили 1
    fmt.Printf("Выход Q после такта 1: %v\n", trigger.Read()) // true

    // Между тактами на вход D можно подавать что угодно,
    // но без вызова Tick() внутреннее состояние неизменно.
    fmt.Printf("Выход Q в середине такта: %v\n", trigger.Read()) // true

    trigger.Tick(false) // Следующий такт защелкнул 0
    fmt.Printf("Выход Q после такта 2: %v\n", trigger.Read()) // false
}

Mechanical Sympathy: SRAM vs DRAM

Для бэкенд-инженера понимание физики последовательностной логики — это не академический этюд. Оно дает четкий ответ на вопрос, почему оперативная память работает в сотни раз медленнее процессора, и почему структуры на стеке работают быстрее аллокаций в куче.

В современной компьютерной индустрии существует два фундаментально разных способа хранить биты информации:

flowchart TD
    subgraph MemoryHierarchy ["Архитектурная дилемма подсистемы памяти"]
        direction TB
        SRAM["<b>SRAM (Статическая память)</b><br>4–6 транзисторов на бит<br>Задержка: от ~1 такта (регистры) до десятков тактов (L1 ≈ 4–5, L2 ≈ 12–16, L3 ≈ 40–70)<br>Дорогая, горячая, объем в мегабайтах<br><i>Регистры CPU, кэши L1, L2, L3</i>"]
        DRAM["<b>DRAM (Динамическая память)</b><br>1 транзистор + 1 конденсатор на бит<br>Задержка: 50–100 нс (200–300 тактов)<br>Дешевая, плотная, объем в гигабайтах<br><i>Основная оперативная память (RAM)</i>"]
    end

    classDef sram fill:#1e293b,stroke:#10b981,stroke-width:1px,color:#f8fafc;
    classDef dram fill:#1e293b,stroke:#f59e0b,stroke-width:1px,color:#f8fafc;
    class SRAM sram;
    class DRAM dram;
  

1. SRAM (Static RAM — Статическая память)

Это память, построенная на базе тех самых триггеров с обратной связью. Классическая ячейка 6T SRAM состоит из 6 транзисторов: 4 транзистора образуют два перекрестно соединенных инвертора (бистабильное кольцо памяти), а 2 транзистора управляют чтением и записью.

  • Преимущества: Молниеносная скорость. Время переключения измеряется пикосекундами. Пока подается питание, заряд не утекает, бит хранится абсолютно стабильно без посторонней помощи.
  • Недостатки: 6 транзисторов на один-единственный бит! Это колоссальная площадь кремниевого кристалла, колоссальное энергопотребление и высокая цена. Поместить 64 гигабайта SRAM внутрь CPU невозможно — кристалл был бы размером с журнальный столик и плавился от тепла.
  • Где применяется: Процессорные регистры (RAX, RSP), а также кэши L1, L2 и L3. (Подробно их внутреннее устройство мы разберем в 18. Кэши CPU. L1, L2, L3 и Cache Line).

2. DRAM (Dynamic RAM — Динамическая память)

В 1968 году Роберт Деннард из IBM изобрел гениальный способ сэкономить кремний: ячейку 1T-1C. Вместо шести транзисторов на бит используется всего один транзистор и один микроскопический конденсатор.

  • Логическая 1 — конденсатор заряжен.
  • Логический 0 — конденсатор разряжен.
  • Преимущества: Феноменальная плотность упаковки. На одном кремниевом чипе можно разместить десятки миллиардов ячеек. Именно поэтому мы можем купить планку DDR5 на 32 или 64 ГБ по доступной цене.
  • Недостатки: Законы термодинамики неумолимы: микроскопические конденсаторы непрерывно разряжаются (ток утечки через p-n переход транзистора). Заряд бесследно растворяется за несколько десятков миллисекунд!
    Чтобы данные не превратились в кашу, контроллер памяти обязан в течение $32\dots 64\text{ миллисекунд}$ обновить все ячейки — процедура регенерации (DRAM Refresh). Делается это порциями: контроллер периодически (примерно каждые 7.8 мкс, tREFI, для DDR3/DDR4; у DDR5 интервал короче — около 3.9 мкс при обычной температуре) посылает команду REF, и микросхема обновляет очередную группу строк, считывая заряд и записывая его обратно. Пока идёт такой цикл (tRFC), обновляемый ранг/банк недоступен для обращений, а не весь чип целиком.
  • Где применяется: Основная оперативная память (RAM). (Мы детально изучим задержки и тайминги в 17. Пирамида памяти. Регистры, SRAM, DRAM и цена доступа).


Итог

  1. Комбинационная логика не имеет памяти: ее выходной сигнал существует только до тех пор, пока присутствует входной.
  2. Память в кремнии рождается из обратной связи: если подать выход логического вентиля обратно на его вход, система замыкается в стабильное состояние (бистабильная ячейка).
  3. SR-триггер — простейший элемент памяти из двух вентилей NOR или NAND (в NAND-варианте входы с активным нулём). Имеет опасное запрещенное состояние ($S=1, R=1$), способное привести к метастабильности и неопределенному поведению.
  4. Тактовый сигнал (Clock) — единый синхронизирующий метроном, генерируемый кварцевым резонатором и умножаемый схемой PLL. Он защищает процессор от переходного шума и паразитных импульсов.
  5. D-триггер со стробированием по фронту защелкивает бит данных строго в момент перепада Clock ($0 \to 1$), гарантируя надежное и безопасное сохранение состояния.
  6. SRAM (на триггерах) дает мгновенный доступ за доли наносекунды (кэши L1–L3), но требует 6 транзисторов на бит. DRAM (на конденсаторах) дешева и компактна, но требует постоянной регенерации и работает в сотни раз медленнее.

Теперь мы умеем не только складывать биты, но и надежно сохранять результаты в кремнии. Что будет, если объединить группу из 64 таких D-триггеров под общим тактовым проводом? Мы получим фундаментальную рабочую лошадку процессора — машинный регистр.

В следующей лекции мы научим кремний считать шаги алгоритмов и переключать состояния программ: 5. Регистры, счетчики и конечные автоматы.