PIC32のDMAモジュールで高速メモリー転送を実現する

Use Direct Access Memory module for PIC32
 
Direct Memory AccessはCPUを介さずにメモリー間のデータを転送する方法です。メモリーにデータを蓄積している間でもルーチンプログラムを動かし続けることができるので、オーディオストリーミングやディスプレイ出力など、組み込み機器で滞りなくデータを送受信し続けたい場合には必須のテクニックです。PIC32シリーズのマイコンではチャンネル単位で処理を確保し、PIC32MXにおいては最大4つの転送(4チャネル)を同時に行えます。

DMAを使用するに当たって必要なメモリーに関連する情報は「転送元の物理アドレス」「転送元のサイズ」「転送先の物理アドレス」「転送先のサイズ」「転送するサイズの上限」「1回につき転送するサイズ(4バイト単位、最大256バイト)」で、チャネル1に割り当てる場合はそれぞれ「DCH1SSA」「DCH1SSIZ」「DCH1DSA」「DCH1DSIZ」「DCH1CSIZ」となります。周辺モジュールからだけではなく、SRAM同士での転送にも使用できます。ただし、指定するアドレスは、マクロなどを使って仮想アドレスから変換する必要があります。
dmaaddress.c
#include <sys/kmem.h>

DCH1SSA = KVA_TO_PA((void*)&SPI1BUF); // DMA1転送元物理アドレス
DCH1SSIZ = 4;                         // DMA1転送元サイズ
DCH1DSA = KVA_TO_PA(active_frame);    // DMA1転送先物理アドレス
DCH1DSIZ = PCM_BUFFER_SIZE;           // DMA1転送先サイズ
DCH1CSIZ = 4;                         // 一度に転送するサイズ
転送を実行するタイミングは「CHSIRQ」に割り込みベクターの値を与えることで有効になります。例えば「DCH1ECONbits.CHSIRQ = _TIMER_1_IRQ」ならタイマーによる一定周期ごとに、「DCH1ECONbits.CHSIRQ = _SPI1_RX_IRQ;」なら、SPI1のデータ受信時に転送が実行されます。

DCHxININレジスタのビットを有効にすることで、それらの値に対応したDMA割り込みが発生します。「DCH1INTbits.CHBCIE = 1」であれば指定したサイズ分のデータ転送が完了したときに割り込まれるので、このタイミングで転送先のアドレスを参照すれば、転送済みであるすべてのデータを取得することができます。

ここで紹介するサンプルはSPIモジュールを使った、デジタルオーディオ信号形式のひとつであるPCM(I2Sと違い、データ信号が途切れないのが特徴)の転送方法です。終了割り込みが発生するたびに、転送先のアドレスを差し替えており、これによりダブルバッファ処理を実装しています。

SPIxBUFのサイズは32bitなので、16bitオーディオを送信する場合でも、バッファには32bitごとにデータを収納する必要がある、つまり、各バッファの上位16bitは詰めないで0x0000のままにしなくてはいけません。
dma.c
#include <p32xxxx.h>
#include <sys/kmem.h>
#include <sys/attribs.h>
#include <stdio.h>

#pragma config FNOSC = FRCPLL
#pragma config FPLLIDIV = DIV_2 // PLL入力分数 = x1/2
#pragma config FPLLMUL = MUL_20 // PLL逓倍比 = x20
#pragma config FPLLODIV = DIV_2 // PLL出力分数 = x1/2

#pragma config ICESEL = ICS_PGx3
#pragma config DEBUG  = ON
#pragma config FWDTEN = OFF

#define PCM_BUFFER_SIZE 32

char frame_buffer0[PCM_BUFFER_SIZE];
char frame_buffer1[PCM_BUFFER_SIZE];
char *active_frame = frame_buffer0;

void __ISR(_DMA1_VECTOR, IPL5AUTO) OnDma1(void)
{     
    if(DCH1INTbits.CHBCIE == 1){
        active_frame = (active_frame == frame_buffer0) ? frame_buffer1 : frame_buffer0;
        DCH1DSA = KVA_TO_PA(active_frame);

        DCH1CONbits.CHEN = 1;   // DMA1を有効にする
        
        DCH1INTbits.CHBCIF = 0; // ブロック終了割り込みフラグのクリア
    }
    IFS1bits.DMA1IF = 0;        // DMA1割り込みフラグのクリア
}

void InitSpi()
{
    SPI2CONbits.ON = 0;
  
    SDI2Rbits.SDI2R = 0b0011;  // B13をSDI2に割り当て
    SS2Rbits.SS2R = 0b0001;    // B14をSS2に割り当て
    
    SPI2CONbits.MSTEN = 0;     // SPI2スレーブモード
    SPI2CONbits.SPIFE = 1;     // フレーム同期モード
    SPI2CONbits.MODE32 = (PCM_BUFFER_SIZE == 32) ? 1 : 0; // 32bitモード
    SPI2CON2bits.IGNROV = 1;   // オーバーフローエラーを無視する

    SPI2CON2bits.AUDEN = 1;     // オーディオプロトコルを使用
    SPI2CON2bits.AUDMOD = 0b11; // PCM/DSPモード

    SPI2CONbits.ON = 1;         // SPI2を有効にする
}

void InitDma()
{
    IEC1bits.DMA1IE = 0;   // 設定のためDMA1を一時停止させる
   
    DCH1CON = 0;
    DCH1ECON = 0;
    DCH1INT = 0;

    DCH1CONbits.CHPRI = 3;              // DMAチャネルの優先度(3)
    DCH1ECONbits.CHSIRQ = _SPI2_RX_IRQ; // DMAを開始する割り込み番号
    DCH1ECONbits.SIRQEN = 1;            // 割り込みによる転送を有効にする
    DCH1INTbits.CHBCIE = 1;             // DMA割り込みを有効にする
    
    DCH1CONbits.CHEN = 1;               // DMA1を使用する
    
    DCH1SSA = KVA_TO_PA((void*)&SPI2BUF);
    DCH1SSIZ = 4;
    DCH1DSA = KVA_TO_PA(active_frame);
    DCH1DSIZ = PCM_BUFFER_SIZE;
    DCH1CSIZ = 4;
    
    IFS1bits.DMA1IF = 0;   // 割り込みフラグのクリア
    IPC10bits.DMA1IP = 5;  // 割り込み優先度
    IPC10bits.DMA1IS = 3;  // 副割り込み優先度
    IEC1bits.DMA1IE = 1;   // DMA1割り込みを有効にする
   
    DMACONbits.ON = 1;     // DMAモジュールを有効にする
}

int main(void)
{
    ANSELB = 0x00; 
    TRISB = 0xFFFFFFFF;
    
    InitSpi();
    InitDma();
      
    INTCONbits.MVEC = 1;
       
    __builtin_enable_interrupts();
     
    while(1) ;
}
割り込みフラグ解除はDMA割り込みだけでなく、DMAブロック終了割り込みなどの、関連する割り込み情報のフラグもリセットしないと次の割り込みが来ないので気をつけましょう。
2018/12/09