はじめに
先日、CrowPi2なるノートパソコンもどきを入手しました。ELECROWの製品で、RaspberryPiを搭載したノートパソコンです。こんな感じの外観です。
オフィシャルのページは、ここです。
見ての通り、キーボードの下に、各種のハードウェアガジェットをこれでもかというくらいに詰め込んでいます。センサーにボタンに光り物に・・・とまぁ、よくもこれだけ繋がったわねというくらいです。これを普通に遊ぶだけでお腹いっぱいになれます。
開発のコンセプトは、いろいろなハードウェアを制御する学習キットといったところのようです。
うちでは、CPUボードにはRaspberry Pi4を搭載しました。
CrowPi2用の公式のOSイメージもちゃんとついてます。中身は、raspbianそのものですが、学習用のソフトや、それ専用のメニュー等、わりと盛り沢山なアプリがついてきます。pythonやscratchによるハードウェア制御のチュートリアルは、結構よく作り込まれています。(ただし、英語です。)
Raspberry Piのハードウェア制御のいろいろな記事は、先日開発者による非推奨が表明されたwiringpi(大変残念です。特に、原因となった現在の世情が)を始めとするユーザー空間のライブラリからのコントロールの記事が多いです。
でも・・・linuxにおいて、ここまでちゃんとハードウェアを作り込んだら、制御の根幹はカーネルモジュールにしておくといろいろなアプリから遊ぶにも、言語を問わず利用できて面白そうな気がします。しかも、カーネルモジュールなんて、ちゃんとハードウェアのお題がないと、なかなかに手を出すことが出来ません。その面でも、このガジェットたちはうってつけです。(悪魔のささやきかも・・・)
というわけで、手を出す機会がなかったlinuxのカーネルモジュールに手を出してみることにしました。
いきなり難しいのも出来ませんから(苦笑)、限りなくLチカに近い、圧電ブザーを題材にとって見ることにしました。
ハードウェアの仕様
残念なことに、CrowPi2の回路図が見つけられません。ボードの部品の近くに印刷されているシルク印刷が、唯一の取っ掛かりです。(とはいえ、必要最低限な情報が有ります。)
シルク印刷を見ると、圧電ブザーの上に「GPIO1」の記載が有ります。この番号(1)は、RaspberryPiのgpio番号です。ところが、ハードウェアを直接制御するためのメモリアドレスや各種の仕様を調べるために、SOCの仕様を紐解くには、この番号ではなく、BCM番号と称されるSOC側がつけた番号が必要です。RaspberryPiのgpioの番号は、この2つの番号が入り混じるので混乱の元になります。
GPIO1のBCM番号は、18となります。gpioの番号としては、この後はBCM番号で通します。
圧電ブザーそのものは、素直に、GPIOピンにつないであるだけのようです。GPIOからHighを出せばブザーがなり、Lowにすれば止まります。うん。単純ですね。
ドライバーの仕様
今回の目標となるドライバーの仕様です。
linuxのドライバー(カーネルモジュール)の基本姿勢は、全てのハードウェア制御は、ファイルの入出力とみなす形です。(実は、他の形式も有ります。)ユーザー空間のアプリケーションから見れば、beepファイルを開いて、このファイルに1を書き込めば鳴り、0を書き込めば止まる。beepファイルを読み込めば、現在、鳴っているか止まっているかを1か0で返してくる。といった感じです。
beepファイルなるものは、どこにあるか?ファイルのパスは、/dev
の下にサブディレクトリの形で出現します。というか、出現するようにします。このディレクリは、各種のハードウェアをファイルシステムの形で見せるためのディレクトリです。普通に、ls /dev
とやってみれば、搭載されているハードウェアの入出力の入り口となるファイル達を見ることが出来ます。こうしてみると、一見、普通にHDD等に保存した文書ファイルとなんの違いもない普通のファイルに見えます。デバイスの仕様によりますが、この/dev
配下のファイルに、echo
やcat
コマンドを発行すると、ちゃんとデバイスを制御できます。
さて、今回のbeepデバイスドライバの仕様です。
今回のデバイスドライバは、キャラクタデバイスの形で実装します。
-
/dev/beep0
ファイルを作成します。 - このファイルに文字の'1'を書き込むと音を鳴らします。'0'を書き込むと止まります。
- その他の文字の書き込みは全て無視します。また、2文字以上の文字列を書き込んだときは2文字目以降も無視します。ただし、書き込みはされたものとして扱います。
- このファイルを読み込むと、音がなっていれば文字の'1'を、止まっていれば'0'を返します。
- このファイルは、読めば、常に現在のブザーの状態を返します。決して、EOFにはなりません。
開発環境の準備
開発機のOS環境
実は、RaspberryPi本体だけでも、ドライバの開発は出来ます。でも、今回は、私の勉強を兼ねて、クロスコンパイル環境で開発することにしました。
母艦は、x86環境のubuntu 20.04です。
ubuntu@localhost:~$ uname -a
Linux ***** 5.4.0-65-generic #73-Ubuntu SMP Mon Jan 18 17:25:17 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
ubuntu@localhost:~$ cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04.2 LTS (Focal Fossa)"
# 以下略
コンパイル環境の整備
さて、linuxのカーネルモジュールをクロスコンパイルするには、大きく分けて、
- RaspberryPiで実行できる実行ファイルを生成できるCコンパイラ環境(ライブラリ含む)(ツールチェーンと呼ばれます。)
- linuxカーネルソースと、そのビルド結果
の2つが必要です。もちろん、成果物をRaspberryPiに転送するための何らかの手段も必要です。ネットワーク経由でscpが一番楽かな。
基本的な情報は、RaspberryPi公式のKernel buildingにあります。
まず、どこかに、crowpi2
というディレクトリを作成してください。今後、ここを基点として作業をします。
ツールチェーンのダウンロード
母艦でcrowpi2ディレクトリに移動して、次のコマンドを実行してください。
母艦に必要となる基本開発プログラムとツールチェーンをインストールします。
ubuntu@localhost:~/crowpi2$ sudo apt install git build-essential libncurses5-dev bc bison flex libssl-dev
# コマンドの結果がでます。
ubuntu@localhost:~/crowpi2$ git clone https://github.com/raspberrypi/tools.git
# コマンドの結果がでます。
これで、crowpi2の下に、toolsというディレクトリが出来ます。ここに、RaspberryPi用のコンパイラ環境一式が入っています。
【2/10追記】
linuxのバージョン5.10.yをコンパイルしようとすると、エラーになり失敗しました。script内をコンパイルしている最中に、コンパイルのパラメータが不正だと言い出します。どうやら、クロス用のコンパイラもバージョンアップする必要が有りそう。guthubのraspberrypiの中にあるツールチェーンなので尊重したかったのですが、今日現在でまだツールチェーンのアップデートはなさそうです。
仕方がありませんので、ツールチェーンを変更しました。
ubuntu@localhost:~/crowpi2$ sudo apt install g++-arm-linux-gnueabihf
cppしか使わないのですが、あえてg++をインストールします。すると、必要なライブラリーがちゃんと芋づる式で揃います。(gcc-arm-linux-gnueabihfだと、ライブラリーのインストールが不足するようです。)
これに伴い、make_crowpiも修正となります。そっちの項に追記します。【追記終わり】
linuxカーネルソースのダウンロードとビルド作業
さて、linuxのドライバをカーネルモジュールと称します。この名前の意味するところは、ドライバは、カーネルの一部となって動作するということです。今から、CrowPi2のlinuxカーネルの一部を作成するわけですから、CrowPi2のカーネル本体が母艦上に必要となります。さらに、この母艦上にあるCrowPi2用のカーネルとCrowPi2にあるカーネルとは同じものである必要があります。実は、カーネルモジュールを組み込む際に、開発に使用したlinuxカーネルと実機のlinuxカーネルが同一のものであることがチェックされ、違うとエラーでカーネルモジュールの組み込みが出来ない仕組みになっています。
というわけで、母艦上に、CrowPi2と同じカーネルを用意する必要があるのですが・・・
これ、結構難航しました。素のraspbianなら、もう少し簡単になるはずなのですが、CrowPi2にその手順を適用しても、CrowPi2と同じとみなされるカーネルソース・オブジェクトを用意することが出来ません。仕方がないので、正攻法で行きます。
手順は、
- カーネルソースのダウンロード
- カーネルのビルドオプションの取得
- カーネルのビルド
- ビルドしたカーネルをCrowPiにインストール
となります。
せっかくカーネルを再インストールしてしまうのですから、もう、今CrowPi2に乗っているカーネルのバージョンを斟酌するのはやめます。(実は、暴挙の可能性も有ります。他のコンピュータボードに応用するときは充分にご注意を。もちろん、作業時のバックアップは「必須」です。)
カーネルソースのダウンロード
現在のraspbianの最新ソースをダウンロードします。
母艦上にて
ubuntu@localhost:~/crowpi2$ git clone --depth 1 https://github.com/raspberrypi/linux.git
ちなみに、非常に大きいです。おそらく、--depth 1
をつけて、ダウンロードする履歴を削減しないと、凄まじい時間がかかります。無しでフルにダウンロードすると、任意の過去のカーネルを対象に出来ますが、必要になった時に後でやるのが良いと思います。
無事完了すれば、~/crowpi2/linux
のディレクトリが出来上がります。
【2/10追記】
このクローンをshallow cloneと呼びます。これには一つ欠点が有ります。新しいバージョンのためにブランチが新設された時に、git fetchでは認識しません。この理由は、.git/config
に、今のブランチだけfetchしますと書いてあるからです。
対処は、2つ。一つは、git fetch --unshallow
。これで、全てのブランチを落としてきます。
もう一つは、git/config
を編集してしまう。fetch=
の行に、linuxのバージョン番号を冠したブランチ名が書いてあります。この行をコピーして、新しいブランチ名の行を追加する。
後者だと、新しい分だけ取れます。でも、また新しいブランチが出来ると同じ操作が必要です。
前者だと、全部取りますから、もうこの件について悩むことはなくなります。でも、すっごく時間がかかります。
いずれも、この後に、git pull
をやり直し、最新バージョンをチェックアウトします。。
カーネルのビルドオプションの取得
さて、linuxカーネルのビルドをするわけですが、linuxには、設定可能なオプションが軽く4桁個ほど存在します。カーネルの基本的な挙動や機能のオンオフ、ドライバの組込の有無とその方法を設定します。
これをすべて書いたファイル.config
を作成する必要が有ります。手元で行数を見ると、7601行・・・。この設定を自分で構築するには、linuxカーネルに関する大量の知識と、対象のハードウェアに関する詳細な知識が必要です。間違えたら当然動きません=^・・;=
んなもん、無理ですから、ショートカットします。せっかく動いている実機がちゃんとあるんですから、そこから、拝借しましょう。
ディストリビューションによって、現在稼働中の.configを取得する方法は違うようですが、RaspberryPiでは、次の方法が使えます。
ここからは、CrowPi2上の作業となります。実機のコンソールでも、ssh接続でも構いません。
次のコマンドを順次実行します。
pi@crowpi2:~/$ mkdir config
pi@crowpi2:~/config$ cd config
pi@crowpi2:~/config$ sudo modprobe configs
pi@crowpi2:~/config$ zcat /proc/config.gz > .config
これで、CrowPi2のカーネルを作成するための.config
ファイルが手に入ります。このファイルを母艦のcrowpi2/linux
へコピーしてください。
ちなみに、母艦からscpでダイレクトに取得することも出来ます。
ubuntu@localhost:~/crowpi2/linux$ scp __crowpi2:/proc/config.gz config.gz
ubuntu@localhost:~/crowpi2/linux$ zcat config.gz > .config
みたいな感じですね。(__crowpi2:は、環境に応じて変更のこと。)ただし、crowpi2上で事前にmodprobe configs
の実行は別途必要です。
コンパイル環境の設定
さて、カーネルのビルド作業に入ります。
でも、その前に。今回やろうとしているのは、母艦上でcrowpi2用のカーネルをビルドしようとしています。つまり、いつも使っているCコンパイラではなく先にダウンロード済みのクロスコンパイル用のコンパイラを使用させなくてはいけません。
Makefileには、そのための数多くのパラメータが用意されており、環境変数から制御することが可能です。この環境は、あくまでクロスコンパイルのときだけに使用する一時的な環境なので、sourceコマンドで取り込むことにします。そのために私なりのスクリプトを起こしました。
次のような感じです。
【2/11追記】
ツールチェーンの項を変更したことに伴い、こちらの内容も修正しました。修正前の行は、コメントとして残しておきます。CCPREFIX
の定義が変更されています。
# ! /bin/bash
export CROWPI2=~/crowpi2
export KERNEL_SRC=${CROWPI2}/linux/
export DTC_DIR=${KERNEL_SRC}/scripts/dtc/
# export CCPREFIX=${CROWPI2}/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian/bin/arm-linux-gnueabihf-
export CCPREFIX=arm-linux-gnueabihf-
export KERNEL_DIR=${KERNEL_SRC}
export KERNEL=kernel7l
export ARCH=arm
export CROSS_COMPILE=$CCPREFIX
こんな感じのcrowpi2/make_crowpi
というファイルを作成します。CROWPI2環境変数は、実際の作業ルートのディレクトリを指定してください。
前提条件が少し。まず、これで作成できるのは、RaspberryPi 32bit環境となります。RasPi4にて64bit環境を作成するためには、ツールチェーンを64bit環境にする必要が有ります。
次に、RasPiの種類により、少し、設定が異なります。上のファイルは、RasPi4用です。
RasPi3B+以前の場合は、KERNEL環境変数の内容をKERNEL=kernel7
に修正してください。
このファイルは、デバイスドライバをビルドするにも同じものを使用します。
取得した.configの適用と、些細な修正
さて、ビルド作業の第一歩、.configの扱いです。
まず、この.configを素直に反映させます。
先程取得した.configをcrowpi2/linux
の下にコピーしてあることを確認してください。
【2/10補足】
この先のmakeコマンドには、ARCH=arm CROSS_COMPILE=$CCPREFIX
がついています。この2つの変数は、先のmake_crowpiでも設定しました。
ただ、linuxカーネルのビルド時には、この2つの変数は環境変数からは取れないような記事が見受けられたので、一応つけています。
ちなみに、単純に、make oldconfig
みたいな感じで、単純に、この部分を取っ払って実験してみた結果、ちゃんとコンパイルできました。
でも不安なので、記事には、残しておきます。
ubuntu@localhost:~/crowpi2$ cd linux
ubuntu@localhost:~/crowpi2/linux$ . ../make_crowpi
ubuntu@localhost:~/crowpi2/linux$ make ARCH=arm CROSS_COMPILE=$CCPREFIX oldconfig
これで、現在のcrowpi2上での環境が、ビルドのオプションとして設定されます。
さて、ホンの少し、些細な設定変更をしておきます。カーネルビルドをしてカーネルをインストールした時に、本当に自分の作ったカーネルかを識別するために、unameで表示されるバージョン表記のサフィックスに飾りをつけます。
ubuntu@localhost:~/crowpi2/linux$ make ARCH=arm CROSS_COMPILE=$CCPREFIX xconfig
これは、良く知られる make menuconfig
のGUIバージョンです。せっかくウィンドウ環境でやっているんだから、楽できるところは楽します。まぁ、端末版より圧倒的に見やすいです。(ちなみに、make gconfig
というものもあります。gtk+ベースの同じ機能のもののはずなのですが、うちでは実行できませんでした。gtk+2.0が必要だとのたまうのですが・・・ちゃんと入ってるのよねぇ・・・なんでだろう?)
端末はロックされて、
のように、全てのパラメータがツリー状に列挙された設定ツールが立ち上がります。
操作方法は、見ての通り、左ペインで項目を選択し、右ペインで修正します。右ペインで修正する小項目を選ぶと、その項目の簡単な説明が右ペインの下側にでてきます。
この中で、Genelal setupの中にある、「Local version - append to kernel release」を修正します。多分、-v7と書かれていると思いますが、これを-v7+nameの様にちょっと修正。これで、unameで見た時に、このバージョンのコメントが付加されます。
出来たら、フロッピーマークの保存ボタンを押して、終了すれば、.configの修正から、必要な設定ファイルの修正まで全部自動です。
カーネルのビルド
さて、ここまでで準備は整いましたので、カーネルをビルドします。
ubuntu@localhost:~/crowpi2/linux$ make -j10 ARCH=arm CROSS_COMPILE=$CCPREFIX zImage modules dtbs
を実行します。
-j10のところは、ご自分の母艦の性能に合わせて調整ください。コア数*1.2~1.5倍程度が快適と言われますが、まぁ、試行錯誤のうちです。
うちでは、10分程度の実行で、カーネルの構築が終了しました。(この作業をRaspberryPiで自力でやらせると、一晩級となります。この高速化がクロスコンパイルの最大の威力となります。)
カーネルのインストールの準備
さて、出来上がったカーネルコードをCrowPi2にインストールする準備にかかります。
linuxディレクトリ配下にちょっとファイル群が散らばっています。必要なものは、linuxカーネル本体と、カーネルモジュール一式です。
ubuntu@localhost:~/crowpi2/linux$ make ARCH=arm CROSS_COMPILE=${CCPREFIX} INSTALL_MOD_PATH=../install/ modules_install
で、モジュール一式をcrowpi2/install
配下にちゃんとした形で集めることが出来ます。install
の下にlib
というディレクトリが出来上がります。これは、ターゲットの/lib
配下にそのままコピーできます。crowpi2/install/lib/modules
の中身を見てみると、
ubuntu@localhost:~/crowpi2/linux$ ls ../install/lib/modules/
5.4.83-v7l+name
というディレクトリが出来上がっているのが見えます。バージョン番号の後ろに先程の設定で設定したサフィックスがちゃんとついていますね。実は、これ変更した理由は自分での識別の他にもう一つ有ります。実機の/lib/modules/
にこれをコピーするわけですが、実機にあるディレクトリ名はこれとは最低限サフィックスの部分が違います。バージョンも違えばそこも変わりますが、実機でちゃんとatp upgrade
をしていれば、バージョンは同じになるはずです。それでもサフィックスが違うおかげで、このディレクリのバックアップの必要はありません。一手間省けます。
crowpi2/install
に、カーネル本体も持ってきましょう。
ubuntu@localhost:~/crowpi2/linux$ cp arch/arm/boot/zImage ../install/
このzImageが作ったカーネル本体になります。
なお、新生カーネルで、RasPiによるカーネルモジュールの自己コンパイルをしないのであれば、転送するファイルが少し整理できます。
ubuntu@localhost:~/crowpi2/linux$ rm -r ../install/lib/modules/5.4.83-v7l+name/build
ubuntu@localhost:~/crowpi2/linux$ rm -r ../install/lib/modules/5.4.83-v7l+name/source
少し容量と、転送時間が助かります。
カーネルのインストール
さて、CrowPiの実機に適当なディレクリを作り、crowpi2/install
配下のファイルを全部コピーしてください。まだ実機で少し作業が必要です。直接目的地にコピーしません。最終コピーは、実機側で操作します。
scpでやるもよし。sd-cardを母艦に挿してcpするもよしです。ただし、scpにせよcpにせよ、-rpのオプションをつけてください。rはディレクトリを再帰的に全部コピーするために、pはファイルの属性を固定するために必要です。
ubuntu@localhost:~/crowpi2/linux$ cd ../install
ubuntu@localhost:~/crowpi2/linux$ scp -rp * __crowpi2:kernel/
みたいな感じですね。
例のごとく、__crowpi2
の部分は、各自の環境に合わせて調整を。
さて、ここからは、CrowPi2実機上の作業となります。ssh接続でも直接実機でやってもokです。
先程、ファイル一式をコピーしたディレクトリに移動します。そしてzImageのファイル名を変更し、配下のファイルの所有権をroot/rootに変更します。
pi@crowpi2:~/$ cd kernel
pi@crowpi2:~/kernel$ mv zImage kernel7l.img
pi@crowpi2:~/kernel$ sudo chown -R root:root *
この時、対象がRasPi3以下であれば、ファイル名は、kernel7.imgとなります。
さらに、現状のカーネル本体をバックアップします。
pi@crowpi2:~/kernel$ sudo cp /boot/kernel7l.img /boot/kernel7l.img.bak
もし、カーネル生成の際に、バージョンのサフィックスを変更していない場合は、/lib/modules/
の下にある現在のバージョンのモジュールもフォルダ名を変更して確保します。
最後に、このファイルを全部、正しい場所にコピーして作業は、終了です。
pi@crowpi2:~/kernel$ sudo cp -p kernel7l.img /boot/kernel7l.img
pi@crowpi2:~/kernel$ sudo cp -rp lib /
後は、CrowPi2実機を再起動して、無事起動することを祈りましょう。
再起動後、実機のコマンドラインで
pi@crowpi2:~$ uname -a
Linux crowpi2 5.4.83-v7l+name #1 SMP Sun Jan 10 20:50:13 JST 2021 armv7l GNU/Linux
のように、バージョンのおしりに自分のつけた名前がついていて、動作に異常がなければ、成功です。
これで、開発環境の準備終了です。
初めてのカーネルモジュール
本題に入る前に、ここで、一番小さなカーネルモジュールを作ってみておくことにします。目的は、開発環境のテストと、カーネルモジュールのコンパイル・インストール・動作確認の基本手順を流してみることです。
機能は、なにもありません。ただログに、メッセージが残るだけです。
母艦のcrowpiの下に、sampleディレクトリを作っておいてください。
この章は、全て、sampleディレクトリ配下で実施します。
最小のカーネルモジュール ソースコード
ここは、あっさりと、まず、モジュールのソースコードをあげておきます。
# include <linux/module.h>
# include <linux/kernel.h>
# include <linux/device.h>
# include <linux/siphash.h>
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("This is sample driver.");
MODULE_AUTHOR("name");
struct sample_driver {
struct device_driver driver;
};
static int sample_init(struct sample_driver *drv) {
printk(KERN_ALERT "driver loaded\n");
return 0;
}
static void sample_exit(struct sample_driver *drv) {
printk(KERN_ALERT "driver unloaded\n");
}
static struct sample_driver sa_drv = {
.driver = {
.name = "sample driver",
.of_match_table = NULL,
},
};
module_driver(sa_drv, sample_init, sample_exit);
ちょっとした解説
まぁ、これが、カーネルモジュールの所謂おまじないの全てです。
最初の行は、最小限のヘッダファイルたちですね。このヘッダファイルは、crowpi2/linux/include
の下に有ります。カーネルソース内にあるファイルです。今後もいろいろなヘッダを取り込みますが、ほとんど全て、このディレクトリ内に存在します。何か疑問が起これば、まず、このヘッダファイルの中身を探索していくことになります。
そのすぐ下のMODULE_で始まる一連のマクロは、モジュールの内容を宣言するために有ります。重要なのは、MODULE_LICENSEです。これは、モジュールのライセンスの宣言です。単に自分だけが利用するモジュールだとしても、このライセンス宣言は、よそ様に迷惑をかけないためにも重要なのですが、これは、他に譲ります。もっと大事なのは、カーネル内部の関数の中には、ある特定のライセンスを満たさないモジュールからは呼び出せないものがあることです。とりあえずここでは、一般的なGPLを指定しておきます。
動的なカーネルモジュールは、まず、カーネルに取り込んだ時に、自分自身をカーネルに対して登録し、取り外す時にその登録を解除します。
その登録と登録解除の動作を決めているのが、最後の行にあるmodule_driver
というマクロです。そのすぐ上にあるstruct sample_driver
という構造体が、ごく基本的なドライバーの内容を定義します。
module_driver
マクロは、この構造体と初期化ルーチン、終了ルーチンの3つの引数を取り、この内容をカーネルに対して登録した後、初期化ルーチンを実行します。
初期化・終了ルーチンにも、このsample_driver
構造体が引数として渡されます。
module_driverマクロでは、sample_initとsample_exitが初期化時と終了時に呼ばれると定義しています。この各々のタイミングでログに出力を吐いているのが、このモジュールの唯一の挙動です。
sample_init(sample_exit)の各関数内に、printk
の呼び出しが有ります。これは、カーネルモジュール内からメッセージをログに残すために使用します。C標準ライブラリのprintfと似た形を取ります。
書式は、次のとおりです。
printk(ログレベル "書式指定", 引数...);
となります。ログレベルと書式指定の文字列の間に,
はありません。これは誤植ではありません。ご注意を。
ログレベルは、次のような値を取ります。
レベル | 表記 | 意味・用途 | 短縮関数 |
---|---|---|---|
0 | KERN_EMERG | システム停止前の緊急メッセージ | pr_emerg |
1 | KERN_ALERT | 早急な対応が必要なエラー | pr_alert |
2 | KERN_CRIT | HWもしくはSWの致命的なエラー | pr_crit |
3 | KERN_ERR | ドライバ共通のエラー | pr_err |
4 | KERN_WARNING | 警告(エラーとなる可能性がある) | pr_warn,pr_warnng |
5 | KERN_NOTICE | 注意(エラーではない) | pr_notice |
6 | KERN_INFO | 情報通知 | pr_info |
7 | KERN_DEBUG | デバッグメッセージ専用 | pr_devel |
今回、KERN_ALERTを使っているのは、このクラスのログ表示は非常に目立つから。ただそれだけです。普通は、ちゃんとメッセージのレベルに合わせて選択します。
書式文字列は、printfに似ていますが違います。これを全部上げるとあまりに冗長ですので、英語ですが、本家本元のこちらをご覧ください。ごく一般的なものなら、最初の一画面に目を通すだけですみます。
これから先は、日本語の情報が限られているものが非常に多いです。英文を怖がる必要はありません。最近の翻訳サイトの出来は随分良くなりました。google翻訳なりDeepLなり、お好きな翻訳サイト用のブラウザプラグインを入れておくと、すごく作業が捗ります。
話を戻して、printkですが、今後、デバッグをしていく際に、最もお手軽なツールとして重宝する関数です。あまりに、重宝するので、ショートカットのマクロまで有ります。右端の短縮関数がそれです。pr_info("message %d\n",kazu);
の様に使います。レベル指定がなくなっただけですね。
大半の関数は、短縮関数も全く同じ挙動です。ただし、pr_develだけは違います。実は、printkをあまりに多用すると、下手するとカーネルの実行速度そのものにまで影響が及びます。dmesgを使っては出力するメッセージを制御できるのですが、それでメッセージをoffにしてもどこ吹く風です。ログ機構を動かしていることに変わりはないのです。そこで、デバッグメッセージが用途のこの短縮関数に限り、本来と違う動きをします。具体的には、#define DEBUG 1
を定義しないと、単に文法チェックするだけで実際にはコンパイルすらされないようになっています。これを活用すると、作成しているドライバが完成した後でも、効率に影響なくデバッグメッセージをソース上にそっと残しておくことが出来ます。
コンパイル
カーネルモジュールのコンパイルには、カーネルソース内に特別なツールが存在します。これを利用するために、少々、普通と違うMakefileを作ります。
必要最小限のMakefileは、次のとおりです。
obj-m := sample.o
PWD := $(shell pwd)
all:
make -C $(KERNEL_DIR) M=$(PWD) modules
clean:
make -C $(KERNEL_DIR) M=$(PWD) clean
これと、先にカーネルビルドの時に作ったmake_crowpiでの環境変数設定をともに利用すると、クロスコンパイルまで、他になんの指定もなく実行できます。
make手順は、sampleディレクトリ内で
ubuntu@localhost:~/crowpi2/sample$ . ./make_crowpi
ubuntu@localhost:~/crowpi2/sample$ make
とただそれだけです。一行目の環境変数取り込みは端末を開けた時に一回だけ実施すればOKです。ちなみに、忘れていると、「そんなディレクトリはないっ」とmakeに怒られますのでちゃんと気がつくことが出来ます。
これを実行すると、sampleディレクトリ内に、sample.ko
というファイルが成果物として出来ます。これが、カーネルモジュールです。
ubuntu@localhost:~/crowpi2/sample$ file sample.ko
sample.ko: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), BuildID[sha1]=9efd54818e16f2fc82b6b3e8e9fb8cd3e28cd0d9, not stripped
ubuntu@localhost:~/crowpi2/sample$ modinfo sample.ko
filename: /home/****/crowpi2/sample/sample.ko
author: name
description: This is sample driver.
license: GPL
srcversion: 8AC063DB1E8115DC34FE9F5
depends:
name: sample
vermagic: 5.4.79-v7l+ SMP mod_unload modversions ARMv7 p2v8
これを見ると、まず、ARM向けにコンパイルされた実行ファイルであることがわかります。また、modinfoの出力には、MODULE_***で指定した内容が提示されています。varmagicも重要です。これが、実行する実機のunameと合わないと、次に行うモジュールの組み込みを拒否されます。(実は、表面的にあっていてもなお拒否されることがあります。実際のチェック機構は複雑でここで表示されている文字だけでは実施されていません。)
実機への転送と組み込み。そして実行
さて、テストも最終段階です。出来上がったsample.koを実機に転送しましょう。
ubuntu@localhost:~/crowpi2/sample$ scp sample.ko __crowpi2:test/
とりあえず、実機のホーム配下のtestディレクトリに転送します。まぁ、どこに転送しても構いません。
さて、ここからは、CrowPi2実機で。先に転送したところにsample.koがあることを確認して、そのディレクトリに移動してください。
pi@crowpi2:~/test$ sudo insmod sample.ko
これで、カーネルに作ったばかりのカーネルモジュールが組み込まれます。何もなければ、なんの応答もなく終わります。モジュール形式が間違っていると文句を言われたときは、カーネルのインストールがうまく出来ていません。そのあたりをチェックしましょう。また、apt upgrade
などでカーネルが更新されてしまうと、やっぱり駄目です。もう一度カーネルのインストールをやり直します。その場合は、母艦のカーネルソースもgit pull
で更新して、カーネルビルドからやり直すことも視野に入れたほうが良いでしょう。
では、動作していることを確認しましょう。
カーネルモジュールのメッセージは、dmesg
コマンドで確認することが出来ます。
pi@crowpi2:~/test$ dmesg
# 中略 延々とログが続きます。
[ 86.721494] sample: loading out-of-tree module taints kernel.
[ 86.721828] driver loaded
大事なのは、最後の2行です。
最後から2行目は、sampleというカーネルモジュールが登録されたとを意味しています。out-of-tree moduleという表示は、今作ったモジュールがデバイスツリーに対応していないことを意味します。これは、また後の話題になります。
最後の行は、作ったカーネルモジュールのprintkで出力したメッセージです。実際には、赤く反転していると思います。ALERTの表示は目立ちますね。
また組み込まれているモジュールの一覧は、lsmod
コマンドで確認が出来ます。そのままやると山ほどのモジュールが表示されますので、grepで絞ります。
pi@crowpi2:~/test$ lsmod | grep sample
sample 16384 0
ちゃんとsampleの名前でモジュールが組み込まれています。
では、カーネルモジュールを取り外してみます。
pi@crowpi2:~/test$ sudo rmmod sample.ko
pi@crowpi2:~/test$
これもなんの応答もありませんが、無事外れています。dmesgを確認してみましょう。
pi@crowpi2:~/test$ dmesg
# 中略 延々とログが続きます。
[ 86.721494] sample: loading out-of-tree module taints kernel.
[ 86.721828] driver loaded
[ 683.314181] driver unloaded
最後の行で、sample_exitでprintkで出力した内容が表示されています。
モジュールの形式で作成したカーネルモジュールは、このように後で組み込み・解除が簡単に出来ます。デバッグの際には、組み込んだり、外したりをひたすら繰り返すことになります。
次回に続く
さて、ずいぶんと、長くなってしまいました。
ここまででもすでに、一つの記事としてはやり過ぎの感が有ります。【最初に気づけよ(笑)】
~~~ここから先は、(2)に分けることにします。近いうちにきっと・・・~~~
CrowPi2によるデバイスドライバ(2)に続きをあげました。
改定記録
【2/11】ちょっと大きな改定をしました。
変更点
- git clone時のshallow cloneに対する追記。ブランチが新設されるとうまくgit pullできない件。
- クロスコンパイラ変更。カーネルバージョン5.10.yをコンパイルしようとするとgithubから落としてきたツールチェーンではエラーになります。raspberrypiのidでアップしてあるツールなのでちょっと残念ですが、汎用のコンパイラに切り替えました。
さらに追記です。どうもいろいろ実験をしている最中ですが、カーネルビルド時のmakeには、ARCHとCROSS_COMPILEの引数は不要なようです。make_crowpiさえ環境に反映してあれば、ちゃんとコンパイルできているみたい。思いっきり、コマンドラインが短くなります。