日曜技術者のメモ

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

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をインスタンスすれば簡単に配線できる。

VivadoHLSで射影変換を実装してみた-3-

↓の続きで今回が多分射影変換の最終回

se.hatenablog.jp

原因追求

前回射影変換をお手軽に実装したがかなり遅かったのでまずは原因を確かめる。
と、言っても原因は↓のAXI HP3の波形を見れば明らか。
f:id:ginnyu-tei:20170716121548j:plain:w400
Write/Read共にシングルアクセスなので1pix読み書きに時間がかかっている。なのでピクセルクロックの3倍で動作させても遅いのである。

しかし、射影変換は座標を変換するのでアドレスが飛び飛びになる。これ自体はどうしようもないので他の方法で速度アップを図る。

クロックアップ(150MHz→250MHz)

まず、手抜き手軽な対応としてバス周りのクロックアップをやってみる。homography_dmaとHP3バス周りを250MHzまで上げてみた。
ちなみにhomography_dmaは300MHzまで合成できたが、PSからPLへ供給できるクロックが250MHzまでだったので250MHzにした。
その結果が約33fpsまで上がった。
f:id:ginnyu-tei:20170716211315j:plain:w400

ただし、AXI HP3の波形はあまり変わっていない。
f:id:ginnyu-tei:20170716215304j:plain:w400

ライトアクセス改善

次にライトアクセスを改善してみる。
上でも書いたとおり射影変換のアドレスが飛び飛びになるが今回の実装では、画像データライトは連続している。
そこで、homography_dmaを改造してライトはバースト転送にする。

f:id:ginnyu-tei:20170716214224j:plain
↑の様にリードしたデータを一旦FIFOにプッシュして別モジュールからバーストライトを実行する。
これでDRAMへの負荷軽減を期待している。

では、コードを記載していく。
まずはhomography_dma_write_fifoここはo_dataのm_axiをap_fifo変えるだけ。この様にI/Fを手軽に変更できるのも高位合成の良い所。

void homography_dma_write_fifo(
        unsigned int *i_data,
        unsigned int *o_data,
        unsigned int *addr
        ){
#pragma HLS INTERFACE ap_fifo depth=786432 port=addr
#pragma HLS INTERFACE m_axi depth=786432 port=i_data
#pragma HLS INTERFACE ap_fifo depth=786432 port=o_data

    unsigned int tmp;
    for (int i = 0 ; i < 1024*768; i++){
#pragma HLS PIPELINE
        tmp = *(addr + i);
        if (tmp > 786432){
            o_data[i] = 0;
        } else {
            o_data[i] = i_data[tmp];
        }
    }
}

次にfifo2axi_xgaのコード

void fifo2axi_xga(
        unsigned int *i_data,
        unsigned int *o_data
        ){
#pragma HLS INTERFACE ap_fifo depth=786432 port=i_data
#pragma HLS INTERFACE m_axi depth=786432 port=o_data

    for (int i = 0 ; i < 1024*768; i++){
        o_data[i] = i_data[i];
    }
}

ただfifoのデータをaxiに書き込むだけである。ここはバーストlengthを指定できないか色々やってみたが、この書き方が16バースト固定で一番速度が早かった。
Vivado HLSでCosimした結果が↓
f:id:ginnyu-tei:20170716223136j:plain:w400

実際にはFIFOにライトする速度の方が早いのでfifoにある程度データが溜まってからライトを開始するようにすれば効率よくAXIライト出来ると思う。
(これ書いてて思いついたが、ap_fifoのempty信号にAlmost Fullを使ってバーストlength分溜まったら信号を出す様にすれば必ず1バースト分は連続で出力できる。でも、1フーレムのデータ数と1バーストの転送量の関係によってはFIFOに半端なデータが残りそう)

これで必要なモジュールができたのでIP Integratorで配線した結果が↓
f:id:ginnyu-tei:20170717001902j:plain:w400

ブロックがかなり増えたのでCreate Hierarchyで階層化した。
この状態でZedboardでhomography_dma_write_fifoのap_doneを観察した結果が↓
f:id:ginnyu-tei:20170717004256j:plain:w400
約47FPSとかなり改善した。
実際はap_doneの後fifo2axi_xgaがまだ動いているのがパイプライン的な動作になっているのでFPSには影響しない(はず)。
(例えパイプライン動作でなくとも残っているは1バースト+α位で多少増えるだけ)
AXI HP3の波形を確認するとリード側はあまり変化していないが、ライト側はバースト転送になっているのがわかる。
f:id:ginnyu-tei:20170717010613j:plain:w400

さらなる高速化

以上で簡単にできそうな改良をして2倍位早くなった。
これ以上早くするアイディアはいくつかあるが実装に時間がかかりそうなのでメモだけしておく。

  1. ブロックRAMを使う
    フレームデータをDRAMでなくブロックRAMに格納する。これならRead/Writeが1サイクルで可能なので高速化できる。でも、ブロックRAMが多いFPGAじゃないと足りなくなる。
  2. 先にアドレス計算をしておく
    一度パラメータを決めたらしばらく変えなくて良いアプリケーション向けだが、先に1フレーム分座標を計算してバースト転送出来るように並び替えをする。
  3. 変換する範囲をなるだけ狭くする
    今回の様にフレーム分の一部のみ変換する場合はフレーム全て変換せず、変換前/変換後の各4座標から変換に必要な範囲だけを転送するようにする。
  4. ブロックサイズでリードする。
    変換する次のアドレスは今のアドレスの近くにある可能性が高いので8×8や16×16のサイズでリードしてデータを貯めておく。

とりあえず、この位思いついた。実装は3以外大変そうなので誰かこれを試してみてほしい。
また思いついたら書き足す。

Zynqのベアメタルアプリケーションで画像をメモリに書き込む

Zynqでベアメタルアプリケーション(OSなし)を走らせる際
テスト入力として画像データをメモリ上に展開したくなる時がある。
その際GIMPを使えば簡単に画像データをソースコードに変換できるので
blogに書いておく。

GIMP

まず、GIMPをインストール&起動する。
そしてメモリに展開したい画像を開く

開いた後、ファイル→名前をつけてエクスポートを選ぶ、
そして、右下のエクスポートされた全ての画像から
「Cソースコードヘッダ」を選んで適当に名前をつけてエクスポートする。
f:id:ginnyu-tei:20170709005400j:plain

(かなり大きなヘッダファイルが出力されるので開く時は注意!)

出力されたファイルを確認すると以下の様なコードが出力される。

static unsigned int width = 1024;
static unsigned int height = 768;

/*  Call this macro repeatedly.  After each use, the pixel data can be extracted  */

#define HEADER_PIXEL(data,pixel) {\
pixel[0] = (((data[0] - 33) << 2) | ((data[1] - 33) >> 4)); \
pixel[1] = ((((data[1] - 33) & 0xF) << 4) | ((data[2] - 33) >> 2)); \
pixel[2] = ((((data[2] - 33) & 0x3) << 6) | ((data[3] - 33))); \
data += 4; \
}
static char *header_data =
    "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
    "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
    "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
    "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
~以下画像データが続く~

SDK

ヘッダファイルができたらXilinx SDKでヘッダファイルをincludeする。
HEADER_PIXELを呼び出す毎にラスタースキャンでRGB色が取得できるので
それを直接メモリに書き込む。

#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"

#include "test.h" //GIMPでエクスポートしたヘッダファイル


int main()
{
    int i,j;
    char pixel[3]; //RGB格納配列
    unsigned int R,G,B;

    volatile unsigned int *Addr;
    volatile unsigned int *reg_base;

    Addr = (unsigned int*)0x10000000;

    init_platform();

    Xil_DCacheDisable();


    print("Hello World\n\r");

    for (j = 0 ; j < 768;j++){
        for (i = 0 ; i < 1024;i++){
            HEADER_PIXEL(header_data,pixel); //1pix読み込む
            R = pixel[0] & 0xFF;
            G = pixel[1] & 0xFF;
            B = pixel[2] & 0xFF;
            Addr[j*1024+i]= (R << 16) | (G << 8) | (B); //メモリへ書き込み
            //Addr[j*1024+i]=j*1024+i;
        }
    }
~以下省略~

この手法はjtagで画像をFPGAに転送するので大きな画像の場合
少し時間がかかるのが難点だが手軽に使えるのが利点。

VivadoHLSで射影変換を実装してみた-2-

前回の続き

se.hatenablog.jp

Vivado HLS

射影変換は以下図のように2モジュールに分けて実装をした。
f:id:ginnyu-tei:20170708185251j:plain:w200

fifo使ってモジュールを小分けにすると
実装が楽になったり速度が出やすくなるので良く使います。
ここら辺の話は需要があればblogかどっかで纏めるかも。

homography

まずはhomographyのC++コード

void homography(
        int a,
        int b,
        int c,
        int d,
        int e,
        int f,
        int g,
        int h,
        unsigned int *addr)
{
#pragma HLS INTERFACE s_axilite port=a bundle=reg
#pragma HLS INTERFACE s_axilite port=b bundle=reg
#pragma HLS INTERFACE s_axilite port=c bundle=reg
#pragma HLS INTERFACE s_axilite port=d bundle=reg
#pragma HLS INTERFACE s_axilite port=e bundle=reg
#pragma HLS INTERFACE s_axilite port=f bundle=reg
#pragma HLS INTERFACE s_axilite port=g bundle=reg
#pragma HLS INTERFACE s_axilite port=h bundle=reg

   #pragma HLS INTERFACE ap_fifo port=addr

    unsigned int u,v;

    for (int y = 0 ; y < 768 ; y++){
        for (int x = 0;x < 1024; x++){
#pragma HLS PIPELINE
            u = (x*a + y*b + c) / (x*g + y*h + 65536);
            v = (x*d + y*e + f) / (x*g + y*h + 65536);
            int tmp = v*1024+u;
            if ((v >= 0) & (v < 768) & (u >= 0) & (u < 1024)){
                *addr = tmp;
            } else {
                *addr = 786433; //786432 + 1
            }
        }
    }
}

↑の図では書いていなかったがlパラメータa~hはAXI LiteのレジスタにしてCPUから設定。
射影変換の式はC++コードそのままで、変換後座標を画像左上から順に変化させ
変換前座標を計算し、そこからメモリアドレスを計算してFIFOに入れる。
変換後座標値の値によっては範囲外の変換前座標になる事があるので
その時は範囲外という意味で1024*768+1の値をFIFOに入れる。
ここではパイプラインオプションを入れているので毎サイクル計算結果が出力される。

modelsimでシミュレーションしてみると毎サイクル出てくるのが確認できる。
f:id:ginnyu-tei:20170708190530j:plain:w200

そして回路規模
f:id:ginnyu-tei:20170708203559j:plain
積和除算があるのでDSPはそこそこ使ってる。

homography_dma

次はhomography_dmaのコード

void homography_dma(
        unsigned int *i_data,
        unsigned int *o_data,
        unsigned int *addr
        ){
#pragma HLS INTERFACE ap_fifo port=addr
#pragma HLS INTERFACE m_axi depth=786432 port=i_data
#pragma HLS INTERFACE m_axi depth=786432 port=o_data

    unsigned int tmp;
    for (int i = 0 ; i < 1024*768; i++){
#pragma HLS PIPELINE
        tmp = *addr;
        if (tmp > 786432){
            o_data[i] = 0;
        } else {
            o_data[i] = i_data[tmp];
        }
    }
}

homographyで計算したアドレスを使ってo_dataから出力するだけ。
元画像の範囲外座標は黒色を出力する。
なお、今回は実装していないがi_data[tmp]だけリードせず、
例えば2x2pixを読み込みしてバイリニアで補完したり、
4x4pixでバイキュービックで補完すると画質が良くなるはずである。
(この際はhomographyから1次元アドレスではなく座標値を送る用変更が必要)

↓がSim結果
AXIリード・ライト共にシングルアクセスである。
f:id:ginnyu-tei:20170708214451j:plain:w200

↓回路規模
f:id:ginnyu-tei:20170708214833j:plain

Vivado

これでモジュールが完成したので実機で試す。
TOPの配線は全てIP Integratorで済ます。
↓が全て配線したTOP
f:id:ginnyu-tei:20170708215611j:plain:w200

↓概略図
f:id:ginnyu-tei:20170708221823j:plain:w200

まず、0x10000000にCPUから画像データを書き込む。
その後、0x10000000からhomography_dmaモジュールが画像を読みこみ0x11000000に書き出す。
アナログRGBモジュールは0x10000000にある画像データを出力する。
なお、上図には書いていないが書くモジュールのap_startはVIOで制御している。
テスト稼働時はVIOを使うと非常に楽である。

本当は射影変換とアナログRGBモジュールのFPSが異なるので
フレームの同期が取れていないが今回はそのままにしておく。
静止画であれば気にならないと思う。

SDK

以下がPSで動かすプログラムである。
テンプレートにあるHello Worldを改変して使っている。

#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"

#include "test.h"

int main()
{
    int i,j;
    char pixel[3];
    unsigned int R,G,B;

    volatile unsigned int *Addr;
    volatile unsigned int *reg_base;

    Addr = (unsigned int*)0x10000000;
    reg_base   =  (unsigned int*)0x43C00000;


    init_platform();

    Xil_DCacheDisable();

    print("Hello World\n\r");

    for (j = 0 ; j < 768;j++){
        for (i = 0 ; i < 1024;i++){
            HEADER_PIXEL(header_data,pixel);
            R = pixel[0] & 0xFF;
            G = pixel[1] & 0xFF;
            B = pixel[2] & 0xFF;
            Addr[j*1024+i]= (R << 16) | (G << 8) | (B);
            //Addr[j*1024+i]=j*1024+i;
        }
    }

    /射影変換パラメータ設定
    reg_base[4]  = 0x2C6C6;
    reg_base[6]  = 0xF6EE;
    reg_base[8]  = 0xFCD32CF7;
    reg_base[10] = 0x248A;
    reg_base[12] = 0x2C492;
    reg_base[14] = 0xFE1BDF57;
    reg_base[16] = 0x49;
    reg_base[18] = 0x2A;

    cleanup_platform();
    return 0;
}

ここで画像データをCPUに書き込むが、
その画像データはGIMPで作成している。 これについては別途blogに書く予定。

Zedboardでテスト

Zedboardで動かした結果が↓
f:id:ginnyu-tei:20170705210623j:plain:w200
前回のソフトで動作させた結果とほぼ同じ形に変形している。

この状態でフレームレートを測定した。
フレームレートの測定はhomography_dmaのap_doneを
Pmodに出力しオシロスコープで間隔を測定した。
↓がオシロスコープの画面
f:id:ginnyu-tei:20170708225509j:plain:w200
ap_doneは1パルスしか出なくオシロで取り逃す可能性がある
そこでap_done毎に信号を反転するモジュールを入れている。
その結果、約24fps出ている事が分かった。
Twitterでは約11fpsとツイートしたが(↓)あれは測定ミスでした。)

以上で射影変換の実装&実機テストは完了である。
今回は射影変換をお手軽に実装するという脳内テーマで
スタートしたので速度や規模はあまり気にしていなかった。
(無事動いたのでOKとする)
とは言え、homography_dmaは150MHz(ピクセルクロックの3倍)で動作させているのに
24フーレムしか出ていないので、次回は原因についてと高速化について言及する。