8-bitマイコンのArduinoでは味わえない32-bitマイコンの醍醐味(?)としてDMAがあります。NXP提供のCMSIS DMAライブラリのデモコードをmbedに移植して実験をしてみました。
実験の内容
- SRAM→SRAM, Flash ROM→SRAM間で32 wordのDMAを起動
- DMA起動後、メインルーチンはループカウンタのインクリメントを行うだけの無限ループに入る。
- DMA完了割り込みルーチン内で再度DMAを起動することで、DMAによるメモリコピーとループが平行して動く(筈)
- DMAが1000回動いた時点でプログラムを止めて、起動からの経過時間とループカウンタ値を表示
- DMAのソースがSRAMとFlash ROMの場合の差分をチェック
ポイントは5項でありまして、図に示す通り、①のSRAM→SRAM間DMAでは、DMAと命令フェッチでメモリアクセスが競合しないと考えられます。一方、②のROM→SRAMのDMAではROMアクセスが競合するためループ処理が①に比べてまわらないというのが期待値です。
実験コード
DMA完了の割り込み処理ルーチンとメインルーチンを以下に示します。コード全体は、mbed.orgにポストしてあります。
static uint32_t DMASrc_Buffer[DMA_SIZE]=
{
0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40
};
uint32_t DMADest_Buffer[DMA_SIZE];
/*-------------------------MAIN FUNCTION------------------------------*/
extern "C" void DMA_IRQHandler (void); // extern "C" required for mbed
/*----------------- INTERRUPT SERVICE ROUTINES --------------------------*/
/*********************************************************************//**
* @brief GPDMA interrupt handler sub-routine
* @param[in] None
* @return None
**********************************************************************/
void DMA_IRQHandler (void)
{
// check GPDMA interrupt on channel 0
if (GPDMA_IntGetStatus(GPDMA_STAT_INT, 0)) { //check interrupt status on channel 0
// Check counter terminal status
if(GPDMA_IntGetStatus(GPDMA_STAT_INTTC, 0)) {
// Clear terminate counter Interrupt pending
GPDMA_ClearIntPending (GPDMA_STATCLR_INTTC, 0);
if (++TC_count < DMA_CYCLES) {
// Setup channel with given parameter
GPDMA_Setup(&GPDMACfg);
// Run DMA again
GPDMA_ChannelCmd(0, ENABLE);
return;
} else {
/* DMA run predetermined cycles */
int elapsedTime = GetTickCount();
Buffer_Verify();
pc.printf("%s", compl_menu);
pc.printf("DMA %d cycles, %d loop executions\n\r", TC_count, loop);
pc.printf("Elapsed time %d ms\n\r\n\r", elapsedTime);
while(1); // Halt program
}
}
if (GPDMA_IntGetStatus(GPDMA_STAT_INTERR, 0)){
// Clear error counter Interrupt pending
GPDMA_ClearIntPending (GPDMA_STATCLR_INTERR, 0);
pc.printf("DMA Error detected.\n\r");
while(1); // Halt program
}
}
}
/*-------------------------MAIN FUNCTION--------------------------------*/
/*********************************************************************//**
* @brief c_entry: Main program body
* @param[in] None
* @return int
**********************************************************************/
int c_entry(void)
{
pc.baud(9600);
// print welcome screen
pc.printf("%s", menu);
/* Disable GPDMA interrupt */
NVIC_DisableIRQ(DMA_IRQn);
/* preemption = 1, sub-priority = 1 */
NVIC_SetPriority(DMA_IRQn, ((0x01<<3)|0x01));
/* Initialize GPDMA controller */
GPDMA_Init();
// Setup GPDMA channel --------------------------------
// channel 0 (highest priority
GPDMACfg.ChannelNum = 0;
// Source memory
GPDMACfg.SrcMemAddr = (uint32_t)DMASrc_Buffer;
// Destination memory
GPDMACfg.DstMemAddr = (uint32_t)DMADest_Buffer;
// Transfer size
GPDMACfg.TransferSize = DMA_SIZE;
// Transfer width
GPDMACfg.TransferWidth = GPDMA_WIDTH_WORD;
// Transfer type
GPDMACfg.TransferType = GPDMA_TRANSFERTYPE_M2M;
// Source connection - unused
GPDMACfg.SrcConn = 0;
// Destination connection - unused
GPDMACfg.DstConn = 0;
// Linker List Item - unused
GPDMACfg.DMALLI = 0;
// Setup channel with given parameter
GPDMA_Setup(&GPDMACfg);
/* Enable GPDMA interrupt */
NVIC_EnableIRQ(DMA_IRQn);
pc.printf("Start transfer...\n\r");
GetTickCount_Start();
// Enable GPDMA channel 0
GPDMA_ChannelCmd(0, ENABLE);
/* Wait for GPDMA processing complete */
while (1) {
loop++;
}
return 1;
}
DMAを何回も起動しながら、平行して117行目のloopが何回まわるかを確認します。CMSISライブラリのexampleからの変更点は以下です:
- 割り込みハンドラー(17行目)のプロトタイプ宣言にextern "C"をつける。これがないとハングしてしまう
- コンソールへの文字出力はmbedライブラリのprintf関数に変更
- 実行時間の計測は1ms周期のSysTick割り込みを使用
実行結果と考察
1行目のDMASrc_Bufferをstatic宣言するとSRAM上に領域が確保され①のSRAM→SRAM DMAになります。1行目をconst宣言するとFlash ROM上に領域が確保され②になります。
①:SRAM→SRAM
DMA 1000 cycles, 22962 loop executions
Elapsed time 5 ms
②Flash ROM→SRAM
DMA 1000 cycles, 7985 loop executions
Elapsed time 5 ms
上記より、期待通り①の方がループの実行回数が多いことが分かります。実行時間は①②とも5msで共通のため、DMA 1回(32 wordの転送)に要する時間は同じで、DMA期間中にDMACとMCUでバスの使用権を融通し合ういわゆるサイクルスチールモードになっていると思われます。
①ではDMACがSRAMアクセス中でもMCUは独立してループ処理の命令フェッチを進めることができ、DMACがバスマスタ権を譲ってくれた隙にloopカウンターの更新値をSRAMに書き戻せるのに対して、②はDMACがROMアクセス中は命令フェッチが止まるためループ回数が低下すると思われます。
おまけ
試しに以下のコードで、DMAと同様のコピーをmemcpyライブラリを使って行いました。
#include "mbed.h"
#include "GetTickCount/GetTickCount.h"
#define LOOP 1000
#define BUF_SIZE 32
Serial pc(USBTX, USBRX); // tx, rx
/* ---- Source data to be copied ---- */
// Allocated to Flash ROM
const uint8_t __align(8) array1[BUF_SIZE] = {
0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40
};
// Allocated to SRAM
static uint8_t __align(8) array2[BUF_SIZE] = {
0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40
};
/*
* My Memcpy function
* If source and destination data is aligned at DWROD, performe copy by 8 byts
*/
void Memcpy(void *dst, const void *src, size_t len)
{
if ( (uint32_t)dst % sizeof(uint64_t) == 0 &&
(uint32_t)src % sizeof(uint64_t) == 0 &&
len % sizeof(uint64_t) == 0 )
{
uint32_t *d = (uint32_t*)dst;
uint32_t *s = (uint32_t*)src;
uint32_t *end = (uint32_t*)(s + len/sizeof(uint64_t));
while (s != end) {
*(d++) = *(s++); // Copy first 4 bytes
*(d++) = *(s++); // Copy second 4 bytes
}
} else {
uint8_t *d = (uint8_t*)dst;
uint8_t *s = (uint8_t*)src;
uint8_t *end = (uint8_t*)(s + len);
while (s != end) {
*(d++) = *(s++); // Copy each byte
}
}
}
int main()
{
unsigned int startTime, endTime;
int i,len;
char __align(8) array3[BUF_SIZE * sizeof(uint32_t)];
pc.baud(9600); // set baud rate
len = BUF_SIZE * sizeof(uint32_t);
pc.printf("Data Length: %d\n", len);
GetTickCount_Start();
/* ---- Flash ROM to SRAM copy ----- */
pc.printf("Memory copy Flash->SRAM using memcpy lib %d bytes.\n", LOOP*len);
startTime = GetTickCount();
for (i = 0; i < LOOP; i++) {
memcpy(array3, array1, len);
}
endTime = GetTickCount();
pc.printf("Elapsed time: %d ms.\n\n", endTime - startTime);
pc.printf("Memory copy Flash->SRAM using My Memcpy %d bytes.\n", LOOP*len);
startTime = GetTickCount();
for (i = 0; i < LOOP; i++) {
Memcpy(array3, array1, len);
}
endTime = GetTickCount();
pc.printf("Elapsed time: %d ms.\n\n", endTime - startTime);
/* ---- SRAM to SRAM copy ----- */
pc.printf("Memory copy SRAM->SRAM using memcpy lib %d bytes.\n", LOOP*len);
startTime = GetTickCount();
for (i = 0; i < LOOP; i++) {
memcpy(array3, array2, len);
}
endTime = GetTickCount();
pc.printf("Elapsed time: %d ms.\n\n", endTime - startTime);
pc.printf("Memory copy SRAM->SRAM using My Memcpy %d bytes.\n", LOOP*len);
startTime = GetTickCount();
for (i = 0; i < LOOP; i++) {
Memcpy(array3, array2, len);
}
endTime = GetTickCount();
pc.printf("Elapsed time: %d ms.\n\n", endTime - startTime);
}
なんと、SRAM→SRAM、Flash ROM→SRAM共に、1msで終了。loop処理を行っていないという差分はありますが、ひょっとして単なるmemoryコピーならDMAを使うよりMCUでやった方が早いのか?
DMACの設定にSoftware Burst Requestというヤツがあり、こいつを使うとDMACがバスを占有するバーストモードとなり、DMA時間を短縮できるのではと思ったのですが、動かし方が分からず断念でした。
参考情報
最近のコメント