exStickGEでMicroBlazeを動かしてみた with MIG

技術紹介

今回はexStickGE上でソフトプロセッサであるMicroBlazeを動かしてみました.
FPGAでもマイコンのような使い方したくなるときもあるので, そんなときにはMicroBlazeが便利ですね.

この記事では, MicroBlazeとDDR3メモリを接続してMicroBalze上のプログラムからDDR3メモリにアクセスする方法を紹介します.

Vivadoは2020.1を使用します.

FPGA側のプロジェクト

まず設定ファイルを入手するため, ここからcloneしてきます.
今回はDドライブ直下にcloneしました.

プロジェクトの作成

次にプロジェクトを作成します.
左上のメニューFile->Project->Newから新規作成

プロジェクト名はprj, 位置はclone先のexStickGE_microblazeを指定します.

RTL Projectを選択

Add Sourcesでは何も追加せず, Add ConstraintsのAdd FilesからexStickGE_microblaze/sources/top.xdcを追加します.

Default PartでexStickGEに搭載しているFPGA xc7a200tsbg484-2 を選択します.

これでプロジェクトの作成は完了です.

ブロックデザインの作成

左のFlow NavigatorからIP INTEGRATORのCreate Block Designをクリック.

名前はtopにでもしておきましょう.

それでは大事なIPの配置作業です.
中央の+をクリックしIPを追加していきます.

まずクロック関連を作成します.
Clocking Wizardを2つ追加.

1つ目(clk_wiz_0)の設定はClocking Option->Input Clock Informationより
Input Frequency(MHz)のAUTOをMANUALにして200MHzを指定,
SourceはDifferential clock capable pinを指定します.

次にOutput Clocksタブ
clk_out1ではわかりやすいようにPort NameをCLK100Mにし, 
一番下のReset TypeをActive Lowにします.

2つ目(clk_wiz_1)の設定はOutput Clocksタブより
clk_out1のPort NameをCLK310M, Requestedを310MHz,
同様に一番下のReset TypeをActive Lowにします.

ブロックデザインに戻りCLK100Mとclk_in1を接続します.

主役のMicroBlazeを追加します.

左上のRun Block Automationをクリックし
設定でLocal Memoryを64KBと設定します.

自動でメモリやデバッグ, リセットモジュールが追加されます.

左上のRun Connection Automationよりclk_wiz_1以外にチェックを入れてOK.

戻ったところでclk_wiz_1のリセットをclk_wiz_0のリセットと接続します.

作成された外部ポートの名前を変更します.

次にシリアル通信とタイマー, メモリモジュールのIPを追加していきます.
名前はそれぞれAXI UARTLITE, AXI Timer, Memory Interface Generator(MIG)です.
シリアル通信とタイマーモジュールは特に設定しないのでそのままです.

重要なのはMIGです. 設定していきましょう.

MIG Output OptionsはCreate Designで次へ
Pin Compatible FPGAsはそのまま次へ
Memory SelectionはDDR3 SDRAMで次へ
Options for Controller 0では
  Clock Periodを3225ps
  Memory PartをMT41K128M16XX-15Eに設定し次へ

Axi Parameter Options C0はそのままで次へ
Memory Options C0もそのままで次へ
ここではSystem Clock及びReference ClockをNo Buffer
IO Power ReductionをOFFに設定し次へ
Internal Termination for High Range Bnaksはそのままで次へ

Pin/Bank Selection Modeはピン配置が既に決まっているので
Fixed Pin Outを選択し次へ
Pin Selection For Controller 0ではDDR3メモリのピン配置を設定しますが
一つ一つ選択するのは大変なので設定ファイルを読み込みます.
Read XDC/UCFよりsources/exStickGE_DRAM_Pinout.ucfを選択します.
そうするとピン配置が読み込まれるのでValidateをクリックしてOK
NEXTが押せるようになるので次へ進みます
System SignalsSelectionでは
  init_calib_completeをD21
  tg_compare_errorをE21に設定し次へ
今までの設定項目が羅列されるので次へ
シミュレーションモデルの規約が表示されるので同意できるのであればAcceptで次へ
最後まで進めGenerateで完成です.

Run Connection Automationで全てにチェックを入れ
mig_7series_0->clk_ref_iとsys_clk_iはClock Sourceを/clk_wiz_1/CLK310Mにします.

ただしこれではmig_7series_0のsys_rstが接続されないので
外部ポートのRESETに手動で接続します.
DDR3の接続ポートがないのでDDR3を右クリックし
Make Externalで外部ポートを作成します.

自動で作成されたuart_rtl_0の外部ポート名をUARTに変更します.

せっかくなのでAXIトランザクションを見るために
Integrated Logic Analyzer(ILA)も追加しましょう.
ila_0のSLOT_0_AXIはmicroblaze_0のM_AXI_DPへ
ila_0のclkはmicroblaze_0のclkと同じ配線へつなげば問題ないです.

これでブロックデザインの完成です!

アドレスの割当はこのようになっています.

論理合成とハードウェアファイルの出力

完成したら保存して左のFlow Navigator内のPROJECT MANAGERへ戻りましょう.

Desing Sources->top(top.bd)を右クリックしてHDL Wrapperを作成します.

Let Vivado manage wrapper and auto-updateのままOK.

ここまできたらFlow Navigator内PROGRAM AND DEBUGの
Generate Bitstreamで論理合成してbitファイルを生成します.
環境にもよりますが長い時間かかります.

bitファイルが生成できたらMicroBalze用のプロジェクトを生成するため
設定ファイルを書き出します.

メニューバーからFile->Export->Export Hardwareをクリック

Platform typeはFixedで次へ, OutputはInclude bitstreamで次へ
Filesはそのままで次へ, Finishでxsaファイルが生成され完了です.

ソフトウェア側のプロジェクト

プロジェクトの生成

先程生成したxsaファイルを元にソフトウェアのプロジェクトを生成します.

Xilinx Vitis 2020.1を起動します. そうするとワークスペースの指定が出てくるので
今回はprjフォルダの中にvitisフォルダを作成, 指定しLaunchします.

メニューバーからFile->New->Application Projectをクリック.

Nextをクリックし, xsaファイルを指定します.
Create a new platform from hardware(XSA)タブを選択し
Browseから生成したxsaファイルを選択, 次へ.

Application Project Detailsではプロジェクト名の指定ができます.
無難にsampleにし次へ.

DomainでOSの指定ができますが, 今回はstandaloneで実行します.

Templateではサンプルコードを含めたテンプレートが指定できます.
今回はHello Worldを選択し, Finishでプロジェクトが生成されます.

とりあえず動作確認

ここまで来ると動作確認することができるので,
問題なくシリアル通信で出力できるか確認します.


現在のソースコード

#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"


int main()
{
    init_platform();

    print("Hello World\n\r");
    print("Successfully ran Hello World application");
    cleanup_platform();
    return 0;
}

それではビルドします. sample_systemを選択した上で
ツールバーのハンマーマークをクリックしてビルドします.
(sample_system右クリックでBuild Projectでも可)

実際に実行する前にシリアル通信モジュールと接続します.
40ピンあるピンヘッダーに対して17ピンがTXピン, 18ピンがRXピンです.
3.3Vレベルのシリアル通信モジュールとTX->RX, RX->TXに接続し, Teraterm等で開きます.

準備ができたら実際に動かしてみます.
ツールバーの虫マークをクリックしてデバッグを実行します.

FPGAにbitファイルが書き込まれ, プログラムの最初で一時停止します.

ツールバーのレジュームボタンやステップオーバーボタンでプログラムを進めます.

シリアル通信でHello Worldなどが表示されます.
とりあえず問題なく動作することがわかりました.

追加したメモリを使用する

動作することがわかったので次に追加したDDR3メモリを使用します.
メモリに関わらず, AXIで接続したものはポインタで直接アクセスすれば
読み書きができる仕様です.

アドレスはブロックデザインのAddress Editorで見ることができますが,
いちいちそれを指定するのは大変なので, アドレスが一覧で定義された
ヘッダーファイルx_parameters.hをインクルードします.
XPAR_MIG_7SERIES_0_BASEADDRでメモリ領域の先頭のアドレスが得られるので
任意の位置にアクセスするために以下の通り定義します.  unsigned int型は4バイトに
なっているので, アクセスするときは4の倍数で指定します.

#define MEM_array(i)	*((volatile unsigned int *) (XPAR_MIG_7SERIES_0_BASEADDR+i))

その他に, 時間を計測したいので追加したAXI Timer用の
ヘッダーファイルxtmrctr.hもインクルードしMicroBlazeで実行時間を測定する方法
参考に初期化コードを追加します.
AXI Timerはクロックごとにカウントするのでそのカウント数を比較することで
時間を計測します. 入力クロックが100MHzなので1カウント10[ns]です.

計測した時間を表示するためにprintfが使用したいのですが, MicroBkazeのメモリを
結構必要とするので今回はxil_printfを使用しました. しかし出力に浮動小数点数型が
使用できないので整数型で表示できるように少しコードを追加しました.

#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"
#include "xparameters.h"
#include "xtmrctr.h"

#define MEM_array(i)	*((volatile unsigned int *) (XPAR_MIG_7SERIES_0_BASEADDR+i))

XTmrCtr TimerCounterInst;

int init_timer(){
	int Status;

	Status = XTmrCtr_Initialize(&TimerCounterInst, XPAR_AXI_TIMER_0_DEVICE_ID);
	if (Status != XST_SUCCESS) {
		print("Timer Initialize Error\n\r");
        return XST_FAILURE;
	}

	XTmrCtr_SetOptions(&TimerCounterInst, 0, XTC_AUTO_RELOAD_OPTION);
	XTmrCtr_Start(&TimerCounterInst, 0);
	return XST_SUCCESS;
}

void show_time_us(unsigned int diff){
	unsigned int tmp_us;
	tmp_us = diff / 100;
    xil_printf("Time = %d.", tmp_us);
    xil_printf("%d us\n\r", diff - tmp_us * 100);
}

int main()
{
    volatile int i;
    u32 tStart, tEnd;

    init_platform();
    init_timer();

    print("Hello World\n\r");
    print("Successfully ran Hello World application\n\r");

    printf("for loop\n\r");
    tStart = XTmrCtr_GetValue(&TimerCounterInst, 0);
	for(i=0;i<128*1024*1024;i+=4){
	}
	tEnd = XTmrCtr_GetValue(&TimerCounterInst, 0);
	show_time_us((unsigned int)(tEnd - tStart));

    printf("for loop and memory access\n\r");
    tStart = XTmrCtr_GetValue(&TimerCounterInst, 0);
	for(i=0;i<128*1024*1024;i+=4){
		MEM_array(i) = i;
	}
   	tEnd = XTmrCtr_GetValue(&TimerCounterInst, 0);

	show_time_us((unsigned int)(tEnd - tStart));


    cleanup_platform();
    return 0;
}

再度ビルドを実行し, デバッグで実行します.

おおよそforループ部分の実行に4秒程かかっていることがわかります.

時間について

ループ回数は128*1024*1024/4=33,554,432回で1ループあたり0.12[us]です.
アセンブリによると繰り返す部分は8命令のようで, begi分岐命令など
命令によって数サイクル必要とするものもあるので
1サイクル0.01[us]と考えるとMicroBlazeのクロックと同じであることがわかります.

メモリアクセスする場合は初期化部分が別途分離しています.(面白い)
アセンブリの該当部分では5命令の増加ですが, それに比べて
時間の増加は7.7[s]なので相当ですね.
1ループあたりに直すと5命令で0.22[us]です.
おそらくこの間にAXIトランザクションが走りメモリからデータを取ってきているのでしょう.

まとめ

Vivado 2020.1を使用してBlock Designを作成し, MicroBlazeの生成を行いました.
Vitis 2020.1を使用してMicroBlaze用のプロジェクトを作成し実際に実行まで確認しました.
MicroBlazeにDDR3メモリをMIG経由でアクセスして今回は書き込みを行いました.
今回は単純に書き込み速度について計測しましたが, 次回はAXIトランザクションについてと
読み書きの速度について検証してみたいと思います.

コメント

  1. […] 前回 exStickGE上でソフトプロセッサであるMicroBlazeを動かしてみましたが,  アクセスの仕方で速度が多少なりとも変わるのかについて調べました. […]

  2. […] 前回作成したプロジェクトを使用して作っていきますが […]

  3. […] 前回作成したプロジェクトを使用して作っていきますが […]

タイトルとURLをコピーしました