4. Последовательностная логика. Учим кремний помнить
В предыдущей главе 3. Комбинационная логика. Учим кремний считать мы собрали из базовых логических вентилей сумматор и арифметико-логическое устройство (ALU). Это впечатляющие инженерные конструкции, но у них есть одно фатальное ограничение: полная амнезия.
Комбинационная логика подобна горному водопаду: пока на вход льется вода (подается напряжение), на выходе крутятся лопасти турбины (вычисляется результат). Но стоит перекрыть вентиль — и поток иссякает, не оставляя и следа. На выходе схемы значение мгновенно исчезает, как только исчезли входные сигналы.
Представьте, что вам предложили написать высоконагруженный бэкенд на Go, в котором строго запрещено использовать переменные в памяти — разрешены только чистые математические функции. Вы не можете выполнить элементарный инкремент счетчика запросов:
i++ // i = i + 1
Почему? Потому что операция i++ требует времени и последовательности:
- Прочитать текущее значение переменной
i. - Подать его в сумматор ALU и вычислить
i + 1. - Запомнить полученный результат в той же ячейке
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, и наоборот).
Как работает сохранение бита по шагам
- Запись единицы (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, подтверждая его ноль. - Хранение (Latch):
Теперь убираем сигнал со входа: $S = 0$, $R = 0$.
Что происходит? ВыходQпо-прежнему равен1, и этот сигнал непрерывно поступает на вход NOR 1, удерживая его выход в0. А этот0удерживает выход NOR 2 в состоянии1.
Схема запомнила бит! Внешнего воздействия больше нет ($S=0, R=0$), но ток циркулирует по замкнутому кольцу, бесконечно освежая собственное состояние. - Сброс в ноль (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-триггера:
- Исключает запрещенное состояние: вход данных всего один —
D(Data). Сигналы Set и Reset формируются автоматически внутри схемы: на одну сторону подаетсяD, на другую — инвертированныйNOT D. Подать1-1физически невозможно! - Синхронизируется с пульсом 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):
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 и цена доступа).
Итог
- Комбинационная логика не имеет памяти: ее выходной сигнал существует только до тех пор, пока присутствует входной.
- Память в кремнии рождается из обратной связи: если подать выход логического вентиля обратно на его вход, система замыкается в стабильное состояние (бистабильная ячейка).
- SR-триггер — простейший элемент памяти из двух вентилей NOR или NAND (в NAND-варианте входы с активным нулём). Имеет опасное запрещенное состояние ($S=1, R=1$), способное привести к метастабильности и неопределенному поведению.
- Тактовый сигнал (Clock) — единый синхронизирующий метроном, генерируемый кварцевым резонатором и умножаемый схемой PLL. Он защищает процессор от переходного шума и паразитных импульсов.
- D-триггер со стробированием по фронту защелкивает бит данных строго в момент перепада Clock ($0 \to 1$), гарантируя надежное и безопасное сохранение состояния.
- SRAM (на триггерах) дает мгновенный доступ за доли наносекунды (кэши L1–L3), но требует 6 транзисторов на бит. DRAM (на конденсаторах) дешева и компактна, но требует постоянной регенерации и работает в сотни раз медленнее.
Теперь мы умеем не только складывать биты, но и надежно сохранять результаты в кремнии. Что будет, если объединить группу из 64 таких D-триггеров под общим тактовым проводом? Мы получим фундаментальную рабочую лошадку процессора — машинный регистр.
В следующей лекции мы научим кремний считать шаги алгоритмов и переключать состояния программ: 5. Регистры, счетчики и конечные автоматы.