Gardnerタイミング誤差検出器によるシンボル同期の理論と実装

無線で飛んでくる電波、光ファイバーを流れる光パルス、Wi-Fiの無線フレーム——どんなディジタル通信でも、受信機は送られてきた波形を「いつ」読み取るかを正確に知らなければなりません。送信機は1秒あたり数百万から数十億回というペースで連続的にシンボルを送り出しますが、受信機の側にはそのタイミングを直接教えてくれる時計はありません。受信機自身のサンプリングクロックは送信機のクロックと周波数も位相もわずかにずれており、放っておけば「シンボルの山」ではなく「山と谷の間の中途半端な場所」を読み取ってしまい、ビット誤りが爆発的に増えます。

この「いつ読み取るか」を受信信号そのものから推定して合わせ込む処理がシンボル同期(symbol timing recovery、クロックリカバリ)です。そして、現在の受信値が正しいサンプリング点に対してどちらにどれだけずれているかを数値として吐き出す心臓部がタイミング誤差検出器(TED: Timing Error Detector)です。本記事で扱うGardner検出器(Gardner TED)は、1シンボルあたりわずか2サンプルで動作し、しかも搬送波の位相同期が完了する前でも正しく機能するという際立った特徴を持ち、QPSKや16QAMといった実用システムのベースバンド受信機で最も広く使われているTEDの一つです。

Gardner検出器を理解すると、次のような場面で何が起きているかが見えるようになります。

  • ソフトウェア無線(SDR)受信機: GNU RadioやMATLABの受信チェーンでは、整合フィルタの直後に必ずタイミング同期ブロックが入ります。その内部はまさに本記事で作る「TED+補間器+ループフィルタ+NCO」です
  • 移動体・長距離通信のモデム: 送受信機の相対運動でクロック比がわずかにずれ続ける環境でも、誤差検出器が連続的にタイミングを追従することで、低いSNRでも安定した復調を実現します
  • 光通信・有線通信のSerDes: 数十Gbpsのシリアル通信でも、受信側はデータ遷移のエッジから最適サンプリング点を復元しており、その原理はGardner検出器と同じ「遷移の対称性」に基づいています

本記事の内容

  • Gardner誤差検出器の直感 — 「遷移の真ん中」を手がかりにする
  • 2サンプル/シンボルでの誤差量 $e=\mathrm{Re}\{(y[k]-y[k-1])\,y^\*[k-\frac{1}{2}]\}$ の導出
  • タイミング位相に対するS字カーブ(弁別特性)の意味と、それが搬送波位相に依存しない理由
  • ファロー補間器・NCO・2次ループフィルタによる全ディジタル同期ループの構成
  • 固定タイミングオフセットを与えて推定値が収束する様子と、収束後の星座図をPythonで可視化

前提知識

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

なぜタイミングがずれるのか — 同期の問題設定

まず、何を合わせ込みたいのかをはっきりさせましょう。送信機はシンボル周期 $T$ ごとにシンボル $a_k$ を送り出します。パルス整形フィルタ $g(t)$ を通すと、送信ベースバンド波形は次のようになります。

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

受信機はこれを整合フィルタに通したのち、自分のクロックでサンプリングします。理想的には各シンボルの「ピーク」、つまり $t = kT + \tau$($\tau$ は真の最適サンプリング点)の瞬間に1点ずつ取れれば、シンボル間干渉(ISI)がゼロになり $a_k$ がきれいに読み出せます。ナイキストの第一基準を満たすレイズドコサイン波形なら、ちょうどそのタイミングで他のシンボルの裾がすべてゼロを横切るからです。

ところが受信機は真の $\tau$ を知りません。送信機と受信機の水晶発振器は別物で、周波数がppmオーダーでずれているうえ、伝搬遅延も未知です。サンプリング点が最適点から $\tau$ だけずれると、読み取った値は隣のシンボルの裾を拾い込んで濁ります。これがタイミング誤差です。

ここで発想を変えます。受信機はサンプリングした離散値の系列だけを持っています。この系列を眺めて、「今のサンプリング点は早すぎるのか、遅すぎるのか」を判定する仕組みがあれば、その判定を使ってサンプリング点を少しずつ動かし、最適点に追い込めます。これがTEDのアイデアです。TEDは誤差の符号と大きさを出力し、それをループフィルタとサンプリング点制御(NCO+補間器)にフィードバックする——典型的なフィードバック制御ループになります。

では、どんな量を見れば「早い/遅い」がわかるのでしょうか。次節でGardnerの巧妙なアイデアを見ていきます。

Gardner検出器の直感 — 遷移の対称性を測る

Gardner検出器の核心は、ひとことで言えば「2つの連続するシンボルの値が変化するとき、その変化の途中点(中間サンプル)の値を見れば、サンプリング点のずれがわかる」という洞察です。

具体的な場面を思い浮かべましょう。あるシンボルが $+1$、次のシンボルが $-1$ だったとします。波形は $+1$ から $-1$ へなめらかに下っていきます。もしサンプリングのタイミングがぴったり合っていれば、2つのシンボル点のちょうど真ん中——時刻でいえば半シンボルずれた中間点——では、波形は対称性によりちょうどゼロを横切ります。上りと下りが対称だからです。

ところがサンプリングが少し早ければ、中間点として拾う値はまだ $+1$ 側に寄っていて正の値になります。逆に少し遅ければ、中間点はすでに $-1$ 側に進んでいて負の値になります。つまり中間サンプルの値の符号が、タイミングが早いか遅いかを教えてくれるのです。

ただし中間サンプルの符号だけでは、シンボルが $+1\to-1$(下り)なのか $-1\to+1$(上り)なのかで意味が逆転してしまいます。そこで「2つのシンボル点の差」$(y[k]-y[k-1])$ を掛け合わせます。下りなら差は負、上りなら差は正になるので、これを掛けることで遷移の向きによらず誤差の符号が一定方向にそろいます。

遷移の中間サンプルでタイミングずれを測るGardner検出器の直感

この図は $+1\to-1$ の遷移波形上に、2つのオンタイム点(緑)と中間点を描いたものです。タイミングが合っていれば中間点はちょうどゼロ交差(赤)に乗りますが、サンプリングが早すぎると中間点は $+1$ 側に残って正の値(オレンジ)になります。つまり中間サンプルの符号と大きさが、そのまま「どちらにどれだけずれているか」を語っていることが視覚的にわかります。

このアイデアを式で書いたものが、本記事の主役である次の誤差量です。

$$ \begin{equation} e_k = \mathrm{Re}\Bigl\{ \bigl(y[k] – y[k-1]\bigr)\, y^\*\!\left[k – \tfrac{1}{2}\right] \Bigr\} \end{equation} $$

ここで $y[k]$ は第 $k$ シンボル点のサンプル(オンタイムサンプル)、$y[k-\frac{1}{2}]$ はその半シンボル前の中間サンプル(midpointサンプル)、$\mathrm{Re}\{\cdot\}$ は実部、$y^\*$ は複素共役です。複素ベースバンドで考えるのは、QPSKや16QAMのようにI軸とQ軸の両方に情報を載せる変調を扱うためです。

注目すべき点は3つあります。第一に、必要なサンプルは1シンボルあたりオンタイム1点と中間1点の合計2点だけです。これがGardner検出器が「2サンプル/シンボル」で動くと言われる理由です。第二に、判定値(送信シンボルの推定 $\hat a_k$)を一切使っていません。受信サンプルだけから誤差を作る、いわゆる非データ援用(NDA: Non-Data-Aided)型です。第三に、後で示すように搬送波位相のずれがあっても誤差の期待値が変わらないという著しい性質を持ちます。

この直感を、きちんと期待値計算で裏付けていきましょう。次節では誤差量の期待値、すなわちS字カーブを導出します。

誤差量の導出 — S字カーブを求める

サンプリングモデルの設定

受信機が補間によって作り出すサンプル時刻を、シンボル周期 $T$ を単位として書きます。タイミング誤差を正規化したものを $\varepsilon$(単位はシンボル、$-\frac{1}{2} \le \varepsilon < \frac{1}{2}$)とすると、オンタイムサンプルと中間サンプルは整合フィルタ出力波形 $x(t)$ を用いて

$$ y[k] = x\bigl((k+\varepsilon)T\bigr), \qquad y\!\left[k-\tfrac{1}{2}\right] = x\bigl((k – \tfrac{1}{2} + \varepsilon)T\bigr) $$

と表せます。$\varepsilon=0$ なら $y[k]$ はちょうどシンボルピーク、$y[k-\frac{1}{2}]$ はちょうど遷移の中間です。

整合フィルタ後の波形は、送信シンボル $a_m$ と送受の整形を合成したパルス $p(t)$(送信パルスと整合フィルタの畳み込み、すなわちレイズドコサイン)を使って

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

と書けます。$p(t)$ はナイキスト条件を満たし、$p(0)=1$、$p(mT)=0\ (m\neq 0)$ という性質を持ちます。ここでは見通しをよくするため、まず実数値の二値振幅($a_m=\pm1$)で考え、後で複素・搬送波位相を加えます。

誤差量の期待値(S字カーブ)の定義

Gardner検出器の出力 $e_k$ を、送信シンボル列 $\{a_m\}$ についてアンサンブル平均したものをS字カーブ(弁別特性、discriminator characteristic)と呼びます。これはタイミング誤差 $\varepsilon$ の関数として

$$ g(\varepsilon) = \mathbb{E}\bigl[e_k\bigr] = \mathbb{E}\Bigl[ \bigl(y[k]-y[k-1]\bigr)\, y\!\left[k-\tfrac12\right] \Bigr] $$

と定義されます。$g(\varepsilon)$ が $\varepsilon=0$ で値ゼロを横切り、$\varepsilon>0$ で正、$\varepsilon<0$ で負(あるいはその逆符号で一貫)であれば、誤差検出器として正しく機能します。グラフにすると原点付近を斜めに横切る「S字」を描くので、この名前があります。

二値振幅での期待値計算

$y[k]$、$y[k-1]$、$y[k-\frac12]$ をパルス展開で書き下します。$y[k]=\sum_m a_m p((k+\varepsilon-m)T)$ などを代入し、シンボルが独立同分布で $\mathbb{E}[a_m a_n]=\sigma_a^2 \delta_{mn}$($\sigma_a^2$ は平均パワー、相異なるシンボルは無相関)であることを使います。

まず差 $y[k]-y[k-1]$ を展開すると、

$$ y[k]-y[k-1] = \sum_m a_m\Bigl[ p\bigl((k+\varepsilon-m)T\bigr) – p\bigl((k-1+\varepsilon-m)T\bigr)\Bigr] $$

となります。これに中間サンプル $y[k-\frac12]=\sum_n a_n\, p((k-\frac12+\varepsilon-n)T)$ を掛けて期待値を取ります。$\mathbb{E}[a_m a_n]=\sigma_a^2\delta_{mn}$ より $m=n$ の項だけが残り、二重和が一重和になります。

$$ g(\varepsilon) = \sigma_a^2 \sum_m \Bigl[ p\bigl((k+\varepsilon-m)T\bigr) – p\bigl((k-1+\varepsilon-m)T\bigr)\Bigr]\, p\bigl((k-\tfrac12+\varepsilon-m)T\bigr) $$

ここで添字を $\ell = k-m$ と置き換えると、$k$ への依存が消えて

$$ g(\varepsilon) = \sigma_a^2 \sum_{\ell} \Bigl[ p\bigl((\ell+\varepsilon)T\bigr) – p\bigl((\ell-1+\varepsilon)T\bigr)\Bigr]\, p\bigl((\ell-\tfrac12+\varepsilon)T\bigr) $$

を得ます。和の中身を見やすくするため、$p$ の時間引数をシンボル単位の連続変数で書き、$p_\ell \equiv p((\ell+\varepsilon)T)$ のように略記すると、各項は「隣り合うシンボル位置のパルス値の差」と「その中間位置のパルス値」の積になっています。これがまさに前節の直感——遷移の差 × 中間値——を、すべてのシンボル位置にわたって足し合わせたものだとわかります。

$\varepsilon=0$ のとき、$p(\ell T)$ はナイキスト条件で $\ell=0$ 以外ゼロ、$p((\ell-1)T)$ も $\ell=1$ 以外ゼロですから、差の項が立つのは $\ell=0,1$ 付近に限られます。一方、中間値 $p((\ell-\frac12)T)$ は対称なレイズドコサインでは $\ell=0$ と $\ell=1$ で同じ値(半シンボル点の対称性)をとり、これらが差の符号と組み合わさって打ち消し合い、$g(0)=0$ になります。$\varepsilon$ が正の方向にずれると、この打ち消しのバランスが崩れて $g(\varepsilon)$ が一方向に符号を持ち始める——これがS字の「斜め横断」を生みます。

周波数領域による見通しの良い表現

和の形のままでは挙動が読みにくいので、$p(t)$ のフーリエ変換 $P(f)$ を使って書き直すと本質が見えます。Gardnerが示したように、$g(\varepsilon)$ は近似的に基本シンボルレート成分 $f=1/T$ のフーリエ係数で決まり、レイズドコサインのロールオフ率 $\beta$ を用いて

$$ g(\varepsilon) \;\approx\; K_\varepsilon \sin(2\pi\varepsilon), \qquad K_\varepsilon \propto \beta $$

という、振幅が $\sin$ 状で原点を横切る特性に帰着します。重要なのは次の2点です。第一に、ロールオフ $\beta$ がゼロだとS字の傾きもゼロになること。帯域ぎりぎりの矩形スペクトル($\beta=0$)にはレートに対応するスペクトル成分が無く、Gardner検出器はタイミング情報を取り出せません。実用では $\beta=0.2\sim0.5$ 程度が選ばれ、十分なS字傾きが得られます。第二に、原点付近では $\sin(2\pi\varepsilon)\approx 2\pi\varepsilon$ なので、誤差出力は小さな $\varepsilon$ に対してほぼ比例します。この比例係数こそが、ループ設計で必要になる検出器利得 $K_d$ です。

S字カーブの形がわかったので、次に最大の特徴である「搬送波位相非依存性」を示します。

なぜ搬送波位相に依存しないのか

実際の受信機では、搬送波同期(キャリアリカバリ)とシンボル同期はどちらも未完了の状態で動き始めます。複素ベースバンド信号には未知の搬送波位相 $\theta$(場合によっては小さな周波数オフセット)が残留し、受信サンプルは真の値に位相回転 $e^{j\theta}$ がかかった形になります。

$$ y[k] \;\to\; y[k]\, e^{j\theta}, \quad y[k-1] \to y[k-1]\,e^{j\theta}, \quad y\!\left[k-\tfrac12\right] \to y\!\left[k-\tfrac12\right] e^{j\theta} $$

ここでGardner誤差量 $e_k = \mathrm{Re}\{(y[k]-y[k-1])\, y^\*[k-\frac12]\}$ に、位相回転後のサンプルを代入してみましょう。差の項には $e^{j\theta}$ が掛かり、共役を取る中間サンプルには $e^{-j\theta}$ が掛かります。

$$ \begin{align} e_k &= \mathrm{Re}\Bigl\{ \bigl(y[k]e^{j\theta} – y[k-1]e^{j\theta}\bigr)\,\bigl(y\!\left[k-\tfrac12\right]e^{j\theta}\bigr)^\* \Bigr\} \\ &= \mathrm{Re}\Bigl\{ \bigl(y[k] – y[k-1]\bigr)\,y^\*\!\left[k-\tfrac12\right]\, e^{j\theta}\,e^{-j\theta} \Bigr\} \end{align} $$

ここで $e^{j\theta}\,e^{-j\theta} = e^{j(\theta-\theta)} = e^{j\cdot 0} = 1$ となり、位相回転が完全に打ち消されます。したがって

$$ e_k = \mathrm{Re}\Bigl\{ \bigl(y[k]-y[k-1]\bigr)\, y^\*\!\left[k-\tfrac12\right] \Bigr\} $$

となり、搬送波位相 $\theta$ がいくつであっても誤差量は変わりません。これがGardner検出器が「位相非依存」と呼ばれる数学的な理由です。

直感的に言えば、差 $(y[k]-y[k-1])$ と中間値 $y[k-\frac12]$ は同じ瞬間の信号ではないにせよ、どちらも同じ未知位相 $\theta$ で同じ向きに回転しています。片方に共役を取って掛けることで、その共通の回転が相殺されるのです。複素平面でいえば「2つのベクトルのなす相対角」だけが効き、絶対的な向き($\theta$)は消えます。

この性質の実用的なありがたみは絶大です。受信機の起動時、搬送波位相がまだぐるぐる回っている段階でも、Gardner検出器はタイミング誤差を正しく出力し続けます。つまりタイミング同期ループと搬送波同期ループを独立に並行して走らせられるのです。もしTEDが位相に依存していたら、両者が互いの未完了状態に足を引っ張られ、同期がいつまでも確立しないという悪循環に陥りかねません。Gardner検出器はこの鶏と卵の問題を、誤差量の構造そのもので回避しています。

位相非依存性まで理解できたので、いよいよこの誤差を使ってサンプリング点を実際に動かす仕組み——補間器・NCO・ループフィルタ——を組み立てていきます。

全ディジタル同期ループの構成要素

TEDが「どれだけずれているか」を教えてくれても、それだけでは同期は完成しません。受信機のADCは固定の高いレートで信号をサンプリングしており、その格子点が必ずしも最適サンプリング点に一致するとは限りません。そこで、固定格子のサンプル列から任意の小数位置の値を作り出す補間器と、その小数位置を誤差に応じて少しずつ更新する制御部が必要になります。全ディジタル同期ループは次の4つの要素からなります。

全ディジタルシンボル同期ループの構成ブロック図

このブロック図が同期ループの全体像です。固定レートの受信サンプルがファロー補間器に入り、補間されたオンタイム点と中間点からGardner TEDが誤差を計算します。誤差はPIループフィルタで平滑化され、NCOがサンプリング位相を管理して補間器の小数位置 $\mu$ を更新する——という閉ループになっている点が読み取れます。以下、各ブロックを順に見ていきます。

1. ファロー補間器(Farrow interpolator)

ファロー補間器は、隣接する数サンプルから多項式補間によって任意の小数遅延 $\mu \in [0,1)$ の点の値を推定する構造です。3次(キュービック)ファロー補間は、4つの連続サンプル $x[n-1], x[n], x[n+1], x[n+2]$ と小数位置 $\mu$ から、ラグランジュ補間に基づいて補間値を計算します。係数をホーナー法で書くと

$$ y_{\text{interp}} = \bigl((c_3\,\mu + c_2)\,\mu + c_1\bigr)\mu + c_0 $$

の形になり、各係数 $c_i$ は4サンプルの線形結合です。代表的なキュービック(Farrow)係数は

$$ \begin{align} c_0 &= x[n] \\ c_1 &= -\tfrac{1}{3}x[n-1] – \tfrac{1}{2}x[n] + x[n+1] – \tfrac{1}{6}x[n+2] \\ c_2 &= \tfrac{1}{2}x[n-1] – x[n] + \tfrac{1}{2}x[n+1] \\ c_3 &= -\tfrac{1}{6}x[n-1] + \tfrac{1}{2}x[n] – \tfrac{1}{2}x[n+1] + \tfrac{1}{6}x[n+2] \end{align} $$

です。ファロー構造のうれしさは、補間したい小数位置 $\mu$ が変わってもフィルタ係数 $c_i$ の計算は同じで、$\mu$ の多項式評価だけ差し替えればよい点にあります。これにより、ループが要求する刻々と変わる小数位置に効率よく対応できます。

2. NCO(Numerically Controlled Oscillator)— サンプリング位相の管理

NCOは、次にどの小数位置でサンプルを取り出すかを管理するカウンタです。シンボルあたりのサンプル数を $N_{sps}$(本記事では2)とすると、理想的には $1/N_{sps}$ シンボルごとに1つ補間サンプルを出します。NCOは内部に位相アキュムレータ $\eta$ を持ち、毎入力サンプルで一定量 $W$ だけ減算(または加算)します。$\eta$ がゼロを下回って巻き戻る瞬間に「ここで補間して1サンプル出力せよ」というストローブ(strobe)を発し、そのときの小数残り $\mu$ を補間器に渡します。ループフィルタの出力が $W$ を微調整することで、出力サンプル時刻が前後に動き、これがタイミング追従そのものになります。

3. ループフィルタ(2次・PI型)

TEDの瞬時出力 $e_k$ には雑音が乗っているので、そのまま使うとサンプリング点がガタつきます。そこで比例・積分(PI: Proportional-Integral)型の2次ループフィルタで平滑化します。比例項は誤差に即応し、積分項は定常的なずれ(クロック周波数差による一定の傾き)を蓄積して打ち消します。離散時間のPIフィルタは

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

で表され、$K_1$ が比例ゲイン、$K_2$ が積分ゲインです。1次ループ(比例のみ)では一定のクロック周波数差を追い切れず定常誤差が残りますが、積分項を加えた2次ループは周波数オフセットがあっても定常誤差ゼロで追従できます。だからこそ実用システムでは2次が標準です。

4. ゲイン設計 — ループ帯域と減衰係数から決める

$K_1, K_2$ は、所望のループ正規化帯域 $B_n T$(小さいほど雑音に強く追従は遅い)と減衰係数 $\zeta$(典型的に $0.707$)、検出器利得 $K_d$(S字の原点傾き)、NCO利得 $K_0$ から決めます。連続時間2次PLLの標準形を離散化した近似式は

$$ \begin{align} \theta_n &= \frac{B_n T}{N_{sps}\,(\zeta + 1/(4\zeta))} \\ K_1 &= \frac{1}{K_d K_0}\cdot\frac{4\zeta\,\theta_n}{1 + 2\zeta\theta_n + \theta_n^2} \\ K_2 &= \frac{1}{K_d K_0}\cdot\frac{4\,\theta_n^2}{1 + 2\zeta\theta_n + \theta_n^2} \end{align} $$

です。式の細部より大事なのは構造です。$\theta_n$ がループ帯域に比例して決まり、$K_1$ と $K_2$ はそれを $K_dK_0$ で割って正規化したゲインだ、という点を押さえておけば十分です。ループ帯域を狭くすれば収束は遅いが雑音に強く、広げれば速いが暴れやすい——このトレードオフを $B_nT$ ひとつで調整できます。

4つの部品がそろいました。これらを「補間 → TED → ループフィルタ → NCO → 次の補間位置」という閉ループに組んで、固定オフセットが消えていく様子をPythonで確かめましょう。

Pythonでの実装

ステップ1: S字カーブを実際に描く

理論で導いたS字カーブが本当に原点を横切るのか、レイズドコサイン波形を作って数値的に確認します。まず日本語フォントとレイズドコサインインパルス応答を用意します。

import numpy as np
import matplotlib, matplotlib.pyplot as plt

# 日本語フォント設定(環境にあるものを選ぶ)
for cand in ["Hiragino Sans", "Yu Gothic", "Noto Sans CJK JP", "IPAexGothic", "Meiryo"]:
    if any(cand == f.name for f in matplotlib.font_manager.fontManager.ttflist):
        plt.rcParams["font.family"] = cand
        break
plt.rcParams["axes.unicode_minus"] = False

def rrc_or_rc_impulse(beta, sps, span, kind="rc"):
    """レイズドコサイン(rc)パルスのインパルス応答を生成。
    beta: ロールオフ率, sps: シンボルあたりサンプル数, span: 片側シンボル数"""
    N = span * sps
    t = np.arange(-N, N + 1) / sps  # シンボル単位の時間
    # 0/0 を避けるため微小オフセットで評価
    eps = 1e-8
    # レイズドコサイン: sinc(t) * cos(pi beta t)/(1-(2 beta t)^2)
    num = np.sinc(t) * np.cos(np.pi * beta * t)
    den = 1 - (2 * beta * t) ** 2
    den = np.where(np.abs(den) < eps, eps, den)
    h = num / den
    return t, h / np.max(h)  # ピークを1に正規化

beta = 0.35       # ロールオフ率
sps = 8           # S字描画用に高い分解能でオーバーサンプリング
span = 8
t, p = rrc_or_rc_impulse(beta, sps, span, kind="rc")
print(f"パルス長={len(p)}サンプル, ピーク値={p.max():.3f}")

ここではS字カーブを高い分解能で観察したいので、あえて1シンボルあたり8サンプル(sps=8)でレイズドコサインを作っています。出力からパルスのピークが1.0に正規化されていることが確認でき、ナイキストパルスの土台が用意できました。

次に、ランダムなシンボル列でこのパルスを変調し、タイミング誤差 $\varepsilon$ を $-0.5$ から $0.5$ まで振って、各 $\varepsilon$ におけるGardner誤差量の平均(S字カーブ)を計算します。

def make_signal(symbols, p, sps):
    """シンボル列をパルス整形して連続風の波形を作る(アップサンプル+畳み込み)"""
    up = np.zeros(len(symbols) * sps, dtype=complex)
    up[::sps] = symbols
    return np.convolve(up, p, mode="same")

rng = np.random.default_rng(0)
M = 4000
# QPSKシンボル(±1±j を正規化)
syms = (rng.integers(0, 2, M) * 2 - 1) + 1j * (rng.integers(0, 2, M) * 2 - 1)
syms /= np.sqrt(2)
wave = make_signal(syms, p, sps)

def gardner_scurve(wave, sps, eps_grid):
    """各タイミング誤差epsについてGardner誤差の平均(S字カーブ)を返す"""
    s_curve = []
    for eps in eps_grid:
        off = int(round(eps * sps))          # epsをサンプル位置に換算
        on = np.arange(sps, len(wave) - sps, sps) + off   # オンタイム点
        mid = on - sps // 2                   # 半シンボル前の中間点
        on = on[(mid > 0) & (on < len(wave))]
        mid = on - sps // 2
        yk, yk1, ymid = wave[on], wave[on - sps], wave[mid]
        e = np.real((yk - yk1) * np.conj(ymid))
        s_curve.append(np.mean(e))
    return np.array(s_curve)

eps_grid = np.linspace(-0.5, 0.5, 51)
sc = gardner_scurve(wave, sps, eps_grid)

plt.figure(figsize=(8, 5))
plt.plot(eps_grid, sc, "o-", color="#0077b6")
plt.axhline(0, color="gray", lw=0.8)
plt.axvline(0, color="gray", lw=0.8)
plt.xlabel("正規化タイミング誤差 $\\varepsilon$ [シンボル]")
plt.ylabel("Gardner誤差の平均 $g(\\varepsilon)$")
plt.title(f"GardnerTEDのS字カーブ(ロールオフ $\\beta$={beta})")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

このグラフがまさにS字カーブです。$\varepsilon=0$ できれいにゼロを横切り、原点付近では右上がりの傾きを持つことが読み取れます。$\varepsilon>0$(サンプリングが遅れている)で誤差が正、$\varepsilon<0$ で負となり、誤差の符号がずれの向きを正しく示しています。原点付近の傾きこそが検出器利得 $K_d$ であり、この傾きがあるおかげでループはどちらに動けばよいかを判断できます。両端で曲線が寝てくるのは $\sin(2\pi\varepsilon)$ 状の振る舞いで、半シンボル以上ずれると引き込み方向が逆転しうる(誤ロックの危険がある)ことも示唆しています。

GardnerTEDの実測S字カーブと原点傾き検出器利得

実測したS字カーブ(青)は理論どおり $\varepsilon=0$ でゼロを横切り、原点近傍を破線(赤)のように一定の傾き $K_d\approx1.07$ で斜めに通過しています。$|\varepsilon|>0.25$ あたりから曲線が寝て折り返すのは $\sin(2\pi\varepsilon)$ 状の振る舞いそのもので、引き込み範囲が半シンボル程度に限られることが図から確認できます。この原点傾きがループフィルタのゲイン設計に使う $K_d$ になります。

ステップ2: ロールオフ依存性と位相非依存性の確認

理論で「ロールオフ $\beta$ が大きいほどS字の傾きが立つ」「搬送波位相 $\theta$ を変えてもS字は不変」と述べました。これを重ねて描いて確かめます。

plt.figure(figsize=(12, 5))

# 左: ロールオフ依存性
plt.subplot(1, 2, 1)
for b in [0.1, 0.25, 0.5, 0.9]:
    _, pb = rrc_or_rc_impulse(b, sps, span, kind="rc")
    w = make_signal(syms, pb, sps)
    scb = gardner_scurve(w, sps, eps_grid)
    plt.plot(eps_grid, scb, "-", label=f"$\\beta$={b}")
plt.axhline(0, color="gray", lw=0.8); plt.axvline(0, color="gray", lw=0.8)
plt.xlabel("$\\varepsilon$ [シンボル]"); plt.ylabel("$g(\\varepsilon)$")
plt.title("ロールオフ率とS字の傾き")
plt.legend(); plt.grid(True, alpha=0.3)

# 右: 搬送波位相を回してもS字は不変
plt.subplot(1, 2, 2)
for theta_deg in [0, 30, 90, 150]:
    w_rot = wave * np.exp(1j * np.deg2rad(theta_deg))
    sct = gardner_scurve(w_rot, sps, eps_grid)
    plt.plot(eps_grid, sct, "-", label=f"$\\theta$={theta_deg}°")
plt.axhline(0, color="gray", lw=0.8); plt.axvline(0, color="gray", lw=0.8)
plt.xlabel("$\\varepsilon$ [シンボル]"); plt.ylabel("$g(\\varepsilon)$")
plt.title("搬送波位相を変えてもS字は重なる")
plt.legend(); plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

左のグラフから、ロールオフ $\beta$ が大きいほど原点付近のS字の傾きが立つことがはっきり読み取れます。$\beta$ が小さいとシンボルレート成分のスペクトルが乏しく、傾き(検出器利得)が小さくなる——理論どおりです。右のグラフでは、搬送波位相を $0°,30°,90°,150°$ と回した4本のS字カーブが完全に重なって1本に見えます。前節で示した $e^{j\theta}e^{-j\theta}=1$ の打ち消しが数値的にも確認でき、Gardner検出器がタイミング同期を搬送波同期から切り離せる根拠が目で見えました。

ロールオフ率によるS字カーブの傾き変化

ロールオフ依存性を実測すると、原点傾きは $\beta=0.1$ で約 $0.34$、$\beta=0.9$ で約 $2.48$ と、$\beta$ にほぼ比例して増えています。帯域ぎりぎりの $\beta\to0$ ではシンボルレート成分が乏しくS字がほとんど寝てしまい、タイミング情報が取り出しにくくなることが定量的に確認できます。

搬送波位相を変えてもS字カーブが重なる位相非依存性

搬送波位相を $0°,30°,90°,150°$ と回した4本のS字カーブは完全に重なって1本にしか見えません。差の項の $e^{j\theta}$ と中間項の $e^{-j\theta}$ が相殺するという数式が、数値実験でもそのまま再現されています。これがタイミング同期と搬送波同期を独立に走らせられる根拠です。

雑音が乗ると検出器出力はどうなるでしょうか。各 $\varepsilon$ でGardner誤差を平均する際に、受信波形に複素ガウス雑音を加えてS字カーブを描き直してみます。

雑音がS字カーブに与える影響と原点交差の不変性

SNRを30 dB→15 dB→5 dBと下げてもS字カーブの全体形状はほぼ崩れず、特に原点のゼロ交差位置は変わらないことが読み取れます。雑音は各サンプルにランダムに乗るため、多数のシンボルで平均すると打ち消され、平均特性(S字)はロバストに保たれます。瞬時の誤差出力には雑音が残るので、後段のループフィルタで平滑化する必要があるわけです。

ステップ3: 全ディジタル同期ループの実装

いよいよ、ファロー補間器・PIループフィルタ・NCOを組み合わせた同期ループを実装します。まず補間器とループのゲイン計算を関数化します。

def farrow_cubic(x, n, mu):
    """3次ファロー補間: 整数インデックスn周辺の4点と小数muから補間値を返す"""
    xm1, x0, xp1, xp2 = x[n - 1], x[n], x[n + 1], x[n + 2]
    c0 = x0
    c1 = -1/3 * xm1 - 1/2 * x0 + xp1 - 1/6 * xp2
    c2 = 1/2 * xm1 - x0 + 1/2 * xp1
    c3 = -1/6 * xm1 + 1/2 * x0 - 1/2 * xp1 + 1/6 * xp2
    return ((c3 * mu + c2) * mu + c1) * mu + c0  # ホーナー法

def loop_gains(BnT, zeta, Kd, K0, sps):
    """正規化ループ帯域BnT・減衰zetaから比例K1・積分K2ゲインを計算"""
    theta = BnT / (sps * (zeta + 1/(4*zeta)))
    d = 1 + 2*zeta*theta + theta**2
    K1 = (1/(Kd*K0)) * (4*zeta*theta) / d
    K2 = (1/(Kd*K0)) * (4*theta**2) / d
    return K1, K2

print("ファロー補間器とゲイン計算を定義しました")

farrow_cubic はホーナー法で多項式を評価しているため乗算回数が少なく、loop_gains はループ帯域からPIゲインを自動算出します。設計者はループ帯域 BnT をいじるだけでよく、ゲインの絶対値を手探りしなくて済みます。

次に、固定タイミングオフセットを与えた受信信号を作り、ループを回します。受信機は1シンボルあたり2サンプル(sps=2)で動くと想定し、補間で最適点を探します。

# --- 受信信号の生成(固定タイミングオフセットを注入)---
rng = np.random.default_rng(1)
Nsym = 2000
tx_syms = ((rng.integers(0, 2, Nsym)*2-1) + 1j*(rng.integers(0, 2, Nsym)*2-1)) / np.sqrt(2)

sps_tx = 16                       # 送信側は高分解能で生成
beta = 0.35
# 送信は root-RC、受信側で整合フィルタ(root-RC)を通すと
# 合成がナイキストRCになりオンタイム点のISIがゼロになる
_, prrc = rrc_or_rc_impulse(beta, sps_tx, 8, kind="rrc")
tx_wave = make_signal(tx_syms, prrc, sps_tx)

true_offset = 0.30                # 与える真のタイミングオフセット[シンボル]
theta_carrier = np.deg2rad(40)    # 搬送波位相オフセット(あえて非ゼロ)
shift = int(round(true_offset * sps_tx))
rx_hi_raw = tx_wave[shift:] * np.exp(1j * theta_carrier)
rx_hi = np.convolve(rx_hi_raw, prrc, mode="same") / sps_tx  # 受信整合フィルタ

# 受信機の動作レート sps=2 へダウンサンプル
sps = 2
rx = rx_hi[::sps_tx // sps]
print(f"受信サンプル数={len(rx)}, 真のオフセット={true_offset}シンボル, 位相={np.rad2deg(theta_carrier):.0f}°")

ここで送信に root レイズドコサイン(kind="rrc")を使い、受信側でも同じ root-RC を整合フィルタとして畳み込んでいます。送受の root-RC を合成するとナイキスト条件を満たすRCになるため、正しいタイミングで取ったオンタイム点はシンボル間干渉がゼロになります。真のタイミングオフセット0.30シンボルと搬送波位相40°をわざと与え、位相に邪魔されずタイミングだけ合わせられることを実証します。続いてループ本体です。

# --- 同期ループ本体 ---
def gardner_loop(rx, sps, BnT, zeta=0.707, Kd=1.0, K0=1.0):
    """補間位置 tau を半シンボルずつ進め、Gardner誤差をPIループで吸収する。
    tau は連続的な小数サンプル位置。オンタイム点と中間点を交互に取る。"""
    K1, K2 = loop_gains(BnT, zeta, Kd, K0, sps)
    half = sps / 2.0
    tau = float(sps * 4)            # 補間位置(小数サンプル)
    integ, step = 0.0, half
    prev_on = prev_mid = 0.0 + 0j
    cnt = 0
    mu_log, sym_out = [], []
    while tau < len(rx) - sps - 2:
        n = int(np.floor(tau)); mu = tau - n          # 整数部と小数部
        interp = farrow_cubic(rx, n, mu)
        if cnt % 2 == 0:                              # オンタイム点
            # Gardner誤差(補間位置の進め方に合わせ符号を取る)
            e = -np.real((interp - prev_on) * np.conj(prev_mid))
            integ += K2 * e                           # 積分項
            v = K1 * e + integ                        # PI出力
            step = half * (1.0 + v)                   # 進み量を微調整
            prev_on = interp
            sym_out.append(interp); mu_log.append((tau / sps) % 1.0)
        else:                                         # 中間点
            prev_mid = interp
        cnt += 1
        tau += step
    return np.array(mu_log), np.array(sym_out)

Kd, K0, zeta, BnT = 1.0, 1.0, 0.707, 0.01
mu_log, sym_out = gardner_loop(rx, sps, BnT, zeta, Kd, K0)
print(f"出力シンボル数={len(sym_out)}, 収束後の推定mu平均={mu_log[-200:].mean():.3f}")

このループは、補間位置 tau を半シンボル(sps/2 サンプル)ずつ進めながらファロー補間で1サンプルずつ生成し、オンタイム点と中間点を交互に蓄えます。オンタイム点がそろうたびにGardner誤差を計算し、PIフィルタを通して次の進み量 step を微修正します。step を半シンボルより少し大きく/小さくすることで、サンプリング点が前後に動いて最適点に追い込まれます。出力された推定小数位置 $\mu$ の収束後の平均が、一定値(およそ0.70)に落ち着くことが確認できます。次にその収束の様子を可視化します。

ステップ4: 収束の可視化と星座図

推定タイミングが時間とともにどう収束するか、そして収束後の星座図がきれいに4点に集まるかを描きます。

fig, axes = plt.subplots(1, 2, figsize=(13, 5))

# 左: 推定muの時間推移(収束カーブ)
axes[0].plot(mu_log, color="#d62828", lw=1)
axes[0].axhline(mu_log[-200:].mean(), color="gray", ls="--",
                label=f"収束値 ≈ {mu_log[-200:].mean():.3f}")
axes[0].set_xlabel("出力シンボル番号")
axes[0].set_ylabel("推定小数位置 $\\mu$")
axes[0].set_title("タイミング推定の収束")
axes[0].legend(); axes[0].grid(True, alpha=0.3)

# 右: 収束後の星座図(前半=未収束, 後半=収束後を色分け)
half = len(sym_out) // 2
axes[1].scatter(sym_out[:half].real, sym_out[:half].imag,
                s=8, alpha=0.3, color="orange", label="収束前(前半)")
axes[1].scatter(sym_out[half:].real, sym_out[half:].imag,
                s=8, alpha=0.5, color="#1d3557", label="収束後(後半)")
axes[1].set_xlabel("同相成分 $I$"); axes[1].set_ylabel("直交成分 $Q$")
axes[1].set_title("同期前後の星座図")
axes[1].axis("equal"); axes[1].legend(); axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

タイミング推定の収束カーブ

収束カーブから、推定小数位置 $\mu$ が初期値から出発して小さなオーバーシュートを経て、数百シンボルのうちに一定値(およそ $0.70$)へ落ち着く様子が読み取れます。これはループが固定タイミングオフセットを捉えて、サンプリング点を最適位置にロックしたことを意味します。

同期前後の星座図とQPSK4点への収束

星座図では、引き込み中(左・オレンジ)の点が円状に散らばっている——タイミングずれと位相回転で振幅も向きも乱れている——のに対し、収束後(右・紺)の点は4つのQPSKシンボル位置にくっきり集まり、$40°$ 回転した理想点(赤×)にほぼ一致しています(収束後の誤差ベクトル振幅 EVM は約 $4.4\%$)。タイミングが合うことでシンボル間干渉が消え、各サンプルが本来のシンボル値を取り戻したのです。星座全体が回って見えるのは搬送波位相 $40°$ が残っているためで、これは後段の搬送波同期が担当します。Gardner検出器が位相に邪魔されずタイミングだけを正しく合わせ込んだ、という設計の狙いどおりの結果です。

タイミングが合うとは、波形を重ね描きした「アイダイアグラム」でいえばアイが最も開く中央で読み取ることに対応します。整合フィルタ後の波形を半シンボルずらして読む場合と、最適点を中央に揃えた場合を比べてみます。

アイダイアグラムによるタイミング整合の確認

左は最適点から $0.25$ シンボルずれた位置を中央に置いた場合で、中央(破線)でのアイの開きが小さく、判定マージンが乏しいことがわかります。右は最適点を中央に揃えた場合で、中央でアイが最大に開き、$\pm1$ の2値がはっきり分離しています。同期ループは、まさにこの「アイが最大に開く瞬間」を自動的に探し当てているわけです。

ステップ5: ループ帯域のトレードオフ

最後に、ループ帯域 $B_nT$ を変えると収束の速さと安定性がどう変わるかを確認します。

# ステップ3の gardner_loop を帯域だけ変えて呼び出す
plt.figure(figsize=(8, 5))
for bw in [0.005, 0.02, 0.05]:
    mus, _ = gardner_loop(rx, sps, bw)
    jitter = np.std(mus[-300:])     # 収束後のタイミングジッタ
    plt.plot(mus, lw=1, label=f"$B_nT$={bw} (定常ジッタ σ={jitter:.4f})")
plt.xlabel("出力シンボル番号"); plt.ylabel("推定小数位置 $\\mu$")
plt.title("ループ帯域による収束速度と安定性のトレードオフ")
plt.legend(); plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

ループ帯域による収束速度と定常ジッタのトレードオフ

このグラフから、ループ帯域 $B_nT$ を広げる(0.05、緑)ほど収束が速い一方で、収束後の $\mu$ がより細かく揺れている(定常ジッタ $\sigma\approx0.010$)ことが読み取れます。逆に帯域を狭くする(0.005、青)と収束はゆっくりですが、定常ジッタは $\sigma\approx0.0012$ と一桁小さく抑えられます。これがループ設計の本質的なトレードオフです。

雑音がある場合、このトレードオフはさらにはっきりします。受信信号に複素ガウス雑音を加え、SNRを振りながら収束後のタイミングジッタ($\mu$ の標準偏差)を測ってみます。

タイミングジッタとSNRの関係

このグラフから2つのことが定量的に読み取れます。第一に、どの帯域でもSNRが上がるほどジッタは単調に減少します。第二に、同じSNRなら狭帯域($B_nT$=0.005、緑)ほどジッタが小さい——たとえばSNR=15 dBで広帯域0.05のジッタが約 $0.025$ に対し、狭帯域0.005では約 $0.007$ と3倍以上低く抑えられています。実システムでは、起動時は広帯域で素早く引き込み、ロック後に狭帯域へ切り替える「ゲインスケジューリング」がよく用いられます。理論で述べた「帯域を狭くすれば雑音に強いが追従は遅い」という定性的な性質が、数値実験で定量的に裏付けられました。

まとめ

本記事では、Gardnerタイミング誤差検出器を中心に、全ディジタルなシンボル同期ループの理論と実装を解説しました。

  • Gardner誤差量: $e_k=\mathrm{Re}\{(y[k]-y[k-1])\,y^\*[k-\frac12]\}$ は、隣接シンボルの差と中間サンプルの積で「遷移の対称性のずれ」を測ります。1シンボルあたり2サンプル、判定値不要(NDA型)で動作します
  • S字カーブ: 誤差量の期待値はタイミング誤差 $\varepsilon$ の関数として原点を横切る $\sin(2\pi\varepsilon)$ 状の特性になり、その原点傾きが検出器利得 $K_d$ です。ロールオフ $\beta$ が大きいほど傾きが立ちます
  • 位相非依存性: 差の項に $e^{j\theta}$、共役を取る中間項に $e^{-j\theta}$ が掛かって相殺するため、搬送波位相 $\theta$ によらず誤差が変わりません。これによりタイミング同期と搬送波同期を独立に走らせられます
  • 同期ループ: ファロー補間器(任意小数位置の値を生成)+PI型2次ループフィルタ(周波数差まで追従)+NCO(サンプリング位相を管理)で閉ループを構成し、固定オフセットを定常誤差ゼロで吸収します
  • トレードオフ: ループ帯域 $B_nT$ が収束速度と定常ジッタのトレードオフを支配します

Gardner検出器は、搬送波同期がまだ確立していない受信機起動時でもタイミングを正しく合わせ込めるという実用上きわめて重要な性質を、誤差量の数式構造そのものから自然に獲得しています。ディジタル受信機の「最初のひと押し」を担う、シンプルかつ強力なアルゴリズムです。

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