STM32F7を使ってみる4 -CPUキャッシュとDMAを考慮する-

前回DMA転送がうまくいかなかったと抜かしていましたが原因は至って単純な
もので私にCPUキャッシュについての知識が無かっただけでした!!!!!
めでたしめでたし♥

20231123追:
古いCortex-M7コアにはキャッシュ周りにバグがあります!!
こちらのエントリも必ず見てください!!1!!1!!

20231123追:




今回は上の三行で終わる内容ですが前回の記事そのものがCortex-M7アーキテク
チャを全く理解してないおまぬけなねむいさんのままで終わってしまう(あたしはまだ
終わってはない!)のでもう少し突っ込んで解説したいと思います。

STM32F7にはI-CacheとD-Cacheの2つのレベル1キャッシュメモリがありこれらを
有効化させることによりリアルタイム性が落ちる代わりに全体の処理の高速化を
図っています。今回はデータ用キャッシュメモリD-Cacheに着目します。
今回の記事を書くにあたってユークエストのビリー様のサイトを参考にさせていただきました。



私のいつものではプログラム開始時にI-CacheとD-Cacheを有効にし、さらにMPUで
SRAM1,2領域(256kByte分)のWriteThroughを有効にしています。CPUからのこの
SRAM領域(以下本メモリ)への読み書きはWriteThroughによって常に本メモリと
D-Cacheの一貫性(Cache Coherency)が保たれています。WriteBackよりも多少は
処理速度は落ちますがそれでもD-Cacheの恩恵に与かれるので確実で強力です。

しかしながらこの関係が破れるのがDMA転送をする時です。

DMAコントローラはキャッシュメモリを介さないのでD-Cacheと本メモリの一貫性を
保てなくなりDMA転送後に本メモリから読みだしたはずの本当のデータはD-Cache
に残った古いデータで正常に読みだせない&転送がコケる(ように見えた)という事に
なります。以下にlibjpegで画像をデコードした際の実例を示します。




こちらは虹裏的美少女画像をSDMMCのデータ転送にDMAを一切使わずFIFOのポーリ
ングで読みだした所の写真です。右下に表示にデコード完了に掛かった時間を表示して
います。I/D-Cacheは有効です。


無対策でDMAで読みだした時の写真です。途中でデータがおかしくなってひでぶ
しているのが分かります。上記のとおりD-Cacheに残った古いデータのせいでめちゃ
くちゃになってる訳ですが、当たり前ですが表示にすら至らないケースもあります。



その前に脇道にそれますがlibjpegとFatFsを結合している関数について、この関数
内ではlibjpeg側で先にmallocで取得したデータ読み出し用バッファをDMAの読み出し
先にしてしまいます。

つまり私のいつもので使うFatFsのデータ転送用汎用バッファBuff(上記画像参照)が
どのメモリ領域にいるかは関係がなく、リンカスクリプトでHEAP領域にD-Cacheの影響
を受けるSRAM1,2を割り当てているとDMA転送時の上記の問題に思い切り引っ掛かる
ことになります。それを踏まえたうえで本題の比較に入ります。



プログラム開始時の時点でD-Cacheそのものを無効にすると当たり前ですがデータ
の齟齬が発生する余地が全く無くなりますので表示がおかしくなることはありません。
しかしながらパフォーマンスはガタ落ちになります。これじゃF7使う価値がないです。


対策その1です。
FatFsの汎用バッファをキャッシュの影響を受けないDTCM領域に配置し、さらにDMAの
4バイトアライメント対策もDTCMの1セクタバッファでフォローする作戦です!
CPUコアと同速度で動くDTCMでキャッシュの影響とレイテンシの不確実性を最小限に
でき、確実な転送が可能です!!

しかしこの方法は以下の制約を受けるのでご留意願います。
1.リンカスクリプトでDATA,BSS,STACK領域を必ずDTCMに割り当てるべし。
2.mallocで引っ張ってきたメモリドレスがDTCMのアドレスじゃなかった場合の
 対処を講じる必要がある。実際は読み書き元のアドレスを検知してDTCMの1セクタ
 事の読み書きに変えるかキャッシュコントロール関数を使用するかです。


私のSTM32F7向けのいつものはこの対策1を施しておあります。


対策その2です。
DMA転送する前にD-Cacheの無効(Invalidate)だけを行います。CMSISのヘッダライ
ブラリでは上記の無効にする関数が用意されています。STM32F7のキャッシュサイズ
は4kByteでlibjpegの一度に読みだすサイズもデフォルトでは4kByteとなってます。
(注:Cortex-M7のキャッシュは1ライン32byte*128本で合計4096Byte)
D-Cacheの1ラインごとにInvalidateする関数もありますが今回は読み出しサイズが
キャッシュサイズと同じなので全部Invalidateする関数を呼び出してます。
SDMMCのDMA転送を行う関数の頭にSCB_InvalidateDCache()を置いて実行してい
ますが対策その1よりも若干遅くなっています。

そういえば・・・Disableも無効って意味なんですがあちらさんではどう
使いわけてるんでしょ…



対策その3です。
DMA転送する前にD-CacheのInvalidateとCleanを行います。
SCB_CleanInvalidateDCache()を呼び出しますが必然的に対策その2よりもさらに
遅くなっています。



対策その2と3は単純にCMSISヘッダライブラリの関数を実行すればいいという訳では
なく、DMA元/先として使うバッファのアライメントとサイズに制約があります。
D-Cacheのキャッシュラインのサイズにアライメントを合わせ、なおかつそのサイズの
整数倍にしないとやっぱりデータの齟齬が起こってしまいます。
特に読み出し時はDMA用に確保したバッファの近辺のアドレスに配置されている変数
までしっかり破壊してくれやがります。STM32F7の場合は1キャッシュライン当たり
32Byteの固定値となっているので32バイトのアラインメント且つデータサイズが32の
倍数になる必要があります。

もしバッファ用のメモリ領域が上の条件を満たさない場合はアライメントとデータサイズ
の丸め込みを行う必要性があり更にオーバヘッドが増えます。RTOS向けのデバイス
ドライバを作成される際はこの点も考慮する必要があります。

以上の点を踏まえると別に無理してDMAしなくてもいいんじゃないかと思われるかも
しれませんがそれではロマンが無いので是が非でもDMAらせていただきます!


そんなわけでD-Cacheを考慮した修正版のいつものをあげておきます・・・


おまけ
libpngやlibjpegは無対策でも平気だったのですが何故ひでぶしないで平気だったのかも
調べてみました。


giflibはSDMMCのブロック転送時にfptrがクラスタの境界になく、その際の一時
転送先がDTCM領域のFatFsオブジェクト内(のwin配列)に確保されています。
つまり対策その1とほぼ同じ事をFatFsライブラリ内でやってくれているわけです。



libpngもgiflibと同様になっています。

Go to top of page