2009年5月16日土曜日

Linuxのモジュール化されたデバイスドライバ#2

さて、デバイスドライバをモジュールの形で作るには、何が必要になって来るか。

まず、Linux実行環境と同じバージョンの、Linuxソースコードが必要です。
ディストリビューションや組み込み機器環境によっては、オリジナルのコードにパッチを当てている事もあるかも知れません。
ソースコードのアーカイブの中には、/Documentation サブディレクトリがあって、この中にデバイスドライバを作る時のヒントになるようなファイルが数多くあります。公式な、最新のドキュメントですから、当たり前なんですけどね。

次に Makefile の記述に慣れた方が良いでしょう。
make コマンドは、カーネルを作る為の特別な拡張がされています。特別な拡張なので、一部の限られた本やWebなどのメディアしか登場してません。

そしてソースコード。デバイスドライバのソースコード記述の仕方も、いろいろと拡張されています。

さて、いろいろと並べるよりもソースコードを見た方が早いと思うので、以下にモジュール化されたデバイスドライバを示します。このドライバは、簡単なI/Oの読み書きをデバイスの read / write として見せるようになってます。実際に使う場合は、このデバイスを open した後、read あるいは write すれば良いです。


/*
* File: module-template.c
* Description: Small demo, how to write a Linux kernel module
*/
#include <linux/kernel.h> // for printk()
#include <linux/module.h> // this is module type device driver
#include <linux/moduleparam.h>
#include <linux/slab.h> // for kmalloc()...
#include <linux/errno.h> // use error codes
#include <linux/types.h> // size_t define
#include <linux/fcntl.h>
#include <linux/cdev.h> // this is character device
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/ioport.h> // I/O drive
#include <asm/io.h> // I/O drive
#include <asm/uaccess.h> // user/kernel access API
#include <linux/ioctl.h> // needed for the _IOW etc stuff used later


インクルードファイルの定義で、普通の C プログラムと違う所に気が付いたでしょうか。
インクルードファイルのディレクトリ指定に linux/ とか asm/ とか付いてますね。
普段はこのようなディレクトリは指定しないと思います。

#define IOMAP_GPIO_BASE 0x00100800 // base for initialize
#define IOMAP_GPIO_RW_BASE 0x001008c8 // base for read/write
#define GPIO_CFG4 (0x00100810-IOMAP_GPIO_BASE)
#define GPIO_CFG5 (0x00100814-IOMAP_GPIO_BASE)
#define GPIO_CFG6 (0x00100818-IOMAP_GPIO_BASE)
#define GPIO_CFG7 (0x0010081c-IOMAP_GPIO_BASE)
#define GPIO_CFG9 (0x00100824-IOMAP_GPIO_BASE)
#define GPIO_OUT (0x001008c8-IOMAP_GPIO_BASE)
#define GPIO_IN (0x001008cc-IOMAP_GPIO_BASE)

ここは処理系や環境によって大幅に書き変わるでしょう。I/Oアドレスを定義しています。


/*
* module parameter
*/
static int major = 0; /* dynamic by default */
static void *pmap = 0; /* I/O base address, translated MMU */
module_param(major, int, 0);
module_param(pmap, int, 0);

モジュール内で使うパラメータの定義です。
上記の値は、このモジュール内でグローバル変数として使います。
あれ、再入可能なプログラムでグローバル変数は御法度では?上記の変数は、例え再入可能であっても唯一つの値を取ります。
デバイスドライバのメジャー番号と、I/Oアドレスの先頭を示すものですから。

但し今回のモジュールはモジュールをマイナー番号で複数定義したり、同時に複数の呼出しが来た時の処理が入ってません。
あくまでも、LinuxデバイスドライバでI/Oを簡単にアクセスする為の土台ですから。

MODULE_DESCRIPTION("Small module, self-learning only, not useful.");
MODULE_AUTHOR ("16min 10sec");
MODULE_LICENSE("Dual BSD/GPL");

上記のマクロのうち、大事なのは MODULE_LICENSE です。これが無いとカーネルをロードする際に警告が出ます。
BSDもしくはGPLライセンスで無いものは保証の限りでは無い、というものです。

/*
* open method : no operation, return zero all times
*/
int hello_open (struct inode *inode, struct file *fp)
{
// printk("hello:open\n");
pmap = ioremap_nocache(IOMAP_GPIO_RW_BASE, 8);
// printk("hello:pmap=%08x\n", pmap);
return 0;
}

オープン処理では、デバイスの物理アドレスを取得する事にします。
ここはデバイスドライバの初期化時に決めてしまっても構わないのですが、
ルールとして、このようにしてます。

/*
* release method : no operation, return zero all times
*/
int hello_release (struct inode *inode, struct file *fp)
{
// printk("hello:release\n");
iounmap(pmap);
return 0;
}

オープン処理でデバイスの物理アドレスを取得するので、リリース(close)時に
デバイスのアドレスをカーネルに返却してます。これで物理アドレスの取得と返却が
一対一で対応してますね。

/*
* read method
*/
ssize_t hello_read(struct file *fp, char __user *buf, size_t count, loff_t *f_pos)
{
unsigned int result;
char kbuff;
result = ioread32(pmap+4); // read address is stored pointer + 4
// printk("hello:read, count is %d, data is %08x\n", count, result);
kbuff = (char)(result & 0x0ff); // valid data is only 8 bits
copy_to_user((void *)buf, (const void *)&kbuff, 1);
// *buf = (char)(result & 0x0ff); // valid data is only 8 bits
return 1; // valid data size is 1
}

読み出し処理は2段階になってます。
最初に ioread32() で、物理アドレス(MMUで変換後のアドレス)からI/O値を読み取ってます。
次に、copy_to_user() を呼び出してます。名前で推測が付くと思いますが、これはデバイスドライバ
のあるカーネルから、ユーザのアプリケーション空間へのデータコピー関数です。

デバイスドライバはカーネルのメモリ空間に置かれてます。データメモリもカーネル全体で共通のもの
です。ですが、ユーザーからのリード・ライト時のバッファ空間は、ユーザ空間にあり、そのままではアクセス出来ません。

そこで、カーネル空間とユーザ空間との橋渡しを行うAPIを呼び出し、データの受け渡しを行います。
※全てのLinuxでユーザ空間がデバイスドライバが見えないとは限りません。
今回のテストで使用した Linux の場合、copy_to_user / copy_from_user 関数を呼ばなくても、データの受け渡しをする事は可能でした。
が、処理系に依存する記述は極力避けておきたいので、copy_to_user / copy_from_user 関数を挟んであります。

/*
* write method
*/
ssize_t hello_write(struct file *fp, char __user *buf, size_t count, loff_t *f_pos)
{
unsigned int data;
unsigned int outdata;
char kbuff;
// printk("hello:write, count is %d\n", count);
if(count) {
data = ioread32(pmap); // get current data from GPIO_OUT
// printk("hello:write, current data is %08x\n", data);
copy_from_user((void *)&kbuff, (const void *)buf, 1);
// printk("hello:write, from user, data is %02X\n", kbuff);
outdata = (*buf & 1);
outdata = outdata <<> bit9
outdata |= (data &= 0xfffffdff); // update
iowrite32(outdata, pmap); // store new data into GPIO_OUT
// data = ioread32(pmap); // get current data from GPIO_OUT
// printk("hello:write complete, wrote %08x, register %08x\n", outdata, data);
return 1;
} else {
return 0;
}
}

書き込み処理も2段階です。
まず最初に、ユーザー空間にある書き込みデータを copy_from_user 関数で取り出します。
次にiowrite32関数でI/O空間に書き込みます。
最後に、書きこんだバイト数を戻り値として返します。

/*
* ioctl method
*/
int hello_ioctl(struct inode *pnode, struct file *fp, unsigned int cmd, unsigned long arg)
{
// for implementation, please refer P.137 of LINUX Device Driver rev.3 book
// printk("hello:ioctl\n");
return 0;
}

IOCTL は現在何も処理関数を定義していませんが、ここにデバイス制御を行う為のAPIを用意したりする事が可能です。

/*
* register driver points
* open, read, write, ioctl, and close
*
*/
struct file_operations hello_fops = {
.owner = THIS_MODULE,
.read = hello_read,
.write = hello_write,
.ioctl = hello_ioctl,
.open = hello_open,
.release = hello_release,
};

この構造体は、デバイスドライバのうち、どのような機能を盛り込んでいるかをカーネルに教える為に重要です。
各処理関数名をここに記載します。

/*
* hello_init - function to insert this module into kernel space
*
* This is the first of two exported functions to handle inserting this
* code into a running kernel
*
* Returns 0 if successfull, otherwise -1
*/

static int __init hello_init(void)
{
int result;

printk("Hello World, here is your module speaking\n");

// request memory mapped I/O region
// GPIO4, 5, 6, 7 are as read (SW), and GPIO9 is as write (LED)
// there GPIO ports are located at 0x001008C8 (output), and 0x001008CC (input)
// note that these two registers are also located GPIO[15:0], are shared another purpose.
//

pmap = ioremap_nocache(IOMAP_GPIO_BASE, 256);
printk("hello: ioremap_nocache() is returned %x\n", pmap);

result = ioread32(pmap+GPIO_CFG9);
if(result != 0) { // already configured ?
printk("hello: GPIO_CFG9 is already configured %08x\n", result);
} else {
printk("hello: GPIO_CFG9 = %08x\n", result);
iowrite32(result | 0x11, pmap+GPIO_CFG9); // set MODE=10, affect GPIO_OUT
}

iounmap(pmap);

// finally, this driver is registered as character device
//
/*
* it is old style, but keep it.
*/
result = register_chrdev(major, "hello", &hello_fops);
printk("hello: tried to regist character device, result is %x\n", result);
if(result < 0) {
printk(KERN_INFO "hello: cannot get major number\n");
return result;
}
if(major == 0) major = result; /* get dynamic device number */
return 0;
}

デバイスドライバの初期処理をここに記述します。 まず、必要なI/Oアドレス空間をマッピングしています。この処理が正常終了しなかった場合、I/Oを制御する事は出来ません。 マッピングを行った後、I/Oの初期処理を行います。このデバイスドライバは評価用ですので、いくつかのI/Oアドレスの内容を読み取り、コンソールへ表示しています。 処理が終われば、unmap しても構いません。その代わり、このデバイスをオープンした際には、改めてI/O空間をマッピングする必要があります。 ※どうしてI/O空間をマッピングしたのに unmap しているかというと、このデバイスドライバが動く基板のI/Oアドレス空間は、他のデバイスが使う空間も一部含まれていて、他のドライバが後からこの空間の一部を使う可能性がある為です。 最後に、デバイスドライバのメジャー番号を登録します。この呼出しは古いスタイルなので、新しい方式に書き換えた方が良いかも知れません。

/*
* hello_cleanup - function to cleanup this module from kernel space *
* This is the second of two exported functions to handle cleanup this
* code from a running kernel
*/
static void __exit hello_cleanup(void) {
printk("Short life for a small module...\n");
unregister_chrdev(major, "hello");
}

モジュール化されたデバイスドライバを削除する時、具体的には rmmod コマンドが呼び出された時に、この関数が呼ばれます。 この時に、デバイスドライバ内部で獲得したメモリ空間の解放などを行う必要があります。 このドライバは、メモリ空間の獲得とかは行っていないので、最後のデバイス解放を行うだけで良いです。

module_init(hello_init);
module_exit(hello_cleanup);

最後に書いてある module_init と module_exit はマクロになっていて、デバイスドライバモジュールの登録・削除を行うのに必要な手続きが、自動的に追加されます。

2009年5月5日火曜日

Dynamic DNS への登録

自宅Linux PC をDynamic DNS に登録。以前も使った事がある DynDNS に登録。
IPアドレスの通知は ddclient を使う。
外からの動作確認は、携帯電話で Web アクセス。まだコンテンツを用意してないのでテスト用の index.html が表示されたので、これで良しとした。

2009年5月3日日曜日

PPPoE router 設定

会社でも使っていたLinux PC(ThinkPadがベース) を、自宅で PPPoE - router として使う事にする。
どうしてかって?会社でも外からでも自由にログインしたいから。オフィスに置いておく場合、自宅からは何も出来なくなるので。

設定は pppoe-setup でアカウント登録と IP masquerade が大事。参考にしたページ(特にmasqueradeは)は、
http://fedorasrv.com/pppoe-iptables.shtml (先頭は2バイト文字なので適当に書き換えて下さい)

このページは ADSL モデムが対象になってますが、やりたい事は光(Flets)でも同じ。

で、今この書き込みも新しい router 経由でアクセスしながら書いてます。無線LANも使っているので転送速度はすごく遅い(1Mbpsも出てない)のですが、チューニングの余地がある、という事で。

[2009/05/05 追記]
その後、速度計測サイトで計ってみたら、下り約51Mbps、上り約25Mbpsをマークした。ライン性能としては、これで十分。
この間の「転送速度全然出てないじゃん」っていうのは、ホスト側の送り出し性能にも問題があったようです。

2009年5月2日土曜日

Linuxのモジュール化されたデバイスドライバ#1

4/25の話を、ドライバの動的な登録と、デバイス番号の与え方の、2つに分けます。

Linuxのデバイスドライバは、カーネルが立ち上がった後に、モジュールとして動的に組み込み・削除をする事が出来ます。何が便利かと言うと、例えば
1) 時々しか使わないデバイスドライバをモジュール化しておき、普段は込み込まなければ、メモリを節約する事が出来ます。また、必要な時だけ動作するのですから、セキュリティ的にも安心かも知れません。
2) デバイスドライバの開発をする際に、少しずつ動作を見ながら組み込み・削除を繰り返す事が出来るので、動作確認やデバッグの効率が上がります。
デメリットとしては、
1) root ユーザーしかデバイスドライバモジュールの追加・削除が出来ない。
ま、システム構成を変えるのですから、root ユーザーしか出来ないのは仕方が無いと考えて頂きたいです。
2) システムを起動するたびに、デバイスドライバを組み込まないといけない。
ここは rc.d サブディレクトリ以降にシェルスクリプトを書いておくのが妥当だと思います。
3) Linuxのバージョンが変わったら、再コンパイルしないと動作が保証出来ない。
安定かつ普段から使用頻度の高いデバイスドライバは、カーネルと結合した状態でリリースしましょう。

デバイス番号の与え方については、別にアップします。