STM32MP2を試す
Playing with the STM32MP2 (serd.es)
要約
この記事は、筆者がSTM32MP2アプリケーションプロセッサを、Linuxや複雑なブートローダーを使わずに、ベアメタルRTOSまたはシンプルなイベントループで動作させる試みについて詳述しています。筆者は、デュアルCortex-A35コア、5Gbps SERDES、そしてPCIeインターフェースを備えたこのSoCを、FPGAとの連携を視野に入れ、マイクロコントローラーのように扱うための初期設定とデバッグプロセスを解説しています。
全文翻訳
STM32MP2を試す
レコードスクラッチ
まず、お詫びです。この投稿は大幅に遅れてしまいました。TCXOの投稿よりも前に、おそらく2025年12月頃に書き始めたのですが、その後、セキュリティ上の影響があるシリコンバグを偶然発見してしまい(この投稿の後半で説明します。このバグに関する仕事からのブログ記事も近日公開予定です)、STがレビューしてアドバイザリを出すまで公表できませんでした。それがようやく公開された後、他のことに忙しくなってしまいました。
とにかく、こうしてここにいます。長年の読者ならご存知の通り、私はSTM32とFPGAを組み合わせるのが好きで、それによってかなりのパフォーマンスを得ています。例えば、MCUからFPGA側のRGMII MACへのイーサネット帯域幅は1Gbps近くに達します。ただ一つの問題があります。STM32H7のFMCは多くのピンを必要とします。チップセレクトとアドレスラインをすべて使用したい場合、30本以上必要です。しかも、それらはパッケージの片側にきれいに配置されているわけではありません。それは簡単すぎます。あちこちに散らばっています。
TFBGA240パッケージのH750におけるFMCの例を以下に示します。
[画像: FMCピンが配列全体に散らばっており、論理的に配置されていないことを示すBGAファンアウト]
TFBGA216も同様です。
[画像: FMCピンが配列全体に散らばっており、論理的に配置されていないことを示すBGAファンアウト]
長年、私はより良い方法を夢見てきました。私の究極の夢のMCUは、おそらく私のような奇特な人間しか買わないでしょうが、1GHzのCortex-M7またはM85で、おそらくTSMC 28LPで製造され、1MBのNORフラッシュまたはReRAM、1MBのSRAM、RGMII MAC、独立したVCCIOレールを持つ複数のFPGAスタイルのI/Oバンク、そしてFPGAベースのペリフェラルと2本の差動ペアのみで直接統合するために使用できるAXI Chip2Chip SERDESブロックを備えているでしょう。
外部FPGAとFMCで接続されたSTM32H735はそれに近いものでしたが、低コストボード(例:4層バッチ製造)では使用するのが面倒でした。なぜなら、FMCはそれらの制約下ではルーティングがほぼ不可能だからです。STM32H7のH75xラインのような他の一部のSTM32パーツは、0.8mmパッケージで入手可能で、バッチ製造設計ルールで(苦労して)対応可能ですが、これらは最大CPU速度が低く(480MHz対550MHz)、これは問題を完全に解決するのではなく、問題を先送りしているに過ぎません。依然として大量のピンを消費します。
PCIeを備えた高速なM7があれば、ほぼ私の望むものになります(PCIeは厳密にはAXIではありませんが、それらをブリッジすることは完全に可能です)。しかし、そのようなものを作るメーカーはありません。PCIeを備えたCortex-Rは入手可能で、真剣に検討しました(そしてまだチェックしたいと思っています)が、それでも馴染みのあるSTM32ペリフェラルセットからは大きな飛躍となります。そして、これらのパーツの多くは、暗号化アクセラレーションを備えていないか、ドキュメントを暗号化アクセラレータにロックしているため、私にとっては絶対的な取引不成立条件です。
STM32MP2の登場
しかし、STはSTM32MP2ラインを発売しました。これは、デュアルCortex-A35、M33やM0+のような低速な補助プロセッサ、いつか試すかもしれないフルGPU/NPU、そして—私にとって最も興味深いのは—5Gbps SERDESとPIPEインターフェースを備えたフルアプリケーションプロセッサSoCです。これは、内部のプロトコルIPに応じてPCIe gen1/2 x1またはUSB 3として動作するように構成できます(それらを多重化するレジスタビットがあります)。また、私がすでにライブラリと作業経験を大量に持っている、同じく馴染み深いSTM32ペリフェラルをほとんど維持しており、学習曲線は短くなります。これには、私がすでにドライバを持っており、比較的よく文書化されていた同じAES-GCMとTRNG IPも含まれます。
これは私が求めていた以上のコンピューティングパワーを持つ強力なデバイスですが、将来的にさらに強力な設計が必要になった場合に備えて、ポケットに入れておけるパーツを欲しかったのです。そしてPCIeは、2本の差動ペアのみでFPGAのメモリマッピングをファームウェアから容易に行える可能性があります。
ただ一つの問題があります。STM32MP2は明らかにLinuxを実行するように設計されており、ベンダーのBSPには私が気にしないu-boot、TrustZoneなどのものがすべて含まれていました。私は単に、シンプルなRTOS、あるいはwhile(1) { doStuff(); } イベントループを実行できる大きなマイクロコントローラーが欲しかったのです。
どのくらい難しいのか?
そこで、MP2に、実際にはPCIeやLPDDR PHYのような珍しいペリフェラルを備えた非常に高速なMCUであると納得させることができるか試してみることにしました。一般的なMCUにはないものです。
実験の目標は以下の通りでした。
- ベンダーSDKやHALを使わず、完全にアップストリームのaarch64 / arm g++とbinutilsのみでビルドする
- A35とM33補助コアでコードを実行させる
- 外部DDRを使用しない(これはボードの複雑さを大幅に増大させ、MCUレベルのBOM/PCB設計努力レベルにできるだけ近づけたいと考えていました)。すべてのコードとデータはオンダイSRAMに格納します。
- TrustZone、uboot、その他の余計なものは使用しない。初期の「ファーストステージブートローダー」(FSBL)フラッシュイメージからEL3でシンプルなイベントループを実行するだけで、それがファームウェア全体となります。
- これらの制約下でFPGAとのPCIeを動作させる
STM32MP257開発ボード(STM32MP257F-EV1)を購入し、他の新しいSTM32パーツと同様に、リンカースクリプトとヘッダーをstm32-cppに追加し、他のSTM32パーツと同じレジスタインターフェースを使用するペリフェラルのアドレスを設定することから始めました。唯一の違いは、イメージのベースアドレスがフラッシュではなくSRAMにあったことです。
M33の起動
次に、Cortex-M33補助コア用のシンプルなLED点滅ファームウェアをビルドし、通常のSTM32と同様にJTAG経由でロードしました。何度か失敗しましたが、比較的早く点滅するLEDを動作させることができました。実績#1解除:チップと通信し、レジスタを操作できます。
次のマイルストーンは、同じ点滅ファームウェアをフラッシュから実際に起動させることでした。例のコードやWikiをいくつか調べる必要がありましたが、すべての詳細(例えば、内部SRAMは異なるTrustZone設定を持つ4つの異なる物理アドレスにマッピングされていますが、ブートROMは1つからしか起動できません)を理解しました。しばらくして、フラットバイナリRAMイメージを取得し、STM32MP2ブートROMがフラッシュからロードして実行できるように必要なFSBLヘッダーを添付するツールを作成しました。その後、SRAMにロードして、別のSRAM場所からフラッシュにイメージを書き込むことができる小さなフラッシャーユーティリティを作成し、プロセス全体をブートストラップしました。実績#2解除:チップは不揮発性メモリから起動し、起動ごとにJTAG経由でロードする必要がなくなりました。
FSBLイメージを準備する際に留意すべき点がいくつかあります。
- FSBL-MコードイメージをRETRAM、具体的にはブートエイリアス(0x0e08_0000)内に配置します。
- スタックと初期グローバル変数もRETRAMに配置する必要がありますが、Dバスエイリアス(0x3008_0000)上に配置します。
- メモリマップは、これらの物理メモリが重複しないようにする必要があります。つまり、0x0e08_4141にコードがある場合、0x3008_4141にデータがあってはなりません。.text、.data、.bssが異なる論理アドレスに配置され、かつ互いに衝突しないようにするリンカースクリプトの操作は冒険ですが、仕組みを理解すればそれほど悪くはありません。
- FSBL-Mイメージ用のスロットが2つあります。プライマリはフラッシュオフセット0x0000_0000、フォールバックは0x0004_0000です。
このファームウェアは「M33-TD」モードで起動しました。このモードでは、ブートROMはFSBLイメージをロードした後、すぐにM33に制御を移し、M33は完全なTrustZone権限を持ちます。私がA35を最初に起動するのと、ベアメタルCortex-Aを初めて扱うのが同じにならないように、プラットフォームの段階的な起動を行いたかったため、A35-TD(A35が最初に起動する)よりもこちらを選択しました。
その後、他の新しいSTM32プラットフォームと同様に、シリアルコンソールを起動し、キャッシュを有効にし、PLLとクロックツリーを設定し、起動時にプラットフォーム情報をコンソールにダンプするなど、しばらく作業を続けました。
コードはすべてGitHubにありますが、M33-TDでのベアメタル起動に必要な手順の簡単な概要は以下の通りです。このSoCセットアップコードはカスタムアセンブリを必要とせず、CPU設定(割り込みの有効化/無効化やFPUの準備など)を除けばすべてC/C++で行えます。
- VDDIO1〜4のVDDIOモニターを有効にします。有効なレポートが返ってくるまで待ち、PWRに準備ができたことを伝えます。この手順なしでは、ほとんどのI/Oが利用できません。
- オプション:バックアップSRAMへの書き込みを有効にします(デフォルトでは、Vcoreドメインがレギュレーションから外れたときのグリッチを防ぐためにPOR/ブラウンアウト検出によってゲートされています)。
- ビデオブロックを使用しないため、SYSCFG.VDERAMCRを設定してビデオエンコーダRAMをプロセッサメモリとして使用します。
- すべてのSRAM(SYSRAM、SRAM1、SRAM2、バックアップSRAM、VDERAM、LPSRAM1〜3)をRCCで有効にします。数が非常に多いです。
- RISAB SECCFGRレジスタの設定を行い、セキュア領域コードがSRAM1/2およびVDERAMにアクセスできるようにします。デフォルトでは、EL3セキュアモードからはアクセスできません。すべてのコードをEL3セキュアモードで記述するため、すべてをそこに割り当てたいと考えています。
- クロックツリーを初期化します。さまざまなクロックが多数あります。設定方法の詳細は私のコードを参照してください。
- M33のIサイドおよびDサイドL1キャッシュを有効にし、無効化します。
A35へ進む
次に、A35を動作させる時が来ました。PCIeを試す前にこれを行う必要がありました。なぜなら、M33のメモリマップではPCIe BAR領域にアクセスできなかったからです。これは面倒でしたが、M33が主に管理プロセッサとして機能し、すべてのアプリケーションコードがA35にあるのであれば、我慢できることでした。
以前は、Cortex-Aのセットアップにかかるオーバーヘッドが大きかったため、Cortex-Aの作業を避けていました。
まず、A35イメージがフラッシュにある場合、それを実際に起動する必要があります。これを理解するには、さまざまなサードパーティライブラリコードなどをかなり読む必要がありました。データシートでは明らかではありませんでした。基本的な手順は以下の通りです。
- PWR.CPU1D1SRのCPUのDSTATEを確認します。STANDBY状態である必要があります。
- EXTI1.RPR3経由でSEVを送信してCPUをウェイクアップします。
- RCC.C1RSTCSETR経由でCPUをリセットします。
- EXTI1.SWIER3を設定します。
- PWR.CPU1D1SRがCPUがHOLD_BOOT状態であることを報告するまで待ちます。
- RCC.CPUBOOTCRを2に設定して、CPUをブートに解放します。
フラッシュ内のFSBL-Aイメージ(プライマリはオフセット0x0008_0000、フォールバックは0x000c_0000)は、aarch64モードで両方のコアで同時に並行して起動します(ただし、何らかの理由でaarch32モードで起動したい場合のために、フューズを吹いて変更することもできます)。この初期化の最初の部分は、ハードウェアレジスタを操作し、スタックなどを初期化するために必要な多くの作業があるため、アセンブリで行う必要があります。
また、このイメージはフラッシュにある必要があります。M33でコードがすでに実行されていても、M33がSYSRAMにイメージをダンプしてA35に実行させる簡単な方法はないようです。A35がブートに解放されると、ROMはストラップピンをチェックし、指定された場所(この場合はSPI NORフラッシュ)からイメージをロードします。M33がすでに実行されていても、A35はブートROMを再実行します。
- イメージは、ブート/セキュアSYSRAMエイリアス(0x0e00_0000)にロードする必要があります。
- イメージは、リセットベクタへのジャンプから始まり、その後、例外テーブルがハードウェアで要求される2KBの境界にアラインされるようにパディングされます。ここには最適化の余地があるかもしれませんが、現時点ではスペースが無駄になっていますが、低レベルの初期化のほとんどをそこに収めることは可能でしょう。
- 例外テーブルは、EL0、上位例外レベル、および下位レベルからの同期例外のために4回繰り返されます。テーブル自体にはハンドラごとに128バイトありますが、ベクター化されていません。ただし、より多くのスペースが必要な場合は外部コードにジャンプできます。私はこれがあまり好きではありません。Cortex-MのNVICの方が好きです。これはブランチ命令を節約し、割り込みレイテンシをわずかに減らすのでしょうか?いずれにせよ、初期テストでは、すべての例外ハンドラを無限ループに設定しました。
- 起動時のレジスタ値は未定義です(ブートROMが残した値です)。そのため、さらに進む前にすべてゼロにクリアします。
- 例外ステータスレジスタもクリアする必要があります。
- 浮動小数点ユニットとNEON/ベクトルユニットを使用する予定がある場合は、それらを初期化します。現在の起動コードではまだ行っていませんが、フロートを使用する予定がある場合は、ここで行う必要があります。
- VBAR_EL3を例外テーブルに向けます。これにより、例外が発生した場合にブートROMの例外ハンドラに戻るのを防ぎます。
- CPUECTLR_EL1でキャッシュコヒーレンシを有効にします。
この時点で、コアの最も低いレベルが設定されていますが、どのコアで実行しているかはまだわかりません。そのため、次のステップはMPIDR_EL1を読み取り、どちらのコアであるかを判断することです。なぜなら、その後のステップはコアによって異なるからです。
コア0の場合:
- スタックポインタを初期化します。これで、C++コードを実行するための有効な環境が整いますが、注意点が1つあります。MMUが有効になるまで、すべてが「デバイス」メモリと見なされ、newlib aarch64のmemcpy実装はアラインされていないアドレスからldpを試みるため、memcpyを呼び出すことはできません。これはデバイスメモリからは許可されていません。
- 初期ページテーブルを作成します。私のマッピングには合計3つのページテーブルがあります。1つはレベル1(メモリマップ全体をカバーする512x 1GBページ)、その下に2つのレベル2(512x 2MBページ)があります。1つはオンチップSRAMとPCIe BAR領域をカバーし、もう1つはペリフェラル、OCTOSPI、およびFMCをカバーします。私のマッピングでは、MMIO範囲を除いてすべてキャッシュ可能として設定されており、変換は1:1(VA = PA)です。MMUをNXとキャッシュ属性の制御にのみ使用しているためです(Cortex-MのMPUがあればよかったのですが)。ブートROM領域をカバーするページは、0x0000_0000にマッピングされており、ヌルポインタがランダムなROMの内容を読み取るのではなく、セグメンテーション違反を起こすように、意図的にマッピングしないままにしました。
- ページテーブルが設定されたら、MMUをオンにします。
- アプリケーションコードの実行前に必要なその他の電源、クロックなどの初期化を行うためのBSPフックを呼び出します。これには、A35自体のメインPLLの設定と起動が含まれます。
- グローバルオブジェクトのコンストラクタを実行するために__libc_init_arrayを呼び出します。
- システム初期化が完了したことをコア1に伝えるフラグを設定します。
コア1の場合:
- スタックポインタを初期化します。
- コア0がセットアップを完了するまでスピンします。
- コア0と同じページテーブルを指すMMUを設定します。ページテーブルはそれぞれ2KBで、オンチップSRAMは限られています。異なるマッピングを各コアに持つ正当な理由がない限り、2ページテーブル分のメモリを消費したくありません(これは私がCortex-Mを好む理由の1つです。MPUはAのMMUよりもはるかに手間がかかりません。しかし、PCIeを備えたMを作るメーカーはいません。それが現状です)。
どちらのコアでも:
- 命令キャッシュとデータキャッシュを無効化し、有効化します。
- BSPが提供するCoreMain()関数を呼び出し、コア番号を引数として渡します。これにより、アプリケーションコードの実行を開始できます。
すべてのコアがセットアップされました。次は何を?
DDRについてはあまり触れていません。なぜなら、使用する予定がないからです。これはSynopsysのDDR3L/DDR4/LPDDR4コントローラーで、ブロブと、すべてのデータラインのための面倒なPCBレイアウトが必要で、多くの電力を消費し、とにかく過剰です。STM32H735からMP2に切り替えた主な目的は、パラレルメモリバスをPCIeに置き換えることであり、DDRを使用するとこれらのメリットのほとんどが無効になります。
とにかく…この時点で、あなたは基本的に強化されたSTM32H7を手に入れたことになります。
- MP2には808KBのSRAMがあります。私の通常のH735は564KBでした。
- フラッシュはありません。そのため、コードとデータをSRAMに収める必要があります(または外部SPIフラッシュからXIPしますが、これは遅いです。しかし、2x 32KB L1$と統合された512KB L2$があるので、ファームウェアがそれに収まるなら、かなり高速になるはずです。まだ試していません)。
- 1.5GHzのデュアルA35コア。M7の500MHzと比較して、6倍のクロックサイクルが利用可能ですが、IPCあたりのパフォーマンスは劣ります(A35は1.7 DMIPS/MHz、M7は2.14 DMIPS/MHz)。そのため、典型的なワークロードではおそらく約4倍高速です。
- ITCM/DTCMがないため、L1$でミスしたものはシングルサイクルになりません。これもAの不満な点の一つです。私はSTM32H7のTCMを高速な処理に多用しています。
- 400MHzのM33と200MHzのM0+補助コア。
MP2はH735と同じFMCハードIPを備えているため、もし私が愚かなことをしたいなら、H7と同じようにFPGAとインターフェースさせることができます。しかし、それならなぜ、より複雑なSoCを起動するためにこれほどの手間をかけたのでしょうか?PCIeを使いたいのです。そうでなければ意味がありません。
PCIeの起動
STM32MP257F-EV1には、Wi-FiカードなどのためのミニPCIeスロットがありますが、M.2に変換するアダプターを見つけました。これにより、初期テストのためにランダムな古いNVMe SSDや、後でM.2 FPGAモジュールとインターフェースさせることができました。
データシートには、設定に関する初期の手がかりがいくつかあります。通常通り、RCCでCOMBOPHYとPCIe IPを有効にし、次にSYSCFG.COMBOPHYCR1を操作して、外部リファレンスクロック(通常100MHzですが、PLLは他の乗数/除数組み合わせで設定できます)を使用するか、RCCから生成された内部クロックを使用するかを指定する必要があります。
すべてがオンになったら、COMBOPHYCR2を設定してPCIeモードを有効にし、COMBOPHYCR3を操作してインピーダンス制御ドライバ/終端をキャリブレーションし、他の内部ハウスキーピングが完了してPHYがすべて準備完了と報告するのを待ちます。
最後に、SYSCFG.PCIECRを操作して、初期バス/デバイスIDを指定し、LTSSMを有効にしてリンクをトレーニングできるようにし、PERSTをペリフェラルにトグルし、SYSCFG.PCIESR1がトレーニングの成功を報告するのを待ちます。驚くべきことに、ファームウェアブロブや奇妙なセットアップフローはありません。数回のレジスタ操作だけで動作し、リンクをトレーニングします。
少なくとも、そう思っていました。バスの列挙を試みる際にいくつかの問題が発生した後、リンクが不安定で常に再トレーニングしていることがわかりました。
PHYレベルのデバッグ
幸いなことに、私は高速なスコープを持っており、ngscopeclientはPCIeとPCIeリンクトレーニングをデコードできます。そこで作業を開始しました。
[画像: ミニPCIeコネクタ全体にハンダ付けされた差動プローブのクローズアップ]
起動プロセスを見ると、すべて順調でした。リンクは問題なくL0にトレーニングされ、数個のTLPを通過した後、Recovery.RcvrLockにドロップし、しばらく回復してから再びドロップしました。その後、しばらくアップしたままになり、ドロップし、波形の終わりまでダウンしたままでした。
しかし、なぜドロップしていたのでしょうか?最初の質問は、リンクのどちら側がドロップを開始したかでした。ズームインすると、STM32が最初にドロップしている(L0のアイドルからトレーニングセットの送信に移行する)ことがはっきりわかりました。リンクパートナーがまだ有効なアイドルを送信している間に、明らかな理由もなくドロップしていました。したがって、問題は少なくともSTM32MP2で検出されていたか、または引き起こされていました。
信号品質の問題ではないかと思いました。そこで、リンクの両方のレッグのクイックアイパターンを起動しました。RXレーン(ペリフェラルからSTM32へ)は問題ありませんでした。短く、ミニPCIeコネクタのすぐ近くでプローブしていたため、少しオーバーイコライズされていましたが、200mV以上のアイ高さで広く開いていました。
TX(STM32からペリフェラルへ)のアイは、これまで見たことのないものでした。ほとんどの時間、クリアで広く開いていましたが、アイ全体が pretty uniform な 1e-4 BER で閉じられていました。ジッターによる水平方向の潰れ、ノイズや反射による垂直方向の潰れなどはよく見ますが、そのようなシナリオでは通常一貫性があり、アイには明確な上下のバーや昇降エッジはありません。
私が考えられる唯一のことは、断続的な何かでした。信号はほとんどの場合良好で、リンクはアップし、その後何かが発生する—ノイズバーストかもしれない—それが一時的にそれを消去し、その後回復する。
そこで、同じ波形の2番目の、より長いドロップを見ました。これはさらに奇妙でした。リンクは正常でしたが、突然、STM32MP2は絶対的なナンセンスを送信し始めました—スクランブラ値が無効であるとデコードされるビットパターン、あるいは8b10bシンボルとしてさえも整形されていないものもありました!その後、約40μs後にリンクパートナーがそれに気づき、再トレーニングに入りました。
リンクドロップの前後数シンボルだけをズームインすると、問題がすぐにわかりました。紫色のThreshold_5信号(アナログ波形の単純な閾値処理)と、その下の緑色の矩形波(DSP PLLによってアナログ波形から回復されたクロック)の相対位相を見てください。
左側では、すべてが順調です。回復されたクロックエッジは、データに対して90°位相シフトされており、十分なセットアップおよびホールドマージンでクリーンにサンプリングできます。しかし、画面の中央付近で、物事がうまくいかなくなります。D15.2シンボルが終わる頃には、クロックエッジはデータエッジからわずか58ps後(2.5GT/sでの400ps UIの理想的な200psセットアップ/ホールドウィンドウの代わりに)になっています。そしてD11.7が終わる頃には、クロックとデータは重なり合っており、セットアップ/ホールドウィンドウはまったくなくなり、プロトコルデコードはビットエラーを見始めます。
DSPと実際の物理CDR PLLの両方には帯域幅が限られているため、20UI未満でこの大きな位相シフトを与えられた場合、それらが追従できる可能性はありません(ただし、試みはします)。
RX側のTIE(時間間隔誤差、基本的にCDR PLLがUI境界をどこで期待するかと実際の波形のレベルクロスとの差)をプロットすると、奇妙なアーティファクトのないかなり均一なジッターが見られます。しかし、TX側ではTIEがいたるところにジャンプし、±200ps(UIの半分—エッジが次のUIの一部と見なされる前の最大誤差)まで振り切れ、CDRは公称データレートの30MHz上下で激しくスルーし、同期を維持しようとして失敗しています。
すべての証拠をまとめると、COMBOPHYのSERDES PLLが断続的にロックを失っていることは明らかです。しばらくはすべて順調ですが、SERDES内部で何かが起こり、PLL VCOがしばらくの間変動し始めます。これにより、送信データが変動し(スコープキャプチャで見たように、リンクパートナーが受信できなくなる)、受信側にも同様の影響を与え、STM32が正常な受信データを誤って読み取る原因にもなるでしょう。
COMBOPHY PLLを設定するためのレジスタは数個しかなく、私がそれらに書き込んでいたものはすべてデータシートによると問題ないように見えたので、設定ミスではないと確信していました。開発ボードはSTのリファレンスデザインであり、問題ないはずだと仮定していました。
では、ハードウェアでもソフトウェアでもない場合、何が原因でしょうか?他に調べるべきことは、リファレンスクロックしかありませんでした。COMBOPHYをRCC(ck_ker_usb3pciephy、25MHzに設定)でクロック供給するように設定し、SYSCFG.COMBOPHYCR1を0xc8に設定して必要な2500MHzのVCO周波数(スペクトラム拡散なし、乗数100)を得ていました。
しかし、STM32MP257F-EV1には専用の100MHzリファレンスクロックも利用可能でした。COMBOPHYCR1を0x33に操作して外部クロックを選択し、25倍すると同じ2500MHzのVCO周波数になりますが、内部PLL(TSMC PLLTS16FFCLAFRAC2フラクショナル-N PLL)とRCCクロック配布ネットワークの追加ジッターがなく、おそらく外部クリスタルがこの用途に対して十分な公差を持っていなかったためです。
そして…それは完璧に機能しました。リンクはアップし、2.5GT/s gen1 x1に問題なくトレーニングされ(gen2は後で保存することにしました)、すべてが正常に見えました。
列挙
この後、IPは実際には文書化されていないため、物事はさらに複雑になります(いつものように、データシートの一部とレジスタマップはインターネットのさまざまな隅に現れた可能性があります)。幸いなことに、ubootとLinuxカーネルコードの中に、セットアップ方法についての手がかりを見つけることができました。PCIe IPブロックはAHB5上に標準的なPCIeルートコンプレックスレジスタセットを持つレジスタブロックを備えています(これらのレジスタのほとんどはPCIeベース仕様で文書化されています。ベンダー固有のレジスタも多数ありますが、機能させるためには必要ありません)。その後、実際のPCIeデバイスは、0x1000_0000から0x1fff_ffffまでの256MBの物理AXIアドレス空間にマッピングされます。
PCIe列挙を扱ったことがなかったので、必要な手順をまとめるのに時間がかかりました。このコードはstm32-cpp/src/peripheral/PCIE. extit{およびsrc/cpu1-a35/bsp/hwinit.cpp}にあります。詳細を確認したい場合は、ここに概要を説明します。
まず、リンクステータス、速度、ハードウェアIPバージョンなどの標準レジスタをいくつか読み取り、ログに記録します。厳密には必要ありませんが、トラブルシューティングに役立ちます。
次に、標準PCIEアドレス空間内でいくつかの基本的な設定を行う必要があります(MISC_CONTROL_1にマジックアンロック値を書き込んで書き込み保護を解除した後)。ルートコンプレックスのバス番号を選択し、メモリ制限を指定します。しかし、次にリンクパートナーのレジスタと通信する必要があります。それはどのように機能するのでしょうか?
PCIe IPには「内部アドレス変換ユニット」(iATU)があり、これは一種のIOMMUです。これは、受信および送信TLPを取得し、AXIブロック内のアドレスをPCIe BARまたは設定アドレスに変換し、適切なTLPタイプを追加します。
したがって、次のステップは、AXI空間の一部(デバイス01:00.0への設定アクセスにこれをマッピングすることを選択しました。これは、リンクパートナーに割り当てるバスとデバイスです)を設定し、iATUを構成することです。完全な機能を持つスタック(PCIeブリッジなど)を実装する予定はありませんでした。FPGAにのみSTM32を直接配線するつもりだったからです。そのため、多くの設定はハードコードで対応できました。
次に、AXI空間の別の範囲(0x1000_0000から0x17ff_ffff、AXI範囲の最初の128MB)を設定し、PCIeアドレス0x0000_0000にマッピングしました。ここにリンクパートナーのBARを配置します。
これで、0x1fff_0000にマッピングされた標準PCIe設定アドレスブロックが利用できるようになり、それをウォークして表示できます。確認のためにいくつかのレジスタを表示するコードを記述しましたが…すべてゼロが返ってきました。
なぜゼロが返ってくるのか?
スタックの上位レベルで何かが明らかに壊れていました。リンクはアップしており、DLLPが流れているのが見え、アイは広く開いていました。iATUは少なくとも有効に近い設定になっていたはずです。なぜなら、完全に無効なメモリマッピングであれば発生するようなセグメンテーション違反は発生しなかったからです。
PCIe信号にはまだプローブを接続していました(このテストはクロックジッター/PLLトラブルシューティングといくらか重複していましたが、話をより直線的に理解しやすくするために、そのように記述しました)。次のステップは、テスト読み書きのプロトコルデコードを試みることでした。
見つかったのは非常に混乱するものでした。
テストファームウェアを作成し、設定スペースの開始位置に0xdeadbeefを書き込みました。設定書き込みTLPは、有効なアドレスとすべてゼロのペイロードでSTM32から出力されました。
[画像: 0x00000000の設定書き込みを示すPCIe TLP]
BAR範囲内の場所に0xfeedfaceを書き込もうとしました。これも正しいアドレスとすべてゼロのペイロードで出力されました。
[画像: 0x00000000のメモリ書き込みを示すPCIe TLP]
さらに混乱したのは、読み取りを試みたことです…そしてデバイスは応答しました!有効な読み取り完了TLPと、明らかにゼロではないペイロードで。しかし、私のファームウェアは読み取りがすべてゼロで返ってきたと認識しました。
[画像: 0x5c 1c 27 13を返す読み取り完了を示すPCIe TLP]
要約すると:設定またはデータスペースへのすべての書き込みTLPは、ヘッダーフィールドはすべて正しいですが、ペイロードはすべてゼロです。すべての読み取りTLPは正常に送信され、デバイスは期待通りに応答しますが、私のファームウェアはすべてゼロとして認識します。
これはSTM32内部のどこか、PCIe IPまたはAXIファブリックのいずれかが、アドレスを変更せずにデータペイロードのみをゼロにしていることを示唆しています。一体何がこれを起こしうるのでしょうか??
RISAFの登場
STM32MP2リファレンスマニュアルを見ると、明白な容疑者がありました:リソース分離スレーブユニット(RISAF)。
これはPCIeとメインAXIインターコネクトファブリックの間のAXIバス上にあり、問題のないトラフィックは通過させますが(例:TrustZone非セキュア領域がセキュアリソースにアクセスする場合)、リクエストをブロックしてセキュア領域が適切に処理できるフォールト割り込みを発行します。
そこで、M33上に簡単なモニタータスクを構築し、RISAF5(PCIe AXIインターフェースを保護するインスタンス)のIASRレジスタをポーリングしました…そして、私のファームウェア(EL3セキュアモードで実行されていました。権限を低下させたことはありません)からPCIe BAR領域に触れた瞬間に、フォールトが発生しました。
確認のため、M33のBSP_Init()にいくつかのレジスタ操作を追加しました(M33-TDモードでは、このモードではシステム信頼の根源と見なされるため、M33のみがRISAF設定に触れることができます)。これにより、A35からのAXIアドレス範囲全体に対するセキュアモードでの特権および非特権の読み書きが両方とも有効になります。
そしてすべてが完璧に機能しました。私の書き込みTLPはスコープで見たとおり正しい値になり、読み取りはファームウェアに正常なデータを返しました。基本的な単一デバイスのみのバス列挙コードパスを作成し、多くの情報をダンプしましたが、すべてうまく機能しました。
[画像: 正しい0xdeadbeef書き込みを示すPCIe TLP]
まあ、クソだ。
この時点で、私がTrustZoneバイパスの可能性のあるシリコンエラッタを抱えていることに疑いの余地はありませんでした。そこで、会社の開示チームに連絡し、STMicroのPSIRTにアドバイザリを送信するプロセスを開始しました(このバグはオフタイムで見つけましたが、誰かが怒った場合に備えて、開示パスに会社名と弁護士がいる方が良いと考えました)。
この時点で確認できたことは以下の通りです。
- RISAFによってブロックされるべき攻撃者は、PCIe設定およびBARアドレス空間に対して32ビット読み取りトランザクションを発行できます。これらのトランザクションは、RISAFが開いている場合と同じように、整形されたTLPとして送信され、ペリフェラルに期待される副作用を引き起こしますが、攻撃者はペリフェラルから返された実際の値の代わりに、すべてゼロの読み取り値を見ることになります。
- 32ビット以外のトランザクションは奇妙な動作を示しましたが、攻撃者が何らかの形で利用できる可能性があるため、完全には調査しませんでした。
- 攻撃者はPCIe範囲に対して32ビット書き込みトランザクションを発行できます。要求された書き込みデータに関係なく、TLPはすべてゼロとして送信され、ターゲットとなったMMIOレジスタを上書きします。
- RISAFのエラー検出メカニズムは機能し、TrustZoneセキュアモニターは問題について警告を受けますが、書き込みがディスパッチされた後のみです。
- これはRISAF自体のシリコンバグです。
アドバイザリを作成してSTに送信し、彼らが何かを公開して詳細を話せるようになるまで、ほとんど棚上げにしていました。
かなりの時間が経過した後、彼らはアドバイザリとエラッタを公開しました。これは現在、エラッタ2.3.33として文書化されています。
いくつかの疑問が残っています。
第一に:現実世界のデプロイメントまたはユースケースで、潜在的に脆弱なものはありますか?PCIeペリフェラルとTrustZoneモニターのどのような組み合わせが、問題に気づかないか、または十分に速く反応せず、読み取り副作用や/またはすべてゼロの書き込みによって、権限昇格やその他の方法で悪用される可能性がありますか?
第二に、より興味深いのは:RISAF5だけが影響を受けているのでしょうか?STのアドバイザリとエラッタはRISAF5について言及していますが、これはRISAF5が私が動作するPoCを持っている唯一のインスタンスであるためかもしれません。STが他のインスタンスをテストして、同じバグに対して脆弱でないことを確認したのか、それとも誰も見ていないのかはわかりません。
RISAF1はバックアップSRAMを制御し、RISAF2はOCTOSPIを、RISAF4はDDRコントローラーを制御します。RISAF3は存在しないようです(存在する場合、データシートには記載されていません)。バックアップSRAM、OCTOSPI PSRAM、または外部DDRの適切な場所にゼロ値を書き込むことは、おそらく特権昇格をはるかに容易にするでしょう(おそらく、RISAFセキュリティ違反例外ハンドラ自体が使用するコードまたはデータのバイトを、コールドキャッシュラインにある場合、0x00に置き換えることによって)。もし誰かがこのアプローチで動作するエクスプロイトをPoCできたり、他のRISAFが脆弱であることを確認できたりしたら、ぜひ見てみたいです。
パフォーマンス計算
CPUからのPIOでは、ソフトウェアmemcpyで達成できた最大パフォーマンスは約1.77Gbpsでした(理論上のピーク4Gbpsのうち)。各strp命令は16バイトのペイロードを持つTLPを生成し、20バイトのヘッダー+CRCを加えると合計36バイトのパケットサイズになり、有用なデータを運んでいるのは44%に過ぎません。
しかし、これはFMC(64ビットAPB書き込みで137.5MHzクロックと9バスサイクルで最大977Mbps)よりもすでに大幅な向上であり、PCBルーティングの簡素化は言うまでもありません。
DMAを使用すれば、間違いなくさらに改善できます。STM32MP2のPCIe IPは、TLPあたり最大256バイト(仕様で許可されている4KBの最大値ではありません)に達し、276バイトのTLPと92.75%の効率、つまり理論上のピーク約3.71Gbpsになります。まだベンチマークでこの値に到達していませんが、おそらくいつかテストするつもりです。
これに近い値が出せれば、PCIe PIOのスループットの約2倍、FMCのスループットの約3.8倍になります。これはかなりまともです。
最終的な考え
まだ多くの作業が残っています。RISAFバグに関する開示期間中に元のテストを棚上げにして以来、Artix-7 FPGAとSTM32MP2で自作のベアボーンPCIeスタックの予備テストを行いました。
Efinix Ti375 FPGA開発ボードも購入し、そのハードPCIe IPをSTM32MP2とペアリングすることも試したいと考えています。これはおそらく今後数週間で実現し、もう少し開発が進んで、例えばFPGAベースのペリフェラルのAPBレジスタをSTM32のPIOコードから操作できることを実証できれば、独自のブログ記事になるかもしれません。
もし時間があれば、RISAFゲートを通してバックアップSRAMを0x00に操作できるか試してみるかもしれません。詳細は近日公開!
この投稿は気に入りましたか?Mastodonでコメントを残してください