Chiselを使ったRISC-Vの勉強(2)
汎用レジスタ(x0-x31)の実装
RISC-V(RV32I)の汎用レジスタは32bit長で32個あります。
https://content.riscv.org/wp-content/uploads/2017/05/riscv-spec-v2.2.pdf
まずRISC-Vの命令デコードには必須なのでこちらを用意します。
Chiselだとレジスタ宣言、ゼロ初期化まで1行でかけます。x0 = zero registerで固定ですが、めんどくさいのでx1-x31とまとめて確保しました。
val rv32i_reg = RegInit(VecInit(Seq.fill(32)(0.U(32.W)))) // x0 - x31:All zero initialized
デバッグトレース
今のところ、デバッグトレースで必要なものは何かを考え、テストオブジェクトに入出力できるようにしました。
- PC
- Program counterの読み書きができるようにしました。halt信号(後述)をtrueにしてから書き換え推奨。
- 命令メモリ
- 前回の記事で既に実装済み。
(ID)命令デコードの実装
力弱く、addiだけ実装しました。これを実装すると、上記で用意したx0-x31までのテストができるからです。
以下のようなアセンブラを記述します。
addi x1, x0, 0; // x1 = x0 + 0 (=0) addi x2, x0, 1; // x2 = x0 + 1 (=1) addi x3, x0, 2; // x3 = x0 + 2 (=2) addi x4, x0, 3; // x4 = x0 + 3 (=3) …. addi x31, x0, 1E; x31 = x0 + 0x1E (=30)
ここまで説明しておいてアレですけどアセンブラ流し込むスクリプトを記述してなかったので(おい)、、機械語で書きましょう。
AddiはRISC-Vの中でI-Typeの命令語で12bitの即値(imm)、1つの代入元(ソース)レジスタ(rs1)、1つの代入先(デスティネーション)レジスタ(rd)から構成されます。rs1はx0~x31、rdはx1~x31上にあるいずれかのレジスタ*1となります。
b????_????_????_????_?000_????_?001_0011
であれば良いことがわかります。16進数で表記すると、
0x00000093, // addi x1,x0,0 (x1 = x0 + 0 = 0) 0x00100113, // addi x2,x0,1 (x2 = x0 + 1 = 1) 0x00200193, // addi x3,x0,2 (x3 = x0 + 2 = 2) 0x00300213, // addi x4,x0,3 (x4 = x0 + 3 = 3) .... 0x01E00F93 // addi x31,x0,30
となります。うっひょ、マシン語とか超楽しいマジ無理。早くアセンブラからバイナリ流し込む機能実装しないと。
Chiselで機械語の判定はBitPatを使うと簡単にできます。
when (inst_code === BitPat("b????_????_????_????_?000_????_?001_0011")){ // ADDI = imm12=[31:20], src=[19:15], funct3=[14:12], rd=[11:7], opcode=[6:0] val dest = inst_code(11,7) // rd-pointer val imm = inst_code(31,20) // imm val src = inst_code(19,15) // src-pointer val dsrc = rv32i_reg(src) //rs when (0.U < dest && dest < 32.U){ //アドレスは1~31か? rv32i_reg(dest) := dsrc + imm } }
と言った感じです。
テストコードを書き直します。1部抜粋です。
poke(c.io.sw.halt, true.B) // CPUを停止 step(1) for (addr <- 0 to (memarray.length * 4 - 1) by 4){ poke(c.io.sw.wAddr, addr) poke(c.io.sw.wData, memarray(addr/4)) // 指定アドレスに機械語をロード println(f"write: addr = 0x${addr}%08X, data = 0x${memarray(addr/4)}%08X") step(1) } poke(c.io.sw.w_pc, 0) // プログラムカウンタをゼロに戻す=reset step(1) // fetch pc poke(c.io.sw.halt, false.B) // CPUを再開させる step(1) for (lp <- 0 to (memarray.length - 1) by 1){ val a = peek(c.io.sw.addr) val d = peek(c.io.sw.data) println(f"read : addr = 0x$a%08X, data = 0x$d%08X") // メモリ上の機械語のダンプ step(1) } step(1) poke(c.io.sw.halt, true.B) step(1) for (lp <- 0 to 31 by 1){ poke(c.io.sw.gAddr, lp) step(1) val d = peek(c.io.sw.gData) println(f"read : x$lp%2d = 0x$d%08X") //x0 ~ x31レジスタの内容をダンプ step(1) }
動作確認
コード全文
コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。
git clone http://github.com/panda5mt/KyogenRV -b 0.0.4 --depth 1
きょうはここまで。
Chiselを使ったRISC-Vの勉強(1)
前回紹介したようにプロトタイプ用のFPGAができて, その後USB Type-C化などもいたしました。
これで電力問題が解決したため、FPGA基板からRaspiに給電かつ給電時の衝突防止までできるようFETを追加する改造もしました。
さて、とても使いやすいRISCVのSCR1を紹介する予定でしたが、こちらはまた後での紹介にしようかなと思います。
なおgithubにSCR1のCyclone10LP向けの修正は置いてありますので参照ください。
GitHub - panda5mt/fpga-sdk-prj: FPGA-based SDK projects for SCRx cores
Chiselやるか
あまり気乗りはしてなかったのですが、昨今のコロナの影響で、思うように部品調達ができず仕事が従来の半分くらいの稼働率なのでChiselを勉強しています。動機としては
- UC BerkeleyでもRISC-Vの実装に使われている
- HDLのようにとりあえずシミュレーションのために合成するという手間が省けそう
- 高位合成言語ではない(抽象度は同じ)
- 自前のRISC-Vが必要になってきた
と言った感じでしょうか。
合成時間中にいろいろ閃いたことを忘れたり、ロジックが複雑になると合成時間だけでかなりもったいない。
あと、自分の開発の仕方に問題があるとは思うのですが、複雑なロジックの場合、単純なロジックから組み立ててシミュレーションするのですが、それでも結構作り込んでからシミュレーションしてしまう(FPGA実機でうごいてるし、わざわざめんどくさいとか。そんなの)。複雑化していくと指数関数的にシミュレーションも遅くなっていきます。
あと、この期に及んでフルスクラッチする理由としては入手できるRISC-V実装がバスがオリジナル(おそらく学習用)、またはAXIベース(その方が汎用性は高い)であり、SCR1でもAvalon-MMへはバスブリッジを利用していてQsysで細かい設定をしたい自分としては不便ということがあります。
またSCR1はパイプラインが最大4段となっていて、CPUストール頻度が5段に比べてかなり高い。
などなどがあります。intel PSG FPGAに実装する前提で、はじめからCPUバスを意識して実装してフルスクラッチで実装していきたい。
参考となったブログ、サイト
- Chisel handbook
https://github.com/schoeberl/chisel-book/wiki/chisel-book.pdf
- Test(PeekPokeTester)の書き方
Chiselで作ったモジュールをPeekPokeTesterでテストするまでの流れのまとめ - ハードウェアの気になるあれこれ
- 初めはここを見ながら書きました。とても丁寧なまとめ
ChiselのBundleの使い方をまとめてみる - ハードウェアの気になるあれこれ
- IFステージの実装はここを参考に
Chisel-Templateを使ってオリジナルデザインを作ってみるチュートリアル (1. デザインの作成) - FPGA開発日記
方針
基本的にCPUは以下のステージ構成を取るはずです。命令が複数存在しているCPUであることが前提ですが。。
(IF)命令フェッチ
(ID)命令デコード
(EX) 演算
(MA)メモリアクセス
(WB)レジスタ書き込み
- それぞれのステージをChiselで記述、テスト
- 上記ステージを接続
- シングルパイプラインCPUとしてテスト
- 5ステージパイプライン化
- 64bit(RV64I)化
とりあえず、命令をフェッチする部分を記述します。
今の状態ですとはっきり言ってVerilog/VHDLで記述した方が10倍速で組めそうですが、我慢して組んでいきます。上記参考ブログでも書かれていますが、本当にBundleは慣れると武器になるはず。
まず、CPUのPCとかメモリの前にインターフェースを記述しておきます。後でAvalon-MMに移行しやすいように。
一部抜粋になります。
// address channel bundle class AddressChannel extends Bundle { val req = Output(Bool()) // request signal val addr = Output(UInt(32.W)) // address (32bit) } // data channel bundle class DataChannel extends Bundle { val ack = Output(Bool()) // data is available ack val data = Output(UInt(32.W)) // data (32bit) } // HOST :read only(IMem) // HOST :read/Write(Dmem) class HostIf extends Bundle { // IO definition val r_ach = new AddressChannel val r_dch = Flipped(new DataChannel) // flipped I/O // write operation val w_ach = new AddressChannel val w_dch = new wDataChannel //(中略) } // Memory-Mapped Slave IF // Slave :read/Write(IMem) // Slave :read/Write(Dmem) class SlaveIf extends Bundle { // IO definition // read operation val r_ach = Flipped(new AddressChannel) // flipped I/O val r_dch = new DataChannel // write operation val w_ach = Flipped(new AddressChannel) // flipped I/O val w_dch = Flipped(new wDataChannel) }
基本的インターフェース(read/write)を記述し、ホスト側スレーブ側に割り当てます。
Flippedを使うとIn/Out logicが反転するので記述が楽になります。
CPU側。PCをカウントアップし、アドレスをインターフェースに与えるだけ。
こちらも一部抜粋
class Cpu extends Module { val io = IO(new HostIf) // initialization val r_addr = RegInit(0.U(32.W)) val r_data = RegInit(0.U(32.W)) val r_req = RegInit(true.B) // fetch signal val r_rw = RegInit(false.B) val r_ack = RegInit(false.B) val w_req = RegInit(true.B) val w_ack = RegInit(false.B) val w_addr = RegInit(0.U(32.W)) val w_data = RegInit(0.U(32.W)) when (io.sw.halt === false.B){ when(r_ack === true.B){ r_addr := r_addr + 4.U(32.W) // increase program counter } } // (中略) io.r_ach.addr := r_addr io.r_ach.req := r_req // write process io.w_ach.addr := w_addr io.w_dch.data := w_data io.w_ach.req := w_req //io.w_dch.ack := w_ack // read process r_ack := io.r_dch.ack r_data := io.r_dch.data }
メモリ側は割愛。必要であればこの記事最後にあるGithubからCloneしてください。
トップモジュールでCPUとmemoryをコネクトします。
class CpuBus extends Module { val cpu = Module(new Cpu) val memory = Module(new IMem) //(中略) // Read memory memory.io.r_ach.req <> cpu.io.r_ach.req memory.io.r_ach.addr <> cpu.io.r_ach.addr cpu.io.r_dch.data <> memory.io.r_dch.data cpu.io.r_dch.ack <> memory.io.r_dch.ack // write memory memory.io.w_ach.req <> cpu.io.w_ach.req memory.io.w_ach.addr <> cpu.io.w_ach.addr memory.io.w_dch.data <> cpu.io.w_dch.data cpu.io.w_dch.ack <> memory.io.w_dch.ack }
この後、テストオブジェクトからメモリへダミーデータを与え、CPUがフェッチできているか確認します。
// CPUを停止させ、メモリに書き込む poke(c.io.sw.halt, true.B) // メモリへ書き込み for (addr <- 0 to (memarray.length * 4 - 1) by 4){ poke(c.io.sw.wAddr, addr) poke(c.io.sw.wData, memarray(addr/4)) step(1) } step(1) poke(c.io.sw.halt, false.B) // CPUを再開させる for (lp <- 0 to (memarray.length - 1) by 1){ val a = peek(c.io.sw.addr) val d = peek(c.io.sw.data) println(f"addr = 0x$a%08X, data = 0x$d%08X") // メモリアドレス、データを16進表記 step(1) }
動作確認。make test
で動作するようMakefileに記述してあります。
ここまでのコード全文は以下で。コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。
git clone http://github.com/panda5mt/KyogenRV -b 0.0.3 --depth 1
Cyclone10LP基板ができたのでLチカ
基板到着
新型コロナの影響なのか国内の基板製造にみなさん移行しているようで、いつもより時間がかかりました。
特に実装エラーもなかったようで安心。基板製造前の指示で認識マークを基板本体にも追加しています。
こんな感じでラズパイにスタックして使います。とてもコンパクト。
ラズパイ経由でFPGAのSRAMを書き換え、デバッグバウンダリスキャンができます。HDLコンパイルは自宅のWindowsで行い、遠隔地に仕掛けたラズパイからFPGAをコンフィグし、結果を取得するといった完全リモートでFPGAを扱うことができます。
クロスコンパイラとJTAGデバッガをラズパイにインストールしておき、
- 低消費電力要求時:動作周波数を下げたソフトコアCPU、最低限のペリフェラル
- 処理性能要求時:高速ソフトコアCPUと差動バス
などと状況に応じた機能を遠隔でコンフィグするなどの動作ができます。その後ラズパイ上のコンパイラ(もちろん遠隔からでも)からバイナリを流し込めます。
全日本Lチカ人権宣言
基板を作ってもLチカができないと人権を剥奪されるおそれがあるらしいようなのでLチカ人権宣言を行いました。
一番簡単なのはHDLでクロックを分周して下位のビットをLEDを接続した端子に出力する方法です。
めんどくさいので今回はやりません。ぼくFPGAとかHDLとかわかんないし。
Raspberry Pi ZeroからCyclone10LPへバウンダリスキャンレジスタからLED端子に1/0を書き込みます。
ラズパイにUrJTAGを使います。GPIOを使うビルドが必要になるので、以前のブログの方法でビルドするといいと思います。
Raspberry Pi 3でUrJTAGを使う - lynxeyedの電音鍵盤
インテル(アルテラ)のFPGAではJTAGモードではTDO/TDI/TCK/TMSの4端子を使います。それぞれがラズパイのGPIOに接続されている必要があります。
UrJTAGにラズパイのどの端子が、FPGAのTDO/TDI/TCK/TMSのどこに対応するか伝えます。
なおラズパイの40pinの端子番号ではなく、マイコン(BCM283x)のどのGPIOに対応しているか、で伝えます。
今回は
TDO=20, TDI=21, TCK=26, TMS=19
に対応して接続しています。ラズパイのピン番号とBCM283xの対応が知りたい時は、以下のようにします。
sudo apt update sudo apt install python3-gpiozero $pinout
UrJTAGに認識させます。
sudo jtag cable gpio tdo=20 tdi=21 tck=26 tms=19 detect
おそらく、以下のメッセージになると思います。
IR length: 10 Chain length: 1 Device Id: 00000010000011110011000011011101 (0x020F30DD) Manufacturer: Altera (0x0DD) Unknown Part! (001000001111001) (/usr/local/share/urjtag/altera/PARTS)
デバイスが不明です。BSDLファイルが必要になります。
インテルのページから取得できます。必要なのはCyclone10LP:10CL025U256のBSDLファイルです。
Intel Cyclone 10 LP Device BSDL Files
UrJTAGへBSDLファイルを導入する方法は多くのブログで取り上げられているので扱いません。
参考になるブログ
BSDLファイルを導入できると、先程のメッセージが変わると思います。
IR length: 10 Chain length: 1 Device Id: 00000010000011110011000011011101 (0x020F30DD) Manufacturer: Altera (0x0DD) Part(0): 10CL025U256 (0x20F3) Stepping: 0 Filename: /usr/local/share/urjtag/altera/10cl025u256/10cl025u256
さてLチカです。バウンダリスキャンレジスタにピンの状態を書き込んで点滅させます。
この基板ではLEDはCyclone10LPのIO84(PIN_L15)/IO85(PIN_L16)に接続されています。
先ほどのBSDLを見ると、
--I/O Pins ......(中略), "IOL16 : L16 , IOL15 : L15 , IOK16 : K16 , ..............
とありました。IOL15,IOL16の状態をバウンダリスキャンレジスタ経由で書き換えます。
以下のスクリプトをhoge.txtという名前で保存します。
cable gpio tdo=20 tdi=21 tck=26 tms=19 detect instruction EXTEST shift ir set signal IOL15 out 0 set signal IOL16 out 1 shift dr usleep 500000 shift ir set signal IOL15 out 1 set signal IOL16 out 0 shift dr usleep 500000 shift ir set signal IOL15 out 0 set signal IOL16 out 1 shift dr usleep 500000 shift ir set signal IOL15 out 1 set signal IOL16 out 0 shift dr usleep 500000 shift ir set signal IOL15 out 0 set signal IOL16 out 1 shift dr usleep 500000 shift ir set signal IOL15 out 1 set signal IOL16 out 0 shift dr usleep 500000 shift ir shift ir set signal IOL15 out 1 set signal IOL16 out 1 shift dr
以下のコマンドで実行します。
sudo jtag hoge.txt
動作風景
youtu.be
次回
RISC-V界隈で完成度がすごく高く、フリーで商用利用OKとホットなSyntacore社のRISC-VコアSCR1をCyclone10LPに移植したので、紹介いたいます。
お楽しみに。
Cyclone10 LP基板をつくる
プロトタイピング環境の刷新
最近基板を作るのも、組み込みや車載の仕事もいろいろ残念なトラブル続きで最小限の一部の知り合いに絞っていたのですが、
そろそろ身の回りの「ラピッドプロトタイプするときのデバイス」のアップデートをしなきゃと思い立ち、開発の合間に「使い捨て」できるボードを設計していました。
USB/WiFi/Subギガ/BLEなどは新しいデバイスがどんどんリリースされているわけですが、オールインワンデバイスを使うとマイコンペリフェラルのお勉強からやり直しだったりとしんどかったので、まずその辺りの設計スタンスの確認。
「機能、概念試作」がおおいのでそこで手間取らないようなものを考えます。
- WiFi/BLE/USBはラズパイで完結させてしまい、どうしても必要になったら組み込みWiFi/BLE/USBデバイスを選定する
- なのでラズパイでできるところは頑張らない(移植できる余地は残しておく)
- intel FPGAのデバッグ環境が充実しているのでマイコンはNiosII/f
- あまりHDLを頑張って書かなくても良いようにする(プロトタイピングの時間短縮のため)
- 簡単な画像処理ができるようSDR SDRAMとSPI SRAMが欲しい。カメラIFなどは別にラズパイにやらせて問題ない気もする。
- RF関連の試作がよくあるのでプログラマブルなPLLを載せておく
- 差動を8ch以上出しておく(もちろん差動じゃない用途にも使えるようにしておく)
- それ以外に汎用GPIOを4chくらい出しておく
- 小さめがいい
「使い捨て」にするためには(壊れない限り処分しませんが)、なるべく基板の製造コストも下げないといけません。
先行デザインの調査
MAX10はCQ出版誌でも取り上げられたせいかホビーでよく使われるようになってきたように見受けられますが、業務プロダクトとしてはCyclone IV派生のCyclone10が多く使われている印象です。プロト用基板もいろいろ豊富です。
- インテル公式
https://www.intel.co.jp/content/www/jp/ja/programmable/products/boards_and_kits/dev-kits/altera/cyclone-10-lp-evaluation-kit.html
プログラマブルPLL搭載。
HyperRAMを搭載しているのですが、商用には別途IPの購入が必要。
SDR SDRAM載せてて欲しかった。
ちょっとでかい。
- Arduino MKR Vidor4000
https://store.arduino.cc/usa/mkr-vidor-4000
小さい。ピンアサインがArduino MKR互換。比較的安価
SDRAMもついている。WiFiいらないな。。
マイコン-FPGA間のコミュニケーションAPIが色々揃ってて便利(実態はSPI2Avalon-MMなのかな?)。ただ、FPGAを直接コンフィグすると不便そうなのでやめ。
- Trentz electronic CYC1000
https://shop.trenz-electronic.de/en/Products/Trenz-Electronic/CYC1000-Intel-Cyclone-10/
ちいさくて良い。とても安価。ピンアサインはArduino MKR互換。SDRAMついてる。Quartus Primeから直接認識できるArrow USB Programmerなるものが搭載されている。便利そうだけど、USB Blaster使いたい時に切り離すのがめんどくさそう。
どれも魅力的な部分はあったのですが、
をすべて満たすものがありませんでした。Trentzは迷いましたが、トレンツ日本(代理店?)がレスポンス遅いという関係者からのタレコミもあり。トレンツの注文が取れんツ。(さむ
設計
プロトタイプに適したボードがないので作ります。
EQFP144デバイスを使おうと以前に調査したのですがSDRAM接続が難しかったので、U256のBGAに。
参考資料:
Cyclone10LPの各々のピンの扱い方など
https://www.intel.co.jp/content/dam/altera-www/global/ja_JP/pdfs/literature/dp/cyclone-10/pcg-01021-j.pdf
パッケージごとのピンアサイン
https://www.intel.co.jp/content/www/jp/ja/programmable/support/literature/lit-dp.html
8割ほど完成した図がこちら。SDRAM、プログラマブルPLLのSi5351Aなどを搭載しています。
3Dで様子を確認。Raspi Zeroと寸法はほぼ同じ。ピンアサインもラズパイにスタックできる配置になっています。RaspiからUrJTAGなどからコンフィグしたり、ピンアサイン通りではないので何らかのジグが必要ですが比較的容易にUSB Blaster IIを接続したりできるでしょう。
大容量のQSPI RAMで便利そう。
下図は基板裏側。ラズパイと接続時には意識する必要はないですが、
VccIO=Vin
のジャンパを短絡させることによって96boardsやSpresenceボードなど3.3Vではないロジックと通信ができます。Vinから入力したロジック電圧で駆動します。(Vin=1.5 V / 1.8 V / 2.5 V / 3.0 V / 3.3 V)この時VccIO=3V3
とのジャンパは開放にしておく必要があります。国内の知り合いのメーカーさんに製造実装までお願いする予定なのでメーカーで規定されている認識マークをつけてパネライズをしました。
次回は到着したCyclone10LP基板とRaspberry Piを接続しバウンダリスキャンして遊びます。
Raspberry PiでBLE Notifyを確認する
LightBlueのLogだとつらい
前回PSoC63でBLEペリフェラルを作りました。
Terminalから文字列を入力するとBLE Notifyとして送信されるというものでした。
LightBlueで簡単に確認するのには便利ですが、センサ情報などを1日中収集するような用途には向きません。
参考サイト
ここのコードをほぼそのまま利用させていただきました。MTUの文字数を変更したくらいでしょうか
ラズパイのセットアップ(bluepy)
ラズパイ上で行う作業です
Python3は導入済みといたします。
sudo apt install cu sudo pip3 install bluepy nano ble_notify_central.py
コードを書きます。
#!/usr/bin/python3 # -*- coding: utf-8 -*- Cy63ble Button Event Notification import sys import time from bluepy.btle import * class ControlCy63ble: def __init__(self, mac): self._data = {} try: self.p = Peripheral(mac, ADDR_TYPE_PUBLIC) self.p.setDelegate(NotificationDelegate()) self.p.setMTU(256) print('Cy63ble connected !') except BTLEException: self.p = 0 print('Connection to Cy63ble failed !', mac) raise def _enableNotification(self): try: # Enable notification print('Notifications enabled') except BTLEException as err: print(err) self.p.disconnect() def _disableNotification(self): try: # Disble notification print('Notifications disabled') except BTLEException as err: print(err) self.p.disconnect() def monitorCy63ble(self): try: # Enable notification self._enableNotification() # Wait for notifications print('Waiting for button pushed 180 second') while self.p.waitForNotifications(180.0): # handleNotification() was called continue print('Notification timeout') self._disableNotification() except: return None def disconnect(self): self.p.disconnect() class NotificationDelegate(DefaultDelegate): def __init__(self): DefaultDelegate.__init__(self) def handleNotification(self, cHandle, data): try: if cHandle == 0x10: print('data :',data.decode()) else: print('handle=', cHandle,":", data) except BTLEException as err: print(err) self.p.disconnect() # main program if __name__== '__main__': print("Cy63blenotify start") argvs = sys.argv argc = len(argvs) if (argc < 2): print("Require Bluetooth address [XX:XX:XX:XX:XX:XX]") quit() Cy63ble_mac_addr = argvs[1] myCy63ble = ControlCy63ble(Cy63ble_mac_addr) print("Cy63ble found :",Cy63ble_mac_addr) myCy63ble.monitorCy63ble()
保存したら
chmod +x ble_notify_central.py
PSoC63ボードをRaspberryPiにUSB接続
この時ハイバネートモードにしない方が楽だと思います。
コンソール1つ目
cu -l /dev/ttyACM0 -s 115200 Connected. ....
/dev/ttyACM0
は /dev/ttyAMA0
、 /dev/ttyUSB0
、/dev/ttyACM1
などの場合もあります。
コンソール2つ目
./ble_notify_Central.py 00:A0:50:00:00:00 Cy63blenotify start Cy63ble connected ! Cy63ble found : 00:A0:50:00:00:00 Notifications enabled Waiting for button pushed 180 second ...
ここの 00:A0:50:00:00:00
は前回プログラムしたPSoC63基板のBLEデバイスのMACアドレスです。
コンソール1つ目に戻ってなにか文字を打ち込み、最後にリターンキーを押します。(例えばtesttesttest
)
コンソール2つ目をみると、こうなっています。
data : testtesttest
できました!おしまい。
PSoC6とModusToolbox IDE v1.1でBLEを試す
前置き
なんだかんだでPSoC63もわりと使えるかもしれないって思ってもらえれば幸い。
補足
ソースコードを逐一追って細かく解説はしません。IDEのインストールとか使い方はのりたんさんがQiitaで手取り足取り解説してくださっています。v1.0の解説だけど、v1.1でもそんなに変わりません。しらんけど。(おい
この記事でできること
- PSoC63ボードでBLE peripheralを作り notify / writeができるようになる。
- 外部からのシリアル入力をBLE notifyとしてセントラルに送ることができる
では参りましょう。
PSoC63基板の準備
プロジェクト一式
git cloneします
git clone https://github.com/panda5mt/psoc63_ble_notify_rtos.git
ModusToolbox IDEv1.1を起動しcloneしたプロジェクトをロードします。
評価ボード
CY8CPROTO-063-BLE
www.cypress.com
基板の書き込み用ファームウェアが古い場合があるので、最新のものに書き換えます。SW3(上の右側写真、親指で押している部分)を押しながらUSBに接続。
(以下はMacの場合です)
Terminal.appを起動し、fw-loaderでアップデートを行います。
/Applications/ModusToolbox_1.1/tools/fw-loader-2.1/bin/fw-loader --update-kp3
ModusToolboxにもどり、左下の[プロジェクト名] Program (KitProg3)
をクリックします
コンソールに書き込み中を示す
[ xx%] [############################ ] [ Programming ]
のような表示がしばらく現れたのち、
** Program operation completed successfully **
という文字がIDE内コンソールに出ていれば成功です。
この時点ですでにPSoC63評価ボードはBLEペリフェラルとして機能しています。
BLE Notifyのテスト
BLEセントラル(iOSデバイス)の準備
iOSデバイスをお持ちの場合は、LightBlueアプリから見ることができます。
p6xble
という名前が見つかれば一応ここまで成功です。
p6xble
をタップしてBLE接続しましょう。Notifyの確認をします。UUID:BBBB、0xCCCC(Properties: Notify)
をタップします。右上のHEX
をタップしUTF-8
に変更した方がわかりやすいと思います。Listen for notifications
をタップしてNotification受信待ちにしておきます
BLEペリフェラル(PSoC63)の準備
Terminal.appに戻ります。
sudo cu --parity=none --nostop --line /dev/tty.usbmodemxxxxx --speed 115200 Password:(パスワードを入力する) Connected.
tty.usbmodemxxxxx
のxxxxxは任意の数値になります。
ここでTerminal.app上で任意の文字を入力し最後にリターンを押すと、
BLE writeのテスト
次にLightBlueアプリのPeripheralのページに戻りUUID:AAAA、0xBBBB(Properties: Write)
をタップします.
右上のHEX
をタップしUTF-8
に変更した方がわかりやすいと思います。Write new value
をタップします。
任意の文字を入力し、最後に完了
またはdone
をタップします。例としてあいうえお
と入力しました。
あいうえお
をタップするたびにTerminalにWriteされた内容が反映されます。
Info : BLE - GATT write request Info : BLE - GATT read request write value = あいうえお Info : BLE - GATT write request Info : BLE - GATT read request write value = あいうえお ....
とりあえず動作確認はできました。
仕組み
Terminal.appからの入力(stdin)は評価ボード上のUSBシリアルを経由し、PSoC63のシリアルポートに入力されます。
// main.c,248行目付近 ble_commandAndData_t bleCommand = {.command = SEND_NOTIFICATION, .data=str }; xQueueSend(bleCommandDataQ, &bleCommand, 0u);
また、BLEタスクでWriteがあった場合は直ちにUSB-UART経由で出力(stdout)しています
// ble_task.c,355行目付近 case CY_BLE_EVT_GATTS_WRITE_REQ: { writeReqParameter = *(cy_stc_ble_gatts_write_cmd_req_param_t*)eventParam; DebugPrintf("write value = %s \r\n", writeReqParameter.handleValPair.value.val); }
応用
USB-UARTではなく別デバイスからの入力データをNotifyで送信する
例として、PSoC63のP9.0からのシリアル入力があった場合にNotifyを送信することができるようにしてあります。
ボーレートは38400baudです。先ほどとボーレートが異なりますので注意。
NMEAシリアル出力機能を持っているGPSモジュールなどの入力に使えるとおもいます。
// stdio_user.h, 157行目付近 #define IO_STDIN_UART KIT_UART_HW // この行のコメントを外す //#define IO_STDIN_UART UART_STDIO // この行をコメントにする
コンパイル後、書き込みをします。
Hibernateモードを有効化する
BLEペリフェラルはフィールドの要求に応じて、動作しない時電池の消費を極限まで抑える必要があります。
PSoC63もいくつかの消費電力を抑える機能を持っており、その一つがハイバネートモードです。
ほとんどのリソースを停止します。今回のコードではBLEセントラルからの接続が1分以上ない場合、基板上のスイッチSW2が押されるまで、Hibernateに遷移します。
この機能を有効化するには、以下のようにします。
// ble_task.c, 59行目付近 #define HIBERNATE_ENABLE 1 // ここを1にする
BLEセントラルからの接続を待つ待ち時間を変更したい場合は、そのすぐ上の行を変更します
// ble_task.c, 58行目付近 define TIMEOUT_INTERVAL pdMS_TO_TICKS(2 * 60000u) // 2分待つように変更
とこんな感じです。ちょっと長くなったのでここまでにします。
お疲れ様でした。
次回
BLE NotifyをLightBlueで確認するのはデータ量が増えるとすこし面倒です。
次回はRaspberry PiをBLEセントラルにして、PSoC63からのNotifyのデータをテキストや標準出力に表示するPythonコードを紹介します。
Raspberry Piでngrokのtcpアドレスをslackへ通知する
NAT越えしたい
自分の家に設置したラズパイにsshで入るならしかるべき方法で公開し、公開鍵設定やVPNを張るなどをして入れるとおもいますが、IoTセンサノードとして屋外で使う場合はモバイルルータなどで接続することが多いかもしれません。
モバイルルータが固定IPを持っていてNATトラバーサルできる契約のもありますが、そうでない時も多い。
できるものもオプション扱いでお金が別途かかる。もちろん常用するならIIJモバイルbiz+とかがいいと思いますが、そうでない時の方法です。
エージェントの選定
いろいろ探しました。いいなと思ったのが以下
Pulsewayはちょっとterminalの出来が悪すぎ…。というかそういう用途ではないですね。
Fluentdで端末のパラメータを監視したりするような作業をお手軽にできる感じがいいですね。
ngrokをNode.jsで使う準備
今回はngrokを使いました。超便利。ですが、起動のたびにアドレスとポートが変更されてしまいます。
アドレスを確認する手段の一つとしてSlackに自身のngrokアドレスを投稿させるコードを記述し、Systemdサービスとして登録します。
Node.jsを使います。ラズパイにサクサク入れていきます。
nohupはhupシグナルがngrokプロセスに送られないように使っています。
他にいい方法があれば教えてください。
sudo apt update
sudo apt install nohup
wget http://node-arm.herokuapp.com/node_latest_armhf.deb
sudo dpkg -i node_latest_armhf.deb
2020年2月4日追記:上記の方法ではできなくなりました。下記の方法に修正いたします。sudo npm cache clean
のところでエラーが出ても気にせず続けてしまいましょう。
sudo apt update sudo apt install -y nodejs npm sudo npm cache clean sudo npm install npm n -g sudo n latest mkdir -p ~/node_app/sendslack_ngrok cd !$ nano package.json
ここでpackage.jsonを作っておきます。下記内容で構いません。
{ "name": "sendslack_ngrok", "description": "sendslack_ngrok", "version": "1.2.3", "private": true, "scripts": { "start": "node app.js" } }
保存します。
npm install ngrok node-slack os --save
Ngrokの設定
www.npmjs.com
ngrokは無料で十分使えますが、アカウントを取得しないとトークンがもらえません。(tcp使うのには必須)
サインアップはこちら。
トークンはサインアップ後の画面の③で示されている部分です。赤ワクで囲んだ部分になります。
コピーしておきます。
ラズパイのターミナルで実行しておく。
./node_modules/ngrok/bin/ngrok authtoken this-is-your-ngrok-auth-token
ホームディレクトリに.ngrok2/ngrok.ymlが生成されます。
あとで記述するthis-is-your-ngrok-auth-tokenの部分です。
Slackのアプリ設定
投稿したい専用チャネルを作るか自分へのDMとしたほうがいいでしょう(この記事の最後まで実施すると、起動するたびにSlackへ投稿します。#generalとかに投稿すると結構うざいです)。Slackアプリからは歯車のマーク -> アプリを追加する。Webからは自分のワークスペースにログイン後 https://{your-company-workspace}.slack.com/apps
で「incoming webhook」を検索。
incoming-webhookアプリ設定画面に移行します。
「設定を追加」をクリック
次画面の「チャンネルへの投稿」で投稿したいチャネルを選択または新規作成します。
登録後、生成された「Webhook URL」をコピーしておく。あとで記述するthis-is-your-slack-incoming-webhook-addressの部分です。
webhookコードの記述
さて、準備が整いました。コードを書いていきます
今回はapp.jsというファイルに記述します。
cd ~/node_app/sendslack_ngrok nano app.js
const Slack = require('node-slack'); const ngrok = require('ngrok'); var slack = new Slack(' this-is-your-slack-incoming-webhook-address '); ngrok_connect().then(url => { console.log('URL : ' + url); var message = 'tcp url: ' + url; slack.send({ text: message, username: 'raspibot', icon_emoji: ":poop:" }); }); // ngrokを非同期で起動 async function ngrok_connect() { await ngrok.authtoken(' this-is-your-ngrok-auth-token '); let url = await ngrok.connect({proto:'tcp',port:22}); // このコマンドを実行するのにトークンが必須 return url; }
いったんサービスとして登録する前に動作確認をします
cd ~/node_app/sendslack_ngrok npm start
こんな感じでSlackに投稿できたら成功。
うんこ絵文字が最初から入ってるなんて素敵。
何に使うのかしら。
サービスとして登録する
適当なフォルダでngroksend.serviceというファイルを作ります。
なお下記例ではホームディレクトリが/home/pi/になってます。
cd ~/ nano ngroksend.service
下記を記述します。
[Unit] Description=ngrokSend After=syslog.target [Service] Type=simple WorkingDirectory=/home/pi/node_app/sendslack_ngrok/ ExecStart=/usr/bin/nohup npm start TimeoutStopSec=5 StandardOutput=null [Install] WantedBy = multi-user.target
保存後、systemdに登録します。
sudo mv ngroksend.service /etc/systemd/system/ sudo systemctl start ngroksend.service sudo systemctl enable ngroksend.service
動作が確認できたらsudo rebootしてください。
参考
ngrok - npm
Raspberry Piをngrokで公開する - Part1: Expressをpm2から起動する - Qiita
Raspberry Piでプログラムを自動起動する5種類の方法を比較・解説(記事移動済み)
余談:ラズパイのケース
Raspberry Pi 3b+になってかなり発熱が心配になってきました。
動作させているプロセスにも依存しますが、2月のまだ寒い屋内で(室内気温12,3度)、ヒートシンクを貼ったファンレス状態では
vcgencmd measure_temp temp=70.1'C
となっておりました。夏にお亡くなりになるのが怖く、ファン付きのものをいくつか試すことに。
- Eleduino Raspberry Pi Case
同じプロセスを走らせつつ、このケースを使用して使ったところ
vcgencmd measure_temp temp=49.7'C
と、かなり冷却効果がありました。でもヒートシンクをCPUに貼っている場合剥がさないとファンが干渉します。
で、この上部に貼ってあるRaspberry Piアクリルステッカーのせいで笛吹き現象が起こってうるさかったので剥がしました。剥がしたら1,2度下がったけど誤差なのかは不明
。
- GeeekPi Raspberry Pi3b+ Case
もう一つ。前者のより安価で、すこし背が高い。ヒートシンク付き。ケースに基板を嵌合してからヒートシンクを貼ったほうがいいでしょう。
前者のケースのファンより回転数が高めに思われますが、すごく静か。
1番目のものより個人的には気に入っています。でも高さがあるのでそこは運用方法に合わせて購入でしょうかねぇ。
vcgencmd measure_temp temp=42.0'C
冷却性能も良さげでした。