PYNQ JTAGでZynqとRISC-Vをまとめてデバッグ(失敗)
Xilinxのフォーラムを見てたらこんなのがあった
以前SCR1をFPGAで動かしたときにFPGAとRISC-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のクロックを接続している
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が二つになっている
下のTargetに接続するとデバイスがないエラーになるがPSからClockを供給していないので想定通り。
次にPSを動作させRISC-Vがリセット解除されてから改めて接続。IDcodeやIR lengthがSCR1と同じなので認識されている様子
これでいけると思ったが、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で開発したいがサンプルプロジェクトがないので自分で作ることにした。 まずは設定が必要そうな電源回りを調査した
参考資料
- M5Core2公式サイト (ただしAXP192のデーターシートは一部しかない)
M5Stack Docs - The reference docs for M5Stack products. - AXP192データシート (こちらには英語版と中国語版があった)
M5-Schematic/Core at master · m5stack/M5-Schematic · GitHub - M5 Core2 library
M5Core2/AXP192.cpp at master · m5stack/M5Core2 · GitHub - M5StackCのAXP192をすでに解析されている方がいましたので参考にさせて頂きました
M5StickCのバッテリー管理AXP192を調べる | Lang-ship
電源管理(AXP192) - M5StickC非公式日本語リファレンス
AXP192周辺接続図
下図が回路図から簡略化した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で公開してみる。
完成した動作は↓を参照。
Keras+TensorflowでCNNを使ったラズパイライントレーサ作ってみた。
— michu (@ginnyu_tei) 2017年9月17日
挙動がかなり怪しい場面もあるがそこそこ動いてる。 pic.twitter.com/jEGCEZ7BjU
(なお、この動画の後は自分の影を黒と認識して机から落ちました・・・)
何分大会がある週の週末に思いついて一日で実装したので適当な実装箇所が
数多くあるが時間がなかったのでご容赦頂きたい。
本体解説
外観
2階建てで下のプレートはタミヤダブルギヤボックスとモーターコントローラ(DRV8830)
上はRaspberry Pi3とRaspberry Piカメラ+スマートフォン用クリップレンズ (広角)
広角のクリップレンズはラズパイカメラの画角が狭くて足元が映らなかったのでその対策。
電源はモバイルバッテリーを使っている
なぜ機械学習で実装しようと思ったか?
もともと機械学習を使うつもりはまったくなかった
機械学習は学習データを大量に用意するのが面倒だと思っていたからだ
なのでOpenCVの画像処理を使って↓の様に輪郭抽出と矩形取得を使って大会に持っていく予定だった。
(上下反対なのは実装の都合上ラズパイカメラを逆さまに取り付けているだけで意図はないです。)
輪郭抽出するためカメラ画像を2値化する必要がある。それが下の画像
発表資料にこの画像を貼り付けた時に
「あれ?2値化してしかも背景がない画像なら大量に自動生成して学習できるんじゃない?」
と大会の最終週にもかからず思いついてしまったので時間が全くないのにやってしまった。
学習データの自動生成
OpenCVの楕円描画関数を使った。
画像サイズ40x30(ラズパイだとこの画像サイズが限界だった) の中にランダムな楕円を描画して
楕円が右か左かの学習データを作成した。
5つ位描画してみたのが↓。若干怪しい画像もあるがまあ良しとした。
カメラの二値化画像と全く似ていないが結果は問題なく左右を認識できた。
これを10000データ自動生成した 。(学習結果を確認するために追加で+300生成)
学習プログラム(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-
前回の続き。
本当は↓の連続送信テストが終了したらブログに纏めようかと思っていたが、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で読み、グラフ化したのが以下ツイートの画像
自作したワイヤレスセンサーの送信回数が120万回(1送信/s)を超えたのでExcelでデータを確認。台風が近づいたのがよく分かる。
— michu (@ginnyu_tei) 2017年8月15日
気圧・湿度がバグっていたが気圧データは修復できた。
湿度はオーバーフローかな? pic.twitter.com/Q4A0zzwaJs
これをツイートした時点では気づかなかったが、原因は台風が近づいていたので気圧が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より大きければ一桁ずらして計算している。
その結果、正常っぽい値になった。
↑でも触れているがこの時台風5号が近づいたので気圧がかなり下がっている。
温湿度はこのグラフに室外の状況やエアコンの稼働状況なんかも入れると良さそう。
電圧の推移
↓は電圧のグラフ
初日は電圧が一気に下がったのですぐにテストは終了するかと思ったが一旦下がった後2、3日は電圧がほぼ変わらなかった。消費による電圧降下より室温による電圧変化の方が大きいくらい。その後緩やかに下がっていったが、ここ数日は電圧が落ちるペースが早くなってきた。
このブログを書いている現時点では1212000回送信で2.48V位である。
かなり下がってきたとはいえ、1.8Vまでは余裕がある。
テスト中間結果
1送信/1秒で1212000回送信という事は1送信/1分だと60倍なので約2.3年の換算になる。
元々1年位電池交換なしで動けばいいと思っていたので予想以上である。
ただし、これは室温の高い夏のテストなので冬場はどうかわからない。
電圧グラフを見ると室温で電圧が上下しているのでかなり影響を受ける様である。
これについては冬にテストして確かめたいと思う。
次回はさらに消費電力を減らすためにTI storeで色々仕入れたのでそれについて書く予定。
リチウムコイン電池で動くワイヤレス温湿度気圧ロガーを作ってみた-1-
秋月で温度・湿度・気圧をまとめて測定できるBME280というセンサーを買ったので
リチウムコイン電池で動くワイヤレスな装置を作ってみた。
(左:センサー・送信 右:受信)
主な部品リスト
名前 | 価格 |
---|---|
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を使って少しずつ動作確認しながら作成したので回路図は書かなかった。
裏側はこんなかんじ↓
なので、ここでは接続の概要だけ記載しておく。
送信側
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で電流測定している。
電流値は送信中とスリープ中で全く異なる値になるのでしばらく動かして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_fifoはfifoと接続されない
Vivado HLSでap_fifoを多用する私ですが、Vivado IP Integrator上でFIFO Generatorと接続しようとしてもそのままでは繋がりません。
↑の様に接続候補に現れない。
ap_fifoは何故かfullやempty信号が負論理になっているので反転する必要があります。
↑この様な形で配線が必要。
この配線を毎回するのは面倒な上にせっかくIP IntegratorでFIFO I/Fがまとめてられているのに毎度ばらして配線するのは見た目が悪いので変換するIPを作成した。
これを使うと↓の様に配線がすっきりする。
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定義が必要になる。
din側はInterface Definitionを"acc_fifo_write_rtl"のslaveにして各ポートを割り当てる。
dout側はInterface Definitionを"fifo_write_rtl"のmasterにして各ポートを割り当てる。
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定義をやっていく。
din側はInterface Definitionを"fifo_read_rtl"のmasterにして各ポートを割り当てる。
dout側はInterface Definitionを"acc_fifo_read_rtl"のslaveにして各ポートを割り当てる。
後はVivadoのプロジェクト上から各IPをインスタンスすれば簡単に配線できる。