37歳になりました。おめでとう俺、ありがとう俺。30代も残りわずかです。
何歳になろうが、本人は特に何も変わってませんが、周りから見ると確実にオジサンです。若者へ説教と昔の自慢話だけは絶対しないように気をつけます。
今に始まったことじゃないんですが、いつも何歳だか良くわからなくなります。もちろん (現在の西暦) - (生まれた西暦) で、1月1日〜3月9日は -1すれば良いのはわかってますが、ぱっと答えられません。なぜなのか……。
目次: ROCK64/ROCKPro64
ASUS TinkerBoardで動かしているlinux-nextを最新版に更新したところ、下記のようなエラーを出力して起動しなくなりました。
[ 0.000000] __clk_core_init: Failed to get phase for clk 'sdmmc_drv' [ 0.000000] rockchip_clk_register_branches: failed to register clock sdmmc_drv: -22 [ 0.000000] rockchip_clk_register_branches: failed to register clock sdmmc_sample: -17 [ 0.000000] rockchip_clk_register_branches: failed to register clock sdio0_drv: -17 [ 0.000000] rockchip_clk_register_branches: failed to register clock sdio0_sample: -17 [ 0.000000] rockchip_clk_register_branches: failed to register clock sdio1_drv: -17 [ 0.000000] rockchip_clk_register_branches: failed to register clock sdio1_sample: -17 [ 0.000000] rockchip_clk_register_branches: failed to register clock emmc_drv: -17 [ 0.000000] rockchip_clk_register_branches: failed to register clock emmc_sample: -17
エラーメッセージで検索しても、特にめぼしい報告やパッチに引っかかりません。うーん。困りましたね。
仕方ないので、更新履歴をgit bisectすると2/13から2/14の間にクロックドライバが変更されたことが原因だとわかりました。
diff --git a/drivers/clk/clk.c b/drivers/clk/clk.c
index dc8bdfbd6a0c..ed1797857bae 100644
--- a/drivers/clk/clk.c
+++ b/drivers/clk/clk.c
@@ -3457,7 +3457,12 @@ static int __clk_core_init(struct clk_core *core)
* Since a phase is by definition relative to its parent, just
* query the current clock phase, or just assume it's in phase.
*/
- clk_core_get_phase(core);
+ ret = clk_core_get_phase(core);
+ if (ret < 0) {
+ pr_warn("%s: Failed to get phase for clk '%s'\n", __func__,
+ core->name);
+ goto out;
+ }
/*
* Set clk's duty cycle.
しかしこの変更は真っ当に見えます。今までエラーを無視していたところをエラーチェックするようにしているだけだからです。
どの辺りでエラーが出るか調べるためprintkを適当に入れたりしながら追ってみると、下記の場所で躓いていました。
// drivers/clk/clk.c
static int __clk_core_init(struct clk_core *core)
{
...
/*
* Set clk's phase by clk_core_get_phase() caching the phase.
* Since a phase is by definition relative to its parent, just
* query the current clock phase, or just assume it's in phase.
*/
phase = clk_core_get_phase(core); //★★変更が影響している場所
if (phase < 0) {
ret = phase;
pr_warn("%s: Failed to get phase for clk '%s'\n", __func__,
core->name);
//goto out;
}
// drivers/clk/clk.c
static int clk_core_get_phase(struct clk_core *core)
{
int ret;
lockdep_assert_held(&prepare_lock);
if (!core->ops->get_phase)
return 0;
/* Always try to update cached phase if possible */
ret = core->ops->get_phase(core->hw); //★★ここでエラーが発生していた
if (ret >= 0)
core->phase = ret;
return ret;
}
独自のget_phaseを持っているドライバがエラーを返すと、clk_core_get_phase() もエラーを返し、先程のエラーメッセージが表示されるみたいです。しかし、この処理自体におかしな箇所はないように思います。真っ当です。
おそらく実装がおかしいのはRockchipのMMCドライバのクロック周りの方でしょう。エラーメッセージに出ているsdmmc_drvを頼りにコードを追います。
// drivers/clk/rockchip/clk-rk3228.c
PNAME(mux_mmc_src_p) = { "cpll", "gpll", "xin24m", "usb480m" };
...
COMPOSITE(SCLK_SDMMC, "sclk_sdmmc", mux_mmc_src_p, 0,
RK2928_CLKSEL_CON(11), 8, 2, MFLAGS, 0, 8, DFLAGS,
RK2928_CLKGATE_CON(2), 11, GFLAGS),
...
//★★sdmmc_drvはこの部分由来です
MMC(SCLK_SDMMC_DRV, "sdmmc_drv", "sclk_sdmmc", RK3228_SDMMC_CON0, 1),
// drivers/clk/rockchip/clk.h
#define MMC(_id, cname, pname, offset, shift) \
{ \
.id = _id, \
.branch_type = branch_mmc, \ //★★この値がカギかな?
.name = cname, \
.parent_names = (const char *[]){ pname }, \
.num_parents = 1, \
.muxdiv_offset = offset, \
.div_shift = shift, \
}
// drivers/clk/rockchip/clk-rk3228.c
static void __init rk3228_clk_init(struct device_node *np)
{
...
rockchip_clk_register_branches(ctx, rk3228_clk_branches, //★★ここでbranch_typeを見ている箇所がある
ARRAY_SIZE(rk3228_clk_branches));
...
}
//★★注: rk3228_clk_initはRK3228のクロックコア初期化関数
// デバイスツリー内のcompatible = "rockchip,rk3228-cru" を持つノードに応じて呼ばれる
CLK_OF_DECLARE(rk3228_cru, "rockchip,rk3228-cru", rk3228_clk_init);
// drivers/clk/rockchip/clk.c
void __init rockchip_clk_register_branches(
struct rockchip_clk_provider *ctx,
struct rockchip_clk_branch *list,
unsigned int nr_clk)
{
...
for (idx = 0; idx < nr_clk; idx++, list++) {
flags = list->flags;
/* catch simple muxes */
switch (list->branch_type) {
...
case branch_mmc: //★★branch_type == branch_mmcだったら
clk = rockchip_clk_register_mmc( //★★クロックの登録をしている
list->name,
list->parent_names, list->num_parents,
ctx->reg_base + list->muxdiv_offset,
list->div_shift
);
break;
...
// drivers/clk/rockchip/clk-mmc-phase.c
struct clk *rockchip_clk_register_mmc(const char *name,
const char *const *parent_names, u8 num_parents,
void __iomem *reg, int shift)
{
...
init.name = name;
init.flags = 0;
init.num_parents = num_parents;
init.parent_names = parent_names;
init.ops = &rockchip_mmc_clk_ops; //★★クロックの操作関数を登録している
...
// drivers/clk/rockchip/clk-mmc-phase.c
static const struct clk_ops rockchip_mmc_clk_ops = {
.recalc_rate = rockchip_mmc_recalc,
.get_phase = rockchip_mmc_get_phase, //★★get_phaseはこの関数
.set_phase = rockchip_mmc_set_phase,
};
// drivers/clk/rockchip/clk-mmc-phase.c
static int rockchip_mmc_get_phase(struct clk_hw *hw)
{
struct rockchip_mmc_clock *mmc_clock = to_mmc_clock(hw);
unsigned long rate = clk_hw_get_rate(hw);
u32 raw_value;
u16 degrees;
u32 delay_num = 0;
/* See the comment for rockchip_mmc_set_phase below */
if (!rate)
return -EINVAL; //★★エラーを返している
...
原因らしき箇所が見つかりました。クロック周波数(rate)が設定されていなくても、エラーを返す必要はなく、phaseの初期値0を返せば良いはずですから、return -EINVALをreturn 0にすれば良さそうです。
変更すると無事linux-nextが起動できるようになりました。良かった良かった。
こんなバグを2週間以上放置するなんてlinuxらしくないなと思って、パッチを送ろうとしましたが……、なんだかとっても嫌な予感がしたので、rochchip_mmc_get_phaseでLKMLを検索してみました。
検索してびっくり、なんと、全く同じ指摘をしているパッチが先週LKMLに送られています(LKMLへのリンク)。しかも既にclk-nextに取り込まれているじゃないですか。ですがlinux-nextへの反映はまだのようです。
明日まで待っていればclk-nextがlinux-nextに取り込まれるので、何もしなくても解決していたんです。パッチ投稿が3/4なので、今回は本当に気づくタイミングが悪かったです。
Author: Jerome Brunet < >
Date: Tue Mar 3 20:29:56 2020 +0100
clk: rockchip: fix mmc get phase
If the mmc clock has no rate, it can be assumed to be constant.
In such case, there is no measurable phase shift. Just return 0
in this case instead of returning an error.
Fixes: 2760878662a2 ("clk: Bail out when calculating phase fails during clk
registration")
Tested-by: Markus Reichl <m.reichl@fivetechno.de>
Signed-off-by: Jerome Brunet <jbrunet@baylibre.com>
diff --git a/drivers/clk/rockchip/clk-mmc-phase.c b/drivers/clk/rockchip/clk-mmc-phase.c
index 4abe7ff31f53..975454a3dd72 100644
--- a/drivers/clk/rockchip/clk-mmc-phase.c
+++ b/drivers/clk/rockchip/clk-mmc-phase.c
@@ -51,9 +51,9 @@ static int rockchip_mmc_get_phase(struct clk_hw *hw)
u16 degrees;
u32 delay_num = 0;
- /* See the comment for rockchip_mmc_set_phase below */
+ /* Constant signal, no measurable phase shift */
if (!rate)
- return -EINVAL;
+ return 0;
raw_value = readl(mmc_clock->reg) >> (mmc_clock->shift);
前にも、原因を突き止めたら、解決済みだったというパターン(2018年12月4日の日記、2019年9月18日の日記)がありました。
またこの「鶏と卵のパターン」にやられました。ついてない。
コードに特攻する前に、ちゃんと探せば?って思われるかもしれませんが、困ったことにエラーメッセージで検索してもパッチに辿り着けないんです。調べに調べて原因と対策がわかった後に、はぁ?もうパッチあるじゃん!?と気づくから、徒労感が激しい……。
目次: GCC
GCCのインラインアセンブラでは __asm__("ins %0" : "=r"(aaa) : : ); のように、"=r" という不思議な文字列が出てきます。
これはconstraintsと呼ばれ(GCCのマニュアルへのリンク)、引数がレジスタ(r)なのかメモリアドレス(m)なのか、書き換えられるのか(=)などを説明しています。
どんな文字でも書けるわけではなく、変な文字(例えば 'v')を指定すると「impossible constraint in 'asm'」と怒られます。これは一体どこでチェックしているのでしょう?また、どうやって足せばよいでしょうか?
このエラーを出すのは、パスでいうと234r.vregsです。
ちなみにbuild_gccというディレクトリ名はGCCのビルドディレクトリのことです。GCCは自動生成コードをかなりの量出力するので、そちらも合わせて見る必要があります。
// gcc/function.c
static void
instantiate_virtual_regs_in_insn (rtx_insn *insn)
{
...
if (asm_noperands (PATTERN (insn)) >= 0)
{
if (!check_asm_operands (PATTERN (insn))) //★★このエラーチェックに引っかかっている
{
error_for_asm (insn, "impossible constraint in %<asm%>"); //★★このエラーメッセージが出ている
// gcc/recog.c
int
check_asm_operands (rtx x)
{
...
for (i = 0; i < noperands; i++)
{
const char *c = constraints[i];
if (c[0] == '%')
c++;
if (! asm_operand_ok (operands[i], c, constraints)) //★★このエラーチェックに引っかかっている
return 0;
}
このチェックは何をしているのかというと、lookup_constraint() で文字(例えば 'v')をテーブルから探し、constraintの種類に変換します。未知の文字の場合はCONSTRAINT_UNKNOWNになります。
次にget_constraint_type() でどのカテゴリに属するか見ます。この関数はおかしくて、なぜかCONSTRAINT__UNKNOWNをCT_REGISTERと判定します。レジスタじゃないですよね?意味不明です。
// gcc/recog.c
int
asm_operand_ok (rtx op, const char *constraint, const char **constraints)
{
int result = 0;
...
while (*constraint)
{
enum constraint_num cn;
char c = *constraint;
int len;
switch (c)
{
...
default:
cn = lookup_constraint (constraint); //★★文字からconstraint_numに変換
// 未知の文字の場合CONSTRAINT__UNKNOWNになる
switch (get_constraint_type (cn))
{
case CT_REGISTER: //★★なぜかここに行く
if (!result
&& reg_class_for_constraint (cn) != NO_REGS //★★レジスタのconstraintがNO_REGSになるとNG
&& GET_MODE (op) != BLKmode
&& register_operand (op, VOIDmode))
result = 1;
break;
// build_gcc/gcc/tm-preds.h
static inline enum constraint_num
lookup_constraint (const char *p)
{
unsigned int index = lookup_constraint_array[(unsigned char) *p]; //★★この配列がカギ
return (index == UCHAR_MAX
? lookup_constraint_1 (p)
: (enum constraint_num) index);
}
enum constraint_num
{
CONSTRAINT__UNKNOWN = 0,
CONSTRAINT_r,
CONSTRAINT_f,
...
static inline enum constraint_type
get_constraint_type (enum constraint_num c)
{
if (c >= CONSTRAINT_p)
{
if (c >= CONSTRAINT_G)
return CT_FIXED_FORM;
return CT_ADDRESS;
}
if (c >= CONSTRAINT_m)
return CT_MEMORY;
if (c >= CONSTRAINT_I)
return CT_CONST_INT;
return CT_REGISTER; //★★cがCONSTRAINT__UNKNOWNつまり0の場合、どれにも当てはまらずCT_REGISTERと判断される
}
// build_gcc/insn-preds.c
const unsigned char lookup_constraint_array[] = {
CONSTRAINT__UNKNOWN,
CONSTRAINT__UNKNOWN,
...
MIN ((int) CONSTRAINT_r, (int) UCHAR_MAX),
MIN ((int) CONSTRAINT_s, (int) UCHAR_MAX),
CONSTRAINT__UNKNOWN,
CONSTRAINT__UNKNOWN,
CONSTRAINT__UNKNOWN, //★★'v' は未定義、未知の文字の場合は全てCONSTRAINT__UNKNOWNになる
CONSTRAINT__UNKNOWN,
...
// build_gcc/gcc/tm-preds.h
static inline enum reg_class
reg_class_for_constraint (enum constraint_num c)
{
if (insn_extra_register_constraint (c))
return reg_class_for_constraint_1 (c); //★★ここで見つからないとNO_REGSが返されてNG
return NO_REGS;
}
// build_gcc/gcc/insn-preds.c
enum reg_class
reg_class_for_constraint_1 (enum constraint_num c)
{
switch (c)
{
case CONSTRAINT_r: return GENERAL_REGS;
case CONSTRAINT_f: return TARGET_HARD_FLOAT ? FP_REGS : NO_REGS;
case CONSTRAINT_j: return SIBCALL_REGS;
case CONSTRAINT_l: return JALR_REGS;
default: break; //★★CONSTRAINT__UNKNOWNはどのcaseにも当てはまらないのでNO_REGSが返されてNG
}
return NO_REGS;
}
明らかにレジスタとは思えないCONSTRAINT__UNKNOWNが返ってきますが、なぜかCT_REGISTERだと思って処理し始め、最終的にエラーとして弾きます。結果オーライですがこれで良いんでしょうか。GCCのコードは訳がわかりません……。
ではvを正当なconstraintの一員にするにはどうしたら良いでしょうか?
第一歩としてはGCCのconfigディレクトリの下にある *.mdファイル(MarkdownではなくMachine Descriptorです)を編集します。
;; config/riscv/riscv.md
(include "predicates.md")
(include "constraints.md")
;; config/riscv/constraints.md
(define_register_constraint "v" "TARGET_VECTOR ? VP_REGS : NO_REGS"
"A vector register (if available).")
これを足すと(他にも色々やらないといけないんですけど)、先程のreg_class_for_constraint_1() に変化が生じます。
// build_gcc/gcc/insn-preds.c
enum reg_class
reg_class_for_constraint_1 (enum constraint_num c)
{
switch (c)
{
case CONSTRAINT_r: return GENERAL_REGS;
case CONSTRAINT_f: return TARGET_HARD_FLOAT ? FP_REGS : NO_REGS;
case CONSTRAINT_j: return SIBCALL_REGS;
case CONSTRAINT_v: return TARGET_VECTOR ? VP_REGS : NO_REGS; //★★これが足されて通過するようになる
case CONSTRAINT_l: return JALR_REGS;
default: break;
}
return NO_REGS;
}
GCCはこの手のピタゴラスイッチの塊で、何を変えると望みの機能が実装できるか、全くわかりません。こんなもの良くメンテナンスできるなあ、と思います。
目次: GCC
全てのRTLの定義はgcc/rtl.defに定義されています。例えばinsnの定義は下記のとおりです。
/* An instruction that cannot jump. */
DEF_RTL_EXPR(INSN, "insn", "uuBeiie", RTX_INSN)
何となくわかるけど、何だこれ?と思いますよね、私は思いました。それもそのはずで、実はこのファイルは単体では意味をなさず、Cファイルから何度も再利用されるからです。Cファイルからrtl.defをインクルードして使いますが、その際にDEF_RTL_EXPR() の意味が変わります。
比較的わかりやすい例を挙げるとしたら、gcc/rtl.cのrtx_formatの定義です。DEF_RTL_EXPR() を定義してから #include "rtl.def" を行う様子がわかるかと思います。
/* Indexed by rtx code, gives a sequence of operand-types for
rtx's of that code. The sequence is a C string in which
each character describes one operand. */
const char * const rtx_format[NUM_RTX_CODE] = {
/* "*" undefined.
can cause a warning message
"0" field is unused (or used in a phase-dependent manner)
prints nothing
"i" an integer
prints the integer
"n" like "i", but prints entries from `note_insn_name'
"w" an integer of width HOST_BITS_PER_WIDE_INT
prints the integer
"s" a pointer to a string
prints the string
"S" like "s", but optional:
the containing rtx may end before this operand
"T" like "s", but treated specially by the RTL reader;
only found in machine description patterns.
"e" a pointer to an rtl expression
prints the expression
"E" a pointer to a vector that points to a number of rtl expressions
prints a list of the rtl expressions
"V" like "E", but optional:
the containing rtx may end before this operand
"u" a pointer to another insn
prints the uid of the insn.
"b" is a pointer to a bitmap header.
"B" is a basic block pointer.
"t" is a tree pointer.
"r" a register.
"p" is a poly_uint16 offset. */
#define DEF_RTL_EXPR(ENUM, NAME, FORMAT, CLASS) FORMAT ,
#include "rtl.def" /* rtl expressions are defined here */
#undef DEF_RTL_EXPR
};
このrtx_formatの場合は、第3引数(FORMAT)とカンマを残して、他は無視するようにDEF_RTL_EXPR() を定義しています。
コメントなどを無視して考えるとrtl.defをインクルードしたあとは、コードは下記のようになります。
const char * const rtx_format[NUM_RTX_CODE] = {
DEF_RTL_EXPR(UNKNOWN, "UnKnown", "*", RTX_EXTRA)
DEF_RTL_EXPR(VALUE, "value", "0", RTX_OBJ)
DEF_RTL_EXPR(DEBUG_EXPR, "debug_expr", "0", RTX_OBJ)
...
};
マクロDEF_RTL_EXPR() の展開後はこうなります。
const char * const rtx_format[NUM_RTX_CODE] = {
"*",
"0",
"0",
...
};
最終的にrtx_formatは文字列の配列になります。
私もrtl.defの用途全てを理解しているわけではなく、全てを説明することもできないので、またわかったら書くことにします。
目次: Zephyr
導入、ブート周り
ボード、ドライバなど
SMP対応編
浮動小数点数命令など
その他
目次: Zephyr
昨日(2020年2月20日の日記参照)の続きです。
シリアルのみですが、ZephyrをQEMU RISC-V 32のvirtモードに移植しました。やっとZephyrが4 CPUで動くぞ、などと思っていましたが、全然ダメでした。2つ目以降のCPUは全く動きませんでした。
$ riscv64-zephyr-elf-gdb zephyr/build/zephyr/zephyr.elf (gdb) info threads Id Target Id Frame * 1 Thread 1.1 (CPU#0 [halted ]) 0x80000f70 in arch_cpu_idle () at zephyr/soc/riscv/riscv-privilege/common/idle.c:21 2 Thread 1.2 (CPU#1 [halted ]) loop_slave_core () at zephyr/arch/riscv/core/reset.S:45 3 Thread 1.3 (CPU#2 [halted ]) loop_slave_core () at zephyr/arch/riscv/core/reset.S:45 4 Thread 1.4 (CPU#3 [halted ]) loop_slave_core () at zephyr/arch/riscv/core/reset.S:45
理由は簡単で、ZephyrのRISC-V版はSMPに対応していないからです。何を言ってるんだ?って?ええ、実は私も今この瞬間まで知りませんでした。ビックリしました。
リセット付近のソースコードをみると、
// zephyr/arch/riscv/core/reset.S
/*
* Remainder of asm-land initialization code before we can jump into
* the C domain
*/
SECTION_FUNC(TEXT, __initialize)
/*
* This will boot master core, just halt other cores.
* Note: need to be updated for complete SMP support
*/
csrr a0, mhartid
beqz a0, boot_master_core
loop_slave_core:
wfi
j loop_slave_core
以上のように、はっきり書いてあります。SMPは一番大事な機能だと思っていただけに、未対応とは思わなんだ。
あー、今日の16550との戦いは一体何だったんだろう。しょんぼりですわ……。
メモ: 技術系の話はFacebookから転記しておくことにした。大幅に加筆した。
目次: Zephyr
先日はZepphyrをQEMUのSpikeモードに移植しましたが、SpikeモードだとCPU数は1以外選べないので、マルチプロセッサにできません。これは知りませんでした。最初に調べておけば良かったですね……。
マルチプロセッサでZephyrを動かしてみたかったので、QEMU RISC-V 32のvirtモード(qemu-system-riscv32のmachineをvirtにすること)に、前回同様にシリアルとメモリだけ動くようなSoCとボードの定義ファイルを作りました。
QEMU virtモードでは、シリアルのハードウェアとして16550をエミュレーションします。Zephyr側には既に16550用のドライバがあるので、わざわざ実装する必要もありません。とても楽です、素晴らしいです、とか何とか思いつつ動かしてみると、シリアルドライバがクラッシュします。んあー。
ZephyrをGDBで追うと16550のLCRレジスタを読もうとして、ロードアクセスフォルトが起きていました。アドレスのオフセットを見ると、0xcになっています。正しいオフセットは3のはずです。
シリアルドライバの実装を見ると、オフセットの計算が0x3 x 4 = 0xcとなっていて、おそらくレジスタサイズが4バイト単位だと思ってアクセスしているのでしょう。
世の中にはレジスタが4バイトの実装もあるかもしれませんが、残念なことにQEMUは由緒正しい(?)実装なので16550のレジスタは「1バイト」単位です。4バイト単位でアクセスすると、全く関係ない領域が破壊されます。困りました。
問題を解決するには、SoCの定義ファイルのどこか(soc.h辺りかな?)で、
#define DT_NS16550_REG_SHIFT 0
を定義し、レジスタのサイズが2^0つまり1バイト単位であることを16550シリアルドライバに教える必要があるみたいです。とても大事なことだと思いますが、全くドキュメントに書いていないように見えます。
そこそこコード見ないとわからないので、つらいです……。
無事シリアルが動作したので、QEMUを4 CPUのマルチプロセッサ設定で起動し、GDBで覗いてみます。
$ qemu-system-riscv32 -nographic -machine virt -net none -chardev stdio,id=con,mux=on -serial chardev:con -mon chardev=con,mode=readline -kernel zephyr/build/zephyr/zephyr.elf -cpu rv32 -smp cpus=4 -s -S qemu-system-riscv32: warning: No -bios option specified. Not loading a firmware. qemu-system-riscv32: warning: This default will change in a future QEMU release. Please use the -bios option to avoid breakages when this happens. qemu-system-riscv32: warning: See QEMU's deprecation documentation for details. *** Booting Zephyr OS build v2.2.0-rc1-123-gcaca3f60b012 *** threadA: Hello World from QEMU RV32 virt board! threadB: Hello World from QEMU RV32 virt board! threadA: Hello World from QEMU RV32 virt board! threadB: Hello World from QEMU RV32 virt board! ... $ riscv64-zephyr-elf-gdb zephyr/build/zephyr/zephyr.elf ... Type "apropos word" to search for commands related to "word"... Reading symbols from build/zephyr/zephyr.elf... (gdb) target remote localhost:1234 Remote debugging using localhost:1234 0x00001000 in ?? () (gdb) info threads Id Target Id Frame * 1 Thread 1.1 (CPU#0 [running]) 0x00001000 in ?? () 2 Thread 1.2 (CPU#1 [running]) 0x00001000 in ?? () 3 Thread 1.3 (CPU#2 [running]) 0x00001000 in ?? () 4 Thread 1.4 (CPU#3 [running]) 0x00001000 in ?? ()
確かに4 CPUが存在していることがわかります。ZephyrのログはGDB側でcontinueすると出てきます。アプリケーションは、hello_worldではなくsamples/synchronizationを使っています。
メモ: 技術系の話はFacebookから転記しておくことにした。大幅に加筆した。
目次: Zephyr
引き続き、ドライバの初期化に重要な役割を果たしている __device_PRE_KERNEL_1_start[] の謎を追っていきます。謎は「__device_PRE_KERNEL_1_start[] の実体はバイナリにあるもののソースコードには見当たらない」ことです。前回まででわかったことを復習すると、
配列の要素がどうやってできたかわかったので、残る謎は下記になります。
これまた復習になりますが、__device_PRE_KERNEL_1_start[] はinitlevelセクションに配置されていました。
$ riscv64-zephyr-elf-readelf -a zephyr/build/zephyr/zephyr.elf ... Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] vector PROGBITS 80000000 000060 000010 00 AX 0 0 4 [ 2] exceptions PROGBITS 80000010 000070 000258 00 AX 0 0 4 [ 3] text PROGBITS 80000268 0002c8 002604 00 AX 0 0 4 [ 4] sw_isr_table PROGBITS 8000286c 0028cc 000080 00 WA 0 0 4 [ 5] devconfig PROGBITS 800028ec 00294c 000030 00 A 0 0 4 [ 6] rodata PROGBITS 8000291c 00297c 000305 00 A 0 0 4 [ 7] datas PROGBITS 80002c24 002c84 000014 00 WA 0 0 4 [ 8] initlevel PROGBITS 80002c38 002c98 000030 00 WA 0 0 4 ★ここに配置される★ [ 9] _k_mutex_area PROGBITS 80002c68 002cc8 000014 00 WA 0 0 4 [10] bss NOBITS 80002c80 002cdc 000140 00 WA 0 0 8 [11] noinit NOBITS 80002dc0 002cdc 000e00 00 WA 0 0 16 ...
思い出していただきたいのは、配列の要素は .init_PRE_KERNEL_130セクションに置かれていて、initlevelセクションではなかったことです。つまり誰かがわざわざinitlevelセクションに置き直しています。
そんな芸当ができるのはリンカーしかいませんので、initlevelをキーワードにリンカースクリプトを探します。
// zephyr/include/linker/common-ram.ld ... SECTION_DATA_PROLOGUE(initlevel,,) { DEVICE_INIT_SECTIONS() } GROUP_DATA_LINK_IN(RAMABLE_REGION, ROMABLE_REGION) // zephyr/include/linker/linker-defs.h /* * generate a symbol to mark the start of the device initialization objects for * the specified level, then link all of those objects (sorted by priority); * ensure the objects aren't discarded if there is no direct reference to them */ #define DEVICE_INIT_LEVEL(level) \ __device_##level##_start = .; \ KEEP(*(SORT(.init_##level[0-9]))); \ KEEP(*(SORT(.init_##level[1-9][0-9]))); \ /* * link in device initialization objects for all devices that are automatically * initialized by the kernel; the objects are sorted in the order they will be * initialized (i.e. ordered by level, sorted by priority within a level) */ #define DEVICE_INIT_SECTIONS() \ __device_init_start = .; \ DEVICE_INIT_LEVEL(PRE_KERNEL_1) \ DEVICE_INIT_LEVEL(PRE_KERNEL_2) \ DEVICE_INIT_LEVEL(POST_KERNEL) \ DEVICE_INIT_LEVEL(APPLICATION) \ __device_init_end = .; \ DEVICE_BUSY_BITFIELD() \
DEVICE_AND_API_INITの宣言を思い出すと、ドライバなどで宣言される初期化セクションの名前は .init_(level)(prio) のような名前でした。DEVICE_INIT_LEVEL() はその法則性に従ってセクションを集めます。最終的に集められたセクションは、全てinitlevelセクションに配置されます。
例えばDEVICE_INIT_SECTIONS() の2行目にあるDEVICE_INIT_LEVEL(PRE_KERNEL_1) ですと、.init_PRE_KERNEL_1(数字) という名前のセクションを集めます。
というわけで、やっと謎が解けました。マクロやリンカースクリプトの見事な連携プレイですね。
実装を見て気づいたと思いますが、DEVICE_AND_API_INIT() のprioに渡せる数字は2桁が上限です。DEVICE_INIT_LEVEL() は [0-9] もしくは [1-9][0-9] のパターンしかマッチしません。
試しに優先度を3桁にするとどうなるでしょう?mempool.cではCONFIG_KERNEL_INIT_PRIORITY_OBJECTSをprioに渡していたので、menuconfigでコンフィグを変えてみます。
$ ninja menuconfig General Kernel Options ---> Initialization Priorities ---> (30) Kernel objects initialization priority このパラメータを300に変更すると…? riscv64-zephyr-elf/bin/ld: Undefined initialization levels used. collect2: error: ld returned 1 exit status
リンク時にエラーで怒られました。これはどうやっているのでしょう?実はそんなに難しくありません。initlevelセクションを作るときとほぼ同じ仕組みです。
// zephyr/include/linker/common-ram.ld
/* verify we don't have rogue .init_<something> initlevel sections */
SECTION_DATA_PROLOGUE(initlevel_error,,)
{
DEVICE_INIT_UNDEFINED_SECTION()
}
ASSERT(SIZEOF(initlevel_error) == 0, "Undefined initialization levels used.")
// include/linker/linker-defs.h
/* define a section for undefined device initialization levels */
#define DEVICE_INIT_UNDEFINED_SECTION() \
KEEP(*(SORT(.init_[_A-Z0-9]*))) \
このinitlevel_errorセクションでは、initlevelセクションが拾い損ねた .init_* 系のセクションを全て集めます。もし1つでもセクションが拾えた場合、initlevel_errorセクションのサイズが0ではなくなるため、ASSERTに引っかかる仕組みになっています。リンカースクリプトでASSERTやSORTができるとは知らなかったですね……。
エラーチェックもきっちり作られていてZephyrはさすが良くできているなあと思います。
目次: Zephyr
ドライバの初期化に重要な役割を果たしている __device_PRE_KERNEL_1_start[] の謎を追っていきます。謎について復習すると「実体はバイナリにあるもののソースコードには見当たらない」という点でした。
ソースコードでわからなければバイナリを見るのも一つの手です。シンボルの一覧を調べます。
$ riscv64-zephyr-elf-nm -n zephyr/build/zephyr/zephyr.elf ... 80002c38 D __device_PRE_KERNEL_1_start 80002c38 D __device_init_start 80002c38 d __device_sys_init_init_static_pools3 80002c44 d __device_uart_spike 80002c50 d __device_sys_init_uart_console_init0 80002c5c D __device_PRE_KERNEL_2_start 80002c5c d __device_sys_init_z_clock_driver_init0 80002c68 D __device_APPLICATION_start 80002c68 D __device_POST_KERNEL_start 80002c68 D __device_init_end ...
メモリイメージを図示すると下記のような感じで、前回のドライバの初期化処理が期待していると予想した並びになっています。違っていたら動きませんから、当たり前ですけども。
======== 80002c38 <-- __device_PRE_KERNEL_1_start, __device_init_start __device_sys_init_init_static_pools3 ======== 80002c44 __device_uart_spike ======== 80002c50 __device_sys_init_uart_console_init0 ======== 80002c5c <-- __device_PRE_KERNEL_2_start __device_sys_init_z_clock_driver_init0 ======== 80002c68 <-- __device_POST_KERNEL_start, __device_APPLICATION_start, __device_init_end
POST_KERNELとAPPLICATIONは何も要素を持っておらず、同じアドレスになってしまっているものの、確かにレベル順(PRE_KERNEL_1, PRE_KERNEL_2, POST_KERNEL, APPLICATION)に並んでいます。
$ riscv64-zephyr-elf-readelf -a zephyr/build/zephyr/zephyr.elf ... Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] vector PROGBITS 80000000 000060 000010 00 AX 0 0 4 [ 2] exceptions PROGBITS 80000010 000070 000258 00 AX 0 0 4 [ 3] text PROGBITS 80000268 0002c8 002604 00 AX 0 0 4 [ 4] sw_isr_table PROGBITS 8000286c 0028cc 000080 00 WA 0 0 4 [ 5] devconfig PROGBITS 800028ec 00294c 000030 00 A 0 0 4 [ 6] rodata PROGBITS 8000291c 00297c 000305 00 A 0 0 4 [ 7] datas PROGBITS 80002c24 002c84 000014 00 WA 0 0 4 [ 8] initlevel PROGBITS 80002c38 002c98 000030 00 WA 0 0 4 ★ここに配置される★ [ 9] _k_mutex_area PROGBITS 80002c68 002cc8 000014 00 WA 0 0 4 [10] bss NOBITS 80002c80 002cdc 000140 00 WA 0 0 8 [11] noinit NOBITS 80002dc0 002cdc 000e00 00 WA 0 0 16 ...
実体はあるのにソースコードに見当たらず、initlevelという変わった名前のセクションに置かれています。どうやら通常のグローバル変数や、static変数ではなさそうです。
一番最初に出てくる __device_sys_init_init_static_pools3がどうやって作られるのか、追いかけたらわかるかもしれません。部分一致でも良いので、それっぽい名前をgrepするとkernel/mempool.cで見つかります。
// zephyr/kernel/mempool.c
SYS_INIT(init_static_pools, PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_OBJECTS);
// zephyr/include/init.h
/* A counter is used to avoid issues when two or more system devices
* are declared in the same C file with the same init function.
*/
#define Z_SYS_NAME(init_fn) _CONCAT(_CONCAT(sys_init_, init_fn), __COUNTER__)
/**
* @def SYS_INIT
*
* @brief Run an initialization function at boot at specified priority
*
* @details This macro lets you run a function at system boot.
*
* @param init_fn Pointer to the boot function to run
*
* @param level The initialization level, See DEVICE_AND_API_INIT for details.
*
* @param prio Priority within the selected initialization level. See
* DEVICE_AND_API_INIT for details.
*/
#define SYS_INIT(init_fn, level, prio) \
DEVICE_AND_API_INIT(Z_SYS_NAME(init_fn), "", init_fn, NULL, NULL, level,\
prio, NULL)
// (参考1)
// init_fn = init_static_pools
// level = PRE_KERNEL_1
// prio = CONFIG_KERNEL_INIT_PRIORITY_OBJECTS
//
// Z_SYS_NAME(init_static_pools) = sys_init_init_static_pools3
マクロの嵐で面食らいますが、基本的に引数をそのまま渡していくだけなので1つずつ見ればさほど難しくありません。
SYS_INIT() はmempoolがシステムドライバであることを宣言するためのAPIです(公式ドキュメント Device Driver Model - Zephyr Project Documentation, System Driver節)。最終的にはDEVICE_AND_API_INIT() が呼ばれます。実はこいつもマクロです、あとで紹介します。
若干難しいのはZ_SYS_NAME() でしょうか?このマクロはsys_init_ と、引数init_fnとカウンタ(__COUNTER__)を連結したトークンを返します。
私の環境でビルドしたときはinit_fnはinit_static_poolsで、__COUNTER__ は3でしたから、sys_init_ とinit_static_poolsと3が連結され、sys_init_init_static_pools3になります。このトークンがDEVICE_AND_API_INIT() の第一引数に渡されます。
シンボル名がsys_init_init_... とinitがダブっていて、変な名前だな?と思った方もいるでしょう、このZ_SYS_NAME() マクロが原因でした。ま、それはさておいて、続きを追いかけます。
// zephyr/include/device.h
/**
* @def DEVICE_AND_API_INIT
*
* @brief Create device object and set it up for boot time initialization,
* with the option to set driver_api.
*
* @copydetails DEVICE_INIT
* @param api Provides an initial pointer to the API function struct
* used by the driver. Can be NULL.
* @details The driver api is also set here, eliminating the need to do that
* during initialization.
*/
#ifndef CONFIG_DEVICE_POWER_MANAGEMENT
#define DEVICE_AND_API_INIT(dev_name, drv_name, init_fn, data, cfg_info, \
level, prio, api) \
static const struct device_config _CONCAT(__config_, dev_name) __used \
__attribute__((__section__(".devconfig.init"))) = { \
.name = drv_name, .init = (init_fn), \
.config_info = (cfg_info) \
}; \
static Z_DECL_ALIGN(struct device) _CONCAT(__device_, dev_name) __used \
__attribute__((__section__(".init_" #level STRINGIFY(prio)))) = { \
.config = &_CONCAT(__config_, dev_name), \
.driver_api = api, \
.driver_data = data \
}
#else
/*
* Use the default device_pm_control for devices that do not call the
* DEVICE_DEFINE macro so that caller of hook functions
* need not check device_pm_control != NULL.
*/
#define DEVICE_AND_API_INIT(dev_name, drv_name, init_fn, data, cfg_info, \
level, prio, api) \
DEVICE_DEFINE(dev_name, drv_name, init_fn, \
device_pm_control_nop, data, cfg_info, level, \
prio, api)
#endif
//DEVICE_AND_API_INITを展開すると最終的に下記のようになる
//
//変数名
// _CONCAT(__config_, dev_name) = __config_sys_init_init_static_pools3
//構造体メンバー
// drv_name = ""
// init_fn = init_static_pools
// cfg_info = NULL
//なので、
static const struct device_config __config_sys_init_init_static_pools3 __used
__attribute__((__section__(".devconfig.init"))) = {
.name = "",
.init = init_static_pools,
.config_info = NULL
};
//変数名
// _CONCAT(__device_, dev_name) = __device_sys_init_init_static_pools3
//セクション名
// level = PRE_KERNEL_1
// prio = 30
// ".init_" #level STRINGIFY(prio) = ".init_PRE_KERNEL_130"
//構造体メンバー
// _CONCAT(__config_, dev_name) = __config_sys_init_init_static_pools3
// api = NULL
// data = NULL
//なので、
static Z_DECL_ALIGN(struct device) __device_sys_init_init_static_pools3 __used
__attribute__((__section__(".init_PRE_KERNEL_130"))) = {
.config = &__config_sys_init_init_static_pools3,
.driver_api = NULL,
.driver_data = NULL
}
//(参考2: DEVICE_AND_API_INITの引数と値)
// dev_name = Z_SYS_NAME(init_fn) = Z_SYS_NAME(init_static_pools) = sys_init_init_static_pools3
// drv_name = ""
// init_fn = init_fn = init_static_pools
// data = NULL
// cfg_info = NULL
// level = level = PRE_KERNEL_1
// prio = prio = CONFIG_KERNEL_INIT_PRIORITY_OBJECTS = 30
// api = NULL
//(参考1: SYS_INITの引数と値)
// init_fn = init_static_pools
// level = PRE_KERNEL_1
// prio = CONFIG_KERNEL_INIT_PRIORITY_OBJECTS = 30
ちょっと長いですがDEVICE_AND_API_INIT() を見ていくと、最後は上記のように __config_sys_init_init_static_pools3と __device_sys_init_init_static_pools3の2つの構造体の宣言に展開されることがわかります。
後者の __device_sys_init_init_static_pools3は、先程nmでzephyr.elfを見たときに出てきた、アイツです。それに加えて __device_sys_init_init_static_pools3は、.init_PRE_KERNEL_130という変な名前のセクションに配置されることもわかると思います。
理解が合っているか不安なときは、答え合わせとしてバイナリをチェックです。ビルド時に生成されるオブジェクトbuild/zephyr/kernel/CMakeFiles/kernel.dir/mempool.c.objのシンボルテーブルをobjdumpで確認するとわかりやすいです。
$ riscv64-zephyr-elf-objdump -t zephyr/build/zephyr/kernel/CMakeFiles/kernel.dir/mempool.c.obj ... 00000000 l O .bss.lock 00000000 lock 00000000 l d .devconfig.init 00000000 .devconfig.init 00000000 l O .devconfig.init 0000000c __config_sys_init_init_static_pools3 00000000 l d .init_PRE_KERNEL_130 00000000 .init_PRE_KERNEL_130 00000000 l O .init_PRE_KERNEL_130 0000000c __device_sys_init_init_static_pools3 ...
セクション .init_PRE_KERNEL_130に __device_sys_init_init_static_pools3が配置されていることが確認できました。じゃあ、どうやって __device_PRE_KERNEL_1_startに含まれるようになるんでしょう?
続きはまた今度。
目次: Zephyr
前回はリセット直後からC言語の関数と出会うところまでを紹介しました。今回はドライバの初期化までを紹介します。
// zephyr/arch/riscv/core/prep_c.c
void _PrepC(void)
{
z_bss_zero();
#ifdef CONFIG_XIP
z_data_copy();
#endif
#if defined(CONFIG_RISCV_SOC_INTERRUPT_INIT)
soc_interrupt_init();
#endif
z_cstart();
CODE_UNREACHABLE;
}
ブート後に最初に出会うC言語の関数 _PrepC() ですが、実はまだグローバル変数の初期化が終わっていませんので、この関数で初期化しています。z_bss_zero() は、ゼロ初期化される変数の領域(BSS)を0で初期化します。z_data_copy() は、変数領域をROMからRAMにコピーします。
XIP(Execution In Place)が有効な場合、コード領域はFlash ROMなどに配置され、CPUはROMから直接コードを読み出して実行します。コード領域はそれで良いですが、変数の初期値もROMにおかれていて、そのままでは変数に書き込みできません。そのためROMからRAMにコピーする必要があります。
最後のz_start() まで来ると、やっと普通のC言語の世界です。
// zephyr/kernel/init.c
FUNC_NORETURN void z_cstart(void)
{
...
/* perform basic hardware initialization */
z_sys_device_do_config_level(_SYS_INIT_LEVEL_PRE_KERNEL_1); //★level = 0
z_sys_device_do_config_level(_SYS_INIT_LEVEL_PRE_KERNEL_2);
// zephyr/include/init.h
#define _SYS_INIT_LEVEL_PRE_KERNEL_1 0
#define _SYS_INIT_LEVEL_PRE_KERNEL_2 1
#define _SYS_INIT_LEVEL_POST_KERNEL 2
#define _SYS_INIT_LEVEL_APPLICATION 3
初期化関数z_cstart() は、色々ごちゃごちゃやっているのですが、真ん中あたりでドライバの初期化が行われます。レベルをPRE_KERNEL_1 (level 0), PRE_KERNEL_2 (level 1) の順に指定して、z_sys_device_do_config_level() という関数を呼びます。ちなみにPOST_KERNEL (level 2), APPLICATION (level 3) はもっと後で、カーネルの初期化が終わった後に使われます。
初期化される対象はデバイスドライバ以外(後述の、メモリ割り当てシステムドライバなど)もありますけど、仕組みは同じみたいです。
// zephyr/kernel/device.c
extern struct device __device_init_start[];
extern struct device __device_PRE_KERNEL_1_start[];
extern struct device __device_PRE_KERNEL_2_start[];
extern struct device __device_POST_KERNEL_start[];
extern struct device __device_APPLICATION_start[];
extern struct device __device_init_end[];
...
void z_sys_device_do_config_level(s32_t level)
{
struct device *info;
static struct device *config_levels[] = {
__device_PRE_KERNEL_1_start,
__device_PRE_KERNEL_2_start,
__device_POST_KERNEL_start,
__device_APPLICATION_start,
/* End marker */
__device_init_end,
};
for (info = config_levels[level]; info < config_levels[level+1];
info++) {
int retval;
const struct device_config *device_conf = info->config;
...
実装は上記のようになっています。level 0の場合は __device_PRE_KERNEL_1_start[] という配列を先頭から処理します。他のレベルも同様ですね。
======== <-- config_levels[0] __device_PRE_KERNEL_1_start[0] __device_PRE_KERNEL_1_start[1] ... __device_PRE_KERNEL_1_start[n] ======== <-- config_levels[1] __device_PRE_KERNEL_2_start[0] __device_PRE_KERNEL_2_start[1] ... __device_PRE_KERNEL_2_start[n] ======== <-- config_levels[2] __device_POST_KERNEL_start[0] __device_POST_KERNEL_start[1] ... __device_POST_KERNEL_start[n] ======== <-- config_levels[3] __device_APPLICATION_start[0] __device_APPLICATION_start[1] ... __device_APPLICATION_start[n] ======== <-- __device_init_end
このようにメモリ上にstruct deviceがレベル順に並んでいることを期待しているようです。しかし配列の実体はCのソースコード上には存在しないように見えます。これは一体何者なんでしょう?
続きはまた今度。
目次: Zephyr
以前(2020年2月1日の日記参照)Zephyrのエントリアドレスを調べましたが、もう少し先までZephyrのブート処理を調べます。復習となりますが、起動の仕方は下記のとおりです。
$ qemu-system-riscv32 -nographic -machine spike -net none -chardev stdio,id=con,mux=on -serial chardev:con -mon chardev=con,mode=readline -kernel zephyr/build/zephyr/zephyr.elf -s -S $ riscv64-zephyr-elf-gdb zephyr/build/zephyr/zephyr.elf ... (gdb) target remote localhost:1234 Remote debugging using localhost:1234 0x00001000 in ?? ()
ブート処理を追うにはデバッガが必須です(ブート部分でシリアル出力はできません)。これも前回の復習となりますが、QEMUはGDBでデバッグするためのオプションを持っています。オプション -sはGDBの接続をlocalhost:1234で受け付けます、という意味で、オプション -SはエミュレータをHalted状態で起動するという意味です。
GDBを接続すると、PCは0x1000を指しています。QEMU Spikeモードはリセット後0x1000から実行を開始するようです。何度かステップ実行siを行うと、0x1010から0x80000000にジャンプするはずです。
0x1000: auipc t0,0x0
0x1004: addi a1,t0,32
0x1008: csrr a0,mhartid
0x100c: lw t0,24(t0)
=> 0x1010: jr t0 # 0x80000000にジャンプ
0x1000: 0x00000297 0x02028593 0xf1402573 0x0182a283
0x1010: 0x00028067 0x00000000 0x80000000 0x00000000
↑この値をロードする
特に難しい話ではなく0x1000 + 24 = 0x1018からロード(0x80000000が書かれている)して、ジャンプするだけです。ジャンプ先にはZephyrのエントリポイント __startがいます。
// zephyr/soc/riscv/riscv-privilege/common/vector.S
SECTION_FUNC(vectors, __start)
.option norvc;
/*
* Set mtvec (Machine Trap-Vector Base-Address Register)
* to __irq_wrapper.
*/
la t0, __irq_wrapper
csrw mtvec, t0
/* Jump to __initialize */
tail __initialize
// zephyr/arch/riscv/core/isr.S
/*
* Handler called upon each exception/interrupt/fault
* In this architecture, system call (ECALL) is used to perform context
* switching or IRQ offloading (when enabled).
*/
SECTION_FUNC(exception.entry, __irq_wrapper)
/* Allocate space on thread stack to save registers */
addi sp, sp, -__z_arch_esf_t_SIZEOF
/*
* Save caller-saved registers on current thread stack.
* NOTE: need to be updated to account for floating-point registers
* floating-point registers should be accounted for when corresponding
* config variable is set
*/
RV_OP_STOREREG ra, __z_arch_esf_t_ra_OFFSET(sp)
RV_OP_STOREREG gp, __z_arch_esf_t_gp_OFFSET(sp)
RV_OP_STOREREG tp, __z_arch_esf_t_tp_OFFSET(sp)
RV_OP_STOREREG t0, __z_arch_esf_t_t0_OFFSET(sp)
...
__startは __irq_wrapper関数のアドレス(C言語の関数ではないので、関数と呼ぶのは変ですが)を割り込みベクタに設定したあと、__initializeにジャンプします。
// zephyr/arch/riscv/core/reset.S
/*
* Remainder of asm-land initialization code before we can jump into
* the C domain
*/
SECTION_FUNC(TEXT, __initialize)
/*
* This will boot master core, just halt other cores.
* Note: need to be updated for complete SMP support
*/
csrr a0, mhartid
beqz a0, boot_master_core
loop_slave_core:
wfi
j loop_slave_core
boot_master_core:
#ifdef CONFIG_INIT_STACKS
...
#endif
/*
* Initially, setup stack pointer to
* _interrupt_stack + CONFIG_ISR_STACK_SIZE
*/
la sp, _interrupt_stack
li t0, CONFIG_ISR_STACK_SIZE
add sp, sp, t0
#ifdef CONFIG_WDOG_INIT
call _WdogInit
#endif
/*
* Jump into C domain. _PrepC zeroes BSS, copies rw data into RAM,
* and then enters kernel z_cstart
*/
call _PrepC
最初にmhartidというCPUコアのIDを読み取ります。0番をマスターコアとして、0番以外のコアは割り込み待ち(wfi)の無限ループに入ります。マスターコアはスタックポインタの初期化を行って、_PrepC() 関数を呼び出します。
ここからやっとC言語の世界です。
目次: Zephyr
Zephyrは開発中なので、ときおり内部構造が変わり、過去と互換性がなくなってしまうことがあります。ボードやSoCの定義も例外ではありません。
前回までの説明ではZephyr 2.1.0を使用していましたが、2.1.0から2.2.0の間で、ボードのdefconfigの書き方が変更されました。具体的に言うと、
下記のパッチにあるような変更をすると、2.2.0でもHogeボードとSpike SoCの対応が適用できます。
それぞれ1行ずつしか変更していませんので、すぐわかると思います。
ZephyrはLinuxと同じKconfigを採用していますが、コンフィグの起点に対する考え方がだいぶ違います。Zephyrは最初にcmakeで「ボード名(-DBOARDオプション)」を指定します。すなわちコンフィグの起点はボードです。一方のLinuxはexport ARCH=x86のようにアーキテクチャを設定の起点にします。
Zephyr 2.1.0では、アーキテクチャ(CONFIG_RISCVなど)、SoC(CONFIG_SOC_RISCV_SIFIVE_FREEDOMなど)、ボードの依存関係は下記のようになっていました。
アーキテクチャ(ボードのdefconfigがyにする)← アーキテクチャが依存の起点 ↑depends on SoC(ボードのdefconfigがyにする) ↑depends on ボード(ボードのdefconfigがyにする)← Zephyrはなぜかボードがコンフィグの起点
依存関係とコンフィグの起点が矛盾しているためmenuconfigが異常な動きをします。おそらくninja menuconfigを弄れば矛盾に気づくと思いますが、Zephyrはなぜかボードが未対応のアーキテクチャやSoC(例えばqemu_riscv32でCONFIG_ARMを選択できる)を有効にできます。Zephyr 2.1.0だと2つ、おかしな設定ができます。
実際にninja menuconfigで、qemu_riscv32が未対応のアーキテクチャを選ぶと下記のようになります。
# qemu_riscv32のデフォルト Modules ---> Board Selection (QEMU RISCV32 target) ---> Board Options ---- SoC/CPU/Configuration Selection (SiFive Freedom SOC implementation) ---> Hardware Configuration ---> RISCV Options ---> Architecture (RISCV architecture) ---> General Architecture Options ---> General Kernel Options ---> ... # ボードが対応していないアーキテクチャを選択すると、SoCが勝手に変わる、ボードは何も選べなくなる Modules ---> Board Selection ---- Board Options ---- SoC/CPU/Configuration Selection (LiteX VexRiscv system implementation) ---> ★なぜかRISC-V用のSoCが出てくる Hardware Configuration ---- Architecture (x86 architecture) ---> ★x86に変えた General Architecture Options ---> General Kernel Options ---> ...
そもそもx86が選択可能な時点でおかしいですが、x86を選択したのにx86じゃないSoCが選択肢に出てくるなど、かなりおかしな動きです。
Zephyr 2.2.0では依存関係は下記のように、少しだけ整理されました。
アーキテクチャ(SoCが強制的にyにする、menuconfigでは触れない) ↑select SoC(ボードのdefconfigがyにする) ↑depends on ボード(ボードのdefconfigがyにする)← ここが起点
アーキテクチャを変えることが出来なくなりました。前よりマシですが、やはり変なところは残っていて、未対応のSoCを選択するとボードの選択肢が消えてしまいます。
# qemu_riscv32のデフォルト Modules ---> Board Selection (QEMU RISCV32 target) ---> Board Options ---- SoC/CPU/Configuration Selection (SiFive Freedom SOC implementation) ---> Hardware Configuration ---> RISCV Options ---> ... # SoCを無理やり未対応のものに変えると、ボードが選べなくなる Modules ---> Board Selection ---- Board Options ---- SoC/CPU/Configuration Selection (LiteX VexRiscv system implementation) ---> Hardware Configuration ---- RISCV Options ---> ...
個人的にはZephyrのKconfigの使い方は変だな、と思います。依存関係があることはわかっているのだから、依存の根本の方から設定(要は、アーキテクチャ → SoC → ボード、の順)したら良いのに、Zephyrはなぜかボードの設定から固定していて、変な動きになっています。
Linuxはどうしているかというと、最初にexport ARCH=x86とします。すなわち、アーキテクチャを設定の起点にしています。自然です。特に変な動きもしません。
Zephyrの方がLinuxより後発ですが、ビルド周りは退化したように感じます。不思議。
< | 2020 | > | ||||
<< | < | 03 | > | >> | ||
日 | 月 | 火 | 水 | 木 | 金 | 土 |
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | - | - | - | - |
合計:
本日: