Edited at

UltraZed 向け Debian GNU/Linux で AXI HPC port を使う (基礎編)


はじめに


ZynqMP の PS-PL Interface

Zynq UltraScale+ MPSoC(ZynqMP) の PS-PL Interface は Fig.1 のように12本あり、そのうち PL 側がマスターとなり PS側にアクセスするためのポートは ACP、ACE、HPC0、HPC1、HP0、HP1、HP2、HP3、LPD の 9本あります。

Fig.1 ZynqMP の PS-PL Interface

これらのポートのうち、APU のキャッシュをスヌープすることによりコヒーレンシを維持して転送することが可能なのは ACP、ACE、HPC0、HPC1 の 4本です。

この記事では ZynqMP の AXI HPC Port でキャッシュコヒーレンシ転送を行うための基本的な事柄について説明します。


Inner Share と Outer Share


ZynqMP のキャッシュインターコネクトの構造

ZynqMP は キャッシュのインターコネクトという構造からみれば、2階層になっています。

Fig.2 Inner Share と Outer Share

まず APU 内の SCU(Snoop Control Unit) によって各 CPU コアのキャッシュと ACP が接続されています。APU 内部だけでメモリ空間を共有してキャッシュコヒーレンシを維持することを Innner Share と言います。

APU の外には CCI(Cache Coherent Interconnect) があり、そこに ACE、HPC0、HPC1 が接続されています。これらのポートと APU 内部も含めてメモリ空間を共有してキャッシュコヒーレンシを維持することを Outer Share と言います。

HPC0 および HPC1 でキャッシュコヒーレンシ転送を行うためには Outer Share にする必要があります。


Inner Share と Outer Share の設定は MMU で行う

メモリ空間が Innser Share か Outer Share か(あるいは Non Share か) を設定するのは MMU で行います。

MMU とは Memory Management Unit の略で主に次の機能を持っています。


  • 仮想アドレスから物理アドレスの変換

  • メモリ保護

  • キャッシュ制御

仮想アドレスから物理アドレスへの変換は Fig.3 のように仮想アドレスの上位ビットから PTE(Page Table Entry) を得て、PTE に格納されているページの先頭物理アドレスから物理アドレスを得ます。

Fig.3 仮想アドレスから物理アドレスへの変換(Aarch32-LPAEの例)

PTE の構造は Fig.4 のようになっています。

Fig.4 PTE の構造(Aarch32-LPAEの例)

PTE の 8-9 bit 目に SH(Shareable attribute) があります。このビットに次のような値を設定することによって仮想アドレスのページ毎に Non Share か Inner Share か Outer Share かを設定できます。


  • 00: Non shareable

  • 10: Outer shareable

  • 11: Inner shareable


Linux Kernel 4.14 の対応状況

前節で MMU によって Outer Share の設定が出来ると説明しましたが、OS に Linux を使う場合、MMU の設定は Linux Kernel で行います。

ここで Xilinx 社が提供している Linux Kernel の リポジトリの xilinx-v2018.2 ブランチをみてみましょう。このブランチの Linux Kernel のバージョンは 4.14 です。このブランチの arm64 の MMU に設定する値は arch/arm64/include/asm/pgtable-hwdef.h で定義されています。


arch/arm64/include/asm/pgtable-hwdef.h

  (前略)

:
/*
* Level 3 descriptor (PTE).
*/

#define PTE_TYPE_MASK (_AT(pteval_t, 3) << 0)
#define PTE_TYPE_FAULT (_AT(pteval_t, 0) << 0)
#define PTE_TYPE_PAGE (_AT(pteval_t, 3) << 0)
#define PTE_TABLE_BIT (_AT(pteval_t, 1) << 1)
#define PTE_USER (_AT(pteval_t, 1) << 6)
/* AP[1] */
#define PTE_RDONLY (_AT(pteval_t, 1) << 7)
/* AP[2] */
#define PTE_SHARED (_AT(pteval_t, 3) << 8)
/* SH[1:0], inner shareable */
#define PTE_AF (_AT(pteval_t, 1) << 10)
/* Access Flag */
#define PTE_NG (_AT(pteval_t, 1) << 11)
/* nG */
#define PTE_DBM (_AT(pteval_t, 1) << 51)
/* Dirty Bit Management */
#define PTE_CONT (_AT(pteval_t, 1) << 52)
/* Contiguous range */
#define PTE_PXN (_AT(pteval_t, 1) << 53)
/* Privileged XN */
#define PTE_UXN (_AT(pteval_t, 1) << 54)
/* User XN */
#define PTE_HYP_XN (_AT(pteval_t, 1) << 54)
/* HYP XN */

/*
* AttrIndx[2:0] encoding (mapping attributes defined in the MAIR* registers).
*/

#define PTE_ATTRINDX(t) (_AT(pteval_t, (t)) << 2)
#define PTE_ATTRINDX_MASK (_AT(pteval_t, 7) << 2)
:
(後略)

残念なことに PTE_SHARED として定義されているのは Inner Shareable のみです。他のソースコードを探しても Outer Share を設定するようなところは見当たりませんでした。

つまり、現在の Linux Kernel は Outer Share をサポートしていません。 これは Linux では AXI HPC でキャッシュコヒーレンシ転送が出来ないことを意味します。せっかくハードウェアでは可能なキャッシュコヒーレンシ転送がソフトウェアの残念な仕様(手抜き?)で使用不可になっています。


Outer Share を Inner Share にする裏技?


Broadcasting Inner Shareable

前節で現在の Linux Kernel は Outer Share をサポートしていないので、AXI HPC によるキャッシュコヒーレンシ転送は出来ないと説明しましたが、Xilinx の wiki(Zynq UltraScale MPSoC Cache Coherency) には Linux で AXI HPC によるキャッシュコヒーレンシ転送の方法が載っています。具体的に次のような記述があります。


5.2 Broadcasting Inner Shareable


This method alters a register of MPSoC to enable inner shareable transactions to be broadcast. The brdc_inner bit of the lpd_apu register in the LPD_SLCR module must be written while the APU is in reset. The requirement to alter the register while the APU is in reset can be accomplished in multiple manners.


簡単に意訳すると


  • lpd_apu レジスタの brdc_inner ビットを 1 にセットすることにより、Inner shareable なトランザクションを Outer sharable 領域にもブロードキャストするようになります。要は Outer Share にある HPC 等も Inner Share にしてしまいます。

  • ただし、APU がリセット中にこのレジスタの値を変更する必要があります。つまり、U-Boot や Linux が起動した後ではこのレジスタを変更しても効果がありません。


boot.bin による設定

lpd_apu レジスタの設定は APU では出来ません。「UltraZed 向け Debian GNU/Linux (v2018.2版) の構築(Boot Loader編)」 で説明したように、FSBL、U-Boot、Linux は APU で動くからです。つまり FSBL の起動前、すなわちステージ0(PMU が内部ROMに格納されているブートローダーを実行している状態)でレジスタを設定する必要があります。

幸い、ステージ0でレジスタを設定できる仕掛けが用意されています。まずは以下のようなファイルを用意しておきます。


boot/regs.init

.set. 0xFF41A040 = 0x3;


boot.bin 生成時に使用する bif file に上記のファイルを init プロパティの値として追加します。


boot/boot.bif

the_ROM_image:

{
[fsbl_config] a53_x64
[init] regs.init
[bootloader] zynqmp_fsbl.elf
[pmufw_image] zynqmp_pmufw.elf
[destination_cpu=a53-0, exception_level=el-3, trustzone] bl31.elf
[destination_cpu=a53-0, exception_level=el-2] u-boot.elf
}

このようにして生成した boot.bin は、ブート時にステージ0で PMU がレジスタを設定するようになります。

この方法の詳細は次の記事で説明しています。

*「UltraZed 向け Debian GNU/Linux で AXI HPC port を使う (実践編1)」 @Qiita


参考