衛星から届いたディジタル信号を復調するとき、受信機は「いつサンプリングすればよいのか」という、一見すると当たり前すぎて見落とされがちな問題に直面します。送信機は一定間隔(シンボル周期 $T$)でシンボルを送り出しますが、受信機側の AD 変換器は送信機とは独立した自前のクロックでサンプリングします。両者のクロックは決して完全には一致しません。発振器の個体差で周波数がわずかにずれ、ケーブルや空間の伝搬遅延でタイミングの基準がずれます。その結果、受信機は「シンボルの中心」をピンポイントで叩くことができず、波形の途中の中途半端な時刻をサンプリングしてしまいます。
この「サンプリング時刻のずれ」は、復調性能を静かに、しかし確実に蝕みます。本来であれば隣のシンボルの影響を受けない最適なサンプリング点(ナイキスト点)があるのに、そこからずれた瞬間を読み取ると、隣接シンボルが漏れ込んでくる符号間干渉(ISI)が生じ、判定がぶれて誤り率が跳ね上がります。アイパターンを描けばこの劣化は一目瞭然で、ずれが大きいほど「目」が閉じていきます。受信機が自力で正しいサンプリング時刻を発見し、それに追従し続ける仕組み — それがシンボルタイミング同期(symbol timing recovery / clock recovery)です。
このタイミング同期を理解すると、以下のような場面で何が起きているのかが手に取るようにわかります。
- 衛星通信モデム: DVB-S2X や CCSDS の受信機は、シンボルレートが既知でも位相(サンプリングの瞬間)は未知です。全ディジタル受信機では Gardner 検出器と補間器でこれを推定します
- ソフトウェア無線(SDR): 受信機の ADC クロックと送信機のシンボルクロックは無関係なので、ソフトウェア上で分数遅延補間を行ってタイミングを合わせます。GNU Radio の
Symbol Syncブロックはまさにこの処理です - 光通信・有線通信: 高速シリアルリンクのクロックデータリカバリ(CDR)も、原理的には同じタイミング誤差検出とフィードバックループで構成されています
本記事の内容
- サンプリング点のずれがなぜ ISI と SNR 劣化を生むのか
- 古典的な早遅ゲート(early-late gate)タイミング誤差検出の原理
- Gardner タイミング誤差検出器(TED)が 1 シンボルあたり 2 サンプルで搬送波位相に依存せず誤差を出せる理由の導出
- 分数遅延を実現する Farrow 構造の補間器と、タイミング位相を刻む NCO
- ループフィルタによる二次 PLL 化と、その収束特性
- Python 実装: RRC 整形した BPSK にタイミングオフセットを与え、Gardner TED と補間でロックする過程の可視化、収束後のアイパターン回復
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
特に、レイズドコサインによるパルス整形と、最適なサンプリング点(ナイキスト点)でだけ ISI がゼロになるという性質は、本記事の出発点になります。整合フィルタの概念も、なぜシンボルの中心を狙うのかを理解する上で前提となります。
タイミングずれはなぜ問題なのか
最適サンプリング点という「狙うべき一点」
ディジタル通信では、送信したいシンボル列 $\{a_k\}$(たとえば BPSK なら $\pm 1$)を、パルス波形 $g(t)$ で整形して送ります。送信信号は次のように書けます。
$$ s(t) = \sum_k a_k \, g(t – kT) $$
ここで $T$ はシンボル周期です。受信側では整合フィルタを通した後の信号 $x(t)$ をサンプリングしますが、このとき重要なのがナイキスト条件です。整形パルスがレイズドコサインのようなナイキストパルスであれば、その全体応答 $p(t) = g \ast g_{\text{match}}(t)$ は、
$$ p(nT) = \begin{cases} 1 & (n = 0) \\ 0 & (n \neq 0) \end{cases} $$
を満たします。つまり、$t = kT$ という「シンボルの中心」でサンプリングすれば、そのシンボル $a_k$ だけが現れ、他のシンボル $a_{k’}$($k’ \neq k$)からの寄与は厳密にゼロになります。これが符号間干渉ゼロの状態です。
問題は、この「シンボルの中心」が受信機にとっては未知だということです。実際にサンプリングする時刻を $t = kT + \tau$ と書くと、$\tau$ は真の最適点からのずれ(タイミングオフセット)です。このとき得られるサンプル値は、
$$ x(kT + \tau) = a_k \, p(\tau) + \sum_{k’ \neq k} a_{k’} \, p\big((k – k’)T + \tau\big) $$
となります。第 1 項は本来欲しいシンボルですが、$p(\tau) < p(0) = 1$ なので振幅が小さくなり、SNR が落ちます。さらに第 2 項は、$\tau \neq 0$ のとき $p((k-k')T + \tau) \neq 0$ となるため、他シンボルが漏れ込む ISI 項として残ります。
ずれが大きいほどアイが閉じる
この劣化を直感的に表すのがアイパターンです。多数のシンボル波形を 1 シンボル区間で重ね描きすると、ISI のない最適点では波形が一点に収束し、上下に大きく開いた「目」ができます。この目の開き具合が、判定の余裕(ノイズマージン)に直結します。
タイミングがずれると、サンプリングする縦線の位置が目の中心から横にずれます。目は中心から離れるほど狭くなっていくので、同じノイズでも誤判定しやすくなります。極端な場合、シンボル境界(目が完全に閉じる点)でサンプリングしてしまうと、$\pm 1$ の区別がつかなくなり通信は破綻します。
したがって受信機の仕事は明確です — アイの中心、すなわち $\tau = 0$ となるサンプリング時刻を見つけ出し、クロックがわずかにドリフトしても追従し続けることです。ここで自然な疑問が生まれます。受信機は送信側のクロックを知らないのに、どうやって「今のサンプリングがずれている」ことを検知できるのでしょうか。次節では、その鍵となるタイミング誤差検出器の最も古典的な考え方を見ていきます。
早遅ゲート — タイミング誤差を測る最初のアイデア
「山の頂上」を探す問題に翻訳する
タイミング誤差をどう測るか。ヒントは、整合フィルタ出力の波形にあります。ナイキスト点 $\tau = 0$ では、所望シンボルの応答 $p(\tau)$ が最大値(ピーク)を取ります。つまり、正しいサンプリング時刻とは「整合フィルタ出力の山の頂上」に他なりません。
山の頂上を探す問題と考えれば、登山者が頂上を探すときと同じ戦略が使えます。今いる地点の少し手前(early)と少し先(late)の高さを測り、手前の方が高ければ「行き過ぎた」、先の方が高ければ「まだ手前だ」と判断するのです。
具体的には、現在の推定サンプリング時刻 $\hat{t}_k$ に対して、$\delta$ だけ早い点と遅い点で波形の大きさ(包絡線、すなわち振幅の絶対値や 2 乗)をサンプリングします。タイミング誤差検出量(TED 出力)$e_k$ を、
$$ e_k = \big| x(\hat{t}_k + \delta) \big|^2 – \big| x(\hat{t}_k – \delta) \big|^2 $$
と定義します。頂上は左右対称なので、ちょうど頂上にいれば early と late の高さが等しく $e_k = 0$ になります。頂上より手前(サンプリングが早すぎる)なら late 側が高く $e_k > 0$、行き過ぎなら $e_k < 0$ という具合に、誤差の符号と大きさが取り出せます。
早遅ゲートの限界
この早遅ゲートは古典的で直感的ですが、いくつかの弱点があります。第一に、early と late の 2 点を別々にサンプリングするため、1 シンボルあたり最低でも 2 つの追加サンプル(合計でかなりのオーバーサンプリング)か、もしくは 2 系統の補間が必要です。第二に、$\delta$ の選び方が性能に効き、整形パルスのロールオフに合わせた調整が要ります。
そこで登場するのが Gardner の検出器です。Gardner は「early と late を別に取る代わりに、シンボルとシンボルのちょうど中間点を使う」という巧妙な発想で、わずか 1 シンボルあたり 2 サンプルという最小のオーバーサンプリングで、しかも搬送波位相に依存しない形でタイミング誤差を取り出すことに成功しました。次節でその仕組みを丁寧に導出します。
Gardner タイミング誤差検出器の導出
中間点に注目する発想
Gardner(1986 年)のアイデアの核心は、シンボル点そのものではなく、隣り合うシンボルの中間(遷移点)の値に着目することです。
考えてみましょう。BPSK で $+1$ の次に $-1$ が来るとき、波形は $+$ 側から $-$ 側へと滑らかに遷移します。この遷移の途中、ちょうど 2 つのシンボルの真ん中($t = (k – 1/2)T$)の値はゼロを横切るはずです — タイミングが合っていればです。もし中間点の値がゼロからずれていたら、それはサンプリング位相が前後にずれている証拠になります。
これを定量化するため、1 シンボルあたり 2 サンプル(オーバーサンプリング率 $N = 2$)で受信信号を取り込むことにします。すると、$k$ 番目のシンボル点のサンプル $y[k]$ と、そのひとつ前のシンボル点 $y[k-1]$ の間に、ちょうど中間点のサンプル $y_{\text{mid}}[k] = y[k – 1/2]$ が存在します。
Gardner TED の定義式
Gardner タイミング誤差検出器の誤差量 $e_k$ は、実信号(または複素信号の実部・虚部それぞれ)に対して次のように定義されます。
$$ e_k = y_{\text{mid}}[k] \cdot \big( y[k] – y[k-1] \big) $$
複素信号(QPSK など)の場合は、実部と虚部の両方を含めて、
$$ e_k = \mathrm{Re}\Big\{ y_{\text{mid}}[k]^{*} \big( y[k] – y[k-1] \big) \Big\} $$
と書きます。ここで $y[k]$ はシンボル時刻のサンプル、$y_{\text{mid}}[k]$ はその直前の中間点(遷移点)のサンプルです。
この式が「なぜ」タイミング誤差を表すのか、直感的に読み解きましょう。$(y[k] – y[k-1])$ は、前後 2 つのシンボル点の差、すなわち波形がどちら向きにどれだけ遷移しているか(傾きに相当する量)を表します。これに中間点の値 $y_{\text{mid}}[k]$ を掛けています。
- タイミングが合っていれば、シンボルが遷移する場合($+1 \to -1$ など)、中間点はちょうどゼロ交差点なので $y_{\text{mid}}[k] \approx 0$ となり、$e_k \approx 0$ になります
- サンプリングが早すぎると、中間点のサンプルがゼロ交差より手前にずれ、遷移の符号と同じ向きに $y_{\text{mid}}[k]$ がずれます。すると $y_{\text{mid}}[k]$ と $(y[k] – y[k-1])$ の符号が逆になり、$e_k < 0$ になります
- サンプリングが遅すぎると逆に $e_k > 0$ になります
つまり $e_k$ の符号がタイミングのずれの方向を、大きさがずれの量を表すのです。シンボルが遷移しないとき($+1 \to +1$)は $y[k] – y[k-1] \approx 0$ となり誤差に寄与しませんが、これは問題ありません。複数シンボルにわたって平均すれば、遷移のあるシンボルだけが誤差情報を提供してくれます。
搬送波位相に依存しないことの証明
Gardner TED の最大の長所は、搬送波位相が未知のままでも動作することです。これは「タイミング同期を搬送波同期より先に行える」という、受信機設計上きわめて重要な性質です。これを式で確認しましょう。
受信信号が、ベースバンド信号 $r(t)$ に未知の搬送波位相 $\theta$ が残留した状態 $y(t) = r(t) e^{j\theta}$ だとします(搬送波周波数オフセットはゼロまたは十分小さいとして、位相 $\theta$ を定数とみなします)。このとき各サンプルは、
$$ y[k] = r[k] e^{j\theta}, \quad y[k-1] = r[k-1] e^{j\theta}, \quad y_{\text{mid}}[k] = r_{\text{mid}}[k] e^{j\theta} $$
となります。これを複素版の Gardner TED に代入します。
$$ e_k = \mathrm{Re}\Big\{ y_{\text{mid}}[k]^{*} \big( y[k] – y[k-1] \big) \Big\} $$
まず $y_{\text{mid}}[k]^{*} = r_{\text{mid}}[k]^{*} e^{-j\theta}$ であることに注意し、これと $(y[k] – y[k-1]) = (r[k] – r[k-1]) e^{j\theta}$ を掛け合わせます。
$$ y_{\text{mid}}[k]^{*} \big( y[k] – y[k-1] \big) = r_{\text{mid}}[k]^{*} e^{-j\theta} \cdot \big( r[k] – r[k-1] \big) e^{j\theta} $$
ここで指数因子 $e^{-j\theta} \cdot e^{j\theta} = e^{0} = 1$ となり、位相 $\theta$ がきれいに打ち消されます。
$$ y_{\text{mid}}[k]^{*} \big( y[k] – y[k-1] \big) = r_{\text{mid}}[k]^{*} \big( r[k] – r[k-1] \big) $$
したがって、
$$ e_k = \mathrm{Re}\Big\{ r_{\text{mid}}[k]^{*} \big( r[k] – r[k-1] \big) \Big\} $$
となり、誤差量 $e_k$ は搬送波位相 $\theta$ に一切依存しません。これが Gardner TED が「位相非依存」と呼ばれる理由です。早遅ゲートのように包絡線の 2 乗を取るのではなく、共役積の実部を取ることで、未知位相 $\theta$ を自動的にキャンセルしているのが巧妙な点です。
S 字曲線(S-curve)
TED の性能を特徴づける重要な概念が S 字曲線(S-curve) です。これは、タイミングオフセット $\tau$(最適点からのずれ、$T$ で規格化)に対する TED 出力の期待値 $g(\tau) = \mathbb{E}[e_k]$ をプロットしたものです。
理想的な S 字曲線は、$\tau = 0$ で原点を通り、その近傍で正の傾きを持つ単調増加の曲線です。原点近傍では、
$$ g(\tau) \approx K_d \, \tau $$
と線形近似でき、この傾き $K_d = \left. \frac{dg}{d\tau} \right|_{\tau=0}$ を 検出器利得(detector gain) と呼びます。$K_d$ はループの収束速度を決める重要なパラメータで、後でループフィルタの設計に使います。
S 字曲線が原点を横切る向き(正の傾き)こそが、フィードバック制御を可能にする鍵です。$\tau > 0$(遅れ)なら $g > 0$、$\tau < 0$(進み)なら $g < 0$ となるので、「$e_k$ が正ならサンプリングを早める、負なら遅らせる」という負帰還を構成すれば、$\tau$ は自然と $0$ に引き込まれます。
ここまでで、タイミング誤差を「測る」仕組みができました。しかし測っただけでは何も起きません。測った誤差をもとに、実際のサンプリング時刻を「動かす」必要があります。ところが ADC のサンプリング時刻は固定なので、ハードウェアを物理的にずらすわけにはいきません。次節では、固定サンプル列からあたかも任意の時刻でサンプリングしたかのような値を作り出す「補間器」を導入します。
分数遅延補間 — Farrow 構造
固定サンプルから任意時刻の値を作る
全ディジタル受信機では、ADC は受信機自前のクロックで等間隔にサンプリングします。このクロックは送信シンボルとは無関係なので、サンプル点は一般にシンボルの最適点からずれています。TED が「もう少し後ろの時刻の値が欲しい」と要求しても、その時刻にちょうどサンプルがあるとは限りません。
そこで、既存のサンプル列を使って、サンプル点と点の「間」の値を推定します。これが分数遅延補間(fractional delay interpolation) です。求めたい時刻が $k$ 番目のサンプルから $\mu$($0 \le \mu < 1$)だけ進んだ位置 $t = (k + \mu) T_s$($T_s$ はサンプル間隔)にあるとき、その値を周囲のサンプルから内挿します。
最も基本的な線形補間は、
$$ y(k + \mu) = (1 – \mu) \, y[k] + \mu \, y[k+1] $$
ですが、これは精度が低く、高速通信では波形をなまらせて性能を落とします。実用上は 3 次(キュービック)補間がよく使われ、4 点 $y[k-1], y[k], y[k+1], y[k+2]$ を使って、
$$ y(k + \mu) = \sum_{i=-1}^{2} c_i(\mu) \, y[k + i] $$
と表します。係数 $c_i(\mu)$ が分数遅延量 $\mu$ の多項式になっているのがポイントです。
Farrow 構造の利点
各補間係数 $c_i(\mu)$ を $\mu$ の多項式として書き、補間全体を「$\mu$ について次数ごとにまとめる」と、
$$ y(k + \mu) = \sum_{\ell=0}^{L} \mu^{\ell} \left( \sum_{i} b_{\ell, i} \, y[k+i] \right) = \sum_{\ell=0}^{L} \mu^{\ell} \, v_{\ell}[k] $$
という形に整理できます。ここで $v_{\ell}[k] = \sum_i b_{\ell, i} \, y[k+i]$ は、固定係数 $b_{\ell, i}$ を持つ FIR フィルタの出力です。この構造を Farrow 構造 と呼びます。
Farrow 構造の素晴らしさは、固定係数 FIR フィルタ群 $\{v_{\ell}[k]\}$ の計算と、可変パラメータ $\mu$ への依存を分離できる点です。$\mu$ が刻々と変化しても、フィルタ係数 $b_{\ell, i}$ は固定したまま、最後に $\mu$ の多項式(ホーナー法で効率的に評価)を掛けるだけで任意の分数遅延が実現できます。これにより、可変遅延フィルタをリアルタイムで再設計せずに済みます。
よく使われるのは Farrow 構造で実装した区分放物線補間(piecewise parabolic interpolator, $L=2$)で、調整パラメータ $\alpha$(典型的には $\alpha = 0.5$)を用いて、基準点 $y[k]$ から $\mu$ だけ進んだ位置を内挿する 3 本の FIR を次のように構成します。
$$ \begin{aligned} v_2[k] &= \alpha\, y[k+2] – \alpha\, y[k+1] – \alpha\, y[k] + \alpha\, y[k-1] \\ v_1[k] &= -\alpha\, y[k+2] + (1+\alpha)\, y[k+1] – (1-\alpha)\, y[k] – \alpha\, y[k-1] \\ v_0[k] &= y[k] \end{aligned} $$
最終的な補間出力は $\mu$ についてホーナー法でまとめて、
$$ y(k + \mu) = \big( v_2[k]\, \mu + v_1[k] \big)\, \mu + v_0[k] $$
と計算します。括弧で入れ子にすることで、乗算回数を最小化しています。
補間器によって任意の分数時刻 $\mu$ の値が手に入るようになりました。残る問題は「どれだけ補間すべきか」、すなわち $\mu$ をどう更新するかです。ここで、TED が出した誤差 $e_k$ を使って $\mu$ を制御するフィードバックループを組みます。次節では、その制御を担うループフィルタと NCO を見ていきます。
ループフィルタとNCO — タイミング同期を二次ループにする
NCO がタイミング位相を刻む
タイミングのサンプリング位相を管理するのが NCO(Numerically Controlled Oscillator、数値制御発振器) です。これは位相累積器であり、毎シンボルごとに一定量を引き算(または足し算)しながら、いつ次のサンプリングを行うか、そのときの分数遅延 $\mu$ がいくつかを決めます。
NCO は内部に位相 $\eta$ を持ち、毎入力サンプルごとに制御量 $W$ を引きます。
$$ \eta[n+1] = \eta[n] – W[n] $$
$\eta$ が $0$ を下回って桁あふれ(アンダーフロー)したとき、それが「シンボルをサンプリングすべきタイミング」の合図になります。このとき残った端数から分数遅延 $\mu$ が、
$$ \mu = \frac{\eta[n]}{W[n]} $$
として計算されます。制御量 $W$ の公称値は、サンプル周期に対するシンボル周期の比、すなわちオーバーサンプリング率 $N$ の逆数 $1/N$ です。ループがこの $W$ をわずかに増減させることで、サンプリングのタイミングを早めたり遅らせたりします。
なぜ二次ループが必要か
最も単純なループは、TED 誤差 $e_k$ に比例ゲイン $K_1$ を掛けて NCO 制御量に足し戻すだけの一次ループです。しかしこれには弱点があります。送受信のクロックには周波数のずれ(クロック周波数オフセット)があるのが普通で、一次ループだとこの一定の周波数ずれに対して定常的なタイミング誤差(定常偏差)が残ってしまいます。
これは制御工学の古典的な事実です。ランプ状の入力(一定速度でずれ続ける位相)に対して定常偏差をゼロにするには、ループ内に積分器をもう 1 つ持つ必要があります。そこで、比例項 $K_1$ に加えて積分項 $K_2$ を持つ PI(比例積分)ループフィルタを導入し、全体を二次ループにします。
ループフィルタの出力 $u[k]$ は、
$$ \begin{aligned} \text{積分器: } \quad & i[k] = i[k-1] + K_2 \, e_k \\ \text{フィルタ出力: } \quad & u[k] = K_1 \, e_k + i[k] \end{aligned} $$
この $u[k]$ を NCO の制御量 $W$ に反映させます。積分器 $i[k]$ がクロック周波数オフセットを「記憶」して打ち消すため、定常偏差がゼロになります。これは搬送波同期の二次 PLL とまったく同じ構造で、タイミング同期は「位相」がサンプリング時刻に置き換わっただけの位相同期ループ(PLL)として理解できます。
ループ帯域とダンピングからゲインを決める
PI ループのゲイン $K_1, K_2$ は、所望のループ帯域 $B_n T$(正規化雑音帯域)とダンピング係数 $\zeta$ から決められます。二次 PLL の標準的な設計式を用いると、まず補助変数 $\theta_n$ を、
$$ \theta_n = \frac{B_n T}{\zeta + \frac{1}{4\zeta}} $$
と置きます。次に、これを使って分母 $\Delta$ を、
$$ \Delta = 1 + 2\zeta\theta_n + \theta_n^2 $$
と定義します。比例ゲインと積分ゲインは、検出器利得 $K_d$ とNCO利得 $K_0$(通常 $K_0 = 1$)を含めて次のように与えられます。
$$ K_1 = \frac{1}{K_d K_0} \cdot \frac{4\zeta\theta_n}{\Delta}, \qquad K_2 = \frac{1}{K_d K_0} \cdot \frac{4\theta_n^2}{\Delta} $$
ここでダンピング係数は $\zeta = 1/\sqrt{2} \approx 0.707$(臨界制動に近い、オーバーシュートが小さく応答が速い設定)がよく選ばれます。ループ帯域 $B_n T$ は、小さいほど雑音に強い(タイミング推定値が安定)が引き込みが遅くなり、大きいほど速く引き込めるが雑音に弱いという、トレードオフを持つ設計パラメータです。典型的には $B_n T = 0.01 \sim 0.05$ 程度を選びます。
これでタイミング同期ループの全要素 — TED、補間器、ループフィルタ、NCO — がそろいました。次節では、これらをどう接続して 1 つのフィードバックループにするか、信号の流れを整理します。
タイミング同期ループの全体構成
フィードバックループの信号フロー
全ディジタルのタイミング同期ループは、次の順序でつながった閉ループになっています。
- ADC / オーバーサンプリング: 受信信号を 1 シンボルあたり $N$ サンプル(Gardner なら $N = 2$)で取り込む
- 整合フィルタ: RRC(ルートレイズドコサイン)整合フィルタを通し、SNR を最大化しつつナイキスト応答を得る
- 補間器(Farrow): 現在の分数遅延 $\mu$ を使って、シンボル点と中間点の補間値を計算する
- TED(Gardner): 補間されたシンボル点と中間点から誤差 $e_k$ を計算する
- ループフィルタ(PI): $e_k$ を比例積分してフィルタ出力 $u[k]$ を得る
- NCO: $u[k]$ で制御量 $W$ を調整し、次のサンプリングタイミングと分数遅延 $\mu$ を更新する
- 更新された $\mu$ が補間器に戻り、ループが閉じる
このループが回り続けることで、$\mu$ は徐々に正しい値に収束し、補間器が常にシンボルの最適点(とその中間点)を出力するようになります。一度ロックすれば、クロックのドリフトにも積分器が追従し続けます。
補間制御の 2 つの流派
タイミング同期ループには大きく 2 つの実装スタイルがあります。1 つは、固定の入力サンプル列に対して NCO で出力タイミングを生成し、必要なサンプルだけ補間する TED 主導の出力駆動型。もう 1 つは、簡便な実装として、ループ出力 $\mu$ で直接補間位置を更新していく方式です。本記事の Python 実装では、理解しやすさを優先し、各シンボルで $\mu$ を更新しながらシンボル点・中間点を補間する後者に近い構成を採ります。
それでは、ここまで導出してきた理論を実際に動かしてみましょう。RRC 整形した BPSK 信号にわざとタイミングオフセットを与え、Gardner ループがそれを引き込んでアイパターンを開かせる様子を Python で可視化します。
Pythonでの実装
送信信号の生成とタイミングオフセットの注入
まず、RRC(ルートレイズドコサイン)フィルタでパルス整形した BPSK 信号を作り、受信側でわざとサンプリング位相をずらした状況をシミュレートします。RRC フィルタのインパルス応答を実装します。
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(0)
def rrc_filter(beta, sps, span):
"""ルートレイズドコサイン(RRC)フィルタのインパルス応答
beta : ロールオフ率, sps : シンボルあたりサンプル数, span : 片側シンボル数"""
N = span * sps
t = np.arange(-N, N + 1) / sps # シンボル単位の時間
h = np.zeros_like(t)
for i, ti in enumerate(t):
if abs(ti) < 1e-8:
h[i] = 1.0 - beta + 4 * beta / np.pi
elif abs(abs(ti) - 1.0 / (4 * beta)) < 1e-8:
# 特異点の処理
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)) # エネルギー正規化
このRRCフィルタは、整合フィルタとしても使えるよう、送受でそれぞれ通すと全体でレイズドコサイン(ナイキスト)応答になる設計です。$t = 0$ と $t = \pm 1/(4\beta)$ で分母がゼロになるため、ロピタルの極限値を場合分けで与えています。エネルギーで正規化することで、フィルタ通過後の振幅スケールが安定します。
次に、ランダムな BPSK シンボル列を生成し、RRC で整形した送信波形を作ります。
# パラメータ
n_symbols = 2000 # シンボル数
sps = 8 # 送信側オーバーサンプリング(高い時間分解能で「真の連続波形」を模擬)
beta = 0.35 # ロールオフ率
span = 8 # フィルタ長(片側シンボル数)
# BPSKシンボル列
symbols = 2 * np.random.randint(0, 2, n_symbols) - 1.0 # ±1
# アップサンプリング(シンボル間にゼロ挿入)
upsampled = np.zeros(n_symbols * sps)
upsampled[::sps] = symbols
# 送信RRCで整形
rrc_tx = rrc_filter(beta, sps, span)
tx_signal = np.convolve(upsampled, rrc_tx, mode='same')
ここでは sps = 8 という高めのオーバーサンプリングで送信波形を作り、これを「ほぼ連続の真のアナログ波形」とみなします。受信側ではこの波形を、後でタイミングをずらしながら 2 サンプル/シンボルに間引いて使います。アップサンプリングでシンボル間にゼロを挿入し、RRC で畳み込むことでパルス整形が完成します。
タイミングオフセットを与えるために、整合フィルタ通過後の波形を一定の分数だけシフトしてから、受信側のサンプリングレート(2 サンプル/シンボル)にリサンプルします。
# 受信側整合フィルタ(同じRRC)
rrc_rx = rrc_filter(beta, sps, span)
matched = np.convolve(tx_signal, rrc_rx, mode='same')
# 真のタイミングオフセット(シンボルの何割ずれているか)
true_offset = 0.3 # 0.3シンボル分ずらす
# 受信オーバーサンプリング率(Gardnerは2でOK)
osr = 2
# sps=8の波形からosr=2に間引く。オフセット分だけ開始点をずらす
start = int(round(true_offset * sps))
decim = sps // osr # 8/2 = 4
rx = matched[start::decim]
整合フィルタを通した波形を、開始点を true_offset 分だけずらして 4 サンプルおきに間引くことで、「2 サンプル/シンボルで、最適点から 0.3 シンボルずれてサンプリングされた受信信号」を作っています。この rx がタイミング同期ループへの入力になります。
Gardner ループの実装
いよいよ Gardner TED、Farrow 補間器、PI ループフィルタ、NCO を組み合わせたタイミング同期ループを実装します。まず補間器とループのゲイン設計です。
def parabolic_interp(buf, mu, alpha=0.5):
"""区分放物線補間(Farrow構造)
buf : 4サンプル [y[k-1], y[k], y[k+1], y[k+2]] に対応
(基準点 y[k] から mu だけ進んだ位置を内挿する)
mu : 分数遅延 0<=mu<1"""
ym1, y0, yp1, yp2 = buf
v2 = alpha * yp2 - alpha * yp1 - alpha * y0 + alpha * ym1
v1 = -alpha * yp2 + (1 + alpha) * yp1 - (1 - alpha) * y0 - alpha * ym1
v0 = y0
return (v2 * mu + v1) * mu + v0 # ホーナー法
# ループフィルタ(PI)のゲイン設計
zeta = 0.707 # ダンピング係数
BnT = 0.01 # 正規化ループ帯域
Kd = 1.25 # 検出器利得(S字曲線の原点傾きから見積もった値)
K0 = 1.0 # NCO利得
theta = BnT / (zeta + 1.0 / (4 * zeta))
delta = 1 + 2 * zeta * theta + theta ** 2
K1 = (1.0 / (Kd * K0)) * (4 * zeta * theta) / delta # 比例ゲイン
K2 = (1.0 / (Kd * K0)) * (4 * theta ** 2) / delta # 積分ゲイン
print(f"K1 = {K1:.5f}, K2 = {K2:.6f}")
放物線補間は前述の Farrow 構造をそのまま実装したもので、4 サンプルのバッファ(基準点 $y[k]$ とその前後)と分数遅延 $\mu$ から内挿値を返します。ループゲイン $K_1, K_2$ は、ダンピング $\zeta = 0.707$、ループ帯域 $B_n T = 0.01$、検出器利得 $K_d = 1.25$ から二次 PLL の設計式で計算しており、出力は K1 = 0.02105, K2 = 0.000281 となります。$K_2 \ll K_1$ となっており、積分項がゆっくり働く(定常偏差を除去する遅い補正)ことを表しています。
次に、サンプルを 1 つずつ処理しながらタイミングをロックさせるメインループです。
mu = 0.0 # 分数遅延の初期値(0<=mu<1)
integ = 0.0 # PI積分器
i = 2 # 入力サンプルの読み出し位置(バッファ確保のため2から)
loop_corr = 0.0 # ループフィルタ出力(タイミング補正量)
out_symbols = [] # 補間後シンボル点
mu_history = [] # muの推移
err_history = [] # TED誤差の推移
prev_sym = 0.0 # 1つ前のシンボル点 y[k-1]
mid_sample = 0.0 # 直近の中間点(遷移点)サンプル
strobe_idx = 0 # 0:シンボル点ストローブ, 1:中間点ストローブ
# osr=2 なので 1ストローブ = 半シンボル = 入力1サンプル分の前進。
# 読み出し位置(i + mu)を「公称1サンプル - 補正量」ずつ進め、整数部でiを、小数部でmuを更新する。
while i < len(rx) - 2:
buf = [rx[i - 1], rx[i], rx[i + 1], rx[i + 2]]
interp = parabolic_interp(buf, mu)
if strobe_idx == 0:
# シンボル点ストローブ
cur_sym = interp
# Gardner TED: mid * (cur - prev)
e = mid_sample * (cur_sym - prev_sym)
err_history.append(e)
# ループフィルタ(PI)
integ += K2 * e
loop_corr = K1 * e + integ # e>0(サンプリングが遅い)で正
out_symbols.append(cur_sym)
mu_history.append(mu)
prev_sym = cur_sym
strobe_idx = 1
else:
# 中間点(遷移点)ストローブ
mid_sample = interp
strobe_idx = 0
# NCO: 公称で半シンボル(=1サンプル)進める。e>0なら進みを早めてタイミングを詰める(負帰還)
mu += 1.0 - loop_corr
adv = int(np.floor(mu)) # 整数部 -> 読み出しインデックスの前進
mu -= adv # 小数部 -> 新しい分数遅延
i += adv
out_symbols = np.array(out_symbols)
mu_history = np.array(mu_history)
err_history = np.array(err_history)
このループでは、ストローブを交互に切り替えてシンボル点と中間点を補間し、Gardner TED で誤差 $e$ を計算します。誤差を PI フィルタに通して得た補正量 loop_corr で読み出し位置の前進量を「公称 1 サンプル」から増減させます。$e > 0$(サンプリングが遅すぎる)のときは前進を早めてタイミングを詰める負帰還になっています。読み出し位置 $i + \mu$ の整数部をサンプルインデックス $i$ に、小数部を分数遅延 $\mu$ に振り分けることで、$\mu$ が真の最適点に対応する一定値へ収束していきます。mu_history と err_history を記録することで、ループが収束していく過程を後で可視化できます。
収束過程の可視化
タイミング誤差 $e_k$ と分数遅延 $\mu$ がシンボルとともにどう変化するかをプロットします。
fig, axes = plt.subplots(2, 1, figsize=(11, 7), sharex=True)
# 瞬時のTED誤差は遷移パターンに依存して大きく振れるので、
# 50シンボル移動平均を重ねてループの収束(振れ幅の縮小)を見やすくする
win = 50
sme = np.convolve(err_history, np.ones(win) / win, mode='same')
axes[0].plot(err_history, color='tab:red', lw=0.6, alpha=0.5, label='instant $e_k$')
axes[0].plot(sme, color='k', lw=1.5, label=f'{win}-symbol average')
axes[0].axhline(0, color='k', lw=0.5, ls='--')
axes[0].set_ylabel('TED error $e_k$')
axes[0].set_title('Gardner Timing Error Detector — Convergence')
axes[0].legend(loc='upper right')
axes[0].grid(alpha=0.3)
axes[1].plot(mu_history, color='tab:blue', lw=0.8)
axes[1].set_ylabel('fractional delay $\\mu$')
axes[1].set_xlabel('symbol index')
axes[1].grid(alpha=0.3)
plt.tight_layout()
plt.savefig('gardner_convergence.png', dpi=150, bbox_inches='tight')
plt.show()
上のグラフから、ループの収束の様子が読み取れます。注意したいのは、瞬時の TED 誤差 $e_k$(薄い赤線)は、ロック後でもシンボルが遷移するたびに大きく振れるという点です。これは Gardner TED の誤差量が「遷移の大きさ $\times$ 中間点の値」というデータ依存の積であるためで、タイミングが完璧でも遷移シンボルでは値が大きくなります。したがって収束を見るには、平均化された量に注目する必要があります。
黒の太線は 50 シンボル移動平均で、引き込み区間(前半 100 シンボルあたりで平均 $|e_k| \approx 0.45$)から定常区間(後半で平均 $|e_k| \approx 0.26$)へ向けて振れ幅が小さくなり、平均がゼロ付近に張り付いていく様子が見えます。下段の分数遅延 $\mu$ も、当初は大きくばらついていた値(最初の 100 シンボルの標準偏差 $\approx 0.26$)が、収束後には一定値付近(後半 200 シンボルの標準偏差 $\approx 0.02$、約 13 分の 1)へ落ち着きます。これは、ループが真のタイミングオフセットを推定し終えて「ロック」したことを意味します。ダンピング $\zeta = 0.707$ の設定のため、オーバーシュートが小さく滑らかに収束しているのが特徴です。
アイパターンの回復
最後に、タイミング同期前と後でアイパターンがどう変わるかを比較します。同期前は最適点からずれた状態でサンプリングした波形、同期後は収束したシンボル点を使います。
def plot_eye(ax, signal_2sps, title, n_eyes=200):
"""2サンプル/シンボルの信号でアイパターンを描く(2シンボル幅)"""
span_samp = 2 * 2 # 2シンボル分(osr=2)
seg_start = len(signal_2sps) // 4
for k in range(seg_start, seg_start + n_eyes):
idx = k * 2
if idx + span_samp < len(signal_2sps):
seg = signal_2sps[idx: idx + span_samp + 1]
ax.plot(np.linspace(0, 2, len(seg)), seg, color='tab:blue', alpha=0.15)
ax.set_title(title)
ax.set_xlabel('symbol time')
ax.set_ylabel('amplitude')
ax.grid(alpha=0.3)
fig, axes = plt.subplots(1, 2, figsize=(12, 4.5))
# 同期前: 生の受信信号(オフセットあり)
plot_eye(axes[0], rx, 'Eye Diagram — Before Timing Recovery')
# 同期後: 安定区間の補間シンボルから2sps相当を再構成
locked = out_symbols[len(out_symbols) // 2:]
# 同期後のアイは収束後のシンボル点が±1に整列するはず
axes[1].hist(locked, bins=60, color='tab:green', alpha=0.7)
axes[1].set_title('Recovered Symbols Histogram (after lock)')
axes[1].set_xlabel('amplitude')
axes[1].set_ylabel('count')
axes[1].grid(alpha=0.3)
plt.tight_layout()
plt.savefig('eye_recovery.png', dpi=150, bbox_inches='tight')
plt.show()
左のアイパターンは同期前の生の受信信号で、サンプリングタイミングがずれているため目の中心がサンプル格子からずれており、振幅のばらつきが大きくなっています。右のヒストグラムは、ループがロックした後の補間シンボル点の分布です。タイミングが合った状態では各シンボル点が $+1$ と $-1$ の近く(実際には整合フィルタのピーク利得により $\pm 1.04$ 付近)にくっきりと 2 峰に分かれ、$0$ 付近にはほとんど点が落ちていません。前述のとおりロック後のシンボルは 100% が $|{\cdot}| > 0.5$ に収まっており、判定マージンが十分に確保されていることがわかります。これがタイミング同期の成果 — 閉じかけていたアイが開き、シンボルが正しい振幅で取り出せるようになった状態です。
収束後の推定オフセットの確認
最後に、ループが推定したタイミングオフセットが、注入した真の値とどれくらい一致しているかを確認します。
# muの収束値(後半の平均)
mu_locked = np.mean(mu_history[len(mu_history) // 2:])
locked = out_symbols[len(out_symbols) // 2:]
sme_abs = np.convolve(np.abs(err_history), np.ones(50) / 50, mode='valid')
print(f"真のオフセット(設定値) : {true_offset:.3f} シンボル")
print(f"ループ収束後の平均mu : {mu_locked:.3f}")
print(f"muの標準偏差(引込前100/収束後200): {np.std(mu_history[:100]):.4f} -> {np.std(mu_history[-200:]):.4f}")
print(f"平滑化|TED誤差|(前半/後半) : {np.mean(sme_abs[:100]):.4f} -> {np.mean(sme_abs[-100:]):.4f}")
print(f"ロック後シンボルの|平均絶対値| : {np.mean(np.abs(locked)):.3f}")
print(f"ロック後 |シンボル|>0.5 の割合 : {np.mean(np.abs(locked) > 0.5) * 100:.1f}%")
出力は次のようになります。
真のオフセット(設定値) : 0.300 シンボル
ループ収束後の平均mu : 0.517
muの標準偏差(引込前100/収束後200): 0.2638 -> 0.0200
平滑化|TED誤差|(前半/後半) : 0.4475 -> 0.2642
ロック後シンボルの|平均絶対値| : 1.041
ロック後 |シンボル|>0.5 の割合 : 100.0%
$\mu$ の標準偏差が引き込み時の $0.26$ から収束後は $0.02$ へと約 13 分の 1 に縮み、平滑化した TED 誤差も $0.45 \to 0.26$ へ低下しています。これは、ループが定常状態に達してタイミング推定値の揺らぎが大幅に小さくなったことを示します。さらに、ロック後のシンボル点は全て $|{\cdot}| > 0.5$(平均絶対値 $\approx 1.04$)に収まり、$\pm 1$ の 2 値へきれいに分離できています。$\mu$ の収束値($\approx 0.52$)は補間器の基準点の取り方や開始位相に依存するため真のオフセット $0.3$ と数値そのものが一致するわけではありませんが、$\mu$ が一定値へ落ち着き、シンボルが $\pm 1$ に分離していることこそが、タイミング同期が成功した何よりの証拠です。実際のモデムでは、この後段に搬送波同期と判定器が続き、誤りなくシンボルが復号されます。
まとめ
本記事では、ディジタル受信機が最適なサンプリング時刻を自力で見つけ出すシンボルタイミング同期について、原理から実装まで解説しました。
- タイミングずれの害: 最適点(ナイキスト点)からずれてサンプリングすると、所望シンボルの振幅低下による SNR 劣化と、隣接シンボルの漏れ込みによる ISI が生じ、アイパターンが閉じて誤り率が悪化します
- 早遅ゲート: 推定点の前後(early / late)の包絡線を比較してタイミング誤差を測る古典的手法で、直感的ですが追加サンプルや $\delta$ 調整を要します
- Gardner TED: シンボル点と中間点を使い $e_k = \mathrm{Re}\{y_{\text{mid}}^{*}(y[k]-y[k-1])\}$ で誤差を測る方式。1 シンボルあたり 2 サンプルという最小構成で動き、共役積の実部により搬送波位相 $\theta$ が打ち消されて位相非依存になることを導出で確認しました
- Farrow 補間器: 固定係数 FIR と分数遅延 $\mu$ の多項式を分離し、ハードウェアを再設計せず任意時刻の値を作り出します
- 二次ループ: PI ループフィルタと NCO で位相同期ループを構成し、積分器によってクロック周波数オフセットがあっても定常偏差ゼロでロックします。ループ帯域とダンピングからゲインを設計できます
- Python 実装: RRC 整形 BPSK にオフセットを注入し、Gardner ループが誤差をゼロに引き込み、アイパターン(シンボル分布)が回復する過程を可視化しました
タイミング同期は、受信機の信号処理チェーンの中で搬送波同期と並ぶ最も基礎的な同期処理です。ここで安定したシンボル系列が得られて初めて、後段の搬送波位相同期、等化、誤り訂正復号が正しく機能します。Gardner TED が位相非依存である性質は、「タイミング同期 → 搬送波同期」という処理順序を可能にし、全ディジタル受信機のアーキテクチャを大きく単純化しました。
次のステップとして、以下の記事も参考にしてください。