Chiselを使ったRISC-Vの勉強(3. ALUの実装とIDステージの改良)
githubを巡回してみていろいろな実装を見て作戦を練っていました。
これまでRISCVの命令セットを見る限り、opcode(7bit) + funct3(3bit)で判別して適当に実装できるかなと考えていました。(10bitなら単純計算で1023命令は用意できるし)
- ALUへの演算命令、2入力への変数指定、結果の格納先(汎用レジスタ、pc、データメモリなど)指定
- データメモリ書き込みイネーブル、アドレス指定、読み込みイネーブル、アドレス指定
- CSR周りのゴニョゴニョ
- CPUストール制御信号
- 分岐制御信号
をIDステージで書けばいいかなと書き始めていました。
マイクロコードの記述方法の調査
IDステージの実装方法を調べているといろいろな実装が見つかります。opcodeから実直にimm,rs1,rs2,funct3を分けて実装しているのが、diningyo氏 (id:diningyo-kpuku-jougeki) のdirv
https://github.com/diningyo/dirv/blob/master/src/main/scala/dirv/pipeline/Idu.scala
とっても簡潔に記述されています。
氏のdirvはステージごとに綺麗に分かれていてパイプラインを組むときにとても作業がしやすそうです。実際、dirvトップモジュールはそれぞれのステージをConnectするだけの記述になっています。
しかし、いろんなrisc-v実装を見てると、マイクロコードはほとんどの実装で示し合わせたようにこんな実装(全く全てが同じではないですが、雰囲気としては同じ)をしてました。
class IDecode { val table: Array[(BitPat, List[BitPat])] = Array( LW -> List(Y, BR_N , OP1_RS1, OP2_IMI , ALU_ADD , WB_MEM, REN_1, MEN_1, M_XRD, MT_W) , LB -> List(Y, BR_N , OP1_RS1, OP2_IMI , ALU_ADD , WB_MEM, REN_1, MEN_1, M_XRD, MT_B ), LBU -> List(Y, BR_N , OP1_RS1, OP2_IMI , ALU_ADD , WB_MEM, REN_1, MEN_1, M_XRD, MT_BU ), LH -> List(Y, BR_N , OP1_RS1, OP2_IMI , ALU_ADD , WB_MEM, REN_1, MEN_1, M_XRD, MT_H ), LHU -> List(Y, BR_N , OP1_RS1, OP2_IMI , ALU_ADD , WB_MEM, REN_1, MEN_1, M_XRD, MT_HU ), …… )}
分岐制御、ALUの入力指定、IMMの種類、ALUでの演算指定、メモリ書き戻し、レジスタ書き戻し、などの指定がListで格納されています。
本家Rocket-chipなどがこの実装だから、なのはわかってたのですが、いろいろ調べてました。
lowRISCというプロジェクトがあり、メモリなどのカスタマイズができるようです。その際にこの体裁で拡張やカスタマイズを進めていくようなのでこの手順を真似しておいた方が今後新たにSoCの設計するときに有利だと思いました。
参考:Adding HW/SW support for the load and store tag instructions · lowRISC: Collaborative open silicon engineering
命令デコード(ID)ステージの実装
自分が参考するにあたって、上記のような手法でIDステージを記述しており、かつChisel3.2以降*1に対応したものはないか、さがしていました。
これが良さげ。
GitHub - ucb-bar/riscv-mini: Simple RISC-V 3-stage Pipeline in Chisel
Chisel3で記述されていて、かなりシンプルでした。これを参考に組み立てていきます。
方針としては、
- RV32Iに対応しているアセンブラのIDステージのみ移植し、いま自作しているプロジェクトにフィットするよう書き換える。
- ALUは32bit固定にして、わかりやすく記述。今後対応しきれないようであれば都度書き換え。
という感じで行くことににしました。
ALUの実装はこんな感じです。へんなところで頑張らない。足し算引き算、(排他的)論理和、論理積、シフトくらい。
ざっと見てVerilogコードがなんとなくでも想像できる形。(一部抜粋)
class ALU extends Module { val io = IO (new Bundle { val op1 = Input(UInt(32.W)) val op2 = Input(UInt(32.W)) val alu_op = Input(UInt(4.W)) val out = Output(UInt(32.W)) }) val shamt = io.op2(4,0).asUInt val w_out = Wire(UInt(32.W)) w_out := MuxLookup(io.alu_op, io.op2, Seq( ALU_ADD -> (io.op1 + io.op2), ALU_SUB -> (io.op1 - io.op2), ALU_SRA -> (io.op1.asSInt >> shamt).asUInt, ALU_SRL -> (io.op1 >> shamt), ALU_SLL -> (io.op1 << shamt), ALU_SLT -> (io.op1.asSInt < io.op2.asSInt), ALU_SLTU -> (io.op1 < io.op2), ALU_AND -> (io.op1 & io.op2), ALU_OR -> (io.op1 | io.op2), ALU_XOR -> (io.op1 ^ io.op2), ALU_COPY1 -> io.op1)) io.out := w_out }
30行足らず。わーい。
このALUをCPU側のモジュールにインスタンシエートし、入力出力をIDステージのフラグから判別してやればOK
ALUの入力セレクタは今のところこんな実装。I-type命令じゃない即値immなどは真面目に実装していないのでこれから頑張る。
// ALU OP1 selector val ex_op1 = MuxLookup(id_ctrl.alu_op1, 0.U(32.W), Seq( OP1_RS1 -> rv32i_reg(idm.io.inst.rs1), OP1_IMU -> 0.U(32.W), // DUMMY OP1_IMZ -> 0.U(32.W) // DUMMY ) ) // ALU OP2 selector val ex_op2 = MuxLookup(id_ctrl.alu_op2, 0.U(32.W), Seq( OP2_RS2 -> rv32i_reg(idm.io.inst.rs2), OP2_IMI -> idm.io.inst.imm, OP2_IMS -> 0.U(32.W) // DUMMY ) ) // ALUの入力にConnectし、ALUのオペレーションを4bit長で指示 import ALU._ val alu = Module(new ALU) alu.io.alu_op := id_ctrl.alu_func alu.io.op1 := ex_op1 alu.io.op2 := ex_op2
ALUの出力先はpcとかデータメモリとか色々あるはずですが、今回はまだ汎用レジスタだけ実装しています。
when (rf_wen === REN_1){ // 汎用レジスタ書き込みイネーブルであれば when (rd_addr =/= 0.U){ rv32i_reg(rd_addr) := alu.io.out // 汎用レジスタに書き込み }.otherwise { // rd_addr = 0 rv32i_reg(0.U) := 0.U(32.W) // ゼロレジスタが指定されていたら、強制的にゼロを書き込む(nop命令などの用途) } }
ALUを使い、かつ汎用レジスタのみで読み書きが完結する命令はI,およびR-typeの命令です。
- rd,rs1,imm(12bit)を使う命令
- rd,rs1,imm(5bit)を使う命令
- rd,rs1,rs2を使う命令
それぞれから1命令づつピックアップしてハンドアセンブルして、結果が期待された値となっているか確認。
- addi
- slli
- add
をピックアップ。
下記のように、バイナリを記述して、汎用レジスタの結果を見ます。
(中略) 0x00A00593L, // addi x11,x0,10 (x11 = 0 + 10 = 10) 0x00259093L, // slli x1,x11,2 (x1 = x11 << 2 = 40) 0x00B00613L, // addi x12,x0,11 (x12 = 11) 0x00C581B3L, // add x3,x11,x12 (x3 = x11 + x12 = 10 + 11 = 21 = 0x15)
期待される結果は、
x1 = 40(= 0x28)
x3 = 21(= 0x15)
x11 = 10(= 0x0A)
x12 = 11( = 0x0B)
です。
結果:
コード全文
コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。
git clone http://github.com/panda5mt/KyogenRV -b 0.0.9.1 --depth 1
*1:初期の頃に記述されたものはChisel2以前のバージョンで記述しているサンプルも多いため
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コードを紹介します。