2012年8月28日火曜日

FPGA FMトランスミッタ: ステレオセパレーションの測定

FPGA FMステレオチューナに接続してステレオセパレーションを測ってみました。チューナのセパレーション特性と比較すると、1kHz以上はチューナの特性に依存していてそれ以下は、トランスミッタの特性が見えているように思います。デジタルフィルタの特性のためにf特が波を打つならわかりますが、低域に向けて単調にセパレーションが低下するのは何故でしょう。

ステレオセパレーション

Lch 1kHz 100%変調(Lチャンネル)

Lch 1kHz 100%変調(Rチャンネルへの漏話)

2012年8月11日土曜日

FPGA FMトランスミッタ: FM変調器

FM変調器については、過去の記事(FM変調器(1)FM変調器(2)FM変調器(3))に書いてますので、今回は具体的なところを書きます。と、言ってもお手軽なCoreGenを使いましたので簡単にできあがってしましました。

CoreGenの設定
DDSの位相アキュムレータは32ビットにしてあります。

DDS Compilerの設定1

DDS Compilerの設定2

DDS Compilerの設定3

DDS Compilerの設定4

DDS Compiler IP Symbol

周波数設定値
D/Aコンバータの動作クロックは77.824MHzですので、基本波としてFM放送波帯の周波数を生成することはできませんが、イメージ成分なら利用できます。例えば、83.5MHzが欲しいとき、DDSで5.676MHzを生成すれば、77.824MHz ±5.676MHz のイメージが出るので83.5MHzだけをフィルタで取り出すという手法があります。
※FMトランスミッタの製作スタート時の発想は、10.7MHzを生成して外付けのアップコンでFM放送波帯に変えようというものでした。

DDSの周波数設定値に適切な重み付けをした音声信号を重畳すればFM変調できたことになります。周波数偏移は、音声のフルスケールで200%変調、すなわち±150kHzになるように合わせます。DDSの動作クロックは77.824MHz、位相アキュムレータは32ビットなので周波数設定入力のMSBから9ビット目以降に音声信号 を接続すれば77.824MHz ×(2^23 / 2^32)  = 152kHzの変調度になります。このため事前に音声信号の振幅を150/152倍しておきます。(mod_level.v)
また、音声信号は2の補数形式のためストレートバイナリ形式に変換します。すると、入力信号のない無音時でも音声信号のMSBビットが立つことになり、あたかも周波数設定値にオフセットがかかったように見えます。従って周波数設定値は、このオフセット分を相殺した数値とします。

DDSの周波数設定値

測定結果
DDSで5.676MHzを発生させて、イメージの83.5MHzを見てみます。最初の2枚は、緑色がアナログ入力、黄色がデジタル入力で無音の場合です。デジタル入力の場合、ソースによってはDCオフセットがあるらしく、ぴょこっと周波数がずれてしまうことがありました。DCカット用にハイパスフィルタが必要だと思います。
3枚目は広帯域のスプリアスの様子です。77.824MHzのクロック周波数のほかイメージ成分2本が見えますが、わけのわからないノイズが多すぎます。スプリアスはフィルタを使えば取り除くことができるのでしょうが、仮に送信機のエキサイタとして利用するには素地が良くない気がしますが、チューナの試験用としてなら十分使えます。
訳のわからないノイズは、基板設計の拙さによって他のデバイスのノイズを受けてしまっているからと思っています。

83.5MHz ステレオモード(SPAN 200kHz)

83.5MHz モノラルモード(SPAN 200kHz)

スプリアス(83.5MHz、SPAN 40MHz)

次に、基本波を見てみます。最初の2枚は、緑色がアナログ入力、黄色がデジタル入力で無音の場合です。イメージよりノイズレベルが低く、コーデックのノイズがよく見えます。
3枚目は、広帯域(DC-20MHz)のスペクトラムで、目立ったスプリアスもなく良い感じです。やはりこれを所定の周波数までアップコンしたいところですね。
4枚目と5枚目は、1kHz 100%変調相当での占有周波数帯域幅です。

5.676MHz ステレオモード(SPAN 200kHz)

5.676MHz モノラルモード(SPAN 200kHz)

スプリアス( 5.676MHz、SPAN 20MHz)

5.676MHz ステレオモード占有帯域幅

5.676MHz モノラルモード占有帯域幅


fm_mod.v

module fm_mod(
  mod_i, mod_o, clk, reset, mod_set
  );

  input [23:0] mod_i;
  output [13:0] mod_o;
  input clk;
  input reset;
  input mod_set;

  wire [13:0] mod_o;
  wire [31:0] mod_data;
  reg [31:0] pinc_in;

//  parameter [24:0] FREQ_OFFSET = 32'h22B286BC;  // 10.7MHz
  parameter [31:0] FREQ_OFFSET = 32'h122BCA1A;  // 5.676MHz(83.5MHz)
//  parameter [24:0] FREQ_OFFSET = 32'h2229E50D7;  // 10.676MHz(88.5MHz)

  assign mod_data = {8'b0, (mod_i[23:0] ^ 24'h800000)};

  always @(posedge clk or posedge reset)
    begin
      if (reset)
        pinc_in <= FREQ_OFFSET + 32'h00800000;
      else
        pinc_in <= FREQ_OFFSET + mod_data;
    end

  dds_mod dds_mod (
    .clk(clk),
    .pinc_in(pinc_in),
    .sine(mod_o));

endmodule


mod_level.v

module mod_level(
  ml_i, ml_o,
  clk, reset, ml_thru
  );

  input [23:0] ml_i;
  output [23:0] ml_o;
  input clk;  // 64fs
  input reset;
  input ml_thru;

  reg [5:0] state64;
  reg [23:0] input_reg;
  reg [23:0] output_reg;
  reg [23:0] ml_o;
  wire latch_out;
  wire ml_thru;

  // State counter
  always @(posedge clk or posedge reset)
    begin
      if (reset)
        state64 <= 0;
      else if (state64 == 63)
        state64 <= 0;
      else
        state64 <= state64 + 1;
    end

  assign latch_out = (state64 ==7);

  // Attenuation
  always @(posedge clk or posedge reset)
    begin
      if (reset)
        begin
          input_reg <= 0;
          output_reg <= 0;
        end

      else
        begin
          case (state64)
            0:  input_reg <= ml_i;
            1:  // GAIN * (505/512)
              output_reg <= {input_reg[23], input_reg[23:1]}
                        + {{2{input_reg[23]}}, input_reg[23:2]};
            2:  output_reg <= output_reg
                        + {{3{input_reg[23]}}, input_reg[23:3]};
            3:  output_reg <= output_reg
                        + {{4{input_reg[23]}}, input_reg[23:4]};
            4:  output_reg <= output_reg
                        + {{5{input_reg[23]}}, input_reg[23:5]};
            5:  output_reg <= output_reg
                        + {{6{input_reg[23]}}, input_reg[23:6]};
            6:  output_reg <= output_reg
                        + {{9{input_reg[23]}}, input_reg[23:9]};
          endcase
        end
    end

  //  output data register
  always @(posedge clk or posedge reset)
    begin
      if (reset)
        ml_o <= 0;
      else if (ml_thru)
        ml_o <= ml_i;
      else if (latch_out)
        ml_o <= output_reg;
    end

endmodule

2012年8月10日金曜日

FPGA FMトランスミッタ: CICフィルタ

4,864kHzから77.824MHzへサンプルレート変換を行うためCICフィルタを使います。細かいことはわかりませんが、お手軽にCoreGenを使ってみます。

CoreGenの設定
CoreGen画面の中で特性を確認できます。通過域はコンポジット信号、阻止域はサンプルレート4,864kHzへのコンポジット信号の折り返しです。
サンプルレートの変換比16、CIC4段にしてあります。(3段にしたらうまく動かず・・・)

通過域(0~55kHz): 0 ~ 0.001414 [πrad/sample]
阻止域(4,809~4,919kHz): 0.1236 ~ 0.1264 [πrad/sample]

この周波数特性を見ると、阻止域のリップルが0.0066dBになっています。

CICフィルタの周波数特性

CIC Compilerの設定1

CIC Compilerの設定2

CIC Compilerの設定3

CIC Compiler IP Symbol

特性の測定
FIRフィルタと同様に152ksps 10kHzを信号源としてスペクトルを確認します。図で緑色がフィルタなし、黄色がフィルタありです。5MHz付近に向かってフィルタの効果が出ていることがわかります。

CICフィルタ出力(DC-5MHz)

CICフィルタ出力(DC-20MHz)

続いて、32倍オーバサンプリングFIRフィルタとCICフィルタをカスケード接続してみます。図で緑色がFIRフィルタのみ、黄色がFIRフィルタとCICフィルタの組み合わせです。ベースバンドをのぞきサンプルレート77.824MHzの半分の周波数までの信号が除去されていることがわかります。

FIR + CICフィルタ出力(DC-5MHz)

FIR + CICフィルタ出力(DC-20MHz)

FIR + CICフィルタ出力(15-50MHz)

cic16x.v

module cic16x(
  cic16x_i, cic16x_o, clk, reset, cic16x_thru
  );

  input [23:0] cic16x_i;
  output [23:0] cic16x_o;
  input clk;
  input reset;
  input cic16x_thru;

  wire [19:0] din;
  wire [31:0] dout;
  
  assign din = cic16x_i[23:4];
  assign cic16x_o = cic16x_thru ? cic16x_i : dout[31:8];

  // CIC Filter
  cicip cicip (
    .din(din),  // input [19:0] din
    .clk(clk),
    .dout(dout)  // output [31:0] dout
//    .rdy(rdy),  // output rdy
//    .rfd(rfd));  // output rfd
    );

endmodule



2012年8月5日日曜日

FPGA FMトランスミッタ: 32倍オーバーサンプリングFIRフィルタ

これまでの処理では、152kHzのサンプルレートで信号処理を行ってきました。
後段のFM変調器は512倍の77.824MHzで動作しているため、サンプルレートの変換が必要です。とはいえ、いきなり512倍のサンプルレート変換は難しいため32倍と16倍の2回に分けることにします。
今回は、この32倍オーバーサンプリングFIRフィルタについて書きます。
基本的なところは、15kHzローパスフィルタと同じで係数データを含めJA2SVZ様の指導を得ています。どうもありがとうございました。

入力データと係数の組み合わせ
FIRデジタルフィルタのアドレッシングはまるでパズルのようです。まずは入出力のサンプルレートが同じ場合を図にしてみます。
下の図は、512タップのFIRフィルタの入力データ(リングバッファ)のアドレスと係数ROMのアドレスの組み合わせを示しています。1回目、2回目と処理を追うごとに入力データのアドレスがずれていきますが、リングバッファのデータは常に更新されるので、左端のデータが最も新しく右側に行くほど古くなっています。
入出力のサンプルレートが同じ場合、512個の入力データと係数の掛け算の総和がデジタルフィルタの出力となります。

入出力サンプルレートが同じ

次は32倍オーバーサンプリングです。
出力のサンプルレートが入力の32倍なので、出力32サンプルにつき入力は1サンプルしかないので残り31サンプルは "0" で補完します。(ゼロパディング; zero padding)
この "0" と係数の掛け算の結果は "0" なので、"0" で補完されたデータの計算はしなくてすみます。結果的に、512タップと言いながら、16回の掛け算の総和でデジタルフィルタの処理ができることになります。

32倍オーバーサンプリング


タイミング関係
ROM、リングバッファの読み出し、乗算器処理のレイテンシを考慮したタイミング関係図です。デジタルフィルタ1サンプル分の16回の乗算に着目しています。

データ処理の時間関係


インパルス応答の確認
コードのデバッグでは、係数アドレスと係数値が比例(単調に増加)するテスト用 coeファイルで妥当なインパルス応答が出るまでシミュレーションと修正を繰り返します。

ISimのシミュレーション画面

テスト用 coeファイルのインパルス応答。

time, l55k_i, l55k_o
104986,020000, 0
105178,020000, 1
105370,020000, 2
105562,020000, 3
105754,020000, 4
105946,020000, 5
106138,020000, 6
106330,020000, 7

202522,000000, 508
202714,000000, 509
202906,000000, 510
203098,000000, 511
203290,000000, 0


最後に、本番用のcoeファイルに差し替えて、インパルス応答を確認します。

インパルス応答(シミュレーション結果)


特性の測定
このフィルタの入力にaf_oscモジュールから10kHz信号を接続。出力を14ビットD/A(サンプルレート77.824MHz)にそのまま接続して、出力のスペクトラムを観測しました。D/Aコンバータの出力にはトランスが入っているので低域まで特性が伸びていませんが傾向を掴めるかと思います。
図で緑色がフィルタなし、黄色がフィルタありです。低域からフィルタの出力サンプルレート4.864kHzまでフィルタの効果があることがわかります。ただ、期待値(70dB以上)ほど効果が見えません。4.864MHz付近に1本のスペクトルが見えますが、拡大すると10kHz信号の折り返しで2本あることがわかります。

出力スペクトラム(DC-10MHz)

出力スペクトラム(4.864MHz付近)

出力スペクトラム(DC-500kHz)

lpf55k.v

module lpf55k(
  l55k_i, l55k_o, clk, reset, l55k_thru
  );

  input clk;
  input reset;
  input l55k_thru;
  input [23:0] l55k_i;
  output [23:0] l55k_o;

  reg [12:0] state512x16;
  reg init_cycle;
  wire [3:0] ring16;
  wire [3:0] state16;
  wire acc_init;
  wire acc_out;

  wire [8:0] COEF_ADDR;
  wire [17:0] COEF_DATA;
  wire [3:0] RING_RADDR;
  wire [17:0] RING_RDATA;
  wire [3:0] RING_WADDR;
  wire [17:0] RING_WDATA;
  wire [0:0] RING_WEN;
  wire [17:0] mul_in1;
  wire [17:0] mul_in2;
  wire [35:0] mul_out;
  wire [27:0] acc_in;  // 24bit + 4bit
  reg [27:0] acc;    // 24bit + 4bit
  reg [23:0] l55k_o;

  ////   state counters and control signals   ////
  always @(posedge clk or posedge reset) begin
    if (reset)
      begin
        state512x16 <= 0;
        init_cycle <= 1;
      end
    else if (state512x16 == 8191)
      begin
        state512x16 <= 0;
        if (init_cycle == 1)
          init_cycle <= 0;
      end
    else
      state512x16 <= state512x16 + 1;
  end

  assign ring16 = state512x16[12:9];
  assign state16 = state512x16[3:0];
  assign acc_init = (state16 == 5);
  assign acc_out = (state16 == 5) && (~init_cycle);
  assign RING_WEN = (state512x16[8:0] == 0);

  ////   ring-buffer RAM and coefficient ROM address generation   ////
  assign COEF_ADDR = {state16, state512x16[8:4]};
  assign RING_WADDR = - ring16;
  assign RING_RADDR = - ring16 + state16;
  
  ////   input data acquisition to ring-buffer RAM   ////
  assign RING_WDATA = l55k_i[23:6];

  ////   ring-buffer RAM and coefficient ROM read, input MUX and multiplier   ////
  assign mul_in1 = COEF_DATA;
  assign mul_in2 = RING_RDATA;
  assign acc_in = {{4{mul_out[34]}}, mul_out[34:11]};

  ////   accumulator   ////
  always @(posedge clk or posedge reset) begin
    if (reset)
      acc <= 0;
    else
      acc <= (acc_init)? acc_in : acc + acc_in;
  end

  ////   output data register   ////
  always @(posedge clk or posedge reset) begin
    if (reset) 
      l55k_o <= 0;
    else if (l55k_thru) 
      l55k_o <= l55k_i;
    else
      if (acc_out)
        begin
          if (~acc[27] && !(acc[26:23] == 4'b0000))
            l55k_o <= 8388607;    // 24-bit positive max
          else if (acc[27] && !(acc[26:23] == 4'b1111))
            l55k_o <= -8388608;    // 24-bit negative max
          else
            l55k_o <= acc[23:0];
        end
  end

  ////   memory and multiplier instantiation (Xilinx)   //// 
  lpf55k_coef U1 (
    .clka(clk),
    .rsta(reset),
    .addra(COEF_ADDR),
    .douta(COEF_DATA)
    );

  lpf55k_rbuf U2 (
    .clka(clk),
    .wea(RING_WEN),
    .addra(RING_WADDR),
    .dina(RING_WDATA),
    .clkb(clk),
    .rstb(reset),
    .addrb(RING_RADDR),
    .doutb(RING_RDATA)
    );

   mul_p U3 (
    .clk(clk),
    .sclr(reset),
    .a(mul_in1),
    .b(mul_in2),
    .p(mul_out)
    );

endmodule


Block Memory Generator(ring-buffer RAM)の設定
リングバッファ用デュアルポートRAMです。読み出しのレイテンシは2クロックです。
Total Port B Read Latency (From Rising Edge of Read Clock): 2 Clock Cycles









Block Memory Generator(coefficient ROM)の設定
係数用ROMです。読み出しのレイテンシは2クロックです。
Total Port A Read Latency (From Rising Edge of Read Clock): 2 Clock Cycles









Multiplierの設定
乗算器です。演算のレイテンシは3クロックです。
ここは、15kHz ローパスフィルタの記事と同一なので割愛します。

フィルタ係数COEファイル
・タップ数: 512
・入力サンプルレート: 152kHz
・出力サンプルレート: 4,864kHz (152kHz×32)
・通過域: 0~55kHz
・阻止域: 97kHz~

MEMORY_INITIALIZATION_RADIX=2;
MEMORY_INITIALIZATION_VECTOR=
000000000011100000,
111111111110101010,
111111111110110010,
111111111110110101,
111111111110110101,
111111111110110001,
111111111110101100,