1.記事一覧
記事は複数回に分けて投稿します。XilinxのARM SoC (Zynq7020SoC)で動作する、YoctoベースのPetaLinuxを使わない素のLinuxをソースコードから構築する記事の続編です。
エッジArm Linux構築編(Armv7)
- 【Zynq7】Yoctoを使わずに素のLinuxをソースから構築する
- 【Zynq7】1.FSBL(1stStageBootLoader)の構築
- 【Zynq7】2.U-bootの構築
- 【Zynq7】3.Linuxカーネルの構築
- 【Zynq7】4.Zyboz7ボードのデバイスツリー構築
- 【Zynq7】5.RootFSのマウント(Ubuntu18.04)
- 【Zynq7】6.カーネルモジュールの配置
- 【Zynq7】番外編1.u-bootが途中で止まる問題
- 【Zynq7】番外編2.MACアドレスをQSPIFlashから読み出して設定する
エッジArm Linux実践編(Armv7)
- 【Zynq7】実践1. LinuxにRTCを認識させる【I2Cデバイス編】
- 【Zynq7】実践2. 温度センサをPython+Dockerで使う【I2Cデバイス】
- 【Zynq7】実践3. 組込みLinuxでコンテナ導入に苦労した話【Docker Engine編】
- 【Zynq7】実践4. LinuxでSPI使う【SPIデバイス】 ←本ページ
- 【Zynq7】実践5. ZynqPLのGPIOをLinuxで使う【液晶編2】
- 【Zynq7】実践6. PythonでSPI液晶を使う【液晶編3】
- 【Zynq7】閑話. SPI バス周波数が上がらない問題
2.【概要】PythonでSPIデバイスを使いたい
以前、I2Cデバイスを使う記事を投稿しましたが、今度はSPIデバイスを使用してみます。
PythonでSPI液晶に適当な絵を映してみたをサクッとやりたかったのですが、3記事ほどの内容になってしまいました。思っていたより難しかったです。ラズパイ使わない縛りやるととてもやりごたえがあります。
今回は5年位前に購入して棚に積んであったaitendoの2.2インチ320×240SPIフルカラー液晶を選択しました。これをPythonから制御します。ラズパイで液晶にデスクトップ画面を表示させる例がweb上でありますが、それはやりません。あくまでIoT機器のステータス情報などを文字で表示する様な目的を想定します。
今回は複数の記事に渡って解説していきます。主な流れは以下になります。
- Zynq7000でPSのSPIの周辺機能IPを有効化して、Linuxから利用できるようにする
- Zynq7000のFPGA上(=PL)にGPIOのIPを構築し、Linuxから利用できるようにする
- 液晶のドライバをPythonで実装し、画面を映すアプリで遊ぶ
液晶のI/FがSPIとGPIOが必要なのでこれを使えるようにして、液晶もライブラリがないので、作ります。少々手間のかかる内容になりそうです。
2-1. Zynq PS SPIの周辺機能IPを有効化して、Linuxから利用できるようにする
今までのLinux構築ではZynqSoCのSPI周辺機能IPがそもそも有効になっていないため、使うには以下が必要です。
- SPIのIPを有効化し、配線情報を追加したBitstreamを生成
- Linuxカーネルのspidevドライバを有効にするため、再ビルドする
- Linuxデバイスツリーを更新し、SPIをシステムから使えるようにする
2-2. FPGA上(=PL)にGPIOのIPを構築し、Linuxから利用できるようにする
使用しているZybo-z7ボードのPmodコネクタの端子に新たにGPIOを割り当てるために、ZynqSoCのPL、つまりFPGA上にGPIOのIPを構築します。
- GPIOのIPを構築し、配線情報を追加したBitstreamを生成
- Linuxデバイスツリーを更新し、GPIOをシステムから使えるようにする
2-3. 液晶のドライバをPythonで実装し、画面を映すアプリで遊ぶ
今回使用する液晶はドライバが無いため、デバイスは簡単に使用できません。
フルカラー液晶の液晶コントローラILI9328はRaspberry piやArduinoで使用する例をweb上で多く見つけることができます。しかし、そのほとんどはAdafruit Industries社の液晶ドライバライブラリを利用しています。このライブラリはボードの差異まで抽象化しているため、Raspberry pi以外のボードでは使用できませんでした。いわゆるBSPライクになってます。移植する手も考えましたが、中身を読んでいるうちに、新規で作ったほうが早そうという結論に至りました。
そのため、液晶に画面を表示するためには以下の手順が必要になります。
- 液晶LSI(ILI9328)のドライバをPythonで実装する
- Pillowライブラリで画面用の画像オブジェクトを生成する
- 画像オブジェクトをSPIで送れるデータ配列に変換する
- ILI9328のグラフィックRAMに転送する
動画やアニメーションの様な表示の仕方をするなら上記の方法はおそらく遅くて無理だと思いますが、ただ映すだけならこれが楽にできそうです。
今回使用する環境です。
開発PC | Ubuntu20.04 |
開発ツール | Xilinx社 Vivado・Vitis v2022 |
ターゲットボード | Digilent社製 Zybo z7-20 SoC:Xilinx社 Zynq7020 armv7hf (Cortex-A9×2、666MHz) SDRAM:DDR3 1GB u-boot:v2022.01 Linux Kernel: v5.15.0 RootFs:Ubuntu22.04 jammy armv7l |
SPIデバイス | 2.2インチ320×240フルカラー液晶 液晶コントローラ:ILI9328 aitendoモジュールキット |
ロジックアナライザ | Zeroplus社 LAP-C 16064 USB接続タイプ |
3.PSのSPIを有効化
VivadoでSPIを設定していきます。プロジェクトは以前のLinux構築で作成したものを改変します。今回はデフォルトでSPIが無効化されているので、有効にしていきます。
3-1. VivadoでZynq PS SPIの有効化
IP INTEGRATOR>Open Block Designでブロック図を開きます。PSブロック(Processing System)をダブルクリックし、設定を開きます。
Peripheral I/O Pinsを開き、SPI1を探し、チェックをつけ有効にします。
SPI端子はZyboz7ボードのJCコネクタに出す予定なので、PL(FPGA)側に配線を出します。PL側に出すためにEMIO(ExtendedMIO)に接続します。
続いてクロック周りを確認しておきます。Clock Configurationを開きます。
SPIクロック周波数はデバイスドライバから可変可能なので、大元の供給クロック、つまり通信のMAX周波数がここで決まるようです。デフォルトの166MHzのままでおそらく大丈夫だと思います。液晶デバイスと通信するバスクロックは数MHz程度に分周して使用します。
ちなみに今回使用している、Zynq7000 SoCのEMIO信号線の許容周波数は25MHzまでです。
3-2. SPI信号線と端子の紐づけ
EMIO側に出した信号線をJCコネクタに接続していきます。PSブロックのSPI_1ポートが出現するので、まずはSPI_1の右の”+”印を押して展開します。(注意:I2C_1がいつの間にか増えていますが、気にしないでください。別件で使ってます)
この中から必要な信号線を右クリックし、”Make External”を押して、外部信号を引き出します。信号名の末尾”_I”はinput、”_O”はoutput、”_T”はトライステート出力と思われます。今回はSPIマスタのみ使用するので、SCLK_O、MOSI_O、MISO_I、SS_Oの4線を引き出します。
信号名を”SPI_1_信号名”に変えます。分かればなんでも良いです。ちなみにLinux上でQSPIが”SPI0″のデバイス名を使用している場合があるので、重複しないようにしておきます。
ここで保存します。”Ctrl+S”。グルグルマークが出て少し時間かかります。
次にプロジェクトの上位階層(wrapper)を再生成します。design_1.bdを右クリックし、”Create HDL wrapper”を実行します。optionsはデフォルトでOKしてください。グルグルマーク出て少し時間かかります。
design_1_wrapperをダブルクリックし、verilog(.v)ファイルを開きます。
SPIの新しい信号が4本追加されています。この信号名をメモしておきます。
最後に制約ファイル(.xdc)でSPIの信号とSoCの端子を紐づけます。制約ファイルはzyboz7ボードメーカのDigilentが配布しているものをベースに使用しています。
今回はJCコネクタに割り当てます。
- jc[0] -> SPI1_SS
- jc[1] -> SPI1_MOSI
- jc[2] -> SPI1_MISO
- jc[3] -> SPI1_SCLK
端子配置は自由ですが、Pmodコネクタという規格に合わせました。SPIのtype2タイプに従っています(ピン配置の参考)。ただし、結局ブレッドボードとジャンパ線で接続するのでそれほど重要ではないです。
これでVivadoの設定は終わりました。論理合成してBitstream生成をして、Vitisプラットフォーム情報をインポートしてFSBLをリビルドし直します。それができたらboot.binで再結合です。
4.Linuxカーネルへspidevを追加し再ビルド
4-1. py-spidevライブラリを使用するにはspidevドライバが必要
今回Pythonを使用するので、SPIインターフェースを使用するライブラリを調べておきます。webではラズパイのSPIを利用する情報が多く見つかり、py-spidevライブラリを使用するのが最も多い印象です。こちらを使ってみましょう。
py-spidev(PyPI,Github)は名前から想像つきますが、User mode SPIデバイスドライバである、spidevを内部で使用する上位レイヤインターフェースライブラリです。そのため、Linuxカーネルでspidevを使えるようにして、デバイスファイルを(/dev/spidev1.0)のように認識させる必要があります。今までのLinux構築手順ではspidevがカーネルに含まれていないので、コンフィグを編集して再ビルドします。
以下はなひたふ氏のブログを元に今回の構成をまとめました。わかりやすくて毎度お世話になっております。
4-2. LinuxカーネルMenuconfigとビルド
以前の記事を参考にMenuconfigの変更とビルドを行います。
Menuconfigで”Device Drivers > SPI support > User mode SPI device driver”をカーネルモジュールとして有効<M>にします。
カーネルのリビルドをします。make並列jobオプション(-j*)はCPUコア数+1程度にしています。
$ make clean
$ make -j5 ARCH=arm UIMAGE_LOADADDR=0x8000 uImage
5.デバイスツリー再生成
カーネルドライバが準備できたので、デバイスを認識してspidevデバイスファイルを自動で生成してもらうためのデバイスツリー記述をしていきます。
5-1. spidev有効化
デバイスツリー構築記事を参考にVivadoプラットフォームファイルから再生成します。
system-top.dtsにspiが追加されています。
個別ボード用はdtsiファイルでincludeするようにしていたので、そこにspidevドライバの追記をします。
なひたふ氏のブログで詳しく解説されていたので、そちらを参考に最後に追記しました。
// zybo z7 custom board by sh-goto / { model = "Zybo z7 custom board by sh-goto"; chosen { bootargs = "console=ttyPS0,115200 root=/dev/mmcblk0p2 rw earlyprintk rootfstype=ext4 rootwait devtmpfs.mount=1"; }; usb_phy0: phy0@e0002000 { compatible = "ulpi-phy"; #phy-cells = <0>; reg = <0xe0002000 0x1000>; view-port = <0x0170>; drv-vbus; }; }; &gem0 { status = "okay"; phy-mode = "rgmii-id"; phy-handle = <ðernet_phy>; ethernet_phy: ethernet-phy@0 { reg = <0>; device_type = "ethernet-phy"; }; }; &usb0 { status = "okay"; dr_mode = "host"; usb-phy = <&usb_phy0>; }; &i2c0 { rtc@32 { compatible = "rx8025"; reg = <0x32>; }; }; &spi1 { spidev@0 { compatible = "spidev"; spi-max-frequency = <20000000>; reg = <0>; }; };
“spidev@0″の0はCS信号の番号です。reg=<0>の値と一致させます。今回はCS信号は0番目の1つしか使用しません。
“compatible”はデバイスドライバの種別です。
“spi-max-frequency”はおそらくバスクロックの設定上限と思われます。
5-2. SPIの番号の指定(SPIx)
systtem-top.dtsにaliases {}で囲まれたブロックがあります。ここで、SPIの番号を指定すると、生成されるデバイスファイル名を指定できます。デフォルトでは
spi0 = &qspi;
spi1 = &spi1;
だったので、そのままにしました。ZynqでSPI0を使用する場合はデフォルトではQSPIと名前が重複するのでxsctで生成時に何らかの適当な番号が割り当てられます。
記述が終わったらDTCコンパイルします。
6.トラブル発生:デバイスファイルが生成されない
boot.bin、uImage、devicetree.dtbが用意できたので、SDカードのBootパーティションに上書きして起動します。
6-1. /dev/spidev*が無い
起動後に/dev以下にspidev1.0のデバイスファイルがあればSPIが使用できるようになります。1.0の1はSPI1の1です。0はCS信号の0番目を指します。
しかし、生成されませんでした。原因を調査します。
6-2. dmesgの確認
$ dmesg | grep spi*
dmesgでspidevのメッセージを確認します。ヒット無しでした。
6-3. Sysfs周りの調査
デバイスファイル(/dev以下)はsysfs(/sys以下)の情報を元にudev(userspace device management)デーモンがデバイスファイルを生成してくれる機能があります。
zynq@zyboz7:~$ ls -l /sys/devices/soc0/axi |grep spi
drwxr-xr-x 4 root root 0 Apr 8 2022 e0007000.spi
drwxr-xr-x 4 root root 0 Apr 8 2022 e000d000.spi
e0007000.spiはSPI1のIPの先頭アドレスです。e000d000.spiはQuadSPIで、SPI0は有効化していないので表示されないはずです。SPI1は一応認識しているようです。
更に調べます。
$ cat /sys/devices/soc0/axi/e0007000.spi/of_node/compatible
zynq@zyboz7:~$ cat /sys/devices/soc0/axi/e0007000.spi/of_node/compatible
xlnx,zynq-spi-r1p6
spi1自体のドライバは”xlnx,zynq-spi-r1p6″が適用されていました。コンパイルしたデバイスツリーをデコンパイルしたSPI1の結合した記述を以下に示します。”xlnx,zynq-spi-r1p6″はデバイスツリーでSPI1のIP自体のドライバとして指定しています。ここも正しいようです。
spi@e0007000 { compatible = "xlnx,zynq-spi-r1p6"; reg = <0xe0007000 0x1000>; status = "okay"; interrupt-parent = <0x04>; interrupts = <0x00 0x31 0x04>; clocks = <0x01 0x1a 0x01 0x23>; clock-names = "ref_clk\0pclk"; #address-cells = <0x01>; #size-cells = <0x00>; is-decoded-cs = <0x00>; num-cs = <0x03>; spidev@0 { compatible = "spidev"; spi-max-frequency = <0x1312d00>; reg = <0x00>; }; };
“xlnx,zynq-spi-r1p6″はLinuxドライバのソースコードを確認すると、Cadence社の SPIドライバになっています。ちなみにZynq7000のSPIはCadenceのIPコアが使われているらしいです。このSPIドライバはspidevの下位ドライバにあたります。sysfs上では正しく認識しているようです。
続いてof_nodeの階層に”spidev@0″のフォルダがあったので、こちらも見てみます。
zynq@zyboz7:~$ cat /sys/devices/soc0/axi/e0007000.spi/of_node/spidev\@0/compatible
spidev
spidevとありました。デバイスツリーは一応正しく読み込めているようです。
6-4. udev周りの調査
udevはあまり仕組みを知らないのでまずは調べました。/sys/class以下にデバイス情報のフォルダを作成し、その内部のueventファイルに’add’と書き込まれるとudevデーモンにトリガがかかり、sysfs情報を解析してデバイスファイルが作られるようです。
/sys/class以下を調べます。/sys/class/spidevのフォルダは作られていました。しかし、中身が空です。本当はこの中にデバイス情報が入っているはずです。結論から言うとこれが原因でudevがうまく働かず、デバイスファイルが作られません。
6-5. /sys/class/spidevは誰が作るのか
こちらのQiitaの記事によると、デバイスドライバ内でデバイス初期化時にdevice_create()関数を呼び、/sys/class/spidevの中身を登録するようです。
何らかの原因でデバイスドライバspidev.cが登録に失敗したようです。
6-6. spidevソースコード調査
spidev.cのソースコードを確認していきます。頻繁にメンテナンスされているようです。ただし、力量不足で理解ができませんでした。
6-7. 原因判明:”spidev”をcompatible名にするのは非推奨
webで数時間ほど調査をしていたら、似た事例を見つけました。比較的新しいVerのLinuxカーネルで/dev/spidev*.*が生成されないことが起こるようです。デバイスツリーのcompatible = “spidev”指定で同様のことが起こったそうです。解決方法は
変更前-:compatible = “spidev”;
変更後+:compatible = “rohm,dh2228fv”;
だそうです。spidevのmasterソースコードを抜粋すると、700行目付近に
/* * spidev should never be referenced in DT without a specific compatible string, * it is a Linux implementation thing rather than a description of the hardware. */ static int spidev_of_check(struct device *dev) { if (device_property_match_string(dev, "compatible", "spidev") < 0) return 0; dev_err(dev, "spidev listed directly in DT is not supported\n"); return -EINVAL; } static const struct of_device_id spidev_dt_ids[] = { { .compatible = "cisco,spi-petra", .data = &spidev_of_check }, { .compatible = "dh,dhcom-board", .data = &spidev_of_check }, { .compatible = "lineartechnology,ltc2488", .data = &spidev_of_check }, { .compatible = "lwn,bk4", .data = &spidev_of_check }, { .compatible = "menlo,m53cpld", .data = &spidev_of_check }, { .compatible = "micron,spi-authenta", .data = &spidev_of_check }, { .compatible = "rohm,dh2228fv", .data = &spidev_of_check }, { .compatible = "semtech,sx1301", .data = &spidev_of_check }, { .compatible = "silabs,em3581", .data = &spidev_of_check }, { .compatible = "silabs,si3210", .data = &spidev_of_check }, {}, }; MODULE_DEVICE_TABLE(of, spidev_dt_ids);
デバイツリーのcompatible名を保持するテーブルがあります。compatibleをデバイスツリーで指定するとデバイスドライバがそれを読み取り、probeしてくれます。
そこに”spidev”は無いようです。spidev_of_check()で”spidev”を判別していそうですが、ネストが深かったので内部処理は把握できていません。
コード抜粋のコメント文で、
spidev should never be referenced in DT without a specific compatible string, it is a Linux implementation thing rather than a description of the hardware.
https://github.com/Xilinx/linux-xlnx/blob/master/drivers/spi/spidev.c#L698
(spidevは、特定の互換性のある文字列なしでDTで参照されるべきではありません、それはハードウェアの説明ではなく、Linuxの実装に関するものです。)機械翻訳
DTはDeviceTreeを指していると思いますが、compatible = “spidev”;の使用は非推奨ということでしょうか…よくわかりません。私が使用しているkernel=5.15.0ではこのコメント文はまだ入っていないです。
さらに調べるとMicrochip社のSAMのドキュメントに説明がありました(2.1デバイスツリー)。
“spidev”はlinuxの実装であって、デバイス名ではないのでなんらかのデバイス名をつけることを推奨しているようです。ドライバソースspidev.cを編集して新しいデバイス名を入れる対策をしていますが、管理しにくそうですね…。抽象レイヤのドライバにデバイス名を入れると無限にテーブルが増えそうですが、意図した結果なんでしょうか…
6-8. 原因対策:デバイスツリーを修正
使用しているLinuxカーネル=5.15.0のspidevを覗いてみて、対応していそうなcompatible = “rohm,dh2228fv”;に変えました。
dh2228fvはロームセミコンダクタ社のSPI接続のADCチップです。違和感がありますがこれにしてみます。
// zybo z7 custom board by sh-goto / { model = "Zybo z7 custom board by sh-goto"; chosen { bootargs = "console=ttyPS0,115200 root=/dev/mmcblk0p2 rw earlyprintk rootfstype=ext4 rootwait devtmpfs.mount=1"; }; usb_phy0: phy0@e0002000 { compatible = "ulpi-phy"; #phy-cells = <0>; reg = <0xe0002000 0x1000>; view-port = <0x0170>; drv-vbus; }; }; &gem0 { status = "okay"; phy-mode = "rgmii-id"; phy-handle = <ðernet_phy>; ethernet_phy: ethernet-phy@0 { reg = <0>; device_type = "ethernet-phy"; }; }; &usb0 { status = "okay"; dr_mode = "host"; usb-phy = <&usb_phy0>; }; &i2c0 { rtc@32 { compatible = "rx8025"; reg = <0x32>; }; }; &spi1 { spidev@0 { //compatible = "spidev"; //deprecated compatible = "rohm,dh2228fv"; spi-max-frequency = <20000000>; reg = <0>; }; };
改めて起動すると、無事/dev/spi1.0が出現しました。
7.ロジックアナライザによる動作確認
7-1. ループバック配線による動作確認
MOSIとMISOを直結してループバックで動作を確認します。
PythonをREPLモードで起動し、配列を送受信してみます。無事できているようです。
$ ls /dev |grep spi*
spidev1.0
$ sudo python3
>>> import spidev
>>> spi = spidev.SpiDev()
>>> bus = 1
>>> device = 0
>>> spi.open(bus, device)
>>> to_send = [0x01, 0x02, 0x03] #data list
>>> recv = spi.xfer2(to_send) #send data and recive
>>> print(recv)
>>> [1, 2, 3]
>>>
7-2. ループバック通信を観測
リスト[0x01, 0x02, 0x03, 0x0a, 0x0b, 0x0c]を送信したときのロジアナ計測結果です。問題なくできていそうです。
最後の転送データ送信完了から、CS(黄)がhighに戻るまで72[usec]とちょっと長めなのが気になりました。なお、クロックは5MHzです。
8.【補足】DockerコンテナからSPIを使う
DockerコンテナからSPIを使うにはデバイスファイルをコンテナ側に共有する必要があります。docker run時に--device引数を設定します。
デバイスファイル名”spidevX.Y”のXはSPIのIPの番号です。SPIポートが複数ある場合はこれが変わります。YはCS信号線(ChipSelect)の番号です。
イメージをdebian/sample、デバイスファイルをspidev1.0とすると以下のような感じです。
$ docker run --device=/dev/spidev1.0:/dev/spidev1.0 --name "py-tft-app" -it "debian/sample" /bin/bash