Spresenseのサブコアからメインコアにアドレス通信すると内部エラーが出る

Spresenseのコア間通信で，```std::vector```型のアドレスをやりとりしようとした際に，メイン→サブ方向だと上手く行くのに，サブ→メイン方向にすると内部エラーが発生しました．

通信方向が逆だと動くことから，[ポインタ周りの間違い](https://qiita.com/bending_gold/items/351979d106bb9568451e)ではないようでした．

色々確認したところ，アドレス空間の違いのため，std::vectorのようなクラスのアドレスをサブコアからメインコアに送って処理するのは難しいということがわかりました．

## 症状
### サブコア→メインコア（動かない）
```std::vector```型をサブコア側で定義し，メインコアで呼び出すコードです．
ソースコードは以下の通りです．
```cpp
#include <MP.h>
#include <vector>

#if (SUBCORE == 1)
std::vector<double> a = {1.5, 2.0};
void setup() {
  MP.begin();
  MP.RecvTimeout(MP_RECV_BLOCKING);
  delay(300);
  MP.Send(0, &a);
}
void loop() { }
#endif

#ifndef SUBCORE // maincore
std::vector <double>* b;
void setup() {
  MP.begin(1);
  MP.RecvTimeout(MP_RECV_BLOCKING);
  uint8_t msgid_ = 255;
  MP.Recv(&msgid_, &b, 1);
}
void loop() { 
  MPLog("size: %d\n", b->size());
  MPLog("contents: %lf, %lf", b->at(0), b->at(1));
  delay(1000);
  }
#endif
```
サブコア1とメインコアに書き込んで，シリアルモニタで出力を見ると ```arm_hardfault```のエラーが出力されていることがわかります．
```
[Main] size: 2
arm_hardfault: PANIC!!! Hard fault: 40000000
up_assert: Assertion failed at file:armv7-m/arm_hardfault.c line: 135 task: init
up_registerdump: R0: 0d0ceca0 00000002 00010980 00010970 000000e0 0d022000 0d000000 0d021bb4
up_registerdump: R8: 00000000 00000000 00000000 00000000 00000000 0d031858 0d000299 0d0002b0
up_registerdump: xPSR: 21000000 BASEPRI: 00000080 CONTROL: 00000000
up_registerdump: EXC_RETURN: ffffffe9
up_dumpstate: sp:     0d022cb8
up_dumpstate: IRQ stack:
up_dumpstate:   base: 0d022518
up_dumpstate:   size: 00000800
up_dumpstate:   used: 00000198

（以下略）
```
つまり，配列のサイズは正しく取れているにも関わらず，値を見ようとするとエラーが生じました．

### メインコア→サブコア（動く）
一方で，同等のプログラムをメインコアからサブコア方向に書き換えると問題なく動きます．
まずソースコードは
```cpp
#include <MP.h>
#include <vector>

#ifndef SUBCORE // maincore
std::vector<double> a = {1.5, 2.0};
void setup() {
  MP.begin(1);
  MP.RecvTimeout(MP_RECV_BLOCKING);
  delay(300);
  MP.Send(0, &a, 1);
}
void loop() { }
#endif

#if (SUBCORE == 1)
std::vector <double>* b;
void setup() {
  MP.begin();
  MP.RecvTimeout(MP_RECV_BLOCKING);
  uint8_t msgid_ = 255;
  MP.Recv(&msgid_, &b);
}
void loop() { 
  MPLog("size: %d\n", b->size());
  MPLog("contents: %lf, %lf\n", b->at(0), b->at(1));
  delay(1000);
  }
#endif
```
シリアルモニタの出力結果は以下です．
```
[Sub1] size: 2
[Sub1] contents: 1.500000, 2.000000
（以下略）
```
サイズも値も問題なく読み取れていることが分かります．

## 何が起こっているのか
原因はメインコアとサブコアのメモリ空間の違いにあります．

公式マニュアルに以下の図があります．
>![diag-64935e6555171d8a58adc590aadf0c3c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3803253/9850f2c5-bd64-58f3-f74d-f3cabd1fcdb0.png)
> [Spresense Arduino 開発ガイド](https://developer.sony.com/spresense/development-guides/arduino_developer_guide_ja.html#_mp_memory) 図12 アプリケーションSRAMより

ここから分かるように，メインコアは物理アドレスを，サブコアは他コアの分オフセットされた仮想アドレスを扱っていることが分かります．

メインコアから送る場合，物理アドレスがそのままサブコアに伝達されます．
一方でサブコアからメインコアに送る場合，ドキュメントに以下の記載があります．
> SubCore からアドレスを送信する場合に、仮想アドレスから物理アドレスへの変換が必要ですが、msgaddr については、 Send() 関数の内部でアドレス変換を行っています。

さらに次のように続きます．

> もし、msgaddr ポインタの指し示す先にもアドレス情報があり、 コアを跨いでそのアドレスを参照する場合は、ユーザー側でアドレス変換を行ってください。

つまるところ，動かなかったコードでは以下のようなことが起こっていたと考えられます．  
- ```a```のアドレスそのものは```Send```関数によって仮想アドレスから物理アドレスに変換されて正しく伝わっている．
- ところが```std::vector```の内部処理でさらにアドレス参照を使っていて，そちらが仮想アドレスのままになっており，アクセスの際にメモリ違反になった．

恐らく，```vector```内部で要素数は値として扱われていたために，```size()```関数は正常に動き，```at()```はアドレスを使って処理されているためにエラーになったと考えることができます．

実際，サブコア→メインコア方向で要素アクセスした際に現れた```arm_hardfault```エラーは不正なメモリ領域にアクセスしようとした際によく起こるようです．

## 対応策
一応MPライブラリには仮想アドレスを物理アドレスに変換する```Virt2Phys```関数が[ドキュメント](https://developer.sony.com/spresense/spresense-api-references-arduino/classMPClass.html)に記載されていますが，内部で行われているアドレス参照の処理に手を加えるのは厳しそうです．

従って，以下のような方法がとられるかと思います．
- おとなしくメインコアで変数宣言してサブコアにアドレスを送る
- 配列変数でなんとかする（でも```size()```など便利関数が使えない）
- 公式サンプルのMessageHelloのように送受信のパケット型を自作してサイズも送る


## 終りに
メモリ管理の都合，メインコアとサブコアではアドレス周りの挙動が異なることがわかりました．
ドキュメント自体は読んでメモリ空間が違うことは知識として知っていても，何が起こるのかわかって大変勉強になりました．

実際は，内部エラーが出たあとに，逆向きだと正常に動くところまで原因を絞るのに手間取りましたが．