STM32H7を使ってみる5 -キャッシュ・ワンダリング(後篇)-

←前回                       次回→

このぶろぐの読者様的にはねむいさんがうっかり「MDMAを使ってみる」などという
公序良俗(こうじょりょうぞく)に大幅に反したタイトルの記事を書いてしまって
警察屋さんに絞られる展開を期待してるのでしょうがそんな安易な手には引っかかり
ませんよぅ!!!てうかそんな明らかにヤバイ合成(まぜ)モノより天然自然のta
ダメ!ゼッタイ!!!








さて今回はエラッタのせいで強制的にライトバックにせざるを得なくなってしまった
キャッシュメモリでDMAとどうやってうまいこと付き合っていくかを解説していこうと
思います。
なお、DMA使うメモリ領域だけキャッシュ不可にするという軟弱な措置はねむいさんの
ライブラリに存在しませんのであしからず!!!!!!!!

●注意すべき前提
キャッシュがきいた状態のメモリアクセスでは、あるひとまとまりのデータの塊
に加えてそのデータ塊のアドレス境界で処理すべきという大大前提があります。
これはキャッシュを搭載するどのCPUでも同一となります。

「あるひとまとまり」はキャッシュメモリ上のキャッシュラインのバイト数を示し、
「アドレス境界」はアドレスがキャッシュラインのバイト数分のビットで同一である
ということを示します。

Cortex-M7においてはデータキャッシュのキャッシュラインサイズが32Byteのため、
「32バイトごと」かつ「アドレスの32バイトに当たる下位5ビットが0x0_0000」
であることが求められます。

過去にSTM32F7でキャッシュ比較したときはあらかじめDMA先を32バイトの境界に
合わせ4kByte刻みで転送という限定的な環境でやってたのでキャッシュコントロールは
かなり端折った記述をしておりましたが今回は腰を据えて汎用的に扱えるよう
落とし込んて行きます。



●DMAで読み取る場合(SDMMCのIDMAの場合)
実はライトバック設定の場合、READの取り扱いが一番やっかいです。
理論的には読み出し先のメモリのキャッシュをInvalidateしてやればよいはずなのですが
なぜかそうは問屋が卸さず、私が試した限りでは必ずCleanもしなけらばならないことが
分かりました?なんでだYO!

DRESULT SD_read(BYTE lun, BYTE *buff, DWORD sector, UINT count)
{
DRESULT res = RES_OK;
uint32_t timer = SysTick->VAL + SD_DATATIMEOUT;

/* first ensure the SDCard is ready for a new operation */
while((SD_GetCardState() == SD_TRANSFER_BUSY))
{
if(timer < SysTick->VAL)
return RES_NOTRDY;
}

#if defined(SD_DMA_MODE) && !defined(SD_POLLING_MODE)
/*
Force Cache Data Lines content write back to memory and Invalidate them.
Someone (CPU) might be still accessing those cache lines so we need to
flush/clean recent content to memory and purge/invalid cache lines before
allowing Hardware to access RAM.
*/
if((uintptr_t)buff & 0x3) /* Check 4Byte Alignment */
{ /* Unaligned Buffer Address Case (Slower) */
for (unsigned int secNum = 0; secNum < count ; secNum++){

SCB_InvalidateDCache_by_Addr ((uint32_t*)dmabuf, SECTOR_SIZE);

if(SD_ReadBlocks_DMA((uint32_t*)dmabuf, (uint32_t)(sector+secNum), 1)!= MSD_OK)
{
MSG_PRINTF("Read error on unaligned buffer¥n");
res = RES_ERROR;
}

memcpy(buff+secNum*SECTOR_SIZE, dmabuf, SECTOR_SIZE);
}
} else {
/* Aligned Buffer Address Case (Faster) */
SCB_CleanInvalidateDCache_by_Addr((uint32_t*)buff, count * SECTOR_SIZE);

if(SD_ReadBlocks_DMA((uint32_t*)buff, (uint32_t)sector, count) != MSD_OK)
{
MSG_PRINTF("Read error on DMA¥n");
res = RES_ERROR;
}
}
#else
if(SD_ReadBlocks((uint32_t*)buff, (uint32_t)sector, count) != MSD_OK)
{
MSG_PRINTF("Read error on polling¥n");
res = RES_ERROR;
}
#endif
return res;
}

↑CMSIS_v5を使ってSTM32H7でDMA転送(READ)する場合です。
READアクションをとる前にSCB_CleanInvalidateDCache_by_Addr()を実行します。
Invalidateだけではだめです。CleanしてInvalidateです。ここテストに出るので
覚えておきまししょう!そうせざるを得なかった例がこちらの議論にもありますが正直
私もピンときません。だれか詳しい解説を教えてください。
当たり前ですがデータが32Byteの倍数かつ32Byte境界にビッチリそろった完全読み出し
専用のメモリ領域ならInvalidateだけで問題ないです。ぇ?自分で答え言ってるだろ
って?ねむいさんは他の人の同意とか同情がほしいのですよぅ!
ぁあそうだ、ライトスルーもInvalidateだけでおk、エラッタでどうせ使えませんが。

それと上記のルーチンでは先の述べたアドレスの丸め込みがないじゃないかと思う
方がいると思いますがCMSIS_v5ライブラリのほうに丸め込み処理が取り込まれており
ゴテゴテしたマクロ組まなくてもシンプルに記述できるようになっています。
STマイクロのCubeライブラリではこちらの更新がまだ取り入れられておらず、自前の
アドレス丸め込みマクロが記述されておりますので二度手間にならないようにご注意
願います。

そんでもってCleanInvalidateしたことによるキャッシュコントロールによる
オーバーヘッドが懸念されますがH7はCPUのコアの速度がMAX480MHzも出るので
多分気にはならないとおもいます(投げやり)



●DMAで送信する場合(SDMMCのIDMAの場合)
DRESULT SD_write(BYTE lun, const BYTE *buff, DWORD sector, UINT count)
{
DRESULT res = RES_ERROR;
uint32_t timer;


#if defined(SD_DMA_MODE) && !defined(SD_POLLING_MODE)
/*
Force Cache Data Lines content write back to memory.
Someone (CPU) might be still accessing those cache lines so we need to
flush/clean recent content to memory before
allowing Hardware to access RAM.
*/
if((uintptr_t)buff & 0x3) /* Check 4Byte Alignment */
{ /* Unaligned Buffer Address Case (Slower) */
for (unsigned int secNum = 0; secNum < count; secNum++){

memcpy(dmabuf, buff+(SECTOR_SIZE*secNum), SECTOR_SIZE);

SCB_CleanDCache_by_Addr((uint32_t*)dmabuf, SECTOR_SIZE);

if(SD_WriteBlocks_DMA((uint32_t*)dmabuf, (uint32_t)(sector+secNum), 1) != MSD_OK)
{
MSG_PRINTF("Write error on unaligned buffer¥n");
res = RES_ERROR;
}
}
} else {

SCB_CleanDCache_by_Addr((uint32_t*)buff, count * SECTOR_SIZE);

if(SD_WriteBlocks_DMA((uint32_t*)buff, (uint32_t)sector, count) != MSD_OK)
{
MSG_PRINTF("Write error on DMA¥n");
res = RES_ERROR;
}
}
#else
if(SD_WriteBlocks((uint32_t*)buff, (uint32_t)sector, count) != MSD_OK)
{
MSG_PRINTF("Write error on polling¥n");
res = RES_ERROR;
}

#endif

/* ensure the SDCard is ready for a next operation */
timer = SysTick->VAL + SD_DATATIMEOUT;
res = RES_ERROR; /* Timeout */
/* block until SDIO IP is ready or a timeout occur */
while(timer > SysTick->VAL)
{
if(SD_GetCardState() == SD_TRANSFER_OK)
{
res = RES_OK;
break;
}
}

return res;
}

↑CMSIS_v5を使ってSTM32H7でDMA転送(WRITE)する場合です。
WRITEのほうは単純明快、転送前にCleanしてやるだけです!
READのほうが扱いがめどくて誤れば致命的なのがなんとも…


ちなみにSTM32のマイコンにおいてREAD/WRITEいずれの場合も
DMAコントローラのWORD(4Byte)制限があるので
それも加味しておきましょう。



●DMAで送信する場合(SAIのCircularDMAの場合)
ライトスル―が使えなくなったせいでいろんなコードにキャッシュコントロールを
突っ込まざるを得なくなってしまいましたがねむいさんのいつものではAudio再生に
SAIのCircularDMAを使用しており、こちらもきっちり対策しております。

ねむいさん最初にH7触った時に無対策で挑んで見事返り討ちに会いました。サンプル
音源のBeachBoysのハーモニック・サウンドがブビビビッブッというげりうんちみたいな
汚い音が得られてしまったのですが移植時にcodecの音量設定が最大になっていたのも
気づかずげりうんち音で鼓膜が破壊されるかと思いましたがどうでもいいですね。


SAIの場合はWrite専用のぶっぱなのでCleanだけで問題ないです。
問題は挿入する箇所です。

/* on the first buffer fill up,start the dma transfer */
if(EVAL_AUDIO_Init(OUTPUT_DEVICE_BOTH, DEFAULT_VOLUME, (uint32_t)mp3FrameInfo.samprate))
{
MSG_PRINTF("Mp3Decode: audio init failed¥r¥n");
nResult = -4;
break;
}
/* I2S DMA Transfer First Trigger */
#if defined(STM32H7XX)
SCB_CleanDCache_by_Addr((uint32_t*)g_pMp3DmaBuffer, MP3_DMA_BUFFER_SIZE);
#endif
/* Needed to AAC_DMA_BUFFER_SIZE*2,16-bits audio data size(2Byte) */
/* See more detail,BSP_AUDIO_OUT_Play() */
EVAL_AUDIO_Play(g_pMp3DmaBuffer, MP3_DMA_BUFFER_SIZE*2);

↑転送なのでCleanのみでおk(mp3.support.cにて)
まずはCircularDMAの最初のキックです。まぁこれは当然。
ていうかなんかtypoしてますね私orz AACじゃなくてここではMP3です。
if(unDmaBufMode == INIT_RINGBUF || unDmaBufMode == FULL_RINGBUF)
{
/* Check FirstHalf Transfer in LastHalf and BufferInit Mode */
if(em & TRANSFER_FIRST_HALF)
{
MSG_PRINTF("Mp3Decode: DMA out of sync (expected TC, got HT)¥r¥n");
nResult = -3;
break;
}
else{
g_pMp3DmaBufferPtr = g_pMp3DmaBuffer + (MP3_DMA_BUFFER_SIZE/2); /* 16bit address pointer calc */
#if defined(STM32H7XX)
SCB_CleanDCache_by_Addr((uint32_t*)g_pMp3DmaBufferPtr, MP3_DMA_BUFFER_SIZE);
#endif
}
}
else /* unDmaBufMode == HALF_RINGBUF */
{
/* Check LastHalf Transfer in FirstHalf Mode */
if(em & TRANSFER_LAST_HALF)
{
MSG_PRINTF("Mp3Decode: DMA out of sync (expected HT, got TC)¥r¥n");
nResult = -3;
break;
}
else{
g_pMp3DmaBufferPtr = g_pMp3DmaBuffer;
#if defined(STM32H7XX)
SCB_CleanDCache_by_Addr((uint32_t*)g_pMp3DmaBufferPtr, MP3_DMA_BUFFER_SIZE);
#endif
}

お次はこちら。
半分転送完了割り込み後の処理で次の転送予定のバッファにCleanをかけます。
g_pMp3DmaBufferPtrに次に転送するバッファのアドレスをコピーした後Cleanです。
g_pMp3DmaBufferPtrはuint16_tのポインタなので注意が必要です。


●DMAじゃないけどLTDCもキャッシュと競合する
ライトスルーが使えなくなった弊害はDMAと同じくキャッシュコヒーレンシーを
求められるLTDCにも波及します。

static void disp_blt (
int left, /* Left end (-32768 to 32767) */
int right, /* Right end (-32768 to 32767, >=left) */
int top, /* Top end (-32768 to 32767) */
int bottom, /* Bottom end (-32768 to 32767, >=right) */
const uint16_t *pat /* Pattern data */
)
{
int yc, xc, xs;
#if !defined(USE_TFT_FRAMEBUFFER)
int xl;
uint16_t pd;
#endif

if (left > right || top > bottom) return; /* Check varidity */
if (left > MaskR || right < MaskL || top > MaskB || bottom < MaskT) return; /* Check if in active area */

yc = bottom - top + 1; /* Vertical size */
xc = right - left + 1; xs = 0; /* Horizontal size and skip */

if (top < MaskT) { /* Clip top of source image if it is out of active area */
pat += xc * (MaskT - top);
yc -= MaskT - top;
top = MaskT;
}
if (bottom > MaskB) { /* Clip bottom of source image if it is out of active area */
yc -= bottom - MaskB;
bottom = MaskB;
}
if (left < MaskL) { /* Clip left of source image if it is out of active area */
pat += MaskL - left;
xc -= MaskL - left;
xs += MaskL - left;
left = MaskL;
}
if (right > MaskR) { /* Clip right of source image it is out of active area */
xc -= right - MaskR;
xs += right - MaskR;
right = MaskR;
}
Display_rect_if(left, right, top, bottom); /* Set rectangular area to fill */
#if defined(STM32F7XX) || defined(STM32H7XX) /* Flush Cache Datas */
SCB_CleanDCache_by_Addr((uint32_t*)pat, xc*yc*2);
#endif
#if defined(USE_TFT_FRAMEBUFFER)
Display_wr_block_if((uint8_t*)pat, xc*yc);
#else
do { /* Send image data */
xl = xc;
do {
pd = *pat++;
Display_wr_dat_if(pd);
} while (--xl);
pat += xs;
} while (--yc);
#endif
}

↑ts_fileload.cにて
Cha'N氏のTinyJPEGライブラリででコード後のデータをブロック転送するときにClean
しないと化けます。DMA2DだけじゃなくCPUが介在するFIFOコピーでも化けます。

大量のデータを一気に吐かせるimgファイル(動画データ)ならその都度キャッシュも
総入れ替えとなるので化けないようですが中途半端に転送サイズが小さいとだめな
ようですね。本来なら動画再生のほうもClean突っ込んでやるのが筋なんですけど。

なお、前回触れたライトアロケートキャッシュ設定にしてしまった場合は逆汚染に
よってせっかく書き込んだデータが潰されてしまうためデータの書き込み都度に
Cleanをしてやらないといけなくなってしまいこれによるオーバーヘッドで見かけの
処理速度が大きく落ちてしまいます。
前回も言いましたがSAIでもビットレート上がるとプチプチノイズ入りまくり使い物に
なりませんしやはりSTM32でライトアロケート設定は使わないのが無難です。






というわけで癖が非常に強いSTM32H7ですが、その特性を把握して上手に
コントロールすれば本来の性能をいかんなく発揮できることがわかりました。
STM32F7で十分と思い、食わず嫌いしてましたがSTM32H7、良い感じです!

Go to top of page