体験!マイコンボードで組込みLinux

第13回組込みLinux上での割り込み

一般的なデバイスドライバ

デバイスドライバについて

今回はSH7706LSRのLinux上での割り込みを扱います。割り込みはハードウェア資源なので、ユーザプログラムでは扱うことはできず、デバイスドライバでの扱いが必須となります。まずは、SH7706LSR上にある汎用スイッチと汎用LEDの入出力を行う一般的なデバイスについてプログラミングをしてみます。

デバイスドライバもユーザプログラムと同様のソフトウェアですが、デバイスドライバはLinuxカーネルの一部分として動作しますので、Linuxカーネル内部実装に従ったプログラミング作法が要求されます。ユーザプログラムはLinuxカーネルと切り離されているので、Linuxカーネルのバージョンが新しくなっても基本的にはユーザプログラムの変更が必要になることはないですが、デバイスドライバではLinuxカーネル内部実装の進化にあわせてデバイスドライバも変更していかなければなりません。

Linuxカーネルのバージョンアップによって、突然、デバイスドライバが不正な動作をしたり、コンパイルが通らないことが珍しくありません。Linuxカーネル内部に関する情報は乏しいので、この点がデバイスドライバのハードルを高くしている原因かもしれません。

Linuxデバイスドライバにはいくつもの種類があり、それらのなかでキャラクタ型デバイスは比較的にハードルも低く、Linuxカーネル内部の実装における変化の影響を受けにくい傾向にあるので、今回は、このキャラクタ型デバイスでデバイスドライバのプログラミングを行います。

対象ハードウェアについて

デバイスドライバの対象とするハードウェアは、SH7706LSR上にある汎用スイッチと汎用LEDです。汎用スイッチはSH7706プロセッサのPTG4(ポートG 4ビット目)の端子とGND端子の間に配線されています。ボード初期化時にコントロールレジスタによる端子設定済みで、入力プルアップの設定になっています。

スイッチはプルアップになっているため、スイッチを押していない(すなわちOFF)時はプルアップにより信号レベルは「1」となっており、スイッチを押す(すなわちON)とGND端子と短絡するために信号レベルは「0」になっています。スイッチ状態と信号レベルが逆になっているので、汎用スイッチは負倫理となります。

汎用LEDはSH7706プロセッサのSCP4(ポートSC 4ビット目)の端子とGND端子の間に配線されています。ボード初期化時にコントロールレジスタによる端子設定済みで、出力設定になっています。LED点灯状態と信号レベルが同じなので、汎用LEDスイッチは正倫理となります。

ハードウェアアクセスについて

デバイスドライバはLinuxカーネルの一部分なのでI/Oポートを直接にアクセスできますが、PC以外の組込みプロセッサの場合は少々注意が必要となります。

Linuxはもともとインテルi80386プロセッサのプロテクトモードを活用するためのソフトウェアが出発点となっているので、現在でもLinuxはIBM PCアーキテクチャのみを対象としたシステムであり、PC以外の組込みプロセッサではIBM-PCをエミュレーション部分を追加して、擬似的なIBM-PCとして動作しています。

そのため、I/Oポートをアクセスするための outx_p関数、inx_p関数はPC-Linuxでは正常に動作をしますが、PC以外の組込みプロセッサでは正常に動作をしません。SHプロセッサでのLinuxでは、ctrl_outx関数、ctrl_inx関数でSHプロセッサのハードウェアにアクセスを行います。

また、LinuxカーネルではSH7706内蔵レジスタが定義されているケースもありますが、ほとんどの内蔵レジスタの定義がされていないので、個別のデバイスドライバでSH7706内蔵レジスタの定義を行う必要があります。SH7706の汎用入出力ポートに関する定義はLinuxでされていないので、汎用スイッチと汎用LEDを使う場合は以下のように個別に定義をします。

#define PORT_PGDR	0xa400012cUL
#define PORT_SCDR	0xa4000136UL

汎用スイッチと汎用LEDのデバイスドライバ

デバイスドライバの入力仕様は、汎用スイッチがOFFの場合は「0」という文字が入力され、汎用スイッチがONの場合は「1」という文字が入力されるようにします。デバイスドライバの出力仕様は、⁠0」という文字が出力された場合はLEDが消灯し、⁠1」という文字が出力された場合はLEDが点灯し、それ以外の文字の場合は何もしないようにします。標準的なデバイスドライバの場合はデバイスドライバのメジャー番号が定義されていますが、そうでない場合はメジャー番号が定義されていないので、今回はLinuxカーネルに動的にメジャー番号の割り当てを任せるようにします。

汎用スイッチと汎用LEDのデバイスドライバのソースコードはリスト1となります。

リスト1

01: #include <linux/module.h>
02: #include <linux/init.h>
03: #include <linux/device.h>
04: #include <linux/ctype.h>
05: #include <linux/poll.h>
06: #include <asm/io.h>
07: 
08: #define PORT_PGDR    0xa400012cUL
09: #define PORT_SCDR    0xa4000136UL
10: 
11: static int    leddev_major;
12: 
13: static ssize_t led_read(struct file * file, char __user * buf, size_t count, loff_t * ppos) {
14:     ssize_t    n;
15:     char    c;
16: 
17:     for(n = 0;n < count;n++) {
18:         if(ctrl_inb(PORT_PGDR) & 0x10) {
19:             c = '0';
20:         } else {
21:             c = '1';
22:         }
23:         copy_to_user(buf + n, &c, 1);
24:     }
25:     return count;
26: }
27: 
28: static ssize_t led_write(struct file * file, const char __user * buf, size_t count, loff_t * ppos) {
29:     ssize_t    n;
30:     char    c;
31: 
32:     for(n = 0;n < count;n++) {
33:         copy_from_user(&c, buf + n, 1);
34:         if(c == '1') {
35:             ctrl_outb(ctrl_inb(PORT_SCDR) | 0x10, PORT_SCDR);
36:         }
37:         if(c == '0') {
38:             ctrl_outb(ctrl_inb(PORT_SCDR) & ~0x10, PORT_SCDR);
39:         }
40:     }
41:     return count;
42: }
43: 
44: static const struct file_operations led_fops = {
45:     .owner    = THIS_MODULE,
46:     .read    = led_read,
47:     .write    = led_write,
48: };
49: 
50: #define CHRDEV "leddev"
51: static int __init leddev_init (void) {
52:     leddev_major = register_chrdev(0, CHRDEV, &led_fops);
53:     printk(KERN_INFO "LED/SW Device Driver\n");
54:     return 0;
55: }
56: 
57: static void __exit leddev_cleanup (void) {
58:     unregister_chrdev(leddev_major, CHRDEV);
59:     printk(KERN_INFO "LED/SW Device Driver Exit\n");
60: }
61: 
62: module_init(leddev_init);
63: module_exit(leddev_cleanup);
64: 
65: MODULE_LICENSE("GPL");

通常のユーザプログラムとLinuxとの呼び出しインターフェースはmain関数となりますが、キャラクタ型デバイスドライバとLinuxカーネルとのインターフェースは初期化関数と終了関数の2つです。ソースコード内での初期化関数の定義はリスト1の62行目のmodule_initのパラメータで初期化関数を指定します。ソースコード内での終了関数の定義はリスト1の63行目の module_exitのパラメータで終了関数を指定します。

初期化関数と終了関数の名称は任意でかまいません。初期化関数では対象となるハードウェアの初期化やキャラクタ型デバイスドライバの登録を行います。今回はハードウェアの初期化は不要なので、リスト1の52行目のregister_chrdev関数でキャラクタ型デバイスドライバの登録をします。

register_chrdev関数の第1引数はメジャー番号を指定しますが、動的にメジャー番号の割り当てる場合は「0」を指定、関数の戻り値がメジャー番号となり、第2引数はデバイスドライバ名称、第3引数はファイル入出力関数構造体のポインタを指定します。リスト1でのファイル入出力関数構造体は44~48行目となります。

終了関数では対象となるハードウェアの停止処理やキャラクタ型デバイスドライバの登録解除を行い、リスト1の58行目のunregister_chrdev関数でキャラクタ型デバイスドライバの登録解除をします。

汎用スイッチと汎用LEDのデバイスドライバでは入力と出力のみを行うので、ファイル入出力関数構造体では .readメンバでは汎用スイッチを対象とした led_read関数を定義し(リスト1の46行目⁠⁠、.writeメンバでは汎用LEDを対象とした led_write関数を定義します(リスト1の47行目⁠⁠。ファイル入出力関数構造体での.readメンバと.writeメンバの関数仕様の引数で必要となるのが、第2引数のデータ領域のポインタと第3引数のデータサイズとなります。

ここで気をつけなければいけない点は、ユーザプログラムとLinuxカーネルではメモリ空間そのものが異なります。入力関数では、リスト1の23行目のようにcopy_to_user関数でLinuxカーネル空間のメモリをユーザ空間のメモリへコピーしなければなりません。出力関数では、リスト1の33行目のようにcopy_from_user関数でユーザ空間のメモリをLinuxカーネル空間のメモリへコピーしなければなりません。

デバイスドライバのコンパイル

今回は適当な開発用のフォルダ内にさらにデバイスドライバ用のフォルダを用意し、開発用フォルダにLinuxカーネルアーカイブとパッチをコピーし、以下のように展開します。

# tar xvjf linux-2.6.28.10.tar.bz2
# cd linux-2.6.28.10
# patch -p1 < ../linux-2.6.28.10-shmin-3.patch
# cd ..

デバイスドライバ用のフォルダにはソースコード(leddev.c)とMakefileを用意します。Makefileの内容はリスト2となります。

リスト2
TARGET:= leddev.ko

all: ${TARGET}

leddev.ko: leddev.c
	make ARCH=sh CROSS_COMPILE=sh3-linux- -C ../linux-2.6.28.10 M=`pwd` modules

clean:
	make ARCH=sh CROSS_COMPILE=sh3-linux- -C ../linux-2.6.28.10 M=`pwd` clean

obj-m:= leddev.o

clean-files := *.o *.ko *.order *.mod.[co] *~

デバイスドライバのコンパイルは以下のようにします。

# make

デバイスドライバはleddev.koで、SH7706LSR上のLinuxファイルシステムの/lib/modules/2.6.28.10/にコピーをします。

デバイスドライバの実行

SH7706LSR上でLinuxを起動させて、以下のようにデバイスドライバを組込みます。

~ # insmod /lib/modules/`uname -r`/leddev.ko
LED/SW Device Driver

デバイスドライバを組み込んだらメジャー番号が割り当てられます。これを用いてリスト3のスクリプトでデバイスドライバ用のデバイスファイルを作成します。

リスト3

01: #!/bin/sh
02: 
03: for item in `cat /proc/devices`;
04: do
05:   if [ $item = leddev ]; then
06:     mknod /dev/leddev c $major 0
07:   fi
08:   major=$item
09: done

デバイスドライバの情報は、/proc/devicesに反映されるので、その情報をもとにデバイスファイルを作成します。デバイスドライバへアクセスするユーザプログラムの言語は何でもいいですが、ここではperl言語を使います。汎用スイッチの状態を読み出すスクリプトはリスト4となります。

リスト4

01: #!/usr/bin/perl
02: 
03: open($fd, "</dev/leddev") or die "Cannot open /dev/leddev: $!";
04: $c = getc $fd;
05: print $c;                                                                        
06: close $fd

汎用LEDの制御をするスクリプトはリスト5となり、LEDを消灯させる場合はリスト5の4行目の内容を「0」に変更します。

リスト5

01: #!/usr/bin/perl
02: 
03: open($fd, ">/dev/leddev") or die "Cannot open /dev/leddev: $!";
04: print $fd "1";
05: close $fd;

デバイスドライバの切り離しは以下のようにします。

~ # rmmod leddev
LED/SW Device Driver Exit

Linuxでのハードウェア割り込み利用

タイマー割り込みドライバ

組込みボードではハードウェア割り込みを使うことが多いですが、それらのハードウェア割り込みのなかでもタイマー割り込みはよく使われます。Linuxカーネルでのタイマー割り込みに関する部分は変化の度合いが多いのでカーネルのバージョンに依存する傾向にあります。ここではLinuxカーネル2.6.28.xを対象にします。

SH7706のタイマーはTMUと呼ばれTMU0~TMU2までの3チャンネル存在しますが、このうちTMU0はLinuxカーネル内部の中核となるタイムベースとして使っているので、自由に使えるのはTMU1とTMU2の2つのみとなります。ここではTMU1を対象としたLinux上で動作するタイマー割り込みドライバについて扱います。タイマー割り込みドライバのソースコードはリスト6となります。

リスト6

01: #include <linux/init.h>
02: #include <linux/irq.h>
03: #include <linux/interrupt.h>
04: #include <linux/module.h>
05: #include <asm/signal.h>
06: #include <asm/irq.h>
07: #include <asm/io.h>
08: #include <asm/timer.h>
09: 
10: #define PORT_SCDR	0xa4000136UL
11: 
12: #define	TSTR_TMU1	0x02
13: #define	TCR_UNF		0x0100
14: #define	TCR_UNIE	0x0020
15: #define	TCR_UP_EDGE	0x0000
16: #define	TCR_DOWN_EDGE	0x0008
17: #define	TCR_BOTH_EDGE	0x0010
18: #define	TCR_CLK4	0x0000
19: #define	TCR_CLK16	0x0001
20: #define	TCR_CLK64	0x0002
21: #define	TCR_CLK256	0x0003
22: 
23: #define TIMER1_IRQ	17
24: 
25: static char *id="tmu1 interrupt";
26: 
27: static irqreturn_t tmu1_interrupt(int irq, void *dev_id) {
28:     ctrl_outb(ctrl_inb(PORT_SCDR) ^ 0x10, PORT_SCDR);
29:     ctrl_outw(ctrl_inw(TMU1_TCR) & ~TCR_UNF, TMU1_TCR);        // TCR_1 &= ~TCR_UNF;
30:     return IRQ_HANDLED;
31: }
32: 
33: static int tmu1_init(void) {
34:     int    i;
35: 
36:     i = request_irq(TIMER1_IRQ, tmu1_interrupt, 0, "tmu1_int", id);
37:     if (i < 0) return i;
38: 
39:     ctrl_outb(ctrl_inb(PORT_SCDR) | 0x10, PORT_SCDR);
40: 
41:     ctrl_outw(TCR_UNIE | TCR_UP_EDGE | TCR_CLK4, TMU1_TCR); // 0.125uS(8MHz)
42:     ctrl_outl(8000000, TMU1_TCOR);
43:     ctrl_outl(8000000, TMU1_TCNT);
44:     ctrl_outb(ctrl_inb(TMU_012_TSTR) | TSTR_TMU1, TMU_012_TSTR);
45: 
46:     printk(KERN_INFO "The interrupt driver by TMU1\n");
47:     return 0;
48: }
49: 
50: static void tmu1_exit(void) {
51:     ctrl_outb(ctrl_inb(PORT_SCDR) & ~0x10, PORT_SCDR);
52:     ctrl_outw(ctrl_inw(TMU1_TCR) & ~TCR_UNIE, TMU1_TCR);
53:     ctrl_outb(ctrl_inb(TMU_012_TSTR) & ~TSTR_TMU1, TMU_012_TSTR);
54:     free_irq(TIMER1_IRQ, id);
55:     printk(KERN_INFO "The interrupt driver removed\n");
56: }
57: 
58: module_init(tmu1_init);
59: module_exit(tmu1_exit);
60: 
61: MODULE_LICENSE("GPL");

仕様としてはTMU1を1秒間隔でタイマー割り込みを発生させ、併せて汎用LEDを割り込み関数内で点滅させています。デバイスドライバの基本的な構造はすでに前章に解説したので、主に割り込みに関する部分について解説します。初期化関数(リスト6の33~48行目)では、割り込みの登録をrequest_irq関数(リスト6の36行目)で行います。

第1引数は割り込み番号、第2引数は割り込み関数、第4引数は割り込みデバイスドライバ名称、第5引数は任意のIDを指定します。割り込み番号は15以下はIBM-PCの割り込み番号で16以上はプロセッサのアーキテクチャーに依存します。SH7706でのTMU関係の割り込み番号は以下のとおりです。

  • TMU0割り込み 16番
  • TMU1割り込み 17番
  • TMU2割り込み 18番

TMU1の設定はリスト6の41行目でTMU1の割り込みを有効にし、TMU1のカウントを0.125マイクロ秒に設定しています。TMU1のオーバフローカウント値はリスト6の42~43行目で、8000000を指定しています。0.125マイクロ秒の8000000回は1秒になるので、タイマー割り込み間隔は1秒となります。リスト6の44行目ではTMU1のカウントを開始させています。割り込み関数(リスト6の27~31行目)では、TMU1の割り込みフラグをクリア(リスト6の29行目)が必須の処理となります。

デバイスドライバのコンパイルと実行

デバイスドライバ用のフォルダにはソースコード(tmu1.c)とMakefileを用意します。Makefileの内容はリスト7となります。

リスト7
TARGET:= tmu1.ko

all: ${TARGET}

tmu1.ko: tmu1.c
	make ARCH=sh CROSS_COMPILE=sh3-linux- -C ../linux-2.6.28.10 M=`pwd` modules

clean:
	make ARCH=sh CROSS_COMPILE=sh3-linux- -C ../linux-2.6.28.10 M=`pwd` clean

obj-m:= tmu1.o

clean-files := *.o *.ko *.order *.mod.[co] *~

デバイスドライバのコンパイルは以下のようにします。

# make

デバイスドライバは tmu1.ko で、SH7706LSR上のLinuxファイルシステムの /lib/modules/2.6.28.10/にコピーをします。SH7706LSR上でLinuxを起動させて、以下のようにデバイスドライバを組込みます。

~ # insmod /lib/modules/`uname -r`/tmu1.ko
The interrupt driver by TMU1

次回は

次回は組込みボードの表示デバイスで多用されるLCDをターゲットとしてデバイスドライバについて解説します。

おすすめ記事

記事・ニュース一覧