FPGA

【ZynqMP】1. 認定UbuntuでPLのGPIOを使う

1.記事一覧

記事は複数回に分けて投稿する予定です。AMD/XilinxのARM64 SoC(ZynqMP Kria K26 SOM)で動作する、AMD/Xilinx公式認定Ubuntuやベアメタル開発を勉強していく覚書きです。

Zynq7000(armv7)版はこちら

2.【概要】KR260ボードでPL側のGPIOを使いたい

今回、Xilinxの評価ボードKR260を入手できたので、初めてのZynqMPの開発にチャレンジしたいと思います。

2-1. KR260ボード+認定Ubuntu22.04開発事始め

本記事でやること:
KR260評価ボードと公式認定Ubuntuを使い、以下の流れでやっていきます。

  • PL(FPGA)上にAXI-GPIO IPを実装するためのBitstreamを作成
  • Devicetree Overlayソース作成とコンパイル
  • Linux起動
  • BitstreamをDevicetree Overlayを利用して書込み
  • ユーザアプリから、SysfsでGPIO入出力(オンボードLED、Pmod接続スイッチ)
  • ユーザアプリから、SysfsでGPIOスイッチ入力ピン変化通知(≒割込み)

※ユーザ用オンボードLEDは矢印のところに2個あります。

Kria SOMと従来のZynqMPボードの違い:

  • ブートローダのBOOT.binにPLのBitstream.bitを含まない
  • Linux起動後にPLをアプリから動的書換えできる
  • Linux起動中に書換えるため、Devicetree Overlayを使う
  • ラズパイComputeModuleのように、ZynqMPSoC、DDR、PMIC配線が実装済

今回自分が初めてやること:

  • 初ZynqMP (Zynq7は少し触った程度)
  • Kria K26 SOM・KR260の回路図読解、制約条件ファイルの設定
  • Devicetree Overlayの扱い
  • AXI-GPIO IPでピン入力変化通知(≒割込み)

2-2. KR260ボードとは

Xilinx社公式で販売しているKria K26 SOMボードベースのロボティクス向け評価ボードです。

https://japan.xilinx.com/products/som/kria/kr260-robotics-starter-kit.html

Kria K26 SOMは今までのZynqMPSoCのチップ単位での供給ではなく、設計難易度の高い、DDR4SDRAM I/F、電源回路(PMIC)、Flashなどの配線をあらかじめ基板上に既に実装し、メザニンコネクタなどで、周辺I/Oを取り出せるようになっています。これをSOM(System On Module)と呼ぶそうです。Raspberry Pi Compute ModuleもSOMで、産業用途で普及し始めてます。ユーザはSOMが載るカスタムキャリアボードを設計すれば良いだけなのでハードルが下がります。

KR260ボードはK26 SOMを載せた評価ボードの1つで、ロボティクス向けです。ROS2をサポートしているようです。KR260ボードよりも以前にKV260ボードが発売されましたが、こちらはAIビジョン向けの評価ボードになっています。

購入先ですが、私はDigikeyで個人購入しました。最終用途証明(米国EAR?)周りが面倒そうだなと思っていたら、注文時に英語で用途を記入するだけらしいことを知り、初めてDigikeyを利用してみました。(実際何の証明にもなってませんが、特に追求されませんでした。まあ個数1ですし脅威は無いと判断されたのでしょう)

円安の影響もあってか、送料込¥67,000ほどかかりました(2023/6)。結構高いです。正直個人趣味で購入するのは金銭的に結構しんどいです。今年で14年目になる骨董品UbuntuデスクトップPCを新調したかったのですが、後2年頑張ってもらいましょう。

2-3. ドキュメント類

公式Wiki
XilinxはWikiとKriaSOM専用ページがあるので、情報はそこから探すことになります。基本的にここに書いてあることをなぞっていく記事内容になります。

公式ドキュメント
データシートやマニュアル類はここで検索できます。ちなみに回路図のようなAMDアカウント認証が必要なものは検索に出てきません(今のところは)。

認定Ubuntuセットアップ
本記事はセットアップ済みを前提としています。Webに情報が豊富なため、内容は説明しません。参考リンクを貼っておきます。xmutilツールも使用するのでセットアップしておいてください。

キャリアボード回路図
回路図はAMDアカウント無料登録しないとダウンロードできません。また、Kria K26 SOMボードの回路図は現在非公開です。制約ファイルの書き方は後述しますが、公開されているSOM制約ファイルがあるので、それを使用します。

  • KR260キャリアボード回路図 ここからBoard Filesをチェックして以下を探す
    XTP743 – Kria KR260 Starter Kit Carrier Card Schematics (v1.0)
  • K26 SOM制約ファイル ここから資料タブ>>Board Filesをチェックして以下を探す
     XTP685 – Kria K26 SOM XDC File (v1.0)

3.前提条件

作業を始める前に、前提条件を示します。既に準備を終えていることが条件となります。

  • KR260ボードのQSPI Firmwareを下記のVer以上に更新しておく
  • SDカードに認定Ubuntuを入れてセットアップ済みとする
  • 開発PCとSSH接続と(できれば)VSCode Remote Developmentで接続の準備済みとする
  • Vivado・Vitis開発環境を使える状態であること
  • 認定Ubuntuはデスクトップ版ですが、GUIを使用せず、端末で操作する
開発PC Ubuntu20.04 LTS
VSCode : Remote-Development(あると便利)
開発ツールXilinx社 Vivado・Vitis v2023.1
Xilinx IPVivadoにて以下を主に使用
・AXI-GPIO
ターゲットボードXilinx社製 Kria KR260 ロボティクススターターキット(SK-KR260-G)
SOM : Xilinx社 Kria K26 SOM (Zynq UltraScale+ MPSoCベース)
   APU : Arm CA53x4 1333MHz arm64
   RPU : Arm CR5x2 533MHz armv7-R
SDRAM : DDR4 4GB
QSPI Firmware : Xilinx download – 2022.2_update1_BOOT.BIN
Linux Kernel : v5.15.0-1023-xilinx-zynqmp by 認定Ubuntu
OS : Xilinx認定Ubuntu22.04 LTS(arm64)
GPIO向け回路部品・ブレッドボード
・ジャンパ線
・10k抵抗2点
・プッシュスイッチ2個

4.VivadoでPL向けbitstream.binを作成

4-1. プロジェクト作成と.bin出力の設定

注意:Vivadoの詳しい操作方法は説明しません。要点だけ載せます。

プロジェクトを作成します。→プロジェクト名demo_kr260_axi_gpio

Project Typeでは”Project is an extensible Vitis platform”にチェックを入れます。今回に限ってはチェックは必要ないのですが、いずれVitisで本格的な開発をする場合、必要になると思われますので、チェックを入れておきます。

パーツ選択画面では以下のようにします。

  1. Boardsタブに切替え
  2. Refreshを押して最新ボードファイルをインポート
  3. Searchに”kria”と入力してフィルターをかける
  4. KR260ボードのConnections設定を押す

SOMボードの240pinコネクタの配線を設定します。これをやるとプロジェクトでキャリアボードの設定が自動的に有効になります。

プロジェクトが作成できたら、bitstreamのbinファイルを作成するように設定します。

PROJECT MANAGER>Settings>Project Settings>Bitstream>”-bin_file”にチェック

※古いVivadoでこの設定がない場合、bootgenコマンドを使用してbit-bin変換するか、bit対応のFPGA-Region対応カーネルを使用します。

基本的にFPGAコンフィグレーション用bitstreamは”.bit”のファイルであるのですが、今回使うDevicetree Overlay用のLinuxの機能である、FPGA-Regionでは直接扱うことができません。最新のカーネルでは.bitを内部で.binに変換することで.bitでも扱うことができるらしいですが、コンフィグレーションに時間がかかるデメリットがあるそうです。

Webで調べると、bootgenコマンドで変換する方法が見つかりますが、最近のVivadoには直接.binを出力できるオプションがあるみたいなので、有効にします。

4-2. IPインテグレータで冷却ファンの制御信号を設定

ポイント:記事と無関係ですが、ファンが全開になってうるさので対策をします。

AXI-GPIOの設定の前に下準備として先にFAN制御信号を配線します。認定Ubuntu22.04では、FANの温度制御サービスが動作していて、FANの回転数をかなりアクティブに制御します。FANへの制御信号がPLを経由してI/Oとして出ているので、制御信号を配線しないと、PLを書き換えたときにFANがフル回転でうるさくなります。

詳しくは先人の方の解説がめちゃくちゃ詳しいです。

ikwzmさんは毎度高度な記事を執筆されています。現状私の知識ではほとんど理解が追いつかないですが、いつか理解できるようになりたいです。それにしてもファンコンでここまで語れるのすごいですね…

さて、IPインテグレータでBlock Designを作成します。名前は任意で良いです。
→Block Design名:demo_kr260_axi_gpio

ブロックは以下のようにします。

PSブロックのTTCタイマの設定は以下のようにします。

Sliceブロックは以下のようにします。2bit目を取り出します。

4-3. AXI-GPIOを追加する

以下のようにAXI GPIOブロックを追加し、IPの設定をします。GPIOの配線は以下のようにします。

  • GPIO:キャリアボードのユーザLED 2個
  • GPIO2:Pmod1コネクタに接続のプッシュスイッチ2個

最初にBoardタブを選択します。ユーザLEDはキャリアボード情報のプリセットがあるのでそれを選びます。スイッチに関してはPmod1のプリセットがあるのですが、接続pin番号をこちらで選ぶのでCustomにします。(※GPIO2が出てこない場合はIP ConfigurationタブでEnable Dual Channelを選択)

続いてIP Configurationタブを選択します。GPIOはユーザLEDのプリセットで自動入力され固定されます(灰色の枠)。スイッチはオレンジの枠の部分で設定します。Pmod1の端子配線は次の工程で行います。

自動配線します。Processor System ResetとAXI Inter connectブロックは自動配線で現れます。

手動で設定・接続するのは以下3つの信号(オレンジ)です。

  • UFx_LED外部信号2bit ー>オンボードLEDを2つ使用します。端子名のみ改名
  • PUSH_SW外部信号2bit ー>Pmodコネクタに押しボタンスイッチ2つ接続します。
  • ip2intc_irpt割込み信号線 ー>スイッチ押下による、ピン入力通知に使用します。

4-4. Platform設定

次にハードウェア設定をエクスポートするための設定を行います。Vitisでハードウェアリソースを扱うための設定ですが、今回使用しないので、最小限の設定を行います。

AXI-Portは最低1ポート選択すれば良いようです。今回使用していないHPM1_FPDを選択します。

つづいてClockです。最低1クロックソースを選択します。ただし、リセットIPが接続されている必要があります。

最後にPlatform名です。今回は以下のようにしました。

Platform Name:demo_kr260_axi_gpio

4-5. 端子制約条件の設定

IPインテグレータによる配線は終了したので、論理合成(Run Synthesis)を実行します。

終了ダイアログが出たら、Open Synthesized Designを開きます(左のFlow Navigatorからも可)。

以下のように設定します。端子の決め方はあとで説明します。

  • PUSH_SW_tri_i[1] : PackagePin=E10, I/OStd=LVCMOS33 <ーPmodスイッチ2に接続
  • PUSH_SW_tri_i[0] : PackagePin=H12, I/OStd=LVCMOS33 <ーPmodスイッチ1に接続
  • UFx_LED_tri_o[1] : PackagePin=E8, I/OStd=LVCMOS18 <ーUF2 LED(設定不要)
  • UFx_LED_tri_o[0] : PackagePin=F8, I/OStd=LVCMOS18 <ーUF1 LED(設定不要)
  • FAN_EN[0] : PackagePin=A12, I/OStd=LVCMOS33 <ー冷却ファンに接続

Ctrl+Sで保存すると、制約ファイルを保存するダイアログが出るので、適当な名前で.xdcファイルを保存します。

4-6. K26 SOM制約条件のための回路図の見方

制約条件の決め方の説明です。長いので次の手順へ進みたい方は5-7. Bitstream生成とハードウェアエクスポートへ進んでください。

VivadoではK26SoCチップの端子レベルで制約条件を記述します。

キャリアボードの回路図→SOMの回路図→K26 SoC端子の順で追えばわかるはずですが、現状SOMの回路図が公開されておらず、SOMの制約条件(.xdc)ファイルが公開されているようです。

詳しくは先人の方の説明を見てください。おかげさまで存在無きSOMの回路図を探す旅から帰らぬ人になるところでした。助かります。

KV260でSystemVerilogでLEDチカしてみる>LED 出力の配線を調べる – Ryuz88様

Vivado 2023.1を使えばSOM I/F端子指定で制約条件記載できるのかなと期待したのですが、私はやり方がわかりません(それかまだ機能が未実装)。

そのため、上記記事を参考にしながらやってみます。

オンボードユーザLED

ユーザLEDはプリセットで自動設定されてますが、customで設定する場合を想定して説明します。

KR260キャリアボード上には”UF1″、”UF2″とシルクで書かれたチップLEDが2個実装されています。

LED回路図は以下の様になっています。

正論理で以下の対応になります。

  • UF1 LED = HPA14P
  • UF2 LED = HPA14N

各線はSOM240_1コネクタに接続されています(SOM240_2もあるので注意)。

コネクタ端子を特定できれば、キャリアボード側はとりあえずOKです。

  • UF1 LED = HPA14P = SOM240_1_D13
  • UF2 LED = HPA14N = SOM240_1_D14

続いてK26 SOMの制約条件ファイルを見ると、K26 SoCの端子がわかります。

TCL
set_property PACKAGE_PIN E8       [get_ports "som240_1_d14"] ;# Bank  66 VCCO - som240_1_d1 - IO_L17N_T2U_N9_AD10N_66
set_property PACKAGE_PIN F8       [get_ports "som240_1_d13"] ;# Bank  66 VCCO - som240_1_d1 - IO_L17P_T2U_N8_AD10P_66
  • UF1 LED = HPA14P = SOM240_1_D13 = F8 I/OStd=LVCMOS18 Drive Strength=12mA
  • UF2 LED = HPA14N = SOM240_1_D14 = E8 I/OStd=LVCMOS18 Drive Strength=12mA

上の制約ファイルのコメントに端子の所属バンク”66″とバンク電源端子”som240_1_d1″とあります。上のSOMコネクタの回路図では”PL_1V8″つまり電源1.8Vなので、両端子とも”LVCMOS18(シングルエンド)”になります。

許容電流(Drive Strength)の決め方ですが、あくまで回路屋ではない素人の意見として聞いてください。この場合は、MAXの12mAにします。FETのゲートドライブなので、基本的に電流は流れません。しかし、ゲート容量分の充放電が発生します。その電流ピーク値が許容電流値を下回る様に設定をします。また、高速スイッチングしないので、許容電流はMAXで大丈夫だと思います。間違っていたらどなたか教えてください。

Pmodプッシュスイッチ

今回はPmod1の端子にスイッチを2つ接続させることにします。

オンボードユーザLEDと同様に追いかけると、以下のようになります。

  • PMOD1_IO1_HDA11 = SOM240_1_A17 = H12  I/OStd = LVCMOS33
  • PMOD1_IO1_HDA12 = SOM240_1_D20 = E10  I/OStd = LVCMOS33

ブレッドボード配線は以下のようになります。

冷却ファン

冷却ファンはローサイドスイッチングの回路です。FET(Q20)がもう一個追加されて論理反転で負論理になっています。制御信号が切断したときにファンが全開で回るようフェイルセーフしてあるようです。

オンボードユーザLEDと同様に追いかけると、以下のようになります。

  • HDA20 = A12 I/OStd = LVCMOS33 Drive Strength = 16mA

許容電流もLEDと同じ理屈でMAXにしておきます。

4-7. Bitstream生成とハードウェアエクスポート

制約条件を記述できたのでGenerate Bitstreamでbinを生成します。以下に出てきます。
demo_kr260_axi_gpio.runs/impl_1/demo_kr260_axi_gpio_wrapper.bin

次にVitis向けのハードウェアエクスポートと同じ要領で.xsaを生成します。Vitisは使用しませんが、XSCTツールで使用します。

IP INTEGRATOR>Export Platformでエクスポートします。注意点として、include bitstreamを選択します。

XSA file name:demo_kr260_axi_gpio

出力フォルダ:任意

5.Devicetree Overlay の生成とビルド

Devicetreeとは

デバイス構成(どのバスにどのデバイスが接続など)、デバイス定数(レジスタの物理アドレス、割込み番号、GPIObit幅など)を記述したデータ定義ファイルです。これをDTS(DeviceTreeSource)と呼びます。デバイスツリーコンパイラでコンパイルして、DTB(DeviceTreeBlob)をLinuxに組込みます。

Kernel v3.xxあたりで、Arm Linux向けに導入されました。x86_64はUEFI(ACPI)なのでDeviceTreeはありません。

現在、Arm Linuxドライバはデバイスツリーとペアで開発されます。AXI-GPIOドライバをmodprobeするときにデバイスツリーからパラメータを読み出して初期化するので、今回はデバイスツリーを用意する必要があります。

Devicetree Overlayとは

現在のFPGA搭載SoCは起動中に動的にFPGAの内容を書き換える、動的再構成が可能です。これはすなわちLinuxから見て起動中にデバイス構成が変更されるということです。そこでデバイスツリーを動的再構成出来る技術が登場しました。それがDevicetree Overlayです。Linuxカーネルの機能のFPGA-RegionでFPGA書換とOverlay、そしてデバイスドライバのmodprobeを実行できるようにしています。

5-1. DTC(Device Tree Compiler)の準備

デバイスツリー向けのコンパイラをインストールします。aptだと簡単にインストールできますが、若干古いです。最新のものを使用する場合ソースビルドします(推奨)。

ShellScript
# ソースビルドする場合
$ git clone https://git.kernel.org/pub/scm/utils/dtc/dtc.git --depth=1
$ cd dtc
$ make
$ sudo cp ./dtc /usr/local/bin/
$ dtc -v
Version: DTC 1.6.1

# apt で入れる場合(Ubuntu20.04 LTS)
$ sudo apt install device-tree-compiler
$ dtc -v
Version: DTC 1.5.0

5-2. XSCTによるDevicetree Overlayソースの出力

Vitisをインストール済みであれば、XSCTでデバイスツリーを生成できます。

createdtsコマンドを使うと以下のこと(など)が出来ます。

  • DTG(Device Tree Generator)のclone
  • Devicetree及びOverlayソースコードの生成
  • DevicetreeソースのDTCコンパイル
  • 注意:PL用DevicetreeOverlayソースのコンパイルは不可

このやり方は以下の記事が参考になりました。

Qiita : KRIA XILINXのSOMボードのVitisプラットフォームの作り方(2022.1 版)|@basaro_k(basaro)

ShellScript
# Vitis環境変数設定
$ source /tools/Xilinx/Vitis/2023.1/settings64.sh
# workspase
$ tree
.
└── demo_kr260_axi_gpio.xsa

#XSCT起動
$ xsct
xsct% createdts -hw demo_kr260_axi_gpio.xsa -platform-name demo_kr260_axi_gpio -git-branch xlnx_rel_v2023.1 -overlay -compile -out my_dtso
xsct% exit

#以下に出力
$ ls my_dtso/my_dtso/demo_kr260_axi_gpio/psu_cortexa53_0/device_tree_domain/bsp/
Makefile  pcw.dtsi        pl-final.dts  system-top.dts  system.dts  zynqmp-clk-ccf.dtsi
include   pl-custom.dtsi  pl.dtsi       system.dtb      system.mss  zynqmp.dtsi

createdtsコマンドのオプションは以下のようにしています。

  • -hw:Vivadoでエクスポートした.xsa
  • -platform-name:Vivadoで設定したPlatform名
  • -git-branch:DTGのブランチ指定。デフォルトはxlnx_rel_<VitisのVer>なので、開発環境のVerに一致する。迷ったら省略する。ちなみにGithubのDTGはmasterブランチが古いので、注意する
  • -overlay:Devicetree Overlayソース出力
  • -compile:Devicetreeソースをコンパイルする。XilinxのDTSは#inclue周りをgccでプリコンパイルしてからDTCコンパイルする必要があるため手動より楽。ただし今回使うPL側のDevicetree Overlayソースのコンパイルはしてくれないので、手動でやる。
  • -out:出力ファイルのフォルダーを指定

5-3. Devicetree Overlayソースを見てみる

pl.dtsiがPL側のデバイスツリーソースになっています。dtsiですが、中身はOverlay記述になっていますので、このままコンパイル可能です。

一見Overlay記述と異なる様に思えますが、Overlay記述の新しいシンタックスシュガー構文のようです。

また、ロードするPLのbinファイル名も既に書かれているので、このファイル名に一致するようにVivadoで作成したbinはあとでリネームします。デフォルトだと”〇〇_wrapper.bin”になっています。

/*
 * CAUTION: This file is automatically generated by Xilinx.
 * Version: XSCT 2023.1
 * Today is: Wed Sep 13 10:16:30 2023
 */


/dts-v1/;
/plugin/;
&fpga_full {
	firmware-name = "demo_kr260_axi_gpio.bit.bin";
	pid = <0x0>;
	resets = <&zynqmp_reset 116>;
	uid = <0x0>;
};
&amba {
	#address-cells = <2>;
	#size-cells = <2>;
	afi0: afi0 {
		compatible = "xlnx,afi-fpga";
		config-afi = < 0 0>, <1 0>, <2 0>, <3 0>, <4 0>, <5 0>, <6 0>, <7 0>, <8 0>, <9 0>, <10 0>, <11 0>, <12 0>, <13 0>, <14 0xa00>, <15 0x000>;
	};
	clocking0: clocking0 {
		#clock-cells = <0>;
		assigned-clock-rates = <99999001>;
		assigned-clocks = <&zynqmp_clk 71>;
		clock-output-names = "fabric_clk";
		clocks = <&zynqmp_clk 71>;
		compatible = "xlnx,fclk";
	};
	clocking1: clocking1 {
		#clock-cells = <0>;
		assigned-clock-rates = <99999001>;
		assigned-clocks = <&zynqmp_clk 72>;
		clock-output-names = "fabric_clk";
		clocks = <&zynqmp_clk 72>;
		compatible = "xlnx,fclk";
	};
	axi_gpio_0: gpio@a0000000 {
		#gpio-cells = <2>;
		#interrupt-cells = <2>;
		clock-names = "s_axi_aclk";
		clocks = <&zynqmp_clk 71>;
		compatible = "xlnx,axi-gpio-2.0", "xlnx,xps-gpio-1.00.a";
		gpio-controller ;
		interrupt-controller ;
		interrupt-names = "ip2intc_irpt";
		interrupt-parent = <&gic>;
		interrupts = <0 89 4>;
		reg = <0x0 0xa0000000 0x0 0x10000>;
		xlnx,all-inputs = <0x0>;
		xlnx,all-inputs-2 = <0x1>;
		xlnx,all-outputs = <0x1>;
		xlnx,all-outputs-2 = <0x0>;
		xlnx,dout-default = <0x00000000>;
		xlnx,dout-default-2 = <0x00000000>;
		xlnx,gpio-width = <0x2>;
		xlnx,gpio2-width = <0x2>;
		xlnx,interrupt-present = <0x1>;
		xlnx,is-dual = <0x1>;
		xlnx,tri-default = <0xFFFFFFFF>;
		xlnx,tri-default-2 = <0xFFFFFFFF>;
	};
};

5-4. Device treeコンパイル

pl.dtsiをDTCでコンパイルします。-@オプションをつけてシンボルも出力します。Warningが出ていますが、コンパイルは完了し、dtboは出力されます。

Warningの理由は調べたのですが、知識が足りずよくわかりませんでした。ただし、今回の記事の内容は問題なく動きます。ちなみにaptでinstall出来る古いDTC(1.5.0)ではWarning出ません。

ShellScript
$ dtc -@ -O dtb -o demo_kr260_axi_gpio.dtbo pl.dtsi
pl.dtsi:42.30-66.6: Warning (interrupt_provider): /fragment@2/__overlay__/gpio@a0000000: Missing #address-cells in interrupt provider
$ ls | grep dtbo
demo_kr260_axi_gpio.dtbo

5-5. おまけ:認定UbuntuにAXI-GPIOドライバは入ってる?

デバイスツリーは基本的にドライバとペアで必要になります。AXI-GPIOドライバの方も認定Ubuntuで使えるようにする必要があります。

ただし、結論を書くと認定Ubuntu22.04にはプリインストールされていますので特に準備することはありません。さすが認定Ubuntuですね。カーネルモジュールのビルド&インストール、またはLinuxカーネルのリビルドは今回は不要になります。

今後認定Ubuntu以外の環境に備えて自分で調べられるように予習してみます。カーネルコンフィグを見て、ドライバが有効になっていれば大丈夫そうです。

Xilinx WikiでAXI-GPIOドライバのコンフィグ名を調べます。以下の3つあれば良いようです。

  • CONFIG_GPIO_SYSFS=y
  • CONFIG_SYSFS=y
  • CONFIG_GPIO_XILINX=y (for axi_gpio)

KR260を起動し、SSHでログインしてカーネルコンフィグを確認します。

ShellScript
ubuntu@kria:~/$ zcat /proc/config.gz > kconfig.txt
ubuntu@kria:~/$ $ cat kconfig.txt |grep -e CONFIG_GPIO_SYSFS -e CONFIG_SYSFS -e CONFIG_GPIO_XILINX
# CONFIG_SYSFS_DEPRECATED is not set
CONFIG_SYSFS_SYSCALL=y
CONFIG_GPIO_SYSFS=y
CONFIG_GPIO_XILINX=y
CONFIG_SYSFS=y

すべて有効”=y”になっているのでドライバは大丈夫です。

ちなみに/proc/config.gzはカーネルコンフィグのCONFIG_IKCONFIGとCONFIG_IKCONFIG_PROCが有効な状態でビルドされていないと見れません。無い場合は/boot以下のフォルダから見つけます。

ShellScript
ubuntu@kria:~$ ls /boot |grep config-$(uname -r)
config-5.15.0-1023-xilinx-zynqmp

6.KR260でPL書込みを実行

6-1. bitstreamとデバイスツリーの転送

以下にbitstreamとデバイスツリーが揃いました。適当なフォルダに取り出してまとめておきます。

  • demo_kr260_axi_gpio/demo_kr260_axi_gpio.runs/impl_1/demo_kr260_axi_gpio_wrapper.bin
  • device-tree-xlnx/my_dtso/demo_kr260_axi_gpio.dtbo

binファイルの名前がデバイスツリーソースの内容と違うので修正します。

TCL
#デバイスツリーソースのfirmware-name ="hogehoge"の名前に改名
$ mv demo_kr260_axi_gpio_wrapper.bin demo_kr260_axi_gpio.bit.bin

修正したら、2ファイルをKR260の適当なフォルダに転送します。

【追記】SCPコマンドを用いた転送を用いても良いですが、もっと楽な方法があります。

VSCodeでRemote-SSH拡張機能でZynqMPに接続している方は、VSCodeエディタにドラッグ&ドロップでファイルを送ることが出来ます。ダウンロードしたい場合は右クリックでダウンロード出来ます。詳しくは以下のQiita記事が参考になります。

Remote-SSH使用時のダウンロード/アップロード:@matsujirushi
(Takashi MATSUOKA)

SCPコマンドの場合は以下の用にします。複数ファイルを一度に送ることも出来ます。

ShellScript
# 転送
$ scp ./demo_kr260_axi_gpio.bit.bin ./demo_kr260_axi_gpio.dtbo ubuntu@<KR260のIPアドレ>:~/hoge

6-2. xmutilによるPL書込み前準備

Linux上でPL書込みを行うにはFPGA-Region機能の仕組みを使う必要があります。しかし、認定Ubuntuにはxmutil(Xilinx-Wiki,Github)というFPGAコンフィグレーションを管理するツールが入っているので、今回はこれを使います。

/lib/firmware/xilinx以下にファームウェアフォルダを作成し、以下のファイルを配置します。

  • Vivadoで作成したPL bitstreamのbin形式ファイル
  • PL用Devicetree Overlay(.dtbo)
  • メタデータファイルshell.json

まず、ファームウェアフォルダを作成し、bitstreamとdtboを格納します。

ShellScript
ubuntu@kria:~$ ls /lib/firmware/xilinx/
k26-starter-kits   #<-起動直後にPLに書き込まれるデフォルトのやつ
#ファームウェアフォルダ作成
ubuntu@kria:~$ sudo mkdir /lib/firmware/xilinx/demo_kr260_axi_gpio
#bitstreamとデバイスツリー格納
ubuntu@kria:~$ sudo mv ./demo_kr260_axi_gpio.bit.bin /lib/firmware/xilinx/demo_kr260_axi_gpio/
ubuntu@kria:~$ sudo mv ./demo_kr260_axi_gpio.dtbo /lib/firmware/xilinx/demo_kr260_axi_gpio/

次にshell.jsonを用意します。dfx-mgrというxmutil内部で呼び出される機能のためのメタデータファイルです。下記のデフォルトのファームウェアからコピーします。

/lib/firmware/xilinx/k26-starter-kits/shell.json

ShellScript
ubuntu@kria:~$ sudo cp /lib/firmware/xilinx/k26-starter-kits/shell.json  /lib/firmware/xilinx/demo_kr260_axi_gpio/

ちなみにshell.jsonの中身は以下の内容です。

JSON
{
    "shell_type" : "XRT_FLAT",
    "num_slots": "1"
}

6-3. xmutilによるPL書込み実行

xmutil listappsで先程の準備が完了しているか確認します。認識されているようです。

ShellScript
ubuntu@kria:~$ sudo xmutil listapps
                       Accelerator          Accel_type                            Base           Base_type      #slots(PL+AIE)         Active_slot

                  k26-starter-kits            XRT_FLAT                k26-starter-kits            XRT_FLAT               (0+0)                  0,
               demo_kr260_axi_gpio            XRT_FLAT             demo_kr260_axi_gpio            XRT_FLAT               (0+0)                  -1
ubuntu@kria:~$ 

現状のPLの内容をアンロードします。制御信号が切断されるため冷却ファンが全開になります。

ShellScript
ubuntu@kria:~$ sudo xmutil unloadapp
  remove from slot 0 returns: 0 (Ok)

PL書込みを実行します。ファームウェアフォルダ名を指定します。成功すればファンの制御が戻るはずです。

ShellScript
ubuntu@kria:~$ sudo xmutil loadapp demo_kr260_axi_gpio
  demo_kr260_axi_gpio: loaded to slot 0

6-4. おまけ:デフォルトのbitstreamの設定変更

2023/8現在、認定Ubuntu22.04では起動直後にPLに書き込まれるbitstreamは以下のk26-starter-kitsファームウェアになっています。

  • /lib/firmware/xilinx/k26-starter-kits

デフォルトのbitstreamを変更したい場合、Xilinx-Wikiによると、dfx-mgrデーモンの設定“/etc/dfx-mgrd/daemon.conf”を見てみれば良いようです。

ShellScript
ubuntu@kria:~$ cat /etc/dfx-mgrd/daemon.conf 
{
	"firmware_location": ["/lib/firmware/xilinx"],
	"default_accel":"/etc/dfx-mgrd/default_firmware"
}

“/etc/dfx-mgrd/default_firmware”の内容を参照するようです。中身はテキストでファームウェアフォルダ名が書いてありました。これを書き換えてみましょう。

ShellScript
ubuntu@kria:~$ cat /etc/dfx-mgrd/default_firmware 
k26-starter-kits

#デフォルトのbitstreamをカスタムのものに変更する
$ sudo sh -c "echo demo_kr260_axi_gpio > /etc/dfx-mgrd/default_firmware"

ubuntu@kria:~$ cat /etc/dfx-mgrd/default_firmware 
demo_kr260_axi_gpio

再起動するとカスタムbitstreamが書き込まれていました。

7.SysfsによるGPIO入出力

7-1. GPIOポートの特定と有効化

PL書込みでデバイツリーが正常にロードされれば、AXI-GPIOドライバがmodprobeされるはずです。

ShellScript
#GPIO I/F一覧表示
ubuntu@kria:~$ ls /sys/class/gpio/
  export  gpiochip326  gpiochip334  gpiochip508  unexport
#PL書込み
ubuntu@kria:~$ sudo xmutil unloadapp
  remove from slot 0 returns: 0 (Ok)
ubuntu@kria:~$ sudo xmutil loadapp demo_kr260_axi_gpio
  demo_kr260_axi_gpio: loaded to slot 0
#GPIO I/F一覧再表示
ubuntu@kria:~$ ls /sys/class/gpio/
  export  gpiochip322  gpiochip326  gpiochip334  gpiochip508  unexport

今回はSysfsを使ったGPIOの操作を行います。

私の環境ではgpiochip322が出現しました。これがAXI-GPIOのI/Fになります。この番号を固定で設定できれば便利なのですが、調べてもよくわかりませんでした。

そのため、現状判別は/sys/class/gpioXXX/labelを見ることにします。

ShellScript
#AXI-GPIO
ubuntu@kria:~$ cat /sys/class/gpio/gpiochip322/label 
a0000000.gpio

#ついでにデフォルトで存在する他の3つも見てみます
ubuntu@kria:~$ cat /sys/class/gpio/gpiochip326/label 
slg7xl45106
ubuntu@kria:~$ cat /sys/class/gpio/gpiochip334/label 
zynqmp_gpio
ubuntu@kria:~$ cat /sys/class/gpio/gpiochip508/label 
firmware:zynqmp-firmware:gpio

a0000000はPL-PS I/FのHPM0_FPDポートに接続されたAXI-GPIOのベース物理アドレスになります。VivadoではIPインテグレータのAddressEditorから確認できます。

デフォルトで存在する他の3つのgpiochipも見てみたのですが、どれが何を指しているかはわかりません。(仮に調べたい場合、KR260実機のデバイスツリーを読み出してデコンパイルする必要がありそうです)

続いてGPIOを有効化します。AXI-GPIOのIP設定でデュアルチャネルポートでそれぞれ2bitずつに設定しました。そのため、計4bit分有効化します。先程のgpiochipXXXのXXXから4つ分の番号をexportします。

ShellScript
#まずsuユーザに移行。Ubuntuセットアップ直後の方はrootパスワード未設定かも
ubuntu@kria:~$ su
Password: 
root@kria:/home/ubuntu# echo 322 > /sys/class/gpio/export 
root@kria:/home/ubuntu# echo 323 > /sys/class/gpio/export 
root@kria:/home/ubuntu# echo 324 > /sys/class/gpio/export 
root@kria:/home/ubuntu# echo 325 > /sys/class/gpio/export 
root@kria:/home/ubuntu# ls /sys/class/gpio/
  export  gpio322  gpio323  gpio324  gpio325  gpiochip322  gpiochip326  gpiochip334  gpiochip508  unexport

#ためしに存在しない5bit目を有効化しようとするとしっかりエラーが出てくれる
root@kria:/home/ubuntu# echo 326 > /sys/class/gpio/export 
  bash: echo: write error: Device or resource busy

bitの対応は以下のようになっていました。1チャネル目と2チャネル目は番号詰めされるようです。

  • gpio322 = GPIO0[0] = UFx_LED_tri_o[0] = UF1_LED
  • gpio323 = GPIO0[1] = UFx_LED_tri_o[0] = UF2_LED
  • gpio324 = GPIO1[0] = PUSH_SW_tri_i[0] = PMOD1_IO1_HDA11 = SW1
  • gpio325 = GPIO1[1] = PUSH_SW_tri_i[1] = PMOD1_IO2_HDA12 = SW2

7-2. ShellでGPIO出力

オンボードユーザLEDを点灯させてみます。suユーザを忘れずに。

ShellScript
#出力に設定
root@kria:/home/ubuntu# echo out > /sys/class/gpio/gpio322/direction
root@kria:/home/ubuntu# echo out > /sys/class/gpio/gpio323/direction

#ためしにUF2_LEDを点灯させる。正論理なので1=Highで点灯
root@kria:/home/ubuntu# echo 1 > /sys/class/gpio/gpio323/value

UF2_LEDだけ点灯させてみました。点きましたね。

7-3. ShellでGPIO入力

Pmodに接続したプッシュスイッチを押してみます。suユーザを忘れずに。

ShellScript
#入力に設定
root@kria:/home/ubuntu# echo in > /sys/class/gpio/gpio324/direction 
root@kria:/home/ubuntu# echo in > /sys/class/gpio/gpio325/direction

#SW1を押さない状態で実行
root@kria:/home/ubuntu# cat /sys/class/gpio/gpio324/value 
1

#SW1を押した状態で実行
root@kria:/home/ubuntu# cat /sys/class/gpio/gpio324/value 
0

スイッチはプルアップ抵抗接続なので負論理です。しっかり読み取れています。

7-4. PythonでGPIO入力変化通知(≒割込み)

GPIO入力変化通知はGPIOピン割込みのことだと思ってください。つまり、スイッチなどをポーリングで監視するのではなく、ハードウェアでGPIO入力ピン状態を監視し、設定した変化が起きたら、ユーザアプリに知らせて非同期イベント処理をさせることができます。

AXI-GPIOのIP設定で、割込み信号線を接続しました。これにより、デバイスツリー生成でGPIO割込みの設定が組み込まれ、GPIO入力変化イベント通知が利用できるようになります。

イベント通知の方法はコールバックハンドラなどいろいろありますが、SysFs GPIOではpoll(2)やepoll(7)システムコールに対応しているので、Pythonでepollを使ってみたいと思います。epoll監視対象のファイルは以下の入出力値を読み書きするvalueです。

/sys/class/gpio/gpioxxx/value

epoll(7)を使うための事前準備

epollに通知するイベントを有効にするために、SysFs-GPIOのトリガを先に設定します。トリガの種類は”none”, “rising”, “falling”, or “both”の4つです。

ShellScript
$ su
#スイッチ1を入力に設定し、立ち下がりエッジトリガを設定
root: echo in > /sys/class/gpio/gpio324/direction
root: echo falling > /sys/class/gpio/gpio324/edge

#スイッチ2を入力に設定し、立ち下がりエッジトリガを設定
root: echo in > /sys/class/gpio/gpio325/direction
root: echo falling > /sys/class/gpio/gpio325/edge

Pythonでepoll(7)の使い方

  1. selectモジュールをimport
  2. 監視対象のファイルをopen
  3. epollオブジェクト生成
  4. epollに監視対象ディスクリプタとイベントを登録
  5. 監視対象ファイルをreadしてイベントクリア
  6. epoll.poll()でイベントブロッキング待ち
  7. ディスクリプタとイベントを見てイベント元を判定

※epoll登録後の監視対象ファイルreadは必須です。イベントが残る場合があります。

#!/usr/bin/env python
#epollのサンプル
import select

#GPIOディスクリプタとepollオブジェクトの作成
with open('/sys/class/gpio/gpioXXX/value',mode='r') as switch1, \
     open('/sys/class/gpio/gpioYYY/value',mode='r') as switch2, \
     select.epoll() as poll:

    #epollに監視対象ディスクリプタとイベント種別を登録
    #監視対象が複数ある場合必要な回数登録
    #イベントはフラグ値なので、複数ある場合はbitORする
    poll.register(switch1, select.EPOLLPRI)
    poll.register(switch2, select.EPOLLPRI)

    #GPIOファイルを読み出してイベントクリア
    switch1.seek(0)
    switch1.read()
    switch2.seek(0)
    switch2.read()

    #イベントを監視する。来るまでブロッキングする
    events = poll.poll()

    #イベントが発生したので発生元を調べる
    #eventsはタプルのリストで監視対象が複数ある場合、
    #複数イベント来る場合があるので、ループでチェックする
    # events -> [(fd1, event1),(fd2, event2),....(fdn, eventn)]
    for fileno, event in events:
        #GPIOスイッチ1
        if fileno == switch1.fileno(): 
            # イベントクリア
            switch1.seek(0)
            switch1.read()
           
            # イベント種別確認
            if False != event & select.EPOLLPRI:
                print('switch1 pushed!')
            else:
                print('switch1 pushed? no EPOLLPRI events.')
        
        #GPIOスイッチ2
        elif fileno == switch2.fileno(): 
            pass
         #以後省略

スイッチ押下通知でLEDを操作するPythonサンプル

上の要領でスイッチを押すとLEDが点灯するサンプルを作成しました。簡略化のためチャタる場合は考慮しません。

  • SW1を押すたびにUF1_LEDがトグルする
  • SW2を押すたびにUF2_LEDがトグルする
  • 10回押すと終了
  • 注: sudoで実行しないといけないので、内容確認せず実行はしないでください
#!/usr/bin/env python
#あらかじめsysfsのgpioをexportしておく
# $su #<-rootユーザに移行
# #echo 322 > /sys/class/gpio/export
# #echo 323 > /sys/class/gpio/export
# #echo 324 > /sys/class/gpio/export
# #echo 325 > /sys/class/gpio/export

import select

LED1 = 'gpio322'
LED2 = 'gpio323'
SW1  = 'gpio324'
SW2  = 'gpio325'

LED1_DIR_PATH = '/sys/class/gpio/' + LED1 + '/direction'
LED2_DIR_PATH = '/sys/class/gpio/' + LED2 + '/direction'
SW1_DIR_PATH  = '/sys/class/gpio/' + SW1  + '/direction'
SW2_DIR_PATH  = '/sys/class/gpio/' + SW2  + '/direction'

LED1_INT_EDGE_PATH = '/sys/class/gpio/' + LED1 + '/edge'
LED2_INT_EDGE_PATH = '/sys/class/gpio/' + LED2 + '/edge'
SW1_INT_EDGE_PATH  = '/sys/class/gpio/' + SW1  + '/edge'
SW2_INT_EDGE_PATH  = '/sys/class/gpio/' + SW1  + '/edge'

LED1_PATH = '/sys/class/gpio/' + LED1 + '/value'
LED2_PATH = '/sys/class/gpio/' + LED2 + '/value'
SW1_PATH  = '/sys/class/gpio/' + SW1  + '/value'
SW2_PATH  = '/sys/class/gpio/' + SW2  + '/value'

GPIO_DIR_OUT = b'out\n'
GPIO_DIR_IN  = b'in\n'
GPIO_INT_EDGE_FALL = b'falling\n'
GPIO_INT_EDGE_RISE = b'rising\n'
GPIO_HIGH = b'1\n'
GPIO_LOW  = b'0\n'

def setup_gpio():
    #GPIO入出力設定
    with open(LED1_DIR_PATH,mode='w+b',buffering=0) as led1_dir,  \
         open(LED2_DIR_PATH,mode='w+b',buffering=0) as led2_dir,  \
         open(SW1_DIR_PATH,mode='w+b',buffering=0) as switch1_dir,\
         open(SW2_DIR_PATH,mode='w+b',buffering=0) as switch2_dir:
        
        led1_dir.seek(0); led1_dir.write(GPIO_DIR_OUT); led1_dir.seek(0)
        print('Set LED1 '+LED1_DIR_PATH+' = ' + led1_dir.read().decode('utf-8'))
        led2_dir.seek(0); led2_dir.write(GPIO_DIR_OUT); led2_dir.seek(0)
        print('Set LED2 '+LED2_DIR_PATH+' = ' + led2_dir.read().decode('utf-8'))

        switch1_dir.seek(0); switch1_dir.write(GPIO_DIR_IN); switch1_dir.seek(0)
        print('Set SW1 '+SW1_DIR_PATH+' = ' + switch1_dir.read().decode('utf-8'))
        switch2_dir.seek(0); switch2_dir.write(GPIO_DIR_IN); switch2_dir.seek(0)
        print('Set SW2 '+SW2_DIR_PATH+' = ' + switch2_dir.read().decode('utf-8'))
    
    #GPIO入力変化エッジ設定
    with open(SW1_INT_EDGE_PATH,mode='w+b',buffering=0) as switch1_int_edge, \
         open(SW2_INT_EDGE_PATH,mode='w+b',buffering=0) as switch2_int_edge:
        
        switch1_int_edge.seek(0); switch1_int_edge.write(GPIO_INT_EDGE_FALL)
        switch1_int_edge.seek(0)
        print('Set SW1 '+SW1_INT_EDGE_PATH+' = ' + switch1_int_edge.read().decode('utf-8'))
        switch2_int_edge.seek(0); switch2_int_edge.write(GPIO_INT_EDGE_FALL)
        switch2_int_edge.seek(0)
        print('Set SW2 '+SW2_INT_EDGE_PATH+' = ' + switch2_int_edge.read().decode('utf-8'))

# LEDトグルする
def toggle_led(led_fd):
    led_fd.seek(0)
    status = led_fd.read(); led_fd.seek(0)

    if GPIO_HIGH == status:
        led_fd.write(GPIO_LOW)
        print('toggle led -> OFF')
    elif GPIO_LOW == status:
        led_fd.write(GPIO_HIGH)
        print('toggle led -> ON')
    else:
        print('Err: LED GPIO')


def main():
    setup_gpio()

    #GPIO出力用のファイルはバイナリモードかつバッファなしでOpenする
    with open(LED1_PATH,mode='w+b',buffering=0) as led1, \
         open(LED2_PATH,mode='w+b',buffering=0) as led2, \
         open(SW1_PATH,mode='r') as switch1, \
         open(SW2_PATH,mode='r') as switch2, \
         select.epoll() as poll:
        
        # 初期化:LED消灯
        led1.seek(0); led1.write(GPIO_LOW)
        led2.seek(0); led2.write(GPIO_LOW)

        # epollにディスクリプタとイベント種別を登録
        poll.register(switch1, select.EPOLLPRI)
        poll.register(switch2, select.EPOLLPRI)

        # イベントをクリアする
        switch1.seek(0); switch1.read()
        switch2.seek(0); switch2.read()

        # イベントループ(10回で終了)
        for _ in range(10):
            #スイッチが押されるまでブロッキングして待つ
            print('wait switch events...')
            events = poll.poll()

            # イベントディスパッチ(振り分け)
            for fileno, event in events:
                if fileno == switch1.fileno(): # switch1イベントがキック
                    # イベントクリア
                    switch1.seek(0); switch1.read()

                    # イベントを判別
                    if False != event & select.EPOLLPRI:
                        print('switch1 pushed!')
                        toggle_led(led1)
                    else:
                        print('switch1 pushed? no EPOLLPRI events.')

                elif fileno == switch2.fileno(): # switch2イベントがキック
                    # イベントクリア
                    switch2.seek(0); switch2.read()

                    # イベントを判別
                    if False != event & select.EPOLLPRI:
                        print('switch2 pushed!')
                        toggle_led(led2)
                    else:
                        print('switch2 pushed? no EPOLLPRI events.')

                else:
                    print('Err: unknown event.')
            
            print('\n')

    print('exit App.')

if __name__ == "__main__":
    main()

ついでに最初にGPIOをexportするためのShellスクリプトも作ってみました。

#!/bin/bash

# Note: Run as su user.
# $ su

# https://www.kernel.org/doc/Documentation/gpio/sysfs.txt
# GPIO_NUM : $ ls /sys/class/gpio |grep gpiochip -> "gpiochip322"
# GPIO_INOUT : 'in' or 'out'
# GPIO_INT_EDGE : 'none' or 'rising' or 'falling' or 'both'
GPIO_NUM=(     '322'   '323'   '324'      '325'    )
GPIO_INOUT=(   'out'   'out'   'in'       'in'     )
GPIO_INT_EDGE=('none'  'none'  'falling'  'falling')

for i in ${!GPIO_NUM[@]} ; do

    echo ${GPIO_NUM[i]} > /sys/class/gpio/export
    echo ${GPIO_INOUT[i]} > /sys/class/gpio/gpio${GPIO_NUM[i]}/direction

    if [ 'in' = ${GPIO_INOUT[i]} ]; then
        echo ${GPIO_INT_EDGE[i]} > /sys/class/gpio/gpio${GPIO_NUM[i]}/edge
        echo "enable gpio${GPIO_NUM[i]} ${GPIO_INOUT[i]} ${GPIO_INT_EDGE[i]}"
    else
        echo "enable gpio${GPIO_NUM[i]} ${GPIO_INOUT[i]}"
    fi
done

ABOUT ME
sh-goto
低レイヤで遊んでいます
関連記事