« 光当たる人工光合成 | トップページ | BeagleBone Blackを試す (4) »

2014年1月10日 (金)

BeagleBone Blackを試す (3)

前回に引き続きBBB (BeagleBone Black、以下同様) 上のDebian weezyでADC (Analog-to-Digital Converter、以下同様) を使ってみる。今回はADCから値を読み取るプログラムを作ってみた。

Pythonを使ってデジタルオシロみたいなものが出来た。

/*-----------  -----------*/

IO Pythonについて

BBBのI/Oを使うにはAdafruitのIO Pythonを利用する選択肢がある。これは個々のI/Oポートを名前"AIN0" や、BBB上のヘッダーピン番号 "P9_39" で参照できるのが便利である。しかし以下のような難点もあるので今回は利用しなかった。

  • 先ずセットアップ関数 [例: ADC.setup() ] を実行する必要があるが、これはI/O出力を行うため、本体の処理がI/O入力のみでもPythonをrootユーザーで実行する必要がある
  • ヘッダーにつながっているAIN0~6は利用可能だが、AIN7が利用できない
  • ADC.read_raw関数を使ってもmV単位の値しか利用できず、12ビット分解能が得られない

前回紹介した /etc/rc.local でADC有効化を行う設定をしておけば、IO Pythonを使わなくても所定のパスを open~read~close することでADCの値を読み込めるので不自由は無い。また12ビット分解能を得ようとするとこの方法しかない。

 

sysfs 上のADCパスを探すモジュール

前回findコマンドでADCのパスを環境変数に設定する処理を /etc/profile に追加する方法を紹介した。それと同じ事をPythonでできるようにしてみる。

#!/usr/bin/python

from glob import glob
from os.path import split as path_split
from warnings import warn

def __get_adc_dir__(pattern):
    r = glob(pattern)
    if r == []:
        raise IOError(-2, "Found no files for %s" % pattern)
    elif len(r) != 8:
        warn("File count for %s != 8" % pattern)
    u = set([path_split(i)[0] for i in r] )
    if len(u) != 1:
        raise IOError(-1, "Found %s in multiple dirs" % pattern)
    return list(u)[0]

AIN_DIR = __get_adc_dir__(r"/sys/devices/ocp.*/helper.*/AIN*")
RAW_DIR = __get_adc_dir__(r"/sys/bus/iio/devices"
                          r"/iio:device*/in_voltage*_raw")

def get_ain_path(c):
    return AIN_DIR + "/AIN%d" % c

def get_raw_path(c):
    return RAW_DIR + "/in_voltage%d_raw" % c

if __name__ == "__main__":
    print "AIN_DIR:\t%s" % AIN_DIR
    print "AIN3:\t\t%s" % get_ain_path(3)
    print "RAW_DIR:\t%s" % RAW_DIR
    print "raw4:\t\t%s" % get_raw_path(4) 

基本的にglobモジュールの機能そのものにエラーチェックを追加しただけだ。最後の5行は機能確認テスト用である。

このコードを適当な名前 (今回はadc_path.pyにした) のファイルに保存し、そのディレクトリーに移動して

debian@arm:~$ cd python/
debian@arm:~/python$ python
Python 2.7.3 (default, Jan  4 2013, 13:44:41)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import adc_path
>>> adc_path.RAW_DIR
'/sys/bus/iio/devices/iio:device0'
>>> adc_path.get_raw_path(4)
'/sys/bus/iio/devices/iio:device0/in_voltage4_raw'
>>> f = open(adc_path.get_raw_path(7) )
>>> f.readline()
'3858\n'
>>> f.close()
>>> ← [Ctrl+D] を押す
debian@arm:~/python$

ADCの有効化を忘れたときなどはimportのタイミングで例外を投げるようにしている。なおPythonを終わらせるときに押すCtrl+DはEOFのショートカットで、WindowsならCtrl+Z [Enter] である。

 

ADCの値の時間変化をグラフにする

前回同様P9_39 (AIN0) とP9_39 (GNDA_ADC)の間に100kΩの抵抗を接続する。

Cimg0495

先ずはメインモジュール

#!/usr/bin/python

from numpy import zeros
from adc_path import get_raw_path
from adc_plot import *
import timeit, os

N = 1000
ain = get_raw_path(0)
adc = os.path.split(ain)[-1]
y = zeros(N)
x = zeros(N)

print "Start reading %d samples from %s ..." % (N, adc),

f = open(ain, "rb", 0)
t = timeit.default_timer()
for i in range(N):
    y[i] = int(f.readline() )
    x[i] = timeit.default_timer() - t
    f.seek(0, 0)
te = timeit.default_timer() - t
f.close()

print "done"
print "Time elapsed:       %.3f [sec]" % te
print "Unit sampling time: %.3f [ms/sample]" % (1e3 * te / N)

plot_value(x, y, adc)
plot_hist_y(y)

この部分はADCから値を読み取り、その値と読み取った瞬間の時刻を配列に格納し、プロットする処理を呼び出している。ADCからの読み取りにどのくらいの時間がかかるかも見たかったので、待ち時間の挿入などタイミング調整は全く行っていない。グラフの作成にはmatplotlibを使う。Debian系ならsudo apt-get install python-matplotlibでインストールできる。また配列など数値計算処理系のモジュールnumpyは依存パッケージとして一緒にインストールされる。

debian@arm:~/python$ apt-cache depends python-matplotlib | grep numpy
  Depends: python-numpy
  Depends: <python-numpy-abi9>
    python-numpy

ÅngströmではLinuxパッケージには準備されていないようなので、pipコマンドを使ってインストールする必要がある。

プロットする処理はadc_plot.pyと言う名前の別モジュールにした。ADCの読み取り値をヒストグラムにする処理も加えた。

#!/usr/bin/python

import numpy as np
import pylab as pl

def plot_value(x, y, label="ADC value", y_min_max=None):
    l = u"%s\nμ=%.3f  σ=%.3f" % (label, np.average(y), np.std(y) )
    pl.plot(x, y, ".-", label=l)
    pl.xlabel("sec")
    pl.ylabel("Raw reading")
    pl.legend(loc="upper center").draggable(state=True)
##    pl.xlim(0.2, 0.25)    # uncomment to zoom the time axis
    i = 0
    while True:
        if ((10 ** (i / 3) ) * [1, 2, 5][i % 3]  ) > y.max():
            break
        i += 1
    pl.ylim(0, (10 ** (i / 3) ) * [1, 2, 5][i % 3] )
    pl.grid(True)
    pl.show()

def plot_hist_y(y):
    ay, ax, p = pl.hist(y, bins=int(y.max()-y.min()+1),
                        range=(y.min()-0.5, y.max()+0.5) )
    pl.xlim(y.min()-0.5, y.max()+0.5)
    pl.xlabel("Raw reading")
    pl.ylabel("Number of samples")
    s = u"μ=%.3f  σ=%.3f" % (np.average(y), np.std(y) )
    ax2 = (ax[:-1] + ax[1:]) / 2.0
    for xx, yy in zip(ax2, ay):
        if yy:
            s += "\nx=%d, y=%d" % (round(xx), yy)
    pl.text((ax.max() + 3 * ax.min() ) / 4.0,
            0.05 * ay.max(), s, color="magenta")
    pl.show()

このプログラムは自動的にグラフを表示するように作っているのでstartxした環境で実行する。

メインモジュールがコンソールに出力する読み取り時間は、実行の都度結構変動するが

Start reading 1000 samples from in_voltage0_raw ... done
Time elapsed:       1.243 [sec]
Unit sampling time: 1.243 [ms/sample]

概ね1.2ms/sampleである。時系列のプロットは

Adc_quiet_100k

こんな具合である。µは平均値、σは標準偏差だが、この場合平均値は直流分の電圧、標準偏差は交流分のRMS値に相当する。これはヒストグラムにしても面白くないのでノイズのある場合を見てみよう。

ノイズの多いACアダプターに取り替え、前回のようにツイストしていないリード線で100kΩの抵抗を短絡する。

Cimg0493

ACアダプター以外のノイズにも影響されるから実行の都度結果は変わるが、代表的な結果はこんな具合である。

Adc_noisy_0k

マイナス側の値が反映されていないので平均値・標準偏差とも正しい値ではない。平均値は本来ゼロになるはずだし、本来の標準偏差は表示された値掛ける2の平方根、つまり約2.5になるはずである。しかし1,000サンプルで観測されたピーク値が5σを超えるのはやや不自然である。

単純なガウス分布では無さそうだ。ヒストグラムを見ても

Adc_noisy_0k_histo

1%を超える数のサンプルが3σを超える範囲に入っている。

しかしその一方で70%以上のサンプルが0と1のビンに収まっている。周波数カウンターやDVMなどで右端の数値がチラチラ変わるのに慣れているからサンプル数が少ないと「まぁこんなものかな」と思うかもしれない。しかし100回に1回くらい7を超える値が出ているのに気付けば不審に思うだろう。

12ビット分解能でフルスケール1.8Vの最小目盛りは約440µVだから、最大値13は約5.7mVである。前回の記事に掲載したオシロの波形のピーク値は40mV近くだったので、ADCのアナログ帯域幅が1~2MHz程度と考えるとつじつまが合いそうだ。

 

少し実用的な事をやってみる

平均830sps位でサンプリングできているので、電源周波数程度の波形であればオシロスコープのような使い方ができるかもしれない。100kΩの抵抗と並列にLEDをつないで、LED電球のフリッカを計ってみよう。この場合LEDは太陽電池モードの光センサーとして働く。

Cimg0507

当たり前だが、極性はアノードがプラスである。

Led_sensor

いくつか試した中で、クリアレンズの赤色LEDが最も感度が良かった。理屈では赤外LEDの感度が良さそうに思うのだが、レンズの材質が可視光を吸収するのか、たいしたことはなかった。なおLEDは受光面積が非常に小さいため今回の条件だと光量、正確には光子の数に比例した電流が100kΩの抵抗に流れると考えられる。つまり光量と電圧の直線性がかなり良いと期待できる。ただしADCから0.1µA程度の電流が流れ込んでいてその値が不安定なので、読み取り値には不安定なオフセットが含まれている。最大の不安定要因はSoCチップ温度の変動ではないかと思う。

先ずLED電球を点灯せずに、ただしコンセントは差し込んで測ってノイズの状況を確認してみる。

Adc_led_nosig

直流分はほとんど変わらないが、交流分が10倍以上増えている。これでは波形が分らないので、時間軸を拡大してみよう。

Adc_led_nosig_zoom1

周期0.02s = 20msなので電源周波数50Hzのノイズを拾っているようだ。LEDからの信号が充分大きければ無視できる程度の振幅である。毎回手動で時間軸を拡大するのは手間なので、adc_plot.pyのコメントアウトしていたpl.xlimの行を活かして自動的に時間軸が拡大されるようにした。またヒストグラムは要らないので最後の行 plot_hist_y(y) はコメントアウトしておく (出すとかなりタイヘンな事になる)。

準備は整ったので、まずは以前アナログな方法で凄いフリッカがあることが分っているLED電球を測ってみよう。

Adc_led_kfe

以前推定したとおり発光している時間の方が短い。先ほど確認した電源ノイズよりも充分に振幅が大きいが、それでも谷底の部分の高さが一つ置きに違っていることで50Hzのノイズの存在が分る。これは結構面白い。

気をよくして、少しフリッカがあると分っているLED電球を測ってみた。

Adc_led_sharp

前のLED電球ではµ:σがほぼ1:1だったがこれは3:1程度までフリッカが減り、波形も全波整流された正弦波のように見えてきている。目で見て体感できるフリッカの違いとあまり違和感の無い結果である。

さて照明用光源として実績の永い白熱電球はどんな具合だろう。

Adc_led_incandescent

µ:σは約15:1だ。しかしプロットを見ると「こんなに変動があるの?」と言いたくなる印象を受ける。フィラメントの熱容量に比べて輻射による熱抵抗が比較的低く、熱的時定数が意外と小さいのだろう。

少し視点を変えてみよう。まったく明るさの変動が無いのが良い照明なのだろうか。もしかすると炎のように少し揺らぎがあるほうが、心が安らぐのかもしれない。

 

気になっていたことは現実だった

今回のプログラムでADCの値を読んでいる間はCPUの使用率が100%に張り付く。作ったプログラムは全く休まないでsysfs上のファイルを繰り返し読む処理を行っているから、これは当然である。仮に読むのがディスク上の普通のファイルでもこんな風に同じファイルを繰り返し読むと、ファイルがよほど大きくない限りCPUの使用率が100%に張り付く。これはデータが全てキャッシュ上にあるのでディスクの回転を待つ時間が無いからである。しかしLinuxはマルチタスクOSなので、今回作ったプログラムが全てのCPU時間を1秒以上も使い続けられる訳が無いのだ。Linuxは時々別のタスクを動かす間、今回のプログラムを止めているはずである。

今までもその兆候はあったのだが、今回ついに動かぬ証拠をつかんだ。LEDをセンサーにしたときのノイズを調べた際、0.2秒の少し前に大きな波形の乱れがあるのがそれだ。その部分を拡大してみよう。

Adc_led_nosig_zoom2

0.15秒を中心に10msくらいサンプルが無い期間がある。1サンプル読み込むのにかかる時間の平均値約1.2msの数回分サンプルが読めていない。

リアルタイムOSではないLinuxのユーザーモードでどの位リアルタイム的な処理が出来るのか、これはそもそも無謀なのかもしれないが、試してみる価値はある。これは次回の記事のネタにとっておくことにしよう。

 

今回のまとめ

BBB上のDebian + Pythonでデジタルオシロのようなものが出来た。結構楽しめる。これは汎用OSが動いているからこんなに手軽に出来ることなのだが、実はOSが邪魔になって時々つまずく。多分100spsとか10sps位ならつまずかずに出来るのだろうけど、これはそもそも使い方をまちがえているような気がする。

正解は採取したデータをI/Oから塊で受け取るようにすることだろう。ディスクI/Oなら平均毎秒400kBでCPU使用率 100%になるとは考えにくい。同じようなデータの取り扱いをすれば、1サンプル2バイト換算で200kspsは楽勝でOKのはずだ。

実はBBBが使っているSoC Ti AM3359には、ADCのContinuous modeと呼ばれる動作モードやPRU (Programmable Real-Time Unit: 200MIPSのリアルタイム処理用マイクロコントローラー) と呼ばれるI/Oサブシステムがあるが、現在のカーネル3.8では利用できない。利用するには自力でドライバーかカーネルのパッチを書く必要があるはずだ。これをやるのはタイヘンなので、将来の課題にしておく。

|

« 光当たる人工光合成 | トップページ | BeagleBone Blackを試す (4) »

趣味」カテゴリの記事

IT」カテゴリの記事

BeagleBone Black」カテゴリの記事

コメント

この記事へのコメントは終了しました。

トラックバック


この記事へのトラックバック一覧です: BeagleBone Blackを試す (3):

« 光当たる人工光合成 | トップページ | BeagleBone Blackを試す (4) »