0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C# 屋の組込み入門:自作カーネルモジュールで、DE1-SoC のいわゆる「Lチカ」に挑戦する

0
Posted at

はじめに

前回の記事「Windows から Mac デビューした C# 屋の組込み入門:UTM の仮想マシンを DE1-SoC につなぐ」では、Mac・Ubuntu・ボードの3つをつなぐ環境を作りました。
今回は、いよいよボードを動かします。
自作のカーネルモジュールで、ボードの LED を点けてみました。
組込みの「Hello World」ともいえる、Lチカの第一歩になります。

Windows の Quartus(FPGA の開発ツール)は使わず、SD カードで起動したボードの Linux を、Mac から操作しています。

開発の流れ

役割を3つに分けました。

  1. Mac:VSCode で Led_Module.c を書きます。
  2. Ubuntu:Mac から SSH でログインして、ARM 向けにクロスビルドします。
  3. 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段階で点きます。

  1. 方向レジスタで、24 ビット目を出力にする
  2. データレジスタの 24 ビット目に 1 を書く

どちらも、ほかのビットを壊さないように、いったん読み出してから OR(消すときは AND)で書き戻します。

作ったもの:LED を点けるカーネルモジュール

コード

ボード上で実際に動いたコードを載せます。

Led_Module.c
#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 とだけ打てばクロスビルドできます。

Makefile(例)
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;)に似ていると感じました。
どちらも「何かが起きたら、この関数を呼んでください」と、あとで呼ばれる関数を登録しておくしくみになっています。
見慣れない世界でも、知っている考え方に置き換えると、ずいぶん身近に感じられました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?