KOZOSをnucleo l4r5ziにポーティングしてみた

# 今回のやりたいこと
タイトルにもありますように、KOZOSをnucleo l4r5ziにポーティングすることになります。

# 1. 環境
* 使用ボード：nucleo l4r5zi
    * Cortex-M4搭載、2MBのFlash、640KのSRAM、USB OTG 
* 開発環境：Atollic TrueSTUDIO® for STM32, Built on Eclipse Neon.1a.(Version: 9.3.0 )
* 対象OS：KOZOS※

※下記書籍で紹介されているOSになります。ソースコードも展開されています。
[12ステップで作る 組込みOS自作入門](http://kozos.jp/books/makeos/)

# 2. 前提
Atollic TrueSTUDIOで、NUCLEO-L4R5ZI向けのプロジェクトを生成します。これに対して、修正を行っていきます。

# 3. 作業
基本的に、コアに依存する部分を修正する作業になります。
上記書籍では、H8/3069Fというマイコンボードを対象とされていますが、今回はnucleo l4r5ziが対象となります。nucleo l4r5ziには、Cortex-M4が載っており、このマイコン上で動作するように修正していきます。

下記に、作業内容を記載します。

1. システムプールの設定
2. ディスパッチャーの作成
3. OSのサービスコールの作成

それぞれについて説明していきます。

### 3.1 システムプールの設定

前提に記載している通り、用意されているプロジェクトに対して修正を行っていきます。KOZOSでは、メモリの動的確保をするサービスコールを提供されているため、その領域を用意する必要があります。この領域をここではシステムプールと呼んでいます。
システムプールの設定を行うためには、リンカスクリプトとソースコードを修正する必要があります。それぞれについて説明していきます。

#### 3.1.1 リンカスクリプトの修正
今回使用しているボードのアドレスマップとプロジェクトで用意されているリンカスクリプトを下記に示します。
* アドレスマップ
[ハードウェアマニュアル](https://www.stmcu.jp/design/document/reference_manual/57884/)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2782242/2f256ace-9007-4a62-3d4d-b7b10415c88e.png)

* リンカスクリプト
```c:リンカスクリプト
MEMORY
{
  FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 2048K
  RAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 192K
  RAM2 (rw)       : ORIGIN = 0x10000000, LENGTH = 64K
  RAM3 (rw)       : ORIGIN = 0x20040000, LENGTH = 384K
  MEMORY_B1 (rx)  : ORIGIN = 0x60000000, LENGTH = 0K
}
```

リンカスクリプトを確認したところ、RAMがdata/bssセクションとして使用されています。
RAM3を、システムプールとして使うことにします。下記コードをリンカスクリプトに追記します。

```c:追記内容
  ._system_pool :
  {
    . = ALIGN(4);
    _task_stack_start = .;
    . = . + 0x30000;
    _task_stack_end = .;
    _system_pool_start = .;
  } >RAM3
```

RAM3の前半(0x20040000 ~ 0x2006FFFF)をタスクのスタック領域、後半(0x20070000 ~ 0x2009FFFF)をシステムプールとして定義しています。ソースコード側で、&_system_pool_start と参照することで、システムプールの先頭アドレスを取得することができます。

#### 3.1.2 ソースコードの修正
KOZOSでは、下記の通りにシステムプールの初期化を行っています。
```c:システムプールの初期化
static int kzmem_init_pool(kzmem_pool *p)
{
  int i;
  kzmem_block *mp;
  kzmem_block **mpp;
  extern char freearea; /* リンカ・スクリプトで定義される空き領域 */
  static char *area = &freearea;

  mp = (kzmem_block *)area;

  /* 個々の領域をすべて解放済みリンクリストに繋ぐ */
  mpp = &p->free;
  for (i = 0; i < p->num; i++) {
    *mpp = mp;
    memset(mp, 0, sizeof(*mp));
    mp->size = p->size;
    mpp = &(mp->next);
    mp = (kzmem_block *)((char *)mp + p->size);
    area += p->size;
  }

  return 0;
}
```
freeareaがリンカスクリプトに設定した_system_pool_startに相当するため、下記のように修正します。
```c:修正内容
  extern char _system_pool_start/* リンカ・スクリプトで定義される空き領域 */
  static char *area = _system_pool_start
```

### 3.2. ディスパッチャーの作成
ここではいろいろすることがあります。
ディスパッチャーを作るには、CortexM4に例外が発生したときに何が自動的にスタックに退避され、何をソフトでスタックに退避するのかを調べる必要があります。これについては下記資料に記載されています。
https://developer.arm.com/documentation/ddi0403/latest
DDI0403E_e_armv7m_arm.pdf

CortexM4はarmv7mというアーキテクチャでCortexM3と同じアーキテクチャになります。

上記資料の536ページを確認したところ、割込み時は、xPSR、ReturnAddress、LR(R14)、R12、R3、R2、R1、R0の順番で自動的に退避されることがわかりました。
つまり、これらは自動的にスタックに退避されるため、残りのR4～R11をソフトで退避する必要があるということがわかります。

これらのことを踏まえて修正点を下記に記載します。
#### 3.2.1 ソースコードの修正
###### 3.2.1.1 kozos.c
* thread_run():タスク生成時にコールされる関数

本関数では、引数として渡されたスタックのサイズ、優先度、タスクとして動作させる関数のポインタといった情報から、タスクを生成します。  
スタックポインタをリンカスクリプトで設定した_task_stack_startから設定していきます。もともとuserstackが使用されているため、下記の通り、_task_stack_startに修正する。この修正で、「1.1 リンカスクリプトの修正」で設定した、_task_stack_startからスタックを確保できるようになります。  
```c:修正前  
  extern char userstack; /* リンカ・スクリプトで定義されるスタック領域 */  
  static char *thread_stack = &userstack;
```
```c:修正後  
  extern char _task_stack_start; /* リンカ・スクリプトで定義されるスタック領域 */
  static char *thread_stack = &_task_stack_start;
```
続いて、上記資料に従い、取得したスタックポインタに対して下記値を設定します。（spはスタックポインタを表しています）

```c:スタックポインタへの設定
		*(--sp) = (uint32_t)thread_end;
		*(--sp) = 0x1000000;             /* APSR */ 
		*(--sp) = (uint32_t)thread_init; /* pc   */
		*(--sp) = 0;                     /* lr(特に設定する必要はなし) */
		*(--sp) = 0;                     /* r12  */
		*(--sp) = 0;                     /* r3   */
		*(--sp) = 0;                     /* r2   */
		*(--sp) = 0;                     /* r1   */
		*(--sp) = (uint32_t)thp;         /* r0(タスクの引数) */
		*(--sp) = (uint32_t)0xFFFFFFF9;　/* lr   3. */
		*(--sp) = 0;                     /* r11  */
		*(--sp) = 0;                     /* r10  */
		*(--sp) = 0;                     /* r9   */
		*(--sp) = 0;                     /* r8   */
		*(--sp) = 0;                     /* r7   */
		*(--sp) = 0;                     /* r6   */
		*(--sp) = 0;                     /* r5   */
		*(--sp) = 0;                     /* r4   */
```
*
    * APSRの設定値について
APSRは、アプリケーションプログラムステータスレジスタの略で、プログラムのステータスを表しています。0x1000000を設定していて、24bit目が1となっています。24bitはthumb状態かどうかを表しており、今回はthumbを使用しているため、1としています。その他ビットは、特に1を立てる必要はないため、0としています。

    * pc、lrの設定値について
割込みが発生した際は、まず最初に、ソフトでr4～r11、lrをスタックへ退避します。（「2.1.2 intr.s」のvec_common参照）
そして最後に、ディスパッチャーで、r4～r11、pcをスタックから復帰させます。（「2.1.3 dispatch.s」の_dispatch_int参照）
lrをpcとしてスタックから復帰することで割込み復帰を行うことができます。これを実行することで、自動的に退避されたxPSR、ReturnAddress、LR(R14)、R12、R3、R2、R1、R0が復帰します。割込み復帰を行う場合には、lrの値を0xFFFFFFF9にする必要があります。0xFFFFFFF9は、"スレッドモードへの復帰で復帰時メインスタックを使用する"という意味です。詳細については、cortexM4のマニュアルを参照ください。
pcには、戻り番地を設定します。ここではthread_init()が設定されていて、ディスパッチ時にthread_initがコールされます。thread_initの引数は1つであり、上記のr0が渡されます。thread_init側で、引数として渡されたthpをもとに対応する関数をコールします。

**2.1.2 intr.s※追加したソースになります**
* vec_common():割込み時にコールされる関数

本関数をすべてのベクタテーブルに設定します。この設定をすることで、割込み時には必ず本関数がコールされるようになります。本関数では、割込み時に自動的に退避されないレジスタを退避させます。下記のような処理になります。
```c:vec_common
vec_common:
  push {r4-r11,lr}  /* Save all registers that are not saved */
  MRS   r0, IPSR    /* first argument = interrupt no         */
  MRS   r1, MSP     /* second argument = stack pointer       */
  bl    thread_intr /* jump to thread_intr                   */
  pop  {r4-r11,pc}
```
tread_intr()には、引数として割込み番号とスタックポインタを渡します。tread_intr()側では、スタックポインタを保存し、割込み番号から対応する割込みハンドラを呼びます。そして最後にディスパッチを行います。

**2.1.3 dispatch.s※追加したソースになります**
* _dispatch_int:():ディスパッチ時にコールされる関数
```c:_dispatch_int
_dispatch_int:
    MOV   R13, R0
    pop {r4-r11,pc}
```
割込みの最後にコールされます。引数は1つで、スタックポインタが渡されます。本関数がコールされる前に、すでにどのタスクをディスパッチするかはスケジューリングされていて、ここでは実行すべきタスクのスタックポインタが渡されます。
### 3.3. OSのサービスコールの作成
kozosでは、以下のようにサービスコールを使用しています。
```c:サービスコール
/* システム・コール呼び出し用ライブラリ関数 */
void kz_syscall(kz_syscall_type_t type, kz_syscall_param_t *param)
{
  current->syscall.type  = type;
  current->syscall.param = param;
  asm volatile ("trapa #0"); /* トラップ割込み発行 */
}
```
第一引数に、メモリ確保、スリープ、ウェイクアップ等のどの機能を使用したいかを指定します。第二引数はそのパラメータになります。

上記のように、ソフトウェアでtrap命令を実行し、割込みを発生させています。cortexM4には、trap命令はないのでsvcを使用することにします。下記のように修正します。

```c:サービスコール(修正後)
/* システム・コール呼び出し用ライブラリ関数 */
void kz_syscall(kz_syscall_type_t type, kz_syscall_param_t *param)
{
  current->syscall.type  = type;
  current->syscall.param = param;
  __asm volatile("    SVC %0 \n" : : "I" (0));
  __asm volatile("    NOP \n");
}
```

以上になります。
簡単なテストしかできておりませんので、その点についてはご了承ください。
何か不明点、ご意見があればコメントしていただけると幸いに存じます。