raspberry piで学ぶ組込みLinuxデバイスドライバ開発Part2
はじめに
前回からの続きです。
前回までで基本的なデバイスドライバを作成して動作確認をしましたが、Linux上で完結するドライバであり、ラズパイ自体は使っていませんでした。
今回はラズパイにLEDとスイッチを簡単な回路で接続して、それを操作するデバイスドライバを作成してみます。
セミナーで使用したボートには4つのLEDと4つのスイッチが付いていたので1つのドライバで4つ同時に制御するものを作りましたが、回路を作るのが面倒なので1つずつです。
ご承知おきくださいませ。
LEDを操作するデバイスドライバ
GPIO18番ピンにLEDを接続してこれを点けたり消したりできるモジュールを作成します。
仕様は以下のようにします。
- デバイスファイル名:
/dev/led
- メジャー番号: 60
- 1が書き込まれたらLEDが点灯し、0が書き込まれたら消える
前回作成したSKELの雛形モジュールをそのまま使います。
必要となる実装はwriteのシステムコールで1が書き込まれたらGPIO18の出力をHighにして、0が書き込まれたらLowにします。
またprobe時にGPIO18番ピンの初期化処理として、出力に設定します。
LEDデバイスドライバはwriteされた時に動作すればいいので、readに対しては何も処理は行いません。
raspberry pi 3のレジスタ操作
raspberry pi 3で使われているCPUはBCM2837というSocチップです。
仕様書を見てCPUのレジスタを操作することで、GPIOの出力設定や出力操作をします。
ここで考えないといけないのが、MMU(Memory Management Uint)の存在です。
MMUによりメモリの物理アドレスは仮想アドレスに変換されます。モジュールは仮想アドレスではなく、物理アドレスを操作しないとLEDを操作できません。
※↓仕様書より
1.2.3 ARM physical addressesに以下のように書かれているので、ペリフェラルIOの設定をする物理アドレスは 0x3F000000
がスタートとわかります。
Physical addresses range from 0x3F000000 to 0x3FFFFFFF for peripherals. The
bus addresses for peripherals are set up to map onto the peripheral bus address range
starting at 0x7E000000.
というわけでモジュールの実装でも、0x3F000000
を define しています。
// raspberry pi 3のペリフェラルIOの物理アドレス
#define BCM2837_PERI_BASE 0x3F000000
仕様書の6.1 Register Viewに行きレジスタのアドレスを確認します。
0x3F000000
が 0x7E000000
のPCIバスにマップされると書かれていますが、GPFSEL0
が 0x7E200000
にマップされているので、
コードのdefineで 0x3F000000
に 200000
を足します。
// raspberry pi 3のペリフェラルIOの物理アドレス
#define BCM2837_PERI_BASE 0x3F000000
#define GPIO_BASE_ADDR (BCM2837_PERI_BASE + 0x200000)
※MMUのイメージ図
MMUによりプロセスAがプロセスBの物理メモリにアクセスしてデータを破壊することはないし、その逆も起きえない。
probe関数
init関数はSKELの雛形ドライバと処理は同じです。
probe関数でデバイスファイルの作成とLEDの初期化処理を行います。
まず request_mem_region
で物理メモリの範囲を取得します。
範囲を取得したら、ioremap
で仮想アドレスにマップしてプログラムから物理メモリを操作できるようにします。
ioremap
でセットしたgpio
変数でレジスタを操作します。
GPIO18番ピンを出力に設定するためには仕様書の92ページによると GPFSEL1
の26-24bitに001
をセットすればいいのでそのように操作します。
gpio[GPFSEL1 / 4] &= ~(0x7 << ((LED_PIN % 10) * 3)); // bitをクリア
gpio[GPFSEL1 / 4] |= (0x1 << ((LED_PIN % 10) * 3)); // 26-24bitに001をセット
デバイスファイルが作成できたら、iounmap
と release_mem_region
でメモリを解放します。
// probe関数
static int led_probe(struct platform_device *pdev)
{
struct device *dev;
printk("LED Probe\n");
request_mem_region(GPIO_BASE_ADDR, MEM_SIZE, DEV_NAME);
gpio = ioremap(GPIO_BASE_ADDR, MEM_SIZE);
// GPIO18を出力に設定
gpio[GPFSEL1 / 4] &= ~(0x7 << ((LED_PIN % 10) * 3));
gpio[GPFSEL1 / 4] |= (0x1 << ((LED_PIN % 10) * 3));
// デバイスファイルを作成する
dev = device_create(&led_class, NULL, MKDEV(LED_MAJOR_NUM, 0), NULL, DEV_NAME);
if (IS_ERR(dev)) {
dev_err(&pdev->dev, "failed to create device.\n");
return PTR_ERR(dev);
}
iounmap((void*)gpio);
release_mem_region(GPIO_BASE_ADDR, MEM_SIZE);
return 0;
}
write関数
echo 1 > /dev/led
のようにwriteされた時の操作です。
まずcopy_from_user
でユーザ空間から書き込まれた文字を取得します。
request_mem_region
とioremap
でペリフェラルIOの物理アドレスを取得して操作できるようにするのはprobe関数と同じです。
echoされた文字が1だったら、GPSET0
に1をセットしてGPIO18をHighにします。0ならGPCLR0
でクリアしてLowにします。
// writeハンドラ
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
char *msg;
printk("LED Write\n");
msg = kmalloc(2, GFP_KERNEL);
if (msg == NULL) {
return -ENOMEM;
}
// echoされた文字を取得
if (copy_from_user(msg + *ppos, buf, count)) {
return -EFAULT;
}
*ppos += count;
msg[*ppos] = '\0';
request_mem_region(GPIO_BASE_ADDR, MEM_SIZE, DEV_NAME);
gpio = ioremap(GPIO_BASE_ADDR, MEM_SIZE);
if (sysfs_streq("1", msg)) {
printk("LED Turn on\n");
// LED点灯
gpio[GPSET0 / 4] = (1 << (LED_PIN % 32));
} else {
printk("LED Turn off\n");
// LED消灯
gpio[GPCLR0 / 4] = (1 << (LED_PIN % 32));
}
iounmap((void*)gpio);
release_mem_region(GPIO_BASE_ADDR, MEM_SIZE);
kfree(msg);
return count;
}
remove関数
remove関数では念のため、GPCLR0
で出力をクリアしておきます。
// remove関数
static int led_remove(struct platform_device *pdev)
{
printk("LED Remove\n");
// デバイスファイルを削除する
device_destroy(&led_class, MKDEV(LED_MAJOR_NUM, 0));
request_mem_region(GPIO_BASE_ADDR, MEM_SIZE, DEV_NAME);
gpio = ioremap(GPIO_BASE_ADDR, MEM_SIZE);
// Turn off the LED
gpio[GPCLR0 / 4] = (1 << (LED_PIN % 32));
iounmap((void*)gpio);
release_mem_region(GPIO_BASE_ADDR, MEM_SIZE);
return 0;
}
LEDデバイスドライバの実装は以上です。
LEDデバイスドライバの動作確認
実際にラズパイで動作確認をしてみます。
modprobe led
をすると /dev/led
ファイルが作成されました。
/dev/led
に1を書き込むとLEDが点灯し、0を書くと消えることがわかります。
pi@raspberrypi:~ $ sudo modprobe led
pi@raspberrypi:~ $ ls /dev/led
/dev/led
pi@raspberrypi:~ $ sudo bash -c 'echo 1 > /dev/led'
pi@raspberrypi:~ $ sudo bash -c 'echo 0 > /dev/led'
SWの値を読み取るデバイスドライバ
GPIO18番ピンにタクトスイッチを接続して、スイッチの値を読み取るモジュールを作成します。
- デバイスファイル名:
/dev/sw
- メジャー番号: 60
- readされた時にスイッチが押されていれば1を返し、押されていれば0を返す。
必要となる実装はreadのシステムコールが発行された時にスイッチの状態を読み取り、1か0の文字を返します。
またprobe時にGPIO18番ピンの初期化処理として、入力に設定します。
SWデバイスドライバはreadされた時に動作すればいいので、writeに対しては何も処理は行いません。
probe関数
probe関数はGPIO18を入力にするために000
をセットしているだけで、LEDドライバのprobe関数を同じです。
// probe関数
static int sw_probe(struct platform_device *pdev)
{
struct device *dev;
printk("SW Probe\n");
request_mem_region(GPIO_BASE_ADDR, MEM_SIZE, DEV_NAME);
gpio = ioremap(GPIO_BASE_ADDR, MEM_SIZE);
// GPIO18を入力に設定
gpio[GPFSEL1 / 4] &= ~(0x7 << ((SW_PIN % 10) * 3));
gpio[GPFSEL1 / 4] |= (0x0 << ((SW_PIN % 10) * 3));
// デバイスファイルを作成する
dev = device_create(&sw_class, NULL, MKDEV(SW_MAJOR_NUM, 0), NULL, DEV_NAME);
if (IS_ERR(dev)) {
dev_err(&pdev->dev, "faisw to create device.\n");
return PTR_ERR(dev);
}
iounmap((void*)gpio);
release_mem_region(GPIO_BASE_ADDR, MEM_SIZE);
return 0;
}
read関数
readのシステムコールに対してスイッチの状態を返す処理を実装します。
SWの状態を読み取ったら、文字列に変換してcopy_to_user
でユーザ空間にコピーします。
gpio[13]
と書いてレジスタのGPLEV0=GPIO Pin Level 0
にアクセスし、GPIO18番ピンのbitの値をval
にセットします。
スイッチを押すとbitが立つので1がval
に入り、押していないと0が入ります。
文字を返すのは前回作成したSTACKドライバと処理は同じです。
// readハンドラ
static ssize_t sw_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
int len, val;
char *msg;
printk("SW Read\n");
msg = kmalloc(2, GFP_KERNEL);
if (msg == NULL) {
return -ENOMEM;
}
request_mem_region(GPIO_BASE_ADDR, MEM_SIZE, DEV_NAME);
gpio = ioremap(GPIO_BASE_ADDR, MEM_SIZE);
// SWの状態を読み取る
val = (gpio[13] & (1 << (SW_PIN % 32))) != 0;
sprintf(msg, "%d\n", val);
len = strlen(msg);
if (len - *ppos == 0) {
return 0;
}
if (count > len - *ppos) {
count = len - *ppos;
}
if (copy_to_user(buf, msg, len)) {
return -EFAULT;
}
*ppos += count;
iounmap((void*)gpio);
release_mem_region(GPIO_BASE_ADDR, MEM_SIZE);
return count;
}
SWドライバの実装は以上です。
SWドライバの動作確認
SWモジュールをmodprobeして動作確認をしましょう。
/dev/sw
ファイルがあることを確認してcatしてみます。
そのままでは0が返ってきますが、スイッチを押しながらcatすると1が返ってくることがわかります。
pi@raspberrypi:~ $ sudo modprobe sw
pi@raspberrypi:~ $ ls /dev/sw
/dev/sw
pi@raspberrypi:~ $ sudo cat /dev/sw
0
pi@raspberrypi:~ $ sudo cat /dev/sw
1
pi@raspberrypi:~ $ sudo cat /dev/sw
0
To be continued...
今回は実際にラズパイにLEDとSWを接続してそれらを操作するデバイスドライバを作成してみました。
これまで作成してきたデバイスドライバのファイルは、/dev
に作成されていました。
Linux 2.6以降ではsysfsを利用して/sys/class
にデバイスドライバを作る方法がありますので次回はそれを取り上げます。
参考文献
Discussion