Edited at

ZynqMP 向け Debian/Linux(v2019.1版) でFPGA クロックの周波数の変更に失敗する件


はじめに

筆者は次の記事で紹介したような Xilinx 製 SoC(通称 ZynqMP) を搭載した評価ボードに Debian/Linux をビルドして公開しています。

最近、Xilinx から v2019.1 版 がリリースされました。そこで現在、試験的に v2019.2版のビルドと動作確認を行っています。その際、FPGA のクロックの周波数を Linux から変更しようとするとエラーになる問題が発生しました。

この記事では、v2019.1 でFPGA のクロックの周波数を Linux から変更しようとするとエラーになる問題の経緯、原因、デバイスドライバの修正までを忘備録的に紹介します。


何が起きているのか


症状

ここでは、エラーになった時の症状を紹介します。

まず、次のようなデバイスツリーを用意します。このデバイスツリーは、PL0 の周波数を 250MHz に変更するものです。


fclk0.dts

/dts-v1/; /plugin/;

/ {
fragment@0 {
target-path = "/amba_pl@0";
__overlay__ {
fclk0 {
compatible = "ikwzm,fclkcfg-0.10.a";
clocks = <&zynqmp_clk 0x47 &zynqmp_clk 0>;
insert-rate = "250000000";
insert-enable = <1>;
remove-rate = "1000000";
remove-enable = <0>;
};
};
};
};


ここで FPGA の周波数を変更する方法については以下の記事を参照してください。

このデバイスツリーは v2018.2版では正常にインストールされます(ただし &zynqmp_clk ではなく &clk)。しかし、v2019.1 版では次のようなエラーが発生します。

root@debian-fpga:~# device-tree-overlay.rb -i fclk0 --dts fclk0.dts

[ 1536.285975] zynqmp_clk_divider_set_rate() set divider failed for pl0_ref_div1, ret = -22
[ 1536.294321] fclkcfg amba_pl@0:fclk0: driver installed.
[ 1536.299470] fclkcfg amba_pl@0:fclk0: device name : fclk0
[ 1536.305044] fclkcfg amba_pl@0:fclk0: clock name : pl0_ref
[ 1536.310780] fclkcfg amba_pl@0:fclk0: resource clock : iopll
[ 1536.316354] fclkcfg amba_pl@0:fclk0: clock rate : 99999999
[ 1536.322197] fclkcfg amba_pl@0:fclk0: clock enabled : 1
[ 1536.327423] fclkcfg amba_pl@0:fclk0: remove rate : 1000000
[ 1536.333166] fclkcfg amba_pl@0:fclk0: remove enable : 0

メッセージによると、Kernel 内のzynqmp_clk_divider_set_rate() で何らかのエラーが発生して、クロックの周波数の変更に失敗しています。本来なら周波数(clock rate) は 250MHz になるはずですが、100MHz のままです。この 100MHz という周波数は、FSBL で設定した PL0 の周波数です。つまり、周波数は変更されていません。


デバッグ(firmware編)

zynqmp_clk_divider_set_rate() は Linux Kernel(linux-xlnx) の drivers/clk/zynqmp/divider.c にあります。ソースコードをみる限り、この関数は firmware の PM_CLOCK_SETDIVIDER ファンクションを呼び出しているだけのようです。つまり、 firmware がエラーを返しているようです。ここで firmware と言っているのは、ATF(ARM Trusted Firmware) です。

さらに ATF のソースコードを調べてみると、これまた PMUFW(Platform Manager Unit Firmware) を呼び出しているだけのようです(なんか多重下請け構造を思いうかべます)。

そこで、ATF と PMUFW にシリアルポートにログを出力させてみました。すると次のような結果が得らえました。

root@debian-fpga:~# device-tree-overlay.rb -i fclk0 --dts fclk0.dts

NOTICE: pm_smc_handler(PM_CLOCK_SETDIVIDER) start
NOTICE: pm_clock_setdivider(71,-65536) start
> ClockSetDivider(71, 0, 0) start
> ClockSetDivider done1(15)
NOTICE: pm_clock_setdivider done4(15)
NOTICE: pm_smc_handler(PM_CLOCK_SETDIVIDER) done(15)
[ 71.984279] zynqmp_clk_divider_set_rate() set divider failed for pl0_ref_div1, ret = -22
NOTICE: pm_smc_handler(PM_CLOCK_SETDIVIDER) start
NOTICE: pm_clock_setdivider(71,65535) start
> ClockSetDivider(71, 1, 0) start
> ClockSetDivider done1(15)
NOTICE: pm_clock_setdivider done4(15)
NOTICE: pm_smc_handler(PM_CLOCK_SETDIVIDER) done(15)
[ 72.014847] fclkcfg amba_pl@0:fclk0: driver installed.
[ 72.019982] fclkcfg amba_pl@0:fclk0: device name : fclk0
[ 72.025548] fclkcfg amba_pl@0:fclk0: clock name : pl0_ref
[ 72.031291] fclkcfg amba_pl@0:fclk0: resource clock : iopll
[ 72.036860] fclkcfg amba_pl@0:fclk0: clock rate : 99999999
[ 72.042708] fclkcfg amba_pl@0:fclk0: clock enabled : 1
[ 72.047932] fclkcfg amba_pl@0:fclk0: remove rate : 1000000
[ 72.053675] fclkcfg amba_pl@0:fclk0: remove enable : 0

"NOTECE:" で始まる行が ATF が出力しているログ、">" で始まる行が PMUFW が出力している行です。

このログで着目するところは、PMUFW が出力している次の2行です。

> ClockSetDivider(71, 0, 0) start

> ClockSetDivider done1(15)

ClockSetDivider() の第一引数はクロックのID番号、2番目の引数は DIV番号(これは後述)、3番目の引数は設定する値です。ClockSetDivider() の結果、15 を返しています。正常に終了した場合は 0 を返すはずです。この ClockSetDivider() でなんらかのエラーが発生しているのでしょう。

PMUFW のソースコードの pm_core.c では、ClockSetDivder() は次のようになっています(pm_printf はデバッグ用に筆者が追加した)。最初のチェックのところで引っかかってエラーを返しているようです。


pm_core.c

static void PmClockSetDivider(PmMaster* const master, const u32 clockId,

const u32 divId, const u32 val)
{
PmClock* clock;
s32 status = XST_SUCCESS;
PmInfo("%s> ClockSetDivider(%lu, %lu, %lu)\\r\\n", master->name, clockId,
divId, val);
pm_printf("%s> ClockSetDivider(%lu, %lu, %lu) start\\r\\n", master->name, clockId,
divId, val);
clock = PmClockGetById(clockId);
if (NULL == clock || 0U == val || INVALID_DIV_ID(divId)) {
status = XST_INVALID_PARAM;
pm_printf("%s> ClockSetDivider done1(%d)\\r\\n", master->name, status);
goto done;
}
#ifndef DISABLE_CLK_PERMS
status = PmClockCheckPermission(clock, master->ipiMask);
if (XST_SUCCESS != status) {
pm_printf("%s> ClockSetDivider done2(%d)\\r\\n", master->name, status);
goto done;
}
#endif
status = PmClockDividerSetVal(clock, divId, val);
pm_printf("%s> ClockSetDivider done3(%d)\\r\\n", master->name, status);
done:
IPI_RESPONSE1(master->ipiMask, status);
}


ここでログを見返すと ClockSetDivider() の第3引数に0 になっていることがわかります。どうやらこれが原因のようです。

そもそもClock Divider という機能は、元のクロックからの出力を分周(指定された数で割った周波数を出力)するものです。その分周する値に0を指定すること自体、0で除算することになるのでおかしいのです。


v2018.2 以前では

前節では v2019.1 では ClockSetDivider() の分周値に 0 を指定すると PMUFW でエラーが発生すると説明しましたが、v2018.2 ではなぜ発生しなかったのでしょうか?。それは、v2019.1 になってから firmware(ATF+PMUFW) の構造が変わったことに起因します。

v2018.2 では、クロックの分周機能は PMUFW ではなく ATF が行っていました。それが v2019.1 からは ATF ではなく PMUFW に機能が移っていて、ATF は単なる中受けになっています。

v2018.2 の時は ATF がクロックの分周機能を担っていたのですが、その時は ATF は分周値のチェックをしていなかったのです。幸い、ZynqMP の PLLは分周値が0でも動作するので(多分1とみなしている?)問題は発覚しなかったのです。

試しに、firmware(ATF+PMUFW) を v2019.1版に、 Linux Kernel を v2018.2 版にして動作させてみたところ、Linux Kernel v2019.1 版と同様にエラーが発生しました。どうやら分周値に0を指定する問題は前々から存在していたようです。


原因究明

この章では、Linuxのデバイスドライバが分周値に0が指定してしまう原因を究明します。


ZynqMP のクロック生成回路の構造

ZynqMP のクロック生成回路は次のような構造になっています。

Fig.1 ZynqMPのクロック生成回路の構造


ここで重要なことは、分周器が2段になっていることです。各々の分周器は6bit のレジスタをもっていて1分周から63分周まで分周できます。この分周器を2つ接続することで最大3969(63×63)分周まで可能になっています。


Linux の clk_core オブジェクト

Linux Kernel ではクロックは clk_core というオブジェクトを使ってクロックを扱っています。clk_core はGate、Mux(Selector)、Divider などの個々の機能の上位クラス的な位置づけになっています。Linux Kernel では、この clk_core を繋げてクロックを構成します。その際、clk_core からみて元のクロックの方向にある clk_core のことを parent としています。

前節の ZynqMP のクロック生成回路は Linux Kernel 内では次のようになっています。

Fig.2 Linux Kernel 内でのZynqMPのクロック生成回路の構造



Linux のclk_core の周波数変更のメカニズム

clk_coreには CLK_SET_RATE_PARENT というフラグがあります。このフラグは、自分の parent 方向にある clk_core が周波数変更可能かどうかを示します。

clk_set_rate() は、まず周波数を変更したいクロックの出口にある clk_core に対して、 clk_calc_new_rates() 関数を呼び出します。clk_calc_new_rates() は、指定されたclk_core に対してdetermine_rate() または rount_rate() を呼び出します。 この関数には希望の周波数と parent の周波数を渡します。もし自分のハードが周波数変更可能であれば、parent 側の周波数を計算して返します。もしCLK_SET_RATE_PARENT フラグが立っていて、かつ、計算した parent 側の周波数と、parent 側ですでに設定される周波数が異なれば、parent に対して clk_calc_new_rates() を呼び出します。そうでなければそのclk_core オブジェクトを返します。このclk_core オブジェクトが周波数設定の起点になります。

こうして得られた起点の clk_core オブジェクトから、今度は出口方向にむかって、周波数を設定していきます。


分周値が0になるメカニズム

FSBL が PL0 の周波数を100MHz に設定したとき、各 clk_core の設定は次の図のようになります。

Fig.3 FSBL が 100MHz に設定した時の各設定



  • mux では IOPLL が選択されます。IOPLL の周波数は1500MHz なのでmux のrate は 1,500,000,000になります。

  • divider0 では分周値が15 に設定されます。divider0 の rate は1,500,000,000 ÷ 15 で 100,000,000 になります。

  • divider1 では分周値が1に設定されます。divider の rate は 100,000,000 ÷ 1 で 100,000,000 になります。

  • gate は on に設定されます。gate は周波数は変更されないので、 divider1 の周波数 100,000,000 がそのまま rate になります。

問題は、この状態の時 PL0 の周波数を250MHz に設定しようとしたときに起こります。divider1 の round_rate() は出力希望周波数を250MHzにして分周値を計算しようとしますが、parent である divider1 の rate は 100MHz になっています。100MHz を 分周 して250MHz を作るのは無理です。その結果 divider1 の round_rate() は分周値0 かつ parent への希望周波数を0にしてしまいます。divider0 の round_rate() は出力希望周波数を0MHz にして分周値を計算しようとします。ここで divider0 は分周値を(間違えて?) 0に計算してしまいます。

これらの計算の結果、divider0 と divider1 に分周値0を設定しようとして firmware がエラーを返して、結局、周波数の変更に失敗します。


対処方法


ZynqMP用クロックドライバを修正

linux-xlnx (v2019.1) の ZynqMP 用のクロックドライバを修正します。具体的には drivers/clk/zynqmp/divider.c に determine_rate() を追加します。この関数は divider1 の round_rate() に変わって呼び出される関数です。具体的には、divider1 の分周値を計算する時には、その前段の clk_core を調べて、それが divider0 だった時はdivider0 の parent の rate から分周値を求めます。こうすることで divider0 に希望周波数よりも小さい rate が設定されていた場合でも正常に分周値を求めることが出来ます。

注意) divider.c では divider0 の dev_type はTYPE_DIV1 に、 divider1 の dev_type は TYPE_DIV2 になっています。


drivers/clk/zynqmp/divider.c

static int zynqmp_clk_divider2_determine_rate(struct clk_hw *div2_hw,

struct clk_rate_request *req)
{
const char * clk_name = clk_hw_get_name(div2_hw);
struct clk_hw* div1_hw = NULL;
struct clk_hw* parent_hw= NULL;
struct zynqmp_clk_divider *divider1 = NULL;
struct zynqmp_clk_divider *divider2 = to_zynqmp_clk_divider(div2_hw);
const struct zynqmp_eemi_ops *eemi_ops = zynqmp_pm_get_eemi_ops();
unsigned long parent_rate = req->best_parent_rate;
u32 min_div1 = 1;
u32 max_div1 = 1;
u32 min_div2 = 1;
u32 max_div2 = 1;
int ret = 0;
if ((clk_hw_get_flags(div2_hw) & CLK_SET_RATE_PARENT) &&
(divider2->flags & CLK_FRAC) &&
(req->rate % req->best_parent_rate)) {
req->best_parent_rate = req->rate;
goto done;
}
if (!(clk_hw_get_flags(div2_hw) & CLK_SET_RATE_PARENT))
goto compute;
if (!(div1_hw = clk_hw_get_parent(div2_hw)))
goto compute;
if (!(divider1 = to_zynqmp_clk_divider(div1_hw)))
goto compute;
if (divider1->div_type != TYPE_DIV1)
goto compute;

if (!(parent_hw = clk_hw_get_parent(div1_hw))) {
goto compute;
} else {
unsigned long new_parent_rate = clk_hw_get_rate(parent_hw);
if (!new_parent_rate)
goto compute;
parent_rate = new_parent_rate;
}
if (divider1->flags & CLK_DIVIDER_READ_ONLY) {
u32 clk_id = divider1->clk_id;
u32 value;
ret = eemi_ops->clock_getdivider(clk_id, &value);
if (ret) {
pr_warn_once("%s() get divider failed for %s, ret = %d\\n",
__func__, clk_hw_get_name(div1_hw), ret);
goto compute;
}
min_div1 = value & 0xFFFF;
max_div1 = value & 0xFFFF;
} else {
min_div1 = 1;
max_div1 = divider1->max_div;
}

compute:
if (divider2->flags & CLK_DIVIDER_READ_ONLY) {
u32 clk_id = divider2->clk_id;
u32 value;
ret = eemi_ops->clock_getdivider(clk_id, &value);
if (ret) {
pr_warn_once("%s() get divider failed for %s, ret = %d\\n",
__func__, clk_name, ret);
return ret;
}
min_div2 = value >> 16;
max_div2 = value >> 16;
} else {
min_div2 = 1;
max_div2 = divider2->max_div;
}
{
long error = LONG_MAX;
u32 bestdiv1 = 1;
u32 bestdiv2 = 1;
u32 div1;
u32 div2;
for (div1 = min_div1; div1 <= max_div1; div1++) {
long div1_rate = DIV_ROUND_UP_ULL((u64)parent_rate,div1);
for (div2 = min_div2; div2 <= max_div2; div2++) {
long div2_rate = DIV_ROUND_UP_ULL((u64)div1_rate, div2);
long new_error = div2_rate - req->rate;
if (abs(new_error) < abs(error)) {
bestdiv1 = div1;
bestdiv2 = div2;
error = new_error;
}
}
}
req->best_parent_hw = div1_hw;
req->best_parent_rate = req->rate * bestdiv2;
}
done:
pr_debug("%s(%s) rate = %lu\\n" , __func__, clk_name, req->rate);
pr_debug("%s(%s) best_parent_rate = %lu\\n", __func__, clk_name, req->best_parent_rate);
pr_debug("%s(%s) parent_rate = %lu\\n" , __func__, clk_name, parent_rate);
pr_debug("%s(%s) done(%d)\\n", __func__, clk_name, ret);
return ret;
}
static const struct clk_ops zynqmp_clk_divider1_ops = {
.recalc_rate = zynqmp_clk_divider_recalc_rate,
.round_rate = zynqmp_clk_divider_round_rate,
.set_rate = zynqmp_clk_divider_set_rate,
};
static const struct clk_ops zynqmp_clk_divider2_ops = {
.recalc_rate = zynqmp_clk_divider_recalc_rate,
.round_rate = zynqmp_clk_divider_round_rate,
.set_rate = zynqmp_clk_divider_set_rate,
.determine_rate = zynqmp_clk_divider2_determine_rate,
};


上記の determin_rate() 関数を divider1 (dev_type=TYPE_DIV2) のオペレーション関数に追加します。


drivers/clk/zynqmp/divider.c

struct clk_hw *zynqmp_clk_register_divider(const char *name,

u32 clk_id,
const char * const *parents,
u8 num_parents,
const struct clock_topology *nodes)
{
struct zynqmp_clk_divider *div;
struct clk_hw *hw;
struct clk_init_data init;
int ret;
const struct zynqmp_eemi_ops *eemi_ops = zynqmp_pm_get_eemi_ops();
struct zynqmp_pm_query_data qdata = {0};
u32 ret_payload[PAYLOAD_ARG_CNT];
/* allocate the divider */
div = kzalloc(sizeof(*div), GFP_KERNEL);
if (!div)
return ERR_PTR(-ENOMEM);
init.name = name;
init.ops = (nodes->type == TYPE_DIV2) ? &zynqmp_clk_divider2_ops : &zynqmp_clk_divider1_ops;
:
:
(後略)