- 背景
- 実現方法
- ロジアナ化するための必要な要素
- ロジアナの仕様決め
- RTLの作成
- 合成制約ファイル .xdcの準備
- Vivadoでの合成作業
- 終わりに...(記事は次回へ繋げますけど...)
- vivadoオペレーションのスクリーンショット集
- 参考にしたもの
背景
タイトルの事を行おうと思ったきっかけ
前回、ILI9486のLCDを扱ったわけですが、LCDをドライブする信号を観測しようと格安ロジアナを使ってプローブしましたが、残念ながらサンプリング周波数が観測信号の速度より遅いため、観測が出来ませんでした。
高速なロジアナは高いですし、ロジアナを置くスペースも我が家にはありません。
なので、手持ちのPYNQ FPGAボードをロジアナ化しようと思いました。
実現方法
XilinxのFPGAはILA(integrated Logic Analyzer)と呼ばれる内蔵ロジックアナライザを持っています。
これを使って回路のデバッグが行えます。
japan.xilinx.com
ILAにクロックを与え、ILAに信号をつなげれば、vivadoから信号を観測可能になります。ILAに供給されるクロックで信号がサンプリングされます。またサンプリングデータはFPGA内のBRAMへ格納されます。
サンプリング開始トリガとして、信号のエッジ検出だけでなく、データの条件が成立した時など色々決められます。
また、サンプリング開始トリガ位置から指定したサイクル数分、過去のデータも取り込めます。この機能により事象が発生する少し前の状態を見たいという時に使えます。
取り込んだデータはJTAG経由でvivadoへ転送されてvivadoのviewerで波形として観測が可能になっています。
ロジアナ化するための必要な要素
FPGAボードをロジアナ化というアイデアは割と思いつくと思います。
僕が所有しているPYNQ-Z1というFPGAボードは豊富な数のGPIOが実装されていますので、GPIOに入ってきた信号をFFで取り込んで、内蔵RAMとかSDRAMへ取り込んだデータを書き込んでいけば多数の信号を同時に取り込むことが可能です。これだけで複数chのロジックアナライザになります。
しかしロジックアナライザはこれだけで成立しません。冒頭でILAの特徴を簡単に記載しましたが、ロジック信号を表示するviewer,取り込み開始のトリガを決められる仕掛けを用意するなど、信号の取り込み以外の部分での出来が重要だと思います。
こういったところを自作するのは大変です。ILAはこの辺りの機能もありますので、この機能をそのまま使います。
というわけで、FPGAをロジックアナライザへ仕立て上げるのにこちらが行う事は以下の作業だけになります。
- ロジックアナライザのチャネル数を決める
- ロジックアナライザの端子としてPYNQ-Z1ボードのどの端子を使うか決める。
- MMCMを使ってILAのサンプリングクロックを作る(ZYNQに入力される外部クロック125MHzでもよいが,せっかくなんでMMCMで高速クロックを作ってこれをサンプリングクロックとする方が、高性能になる)
- 端子に入力された信号を一旦FFで受ける回路を作成
- 上記FFの出力をILAのprobe入力とする
これだけで、もはや最低限は完成です。あぁ、なんて簡単なのでしょう(しかし実際は色々苦労しました)。
更に以下の機能も追加する事にします。
- PYNQ-Z1ボードにあるプッシュボタンをトリガとする
ILAの機能として、取り込んだ信号でサンプル開始トリガを作る事ができますが、外部要因(操作者の指)でのトリガ機能も設けます。
本機能を設ける場合は、ボタンが押されたという信号を作ればよいです。この信号をILAへ入力してこの信号をトリガとすれば実現が可能です。
ロジアナの仕様決め
- 16chとします
- PYNQ-Z1ボードのIO26~IO41の16本を割り付けます
RTLの作成
さっそくロジアナ化するためのRTLを作成します。
クロック生成モジュールとメイン部(信号取り込みと手動トリガ作成部)のモジュールの2ファイル構成としました。
ソースを見ていただくのが早いので、RTLを示します。
クロック生成モジュール
クロック生成モジュールです。ファイル名は"clkgen2.v"です。MMCMを呼び出しています。外部クロック入力125MHzから周波数333MHz,200MHz,166MHz,100MHzの4種類のクロックを作っています。
`timescale 1ns/1ps // 2022/05/01 // 125MHz 入力 module clkgen2 ( input RST, input SYSCLK, // 125MHz output wire locked, // 安定待ち output wire CLK333, // 333MHz output wire CLK200, // 200MHz output wire CLK166, // 166MHz output wire CLK100 // 100MHz ); wire iCLK333; wire iCLK200; wire iCLK166; wire iCLK100; BUFG iBUFG0 (.I(iCLK333), .O(CLK333)); BUFG iBUFG1 (.I(iCLK200), .O(CLK200)); BUFG iBUFG2 (.I(iCLK166), .O(CLK166)); BUFG iBUFG3 (.I(iCLK100), .O(CLK100)); MMCME2_BASE #( .BANDWIDTH("OPTIMIZED"), // Jitter programming (OPTIMIZED, HIGH, LOW) .CLKFBOUT_MULT_F(48.0), // 乗数M(2.000-64.000) .CLKFBOUT_PHASE(0.0), // 位相(-360.000-360.000) .CLKIN1_PERIOD(8.0), // CLKINの周期 // CLKOUT0_DIVIDE - CLKOUT6_DIVIDE: Divide amount for each CLKOUT (1-128) .CLKOUT1_DIVIDE(5), .CLKOUT2_DIVIDE(6), .CLKOUT3_DIVIDE(10), .CLKOUT4_DIVIDE(1), .CLKOUT5_DIVIDE(1), .CLKOUT6_DIVIDE(1), .CLKOUT0_DIVIDE_F(3), // 除数Q(1.000-128.000) FX68 for 100MHz // CLKOUT0_DUTY_CYCLE - CLKOUT6_DUTY_CYCLE: Duty cycle for each CLKOUT (0.01-0.99). .CLKOUT0_DUTY_CYCLE(0.5), // デューティ比 .CLKOUT1_DUTY_CYCLE(0.5), .CLKOUT2_DUTY_CYCLE(0.5), .CLKOUT3_DUTY_CYCLE(0.5), .CLKOUT4_DUTY_CYCLE(0.5), .CLKOUT5_DUTY_CYCLE(0.5), .CLKOUT6_DUTY_CYCLE(0.5), // CLKOUT0_PHASE - CLKOUT6_PHASE: Phase offset for each CLKOUT (-360.000-360.000). .CLKOUT0_PHASE(0.0), .CLKOUT1_PHASE(0.0), //.CLKOUT2_USE_FINE_PS("TRUE"), .CLKOUT2_PHASE(0.0), //.CLKOUT3_USE_FINE_PS("TRUE"), .CLKOUT3_PHASE(0.0), .CLKOUT4_PHASE(0.0), .CLKOUT5_PHASE(0.0), .CLKOUT6_PHASE(0.0), .CLKOUT4_CASCADE("FALSE"), // Cascade CLKOUT4 counter with CLKOUT6 (FALSE, TRUE) .DIVCLK_DIVIDE(6), // 除数D(1-106) .REF_JITTER1(0.0), // Reference input jitter in UI (0.000-0.999). .STARTUP_WAIT("FALSE") // Delays DONE until MMCM is locked (FALSE, TRUE) ) MMCME2_BASE_inst ( // Clock Outputs: 1-bit (each) output: User configurable clock outputs .CLKOUT0(iCLK333), // 1-bit output: CLKOUT0 .CLKOUT0B(), // 1-bit output: Inverted CLKOUT0 33.16062MHz .CLKOUT1(iCLK200), // 1-bit output: CLKOUT1 8MHz .CLKOUT1B(), // 1-bit output: Inverted CLKOUT1 .CLKOUT2(iCLK166), // 1-bit output: CLKOUT2 .CLKOUT2B(), // 1-bit output: Inverted CLKOUT2 .CLKOUT3(iCLK100), // 1-bit output: CLKOUT3 .CLKOUT3B(), // 1-bit output: Inverted CLKOUT3 .CLKOUT4(), // 1-bit output: CLKOUT4 .CLKOUT5(), // 1-bit output: CLKOUT5 .CLKOUT6(), // 1-bit output: CLKOUT6 // Feedback Clocks: 1-bit (each) output: Clock feedback ports .CLKFBOUT(CLKFBOUT), // 1-bit output: Feedback clock .CLKFBOUTB(), // 1-bit output: Inverted CLKFBOUT // Status Ports: 1-bit (each) output: MMCM status ports .LOCKED(locked), // 1-bit output: LOCK // Clock Inputs: 1-bit (each) input: Clock input .CLKIN1(SYSCLK), // 1-bit input: Clock // Control Ports: 1-bit (each) input: MMCM control ports .PWRDWN(1'b0), // 1-bit input: Power-down .RST(1'b0), // 1-bit input: Reset // Feedback Clocks: 1-bit (each) input: Clock feedback ports .CLKFBIN(CLKFBOUT) // 1-bit input: Feedback clock ); endmodule
4種類のクロック出力がありますが、利用するのはこのうち一つのみです。これがサンプリングクロックになります。
最も高速な333MHzを利用すれば、最も高速なロジアナになりますが、サンプリングデータが格納されるBRAMは有限なので、使えるBRAMの容量分だけしかサンプリングが出来ません。よって、サンプリング周波数を遅くすれば、より多くの時間をサンプリング可能になります。遅いサンプリング周期がすぐに選択できるように、あらかじめ遅いクロックを作っておきました。
最大周波数を333MHzとした理由は後ほど書きます。
メインモジュール
外部端子の信号をサンプリングする部分と、手動トリガ信号生成部(PYNQ-Z1ボードのBTN0押下)の論理です。
プッシュボタン押下検出はチャタリング除去として、プッシュボタンの信号を0x20_0000クロック毎にサンプリングして、2回連続で"H"検出をしないとエッジ検出できないようになっています。
ロジアナとしてのメイン機能である信号取り込み部は16bitのフリップフロップDATOUT[15:0]へ入力信号DATIN[15:0]を毎サイクル格納しているだけです。
単に取り込みだけを行う論理になっており、フリップフロップDATOUT出力はどこにも使われていないため、論理合成の最適化により不要な論理とみなされ消えてしまいます。
消えないように、フリップフロップのでreg宣言部"reg [15:0] DATOUT"行に消さないで指示になる(* mark_debug = "true" *)を付加しています。本指示は論理SIMにはなんら影響を与えません。Vivadoの論理合成エンジンに伝えるのみになります。
サンプリング周波数の切り替えは本ファイルの
assign sel_clk = CLK333;
CLK333部分をCLK200など他のクロックのwireへ結線変更して、再合成する事で実現させています。
サンプリングクロックの変更をボードに実装されているスライドスイッチで動的に行う事を当初、目論んでいましたが、ILA呼び出し時にクロックが繋がってないといった意味のエラーが出てbitsteamの生成がうまくできませんでした。
スライドスイッチの入力をset_case_analysisでレベルを固定してクロック入力が一意に決まるよう合成制約を入れてエラーの回避を試みましたが同じエラーが出ました。。。なので本当の理由は分からずです。
といった訳で、RTLでのwire接続としました。
ファイル名は"logic_analyzer1.v"です。
`timescale 1ns/1ps // 16ch logic analyzer // vivado 内蔵ILAを利用 // module logic_analyzer ( // System Signals input wire CLK, input wire RST, // push bottun トリガーで取り込みも可能 input BTN, //BTN[0] // switch input [1:0] SW, // logic アナライザー端子16ch input [15:0] DATIN, output wire LED0, // SW[0]状態を表示 output wire LED5_B ); assign LED0 = SW[0]; // PLL wire locked; wire CLK333; wire CLK200; wire CLK166; wire CLK100; clkgen2 clkgen ( .RST(RST), .SYSCLK(CLK), // 125MHz .locked(locked), // 安定待ち .CLK333(CLK333), // 333MHz .CLK200(CLK200), // 200MHz .CLK166(CLK166), // 166MHz .CLK100(CLK100) // 100MHz ); wire isel_clk; wire sel_clk; assign sel_clk = CLK333; reg [1:0] rst_sync; wire SRST = rst_sync[1]; (* mark_debug = "true" *) reg [15:0] DATOUT; always @( posedge sel_clk or posedge RST) if(RST) rst_sync <= 2'b11; else begin rst_sync[0] <= 1'b0; rst_sync[1] <= rst_sync[0]; end // チャタリング防止用カウンタ reg [21:0] cnt; wire cnt_end = (cnt == 22'h1f_ffff); always @( posedge sel_clk ) if(SRST) cnt <= 22'h00_0000; else if( cnt_end ) cnt <= 22'h00_0000; else cnt <= cnt + 22'h00_0001; // ボタンを押すと以下の様になる // ボタン押下 // BTN0 _____|~~~~~~~|__________ // 立ち上がり検出 reg [1:0] push_button; reg detect_push; always @( posedge sel_clk ) if(SRST) push_button <= 2'b00; else if(cnt_end) begin // cnt_endの時だけ状態取り込み push_button[0] <= BTN; push_button[1] <= push_button[0]; end always @( posedge sel_clk ) detect_push <= cnt_end&(push_button[0]&(~push_button[1])); // logic analyzer // 端子サンプル always @( posedge sel_clk ) DATOUT <= DATIN; assign LED5_B = detect_push; endmodule
合成制約ファイル .xdcの準備
合成制約ファイルであるxdcファイルを準備します。クロックの定義、ボードのピン名とRTLのポートの対応付けを本ファイルで行います。
今回はクロック入力、リセット入力、ロジアナの入力用に16本、外部トリガ用にプッシュボタンを使っています。RTLのポートとして存在するものは過不足なく、xdcファイル中"set_property"コマンドで定義する必要があります。
xdcファイル中、set_property -dictで指定しているPACKAGE_PINがボードのピン名なのですが、これはdigilentのwebページにあるPYNQ-Z1のボード回路図に記載があります。
例えばボードのIO26をDATOUT[0]として利用しますが、ボード回路図からIO26を検索します。すると以下のところにヒットします。ここからPACKAGE_PINはU5だと分かります。
また、-dictには、他にIOSTANDARD, LVCMOS33という情報が定義されています。これらは、ボードファイルとしてコピーしたファイルの中に定義されています("install_dir"\2020.1\data\boards\board_files\pynq-z1\1.0\part0_pins.xml)。loc="U5"の記載の行にiostandard="LVCMOS33"と示されています。
以下に今回作成したxdcファイル"logic_analyzer.xdc"を示します。
## PYNQ-Z1 constraints file #part0_pins.xml 端子名がある ## logic analyzer # Clock signal 125 MHz set_property -dict {PACKAGE_PIN H16 IOSTANDARD LVCMOS33} [get_ports CLK] create_clock -period 8.000 -name sys_clk_pin -waveform {0.000 4.000} -add [get_ports CLK] # Reset set_property -dict {PACKAGE_PIN L19 IOSTANDARD LVCMOS33} [get_ports RST] ## LEDs #set_property -dict {PACKAGE_PIN R14 IOSTANDARD LVCMOS33} [get_ports {LED[0]}] #set_property -dict {PACKAGE_PIN P14 IOSTANDARD LVCMOS33} [get_ports {LED[1]}] #set_property -dict {PACKAGE_PIN N16 IOSTANDARD LVCMOS33} [get_ports {LED[2]}] #set_property -dict {PACKAGE_PIN M14 IOSTANDARD LVCMOS33} [get_ports {LED[3]}] set_property -dict {PACKAGE_PIN R14 IOSTANDARD LVCMOS33} [get_ports LED0] set_property -dict {PACKAGE_PIN G14 IOSTANDARD LVCMOS33} [get_ports LED5_B] # Buttons set_property -dict {PACKAGE_PIN D19 IOSTANDARD LVCMOS33} [get_ports BTN] #set_property -dict { PACKAGE_PIN D20 IOSTANDARD LVCMOS33 } [get_ports { BTN[1] }] #set_property -dict { PACKAGE_PIN L20 IOSTANDARD LVCMOS33 } [get_ports { BTN[2] }] # Switch set_property -dict {PACKAGE_PIN M20 IOSTANDARD LVCMOS33} [get_ports {SW[0]}] set_property -dict {PACKAGE_PIN M19 IOSTANDARD LVCMOS33} [get_ports {SW[1]}] # Logic analyzer inputs #IO33-26 set_property -dict {PACKAGE_PIN U5 IOSTANDARD LVCMOS33} [get_ports {DATIN[0]}] set_property -dict {PACKAGE_PIN V5 IOSTANDARD LVCMOS33} [get_ports {DATIN[1]}] set_property -dict {PACKAGE_PIN V6 IOSTANDARD LVCMOS33} [get_ports {DATIN[2]}] set_property -dict {PACKAGE_PIN U7 IOSTANDARD LVCMOS33} [get_ports {DATIN[3]}] set_property -dict {PACKAGE_PIN V7 IOSTANDARD LVCMOS33} [get_ports {DATIN[4]}] set_property -dict {PACKAGE_PIN U8 IOSTANDARD LVCMOS33} [get_ports {DATIN[5]}] set_property -dict {PACKAGE_PIN V8 IOSTANDARD LVCMOS33} [get_ports {DATIN[6]}] set_property -dict {PACKAGE_PIN V10 IOSTANDARD LVCMOS33} [get_ports {DATIN[7]}] #IO41-34 set_property -dict {PACKAGE_PIN W10 IOSTANDARD LVCMOS33} [get_ports {DATIN[8]}] set_property -dict {PACKAGE_PIN W6 IOSTANDARD LVCMOS33} [get_ports {DATIN[9]}] set_property -dict {PACKAGE_PIN Y6 IOSTANDARD LVCMOS33} [get_ports {DATIN[10]}] set_property -dict {PACKAGE_PIN Y7 IOSTANDARD LVCMOS33} [get_ports {DATIN[11]}] set_property -dict {PACKAGE_PIN W8 IOSTANDARD LVCMOS33} [get_ports {DATIN[12]}] set_property -dict {PACKAGE_PIN Y8 IOSTANDARD LVCMOS33} [get_ports {DATIN[13]}] set_property -dict {PACKAGE_PIN W9 IOSTANDARD LVCMOS33} [get_ports {DATIN[14]}] set_property -dict {PACKAGE_PIN Y9 IOSTANDARD LVCMOS33} [get_ports {DATIN[15]}]
Vivadoでの合成作業
ここからはILAをアタッチしての合成作業です。通常のオペレーションと変わりません。vivadoのスクリーンショットは撮りましたが手順の間に挟んでいくと記事が読みづらくなるため、スクリーンショットは記事の最後へ集めてみました。手順を文章化したものを記載します。
vivadoのversionは2020.1を使っています。
■VIVADOを立ち上げ後、
- Create Project→Next→Project nameで適当なプロジェクト名を付ける(スクリーンショットではlogic_analyzer2になっています)(*)
- Project locationでは上記プロジェクトを置くフォルダを設定します。
- Project TypeはRTL Projectを指定し、後から上で準備したRTLソースを指定するので、ここでは"Do not specify sources at this time"にはチェックを入れてNextボタンを押します。(*)
- Part選択画面ではBoardsを選択し、PYNQ-Z1を指定する。(BoardsでPYNQ-Z1が現れるのは、vivadoのインストールdir "C:\Xilinx\Vivado\2020.1\data\boards\board_files"へココのファイルを解凍したものをコピーしている為です)(*)
- Add Sourcesで、Add or create design sourcesにチェックを入れて、先ほど準備したRTLを2fileとも指定します。(*)
- これで以下の写真の様にDesign Sourcesの下にlogic_analyzerというモジュールが現れます。このモジュールがインスタンスしているcllgenが存在する事も分かります。
- Add Sources→Add or create constraintsで上記で準備したxdcファイルをvivadoへ読み込ませます。(*)
- 次にRun Synthesisボタンを押してILAを組み込むための前合成を行います。Run Synthesis→OK(*)
- 合成が終了すると,次どうするか?のwindowが出ますがここは一旦Cancelボタンを押します。(*)
- Open Synthesized Designを開いて、Set Up Debugボタンを押します。Set Up Debugのウイザードが出ますのでNextを押してNets to Debugの画面を出します。ここでILAでprobeする信号を選択します。既にDATOUT(16)は出ていると思います。これは最適化防止の為、RTLのreg [15:0] DATOUTにmark_debugを埋め込んだためです。(*)
- Find Nets to Add..ボタンを押すと、Find Nets画面が出ます。PropertiesのところにNAME, containts, * と3つ並んでいますが、最後の*の部分が検索するキーワードになります。今回は信号数が少ない事が分かってるので、このままとしますが、多い場合にはここで信号名のフィルタを行うことが出来ます。
*のままOKボタンを押してネットの選択画面へ移動します。ここで"LED5_B_OBF"を選択します。この信号はRTLを見ると分かりますが、ボードのBTN0を押したことを検出した際に発生するパルス信号です。FPGAボードのボタンの押下で信号の取り込みを行いたいので追加します。(*) - Nextボタンを押すとILA Core Options画面が出て、"Sample of data depth"を設定します。デフォルトでは1024が入っています。どれだけサンプルするかを設定します。8192としました。今回は1sampleで17bit使います。8192sampleで8192x17bit=17KbyteのBRAMを消費します。PYNQはBRAMが630KBあるそうなので、まだまだ余裕があります。Nextボタンを押してFinishボタンを押します。するとILAコアにclk(1),probe0(16),probe1(1)が繋がっているという事が分かる画面が出ます。
- PCとPYNQボードをUSB接続して、ジャンパをJTAG側へ繋いでモードを切り替えておき、電源を入れます。
- Generate Bitstreamをクリックします。Saveボタン→OK
- Save Constraintsの画面はSelect an existing fileを選択(Add sourcesで読み込んだものが入っているハズです)→OK
- Yesボタン→Launch RunsでOK
これでbit streamを作成してくれます。(このフェーズでは配置まで行いますので合成のみよりずっと時間がかかります) - Bitstream Generation successfully completed の画面が出たら,bit streamの作成は終了です。
記事が長くなったので今回はここまでです。
次回はILAを使って信号を取り込み波形を見る記事を書きます。
終わりに...(記事は次回へ繋げますけど...)
最大周波数を333MHzとした理由
今回の簡易ロジアナはサンプリングクロックとして333MHzが最大になっています。結論だけ書くと、この周波数がPYNQ-Z1では、合成できる最大周波数が333MHzだったからです。
(文章に書くと当たり前な気がしますけど。。。ILAを繋がなきゃFFで外部信号を取り込んでいるだけなので、、、合成できないという事に理解が追い付かず、かなり手間取りました)
PYNQ-Z1であれば、MMCMを使って出力周波数は600MHzまで作れます。しかし,600MHzだとタイミングがミートしないためか、bit streamが生成出来ずエラーで終わってしまいました。いくつか試したところ333MHzまで下げたところbit streamが生成できました。エラーの内容がタイミングがmeet出来なかったというエラーではなく、前のエラーを消しなさい的な内容で、そのエラーメッセージが見つけられなくて本当の原因が分かりませんでした。試行錯誤をしているうち周波数を下げたら生成が出来たので、合成周波数が早いからという結論に達しました。
周波数選択を動的(スライドスイッチによる切替)にしなかった理由
PYNQ-Z1はスライドスイッチが2個あります。この2個のスイッチでクロックを切り替えるアイデアがありました。
クロックを切り替えるのに再合成は面倒くさい為です。
これもbit stream生成段階でエラーが出て出来ませんでした。クロックラインにセレクタを噛まして、セレクト信号(スライドスイッチ)で周波数を決めるように構成していたので、
周波数が合成時には決まらないからか?と思って、
合成制約にset_case_analysisでスライドスイッチの状態を確定させる指示を入れましたが、
入れる前と同じエラーメッセージが出てbit streamが生成されませんでした。
とまぁ、色々苦労しました。
今回の記事はココまでです。