STM32H7でDMAやFMCを使う場合の注意点

STM32H7でDMAやFMCを使う場合の注意点まとめ。参考文献と参考記事も参照してください。

(1) DTCMにはDMAの手が届かない

【問題】 STM32F7からSTM32H7に移植したら、DMA転送が機能しなくなった。
【原因】 STM32H7のDTCMは、DMAコントローラからアクセスできない。
【対策】 DMA転送元/先の変数は、DTCMではなく汎用SRAMに配置する。

STM32H7のCPUコアは最大480MHzないし550MHzの超高速なクロックで動作できます。しかし汎用メモリはこんな高速なクロックにはついてこれません。そこでSTM32H7はL1キャッシュ(後述)および密結合メモリ(TCM)を備えています。密結合メモリは、専用バスでCPUコアと接続された高速なメモリです。密結合メモリにはデータ用のDTCMと命令用のITCMがあります。例えばSTM32H743ZIは、全1MバイトのRAMのうち、128kバイトがDTCMです。DTCM領域に変数を配置すれば、変数へのアクセスが高速になります。

それは良いのですが、STM32H7のDTCMはDMAコントローラからアクセスできません(※)。DMAの転送元や転送先にDTCM領域のアドレスを指定すると、ハードフォールトを起こします。STM32F7にもDTCMはありましたが、STM32F7のDTCMはDMAコントローラからアクセスできました。STM32F7もSTM32H7も、SRAM領域の先頭(0x2000_0000~)にDTCMがあります。STM32F7でDTCMに変数を配置してDMA転送していた場合、STM32H7に移植するとDMA転送が機能せず、ハードフォールトを起こします。

対策として、DMAの転送元や転送先の変数はDTCMではなく汎用SRAM (AXI SRAM および AHB SRAM1~4) に配置します。 その指定方法は処理系依存ですが、IARのEWARMの場合、#pragma location で変数を配置するセクションを指定し、リンカ設定ファイル(.icf)でセクションのアドレスを指定します。

// セクション DMA_RAM にhoge_buffを配置する
#pragma location = "DMA_RAM"
uint8_t hoge_buff[BUFF_SIZE];
/* リンカ設定ファイルの抜粋 */

/* AXI SRAMの先頭アドレスと末尾アドレス */
define symbol __ICFEDIT_region_AXISRAM_start__ = 0x24000000;
define symbol __ICFEDIT_region_AXISRAM_end__   = 0x2407FFFF;

/* AXI SRAM領域 */
define region AXISRAM_region  = mem:[from __ICFEDIT_region_AXISRAM_start__ to __ICFEDIT_region_AXISRAM_end__];

/* セクションDMA_RAM を AXI SRAM領域 に配置 */
place in AXISRAM_region { readwrite section DMA_RAM};

GCCの場合には、#pragma ではなく __attribute__ を、リンカ設定ファイル(.icf)ではなくリンカスクリプト(.ld)を用いて設定するようです。

※ STM32H7にはMDMAとかいうのがあって、DTCMにもアクセスできるヤバいDMAコントローラらしいのですが、なんかヤバそうなので今回は触れないことにします。

(2) DMA転送のデータが化ける

【問題】 上記(1)は解決したが、DMA転送したデータが化けている。
【原因】 実メモリとキャッシュの一貫性が崩れている。
【対策】 MPU(メモリ保護ユニット)で、DMA転送用のメモリ領域の属性を設定する。

STM32H7はL1キャッシュ(※)を搭載しています。DTCMはそもそもCPUコアと同一クロックで動作するのでキャッシュが介在しませんが、CPUコアより低速な汎用SRAM (AXI SRAM および AHB SRAM1~4)やFlashメモリなどはキャッシュを有効にすることでパフォーマンスが向上します。ただし、キャッシュはデフォルト設定では無効です。

それは良いのですが、DMA転送を用いた場合、実メモリとキャッシュの一貫性が崩れる可能性があります。CPUがキャッシュにライトしても実メモリに書き戻されなければDMAコントローラは古いデータをリードすることになりますし、DMAコントローラが実メモリにライトしてもキャッシュが破棄されなければCPUはキャッシュ上の古いデータをリードすることになります。

いちばん安直な解決策はキャッシュを無効にするか、MPU(メモリ保護ユニット)でDMA転送用のメモリ領域をキャッシュ不可に設定することです。もちろんパフォーマンスは犠牲になります。キャッシュを有効にしたい場合には、けしかんさんの記事ねむいさんの記事を参考にしてください。

どちらにしろ、MPUでDMA転送用のメモリ領域の属性を設定することになるのですが、この設定が非常に分かりにくいです。


 TEX: Type extension field, C: Cacheable, B: Bufferable, S: Shareable

  • Strongly Ordered: アクセスの順番を守る。ライト完了まで次に進まない。
  • バイス: アクセスの順番を守る。ライトはバッファされる。
  • ノーマル (上記以外): アクセスの順番を入れ替えたり、投機的先読みをしたりする。
    • キャッシュを使用しない。
    • ライトスルーでキャッシュを使用する。
    • ライトバックでキャッシュを使用する。

通常のメモリは「ノーマル」に、メモリマップドI/Oのペリフェラルは「Strongly Ordered」または「デバイス」に設定します。(後述)

※ STM32H7に搭載されているCPUコア ARM Cortex-M7は、L1キャッシュ (1次キャッシュ) のみサポートしています。MMUを持つARM Cortex-A系のCPUは、L2キャッシュ以上の多層のキャッシュをサポートしています。

(3) FMCも要注意 (とくにLCDを接続するとき)

【問題】 8080 I/FのLCDを外部メモリバスに接続したが映らない。
【原因】 メモリアクセスの順序が入れ替わってる?
【対策】 MPU(メモリ保護ユニット)で、LCDのアドレスの属性を設定する。

8080インターフェースあるいは6800インターフェースのLCDは、CPUの外部メモリバスに接続して制御します。STM32マイコンでは外部メモリバスを制御するユニットはFMCと呼ばれます。FMCはNOR Flash, NAND Flash, SDRAMなどの各種メモリデバイスの他に8080/6800インターフェースのLCDにも対応しています。(※)

それは良いのですが、LCDのようなデバイスFMCに接続する場合には、MPUの設定が必要です。通常のメモリデバイスとは異なり、LCDへのアクセスには副作用 (つまりLCDの制御) が伴いますので、アクセスの順序を守らなければなりません。いっぽうで、STM32Hは効率化のためにメモリアクセスの順序を入れ替えたり、投機的先読みをしたりできます。しかし、LCDの場合にはアクセスの順序が入れ替わってしまうと意図した制御ができません。

例えば下記のようなコードです。これが通常のメモリへのアクセスなら順序を並び替えても問題なさそうです。しかし当然ながら、順序を入れ替えると正しくLCDを制御できません。

volatile uint16_t* lcd_cmd = (volatile uint16_t*)0x60000000; // コマンド
volatile uint16_t* lcd_dat = (volatile uint16_t*)0x60080000; // データ

*lcd_cmd = 0xC5;
*lcd_dat = 0x0E;
*lcd_cmd = 0x36;
*lcd_dat = 0xC8;

このような場合、LCDのアドレスの属性を、MPUで「Strongly Ordered」または「デバイス」に設定します。これらに設定された領域については、CPUはアクセスの順序を守ります。MPUの設定については (2) で説明した通りです。「Strongly Ordered」に設定するソースを以下に示します。

    MPU_InitStruct.Enable=MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x60000000;       // ベースアドレス 
    MPU_InitStruct.Size = ARM_MPU_REGION_SIZE_1MB; // 範囲
    MPU_InitStruct.AccessPermission=MPU_REGION_FULL_ACCESS;
    
    MPU_InitStruct.TypeExtField=MPU_TEX_LEVEL0;            // TEX = 000
    MPU_InitStruct.IsCacheable =MPU_ACCESS_NOT_CACHEABLE;  // C = 0
    MPU_InitStruct.IsBufferable=MPU_ACCESS_NOT_BUFFERABLE; // B = 0
    MPU_InitStruct.IsShareable =MPU_ACCESS_SHAREABLE;      // S = 1
    
    MPU_InitStruct.Number=MPU_REGION_NUMBER0;
    MPU_InitStruct.SubRegionDisable=0x00;
    MPU_InitStruct.DisableExec=MPU_INSTRUCTION_ACCESS_DISABLE;
     
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

※ 6800インターフェースのLCDを接続する場合には若干の細工が必要です。

参考記事