OpenAMP on ZYBO (Zynq) 2回目: 通信方法やメモリマップについて

- [1回目: とりあえずサンプルを動かす](https://qiita.com/take-iwiw/items/ff46792eb107d978cd0b)
- __[2回目: 通信方法やメモリマップについて](https://qiita.com/take-iwiw/items/3922ec5cae6aa35c5e81) <--- 今回の内容__
- [3回目: How to Debug Linux Application](https://qiita.com/take-iwiw/items/8b589650c15a6f705f35)
- [4回目: How to Debug Baremetal Firmware](https://qiita.com/take-iwiw/items/5f7c24bd0475274ded23)


# 本記事について
前回、Linux + ベアメタル(or FreeRTOS)のマルチ環境を、OpenAMPを使用してZYBOで実現させました。

- CPU0: Linux
- CPU1: ベアメタル (or FreeRTOS)

前回はサンプルコードをそのまま使用しました。次回以降、Linux側アプリとベアメタルファームウェアの開発に取り掛かる予定です。その前に、通信方法やメモリ周りに関して、現時点で分かったことをまとめておこうと思います。毎回注意書きを書くのが面倒なので、本記事ではベアメタル(BM)とだけ記載しますが、FreeRTOSでも同じです。

# 環境
- 開発用PC: Windows 10 64-bit 
	- Vivado 2017.4 WebPACKライセンス
	- Xilinx SDK 2017.4
- 開発用PC (Linux): Ubuntu 16.04 本家 (日本語版じゃない) (on VirtualBox 5.2.4)
	- PetaLinux 2017.4
- ターゲットボード: ZYBO (Z7-20) 

# Linux側
## ベアメタルファームウェアの用意
ベアメタル(BM)ファームウェアは、`/lib/firmware/XXX.elf`にインストールするのが一般的なようです。インストール方法は1回目の記事をご参照ください。

## OpenAMPの始め方
Linux起動後は、LinuxがCPU0とCPU1の両方を使用しているSMP動作になります。AMPモードに入るには、通信用モジュール(`rpmsg_user_dev_driver`)のロード、BMファームウェアのファイル指定、AMPの開始指示、をする必要があります。具体的には、下記コマンドになります。

```sh:OpenAMP開始コマンド
modprobe rpmsg_user_dev_driver
echo XXX.elf > /sys/class/remoteproc/remoteproc0/firmware
echo start > /sys/class/remoteproc/remoteproc0/state
```

これで、指定されたBMファームウェアがロードされ、main関数から処理が実行されます。その後、必要に応じて、BMと協調動作するLinuxアプリケーションを走らせます。待ち合わせ処理などがないと、エラーが発生するため、BM実行後にLinuxアプリケーションを起動した方がいいです。

start後は、Linux側プロセスはCPU0でのみ実行され、CPU1は管轄外になります。

## OpenAMPの終わり方
下記コマンドで、OpenAMP環境を終了します。モジュールのアンロードはなくてもいいです。

```sh:OpenAMP終了コマンド
echo stop > /sys/class/remoteproc/remoteproc0/state
modprobe -r rpmsg_user_dev_driver
```

stop後は、Linuxは再度CPU0, CPU1のどちらも使用可能になります。

## BMとの通信方法
BMとの通信方法はいくつかあるようですが、ここではrpmsg_user_dev_driverモジュールを使用した方法を記載します。
やり方は非常に簡単で、`/dev/rpmsg0`へのwrite/readで実現できます。
例えば、"ABC"という文字列を送信して、その後受信するというシンプルなコードは以下になります。

```c:Linuxユーザアプリ(rpmsg)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
	int fd = open("/dev/rpmsg0", O_RDWR | O_NONBLOCK);
	if (fd < 0) {
		perror("open");
		return -1;
	}

	char bufSend[] = "ABC";
	char bufRecv[10];
	if (write(fd, bufSend, sizeof(bufSend)) < 0) perror("write");
	printf("send: %s\n", bufSend);
	if (read(fd, bufRecv, sizeof(bufRecv)) < 0) perror("read");
	printf("recv: %s\n", bufRecv);

	close(fd);
	return 0;
}
```

ちなみに、前回動作確認に使用したecho_testのソースはこちら
https://github.com/Xilinx/meta-openamp/blob/rel-v2017.4/recipes-openamp/rpmsg-examples/rpmsg-echo-test/echo_test.c

### 注意点
- `/dev/rpmsg0`は、BMファームウェアを開始して、BM側で初期化処理が完了してから、見えるようになります。
- write直後に直ぐにreadすると、デバイスがunavailable状態になってreadできなかった。
- 一度closeすると、`/dev/rpmsg0`を再open出来なくなった。この場合は、再度OpenAMPをstop/startする必要がある。恐らく、同様の理由で、シェルからechoやcatでの操作は出来なかった。


# ベアメタル(BM)側
Linux側の処理は非常に簡単でした。複雑なことは全てドライバが吸収してくれています。BM側ではこれらをコードで実装する必要があります。
基本的には、「OpenAMP echo-test」のテンプレートを流用するのが間違いないです。そのうえで、重要な点だけを記載します。

## リンカスクリプトの設定
![02.jpg](https://qiita-image-store.s3.amazonaws.com/0/214268/29d6b8f2-43dd-61ed-992b-9f485a943dfb.jpeg)

BMファームウェア(コード+ヒープ+スタック)の展開先を指定します。ここでは、0x3E00_0000 ~ 0x3E40_0000を設定しています。メモリ周りの詳細は、後述します。

## 通信用リソースの設定
前回、軽く説明しましたが、Linux(CPU0)とベアメタル(CPU1)の通信は、1. 共有領域にデータを書いて、2. 割り込みをかける、ことによって実現されます。

### 割り込み番号 (platform_info.h)
割り込み番号を定義します。デフォルトでは14と15が使用されます。この番号と、Linuxのデバイスツリーで指定する番号は同じである必要があります。変更する場合は、どちらも変更する必要があります。基本的にはデフォルトのままでいいはずです。

```c
#define VRING0_IPI_INTR_VECT              15
#define VRING1_IPI_INTR_VECT              14
```

### 共有メモリ (rsc_table.c)
コードから察するに、共有領域をリングバッファのように使用しているようです。各バッファの先頭アドレスを定義しています。この領域は、LinuxとBMのどちらからも使われない空き領域である必要があります。BMのロード先やサイズが変わる際には、変更する必要があります。メモリ周りの詳細は、後述します。

```c
#define RING_TX                     0x3e600000
#define RING_RX                     0x3e604000
struct remote_resource_table __resource resources = {
	- 省略 -
	{RSC_RPROC_MEM, 0x3e600000, 0x3e600000, 0x100000, 0},

```

## 初期化方法
よく分からないので、サンプルコードをパクる

## 通信方法
- 受信時には、初期化時に登録したコールバック関数(`rpmsg_read_cb`)が呼ばれる
- 送信は、`rpmsg_send`関数を使う。

「送信のみ」ということが出来なかった。サンプルと同様に、受信→送信、ならできた。rpmsg_channelの指定方法が良くないのかもしれない。なぜだかは不明。仕様かどうかも不明。


# メモリマップについて
## 物理的なメモリマップ
![01.jpg](https://qiita-image-store.s3.amazonaws.com/0/214268/ba00bd16-d67b-b471-f74f-255045b358a3.jpeg)

ZYBOの物理的なメモリマップは上記のようになります。デバイス周りのアドレスは設定によって変わる可能性がありますが、大体こんな感じです。重要な点は、0x0000_0000 - 0x4000_0000の1GByteがDDRに割り当てられているという点です。シングル環境だと、この領域全体がLinuxやベアメタルファームウェアに割り当てられます。

## OpenAMPでのメモリマップ
![03.jpg](https://qiita-image-store.s3.amazonaws.com/0/214268/5128b742-1105-6c5b-6dc6-5cd0470893a2.jpeg)

前回作成したOpenAMP環境のメモリマップは上記のようになります。絶対にこのようにしないといけないというのではなく、一例としてご覧ください。

- 0x3E00_0000-0x3EFF_FFFF以外のDDR領域は全て、Linuxに割り当てられています。
	- これは、Linuxのデバイスツリー設定で、0x3E00_0000-0x3EFF_FFFFをreservedにしたためです。
	- また、この領域内の0x3E00_0000-0x3E3F_FFFFを、BMファームウェア用に割り当てています。これも、デバイスツリーで設定した通りです。
		- BMファームウェアのリンカスクリプトでも同じ領域になるようにする必要があります。
- 結果として、0x3E40_0000-0x3EFF_FFFFは空き領域となります。そのため、この領域を通信用の共有メモリとして使用しています。前述のコードでは、0x3e600000と0x3e604000

アドレスやサイズは変わってもいいと思うのですが、このように矛盾なく各領域が設定される必要があります。設定変更する場合は、この記事で示したBM側のリンカスクリプト、ソースコード、そして、Linux側のデバイスツリー設定を、整合性が取れるように編集してください。

### 物理メモリマップの確認方法
ちなみに、Linuxで物理的なメモリマップを確認するには、`/proc/iomem`を確認します。

```sh
root@PjOpenAmp:~# cat /proc/iomem
00000000-3dffffff : System RAM
  00008000-008fffff : Kernel code
  00a00000-00a6b697 : Kernel data
3e000000-3e3fffff : 3e000000.ddr
3f000000-3fffffff : System RAM
e0001000-e0001fff : xuartps
e000a000-e000afff : /amba/gpio@e000a000
e000b000-e000bfff : /amba/ethernet@e000b000
e000d000-e000dfff : /amba/spi@e000d000
e0100000-e0100fff : /amba/sdhci@e0100000
f8003000-f8003fff : /amba/dmac@f8003000
  f8003000-f8003fff : /amba/dmac@f8003000
f8005000-f8005fff : /amba/watchdog@f8005000
f8007000-f80070ff : /amba/devcfg@f8007000
f8007100-f800711f : /amba/adc@f8007100
f800c000-f800cfff : /amba/ocmc@f800c000
fffc0000-ffffffff : f800c000.ocmc
```

### Linux デバイスツリー設定 (再掲)
前回も載せましたが、本記事の内容と密接にかかわるので、デバイスツリー設定を再度貼り付けておきます。

```txt:project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi
/include/ "system-conf.dtsi"
/ {
	reserved-memory {
		#address-cells = <1>;
		#size-cells = <1>;
		ranges;
		rproc_0_reserved: rproc@3e000000 {
			no-map;
			reg = <0x3e000000 0x01000000>;
		};
	};
	amba {
		elf_ddr_0: ddr@0 {
			compatible = "mmio-sram";
			reg = <0x3e000000 0x400000>;
		};
	};
	remoteproc0: remoteproc@0 {
		compatible = "xlnx,zynq_remoteproc";
		firmware = "firmware";
		vring0 = <15>;
		vring1 = <14>;
		srams = <&elf_ddr_0>;
	};
};
```

# よく分かっていない点
通信用の共有メモリのアドレスは、前述の通りBM側のソースコードで設定しています(RING_TX, RING_RX)。が、これをどうやってLinux側に教えているのかが不明。OpenAMPがよしなにやってくれているのだと思うが。。。OCMとか使ってるのかな。