ZYBOで遊ぶ02:AXI CDMA IPを使ってみた(2)

ZYBOで遊んだメモ

ZYBOで遊ぶ02:AXI CDMA IPを使ってみた(1) - ThuruThuruToru’s blog の続き


ZYBOで遊ぶ02:AXI CDMA IPを使ってみた(1) - ThuruThuruToru’s blog
ZYBOで遊ぶ02:AXI CDMA IPを使ってみた(2) - ThuruThuruToru’s blog



2. AXI CDMAの最低限の仕様を確認

Xilinx SDKでSWを書き始める前に,AXI CDMAの動かし方を調べる.

XilinxのWebページ XilinxのAXI CDMA のPG034 - AXI Central Direct Memory Access v4.1 Product Guide に仕様書が置かれている.

Register Details, Programming Sequence の節を読めば動かし方がなんとなくわかる.

2.1. 制御レジスタ

今回はScatter/Gatherはなしなので,以下の制御レジスタを使うだけで動かせる.

  • CDMACR
    • Err_IrqEn
      • 1にセットでエラー時の割り込みを有効化
    • IOC_IrqEn
      • 1にセットで転送完了割り込みを有効化
  • CDMASR
    • Err_Irq
      • エラー発生時に1がセットされる
      • 1をWriteするとクリア
    • IOC_Irq
      • 転送完了時に1がセットされる
      • 1をWriteするとクリア
    • DMADecErr, DMASlvErr, DMAIntErr
      • エラー情報が読める
    • Idle
      • Idle状態なら1が読める
  • SA
    • Sourceアドレスを指定する
  • DA
  • BTT
    • 転送するバイト数を指定する
    • このレジスタへのWriteでDMA転送がキックされる

2.2. 制御の流れ

SWは以下の処理を行えば良い

  1. 割り込みハンドラの設定
  2. CDMASR.IDLE=1を確認
  3. CDMACR.IOC_IrqEnとCDMACR.IrqEnに1をセット
  4. SAに転送元のアドレスをセット
  5. DAに転送先のアドレスをセット
  6. BTTに転送するバイト数をセットし,DMA転送を開始させる
  7. 割り込みが上がるのを待つ
  8. 割り込み処理を行う
    • CDMASR.Err_Irq, CDMASR.IOC_Irqを確認し,割り込み要因をクリアする
  9. 上記2に戻る

3. Xilinx SDK で DMACを操作するSWを書く

2つバッファを確保し,DMAでデータを転送するSWを書く. 今回は勉強のため, AXI CDMA用の関数(xaxicdma.h内で宣言)は使わず,制御レジスタを自分で操作する.

コード全部を載せると長くなるので完成したコードは ZYBOで遊ぶ02 · GitHub に置いた.

DMACキック後は割り込みが上がるのを待つようにし,割り込みハンドラでは割り込み要因のクリアを行った後,転送先のバッファのデータをUARTを使ったPC側のターミナルに表示させる.

3.1. プロジェクト作成

File->New->Application Projectをクリック.
Project nameを埋めて,Nextボタンをクリック.
TemplatesにHello Worldを指定してFinishボタンをクリック.

3.2. 制御レジスタ関連の定数の定義

xaxicdma.h にCDMAを操作するための関数,構造体,定数が宣言されているが,今回はこれらを使わずに自分で定数の定義をおこない,制御レジスタをひとつひとつ読み書きして操作する.

#define CDMA_BASE_ADDR XPAR_AXI_CDMA_0_BASEADDR
#define CDMA_CR_ADDR  CDMA_BASE_ADDR+0x00
#define CDMA_SR_ADDR  CDMA_BASE_ADDR+0x04
#define CDMA_SA_ADDR  CDMA_BASE_ADDR+0x18
#define CDMA_DA_ADDR  CDMA_BASE_ADDR+0x20
#define CDMA_BTT_ADDR CDMA_BASE_ADDR+0x28

#define CDMA_CR_ERR_IRQ_MASK 0x00004000
#define CDMA_CR_IOC_IRQ_MASK 0x00001000

#define CDMA_SR_ERR_IRQ_MASK 0x00004000
#define CDMA_SR_IOC_IRQ_MASK 0x00001000
#define CDMA_SR_DEC_ERR_MASK 0x00000040
#define CDMA_SR_SLV_ERR_MASK 0x00000020
#define CDMA_SR_INT_ERR_MASK 0x00000010
#define CDMA_SR_IDLE_MASK    0x00000002

3.3. バッファの確保

転送元のバッファと転送先のバッファを以下の様に静的に確保する.

volatile u8 src_buf[BUF_BYTE_NUM] __attribute__ ((aligned(32)));
volatile u8 dst_buf[BUF_BYTE_NUM] __attribute__ ((aligned(32)));

バス幅が64bitなので8Byteアライン指定で良いかと思ったが,8Byteアラインとするとキャッシュのinvalidateが一部うまくいかなかった(Xil_DCacheInvalidateRange関数の説明を読めば謎が解けるはずなので読む).
キャッシュラインサイズに合わせておけばキャッシュ操作もうまくいくと考え,Cortex-A9のキャッシュラインサイズは32Byteのようなので32Byteアラインとした. BUF_BYTE_NUMは自分で定義した定数で,今回は128Byteとした.

3.4. バッファに初期値をセットする

以下のset_data関数で転送元のバッファには0, 1, 2,・・・という様なインクリメントした値を順にセットし, 転送先のバッファにはall 0 データをセットする.

void set_data(u8 *buf_ptr, u32 num, int mode/* 0:incr/1:zero*/)
{
    int i;

    print("\n[set_data]\n\r");
    for (i = 0; i < num; i++) {
        buf_ptr[i] = (mode) ? 0 : i;
    }

    // flush data
    Xil_DCacheFlushRange((u32)buf_ptr, num);
}

重要なのが最後の Xil_DCacheFlushRange関数の実行.
この関数で明示的にキャッシュフラッシュをしないと,転送元のバッファにセットしたデータがDRAMまで書き込まれず,DMA転送実行後の結果が意図した通りにならない.
最初はキャッシュの存在を意識していなかったため,データが化ける?????としばらく悩んだが,キャッシュをフラッシュすることで解決.

3.5. DMAキック前のバッファ内のデータ確認

転送元と転送先のバッファ内のデータをprint_buf関数で,PCのターミナルに表示させ,値を確認する.

3.6. 割り込み設定

SetupIntrSystem関数に記述.
基本的には ZYBOで遊ぶ01:簡易タイマーIPの自作(4) - ThuruThuruToru’s blog と同じだが,

typedef struct {
  u8 *src_buf_ptr;
  u8 *dst_buf_ptr;
} BufSt;

という転送元,転送先バッファのポインタをまとめた構造体を定義し,割り込みハンドラにこの構造体のポインタを渡すようにしている.

3.7. 割り込みハンドラ

cdma_int_handler関数に記述.
CDMAが割り込みをあげる度にこの関数が読み出される.
引数には,割り込み設定で指定した BufSt構造体のポインタが渡される.

void cdma_int_handler(void *callback)
{
  u32 data32;
  char str[STR_BYTE_NUM];

  print("\n[cdma_int_handler]\n\r");

  // check SR
  data32 = Xil_In32(CDMA_SR_ADDR);
  snprintf(str, BUF_BYTE_NUM, "CDMA_SR = %lx\n\r", data32);
  print(str);
  if (data32 & CDMA_SR_ERR_IRQ_MASK)
    print("ERR_IRQ\n\r");
  if (data32 & CDMA_SR_IOC_IRQ_MASK)
    print("IOC_IRQ\n\r");
  if (data32 & CDMA_SR_DEC_ERR_MASK)
    print("DEC_ERR\n\r");
  if (data32 & CDMA_SR_SLV_ERR_MASK)
    print("SLV_ERR\n\r");
  if (data32 & CDMA_SR_INT_ERR_MASK)
    print("INT_ERR\n\r");
  if (data32 & CDMA_SR_IDLE_MASK)
    print("IDLE\n\r");

  // clear interrupt
  print("clear interrupt\n\r");
  data32 = CDMA_SR_ERR_IRQ_MASK | CDMA_SR_IOC_IRQ_MASK;
  Xil_Out32(CDMA_SR_ADDR, data32);

  // check SR
  data32 = Xil_In32(CDMA_SR_ADDR);
  snprintf(str, BUF_BYTE_NUM, "CDMA_SR = %lx\n\r", data32);
  print(str);

  // dcache invalidate
  print("dcache invalidate\n\r");
  Xil_DCacheInvalidateRange((unsigned int)((BufSt *)callback)->dst_buf_ptr, BUF_BYTE_NUM);

  // print dst buffer
  print_buf(((BufSt *)callback)->dst_buf_ptr, BUF_BYTE_NUM);
}

処理としては,割り込みクリア,データキャッシュinvalidate,転送先バッファ内のデータの表示をしている.

初期データの確認を3.5で行ったので,転送先バッファのデータはデータキャッシュに格納された状態となっている.そのため,データキャッシュのinvalidateをしないと,DMACがDRAMに転送したデータではなく,キャッシュにヒットした古いデータを読み出してしまう.
ここもはまった.

3.8. CDMA初期化

init_cdma関数に以下の処理を記述.

  • CDMASR, CDMACR を読んでIDLEであることを確認(IDLEになるまでポーリング)
  • CDMACR.IOC_IrqEnとCDMACR.IrqEnに1をセット

3.9. DMACキック

trans_data関数内に,CDMAの制御レジスタへ転送元,転送先アドレスと転送バイト数を書き込む処理を記述.

3.10. 動かす

転送先のバッファに転送元のバッファのデータが書き込まれることが確認できるはず.

qと打つとプログラムが終了するようにしている.

まとめ

AXI CDMA を使って DRAM<->DRAM のデータ転送を行ってみた.
次は AXI VDMA(AXI4 <-> AXI4-Stream)を使ってみる予定.