日曜技術者のメモ

趣味でやった事のメモ書きです。

PYNQ JTAGでZynqとRISC-Vをまとめてデバッグ(失敗)

Xilinxのフォーラムを見てたらこんなのがあった

forums.xilinx.com

以前SCR1をFPGAで動かしたときにFPGARISC-Vで別々のJTAGケーブルが必要なのが面倒だと思った。これができれば1本のケーブルで全部できるやん!と思ってやってみた
結果、JTAGは繋がった様だがOpenOCDからRISC-Vに接続できなかった。

他にも調べるとZynq UltraScale とRocket Chipについての記事が見つかった
これもBSCANについて記載があるがnot workとも書いてる
RISC-V Hardware Design: Debug via BSCAN Chain - Edgeboard RISC-V Series - Pengcheng Xu's Place

実験環境

  • PYNQ-Z1
  • Vivado&Vitis IDE 2020.2
  • riscv-openocd (最新版をgit clone)
  • riscv-gnu-toolchain (最新版をgit clone)
  • SCR1

PL部

以下JTAG回りの実装で認識できそうなところまではできた
BSCANマスターが二つあるのは一つをDebug Hubに繋げないとILAが使えなくなった為
Debug HubのクロックはPLのクロックを接続している
f:id:ginnyu-tei:20210911103803p:plain:w400

PS部

JTAGに関しては何もしていない
PLへのクロック・リセット供給とSCR1コアをスタートする位

OpenOCD

cfgファイル

interface ftdi
transport select jtag
ftdi_vid_pid 0x0403 0x6010
ftdi_layout_init 0x0098 0x008b

ftdi_channel 0

adapter speed 1000
jtag newtap zynq_pl bs -irlen 6 -ircapture 0x1 -irmask 0x03 \
    -expected-id 0x23727093 \
    -expected-id 0x13722093 \
    -expected-id 0x03727093 \
    -expected-id 0x03736093

jtag newtap zynq cpu -irlen 4 -ircapture 0x1 -irmask 0xf -expected-id 0x4ba00477

jtag newtap riscv cpu -irlen 5

target create riscv.cpu riscv -endian little -chain-position riscv.cpu -coreid 0

実験結果

まずはそのままBitStreamを書き込みHardware Managerで見てみると
Hardware Targetが二つになっている
f:id:ginnyu-tei:20210911105819p:plain:w400
下のTargetに接続するとデバイスがないエラーになるがPSからClockを供給していないので想定通り。
次にPSを動作させRISC-Vがリセット解除されてから改めて接続。IDcodeやIR lengthがSCR1と同じなので認識されている様子
f:id:ginnyu-tei:20210911110737p:plain:w400
これでいけると思ったが、Hardware Managerを切断しOpenOCDを起動するとPLとPSは繋がるがRISC-Vが見つからずエラーになる

Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : clock speed 1000 kHz
Info : JTAG tap: zynq_pl.bs tap/device found: 0x23727093 (mfg: 0x049 (Xilinx), part: 0x3727, ver: 0x2)
Info : JTAG tap: zynq.cpu tap/device found: 0x4ba00477 (mfg: 0x23b (ARM Ltd), part: 0xba00, ver: 0x4)
Info : JTAG tap: riscv.cpu tap/device found: 0xffffffff (mfg: 0x7ff (<invalid>), part: 0xffff, ver: 0xf)
Error: riscv.cpu: IR capture error; saw 0x1f not 0x01
Warn : Bypassing JTAG setup events due to errors
Error: dtmcontrol is 0. Check JTAG connectivity/board power.
Warn : target riscv.cpu examination failed
Info : starting gdb server for riscv.cpu on 3333
Info : Listening on port 3333 for gdb connections

多分Hardware TargetのPS、PLがある方をopenしているからだと思うがOpenOCD上でHardware Targetの切り替え方法が分からずここで終了

M5Stack Core2の電源管理IC(AXP192)を調べてみた-1-

PM5Stack Core2をESP-IDFで開発したいがサンプルプロジェクトがないので自分で作ることにした。 まずは設定が必要そうな電源回りを調査した

参考資料

AXP192周辺接続図

下図が回路図から簡略化したAXP192の周辺接続図になる。

f:id:ginnyu-tei:20210305001926j:plain
M5Stack Core2のAXP192周辺図

AXP192の電源入力はUSBがACINで外部給電(M5.BUSやEXT.PORTAの5V)がVBUSに割り当てられている。
VBUSがループしていて回路図だけでは理解できなかったがM5 Core libraryを読んで動作が分かった

  • USB給電、バッテリー給電
    ACIN → IPS_OUTで電源供給。昇圧チップ(SY7088)で5Vに昇圧して外部へ出力
    この時にVBUS入力は使わない
  • 外部給電
    VBUS → IPS_OUTで電源供給。EXTENをOFFにしてSY7088を無効にする

※最新のM5Core2 libraryではM5.begin()で選択可。↓のmode

  void M5Core2::begin(bool LCDEnable, bool SDEnable, bool SerialEnable, bool I2CEnable, mbus_mode_t mode);

PlatformIOでM5Stack Core2のCore2_Factory_testをビルドしてみた

久しぶりに更新。
仕事と趣味が一緒だとブログに書く内容に困る・・・

M5Stack Core2を買ったのはいいがPlatformIOに登録されていなかったので自分で追加して出荷時のプログラム「Core2_Factory_test」をビルドしてみた
※この方法はPlatformIO公式でサポートされるまでの暫定的な対応ですべての動作を確認していませんので悪しからず

続きを読む

Keras+TensorflowでRapsberry Piライントレーサー作ってみた

某所のライントレース大会向けに機械学習を使ったライントレーサーを作ってみた。
そろそろいいかなと思うのでblogで公開してみる。
完成した動作は↓を参照。


(なお、この動画の後は自分の影を黒と認識して机から落ちました・・・)

何分大会がある週の週末に思いついて一日で実装したので適当な実装箇所が
数多くあるが時間がなかったのでご容赦頂きたい。

本体解説

外観
f:id:ginnyu-tei:20180412223813p:plain

2階建てで下のプレートはタミヤダブルギヤボックスとモーターコントローラ(DRV8830)
上はRaspberry Pi3とRaspberry Piカメラ+スマートフォン用クリップレンズ (広角)
広角のクリップレンズはラズパイカメラの画角が狭くて足元が映らなかったのでその対策。
電源はモバイルバッテリーを使っている
f:id:ginnyu-tei:20180412223859p:plain

なぜ機械学習で実装しようと思ったか?

もともと機械学習を使うつもりはまったくなかった
機械学習は学習データを大量に用意するのが面倒だと思っていたからだ
なのでOpenCVの画像処理を使って↓の様に輪郭抽出と矩形取得を使って大会に持っていく予定だった。
f:id:ginnyu-tei:20180412223707p:plain
(上下反対なのは実装の都合上ラズパイカメラを逆さまに取り付けているだけで意図はないです。)
輪郭抽出するためカメラ画像を2値化する必要がある。それが下の画像
f:id:ginnyu-tei:20180412225731p:plain
発表資料にこの画像を貼り付けた時に
「あれ?2値化してしかも背景がない画像なら大量に自動生成して学習できるんじゃない?」
と大会の最終週にもかからず思いついてしまったので時間が全くないのにやってしまった。

学習データの自動生成

OpenCVの楕円描画関数を使った。
画像サイズ40x30(ラズパイだとこの画像サイズが限界だった) の中にランダムな楕円を描画して
楕円が右か左かの学習データを作成した。
5つ位描画してみたのが↓。若干怪しい画像もあるがまあ良しとした。
カメラの二値化画像と全く似ていないが結果は問題なく左右を認識できた。
これを10000データ自動生成した 。(学習結果を確認するために追加で+300生成)
f:id:ginnyu-tei:20180412230224p:plain

学習プログラム(Windows)

ラズパイでもWindows PCでも動く機械学習の環境を探した結果
言語はPythonでライブラリはTensorFlow+Kerasを使った。
↓はWindowsPCで実行した学習

import cv2
import numpy as np
import matplotlib.pyplot as plt

import keras
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras import backend as K

from IPython.display import SVG
from keras.utils.vis_utils import model_to_dot

width = 40
height = 30
line_width = 8

#plt.figure(figsize=(50,40))

train_num = 10000
#train_num = 9
data_num = 300
x_train = np.zeros((train_num,height,width,1), dtype=np.uint8)
y_train = np.zeros((train_num), dtype=np.uint8)
x_test = np.zeros((data_num,height,width,1), dtype=np.uint8)
y_test = np.zeros((data_num), dtype=np.uint8)
#########学習データ生成
for i in range(2):
    if i == 0:
        loop_num = train_num
    else:
        loop_num = data_num
    j = 0
    while(j<loop_num):
        #背景作成
        image = np.zeros((height,width,1), dtype=np.uint8)
        #image.fill(255)
        #ランダム座標生成
        #rand_num = 1:左0:右
        rand_num = np.random.randint(0,2)
        rand_center = np.random.randint(0,10)
        if rand_num == 1:
            center = (rand_center,height)
        else:
            center = (width-rand_center,height)
        angle = (np.random.randint(0,60),np.random.randint(10,120))
        #print(center,angle)
        cv2.ellipse(image,center,angle,360,0,360,255,line_width)
        
        #真っ黒は削除
        if(np.max(image)==0):
            continue
        index = np.where(image == 255)
        #楕円の中心が偏っても楕円が画像中心を超える場合は左右逆にする
        if (index[0][0] == 0):
            #print(j,index)
            if (index[1][0] < width/2+line_width) & (rand_num==0):
                rand_num = 1
            elif(index[1][0] > width/2-line_width) & (rand_num==1):
                rand_num = 0
        if i == 0:
            x_train[j] = image.copy()
            y_train[j] = rand_num
            #画像チェック
            #image=cv2.cvtColor(image,cv2.COLOR_GRAY2RGB)
            #plt.subplot(190+j+1)
            #plt.title(str(rand_num) +" "+ str(index[0][0]) +" "+ str(index[1][0]))
            #plt.imshow(image)
        else:
            x_test[j] = image.copy()
            y_test[j] = rand_num
        j = j + 1
            
x_train = x_train / 255
x_test = x_test / 255

y_train = keras.utils.to_categorical(y_train, 2)
y_test = keras.utils.to_categorical(y_test, 2)

#NN構築
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3),activation='relu',input_shape=(height,width,1)))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(32, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(2, activation='softmax'))
model.summary()
model.compile(loss=keras.losses.categorical_crossentropy,optimizer=keras.optimizers.Adadelta(),metrics=['accuracy'])
#学習
model.fit(x_train, y_train,batch_size=128,epochs=12,verbose=1,validation_data=(x_test, y_test))

#NNと重みをファイル出力
json_string = model.to_json()
open('c:\cnn_model_40_30.json', 'w').write(json_string)
model.save_weights("c:\cnn_model_weights_40_30.hdf5")
#学習結果確認
score = model.evaluate(x_test, y_test, verbose=1)

print('Test loss:', score[0])
print('Test accuracy:', score[1])

↑の実行結果
tensorflow-gpu使ったのですぐに終了した

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_3 (Conv2D)            (None, 28, 38, 32)        320       
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 26, 36, 64)        18496     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 13, 18, 64)        0         
_________________________________________________________________
dropout_3 (Dropout)          (None, 13, 18, 64)        0         
_________________________________________________________________
flatten_2 (Flatten)          (None, 14976)             0         
_________________________________________________________________
dense_3 (Dense)              (None, 32)                479264    
_________________________________________________________________
dropout_4 (Dropout)          (None, 32)                0         
_________________________________________________________________
dense_4 (Dense)              (None, 2)                 66        
=================================================================
Total params: 498,146
Trainable params: 498,146
Non-trainable params: 0
_________________________________________________________________
Train on 10000 samples, validate on 300 samples
Epoch 1/12
10000/10000 [==============================] - 14s - loss: 0.3082 - acc: 0.8600 - val_loss: 0.2049 - val_acc: 0.8867
Epoch 2/12
10000/10000 [==============================] - 2s - loss: 0.1949 - acc: 0.9089 - val_loss: 0.1447 - val_acc: 0.9300
Epoch 3/12
10000/10000 [==============================] - 1s - loss: 0.1539 - acc: 0.9280 - val_loss: 0.1327 - val_acc: 0.9433
Epoch 4/12
10000/10000 [==============================] - 1s - loss: 0.1338 - acc: 0.9372 - val_loss: 0.1316 - val_acc: 0.9400
Epoch 5/12
10000/10000 [==============================] - 1s - loss: 0.1140 - acc: 0.9504 - val_loss: 0.1181 - val_acc: 0.9500
Epoch 6/12
10000/10000 [==============================] - 1s - loss: 0.1043 - acc: 0.9579 - val_loss: 0.1069 - val_acc: 0.9500
Epoch 7/12
10000/10000 [==============================] - 1s - loss: 0.0997 - acc: 0.9631 - val_loss: 0.0847 - val_acc: 0.9733
Epoch 8/12
10000/10000 [==============================] - 1s - loss: 0.0878 - acc: 0.9615 - val_loss: 0.1088 - val_acc: 0.9600
Epoch 9/12
10000/10000 [==============================] - 1s - loss: 0.0815 - acc: 0.9690 - val_loss: 0.0981 - val_acc: 0.9567
Epoch 10/12
10000/10000 [==============================] - 1s - loss: 0.0784 - acc: 0.9691 - val_loss: 0.0958 - val_acc: 0.9700
Epoch 11/12
10000/10000 [==============================] - 1s - loss: 0.0746 - acc: 0.9714 - val_loss: 0.0693 - val_acc: 0.9800
Epoch 12/12
10000/10000 [==============================] - 1s - loss: 0.0715 - acc: 0.9737 - val_loss: 0.0732 - val_acc: 0.9667
 32/300 [==>...........................] - ETA: 0sTest loss: 0.073219653219
Test accuracy: 0.966666666667

このCNN自体はKerasのサンプルコードの層をそのまま流用して
画像サイズが小さくなった分と出力層が右・左の2つになった分ネットワークを小さくしている。
半年前はこれにTensorBoard組み込んで学習結果確認をしていましたがそのコードを紛失しましたorz
各層の大きさは適当に決めました。時間がなかったので追い込んでいません。
今の結果を見る限りもっと小さくても良さそうです。

推論&モータ制御プログラム(raspbian)

RaspberryPiで動かしたコードを↓に載せる
学習したモデルと重みを読み込んで推論している。 推論した結果に適当な重みを付けて左右のモーター電圧を調整することで
左右に曲がるようになっている。

# -*- coding: utf-8 -*-
import cv2
import numpy as np

import keras
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras.models import model_from_json
from keras.preprocessing.image import load_img, img_to_array
import smbus
import time

i2c = smbus.SMBus(1)
#学習に使ったモデルを読み込み
json_string = open('./cnn_model_40_30.json', 'r').read()
model = model_from_json(json_string)
#学習した重みを読み込み
model.load_weights("./cnn_model_weights_40_30.hdf5")
model.compile(loss=keras.losses.categorical_crossentropy,optimizer=keras.optimizers.Adadelta(),metrics=['accuracy'])
model.summary()
print "eval start"
cap = cv2.VideoCapture(0)

try:
  while True:
    ret,frame = cap.read()
    #cv2.imwrite('orig.bmp', frame);
    frame = cv2.resize(frame,(40,30))
    frame = cv2.flip(frame, -1);
    #グレースケール→2値化
    img_gray = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
    ret, img_dst = cv2.threshold(img_gray,0,255,cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)
    img_dst /= 255
    #学習データからの予測
    #predicted[0][0]:Right  predicted[0][1]:Left
    predicted = model.predict(np.array([img_to_array(img_dst)]), 1, 0)
    print(predicted);
    r_motor = (int(predicted[0][1] * 3) << 2);
    l_motor = (int(predicted[0][0] * 3) << 2);
    #予測結果に適当に重みをつけてモーター制御
    if (r_motor == 0):
      r_motor = 0x00
    else:
      r_motor = 0x1A + r_motor

    if (l_motor == 0):
      l_motor = 0x00
    else:
      l_motor = 0x1A + l_motor
    #DRB8830の設定
    i2c.write_byte_data(0x63, 0x00, r_motor)
    i2c.write_byte_data(0x64, 0x00, l_motor)
except KeyboardInterrupt:
  print("get SIGINT")
#割り込み(Ctrl+C)でモーターを止めて終了
i2c.write_byte_data(0x63, 0x00, 0x00) #R
i2c.write_byte_data(0x64, 0x00, 0x00) #L

cap.release()
cv2.destroyAllWindows()

結果

上のツイートの通り一日で実装できた。
自分の影を黒認識するのは二値化部分の問題だと思うのでまあ、セーフ

反省点

真面目に(?)CNN構築するの初めてだったので各層のサイズや層の数などは適当にやったのが反省点
もっと小さくても左右2値なんだから認識出来る気がする。
それと今回は割り切って左右にしたが分類数を増やしてちょっと右とかかなり左とか入れたかった。

これ書いてて思ったが入力が2値なんだからCNNも2値化できるんじゃないかな?
やる気がでたらこれも実験してみたい。

リチウムコイン電池で動くワイヤレス温湿度気圧ロガーを作ってみた-2-

前回の続き。

se.hatenablog.jp

本当は↓の連続送信テストが終了したらブログに纏めようかと思っていたが、2週間待っても電池が切れないので途中結果で書いておく。

連続送信テスト

前回の投稿から電池の持ちを調べる為に電池での連続送信テストしてみた。
実際の使用は1送信/1分の予定だがかなり時間がかかりそうなので 1送信/1秒という高頻度で送信をした。

センサーデータはシリアルポートでPCに接続されているのでTeraTermとかで適当にログを取れば済むのだが、日付や電池の電圧もログに取りたかったのでPythonを使ってプログラムを書いた。
↓が書いたプログラム

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

import serial
import signal
import sys
import datetime

def handler(signal, frame):
    f.close()
    dmm_ser.write(("SYSTem:LOCal\r\n").encode(sys.stdout.encoding))
    dmm_ser.close()
    sensor_ser.close()
    sys.exit(0)


f = open('test.txt','w')

signal.signal(signal.SIGINT, handler)
sensor_ser = serial.Serial(port="COM4", timeout=3,baudrate=19200)
dmm_ser = serial.Serial(port="COM3", timeout=3,baudrate=9600)

dmm_ser.write(("SYSTem:REMote\r\n").encode(sys.stdout.encoding))
i = 0;

while(1):
    line = sensor_ser.readline()
    dmm_ser.write(("READ?\r\n").encode(sys.stdout.encoding))
    dmm_val = dmm_ser.readline()
    d = datetime.datetime.today()
    ss = d.strftime("%Y-%m-%d %H:%M:%S")
    print( str(i) + "," + ss + "," + line.strip().decode('utf-8').replace(':', ',')+"," + dmm_val.decode('utf-8').strip())
    f.write( str(i) + "," + ss + "," + line.strip().decode('utf-8').replace(':', ',')+","+dmm_val.decode('utf-8').strip()+"\n")
    i = i + 1

f.close()
dmm_ser.close()
sensor_ser.close()

今使っているHP 34401Aはシリアルポートでリモート測定ができるので pyserialで両方のシリアルポートに接続し、日付を追加してファイルに保存している。
ファイルの中は↓のようになっている。

0,2017-08-02 00:22:08,00,XXXX,46,29,64,10,08,78,49,88,00,+3.15603450E+00
1,2017-08-02 00:22:09,00,XXXX,46,29,64,10,08,78,49,65,00,+3.16726560E+00
2,2017-08-02 00:22:10,00,XXXX,46,29,63,10,08,79,49,51,00,+3.17619240E+00
3,2017-08-02 00:22:11,00,XXXX,46,29,61,10,08,77,49,44,00,+3.18370380E+00
4,2017-08-02 00:22:12,00,XXXX,46,29,60,10,08,79,49,41,00,+3.18991540E+00
5,2017-08-02 00:22:13,00,XXXX,46,29,59,10,08,78,49,41,00,+3.19537430E+00
6,2017-08-02 00:22:14,00,XXXX,46,29,57,10,08,74,49,40,00,+3.20004990E+00
7,2017-08-02 00:22:15,00,XXXX,46,29,55,10,08,79,49,39,00,+3.20418880E+00

ファイルの中身はカンマ区切りで先頭から

0.受信回数
1.受信日時
2.送信モジュールのノード番号 3.送信モジュールの固有ID(↑のログでは一応XXXXに変えています)
4.受信データの RSSI 値
5.温度(整数部)
6.温度(少数部)
7.気圧(千・百の位)
8.気圧(十・一の位)
9.気圧(小数部)
10.湿度(整数部)
11.湿度(少数部)
12.空き
13.電圧
と並んでいる。

IM315TXは一度に8byteしか送れないので詰めて送信している。

温度・気圧・湿度の推移

Pythonスクリプトから出力されたファイルをExcelで読み、グラフ化したのが以下ツイートの画像

これをツイートした時点では気づかなかったが、原因は台風が近づいていたので気圧が1000hPaを下回り、 3桁になってので気圧とその後ろの湿度が一文字ずつずれていたから。

これもスクリプトをちゃちゃっと書いて修正

# coding: UTF-8

import sys
 
f = open('test2.txt','r')
fw = open('test3.txt','w')
lines2 = f.readlines()
f.close()
cnt = 0
for line in lines2:
    

    if (cnt % 10 == 0):
        tmp = line.split(",")
        #0,2017-08-02 00:22:08 ,00,XXXX,46,29,64,10,08,78,49,88,00,+3.15603450E+00
        #0|           1        |2 |   3| 4| 5| 6| 7| 8| 9|10|11|12|     13        |

        log_cnt = tmp[0];
        log_date = tmp[1];
        log_temp = int(tmp[5])+int(tmp[4])/100
        if (int(tmp[7]) < 20):
            log_press = int(tmp[7]) * 100 + int(tmp[8]) + int(tmp[9])/100
            log_hum = (int(tmp[10]) * 1000 + int(tmp[11])*10) / 1024
        else:
            #490011,2017-08-07 16:30:52 ,00,XXXX,46,36,39,99,95,15,58,45,00,+2.92671440E+00
            #  0   |           1        |2 |   3| 4| 5| 6| 7| 8| 9|10|11|12|     13        |
            log_press = int(tmp[7][0]) * 100 + int(tmp[7][1] + tmp[8][0]) + int(tmp[8][1] + tmp[9][0])/100
            log_hum = (int(tmp[9][1] + tmp[10][0] ) * 1000 + float(tmp[10][1] + tmp[11][0] + tmp[11][1])) / 1024
        fw.write(log_cnt + "," + log_date + "," + str(log_temp) + "," + str(log_press) + "," +  str(log_hum) + "\n" )
    cnt = cnt + 1

fw.close()

気圧(千・百の位) が20より大きければ一桁ずらして計算している。
その結果、正常っぽい値になった。

f:id:ginnyu-tei:20170816005148p:plain:w600
↑でも触れているがこの時台風5号が近づいたので気圧がかなり下がっている。
温湿度はこのグラフに室外の状況やエアコンの稼働状況なんかも入れると良さそう。

電圧の推移

↓は電圧のグラフ

f:id:ginnyu-tei:20170816005913p:plain:w600
初日は電圧が一気に下がったのですぐにテストは終了するかと思ったが一旦下がった後2、3日は電圧がほぼ変わらなかった。消費による電圧降下より室温による電圧変化の方が大きいくらい。その後緩やかに下がっていったが、ここ数日は電圧が落ちるペースが早くなってきた。
このブログを書いている現時点では1212000回送信で2.48V位である。
かなり下がってきたとはいえ、1.8Vまでは余裕がある。

テスト中間結果

1送信/1秒で1212000回送信という事は1送信/1分だと60倍なので約2.3年の換算になる。
元々1年位電池交換なしで動けばいいと思っていたので予想以上である。
ただし、これは室温の高い夏のテストなので冬場はどうかわからない。

電圧グラフを見ると室温で電圧が上下しているのでかなり影響を受ける様である。
これについては冬にテストして確かめたいと思う。

次回はさらに消費電力を減らすためにTI storeで色々仕入れたのでそれについて書く予定。

リチウムコイン電池で動くワイヤレス温湿度気圧ロガーを作ってみた-1-

秋月で温度・湿度・気圧をまとめて測定できるBME280というセンサーを買ったので
リチウムコイン電池で動くワイヤレスな装置を作ってみた。

f:id:ginnyu-tei:20170801215519j:plain:w400
(左:センサー・送信 右:受信)

主な部品リスト

名前 価格
MSP430G2553 210円
BME280モジュール 1080円
315MHz無線送信モジュール 2160円
315MHz無線受信モジュール 2160円
32.768kHz 水晶発振子 30円
ボタン電池基板取付用ホルダー CR2032用 50円
1.27mmピッチユニバーサル基板 100円
47kΩ
1.5kΩ
LED

リチウムコイン電池でマイコンを動かすのは初めてなので色々試行錯誤してやってみたい。
マイコンはTiの超低消費電力マイコンMSP430をチョイス。使った事のないマイコンなのでMSP430 Value Line LaunchPad Development Toolと追加でMSP430G2553を購入。 ユニバーサル基板はハーフピッチなのはIM315のコネクタが1.27mmピッチなのでそれに合わせた。

回路概要

今回はUEWを使って少しずつ動作確認しながら作成したので回路図は書かなかった。
裏側はこんなかんじ↓
f:id:ginnyu-tei:20170801231856j:plain:w400
なので、ここでは接続の概要だけ記載しておく。

送信側

BM280はSPI4線でMSP430のUSCI_B0に接続、CSはP1.3を使った。
IM315TXとはUART(P1.2、P1.3)で接続。(送信頻度が少ないのでBusyは配線してない)
32.768kHz 水晶発振子はLPM3からの復帰用にXIN、XOUTに接続。

受信側

受信側はPCでロギングする為にIM315RXとFT232xを接続した。

MSP430プログラム

ソフトはmain.cとUARTでの文字送信用関数群のsimple_uart.hとBM280を制御するbm280.hに分かれている。

BM280の制御プログラムは以下ページのコードを使わせて頂きました。
www.mgo-tec.com

main.c

#include <msp430.h> 
#include <stdint.h>
#include <simple_uart.h>
#include <bme280.h>

/*
 * main.c
 */
int main(void)
{
    //WDTCTL = WDTPW + WDTHOLD; // Stop WDT
    WDTCTL = WDT_ADLY_1000;
    IE1 |= WDTIE;

    //クロック設定
    DCOCTL = 0;
    BCSCTL1 = CALBC1_1MHZ;
    DCOCTL = CALDCO_1MHZ;

    //P1.5 SCLK
    P1SEL |= BIT5;
    P1SEL2 |= BIT5;
    //P1.6 SOMI
    P1SEL |= BIT6;
    P1SEL2 |= BIT6;
    //P1.7 SIMO
    P1SEL |= BIT7;
    P1SEL2 |= BIT7;
    //P1.3 SS
    P1DIR |= BIT3;
    P1OUT |= BIT3;

    //P1.1 RXD
    P1SEL |= BIT1;
    P1SEL2 |= BIT1;
    //P1.2 TXD
    P1SEL |= BIT2 ;
    P1SEL2 |= BIT2;

    //UART
    //enable software reset
    UCA0CTL1 = UCSWRST;

    //UART設定
    UCA0CTL1 |= UCSSEL_2; // SMCLK
    UCA0BR0 = 104; // 1MHz 9600
    UCA0BR1 = 0; // 1MHz 9600
    UCA0MCTL = UCBRS0; // Modulation UCBRSx = 1
    //clear software reset
    UCA0CTL1 &= ~UCSWRST;

    //SPI
    //enable software reset
    UCB0CTL1 = UCSWRST;
    //SPI設定
    UCB0CTL0 = UCCKPL|UCMODE0|UCMSB|UCMST|UCSYNC;
    //SCK設定
    UCB0CTL1 = UCSSEL1;
    UCB0BR0 = 1;
    UCB0BR1 = 0;
    //clear software reset
    UCB0CTL1 &= ~UCSWRST;

    //BME280
    uint8_t t_sb = 5; //stanby 1000ms
    uint8_t filter = 0; //filter O = off
    uint8_t spi3or4 = 0; //SPI 3wire or 4wire, 0=4wire, 1=3wire
    uint8_t osrs_t = 4; //OverSampling Temperature x4
    uint8_t osrs_p = 4; //OverSampling Pressure x4
    uint8_t osrs_h = 4; //OverSampling Humidity x4
    uint8_t Mode = 3; //Normal mode
    uint8_t ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | Mode;
    uint8_t config_reg    = (t_sb << 5) | (filter << 2) | spi3or4;
    uint8_t ctrl_hum_reg  = osrs_h;
    writeReg(0xF2,ctrl_hum_reg);
    writeReg(0xF4,ctrl_meas_reg);
    writeReg(0xF5,config_reg);
    readTrim();

    while (1) {
        __bis_SR_register(LPM3_bits+GIE); //LPM3に移行

        volatile double temp_act = 0.0, press_act = 0.0, hum_act = 0.0, altitude_act = 0.0;
        int32_t temp_cal;
        uint32_t press_cal,hum_cal;

        readData();

        temp_cal = calibration_T(temp_raw);
        press_cal = calibration_P(pres_raw);
        hum_cal = calibration_H(hum_raw);
        //temp_act = (double)temp_cal / 100.0;
        //press_act = (double)press_cal / 100.0;
        //hum_act = (double)hum_cal / 1024.0;
        //IM315TXへ送信
        uart_puts("TXDT ");
        uart_putdec(temp_cal);
        uart_putdec(press_cal);
        uart_putdec(hum_cal);
        uart_puts("\r\n");
        //_delay_cycles(1000000);  // 1sec

    }
}

#pragma vector=WDT_VECTOR
__interrupt void watchdog_timer (void)
{
    __bic_SR_register_on_exit(LPM3_bits); // LPMを解除
}


// スタートアップルーチンから呼び出される初期化処理
void __low_level_init(void)
{
    WDTCTL = WDT_ADLY_1000; // WDTのクロック・ソースをACLK、
                            // 1sのインターバルタイマに設定
}

simple_uart.h

/*
 * simple_uart.h
 *
 */

#ifndef SIMPLE_UART_H_
#define SIMPLE_UART_H_

#include <stdint.h>

void uart_putc(char c)
{
    while (!(IFG2&UCA0TXIFG));
    UCA0TXBUF = c;
}

int uart_puts(const char *s) {
    int n;
    for (n = 0; *s; s++, n++) {
        uart_putc(*s);
    }
    return n;
}


void uart_putdec(uint32_t value) {
    int         i;          // General purpose counter.
    uint32_t    m;          // Mask for decimal number parser.
    char        nbuf[12];   // Buffer to store the decimal number.

    // Maximum power of 10 number in 32-bit.
    m = 1000000000;
    // Find the first masking number.
    while ((m > 1) && (value < m)) {
        m /= 10;
    }
    // Parse the value into a decimal number.
    i = 0;
    while (m >= 1) {
        nbuf[i++] = '0' + (value / m);
        value %= m;
        m /= 10;
    }
    nbuf[i++] = 0;      // End of string.
    uart_puts(nbuf);    // Put the decimal number.
}




#endif /* SIMPLE_UART_H_ */

bme280.h

/*
 * bme280.h
 *
 */

#ifndef BME280_H_
#define BME280_H_

#include <stdint.h>

uint16_t read16bit(uint8_t reg);
uint8_t read8bit(uint8_t reg);
void SpiWrite(uint8_t data);
uint8_t SpiRead();

uint32_t hum_raw, temp_raw, pres_raw;
 int32_t t_fine;

uint16_t dig_T1;
 int16_t dig_T2;
 int16_t dig_T3;

uint16_t dig_P1;
 int16_t dig_P2;
 int16_t dig_P3;
 int16_t dig_P4;
 int16_t dig_P5;
 int16_t dig_P6;
 int16_t dig_P7;
 int16_t dig_P8;
 int16_t dig_P9;

uint8_t  dig_H1;
 int16_t dig_H2;
uint8_t  dig_H3;
 int16_t dig_H4;
 int16_t dig_H5;
 int8_t  dig_H6;

 double SeaLevelPressure_hPa = 1013.25; //標準は1013.25


//*****************初期値読み込み**************************************
void readTrim(void) {
    dig_T1 = read16bit(0x88);
    dig_T2 = (int16_t)read16bit(0x8A);
    dig_T3 = (int16_t)read16bit(0x8C);

    dig_P1 = read16bit(0x8E);
    dig_P2 = (int16_t)read16bit(0x90);
    dig_P3 = (int16_t)read16bit(0x92);
    dig_P4 = (int16_t)read16bit(0x94);
    dig_P5 = (int16_t)read16bit(0x96);
    dig_P6 = (int16_t)read16bit(0x98);
    dig_P7 = (int16_t)read16bit(0x9A);
    dig_P8 = (int16_t)read16bit(0x9C);
    dig_P9 = (int16_t)read16bit(0x9E);

    dig_H1 = read8bit(0xA1);
    dig_H2 = (int16_t)read16bit(0xE1);
    dig_H3 = read8bit(0xE3);
    dig_H4 = (int16_t)((read8bit(0xE4) << 4) | (read8bit(0xE5) & 0x0F));
    dig_H5 = (int16_t)((read8bit(0xE6) << 4) | (read8bit(0xE5) >> 4));
    dig_H6 = (int8_t)read8bit(0xE7);
}
//***************BME280へ初期レジスタ書き込み関数****************************
void writeReg(uint8_t reg_address, uint8_t data) {
  P1OUT &= ~BIT3;
  SpiWrite(reg_address & 0x7F); // write, bit 7 low
  SpiWrite(data);
  P1OUT |= BIT3;
}
//***************BME280からの温度、湿度、気圧データ読み込み関数****************************
void readData() {

  uint32_t data[8];
  uint8_t i;

  P1OUT &= ~BIT3;
  SpiWrite(0xF7 | 0x80); //0xF7 pressure msb read, bit 7 high
  for(i=0; i<8; i++){
    data[i] = SpiRead();
  }
  P1OUT |= BIT3;
  pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4); //0xF7, msb+lsb+xlsb=19bit
  temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4); //0xFA, msb+lsb+xlsb=19bit
  hum_raw  = (data[6] << 8) | data[7];  //0xFD, msb+lsb=19bit(16:0)
}
//***************温度キャリブレーション関数****************************
int32_t calibration_T(int32_t adc_T) {
    int32_t var1, var2, T;
    var1 = ((((adc_T >> 3) - ((int32_t)dig_T1<<1))) * ((int32_t)dig_T2)) >> 11;
    var2 = (((((adc_T >> 4) - ((int32_t)dig_T1)) * ((adc_T>>4) - ((int32_t)dig_T1))) >> 12) * ((int32_t)dig_T3)) >> 14;

    t_fine = var1 + var2;
    T = (t_fine * 5 + 128) >> 8;
    return T;
}
//***************気圧キャリブレーション関数****************************
uint32_t calibration_P(int32_t adc_P) {
    int32_t var1, var2;
    uint32_t P;
    var1 = (((int32_t)t_fine)>>1) - (int32_t)64000;
    var2 = (((var1>>2) * (var1>>2)) >> 11) * ((int32_t)dig_P6);
    var2 = var2 + ((var1*((int32_t)dig_P5))<<1);
    var2 = (var2>>2)+(((int32_t)dig_P4)<<16);
    var1 = (((dig_P3 * (((var1>>2)*(var1>>2)) >> 13)) >>3) + ((((int32_t)dig_P2) * var1)>>1))>>18;
    var1 = ((((32768+var1))*((int32_t)dig_P1))>>15);
    if (var1 == 0) {
        return 0;
    }
    P = (((uint32_t)(((int32_t)1048576)-adc_P)-(var2>>12)))*3125;
    if(P<0x80000000) {
       P = (P << 1) / ((uint32_t) var1);
    }else{
        P = (P / (uint32_t)var1) * 2;
    }
    var1 = (((int32_t)dig_P9) * ((int32_t)(((P>>3) * (P>>3))>>13)))>>12;
    var2 = (((int32_t)(P>>2)) * ((int32_t)dig_P8))>>13;
    P = (uint32_t)((int32_t)P + ((var1 + var2 + dig_P7) >> 4));
    return P;
}
//***************湿度キャリブレーション関数****************************
uint32_t calibration_H(int32_t adc_H) {
    int32_t v_x1;

    v_x1 = (t_fine - ((int32_t)76800));
    v_x1 = (((((adc_H << 14) -(((int32_t)dig_H4) << 20) - (((int32_t)dig_H5) * v_x1)) +
              ((int32_t)16384)) >> 15) * (((((((v_x1 * ((int32_t)dig_H6)) >> 10) *
              (((v_x1 * ((int32_t)dig_H3)) >> 11) + ((int32_t) 32768))) >> 10) + ((int32_t)2097152)) *
              ((int32_t) dig_H2) + 8192) >> 14));
   v_x1 = (v_x1 - (((((v_x1 >> 15) * (v_x1 >> 15)) >> 7) * ((int32_t)dig_H1)) >> 4));
   v_x1 = (v_x1 < 0 ? 0 : v_x1);
   v_x1 = (v_x1 > 419430400 ? 419430400 : v_x1);
   return (uint32_t)(v_x1 >> 12);
}
//***************標高計算関数****************************************************
//double ReadAltitude(double SeaLevel_Pres, double pressure) {
//  double altitude = 44330.0 * (1.0 - pow(pressure / SeaLevel_Pres, (1.0/5.255)));
//  return altitude;
//}
//***************BME280から16bitデータ読み込み関数****************************
uint16_t read16bit(uint8_t reg) {
  uint16_t d1, d2;
  uint16_t data;
  P1OUT &= ~BIT3;
  SpiWrite(reg | 0x80); // read, bit 7 high
  d1 = SpiRead();
  d2 = SpiRead();
  data = (d2 << 8) | d1;
  P1OUT |= BIT3;
  return data;
}
//***************BME280から8bitデータ読み込み関数****************************
uint8_t read8bit(uint8_t reg) {
  uint8_t data;
  P1OUT &= ~BIT3;
  SpiWrite(reg | 0x80); // read, bit 7 high
  data = SpiRead();
  P1OUT |= BIT3;
  return data;
}
//***************BME280へSPI信号データ送信関数****************************
void SpiWrite(uint8_t data) {
    volatile char hoge;
    while (!(IFG2 & UCB0TXIFG));
    UCB0TXBUF = data;
    while (!(IFG2 & UCB0RXIFG));
    hoge = UCB0RXBUF;
}
//***************BME280からのSPI信号データ読み込み関数****************************
uint8_t SpiRead() {
  uint8_t r_data = 0;
  UCB0TXBUF = 0x00;
  while (!(IFG2 & UCB0RXIFG));
  r_data = UCB0RXBUF;
  return r_data;
}

#endif /* BME280_H_ */

電圧・電流測定

作成後、電圧をどこまで下げても動作するかとその際の電流値を測定した。
下の様に安定化電源で電源供給してDMMで電流測定している。 f:id:ginnyu-tei:20170801234005j:plain:w400
電流値は送信中とスリープ中で全く異なる値になるのでしばらく動かしてMaxを確認する。

電圧 Imax Imin
3.3V 957uA 541uA
3.0V 920uA 531uA
2.5V 865uA 516uA
2.0V 820uA 502uA
1.8V 799uA 496uA

1.8Vまでは起動・送信が問題なかったが1.8Vを下回ると不安定になった。
これで動作確認もできたので次回は長時間のロギングをやってみる。

参考にしたサイト

BME280 – スイッチサイエンス
MSP430 Tutorial and Resources · Argenox Technologies
MSP430班(2006/05/02) ウォッチドッグタイマ2

Vivado IP Integratorでap_fifoとfifoを簡単に接続する

ap_fifofifoを接続するときの小ネタ。

ap_fifofifoと接続されない

Vivado HLSでap_fifoを多用する私ですが、Vivado IP Integrator上でFIFO Generatorと接続しようとしてもそのままでは繋がりません。
f:id:ginnyu-tei:20170717120816j:plain:w400
↑の様に接続候補に現れない。

ap_fifoは何故かfullやempty信号が負論理になっているので反転する必要があります。

f:id:ginnyu-tei:20170717121804j:plain:w400
↑この様な形で配線が必要。

この配線を毎回するのは面倒な上にせっかくIP IntegratorでFIFO I/Fがまとめてられているのに毎度ばらして配線するのは見た目が悪いので変換するIPを作成した。
これを使うと↓の様に配線がすっきりする。
f:id:ginnyu-tei:20170717123204j:plain:w400

ap_fifo2fifo

まずIPにするRTLだが↓の様にFULLを反転するコードを用意する。
これでIPを作成すれば良い。

module ap_fifo2fifo(
    WR_DATA_o,
    FULL_N_o,
    WR_EN_o,
    WR_DATA_i,
    FULL_i,
    WR_EN_i
);

parameter WIDTH = 32;

//ap_fifo
input [WIDTH-1:0] WR_DATA_o;
output FULL_N_o;
input WR_EN_o;

//fifo
output [WIDTH-1:0] WR_DATA_i;
input FULL_i;
output WR_EN_i;


assign WR_DATA_i = WR_DATA_o;
assign FULL_N_o = ~FULL_i;
assign WR_EN_i = WR_EN_o;

endmodule

このままIPを生成すると各ポートがそのままになるのでI/F定義が必要になる。
f:id:ginnyu-tei:20170717124414j:plain:w400

din側はInterface Definitionを"acc_fifo_write_rtl"のslaveにして各ポートを割り当てる。
f:id:ginnyu-tei:20170717124525j:plain:w400
f:id:ginnyu-tei:20170717124718j:plain:w400
dout側はInterface Definitionを"fifo_write_rtl"のmasterにして各ポートを割り当てる。
f:id:ginnyu-tei:20170717124838j:plain:w400 f:id:ginnyu-tei:20170717124851j:plain:w400

fifo2ap_fifo

ap_fifo2fifoもemptyを反転するRTLを用意してIPを作成する。

module fifo2ap_fifo(
    RD_DATA_i,
    EMPTY_i,
    RD_EN_i,
    RD_DATA_o,
    EMPTY_N_o,
    RD_EN_o
);

parameter WIDTH = 32;

//fifo
input [WIDTH-1:0] RD_DATA_i;
input EMPTY_i;
output RD_EN_i;

//ap_fifo
output [WIDTH-1:0] RD_DATA_o;
output EMPTY_N_o;
input RD_EN_o;

assign RD_DATA_o = RD_DATA_i;
assign EMPTY_N_o = ~EMPTY_i;
assign RD_EN_i = RD_EN_o;

endmodule

こちらもI/F定義をやっていく。
f:id:ginnyu-tei:20170717130240j:plain:w400

din側はInterface Definitionを"fifo_read_rtl"のmasterにして各ポートを割り当てる。 f:id:ginnyu-tei:20170717130343j:plain:w400
f:id:ginnyu-tei:20170717130354j:plain:w400
dout側はInterface Definitionを"acc_fifo_read_rtl"のslaveにして各ポートを割り当てる。
f:id:ginnyu-tei:20170717130425j:plain:w400
f:id:ginnyu-tei:20170717130439j:plain:w400

後はVivadoのプロジェクト上から各IPをインスタンスすれば簡単に配線できる。