はじめに
前回の記事「Windows から Mac デビューした C# 屋の組込み入門:UTM の仮想マシンを DE1-SoC につなぐ」では、Mac・Ubuntu・ボードの3つをつなぐ環境を作りました。
今回は、いよいよボードを動かします。
自作のカーネルモジュールで、ボードの LED を点けてみました。
組込みの「Hello World」ともいえる、Lチカの第一歩になります。
Windows の Quartus(FPGA の開発ツール)は使わず、SD カードで起動したボードの Linux を、Mac から操作しています。
開発の流れ
役割を3つに分けました。
-
Mac:VSCode で
Led_Module.cを書きます。 - Ubuntu:Mac から SSH でログインして、ARM 向けにクロスビルドします。
-
DE1-SoC:できた
.koファイルをscpでボードに送り、insmodで読み込みます。
Mac のターミナルを3枚開いて、それぞれに「ビルド」「転送」「実行」の役割を持たせると、作業のリズムがぐっとよくなりました。
使った環境
前回の記事の環境に、次のものを加えています。
| 項目 | 内容 |
|---|---|
| ボードのカーネル | 5.10.50-altera |
| カーネルソース | linux-socfpga |
| クロスコンパイラ |
arm-linux-gnueabihf-gcc 9.4.0(Ubuntu 20.04 のパッケージ) |
調べたこと:LED はどのレジスタで動くのか
DE1-SoC の HPS 側(ARM 側)のユーザー LED(HPS_LED)は、HPS_GPIO53 につながっています。
HPS の GPIO は、GPIO0 と GPIO1 のコントローラーに 29 本ずつ割り当てられています。
そのため、GPIO53 は GPIO1 コントローラーの 24 ビット目(GPIO1[24])になります。
| 項目 | 値 |
|---|---|
| GPIO1 コントローラーの物理アドレス | 0xFF709000 |
GPIO_SWPORTA_DR(データ) |
オフセット 0x00。1 を書くと High(点灯) |
GPIO_SWPORTA_DDR(方向) |
オフセット 0x04。1 を書くと出力 |
| LED のビット | 24 ビット目(1 << 24) |
LED は、次の2段階で点きます。
- 方向レジスタで、24 ビット目を出力にする
- データレジスタの 24 ビット目に 1 を書く
どちらも、ほかのビットを壊さないように、いったん読み出してから OR(消すときは AND)で書き戻します。
作ったもの:LED を点けるカーネルモジュール
コード
ボード上で実際に動いたコードを載せます。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/io.h>
#define GPIO2_BASE 0xFF709000 // HPS GPIO2の物理ベースアドレス
#define GPIO2_SIZE 0x1000 // マップするサイズ
#define GPIO_SWPORTA_DR 0x00 // データレジスタのオフセット
#define GPIO_SWPORTA_DDR 0x04 // 方向レジスタのオフセット
#define LED_BIT (1 << 24) // HPS_LEDは24ビット目
void __iomem *gpio_base;
static int __init led_init(void) {
uint32_t ddr; // 先頭で宣言
uint32_t dr; // 先頭で宣言
// 物理アドレスを仮想アドレスにマップ
gpio_base = ioremap(GPIO2_BASE, GPIO2_SIZE);
// GPIO24を出力に設定
ddr = ioread32(gpio_base + GPIO_SWPORTA_DDR); // 名前を定義に合わせる
iowrite32(ddr | LED_BIT, gpio_base + GPIO_SWPORTA_DDR);
// LEDを点灯
dr = ioread32(gpio_base + GPIO_SWPORTA_DR);
iowrite32(dr | LED_BIT, gpio_base + GPIO_SWPORTA_DR);
printk(KERN_INFO "L-Chika: LED ON!\n");
return 0;
}
static void __exit led_exit(void) {
// LEDを消灯
uint32_t dr = ioread32(gpio_base + GPIO_SWPORTA_DR);
iowrite32(dr & ~LED_BIT, gpio_base + GPIO_SWPORTA_DR);
iounmap(gpio_base);
printk(KERN_INFO "L-Chika: LED OFF! Goodbye.\n");
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
白状すると、コードの中の GPIO2 という名前とコメントは誤りで、当時は気づいていませんでした。
0xFF709000 は、GPIO1 コントローラーのアドレスになります。
動きには影響しないので、実際に動いたコードをそのまま載せています。
コードのポイントを3つ挙げます。
-
ioremap:カーネルは、物理アドレスを直接読み書きしません。
物理アドレス0xFF709000を仮想アドレスに対応付けてから、ioread32/iowrite32でレジスタを読み書きします。 -
__init/__exit:module_init/module_exitで、「読み込むとき」と「取り外すとき」に呼ばれる関数を登録します。
__initを付けた関数は、初期化が終わるとメモリから捨てられます。 -
戻り値:init 関数は
intを返して、成功(0)か失敗かをカーネルに伝えます。
exit 関数はvoidで、何も返しません。
Makefile
一般的な書き方の例を載せます(この記事のために動作を確かめたものではありません)。
ARCH と CROSS_COMPILE を Makefile に書いておくと、make とだけ打てばクロスビルドできます。
obj-m := Led_Module.o
# カーネルソースの場所(環境に合わせて変える)
KDIR := $(HOME)/linux-socfpga
all:
$(MAKE) -C $(KDIR) M=$(CURDIR) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
clean:
$(MAKE) -C $(KDIR) M=$(CURDIR) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean
ビルドと実行
Ubuntu で、古いビルド結果を消してから、クロスビルドします。
make clean
make
できた .ko ファイルを、Ubuntu からボードに送ります。
scp Led_Module.ko root@192.168.1.10:~/my_project/
ボードで読み込むと、LED が点きます。
取り外すと、LED が消えます。
insmod Led_Module.ko # LED が点く
dmesg | tail # 「L-Chika: LED ON!」が出る
rmmod Led_Module # LED が消える
つまずいたところ
asm/types.h が見つからない
ビルドすると、asm/types.h が見つからない、というエラーで止まりました。
原因は、ARCH=arm の付け忘れでした。
ARCH を付けないと、Ubuntu 自身のアーキテクチャ(今回は arm64)向けにビルドしようとして、ARM(32 ビット)用のヘッダーが見つかりません。
クロスビルドでは、ARCH と CROSS_COMPILE の両方が欠かせません。
上の Makefile のように書いておけば、付け忘れを防げます。
変数の宣言位置で警告が出る
関数の途中で変数を宣言したところ、警告が出ました。
カーネルのビルドには -Wdeclaration-after-statement が付いていて、変数をブロックの先頭で宣言しないと警告になります(古い C の規格、C90 の流儀)。
コードの中の // 先頭で宣言 というコメントは、このときの名残りです。
void の関数で return 0; を書いてエラー
exit 関数(void)に、うっかり return 0; を書いてしまい、ビルドが止まりました。
カーネルのビルドには -Werror=return-type が付いているので、普通なら警告で済むところが、エラーになります。
おわりに
LED が1つ点いただけなのに、思わず声が出ました。
レジスタに1ビット書いただけで、それが目に見える光になる感覚は、業務アプリではなかなか味わえません。
この段階では、insmod すると点きっぱなしで、外から操作することはできません。
それでも、Mac だけで「書く → ビルドする → ボードで動かす」が一周できたことは、大きな一歩になりました。
同じように、はじめてカーネルモジュールを書く方の参考になればうれしいです。
C# 屋から見たカーネルモジュール
module_exit(led_exit); は、C# のイベント登録(button.Click += OnClick;)に似ていると感じました。
どちらも「何かが起きたら、この関数を呼んでください」と、あとで呼ばれる関数を登録しておくしくみになっています。
見慣れない世界でも、知っている考え方に置き換えると、ずいぶん身近に感じられました。