はじめに
以前に「FreeRTOS を Raspberry Pi 4B へ移植する」にて、FreeRTOSをRaspberry Pi 4B上に移植してUART出力させるようにしました。
今回はさらに手を加え、コア間通信として組み込み業界では割と有名なOpenAMPを移植しました。これにより、RPMSGプロトコルを使ってFreeRTOSとLinux間での通信が出来るようになりました。
リポジトリ
ここにあります。ビルドや使い方はREADME.md
を参照してください。
OpenAMPとは
詳しいことは 本家の資料 を参照するとよいかもしれません。ざっくり言ってしまうと、
「CPUコア間でのデータ共有用に共有メモリ空間を仕立てて、割込み使ってコア間でデータ通信させよう」
というものです。
大まかな流れとしては
- データ送信側のCPUコアが送信したいデータを共有メモリに書き込み、さらにデータ受信側CPU側コアへ割込みを投げる
- データ受信側のCPUコアは、割込み受信後に共有メモリからデータを読み出す
というとても単純な方法です。通信のためのデータフォーマットやデータ送受信のプロトコルをOpenAMPにて定義しています。
上記の本家資料にも出てくるのですが、この発想は仮想マシンでよく見かけるvirtioの一実装と同じ仕組みであり、OpenAMPではvirtioを内部で使っています。
構成
Rasbperry Pi 4BのCPUコア#0-2にてLinuxを動作させ、CPUコア#3にてFreeRTOSを動作させるようにしています。Linux側のプログラムはシングルスレッド/シングルプロセスで動作します。
通信内容としては、FreeRTOS側を起点としたping-pongプログラムです。とても単純です。
- FreeRTOS側からメッセージをLinux側へ投げる (rpmsg-sample-ping)
- Linux側で受信したメッセージをFreeRTOS側へ投げ返す (rpmsg-sample-echo)
ちょっと話が深くなりますが、通信をしようとするとCPUコア間での割込み生成と受信する機能を新たに実装する必要があります。Raspberry Pi 4Bで採用されているARM社のCortex-A72では標準でSGI(Software Generated Interrupt)を使うことでCPUコア間の割込みを生成することが出来ますが、今回のlibmetalにてLinux側の標準環境とされる "Device tree + UIO" の組み合わせだとSGIを受信することが出来ません。
したがって今回の移植ではRaspberry Pi 4Bに搭載されている別の割込み機能 "ARM Mailbox" を使用することにしました。こちらはSPI(Shared Peripheral Interrupt)の扱いになるので、UIOでも問題なく扱えます。ちなみに "ARM Mailbox" は名前に "ARM" と冠しているもののARM社のIPではなく、どうやらRaspberry Pi 4BのプロセッサであるBroadcom社のBCM2711固有の機能っぽいです。
(UIOを使うとユーザ空間での割込み受信となりますが、Linuxではカーネル空間でしかSGIを扱えないようです)
移植について
大まかなところ
OpenAMPアプリケーションの移植においては、まず
- アプリ部分(RPMSGを呼び出すところ)の作成
- ハード依存部分(ハード初期化とか割込み操作を実施するところ)の移植
の2つを考えないといけません。
前者のアプリ部分はハードに依存しないため、どのようなハードでも同じコードを書くことが出来ます。ここ にサンプルが幾つかあるので、そのまま持ってくることも出来ます。今回の移植で使用したサンプルは、rpmsg_sample_echo です。
後者のハード依存部分は、libmetalレイヤとopen-ampレイヤにおいて移植が必要となります。libmetal レイヤでは割り込みコントローラに関わるコードやキャッシュ操作のコードを定義する必要があります。また、open-amp レイヤでは割込みコントローラなどハードウェアの初期化/終了処理、リソース定義情報の共有処理、割込み発行処理などを定義する必要があります。
ワタシが実装したコードは、ここ(libmetal) と ここ(open-amp) にあります。
ちなみに今回の移植はlibmetalおよびopen-ampのv2020.04.0
をベースにしました。
libmetal に関わる移植
libmetalに関わる移植はボリュームが多くないので難しいことはありません。aarch64用のキャッシュ制御用コードをどうしようかなぁとは思いましたが、u-bootのコードを流用させてもらいました。
open-amp に関わる移植
一方でこちらはやることが沢山あります。どのハードウェアへの移植でも同じですが、基本的には
-
platform_***
で定義される関数群の作成 -
remoteproc_***
関数から呼ばれる関数群raspi4_a72_proc_***
の作成
が必要です。remoteproc_***
関数は既に open-amp において定義された関数です。これらの関数がどのような感じで呼ばれていくかというと、platform_create_proc関数の例がとてもわかりやすいです。platform_create_proc
関数の中で、remoteproc_init
やremoteproc_mmap
関数を呼んでいます。
static struct remoteproc *
platform_create_proc(int proc_index, int rsc_index)
{
(void) proc_index;
(void) rsc_index;
void *rsc_table, *buf;
int ret;
metal_phys_addr_t pa;
/* Initialize remoteproc instance */
if (!remoteproc_init(&rproc_inst, &raspi4_a72_proc_ops, &rproc_priv))
return NULL;
/* Mmap resource table */
pa = RSC_MEM_PA;
LPRINTF("Calling mmap resource table.\r\n");
rsc_table = remoteproc_mmap(&rproc_inst, &pa, NULL, RSC_MEM_SIZE,
0, &rproc_inst.rsc_io);
if (!rsc_table) {
LPERROR("ERROR: Failed to mmap resource table.\r\n");
goto err;
}
LPRINTF("Successfully mmap resource table.\r\n");
/* Mmap shared memory */
pa = SHARED_BUF_PA;
LPRINTF("Calling mmap shared memory.\r\n");
buf = remoteproc_mmap(&rproc_inst, &pa, NULL, SHARED_BUF_SIZE,
0, NULL);
if (!buf) {
LPERROR("ERROR: Failed to mmap shared memory.\r\n");
goto err;
}
LPRINTF("Successfully mmap the buffer region.\r\n");
/* parse resource table to remoteproc */
ret = remoteproc_set_rsc_table(&rproc_inst, rsc_table, RSC_MEM_SIZE);
if (ret) {
LPERROR("Failed to intialize remoteproc\r\n");
goto err;
}
LPRINTF("Initialize remoteproc successfully.\r\n");
return &rproc_inst;
err:
remoteproc_remove(&rproc_inst);
return NULL;
}
さて、remoteproc_init
やremoteproc_mmap
の関数がどのようにraspi4_a72_proc_***
関数群に繋がっていくかというと...、open-amp にて定義されている関数を経由します。例えば、remoteproc_init
関数(コード)では下記のように定義されています。よくよく見ると、ops->init
関数を呼び出しています。
struct remoteproc *remoteproc_init(struct remoteproc *rproc,
struct remoteproc_ops *ops, void *priv)
{
if (rproc) {
memset(rproc, 0, sizeof(*rproc));
rproc->state = RPROC_OFFLINE;
metal_mutex_init(&rproc->lock);
metal_list_init(&rproc->mems);
metal_list_init(&rproc->vdevs);
}
rproc = ops->init(rproc, ops, priv);
return rproc;
}
実は、このops->init
関数の実体はraspi4_a72_proc_init
関数(コード)です。raspi4_a72_proc_init
関数は変数raspi4_a72_proc_ops
のメンバ変数init
として登録されているので、これが呼ばれることになります。
static struct remoteproc *
raspi4_a72_proc_init(struct remoteproc *rproc,
struct remoteproc_ops *ops, void *arg)
{
...
...
}
...
...
/* processor operations from between a72 cores. It defines
* notification operation and remote processor managementi operations. */
struct remoteproc_ops raspi4_a72_proc_ops = {
.init = raspi4_a72_proc_init,
.remove = raspi4_a72_proc_remove,
.mmap = raspi4_a72_proc_mmap,
.notify = raspi4_a72_proc_notify,
.start = NULL,
.stop = NULL,
.shutdown = NULL,
};
上記と同じように、他のraspi4_a72_proc_***
関数も幾つか定義しています。remoteproc_***
関数との関係は下記表のようになり、とても簡単なマッピングです。
remoteproc_*** | raspi4_a72_proc_*** | 役割 |
---|---|---|
remoteproc_init | raspi4_a72_proc_init | 初期化処理 |
remoteproc_remove | raspi4_a72_proc_remove | 終了処理 |
remoteproc_mmap | raspi4_a72_proc_mmap | 物理メモリマッピング |
remoteproc_notify | raspi4_a72_proc_notify | 割込みを投げる |
uioによるユーザ空間からの特定メモリ領域へのアクセス
通常Linuxのユーザアプリケーションはユーザ空間で動作するため、特定の物理メモリ領域やレジスタ領域を明示的にアクセスことができません。これを可能にする1つの方法が、uio (userspace I/O) です。
uioを使うと、ユーザ空間から特定の物理メモリ領域やレジスタ領域へアクセスできる出来るだけでなく、ユーザ空間で割込みを受信することが出来るようになります。今回の移植に使ったlibmetalでは、Linux側はデフォルトでuioによる割込みを使用することが出来ます。
ユーザ空間からアクセスするメモリ領域はdevice-tree上で定義される必要があります。今回のdevice-treeでは下記の内容を定義しています。
shm@20600000 {
compatible = "generic-uio";
status = "okay";
reg = <0x0 0x20600000 0x200000>;
};
armlocal_uio@ff800000 {
compatible = "generic-uio";
status = "okay";
reg = <0x0 0xff800000 0x1000>;
interrupt-parent = <0x1>;
interrupts = <0x0 0x0 0x4>;
};
gic_uio@40041000 {
compatible = "generic-uio";
status = "okay";
reg = <0x0 0x40041000 0x7000>;
};
上から順に、
-
shm@20600000
はresource tableを含むOpenAMP用の共有メモリ領域 -
armlocal_uio@ff800000
はCPUコア間割込みデバイスである"ARM Mailbox"のレジスタ領域 -
gic_uio@40041000
は割込みコントローラGIC-400のレジスタ領域
となります。特に割込みを受信する必要があるarmlocal_uio@ff800000
の定義には、割込み関係の定義を追加しています。
resource tableの共有
OpenAMPの移植においてもう一つ大きな事項は、resource table(コード)です。resource tableが何者かと言うと、
「共有メモリアドレスとかvirtioのメタデータを保持したデータ領域」
です。テーブルフォーマットは自由に決められますが、今回の移植ではZynq MPSoC用のやつを流用しています。
resource table領域をFreeRTOSとLinuxで共有させないといけないのですが、FreeRTOS側でデータを保持しつつLinux側からマッピングさせる必要があります。FreeRTOSおよびLinuxのどちらもremoteproc_mmap
関数を呼び出すので、raspi4_a72_proc_mmap
関数が実行されます。
さて、raspi4_a72_proc_mmap
関数の中身はというと...、よくよく中身をじっくり見るとFreeRTOSおよびLinuxどちらの場合もmetal_io_phys_to_virt
関数の戻り値がそのままraspi4_a72_proc_mmap
関数の戻り値になります。
static void *
raspi4_a72_proc_mmap(struct remoteproc *rproc, metal_phys_addr_t *pa,
metal_phys_addr_t *da, size_t size,
unsigned int attribute, struct metal_io_region **io)
{
...
...
...
#if defined(__linux__)
prproc = rproc->priv;
remoteproc_init_mem(mem, NULL, lpa, lda, size, prproc->shm_io);
va = metal_io_phys_to_virt(mem->io, lpa);
if (va) {
if (io)
*io = mem->io;
remoteproc_add_mem(rproc, mem);
}
return va;
#else /* FreeRTOS */
tmpio = metal_allocate_memory(sizeof(*tmpio));
if (!tmpio) {
metal_free_memory(mem);
return NULL;
}
remoteproc_init_mem(mem, NULL, lpa, lda, size, tmpio);
/* va is the same as pa in this platform */
metal_io_init(tmpio, (void *)lpa, &mem->pa, size,
sizeof(metal_phys_addr_t)<<3, 0x0, NULL);
remoteproc_add_mem(rproc, mem);
if (io)
*io = tmpio;
return metal_io_phys_to_virt(tmpio, mem->pa);
#endif
}
metal_io_phys_to_virt
関数は、第二引数で指定される物理メモリアドレスを第一引数で指定されたioデバイス(=メモリマップされたioデバイス)から探し出し、同物理メモリアドレスにアクセス可能な仮想メモリアドレスを返してくれます。
Linuxの場合は、上記で説明したuioの力を借りてresource tableにマップされた物理メモリ領域の仮想アドレスが戻り値として返ってきます。一方FreeRTOSの場合は「物理メモリアドレス値と仮想メモリアドレス値が同じ」となっているため、第二引数であるmem->pa
の値がそのまま戻り値として返ってきます。このときFreeRTOS側はそのmem->pa
の領域がresource tableの定義領域となるようになっています。
FreeRTOSとLinuxのresource tableアドレスを一致させるのは簡単です。Linux側ではdevice-treeの定義で 0x20600000
からの物理メモリ領域をアクセスできるようにしています。
shm@20600000 {
compatible = "generic-uio";
status = "okay";
reg = <0x0 0x20600000 0x200000>;
};
FreeRTOS側では.resource_table
セクションに resource table の定義を配置し、リンカスクリプトにて同セクションの配置アドレスを 0x20600000
からとしています。
/* Place resource table in special ELF section */
/* Redefine __section for section name with token */
#define __section_t(S) __attribute__((__section__(#S)))
#define __resource __section_t(.resource_table)
...
...
struct remote_resource_table __resource resources = {
...
};
...
OAMP_SHM = 0x20600000;
...
SECTIONS
{
...
. = OAMP_SHM ; /* Shared memory for OpenAMP */
.oamp :
{
*(.resource_table)
*(.oamp)
}
...
}
おわりに
今回の移植は何だかんだで盛りだくさん&かなり複雑なモノとなってしまいました。反省...。
とはいえRaspberry Pi 4Bでやるべきことは出来たので、ヨシとします。