やっさんの雑記

プログラミングでやってみたこととか

BeagleBone Blackで7セグLEDをダイナミック点灯する方法

みなさんあけましておめでとうございます。今年もよろしくお願いします。

さて今回のネタは何かというと、プログラミングっぽさが少し減りますが、BeagleBone Blackで7セグLEDをダイナミック点灯する方法を扱いたいと思います。

BeagleBone Blackとは?

簡単にいうと手のひらサイズのLinuxボードです。最近だとRaspberry Piが人気(かつ有名)だと思いますが、Raspberry PiよりもIOが充実していてしかも高性能です。ただし、日本語の資料がRaspberry Piよりも圧倒的に少ないのが難点なので、入門には少しハードルが高いと思います。ただし、最近は関連書籍も少しずつ出版されてきていたりして、日本語資料は今後どんどん増えていくと思います。

最新のリビジョンは2013/01/26現在、Rev. A6Aです。リビジョンによって中身にそこまで差があるわけではないですが、リビジョンが上がるにつれてバグフィックスがされているようなので、できるだけ最新のリビジョンのものを購入するようにしましょう。

また、名前の似た製品としてBeagleBoneがあります(Blackがない)が、これはBeagleBone Blackの前の製品で、BeagleBone Blackよりもスペックが低い上に高いので、買わないように注意しましょう。

最近では秋月でも取り扱いをしているようですが、すぐ売り切れてしまっているようです。

回路設計

BeagleBone Blackは各IOピンの入出力電流の制限が結構シビアです。共通のVDD_5V端子については1000mA、VDD_3V3とSYS_5V端子については250mA流せます*1が、その他GPIOについては3mAしか流せません。

また、今回の記事を書くネタになったものは7セグLED以外にもたくさん入出力部があってそのままではBeagleBone BlackのGPIO端子だけでは足りなくなってしまうので、TC4511BP*2という7セグLEDのドライバICを使って信号線の本数を減らすことにします。

このドライバICは基本的には4本の入力をとって、それを4桁の2進数と解釈してその数に対応する7セグの発光パターンを出力するというものです*3。(アノードコモンではなく)カソードコモンのLEDを直接駆動できるので便利です。

ということで回路図はこんな感じになります。
f:id:yasuand:20140126074830p:plain

本当はこの回路図はあまりよくない(1つの抵抗に2つ以上のLEDがつながっている)のですが、今回はダイナミック点灯ということで2つ以上7セグが同時に点灯するのはありえない(その辺の制御はソフト側で行う)ので、問題ありません。

点灯試験や全消灯に使う端子はVCCに釣り上げておいて、また今回は.の表示はしないのでそこはGNDに落としておきます。.の表示をしたい場合はGNDに落とさずにトランジスタを介して制御しましょう。トランジスタを何個もブレッドボード(もしくは基板)上に並べるのはコンパクトでないので、TD62003APGのようなトランジスタアレイを使えばいいと思います。

また、4511のLE端子が今までの説明にないのにもかかわらずBeagleBone Blackから入力を取るように書かれていますが、これの役割は後述します。プログラムを書くときはとりあえずそこには0を出力しておきましょう。

とりあえず1桁だけ点灯させてみる

回路が組み終わったら点灯試験です。

4桁をいきなりダイナミック点灯させるのは難しいので、1桁だけ点灯させてみたいと思います。

GPIOに出力するには、まず"/sys/class/gpio/export"という特殊ファイルに出力に使いたいGPIOの番号を書き込み、次に"/sys/class/gpio/gpio(GPIO番号)/direction"に"out"を書き込むとそのGPIO端子が出力として使えるようになって、0を出力するには"/sys/class/gpio/gpio(GPIO番号)/value"に0を、1を出力するには1を書き込み、値の書き込みが終わってもうそのGPIOを使わないとなったら"/sys/class/gpio/unexport"にGPIO番号を書き込めばいいのでした。

例えばP9ヘッダ(Ethernetアダプタが上にくるように見たときに左側にくる方のヘッダ)の11番ピンを使う場合は、そのGPIO番号は30なので、

#!/bin/sh

echo 30 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio30/direction
echo 0 > /sys/class/gpio/gpio30/value
sleep 1
echo 1 > /sys/class/gpio/gpio30/value
sleep 1
echo 0 > /sys/class/gpio/gpio30/value
echo 30 > /sys/class/gpio/unexport

などとシェルスクリプトを書いてGPIO端子にLEDをつなげば、1秒後にLEDが光ってさらにその1秒後にLEDが消灯するようになるわけですね。

UbuntuのBeagleBone Black用イメージの場合、gccが最初からは入っていない一方、Pythonは最初から入っているのでPythonでプログラムを書いてみます*4。ピン番号は適当に変更してください。このプログラムのままピン番号を変更しない場合、4511の1, 2, 5, 6, 7番ピンがそれぞれBeagleBone BlackのP9ヘッダの11, 13, 15, 21, 23番ピンに、4桁の7セグのうち1番上の桁に相当するトランジスタアレイのピンをBeagleBone BlackのP9ヘッダの12番ピンにつなげてください。他の桁については接続せずに放置しておきましょう。

# -*- coding: utf-8 -*-

import os
import time

class GpioWriter:
    def __init__(self, pinNumber):
        self.pinNumber = pinNumber
        os.system("echo " + str(pinNumber) + " > /sys/class/gpio/export")
        os.system("echo out > /sys/class/gpio/gpio" + str(pinNumber) + "/direction")
    
    def release(self):
        os.system("echo " + str(pinNumber) + " > /sys/class/gpio/unexport")
    
    def setBit(self, bit):
        os.system("echo " + str(bit) + " > /sys/class/gpio/gpio" + str(self.pinNumber) + "/value");

if __name__ == "__main__":
    digits = map(lambda x: GpioWriter(x), [ 49, 30, 31, 3 ]) # 左から順にP9ヘッダの23, 11, 13, 21番ピン
    le     = GpioWriter(48)                                  # P9ヘッダの15番ピン
    enable = GpioWriter(60)                                  # P9ヘッダの12番ピン
    le.setBit(0)
    enable.setBit(1)
    for i in xrange(10):
        for j in xrange(10):
            for k in xrange(4):
                digits[k].setBit((j >> k) & 1)
            time.sleep(0.5)
    enable.release()
    le.release()
    for d in digits:
        d.release()

これは0.5秒毎に7セグに表示される数字がインクリメントされるのが10周期続いて終わるというプログラムですが、これを実行してみるとちょっと表示が"変"になるはずです。

具体的にどこが変かというと、3->4とか7->8とかに遷移するときにわかりやすいと思いますが、表示したくないセグメントまで一瞬表示されるのが目で確認できるレベルで起こってしまうということです(実際に組んでみてやってみるのが一番わかりやすいと思います)。

これの原因は何かというと、GPIOへの書き込み速度です。0~9の数字を2進数で表したときの各桁の更新は同時にGPIOに反映されるわけではなく、しかもそのタイムラグは無視できるものではないので、数字を更新しているときに変な表示になってしまうわけですね。

これを解決するために出てくるのが4511のLE端子です。

4511はLE端子に0が入力されている間は入力されている4桁の2進数に応じた7セグの発光パターンを出力しますが、1が入力されていると、前に出力していた発光パターンを維持します。つまり、A~Dの入力に何が来ようが出力はそのままということです。

ということでプログラムを書き直します。変更箇所はかなり少ないです。

# -*- coding: utf-8 -*-

import os
import time

class GpioWriter:
    def __init__(self, pinNumber):
        self.pinNumber = pinNumber
        os.system("echo " + str(pinNumber) + " > /sys/class/gpio/export")
        os.system("echo out > /sys/class/gpio/gpio" + str(pinNumber) + "/direction")
    
    def release(self):
        os.system("echo " + str(pinNumber) + " > /sys/class/gpio/unexport")
    
    def setBit(self, bit):
        os.system("echo " + str(bit) + " > /sys/class/gpio/gpio" + str(self.pinNumber) + "/value");

if __name__ == "__main__":
    digits = map(lambda x: GpioWriter(x), [ 49, 30, 31, 3 ]) # 左から順にP9ヘッダの23, 11, 13, 21番ピン
    le     = GpioWriter(48)                                  # P9ヘッダの15番ピン
    enable = GpioWriter(60)                                  # P9ヘッダの12番ピン
    enable.setBit(1)
    for i in xrange(10):
        for j in xrange(10):
            le.setBit(1)          # 発光パターンの変更をロック
            for k in xrange(4):
                digits[k].setBit((j >> k) & 1)
            le.setBit(0)          # 発光パターンの変更を反映
            time.sleep(0.5)
    enable.release()
    le.release()
    for d in digits:
        d.release()

これでスムーズに0~9の数字が移り変わるようになったはずです(パチパチ

いよいよダイナミック点灯

数字を1桁表示することができたので、いよいよダイナミック点灯させてみます。

ためしに"1234"という数字のパターンを出力してみましょう。先ほど放置していたピンは、上位から順にP9ヘッダの16, 22, 24番ピンにつなげてください。

# -*- coding: utf-8 -*-

import os
import time

class GpioWriter:
    def __init__(self, pinNumber):
        self.pinNumber = pinNumber
        os.system("echo " + str(pinNumber) + " > /sys/class/gpio/export")
        os.system("echo out > /sys/class/gpio/gpio" + str(pinNumber) + "/direction")
    
    def release(self):
        os.system("echo " + str(pinNumber) + " > /sys/class/gpio/unexport")
    
    def setBit(self, bit):
        os.system("echo " + str(bit) + " > /sys/class/gpio/gpio" + str(self.pinNumber) + "/value")

if __name__ == "__main__":
    digits  = map(lambda x: GpioWriter(x), [ 49, 30, 31,  3 ]) # 左から順にP9ヘッダの23, 11, 13, 21番ピン
    le      = GpioWriter(48)                                   # P9ヘッダの15番ピン
    enables = map(lambda x: GpioWriter(x), [ 60, 51,  2, 15 ]) # 左から順にP9ヘッダの12, 16, 22, 24番ピン
    number  = [ 1, 2, 3, 4 ]                                   # 表示する数字列
    count = 3
    for i in xrange(3000):
        for j in xrange(4):
            n = number[j]
            le.setBit(1)                       # 表示する数字の変更をロック
            for k in xrange(4):
                digits[k].setBit((n >> k) & 1)
            enables[count].setBit(0)           # 今点灯している桁を消灯する
            le.setBit(0)                       # 表示する数字の変更を反映する
            count = (count + 1) % 4            # 点灯する桁を移動
            enables[count].setBit(1)           # 次の桁を点灯する
            time.sleep(0.001)
    for e in enables:
        e.release()
    le.release()
    for d in digits:
        d.release()

さて、これを実行するとうまく点灯するでしょうか。

実際には確かに表示される桁が移り変わって見えますが、周期が遅すぎィ!という感じになると思います。

キレイにダイナミック点灯するために

なぜ周期が遅くなって、ダイナミック点灯がキレイにできないかというと、これもやっぱりGPIOの書き込み速度のせいです。

そういえば1桁だけをスタティック点灯させたときは、4511のLE入力を使って解決して、根本的な解決はしていないのでした。

BeagleBone Blackには24.576MHzで書き込みができる端子が(確か)2つ用意されていますが数が少なすぎで7セグのダイナミック点灯には厳しそうだし、そもそも"/sys/class/gpio/gpio30/value"を通じた書き込みが常識的に考えられる以上に遅すぎます*5

ということでこの解決方法はかなり難しそうなのですが、実際にはかなり簡単です。echoを使って書き込まずに、PythonのファイルIOを使って直接書き込んでしまえばいいのです*6

# -*- coding: utf-8 -*-

import os
import time

class GpioWriter:
    def __init__(self, pinNumber):
        self.pinNumber = pinNumber
        os.system("echo " + str(pinNumber) + " > /sys/class/gpio/export")
        os.system("echo out > /sys/class/gpio/gpio" + str(pinNumber) + "/direction")
        self.fd = os.open("/sys/class/gpio/gpio" + str(pinNumber) + "/value", os.O_WRONLY)
    
    def release(self):
        os.close(self.fd)
        os.system("echo " + str(self.pinNumber) + " > /sys/class/gpio/unexport")
    
    def setBit(self, bit):
        os.write(self.fd, str(bit))

if __name__ == "__main__":
    digits  = map(lambda x: GpioWriter(x), [ 49, 30, 31,  3 ]) # 左から順にP9ヘッダの23, 11, 13, 21番ピン
    le      = GpioWriter(48)                                   # P9ヘッダの15番ピン
    enables = map(lambda x: GpioWriter(x), [ 60, 51,  2, 15 ]) # 左から順にP9ヘッダの12, 16, 22, 24番ピン
    number  = [ 1, 2, 3, 4 ]                                   # 表示する数字列
    count = 3
    for i in xrange(3000):
        for j in xrange(4):
            n = number[j]
            le.setBit(1)                       # 表示する数字の変更をロック
            for k in xrange(4):
                digits[k].setBit((n >> k) & 1)
            enables[count].setBit(0)           # 今点灯している桁を消灯する
            le.setBit(0)                       # 表示する数字の変更を反映する
            count = (count + 1) % 4            # 点灯する桁を移動
            enables[count].setBit(1)           # 次の桁を点灯する
            time.sleep(0.001)
    for e in enables:
        e.release()
    le.release()
    for d in digits:
        d.release()

これでキレイにダイナミック点灯できましたね(パチパチパチパチ

おまけ

これで7セグのダイナミック点灯に耐えられるくらいの書き込み速度でGPIOに出力することができるようになったわけですが、実際に作るような回路はもっと複雑で、出力がもっとたくさんあったり、入力もあったりするはずです。

入出力をとるときは本当はこのようにメインプログラムの中にベタ書きするのではなくマルチスレッド化したりすると思いますが、このときGPIOへの入出力をする部分が1つでも

os.system("echo 1 > /sys/class/gpio/gpio30/value")

というように書いてしまっていると、この処理に時間がかかって結果的にときどきチラつくというような動作になってしまいます。

今回の記事では出力だけを扱って入力は扱いませんでしたが、入力についても

class GpioReader:
    def __init__(self, pinNumber):
        self.pinNumber = pinNumber
        os.system("echo " + str(pinNumber) + " > /sys/class/gpio/export")
        os.system("echo in > /sys/class/gpio/gpio" + str(pinNumber) + "/direction")
        self.fd = os.open("/sys/class/gpio/gpio" + str(pinNumber) + "/value", os.O_RDONLY)
    
    def release(self):
        os.close(self.fd)
        os.system("echo " + str(self.pinNumber) + " > /sys/class/gpio/unexport")
    
    def getBit(self):
        os.lseek(self.fd, 0, os.SEEK_SET)
        bytes = os.read(self.fd, 1)
        if bytes.isdigit():
            result = int(bytes)
            return result
        else:
            return None

などとして取得するようにすると、GPIOの性能を十分活かした入出力ができます*7

最後に

BeagleBone Blackは安くて高性能な素晴らしいLinuxボードですが、日本語資料が少ない上、過電流保護回路もない(実際僕は1個ふっ飛ばしました)ので、取り扱いには十分注意が必要です。しかし、正しく使えばとても良いものだと思うので、日本にもっともっと普及して日本語資料も充実してみなさんが素晴らしいBeagleBone Blackライフを贈れることを願っています!

*1:VDD_5V端子はDC電源からとってきたときに供給されて、SYS_5V端子はUSBからの給電のときに供給されるらしい…?

*2:これは東芝が出しているICですが、別に日立が出している74HC4511とかでも特に変わらない思います。いろいろ調べていて「具体的な品番を出してくれよ!!」と結構思ったので具体的な品番を出しました。とりあえず4511シリーズであるというのが重要です。

*3:個人的には6と9の字形が気に入らないのですがしょうがない

*4:python触ったのは実はこれが初めてなので、Pythonのプログラムとしては少し変になってるかもしれません…

*5:せいぜい10Hzくらいしか出てないのでは…?

*6:もちろんPythonに限らずC++とかJavaとかでも同じはずです

*7:このようにファイルディスクリプタ経由で値を読み込むと、実際には1byteも読み込めていないという瞬間が結構出てくるので、本当に値が読めているかどうかのチェックが必要です