フリップフロップとレジスタ -- コンピュータはどのようにデータを「記憶」するか
組み合わせ論理から順序論理へ、Dフリップフロップとレジスタの動作原理を理解する
が最上位ビット、bit
あなたの手にカメラがあると想像してください。シャッターを押す前、外の世界は変化し続けています——歩行者が動き、葉が揺れ、光の明暗が入れ替わります。
しかし、シャッターを押した瞬間(クロックの立ち上がりエッジ)、現在の画面が写真に「ラッチ」されます。
その後、外の世界がどう変わっても、写真の内容はずっとその瞬間に留まります。これがDフリップフロップの核心的な動作です——クロック信号の立ち上がりエッジでデータを捕捉して保持します。
組み合わせ論理 vs 順序論理
これまでの講義で学んだANDゲート、ORゲート、NOTゲートなどの回路は、すべて組み合わせ論理に属します。それらの出力は現在の入力だけで決まります——入力が変われば、出力もすぐに変わります(伝播遅延は無視)。
しかし、コンピュータがデータを「記憶」するには、入力が消えた後も出力を保持できる回路が必要です。それが順序論理。
核心的な違い
組み合わせ論理は電灯のようなものです——スイッチを押せば点灯し、離せば消えます。出力は完全に現在の入力に依存します。
順序論理は金庫のようなものです——ものを入れて鍵をかけ(クロックの立ち上がりエッジが到来)、その後、あなたが離れても(入力が消えても)、中身はそのまま残っています。
| 比較の観点 | 組み合わせ論理 | 順序論理 |
|---|---|---|
| 出力の依存 | 現在の入力のみに依存 | 現在の入力と以前の状態に依存 |
| 記憶能力 | なし(ステートレス) | あり(履歴を記憶できる) |
| 代表的な回路 | ANDゲート、ORゲート、NOTゲート、加算器 | Dフリップフロップ、レジスタ、カウンタ |
| クロック依存 | クロックに依存しない | クロック信号に依存して状態更新を同期する |
| 帰還回路 | 帰還なし(ループなし) | 状態を保持するための帰還回路あり |
Dフリップフロップの構造と動作
Dフリップフロップ(D Flip-Flop)は最も基本的な順序論理ユニットです。2つの入力と1つの出力があります:
D(Data):データ入力端子。保存する値(0または1)を表します。
CLK(Clock):クロック入力端子。いつデータをラッチするかを制御します。
Q:データ出力端子。現在保存されている値を出力します。
重要な動作:Dフリップフロップはクロック信号の立ち上がりエッジ(0から1へ遷移する瞬間)にDの値をQにラッチします。それ以外の時間は、Dがどう変わってもQは変わりません。
これは、カメラがシャッターを押した瞬間だけを記録し、他の時間は外の画面がどう変わっても写真に影響しないのと同じです。
なぜクロックが必要なのか?
クロック信号がなければ、Dフリップフロップの出力は入力に応じて絶えず変化し、また別の組み合わせ論理回路になってしまいます。「記憶」は語れなくなります。
クロック信号は同期の拍子(タイミング)を提供し、すべてのフリップフロップが統一された時点で状態を更新できるようにします。コンピュータシステム全体はこの拍子に合わせて協調動作しています。
レジスタ:複数のフリップフロップの並列接続
1つのDフリップフロップは1ビットのデータしか保存できません。1バイト(8ビット)や1ワード(32ビット)を保存するには、複数のフリップフロップを並列に接続する必要があります。
N個のDフリップフロップを並列接続し、それらに同じクロック信号を共有させることで、Nビットのレジスタ(Register)。
クロックの立ち上がりエッジが来ると、すべてのフリップフロップが同時にそれぞれのD端子の値をラッチし、一度に完全なNビットデータが保存されます。
多段レジスタとパイプライン
CPU内部では、レジスタの出力は次の段の組み合わせ論理に接続され、さらに次の段のレジスタに接続されることがよくあります——レジスタ-組み合わせ論理-レジスタというパイプライン構造を形成します。
各クロックサイクルで、データはある段のレジスタから組み合わせ論理で計算され、次の立ち上がりエッジで次の段のレジスタに保存されます。これが現代のCPUパイプラインの基礎です。
コードデモ:Python による Dフリップフロップとレジスタのシミュレーション
以下では、PythonコードでDフリップフロップと4ビットレジスタの動作をシミュレーションします。特に立ち上がりエッジ検出のロジックに注目してください:clkが0から1へ遷移することを検出したときのみデータを捕捉します。
例
"""
Dフリップフロップ:クロックの立ち上がりエッジで入力データをラッチ
動作ルール:
- クロックが0から1へ遷移する(立ち上がりエッジ)とき、D入力をQ出力にラッチする
- それ以外の時間(clk=0、またはclk=1で遷移でないとき)、Qは元の値を保持する
"""
def __init__(self, name="DFF"):
self.q = 0 # 出力Q、初期値は0
self.prev_clk = 0 # 前の時刻のクロック値。立ち上がりエッジ検出に使用
self.name = name
def update(self, d, clk):
"""
フリップフロップの状態を更新
パラメータ:
d: データ入力 (0 または 1)
clk: クロック信号 (0 または 1)
戻り値:
現在のQ出力値
立ち上がりエッジ検出:prev_clk == 0 かつ clk == 1
この条件を満たす場合のみ、dの値をqにラッチする
"""
# === 立ち上がりエッジ検出 ===
if self.prev_clk == 0 and clk == 1:
self.q = d # 立ち上がりエッジでDの値をQにラッチ
self.prev_clk = clk # 現在のクロック状態を記録し、次のサイクルで使用する
return self.q
def reset(self):
"""フリップフロップの状態をリセット"""
self.q = 0
self.prev_clk = 0
class Register4Bit:
"""
4ビットレジスタ:4つのDフリップフロップを並列接続し、同じクロック信号を共有
構造の概要:
D3 ──▶ [DFF3] ──▶ Q3 (上位ビット)
D2 ──▶ [DFF2] ──▶ Q2
D1 ──▶ [DFF1] ──▶ Q1
D0 ──▶ [DFF0] ──▶ Q0 (下位ビット)
↑↑↑↑ 同じCLKを共有
クロックの立ち上がりエッジが到達すると、4つのフリップフロップが同時にそれぞれのD入力をラッチする
"""
def __init__(self):
# 4つのフリップフロップを作成。bit
self.flip_flops = [
DFlipFlop("bit3"),
DFlipFlop("bit2"),
DFlipFlop("bit1"),
DFlipFlop("bit0"),
]
def update(self, data, clk):
"""
レジスタを更新
パラメータ:
data: 保存するデータ (0〜15の整数。範囲外は切り捨て)
clk: クロック信号 (0 または 1)
戻り値:
レジスタに現在保存されている値 (0〜15)
"""
data = data & 0b1111 # 4ビットだけが有効であることを確認
# ビットごとに対応するフリップフロップに渡す。上位から順に
for i in range(4):
# 上位から下位へ各ビットを取り出す:bit3, bit2, bit1, bit0
bit = (data >> (3 - i)) & 1
self.flip_flops[i].update(bit, clk)
return self.read()
def read(self):
"""レジスタに現在保存されている値を読み出す(状態は変更しない)"""
value = 0
for ff in self.flip_flops:
value = (value << 1) | ff.q
return value
def reset(self):
"""すべてのフリップフロップをリセット"""
for ff in self.flip_flops:
ff.reset()
# ============================================================
# シミュレーションデモ:複数のクロックサイクルでのレジスタの動作
# ============================================================
if __name__ == "__main__":
reg = Register4Bit()
# テストデータ系列:異なるクロックサイクルで異なるデータを書き込むシミュレーション
test_data = [5, 10, 15, 0, 7, 13, 3, 8, 1, 14]
# 5=0101, 10=1010, 15=1111, 0=0000, 7=0111
# 13=1101, 3=0011, 8=1000, 1=0001, 14=1110
print("=" * 55)
print(" サンプル - 4ビットレジスタシミュレーションデモ")
print(" 観察:データはクロックの立ち上がりエッジでのみレジスタにラッチされる")
print("=" * 55)
print(f"{'周期':<6} {'时钟':<10} {'输入数据 D':<12} {'寄存器值 Q':<12} {'说明'}")
print("-" * 55)
for cycle in range(10):
data = test_data[cycle]
# フェーズ1:クロックがローレベル (clk=0) のとき、レジスタ値は変わらないはず
reg.update(data, 0)
# フェーズ2:クロックの立ち上がりエッジ (clkが0→1)、データをラッチ
result = reg.update(data, 1)
data_bin = f"{data:04b}"
result_bin = f"{result:04b}"
note = "← 立ち上がりエッジでラッチ" if result == data else "状態は変化なし"
print(f"{cycle:<6} {'0→1':<10} {data}({data_bin}){'':<4} {result}({result_bin}){'':<4} {note}")
print("-" * 55)
print()
# ============================================================
# 追加デモ:データは立ち上がりエッジでのみラッチされ、他の時間は保持される
# ============================================================
print("=" * 55)
print(" デモ2:立ち上がりエッジ以外では、データの変化はラッチされない")
print("=" * 55)
reg2 = Register4Bit()
# まず初期値として5を書き込む
reg2.update(5, 0)
reg2.update(5, 1)
print(f"立ち上がりエッジで5を書き込み → レジスタ値: {reg2.read()}")
# clkがハイレベルのまま、データを10に変更——ラッチされるべきではない
reg2.update(10, 1)
print(f"clk=1 のときデータを10に変更 → レジスタ値: {reg2.read()} (まだ5のはず)")
# clkをローレベルに戻し、再度立ち上がりエッジで10を書き込む
reg2.update(10, 0)
result = reg2.update(10, 1)
print(fclk 0→1 時に 10 を書き込む → レジスタ値: {reg2.read()} (現在は 10))
print()
print("=" * 55)
print(デモ完了!サンプル - フリップフロップとレジスタの理解)
print("=" * 55)
インタラクティブデモ:Dフリップフロップのタイミング図
下記ではChart.js の stepped:trueオプションを使用して、クロック信号(方形波)とデータ信号のタイミング図を描画しています。赤い縦線は、各クロックの立ち上がりエッジ(CLK が 0 から 1 に変化する瞬間)を表しています。これらの瞬間こそ、Dフリップフロップがデータをラッチする重要な瞬間です。
Dフリップフロップのタイミング図 —— 立ち上がりエッジでのラッチデモ
Q出力は立ち上がりエッジでのみ更新され、それ以外の時間は変化しません。