Kinetis マイコンのDMA転送スループットを測定してみた


今回は、KinetisマイコンのDMA転送は、どのくらいの転送スピード(スループット)があるのか気になったので、測定してみました。
DMA転送は、RAM-TO-RAM転送、ROM-TO-RAM転送の2種類です。Kinetisマイコンでは、DMAのRAMアクセスは、ノーウェイトですが、ROM(FlashROM)アクセスの場合はウェイトが入ります。

RAMや ROMへの転送でスループットはどの程度変化があるでしょうか。

では、早速行ってみましょう。


やりたいこと

やりたいことは、シンプルです。DMA転送で、RAMからRAMへ転送する場合と、FlashROMからRAMへ転送する場合のスループットを測定することです。

FlashROMへのアクセスと、RAMへのアクセススピードがどの程度異なるのか見てみます。

使用したボード

FRDM-K64F

ソースコード

ソースコードは、gitHubからダウンロード可能なので、是非試してみてください。
GitHub : https://github.com/mcuthings/dma-throughput


/*******************************************************************************
 * Definitions
 ******************************************************************************/
//#define TRANSFER_ROM_TO_RAM
#define TRANSFER_RAM_TO_RAM

#define EXAMPLE_DMA DMA0
#define EXAMPLE_DMAMUX DMAMUX0
#define DMA_CH0 0
#define DMA_CH1 1
#define DMA_CH2 2
#define DMA_CH3 3
#define TRANSFER_MODE 
#define BUFF_LENGTH 8192UL

/* Systick Counter */
#define GET_TICKS() (0xFFFFFF - SysTick->VAL)
#define STOP_COUNTING() (SysTick->CTRL &= (~SysTick_CTRL_ENABLE_Msk))


/*******************************************************************************
 * Prototypes
 ******************************************************************************/
void SysTick_Handler(void);
void dmaTransfer(void* srcAddr, void* destAddr);
/*******************************************************************************
 * Variables
 ******************************************************************************/
/*DMA Variables*/
edma_handle_t* g_EDMA_Handle[4];
edma_handle_t g_EDMA_Handle_ch0; //for the test of 1byte(8bits) transfer
edma_handle_t g_EDMA_Handle_ch1; //for the test of 4byte(32bits) transfer
edma_handle_t g_EDMA_Handle_ch2; //for the test of 16byte transfer
edma_handle_t g_EDMA_Handle_ch3; //for the test of 32byte transfer

static edma_transfer_config_t transferConfig;
static edma_config_t userConfig;
static uint32_t dma_start,dma_end;
static void* srcAddr; //Source addr for DMA
extern char _binary_randomData_bin_start[]; //Source addr in case of ROM-to-RAM transfer 
static uint32_t SysIsrcount=0;

__attribute__ ((section (".data.$SRAM_LOWER") )) volatile bool g_Transfer_Done = false;
__attribute__ ((section(".data.SRAM_UPPER"))) __attribute__ ((aligned(32))) uint8_t srcRAM[BUFF_LENGTH]={0}; //source table array for RAM-to-RAM
__attribute__ ((section(".data.SRAM_UPPER"))) __attribute__ ((aligned(32))) uint8_t destRAM[BUFF_LENGTH] = {0};




/*******************************************************************************
 * Code
 ******************************************************************************/

/* User callback function for EDMA transfer. */
void EDMA_Callback(edma_handle_t *handle, void *param, bool transferDone, uint32_t tcds)
{
    
    dma_end = GET_TICKS();
    STOP_COUNTING();
    
    if (transferDone)
    {
        g_Transfer_Done = true;
    }
}

void SysTick_Handler(void){

    SysIsrcount++;
}

__attribute__ ((long_call, section (".ramfunc.$SRAM_LOWER") )) void dma_polling(void) 
{
    /* Wait for EDMA transfer finish */
    while (g_Transfer_Done != true)
    {
    }
}


/*!
 * @brief Main function
 */
int main(void)
{
    uint32_t i = 0;
    uint32_t srcRamTemp[BUFF_LENGTH];

    BOARD_InitPins();
    BOARD_BootClockRUN();
    BOARD_InitDebugConsole();

    /* Print source buffer */
    PRINTF("EDMA memory to memory transfer example begin.\r\n\r\n");

    /* For RAM-to-RAM transfer test
     * Prepare source data */
    for (uint32_t i = 0; i < BUFF_LENGTH; i++)
    {
        srcRAM[i] = i%256; //mod(256) to store a byte
    }

    /* Configure DMAMUX */
    DMAMUX_Init(EXAMPLE_DMAMUX);
#if defined(FSL_FEATURE_DMAMUX_HAS_A_ON) && FSL_FEATURE_DMAMUX_HAS_A_ON
    DMAMUX_EnableAlwaysOn(EXAMPLE_DMAMUX, 0, true);
#else
    for (uint8_t i=0; i<4;i++){
        DMAMUX_SetSource(EXAMPLE_DMAMUX, i, 63);
    }
#endif /* FSL_FEATURE_DMAMUX_HAS_A_ON */
    for (uint8_t i=0; i<4;i++){
        DMAMUX_EnableChannel(EXAMPLE_DMAMUX, i);
    }
    /* Configure EDMA one shot transfer */
    /*
     * userConfig.enableRoundRobinArbitration = false;
     * userConfig.enableHaltOnError = true;
     * userConfig.enableContinuousLinkMode = false;
     * userConfig.enableDebugMode = false;
     */
    

    EDMA_GetDefaultConfig(&userConfig);
    EDMA_Init(EXAMPLE_DMA, &userConfig);
    /*DMA handle creation*/
    EDMA_CreateHandle(&g_EDMA_Handle_ch0, EXAMPLE_DMA, DMA_CH0);
    EDMA_CreateHandle(&g_EDMA_Handle_ch1, EXAMPLE_DMA, DMA_CH1);
    EDMA_CreateHandle(&g_EDMA_Handle_ch2, EXAMPLE_DMA, DMA_CH2);
    EDMA_CreateHandle(&g_EDMA_Handle_ch3, EXAMPLE_DMA, DMA_CH3);
        
    g_EDMA_Handle[0] = &g_EDMA_Handle_ch0;
    g_EDMA_Handle[1] = &g_EDMA_Handle_ch1;
    g_EDMA_Handle[2] = &g_EDMA_Handle_ch2;
    g_EDMA_Handle[3] = &g_EDMA_Handle_ch3;

    /* DMA Transfer throughput test */    
    PRINTF("\r\nDMA Transfer from RAM to RAM\r\n");
    dmaTransfer(srcRAM, destRAM);

    PRINTF("\r\nDMA Transfer from ROM to RAM\r\n");
    dmaTransfer(_binary_randomData_bin_start, destRAM);

    PRINTF("\r\n\r\nEDMA memory to memory transfer example finish.\r\n\r\n");

    while (1)
    {
    }
}

void dmaTransfer(void* srcAddr, void* destAddr){
    /* DMA Throughput counter */
    uint32_t transferByte; 
    volatile uint32_t cnt;
    volatile uint32_t ret;
    double coreClock;
    double scalingFactor;
    uint32_t result;

    for (uint8_t i=0; i<4;i++){ //loop while ch0 - ch3 
        EDMA_SetCallback(g_EDMA_Handle[i], 
        EDMA_Callback, NULL); 

    switch (i){ 
        case 0: transferByte = (uint32_t) 1; //1Byte transfer 
            break; 
        case 1: transferByte = (uint32_t) 4; //4Byte transfer 
            break; 
        case 2: transferByte = (uint32_t) 16; //16Byte transfer 
            break; 
        case 3: transferByte = (uint32_t) 32; //32Byte transfer 
            break; 
        default: 
            break; 
    } 
    EDMA_PrepareTransfer(&transferConfig, srcAddr, transferByte, destAddr, transferByte, (uint32_t)(BUFF_LENGTH), (uint32_t)(BUFF_LENGTH*512), kEDMA_MemoryToMemory); 
    EDMA_SubmitTransfer(g_EDMA_Handle[i], &transferConfig); EDMA_SetModulo(EXAMPLE_DMA, g_EDMA_Handle[i]->channel, kEDMA_Modulo8Kbytes, kEDMA_Modulo8Kbytes);


        ret= SysTick_Config(0xFFFFFF);/*<---- Here starts the cycle count */
        
        if(ret){
            PRINTF("SysTick configuration is failed.\n\r");
            while(1);
        }
    
        dma_start = GET_TICKS();
        g_Transfer_Done = false;
        SysIsrcount = 0;
        EDMA_StartTransfer(g_EDMA_Handle[i]);
        
        dma_polling(); //Polling the DMA transfer complete status
        
        cnt = dma_end - dma_start;
        cnt += (SysIsrcount*0xFFFFFF);
        coreClock = CLOCK_GetCoreSysClkFreq();
        scalingFactor = (double)cnt/coreClock;
        result=(BUFF_LENGTH*512/scalingFactor)/(1024*1024);//Unit is [MB/Sec]
        
        /* Print out result */
        PRINTF("DMA throughput (Transfer size %dByte) is %d MB/Sec\r\n",transferByte ,result);          

    }
    
}



コアクロック、システムクロック

コア、およびシステムのクロックは、100MHzとしています。もし、ウェイトがなければ、転送スピードは、200MB/Secが出ることになります。

転送するデータは、配列として8KB用意しています。転送元をRAMとROMに配置しています。変数のRAM配置は、以前の記事を参考にしてください。

参考:GCCで任意の関数(ramfunc)をRAM実行するには???

KInetisのSRAMは、LOWERとUPPERの2バンクで構成されていて、スループットをあげるには、DMAはUPPER側、コアはLOWER側にアクセスすると、SRAMに同時アクセスが可能なデュアルポート仕様になっています。

転送サイズは、BUF_LENGTH(8KB) x 512=4MB

DMAコンフィグ

基本は、SDKのデフォルトの設定のままです。DMAの転送元(Source)および転送先(Destination)は、8KBのモジューロ設定にしてあります。

4MBものRAM領域はないので、8KBのモジューロで繰り返し転送している感じですね。

EDMA_Set_Callback():

メジャーループカウントが完了すると、設定したコールバック関数を呼んでくれます。

EDMA_PrepareTransfer():

DMAの転送元や転送先、転送サイズや転送合計バイトなどを設定する関数です。

EDMA_SubmitTransfer():

設定した転送情報をTCDに設定してくれます。

EDMA_SetModulo():

8KBモジューロの設定はここでしています。

EDMA_StartTransfer():

最後にDMAを走らせます。

DMA転送完了をポーリング

DMA転送が完了した時に割り込みが発生する設定になっています。DMA転送の完了フラグをコアがポーリング(dma_polling()関数)しますが、この関数もコアはLOWER側にアクセスするようにしています。

コアのアクセスはSRAM_LOWER、DMAアクセスはSRAM_UPPERということですね。

転送時間の計測

転送時間の計測には、今回はSystickタイマーを使用しました。Systickタイマーによる計測はとっても簡単です。

以前の記事で詳しく説明していますので、参考にしてください。

参考:CMSISで簡単!サイクル数を計測する方法

Systickタイマーは、24ビットしかないので、転送時間が長い場合、注意が必要です。

24ビットカウンターで測定できる時間以上の転送時間が掛かると、カウンタが足りずに0になってしまい、正しく計測できません。 そこで、次のように今回はSystickタイマーのカウンター0で割り込みが発生したら、上記の割り込みコールバック内で何回カウンター0になったかカウント(SysIsrcount)します。

void SysTick_Handler(void){
SysIsrcount++;
}

計測の集計時に24ビットカウンターが回った回数分を追加して計測しています。

最後にDMA転送完了時のカウントから開始時のカウントを引いて、Systickcount(Systickタイマーが何周したか)を加味します。

あとは、カウントをクロックで割ると処理時間になります。


cnt = dma_end - dma_start;
        cnt += (SysIsrcount*0xFFFFFF);
        coreClock = CLOCK_GetCoreSysClkFreq();
        scalingFactor = (double)cnt/coreClock;
        result=(BUFF_LENGTH*512/scalingFactor)/(1024*1024);//Unit is [MB/Sec]

計測結果

Kinetis K64 DMA転送スループット測定結果

RAM-TO-RAM転送の場合には、転送サイズ32バイトで、169MB/Secという結果になりました。レファレンスマニュアルに記載されている値より低いですね〜。

DMAからRAMにRead・Writeすると、ウェイトが発生するのだと思います。

一方、ROM-TO-RAM転送の場合には、152MB/Secとなり、転送サイズが16バイトから大きくしても、152MB/Secとなり、この転送スピードがピークのようです。DMAによるROMへのアクセスは、さらにウェイトが発生していることになります。

まとめ

今回は、Kinetis K64のDMA転送のスループットを測定してみました。積極的にDMAを使用してシステムのパフォーマンスを向上することが出来ます。

レファレンスマニュアル通りのピーク転送速度は計測出来ませんでしたが、KinetisのDMAは、非常に柔軟で高機能な動作が設定可能です。是非、積極的に使いたいものです。