はじめに
seeed社のreTerminalという端末を手に入れました。中身は、Raspberry Pi Computer Module 4に、4GのRAM、32GのeMMC、5インチのタッチパネル他を搭載したものです。
初期に搭載されているOSは、若干古めのRaspberryPi OSに独自のドライバを組み込んだものです。が、比較的簡単に最新のRaspberryPi OSに載せ替えることも可能です。
今回は、現時点で、公式でリリースされている最新のRaspberry Pi OS(64-bit)(2022-4-4リリース)にOSを換装した機体で、ubuntu22.04を母艦としてカーネルモジュールのクロスコンパイル環境を構築するまでのメモです。
ターゲットと、開発母艦の環境
ターゲットの環境
ターゲットのOS環境は、次の通り。
pi@reterminal:~ $ uname -a
Linux reterminal 5.15.32-v8+ #1538 SMP PREEMPT Thu Mar 31 19:40:39 BST 2022 aarch64 GNU/Linux
母艦の環境
母艦になるubuntu機の環境は次の通り
aaa@aaaa-linux: $ uname -a
Linux aaaa-linux 5.15.0-33-generic #34-Ubuntu SMP Wed May 18 13:34:26 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
aaa@aaaa-linux: $ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy
開発環境の構築
概略
linuxのドライバ(カーネルモジュール)を開発するためには、対象となるカーネルのソース一式が必要となります。
また、開発する母艦とターゲットになるreTerminalのCPUが異なります。そのため、母艦上で動く、arm64用のコンパイラ一式(ツールチェーンと称されます。)が必要となります。
基本的な手順は、ここにあります。
母艦のツールチェーンの用意
まず、母艦上に必要なソフト群のインストールです。
sudo apt-get update
sudo apt-get -y install git build-essential libncurses5-dev bc bison flex libssl-dev
sudo apt install crossbuild-essential-arm64
reTerminal側で必要なユーティリティーの準備
母艦上には、reTerminalで実行されているものと同じバージョンのlinuxソースが必要です。
基本的には、RaspberryPi公式で、ここに揃っていますが、この中から同じバージョンを探す必要があります。
ちょっと面倒なので、手抜きをします。そのために、reTerminal上でrpi-sourceというユーティリティーを利用します。
sudo apt install git bc bison flex libssl-dev python2
sudo wget https://raw.githubusercontent.com/RPi-Distro/rpi-source/master/rpi-source -O /usr/local/bin/rpi-source
sudo chmod +x /usr/local/bin/rpi-source
sudo rpi-source -q --tag-update
詳細は、ここを参照してください。
reTerminalでの作業
つづけて、reTerminal上での作業が続きます。
rpi-sourceを実行して、linuxのソースがある場所を確認します。
pi@reterminal:~ $ mkdir kernel
pi@reterminal:~ $ sudo rpi-source --skip-gcc -d /home/pi/kernel
*** SoC: BCM2711
*** Using: /usr/share/doc/raspberrypi-bootloader/changelog.Debian.gz
*** Latest firmware revision: 61966732d03de9b71baf561f920e018b54c241ac
*** Linux source commit: 6f921e98008589258f97243fb6658d09750f0a2f
*** Download kernel source
--2022-06-05 16:46:23-- https://github.com/raspberrypi/linux/archive/6f921e98008589258f97243fb6658d09750f0a2f.tar.gz
Resolving github.com (github.com)... 52.192.72.89
Connecting to github.com (github.com)|52.192.72.89|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://codeload.github.com/raspberrypi/linux/tar.gz/6f921e98008589258f97243fb6658d09750f0a2f [following]
--2022-06-05 16:46:24-- https://codeload.github.com/raspberrypi/linux/tar.gz/6f921e98008589258f97243fb6658d09750f0a2f
Resolving codeload.github.com (codeload.github.com)... 13.112.159.149
Connecting to codeload.github.com (codeload.github.com)|13.112.159.149|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [application/x-gzip]
Saving to: ‘/home/mito/kernel/linux-6f921e98008589258f97243fb6658d09750f0a2f.tar.gz’
【以下略】
みたいな感じで表示が続き、linuxのソースファイルのダウンロードが始まります。
このユーティリティーは、reTerminal上に自己コンパイルでlinuxカーネルを生成するための環境を整えてくれるものです。
が、今回必要なのは、この表示です。「*** Download kernel source」の下に、「https://github.com/raspberrypi/linux/archive・・・tar.gz」とURLが表示されます。これが、目的のlinuxソースの在処です。
このURLをメモしておきます。(母艦からsshでつないでいるなら、コピーしちゃいましょう。それが一番間違いがなく楽です。)
それさえ取得すれば、「ctrl-c」で止めてしまっても構いません。
「~/kernel/の下に生成されたファイルも全部削除してOKです。
linuxソースファイルの準備(母艦上にて)
ここからは、母艦のubuntuマシンでの作業となります。
まず、作業用のディレクトリを用意します。
このディレクトリは、今後、ドライバを開発するためのトップディレクトリとなります。今回は、「~/reterminal」としておきます。
mkdir reterminal
cd reterminal
mkdir kernel
cd kernel
wget https://github.com/raspberrypi/linux/archive/****.tar.gz
tar -xvf ****.tar.gz
ln -s linux-**** linux
ここで、「***」の部分は、先にメモしたファイル名に置き換えてください。
これで、reterminalの配下には、tar.gzファイルと展開済みのlinuxで始まるディレクトリがあるはずです。今後、このディレクトリを簡単にアクセスできるようにシンボリックリンクを作っておきます。
linuxカーネルコンパイルオプション群の設定ファイルの取得
さて、linuxのカーネルをコンパイルするには、ゆうに4桁後半個ほどあるオプションの数々を正しく設定する必要があります。設定には、対象のハードウェアとlinuxカーネルへの深い知識が必要です。また、この設定は現在reTerminalで動いているlinuxと同じである必要があります。
ちょっと最初から構築するのは不可能に近いので現在動いているreTerminalの設定を頂いてくることにします。
この作業は、reTerminal上で行います。
pi@reterminal:~ $ mkdir temp
pi@reterminal:~ $ cd temp
pi@reterminal:~/temp $ sudo modprobe configs
pi@reterminal:~/temp $ sudo zcat /proc/config.gz > .config
pi@reterminal:~/temp $ ls -a
. .. .config
これで、「.config」というテキストファイルが手に入ります。これが、linuxカーネルをコンパイルする際の設定ファイルとなります。
このファイルをubuntu上の母艦の「reterminal/kernel/linux」の下にコピーします。
sshでつながっているなら、scpがお手軽でしょう。
ここからは、ubuntu母艦上で、
cd ~/reterminal/kernel/linux
scp reterminal.local:temp/.config .
ubuntu母艦の環境変数の設定
カーネルとカーネルモジュールのクロスコンパイルをするにあたって、必要ないくつかの環境変数があります。
作業の度に設定が必要なので、スクリプトにまとめておきます。
~/reterminalの下に、envsetという名前で、ファイルを作ります。内容は、次のようになります。
#! /bin/bash
export RETERMINAL=~/reterminal
export KERNEL_SRC=${RETERMINAL}/kernel/linux/
export DTC_DIR=${KERNEL_SRC}/scripts/dtc/
export CCPREFIX=aarch64-linux-gnu-
export KERNEL_DIR=${KERNEL_SRC}
export KERNEL=kernel8
export ARCH=arm64
export CROSS_COMPILE=$CCPREFIX
RETERMINALの内容は、お使いの環境に合わせて変更してください。
これを
cd ~/reterminal
chmod 777 envset
. ./envset
で適用します。
最後の行は、ターミナルウィンドウを開ける度に必要です。忘れると、makeをした際に文句を言われますので、まぁ、気がつくでしょう。
ちなみに、これは、reTerminalが64bit OSで動いているときの設定です。32bitの際には、KERNELの部分がkernel7に、ARCHの部分がarmに、さらに、CCPREFIXの部分が、arm-linux-gnueabihf-になります。また、ツールチェーンも32bit用のものが必要です。
reTerminalのlinuxバージョンの確認。
さて、ほんの少しだけ、.configファイルに修正が必要です。これをやらないと正確に同じカーネルソースと認めてもらえないようです。
まず、reTerminal上で、linuxのバージョンを確認します。
pi@reterminal:~/temp $ uname -a
Linux reterminal 5.15.32-v8+ #1538 SMP PREEMPT Thu Mar 31 19:40:39 BST 2022 aarch64 GNU/Linux
この結果の3項目と4項目がバージョンに関わる内容です。この場合だと、「5.15.32」の部分がlinuxのバージョンとなります。次の「-v8+」がlocal versionを言われる部分になります。ここは、自分でlinuxをビルドしたときに他のビルドと区分するためにつけます。さらに、次の#1538の部分がBuild ID Saltという数字のようです。
local versionとBuild ID Saltをメモっておきます。
母艦上にて、.configファイルの設定変更
では、母艦上の.configファイルを修正します。
わかりやすく、GUIでやりましょう。
cd ~/reterminal/kernel/linux
. ../../envset
make xconfig
左上のペインで、general setupを選ぶと、画像の右ペインが出てきます。
ここで、Local versionを、先にメモった「-v8+」に修正します。さらに、Build ID Saltを、同じく先にメモった「#1538」に修正します。(調べた内容に合わせて、修正内容は適時変えてください。)
メニューのfileから、saveを実行してセーブします。そして、終了すれば、.configの内容は変更されます。
linuxカーネルのビルド
さて、今用意したlinuxカーネルをビルドします。ビルドした結果の一部が、自分で作成したカーネルモジュールのビルドの際に必要となります。
cd ~/reterminal/kernel/linux
. ../../envset
make -j20 Image modules dtbs
とやれば、カーネル一式のビルドが実行されます。
うちの環境では、10分程度かかりました。環境によってはもっとかかるかもしれません。「-j20」の部分は、並列処理の指定です。コア数の1.5倍程度が適切と言われています。
今回のメモでは、全部のコマンドラインに、envsetの実行をかけていますが、連続した作業でやるなら、当然不要です。
エラーなく、実行できれば、これで、環境の構築は終了となります。
テストモジュールの作成
さて、出来上がった環境で、テストモジュールを作成してみます。
お題は、恒例のhello worldです。
先程のreterminalの下に、dirver/helloフォルダを作成します。
cd ~/reterminal
mkdir driver
cd driver
mkdir hello
cd hello
この中に、hello.cの名前で、ソースを作成します。
hello.cの内容は、次の通り。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <linux/siphash.h>
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Hello driver.");
MODULE_AUTHOR("mito");
struct hello_driver {
struct device_driver driver;
};
static int hello_init(struct hello_driver *drv) {
pr_alert(KERN_ALERT "hello world!\n");
return 0;
}
static void hello_exit(struct hello_driver *drv) {
pr_alert("bye bye!\n");
}
static struct hello_driver hello_drv = {
.driver = {
.name = "hello driver",
.of_match_table = NULL,
},
};
module_driver(hello_drv, hello_init, hello_exit);
実行内容は、単に起動されたら、hello world!と、システムログにつぶやき、終了時に、システムログに、bye byeとつぶやくだけの何の役にもたたないドライバです。
最初のMODULE***は、このモジュールの概要を記します。この内容が、modinfoを実行したときに表示されます。MODULE_LICENSEだけは注意が必要です。ここが、適正なライセンスでないと、呼び出せないシステムモジュールが数多く存在します。ここでは、一般的にGPLとしておきます。
hello_driver構造体は、このモジュールの内容をシステムに登録するためのものです。
module_driverマクロで、この構造体と、初期化関数・終了関数を指定すると、システムにドライバが登録されます。
hello_initとhello_exitが、その関数になります。やっていることは、pr_alertでシステムログにアラートメッセージをつぶやいているだけです。本来は、適正なレベルでメッセージを出すべきですが、今回は、お遊びに近いので、派手な表示のものを。
このソースをコンパイルするためのMakefileが必要です。
普通のプログラムを作るときとちょっと色合いが違います。
obj-m := hello.o
PWD := $(shell pwd)
all:
make -C $(KERNEL_DIR) M=$(PWD) modules
clean:
make -C $(KERNEL_DIR) M=$(PWD) clean
さて、hello.cとMakefileを用意すれば、あとは、makeするだけです。
setenvを忘れずに・・・
cd ~/reterminal/driver/hello
. ../../setenv
make
エラーがなければ、hello.koというファイルが出来ているはずです。
ファイルを確認してみましょう。
***@****:~/reterminal/driver/hello$ modinfo hello.ko
filename: /home/data/mito/develop/re_terminal/driver/hello/hello.ko
author: mito
description: Hello driver.
license: GPL
srcversion: CAF04A91FC843AEC3956178
depends:
name: hello
vermagic: 5.15.32-v8+ SMP preempt mod_unload modversions aarch64
authorとdeskriptionの内容は、先のソースでMODULE***で指定した内容そのままです。
vermagicは、どのlinuxカーネルに対して作成されたモジュールかが記されています。一番うしろがaarch64になってますから、arm64用のブログラムであることがわかります。linuxカーネルバージョンも記されています。
実は、このvermagicの内容がカーネルモジュールを実行する際に実行中のlinuxカーネルと比較されます。(実は、vermagicの内容はここに表示されているより更に内容があるようです。)ここに差異があるとモジュールを実行した際に、「insmod: ERROR: could not insert module hello.ko: Invalid module format」とエラーになり実行できません。そのために、環境構築のときに、reTerminalで実行されているのと同じもの同じものと努力してきたわけです。
このファイルをreterminalの適当な場所にコピーすれば、実行することが出来ます。
reterminalのhomeディレクトリに、testとでもディレクトリを作成して、そこに、hello.koをコピーします。
cd ~/reterminal/driver/hello
scp hello.ko reterminal.local:test
さて、reterminal側で、モジュールを実行してみます。
pi@reterminal:~ $ cd test/
pi@reterminal:~/test $ ls
hello.ko
pi@reterminal:~/test $ sudo insmod hello.ko
pi@reterminal:~/test $ lsmod | grep hello
hello 16384 0
pi@reterminal:~/test $ dmesg | tail
[ 614.191724] [DSI]panel_disable:
[ 614.195172] [DSI]panel_unprepare:
[20876.307705] hello world!
pi@reterminal:~/test $ sudo rmmod hello.ko
pi@reterminal:~/test $ dmesg | tail
[ 614.195172] [DSI]panel_unprepare:
[20876.307705] hello world!
[20953.362358] bye bye!
と言った感じです。
作成したカーネルモジュールは、insmodコマンドでシステムに登録することが出来ます。
現在実行しているカーネルモジュールの一覧は、lsmodで見ることが出来ます。helloモジュールが実行されていることがわかります。
dmesgを実行すると、カーネルメッセージを一覧できます。長いので、tailをつけておきます。最後に、hello world!のつぶやきが表示されています。
rmmodで登録したカーネルモジュールを取り外すことが出来ます。
もう一度、dmesgを確認すると、bye byeのつぶやきを確認することが出来ます。