FPGA 版 FM 音源 (25) -- EG シミュレーション (1)

約 3 年間放置していた「FPGA 版 FM 音源」を再開しました。
アタックとディケイのみを実装した EG を Verilog-HDL で書いて「ModelSim」でシミュレーションを行い、以前の記事の C プログラムでの実現と同様の結果を得ました。
アタック/ディケイ・タイムが長くてシミュレーションでは確認が困難な領域については、実際に FPGA 上に「ハード」をインプリメントして確認しました。
Altera の (無印) Cyclone をターゲットにして、 EG 部分は 105 ロジック・セルとなりました。
具体的には、CQ 出版社の DESIGN WAVE MAGAZINE 2003 年 10 月号付属の FRK1C3 基板 (EP1C3T100 使用) を使いました。
ModelSim の WAVE 画面のキャプチャを下に示します。 (クリックで拡大します)

opl3_eg_scr_thumb_131204.jpg

AR (アタック・レート) と DR (ディケイ・レート) を同じ値に設定してあり、AR/DR = 7 の Rof = 0 から AR/DR = 15 の Rof = 3 までをシミュレーションしています。
上下中央やや上のシアン色のトレースが EG 出力値 (減衰量の対数) を ModelSim の「アナログ波形」表示機能を使って表示したもので、中央やや下の赤色のトレースが EG 出力をリニア値に変換した結果のアナログ波形表示です。
赤色のトレースの下が AR/DR の値で、その下が Rof の値です。
アタック/ディケイ・タイムの長い領域についてはゲート・タイムが十分ではなく、ディケイの途中でエンベロープが強制的に落とされています。
下に AR/DR = 14、Rof = 1 の部分を拡大して、ディジタル・データの数値が読める状態にしたものを示します。 (クリックで拡大します)

opl3_eg_ar14_rof1_scr_131208_thumb.jpg

各トレースは、上から順に、

  • クロック
  • ゲート信号
  • (EG 外部回路の)ゲート・カウンタ値
  • EG 出力値「eg_out」
  • リニア値に変換した「lin_out」

となっており、一番下のトレースがシフト量「shift_amount」です。
アタック部分の eg_out、eg_lin、shift_amount は、2011 年 1 月 31 日の記事 (→こちら) の表と完全に一致しています。
レート・マルチプライア内のフリーラン・オクタル・カウンタの位相と、ゲート信号のタイミングとの関係により、この数値の系列は変化しますが、他の位相については確認していません。
最後に、EG 部分の Verilog-HDL ソースを下に示します。
これは主にレート・マルチプライアによるシフト量/クロック間引き機能の検証のためで、アタック/ディケイしか実装していないこともあり、コントロール部分については「動けばいい」という程度のレベルになっています。
本来は AR/DR = 0 の場合にはエンベロープの更新を停止 (アタック/ディケイ・タイム無限大) しなければなりませんが、それも実装していません。

// OPL3 EG (Attack / Decay only)
// 2013/12/04 Created by pcm1723
//
module opl3_eg(reset, m_clk, clk_en, gate, egt,
               rof, ar, dr, sl, rr,
               eg_out);
               
  input         reset;  // reset for simulation
  input         m_clk;  // master clock
  input         clk_en; // clock enable
  input         gate;   // GATE signal
  input         egt;    // EG type
  input  [1:0]  rof;    // Rof (0..3)
  input  [3:0]  ar;     // Attack  Rate  (0..15)
  input  [3:0]  dr;     // Decay   Rate  (0..15)
  input  [3:0]  sl;     // Sustain Level (0..15)
  input  [3:0]  rr;     // Release Rate  (0..15)

  output [8:0]  eg_out; // EG output
  
  reg    [2:0]  rm_cnt; // rate multiplier octal counter
  reg    [8:0]  eg_acc; // EG accumulator
  wire   [8:0]  eg_inc; // EG increment
  reg           gate_dly; // delay element for GATE
  wire   [3:0]  rate;     // AR/DR/RR muxed RATE
  wire   [1:0]  shift_amount; // shift amount for eg_inc
  reg    [11:0] psc_cnt;     // prescaler conter
  reg    [11:0] psc_cmp_reg; // prescaler compare register
  wire   [11:0] psc_mask;    // prescaler compare mask
  reg           decay_state;
  reg           mute_state;
  
// AR/DR/RR rate selector
  assign rate = ( decay_state ? dr : ar);
  
  assign eg_out = eg_acc;
  
// start of Attack
  assign A_trig = (gate & (~gate_dly));  
  
// start of Decay (EG acc zero detection)
  assign D_trig = (gate & ~(| eg_acc));  
  
// mute condition detection (level < -94.5 dB)
  assign below_94dB = (decay_state & (&eg_acc[8:4]));
    
// (0 != eg_acc) indication
  assign acc_non_zero = (A_trig | ( | eg_acc ));  

// lo rate prescaler enable flag (rate < 12)  
  assign psc_mode     = ~( & rate[3:2]);
  
// rate flags  
  assign rate_eq_13 = (13 == rate); 
  
  assign rate_eq_14 = (14 == rate); 
  
  assign rate_eq_15 = (15 == rate); 
  
  assign hi_rate = (rate_eq_15 | rate_eq_14 | rate_eq_13);
  assign lo_rate = ~hi_rate;
  
// rate multiplier output
  assign eg_sft_sel = ( (rm_cnt[1])
                        ?  (rof[0] & rm_cnt[2])
                        :  (rof[1]) ); 
                        
  assign eg_clk_en = (hi_rate | eg_sft_sel | (~rm_cnt[0]));  
  
// EG increment shift amount
  function [1:0] shift_calc;
    input       lo_rate;
    input [1:0] rate;
    input       eg_sft_sel;  
    
    case ({lo_rate, rate, eg_sft_sel})
      4'b0_01_0 : shift_calc = 3;
      4'b0_01_1 : shift_calc = 2;
      4'b0_10_0 : shift_calc = 2;
      4'b0_10_1 : shift_calc = 1;
      4'b0_11_0 : shift_calc = 1;
      4'b0_11_1 : shift_calc = 1;
      default   : shift_calc = 3;
    endcase
  endfunction
  
  assign shift_amount = shift_calc(lo_rate, 
                                   rate[1:0], 
                                   eg_sft_sel);

// EG increment shifter
// fixed pattern for DR/RR
// EG feedback (inverted) for AR
  assign eg_inc = ( ( (decay_state) 
                      ?  (9'b0000_01000)   // for Decay
                      :  {3'b111, ~eg_acc} // for Attack 
                    ) >> shift_amount
                  );

// prescaler compare
  function [11:0] psc_mask_calc;
    input [3:0] rate;
    case (rate)
      1  : psc_mask_calc = 11'b11111111111; // care all
      2  : psc_mask_calc = 11'b01111111111; // omit b10
      3  : psc_mask_calc = 11'b00111111111; // omit b10..9
      4  : psc_mask_calc = 11'b00011111111; // omit b10..8
      5  : psc_mask_calc = 11'b00001111111; // omit b10..7
      6  : psc_mask_calc = 11'b00000111111; // omit b10..6
      7  : psc_mask_calc = 11'b00000011111; // omit b10..5
      8  : psc_mask_calc = 11'b00000001111; // omit b10..4
      9  : psc_mask_calc = 11'b00000000111; // omit b10..3
      10 : psc_mask_calc = 11'b00000000011; // omit b10..2
      11 : psc_mask_calc = 11'b00000000001; // omit b10..1
      default : psc_mask_calc = 11'b00000000000; // ignore all
    endcase
  endfunction
  
  assign psc_mask = psc_mask_calc(rate);
  
  assign psc_match = ~( | (psc_mask & (psc_cnt ^ psc_cmp_reg)));  
  
  assign psc_clk_en = (psc_match);
  
// EG accumulator
  always @(posedge reset or posedge m_clk)
    begin
      if (reset)
        eg_acc <= 9'b1111_11111;
      else
        if (clk_en & (decay_state | acc_non_zero))
          begin
            if ((~A_trig) & (mute_state | (~gate)))
              eg_acc <= 9'b1111_11111 ;
            else
              if (eg_clk_en & psc_clk_en)
                eg_acc <= ( (A_trig & rate_eq_15)
                            ?  (9'b0000_00000)
                            :  (eg_acc + eg_inc) );
          end
    end // always
    
// rate multiplier octal counter (free running)
  always @(posedge reset or posedge m_clk)
    begin
      if (reset)
        rm_cnt <= 0;
      else
        if (clk_en & psc_clk_en)
          begin
            rm_cnt <= (rm_cnt + 1); 
          end
    end // always
    
// clock prescaler counter for EG acc (free running)
  always @(posedge reset or posedge m_clk)
    begin
      if (reset)
        psc_cnt <= 0;
      else
        if (clk_en)
          begin
            psc_cnt <= (psc_cnt + 1); 
          end
    end // always
    
// clock prescaler compare register
  always @(posedge reset or posedge m_clk)
    begin
      if (reset)
        psc_cmp_reg <= 0;
      else
        if (clk_en & (A_trig | (D_trig & ~decay_state)))
          begin
            psc_cmp_reg <= psc_cnt; // remember counter value
          end
    end // always
    
// gate signal delay element
  always @(posedge reset or posedge m_clk)
    begin
      if (reset)
        gate_dly <= 0;
      else
        if (clk_en)
          gate_dly <= gate; 
    end // always
    
// decay state
  always @(posedge reset or posedge m_clk)
    begin
      if (reset)
        decay_state <= 0;
      else
        if (clk_en)
          begin
            if (~gate)
              decay_state <= 0;
            else
              if (D_trig)
                decay_state <= 1; 
          end // if (clk_en)
    end // always
    
// mute state
  always @(posedge reset or posedge m_clk)
    begin
      if (reset)
        mute_state <= 0;
      else
        if (clk_en)
          begin
            if (A_trig) // exit mute state at attack
              mute_state <= 0;
            else
              if (below_94dB) // mute condition
                mute_state <= 1; 
          end // if (clk_en)
    end // always
    
endmodule // opl3_eg()