Linux

【Zynq7】閑話. SPI バス周波数が上がらない問題

1.記事一覧

記事は複数回に分けて投稿します。XilinxのARM SoC (Zynq7020SoC)で動作する、YoctoベースのPetaLinuxを使わない素のLinuxをソースコードから構築する記事の続編です。

ZynqMP(arm64版)はこちら

エッジArm Linux構築編(Armv7)

エッジArm Linux実践編(Armv7)

2.【概要】SPI bus clockが4MHzで飽和する

SPI液晶編でILITEKの液晶LSIを使ってみたのですが、転送速度が遅いのでSPI1のバス周波数を上げられるか試していたところ、4MHz(ロジアナ計測)から上がらなくなりました。

py-spidevでバッファサイズが64KBに制限される件がSPI液晶編であったため、何か制限していないか、py-spidevのソースを再度確認しました。
https://github.com/doceme/py-spidev/blob/master/spidev_module.c#L1253

Pythonでspi.max_speed_hzに値を代入すると、

ioctl(self->fd, SPI_IOC_WR_MAX_SPEED_HZ, &max_speed_hz)

が呼ばれていますね。fdは”/dev/spidev1.0″などをopenしたときのディスクリプタです。特にスピードを制限するようなロジックは無いですね。

となると、問題はOS以下の下層、Linuxカーネル、ブートローダ、HWの問題になりそうです。今回はこの調査の過程を残します。

【諸注意】この記事は(過去もそうですが)ナレッジ記事ではありません。Linuxカーネル詳しくない人間がわからないなりに調べて勉強しながら、トラブルと相まみえる様子を書いています。生暖かい目で見守ってください。

2-1. 結論

今回はだいぶ長めな内容が続くので、先に結論を話しておきます。

ZynqのSPIにクロックを供給するPSクロック生成の”Clocks”にある、分周器のレジスタ値に異常がありました。このレジスタ値の設定値はどこでセットされるかをClockのデバイスドライバソースで調べたところ、元から設定されていた値をそのまま設定値としているようでした(若干不確実ですが)。つまり、u-boot、FSBLで設定する処理に異常があります。私の場合はVitisのプロジェクト作成方法をミスっており、Vivadoで変更したHW情報をVitisで更新しても初期化コードのソースが反映されないということでした。何とも間抜けなミスでしたが、早いうちにわかってよかったです。

Vitisで新たにプロジェクトを作成し、SPIの速度も限界まで上げられることを確認しました。また、以前のLinux構築記事の手順も修正しました。

実際は解決まで2時間程度だったのですが、その過程は様々な仮説を立てて無数の選択肢を潰す事を並列で行いゴールまで進んだ、という感じでした。せっかくならブログのネタにしてしまえということでいざ記事を書こうとしたら、自分が立てた仮説はどれもエビデンスが弱くて人に説明できないな…と気付かされました。

それなりに掘り下げて説明材料を集めようとしたら、2週間程度の時間が経っていました。仕事ではこのトラブルの粒度で報告書を書くことはあまり無いので、問題解決の考え方が正しいか見直すいい機会になりました。

説明材料を集める過程でCCFの知見が得られたので結果的にはプラスかなと思います。早いうちに遊んでみてまた記事を書こうと思います。

3.【原因調査1】Clocks・SPIレジスタ値を見る

使用しているSPI1のバス周波数がどう決まるかを知るためにまずは現状のレジスタ値を見てみます。SPI0は今回使用していません。

どのレジスタか知らないので、まずは水晶発振器(33.333MHz)の入力からSPIに入力されるまでのクロックツリーを確認します。

3-1. 【Clocks】水晶発振器からSPI基準クロックまでのクロックツリーの調査

テクニカルリファレンス(UG585)からクロック周りを調べます。まずは水晶発振器から周辺機能IP にクロックを供給する、Clocksのオレンジ枠部分です。

PS_CLK端子に水晶発振回路(33.333MHz)が接続され、I/O PLLで逓倍されます。SPIはI/O PLLを使用使用しているので、ARM PLL、DDR PLLは置いておきます。一応PLL後段のマルチプレクサ(Mux)の値を見ればどれが使用されているかわかります。

PLLでいくつに逓倍されているかを見るため、System Level Controlレジスタ(slcr)グループのIO_PLL_CTRLレジスタの値を見てみます。以下のPLL_FDIVの値です。

devmem2コマンドで見てみます。

Bash
$ sudo devmem2 0xf8000108 word
/dev/mem opened.
Memory mapped at address 0xb6f9e000.
Value at address 0xF8000108 (0xb6f9e108): 0x1E000

PLL_FDIV=30 つまり、30逓倍ですね。PLL出力は33.333*30=999.99…≒1000MHzになります。

次にPLL以降のツリーを見ていきます。

今回使用しているSPI1の基準クロックは以下のslcr.SPI_CLK_CTRLレジスタで制御しています。ここを通過したクロック(SPI1_REF_CLK)がSPI1に供給されます。PLLクロックソースセレクタ、6bit分周器、クロックゲーティング制御の3段階です。

また、SPI0_REF_CLOCKとSPI1_REF_CLOCKは共通の分周器を通過しています。

SPI_CLK_CTRLレジスタの詳細です。

devmem2で読んだ値は0x00003f00でした。

  • DIVISOR=0x3f 63分周。リセット初期値と同じ
  • SRCSEL=IOPLL つまり1000MHz入力
  • CLKACT1=SPI1へのクロック供給停止(クロックゲーティング有効)

クロックが停止しているのはSPI送受信中以外はクロックゲーティングで供給を停止しているからと思われます。

DIVISORは分周器なので、クロックは以下の計算になります。
IO_PLL/DIVISOR=1000/63≒15.9MHz=SPI1_REF_CLOCK
しかし、なぜDIVISOR=0x3Fなのかはわかりません。後述する、SPI1内の分周器と組み合わせてSPIバス周波数が決定されますので誰かが決めているはずです。FSBL、u-boot、Linuxカーネル(デバイスツリー)のどこかで設定されたか、あるいは設定されずにリセット値のままかもしれません。

Clocksのまとめ

  • クロックソースはIO_PLLで1000MHz
  • SPI1へ供給されるクロックはSPI1_REF_CLOCK≒15.9MHz
  • SPI_CLK_CTRL.DIVISOR=63(0x3F)の値がいつどこで設定されるか不明
  • SPI_CLK_CTRL.DIVISORのリセット値は63なので設定されていない可能性あり
  • SPI0、SPI1へは共通のクロック源となるので、周波数可変に制約あり

3-2. SPI内でSPIバス周波数が決まるまでの調査

SPI基準クロックから出力されたSPI1_REF_CLOCK≒15.9MHzが、SPI1に供給されていることがわかりました。次はそのクロックからどのようにバス周波数が決定されるかを調べます。

図の”Clocking”の部分がSPI0と1の分周器です。

実際には単純で、バス周波数は以下の計算式で決まります。
SPIバス周波数SCLK=SPI1_REF_CLOCK/BAUD_RATE_DIV

BAUD_RATE_DIVはSPI.XSPIPS_CR_OFFSETレジスタ内の値です。

レジスタの値を読んでみます。

Bash
$ sudo devmem2 0xe0007000 word
/dev/mem opened.
Memory mapped at address 0xb6f6a000.
Value at address 0xE0007000 (0xb6f6a000): 0x0

0でした。あれ…おかしいですね。いろいろ試した結果、以下のことがわかりました。C言語環境を用意していなかったので申し訳ないですが、pythonで検証しました。

  • SPI送受信開始しないとレジスタ値が入らない(ただし、py-spidevで検証)
  • 送受信終了から2秒くらいでレジスタ値は0になる
  • デバイスファイルopenしただけではレジスタ値は入らない。

py-spidevでopenされると、内部で/dev/spidev*.*がopenするC言語実装なので、openしただけではレジスタ値は有効になりません。あくまで送受信中のみです。パワーマネジメント機能(パワーゲーティング)による省電力化が働いているようです。

レジスタ値の変化をトレースするスクリプト

通信データが少ないとdevmem2コマンドで値を見るのが難しいので、簡単なスクリプトを作成しました。詳細は最後に後述しますが、以下のような感じです。

  • 物理アドレスをビジーループで読み、値が変化したときに値の差分を標準出力する
  • 16進数と2進数を表示
  • ビジーループなので処理負荷は高くなる
  • ビジーループなので読み飛ばす可能性あり

クリティカルな用途としては使えませんが、簡単にリアルタイムで変化を見たいときには使えます。これを使ってSPI通信のサンプルコードを実行した上で、再度レジスタを読んでみます。SPIのバス周波数設定は10MHzです。

Bash
# 第1引数は読み取る物理アドレス(word)、第2引数は変化イベントの検知回数
$ sudo bash ./devmem_edge_detect.sh 0xe0007000 8

Bash
#別端末でテストコードを実行する。SPI液晶に画像1枚表示するサンプル
root# python3 sample_ili9328.py 

レジスタ値が4回変化しました。

変化の回SPI.XSPIPS_CR_OFFSETレジスタの変化の詳細
1レジスタ値セット(省電力状態から復帰)

bit17 : ModeFail生成無効
bit16 : マニュアルスタートコマンド無効
bit15 : マニュアルスタート無効。自動モード
bit14 : マニュアルCSモード有効
bit13-10 : CS端子のslaveセレクトは無効
bit9 : 周辺機能デコードセレクト 3セレクタの1
bit8 : SPI1_REF_CLOCKを使用する
bit5-3 : BAUD_RATE_DIV=1 -> 4分周
bit2 : CPHA=1
bit1 : CPOL=1
bit0 : SPI Master mode
2bit13-10 : CS端子のslave0を選択
3bit13-10 : CS端子のslaveセレクトを無効
4レジスタ値クリア(省電力状態へ移行)

結果はBAUD_RATE_DIV=4、つまり4分周されていました。したがって、SPIバス周波数を求められます。

SPIバス周波数SCLK=SPI1_REF_CLOCK/BAUD_RATE_DIV=3.967…≒4MHz

原因の4MHzが出てきました。BAUD_RATE_DIVの最低値が4なので、SPI側ではバス周波数は飽和しています。試しにバス周波数を1MHzにすると、BAUD_RATE_DIVの値が増えてバス周波数が落ちます。つまり、ドライバでBAUD_RATE_DIVは制御されているようです。ですが、ClocksのSPI_CLK_CTRL.DIVISORの変化はありませんでした。

SPIを速くするならClocksのSPI_CLK_CTRL.DIVISORの値を変えるしかありません。原因はここにありそうです。

4章レジスタ値の確認のまとめ

  • SPIバス周波数はClocks側SPI基準クロックとSPI側の2つの分周器で決まる
  • IO_PLLは各周辺機能で共有のため、変更されることは無い(はず)
  • SPI基準クロックはSPI0、SPI1で共通
  • SPIの分周器の値はドライバで適切な値がセットされる
  • Clocks側の分周器は変化がないため、SPIバス周波数は4MHzで飽和
  • 原因の鍵は”Clocks側の分周器の値は誰が決めるか”にありそう

4.【原因調査2】Clocks・SPIドライバを読む

ユーザアプリからSPIのバス周波数を設定すると、速度によってSPI側の分周器の値が変わっていました。これはSPIドライバが行っていると考えられます。ここで、SPIドライバがClocks側SPI基準クロックの分周器も設定値によって可変するのかがわかりません。しかし、SPI0、SPI1は共通クロック源となるため、おそらく可変することは無いという仮定で見ていくことにします。

また、仮にSPIのドライバが関与していない場合、Clocksの各レジスタの初期設定をLinuxのClockドライバ、u-boot、FSBLのいずれかが行っているはずなので、そのあたりを探っていきます。

方針

  • SPIの分周器はドライバがユーザ設定に応じて変更していると仮定→確認する
  • Clocksの分周器はSPI0、1共通のため、変更しないと仮定→確認する
  • Clocksの分周器の初期値は誰がどこで設定するのか→確認する

4-1. SPIドライバのバス周波数設定周りのコードを追いかける

SPIのデバイスドライバを探します。デバイスツリーdtbをデコンパイルして、使用しているSPI1の項目を探します。また、コンパイル前のzynq-7000.dtsiも比較用に下に貼っておきます。私の環境では以下のような感じです。

デバイスツリーを扱った経験はほとんどないのですが、以下の記述がドライバの特定に使えます。

  • compatible = “xlnx,zynq-spi-r1p6”;

また、以下のクロックソースの項目も参考になりそうです。

  • clocks = <&clkc 26>, <&clkc 35>;
  • clock-names = “ref_clk”, “pclk”;

おそらく”ref_clk”=&clkc 26=SPI1_REF_CLOCKなのだろうと予想します(間違っているかもしれません)。&clkcはシンボルなので、別でclkcが定義されているはずです。”pclk”はおそらくCPU_1xクロック(APBバススレーブポート)かと思われます。

Githubで使用したLinuxカーネルソースのリビジョンに合わせてから”xlnx,zynq-spi-r1p6″を検索します。以下のspi-cadence.cドライバがヒットします。

linux-xlnx/drivers/spi/spi-cadence.c
https://github.com/Xilinx/linux-xlnx/blob/xilinx-v2022.2/drivers/spi/spi-cadence.c#L714

Zynq7000のSPIはケイデンスIPなので、その通りケイデンスのドライバを使用しています。SPIのドライバはなひたふ氏のブログで解析された話を見てほしいのですが、ざっと3層あるようです。このSPIドライバは最下層の部分に当たります。

私はLinuxドライバ開発経験は皆無なのでここから先はかなりの憶測で進めます。正しい内容とは限らないのでご注意ください。

まず、SPI側のバス周波数設定の処理になります。

cdns_spi_config_clock_freq():
バス周波数設定。
L.260~L271の間で、ユーザアプリのバス周波数設定値(transfer->speed_hz)と現在のSPIのバス周波数値(xspi->speed_hz)が異なるとき、以下の手順で設定。

  1. 分周べき乗値X:2^Xの X=1をセット
  2. 分周値<128かつバス周波数設定値を上回る分周値の場合は③を実行
  3. X++して②を再実行(つまり、分周値が4->8->16->32->64->128になってく)

要約するとユーザのバス周波数設定値未満になる様にSPIの分周器(BAUD_RATE_DIV)の値がセットされます。ここで、Clocks側の分周器は登場しません。コメント見ると、SPIコントローラが設定できる最大値、つまりSPI分周器が4未満の場合よりも高い周波数がユーザ側で設定された場合、サポートする最大周波数を設定する。とあります。つまり分周器は4を設定して飽和しますよ、ということだと思います。

先程、Clocks側の分周器は登場しないと言ったのですが、あくまでこの関数だけを見るとそう言えるだけです。仮にClocks側の分周器を変更する場合、責務はClockドライバにあるはずなので、ClockAPIがおそらく存在し、それを利用して変更を要求するような処理があるかもしれません。

次はClock関係を見ていきます。先程の関数cdns_spi_config_clock_freq()の呼び出し元を見ていきます。

cdns_spi_probe()
master->transfer_one = cdns_transfer_one;

spi_register_master(master);

cdns_transfer_one()
--->cdns_spi_setup_transfer()
----->cdns_spi_config_clock_freq()

cdns_transfer_one()関数はSPI送信開始処理を行う関数です。クロック設定後、送信バッファにデータを詰めて、割り込みを有効にします。そしてこの関数はcdn_spi_probe()関数内で関数ポインタを登録(spi_register_master())しているようです。cdns_transfer_one()関数はSPIドライバミドル層のspi.cから呼ばれることになります。

とりあえずざっと見た感じClocks側の分周器の変更をするような処理はなさそうでした。ドライバのprobe関数である、cdn_spi_probe()関数を最後に確認してClockドライバの方を見てみたいと思います。

C
cdns_spi_probe(){
...(略)
xspi->ref_clk = devm_clk_get(&pdev->dev, "ref_clk");
...(略)
xspi->clk_rate = clk_get_rate(xspi->ref_clk);
/* Set to default valid value */
master->max_speed_hz = xspi->clk_rate / 4;
xspi->speed_hz = master->max_speed_hz;
...(略)
}

cdns_spi_probe()

デバイスツリーのclocks =<&clkc 26>の情報をクロック周りのAPIから取得しています。”ref_clk”のクロック周波数を取り出し、SPIの最大バス周波数、現在のバス周波数を設定していました。

SPIドライバ読解のまとめ

  • SPIドライバは転送開始直前にSPIバス周波数を設定する
  • バス周波数の設定はSPIのIPの分周器のみ変更する
  • “ref_clk”の情報を取得しているが、SPI基準クロックの周波数のみ取得
  • SPI基準クロックの分周器は特に変更していなさそうに見える

4-2. ClockドライバのSPI基準クロック設定周りを追いかける

SPIドライバ側ではSPI基準クロックの分周器を操作してなさそうでした。すべてのコードを見たわけではないので確証は持てませんが、すべてのコードは私のスキル的にとても読めないので、Clockドライバも並行して手ががりを探していきます。

まずはSPI1のデバイスツリーからドライバを特定していきます。

clocksにクロックソースの情報があります。

clocks = <&clkc 26>, <&clkc 35>;

&clkcはクロックコントローラ定義のシンボルです。(phandle?というらしい)。clkcを探してみます。

slcr(System Level Control Register 0xF8000000)の中にclkcが定義されているのでここで間違いなさそうです。

また、clock-output-namesの0から数えて26番目に”spi1″があるので、
SPI1のclocks = <&clkc 26>はおそらくここを指していると思われます。

早速以下に対応するドライバを探します。clkc.cですね。

compatible = “xlnx,ps7-clkc”;

https://github.com/Xilinx/linux-xlnx/blob/xilinx-v2022.2/drivers/clk/zynq/clkc.c#L620

ソースの先頭に行くとレジスタアドレスのオフセット値が定義されています。
SPI関係のクロックレジスタもあります。

C
#define SLCR_SPI_CLK_CTRL (zynq_clkc_base + 0x58)

このレジスタを参照していそうなところは、1箇所ありました。

https://github.com/Xilinx/linux-xlnx/blob/xilinx-v2022.2/drivers/clk/zynq/clkc.c#L418

zynq_clk_register_periph_clk()関数です。SPI基準クロックを登録しています。この関数は同じソースファイル内部に定義があります。この関数内で、
clk_register_divider()関数マクロ(include/linux/clk-provider.h)
を呼んでいるのが内部の分周器の登録と思われます。

マクロ先は__clk_hw_register_divider()関数で(drivers/clk/clk-driver.c)内にあります。ここで、クロック構造体を設定し、clk_hw_register()関数で登録しているようです。

zynq_clk_setup()[drivers/clk/zynq/clkc.c]
->zynq_clk_register_periph_clk()
--->clk_register_divider()↓の関数マクロ[include/linux/clk-provider.h]
----->clk_register_divider_table()[drivers/clk/clk-divider.c]
------->__clk_hw_register_divider()
———>clk_hw_register()[drivers/clk/clk.c]

ここで、手がかりとなりそうな関数の引数をピックアップします。

  • clk_register_divider()関数マクロ
    • 第8引数flag : CLK_DIVIDER_ONE_BASED
    • 第8引数flag : CLK_DIVIDER_ALLOW_ZERO
  • clk_hw_register()
    • 第2引数 : struct clk_hw構造体

このあたりを中心にwebで調べてみました。

4-3. Common Clock Framework(CCF)で出口が見えてきた

前述の定数や関数はLinuxカーネルが提供する、Common Clock Framework(CCF)が提供するAPIに関するものでした。私は初めて聞いたので、にわかなりに理解した事をまとめてみます。間違っていたらすみません。

CCFはKernel v4.9から登場した比較的最近のAPIのようです。ARM SoCのようなデバイスごとの仕様の違いを共通化するために作られたらしいです。

このフレームワークのAPIは2サイドのユーザ向けに分かれています。

プロバイダ向けAPIクロックドライバの提供者などが利用
関数プレフィクス:clk_hw_*()
例:XilinxのClockドライバ開発者などが、SoCのクロックデバイスをLinuxカーネルに登録し、木構造のクロックツリーオブジェクトを構築。コンシューマ向けに機能を提供する。
コンシューマ向けAPIクロックを使うドライバ開発者などが利用
関数プレフィクス:clk_*()
例:SPIのドライバ開発者がSPIに供給されるクロック周波数を取得する

clk_hw_register()関数はクロックハードウェアをクロックツリーに登録するためのプロバイダ向けAPIです。clk_register_divider()関数マクロで指定したフラグは第2引数 : struct clk_hw構造体に収められます。

SPI0、1へ供給されるクロックの分周器では以下のフラグ設定がされていました。

CLK_DIVIDER_ONE_BASEDデフォルトの除算器はレジスタから読み出された値に1を加えた値であるため、これは除算器がレジスタから読み出された生の値であることを意味します。これは、CLK_DIVIDER_ALLOW_ZEROフラグが設定されていない限り、0が無効であることを意味します(機械翻訳)
CLK_DIVIDER_ALLOW_ZERO上記において分周値の0を有効とする

また、他のフラグ設定で気になる部分を抜粋します。

CLK_DIVIDER_READ_ONLY
(こちらは未設定)
クロックがあらかじめ設定されていることを示し、フレームワークに何も変更しないように指示します。このフラグは、クロックに割り当てられているopsにも影響します(機械翻訳)

>>ROM専なので、もとから分周器レジスタに入っている値を固定する。つまり、初期化時点で分周器レジスタに入っている値をそのまま登録する。したがって初期化値はu-boot以前に決まる。しかし今回は設定されていないのでそうではない。

結論を言うと上記の設定では分周器の初期値を誰が決めるのかはわかりません。READ ONlYが設定されていないということは初期化を含め分周器の値の変更を許可しているからです。

続いてclk_divider_opsと呼ばれる、コンシューマAPIを呼び出した際のハードウェア固有の処理を実装したコールバック構造体について説明します。

コンシューマAPI、例えばclk_get_rate(xspi->ref_clk)関数をSPIドライバが呼び出す際、親クロックの周波数を取得するときに、クロックドライバはクロック周波数を計算して返すという処理をします。この処理はハードウェア固有の処理になる場合があるので、プロバイダ側(Xilinxなど)がコールバック関数として実装して登録する仕組みになっています。

そのコールバック構造体opsはstruct clk_hw->init.opsにあります。

clk_hw構造体

C
struct clk_hw {
    struct clk_core *core;
    struct clk *clk;
    const struct clk_init_data *init;    //<--クロックデバイス登録時に使用する初期化データ
};

clk_init_data構造体

C
struct clk_init_data {
	const char		*name;
	const struct clk_ops	*ops;    //<-クロックデバイスを操作するコールバック構造体
	/* Only one of the following three should be assigned */
	const char		* const *parent_names;
	const struct clk_parent_data	*parent_data;
	const struct clk_hw		**parent_hws;
	u8			num_parents;
	unsigned long		flags;
};

clk_divider_ops構造体

C
struct clk_ops {
	int		(*prepare)(struct clk_hw *hw);
	void		(*unprepare)(struct clk_hw *hw);
	int		(*is_prepared)(struct clk_hw *hw);
	void		(*unprepare_unused)(struct clk_hw *hw);
	int		(*enable)(struct clk_hw *hw);
	void		(*disable)(struct clk_hw *hw);
	int		(*is_enabled)(struct clk_hw *hw);
	void		(*disable_unused)(struct clk_hw *hw);
	int		(*save_context)(struct clk_hw *hw);
	void		(*restore_context)(struct clk_hw *hw);
	unsigned long	(*recalc_rate)(struct clk_hw *hw,
					unsigned long parent_rate);
	long		(*round_rate)(struct clk_hw *hw, unsigned long rate,
					unsigned long *parent_rate);
	int		(*determine_rate)(struct clk_hw *hw,
					  struct clk_rate_request *req);
	int		(*set_parent)(struct clk_hw *hw, u8 index);
	u8		(*get_parent)(struct clk_hw *hw);
	int		(*set_rate)(struct clk_hw *hw, unsigned long rate,
				    unsigned long parent_rate);
	int		(*set_rate_and_parent)(struct clk_hw *hw,
				    unsigned long rate,
				    unsigned long parent_rate, u8 index);
	unsigned long	(*recalc_accuracy)(struct clk_hw *hw,
					   unsigned long parent_accuracy);
	int		(*get_phase)(struct clk_hw *hw);
	int		(*set_phase)(struct clk_hw *hw, int degrees);
	int		(*get_duty_cycle)(struct clk_hw *hw,
					  struct clk_duty *duty);
	int		(*set_duty_cycle)(struct clk_hw *hw,
					  struct clk_duty *duty);
	int		(*init)(struct clk_hw *hw);          //<--クロックデバイス登録時の初期化処理
	void		(*terminate)(struct clk_hw *hw);
	void		(*debug_init)(struct clk_hw *hw, struct dentry *dentry);
};

clk_divider_ops構造体にはint (*init)(struct clk_hw *hw)メンバが居て、クロック登録時に実行されるコールバックとなっています。しかし、デフォルト実装ではこのコールバックの実装はありません。Xilinxのクロックドライバは特に改変せず、以下のようにデフォルト実装をそのまま使っています。

実際のclk_divider_ops構造体変数宣言

C
const struct clk_ops clk_divider_ops = {
	.recalc_rate = clk_divider_recalc_rate,
	.round_rate = clk_divider_round_rate,
	.determine_rate = clk_divider_determine_rate,
	.set_rate = clk_divider_set_rate,
};

上記の変数宣言のコールバックは分周器の値を読み出して周波数レートを計算する、あるいは周波数レートを設定するときのコールバックのみです。つまり、クロック登録時の初期化処理は行われません。

クロックドライバはSPI基準クロックの周波数設定を行わない。つまりu-boot、FSBLの初期化値をそのまま使うという。普通に考えればu-bootでクロック初期化されないとまず動かないのでこの時点でClocksの中枢部は初期化されているはずなのですが、SPIを含む周辺機能のクロック周りに関しては未使用の場合初期化が必須ではないです。その点について確証が欲しかったので、あえて遠回りして調べてみました。

余談になりますが、前章でSPIドライバ側からClocks側の親分周器を変更していないとは言い切れなかったのですが、CCFのAPIを調べていて参考になりそうな情報が見つかりました。

CCFのコンシューマAPI(例えばclk_set_rate())で仮に周波数レート変更する場合、親クロックデバイス(SPI基準クロックやPLL)にさかのぼって変更をかけるわけですが、親クロックデバイスに対し子クロックデバイスが2つ以上ある場合、変更に失敗します。SPI基準クロックの分周器はSPI0、1共通なので、SPIが両方生きている場合、SPI基準クロックの分周器変更がもう一方のSPIxに影響するため変更を中止させるようです。

この仕様がある限りSPIドライバが親分周器を変更するような実装はしないと思われます。

5章 ”Clocks・SPIドライバを読む”のまとめ

  • SPIドライバではSPIのIP側の分周器を設定する
  • ClockドライバではSPI基準クロックの分周器の値を初期化しない
  • Clocks周りの初期化値の設定はu-bootかFSBLが行っているはず
  • Common Clock FrameworkのAPIは今後使えそう

5.u-boot上でクロックレジスタを確認

ClocksのSPIへ供給するルートの分周器の値がu-boot以前で決まることがわかったため、u-bootでレジスタの値を見てみます。

u-bootでSPIは有効にしていないため、おそらくFSBLの設定値そのままだと思われます。なのでやる意味は薄いのですが、u-bootでメモリを見たことがなかったので、これを期にやってみます。

5-1. シリアルコンソールでu-bootコンソールに入る

Zybo z7ボードとUSBケーブルで接続し、シリアルコンソールでブート中の様子が見えるようにします。電源投入後、FSBLのログの後、u-bootのログでカウントダウンがあるので、0になる前に何らかのキーを押下します。

私はscreenコマンドで入るのですが、デバイスファイル(/dev/ttyUSB*)がZybo z7ボードの電源を入れないと現れないので、ubuntuが起動するまで待ち、ログイン後、rebootコマンドを打ってからu-bootコンソールに入っています。

Bash
(...略)
U-Boot 2022.01-dirty (Feb 22 2023 - 08:32:22 +0000)

CPU:   Zynq 7z020
Silicon: v3.1
Model: Zybo z7 Custom Board by sh-goto
DRAM:  ECC disabled 1 GiB
Flash: 0 Bytes
NAND:  0 MiB
MMC:   mmc@e0100000: 0
Loading Environment from FAT... *** Warning - some problems detected reading environment; recovered successfully
OK
In:    serial@e0001000
Out:   serial@e0001000
Err:   serial@e0001000
Net:   No ethernet found.
Hit any key to stop autoboot:  0     <----これが0になる前に適当なキーを押す
Zynq>       <----コンソールのプロンプト。自分はプロンプト名を"Zynq"に変更済

5-2. レジスタ値を読み出す

メモリ読み出しはmdコマンドを使用します。使い方はマクニカさんの所がわかりやすいです。

Clocks SPI分周器レジスタ
slcr.SPI_CLK_CTRL 0xF8000158
DIVISOR[13:8]=0x3F

Zynq> md.l 0xF8000158 1 
f8000158: 00003f03                             .?..
Zynq> 

案の定と言いますかu-bootの時点で0x3Fです。この値はリセット値なので、もしかしたらFSBLで設定されていない説が濃厚です。最後にFSBLを確認します。

6.FSBLのClocks初期化コードを確認する

6-1. Vivadoの設定を確認

VivadoでSPI ref clockを確認します。

SPIはIOPLL選択の166.67MHzになっていますので、分周器の値は以下になります。

分周値=水晶発振器周波数*IOPLL逓倍値/出力周波数=33.333*30/166.67≒6

分周値は6です。続いてVitisのコードを確認します。

6-2. Vitisの初期化コードを確認

初期化コードを見る前に、レジスタ設定値をまとめたHTMLが生成されているのを見つけたので、これを見てみます。

結構手が込んだHTMLを生成していて驚きました。分周器(DIVISOR)の値は先程計算した”6″ですね。

続いてソースコードを確認します。ps7_init.cが初期化コードのようです。
slcr.SPI_REF_CLOCKレジスタアドレス=0xF8000158で検索すると、出てきません。あれ?と思いつつ、原因がわかりました。

C
    EMIT_MASKWRITE(0XF8000154, 0x00003F33U ,0x00000A02U),
    // .. .. START: TRACE CLOCK
    // .. .. FINISH: TRACE CLOCK
    // .. .. CLKACT = 0x1
    // .. .. ==> 0XF8000168[0:0] = 0x00000001U
    // .. ..     ==> MASK : 0x00000001U    VAL : 0x00000001U
    // .. .. SRCSEL = 0x0
    // .. .. ==> 0XF8000168[5:4] = 0x00000000U
    // .. ..     ==> MASK : 0x00000030U    VAL : 0x00000000U
    // .. .. DIVISOR = 0x5
    // .. .. ==> 0XF8000168[13:8] = 0x00000005U
    // .. ..     ==> MASK : 0x00003F00U    VAL : 0x00000500U
    // .. .. 
    EMIT_MASKWRITE(0XF8000168, 0x00003F31U ,0x00000501U),
    // .. .. SRCSEL = 0x0
    // .. .. ==> 0XF8000170[5:4] = 0x00000000U
    // .. ..     ==> MASK : 0x00000030U    VAL : 0x00000000U
    // .. .. DIVISOR0 = 0x5
    // .. .. ==> 0XF8000170[13:8] = 0x00000005U
    // .. ..     ==> MASK : 0x00003F00U    VAL : 0x00000500U
    // .. .. DIVISOR1 = 0x4
    // .. .. ==> 0XF8000170[25:20] = 0x00000004U
    // .. ..     ==> MASK : 0x03F00000U    VAL : 0x00400000U
    // .. .. 

レジスタ初期化コードがありませんでした。0xF8000154まではありました。

理由がわからずしばらく探していると、ps7_init.cがなぜかもう一つありました。

C
EMIT_MASKWRITE(0XF8000154, 0x00003F33U ,0x00000A02U),
    // .. CLKACT0 = 0x0
    // .. ==> 0XF8000158[0:0] = 0x00000000U
    // ..     ==> MASK : 0x00000001U    VAL : 0x00000000U
    // .. CLKACT1 = 0x1
    // .. ==> 0XF8000158[1:1] = 0x00000001U
    // ..     ==> MASK : 0x00000002U    VAL : 0x00000002U
    // .. SRCSEL = 0x0
    // .. ==> 0XF8000158[5:4] = 0x00000000U
    // ..     ==> MASK : 0x00000030U    VAL : 0x00000000U
    // .. DIVISOR = 0x6
    // .. ==> 0XF8000158[13:8] = 0x00000006U
    // ..     ==> MASK : 0x00003F00U    VAL : 0x00000600U
    // .. 
    EMIT_MASKWRITE(0XF8000158, 0x00003F33U ,0x00000602U),
    // .. .. START: TRACE CLOCK
    // .. .. FINISH: TRACE CLOCK
    // .. .. CLKACT = 0x1
    // .. .. ==> 0XF8000168[0:0] = 0x00000001U
    // .. ..     ==> MASK : 0x00000001U    VAL : 0x00000001U
    // .. .. SRCSEL = 0x0
    // .. .. ==> 0XF8000168[5:4] = 0x00000000U
    // .. ..     ==> MASK : 0x00000030U    VAL : 0x00000000U
    // .. .. DIVISOR = 0x5
    // .. .. ==> 0XF8000168[13:8] = 0x00000005U
    // .. ..     ==> MASK : 0x00003F00U    VAL : 0x00000500U
    // .. .. 
    EMIT_MASKWRITE(0XF8000168, 0x00003F31U ,0x00000501U),

こちらはありました。なぜ2つあるのかを調べたら、しょうもないミスをしていたことが判明しました。

6-3. 【原因判明】Vitisのプロジェクト作成方法が間違っていた

原因は私のVitisのプロジェクト生成の方法が間違っていました。

FSBLのプロジェクトを作成するときに以下の構造でプロジェクトを作成していました。

Platformプロジェクト
 |_Aplicationプロジェクト(FSBLテンプレートで作成)

FSBLバイナリはApplicationプロジェクトを使用していたため、Vivadoの更新をかけても、Platformプロジェクトのみ更新されます。FSBLは更新されず、途中で追加されたSPIは初期化を行っていなかったというわけです。

私はLinuxに関わる前はZyboz7ボードをベアメタル(standalone)で遊んでいたため、ソフトウェアはアプリケーションプロジェクトで作成するものばかりと勘違いしていました。いつも作成する際はプロジェクトサンプルのテンプレート画面が表示され、FSBLテンプレートもあります。ついそれ使うものばかりと思い込んでしまったということです。

ただ、実際はPlatformプロジェクトにFSBLが入っていて、ApplicationプロジェクトのベアメタルアプリはFSBLからハンドオフされていたようです。

つまり、今回のLinux構築ではApplicationプロジェクトは作成不要だったということです。これはなんとも情けないミスですね…

PlatformプロジェクトのFSBLバイナリに入れ替え、SPI基準クロックが無事初期化されたことを確認し、速度もMAXの25MHz(EMIOの最大)まで出ることを確認しました。

以上無事原因がわかり、以前の記事も修正を加えて事なきを得ました。

7.レジスタの値変化を調べる簡易ツール

3-2.のレジスタ値の変化をトレースするスクリプトでSPIのレジスタ値をdevmem2コマンドで調べようとしたら、値がクリアされていました。これはパワーマネジメント機能によって通信中以外は機能がOFFになっているためと考えられます。

通信中のみ値が有効になるとすると、devmem2コマンドを手動で実行するのが面倒になります。そこで、通信中にレジスタの値を読めるように簡易的なツールをshellで作成しました。機能は単純です。

  • devmem2コマンドでポーリングし、値変化したら標準出力
  • ポーリングにより負荷が高くなるのであくまで簡易測定
  • ポーリングがゆえ、読み飛ばしの可能性あり

使い方

  1. ツールを起動する。引数は物理アドレスと検知回数
  2. SPIなどの通信をさせて対象のレジスタをアクティブにする
  3. 検知回数に達すると終了。それ以外は手動停止(Ctrl+C)

Githubに上げました。使う方はいらっしゃらないと思いますが(笑)

https://github.com/kern-gt/devmem-edge-detecter

ABOUT ME
sh-goto
組込エンジニア. 最近の遊びはLinuxの低レイヤいじりとXilinxのZynqとFPGAを使った電子工作
関連記事