STM32H7でキャッシュ一貫性を保持したDMA転送(Memory-to-Peripheral)

STM32H7におけるキャッシュ一貫性を保ったDMA転送の方法として、MPUによる設定を解説します。
STM32H7ではF7と異なり、DMAコントローラからDTCM領域にアクセスできないため注意が必要です。
GitHubでソースコードを公開しています。(Memory-to-Peripheralの場合のみ

概要

Cortex-M7コアにはL1キャッシュが搭載されているため、DMA転送時にはデータ化け(キャッシュの一貫性が崩れる問題)に気を配る必要があります。

STM32F7では、DMA対象データをキャッシュの影響を受けない「DTCM領域」に置くことが、データ化け対策として有効でした[脚注1]。一方でSTM32H7では、STM32F7からアーキテクチャが変更され、DMAコントローラからDTCM領域(0x2000 0000~)にアクセスできなくなりました[脚注2]。無理にアクセスするとHardFaultになります。

したがって、STM32H7では、DMA対象データをDTCM以外に置く必要があり、別のデータ化け対策も必須となります。STのアプリケーションノート“Level 1 cache on STM32F7 Series and STM32H7 Series”(AN4839)のP9-P10に次の4種類が紹介されていました。

  1. データを書き込んだ後、CMSISの関数SCB_CleanDCache()を呼ぶ
  2. MPUを設定し、「ライトバック(デフォルト)」から「ライトスルー」にする
  3. MPUを設定し、Sharedにする
  4. レジスタCACRの2ビット目FORCEWTを1に設定し、強制的にライトスルーにする

この中では、2.または3.が良いと思います。1.はソフト側で制御するため、CPUのコストが増えます。4.は設定が簡単ですが、メモリの全領域がライトスルーになるため若干パフォーマンスが落ちます。2.と3.はMPU(Memory Protection Unit)を利用する方法で、メモリ領域を限定して属性を設定できるため、パフォーマンスへの影響を最小限にできます。

以下、上記2.と3.の方法を解説します。

また、本ページの設定を実施し、SAIペリフェラルに対しCircularモードでDMA転送(Memory-to-Peripheral)をすることで、効果確認をしています。

※以下の場合は、本稿の方法では対策できないため、ソフト側でキャッシュを制御する必要があるようです[脚注3]
・ADCの受信など、ペリフェラルからDMA転送で受信する場合(Peripheral-to-Memory)
・メモリ間転送の場合(Memory-to-Memory)

実験環境は次の通りです。

ソースコードはこちら(GitHubのリポジトリに飛びます)。

手順1. STM32CubeMXの設定

下記の画像の通り設定します。

今回は、SAI1(BlockA)に対して、対象領域を繰り返し転送する「Circularモード」でDMA転送する設定としています。またキャッシュ(I-Cache, D-Cache)は有効としています。MPUは手動で設定するため、ここでは弄りません。

手順2. 対象データをSRAM1領域に配置

DMAコントローラがアクセス出来る領域は、D1~D3ドメインです[脚注4]。ここでは、D2ドメイン内のSRAM1(0x3000 0000)に配置することにします。

まず、リンカスクリプトSTM32H743ZITx_FLASH.ldに追記します。D2ドメインにセクション名”RAM_D2″を定義しておきます。

/*STM32H743ZITx_FLASH.ldより抜粋 */
/* Specify the memory areas */
MEMORY
{
 DTCMRAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 128K
 RAM_D1 (xrw)      : ORIGIN = 0x24000000, LENGTH = 512K
 RAM_D2 (xrw)      : ORIGIN = 0x30000000, LENGTH = 288K
 RAM_D3 (xrw)      : ORIGIN = 0x38000000, LENGTH = 64K
 ITCMRAM (xrw)      : ORIGIN = 0x00000000, LENGTH = 64K
 FLASH (rx)      : ORIGIN = 0x8000000, LENGTH = 2048K
}

SECTIONS
{
/* (略) */

/* (ここから追記する) */
/* (2020/08/19更新:(NOLOAD)を追加。バイナリサイズ巨大化を防ぐため) */
 .RAM_D2 (NOLOAD) :
  {
     KEEP(*(.RAM_D2)) 
  } >RAM_D2
/* (ここまで追記する) */

/* (略) */
}
※CubeH7(Ver 1.1.0)の出力するリンカスクリプトは、.bss._user_heap_stackのセクション名が”RAM”になるようです。”RAM”はMEMORY{}内で宣言されていませんので、makeの際にエラーとなります。”RAM”を”RAM_D1″に修正することが、とりあえずの解決策です。
2018/4/29追記:CubeH7(Ver 1.2.0)でエラーは解消されましたが、D1~D3が宣言されていません。SRAMを有効に使うには、結局リンカスクリプトを書き換える必要がありますね。

次に、DMA用のバッファ領域を宣言します。

#define __ATTR_RAM_D2	__attribute__ ((section(".RAM_D2"))) __attribute__ ((aligned (4)))
#define BUFSIZE (128) 

uint16_t buffer[BUFSIZE*2] __ATTR_RAM_D2;

バッファの宣言に、__attribute__((section("RAM_D2")))を追加することで、配置先をRAM_D2(0x3000 0000~)とできます。

またSTM32のDMAではアライメントを4バイト(32bit)にする必要があるため、__attribute__ ((aligned (4)))を付けます。

バッファサイズも32bitの整数倍にしなければなりません。例えばuint16_t buffer[3](計48bit)は、DMA用の領域として不適です。今回はuint16_t buffer[256]とし、512バイトに設定しました。

手順3. MPUの設定

概要で述べたように、方法が2種類あります。

  1. MPUを設定し、「ライトバック(デフォルト)」から「ライトスルー」にする
  2. MPUを設定し、Sharedにする

これらは、下記のMPU_Cfg()をmain関数の先頭で呼び、設定することができます。

void MPU_Cfg()
{
  MPU_Region_InitTypeDef MPU_InitStruct;

  //MPUを無効にする
  HAL_MPU_Disable();

  //これから行う設定内容が有効であることを示す。
  MPU_InitStruct.Enable = MPU_REGION_ENABLE;

  //D2 DomainのSRAM1領域(0x3000 0000~)から512バイト(バッファサイズ分)を対象とする
  MPU_InitStruct.BaseAddress = 0x30000000;
  MPU_InitStruct.Size = MPU_REGION_SIZE_512B;

  //アクセス制限の必要はないので、Full accessとする
  MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;

  //AN4838のP10の記述に従う。
  //ライトスルーモードにする場合(2.の方法)は次の通り
  MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
  MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
  MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
  MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;

 //Shared Deviceとして設定する場合(3.の方法)は以下をコメントアウト
 //MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
 //MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
 //MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
 //MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;

  //リージョンナンバを設定する。(設定を一意に識別する番号)
  MPU_InitStruct.Number = MPU_REGION_NUMBER0;

  //サブリージョンを有効にする(今回は関係ない)
  MPU_InitStruct.SubRegionDisable = 0x00;

  //対象領域からコードを実行できるようにする(今回は関係ない)
  MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;

  //設定を反映する
  HAL_MPU_ConfigRegion(&MPU_InitStruct);

  //MPUを有効にする
  HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

int main(void)
{
  MPU_Cfg();
//(略)
}

MPUの各パラメータについては、アプリケーションノート“Managing memory protection unit (MPU) in STM32 MCUs”(AN4838)のP9-P10が参考になります。

手順4. バッファの出力

以下のHAL_SAI_Transmit_DMA()を呼ぶと、buffer[]が、SAIに対して途切れなく出力されます。

if(HAL_OK != HAL_SAI_Transmit_DMA(&hsai_BlockA1, (uint8_t *)(buffer), BUFSIZE * 2))
{
  Error_Handler();
}

結果

データ化け対策の有無で出力音を比較すると、次の通りになります。MPU設定の効果が現れていることが確認できます。

この例では、256サンプルのSin波を出力しています。

周期32kHzのタイマ割り込みでバッファ領域を1サンプルずつ書き換えているため、32kHz/256sample=125Hzの出力となります(上図左)。

データ化け未対策(上図右)では、タイマ割り込みによってサンプルを書き換えても、キャッシュが追い出されずRAM上のバッファ領域が書き換わらないことを示しています。バッファサイズは128サンプル分なので、32kHz/128sample=250Hzの波形が出力されます。

ソースコード

GitHubのリポジトリを参照して下さい。Ac6 SW4STM32用のプロジェクト一式です。

脚注

[脚注1]

「ねむいさんのぶろぐ」様の記事「STM32F7を使ってみる4 -CPUキャッシュとDMAを考慮する」に、STM32F7におけるキャッシュ一貫性とその対策について、詳しい情報がまとめられております。また、ソースコードも公開されており、参考になります。

[脚注2]

STの公式フォーラムの記事(ログイン必要なので注意)に、DMAコントローラからDTCMを扱えない旨の情報がありました。こちらではADCをDMAで扱う際の、STM32F7→STM32H7への移植について言及されています。

また、DTCMについて、STM32F7とH7のリファレンスマニュアル内の記述を比較すると、次の通りとなります。

[STM32F75xxx, F74xxxリファレンスマニュアル(RM0385)のP69より抜粋]
DTCM-RAM on TCM interface (Tightly Coupled Memory interface) mapped at
address 0x2000 0000 and accessible by all AHB masters from AHB bus Matrix but
through a specific AHB slave bus of the CPU.

[STM32H7x3リファレンスマニュアル(RM0433)のP109より抜粋]
DTCM-RAM on TCM interface is mapped at the address 0x2000 0000 and accessible
by Cortex®-M7, and by MDMA through AHBS slave bus of the Cortex®-M7 CPU.

F7では「AHBバスマトリクスのマスター(DMAコントローラも含まれる?)からアクセスできる」とありますが、H7には記述がありません。どうやらDMAではアクセス出来なくなっているようです。厄介ですね。。

[脚注3]

“Level 1 cache on STM32F7 Series and STM32H7 Series”(AN4839)に、次の注意書きがあります。

Another case is when the DMA is writing to the SRAM1 and the CPU is going to read data from the SRAM1.
To ensure the data coherency between the cache and the SRAM1,
the software must perform a cache invalidate before reading the updated data from the SRAM1.

「SRAM1にDMAが書き込み、CPUで読み出す場合には、一貫性を保つために、読み出し前にソフト側でキャッシュをInvalidateせよ」とあります。おそらくCMSISの関数SCB_InvalidateDCache()をソフト側で呼び出すものと考えていますが、まだ検証していません。

[脚注4]

ドメインD1~D3については、以下に情報があります。
STM32H7x3リファレンスマニュアル(RM0433)のP109「2.3 Embedded SRAM」
“Migration of microcontroller applications from STM32F7 Series to STM32H7x3 line microcontrollers”(AN4936)のP8ブロック図内

参考資料

  1. ユークエスト株式会社様の記事DMA対応と言われたら(1), (2)
  2. DMAについては、こちらの記事が大変分かりやすいです。DMAとキャッシュの話や、アライメントの話などが記載されています。

  3. Inscape Inc.様の記事キャッシュ(前編), (中編), (後編)
  4. Cortex-M7コアのキャッシュについて、詳しくまとめられています。Shared, Non-Sharedといった概念や、ライトバック/スルーの仕組みなど、レベルの高い内容があります。

  5. “Migration of microcontroller applications from STM32F7 Series to STM32H7x3 line microcontrollers”(AN4936)
  6. STM32F7からH7へ移行する際の差違や注意点がまとめられています。

  7. “Managing memory protection unit (MPU) in STM32 MCUs”(AN4838)
  8. MPU(Memory Protection Unit)についての説明です。MPUを設定するためのサンプルコードも記載されています。

  9. “Level 1 cache on STM32F7 Series and STM32H7 Series”(AN4839)
  10. STM32F7搭載のL1キャッシュについて記載されています。STM32H7用は見つかりませんでした。F7とH7で何か違いがあるんでしょうか? 2018/4/29追記:Rev.Upされ、H7についての記述が追加されていますね。

コメント

タイトルとURLをコピーしました