- プログラミングで「抽象化」ってどんなことをやるの?
- 「抽象化」とセットで「ドライバ」も聞くけど、ドライバはどんなことをするの?
- コードも示しながらわかりやすく教えてほしい!!
こんな疑問に回答します。
エンジニアとして現場に入ってプログラムを作成すると「抽象化」というワードはよく耳にするものの、実際にどんなことをやっているのだとか、どんなことをやればいいのかは誰も教えてくれません。
またこちらとよくセットで登場する「ドライバ」も、プログラム自体よく見かけるものの、何がドライバになるのかはイマイチ理解できません。
今回はそんな「抽象化」と「ドライバ」について、実際にコードを示しながら分かりやすく解説していきます。
抽象化とは何か
一言で言えば「とあるプログラムを簡潔、かつ、より簡単に扱えるようにすること」です。
例えばマイコン制御においてアクチュエーターを制御するとなると、GPIOに指示を出さなくてはならず、コードとしてもゴチャゴチャしがちです。
まずはサンプルとして、下記のLEDを制御するプログラムをご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#define GPIO_PORT (*(volatile unsigned int*)0x40021000) #define GPIO_DDR (*(volatile unsigned int*)0x40021004) #define LED_PIN (1 << 5) // GPIOの5番ピンをLEDに割り当て void main() { GPIO_DDR |= LED_PIN; // 5番ピンを出力に設定 while (1) { GPIO_PORT |= LED_PIN; // LED ON for (volatile int i = 0; i < 1000000; i++); // 遅延 GPIO_PORT &= ~LED_PIN; // LED OFF for (volatile int i = 0; i < 1000000; i++); // 遅延 } } |
このままだと色々問題点があります。
- GPIO_PORT や GPIO_DDR の詳細が直接記述されている ため、他のマイコンに移植しにくい
- 毎回ビット演算をするのが面倒
- 他のプログラマーがコードを読むと、何をしているのか理解しにくい
そこで関数を使った抽象化を行い、ハードウェアの詳細を隠し、簡単な操作を提供するようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#define GPIO_PORT (*(volatile unsigned int*)0x40021000) #define GPIO_DDR (*(volatile unsigned int*)0x40021004) #define LED_PIN (1 << 5) void gpio_init() { GPIO_DDR |= LED_PIN; // 5番ピンを出力に設定 } void gpio_set_high() { GPIO_PORT |= LED_PIN; // LED ON } void gpio_set_low() { GPIO_PORT &= ~LED_PIN; // LED OFF } void delay() { for (volatile int i = 0; i < 1000000; i++); } int main() { gpio_init(); // GPIO 初期化 while (1) { gpio_set_high(); // LEDを点灯 delay(); gpio_set_low(); // LEDを消灯 delay(); } return 0; } |
こうすることでgpio_set_high() / gpio_set_low() を呼ぶだけでLEDを制御できます。またレジスタの詳細が隠蔽され、可読性が向上。他のマイコンに移植しやすくなるのです。
また今のままだとひとつのピンしか扱えないので、今度は構造体を使って色んなピンを扱えるようにしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// GPIOピン情報を格納する構造体 typedef struct { volatile long *port; // 出力値を書き込むレジスタ volatile long *ddr; // ピンの方向を設定するレジスタ long pin; // ピン番号(ビット位置) } GPIO; // 指定されたGPIOピンを出力モードに設定 void gpio_init(GPIO *gpio) { *(gpio->ddr) |= gpio->pin; } // 指定されたGPIOピンをHIGH(1)にする void gpio_set_high(GPIO *gpio) { *(gpio->port) |= gpio->pin; } // 指定されたGPIOピンをLOW(0)にする void gpio_set_low(GPIO *gpio) { *(gpio->port) &= ~(gpio->pin); } // 簡易的な遅延ループ(実際のタイミングはCPUクロック依存) void delay() { for (volatile long i = 0; i < 1000000; i++); } int main() { // GPIO構造体のインスタンスを定義(GPIO5番ピンを使う) GPIO led = { (long*)0x40021000, // 出力レジスタのアドレス (long*)0x40021004, // データ方向レジスタのアドレス (1L << 5) // 5番ピン(ビット位置)※リテラルに L を付けて明示 }; gpio_init(&led); // LED用ピンを出力に初期化 while (1) { gpio_set_high(&led); // LEDを点灯 delay(); // 一定時間待機 gpio_set_low(&led); // LEDを消灯 delay(); // 一定時間待機 } return 0; } |
ドライバについて
ドライバは抽象化とほぼ同意なのですが、もう少し詳しく言うと「抽象化をしてセンサやアクチュエーターを扱いやすくしたもの」になります。マイコンに接続された周辺機器(例: GPIO、UART、I2C、SPI など)を制御するためのソフトウェア層です。ハードウェアの詳細を抽象化し、アプリケーションが直接ハードウェアを操作しなくても簡単に利用できるようにします。
ドライバの役割とは
・ハードウェアの直接制御
マイコンのレジスタを操作して、GPIOのON/OFF、シリアル通信、タイマー設定などを行う。
・アプリケーションとの橋渡し
アプリケーション側が簡単に使えるように、抽象的な関数を提供する。
・移植性の向上
ドライバを作っておけば、アプリケーションのコードを書き換えずに、異なるマイコンでも動作させやすくなる。
抽象化やドライバについてはどこも難しく書いているのでなかなか理解できませんが、実際にコードとして何をやろうとしている見てみると非常に簡単だということが分かります。
レジスタの操作について
さて、ここで今日のお話は終わりなのですが、途中に出てきたレジスタの操作について少しだけ掘り下げて話をしたいと思います。
1 2 |
#define GPIO_PORT (*(volatile unsigned int*)0x40021000) #define LED_PIN (1 << 5) |
みなさん、上のコードが何をやっているか分かりますか?
ちなみに僕はつい最近まで正確には理解していませんでした。
特に「*(volatile unsigned int*)0x40021000」が意味わからないですよね。なんでふたつも「*」がついているのか。
答えを言ってしまうと「レジスタ(メモリ)を直接操作するため」なのです。
今回例として示しているマイコンはレジスタの0x40021000:5bit目がLEDと接続されています。ここが1(ON)になればLEDは点灯するし、0(OFF)になれば消灯します。
ここで「*」の有り無しでプログラムにおいて何が変わるかを解説します。
- 0x4002100とそのまま書いただけでは、マイコンはこれがレジスタを指しているとは理解できない。(単なる数値だと認識する)
- (volatile unsigned int*)0x40021000と書くと、マイコンはこれがレジスタの番地を指していると理解する。
- *(volatile unsigned int*)0x40021000と、更に「*」を付与すると、レジスタに値を入れることができる。
1 |
*(volatile unsigned int*)0x40021000 = 1; //このように値を入れることが可能になる |
以上が「抽象化」と「ドライバ」の解説でした。
シンプルに説明されると難しいということはほとんどなかったですよね。
実際のITの現場では抽象化やドライバの作成をしないといけない場面は多々出てきます。ここで理解して、ぜひ仕事に活かせるようになっていきましょう!