シングルボードコンピュータ Raspberry Pi を活用して,Unix (Linux) が動作する小型パソコンをカスタマイズします.この応用編では,ディジタル入出力の基礎と,アナログ入力(SPI を介した ADC の利用)の基本を学びます.また表示装置として,キャラクタ液晶ディスプレー,(超小型) 有機 EL ディスプレー,カラー液晶ディスプレーの接続に挑戦します.
Raspberry Pi には,Model A, Model A+ (上図左), Model B (上図中), Model B+ (上図右) の3種類があります.ここでは例として Model B を扱いますが,他の種類についてもほぼ同じようにプログラミングや配線を行うことができます.なお,Model B+ をお使いの方は,SD カードが microSD カードに変更された点,RCA コネクタからのビデオ出力の代わりにヘッドフォン端子(4極)からのビデオ出力となった点,そしてピンヘッダ P1 に新しく 14 本の端子が追加された点(1〜26 番ピンは変更なし)と P5 が削除された点に注意してください.Model A+ は,Model A 同様にメモリが 256MB(Model B/B+ は 512MB)の点と,USB 端子が1つになり,有線イーサネット端子が無くなった点を除けば,Model B+ と同等であると考えてよいでしょう.
- ダウンロード:このページで扱うプログラム(一括)IOKit.tar
Raspberry Pi の P1 および P5 端子群(上の写真で矢印の先)には,外部の電子回路と信号をやりとりするための入出力端子(+電源端子)が並んでいます.右図は,P1 端子群のピン配列,P5 端子群のピン配列を表したものです.いずれも1番ピンは,Raspberry Pi 基板上に白いシルク印刷で四角いマークがついています.(P5 端子群は基板裏側から見てください.Model B+ に P5 端子群はありません.)
これら端子は,シリアル通信などの特定の機能をもつものもありますが,基本的にすべてが汎用ディジタル入出力端子(GPIO: General Purpose I/O)で,たとえば LED を点滅させたり,スイッチの状態を読み取ることなどが可能です.ここでは,まずディジタル出力(つまり LED をチカチカ点越させること)について,つづいてディジタル入力(つまりスイッチを読み取ること)について解説します.
以下のプログラム断片から GPIO-A + GPIO-B + GPIO-C + Main-1 をつなげればディジタル出力(LED チカチカ)が実験できます.GPIO-A + GPIO-B + GPIO-D (+ GPIO-E) + Main-2 をつなげればディジタル入力(スイッチの読み取り)が可能です.また,GPIO-A〜E をまとめて「モジュール」とした raspGPIO.h と raspGPIO.cpp(後に解説)を,ここにあげておきます.
- ダウンロード:GPIO モジュール raspGPIO.h, raspGPIO.cpp
Raspberry Pi 2 では,以下の説明のなかの 0x20200000 のような部分を,すべて 0x3F200000 のように読み替えてください.
ディジタル入出力端子は,物理メモリ 0x20200000〜0x202000B3 に置かれたレジスタ(メモリマップト レジスタ)をとおして設定・出力・入力を行います.ここで使用するレジスタのアドレス・名前・意味(機能)はおよそ下表のとおりです.各レジスタは 32 ビット(= 4 バイト)で,4 の倍数のアドレスを持ちます.また,0x20200000 を起点とした 32 ビット整数配列として参照するときのインデックスを index として表示しています.(詳しくは "BCM2835 ARM Peripherals" を参照してください.)
プログラムから直接アクセスできるのは仮想メモリ(物理メモリとはアドレスが異なる)であるため,両者の間を mmap() というシステムコールで対応づける必要があります.ちょっとややこしいですが,オマジナイだと思っていただいて結構です.
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/mman.h> #include <unistd.h> // レジスタブロックの物理アドレス #define PERI_BASE 0x20000000 // 0x3F000000 for RPi2 #define GPIO_BASE (PERI_BASE + 0x200000) #define BLOCK_SIZE 4096 // gpio[n]: GPIO 関連レジスタ (volatile=必ず実メモリにアクセスさせる) static volatile unsigned int *Gpio = NULL; // gpio_init: GPIO 初期化(最初に1度だけ呼び出すこと) void gpio_init () { // 既に初期化済なら何もしない if (Gpio) return; // ここから GPIO 初期化 int fd; void *gpio_map; // /dev/mem(物理メモリデバイス)を開く(sudo が必要) fd = open("/dev/mem", O_RDWR | O_SYNC); if (fd == -1) { printf("error: cannot open /dev/mem (gpio_setup)\n"); exit(-1); } // mmap で GPIO(物理メモリ)を gpio_map(仮想メモリ)に対応づける gpio_map = mmap(NULL, BLOCK_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, GPIO_BASE ); if ((int) gpio_map == -1) { printf("error: cannot map /dev/mem on the memory (gpio_setup)\n"); exit(-1); } // mmap 後は不要な fd をクローズ close(fd); // Gpio[index]: 整数 uint32 の配列としてレジスタへのアクセスを確立 Gpio = (unsigned int *) gpio_map; }
GPIO 関連のレジスタへのアクセスが確立したら,それをとおしてピンごとの機能(ディジタル入力・ディジタル出力など)を設定します.gpio_configure() はピンごとの機能を設定する関数で,ピンを使用する前に必ず1度は呼び出してください.なお,ピンには「拡張機能」として ALT0〜5 を設定することもでき,たとえばシリアル通信などの機能を持たせることができます.
// ピン機能(BCM2835) #define GPIO_INPUT 0x0 // 入力 #define GPIO_OUTPUT 0x1 // 出力 #define GPIO_ALT0 0x4 #define GPIO_ALT1 0x5 #define GPIO_ALT2 0x6 #define GPIO_ALT3 0x7 #define GPIO_ALT4 0x3 #define GPIO_ALT5 0x2 // gpio_configure: ピン機能を設定する(ピンを使用する前に必ず設定) // pin : (P1) 2,3,4,7,8,9,10,11,14,15,17,18,22,23,24,25,27 // (P5) 28,29,30,31 // mode: GPIO_INPUT, _OUTPUT, _ALT0, _ALT1, _ALT2, _ALT3, _ALT4, _ALT5 void gpio_configure (int pin, int mode) { // ピン番号チェック if (pin < 0 || pin > 31) { printf("error: pin number out of range (gpio_configure)\n"); exit(-1); } // レジスタ番号(index)と3ビットマスクを生成 int index = pin / 10; unsigned int mask = ~(0x7 << ((pin % 10) * 3)); // GPFSEL0/1 の該当する FSEL (3bit) のみを書き換え Gpio[index] = (Gpio[index] & mask) | ((mode & 0x7) << ((pin % 10) * 3)); }
ピン機能を GPIO_OUTPUT に設定した後は,そのピンに 1 (=3.3V) を出力したければ gpio_set() を,0 (=0V) を出力したければ gpio_clear() をピン番号を与えて呼び出すだけでオーケーです.
// gpio_set/clear: ピンをセット (3.3V),クリア (0V) void gpio_set (int pin) { // ピン番号チェック(スピードを追求するなら省略してもよい) if (pin < 0 || pin > 31) { printf("error: pin number out of range (gpio_set)\n"); exit(-1); } // ピンに1を出力(3.3V 出力) Gpio[7] = 0x1 << pin; // GPSET0 } void gpio_clear (int pin) { // ピン番号チェック(スピードを追求するなら省略してもよい) if (pin < 0 || pin > 31) { printf("error: pin number out of range (gpio_clear)\n"); exit(-1); } // ピンに0を出力(0V 出力) Gpio[10] = 0x1 << pin; // GPCLR0 }
上記の GPIO-A + GPIO-B + GPIO-C とつぎの Main-1 をつなげて testGPIO1.c とします.これをコンパイル・実行してみてください.ここでは GPIO_25(つまり端子群 P1 の 22番ピン)から 1Hz 周期の矩形波(デューティ比 50%)を出力しています.GPIO_25 から LED と抵抗(1kΩ 程度)を直列にとおして GND につなぐことで,LED をチカチカさせることができます.適宜,ブレッドボードやジャンパ線を使ってください.(GND は複数あり,どれを使っても構いません.)
// GPIO-A をここに! // GPIO-B をここに! // GPIO-C をここに! int main () { gpio_init(); // オマジナイ gpio_configure(25, GPIO_OUTPUT); // GPIO_25 を出力に設定 while (1) { gpio_set(25); // 1 を出力(3.3V) usleep(500000); // 0.5秒待ち gpio_clear(25); // 0 を出力(0V) usleep(500000); // 0.5秒待ち } }
なお,このプログラムを実行(具体的には /dev/mem にアクセス)するには,管理者権限が必要になります.以下のようにコンパイルし,sudo で実行してください.(これ以降のプログラムについても同様です.)
pi@kozPi ~/Projects/IOKit $ cc testGPIO1.c -o testGPIO1 pi@kozPi ~/Projects/IOKit $ sudo ./testGPIO1
つぎにあげる gpio_read() は,ピンの電圧(3.3V か 0V か)を読み取るための関数です.ディジタル入力として使うには,あらかじめ gpio_configure() でピンを入力として使うように設定しておきます.(ディジタル出力を設定した端子についても gpio_read() で出力電圧を読み出すことができます.あまり有用ではありません.)
int gpio_read (int pin) { // ピン番号チェック(スピードを追求するなら省略してもよい) if (pin < 0 || pin > 31) { printf("error: pin number out of range (gpio_read)\n"); exit(-1); } // ピンの電圧を返す(入力/出力を問わず 3.3V なら 1,0V なら 0) return (Gpio[13] & (0x1 << pin)) != 0; // GPLEV0 }
ピンとスイッチの接続は,右図を参考にしてください.方式 (A) は,スイッチが切れている(開いている)ときは,抵抗(1k〜100kΩ 程度;10kΩ を推奨)を介して 3.3V の電源電圧が入力端子(ここでは GPIO_25)に加えられていますが,スイッチを入れる(閉じる)と,入力端子は GND に接続され 0V となります.(代わりにプラス電源から抵抗を通して GND へ僅かな電流が流れます.) 方式 (B) はこの逆で,スイッチが開いているときは,抵抗を介して GND に接続され,が入力端子は 0V となります.スイッチを閉じると,入力端子は直接 3.3V に接続され,3.3V となります.(やはり僅かな電流が抵抗を流れます.)
// GPIO-A をここに! // GPIO-B をここに! // GPIO-D をここに! int main () { gpio_init(); // オマジナイ gpio_configure(25, GPIO_INPUT); // GPIO_25 を出力に設定 while (1) { int val; val = gpio_read(25); // ピン電圧を読み込む printf("input: %d\n", val); // 0/1 をプリント usleep(500000); // 0.5秒待ち } }
ここで登場した抵抗は,「プルアップ抵抗 (A)」あるいは「プルダウン抵抗 (B)」と呼ばれます.これにより,電源電圧(3.3V)あるいは GND 電圧(0V)のいずれかが入力端子に常に加えられ,安定したディジタル入力が保証されます.(入力端子をオープン(どこにも接続しない状態)にすると,静電気などによるノイズを拾いやすくなり,0/1 の状態が不定になります.)
プルアップ抵抗・プルダウン抵抗を外付けするのは面倒です.Raspberry Pi(BCM2835)のディジタル入出力ピンには,プルアップ抵抗・プルダウン抵抗が内蔵されていて,プログラムから有効にすることで,外付けのプルアップ抵抗・プルダウン抵抗と同じように機能させることができます.つぎの gpio_configure_pull() は,プルアップ有り・プルダウン有り・いずれも無しを設定する関数です.
#define GPIO_PULLNONE 0x0 #define GPIO_PULLDOWN 0x1 #define GPIO_PULLUP 0x2 // gpio_configure_pull: プルアップ/ダウン抵抗の設定 // pullmode: GPIO_PULLNONE/PULLDOWN/PULLUP void gpio_configure_pull (int pin, int pullmode) { // ピン番号チェック if (pin < 0 || pin > 31) { printf("error: pin number out of range (gpio_configure_pull)\n"); exit(-1); } // プルモード (2bit)を書き込む NONE/DOWN/UP Gpio[37] = pullmode & 0x3; // GPPUD usleep(1); // ピンにクロックを供給(前後にウェイト) Gpio[38] = 0x1 << pin; // GPPUDCLK0 usleep(1); // プルモード・クロック状態をクリアして終了 Gpio[37] = 0; Gpio[38] = 0; }
これを使ったテストプログラムをつぎにあげます.プルアップ・プルダウンの指定を間違えると,スイッチを押しても端子電圧が変化しなくなります.注意してください.
// GPIO-A をここに! // GPIO-B をここに! // GPIO-D をここに! // GPIO-E をここに! int main () { gpio_init(); // オマジナイ gpio_configure(25, GPIO_INPUT); // GPIO_25 を出力に設定 gpio_configure_pull(25, GPIO_PULLUP); // プルアップ抵抗を有効化 gpio_configure(18, GPIO_INPUT); // GPIO_18 を出力に設定 gpio_configure_pull(18, GPIO_PULLDOWN); // プルダウン抵抗を有効化 while (1) { int val; val = gpio_read(25); // ピン電圧を読み込む printf("input(25): %d\n", val); // 0/1 をプリント val = gpio_read(18); // ピン電圧を読み込む printf("input(18): %d\n", val); // 0/1 をプリント usleep(500000); // 0.5秒待ち } }
GPIO にアクセスするための関数群(GPIO-A〜E)を,ヘッダファイル raspGPIO.h と実装ファイル raspGPIO.cpp に整理し,さまざまなプログラムから再利用できるようにします.(ここから raspGPIO.h と raspGPIO.cpp をダウンロードできます.)
なお,これ以降このページでは,実装ファイルを C++ のソースファイル (.cpp) としていきます.raspGPIO.cpp は C のソースファイルと(見かけ上)何ら変わりませんが,後続するモジュール(C++ で記述したディスプレー制御モジュールなど)から簡単に利用できるようになります.
// // Raspberry Pi GPIO module #ifndef GPIO_H #define GPIO_H #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/mman.h> #include <unistd.h> // gpio_init: GPIO 初期化(最初に1度だけ呼び出すこと) void gpio_init(); // // ピン機能(BCM2835) #define GPIO_INPUT 0x0 // 入力 #define GPIO_OUTPUT 0x1 // 出力 #define GPIO_ALT0 0x4 #define GPIO_ALT1 0x5 #define GPIO_ALT2 0x6 #define GPIO_ALT3 0x7 #define GPIO_ALT4 0x3 #define GPIO_ALT5 0x2 // gpio_configure: ピン機能を設定する(ピンを使用する前に必ず設定) // pin : (P1) 2,3,4,7,8,9,10,11,14,15,17,18,22,23,24,25,27 // (P5) 28,29,30,31 // mode: GPIO_INPUT, _OUTPUT, _ALT0, _ALT1, _ALT2, _ALT3, _ALT4, _ALT5 void gpio_configure(int pin, int mode); // // gpio_set/clear: ピンをセット (3.3V),クリア (0V) void gpio_set(int pin); void gpio_clear(int pin); // // gpio_read: ピンの読み取り(3.3V->1, 0V->0) int gpio_read (int pin); // // プルアップ・プルダウン機能 #define GPIO_PULLNONE 0x0 #define GPIO_PULLDOWN 0x1 #define GPIO_PULLUP 0x2 // gpio_configure_pull: プルアップ抵抗またはプルダウン抵抗の設定 void gpio_configure_pull(int pin, int pullmode); #endif
// // Raspberry Pi GPIO module #include "raspGPIO.h" // // レジスタブロックの物理アドレス #define PERI_BASE 0x20000000 // 0x3F000000 for RPi2 #define GPIO_BASE (PERI_BASE + 0x200000) #define BLOCK_SIZE 4096 // gpio[n]: GPIO 関連レジスタ (volatile=必ず実メモリにアクセスさせる) static volatile unsigned int *Gpio; // gpio_init: GPIO 初期化(最初に1度だけ呼び出すこと) void gpio_init() { ...(GPIO-A 参照)... } // gpio_configure: ピン機能を設定する(ピンを使用する前に必ず設定) // pin : (P1) 2,3,4,7,8,9,10,11,14,15,17,18,22,23,24,25,27 // (P5) 28,29,30,31 // mode: GPIO_INPUT, _OUTPUT, _ALT0, _ALT1, _ALT2, _ALT3, _ALT4, _ALT5 void gpio_configure(int pin, int mode) { ...(GPIO-B 参照)... } // gpio_set/clear: ピンをセット (3.3V),クリア (0V) void gpio_set(int pin) { ...(GPIO-C 参照)... } void gpio_clear(int pin) { ...(GPIO-C 参照)... } // // gpio_read: ピン状態の読み取り(3.3V->1, 0V->0) int gpio_read (int pin) { ...(GPIO-D 参照)... } // // gpio_configure_pull: プルアップ/ダウン抵抗の設定 // pullmode: GPIO_PULLNONE/PULLDOWN/PULLUP void gpio_configure_pull (int pin, int pullmode) { ...(GPIO-E 参照)... } //
このように「ヘッダファイル」と「実装ファイル」に分離し,ヘッダファイルはこの関数群の利用者(あなた or 第3者)に参照してもらいます.ゆえにヘッダファイルには,この関数群の使い方がわかるだけの(過不足ない)仕様を記述しておきます.一方,実装ファイルは,その仕様を実装した関数群の本体です.仕様に変更がない範囲であれば,実装ファイルを変更(たとえば高速化)しても利用者には直接的な影響はありません.
この関数群(ここでは「GPIO モジュール」と呼びます)を単体でコンパイルするには,以下のように -c オプションを付けて cc を起動します.これで raspGPIO.o というファイルが生成されるはずです.
pi@kozPi ~/Projects/IOKit $ cc -c raspGPIO.cpp
生成された raspGPIO.o は「オブジェクトファイル」と呼ばれるものですが,それ単体では実行可能ではありません.実行可能にするには,main() 関数を書いたプログラム sampleGPIO.cpp と一緒にコンパイルし,実行可能ファイルを生成します.
#include "raspGPIO.h"
int main ()
{
gpio_init(); // オマジナイ
gpio_configure(25, GPIO_OUTPUT); // GPIO_25 を出力に設定
while (1) {
gpio_set(25); // 1 を出力(3.3V)
usleep(500000); // 0.5秒待ち
gpio_clear(25); // 0 を出力(0V)
usleep(500000); // 0.5秒待ち
}
}
pi@kozPi ~/Projects/IOKit $ cc sampleGPIO.cpp raspGPIO.o -o sampleGPIO
このように sampleGPIO.cpp の冒頭に #include "raspGPIO.h" と書き,raspGPIO.o と一緒にコンパイル(リンク)するだけで,raspGPIO モジュールを利用したアプリケーションを開発することができます.(ただし raspGPIO.h と raspGPIO.o は sampleGPIO.cpp と同じディレクトリに置かなければなりません.)
GTK+ プログラムから GPIO をアクセスする場合: GTK+ プログラムはユーザ名 pi で起動した X Window 上で動作します.一方,GPIO にアクセスするプログラムは sudo で管理者として起動しないと動きません.GTK+ プログラムの中で GPIO にアクセスしたい場合,sudo で管理者としてプログラムを起動すると,このプログラム(X クライアント)は管理者で動作しようとし,X サーバは pi で動作していて,接続がうまく行かなくなり,エラーとなります.そこで,X Window 上の端末の中で,以下のように GTK+ プログラムを起動してください.
pi@kozPi ~/Projects/gtkTutorials $ xhost +localhost localhost is being added to access control list pi@kozPi ~/Projects/gtkTutorials $ sudo ./gtkGpioExample
最初のコマンドは「localhost(つまりこの Raspberry Pi)上の X クライアントからの『接続』を受け付ける」ように,X サーバを設定します.次のコマンドでは,管理者権限で X クライアント(gtkGpioExample)を起動しています.この X クライアントは,X サーバ(所有者 pi)に「接続」し,その画面を「借りる」ことで画面表示やマウス入力が可能となります.
GPIO モジュールを応用して,キャラクタ液晶ディスプレーを接続・制御してみましょう.ここでは,秋月電子通商から入手できる SD1602HULB を利用します.端子が1列に並んでいるが特徴です.横16文字・縦2行に英数字とカナを表示できます.これ以外のキャラクタ液晶ディスプレーでも,HD44780 準拠のインタフェースを持っていれば大抵は利用可能です.どれも 500〜1000円程度でしょう.
Raspberry Pi との接続には,右図のようなピンソケットを使うとよいでしょう.このピンソケットを Raspberry Pi の P1 に接続し,そのハンダづけ端子からキャラクタ液晶ディスプレーの端子までを細めの電線をハンダづけします.(配線の様子は【活用(1)メッセージ表示】の節をご覧ください.)
- ダウンロード:CLCD モジュール raspCLCD.h, raspCLCD.cpp
- ダウンロード:サンプルプログラム sampleCLCD.cpp
ここでは Raspberry Pi の入出力端子を節約する(そして配線の手間を減らす)ために,4-bit のパラレル接続によってキャラクタ液晶ディスプレーと通信するようにします.具体的な配線は,下図のように,Raspberry Pi の P1 端子群の偶数番号端子(外側)をキャラクタ液晶ディスプレーに配線します.
ディスプレーの電源(Vdd=5V, Vss=GND)は,Raspberry Pi の 5V0, GND から供給します.また,バックライト LED には,この Vdd から 100Ω の抵抗を介して A (アノード) につなぎ,K (カソード) は Vss に接続します.明る過ぎる場合は,220Ω など,少し大きめの抵抗に換えてください.バックライトが不要であれば,これらの接続は不要です.また Vo 端子に 0.3〜0.8V 前後の電圧を加えることでコントラストを調節できます.ここでは2本の抵抗(10kΩ・1kΩ)で Vdd-Vss 間を分圧した 0.45V 程度を与えています
4-bit のパラレル信号の送受信は,Raspberry Pi の P1#22, 18, 16, 12 端子,すなわち GPIO_25, 24, 23, 18 から,キャラクタ液晶ディスプレーの DB7, 6, 5, 4 への接続によって実現しています.パラレル信号の通信タイミングをとるための E 信号 (P1#8=GPIO_14),コマンド/データを区別する RS (Register Select) 信号 (P1#10=GPIO_15) を接続します.
キャラクタ液晶ディスプレーには,書き込み/読み出しを区別する R/W (Read/Write) 入力もありますが,このページでは Raspberry Pi からキャラクタ液晶ディスプレーへの単方向通信のみを扱っているため,R/W を Vss (GND) に接続することで R/W=LOW (Write) に固定しています.
DB7-4 (4-bit のパラレル信号線), E (通信タイミング信号), RS (コマンド/データ区別信号) を,下図のタイミングで変化させることで,4-bit の情報をキャラクタ液晶ディスプレーに転送します.
まず,転送する情報が制御コマンド (RS=0) なのか表示データ (RS=1) なのかを,Raspberry Pi から出力します.続いてタイミング信号 E=1 を出力し,転送したい 4-bit の情報(上図グレーの部分)を DB4-7 に出力します.DB7 が上位ビットです.つぎに,E=0 に変化させると,その立ち下がりのタイミングで DB4-7 がキャラクタ液晶ディスプレーに取り込まれます.これが 4-bit 転送の基本的な手順です.この 4 ビット転送を2回連続して実行することで,8-bit の制御コマンドあるいは表示データをキャラクタ液晶ディスプレーに転送します.キャラクタ液晶ディスプレーは,それが制御コマンドであれば実行し,表示データであれば文字を表示します.
上述の 4-bit パラレル接続による通信を C++ のクラスにまとめたのが CLCD モジュールです.(ここから そのソースプログラム raspCLCD.h と raspCLCD.cpp をダウンロードできます.) プログラム自体は,GPIO モジュールを使って 4-bit 転送の信号タイミングを実現しただけのものです.ヘッダファイル raspCLCD.h は以下のようになっています.
// // Raspberry Pi Character LCD module (using 4bit parallel) #ifndef CLCD_H #define CLDC_H #include <string.h> #include "raspGPIO.h" // キャラクタ液晶ディスプレー (SD1602H) への接続 // 必要に応じて remap してよい. // RW は Vss (GND) に接続する. #define CLCD_RS 14 // #08 #define CLCD_E 15 // #10 #define CLCD_DB4 18 // #12 #define CLCD_DB5 23 // #16 (#14=GND) #define CLCD_DB6 24 // #18 #define CLCD_DB7 25 // #22 (#20=GND) class CLCD { public: // 初期化(最初に1回呼び出す) void init(); // 表示クリア void clear(); // カーソルを (x, y) へ(x=0..15, y=0..1) void locate(int x, int y); // カーソル位置から文字列 buf を表示 void print(const char *buf); private: void sendCommand4(unsigned char command); void sendCommand8(unsigned char command); void sendData8(unsigned char data); }; #endif
sendCommand4() と sendCommand8() は,それぞれ 4-bit あるいは 8-bit の制御コマンドを転送する内部メソッド,sendData8() は 8-bit の表示データを転送する内部メソッドです.8-bit データの転送は 4-bit の転送を2回繰り返すことで実現します.実装ファイル raspCLCD.cpp では,以下のように定義されています.
#include "raspGPIO.h" // 4-bit をコマンドとして転送 void CLCD::sendCommand4 (unsigned char command) { // RS=0, DB7-4=command gpio_clear(CLCD_RS); if (command & 0x08) gpio_set(CLCD_DB7); else gpio_clear(CLCD_DB7); if (command & 0x04) gpio_set(CLCD_DB6); else gpio_clear(CLCD_DB6); if (command & 0x02) gpio_set(CLCD_DB5); else gpio_clear(CLCD_DB5); if (command & 0x01) gpio_set(CLCD_DB4); else gpio_clear(CLCD_DB4); // issue E gpio_set(CLCD_E); usleep(1); // min 140ns gpio_clear(CLCD_E); usleep(1); } // 8-bit をコマンドとして転送 void CLCD::sendCommand8 (unsigned char command) { // 上位4ビット sendCommand4(command >> 4); // 下位4ビット sendCommand4(command & 0x0f); } // 8-bit をデータとして転送 void CLCD::sendData8 (unsigned char data) { // RS=1, DB7-4=data gpio_set(CLCD_RS); // 上位4ビット if (data & 0x80) gpio_set(CLCD_DB7); else gpio_clear(CLCD_DB7); if (data & 0x40) gpio_set(CLCD_DB6); else gpio_clear(CLCD_DB6); if (data & 0x20) gpio_set(CLCD_DB5); else gpio_clear(CLCD_DB5); if (data & 0x10) gpio_set(CLCD_DB4); else gpio_clear(CLCD_DB4); // Eを発行 gpio_set(CLCD_E); usleep(1); // min 140ns gpio_clear(CLCD_E); usleep(1); // 下位4ビット if (data & 0x08) gpio_set(CLCD_DB7); else gpio_clear(CLCD_DB7); if (data & 0x04) gpio_set(CLCD_DB6); else gpio_clear(CLCD_DB6); if (data & 0x02) gpio_set(CLCD_DB5); else gpio_clear(CLCD_DB5); if (data & 0x01) gpio_set(CLCD_DB4); else gpio_clear(CLCD_DB4); // Eを発行 gpio_set(CLCD_E); usleep(1); // min 140ns gpio_clear(CLCD_E); usleep(1); // wait 43us usleep(43); }
キャラクタ液晶ディスプレーは,電源投入後,一連の手続きで初期化する必要があります.それを担う公開メソッドが init() です.内部では,まず 8-bit モードで起動させた後,インタフェースを 4-bit に変更しています.加えて,文字画面表示,カーソル非表示・非点滅,カーソル右移動,文字画面シフト無効,文字画面クリアなどの設定もしています.
#include "raspGPIO.h" void CLCD::init () { // 出力端子の設定 gpio_init(); gpio_configure(CLCD_RS, GPIO_OUTPUT); gpio_configure(CLCD_E, GPIO_OUTPUT); gpio_configure(CLCD_DB7, GPIO_OUTPUT); gpio_configure(CLCD_DB6, GPIO_OUTPUT); gpio_configure(CLCD_DB5, GPIO_OUTPUT); gpio_configure(CLCD_DB4, GPIO_OUTPUT); // 初期化シーケンス // 電源投入後は 15ms 以上待つ (Vdd=5V) usleep(15000); // 8-bit で起動(1): 0011**** (8-bit) + 4.1ms sendCommand4(0x3); usleep(4100); // 8-bit で起動(2): 0011**** (8-bit) + 100us sendCommand4(0x3); usleep(100); // 8-bit で起動(3): 0011**** (8-bit) + 39us sendCommand4(0x3); usleep(39); // 次に 4-bit に設定: 0010 (4-bit) + 39us (ここから4-bit) sendCommand4(0x2); usleep(39); // 機能設定: 0010,1000 (4-bit, 2-line, 5x8font) + 39us sendCommand8(0x28); usleep(39); // 表示設定: 0000,1100 (Display-on, no-Cursor, no-Blinking) + 39us sendCommand8(0x0c); usleep(39); // 表示クリア: 0000,0001 (clear display) + 1.53ms sendCommand8(0x01); usleep(1530); // 描画モード: 0000,0110 (cursor++, no-displayShift) sendCommand8(0x06); usleep(39); }
init() 以外に公開メソッドが3つあります.clear() メソッドは,キャラクタ液晶ディスプレーの文字画面(横16字・縦2行)全体を空白で埋めます.locate(int x, int y) メソッドは,カーソル(ディスプレー上には見えない)の位置を,横 x 文字目・縦 y 行目とします.ディスプレーの左上端の位置は (0, 0) となります.print(const char *buf) メソッドは,カーソル位置から(右方向に)文字列 buf を描画し,描画した文字数分だけカーソルを右に動かします.ただし,buf は '\0' で終端していなければなりません.行末を越えても折り返しなどはしません.使用できる文字は,ASCII 文字に半角カナ文字を加えた JIS X 0201 の文字(に若干の拡張を加えたもの)となっています.
#include "raspGPIO.h" // 全画面クリア void CLCD::clear () { // 0000, 0001 (clear display) sendCommand8(0x01); usleep(1530); // 1.53ms } // 書き込み位置の指定(左上(0,0),左下(0,1),右上(15,0),右下(15,1)) void CLCD::locate (int x, int y) { if (y % 2 == 0) sendCommand4(0x8); else sendCommand4(0xc); sendCommand4(x & 0xf); usleep(39); } // 文字列の書き込み void CLCD::print (const char *buf) { for (int i = 0; i < strlen(buf); i++) sendData8(buf[i]); }
CLCD モジュールは以下の手順でコンパイルしてください.CLCD モジュールは内部で GPIO モジュールを利用していますので,GPIO モジュールのコンパイル完了が前提となります.
pi@kozPi ~/Projects/IOKit $ cc -c raspCLCD.cpp
制御コマンドや文字コード,初期化の手順などの詳細については「SD1602HULB データシート」を参照するか,インターネットで情報検索してみてください.
CLCD モジュールを使った簡単なメッセージ表示プログラムを紹介します.キャラクタ液晶ディスプレーの上側の行 (y=0) に(中央寄せで)"Hello, world!" という文字列を,下側の行(y=1) に "= Raspberry Pi =" という文字列を表示するというものです.
#include "raspCLCD.h" int main () { CLCD display; display.init(); display.locate(2, 0); display.print("Hello, world!"); display.locate(0, 1); display.print("= Raspberry Pi ="); }
このプログラム testCLCD.cpp を,以下のようにコンパイル・実行すると,下図のような文字列がキャラクタ液晶ディスプレーに表示されます.プログラムが終了しても,ディスプレーに表示された内容は保持されたままになります.
pi@kozPi ~/Projects/IOKit $ cc testCLCD.cpp raspCLCD.o raspGPIO.o -o testCLCD pi@kozPi ~/Projects/IOKit $ sudo ./testCLCD
長いメッセージがスクロールしながら表示されるプログラムをつくります.電車やバスの車内にあるメッセージ表示装置をイメージしてください.(ここからプログラム sampleCLCD.cpp をダウンロードできます.)
#include "raspCLCD.h" #define LINE_LEN 16 // 1行の長さ #define BUF_SIZE 100 // バッファの大きさ #define DELAY_MS 200 // スクロール速度 int main (int argc, char *argv[]) { CLCD display; char buffer[BUF_SIZE]; char line[LINE_LEN + 1]; // null 文字分も入れる // init LCD display.init(); display.locate(0, 1); display.print("= Raspberry Pi ="); // line を空白で埋める(null 留めも忘れずに) for (int i = 0; i < LINE_LEN; i++) line[i] = ' '; line[LINE_LEN] = '\0'; // 標準入力から最大100バイトずつ buffer に読み込んで表示 while (fgets(buffer, BUF_SIZE, stdin)) { // buffer の各文字について... for (int i = 0; i < strlen(buffer); i++) { // まず line を1文字左に送る(右端の1文字はそのまま) for (int j = 0; j < LINE_LEN - 1; j++) line[j] = line[j+1]; // line の右端に buffer からの1文字を入れる if (buffer[i] == '\n') // 改行コードは空白に強制変換 line[LINE_LEN - 1] = ' '; else // そうでなければ1文字を右端に入れる line[LINE_LEN - 1] = buffer[i]; // line を LCD に表示する display.locate(0, 0); display.print(line); // 1文字分のディレイ usleep(DELAY_MS * 1000); } } // 最後に line を空送りして,すべての文字を左端から追い出す for (int i = 0; i < LINE_LEN; i++) { // まず line を1文字左に送る(右端の1文字はそのまま) for (int j = 0; j < LINE_LEN - 1; j++) line[j] = line[j+1]; // line の右端に空白文字を入れる line[LINE_LEN - 1] = ' '; // line を LCD に表示する display.locate(0, 0); display.print(line); // 1文字分のディレイ usleep(DELAY_MS * 1000); } }
このプログラムは,標準入力(つまりコンソール端末)から文字列が入力されるのを待ちます.キーボードから適当なメッセージを入力してもよいし,ウェブブラウザなどから文字列をコピーし,それをコンソール端末にペーストしてもよいでしょう.プログラムを終了するには,行頭で(つまり【改行】してから)^D を打ちます.
pi@kozPi ~/Projects/IOKit $ cc sampleCLCD.cpp raspCLCD.o raspGPIO.o -o sampleCLCD pi@kozPi ~/Projects/IOKit $ sudo ./sampleCLCD This must be the ugliest piece of bread I've ever eaten.【改行】 Wrapped in a foil-like substance...【改行】 ^D(Control-D) pi@kozPi ~/Projects/IOKit $ cat message.txt | sudo ./sampleCLCD pi@kozPi ~/Projects/IOKit $ _
上の最後の動作例は,message.txt というテキストファイルを用意し,その内容を Unix コマンド cat を使って標準出力に出力し,それを sampleCLCD の標準入力に接続しています.タテ棒(|)は「パイプ」と呼ばれ,その左側にあるプログラムの標準出力を,右側にあるプログラムの標準入力につなぎます.message.txt の最後の文字が出力されると,EOF(End of File)を表す ^D が sampleCLCD に送られ,プログラムの実行が終了します.
多くのキャラクタ液晶ディスプレーは,ASCII 文字だけでなく半角カナ文字も表示できるようです.ただし,半角カナをキーボードから入力するのはやっかいです.そこで,kakasi という外部プログラムを利用して,漢字かな交じりのテキストを,その漢字の読みを推定し,すべて半角カナ文字に変換してから,sampleCLCD に入力するようにしてみます.kakasi をインストールするには,以下のように打ち込みます.
pi@kozPi ~/Projects/IOKit $ sudo apt-get install kakasi
kakasi は標準入力からテキストを読み取り,指定された文字種を別の指定された文字種に変換します.漢字については,辞書を使って読み方を調べ,かな(またはカナ)に変換することができます.ただし kakasi は,Raspberry Pi 標準の UTF-8 を扱えません.そこで iconv という文字コード変換プログラム(標準でインストール済)を使って,Shift-JIS に変換してから kakasi に入力するようにしています.また,kakasi -w によって,漢字かな交じりのテキストを,適宜空白を入れた分かち書きに変換してから,半角カナ文字への変換を行っています.
pi@kozPi ~/Projects/IOKit $ iconv -f utf-8 -t sjis | kakasi -w | kakasi -Hk -Kk -Jk -Ea | sudo ./sampleCLCD ここに書いた(あるいはペーストした)漢字かな交じりのテキストが, 漢字はその読みを半角カナ文字で (-Jk), かな文字・カナ文字は半角カナ文字で (-Hk -Kk), 全角英数字は半角英数字で (-Ea) , このキャラクタ液晶ディスプレーに表示されます. ^D pi@kozPi ~/Projects/IOKit $ cat messageJP.txt | iconv -f utf-8 -t sjis | kakasi -w | kakasi -Hk -Kk -Jk -Ea | sudo ./sampleCLCD pi@kozPi ~/Projects/IOKit $ _
どんなテキストでも ASCII 文字と半角カナ文字で表示ができるようになったので,インターネット上のテキスト情報(ツイートなど)を表示させてみましょう.
最初はウェブページです.コンソール端末から利用できるブラウザはいくつかありますが,ここでは日本(東北)で開発された w3m を利用します.インストールは,"sudo apt-get install w3m" です.たとえば "w3m http://www.myu.ac.jp/~xkozima/" とすれば,画像は出ませんが,テキスト部分が整形され,コンソール端末上に表示されます.カーソルキーでカーソルを動かし,リンクの上で [Enter] を押せば,リンク先に移動できます.'H' を押すとヘルプページを見ることができます.前ページに戻るには 'B',終了するには 'Q' です.
この w3m に,Shift-JIS(-O sjis)でのテキスト出力をダンプ(-dump)させ,その出力を kakasi 経由で sampleCLCD に入力することで,ウェブページの内容(テキスト部分)を半角カナ文字でキャラクタ液晶ディスプレーに表示することができます.(リンクをクリックするなど,インタラクティブな操作はできません.テキスト内容を表示するだけです.)
pi@kozPi ~/Projects/IOKit $ w3m -dump -O sjis http://www.myu.ac.jp/~xkozima/ | kakasi -w | kakasi -Hk -Kk -Jk -Ea | sudo ./sampleCLCD
つぎにツイッターのタイムラインを表示してみましょう.ここではコンソール端末上で動作する tw をインストールします.tw は Ruby で書かれた,日本製のツイッタークライアントです.Raspberry Pi での tw のインストールは,やや複雑ですが,以下の手順のとおりです.(最後のステップは,かなり時間がかかるようです.)
pi@kozPi ~/Projects/IOKit $ sudo apt-get update pi@kozPi ~/Projects/IOKit $ sudo apt-get install ruby rubygems ruby1.9.1-dev pi@kozPi ~/Projects/IOKit $ sudo gem install tw
これでインストールが完了しましたが,初めて起動するときだけ,X-Window を動作させ,LXTerminal(コンソール端末アプリケーション)を立ち上げ,そこから tw を動かしてください.tw の指示に従ってアカウント情報を入力すると,あなたのツイッター PIN が別ウィンドウに表示されますので,その番号を tw に入力してください.これ以降,tw を動かすために X-Window は必要ありません.
pi@kozPi ~/Projects/IOKit $ tw @nhk_news | iconv -f utf-8 -t sjis | kakasi -w | kakasi -Hk -Kk -Jk -Ea | sudo ./sampleCLCD
tw はコンソール端末からのツイート(発信)も可能です.詳しい使い方は,tw 開発者である橋本商会さんのページを参照してください.
Raspberry Pi にはアナログ入力端子がありません.センサ(温度センサなど)からのアナログ信号を読み取るには,外付けの ADC (Analog-Digital Converter) を介してディジタル信号(二進数表現)に変換する必要があります.ここでは,ADC を Raspberry Pi に接続するための方法として SPI 通信を紹介し,一般的な ADC (MCP3204/08) を使用するための ADC モジュールを解説します.
- ダウンロード:SPI モジュール raspSPI.h, raspSPI.cpp
- ダウンロード:ADC モジュール raspADC.h, raspADC.cpp
- ダウンロード:サンプルプログラム sampleADC.cpp
SPI (Serial Peripheral Interface) は,マスター(Raspberry Pi)とスレーブ(ADC など外部デバイス)の間でのデータ通信を可能にします.SPI 通信では4本の信号線(SCLK, MOSI, MISO, CE)を使います.SCLK はデータ転送の同期をとるためのクロック(マスターから出力),MOSI (Master-Out Slave-In) はマスターからスレーブへのデータ伝送線,MISO (Master-In Slave-Out) はスレーブからマスターへのデータ伝送線です.CE (Chip Enable) は,通信相手となるスレーブを選択する信号線で,CS (Chip Select) や SS (Slave Select) と呼ばれることもあります.CE を複数用意し,他の3線を共有することで,複数のスレーブから1つを選択して通信することが可能です.
データを取り込むタイミングとして mode 0〜3 の4方法が定められていますが,SCLK の立ち上がり(LOW から HIGH への変化)のタイミングで,信号線から1ビットのデータを受信・送信する mode 0 (mode 0,0) と mode 3 (mode 1,1) が一般的に使われているようです.ここでは,クロックが正パルス(LOW-HIGH-LOW が1パルス)となる mode 0 (mode 0,0) を使います.ここで利用する ADC (MCP3204/08) だけでなく,後述する有機 EL ディスプレーやカラー液晶ディスプレーも mode 0 で動作します.
Raspberry Pi の SPI からデータを入出力するには,あらかじめ Linux に spidev モジュールを組み込んでおくことが必要です.これを設定するには,以下のように,/etc/modules に spidev の行を追加します.
pi@kozPi ~/Projects/IOKit $ sudo vi /etc/modules ... snd-bcm2835 spidev
また,モジュールの「ブラックリスト」から spi-bcm2708 を外しておくことも必要です.以下のように /etc/modprobe.d/raspi-blacklist.conf の spi-bcm2708 の行をコメントアウト(# を追加)してください.
pi@kozPi ~/Projects/IOKit $ sudo vi /etc/modprobe.d/raspi-blacklist.conf ... #spi-bcm2708
これら設定の後,再起動(sudo reboot)することで,SPI 関連のモジュール(下線のもの)が組み込まれた状態となります.確認するには lsmod です.
pi@kozPi ~/Projects/IOKit $ lsmod Module Size Used by snd_bcm2835 16304 0 snd_pcm 77560 1 snd_bcm2835 snd_seq 53329 0 snd_timer 19998 2 snd_pcm,snd_seq snd_seq_device 6438 1 snd_seq snd 58447 5 snd_bcm2835,snd_timer,snd_pcm,snd_seq,snd_seq_device snd_page_alloc 5145 1 snd_pcm spidev 5224 2 evdev 9426 2 8192cu 490353 0 leds_gpio 2235 0 led_class 3562 1 leds_gpio spi_bcm2708 4816 0
ここでは C++ のクラスにまとめた SPI モジュールを紹介します.下記のように,raspSPI.h と raspSPI.cpp からなります.ここでは,ヘッダファイル raspSPI.h を見ながら,公開メソッドの使い方を解説します.
// // Raspberry Pi SPI module #ifndef SPI_H #define SPI_H #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <fcntl.h> #include <sys/ioctl.h> #include <linux/types.h> #include <linux/spi/spidev.h> #define SPI_DEVICE "/dev/spidev0.0" // SPI デバイス(デフォルト) #define SPI_SPEED 8000000 // クロック 8MHz(デフォルト) #define SPI_BITS 8 // ビット数(8bit のみ可能) class SPI { public: // init: 最初に1回呼び出す(デバイス名・クロック周波数を指定可) void init (); void init (const char *device, int clockInHz); // quit: SPI を使い終わったら呼び出す(礼儀正しい人のみ) void quit (); // sendBuffer: 大きなバイト配列の送信 void sendBuffer (unsigned char *data, int len); // sendN: Nバイトデータの送信(n <= 2048) void sendN (unsigned char *data, int n); // send1: 1バイトデータの単発送信 void send1 (unsigned char data); // receiveN: Nバイトデータの受信 void sendRecN (unsigned char *send, unsigned char *rec, int n); private: // ファイルディスクリプタ / クロック(Hz) int fd; int clock; // 満載のブロック数 / 積み残しブロックのバイト数 int numFullBlocks, lastBlockSize; // SPI 転送の構造体 struct spi_ioc_transfer *trStruct; }; #endif
init() は,SPI モジュールを初期化します.SPI モジュールを利用するプログラムは,このメソッドを1回だけ呼び出す必要があります.SPI デバイス名を与えることができ,SPI0_CE0 を外部デバイスに接続する場合は "/dev/spidev0.0" を,SPI0_CE1 を外部デバイスに接続する場合は "/dev/spidev0.1" とします.たとえば,あるプログラムが spidev0.0 を利用し,同時に別のプログラムが spidev0.1 を利用することも可能です.また,SCLK の周波数を(たとえば 8MHz であれば 8000000 のように)与えることができます.64MHz まで(ただし 2n MHz)の周波数を指定できます.引数を与えずに init() とした場合は,spidev0.0 を 8MHz で初期化します.
スレーブにデータを送信するには,send1(), sendN(), sendBuffer() のメソッドを使います.send1() は1バイトの送信,sendN() は 2048バイトまでのバイト列の送信,sendBuffer() は任意の長さのバイト列の送信に使います.
一方,スレーブからデータを受信するには,sendRecN() を使います.SPI 通信では,N バイトのバイト列をマスターからスレーブに送信すると同時に,スレーブからマスターに同じ長さのバイト列が送信されます.同じ長さのバイト列を,マスターとスレーブの間で「同時交換」することで,双方向の通信が実現されるわけです.(上記の send1(), sendN(), sendBuffer() では,スレーブから受信したデータは破棄されていました.)
ここで利用する ADC である MCP3204/08 は,そのデータシートによれば,5 ビットからなるコマンド(1, S/D, D2, D1, D0)とそれに続く任意の 14 ビットのデータ(合計 19 ビット)をマスターから受信すると,その 7 ビット目の時点から,0 とそれに続く 12 ビットのデータ(合計 13 ビット)をマスターに送信するようになっています.Raspberry Pi の SPI は 8 ビット単位の送受信しかできない(たぶん)ので,下図のような 3 バイトの「交換」によってスレーブにコマンド(ADC のチャネル指定など)を送り,その結果(12 ビットのデータ)をスレーブから受け取ります.
ADC へのコマンド(上図でピンクの部分)は,コマンドの先頭を表示する 1 から始まり,シングルエンド入力(Single)と差動入力(Differential)を選択する S/D ビット,そしてチャネルを指定する D2, D1, D0 が続きます.MCP3208 であれば D2×4+D1×2+D0 がチャネル番号(0〜7)となり,MCP3204 であれば D2 は無視され,D1×2+D0 がチャネル番号(0〜3)となります.ここではシングルエンド入力のみを扱いますので S/D = 1 とします.この 5 ビットのコマンドに先行する 5 ビット(グレーの部分)はすべて 0 としなければなりません.一方,後続する 14 ビット(グレーの部分)は 1 でも 0 でも構いません.
ADC から返答されるデータは,通信開始から 12 ビット目以降に有意味なデータが含まれています.12 ビット目にデータの先頭を表示する 0,その後ろに 12 ビットの二進数(B11〜0)が続きます.これらに先行する 11 ビット分については,ADC からは何もデータが送られません.Raspberry Pi 側で受信すると 0, 1 の混ざった意味のないデータに見えるでしょう.これは無視してください.
以上のデータ転送処理を ADC::get() というメソッドにまとめると,以下のようになります.spi は SPI クラスのインスタンスで,初期化(init() の呼び出し)は済んでいるものとします.
int ADC::get(int channel)
{
// ADC_3204 or ADC_3208
// SPI 3byte (4ch/8ch; 12bit data = 0..4095)
unsigned char send[3], rec[3];
send[0] = (channel & 0x04)? 0x07: 0x06; // 0,0,0,0:0,1,1,D2
send[1] = (channel & 0x03) << 6; // D1,D0,0,0:0,0,0,0
send[2] = 0; // 0,0,0,0:0,0,0,0
// send and receive
spi.sendRecN(send, rec, 3);
// AD 変換結果(12bit: 0〜4095)を返す
return ((rec[1] & 0x0f) << 8) | rec[2]; // last 12 bits
}
このメソッドの戻り値は,指定したチャネルの電圧を表す 0〜4095 の整数となります.戻り値と電圧の間には,0 を 0V (=GND),4095 を 3.3V (=VREF) とする,ほぼ線形な対応関係があります.
ADC モジュールを C++ クラスにまとめたものが,raspADC.h と raspADC.cpp です.ここでは,ヘッダファイル raspADC.h を見ながら,公開メソッドの使い方を解説します.
// // Raspberry Pi ADC module (for MCP3204/08 SPI) #ifndef ADC_H #define ADC_H #include "raspSPI.h" #define ADC_3204 1 #define ADC_3208 2 #define ADC_CLOCK 1000000 // 1MHz at 2.7V Vcc class ADC { public: // 初期化(デフォルト: "/dev/spidev0.0" , ADC_3208) void init(); void init(const char *spiDevice); void init(int adc320X); void init(const char *spiDevice, int adc320X); // AD 変換結果(12bit=0..4095)を返す int get(int channel); private: SPI spi; int adcChip; }; #endif
公開メソッドは初期化のための init() と,指定したチャネルのアナログ電圧を 0〜4095 のディジタル値として読み出す get() の2種類だけです.init() は,使用する SPI デバイス("/dev/spidev0.0" または "/dev/spidev0.1")を与えてもよいし,ADC の種類(ADC_3204 または ADC_3208)を与えてもよいです.デフォルトでは "/dev/spidev0.1" と ADC_3208 に接続するようになっています.
この raspADC モジュールの使用例を以下に示します.ここでは ADC3208 を利用し,8 つのアナログチャネルの電圧を読み取り,約 100ms ごとにコンソール(標準出力)に表示するというものです.
#include <stdio.h>
#include "raspADC.h"
int main() {
ADC adc;
adc.init("/dev/spidev0.1", ADC_3208);
while (1) {
for (int i = 0; i < 8; i++)
printf("%6d", adc.get(i));
printf("\n");
usleep(100000);
}
}
Raspberry Pi と MCP3208 との接続は下図のようになります."analog sensors, etc." の部分には,アナログ電圧を出力するセンサ(ポテンショメータなど)を接続します.これらセンサには Raspberry Pi から電源を供給することを想定しています.
raspADC モジュールは raspSPI モジュールを利用していますので,これらモジュールのコンパイル,そして sampleADC.cpp のコンパイル・実行は,つぎのように行います.(この実行例では,CH0 は 3V3 へ,CH3〜7 は GND へ接続し,CH1, CH2 にセンサからのアナログ信号を入力しています.)
pi@kozPi ~/Projects/IOKit $ cc -c raspSPI.cpp pi@kozPi ~/Projects/IOKit $ cc -c raspADC.cpp pi@kozPi ~/Projects/IOKit $ cc sampleADC.cpp raspSPI.o raspADC.o -o sampleADC pi@kozPi ~/Projects/IOKit $ ./sampleADC 4095 1210 245 0 0 0 0 0 4095 1204 244 0 0 0 0 0 ...
SPI 通信はディスプレーの接続にも使えます.文字だけでなくグラフィック出力が,それもカラーで可能な超小型有機 EL ディスプレー(一般に OLED と呼ばれるので,以下 OLED)を Raspberry Pi に接続してみましょう.ここでは,aitendo から入手できる ALO-095BWNN-J9 を利用します.これは SSD1332 を利用した 96x64xRGB の OLED です.キャリー基板を含めても 1000円程度で入手できると思います.
- 参考資料:ALO-095BWNN-J9 の仕様, SSD1332 のデータシート
- ダウンロード:OLED モジュール raspOLED.h, raspOLED.cpp
- ダウンロード:6x8 フォント font6x8.h
- ダウンロード:サンプルプログラム sampleOLED.cpp, サンプル画像 image.h
この OLED は,それ単体で SPI による通信ができるため,配線は下図のようにシンプルで,接続用のピンヘッダ以外に外付け部品は不要です.SPI 通信は,SPI0_SCLK (DB0), SPI0_MOSI (DB1), SPI0_CE0 (CS) の3線で行います.SPI0_MIS を使わないのは,Raspberry Pi から OLED への単方向通信のみを利用しているためです.このほかに,OLED をリセットするための信号として GPIO_17 (RESET),転送データがコマンドなのかデータなのかを知らせる信号として GPIO_22 (RS) を使います.残りの接続は電源(3V3, GND)のみです.(OLED の VDD は画面処理 LSI である SSD1332 への電源,VCCLED は画面を構成する LED 群への電源です.NC (No Connection) を含め,未使用の端子はオープン(接続なし)のままでオーケーです.)
この SPI 接続をとおして,Raspberry Pi から1バイトあるいは複数バイトのコマンド(RS=0)や複数バイトのデータ(RS=1)を送信します.
OLED モジュールを C++ クラスにまとめたものが,raspOLED.h と raspOLED.cpp です.ここでは,ヘッダファイル raspOLED.h を見ながら,公開メソッドの使い方を解説します.
// // 96x64 OLED driver for Raspberry Pi // xkozima@myu.ac.jp (140602) #ifndef OLED_H #define OLED_H #include >string.h< #include "raspGPIO.h" #include "raspSPI.h" #define OLED_DC_GPIO 17 // RPi から RS(DC) への GPIO ピン #define OLED_RST_GPIO 22 // RPi から RST への GPIO ピン class OLED { public: // OLED 初期化(最初に1回呼び出す) // デフォルト値は "/dev/spidev0.0", 8000000 (8MHz) void init(); void init(const char *spiDevice); void init(const char *spiDevice, int clock); // 画面クリア(0: 黒,0xffff: 白, etc.) void clear(unsigned short color); // 図形描画メソッド(点・線分・長方形・円・楕円) void point(int x, int y, unsigned short color); void line(int x1, int y1, int x2, int y2, unsigned short color); void rect(int x, int y, int w, int h, unsigned short color, int fill); void circle(int x, int y, int r, unsigned short color, int fill); void ellipse(int x, int y, int rx, int ry, unsigned short color, int fill); // 画像描画メソッド(16bit(RGB565) or 24bit(RGB24)) void image(int x, int y, int w, int h, unsigned short *image16); void image(int x, int y, int w, int h, const unsigned short *image16); void image(int x, int y, int w, int h, unsigned char *image24); void image(int x, int y, int w, int h, const unsigned char *image24); // テキスト描画メソッド void text(int x, int y, char *string, unsigned short color); void text(int x, int y, char *string, unsigned short colorF, unsigned short colorB); // 色変換(16bit(RGB565) 色情報を 24bit(RGB24) 色情報から生成) unsigned short color(int r, int g, int b); private: SPI spi; void sendCommand1(unsigned char cmdOne); void sendCommandN(unsigned char *cmd, int length); void sendDataN(unsigned char *data, int length); void sendBuffer(unsigned char *buffer, int length); bool reversal; bool filling; }; #endif
初期化メソッド init() は最初に1回だけ呼び出す必要があります.描画メソッドの中で,線分と長方形については,SSD1332 のグラフィックアクセラレータ機能を使って高速に描画しています.円と楕円については,OLED モジュールの中で Bresenham のアルゴリズムにもとづき,point() や line() を使って描画しています.そのため,円と楕円の描画はやや遅いようです.テキスト描画は,6x8 ドットの ASCII フォント font6x8.h を point() によって(背景色は rect() によって)描画しています.
前景色や背景色は RGB565(赤5ビット,緑6ビット,青5ビットをまとめた 16 ビット=unsigned short)で与えます.color() メソッドを使えば,RGB の各成分を 0〜255 の数値で与えた色表現,この RGB565 に変換することができます.
image() は,各成分が unsigned short (RGB565) または3つの連続する unsigned char (RGB24) となるピクセル配列 (w × h 要素) を表示するメソッドです.
簡単な図形を描画するプログラム sampleOLED.cpp を例としてあげます.OLED オブジェクトは init() で初期化されます.それ以降は,描画メソッドで描画した結果を常に画面に保持します.このプログラムを終了するには,コンソール端末から ^C を打ってください.プログラム終了後も,その時点での描画結果を OLED は表示しつづけます.
#include <unistd.h> #include <math.h> #include "raspOLED.h" int main() { OLED display; display.init("/dev/spidev0.0"); // show the flag display.clear(display.color(255, 255, 255)); display.circle(48, 32, 20, display.color(215, 19, 69), true); usleep(2000000); // Lissajous 5/6 display.clear(display.color(255, 255, 255)); float rad = 0; while (1) { int posX = 48 + 25 * sin(rad * 5); int posY = 32 + 25 * cos(rad * 6); display.circle(posX, posY, 2, display.color(0, 0, 0), true); usleep(10000); display.circle(posX, posY, 2, display.color(215, 19, 69), true); rad += 0.01; } }
上のサンプルプログラムのコンパイルと実行は以下のように行います.コンパイルに -lm オプションを与えているのは,sin() や cos() という数学関数を使っているためです.このオプション(-l<ライブラリ名>)によって数学ライブラリ m が組み込まれます.sampleOLED.cpp の冒頭にある #include <math.h> にも注意してください.(なお,#include <unistd.h> は usleep() のために必要ですが,C 標準ライブラリに含まれているので,-l オプションは不要です.)
pi@kozPi ~/Projects/IOKit $ cc sampleOLED.cpp raspGPIO.o raspSPI.o raspOLED.o -lm -o sampleOLED pi@kozPi ~/Projects/IOKit $ sudo ./sampleOLED
さらに多様な描画機能や描画速度を評価するためのプログラムとして,sampleOLED2.cpp もあげておきます.同じ方法でコンパイル・実行してください.線分と長方形の描画については,かなり高速ではないでしょうか.
なお,OLED の一般的な特徴として,画面に焼き付けを起こすことがありますので,長時間同じ内容を表示しつづけないように注意してください.
Raspberry Pi にカラー液晶ディスプレーを接続します.ここでは液晶ディスプレー部品の入手容易性を考慮して,aitendo から入手できる M032C1289TP という 3.2 インチ TFT 液晶モジュール(タッチパネル付き)を使用します.(ebay 等でも同等の液晶モジュールが入手可能です.内蔵コントローラ SSD1289 をもつ 320x240 のカラー液晶モジュールであれば大丈夫でしょう.ただしピン配列は異なるかもしれません.)
- 参考資料:M032C1289TP の回路図, SSD1289 のデータシート
- ダウンロード:TFT モジュール raspTFT.h, raspTFT.cpp
- ダウンロード:サンプルプログラム sampleTFT.cpp
ここで問題となるのが,Raspberry Pi との接続方法です.M032C1289TP は 16bit パラレルのインタフェースのみが使用可能になっています.ところが Raspberry Pi で 16 本のデータ信号線+数本の制御信号線を用意するのは難しいでしょう.そこで,SPI 通信を使って画像情報を送出し,シリアル ⁄ パラレル変換器 (Serial to Parallel Converter) を別途用意して,TFT 液晶モジュールへの接続に必要な 16bit パラレル信号をつくりだします.
Raspberry Pi Model B+ であれば,20本以上の GPIO を利用できるので,ここで紹介するシリアル ⁄ パラレル変換器はかならずしも必要ではありません.
Raspberry Pi の SPI から送出されるシリアル信号を TFT 液晶モジュールが受け取れる 16bit パラレル信号に変換するディジタル回路をつくります.Raspberry Pi から液晶モジュールへの一方向の変換です.SPI 信号は,シリアルデータ (SPI0_MOSI) とクロック (SCLK), そしてスレーブ選択信号 (CE0) の3本となります.
SCLK が 0 から 1 に立ち上がるタイミングで MOSI の 0/1 の状態をシフトレジスタ(shift register)の左端に読み込みます.それと同時に,シフトレジスタの各要素の内容(0/1)を右隣の要素に転送します.これにより,つねに最新 16bit 分のシリアルデータをシフトレジスタが保持するようにします.一方,カウンタ(counter)は SCLK のパルス数を(立ち下がりで)カウントし,8 パルスごとに出力ストローブ(strobe)信号のオン・オフを入れ替えます.出力ストローブ信号 1 を受けたシフトレジスタは,各要素が保持している内容を,TFT 液晶モジュールに向けてパラレル出力します.ストローブ信号が 0 であるあいだ,シフトレジスタはパラレル出力を変化させません.カウンタの値は,CE0 が選択された(つまり SPI データ転送が開始された)ときに 0 にリセットされるようにしてあります.
これら機能を回路図に落とし込むと,上図のようになります.74HC4094 は 8bit のシフトレジスタで,これを2つカスケード接続することで 16bit のシフトレジスタを実現しています.D がデータ入力,CP↑ がクロック入力(立ち上がり),STR がストローブ信号で,QP0〜7 がパラレル出力,QS1 はカスケード接続用のデータ出力です.一方,74HC4040 はカウンタで,CP↓ はクロック入力(立ち下がり),MR はリセット入力,Q0〜9 は 10bit の2進出力で,最大 1024 パルスまでカウントできます.ここでは Q3 を取り出し,74HC4094 へのストローブ信号(STR)や TFT 液晶モジュールへの書き込み信号(WR)を作り出しています.Q3 はクロック 8 パルス目に 1 になり,16 パルス目に 0 に戻ることを繰り返します.74HC4094 は,STR が 0 のあいだ,パラレル出力をホールドします.したがって,16 パルス目の出力状態が(24 パルス目まで)ホールドされます.この STR を反転した信号を液晶モジュールの書き込み信号 WR とし,その立ち上がりのタイミング(WR↑)で,ホールド状態にあるパラレル信号が液晶モジュールに取り込まれます.
Raspberry Pi の CPU である ARM プロセッサは「リトルエンディアン」がデフォルトとなっています.16bit の整数データは,下位 8bit が先に,上位 8bit がその次に送信されます.また,SPI は MSB(最上位ビット)から順次送信されます.つまり,最初に送信されるのは,下位バイトの MSB です.これを 16bit のパラレル信号に復元するために,上の回路図のような配線(DB0〜15)となっています.
この回路図に,電源まわりのバイパスコンデンサ(".01u" = 0.01µF = "103" 積層セラミックコンデンサ,および "10u" =10µF 電解コンデンサ)などを加え,実体配線に近づけたものが上図です.左側に Raspberry Pi の P1 端子群,右側に液晶モジュールの JP1 端子群を示します.青字の端子は Raspberry Pi へ,赤字の端子は液晶モジュールに向かいます.ディスプレーへの電源は,Raspberry Pi から供給します.P1-17 の 3V3 と P1-20 の GND を利用すればよいでしょう.P1-22 の GPIO_25 からは,ディスプレーに送信した 16bit データが「制御コマンド」なのか「データ」なのかを区別するための D/C 信号を出力します.このピンを適切に出力した状態で SPI からデータを送信する必要があります.また,BL にはバックライトの輝度を調節するための PWM 信号(P1-12 の PWM0)を与えますが,ここでは 10kΩ でプルアップされた状態で,端子を開放しておきます.この回路をブレッドボード上に実装するとすれば,およそ下図のような配線となるでしょう.
1画面(320x240 ピクセル)の画像データが与えられると,その内容を TFT 液晶ディスプレーに表示する「TFT モジュール」を用意します.ここにはヘッダファイル raspTFT.h のみを載せますが,実装ファイル raspTFT.cpp も必要です.TFT 液晶ディスプレーに転送する画像データは unsigned char の配列で,最上段のライン(左端→右端)から1段ずつ下に降りることで,画像を1次元配列にしたものです.2バイト(16bit)ごとに1つの画素(ピクセル)を構成し,MSB 側から赤 5bit, 緑 6bit,青 5bit(ただしリトルエンディアン)となっています.1画面分の配列をフレームと呼ぶことにします.
// // Raspberry Pi TFT LCD module (for M032C1289TP/SSD1289) #ifndef TFT_H #define TFT_H #include "raspGPIO.h" #include "raspSPI.h" // 画面サイズ #define TFT_WIDTH 320 #define TFT_HEIGHT 240 #define TFT_BYTES (TFT_WIDTH*TFT_HEIGHT*2) // タッチの種類 #define TFT_TOUCH_NONE 0 #define TFT_TOUCH_DOWN 1 class TFT { public: // 初期化 void init(); // フレーム転送 void sendFrame(unsigned char *frame); void sendBuffer(unsigned char *buffer, int x, int y, int width, int hegith); // テストパターン代入 void setTestPattern(unsigned char *buffer, int width, int height); // タッチパネル(TFT_TOUCH_NONE, TFT_TOUCH_DOWN を返す) int getTouch(int *x, int *y); private: SPI spi; void sendCommand1(unsigned char cmdOne); void sendCommand2(unsigned char upper, unsigned char lower); void sendData2(unsigned char upper, unsigned char lower); void sendCommandData2(unsigned char cmd, unsigned char data1, unsigned char data2); void sendBufferN(unsigned char *buffer, int len); }; #endif
TFT 液晶ディスプレーに画像を表示するには,制御コマンド(初期化など)やそれに続く画像データを適切に伝達する必要があります.init() メソッドが初期化を,sendFrame() がフレームの転送(表示)を担うメソッドです.sendBuffer() は,画面の (x, y) の位置に w×h の大きさの画像を転送(表示)します.また,setTestPattern() はテストパターン画像をフレームに代入するためのものです.
なお,ここで紹介する設定条件では,液晶モジュールを上下を逆さま(基板上のシルク印刷が逆さまに見える向き)に置いた状態で画像表示することを前提にしています.これは,やや上方から液晶画面を見下ろしたときの発色やコントラストを良くするためです.
つぎにあげる sampleTFT.cpp をコンパイルし,sudo で実行してみてください.テストパターンがディスプレーに表示されれば合格です.表示されない場合は,配線をよく確かめてから再チャレンジしてください.
#include "raspTFT.h"
// 画像メモリ
// テストプログラム
int main ()
{
TFT display;
unsigned char frame[TFT_BYTES];
// TFT を初期化
display.init();
// テストパターン代入
display.setTestPattern(frame, TFT_WIDTH, TFT_HEIGHT);
// 画像を転送・表示
display.sendFrame(frame);
}
コンパイルと実行は以下のように行います.sampleTFT.cpp のヘッダファイルとモジュール間の依存関係から,どのモジュール(.o)を組み込めばよいかわかります.
pi@kozPi ~/Projects/IOKit $ cc -c raspTFT.cpp pi@kozPi ~/Projects/IOKit $ cc sampleTFT.cpp raspGPIO.o raspSPI.o raspTFT.o -o sampleTFT pi@kozPi ~/Projects/IOKit $ sudo ./sampleTFT
フレームバッファとは,画面全体に表示される内容を1枚の画像データとして保持するデバイスで,Raspberry Pi では /dev/fb0 として実装されています.ここでは,/dev/fb0 を mmap でメモリに対応づけ,その内容を逐次ディスプレーに転送することで,Raspberry Pi のコンソールや X Window デスクトップを表示するプログラム(ディスプレイ ドライバ)を作ります.
- ダウンロード:FB モジュール raspFB.h, raspFB.cpp
- ダウンロード:サンプルプログラム sampleFB.cpp
公開メソッドの init() は,希望する幅 width と高さ height を格納した int 型変数へのポインタを与えることで,フレームバッファを初期化し,得られた幅と高さにこれら変数の内容を書き換えます.たとえば int w = 320, h = 240 として init(&w, &h) を呼び出せば,320×240 のフレームバッファ(16bpp)へのアクセスが確保されるはずです.getFrame() メソッドは,確保されたフレームバッファの先頭アドレスを返します.このアドレスから始まる 320×240×2 バイトが1画面を構成するフレームとなります.
// // Raspberry Pi FB module #ifndef FB_H #define FB_H #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <fcntl.h> #include <linux/fb.h> #include <stdio.h> #define FB_DEVICE "/dev/fb0" // FB デバイス class FB { public: void init(int *width, int *height); unsigned char *getFrame(); private: int fd; volatile unsigned char *frame; }; #endif
この FB モジュールを使ったサンプルプログラムはつぎのようになります.TFT を初期化し,FB を 320×240 で初期化した後,フレームバッファをそのままディスプレーに転送しています.
#include "raspTFT.h"
#include "raspFB.h"
// フレームバッファ
// テストプログラム
int main ()
{
TFT display;
FB fb;
// TFT を初期化
display.init();
// FB を初期化
int width = TFT_WIDTH;
int height = TFT_HEIGHT;
fb.init(&width, &height);
// 画像を転送・表示
while (1) {
display.sendFrame(fb.getFrame());
}
}
このプログラムを動かす前に,Raspberry Pi 起動時にフレーム バッファの大きさを 320x240 に設定するように仕込みます.以下の作業を行ってください.
pi@kozPi ~/Projects/IOKit $ sudo vi /boot/config.txt ... #framebuffer_width=1280 #framebuffer_height=720 framebuffer_width=320 framebuffer_height=240 ...
また,小さなコンソール画面(X Window 起動前の文字画面)でも多くの文字を表示できるフォントに切り替えます.以下の作業を行ってください.
pi@kozPi ~/Projects/IOKit $ sudo vi /etc/default/console-setup ... CODESET="guess" FONTFACE="Terminus" FONTSIZE="6x12" ...
キーボードを接続して,リブートすれば準備完了です.X-Window を動かす場合は,マウスも接続しておいてください.準備ができたら,以下の手順で sampleFB.cpp をコンパイルし,実行してみてください.Raspberry Pi の画面がディスプレーに表示されるはずです.これで(SPI が 16MHz のとき)約 10 fps の実行速度となるようです.
pi@kozPi ~/Projects/IOKit $ cc -c raspFB.cpp pi@kozPi ~/Projects/IOKit $ cc sampleFB.cpp raspGPIO.o raspSPI.o raspTFT.o raspFB.o -o sampleFB pi@kozPi ~/Projects/IOKit $ sudo ./sampleFB & (これでコンソール画面がディスプレーに表示される. まだ X Window を起動していなければ,次も試してみる.) pi@kozPi ~/Projects/IOKit $ startx
なにせ画面が小さく,マウスポインタや画面の動きがモッサリしているかと思いますが,1台のパソコンとして必要な入出力機能をもっていることがわかります.なお,Raspberry Pi 起動時からディスプレー表示を開始するには,以下のように,/etc/init.d/bootmisc.sh の do_start() {...} 内部の最後に赤字部分を追加してください.
pi@kozPi ~/Projects/IOKit $ cc -O3 sampleFB.cpp raspGPIO.o raspSPI.o raspTFT.o raspFB.o -o fb2tft (最適化コンパイルでプチ高速化) pi@kozPi ~/Projects/IOKit $ sudo cp fb2tft /usr/local/bin (fb2tft を /usr/local/bin の中にコピー) pi@kozPi ~/Projects/IOKit $ sudo vi /etc/init.d/bootmisc.sh ... do_start () { ... # fb2tft (FrameBuffer to TFT) if [ -f /usr/local/bin/fb2tft ]; then /usr/local/bin/fb2tft & fi } ...
描画の遅さといい画面の狭さといい,まだまだ改善の余地がありそうです.次節では,VNC サーバを活用して高速に画面を表示する方法や,タッチパネルを利用する方法を解説し,より実用的な X-Window 端末に仕上げます.
ここでは,前節で扱った TFT 液晶モジュールを実用的な X-Window 端末に仕立てることをめざし,描画の高速化とタッチパネルの利用について解説します.描画の高速化には,前出の VNC サーバ (tightvncserver) を利用し,フレーム全体(320x240)を再描画するのではなく,更新された画面領域だけを再描画するようにします.タッチパネルは,既にこの TFT 液晶モジュール(M032C1289TP)に装着されていますので,数本の配線を追加するだけで利用可能です.
- ダウンロード:VNC モジュール raspVNC.h, raspVNC.cpp
- ダウンロード:サンプルプログラム sampleVNC.cpp
ここからは,やや上級者向けの内容になります.すべてを詳しく説明することはできませんので,ソースプログラムを参照しながら読み進めてください.
VNC サーバを使うことで,X-Window のデスクトップを別 PC(VNC クライアント)の画面に表示し,そこからマウスやキーボードで X-Window を操作することができます.ここでは,この仕組みを Raspberry Pi 内部で利用します.VNC サーバ(tightvncserver)を動かし,そこに接続する VNC クライアントを作成します.この VNC クライアントは,X-Window のデスクトップを高速に TFT 液晶ディスプレーに表示するものです.
VNC サーバとの通信には TCP/IP を使います.したがって,Raspberry Pi 内部に閉じた使い方だけでなく,他の PC や Raspberry Pi のデスクトップを TFT 液晶ディスプレーに表示することも可能です.ですが,ここでは,自分自身を表す IP アドレスである "127.0.0.1" のポート 5901 に接続することで,自分自身の VNC サーバに接続するようにします.そのためのプログラムはおよそ次のようになります.connect() できれば,read(sock, buffer, length) や write(sock, buffer, length) によって,サーバとの情報交換が可能になります.
void VNC::init (int w, int h, int fmt) { int res; // サーバに接続 struct sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = inet_addr(addr); // addr: "127.0.0.1" address.sin_port = htons(port); // port: 5901 sock = socket(AF_INET, SOCK_STREAM, 0); connect(sock, (struct sockaddr *) &address, sizeof(address)); ... }
VNC サーバとの通信プロトコル(正確には RFB プロトコル)は,ちょっと複雑です.まず,プロトコルのバージョンをサーバとクライアント間で交換し,下位プロトコルに合わせます.ここではバージョン 3.3 の利用を想定しています.つぎのプログラム断片にある VNC::secureRead() と VNC::secureWrite() は,任意の長さのバイト列を確実に受信・送信する関数です.(read() や write() は1回の呼び出しでは送受信が完了しない場合があります.)
void VNC::init (int w, int h, int fmt) {
...
unsigned char serverVer[12];
secureRead(sock, serverVer, 12); // たぶん "RFB 003.008\n"
unsigned char clientVer[13] = "RFB 003.003\n";
secureWrite(sock, clientVer, 12); // こちらは Ver 3.3
...
}
さらに,VNC のセキュリティ認証を行います.「デスクトップ画面の共有」で VNC サーバにパスワードを設定した場合は,クライアントはそのパスワードを使ってサーバから認証を受ける必要があります.サーバにパスワードを設定していなければ,ここはスキップです.
セキュリティ認証には,サーバから受信した 16 バイトのデータ(「チャレンジ」と呼ばれる)を,パスワードを使って暗号化した結果(やはり 16 バイトのデータで「レスポンス」と呼ばれる)をサーバに返すことで行います.まず,8 文字のパスワード(7bit ASCII;長ければ先頭 8 文字,短ければ末尾に 0x00 をパディング)の各文字について MSB〜LSB のビット並びを入れ替え,さらに 0 となった LSB に奇数パリティを入れたもの (desKey) を用意します.この desKey を「鍵」として,チャレンジを DES 暗号化(ecb_crypt() を利用)したものがレスポンスとなります.これをサーバに送信すると,認証結果が 4 バイトの整数(0 なら成功, それ以外なら失敗)で返されます.
void VNC::init (int w, int h, int fmt) {
...
// チャレンジ受信・暗号化・レスポンス送信
unsigned char chaRes[16];
secureRead(sock, chaRes, 16);
ecb_crypt(desKey, (char *) chaRes, 16, DES_ENCRYPT);
secureWrite(sock, chaRes, 16);
// 結果をチェック(0: 成功, non-zero: 失敗)
unsigned char result[4];
secureRead(sock, result, 4);
if (result[0] | result[1] | result[2] | result[3]) {
printf("VNC (error): authentification failed (VNC)\n");
exit(1);
}
...
}
セキュリティ認証の後,クライアントからサーバ初期化メッセージ(1 バイトの 1)を送ると,サーバから画面の大きさやピクセル形式,タイトル(文字列)などが送られてきます.これを受けて,クライアントからは必要とするピクセル形式やエンコーディング形式を指定します.ここではピクセル深さが 16bit または 24bit のいずれかを指定できるようにしてありますが,TFT 液晶ディスプレーが 16bit なので,前者を指定した方が効率よいでしょう.また,エンコーディング形式としては,シンプルな Raw 形式(矩形領域のピクセルデータの配列)を指定しています.なお,クライアント側は(通常どおり)リトルエンディアンで動作していますが,VNC サーバとの数値データのやりとりのみビッグエンディアンとなっているので注意してください.ピクセルデータにはリトルエンディアンを指定していますが,リトルエンディアンになるのはピクセルデータのみです.
これ以降,クライアントからリクエストを送信すると,サーバ側の X-Window の画面データ(ただし (serverX, serverY) を左上端とする width × height の画面領域)がクライアントに送信されます.初回のみ全画面,それ以降は更新部分だけをリクエストすることで,効率的な画面更新が可能になります.サーバからの更新情報を受け取るのが update() です.これを繰り返し呼び出すことで,X-Window の画面の更新部分だけを効率よく TFT 液晶ディスプレーに表示することができます.
void VNC::update () { // FramebufferUpdate の処理 int bytesAvailable; ioctl(sock, FIONREAD, &bytesAvailable); if (bytesAvailable) { unsigned char header[4]; secureRead(sock, header, 4); if (header[0] == 0) { // フレーム情報を読み出す int num = header[2] * 256 + header[3]; for (int i = 0; i < num; i++) { unsigned char xy[4], wh[4], enc[4]; secureRead(sock, xy, 4); secureRead(sock, wh, 4); secureRead(sock, enc, 4); int x = (xy[0] << 8) + xy[1]; int y = (xy[2] << 8) + xy[3]; int w = (wh[0] << 8) + wh[1]; int h = (wh[2] << 8) + wh[3]; int e = enc[3]; // バッファ確保 int bytes = w * h * bytesPP; if (buffer == NULL) { buffer = (unsigned char *) malloc(bytes); bufferSize = bytes; } else if (bytes > bufferSize) { free(buffer); buffer = (unsigned char *) malloc(bytes); bufferSize = bytes; } // 画像を読み出す secureRead(sock, buffer, bytes); // コールバックで処理する if (drawCB) drawCB(buffer, x - serverX, y - serverY, w, h); } // FramebufferUpdateRequest(つぎの画面更新に向けて) // ROI = (serverX, serverY, width, height) unsigned char fbReq[10] = { 0x03, 0x01 }; // 差分のみ fbReq[2] = (serverX >> 8) & 0xff; fbReq[3] = serverX & 0xff; fbReq[4] = (serverY >> 8) & 0xff; fbReq[5] = serverY & 0xff; fbReq[6] = (width >> 8) & 0xff; fbReq[7] = width & 0xff; fbReq[8] = (height >> 8) & 0xff; fbReq[9] = height & 0xff; secureWrite(sock, fbReq, 10); } ... } ... }
通常 read() やそれを利用した VNC::secureRead() は,データが届くまで戻りません(つまり処理がブロックされます)が,上にあげた VNC::update() では,サーバから何バイト届いているのかを ioctl() で調べ,データが届いていれば読み出して処理し,届いてなければスキップするようにしています.画面表示のみを行うのであれば,このような仕組みは不要ですが,後述するタッチパネルの処理をこの VNC::update() に組込むために,このようなノンブロッキング処理をしています.(複数スレッドの利用なども考えられますが,ここではシンプルな実装にしたいと思います.)
つぎにあげるプログラム testVNC.cpp は,VNC サーバとの接続と画面表示を確認するためのものです.サーバからの画面更新データが届くと,それを描画コールバック(VNC::drawCB())で処理するようになっています.描画コールバックは,vncDraw() のような呼出形式をとり,矩形領域をディスプレーに表示する処理を担います.描画コールバックは VNC::presetDrawCB() に関数名を与えて登録します.
#include "raspVNC.h" #include "raspTFT.h" TFT display; VNC server; // 描画用コールバック(画面が更新されると呼ばれる) void vncDraw (unsigned char *buf, int x, int y, int w, int h) { // buf (w*h*2 bytes) を TFT の (x, y) に表示 display.sendBuffer(buf, x, y, w, h); } // メイン int main () { // TFT 初期化 display.init(); // VNC 初期化 server.presetServer("127.0.0.1", 5901); server.presetDrawCB(vncDraw); server.presetPassword(getpass("VNC password: ")); server.init(TFT_WIDTH, TFT_HEIGHT, VNC_FMT_RGB16); printf("testVNC: %s\n", server.getTitle()); // FramebufferUpdate を補足し vncDraw で表示 while (1) { server.update(); } }
以下の手順でコンパイル・実行してみてください.testVNC を動かす前に,VNC サーバを起動させておく必要があります.VNC サーバでパスワードを設定してある場合は,"VNC password:" と聞かれますのでタイプしてください.画面にはパスワードは表示されません.これで X-Window のデスクトップが表示されるはずです.
pi@kozPi ~/Projects/IOKit $ cc -c raspVNC.cpp pi@kozPi ~/Projects/IOKit $ cc testVNC.cpp raspGPIO.o raspSPI.o raspTFT.o raspVNC.o -o testVNC pi@kozPi ~/Projects/IOKit $ vncserver :1 -geometry 320x240 -depth 16 New 'X' desktop is kozPi:1 Starting applications specified in /home/pi/.vnc/xstartup Log file is /home/pi/.vnc/kozPi:1.log pi@kozPi ~/Projects/IOKit $ sudo ./testVNC VNC password: ******** VNC (info): VNC authentificationi successful VNC (info): server PixelFormat is 320x240@16bpp VNC (info): set PixelFormat to 16bpp/16depth VNC (info): set Encodings to RAW testVNC: pi's X desktop (kozPi:1)
ここで扱っている TFT 液晶モジュール (M032C1289TP) には,抵抗薄膜を利用したタッチパネルと専用 IC (ADS7843 (XPT2046)) が標準で装備されています.これに類似した TFT 液晶モジュールでも,ほぼ同じような構成になっているようです.この IC は,SPI 通信によって Raspberry Pi と接続できるので,下図のように,数本の配線を追加するだけで利用可能です.SPI0_SCLK と SPI0_MOSI は,シリアル ⁄ パラレル変換器への接続と共有します.シリアル ⁄ パラレル変換器には SPI0_CE0(つまり "/dev/spidev0.0")を接続していますので,ADS7843 には SPI0_CE1(つまり "/dev/spidev0.1")を接続するようにします.D_Penirq は,ペンや指がタッチパネルに触れているとき,LOW 出力となります.ここでは,プルアップ設定した GPIO24 に接続するようにします.
タッチパネルは,一様な電気抵抗をもつ長方形の抵抗薄膜を2枚を,わずかな隙間を与えて重ね,ペンや指がタッチしたとき,これら抵抗薄膜がタッチ位置で電気的に接続されるような構造をもっています.下図に示すように,上側の抵抗薄膜の左右の電極間(距離 d)に一定電圧 V をかけておきます.タッチされた位置(矢印)の電圧 Vx は,左辺からタッチ位置までの距離 x に比例し,V x ⁄ d となるはずです.この電圧 Vx は,下側の抵抗薄膜の電極から読み出すことができます.ADC の入力インピーダンスは十分大きい(つまり ADC に電流は流れ込まない)ので,計測される電圧は下側の抵抗薄膜におけるタッチ位置には依存しないのです.Vx が計測できたら,上下の抵抗薄膜の役割を入れ替えれば,縦方向の電圧 Vy も同じように計測できます.ここからタッチ座標 (x, y) を求めるのは簡単です.(なお,この原理から自明なように,マルチタッチは検出できません.2点以上を同時にタッチすると,その中央付近の1点として検出されるでしょう.)
ADS7843 に Vx, Vy を計測させるには,下図の SPI 通信プロトコルによって,(1) 計測する軸(横・縦)などを指定して電圧勾配をつくり,(2) 計測結果を 12 ビット符号なし整数で受け取ることが必要です.byte 1 として 0x94 を送れば横軸(長辺)方向の電圧 Vx が,0xD4 を送れば縦軸(短辺)方向の電圧 Vy が転送されます.詳しくは ADS7843 のデータシートを参照してください.
この機能を実装したのが,下にあげる TFT::getTouch() です.このメソッドをポーリングする(繰り返し呼び出す)ことで,タッチの有無やタッチ位置を読み出すことができます.ここでは説明を簡単にするために,12 ビット整数として読み出した Vx と Vy を,そのまま簡単な線形関数によって画面座標(320x240)に変換しています.この変換式は,タッチパネルごとの個体差にある程度依存したものになるでしょう.実際の raspTFT.cpp には,ノイズ除去などの機能を加えた TFT::getTouch() が入っています.
// タッチパネルの読み取り(簡易版) int TFT::getTouch(int *x, int *y) { // タッチの有無をチェック (タッチすると GPIO24 が LOW) int mode = (gpio_read(24) == 1)? TFT_TOUCH_NONE: TFT_TOUCH_DOWN; // タッチがあれば座標を読み取る if (mode != TFT_TOUCH_NONE) { unsigned char send[3], rec[3]; // X軸の読み取り(12bit: 0x00:0x7f:0xf8) send[0] = 0x94; send[1] = 0x00; send[2] = 0x00; spi1.sendRecN(send, rec, 3); int xraw = ((rec[1] & 0x7f) << 5) | ((rec[2] & 0xf8) >> 3); // Y軸の読み取り(12bit: 0x00:0x7f:0xf8) send[0] = 0xD4; send[1] = 0x00; send[2] = 0x00; spi1.sendRecN(send, rec, 3); int yraw = ((rec[1] & 0x7f) << 5) | ((rec[2] & 0xf8) >> 3); // 画面座標に変換(要キャリブレーション) int xpos = -0.0918 * xraw + 350.00; int ypos = -0.0678 * yraw + 260.00; if (xpos < 0) xpos = 0; else if (xpos >= TFT_WIDTH) xpos = TFT_WIDTH - 1; if (ypos < 0) ypos = 0; else if (ypos >= TFT_HEIGHT) ypos = TFT_HEIGHT - 1; *x = xpos; // 画面座標 *y = ypos; // を返す } return mode; // タッチの有無(または種類)を返す }
タッチ位置が取得できているかを試すためのテストプログラムを下にあげます.このプログラムでは,あらかじめ黒画面を用意し,TFT::getTouch() で取得したタッチ位置に赤ドット(2x2)を置くことを繰り返しています.コンパイルと実行は "cc testTouch.cpp raspGPIO.o raspSPI.o raspTFT.o -o testTouch" と "sudo ./testTouch" です.指先やタブレット用のペンなどを使って,タッチパネルに文字や絵を描いてみてください.
#include "raspTFT.h" TFT display; int main () { // 黒画面を用意 unsigned char *frame; frame = (unsigned char *) calloc(TFT_BYTES, 1); display.init(); display.sendFrame(frame); // タッチ位置に赤ペン while (1) { int x, y; if (display.getTouch(&x, &y) != TFT_TOUCH_NONE) { // はみだし対応 if (x > 318) x = 318; if (y > 238) y = 238; // 赤ペン/中太(RGB565 リトルエンディアン) unsigned char pen[] = {0x00, 0xf8, 0x00, 0xf8, 0x00, 0xf8, 0x00, 0xf8 }; display.sendBuffer(pen, x, y, 2, 2); } } }
これで VNC サーバへの接続と,タッチパネルとの接続ができました.これらを組み合わせることで X-Window 端末が実現できます.前出の testVNC.cpp をベースとし,タッチ処理を追加します.追加するのは,TFT::getTouch() によってタッチ状態・タッチ位置を読み取り,その情報を VNC サーバに「ポインタイベント」として送信するという機能です.この追加機能を,タッチ処理用コールバックとして(描画用コールバックと同じように)仕込みます.このコールバック関数は,VNC::update() の中で繰り返し呼び出されます.
#include "raspVNC.h" #include "raspTFT.h" TFT display; VNC server; // 描画用コールバック(画面が更新されると呼ばれる) void vncDraw (unsigned char *buf, int x, int y, int w, int h) { // buf (w*h*2 bytes) を TFT の (x, y) に表示 display.sendBuffer(buf, x, y, w, h); } // タッチ処理用コールバック(VNC 内部で繰り返し呼ばれる) void vncTouch (int *x, int *y, int *m) { // タッチ状態を取得 int xpos, ypos, mode; mode = display.getTouch(&xpos, &ypos); // タッチ状態をサーバへ返す *x = xpos; *y = ypos; *m = mode; } // メイン int main () { // TFT 初期化 display.init(); // VNC 初期化 server.presetServer("127.0.0.1", 5901); server.presetDrawCB(vncDraw); server.presetTouchCB(vncTouch); server.presetPassword(getpass("VNC password: ")); server.init(TFT_WIDTH, TFT_HEIGHT, VNC_FMT_RGB16); printf("sampleVNC: %s\n", server.getTitle()); // FramebufferUpdate を補足し vncDraw で表示 while (1) { server.update(); } }
これをコンパイルするには "cc sampleVNC.cpp raspGPIO.o raspSPI.o raspTFT.o raspVNC.o -o sampleVNC" と打ち込みます.実行するには,testVNC の場合と同じように,まず VNC サーバを起動("vncserver :1 -geometry 320x240 -depth 16")してから,"sudo ./sampleVNC" と打ち込みます.VNC パスワードを設定してある場合は,それを入力してください.
これで X-Window 端末として,なんとか使えるレベルになったのではないでしょうか.デスクトップのアイコンや,あるいはウェブブラウザ midori で開いたウェブページ上のリンクを,指先などで触れてみてください.相変わらず小さい画面ですが,midori で「表示 > フルスクリーン」とし,右端と下端のスクロールバーを動かせば,わりと快適?にブラウジングできると思います.
最後に画面の狭さを解決したいと思います.もちろん表示できるのは 320x240 の画面です.ここでは,640x480 のような大きな仮想デスクトップ(左下図)をつくり,その任意の位置にある 320x240 の部分画像をディスプレーに表示するようにします (右下図).切り出す部分画像の位置は,タッチスクリーンを使って上下左右にスクロールできるようにします.また,前出の sampleVNC ではタッチ位置に「左ボタン」を押すことしかできませんでしたが,ここでは2つのスイッチを加えて,左右クリック(同時押しで中クリック)が可能なタッチパネルに仕立てます.スイッチを押さずにドラッグすれば画面スクロールとなります.
画面スクロールは,VNC サーバに転送対象となる画面領域(いわゆる ROI)の左上端の座標 (serverX, serverY) を指定することで実現しています.この座標を新しく指定し,一度だけ全画面データをリクエストすればよいでしょう.注意すべき点は,画面スクロール後は,サーバ側の画面座標とディスプレー側の画面座標がずれることです.タッチ座標をサーバに受け渡すときや,転送された部分画像をディスプレー上に描画するときは,このズレを考慮しなければなりません.
左右のボタンは,ここでは GPIO22 と GPIO23 に取り付けたスイッチ(いずれもオン状態で GND に接続)で読み取ります.これらのピンはプルアップを設定しておきます.どのボタンを押されたのかを読み取り,それに対応したポインタイベントを VNC サーバを送信すればよいでしょう.
これら機能(画面スクロールとボタン状態の読み取り)を raspTFT.h/cpp と raspVNC.h/cpp に追加したものが,つぎにあげる raspTFT2.h/cpp と raspVNC2.h/cpp です.また,これらを使った X-Window 端末プログラム sampleVNC2.cpp(ほとんど sampleVNC.cpp と同じ)もあげておきます.
- ダウンロード:機能強化 TFT モジュール raspTFT2.h, raspTFT2.cpp
- ダウンロード:機能強化 VNC モジュール raspVNC2.h, raspVNC2.cpp
- ダウンロード:機能強化サンプルプログラム sampleVNC2.cpp
sampleVNC2 を動作させるには,つぎのようにします.おそらく,VNC サーバが 320x240 で動いているでしょうから,ここではそれを停止し,改めて 640x480 で起動しています.仮想デスクトップの左上部分が表示されたら,指先などでディスプレー画面をドラッグしてみてください.
pi@kozPi ~/Projects/IOKit $ cc -c raspTFT2.cpp pi@kozPi ~/Projects/IOKit $ cc -c raspVNC2.cpp pi@kozPi ~/Projects/IOKit $ cc sampleVNC2.cpp raspGPIO.o raspSPI.o raspTFT2.o raspVNC2.o -o sampleVNC2 pi@kozPi ~/Projects/IOKit $ vncserver -kill :1 Killing Xtightvnc process ID 2301 pi@kozPi ~/Projects/IOKit $ vncserver :1 -geometry 640x480 -depth 16 New 'X' desktop is kozPi:1 Starting applications specified in /home/pi/.vnc/xstartup Log file is /home/pi/.vnc/kozPi:1.log pi@kozPi ~/Projects/IOKit $ sudo ./sampleVNC2 VNC password: ******** VNC (info): VNC authentificationi successful VNC (info): server PixelFormat is 640x480@16bpp VNC (info): set PixelFormat to 16bpp/16depth VNC (info): set Encodings to RAW sampleVNC: pi's X desktop (kozPi:1)
ディスプレー表示部分(320x240)に比べてあまりに大きな仮想デスクトップ(1920x1080 など)を用意すると「迷子」になってしまうかもしれません.ですが,ここまでだけでも,それなりに実用性のある X-Window 端末が実現できたと思います.さらに改良したい点としては,ボタン(スイッチ)を使わずに,スマートフォンのように,タッチの種類(タップ,ダブルタップ,パン (=スクロール),スワイプなど)を判別し,より直感的な操作を可能にすることなどが考えられます.あと,キーボード入力をどのように実現するかも課題です.ぜひチャレンジしてみてください.