Raspberry Pi Zero Wに洗濯機を監視させる
最近洗濯乾燥機を購入したのですが、乾燥の度合いによって残り時間が前後し表示されている数字があまり当てにならないため監視アプリケーションを作成しました。
設置
部材
すべてオンラインショップで調達
- Raspberry Pi Zero W
- コスパ最強、無線付き、最高では
- Logicool Webcam C270
- 電源タップ
- 3M コマンドタブ
- 壁に貼ってはがせるやつ
設置
3Mのコマンドタブが最高すぎるので、すごく適当にひとまず固定します。
RasPi 準備
OSを焼く
公式サイトからOSを落とします。GUIとかいらないのでRASPBIAN STRETCH LITEを選択。
ダウンロード後dd
でSDカードにそのまま書き込みます
sshを有効化
どうやら最新版はsshが無効化されているらしいのでルートにsshという名前でからファイルを作成。
無線LAN設定
初回起動後に無線設定をするのが面倒なので調べていたら(microHDMI+microUSBとか) 素晴らしいツールを作られている方がいたので使用します。
作成したwpa_supplicant.conf
をSDカード直下に配置
起動
無事立ち上がってssh経由でアクセスできることを確認します。また、すぐにパスワードを変えます。
実装
細かい話をするとgcfとかvision apiとかtesserct-ocrとか色々遠回りしたのですが、必要な箇所だけ書きます。
Pythonでやってもよかったのですが、気分でnode.jsを採用。
Webカメラの制御
opencvを使うか他の手法で悩みましたが、fswebcamを利用したnode-webcamを利用。
NodeWebcam.capture
だけで写真を取って保存できるので最高。
ただし、awaitして使うと写真が保存されていないタイミングがあるっぽい挙動が見えたため以下のように実装。
const onCapture = (opts) => { return new Promise((resolve, reject) => { const identify = "capture" const filename = `${identify}.jpg`; const filepath = path.resolve(__dirname, filename); try { if (fs.existsSync(filepath)) { fs.unlinkSync(filepath); } NodeWebcam.capture(identify, opts, (err, result) => { // ファイル存在確認 if (!fs.existsSync(filepath)) { reject("capture failure"); return; } console.log('captured!') resolve(filename); }); } catch (e) { console.error(e); reject(e); } }); };
opts
は解像度やフォーマット等の書式設定、configに逃がしてある。
Slackに画像アップロード
非常に残念なことにデータのアップはWebhookではできないため、File Upload APIを利用。*1
面倒なのでyarn add @slack/client
を利用する。
const web = new WebClient(slackToken); const result = await web.files.upload({ filename: filename, file: fs.createReadStream(filepath), title: filename, channels: slackChannel, initial_comment: comment }); if (result.ok) { console.log(`slack post! ${result.file.permalink}`); } else { console.error('slack post error', result.error); }
slackTokenにはSlack管理画面で取得したトークンを入れ、slackChannelには投稿したいチャンネルのIDを設定します。*2
定期実行
node-cronを使用。
let count = 0; new CronJob(cronTime, async () => { console.log(`Job Start #${count++}`) const filename = await onCapture(cameraOption); const result = await onPost(filename, slackWebhookUrl, slackToken, slackChannel, slackComment); console.log('Done'); }, null, true);
Dockerfile作成
fswebcamとかnode versionに依存すると最悪なので作成。
コンテナ内から実デバイス等いじるときは--privileged
するかデバイスを指定する必要があるらしい。
結果
うざいぐらい定期的にSlackに洗濯機写真が流せるようになりました。
まとめ
最初は7segフォントのOCRするつもりでしたが、作っていてこれで要件が完全に満たせているのでこれで運用してみます。
GKEでCronJobを使い、定期処理を実行する
以前Splatoonの戦績管理のためのDockerコンテナを作成し、Google Kubernetes Engine(GKE)にアップロードしました。
数日間の運用をしてみたところの課題感を以下に示します。
定期実行のために常にタスクが動いている
これは予想していたことですが、splatnet2statinkのモニターモードはtime.sleep(1)
でカウントダウンしており*1定期実行のためだけに余計な処理をしています。
これはcronやsupervisorのようにいい感じの定期実行タスクに載せたいですね。
stackdriverのログ汚染
これもカウントダウンに関連する話ですが、残り時間のstdoutがそのままログに載ってくるので自ずとログがでかくなります。
ちゃんと動いているのか不明
更新されたときの結果ぐらいは、slackあたりで見たい...(次の記事で)
というような課題感があるため、Kubernetes 1.8から実装されたCronJobでバッチ処理に置き換え、結果もSlackに通知できるようにしてみます。
CronJobの導入
Web上のUIからでは、2018年7月現在はGKEのDeploymentしかワークロードが作成できません。ですのでCronJobなどを追加するにはgcloud sdk, kubectlを使ったコマンドライン操作が必須です。*2
公式ドキュメントを参考に進めていきます。
CronJobs | Kubernetes Engine | Google Cloud
これが毎分busyboxイメージで毎分こんにちわさせる設定ファイルです。
apiVersion: batch/v1beta1 kind: CronJob metadata: name: hello spec: schedule: "*/1 * * * *" jobTemplate: spec: template: spec: containers: - name: hello image: busybox args: - /bin/sh - -c - date; echo "Hello, World!" restartPolicy: OnFailure
見てみるとかなりシンプルです。scheduleにcrontabと同じ書式で周期を書き、あとはspec/containersに実行したいイメージと引数を与えればいいようです。
前回デプロイしたコンテナであるsplatnet2statink-dockerで同じことができるように書いてみます。
apiVersion: batch/v1beta1 kind: CronJob metadata: labels: app: <name> name: <name> namespace: default spec: schedule: 0 */3 * * * spec: template: spec: containers: - env: # ConfigMapからconfigMapKeyRefで引っ張ってくる - name: api_key - name: cookie - name: session_token - name: user_lang - name: run_flags # ここまで image: path/to/splatnet2statink-docker:1.0 name: splatnet2statink-docker restartPolicy: OnFailure
ほとんどテンプレ通りです。envに指定した環境変数は直に値を書いてもいいですが、後のことを考えるとConfigMap,Secretに書いた値を持ってくるようにします。
あとは、splatnet2statinkをモニターモードで起動する必要がないのでConfigMapでrun_flags
を-r
だけに変更しておきます。
此処から先はコマンドラインで作業をします。kubectlでターゲットのプロジェクトが使える状態にしておきます。
あとは新規作成であれば以下のコマンドだけでデプロイできます。yamlの中身で処理は変えてくれるのでCronJobに限らず、ConfigMapの編集やDeploymentも同様に扱えます。
$ kubectl create -f <設定ファイル.yml>
あとはWebコンソールを眺めに行くと、3時間ごとにPodが割当てられて、処理されていることが確認できます。
おわりに
定期実行のタイマーをKubernetes側に任せることで、処理が必要なときのみPodを確保するようになります。これにより以下の利点があります。
Compute Engineのリソース削減
共有CPUリソースであるf1-microを使用していますが、これに限らず課金額はCPU使用時間(*使用率)で決定します。ただカウントダウンをするのにリソースを使うのはもったいないですね。
他バッチ処理との共存
CronJobタスクを増やしていけば、同一クラスタ上で複数個のCronJobワークロードを実行できます。このおかげでVPSを解約しました。
プリエンプティブVMの使用
クラスタに使っている各ノードもGCEのマシンで常時確保されています。これはそのまま課金額に直結してしまうのですが、正直バッチ処理のときにスケジューリングできるノードがあれば、ほかは落ちていても良くない?という考えになります。
そこでプリエンプティブVMです。これは最長24hでシャットダウンされ可用性が保証されないVMです、その代わりに価格が安いというものです。
バッチ処理にしておけば、これらのVMでクラスタを構成しても運用できるようになります。詳細は割愛します。
Kubernetes Engine でプリエンプティブ VM を使用する | Kubernetes Engine | Google Cloud
*1:https://github.com/frozenpandaman/splatnet2statink/blob/59af7bf4fe582ba56bca8858013d5c9ab841846a/splatnet2statink.py#L351
*2:慣れればこっちのほうが楽ですが、コマンドの規模感が分かりづらいので...
スプラトゥーン2の戦績をsplatnet2statinkとGKEを使って自動アップロードする
きっかけ
私(kamiya)は任天堂から発売されているスプラトゥーン2を遊んでいます。
そこでスプラトゥーン上での戦績は、スマホアプリから閲覧できるのですがこれが直近の50戦しか閲覧することができません。
そこで非公式ではあるのですが、この戦績を管理するサービスであるstat.inkとアップロードするツールであるsplatnet2statink を使って戦績を管理しています。(※公式の手法ではないため推奨はいたしません、運営サーバに負荷をかけるような使い方はサービス妨害と見なされ戦績公開自体がなくなってしまうかもしれないのでお辞めください)
今回はVPS上で運用しているsplatnet2statinkをGKE(Google Kubernetes Engine)に移行します。*1 既存のpythonアプリケーションをコンテナ化してクラウド運用する手順の紹介、という立ち位置で読んでいただけると幸いです。
手順
大まかには以下の手順を踏んで行います。3.項移行はgcloud sdkを使う方法とGoogle CloudのWebページからでもどちらでも行なえます。ここはGoogleの公式ドキュメントのチュートリアルに詳しく記載されています。
単体で動作確認をする
Dockerコンテナ上で動作できるようする
Google Docker Registry(もしくはDockerHub)にイメージをpushする
Kubernetesクラスタに登録済のコンテナイメージをデプロイする
splatnet2statinkの動作
pythonで書かれた任天堂のサーバから戦績情報を取得するソフトです。コンテナ化する上でも簡単に動作を確認してみます。*2
7~40行目
config.txt
を読み出すか、作成しています。その内容はapi_key, cookie, user_lang, session_token
の4つです。
注目すべきはコンフィグファイルの指定はpython実行時のカレントディレクトリになる点です。
1170行目 name == "main"
pythonで直接実行されているときのみ実行されます。今回ではメインルーチンになっています。
1171行目→228行目 : main()
以下の動作をするようです。
- 224行目 → 194行目 : check_for_update()
どうやらgithubからコードの最新版があるか調べ、gitが実行できる環境ならgit pull
、できなければrequest.get
をしてリソース更新をしているようです。
よく見ると204行目でアップデートを行うかのプロンプトが必ず表示されるようです。
- 237行目~270行目
argparseライブラリを使って、コマンドライン引数の設定・取得を行っています。動作としては-M <更新間隔>
で 一定時間ごとの自動更新と、-r
でアップロードいていない戦績のみを対象にするオプションが使えれば良さそうです。
1175行目→295行目 : monitor_battle()
一定時間ごとに結果を取得してアップしています。
他にもいろいろありますが、以下の3点がコンテナ化する上でのポイントです。
自身のアップデートにrequest及びgitを使用している
設定ファイルは
config.txt
を使うこと実行時は
python splatnet2statink.py -M <秒数> -r
を実行できればよい
単体で動作確認をする
python splatnet2statink.py
で正常に動作できることを確認します。認証などについては詳細は触れません。
Dockerコンテナ上で動作できるようにする
まずは本アプリケーションをコンテナ化します。コンテナ化する上で認証情報などはあとから環境変数などで与えられるようにするのが通例です。
まずはgitを使ったアップデートですが、プロンプトにy
を打ち込ませるのも面倒なのでgit submodule
としてsplatnet2statinkを追加してgitコマンドを叩いてアップロードします。
ですので、実際に実行指定するスクリプトとしてrunner.py
を作成していきます。
$ git submodule add https://github.com/frozenpandaman/splatnet2statink splatnet2statink
pythonからは$ git submodule foreach git pull origin master
を叩ければ良いのでsubprocessモジュールを使って以下のようにします
subprocess.call(["git", "submodule", "foreach", "git", "pull", "origin", "master", ])
次に設定ファイルですが、環境変数を読み出してconfig.txt
を書き換えるようにします。os.getenv
で簡単に取得できるのでこれを利用します。
ファイルの書き込みはjsonモジュールを使うと、dictionaryをそのままjsonテキストにできるのでこれを書きます。
def env_to_config(envs_src): envs = dict([(e, os.getenv(e, "")) for e in envs_src]) envs_available = all(envs.values()) if envs_available: with open('config.txt', 'w') as file: file.write(json.dumps(envs)) print('#update config.txt')
最後にスクリプトの実行ですが、これはgitコマンドの実行と同じです。
args = ["python", "./splatnet2statink/splatnet2statink.py"] flags = os.getenv("run_flags", "-M 14400 -r").split(" ") # monitor per 4hour args.extend(flags) subprocess.call(args)
これらを統合してhttps://github.com/kamiyaowl/splatnet2statink-docker/blob/master/runner.pyとしました。
これをpython runner.py
して問題なく動作することを確認します。
あとはsplatnet2statinkに必要なライブラリをインストールするだけのDockerfileを書きます
FROM python:3.6 COPY . /app WORKDIR /app RUN pip3 install --upgrade -r splatnet2statink/requirements.txt CMD ["python", "-u", "runner.py"]
私は面倒くさがりでコマンドライン引数を大量に書くのが苦手なので、docker-composeファイルも一緒に作成しました。 あくまで動作確認用に作成しましたが、自分で行う場合このファイルの管理は注意してください。
splatnet2-statink: build: ./ # volumes: # - cache:/app # splatnet2statinkのリポジトリ更新キャッシュ用 environment: # Containerサービスの環境変数に設定する - api_key=your_api_key - cookie=your_cookie - session_token=your_session.token - user_lang=en-US
あとは正常に実行できることを確認します。
$ docker-compose up
Google Container Registryに作成したイメージをアップする
Google Cloud SDKのセットアップについては公式ドキュメントなどを参照してください。
// イメージ名を確認 $ docker image ls // タグ付けを行う $ docker tag <イメージ名> gcr.io/<プロジェクト名><イメージ名>:<タグ> // 作成したイメージをGCRにアップする $ gcloud docker -- push gcr.io/<プロジェクト名><イメージ名> // イメージ一覧を確認 $ gcloud container images list
不安なのでWebコンソールでも見てみます。
良さそうです。
Google Kubernetes Engineにアップする
ここからはGUIでもkubectlでもできます。おおまかな説明だけするとまずクラスタを作成します。
構成は任意ですが、動作確認なのでus-central1-a, g1-small, auto-scaling=off, size=1
で作成しました。
次にワークロードを作成します。作成時に先程GCRにアップしたイメージをベースイメージとして指定します。この際の環境変数にdocker-composeで指定していたconfig.txt
の項目を設定します。
この設定はGKEのConfigMapに保存されるのであとから編集できます。
静的IPやLBは外部ネットワークが必要ないので設定せず、あとはクラスタが構成されるまで少し待ちます。
無事に立ち上がった後、stat.inkの結果を見るなりログの内容を閲覧して正常に動作していることを確認できれば完了です。
WebコンソールからStackdriverの起動時のログを見てみましたが、ローカル起動時と同様に出力されており問題なく動作できていることが確認できます。
まとめ
各種サービスをコンテナ化してクラウドサービスに移行していくと、何よりサーバ管理の手間がないので面倒が減るのでかなり楽です。
従量課金の具合などから構成を見直したりする必要はあると思いますが*3、ちょっとした小回りから大掛かりなものまで活用できそうです。
今回作成したものも、githubで公開しておきます。
2018/7/28追記
CronJobで定期実行できるようにしました。
*1:正直Kubernetesが使いたいだけなのでCompute EngineのVMに当てても何ら問題ありません。
*2:2018/07/16現在のコードです
*3:例えばGAEでcronするようなジョブを作ったほうが
Kerasで作った手書き文字認識をWebアプリにしてDockerコンテナにする
タイトルのとおりです。今更ながらではありますが機械学習に足を踏み入れたWebアプリを作ってみます。
機械学習に足を踏み入れかけの人をターゲットにしています、が投げやりなので不足点は公式ドキュメント等をご参照ください。
今回は機械学習をKerasで簡単に実装し、それを使ったWebアプリの作成を一通り行います。 また動作環境をDockerのコンテナにまとめ、どこでも使えるようなイメージにします。
MNISTとは
機械学習で言うところのHello Worldのようなものです。28×28の手書き文字画像が、どの数字が書かれているか予測する問題です。
学習用に28×28の画像と、答えの数字(0~9)が与えられます。
TF.Kerasで学習する
今回はそんなに細かいことをやるわけではないので、Kerasを使います。
Kerasは(TensorFlowなどに比べ)、ニューラルネットの記述に特化している、評価関数などの初期値がいい感じに定まっているという使いやすい点があります。
さておき細かい構成や実装については、各種書籍や記事にまとめられているので割愛し以下の構成で実装しました。
Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) (None, 28, 28, 1) 0 _________________________________________________________________ conv2d_1 (Conv2D) (None, 26, 26, 32) 320 _________________________________________________________________ conv2d_2 (Conv2D) (None, 24, 24, 64) 18496 _________________________________________________________________ max_pooling2d_1 (MaxPooling2 (None, 12, 12, 64) 0 _________________________________________________________________ dropout_1 (Dropout) (None, 12, 12, 64) 0 _________________________________________________________________ flatten_1 (Flatten) (None, 9216) 0 _________________________________________________________________ dense_1 (Dense) (None, 120) 1106040 _________________________________________________________________ dropout_2 (Dropout) (None, 120) 0 _________________________________________________________________ dense_2 (Dense) (None, 10) 1210 ================================================================= Total params: 1,126,066 Trainable params: 1,126,066 Non-trainable params: 0
28×28の画像を正規化し、学習させます。学習したモデルはあとのWebアプリで使用するので保存しておきます。
#%% インポート関連 import tensorflow as tf # tf.enable_eager_execution() print(tf.__version__) print(tf.test.is_built_with_cuda()) from tensorflow.python import keras print(keras.__version__) from tensorflow.python.keras.callbacks import EarlyStopping import numpy as np from IPython.display import display import matplotlib.pyplot as plt from PIL import Image %matplotlib inline np.set_printoptions(threshold=100) #%% データを読みこみ (x_train_src, y_train_src), (x_test_src, y_test_src) = keras.datasets.mnist.load_data() print(x_train_src.shape) print(y_train_src.shape) print(x_test_src.shape) print(y_test_src.shape) # channel last前提で処理 keras.backend.image_data_format() #%% numpy配列に変換 input_shape =(28,28,1) x_train = x_train_src.reshape(x_train_src.shape[0], 28, 28, 1) x_test = x_test_src.reshape(x_test_src.shape[0], 28, 28, 1) # テストデータを正規化 x_train = x_train / 255.0 x_test = x_test / 255.0 # 分類問題なのでone-hot enc y_train = keras.utils.to_categorical(y_train_src, 10) y_test = keras.utils.to_categorical(y_test_src, 10) print(x_train.shape) print(x_test.shape) # 画像を表示、arrは28x28x1の正規化されたもの def convert_image(arr, show=True, title="", w=28, h=28): img = Image.fromarray(arr.reshape(w,h) * 255.0) if show: plt.imshow(img) plt.title(title) return img def convert_images(srcs, length, show=True, cols=5, w=28, h=28): rows = int(length / cols + 1) dst = Image.new('1', (w * cols, h * rows)) for j in range(rows): for i in range(cols): ptr = i + j * cols img = convert_image(srcs[ptr], show=False, w=w, h=h) dst.paste(img, (i * w, j * h)) if show: plt.imshow(dst) return dst plt.subplot(1,2,1) convert_images(x_train, 50,) plt.subplot(1,2,2) convert_images(x_test, 50,) plt.show() #%% モデル構築・学習 def MNISTConvModel(input_shape, predicates_class_n): inputs = keras.layers.Input(shape=input_shape) x = keras.layers.Conv2D(32, kernel_size=(3,3), activation='relu')(inputs) x = keras.layers.Conv2D(64, kernel_size=(3,3), activation='relu')(x) x = keras.layers.MaxPooling2D(pool_size=(2,2))(x) x = keras.layers.Dropout(0.25)(x) x = keras.layers.Flatten()(x) # 2D(12*12*64) -> 1d(9216) x = keras.layers.Dense(120, activation='relu')(x) x = keras.layers.Dropout(0.5)(x) predicates = keras.layers.Dense(predicates_class_n, activation='softmax')(x) return keras.models.Model(inputs=inputs, outputs=predicates) model = MNISTConvModel(input_shape=input_shape, predicates_class_n=10) model.summary() # モデルをコンパイルして実行 batch_size = 128 epochs = 20 model.compile( loss=keras.losses.categorical_crossentropy, optimizer='adadelta', metrics=['accuracy'] ) tensorboard_cb = keras.callbacks.TensorBoard(log_dir="./tflogs/", histogram_freq=1) history = model.fit( x_train, y_train, batch_size=batch_size, epochs=epochs, verbose=2, validation_data=(x_test, y_test), callbacks=[tensorboard_cb], ) #%% 学習結果の確認 plt.subplot(2,1,1) plt.plot(range(epochs), history.history['acc'], label='acc') plt.plot(range(epochs), history.history['val_acc'], label='val_acc') plt.legend(loc='center left', bbox_to_anchor=(1, 0.5)) plt.subplot(2,1,2) plt.plot(range(epochs), history.history['loss'], label='loss') plt.plot(range(epochs), history.history['val_loss'], label='val_loss') plt.legend(loc='center left', bbox_to_anchor=(1, 0.5)) plt.show() #%% 性能 scores = model.evaluate(x_test, y_test, verbose=2) print('loss', scores[0], 'accuracy', scores[1]) #%% モデルの保存 model.save('model.h5')
これで学習させると正答率99.3%でした。グラフを見ても10epochs付近で過学習に陥っているのでEarlyStopping入れても良かったかもしれません。
学習済みモデルを利用したREST APIサーバを作成する
モデルを変換したりKeras.jsを使う方法などがありますが、ユーザにモデルのダウンロードをさせるのは重荷なのでAPIとして提供します。
今回Pythonを使っているのでFlaskというライブラリを使用します。
下のようなコードhttpでjsonが返せるすぐれものです。
@app.route("/") def index(): return make_response(jsonify({"hello": "world"})
今回はkerasのモデルを読み込みpredictを使って予測値を返します。
# GPUは使わない import os os.environ["CUDA_VISIBLE_DEVICES"] = "-1" import time import tensorflow as tf from tensorflow.python import keras from flask import Flask, jsonify, abort, make_response, request, send_from_directory import numpy as np graph = tf.get_default_graph() model = None app = Flask(__name__) # 疎通確認 @app.route("/info") def index(): return make_response(jsonify({ "name": "mnist-cnn server", "time": time.ctime(), })) # 28*28の画像をPOSTで配列にして送ると、0~9の推論結果を返してくれる @app.route("/predict", methods=['POST']) def mnist(): data = request.json if data == None: return abort(400) src = data["src"] if (src == None) | (not isinstance(src, list)): return abort(400) src = np.array(src) # 正規化する src = src.astype('float32') / 255.0 src = src.reshape(-1,28,28,1) # 推論する with graph.as_default(): start = time.time() dst = model.predict(src) elapsed = time.time() - start return make_response(jsonify({ "predict" : dst.tolist(), "elapsed" : elapsed, })) # 静的ファイル公開 @app.route("/", defaults={"path": "index.html"}) @app.route("/<path:path>") def send_file(path): return send_from_directory("dist", path) if __name__ == '__main__': model = keras.models.load_model("./model.h5") app.run(host="0.0.0.0", port=3000, debug=True)
最後の行でhostを0.0.0.0にしないと外部からアクセスできないので注意します。
ついでにこのあと作成する静的ページもホスティングできるようにしています。
ここまでで、POSTすると学習済みモデルのpredictの結果が得られるようになりました。
手書きができるWebページを作成する
最後に手書きされたデータを先程のFlaskの/predict
に投げるWebページを作ります。
手書きにはhtml5のcanvasを使います。また作りやすくするためにvue.jsを読み込んで使います。
@touchmove
みたいな箇所はvue側のmethodsを呼び出してくれます。
v-model
は変数の双方向バインディング、{{ variable_name }}
はViewへの単方向バインディングです。
<!doctype html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>MNIST CNN Demo</title> <link href="style.css" rel="stylesheet"> </head> <body> <div id="container"> <h3>{{ message }}</h3> <div id="canvas_container"> <canvas id="draw_canvas" width="28" height="28" @touchmove="touch_draw" @mousemove="drag_draw" @mouseup="predict" @touchend="predict"></canvas> </div> <input v-model="pen_size" type="range" min="1" max="100" step="1"> <span>Pen Size:{{ pen_size }}</span> <button @click="clear">Clear</button> </div> <script src="vue.min.js"></script> <script type="text/javascript" src="index.js"></script> </body> </html>
次に動作を書きます。.new Vueする際にelで指定した要素に対して適用されます。
dataにはバインドする変数を定義し、methodsに使用する関数を記述します。もっと複雑なロジックや状態遷移がある場合はvuexなども検討してもいいかもしれません。
ポイントはcanvasのドラッグやタッチした際の位置を修正することと、サイズと実際に表示されるサイズが異なるためその変換を行っています。
最後にctx.getImageData
を叩いて得られたデータから、Flaskに送る配列に変換しています。(一緒にRGBからGrayScale画像にしています)
あとはpredictの結果から一番近しい数字を表示して終わりです。
const container = new Vue({ el: '#container', data: { message: '0から9の数字を書いたら識別します!', pen_size: "50", is_debug: false, }, methods: { update_message: function(str) { this.message = str; }, clear: function() { const canvas = document.getElementById('draw_canvas'); const ctx = canvas.getContext('2d'); ctx.fillStyle = 'black'; ctx.fillRect(0, 0, canvas.width, canvas.height); this.update_message('また書いてね!'); }, touch_draw: function(e) { const rect = e.target.getBoundingClientRect(); // 気分でマルチタッチ対応してみる for(const t of e.touches) { const x = t.clientX - rect.left; const y = t.clientY - rect.top; this.draw(x, y); } }, drag_draw: function(e) { if(!e.buttons) return; const rect = e.target.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; this.draw(x, y); }, draw: function(mx, my) { const canvas = document.getElementById('draw_canvas'); // 表示サイズとcanvasサイズは異なるので変換しておく const x = mx / canvas.clientWidth * canvas.width; const y = my / canvas.clientHeight * canvas.height; if (x < 0 || y < 0 || canvas.width < x || canvas.height < y) return; // 点を書く const ctx = canvas.getContext('2d'); const r = parseFloat(this.pen_size) / 100.0 * (canvas.width / 8); ctx.beginPath(); ctx.fillStyle = 'white'; ctx.arc(x, y, r, 0, Math.PI * 2, true); ctx.fill(); }, predict: function() { const canvas = document.getElementById('draw_canvas'); const ctx = canvas.getContext('2d'); // RGBA32 const img = ctx.getImageData(0,0,28,28).data; const length = img.length / 4; // とりあえず面倒なので加重平均とかはしない const src = []; for(let i = 0 ; i < length ; ++i) { const ptr = i * 4; src.push(Math.floor((img[ptr] + img[ptr + 1] + img[ptr + 2]) / 3.0)); } // flaskで作った推論機に投げる callback = this.update_message; // then内でthis参照させるのがかったるい fetch('/predict', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({'src': src }), }).then(function(res) { return res.json(); }).then(function(data) { // predict[1][20], elapsed[sec]が帰ってくるので適当に表示する const predict = data.predict[0]; let index = 0; for(let i = 0 ; i < predict.length ; ++i) { if (predict[index] < predict[i]) { index = i; } } callback(`たぶん${index}だと思う。(${Math.floor(predict[index] * 100)}% ${data.elapsed}[sec])`); }); // 確認用 // this.debug_print(src, null); }, debug_print: function(src, predict) { if (this.is_debug) { let debug = ""; for(let j = 0 ; j < 28 ; ++j) { for(let i = 0 ; i < 28 ; ++i) { debug += ` ${src[j * 28 + i].toString(16)} `.slice(-3); } debug += '\r\n'; } console.log(debug); console.log(predict); } } }, });
※面倒なのでhttpリクエストにfetchを使っていますが、vue公式としてはaxiosを推奨しています。
作成したKeras+FlaskアプリケーションをDockerコンテナにまとめる
システムのポータビリティを考え、コンテナで実行できるようにします
特にPythonのパッケージバージョン管理はいろいろと難があるので
pipのインストールやregistryなどにアップすることを考えてDockerfileからビルドします。
まず現在動作している環境のライブラリをrequirements.txtに出力します
$ pip freeze > requirements.txt
以下のようにパッケージ一覧が得られます。乱雑する場合は最小限動作する仮想環境を作り直してから同作業を行います。
absl-py==0.2.2 astor==0.7.1 bleach==1.5.0 certifi==2018.4.16 click==6.7 Flask==1.0.2 gast==0.2.0 grpcio==1.13.0 h5py==2.8.0 html5lib==0.9999999 itsdangerous==0.24 Jinja2==2.10 Markdown==2.6.11 MarkupSafe==1.0 numpy==1.14.5 protobuf==3.6.0 six==1.11.0 tensorflow==1.8.0 termcolor==1.1.0 Werkzeug==0.14.1 wincertstore==0.2
あとは、Dockerfileで公式のPythonパッケージを引っ張ってきてファイルのコピーとライブラリの追加を行います。
FROM python:3.5 # file copy COPY . /app WORKDIR /app # lib install RUN pip3 install --upgrade -r requirements.txt # run flask server EXPOSE 3000 CMD ["python", "mnist-server.py"]
EXPOSE 3000
はポート開放なので忘れずに入れておきます。
最後にコンテナをビルドします
$ docker build -t <image-name> .
実行したいときはポート開放と合わせて以下のコマンドを実行します
$ docker run -p 3000:3000 -it <image-name>
pythonで実行したときと同様に動けば成功です。
毎回ビルドと実行が面倒なのでdocker-compose.yml
ファイルも用意しておきます
mnist-server: build: ./ ports: - 3000:3000
これによってビルドと起動を$ docker-compose up
でできるようになります。
まとめ
今回は手書き文字認識をベースに、ポータビリティに優れたWebアプリ作成例を示しました。
環境構築の手間がなくなるだけかなり便利なのでぜひ試してください。
今回作成したアプリをgithubに上げておきます。
中華カメラとモニタで来客確認用ドアカメラを作る
こんにちは、今日はFPGAとは無関係ですが、Amazonで売っている中華バックカメラとモニターでドアスコープを作ってみたいと思います。
私の住んでいるアパートには来客確認用のカメラがついていないため、ドアスコープでのぞく必要があります。
宅配ならいいのですが、不要な勧誘訪問なども多く、すぐに外を確認できたらいいなあと思っていました。
ふとAmazonで車に取り付ける用のバックカメラが1000円程度で売っていることに気が付きました。
こちらです。
https://www.amazon.co.jp/dp/B0757GNZ5F/ref=cm_sw_r_tw_dp_U_x_UtsGAbMNFX3DM
サイズは2cmほどです。とても小さいですね。
(画像は後述する改造を施した後です。)
今回これと組み合わせるモニターはこちらです。
https://www.amazon.co.jp/dp/B06Y16WSWM/ref=cm_sw_r_tw_dp_U_x_YDsGAbCPJDCW9
これも1000円程度で購入できます。
これらを使ってドアスコープを作ってみたいと思います。
月並みですが、本記事の内容の実施は自己責任でお願いします。
この2つを直接接続して動作を確認します。
車用なので、ACアダプタはついていません。
その辺に転がっている12Vのアダプタを接続し動作チェックします。
ここで注意が必要なのが、センターの極性です。
センタープラスが主流ですが、一部センターマイナスのものもあるため、付属のケーブルにテスターを当て確認します。
このようなケーブルが付属しています。
テスターで測ったところ、センターはプラスでした。
まずは普通に接続し、動作チェックです。
問題ありません。
ここで注意が必要なのは、今回購入したカメラはPAL方式です。
購入したモニタはNTSC/PAL両対応なので問題ありませんが、別のモニタを使用する場合は映像信号フォーマットに注意が必要です。
そしてこのカメラをドアの外に出してみたいと思います。
が、しかしケーブルが太すぎてドアが閉まりません。。
写真を撮るのを忘れましたが、付属のケーブルは3mm程度あります。
どうしたものかと考えていたら、信号が通ればいいのだからケーブルを変えてしまえば?と気づきました。
とりあえず分解してみた
信号線は全部で5本出ていますが、必要な信号の本数は電源、GND、コンポジット信号の3本のはずです。
コンポジット信号のGNDを別の線で返していたとしても1本余ります。
そして、このケーブル、途中で緑と白の線が飛び出しています。
何かの切り替え線であることを信じて、とりあえず切断しました。
コネクタ側をチェックすると、緑と白の線は、GNDと導通していました。
処理としては、カメラ側で緑と白をGNDに落としてしまえば3本しか必要ないことになります。
切った状態で動かしてみたところ、画面が反転してバックガイドが表示されました。
それぞれ、バックガイドONと反転ONの信号のようです。
今回の用途では不要なのでカメラ側でGNDに落としました。
これでカメラからは3本のケーブルのみ出してあげればOKです。
3本ということで、3.5mmジャックを使用しようと思います。
100均で適当なイヤホンを買ってきて、ケーブルを切断します。
このように加工しました。
3.5mmコネクタは差し込み途中で別の極に触れるため、チップを+12Vにしました。
念のため通電中の3.5mm抜き差しはしないことにします。
カメラのキャップを先に通すのをお忘れなく
これでカメラのケーブルが細くなりました。
これならドアの隙間にも入りそうです。
受けのケーブル側も加工をします。
3.5mmジャックを付けます。
(スリーブを通し忘れてやり直しました。。。)
動作チェックです。問題ありません。
これで準備はすべて整いました。
ドアへのマウントですが、薄いアクリル板を使用しました。
大丈夫そうです。
これをドアの寸法に合わせて曲げてやります。
薄いので手で曲げることができます。
動作確認をしてみます。
試しに外に出て立ってみました。夜でもばっちりですね。
これで居間にいながら来客を確認することができるようになりました!
Microblaze MCSをSpartan6で動かす(ISE)
FPGAを使っていると、ソフトウェアCPUを組み込みたくなることがあります。
CPUを組み込むことで複雑な分岐の処理などを楽に作ることができます。
今回はSpartan-6 FPGAにMicroblaze MCSを実装する方法を説明します。
前提条件としてFPGAでLチカができる程度の基礎知識はあるものとします。
まずISEを開き、プロジェクトを作ったら、新規IPをプロジェクトに追加します。
IPの選択でMicroblaze MCSのコアを選択します。
IPが作成出来たらIPの設定画面が開きます。
ここではMicroblazeの設定をします。RAM容量や使用するペリフェラルなどを選択することができます。
注意する点としては、インスタンスネームの指定では実際にインスタンシエートするモジュールのパスを入力する点です。
例えば、topの下にcpu0という名前でmicroblazeをインスタンス化する場合は、Instance Hierarcical Design Nameの欄にcpu0と入力します。
次にペリフェラルの設定をします。
今回はLED点灯を試すため、GPOを使用する設定にします。
GPO1に4bitの出力ポートを作っておきます。
OKを押してIPの設定を終了します。
次にMicroblazeをインスタンス化します。
topモジュールを適当に作り、その下にMicroblazeをインスタンス化します。
メニューのView Instantiation Templateからテンプレートを取得できます。
topに図のようなコードを追加しました。
module top( input Clk, input Reset, output [3:0] GPO1 ); mb_mcs cpu0 ( .Clk(Clk), // input Clk .Reset(Reset), // input Reset .GPO1(GPO1) // output [3 : 0] GPO1 ); endmodule
ここまで来たら一度シンセサイズを行い問題なく通ることを確認します。
次に、Implement Designを正しく行うため、tclスクリプトを実行します。
TCLコンソールを使いますが、コンソールウインドウが初期設定では表示されていないので、図のようにView-Panelsから設定して表示させます。
次にtclコンソールで下記のコマンドを実行します。
source ipcore_dir/microblaze_mcs_setup.tcl
なお、このtclスクリプトはデフォルトではipcore_dirディレクトリの中にあります。
注意としては、カレントディレクトリがプロジェクトルートの状態でtclファイルを指定しないとTranslateでコケます。
ここまでやったら通常と同様にbitファイル作成まで行います。
これでハードウェアの生成は完了です。
SDKでソフトウェアを作成する
次にソフトウェアを作成します。
tclコンソールでxsdkと入力しSDKを起動します。
BSPの作成
起動したらまずはBSPを作成します。
メニューからCreate BSP Projectを選択します。
ipcore_dirの中にハードウェア情報が入ったxmlファイルがあるため、hardware specificationに指定します。
BMMファイルも同じフォルダに入っているため、同様に指定します。
OSはスタンドアロンを選択します。
ここまでの手順でボードサポート部までは完成です。
アプリケーションプロジェクトの作成
次にアプリケーションプロジェクトを作成します。
基本デフォルトで良いですが、以前作ったBSPを選択するようにします。
コーディングする
実際にLチカをさせるプログラムを記載していきます。
microblaze上のペリフェラルはすべてXIOModuleというモジュール経由で操作するようになっています。
下記のようにXIOModuleのドライバ経由でGPOを制御しました。
waitのカウンタは動作周波数に応じて適宜調整してください。
#include "platform.h" #include "xparameters.h" #include "xiomodule.h" void wait(void) { volatile int cnt = 100000; while(cnt--); } int main() { XIOModule xio; init_platform(); XIOModule_Initialize(&xio, XPAR_IOMODULE_0_DEVICE_ID); while(1){ XIOModule_DiscreteWrite(&xio, 1, 0xF); wait(); XIOModule_DiscreteWrite(&xio, 1, 0); wait(); } return 0; }
SPI Flashの使い方 (2) - Arduinoから読み書き
組み込みでデータを保持したいときに使用するSPI Flashを実際にArduinoから制御してみます。
偶然手元にあった構成で、Arduino MicroとCypress製のSPI FlashであるS25FL032*1を使用します。
この記事は以下の記事の続きとなるので、部品の機能やコマンドについての詳細はこちらをご参照願います。
結線方法
基本的には前記事を参照していただければピン機能はわかると思います。
ArduinoなどでI/O電圧が5Vである場合、レベル変換が必要になります。
Arduino | SPI Flash | 備考 |
---|---|---|
D10 | CS# | digitalWrite可能なピンであればどこでも良い |
SCK | SCK | クロック |
MOSI | IO0/SI | データ入力 |
MISO | IO1/SO | データ出力 |
- | IO2/W# | QSPIは使わず、W#も使わないためH固定 |
- | IO3/HOLD# | QSPIは使わず、HOLD#も使わないためH固定 |
まずはSPI転送とCS制御を書く
コマンドを幾つか実装する上で、まずはGPIOの制御とSPI自体の制御が必要になります。
Arduino以外のプラットフォームでも動作できるよう、ある程度汎用性をもたせるように設計します。
SPIはSPI.h
を利用して、GPIOはpinMode
, digitalWrite
で実装します。
Serialでの表示はデバッグ用に残しているもので、不要であれば削除します。
#include <SPI.h> #define FLASH_CS 10 void spi_init(){ SPI.begin(); pinMode(FLASH_CS, OUTPUT); spi_set_cs(0x1); } void spi_set_cs(uint8_t is_high) { Serial.print("#spi_set_cs is_high= "); Serial.print(is_high, DEC); digitalWrite(FLASH_CS, is_high & 0x1); Serial.print(" ... Done\n"); } void spi_transfer(const uint8_t* tx_buf, uint8_t* rx_buf, uint16_t length){ Serial.print("#spi_transfer length= "); Serial.print(length, DEC); for(uint16_t i = 0 ; i < length ; ++i){ Serial.print("."); uint8_t data = SPI.transfer(tx_buf != NULL ? tx_buf[i] : 0x0); if(rx_buf != NULL){ rx_buf[i] = data; } } Serial.print(" Done\n"); }
void spi_transfer(const uint8_t* tx_buf, uint8_t* rx_buf, uint16_t length)
はlengthで指定した分のSPI転送を行います。
その際にtx_bufが指定されていなければ0x0のダミーデータを、rx_bufが指定されていたときのみ受信データを格納するように工夫します。
Flash制御の共通的な制御を書く
どのコマンドにも共通して、コマンドを送る、アドレスを送る、ダミーバイトを送る、データを送る、データを読み取る動作が存在します。
毎回spi_transfer
を呼び出してもいいですが、面倒なのと可読性を考慮して一段ラップします。呼び出しコストが気になるようであればコンパイル時にインライン展開されるような修飾を施します。
void flash_send_cmd(uint8_t cmd){ spi_transfer(&cmd, NULL, 1); } void flash_send_addr(uint32_t addr){ const uint8_t data[] = { (addr >> 16) & 0xff, (addr >> 8) & 0xff, (addr >> 0) & 0xff, }; spi_transfer(data, NULL, 3); } void flash_send_dummy(uint16_t length){ spi_transfer(NULL, NULL, length); } void flash_read_data(uint8_t* rx_buf, uint16_t length){ spi_transfer(NULL, rx_buf, length); } void flash_write_data(const uint8_t* tx_buf, uint16_t length){ spi_transfer(tx_buf, NULL, length); }
基本的なコマンドを実装する
いよいよコマンドの実装です。データシートに記載された全種類を実装してもいいですが、冗長なので基本的な命令を一通り実装します。
メーカー間で差があるといえど全く異なるわけではないので、コマンドのニーモニックやダミーバイト数などを調節すれば他のデバイスも制御できます。
新しいコマンドを追加したい場合も既存の記述を参照すれば簡単に追加可能です。もっと高機能にしたくなりますが、高級な実装は後回しです。
//Flashのコマンドを発行(S25FL032) #define CMD_RDID (0x9f) #define CMD_READ (0x03) #define CMD_WREN (0x06) #define CMD_WRDI (0x04) #define CMD_P4E (0x20) #define CMD_P8E (0x40) #define CMD_BE (0x60) #define CMD_PP (0x02) #define CMD_RDSR (0x05) void flash_rdid(uint8_t* rx_buf, uint16_t length){ spi_set_cs(0x0); flash_send_cmd(CMD_RDID); flash_read_data(rx_buf, length); spi_set_cs(0x1); } void flash_read(uint32_t addr, uint8_t* rx_buf, uint16_t length){ spi_set_cs(0x0); flash_send_cmd(CMD_READ); flash_send_addr(addr); flash_read_data(rx_buf, length); spi_set_cs(0x1); } void flash_wren(){ spi_set_cs(0x0); flash_send_cmd(CMD_WREN); spi_set_cs(0x1); } void flash_wrdi(){ spi_set_cs(0x0); flash_send_cmd(CMD_WRDI); spi_set_cs(0x1); } void flash_p4e(uint32_t addr){ spi_set_cs(0x0); flash_send_cmd(CMD_P4E); flash_send_addr(addr); spi_set_cs(0x1); } void flash_be(){ spi_set_cs(0x0); flash_send_cmd(CMD_BE); spi_set_cs(0x1); } void flash_pp(uint32_t addr, const uint8_t* wr_data, uint16_t length){ spi_set_cs(0x0); flash_send_cmd(CMD_PP); flash_send_addr(addr); flash_write_data(wr_data, length); spi_set_cs(0x1); } void flash_rdsr(uint8_t* rx_buf, uint16_t length){ spi_set_cs(0x0); flash_send_cmd(CMD_RDSR); flash_read_data(rx_buf, length); spi_set_cs(0x1); }
その他便利関数
EraseやProgramには完了を確認するためにステータスレジスタのWIP(Write In Progress)を監視する必要があるので、確認関数を追加します。
また、データのデバッグを楽にするためのデバッグプリントも実装しておきます。
//(WIP)Write In Progressかどうか確認 uint8_t flash_is_wip(){ uint8_t data; flash_rdsr(&data, 0x1); return (data & 0x1);//WIP } //配列を表示 void data_debug_print(const uint8_t* data, uint16_t bytes){ Serial.print("+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F\n"); Serial.print("-----------------------------------------------\n"); uint16_t depth = ((bytes - 1) >> 4) + 1; for(uint16_t j = 0 ; j < depth ; ++j){ for(uint16_t i = 0 ; i < 16 ; ++i){ uint8_t d = data[i + (j << 4)]; Serial.print((d >> 4) & 0x0f, HEX); Serial.print((d >> 0) & 0x0f, HEX); Serial.print(" "); } Serial.print("\n"); } }
まずは動作確認
まずは確実に定値が読み出せるコマンドで、デバイスと正しく疎通できているか確認します。うまくいかない場合はコマンドか結線を疑います。オシロがあると楽です。
今回の場合、Read Identification(RDID)
を使えば{Manufacturer Id, Device Id, Extended Bytes}
の固定値が読み出せそうなのでこれを使います。
期待値は{0x01, 0x02, 0x15, 0x4d}
です。
検証コード
void setup() { uint8_t rx_buf[256]; spi_init(); Serial.begin(9600); while (!Serial) {} Serial.print("RDID\n"); flash_rdid(rx_buf, 4); data_debug_print(rx_buf, 4); }
結果
RDID #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 4.... Done #spi_set_cs is_high= 1 ... Done +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ----------------------------------------------- 01 02 15 4D 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
CSをLowにした後1byte(コマンド) -> 4byte(RDIDのデータ)の順にspi_transferが実行されています。
読み出した結果も先頭に注目すると期待通りの結果が読み出せています。(残りはゴミです)
直接関係ないのですが、USBを内蔵したATMELマイコンをベースにしたArduino特有の現象でSerial.begin()
の後に初期化が終わるまで待たなければならないようです。
これはSerialの読み書きを、外部ではなく内部のUSB CDCを利用していることに起因しているようです。*2
特定の領域でRead, Erase, Program, Verifyをしてみる
デバイスとの疎通は出来ているようなので、以下のシーケンスで任意のデータを書き込んでみます。
- 現在のデータを読み出し
- セクタイレースを実行 (全体削除のBulk Eraseは検証には時間がかかりすぎるため)
- 適当な値を書き込み(シーケンシャルなデータを書き込む)
- データを読み出して書き込んだ値と相違ないか確認
注意すべき点として、保持されているデータを変化させる命令にはWriteEnableを事前実行する必要があることです。
ちなみにステータスレジスタあたりから確認できます。またWriteDisable命令もあります。
検証コード
void setup() { uint8_t tx_buf[256]; uint8_t rx_buf[256]; uint32_t addr = 0x0; uint16_t length = 0x100; spi_init(); Serial.begin(9600); while (!Serial) {} Serial.print("RDID\n"); flash_rdid(rx_buf, 4); data_debug_print(rx_buf, 4); Serial.print("Read\n"); flash_read(addr, rx_buf, length); data_debug_print(rx_buf, length); Serial.print("Read after P4E\n"); flash_wren();//P4Eの前に必要 flash_p4e(addr); while(flash_is_wip()){//消去待ち Serial.print("."); } Serial.print("\n"); flash_read(addr, rx_buf, length); data_debug_print(rx_buf, length); Serial.print("PP Sequential Data\n"); for(uint16_t i = 0 ; i < length ; ++i){ tx_buf[i] = i & 0xff; } flash_wren(); flash_pp(addr, tx_buf, length); while(flash_is_wip()){//書き込み完了待ち Serial.print("."); } Serial.print("\n"); Serial.print("Read after PP\n"); flash_read(addr, rx_buf, length); data_debug_print(rx_buf, length); }
結果
一部見やすいように加工しています
RDID #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 4.... Done #spi_set_cs is_high= 1 ... Done +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ----------------------------------------------- 01 02 15 4D 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F (RDIDの結果、正しく通信できている) Read #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 3... Done #spi_transfer length= 256..(省略).. Done #spi_set_cs is_high= 1 ... Done +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ----------------------------------------------- 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F 80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F 90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF (同じコードを何度か実行しているので前のデータがそのまま見えている) Read after P4E #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_set_cs is_high= 1 ... Done (ここまでがWrite Enableコマンド、データを改変するコマンドには事前にWRENする必要がある) #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 3... Done #spi_set_cs is_high= 1 ... Done (ここがP4Eコマンド、指定したセクタのデータが全部1になる、コマンドの完了には時間がかかるためWIPを見て待つ) (ここからWIP待ち) #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 1. Done #spi_set_cs is_high= 1 ... Done (長いので省略...) #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 3... Done #spi_transfer length= 256..(省略).. Done #spi_set_cs is_high= 1 ... Done +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ----------------------------------------------- FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF (削除後のデータ、全セルの値が1なのでffで埋め尽くされている) PP Sequential Data (WREN) #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_set_cs is_high= 1 ... Done (PP) #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 3... Done #spi_transfer length= 256...(省略).. Done #spi_set_cs is_high= 1 ... Done (WIP待ち) #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 1. Done #spi_set_cs is_high= 1 ... Done Read after PP #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 3... Done #spi_transfer length= 256..(省略).. Done #spi_set_cs is_high= 1 ... Done +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ----------------------------------------------- 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F 80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F 90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF (書き込み後のデータ、正しくインクリメンタルな値になった)
ログ中に注釈したとおり、Erase後にffになったデータがPPで書き込めていることが確認できます。
全面書き換えを行う
動作確認が出来たので、全面書き換えをしてみます。
Page Programはページごとに処理して、WIP待ちをしなければならないので0x100刻みに適当なデータを作って処理します。 ページをまたがなければ分ける必要はないので、今回は考慮していませんが0x3から100byteだけ書き換えたりなどは大丈夫です。
検証コード
uint8_t tx_buf[256]; uint8_t rx_buf[256]; uint32_t length = 0x400000; /* 全面書き換え */ flash_wren(); Serial.print("Bulk Erase ..."); flash_be(); while(flash_is_wip()){ //Serial.print("."); } Serial.print(" Done\n"); //1Pageごと(256byteごと)処理をする for(uint32_t addr = 0x0 ; addr < length ; addr += 0x100){ Serial.print("PP @"); Serial.print(addr, HEX); //適当なデータを作る for(uint16_t i = 0 ; i < 0x100 ; ++i){ tx_buf[i] = (i + (addr >> 8)) & 0xff; } flash_wren(); flash_pp(addr, tx_buf, 0x100); while(flash_is_wip()){//書き込み完了待ち Serial.print("."); } Serial.print(" Done\n"); } //読み出して照合する bool is_all_match = 0x1; for(uint32_t addr = 0x0 ; addr < length ; addr += 0x100){ Serial.print("Verify @"); Serial.print(addr, HEX); flash_read(addr, rx_buf, 0x100); //結果を比較 bool is_match = 0x1; for(uint16_t i = 0 ; i < 0x100 ; ++i){ tx_buf[i] = (i + (addr >> 8)) & 0xff; if(tx_buf[i] != rx_buf[i]) { is_match = 0x0; is_all_match = 0x0; Serial.print(" Fail\n"); break; } } if(is_match){ Serial.print(" Pass\n"); } } //先頭だけでも見ておく Serial.print("\n\n\n@0x000000\n"); flash_read(0x0, rx_buf, 0x100); data_debug_print(rx_buf, 0x100); if(is_all_match){ Serial.print("Pass: spi_flash_test2()\n"); } else { Serial.print("Fail: spi_flash_test2()\n"); }
結果
(めちゃくちゃ長いため省略) Verify @3FF800 Pass Verify @3FF900 Pass Verify @3FFA00 Pass Verify @3FFB00 Pass Verify @3FFC00 Pass Verify @3FFD00 Pass Verify @3FFE00 Pass Verify @3FFF00 Pass @0x000000 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ----------------------------------------------- 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F 80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F 90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF Pass: spi_flash_test2()
動画
ArduinoからSPI Flashメモリを書き換えています。 pic.twitter.com/0UvuXtIa62
— LogiClover (@logiclover_jp) 2017年8月14日
どうやら全データ書き換えられているようです。
終わりに
コード全体は以下に示します。自己責任でお願い致します。
間違い等ございましたらご指摘いただけると幸いです。
#include <SPI.h> #define FLASH_CS 10 ///////////////////////////////////////////////////////////////////////////// //デバイスによって書き換える void spi_init(){ SPI.begin(); pinMode(FLASH_CS, OUTPUT); spi_set_cs(0x1); } void spi_set_cs(uint8_t is_high) { //Serial.print("#spi_set_cs is_high= "); //Serial.print(is_high, DEC); digitalWrite(FLASH_CS, is_high & 0x1); //Serial.print(" ... Done\n"); } void spi_transfer(const uint8_t* tx_buf, uint8_t* rx_buf, uint16_t length){ //Serial.print("#spi_transfer length= "); //Serial.print(length, DEC); for(uint16_t i = 0 ; i < length ; ++i){ //Serial.print("."); uint8_t data = SPI.transfer(tx_buf != NULL ? tx_buf[i] : 0x0); if(rx_buf != NULL){ rx_buf[i] = data; } } //Serial.print(" Done\n"); } ///////////////////////////////////////////////////////////////////////////// //Flashの制御 void flash_send_cmd(uint8_t cmd){ spi_transfer(&cmd, NULL, 1); } void flash_send_addr(uint32_t addr){ const uint8_t data[] = { (addr >> 16) & 0xff, (addr >> 8) & 0xff, (addr >> 0) & 0xff, }; spi_transfer(data, NULL, 3); } void flash_send_dummy(uint16_t length){ spi_transfer(NULL, NULL, length); } void flash_read_data(uint8_t* rx_buf, uint16_t length){ spi_transfer(NULL, rx_buf, length); } void flash_write_data(const uint8_t* tx_buf, uint16_t length){ spi_transfer(tx_buf, NULL, length); } ///////////////////////////////////////////////////////////////////////////// //Flashのコマンドを発行(S25FL032) #define CMD_RDID (0x9f) #define CMD_READ (0x03) #define CMD_WREN (0x06) #define CMD_WRDI (0x04) #define CMD_P4E (0x20) #define CMD_P8E (0x40) #define CMD_BE (0x60) #define CMD_PP (0x02) #define CMD_RDSR (0x05) void flash_rdid(uint8_t* rx_buf, uint16_t length){ spi_set_cs(0x0); flash_send_cmd(CMD_RDID); flash_read_data(rx_buf, length); spi_set_cs(0x1); } void flash_read(uint32_t addr, uint8_t* rx_buf, uint16_t length){ spi_set_cs(0x0); flash_send_cmd(CMD_READ); flash_send_addr(addr); flash_read_data(rx_buf, length); spi_set_cs(0x1); } void flash_wren(){ spi_set_cs(0x0); flash_send_cmd(CMD_WREN); spi_set_cs(0x1); } void flash_wrdi(){ spi_set_cs(0x0); flash_send_cmd(CMD_WRDI); spi_set_cs(0x1); } void flash_p4e(uint32_t addr){ spi_set_cs(0x0); flash_send_cmd(CMD_P4E); flash_send_addr(addr); spi_set_cs(0x1); } void flash_be(){ spi_set_cs(0x0); flash_send_cmd(CMD_BE); spi_set_cs(0x1); } void flash_pp(uint32_t addr, const uint8_t* wr_data, uint16_t length){ spi_set_cs(0x0); flash_send_cmd(CMD_PP); flash_send_addr(addr); flash_write_data(wr_data, length); spi_set_cs(0x1); } void flash_rdsr(uint8_t* rx_buf, uint16_t length){ spi_set_cs(0x0); flash_send_cmd(CMD_RDSR); flash_read_data(rx_buf, length); spi_set_cs(0x1); } ///////////////////////////////////////////////////////////////////////////// //その他 //(WIP)Write In Progressかどうか確認 uint8_t flash_is_wip(){ uint8_t data; flash_rdsr(&data, 0x1); return (data & 0x1);//WIP } //配列を表示 void data_debug_print(const uint8_t* data, uint16_t bytes){ Serial.print("+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F\n"); Serial.print("-----------------------------------------------\n"); uint16_t depth = ((bytes - 1) >> 4) + 1; for(uint16_t j = 0 ; j < depth ; ++j){ for(uint16_t i = 0 ; i < 16 ; ++i){ uint8_t d = data[i + (j << 4)]; Serial.print((d >> 4) & 0x0f, HEX); Serial.print((d >> 0) & 0x0f, HEX); Serial.print(" "); } Serial.print("\n"); } } ///////////////////////////////////////////////////////////////////////////// //Arduinoの適当な制御コード void spi_flash_test(){ uint8_t tx_buf[256]; uint8_t rx_buf[256]; uint32_t addr = 0x0; uint16_t length = 0x100; Serial.print("RDID\n"); flash_rdid(rx_buf, 4); data_debug_print(rx_buf, 4); Serial.print("Read\n"); flash_read(addr, rx_buf, length); data_debug_print(rx_buf, length); Serial.print("Read after P4E\n"); flash_wren();//P4Eの前に必要 flash_p4e(addr); while(flash_is_wip()){//消去待ち Serial.print("."); } Serial.print("\n"); flash_read(addr, rx_buf, length); data_debug_print(rx_buf, length); Serial.print("PP Sequential Data\n"); for(uint16_t i = 0 ; i < length ; ++i){ tx_buf[i] = i & 0xff; } flash_wren(); flash_pp(addr, tx_buf, length); while(flash_is_wip()){//書き込み完了待ち Serial.print("."); } Serial.print("\n"); Serial.print("Read after PP\n"); flash_read(addr, rx_buf, length); data_debug_print(rx_buf, length); } void spi_flash_test2(){ uint8_t tx_buf[256]; uint8_t rx_buf[256]; uint32_t length = 0x400000; /* 全面書き換え */ flash_wren(); Serial.print("Bulk Erase ..."); flash_be(); while(flash_is_wip()){ //Serial.print("."); } Serial.print(" Done\n"); //1Pageごと(256byteごと)処理をする for(uint32_t addr = 0x0 ; addr < length ; addr += 0x100){ Serial.print("PP @"); Serial.print(addr, HEX); //適当なデータを作る for(uint16_t i = 0 ; i < 0x100 ; ++i){ tx_buf[i] = (i + (addr >> 8)) & 0xff; } flash_wren(); flash_pp(addr, tx_buf, 0x100); while(flash_is_wip()){//書き込み完了待ち Serial.print("."); } Serial.print(" Done\n"); } //読み出して照合する bool is_all_match = 0x1; for(uint32_t addr = 0x0 ; addr < length ; addr += 0x100){ Serial.print("Verify @"); Serial.print(addr, HEX); flash_read(addr, rx_buf, 0x100); //結果を比較 bool is_match = 0x1; for(uint16_t i = 0 ; i < 0x100 ; ++i){ tx_buf[i] = (i + (addr >> 8)) & 0xff; if(tx_buf[i] != rx_buf[i]) { is_match = 0x0; is_all_match = 0x0; Serial.print(" Fail\n"); break; } } if(is_match){ Serial.print(" Pass\n"); } } //先頭だけでも見ておく Serial.print("\n\n\n@0x000000\n"); flash_read(0x0, rx_buf, 0x100); data_debug_print(rx_buf, 0x100); if(is_all_match){ Serial.print("Pass: spi_flash_test2()\n"); } else { Serial.print("Fail: spi_flash_test2()\n"); } } void setup() { spi_init(); Serial.begin(9600); while (!Serial) {} spi_flash_test2(); } void loop() { }
*1:2017年8月現在 新規設計非推奨となっていてS25FL-Lシリーズが推奨されています
*2:https://www.arduino.cc/en/Guide/ArduinoLeonardoMicro#toc6