FPGA 版 FM 音源 (79) -- TX7 (OPS) 測定 (6)

今回はソフトウェアについて触れます。
まず、DMA チャネルの初期化部分のソースを下に示します。 16 ビット・パラレル・データ転送を 8 ビット DMA ふたつ (DMA_H, DMA_L) で実現しており、両者の内容はほとんど同じなので、代表として DMA_L の方だけを示します。

#define N_POLY (16)

volatile int flag = 0;
volatile int val_ofs = 0;

uint8_t da_val_l[2*N_POLY];

/* Defines for DMA_L */
#define DMA_L_BYTES_PER_BURST 1
#define DMA_L_REQUEST_PER_BURST 1
#define DMA_L_SRC_BASE (CYDEV_PERIPH_BASE)
#define DMA_L_DST_BASE (CYDEV_SRAM_BASE)

static uint8 DMA_L_Chan;
static uint8 DMA_L_TD[2];

void setup_DMA_L( void )
{
/* DMA Configuration for DMA_L */
DMA_L_Chan = DMA_L_DmaInitialize(DMA_L_BYTES_PER_BURST, 
                                 DMA_L_REQUEST_PER_BURST, 
                                 HI16(DMA_L_SRC_BASE), 
                                 HI16(DMA_L_DST_BASE));
DMA_L_TD[0] = CyDmaTdAllocate();
DMA_L_TD[1] = CyDmaTdAllocate();
CyDmaTdSetConfiguration(DMA_L_TD[0], N_POLY, 
                        DMA_L_TD[1], (CY_DMA_TD_INC_DST_ADR | DMA_L__TD_TERMOUT_EN));
CyDmaTdSetConfiguration(DMA_L_TD[1], N_POLY, 
                        DMA_L_TD[0], (CY_DMA_TD_INC_DST_ADR | DMA_L__TD_TERMOUT_EN));
CyDmaTdSetAddress(DMA_L_TD[0], LO16((uint32)DA_REG_L_Status_PTR), 
                               LO16((uint32)&da_val_l[0]));
CyDmaTdSetAddress(DMA_L_TD[1], LO16((uint32)DA_REG_L_Status_PTR), 
                               LO16((uint32)&da_val_l[N_POLY]));
CyDmaChSetInitialTd(DMA_L_Chan, DMA_L_TD[0]);
CyDmaChEnable(DMA_L_Chan, 1);
} // void setup_DMA_L()

CY_ISR(DMA_L_isr_InterruptEx)
{
    uint8_t curr_td;
    
    CyDmaChStatus(DMA_L_Chan, &curr_td, NULL);
    val_ofs = (N_POLY * (DMA_L_TD[0] == curr_td));
    flag |= 0x01;
} // CY_ISR()

メモリ内に DMA バッファとして、16 エントリ × 2 面の 32 エントリの 8 ビット・データ配列 (da_val_h、da_val_l) を確保しています。
DMA の TD (Transaction Descriptor) として、下の図のように、転送カウント数 16 の DMA_L_TD[0], DMA_L_TD[1] を定義し、お互いを連結して環状リストとしています。

DMA_L_TD[0] の DST_ADR として &(da_val_l[0])、DMA_L_TD[1] の DST_ADR として &(da_val_l[N_POLY]) を定義して、ダブル・バッファを実現しています。
それぞれの TD の属性として、終了割り込み発生が定義してあり、呼び出される DMA_L_isr_InterruptEX 関数でメインループ側で読み出す DMA バッファの「面」を切り替えます。
ハード的に DMA_H と DMA_L との両方に同じデータ・リクエスト信号が供給されているので、DMA 要求は DMA_H 側と DMA_L 側とで同時に発生しますが、実際には DMA チャネルの優先度の高い方が先にサービスされます。
初期化コードの中では特に優先度の設定はありませんが、回路図作成時の DMA コンポーネントの配置順からシステム側が回路合成/API 生成時に勝手に設定するようです。
優先度の設定に依存せず、DMA_H 側と DMA_L 側のどちらの終了割り込み関数が先にサービスされても問題ないように、DMA の「面」切り替えは両方の関数で行ない、転送完了を示すフラグは H 側、 L 側それぞれ別のビットを操作するようにしています。
メイン・ループ側では両方のビットのフラグが立っているのを検出すると、書き込みが終了した「面」のバッファから 16 音分のデータを読み出し、累算して 48 kHz サンプルひとつを作りだします。
次にメイン・ループ部を示します。

  int     i, j;
  int     acc;
  int     mant;
  int     scale;
  uint8_t dac_h;
  uint8_t dac_l;

. . . . . <中略> . . . . .

  while (1) { // main loop
    if (0x03 == flag) { // buffer filled
      flag = 0; // clear flag
      acc  = 0; // clear accumulator
      j = val_ofs;
      for (i = 0; i < N_POLY; i++) {
        dac_h = da_val_h[j]; // DAC hi byte
        dac_l = da_val_l[j]; // DAC lo byte
        mant  = (((0x0f & (uint16_t)dac_h) << 8) | dac_l);
        mant -= 2048; // convert to 2's complement
        scale = ((0xf0 & dac_h) >> 4); // scale factor
        mant *= scale; // apply scaling
        acc  += mant;  // accumulate
        j++;           // advance array index
      } // for (i = 0; ...
      if      ( 32767L < acc) { acc =  32767L; }
      else if (-32768L > acc) { acc = -32768L; }
      dac_h = ((uint16_t)acc) >> 8;
      dac_l =  (uint8_t )acc;
      fifo[wr_ix++] = dac_l;
      fifo[wr_ix++] = dac_h; // L-ch
      fifo[wr_ix++] = dac_l;
      fifo[wr_ix++] = dac_h; // R-ch
      if (FIFO_BYTES <= wr_ix) {
        wr_ix = 0; // wrap around
      } // if (FIFO_BYTES <= ...
    } // if (0x03 == sem) { ...
    if (SPDIF0_AUDIO_0_FIFO_NOT_FULL & SPDIF0_ReadStatus() ) { // FIFO has room
      SPDIF0_WriteTxByte(fifo[rd_ix], 0x01 & (rd_ix >> 1));
      if (FIFO_BYTES <= (++rd_ix)) {
        rd_ix = 0; // wrap around
      } // if (FIFO_BYTES <= ...
    } // if ()
  } // while (1) { ...

変数「flag」の値が 0x03 であれば H 側 L 側ともに 16 音分の DMA 転送が完了したということですから、DMA バッファから値を読み出し、変換して、16 音分を加算して 48 kHz サンプルひとつ分を作りだします。 そうでなければ、DMA バッファについては何もしません。
次に、SPDIF モジュールのデータ FIFO に「空き」があるかどうかを調べて、空きがあればメモリ中の fifo[] 配列から 1 バイト読み出してデータ FIFO に書き込みます。
OPS からの信号 SF0 〜 SF3 は、12 ビット DAC 信号 DA[12:1] のシフト/スケーリング量を示すものです。
SF0 / SF1 / SF2 / SF3 の値は互いに排他的で、常にどれかひとつだけが「1」となり、他の 3 つは「0」となります。
アナログ的には、

  • SF0 == 1 なら、DAC 出力値そのまま
  • SF1 == 1 なら、DAC 出力値を 1/2 に
  • SF2 == 1 なら、DAC 出力値を 1/4 に
  • SF3 == 1 なら、DAC 出力値を 1/8 に

という操作をします。
アナログ的に SFn の「n」ビット右シフトを実行していることになります。
ディジタル的には、右シフトで下位ビットが「桁落ち」するのは好ましくなく、逆に、

  • SF0 == 1 なら、DA[12:1] を 8 倍に (15 ビットに拡大)
  • SF1 == 1 なら、DA[12:1] を 4 倍に (14 ビットに拡大)
  • SF2 == 1 なら、DA[12:1] を 2 倍に (13 ビットに拡大)
  • SF3 == 1 なら、DA[12:1] そのまま (12 ビットそのまま)

と、(3 - n) ビット左シフトして、下位ビットが失われないようにします。
SF0, SF1, SF2, SF3 をこの順番に並べ、「2 進数」として読んだものは上に示す「倍率」の値となっています。
ます、下のように「仮数部」(mantissa) として、上位 8 ビット、下位 8 ビットから抽出したストレート・バイナリの 12 ビット・データを 2 の補数に変換します。

        mant  = (((0x0f & (uint16_t)dac_h) << 8) | dac_l);
        mant -= 2048; // convert to 2's complement

上位 8 ビットに含まれる SF0 〜 SF3 を「倍率」として、

        scale = ((0xf0 & dac_h) >> 4); // scale factor
        mant *= scale; // apply scaling
        acc  += mant;  // accumulate
        j++;           // advance array index

のようにスケーリングを施し、累算して、次のデータに進みます。