この便利な時代にファイルウォッチャーを自作した

f:id:puhitaku:20200615004836p:plain

コンピューターにまつわるたくさんの知見が存在する2020年。GoogleやGitHubを通じて世界の知見を見渡しても、意外と自分が欲しい道具が見つからないことがある。

今回自作したのは、既に星の数ほどありそうな道具、ファイルウォッチャー。名前から連想できるように、「ファイルの変化を監視して、変化をトリガーにコマンドを実行するソフト」だ。

開発の背景紹介や既存との比較は後の方でするとして、まずは今回作ったファイルウォッチャーその名も "r3build" の特徴をピッチしていく。

github.com

r3build紹介ピッチ

さて、あなたはいま組み込みLinuxのカーネルをいじっているとしよう(まあ、PythonでもGoでも対象は何でも良い)。試行錯誤のたびに make を手で起こすのは辛いので、makeを自動化したいとする。

ここでr3buildが登場する。r3buildの真髄は r3build.toml という設定ファイルで容易かつ直感的に設定できることにある。以下のような要求を、非常に簡潔に記述して監視させることができる。

  • コード(c, h, dts)を保存したら勝手にmake allしてほしい
  • defconfigを保存したら勝手にdefconfigしてほしい
  • 関係ないファイルの変更は無視して欲しい
  • make中のファイルの変化は無視して欲しい

この場合、r3buildに読ませる r3build.toml は以下のようになる。

[[job]]
  name = "All"
  type = "make"
  regex = [
    ".+\.[ch]$",
    ".+\.dts$",
  ]
  environment.ARCH = "arm"
  environment.CROSS_COMPILE = "arm-linux-gnueabi-"

[[job]]
  name = "Defconfig"
  type = "make"
  target = "mxs_defconfig"
  glob = [
    "./**/mxs_defconfig",
  ]
  environment.ARCH = "arm"
  environment.CROSS_COMPILE = "arm-linux-gnueabi-"

キーとバリューの説明は以下の通り。

name

Human-readableなJobの名前。処理には影響せず、ログでのみ登場する。

type

検知したファイルイベントを処理する実装を指定する。この「実装」はprocessorと呼ぶ。processorには make(makeのターゲットを実行), command(任意のコマンド実行), pytest(pytestを実行)がある。今回はターゲット指定(任意)がないので make がそのまま(= make all)実行される。

regex

ファイルパスをマッチするルール。名前の通りregexで指定する。これにマッチするファイルでイベントが検知されると、processorがトリガーされる。文字列を置くと単一ルールのマッチになり、文字列のArrayを置くとOR条件扱いでマッチする。同様に regex_exclude を指定すると、それにマッチするファイルは無視される。

glob

ファイルパスをマッチするルール。こちらはglobで指定する。使い方はexcludeも含めてregexと一緒、どちらでも好きな方を使える。一応両方同時に使うこともでき、OR条件としてマッチする。

environment

環境変数を指定する。今回は組み込みLinuxなのでARCHとCROSS_COMPILEを指定している。

target

makeのtarget。make processor特有のオプション。

その他

今回は盛り込んでいないが、みんな大好き -j オプションの数字は標準で自動推定される。自分で指定したい場合は、 jobs キーで設定できる。

[[job]]
...
type = make
target = hoge
jobs = 8  # 0にするとCPUコア数から推定させられる

このようにして設定を記述し、 r3build を常駐させておくと *.c, *.h, *.dts, mxs_defconfig の変更を検知次第makeが走る。r3buildを起動するには以下のように実行する。

$ r3build

もしくは

$ python3 -m r3build

r3buildのアーキテクチャ

r3buildでは、こういった「ファイルのマッチ条件」と「イベントを受け取るprocessor」と「processorの設定」をまとめた1単位を Job と呼んでいる。[[job]] はTOMLの Array-of-Tables文法を使ったもので、見た目にもスッキリと監視対象を列挙することができる。

また、Jobが実行中に検知したイベントは標準で捨てるようになっている(注: 並列実行はまだ実装していない)。これは event.ignore_events_while_run = false と指定することで無効化できる。

[event]
ignore_events_while_run = false

[[job]]
...

この例ではコードのコンパイルと自動configしかやっていないが、任意のコマンドをJobに指定することができるので、例えばコンパイルして実機に自動デプロイまでやるとか、もっと大掛かりな仕組みを作ることも可能なハズ。想像は無限大!

ドキュメント

r3build.toml の書き方についてはリファレンスやサンプルを色々用意した。

残念ながらSphinxで生成したような「いわゆるドキュメント」はまだない。疑問があれば随時Twitterで質問してもらって構わないし、バグや機能リクエストがあればCONTRIBUTING.mdに従ってIssueやPRを立てて欲しい。知り合いからのバグ報告であればASAPで直しにかかる…はず!

開発の背景

言うまでもなく、「ファイルの変化を監視して、変化をトリガーにコマンドを実行するソフト」は既にたくさんあるし、気軽に使えるCLIツールの範囲でいくつか試した。

例えば、joh/when-changed(gorakhargosh/watchdogのラッパー)。when-changedは、監視したいファイル名と実行したいコマンドを指定すればそれっぽく起動してくれる。秒で自動化できるのは良いが、複数の問題をはらんでいる。

  • exclude対象が作者の好みにハードコードされている
  • コマンドを発火させている間のファイルの変更も全部拾ってしまい、永遠に実行が終わらない
  • イベントの濁流みたいなことになる(debounceされていない)

別な例として、when-changedの中身である gorakhargosh/watchdog もまたCLIツール watchmedo を内蔵しているが、Tricksなどの便利そうな仕組みがどうも見た目に直感的でないというか、好みではなかった。ちなみにwatchdog自体はマルチプラットフォーム対応も手厚く使いやすいモジュールなので、r3build内部でイベントを拾うソースとして使用している。

上記では「そこが美しくない」とだけ書いているが、この中間処理の記述がめちゃくちゃめんどくさい。自分はただ「Linuxのソースコードをいじったらmakeしてほしい」だけなのに、なぜお手製Pythonスクリプトをわざわざ書かなければならないのか。これから先も似たような汚い手法の繰り返しは嫌だな、と早々にうんざりしてしまった。

結局の所、上に書いたことをそのままエレガントにしたツールが欲しいという結論に達した。

  • エレガントな設定ファイルで秒で自動化できて欲しい(自動化のための苦労をゼロに近づけたい)
  • 監視・除外対象をglobやregexでいい感じに指定したい
  • コマンドを発火させている間のファイルの変更は捨てたい(わざと拾うのも可能にしたい)
  • イベントの種類を絞り込みたい(作成・変更・削除など)
  • 謎の立て続けにくるイベントとかはdebounceしたい

2月の頭に「ぼくのかんがえたさいきょうの設定ファイル」の構想から着手し、だいたい4ヶ月半経った今日ついに完成した。せっかくなのでロゴも作った。

github.com

ちなみにr3buildという名前はどこかの国のプロテインと被ってるっぽいけど、それ以外はなかったのでGoogleabilityも良い。

今後

そもそも日頃の課題を解決したくて作ったものなので、これからもBrainハックとかで使いつつ改善を続ける。今のところ実装したいことは以下の通り。

  • 同じJobで設定を変化させながら何度も実行する
  • もっとエレガントなConfigの模索?
  • Coverageを上げてテストケースを増やす

あとはとにかくいろんな人に使って欲しい!これを呼んでいるあなたも、ファイルの変化をトリガーに自動化したい事があれば、是非r3buildを思い浮かべて欲しい。

フィードバック、お待ちしております!

TrelloのButler機能でマイルストーンからタスクをいい感じに生成する

f:id:puhitaku:20200427020916p:plain

tl;drな方はButler機能へジャンプしてください。

カンバンスタイルのTODO管理ツールとして、Trelloは結構な知名度で世界に普及している。

タスク管理、殊に「カンバン」というタームが絡むといつも「タスクカードは物理とデータどちらで管理すべきか」論争が起きる。2017年あたりに前々職でTrelloをひとしきり使った頃は、当時読んだ「Kanban in Action」の影響もあって「タスクカードは物理が最強!」とかのたまっていたものだった。

その勢いのままに、前職では完全物理カンバンを全社で2年間使い倒した*1。その末に「物理とデータ、どっちもどっち」という一番面白くない(?)結論に達したのは、タスク管理がいかに難しいかを体現しているといえる*2

ひと月前から家で独りで仕事をするようになり、物理かどうかはさておき「カンバンが欲しい」と思うのは自然な流れだった。「ものづくりのタスク管理」にはやっぱりカンバンが使い心地が良い。

人気を伸ばすAsanaやWrikeを横目に、「アカウントを持ってるから」という単純な理由で、苔むしたTrelloアカウントに再び足を踏み入れたのがつい二日前の事だ。

「マイルストーン」と「構成タスク」

Trelloでは、カード間の関係性はとてつもなく質素な「リンク」で表現する。カードに別なカードを "Attachment" として添付するか、Checklist itemやDescriptionの中にカードのリンクを置く。たったそれだけ。片方がリンクを張っていても、もう片方がリンクを張っていなければ相互に渡り歩くこともできない。

流石にこれが2017年からなにひとつ変わっていないのはちょっとガッカリした。

ではその「リンク」を使って自分が何をやりたいのかというと、割とデカい目標を「マイルストーンカード」として作っておき、それを分解した「サブタスクカード」を紐付けるというもの。

バカ正直に前者と後者の間でリンクを張り合うなど正直2020年のサービスとは思えない。なんとかならないものかと考えていたら、画面右上に見慣れない "Butler" ボタンを発見した。

Butler機能

"Butler" = 「執事」。要はボードで起きたイベントをトリガしていろんな操作をオートメーションするというもの。某CIおじさんっぽくていい名前だと思う。

前節で紹介した親子カード間のリンク貼りは、実は「親カードのチェックリストを子カードに変換する」という手段を使うと若干ショートカットすることができる。ここにButlerをくっつけて、以下のようなことが可能になる。

  1. 手動で "Milestones" リストにカードを作る
    (例: Implement API) f:id:puhitaku:20200427011336p:plain

  2. 「"Milestones" リストにカードを作った」をトリガにしてButlerが目を覚まし、 "Implement API" カードに "Tasks" という名前のチェックリストを自動で追加する f:id:puhitaku:20200427011512p:plain

  3. Butlerが追加したチェックリストに手動でアイテムを追加する(例: Create a repository) f:id:puhitaku:20200427011528p:plain

  4. 「"Milestones" リストにあるカードの "Tasks" という名前のチェックリストにアイテムを追加した」をトリガにしてButlerが目を覚まし

    • カードの名前から "Tasks of Implement API" というリストを自動でつくる
    • "Create a repository" をカードに変換し "Tasks of Implement API" に自動で追加する

f:id:puhitaku:20200427011600p:plain

自動でカードになっていることがわかる。さらに元のボード画面に戻ると、リストまでもが自動で作られていることがわかる。

f:id:puhitaku:20200427011616p:plain

子カードの詳細を覗くと、親へリンクされている。

f:id:puhitaku:20200427011623p:plain

このようにして、「マイルストーンカードを作ったら、タスクを書く場所が中に勝手にできて、さらにタスクを書くと勝手にカードになる」という嬉しいワークフローが実現する。

定義は直感的

実際の定義はというと、これまた直感的で好感が持てる。

「"Milestones" リストのカードにある "Tasks" チェックリストにアイテムを追加したらカード化」の定義の例でいうと、まずトリガが以下のようになる。

f:id:puhitaku:20200427012838p:plain

もう見た目通りという感じ。条件をポチポチで追加していくと英文法で記述されたトリガが完成する。今回はフィルタとして「in list "Milestones"」しか入れていないが、他にも「自分にアサインされている」とか「○○色のラベルが付いている」みたいな細かいフィルタが可能。

次にどういうアクションを起こすかの定義をする。

f:id:puhitaku:20200427013523p:plain

Tasks of {conditio と見切れているのは Tasks of {conditioncardname} と入っている。この {} で囲まれた文字は変数 (Variable)で、 {conditioncardname} の場合はトリガしたチェックリストを抱えるカードの名前がFormatされる。

このようにして、 "Tasks of {親カードの名前}" リストにチェックリストから変換した子カードを追加するという処理が実装できる。

保存したRuleは英文法で表示され、これまたわかりやすい。

f:id:puhitaku:20200427014200p:plain

無料プランだと 1 Rule のみ

現状、Butlerのトリガを2つ同時に登録することは無料ユーザーではできない。私の場合は、「MilestoneリストのカードにあるTasksチェックリストにアイテムを追加したら…」だけ有効にしている。そこが一番めんどくさくて省略したいところだからね!

しかしながら、この制限はかなーりきついので、もうちょっと緩和してくれたら超嬉しいなとは思う。

まとめ: Trelloがんばってね

ここまでTrello + Butlerを推しておきながら、正直なところ近頃人気のAsanaを見るとTrelloはもう空前の灯火なのではないかと心配になる。それくらいAsanaはよくできている。今回も、もしTrelloのアカウントを以前から持っていなければAsanaを採用していただろう。

まあButlerでできることが極度に目新しいかというとそうではないけども、Trelloの延命へとポジティブに働いてくれるのはきっと間違いない。「タスク管理といえばTrello」が過去のものにならないように、これからも強大なライバルたちとしのぎを削ってもらえたらと願っている。

*1:これは私がやりたいと言ったわけではなく、社長を筆頭にそういう思想だった

*2:スクラムとかチームの可視化みたいな話はもちろん背景にたくさんあるが、今回は割愛する

RasPiエミュでちょっとわかるLinux Kernelのグラフィック系

UPDATE: 出してたPRがMergeされました。やったね!

Raspberry Pi、言わずとも皆さんご存知ですね。

Raspbian、言わずとも皆さんご存知ですね。

今ちょうど仕事で RPi-Distro/pi-gen ていうソフトを使ったRaspbianのカスタマイズをやってて、出力したOSイメージを毎度SDに焼くのはかったるいということでQEMUでエミュレートすることにしました。まあ細かく言えば環境が少々変わっちゃうんですが、実機でなくても確かめられる範囲はこれで検証できます。

Raspbian内蔵のカーネルでQEMUが仮想化してくれるかというとそういうわけではなく、QEMU用にカスタマイズされたカーネルを使います。これはGitHubにいる優しいお兄さんのおかげで簡単に手に入ります。それが dhruvvyas90/qemu-rpi-kernel です。

github.com

ところがどっこい、ここにあるRaspbian Buster向けのKernel (Linux 4.19) だとQEMUにXorgはおろかfbcon(RasPiのロゴとカーネルログがずらずら流れるやつ)すら出ません。ちょっと探ってみると原因もすぐ見つかりPR(Merge済み)も出せましたが、Device Tree 〜 Kernelのグラフィック系の前提知識がないとちょっと難しそうな印象でした。

結果的に、ブログでまとめるとちょうどよさそうな重さになったので、このトラブルシューティング事例を題材にして、組み込みLinuxの片鱗やDRM/KMS(簡単に言うとLinux Kernelのグラフィック周りのスタック)がどのように構成されているかちょっと学んでみましょう。学びを重視したいので、文章は「問題を深掘っていった流れ」で進めます。

環境

  • Host
    • Rootfs: Debian 10 Buster
    • Linux: 4.19.0
    • QEMU: 3.1.0
  • Guest
    • Rootfs: 2020-02-13-raspbian-buster.img
    • Linux: qemu-rpi-kernel/kernel-qemu-4.19.50-buster
    • Device Tree: qemu-rpi-kernel/versatile-pb.dtb(修正前)

まず動かしてみる

QEMUを入れます。

$ sudo apt install qemu-system-arm

qemu-rpi-kernelに書いてあるように、Raspbianとリポジトリをダウンロードします。

$ git clone git@github.com:dhruvvyas90/qemu-rpi-kernel.git
$ wget http://ftp.jaist.ac.jp/pub/raspberrypi/raspbian/images/raspbian-2020-02-14/2020-02-13-raspbian-buster.zip
$ unzip 2020-02-13-raspbian-buster.zip

ひとまず書いてあるとおりに起動してみます。Shellも欲しいので追加で -serial stdio を入れます。

$ qemu-system-arm \
  -M versatilepb \
  -cpu arm1176 \
  -m 256 \
  -hda 2020-02-13-raspbian-buster.img \
  -net user,hostfwd=tcp::5022-:22 \
  -dtb ./qemu-rpi-kernel/versatile-pb.dtb \
  -kernel ./qemu-rpi-kernel/kernel-qemu-4.19.50-buster \
  -append 'root=/dev/sda2 panic=1' \
  -no-reboot \
  -serial stdio

するとウインドウには "Guest has not initialized the display (yet)." としか出ません。 "yet" とあるのでちょっと待ってみても変化なし。

f:id:puhitaku:20200424125644p:plain
なしのつぶて……

Shellを見ると起動自体はできているようです。

Raspbian GNU/Linux 10 raspberrypi ttyAMA0

raspberrypi login:

Xのログを読む

GUIが出ないということは、Xorgのログにヒントがないか見ることが多いです。見てみましょう。

pi@raspberrypi:~$ cat /var/log/Xorg.0.log | grep EE
        (WW) warning, (EE) error, (NI) not implemented, (??) unknown.
[   103.384] (EE) open /dev/fb0: No such file or directory
[   103.390] (EE) open /dev/fb0: No such file or directory
[   103.390] (EE) No devices detected.
[   103.390] (EE)
[   103.391] (EE) no screens found(EE)
...

おや、 /dev/fb0 がないと言っています。Xorgが絵を描く先であるFramebufferは、デバイスファイル /dev/fb0 として露出します。

Xorgも /dev/fb0 もしくはDRM(後述)を叩くおあつらえの共有ライブラリがなければお手上げです。これ以上の情報は得られません。

Device Tree を見てあたりをつける

fb0 がないというのは、グラフィック系のドライバが初期化に失敗していることを意味します。ここからKernelを見に行きたい…とその前に、Device Treeを見ておきましょう。Treeをヒントに、SoCのグラフィックドライバに何が使われているかを見つけ出しておく必要があるからです。

Device Treeというのは、超短く言うと「ここのアドレスにこういうコンポーネントがいるよ・コンポーネントの動作設定もついでに書けるよ・コンポーネント間の関係性も記述できるよ」という「システムの地図」みたいなやつです。各コンポーネントに対応するドライバがカーネル内で叩き起こされて、記述された動作設定を元にドライバが初期化を行います。

x86だと使われることがなくいまいちカーネルの人々も話題にしている印象がないものの、ARMのようなメモリマップドIOのアーキテクチャでは広く使われています。

かつてはどのアーキテクチャでも、ボード毎に初期化コードをCで丹精込めて書いていました。Mainline Linuxでいうと /arch/arm/mach-* なんかは全部そうです。ところが、ARMアーキテクチャの多種多様なボードの多種多様な初期化コードを世界中の人が送りつけまくった結果、LinusがブチギレてDevice Treeによるハード構造の表現が一般的になりました。Device Treeの元となったOpen FirmwareはLinuxとは別に1999年から存在していて、Open Firmwareに参画していたIBMの影響か /arch/powerpc 内でのみ使われていたものが、2007年5月に汎用化されました。このあたりの歴史は英語版Wikipediaが詳しいです。

少し脱線してしまいました。では、Device Treeを実際に読んで、 fb0 を喋るはずだった「画面出力を担っているコンポーネントの名前」を見つけに行きましょう。

Device Tree Compilerを入れます。

$ sudo apt install device-tree-compiler

qemu-rpi-kernel に同梱されている versatile-pb.dtb をデコンパイルします。ちなみにversatile pbはVersatile Platform Baseboardの略で、ARM公式の開発ボードのことを指しています。開発ボードのSoCをエミュレートしているということですね。

$ dtc -I dtb -O dts versatile-pb.dtb > versatile-pb.dts
$ editor versatile-pb.dts

するとJSONっぽいようでそんなこともない、構造化されたデータが見えてきます。Gistに全文上げたので見てみてください。

わかりやすい部分をピックアップすると、カーネルのログが出るstdoutのパスとか…

I2Cコントローラーが 0x10002000 にあり、それにRTC(リアルタイムクロック)DS1338が接続されていて、さらにアドレスが 0x68 であるとわかるとか…

AMBAバス(多分AHB)にUARTコントローラーが接続されているとか…

なかなか興味深いですね。ここでお気づきの方もいるかもしれませんが、各ノードの compatible プロパティに設定されている文字列が、「このコンポーネントを制御するドライバ」を判別するヒントになります。実際にカーネル内でドライバを見つけるときもこの文字列がいろいろに使われます。

では画面出力を担当するコンポーネントはどれでしょうか。答えは /amba/display@10120000 です。

どうしてわかるかというと、 display というそれっぽいノード名に加えて、Mainline Linux内で pl110 で文字列検索すると /drivers/gpu/drm/pl111/*.c がぞろぞろ出てくるからですね。 /drivers/gpu/ 以下はすべてグラフィック系ドライバです。

dmesgを見る

デバイスドライバが /drivers/gpu/drm/pl111 であるとわかったので、dmesgにエラーが出ていないかチェックしましょう。

pi@raspberrypi:~$ dmesg | grep 'pl11.'
[    0.293567] drm-clcd-pl111 dev:20: no max memory bandwidth specified, assume unlimited
[    0.294505] drm-clcd-pl111 dev:20: set up callbacks for Versatile PL110
[    0.295974] drm-clcd-pl111 dev:20: No bridge, exiting

最後の行が少し怪しいですね。 ドライバの初期化がうまくいったのに exiting と出すことはまずないので、失敗してそうなニオイがします。

該当箇所を探してみましょう。

$ ag "No bridge, exiting"
drivers/gpu/drm/pl111/pl111_drv.c
166:            dev_err(dev->dev, "No bridge, exiting\n");

前後を取り出すと(GitHubはこちら

165     } else {
166         dev_err(dev->dev, "No bridge, exiting\n");
167         return -ENODEV;
168     }

やはりそうですね。 dev_err を呼んでエラーメッセージとして出した後、 -ENODEV を返しています。

真因に迫る

かなり怪しいポイントには近付いた気がするものの、次は "No bridge, exiting" の意味を理解して直してやらねばなりません。

ところで、qemu-rpi-kernelで配っているカーネルのうち、 Raspbian Stretch用の kernel-qemu-4.14.79-stretch だと画面は普通に表示されます。この 4.14 から 4.19 の間に、何かヒントがありそうです。

例の "No bridge, exiting" の行を git blame してみます。 f:id:puhitaku:20200424194144p:plain

4.14〜4.19の間である4.17の時代にマージされた、このコミットが出てきました。

github.com

        if (panel) {
            bridge = drm_panel_bridge_add(panel,
                              DRM_MODE_CONNECTOR_Unknown);
            if (IS_ERR(bridge)) {
                ret = PTR_ERR(bridge);
                goto out_config;
            }
-               /*
-                * TODO: when we are using a different bridge than a panel
-                * (such as a dumb VGA connector) we need to devise a different
-                * method to get the connector out of the bridge.
-                */
+       } else if (bridge) {
+               dev_info(dev->dev, "Using non-panel bridge\n");
+       } else {
+               dev_err(dev->dev, "No bridge, exiting\n");
+               return -ENODEV;
+       }
+
+       priv->bridge = bridge;
+       if (panel) {
+               priv->panel = panel;
+               priv->connector = panel->connector;
        }

上の方で panel という変数がNULLかどうかチェックしています。このコミット以前は、仮に panel がNULLだったとしてもそのまま放って初期化を継続していたのが、このコミット以降は諦めるようになりました。これがStretch (4.14)とBuster (4.19)の差分の正体であり、真の原因となります。

ここをもとに戻せば動きそうですが、あまり気持ちのいい対処方法ではありません。より正統派の対処として、ここはドライバが所望している panel もしくは bridge なるものを用意して渡してやろうということになります。

DRM/KMSの構造

ここから先はDRMの紹介なくしては話せないので、DRMの紹介をしましょう。

DRM (Direct Rendering Manager)とは、GPUなどグラフィック系デバイスとUserspaceがやりとりするためのLinuxのサブシステムです。平たく言うと、GPUとのやりとりを抽象化していい感じにしてくれるやつです。

KMS (Kernel Mode Setting)とは、画面の表示モードの設定をカーネル内で行う仕組みの名称です。DRMはもともとGPUの複数プログラムによる共同利用などを意図した仕組みですが、3Dとか高度なグラフィックを使わないパイプライン(メモリ上のFramebufferをちょっと変換しつつDMAで液晶に飛ばすだけのコンポーネントとか)においては画面の初期化と表示モードの設定が主たる実装内容になります。そのためか、DRM/KMSとニコイチで呼ばれることが多くなっています。

DRMは、ソフトが絵が描くメモリ上の場所からその絵が表示されるLCDなどまでのパイプラインを機能ごとに区別して管理しています。図にすると以下のような感じ。

(引用元: Kernel Mode Setting (KMS) — The Linux Kernel documentation

Framebufferは、ソフトウェアが絵を描く先です。Planeは、Framebufferやその他のアセット・マウスポインタなど描画される対象を示します(説明が難しい)。CRTCはCRT Controllerの略で、もうCRTなんて使われてないですが、解像度やVblank(GPUが絵を描けるタイミング)の設定を司るコントローラを指します。Encoderはデジタルなデータをアナログの信号に変調したりする部分を指します。Connectorは名前の通りモニタを接続するコネクタを指します。

組み込みLinuxにおいては、Device Treeが、このパイプラインの記述を担うことが多いです。GPUが描いた絵の出力先をノードとして記述しておくと、ドライバはこの情報を参考にして画面出力の設定を行います。この「出力先」が前節で登場した "Panel" や先ほど説明した "Encoder" になるわけです。ちなみに "Encoder" と "Panel/Connector" の間に置いて互いを接続する部分もOptionalで利用でき、 "Bridge" と呼ばれます。

今の時点では、解像度の設定などを司るCRTCは実装上存在する扱いになっています。一方で、Encoder、Connectorはまだありません。これらに相当する部分をDevice Treeに書いてやれば良さそうということになります。

(細かい説明は省略します。ここここをご覧ください。)

ではRasPi on QEMUの場合は?

まだ現時点でわかっていないのが、デバイスドライバである pl111 がDevice Treeにはたしてどういう記述を期待しているかです。これはDocumentationを読むとわかります。

github.com

40行目の "Required sub-nodes" で書かれている ports は、どういうLCDやBridgeやConnectorがどう接続されているか書く場所です。文字通り "Required" な値です。これが書かれていないから、先述のif-elseで弾かれてしまったというわけです。

port の記述例についても同じテキストに書いてあります。黄色の部分が追記するべき場所です。

この例では、Panelを定義して直接接続しています。 panel-dpi はMIPI DSIという規格で接続されたPanelを表し、これもまたドライバが存在します。

なお、画面サイズとタイミング情報もあるためこれをコピーすれば勝手に上手く行ってくれるかなと思ったのですが、どういうわけかうまくいきませんでした。panel-dpi はMIPI DSIのドライバなので、ちゃんと物理的に接続して初期化シーケンスを踏まないと認識してくれないのかもしれません。

そこで、別のボードの例を参考にして、あたかもVGAが接続されているかのような記述を試しました。その結果のdiffが以下の通りです。

--- versatile-pb.dts    2020-04-24 21:11:25.649481714 +0900
+++ versatile-pb-buster.dts     2020-04-24 21:12:04.381243808 +0900
@@ -31,6 +31,38 @@
                phandle = < 0x02 >;
        };

+       bridge {
+               compatible = "dumb-vga-dac";
+
+               ports {
+                       #address-cells = <1>;
+                       #size-cells = <0>;
+
+                       port@0 {
+                               reg = <0>;
+                               vga_bridge_in: endpoint {
+                                       remote-endpoint = <&clcd_pads>;
+                               };
+                       };
+                       port@1 {
+                               reg = <1>;
+                               vga_bridge_out: endpoint {
+                                       remote-endpoint = <&vga_con_in>;
+                               };
+                       };
+               };
+       };
+
+       vga {
+               compatible = "vga-connector";
+
+               port {
+                       vga_con_in: endpoint {
+                               remote-endpoint = <&vga_bridge_out>;
+                       };
+               };
+       };
+
        core-module@10000000 {
                compatible = "arm,core-module-versatile\0syscon\0simple-mfd";
                reg = < 0x10000000 0x200 >;
@@ -241,7 +273,14 @@
                        reg = < 0x10120000 0x1000 >;
                        interrupts = < 0x10 >;
                        clocks = < 0x04 0x03 >;
-                       clock-names = "clcd\0apb_pclk";
+                       clock-names = "clcdclk\0apb_pclk";
+
+                       port {
+                               clcd_pads: endpoint {
+                                       remote-endpoint = <&vga_bridge_in>;
+                                       arm,pl11x,tft-r0g0b0-pads = <0 8 16>;
+                               };
+                       };
                };

                sctl@101e0000 {

まずpl110の方(下の方のdiff)で、件の port を追加し、その中のendpointとして上の方に書いた dumb-vga-dac をつないでやります。

dumb-vga-dacドキュメントはこちら)はRGBを入力してVGAの信号として出すEncoder的なBridgeで、 port@0 にGPUからの出力、 port@1 にVGAコネクタをつなぎます。

そして最後に dumb-vga-dac の出力を vga-connector を接続してやれば、DRM/KMSで求められる構造はすべて定義できたことになります。

ちなみに、 clock-names のdiffは、pl110に入力されるクロックの名前が clcdclk でないとエラーが出たので修正したものです。

動作確認

テキストで書いたDevice Tree (dts) をバイナリ形式 (dtb) にコンパイルします。

$ dtc -I dts -O dtb versatile-pb-buster.dts > versatile-pb-buster.dtb

QEMUに読ませます。

$ qemu-system-arm \
  -M versatilepb \
  -cpu arm1176 \
  -m 256 \
  -hda 2020-02-13-raspbian-buster.img \
  -net user,hostfwd=tcp::5022-:22 \
  -dtb ./qemu-rpi-kernel/versatile-pb-buster.dtb \
  -kernel ./qemu-rpi-kernel/kernel-qemu-4.19.50-buster \
  -append 'root=/dev/sda2 panic=1' \
  -no-reboot \
  -serial stdio

お!!!

やったー!!!

バージョンとかも見ときましょう。

うんうん、ちゃんと4.19ですね。XGA (1024x768) なのは、 dumb-vga-dac にハードコードされた解像度がXGAだからです。普通ならI²CでディスプレイからEDIDを得ますが、それがないのでフォールバックしています。

まとめ

なんか見る範囲が広くてあまりついてこれなかったという人も多いかもしれません。実際のところLinuxはドライバとか触るだけでかなりの情報量なので最初は大変だと思います。ところがDocumentationの探し方やコードがだいたいどこにあるかがわかってくると触りやすくなってきます。

ぜひ組み込みLinuxのグラフィック周りのような、ハードとソフトの境目を渡り歩くような面白い世界をお手元のRaspberry Piで体感してみてください。もし機会があれば!