PLL(位相同期回路)の理論 — 周波数合成と搬送波同期の仕組み

ラジオで放送局の周波数に「チューニング」する — この当たり前の操作の裏側には、電子回路がリアルタイムで周波数を追いかけ続ける精巧な仕組みが隠れています。受信した電波の微弱な信号から正確な搬送波周波数を再生し、数百 MHz の発振器をピタリと同期させる。この仕組みの核心が PLL(Phase-Locked Loop:位相同期回路) です。

PLL は「入力信号の位相に自分の発振器の位相を合わせ続ける」フィードバックシステムです。温度調節のサーモスタットが「設定温度と室温の差」を検出してエアコンを制御するように、PLL は「入力位相と出力位相の差」を検出して発振器の周波数を制御します。この単純なフィードバック原理から、驚くほど多様な応用が生まれます。

PLL を理解すると、以下のような幅広い応用に直結します。

  • 周波数合成器 — 1つの基準発振器(水晶発振器)から任意の周波数を高精度に生成。携帯電話の送受信、衛星通信の局部発振器
  • 搬送波同期 — 受信した変調信号から搬送波を再生。FM復調、BPSK/QPSK復調(Costas ループ)
  • クロック再生 — ディジタル通信でデータ信号からクロックを抽出。イーサネット、USB、シリアル通信
  • ジッタ低減 — クロック信号の時間的ゆらぎ(ジッタ)を抑制。高速ADC、FPGA のクロック分配

本記事の内容

  • PLL の基本構成(位相比較器、ループフィルタ、VCO)
  • 位相領域での線形モデルと伝達関数
  • ロックレンジとキャプチャレンジ
  • 2次PLL の設計(自然周波数と減衰係数)
  • 応用: 周波数合成(分周器PLL)
  • 応用: 搬送波同期(Costas ループ)
  • Python シミュレーション: 過渡応答、ロック過程の可視化

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

PLL の基本構成

3つの構成要素

PLL は次の3つの要素がフィードバックループを形成するシステムです。

  1. 位相比較器(PD: Phase Detector) — 入力信号と VCO 出力の位相差を検出する
  2. ループフィルタ(LF: Loop Filter) — 位相差信号を平滑化し、VCO の制御電圧を生成する
  3. 電圧制御発振器(VCO: Voltage-Controlled Oscillator) — 制御電圧に応じて発振周波数を変化させる

信号の流れを追ってみましょう。入力信号の位相を $\theta_i(t)$、VCO 出力の位相を $\theta_o(t)$ とします。

まず、位相比較器が入力と VCO 出力の 位相差 $\theta_e(t) = \theta_i(t) – \theta_o(t)$ に比例した電圧 $v_d(t)$ を出力します。

$$ v_d(t) = K_d \cdot g(\theta_e(t)) $$

ここで $K_d$ は位相比較器のゲイン [V/rad]、$g(\cdot)$ は位相差と出力の関係を表す特性関数です。最も単純なアナログ乗算器型の位相比較器では $g(\theta_e) = \sin(\theta_e)$ ですが、位相差が小さい範囲では $\sin(\theta_e) \approx \theta_e$ と線形近似できます。

次に、ループフィルタがこの電圧を平滑化して VCO の制御電圧 $v_c(t)$ を生成します。ループフィルタのインパルス応答を $f(t)$ とすると

$$ v_c(t) = v_d(t) * f(t) $$

です($*$ は畳み込み)。

最後に、VCO がこの制御電圧に応じて発振周波数を変化させます。VCO の瞬時角周波数は

$$ \omega_o(t) = \omega_{\text{free}} + K_v v_c(t) $$

です。ここで $\omega_{\text{free}}$ は VCO の自走周波数(制御電圧ゼロのときの発振周波数)、$K_v$ は VCO の感度 [rad/s/V] です。VCO の出力位相は角周波数の積分ですから

$$ \theta_o(t) = \omega_{\text{free}} t + K_v \int_0^t v_c(\tau) \, d\tau $$

となります。

フィードバックの直感的理解

PLL の動作を直感的に理解するために、次のシナリオを考えましょう。

VCO が自走周波数 $\omega_{\text{free}}$ で発振しているところに、わずかに周波数が高い入力信号 $\omega_i > \omega_{\text{free}}$ が入ってきたとします。

  1. 入力の位相が VCO より速く進むので、位相差 $\theta_e$ が徐々に増加する
  2. 位相比較器が正の電圧を出力する
  3. ループフィルタを通った制御電圧が VCO に印加される
  4. VCO の周波数が上昇し、入力周波数に近づく
  5. 周波数が一致すると位相差が一定値に落ち着き、定常状態(ロック状態)になる

このフィードバック機構により、PLL は入力信号の周波数と位相に「ロック(同期)」します。ロック状態では位相差 $\theta_e$ は一定(もしくはゼロに近い)値に保たれます。

ここまでで PLL の基本動作を直感的に把握しました。次に、この系を数学的に定式化し、線形制御理論の枠組みで解析しましょう。

線形モデルと伝達関数

位相領域での線形化

PLL の解析を容易にするために、位相差 $\theta_e$ が十分小さい場合の 線形モデル を導出します。

VCO の出力位相から自走成分 $\omega_{\text{free}} t$ を除いた「超過位相」を $\phi_o(t) = \theta_o(t) – \omega_{\text{free}} t$ と定義します。同様に、入力信号の超過位相を $\phi_i(t) = \theta_i(t) – \omega_{\text{free}} t$ とします。位相誤差は

$$ \phi_e(t) = \phi_i(t) – \phi_o(t) $$

です。位相比較器の出力は、線形近似($\sin(\phi_e) \approx \phi_e$)のもとで

$$ v_d(t) = K_d \phi_e(t) $$

VCO の超過位相は制御電圧の積分で

$$ \phi_o(t) = K_v \int_0^t v_c(\tau) \, d\tau $$

すなわち

$$ \dot{\phi}_o(t) = K_v v_c(t) $$

です。この関係をラプラス変換すると $s \Phi_o(s) = K_v V_c(s)$、つまり

$$ \Phi_o(s) = \frac{K_v}{s} V_c(s) $$

となります。VCO は位相領域では 積分器 $K_v / s$ として振る舞います。これは直感的にも納得できます — VCO は「周波数」を制御するデバイスであり、位相は周波数の積分だからです。

ブロック図と閉ループ伝達関数

位相領域でのブロック図を整理すると、フィードバック系の各要素は次のように表されます。

  • 位相比較器: $K_d$(ゲイン)
  • ループフィルタ: $F(s)$(伝達関数)
  • VCO: $K_v / s$(積分器)

開ループ伝達関数は

$$ G(s) = K_d F(s) \cdot \frac{K_v}{s} = \frac{K_d K_v F(s)}{s} $$

です。ここで $K_d K_v$ をまとめて $K = K_d K_v$ と書くことが多く、これを ループゲイン [rad/s] と呼びます。

閉ループ伝達関数(入力位相 $\Phi_i(s)$ から出力位相 $\Phi_o(s)$ への伝達関数)は

$$ H(s) = \frac{\Phi_o(s)}{\Phi_i(s)} = \frac{G(s)}{1 + G(s)} = \frac{K F(s)}{s + K F(s)} $$

となります。また、位相誤差の伝達関数は

$$ H_e(s) = \frac{\Phi_e(s)}{\Phi_i(s)} = 1 – H(s) = \frac{s}{s + K F(s)} $$

です。$H_e(s)$ は入力の位相変動がどれだけ位相誤差として残るかを表し、PLL の追従性能を評価する上で重要です。

これらの伝達関数はループフィルタ $F(s)$ の形で特性が大きく変わります。次に、代表的なループフィルタの設計と、対応する PLL の特性を見ていきましょう。

1次 PLL と 2次 PLL

1次 PLL(ループフィルタなし)

最も単純な構成は、ループフィルタを省略した($F(s) = 1$)1次 PLL です。閉ループ伝達関数は

$$ H(s) = \frac{K}{s + K} $$

です。これは時定数 $\tau = 1/K$ の1次ローパスフィルタであり、カットオフ周波数(ループ帯域幅)は $\omega_n = K$ [rad/s] です。

位相誤差の伝達関数は

$$ H_e(s) = \frac{s}{s + K} $$

で、ハイパスフィルタの形をしています。低周波の位相変動は追従できますが、$K$ より高い周波数の位相変動は追従できません。

1次 PLL の 定常位相誤差 を調べましょう。入力が一定の周波数オフセット $\Delta\omega$(位相が $\phi_i(t) = \Delta\omega \cdot t$ で変化)の場合、ラプラス変換は $\Phi_i(s) = \Delta\omega / s^2$ です。最終値定理を適用すると

$$ \phi_{e,\text{ss}} = \lim_{s \to 0} s \cdot H_e(s) \cdot \Phi_i(s) = \lim_{s \to 0} s \cdot \frac{s}{s + K} \cdot \frac{\Delta\omega}{s^2} = \frac{\Delta\omega}{K} $$

となります。つまり、1次 PLL は周波数ステップ入力に対して 有限の定常位相誤差 $\Delta\omega / K$ を持ちます。誤差を小さくするにはループゲイン $K$ を大きくする必要がありますが、ゲインを上げるとノイズ帯域も広がり、雑音が増えるというトレードオフがあります。

この限界を克服し、周波数ステップ入力に対して定常位相誤差をゼロにするには、ループフィルタに積分要素を含める必要があります。そこで登場するのが2次PLLです。

2次 PLL(PI型ループフィルタ)

実用的な PLL では、比例積分(PI)型 のループフィルタを使います。

$$ F(s) = 1 + \frac{\omega_z}{s} = \frac{s + \omega_z}{s} $$

ここで $\omega_z$ はフィルタのゼロ周波数です。このフィルタは制御工学の PI 制御器に相当し、積分項が定常誤差を除去します。

このとき、開ループ伝達関数は

$$ G(s) = \frac{K(s + \omega_z)}{s^2} $$

となり、$s = 0$ に2つの極があるため タイプII のループになります。閉ループ伝達関数は

$$ H(s) = \frac{K(s + \omega_z)}{s^2 + Ks + K\omega_z} $$

です。この分母を標準的な2次系の形 $s^2 + 2\zeta\omega_n s + \omega_n^2$ と比較すると

$$ \omega_n = \sqrt{K \omega_z}, \quad \zeta = \frac{1}{2}\sqrt{\frac{K}{\omega_z}} $$

が得られます。ここで

  • $\omega_n$: 自然周波数(natural frequency)[rad/s] — ループの応答速度を決める
  • $\zeta$: 減衰係数(damping factor)— 過渡応答の振動特性を決める

逆に、所望の $\omega_n$ と $\zeta$ を与えてループパラメータを設計する場合は

$$ K = 2\zeta\omega_n, \quad \omega_z = \frac{\omega_n}{2\zeta} $$

となります。

2次 PLL の定常誤差

2次 PLL(タイプII)の位相誤差伝達関数は

$$ H_e(s) = \frac{s^2}{s^2 + 2\zeta\omega_n s + \omega_n^2} $$

です。周波数ステップ入力 $\Phi_i(s) = \Delta\omega / s^2$ に対して最終値定理を適用すると

$$ \phi_{e,\text{ss}} = \lim_{s \to 0} s \cdot \frac{s^2}{s^2 + 2\zeta\omega_n s + \omega_n^2} \cdot \frac{\Delta\omega}{s^2} = 0 $$

となり、周波数ステップに対する定常位相誤差がゼロ になります。これは積分要素が周波数の定常偏差を吸収してくれるためです。

さらに、周波数ランプ入力(周波数が時間とともに線形に変化する場合)$\Phi_i(s) = R / s^3$($R$ は周波数変化率 [rad/s$^2$])に対しては

$$ \phi_{e,\text{ss}} = \lim_{s \to 0} s \cdot \frac{s^2}{s^2 + 2\zeta\omega_n s + \omega_n^2} \cdot \frac{R}{s^3} = \frac{R}{\omega_n^2} $$

となり、有限の定常誤差が残ります。周波数が変化し続ける信号(ドップラーシフトなど)を追従するには、$\omega_n$ を十分大きくする必要があります。

2次 PLL の過渡応答は減衰係数 $\zeta$ によって大きく変わります。次に、$\zeta$ の選び方と PLL のロックレンジ・キャプチャレンジについて解説します。

ロックレンジとキャプチャレンジ

ロックレンジ(Hold-in Range)

PLL がロック状態を維持できる入力周波数の範囲を ロックレンジ(lock range) または ホールドインレンジ(hold-in range) $\Delta\omega_L$ と呼びます。

アナログ乗算器型の位相比較器($g(\theta_e) = \sin(\theta_e)$)を使う場合、位相比較器の出力が飽和する条件から、ロックレンジは

$$ \Delta\omega_L = K_d K_v |F(0)| = K |F(0)| $$

で与えられます。PI型ループフィルタでは $F(0) \to \infty$(積分器を含む)なので、理論的にはロックレンジは無限大です。実際には VCO の調整範囲や位相比較器の動作範囲で制限されます。

ロックレンジは「既にロックしている PLL がロックを外さずにいられる周波数範囲」です。では、初期状態(ロックしていない状態)から PLL がロックを獲得できる周波数範囲はどうでしょうか?

キャプチャレンジ(Pull-in Range)

PLL が非ロック状態からロック状態に遷移できる入力周波数の範囲を キャプチャレンジ(capture range) または プルインレンジ(pull-in range) $\Delta\omega_C$ と呼びます。

一般に $\Delta\omega_C \leq \Delta\omega_L$ です。ロックレンジ内の全ての周波数にキャプチャできるわけではありません。非ロック状態ではビート信号(入力と VCO の周波数差による差周波信号)が発生し、ループフィルタがこの信号から DC 成分を抽出して VCO を徐々に引き込みます。

1次 PLL ではキャプチャレンジとロックレンジが等しく $\Delta\omega_C = \Delta\omega_L = K$ です。2次 PLL の場合は設計パラメータに依存しますが、一般的な近似として

$$ \Delta\omega_C \approx \sqrt{2\zeta\omega_n \cdot \Delta\omega_L} $$

が知られています。キャプチャレンジの外の周波数に対しては、PLL は自力ではロックを獲得できません。実用上は 周波数スイープ周波数プリセット を併用してキャプチャを助ける手法が使われます。

プルイン時間

キャプチャレンジ内の周波数にロックを獲得するまでの時間を プルイン時間(pull-in time) $T_p$ と呼びます。初期周波数オフセット $\Delta\omega_0$ に対する近似式は

$$ T_p \approx \frac{(\Delta\omega_0)^2}{2\zeta\omega_n^3} $$

です。初期オフセットが大きいほど、また自然周波数が小さいほど、ロック獲得に時間がかかることがわかります。

ここまでで PLL の基本的な理論と設計パラメータを整理しました。次に、これらの理論を実際の応用にどう結びつけるかを見ていきましょう。まずは最も重要な応用の一つ、周波数合成器です。

応用1: 周波数合成(分周器PLL)

なぜ周波数合成が必要なのか

無線通信では、送受信の局部発振器(ローカルオシレータ)として安定で正確な周波数源が必要です。水晶発振器は高い周波数安定度を持ちますが、固定周波数しか出せません。一方、VCO は広帯域に周波数を可変できますが、単体では精度が低く、温度変動やノイズに弱いです。

PLL を使えば、水晶発振器の高精度と VCO の可変性を両立できます。つまり「水晶発振器の精度で、任意の周波数を生成する」ことが可能になります。

分周器PLL の構成

最も基本的な周波数合成器は、PLL のフィードバック経路に 分周器(divider) を挿入した構成です。

VCO 出力の周波数 $f_{\text{out}}$ を $N$ で割った $f_{\text{out}}/N$ が位相比較器にフィードバックされます。基準周波数 $f_{\text{ref}}$(水晶発振器由来)と比較するため、ロック状態では

$$ \frac{f_{\text{out}}}{N} = f_{\text{ref}} \quad \Rightarrow \quad f_{\text{out}} = N \cdot f_{\text{ref}} $$

が成り立ちます。分周比 $N$ を変えるだけで、出力周波数を $f_{\text{ref}}$ の整数倍に設定できます。

例えば、基準周波数 $f_{\text{ref}} = 200$ kHz で分周比 $N = 4500$ とすれば、$f_{\text{out}} = 900$ MHz が得られます。$N = 4501$ に変えれば $f_{\text{out}} = 900.2$ MHz です。周波数のステップ幅は $f_{\text{ref}}$(この例では200 kHz)で決まります。

分周器PLL の位相雑音

分周器 PLL では、分周比 $N$ が大きいほど位相雑音が悪化します。入力位相雑音 $\mathcal{L}_{\text{ref}}(f)$ に対して、ループ帯域内の出力位相雑音は

$$ \mathcal{L}_{\text{out}}(f) = 20\log_{10}(N) + \mathcal{L}_{\text{ref}}(f) \quad [\text{dBc/Hz}] $$

のように $20\log_{10}(N)$ だけ劣化します。$N = 4500$ では $20\log_{10}(4500) \approx 73$ dB の劣化です。これは分周によってフィードバック信号の「位相分解能」が $1/N$ に粗くなるためです。

この位相雑音の劣化を低減するために、フラクショナル N PLLデルタシグマ変調 を用いた高度な周波数合成器が開発されています。

周波数合成器は PLL の最も普及した応用ですが、PLL のもう一つの重要な応用が通信における搬送波同期です。

応用2: 搬送波同期(Costas ループ)

BPSK 復調における搬送波同期問題

BPSK(二位相偏移変調)の受信信号は

$$ r(t) = d(t) \cos(\omega_c t + \theta) $$

です。ここで $d(t) \in \{+1, -1\}$ はデータ信号、$\omega_c$ は搬送波角周波数、$\theta$ は未知の搬送波位相です。

コヒーレント復調(同期検波)を行うには、受信側で $\cos(\omega_c t + \theta)$ を正確に再生する必要があります。しかし、通常の PLL を直接適用することはできません。なぜなら、BPSK 信号はデータ $d(t)$ による位相反転(0 と $\pi$ の切り替わり)を含むため、通常の位相比較器は位相反転のたびに極性が反転してしまい、正しく追従できないからです。

Costas ループの構成

Costas ループ は、この問題を巧みに解決する PLL の変形です。受信信号を同相成分(I)と直交成分(Q)に分離し、両者の積をとることでデータの影響を除去します。

受信信号 $r(t)$ と VCO 出力のI成分 $\cos(\omega_c t + \hat{\theta})$ およびQ成分 $-\sin(\omega_c t + \hat{\theta})$ を掛けて、低域通過フィルタ(LPF)を通すと

$$ \begin{cases} I = d(t) \cos(\theta – \hat{\theta}) \\ Q = d(t) \sin(\theta – \hat{\theta}) \end{cases} $$

が得られます(搬送波の2倍波成分は LPF で除去)。位相誤差 $\phi_e = \theta – \hat{\theta}$ が小さいとき、I は $d(t)$(データそのもの)に近く、Q は $d(t)\phi_e$ に近い値をとります。

ここで、I と Q の積を計算すると

$$ I \times Q = d(t)^2 \sin(\phi_e) \cos(\phi_e) = \frac{1}{2}\sin(2\phi_e) $$

$d(t)^2 = 1$ であるため、データの影響が完全に消える のがポイントです。この信号をループフィルタに通して VCO を制御すれば、搬送波位相に同期できます。

Costas ループの位相曖昧性

Costas ループの位相比較特性は $\sin(2\phi_e)$ であり、周期が $\pi$ です。つまり、$\phi_e = 0$ と $\phi_e = \pi$ を区別できません。これは 位相曖昧性(phase ambiguity) と呼ばれ、BPSK の場合は $\pi$ の位相曖昧性が生じます($d(t)$ と $-d(t)$ を区別できない)。

実用上は、データに 差動符号化(differential encoding) を施すことでこの曖昧性を解消します。差動符号化では、ビットの値そのものではなく、ビット間の変化(0→0, 1→1 は変化なし、0→1, 1→0 は変化あり)で情報を伝送するため、搬送波の絶対位相が $\pi$ ずれていても正しくデータを復元できます。

ここまでで PLL の理論と代表的な応用を解説しました。それでは Python シミュレーションで PLL の動作を可視化し、理論との一致を確認しましょう。

2次 PLL の減衰係数と過渡応答

2次 PLL の過渡応答は減衰係数 $\zeta$ によって大きく変わります。設計における $\zeta$ の選び方を理解するために、位相ステップ入力に対する応答を解析的に求めましょう。

閉ループ伝達関数

$$ H(s) = \frac{2\zeta\omega_n s + \omega_n^2}{s^2 + 2\zeta\omega_n s + \omega_n^2} $$

に対して、位相ステップ入力 $\Phi_i(s) = \Delta\phi / s$ を印加したときの出力位相は

$$ \Phi_o(s) = H(s) \cdot \frac{\Delta\phi}{s} $$

です。これをラプラス逆変換すると、$0 < \zeta < 1$(不足減衰)の場合

$$ \phi_o(t) = \Delta\phi \left[ 1 – \frac{e^{-\zeta\omega_n t}}{\sqrt{1 – \zeta^2}} \sin\left(\omega_n\sqrt{1-\zeta^2}\, t + \cos^{-1}(\zeta)\right) \right] $$

が得られます。この式から

  • $\zeta < 1$(不足減衰): 振動しながら最終値に収束。$\zeta$ が小さいほど振動が激しい
  • $\zeta = 1$(臨界減衰): 振動なしで最速収束
  • $\zeta > 1$(過減衰): 振動なしだが収束が遅い

実用的な PLL 設計では $\zeta = 0.707$($= 1/\sqrt{2}$)が広く使われます。これは2次バターワースフィルタの減衰係数に対応し、以下の良好な特性を持ちます。

  • オーバーシュートが約4.3% と適度に小さい
  • 整定時間が比較的短い
  • ループ帯域幅(3 dB帯域)が $\omega_n$ にほぼ等しい

$\zeta$ と過渡応答の関係は Python シミュレーションで視覚的に確認しましょう。

Python シミュレーション

2次 PLL の位相ステップ応答

まず、2次 PLL の位相ステップ応答を解析解に基づいて描画し、減衰係数 $\zeta$ の影響を確認します。

import numpy as np
import matplotlib.pyplot as plt

# 自然周波数
omega_n = 2 * np.pi * 100  # 100 Hz

# 時間軸
t = np.linspace(0, 0.05, 2000)

# 位相ステップの大きさ
delta_phi = 1.0  # [rad]

# 各zetaでのステップ応答
zetas = [0.3, 0.5, 0.707, 1.0, 1.5]

plt.figure(figsize=(10, 6))

for zeta in zetas:
    if zeta < 1.0:
        # 不足減衰
        omega_d = omega_n * np.sqrt(1 - zeta**2)
        phi_angle = np.arccos(zeta)
        phi_o = delta_phi * (1 - np.exp(-zeta * omega_n * t)
                / np.sqrt(1 - zeta**2) * np.sin(omega_d * t + phi_angle))
    elif zeta == 1.0:
        # 臨界減衰
        phi_o = delta_phi * (1 - (1 + omega_n * t) * np.exp(-omega_n * t))
    else:
        # 過減衰
        s1 = -zeta * omega_n + omega_n * np.sqrt(zeta**2 - 1)
        s2 = -zeta * omega_n - omega_n * np.sqrt(zeta**2 - 1)
        phi_o = delta_phi * (1 + (s1 * np.exp(s2 * t) - s2 * np.exp(s1 * t)) / (s2 - s1))

    plt.plot(t * 1000, phi_o, label=f'$\\zeta$ = {zeta}')

plt.axhline(y=delta_phi, color='k', linestyle='--', alpha=0.3, label='Final value')
plt.xlabel('Time [ms]')
plt.ylabel('Output Phase [rad]')
plt.title(f'2nd-order PLL Phase Step Response ($\\omega_n / 2\\pi$ = 100 Hz)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

上のグラフから、減衰係数 $\zeta$ が PLL の過渡応答に与える影響が明確に読み取れます。

  1. $\zeta = 0.3$ は大きなオーバーシュートと振動 — 最終値の約37%のオーバーシュートが発生し、振動が長く続きます。ロック後も位相が不安定に揺れるため、実用上は好ましくありません。
  2. $\zeta = 0.707$ は適度なオーバーシュートと速い整定 — オーバーシュートは約4%に抑えられ、1サイクル程度で整定します。多くの実用 PLL で採用される設計値です。
  3. $\zeta = 1.0$(臨界減衰)と $\zeta = 1.5$(過減衰)は振動なし — ただし、$\zeta = 1.5$ は収束が明らかに遅くなります。応答速度を犠牲にして安定性を優先する場合に選ばれます。

PLL の時間領域シミュレーション(非線形モデル)

次に、PLL の非線形動作を直接シミュレーションし、ロック獲得の過程を可視化します。正弦波の入力信号に対して VCO が周波数を追いかけていく様子をリアルタイムで計算します。

import numpy as np
import matplotlib.pyplot as plt

# PLLパラメータ
f_ref = 1000.0        # 入力周波数 [Hz]
f_free = 950.0        # VCO自走周波数 [Hz](入力より50Hz低い)
Kd = 1.0              # 位相比較器ゲイン [V/rad]
Kv = 2 * np.pi * 200  # VCO感度 [rad/s/V]

# 2次PLL(PI型ループフィルタ)の設計
omega_n = 2 * np.pi * 50   # 自然周波数 50 Hz
zeta = 0.707               # 減衰係数

# PIフィルタ係数の算出
# K = Kd * Kv, omega_n^2 = K * omega_z, 2*zeta*omega_n = K * 1 (比例項)
# ただしPI型: F(s) = a + b/s (a:比例, b:積分)
# 閉ループ: H(s) = K(as+b) / (s^2 + Kas + Kb)
# 比較: 2*zeta*omega_n = Ka, omega_n^2 = Kb
K = Kd * Kv
a = 2 * zeta * omega_n / K
b = omega_n**2 / K

# シミュレーション設定
fs = 100000.0         # サンプリング周波数
dt = 1.0 / fs
T = 0.1               # シミュレーション時間 [s]
N = int(T * fs)
t = np.arange(N) * dt

# 初期化
phase_ref = 0.0        # 入力位相
phase_vco = 0.0        # VCO位相
integrator = 0.0       # 積分器の状態
freq_vco = f_free      # VCO周波数

# 記録用配列
phase_error_log = np.zeros(N)
freq_vco_log = np.zeros(N)
vco_control_log = np.zeros(N)

omega_ref = 2 * np.pi * f_ref

for n in range(N):
    # 入力信号の位相(一定周波数)
    phase_ref = omega_ref * t[n]

    # 位相誤差
    phase_err = phase_ref - phase_vco

    # 位相比較器(正弦型: 非線形)
    pd_out = Kd * np.sin(phase_err)

    # PIループフィルタ
    integrator += b * pd_out * dt
    filter_out = a * pd_out + integrator

    # VCO
    freq_vco = f_free + Kv / (2 * np.pi) * filter_out
    phase_vco += 2 * np.pi * freq_vco * dt

    # 記録
    phase_error_log[n] = phase_err
    freq_vco_log[n] = freq_vco
    vco_control_log[n] = filter_out

# 位相誤差をラップ(-pi, pi)
phase_error_wrapped = np.mod(phase_error_log + np.pi, 2 * np.pi) - np.pi

# プロット
fig, axes = plt.subplots(3, 1, figsize=(12, 10), sharex=True)

# VCO周波数
axes[0].plot(t * 1000, freq_vco_log, 'b-', linewidth=0.8)
axes[0].axhline(y=f_ref, color='r', linestyle='--', alpha=0.7, label=f'Reference: {f_ref} Hz')
axes[0].axhline(y=f_free, color='g', linestyle=':', alpha=0.7, label=f'Free-running: {f_free} Hz')
axes[0].set_ylabel('VCO Frequency [Hz]')
axes[0].set_title('PLL Lock Acquisition Process')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# 位相誤差
axes[1].plot(t * 1000, phase_error_wrapped, 'b-', linewidth=0.8)
axes[1].axhline(y=0, color='r', linestyle='--', alpha=0.3)
axes[1].set_ylabel('Phase Error [rad]')
axes[1].set_ylim([-np.pi, np.pi])
axes[1].grid(True, alpha=0.3)

# 制御電圧
axes[2].plot(t * 1000, vco_control_log, 'b-', linewidth=0.8)
axes[2].set_ylabel('Control Voltage [V]')
axes[2].set_xlabel('Time [ms]')
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

上のグラフから、PLL のロック獲得過程が明確に観察できます。

  1. VCO 周波数(上段) — 初期値 950 Hz から基準周波数 1000 Hz に向かって収束しています。$\zeta = 0.707$ の設計通り、わずかなオーバーシュートの後に速やかに整定し、約30 ms でほぼロックが完了しています。自然周波数 $\omega_n/(2\pi) = 50$ Hz のPLLとして、整定時間は理論的に $t_s \approx 4/({\zeta\omega_n}) \approx 18$ ms 程度と予測され、シミュレーション結果と概ね一致しています。
  2. 位相誤差(中段) — 初期のビート信号(入力と VCO の周波数差による位相の周回)が観察され、周波数が近づくにつれて位相差が $2\pi$ を何度か通過した後、ゼロ付近に収束しています。これが非線形な「プルイン」過程であり、線形モデルの前提($\phi_e \ll 1$)が成立する前の動作です。
  3. 制御電圧(下段) — ロック後の定常的な制御電圧は、VCO を自走周波数から基準周波数にオフセットさせるための値に収束しています。PI フィルタの積分要素がこの定常オフセットを保持しています。

周波数ステップ応答

ロック状態から入力周波数が急変したときの追従特性を確認しましょう。

import numpy as np
import matplotlib.pyplot as plt

# PLLパラメータ(前と同じ)
f_ref_initial = 1000.0
f_ref_final = 1050.0     # 50 Hzの周波数ステップ
f_free = 1000.0           # VCO自走周波数を入力に一致させて初期化
Kd = 1.0
Kv = 2 * np.pi * 200
omega_n = 2 * np.pi * 50
zeta = 0.707
K = Kd * Kv
a_coeff = 2 * zeta * omega_n / K
b_coeff = omega_n**2 / K

# シミュレーション設定
fs = 100000.0
dt = 1.0 / fs
T = 0.15
N_sim = int(T * fs)
t = np.arange(N_sim) * dt

# 周波数ステップのタイミング
t_step = 0.05  # 50 ms でステップ

# 初期化(ロック状態から開始)
phase_vco = 0.0
integrator = 0.0

phase_error_log = np.zeros(N_sim)
freq_vco_log = np.zeros(N_sim)
freq_ref_log = np.zeros(N_sim)

for n in range(N_sim):
    # 入力周波数(ステップ変化)
    if t[n] < t_step:
        f_ref = f_ref_initial
    else:
        f_ref = f_ref_final

    freq_ref_log[n] = f_ref
    omega_ref = 2 * np.pi * f_ref

    # 入力位相
    if n == 0:
        phase_ref = 0.0
    else:
        phase_ref = phase_ref + omega_ref * dt

    # 位相誤差
    phase_err = phase_ref - phase_vco

    # 位相比較器
    pd_out = Kd * np.sin(phase_err)

    # PIループフィルタ
    integrator += b_coeff * pd_out * dt
    filter_out = a_coeff * pd_out + integrator

    # VCO
    freq_vco = f_free + Kv / (2 * np.pi) * filter_out
    phase_vco += 2 * np.pi * freq_vco * dt

    # 記録
    phase_error_log[n] = phase_err
    freq_vco_log[n] = freq_vco

# 位相誤差ラップ
phase_error_wrapped = np.mod(phase_error_log + np.pi, 2 * np.pi) - np.pi

fig, axes = plt.subplots(2, 1, figsize=(10, 7), sharex=True)

axes[0].plot(t * 1000, freq_vco_log, 'b-', linewidth=1, label='VCO freq')
axes[0].plot(t * 1000, freq_ref_log, 'r--', linewidth=1, label='Reference freq')
axes[0].axvline(x=t_step * 1000, color='gray', linestyle=':', alpha=0.5)
axes[0].set_ylabel('Frequency [Hz]')
axes[0].set_title('PLL Frequency Step Response (1000 Hz → 1050 Hz)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].plot(t * 1000, phase_error_wrapped, 'b-', linewidth=1)
axes[1].axvline(x=t_step * 1000, color='gray', linestyle=':', alpha=0.5)
axes[1].axhline(y=0, color='r', linestyle='--', alpha=0.3)
axes[1].set_ylabel('Phase Error [rad]')
axes[1].set_xlabel('Time [ms]')
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

上のグラフから、PLL の周波数ステップ応答が理論通りであることが確認できます。

  1. VCO 周波数(上段) — ステップ後、VCO は新しい入力周波数 1050 Hz にオーバーシュートしながら追従しています。$\zeta = 0.707$ の設計により、オーバーシュートは小さく(数 Hz 程度)、約20 ms で新しい周波数に整定しています。
  2. 位相誤差(下段) — ステップ直後に位相誤差が急増し、その後振動しながらゼロに収束しています。2次 PLL(タイプII)の特性として、周波数ステップに対する定常位相誤差がゼロ であることがシミュレーションでも確認できます。これは理論的な予測と一致します。

Costas ループによる BPSK 搬送波同期

最後に、Costas ループによる BPSK 信号の搬送波同期をシミュレーションします。

import numpy as np
import matplotlib.pyplot as plt

# パラメータ
f_carrier = 1000.0      # 搬送波周波数 [Hz]
f_data = 50.0            # データレート [bps]
f_vco_free = 998.0       # VCO自走周波数(わずかにオフセット)

# PLL設計
Kd = 2.0
Kv = 2 * np.pi * 150
omega_n = 2 * np.pi * 30
zeta = 0.707
K = Kd * Kv
a_coeff = 2 * zeta * omega_n / K
b_coeff = omega_n**2 / K

# シミュレーション設定
fs = 50000.0
dt = 1.0 / fs
T = 0.2
N_sim = int(T * fs)
t = np.arange(N_sim) * dt

# BPSKデータ生成
samples_per_bit = int(fs / f_data)
num_bits = int(T * f_data) + 1
data_bits = np.random.randint(0, 2, num_bits)
data_signal = np.zeros(N_sim)
for i in range(num_bits):
    start = i * samples_per_bit
    end = min((i + 1) * samples_per_bit, N_sim)
    data_signal[start:end] = 2 * data_bits[i] - 1  # +1 or -1

# BPSK送信信号
omega_c = 2 * np.pi * f_carrier
tx_signal = data_signal * np.cos(omega_c * t + np.pi / 6)  # 位相オフセット30度

# Costasループ
phase_vco = 0.0
integrator = 0.0

phase_error_log = np.zeros(N_sim)
freq_vco_log = np.zeros(N_sim)
i_signal_log = np.zeros(N_sim)
q_signal_log = np.zeros(N_sim)

# 簡易LPFの状態
lpf_i = 0.0
lpf_q = 0.0
alpha_lpf = 2 * np.pi * 200 * dt  # LPFカットオフ 200 Hz

for n in range(N_sim):
    # VCO出力(I, Q)
    vco_i = np.cos(2 * np.pi * f_vco_free * t[n] + phase_vco)
    vco_q = -np.sin(2 * np.pi * f_vco_free * t[n] + phase_vco)

    # ミキサ
    mix_i = tx_signal[n] * vco_i * 2  # x2 for amplitude normalization
    mix_q = tx_signal[n] * vco_q * 2

    # 簡易1次LPF(搬送波の2倍波を除去)
    lpf_i += alpha_lpf * (mix_i - lpf_i)
    lpf_q += alpha_lpf * (mix_q - lpf_q)

    # Costas位相検出: I * Q
    pd_out = lpf_i * lpf_q

    # PIループフィルタ
    integrator += b_coeff * pd_out * dt
    filter_out = a_coeff * pd_out + integrator

    # VCO位相更新
    phase_vco += Kv * filter_out * dt

    # VCO周波数の推定
    freq_vco = f_vco_free + Kv / (2 * np.pi) * filter_out

    # 記録
    freq_vco_log[n] = freq_vco
    i_signal_log[n] = lpf_i
    q_signal_log[n] = lpf_q

    # 位相誤差(参照用)
    ref_phase = omega_c * t[n] + np.pi / 6
    vco_phase = 2 * np.pi * f_vco_free * t[n] + phase_vco
    phase_error_log[n] = ref_phase - vco_phase

phase_error_wrapped = np.mod(phase_error_log + np.pi, 2 * np.pi) - np.pi

fig, axes = plt.subplots(4, 1, figsize=(12, 12), sharex=True)

# VCO周波数
axes[0].plot(t * 1000, freq_vco_log, 'b-', linewidth=0.8)
axes[0].axhline(y=f_carrier, color='r', linestyle='--', alpha=0.7, label=f'Carrier: {f_carrier} Hz')
axes[0].set_ylabel('VCO Frequency [Hz]')
axes[0].set_title('Costas Loop: BPSK Carrier Synchronization')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# 位相誤差
axes[1].plot(t * 1000, phase_error_wrapped, 'b-', linewidth=0.8)
axes[1].axhline(y=0, color='r', linestyle='--', alpha=0.3)
axes[1].set_ylabel('Phase Error [rad]')
axes[1].grid(True, alpha=0.3)

# I信号(復調データ)
axes[2].plot(t * 1000, i_signal_log, 'b-', linewidth=0.8)
axes[2].plot(t * 1000, data_signal * 0.8, 'r--', linewidth=0.5, alpha=0.5, label='Original data')
axes[2].set_ylabel('I-channel (demod data)')
axes[2].legend()
axes[2].grid(True, alpha=0.3)

# Q信号
axes[3].plot(t * 1000, q_signal_log, 'b-', linewidth=0.8)
axes[3].set_ylabel('Q-channel')
axes[3].set_xlabel('Time [ms]')
axes[3].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

上のグラフから、Costas ループによる BPSK 搬送波同期の過程が観察できます。

  1. VCO 周波数(1段目) — 初期の自走周波数 998 Hz から搬送波周波数 1000 Hz に収束しています。約50 ms でロックが完了し、データの位相反転(BPSK のシンボル境界)が存在しても安定にロックを維持しています。
  2. 位相誤差(2段目) — ロック獲得後、位相誤差がゼロ付近に収束しています。BPSK のデータ反転時にも位相誤差が大きく乱れないのは、Costas ループの $I \times Q$ 演算によりデータの影響が除去されているためです。
  3. I チャネル信号(3段目) — ロック獲得後の I チャネルは、元のデータ信号(赤の点線)とよく一致しています。これは搬送波同期が正しく行われ、コヒーレント復調が成功していることを示します。ロック前の区間では振幅が不安定で、データを正しく復調できていません。
  4. Q チャネル信号(4段目) — ロック後の Q チャネルはゼロ付近に収束しています。Q チャネルがゼロになることは、VCO の位相が入力搬送波の位相と正確に同期していることの証拠です。

まとめ

本記事では、PLL(位相同期回路)の理論について解説しました。

  • PLL は位相比較器・ループフィルタ・VCO の3要素からなるフィードバック系で、入力信号の位相に VCO の位相を同期させる
  • 位相領域の線形モデルでは、VCO は積分器 $K_v/s$ として振る舞い、閉ループ伝達関数 $H(s) = KF(s)/(s + KF(s))$ で特性が記述される
  • 1次 PLL($F(s) = 1$)は構造が単純だが、周波数ステップに対して定常位相誤差 $\Delta\omega/K$ が残る
  • 2次 PLL(PI型フィルタ)は周波数ステップに対する定常誤差がゼロで、自然周波数 $\omega_n$ と減衰係数 $\zeta$ で特性を設計する。$\zeta = 0.707$ が実用的な最適値
  • ロックレンジは PLL がロックを維持できる周波数範囲、キャプチャレンジは非ロック状態からロックを獲得できる範囲であり、一般にキャプチャレンジの方が狭い
  • 周波数合成(分周器PLL)は基準周波数の $N$ 倍の周波数を高精度に生成する。ただし分周比が大きいと位相雑音が $20\log_{10}(N)$ dB 劣化する
  • Costas ループ は BPSK 信号からデータの影響を除去して搬送波を再生する。$I \times Q$ 演算により $d(t)^2 = 1$ となるのがポイント

PLL は無線通信の根幹を支える技術であり、現代の携帯電話、衛星通信、高速デジタルインタフェースのいずれにも不可欠です。デジタル実装の DPLL(Digital PLL)や ADPLL(All-Digital PLL)も広く使われており、FPGA や SoC 上でソフトウェア的に PLL を実現することも一般的です。

次のステップとして、以下の記事も参考にしてください。