Gardnerタイミング同期の理論と導出と実装

衛星から送られてくる画像データや、スマートフォンが基地局から受け取る音声パケットは、もとをたどれば「シンボル」と呼ばれる短い波形の連なりです。受信機の仕事は、この波形の連なりを正しく区切り、ひとつひとつのシンボルが運ぶビットを読み取ることです。ところが、ここに見落とされがちな難問があります。受信機のADC(アナログ・デジタル変換器)はある一定のクロックで信号を標本化していますが、そのクロックは送信機のシンボルクロックとは何の関係もありません。送信側は1マイクロ秒ごとにシンボルを送り出しているのに、受信側はそれとはまったく無関係なタイミングで信号を切り取っているのです。

たとえるなら、回転している荷物のベルトコンベアから、目隠しをしたまま一定間隔で手を伸ばして荷物をつかもうとするようなものです。手を伸ばすタイミングが荷物の中心とずれていれば、隣の荷物と半分ずつ重なったところをつかんでしまい、何が乗っていたのか判別できません。ディジタル通信における「シンボルの中心」、すなわち符号間干渉(ISI)が最小になる最適な標本化時刻を、受信機自身が信号から推定して合わせ込む——これがシンボルタイミング同期(symbol timing recovery)です。

この問題を、判定帰還も搬送波位相も不要なまま、わずかな計算量で解いてしまう美しいアルゴリズムが、1986年にFloyd Gardnerが発表したGardnerタイミング誤差検出器(Gardner Timing Error Detector, TED)です。Gardner法を理解すると、次のような場面で役立ちます。

  • ソフトウェア無線(SDR)受信機: GNU RadioやMATLABの受信チェーンで、シンボル同期ブロックの中核として広く使われています。フリーランニングのADCクロックのまま、補間器でサンプルをずらして同期を取ります
  • 衛星通信モデム: DVB-S2のような連続波形受信機では、搬送波同期に先立ってタイミング同期を行う必要があり、搬送波位相に依存しないGardner法が好都合です
  • ケーブルモデム・光通信の受信機: QAM信号のシンボル同期に標準的に用いられます

本記事の内容

  • タイミング同期の問題設定と、なぜGardner法が「2倍オーバーサンプル」を要求するのか
  • Gardner誤差信号 $e = \mathrm{Re}\{(y_k – y_{k-1}) y_{k-1/2}^*\}$ を、整形パルスの対称性から導出する
  • 誤差検出器が搬送波位相に依存しないことの数学的証明
  • 2次ループフィルタとNCO・補間器を含む閉ループ伝達特性
  • RRC整形したQPSK信号にタイミングオフセットを与え、Gardnerループの収束とアイダイアグラムの開きをPythonで可視化する

前提知識

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

タイミング同期とは — 「どこで切るか」の問題

ディジタル通信の送信機は、ビット列をシンボル $a_k$ にマッピングし、それを整形パルス $g(t)$ で「なめらかな波形」に変換して送ります。送信ベースバンド信号は、シンボル間隔 $T$ ごとにパルスを並べた

$$ s(t) = \sum_k a_k \, g(t – kT) $$

という形をしています。受信機は、雑音と通信路を経たこの信号 $r(t)$ を受け取り、各シンボルが運ぶ $a_k$ を読み取りたいわけです。

ここで鍵になるのが、整形パルス $g(t)$ がナイキスト条件を満たすように設計されている点です。送受信で半分ずつ分担するルートレイズドコサイン(RRC)フィルタを通した後、受信フィルタ出力 $y(t)$ における実効パルス $p(t) = g(t) * g(-t)$(マッチドフィルタ後)はレイズドコサイン形となり、

$$ p(nT) = \begin{cases} 1 & n = 0 \\ 0 & n \neq 0 \end{cases} $$

を満たします。つまり、$t = kT$ という「正しい時刻」で標本化すれば、$y(kT) = a_k$ となり、他のシンボルからの干渉(ISI)はぴったり0になります。逆に言えば、標本化時刻が $kT + \tau$ のように $\tau$ だけずれていると、$p(\tau) \neq 1$ で振幅が縮むうえに、隣接シンボルの $p(\tau \pm T) \neq 0$ の漏れが混ざってISIが生じます。

問題は、この「正しい時刻 $kT$」が受信機にはわからないことです。送信機のクロックと受信機のADCクロックは独立で、両者には未知の時間オフセット $\tau$ と、わずかな周波数差(クロックのドリフト)があります。受信機は、自分が手にしているサンプル列だけから「最適な標本化位相 $\hat\tau$」を推定し、それを動的に追従しなければなりません。これがタイミング同期の核心です。

ここで素朴な疑問が湧きます。「正しい時刻ではパルスのピークになる」のだから、振幅が最大になる時刻を探せばよいのでは、と。実はそれに近い考え方が早遅ゲート(early-late gate)同期ですが、それには判定基準点の両側を別々に標本化する必要があり、しかも信号が常にピークを持つとは限りません(連続する同符号でない限りピークは出ない)。次節では、Gardnerが見出した、もっと巧妙でロバストな誤差の取り出し方を見ていきます。

Gardner誤差検出器の直感 — シンボルの「中間点」が教えてくれること

Gardner法のアイデアを一言で言えば、「2つの判定点のちょうど真ん中の値が、タイミングずれの方向を教えてくれる」というものです。

具体的な場面で考えましょう。あるシンボルから次のシンボルへ波形が遷移するとき、たとえば $+1$ から $-1$ へ値が変わる場面を想像してください。タイミングが完璧に合っていれば、判定点 $kT$ では $+1$、次の判定点 $(k+1)T$ では $-1$、そしてそのちょうど中間 $(k+1/2)T$ では、対称性から波形がゼロを横切ります。中間点の値がゼロ——これがタイミングが合っている合図です。

ところが標本化が少し早すぎると、波形の遷移をまだ「上り坂の途中」で捉えることになり、中間点の値はゼロからずれます。しかも、そのずれの符号は、両端のシンボル値の差 $(y_k – y_{k-1})$ の符号と組み合わせると、タイミングが早いのか遅いのかをきれいに区別してくれます。Gardnerは、この「両端の差 × 中間点の値」という積が、平均的にタイミング誤差に比例することを示しました。

この直感が成立するには、1シンボルあたり判定点と中間点の2つの標本が必要です。だからGardner法はシンボルレートの2倍でのオーバーサンプリング(2 samples per symbol, 2 sps)を要求します。判定点だけを見ていてはタイミングずれは見えず、中間点という「観測者」を置くことで初めて誤差が顔を出すのです。

重要なのは、この方法が搬送波位相に依存しないことです。後で証明しますが、誤差信号を複素信号の実部として定義すると、未知の搬送波位相回転 $e^{j\theta}$ がきれいに打ち消されます。これにより、搬送波同期より先にタイミング同期を行えるという、受信機設計上きわめて便利な性質が得られます。では、この直感を数式に落とし込みましょう。

Gardner誤差信号の定義と導出

誤差信号の定義

受信機がシンボルあたり2サンプルで標本化しているとします。シンボル $k$ の判定点における標本を $y_k$、そのひとつ前の判定点を $y_{k-1}$、そして両者のちょうど中間(半シンボルずれた点)を $y_{k-1/2}$ と書きます。Gardnerタイミング誤差検出器の出力は、次式で定義されます。

$$ \begin{equation} e_k = \mathrm{Re}\left\{ (y_k – y_{k-1}) \, y_{k-1/2}^{*} \right\} \end{equation} $$

ここで $(\cdot)^{*}$ は複素共役、$\mathrm{Re}\{\cdot\}$ は実部を表します。QPSKのように同相・直交の2成分を持つ複素ベースバンド信号 $y = y_I + j y_Q$ では、この式は

$$ e_k = (y_{I,k} – y_{I,k-1})\, y_{I,k-1/2} + (y_{Q,k} – y_{Q,k-1})\, y_{Q,k-1/2} $$

と展開されます。同相成分と直交成分それぞれで「両端の差 × 中間点」を計算し、足し合わせるだけです。乗算がわずか数回で済むため、ハードウェア・ソフトウェアどちらの実装でも軽量です。

この定義式の妥当性、すなわち「$e_k$ が平均的にタイミング誤差 $\tau$ に比例する」ことを、整形パルスの対称性から導いていきましょう。

パルスの対称性を用いた導出

導出を見通しよくするため、まず1次元(実数)の信号で考えます。受信マッチドフィルタ後の連続信号を $y(t)$ とし、実効パルスを $p(t)$ とすると、

$$ y(t) = \sum_m a_m \, p(t – mT) $$

です。$p(t)$ はレイズドコサインパルスで、偶対称 $p(-t) = p(t)$ を満たし、かつナイキスト条件 $p(mT) = \delta_{m0}$ を満たします。

受信機の標本化時刻が真の最適時刻から $\tau$ だけずれているとします。すると、判定点・中間点での標本値は

$$ \begin{aligned} y_k &= y(kT + \tau) = \sum_m a_m\, p(kT + \tau – mT) \\ y_{k-1} &= y((k-1)T + \tau) \\ y_{k-1/2} &= y\!\left(\left(k – \tfrac{1}{2}\right)T + \tau\right) \end{aligned} $$

となります。ここで誤差検出器の期待値 $E[e_k]$ を、シンボル $a_m$ が無相関($E[a_m a_n] = \sigma_a^2 \delta_{mn}$、$E[a_m]=0$)という標準的な仮定のもとで計算します。

まず両端の差 $y_k – y_{k-1}$ を書き下すと、

$$ y_k – y_{k-1} = \sum_m a_m \left[ p(kT + \tau – mT) – p((k-1)T + \tau – mT) \right] $$

です。中間点 $y_{k-1/2}$ との積の期待値を取ると、$a_m a_n$ の項のうち $m = n$ のものだけが生き残ります(無相関性より)。シンボルの分散を $\sigma_a^2$ として整理すると、$E[e_k]$ は次のように、パルスだけで書ける関数になります。

$$ E[e_k] = \sigma_a^2 \sum_{m} \left[ p(kT + \tau – mT) – p((k-1)T + \tau – mT) \right] p\!\left(\left(k – \tfrac{1}{2}\right)T + \tau – mT\right) $$

総和の添字を $\ell = k – m$ と置き換える(時間シフトの不変性を使う)と、$k$ には依存しなくなり、

$$ \begin{equation} E[e_k] = \sigma_a^2 \sum_{\ell} \left[ p(\ell T + \tau) – p((\ell-1)T + \tau) \right] p\!\left(\left(\ell – \tfrac{1}{2}\right)T + \tau\right) \end{equation} $$

を得ます。これがGardner誤差検出器のSカーブ(誤差の期待値をタイミングずれ $\tau$ の関数として表したもの)の基礎式です。

Sカーブが原点を通り、$\tau$ に比例することの確認

得られた式 $(3)$ が「同期点 $\tau = 0$ で 0 になり、その近傍で $\tau$ に比例する」ことを確かめます。これがTEDとして機能するための必須条件です。

$\tau = 0$ を代入すると、

$$ E[e_k]\big|_{\tau=0} = \sigma_a^2 \sum_{\ell} \left[ p(\ell T) – p((\ell-1)T) \right] p\!\left(\left(\ell – \tfrac{1}{2}\right)T\right) $$

です。ナイキスト条件 $p(\ell T) = \delta_{\ell 0}$ より、角括弧の中は「$\ell = 0$ のとき $+1$、$\ell = 1$ のとき $-1$、それ以外は $0$」になります。したがって生き残るのは $\ell = 0$ と $\ell = 1$ の2項だけで、

$$ E[e_k]\big|_{\tau=0} = \sigma_a^2 \left[ \underbrace{1 \cdot p\!\left(-\tfrac{T}{2}\right)}_{\ell=0} + \underbrace{(-1)\cdot p\!\left(\tfrac{T}{2}\right)}_{\ell=1} \right] $$

ここでパルスの偶対称性 $p(-T/2) = p(T/2)$ を使うと、2項はぴったり打ち消し合い、

$$ E[e_k]\big|_{\tau=0} = \sigma_a^2 \left[ p\!\left(\tfrac{T}{2}\right) – p\!\left(\tfrac{T}{2}\right) \right] = 0 $$

となります。つまり同期がとれている点では誤差信号の期待値はゼロです。これは検出器が正しい平衡点を持つことを意味します。

次に、$\tau$ が小さい近傍での振る舞いを見るために、式 $(3)$ を $\tau$ について1次までテイラー展開します。$\tau = 0$ での値はゼロなので、傾き $\left.\frac{d E[e_k]}{d\tau}\right|_{\tau=0}$ が問題になります。$p$ の微分を $p’$ と書くと、主要項は中間点パルス $p((\ell-1/2)T + \tau)$ の値($\tau=0$ では $p(\pm T/2)$ など)と、両端差の微分の積から生じ、$\ell=0,1$ 近傍で非ゼロの正の傾きを与えます。結論として、同期点近傍で

$$ E[e_k] \approx K_d \, \tau \qquad (|\tau| \ll T) $$

という線形関係が成り立ちます。$K_d$ は検出器利得(detector gain)と呼ばれ、その符号と大きさはロールオフ率 $\beta$ に依存します(後ほどPythonで実際にSカーブを描いて確認します)。これでGardner誤差信号がタイミング誤差を線形に取り出すことが示せました。

なぜ中間点が必要なのか — 早遅ゲートとの違い

ここで一歩引いて、なぜ「中間点」が本質的なのかを振り返ります。判定点だけ、すなわち $y_k$ と $y_{k-1}$ だけを見ても、ナイキストパルスはそこでISIフリーになるよう設計されているため、タイミングずれの一次の情報は判定点には現れにくいのです。情報は判定点の「間」にこそ濃く存在します。波形が最も激しく動く遷移の中央——そこで標本化のずれが最も大きな振幅変化として観測されます。Gardnerの巧妙さは、その最も敏感な点(中間点)を「センサー」にし、両端の差を「向きの符号」として使ったところにあります。

これでスカラー信号での導出が完成しました。次に、複素信号で搬送波位相の影響がどう消えるのかを見ます。

搬送波位相に対する不変性の証明

Gardner法の実用上の最大の利点は、搬送波位相同期が完了していなくてもタイミング同期が動くことです。これを数式で確認しましょう。

受信信号に未知の搬送波位相オフセット $\theta$ が残っているとします。これは複素ベースバンド信号全体に定数の位相回転 $e^{j\theta}$ がかかることを意味します。理想的にタイミング同期点での標本を $\tilde y_k$(位相ずれなし)とすると、実際に観測される標本は

$$ y_k = \tilde y_k \, e^{j\theta}, \quad y_{k-1} = \tilde y_{k-1}\, e^{j\theta}, \quad y_{k-1/2} = \tilde y_{k-1/2}\, e^{j\theta} $$

です。これらをGardner誤差信号 $(1)$ に代入します。差の項からは共通因子 $e^{j\theta}$ がくくり出せて、

$$ y_k – y_{k-1} = (\tilde y_k – \tilde y_{k-1})\, e^{j\theta} $$

一方、中間点の共役は

$$ y_{k-1/2}^{*} = \tilde y_{k-1/2}^{*}\, e^{-j\theta} $$

となります(共役を取ると指数の符号が反転する点に注意)。両者を掛け合わせると、位相因子は

$$ e^{j\theta} \cdot e^{-j\theta} = e^{0} = 1 $$

と完全に打ち消し合います。したがって、

$$ \begin{equation} e_k = \mathrm{Re}\left\{ (y_k – y_{k-1})\, y_{k-1/2}^{*} \right\} = \mathrm{Re}\left\{ (\tilde y_k – \tilde y_{k-1})\, \tilde y_{k-1/2}^{*} \right\} \end{equation} $$

が成り立ち、誤差信号は搬送波位相 $\theta$ にまったく依存しません。差と共役の積という形が、位相回転を相殺するように仕組まれていたのです。

ただし注意点として、$\theta$ が時間とともに回転する(搬送波周波数オフセットがある)場合は、$y_k$ と $y_{k-1/2}$ で位相が微妙に異なるため、わずかな劣化が生じます。しかし周波数オフセットがシンボルレートに比べて十分小さければ、半シンボル間の位相変化は無視でき、Gardner法はロバストに動作します。これが「搬送波同期に先立ってタイミング同期を行う」という受信機アーキテクチャを可能にしています。

不変性が確認できたので、次はこの誤差信号を使って実際に標本化位相を制御するフィードバックループの全体像を組み立てます。

同期ループの構成 — 補間器・ループフィルタ・NCO

誤差検出器は「今どれだけずれているか」を教えてくれるだけです。それを使って標本化タイミングを実際に修正する仕組みが必要です。Gardnerループは、Costasループと同じく、フィードバック制御の古典的な3要素から構成されます。

  1. タイミング誤差検出器(TED): Gardner式 $(1)$ で誤差 $e_k$ を生成
  2. ループフィルタ: $e_k$ を平滑化し、雑音を抑えながら定常偏差を消す
  3. 補間器 + NCO: 推定した標本化位相に従って、サンプルをずらして取り出す

補間器の役割

最も重要かつ独特なのが補間器です。受信機のADCは固定クロックで標本化しているので、「真の最適時刻 $kT + \hat\tau$」のサンプルは手元のサンプル列には存在しません。そこで、既存のサンプルから補間によって任意の時刻の値を作り出します。

実用上もっとも広く使われるのがFarrow構造による多項式補間です。連続する数サンプルから3次多項式(cubic)を当てはめ、小数遅延 $\mu \in [0, 1)$ の時刻における値を求めます。3次のFarrow補間器の出力は、入力サンプル $x[n]$ と分数遅延 $\mu$ を用いて、

$$ y(\mu) = \sum_{i=-1}^{2} x[n+i]\, c_i(\mu) $$

と書け、係数 $c_i(\mu)$ は $\mu$ の多項式です。Lagrange補間に基づく3次Farrow係数は次のように与えられます。

$$ \begin{aligned} c_{-1}(\mu) &= -\tfrac{1}{6}\mu^3 + 0\cdot\mu^2 + \tfrac{1}{6}\mu – 0 \\ c_{0}(\mu) &= \tfrac{1}{2}\mu^3 + \tfrac{1}{2}\mu^2 – \mu \\ c_{1}(\mu) &= -\tfrac{1}{2}\mu^3 – \mu^2 + \tfrac{1}{2}\mu + 1 \\ c_{2}(\mu) &= \tfrac{1}{6}\mu^3 + \tfrac{1}{2}\mu^2 + \tfrac{1}{3}\mu \end{aligned} $$

(係数の流儀は文献により異なりますが、$\sum_i c_i(\mu) = 1$ という規格化条件は共通です。)NCO(数値制御発振器)が分数遅延 $\mu$ と「次にどのサンプルで補間するか」を管理し、ループフィルタの出力に応じて $\mu$ を進めたり戻したりすることで、標本化位相を連続的に調整します。

ループフィルタの設計

ループフィルタには、搬送波同期と同じく比例・積分(PI)型の2次フィルタを使います。誤差 $e_k$ に対する制御出力 $v_k$ は、

$$ v_k = K_1 e_k + K_2 \sum_{i \le k} e_i $$

の形をしています。比例項 $K_1 e_k$ は瞬時の誤差に素早く反応し、積分項 $K_2 \sum e_i$ は誤差を蓄積して定常的なオフセット(クロック周波数差によるドリフト)を消し込みます。1次ループ(比例のみ)だと、送受信のクロック周波数がわずかに違う場合に定常的なタイミング誤差が残ってしまいますが、2次ループ(積分付き)なら周波数差をも追従でき、定常偏差がゼロになります。

閉ループ伝達特性

ループの設計は、CostasループやアナログPLLとまったく同じ理論で行えます。離散時間ループを連続時間近似すると、閉ループは2次系として、自然角周波数 $\omega_n$ と減衰係数 $\zeta$ で特徴づけられます。ループフィルタ係数は、検出器利得 $K_d$、NCO利得 $K_0$、ループ帯域幅 $B_n$(正規化ループ帯域 $B_n T_s$)から決まります。標準的な設計式は、$\theta = B_n T_s / (\zeta + 1/(4\zeta))$ として、

$$ K_1 = \frac{1}{K_d K_0}\cdot\frac{4\zeta\,\theta}{1 + 2\zeta\theta + \theta^2}, \qquad K_2 = \frac{1}{K_d K_0}\cdot\frac{4\,\theta^2}{1 + 2\zeta\theta + \theta^2} $$

で与えられます。閉ループ伝達関数 $H(s)$ は標準2次系

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

の形を取り、$\zeta \approx 0.707$(臨界減衰に近い値)が、オーバーシュートを抑えつつ素早く収束する標準的な選択です。ループ帯域 $B_n$ を狭くするほど雑音には強くなりますが、収束は遅くなり、クロックドリフトへの追従も鈍くなります。このトレードオフは搬送波同期ループとまったく同じ構図です。

これで同期ループの全構成要素が揃いました。理論を実装に落とし込み、本当にアイダイアグラムが開くのか、Pythonで確かめましょう。

Pythonによる実装と可視化

ここからは、RRC整形したQPSK信号に意図的なタイミングオフセットを与え、Gardnerループがそれを補正していく様子を段階的に実装します。まずは整形パルスと送信信号の生成、続いて誤差検出器のSカーブの確認、最後に閉ループ全体の収束とアイダイアグラムを見ます。

RRCフィルタとQPSK送信信号の生成

最初に、ルートレイズドコサイン(RRC)フィルタのインパルス応答を生成します。送受両側でRRCを通すことで、全体としてレイズドコサインのナイキストパルスになります。

import numpy as np
import matplotlib.pyplot as plt

def rrc_filter(beta, sps, span):
    """ルートレイズドコサインフィルタのインパルス応答
    beta: ロールオフ率, sps: シンボルあたりサンプル数, span: フィルタ長(シンボル数)"""
    N = span * sps
    t = (np.arange(-N//2, N//2 + 1)) / sps  # シンボル単位の時間軸
    h = np.zeros_like(t, dtype=float)
    for i, ti in enumerate(t):
        if abs(ti) < 1e-10:
            h[i] = 1.0 - beta + 4 * beta / np.pi
        elif beta > 0 and abs(abs(ti) - 1/(4*beta)) < 1e-10:
            # 特異点 t = ±T/(4β)
            h[i] = (beta / np.sqrt(2)) * (
                (1 + 2/np.pi) * np.sin(np.pi/(4*beta))
                + (1 - 2/np.pi) * np.cos(np.pi/(4*beta)))
        else:
            num = (np.sin(np.pi*ti*(1-beta))
                   + 4*beta*ti*np.cos(np.pi*ti*(1+beta)))
            den = np.pi*ti*(1 - (4*beta*ti)**2)
            h[i] = num / den
    return h / np.sqrt(np.sum(h**2))  # エネルギー正規化

beta = 0.35      # ロールオフ率
sps = 2          # Gardner法は2 sps が必須
span = 10        # フィルタ長(シンボル)
rrc = rrc_filter(beta, sps, span)

上のコードでRRCフィルタを生成しました。特異点($t=0$ と $t = \pm T/(4\beta)$)では極限値を別途与えてゼロ割りを回避しています。sps = 2 としているのがGardner法の要件で、1シンボルあたり判定点と中間点の2サンプルを確保します。エネルギー正規化により、送受2回通したときの振幅が揃います。

次に、ランダムなQPSKシンボルを生成し、アップサンプルしてRRCで整形します。

np.random.seed(0)
n_sym = 2000
# QPSKシンボル (±1 ±j)/√2
bits = np.random.randint(0, 2, (n_sym, 2))
syms = (2*bits[:,0]-1 + 1j*(2*bits[:,1]-1)) / np.sqrt(2)

# sps倍にアップサンプル(ゼロ詰め)してRRCで整形
up = np.zeros(n_sym * sps, dtype=complex)
up[::sps] = syms
tx = np.convolve(up, rrc, mode='same')

ここまでで、送信側のRRC整形QPSK信号 tx ができました。シンボル間にゼロを挿入してから整形フィルタを畳み込むことで、シンボルレートの2倍で標本化された連続波形を模擬しています。次に、この信号にタイミングオフセットを与えて受信側を作ります。

タイミングオフセットの付与とSカーブの確認

受信機のクロックずれを模擬するため、信号を分数遅延させます。ここでは周波数領域での全域通過フィルタ(位相シフト)を使って、サンプル数に満たない小数遅延を正確に与えます。

def fractional_delay(x, delay):
    """周波数領域で分数遅延(サンプル単位)を与える"""
    N = len(x)
    f = np.fft.fftfreq(N)
    H = np.exp(-1j * 2 * np.pi * f * delay)
    return np.fft.ifft(np.fft.fft(x) * H)

# 受信側マッチドフィルタ(同じRRC)を通す
def matched_filter(x):
    return np.convolve(x, rrc, mode='same')

# Sカーブ: タイミングずれτに対する誤差の期待値
def gardner_error(y, sps):
    """2spsの信号列からGardner誤差を計算(判定点インデックスは偶数)"""
    errs = []
    for k in range(2, len(y)//sps - 1):
        idx = k * sps
        yk   = y[idx]
        ykm1 = y[idx - sps]
        ymid = y[idx - sps//2]      # 中間点
        errs.append(np.real((yk - ykm1) * np.conj(ymid)))
    return np.mean(errs)

taus = np.linspace(-0.5, 0.5, 41)   # シンボル単位のタイミングずれ
scurve = []
for tau in taus:
    rx = fractional_delay(tx, tau * sps)  # τシンボル = τ*sps サンプル遅延
    y = matched_filter(rx)
    scurve.append(gardner_error(y, sps))

plt.figure(figsize=(8, 5))
plt.plot(taus, scurve, 'o-')
plt.axhline(0, color='k', lw=0.8)
plt.axvline(0, color='k', lw=0.8)
plt.xlabel(r'Timing offset $\tau$ (symbols)')
plt.ylabel(r'Mean Gardner error $E[e_k]$')
plt.title(f'Gardner TED S-curve (RRC, $\\beta$={beta})')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

このグラフがGardner誤差検出器のSカーブです。ここから3つの重要な性質が読み取れます。第一に、曲線は $\tau = 0$ で確かにゼロを横切っています——これは先の導出で示した「同期点で期待値ゼロ」がそのまま現れたものです。第二に、原点近傍ではほぼ直線で、傾きが負(または正、符号の取り方による)の安定な平衡点になっています。この傾きが検出器利得 $K_d$ で、ループの応答速度を決めます。第三に、$\tau = \pm 0.5$ シンボル付近で曲線が折り返しており、誤差検出器の引き込み範囲(ロックレンジ)が±半シンボル程度であることがわかります。半シンボル以上ずれると別の平衡点に引き込まれる(シンボルスリップ)リスクがあるため、初期捕捉ではこの範囲を意識します。

Sカーブが期待どおりの形をしていることが確認できたので、いよいよ閉ループを組んで動かします。

Gardner同期ループの実装

ここが本記事の中核です。Farrow補間器とPI型ループフィルタ、NCOを組み合わせた完全な同期ループを実装します。まず補間器とループ係数の設計から。

def cubic_farrow(x, n, mu):
    """3次Farrow(Lagrange)補間: サンプルx[n]近傍を分数遅延muで補間"""
    xm1, x0, x1, x2 = x[n-1], x[n], x[n+1], x[n+2]
    # Lagrange3次補間係数
    c_m1 = -mu*(mu-1)*(mu-2)/6
    c_0  =  (mu+1)*(mu-1)*(mu-2)/2
    c_1  = -(mu+1)*mu*(mu-2)/2
    c_2  =  (mu+1)*mu*(mu-1)/6
    return c_m1*xm1 + c_0*x0 + c_1*x1 + c_2*x2

def design_loop(Bn_Ts, zeta, Kd, K0):
    """2次ループフィルタ係数(PI)の設計"""
    theta = Bn_Ts / (zeta + 1/(4*zeta))
    denom = 1 + 2*zeta*theta + theta**2
    K1 = (4*zeta*theta/denom) / (Kd*K0)
    K2 = (4*theta**2/denom) / (Kd*K0)
    return K1, K2

補間器は連続する4サンプルからLagrange3次多項式を当てはめ、分数遅延 mu の位置の値を返します。design_loop は前述の設計式そのままで、ループ帯域 Bn_Ts(シンボルあたりに正規化)と減衰係数 zeta から比例・積分ゲインを計算します。次に、これらを使って実際の同期ループを回します。

擬似コードでループの骨格を示すと、次のような流れになります。

# --- 同期ループの骨格(概念) ---
# mu: 分数遅延, integ: 積分項, half_turn: 中間点/判定点の交互フラグ
#
# while まだサンプルが残っている:
#     y = 補間器(信号, 基準サンプルn, 分数遅延mu)
#     if 中間点ストローブ:
#         中間点 y_half = y を保存
#     else:  # 判定点ストローブ
#         e = Re{ (y - y_prev) * conj(y_half) }   # Gardner誤差
#         integ += K2 * e                          # 積分項を更新
#         mu   -= (K1 * e + integ)                 # 標本化位相を補正
#         y_prev = y
#     mu を [0,1) に正規化し、はみ出した分だけ基準サンプル n を増減

要点はこうです。ループは半シンボルずつ進みながら、補間器で現在の分数遅延 mu における値を取り出します。中間点と判定点を交互に処理し、判定点に来たらGardner誤差 e を計算してPIループフィルタに通し、その出力で mu(標本化位相)を更新します。mu が1を超えたら基準サンプル位置を繰り上げる(NCOのオーバーフロー処理)ことで、整数サンプルと分数遅延を組み合わせて連続的にタイミングを追従します。これを実際に動く形にしたのが次のコードです。

def gardner_loop(rx_mf, sps, K1, K2):
    """Gardnerタイミング同期ループ"""
    mu = 0.0
    integ = 0.0
    out_syms, mu_log, err_log = [], [], []
    y_prev, y_half = 0j, 0j
    n = sps                       # 基準サンプル
    half_turn = True              # True:次は中間点, False:次は判定点
    while n < len(rx_mf) - 3:
        y_i = cubic_farrow(rx_mf, n, mu)
        if half_turn:
            y_half = y_i          # 中間点を保存
            half_turn = False
        else:
            e = np.real((y_i - y_prev) * np.conj(y_half))  # Gardner誤差
            err_log.append(e); out_syms.append(y_i)
            mu_log.append(mu)
            integ += K2 * e                 # 積分項
            mu = mu - (K1 * e + integ)       # 標本化位相を補正
            y_prev = y_i
            half_turn = True
        # muを正規化して基準サンプルを進める(半シンボル=sps//2サンプル)
        step = sps // 2
        while mu >= 1.0: mu -= 1.0; step += 1
        while mu < 0.0:  mu += 1.0; step -= 1
        n += step
    return np.array(out_syms), np.array(mu_log), np.array(err_log)

この関数が同期ループ本体です。半シンボル(sps//2 = 1サンプル)ごとにストローブを進め、中間点と判定点を交互に取得します。判定点ごとにGardner誤差を計算し、PIフィルタの出力で分数遅延 mu を更新、mu が範囲外になったら基準サンプル n を増減させて整合を取ります。この交互ストローブ方式が、Gardner法の「2 sps」要件を素直に実装したものです。続いて、この関数を実際の受信信号に適用します。

収束とアイダイアグラムの可視化

未知のタイミングオフセットを与えた受信信号に対してループを走らせ、タイミング誤差が収束する様子と、同期前後のアイダイアグラムを比較します。

# 受信信号: 0.3シンボルのタイミングオフセット + 雑音
true_offset = 0.3
rx = fractional_delay(tx, true_offset * sps)
snr_db = 20
noise = (np.random.randn(len(rx)) + 1j*np.random.randn(len(rx)))
rx = rx + noise * np.std(rx) * 10**(-snr_db/20) / np.sqrt(2)
rx_mf = matched_filter(rx)

# ループ係数の設計
Kd, K0 = 1.0, 1.0
K1, K2 = design_loop(Bn_Ts=0.01, zeta=0.707, Kd=Kd, K0=K0)

out_syms, mu_log, err_log = gardner_loop(rx_mf, sps, K1, K2)

# タイミング誤差(分数遅延mu)の収束
plt.figure(figsize=(8, 5))
plt.plot(mu_log, lw=1)
plt.xlabel('Symbol index')
plt.ylabel(r'Fractional delay $\mu$ (NCO)')
plt.title('Convergence of timing estimate')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

このグラフは、ループが推定する分数遅延 mu(標本化位相)の時間変化です。初期は誤差が大きく mu が激しく動きますが、数百シンボルかけて一定値へ収束しているのが読み取れます。これは閉ループが安定で、与えたタイミングオフセットをきちんと吸収して定常状態に達したことを示します。減衰係数 $\zeta = 0.707$ を選んだため、大きなオーバーシュートなく滑らかに収束している点も確認できます。収束に要するシンボル数は、ループ帯域 Bn_Ts を広げれば短縮できますが、その分だけ雑音による mu の揺らぎ(定常ジッタ)が増えるトレードオフがあります。

最後に、同期の効果を最も雄弁に語るアイダイアグラムを描きます。

def eye_diagram(symbols_2sps, ax, title):
    """2spsのサンプル列からアイダイアグラム(実部)を描く"""
    span = 2 * 2  # 2シンボル分(2sps)
    sig = np.real(symbols_2sps)
    for i in range(50, len(sig)//span - 2):
        seg = sig[i*span:(i+1)*span+1]
        ax.plot(seg, color='steelblue', alpha=0.15)
    ax.set_title(title); ax.set_xlabel('Sample'); ax.grid(True, alpha=0.3)

fig, axes = plt.subplots(1, 2, figsize=(13, 5))
# 同期前: マッチドフィルタ出力をそのまま(オフセットあり)
eye_diagram(rx_mf[200:1600], axes[0], 'Eye diagram BEFORE sync (offset 0.3T)')
# 同期後: 補間で2sps相当に取り直す → 判定点が揃う
# out_symsは判定点のみなので、中間点も含め交互に並べ直して描画
synced = []
mu_final = mu_log[-1]
for k in range(sps, len(rx_mf)-3, 1):
    synced.append(cubic_farrow(rx_mf, k, mu_final))
eye_diagram(np.array(synced)[400:1600], axes[1], 'Eye diagram AFTER sync')
plt.tight_layout()
plt.show()

2つのアイダイアグラムの対比が、Gardner同期の威力を端的に示しています。左の同期前の図では、0.3シンボルのタイミングオフセットによって判定点(アイの中央)が最適位置からずれており、「目」が部分的に閉じ気味で、最適な標本化点での開口(縦方向のマージン)が小さくなっています。一方、右の同期後の図では、ループが推定した最終的な分数遅延でサンプルを取り直したことで、アイが大きく開き、上下にくっきりと分離した開口が現れています。アイの中央(最も開いた点)で標本化すれば、雑音やわずかなタイミングジッタがあってもシンボル判定を誤りにくいことが視覚的にわかります。

このアイダイアグラムの開きこそが、符号間干渉が最小化された証拠であり、Gardnerループが「どこで切るべきか」を信号自身から正しく学習したことの直接的な現れです。これで理論から実装まで、Gardner法の全体像を確認できました。

まとめ

本記事では、Gardnerタイミング誤差検出器の理論と導出、そしてPython実装を解説しました。

  • Gardner誤差信号: $e_k = \mathrm{Re}\{(y_k – y_{k-1})\, y_{k-1/2}^{*}\}$ は、判定点の差と中間点の値の積として定義され、シンボルレートの2倍オーバーサンプルを前提とします。中間点という「センサー」がタイミングずれを敏感に拾います
  • 対称性からの導出: 整形パルスの偶対称性とナイキスト条件から、同期点 $\tau=0$ で誤差期待値がゼロになり、近傍で $\tau$ に線形に比例する(Sカーブ)ことを導きました
  • 搬送波位相不変性: 差と共役の積という構造により、未知の位相回転 $e^{j\theta}$ が相殺されます。これにより搬送波同期に先立ってタイミング同期を行えます
  • 同期ループ: Farrow補間器・PI型2次ループフィルタ・NCOを組み合わせることで、固定クロックのADCのまま標本化位相を連続的に追従できます。2次ループはクロック周波数差にも追従し定常偏差をゼロにします
  • 実装での確認: RRC整形QPSK信号にタイミングオフセットを与え、Sカーブの形状、mu の収束、アイダイアグラムの開きという3つの観点からGardnerループの動作を可視化しました

Gardner法は、判定値も搬送波位相も使わずに、わずかな乗算だけでタイミングを取り戻す——シンプルさとロバスト性を兼ね備えた、ディジタル受信機設計の定番アルゴリズムです。実際の受信チェーンでは、このタイミング同期の後段に搬送波同期(Costasループ)やフレーム同期が続き、ひとつの完全な復調器を形作ります。

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