Zephyr RTOS 〜 起動シーケンスと任意処理の追加 〜

# 1. はじめに

Zephyr では、任意の関数を Zephyr のカーネル初期化前後や `main()` 関数実行前など、`main()` 関数のルーチンの外で実行する方法も提供されています。

この記事では、Zephyr カーネルの起動シーケンスの紹介とともに、この `main()` 関数の外で任意の処理を実行するための仕組みについて紹介していきます。

なお、他の記事で教材として使用している playbook はこの記事では出てきませんが、
README.md 下部にも記載してある関連記事も参考にしていただくと幸いです(鋭意更新中)。

https://github.com/Corgeek/ZephyrOpsPlaybook

# 2. Zephyr の起動シーケンス

それでは早速、Zephyr の起動シーケンスを流し読みしていきます。
Zephyr カーネルの初期化や main() 関数が呼び出されるまでの全体のシーケンスは以下のように実装されています。
(これより以前に、ローレベルな z_init_cpu() や SoC 固有の処理なども走ります)

<details open><summary> Zephyr カーネルの起動シーケンス(前半) </summary>

```c:zephyr/kernel/init.c
__boot_func
FUNC_NO_STACK_PROTECTOR
FUNC_NORETURN void z_cstart(void)
{
	/* gcov hook needed to get the coverage report.*/
	gcov_static_init();

	/* initialize early init calls */
	z_sys_init_run_level(INIT_LEVEL_EARLY);

	/* perform any architecture-specific initialization */
	arch_kernel_init();

	LOG_CORE_INIT();

#if defined(CONFIG_MULTITHREADING)
	z_dummy_thread_init(&_thread_dummy);
#endif /* CONFIG_MULTITHREADING */
	/* do any necessary initialization of static devices */
	z_device_state_init();

#if CONFIG_SOC_EARLY_INIT_HOOK
	soc_early_init_hook();
#endif
#if CONFIG_BOARD_EARLY_INIT_HOOK
	board_early_init_hook();
#endif
	/* perform basic hardware initialization */
	z_sys_init_run_level(INIT_LEVEL_PRE_KERNEL_1);
#if defined(CONFIG_SMP)
	arch_smp_init();
#endif
	z_sys_init_run_level(INIT_LEVEL_PRE_KERNEL_2);

#ifdef CONFIG_STACK_CANARIES
	uintptr_t stack_guard;

	z_early_rand_get((uint8_t *)&stack_guard, sizeof(stack_guard));
	__stack_chk_guard = stack_guard;
	__stack_chk_guard <<= 8;
#endif	/* CONFIG_STACK_CANARIES */

#ifdef CONFIG_TIMING_FUNCTIONS_NEED_AT_BOOT
	timing_init();
	timing_start();
#endif /* CONFIG_TIMING_FUNCTIONS_NEED_AT_BOOT */

#ifdef CONFIG_MULTITHREADING
	switch_to_main_thread(prepare_multithreading());
#else
#ifdef ARCH_SWITCH_TO_MAIN_NO_MULTITHREADING
	/* Custom ARCH-specific routine to switch to main()
	 * in the case of no multi-threading.
	 */
	ARCH_SWITCH_TO_MAIN_NO_MULTITHREADING(bg_thread_main,
		NULL, NULL, NULL);
#else
	bg_thread_main(NULL, NULL, NULL);

	/* LCOV_EXCL_START
	 * We've already dumped coverage data at this point.
	 */
	irq_lock();
	while (true) {
	}
	/* LCOV_EXCL_STOP */
#endif /* ARCH_SWITCH_TO_MAIN_NO_MULTITHREADING */
#endif /* CONFIG_MULTITHREADING */

	/*
	 * Compiler can't tell that the above routines won't return and issues
	 * a warning unless we explicitly tell it that control never gets this
	 * far.
	 */

	CODE_UNREACHABLE; /* LCOV_EXCL_LINE */
}
```

</details>

続いて、`bg_thread_main()` の処理に入り、`main()` 関数が呼ばれています。

<details open><summary> Zephyr カーネルの起動シーケンス(後半) </summary>

```c:zephyr/kernel/init.c
__boot_func
static void bg_thread_main(void *unused1, void *unused2, void *unused3)
{
	ARG_UNUSED(unused1);
	ARG_UNUSED(unused2);
	ARG_UNUSED(unused3);

#ifdef CONFIG_MMU
	/* Invoked here such that backing store or eviction algorithms may
	 * initialize kernel objects, and that all POST_KERNEL and later tasks
	 * may perform memory management tasks (except for
	 * k_mem_map_phys_bare() which is allowed at any time)
	 */
	z_mem_manage_init();
#endif /* CONFIG_MMU */
	z_sys_post_kernel = true;

#if CONFIG_IRQ_OFFLOAD
	arch_irq_offload_init();
#endif
	z_sys_init_run_level(INIT_LEVEL_POST_KERNEL);
#if CONFIG_SOC_LATE_INIT_HOOK
	soc_late_init_hook();
#endif
#if CONFIG_BOARD_LATE_INIT_HOOK
	board_late_init_hook();
#endif

#if defined(CONFIG_STACK_POINTER_RANDOM) && (CONFIG_STACK_POINTER_RANDOM != 0)
	z_stack_adjust_initialized = 1;
#endif /* CONFIG_STACK_POINTER_RANDOM */
	boot_banner();

	void z_init_static(void);
	z_init_static();

	/* Final init level before app starts */
	z_sys_init_run_level(INIT_LEVEL_APPLICATION);

	z_init_static_threads();

#ifdef CONFIG_KERNEL_COHERENCE
	__ASSERT_NO_MSG(arch_mem_coherent(&_kernel));
#endif /* CONFIG_KERNEL_COHERENCE */

#ifdef CONFIG_SMP
	if (!IS_ENABLED(CONFIG_SMP_BOOT_DELAY)) {
		z_smp_init();
	}
	z_sys_init_run_level(INIT_LEVEL_SMP);
#endif /* CONFIG_SMP */

#ifdef CONFIG_MMU
	z_mem_manage_boot_finish();
#endif /* CONFIG_MMU */

#ifdef CONFIG_BOOTARGS
	extern int main(int, char **);

	int argc = 0;
	char **argv = prepare_main_args(&argc);
	(void)main(argc, argv);
#else
	extern int main(void);

	(void)main();
#endif /* CONFIG_BOOTARGS */

	/* Mark non-essential since main() has no more work to do */
	z_thread_essential_clear(&z_main_thread);

#ifdef CONFIG_COVERAGE_DUMP
	/* Dump coverage data once the main() has exited. */
	gcov_coverage_dump();
#endif /* CONFIG_COVERAGE_DUMP */
} /* LCOV_EXCL_LINE ... because we just dumped final coverage data */
```
</details>

ここまでが Zephyr の起動シーケンスとなるのですが、その中で複数回登場する `z_sys_init_run_level()` があります。

これが今回紹介する「任意処理の追加」であり、カーネル初期化前後に、任意の処理を仕込むための仕組みとして提供されている機能となります。

内容は下記で、`struct init_entry *levels` にリスト化されていて、
指定された `level` のリストが順次呼ばれる処理になっています。

<details open><summary> 登録された任意処理の実行 </summary>

```c:zephyr/kernel/init.c
static void z_sys_init_run_level(enum init_level level)
{
	static const struct init_entry *levels[] = {
		__init_EARLY_start,
		__init_PRE_KERNEL_1_start,
		__init_PRE_KERNEL_2_start,
		__init_POST_KERNEL_start,
		__init_APPLICATION_start,
#ifdef CONFIG_SMP
		__init_SMP_start,
#endif /* CONFIG_SMP */
		/* End marker */
		__init_end,
	};
	const struct init_entry *entry;

	for (entry = levels[level]; entry < levels[level+1]; entry++) {
		const struct device *dev = entry->dev;
		int result;

		sys_trace_sys_init_enter(entry, level);
		if (dev != NULL) {
			result = do_device_init(entry);
		} else {
			result = entry->init_fn.sys();
		}
		sys_trace_sys_init_exit(entry, level, result);
	}
}
```
</details>

# 3. 任意処理の登録方法

それでは、実際に処理を登録する方法について説明を行います。
処理の登録には下記のように `SYS_INIT()` マクロを利用します。

```c: SetupRegisters 関数をカーネル初期化前に実行
int SetupRegisters(void)
{
    (*(volatile uint32_t*)0x12345678) = 0;
    return 0;
}

SYS_INIT(SetupRegisters, PRE_KERNEL_1, 0);
```


以下は、公式の [SYS_INIT()](https://docs.zephyrproject.org/4.0.0/doxygen/html/group__sys__init.html#gaf507cc0613add8113c41896bd631254f) からの引用(レイアウト加工)です。

---

```
#define SYS_INIT(init_fn, level, prio)
```

Register an initialization function.
The function will be called during system initialization according to the given level and priority.

| Params | Description |
|---|---|
| init_fn | Initialization function. |
| level | Initialization level. Allowed tokens: `EARLY`, `PRE_KERNEL_1`, `PRE_KERNEL_2`, `POST_KERNEL`, `APPLICATION` and `SMP` if `CONFIG_SMP` is enabled. |
| prio | Initialization priority within _level. Note that it must be a decimal integer literal without leading zeroes or sign (e.g. 32), or an equivalent symbolic name (e.g. #define MY_INIT_PRIO 32); symbolic expressions are not permitted (e.g. `CONFIG_KERNEL_INIT_PRIORITY_DEFAULT` + 5). |

---

つまり、第一引数に登録したい関数を、第二引数と第三引数で呼びたいタイミングを調整することになります。第二引数のおよそのタイミングは以下となっています。
ちなみに、Zephyr のカーネル初期化前だと一部 API が使えないため注意も必要です(特にwait系)。

| 第二引数の候補 | タイミング | 注意点 |
|---|---|---|
| `EARLY` | カーネル初期化前 | NOP の ループなどで時間を稼ぐ |
| `PRE_KERNEL_1` | 一部カーネル初期化 | 上記に加えて `k_busy_wait()` |
| `PRE_KERNEL_2` | 一部ドライバ等が有効な状態 | 同上 |
| `POST_KERNEL` | スケジューラが動作 | 上記に加えて `k_msleep()` など |
| `APPLICATION` | `main()` 関数実行直前 | 同上 |
| `SMP` | `APPLICATION` 〜 `main()` で動作 | `CONFIG_SMP` が有効な時のみ |

# 4. 実際の動き

実際に `zephyr/samples/hello_world` をベースに、`App1()`, `App2()` を `APPLICATION` として登録してみます。

```c:zephyr/samples/hello_world/src/main.c
#include <stdio.h>

int main(void)
{
	printf("Hello World! %s\n", CONFIG_BOARD_TARGET);

	return 0;
}

#include <zephyr/kernel.h>

int App1(void)
{
	printk(__func__);
	return 0;
}

int App2(void)
{
	printk(__func__);
	return 0;
}

SYS_INIT(App1, APPLICATION, 0);
SYS_INIT(App2, APPLICATION, 1);
```

実行結果 (nucleo_f401reで確認)

```
*** Booting Zephyr OS build v4.0.0 ***
App1App2Hello World! nucleo_f401re/stm32f401xe
```
第三引数の `prio` を入れ替えてみる

```c:zephyr/samples/hello_world/src/main.c の登録部分
SYS_INIT(App1, APPLICATION, 1);
SYS_INIT(App2, APPLICATION, 0);
```

実際の優先順位に沿って実行されている

```
*** Booting Zephyr OS build v4.0.0 ***
App2App1Hello World! nucleo_f401re/stm32f401xe
```

# 5. いつ使うのか

使用頻度はあまり高くないかもしれませんが、実務経験で下記のような場面がありました。
下記以外でも、こんな理由で利用せざるを得なかったなど、コメントいただけると幸いです。

## 5.1. pinmux の設定

STM32 F446RE で pinmux を C 言語実装で設定する際、タイミングによって(確かSerial通信が)うまく動作しないことがありました。

ただしこれは v2.x.y 時代に起因するもので v3 世代以降で pinmux キーワードは削除されています。
現在は標準的なインターフェースとして dts での設定しか提供されておらず、動的な変更手段がないためこの現象は起きないかもしれません。

## 5.2. SoC固有の特殊な起動シーケンス

とあるボードは自由に書き換えられる内蔵 FLASH を持たず、内蔵ブートローダーが外付けの SPI FLASH からロードして立ち上がる仕組みが提供されていました。
一方で、ベンダーから提供された SWD 有効化のタイミングはカーネル初期化のかなり後段だったため、
基板立ち上げの最初期に、EARLY で SWD 有効化レジスタを設定して Zephyr の起動シーケンスを追いかけていました。

# 6. まとめ

今回は、Zephyr カーネルの初期化ルーチンを追いながら、`main()` 以前のタイミングで任意の処理を呼ぶ仕組みについて紹介しました。

利用するシーンが少ないかもしれませんが、特殊な機能なのもあって埋もれがちな一方で、知っておいて損はない機能だと思いチョイスしてみました。

今後も得た知識を共有していければと思っています。