I/O Expander(2)

I2C接続のI/O拡張チップMCP23017をRaspberry Piに接続して、I/Oポートを16bit分増やすことができたので、試しにそのポート全てにLEDをつなげてみた。R0012002

写真の最も右側のLEDが拡張ポートBのB7、最も左のLEDが拡張ポートAのB0になるように接続してある。LEDの+極が電流制限抵抗を介してVCCにつながっているので、拡張ポートの出力を0にするとLEDが点灯、1にするとLEDが消灯する。

これを見ただけで分かる人には何をやろうとしているか判ると思うが、残像によって空間に文字や図形を描くPOV(Persistent Of Vision)またはバーサライタと呼ばれるものをRaspberry Piで試してみようと思った次第。

実際に暗い所でこれを左右に振ると、こんなふうに見える。
R0012001表示したい図形を縦一列ごとに高速に切り替えて表示しているから、左右に振ると残像で元の画像が見える。

プログラムは前回のテストプラグラムとほとんど同じで、表示する図形データを順番に読み込んでMCP23017のポートAとポートBに書き込んでいるだけ。負論理でLEDが点灯するようになっているので、図形データは予めビット反転させてある。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import smbus
import time

pb = (0x80,0x80,0xFC,0xFC,
      0xFC,0x80,0x80,0x80,
      0xFF,0xFF,0xFC,0xF8,
      0xF9,0xF9,0xF8,0xFC,
      0xFF,0x80,0x80,0xFF,
      0xFF,0x80,0x80,0xFF,
      0xFF,0xFC,0xF8,0xF8,
      0xF1,0xF8,0xF8,0xFC,
      0xFF,0xFF,0xFF,0x80,
      0x80,0x9E,0x9E,0x8E,
      0x80,0xC0,0xF1,0xFF,
      0x88,0x88,0xFF,0xFF)
pa = (0x01,0x01,0xFF,0xFF,
      0xFF,0x03,0x01,0x01,
      0xFF,0x0F,0x03,0x03,
      0x31,0x21,0x23,0x27,
      0xFF,0x01,0x01,0xFF,
      0xFF,0x01,0x01,0xFF,
      0xFF,0x07,0x03,0xE1,
      0xF1,0xE1,0x03,0x07,
      0xFF,0xFF,0xFF,0x01,
      0x01,0x7F,0x7F,0x7F,
      0x7F,0xFF,0xFF,0xFF,
      0x01,0x01,0xFF,0xFF)

class i2c:
    def __init__(self, bus, addr):
        self.b = smbus.SMBus(bus)
        self.addr = addr
    def put(self, cmd, data):
        self.b.write_byte_data(self.addr, cmd, data)
    def get(self, cmd):
        return self.b.read_byte_data(self.addr, cmd)

class MCP23017(i2c):
    def __init__(self, porta, portb):
        i2c.__init__(self, 1, 0x27)
        i2c.put(self, 0x05, 0x00)   # Config
        i2c.put(self, 0x00, porta)  # PortA I/O Set
        i2c.put(self, 0x01, portb)  # PortB I/O Set
    def invert(self, port, data):
        i2c.put(self, 0x02 + port, data)    # Invert Input Bit
    def pullup(self, port, data):
        i2c.put(self, 0x0C + port, data)    # Pull-Up Input
    def write(self, port, data):
        i2c.put(self, 0x12 + port, data)
    def read(self, port):
        return i2c.get(self, 0x12 + port)

if __name__ == '__main__':
    try:
        expand = MCP23017(0x00, 0x00)
        while True:
            for i in range(48):
                expand.write(1, pb[i])
                expand.write(0, pa[i])
                time.sleep(0.001)
    except KeyboardInterrupt:
        print '\nbreak'

ソースを見ても判るように、このコードではLEDの振られている方向を関知しないので、逆方向に振ると裏返しの図形が表示されてしまう。これを防ぐためには何らかのセンサーが必要で、水銀スイッチや加速度センサを使って順方向の時だけ表示するようにすれば良い。

そのあたりについてはまた後日。

広告

I/O Expander

Raspberry PiのGPIOには17ビット分のI/Oピンが存在しているが、他の機能と兼用になっているピンも多いため、17ビット全てを使用する事ができない場合もある。

I/Oが不足した時にそれを拡張する方法は何種類かあって、I/O拡張チップを使うのがそのうちの一つ。Microchip社のMCP23017はI2C接続で16bitのI/Oポート拡張ができる。

比較的入手が容易で、このチップを搭載した基板も出ているので試しやすい。一個では16bitまでだが、I2Cバスに8個まで同時に接続できるので、最大で128bitの拡張が可能だ。出力ポートのラッチや、入力ポートのプルアップ、割り込み機能もある。

今回はこのキットを使用したが、表面実装ICのハンダ付けが必要なので慣れていないと組み立ては難しいかも知れない。実装サービスを利用するか、DIPタイプを入手してブレッドボードで試すという手もある。DIPタイプはマルツや共立エレショップで入手可能。

Raspberry PiのI2Cポート(SCL/SDA)と基板のSCL/SDAピンを接続し、MCP23017のポートAのA0とA1にLED、ポートBのB0にタクトスイッチを接続した。スイッチは押すとGNDに接続されるようにしてある。
2013-05-23 11.23.46

このままだとスイッチが押されていない時は入力が開放状態となり、値が不安定になるので初期設定で内蔵プルアップが有効になるようにしてある。プログラムは以下の通り。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import smbus
import time

class i2c:
    def __init__(self, bus, addr):
        self.b = smbus.SMBus(bus)
        self.addr = addr
    def put(self, cmd, data):
        self.b.write_byte_data(self.addr, cmd, data)
    def get(self, cmd):
        return self.b.read_byte_data(self.addr, cmd)

class MCP23017(i2c):
    def __init__(self, porta, portb):
        i2c.__init__(self, 1, 0x27)
        i2c.put(self, 0x05, 0x00)   # Config
        i2c.put(self, 0x00, porta)  # PortA I/O Set
        i2c.put(self, 0x01, portb)  # PortB I/O Set
    def invert(self, port, data):
        i2c.put(self, 0x02 + port, data)    # Invert Input Bit
    def pullup(self, port, data):
        i2c.put(self, 0x0C + port, data)    # Pull-Up Input
    def write(self, port, data):
        i2c.put(self, 0x12 + port, data)
    def read(self, port):
        return i2c.get(self, 0x12 + port)

if __name__ == '__main__':
    try:
        expand = MCP23017(0xFC, 0xFF)
        expand.pullup(1, 0x01)
        expand.invert(1, 0x01)
        blink = False
        ss = expand.read(1) & 0x01
        while True:
            sw = expand.read(1) & 0x01
            if ss != sw:
                blink = not blink if sw else blink
                if sw:
                    print 'Blink Start' if blink else 'Blink Stop'
                ss = sw
            if blink:
                expand.write(0, 0x01)
                time.sleep(0.5)
                expand.write(0, 0x02)
                time.sleep(0.5)
    except KeyboardInterrupt:
        print '\nbreak'

プログラムを実行するとスイッチが押されるまで待機して、押されるとLEDを交互に点滅、再びスイッチを押すと点滅が停止する。

線2本(VCCとGNDで4本)だけで16bitのI/O拡張が可能で、必要なら更に増やすこともできる。値段も200円以下と安価なので、GPIOのI/Oピンだけでは不足するような場合に役立つだろう。

遠隔メール

IPアドレスの通知でRaspberry PiからGMailへメールを送信するのは既に試したが、受信はまだだったのでPythonで簡単なスクリプトを組んで試してみた。
送信は非常に単純に行えたのだが、受信はメールの構造やエンコードの仕組みについて知らないと正しく処理できないので、ちょっとばかり面倒。エンコードをとりあえず考えなければ、どうにか十数行でGMailから受信テキストを取得する所まではできた。

せっかくなのでそれを使って、メールでRaspberry Piに接続されたI2Cデバイスに対し、汎用的に出力を行えるような仕組みも考えてみた。やっている事は単純で、メールの本文に記載されたコマンドに従い、I2Cデバイスに対して順次出力を行うだけ。

I2Cデバイスのアドレスやコマンド、コマンド送信間のディレイもメールで指定できるので、どんなデバイスが接続されていても汎用的に使えるはずだ。プログラムは以下の通り。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import imaplib
import email
import smbus
import time

login_user = 'xxxxxxxx@gmail.com'
login_pass = 'xxxxxxxx'
remote_subject = '[RaspRemote]'
i2c = smbus.SMBus(1)
interval = 5

def cmd_i2c(param):
    try:
        addr = int(param[1].strip(), 0)
        rw = param[2].strip().upper()
        cmd  = int(param[3].strip(), 0)
        if rw[0] == 'W':
            val = int(param[4].strip(), 0)
            i2c.write_byte_data(addr, cmd, val)
        elif rw[0] == 'P':
            put = param[4].strip()
            [i2c.write_byte_data(addr, cmd, ord(c)) for c in put]
        elif rw[0] == 'R':
            pass
        elif rw[0] == 'G':
            pass
    except ValueError:
        pass

def cmd_wait(param):
    try:
        val = float(param[1].strip())
        time.sleep(val)
    except ValueError:
        pass

def cmd_gpio(param):
    pass

def execute(lines):
    for line in lines:
        param = line.split(',')
        print param
        cmd = param[0].strip()
        if cmd[0] != '$':
            continue
        if cmd == '$I2C':
            cmd_i2c(param)
        elif cmd == '$WAIT':
            cmd_wait(param)
        elif cmd == '$GPIO':
            cmd_gpio(param)

def check():
    mail = imaplib.IMAP4_SSL('imap.gmail.com', 993)
    mail.login(login_user, login_pass)
    mail.list()
    mail.select('inbox')
    # New Mail?
    stat, mlist = mail.search(None, '(UNSEEN)')
    if mlist[0] is not '':
        for num in mlist[0].split():
            res, data = mail.fetch(num, '(RFC822)')
            msg = email.message_from_string(data[0][1])
            # Get Subject
            subject = email.Header.decode_header(msg.get('Subject'))
            text, charset = subject[0]
            if text[0:len(remote_subject)] == remote_subject:
                # Get Body
                for part in msg.walk():
                    if part.get_content_type() == 'text/plain':
                        lines = part.get_payload().splitlines()
                        execute(lines)
                        break
    mail.logout()

if __name__ == '__main__':
    try:
        while True:
            check()
            time.sleep(interval)
    except KeyboardInterrupt:
        print '\nbreak'

使用できるコマンドは今のところI2Cデバイスに対する1バイトの出力と複数バイトの連続出力、それとディレイだけだが、GPIOへの出力やデバイスから取得したデータをメールで送り返すような拡張も容易だろう。

このプログラムをRaspberry Piのターミナルから起動すると、5秒おきにメールサーバへ未読チェックを行い、特定のサブジェクト([RaspRemote])が付いたメールが届いていたらそれを受信して本文を解釈実行する。テストデバイスとして今回もストロベリーリナックスI2C液晶を使用した。

送信するメールの内容は、以下のような感じになる

// I2C液晶初期化
$I2C,0x3e,W,0,0×38
$I2C,0x3e,W,0,0×39
$I2C,0x3e,W,0,0×14
$I2C,0x3e,W,0,0x7A
$I2C,0x3e,W,0,0x7F
$I2C,0x3e,W,0,0×56
$I2C,0x3e,W,0,0x5E
$I2C,0x3e,W,0,0x6C
$WAIT,0.2
// クリア
$I2C,0x3e,W,0,0×38
$I2C,0x3e,W,0,0x0C
$I2C,0x3e,W,0,0×01
$I2C,0x3e,W,0,0×06
$WAIT,0.2
// 出力
$I2C,0x3e,W,0,0×38
$I2C,0x3e,W,0,0×80
$I2C,0x3e,W,0,0×38
$I2C,0x3e,P,0x40,MailRemote Ready

先頭が’$’以外の行は読み飛ばすので、何を書いてもかまわない。送信するときは表題の先頭に’[RaspRemote]’と書いておく。

remotemail01

プログラムが正しく動いていれば、I2C液晶にメールで指定した文字列が表示されるはず。

もちろん接続されるデバイスが決まっているなら、わざわざこんな面倒なことをしなくても専用のプログラムを書けば済む話なので、実用的にはあまり意味のないプログラムかも知れないが、メールを受信したときにRaspberry Piに何かさせたいという時には応用できるだろう。

Virtual Network Computing

Raspberry PiにRaspbianのSDメモリを装着して、HDMIモニタを接続して起動すれば何の問題もなくデスクトップ画面が表示されるが、見た目はWindowsと同じようでも中身はだいぶ違う。
Linuxのデスクトップ(X-Window)は内部的にクライアントサーバ構成になっていて、クライアントとサーバ間はXプロトコルによって通信が行われるので、ネットワークにXプロトコルを通せばリモートデスクトップとして使用できる。

Raspberry Piのリモートデスクトップでよく使われているVNC(Virtual Network Computing)はそれとは異なる仕組みで、独自のVNCプロトコルによって通信が行われている。これは画面イメージを細分化されたピクセルデータとして送信する仕組みで、負荷が大きくなってしまうがWindowsからでも利用可能となるため、Raspberry Piではこちらの使用例が多い。

よく使用されているTightVNCは、コマンドラインで以下のようにインストールする。

$ sudo apt-get install tightvncserver

画面解像度等を変更したい場合は、起動時のオプションで指定する。初回起動時のみ接続パスワードの設定が要求される。

$ vncserver
You will require a password to access your desktops.
Password:
Verify:
Would you like to enter a view-only password (y/n)? n

New ‘X’ desktop is raspberrypi:1

Creating default startup script /home/pi/.vnc/xstartup
Starting applications specified in /home/pi/.vnc/xstartup
Log file is /home/pi/.vnc/raspberrypi:1.log

VNCで接続するためにはVNCクライアントソフトを用意する必要があるが、WebブラウザにChromeを使用しているのであれば、ChromeにVNC Viewerアプリを追加して使う事もできる。vnc1Raspberry PiのIPアドレスを入力して[Connect]をクリックすると、 暗号化されていない接続という警告が出る。
vnc2[Connect]をクリックして次へ進み、vncserver起動時に設定した接続パスワードを入力する。 vnc3正常に接続されればブラウザにRaspberry Piのデスクトップが表示されるはずだ。
vnc4

注意点がひとつ。
VNCでリモート接続している状態で、ウィンドウを表示するアプリをsudoを付けて実行すると、以下のようなエラーが出る場合がある。

Invalid MIT-MAGIC-COOKIE-1 key

Raspberry PiのGPIOをRPi.GPIOで操作する場合、RPi.GPIOは特権レベルを必要とするのでこれでは困る。Xの認証がらみでこのへんは色々とややこしく、色々と試してみた結果xhostコマンドで以下のように権限を与えてしまうのが最も簡単なようだ。

$ xhost +local:user

VNCでRaspberry Piのデスクトップに接続した時、VNCクライアントを終了させてもVNCサーバ側は終了せずに動き続けているので、再び接続すると終了時の画面が表示される。VNCサーバ側を終了する場合は、以下のように指定する。

$ vncserver -kill :1

最後の1はディスプレイ番号で、vncserverで複数のディスプレイを作成した場合は、終了させたいディスプレイ番号を入力する必要がある。

表示速度はかなり遅いが、HDMIモニタを接続しなくてもデスクトップが使用できるので、ゲームは無理でもGUIを持つアプリのデバッグ等では役に立つと思う。