lynxeyedの電音鍵盤

MBDとFPGAと車載で使うデバイスの備忘録

Chiselを使ったRISC-Vの勉強(2)

汎用レジスタ(x0-x31)の実装

RISC-V(RV32I)の汎用レジスタは32bit長で32個あります。


f:id:Lynx-EyED:20200423201049j:plain
下記Manualのpp.10(Fig2.1)参照
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

デバッグトレース

今のところ、デバッグトレースで必要なものは何かを考え、テストオブジェクトに入出力できるようにしました。

  • 汎用レジスタ (x0-x31)
    • x0は要らないと思ったのですが、論理回路の設計ミスでx0にゼロ以外が書き込めちゃったりしたのでしばらくの間監視できるようにします。アセンブラによるエラーを期待してもいいのですが、そうすると悪意あるコードからHWを守れないので。

  • PC
    • Program counterの読み書きができるようにしました。halt信号(後述)をtrueにしてから書き換え推奨。
  • halt信号
    • 前回の記事で既に実装済み。命令メモリへの書き込みや汎用レジスタの読み書き、PC書き換え、その他デバッグの時にこの信号をtrueにしてCPUを停止させます。
  • 命令メモリ

(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となります。


f:id:Lynx-EyED:20200423203344j:plain
ADDIのopcodeはb0010011、funct3はb000です。

f:id:Lynx-EyED:20200423203521j:plain
よって、addiの機械語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)
}

動作確認


f:id:Lynx-EyED:20200423205112p:plain
うん、うごいた。

コード全文

コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。

git clone http://github.com/panda5mt/KyogenRV -b 0.0.4 --depth 1 

きょうはここまで。

*1:x0はゼロレジスタなので代入先(デスティネーション)レジスタにはなれない

Chiselを使ったRISC-Vの勉強(1)

前回紹介したようにプロトタイプ用のFPGAができて, その後USB Type-C化などもいたしました。
これで電力問題が解決したため、FPGA基板からRaspiに給電かつ給電時の衝突防止までできるようFETを追加する改造もしました。


f:id:Lynx-EyED:20200420161916j:plain

さて、とても使いやすい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に記述してあります。


f:id:Lynx-EyED:20200420161633p:plain
一応動いてそう。。


ここまでのコード全文は以下で。コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。

git clone http://github.com/panda5mt/KyogenRV -b 0.0.3 --depth 1 

sbtやjava導入後、上記Githubからcloneして,make testすると動作します。
きょうはここまで。

Cyclone10LP基板ができたのでLチカ

基板到着

新型コロナの影響なのか国内の基板製造にみなさん移行しているようで、いつもより時間がかかりました。


f:id:Lynx-EyED:20200314105024j:plain
2層基板、L/S=5milです。Raspberry Pi Zeroにスタックできる設計。
特に実装エラーもなかったようで安心。基板製造前の指示で認識マークを基板本体にも追加しています。
こんな感じでラズパイにスタックして使います。とてもコンパクト。

f:id:Lynx-EyED:20200314113714j:plain
基板検査がてらラズパイと一緒にいじりました。
ラズパイ経由でFPGASRAMを書き換え、デバッグバウンダリスキャンができます。HDLコンパイルは自宅のWindowsで行い、遠隔地に仕掛けたラズパイからFPGAをコンフィグし、結果を取得するといった完全リモートでFPGAを扱うことができます。

f:id:Lynx-EyED:20200314111852p:plain
この基板は遠隔でソフトコアCPUを実装する必要がある時に活用できるかもしれません。
ロスコンパイラと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載せてて欲しかった。
ちょっとでかい。

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


f:id:Lynx-EyED:20200128002402p:plain
バーン。IOを欲張らなければ2層で引けるとはいえ、めんどくさい感じでした。結局BGA裏側はパスコンとフィルタで埋まるのであまり自由度はなく…。
8割ほど完成した図がこちら。SDRAMプログラマブルPLLのSi5351Aなどを搭載しています。

f:id:Lynx-EyED:20200128094549p:plain
電源とBGAからの配線引き出しと差動以外の配線はKiCadの自動配線に任せ、少し手直ししておしまいにしました。
3Dで様子を確認。Raspi Zeroと寸法はほぼ同じ。ピンアサインもラズパイにスタックできる配置になっています。RaspiからUrJTAGなどからコンフィグしたり、ピンアサイン通りではないので何らかのジグが必要ですが比較的容易にUSB Blaster IIを接続したりできるでしょう。

f:id:Lynx-EyED:20200215120210p:plain
FPGA(中央左寄りの大きい正方形の物体)の右横にある8pinのSOICはIoT RAMというものだそうです。(IPS6404L-SQ)
大容量の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とのジャンパは開放にしておく必要があります。

f:id:Lynx-EyED:20200215120145p:plain

国内の知り合いのメーカーさんに製造実装までお願いする予定なのでメーカーで規定されている認識マークをつけてパネライズをしました。



次回は到着したCyclone10LP基板とRaspberry Piを接続しバウンダリスキャンして遊びます。

Raspberry PiでBLE Notifyを確認する

LightBlueのLogだとつらい

前回PSoC63でBLEペリフェラルを作りました。
Terminalから文字列を入力するとBLE Notifyとして送信されるというものでした。
LightBlueで簡単に確認するのには便利ですが、センサ情報などを1日中収集するような用途には向きません。

参考サイト

www.ratoc-e2estore.com

ここのコードをほぼそのまま利用させていただきました。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接続

この時ハイバネートモードにしない方が楽だと思います。

f:id:Lynx-EyED:20190429132542j:plain:w250
ラズパイのコンソールを2つ開きます。

コンソール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

f:id:Lynx-EyED:20190320231803j:plain:h200f:id:Lynx-EyED:20190428000712p:plain:h200
2019年4月時点で価格は20ドル。

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)をクリックします
f:id:Lynx-EyED:20190428002420p:plain:h300
コンソールに書き込み中を示す

[ xx%] [############################    ] [ Programming ]

のような表示がしばらく現れたのち、
** Program operation completed successfully **という文字がIDE内コンソールに出ていれば成功です。
この時点ですでにPSoC63評価ボードはBLEペリフェラルとして機能しています。

BLE Notifyのテスト

BLEセントラル(iOSバイス)の準備

iOSバイスをお持ちの場合は、LightBlueアプリから見ることができます。

LightBlue® Explorer

LightBlue® Explorer

  • Punch Through
  • ユーティリティ
  • 無料

p6xbleという名前が見つかれば一応ここまで成功です。


f:id:Lynx-EyED:20190428005814j:plain:w150
f:id:Lynx-EyED:20190428155030j:plain:w150
f:id:Lynx-EyED:20190428160419j:plain:w150
先ほどのLightBlueで表示されたデバイス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.usbmodemxxxxxxxxxxは任意の数値になります。 
ここでTerminal.app上で任意の文字を入力し最後にリターンを押すと、
f:id:Lynx-EyED:20190428014505p:plain:w200

BLE writeのテスト

次にLightBlueアプリのPeripheralのページに戻りUUID:AAAA、0xBBBB(Properties: Write)をタップします.
右上のHEXをタップしUTF-8に変更した方がわかりやすいと思います。Write new valueをタップします。
任意の文字を入力し、最後に完了またはdoneをタップします。例としてあいうえおと入力しました。


f:id:Lynx-EyED:20190428155318j:plain:w200
f:id:Lynx-EyED:20190428160419j:plain:w200
f:id:Lynx-EyED:20190428161010p:plain:w200
f:id:Lynx-EyED:20190428161340j:plain:w200
このあいうえおをタップするたびに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のシリアルポートに入力されます。


f:id:Lynx-EyED:20190428232919p:plain:w400
シリアル入力があった場合にFreeRTOSのBLEタスクにNotificationを送信するキューを書き込みます。

// 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で送信する


f:id:Lynx-EyED:20190428234914j:plain:w400

例として、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使うのには必須)
サインアップはこちら
トークンはサインアップ後の画面の③で示されている部分です。赤ワクで囲んだ部分になります。
f:id:Lynx-EyED:20190410213929p:plain
コピーしておきます。
ラズパイのターミナルで実行しておく。

./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」を検索。
f:id:Lynx-EyED:20190410203156p:plain
incoming-webhookアプリ設定画面に移行します。
「設定を追加」をクリック

f:id:Lynx-EyED:20190410203626p:plain
次画面の「チャンネルへの投稿」で投稿したいチャネルを選択または新規作成します。
登録後、生成された「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に投稿できたら成功。
f:id:Lynx-EyED:20190410205756p:plain
うんこ絵文字が最初から入ってるなんて素敵。
何に使うのかしら。

サービスとして登録する

適当なフォルダで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してください。

余談:ラズパイのケース

Raspberry Pi 3b+になってかなり発熱が心配になってきました。
動作させているプロセスにも依存しますが、2月のまだ寒い屋内で(室内気温12,3度)、ヒートシンクを貼ったファンレス状態では

vcgencmd measure_temp
temp=70.1'C

 
となっておりました。夏にお亡くなりになるのが怖く、ファン付きのものをいくつか試すことに。


同じプロセスを走らせつつ、このケースを使用して使ったところ
f:id:Lynx-EyED:20190410220246j:plain

vcgencmd measure_temp
temp=49.7'C

と、かなり冷却効果がありました。でもヒートシンクをCPUに貼っている場合剥がさないとファンが干渉します。
で、この上部に貼ってあるRaspberry Piアクリルステッカーのせいで笛吹き現象が起こってうるさかったので剥がしました。剥がしたら1,2度下がったけど誤差なのかは不明

  • GeeekPi Raspberry Pi3b+ Case

もう一つ。前者のより安価で、すこし背が高い。ヒートシンク付き。ケースに基板を嵌合してからヒートシンクを貼ったほうがいいでしょう。

前者のケースのファンより回転数が高めに思われますが、すごく静か。
1番目のものより個人的には気に入っています。でも高さがあるのでそこは運用方法に合わせて購入でしょうかねぇ。
f:id:Lynx-EyED:20190410220901j:plain

vcgencmd measure_temp
temp=42.0'C


冷却性能も良さげでした。