MicroBlazeでAXI GPIOの外部ピン割り込みを実装

概要

MicroBlazeで外部ピンからの割り込みを実装し、AXI GPIOのInterruptとAXI Interrupt Controllerの使い方を学びます。

ハードウェア

MicroBlaze
・AXI GPIO (Enable Interruptを有効にする)
・AXI Interrupt Controller
をBlock Designに追加する。

AXI GPIOのip2intc_irptをConcatを挟みAXI Interrupt Controllerのintrに接続する。

ソースコード

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

#define GPIO1 1
#define GPIO2 2

XGpio Gpio;
XIntc Intc;

// GPIO Interrupt Handler
void GpioHandler(void *CallbackRef)
{
    XGpio *GpioPtr = (XGpio *)CallbackRef;
    xil_printf("Gpio Interrupt!\r\n");
    // Clear the Interrupt
    XGpio_InterruptClear(GpioPtr, 1);
}

int main()
{
    int Status;

    // XGpioの初期化
    Status = XGpio_Initialize(&Gpio, XPAR_GPIO_0_DEVICE_ID);
    if(Status != XST_SUCCESS) return XST_FAILURE;
    XGpio_SetDataDirection(&Gpio, 1, 1);

    // XIntcの初期化
    Status = XIntc_Initialize(&Intc, XPAR_INTC_0_DEVICE_ID);
    if(Status != XST_SUCCESS) return XST_FAILURE;

    // XGpioの割り込みで呼び出される関数を指定
    Status = XIntc_Connect(&Intc, XPAR_INTC_0_GPIO_0_VEC_ID,
                (XInterruptHandler)GpioHandler,
                (void *)&Gpio);
    if(Status != XST_SUCCESS) return XST_FAILURE;

    //XIntcをhardware interrupts onlyで開始
    Status = XIntc_Start(&Intc, XIN_REAL_MODE);
    if(Status != XST_SUCCESS) return XST_FAILURE;

    // XGpioの割り込みを有効化
    XIntc_Enable(&Intc, XPAR_INTC_0_GPIO_0_VEC_ID);

    // MicroBlazeの割り込み設定を初期化
    Xil_ExceptionInit();
    // MicroBlazeにXIntcからの割り込みを登録
    Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
                (Xil_ExceptionHandler)XIntc_InterruptHandler,
                &Intc);
    // MicroBlazeの割り込みを有効化
    Xil_ExceptionEnable();

    // GPIOの割り込みを有効化
    XGpio_InterruptEnable(&Gpio, GPIO1);
    // GPIOのグローバル割り込みを有効化
    XGpio_InterruptGlobalEnable(&Gpio);

    while(1);
    cleanup_platform();
    return 0;
}

実行結果

AXI GPIOに接続したボタンスイッチを押すと
「GPIO Interrupt!」と表示されます。

AXI IICの使い方

概要

XilinxのAXI IICの使い方について書いていこうと思います。
今回はZYNQでAXI IICを制御する方法について書きます。

AXI IICの接続と設定

AXI IICとの接続は次のようになっています。
AXIバスでZYNQと接続しています。
IICポートは外部のピンと接続されており、IIC接続するデバイスと繋がっています。
f:id:taltalp:20180506035016p:plain

IPコアの設定は次のようになっています。
f:id:taltalp:20180506035054p:plain

デバイスドライバの使い方

次にXilinx SDKでCPUのプログラムを書いていきます。
ソースコードは次の通りです。

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

// 接続するIICデバイスのアドレス
#define LCD_DEVICE_ADDRESS 0x3E

int WriteData(u16 ByteCount, u8 *WriteBuffer);

XIic xiic;

int main()
{
	int Status;
	XIic_Config *ConfigPtr;
	u8 WriteBuffer[16];

    init_platform();

    // AXI IICの初期化
	ConfigPtr = XIic_LookupConfig(XPAR_IIC_0_DEVICE_ID);
	if (ConfigPtr == NULL) {
		return XST_FAILURE;
	}

	Status = XIic_CfgInitialize(&xiic, ConfigPtr, ConfigPtr->BaseAddress);
	if (Status != XST_SUCCESS) {
		return XST_FAILURE;
	}

	// 送信するデータ (今回はアドレス+データで、合計3byte送信)
	WriteBuffer[0] = 0x00;
	WriteBuffer[1] = 0x38;

	Status = WriteData(2, WriteBuffer);
	if (Status != XST_SUCCESS) {
		return XST_FAILURE;
	}

    cleanup_platform();
    return 0;
}

int WriteData(u16 ByteCount, u8 *WriteBuffer)
{
	int Status;

	Status = XIic_Start(&xiic);
	if (Status != XST_SUCCESS) {
		return XST_FAILURE;
	}

	// STOP conditionを発行するためにXIic_DynSendを使っている
	Status = XIic_DynSend(xiic.BaseAddress, LCD_DEVICE_ADDRESS, WriteBuffer, ByteCount, XIIC_STOP);
	if (Status != XST_SUCCESS) {
		return XST_FAILURE;
	}

	Status = XIic_Stop(&xiic);
	if (Status != XST_SUCCESS) {
		return XST_FAILURE;
	}

	return XST_SUCCESS;
}

実行結果

ロジック・アナライザを使って出力波形を見た結果がこちらです。
START → ADDRESS → DATA → DATA → STOP
の順に出力されていることが確認できました。
f:id:taltalp:20180506040237p:plain

XIic_MasterSendでSTOP conditionにならない?

今回データの送信にはXIic_DynSendという関数を使っています。
普通このデバイスドライバではXIic_MasterSendや、XIic_DynMasterSendを使って送信するはずです。
しかし、これらを使って記述したとろ、STOPビットが発行されず、うまく通信が行えませんでした。(下図参考)
f:id:taltalp:20180506040930p:plain

データシートをよく読んでみたところ、AXI IICのレジスタ Transmit FIFO (Address: 0x108) の
8bit目と9bit目にそれぞれStartとStop Conditionを発行するためのレジスタがありました。
これらを1にしてあげればよいのですが、XIic_MasterSendや、XIic_DynMasterSendにはその記述が見当たりませんでした。

一方で、XIic_DynSendの中身を見てみると、DynSendDataという関数を呼び出しており、

XIic_WriteReg(BaseAddress,  XIIC_DTR_REG_OFFSET, XIIC_TX_DYN_STOP_MASK | *BufferPtr++);

という記述があります。
XIIC_TX_DYN_STOP_MASK によってTransmit FIFOレジスタの9bit目をセットしているようです。

以上の理由により、今回はXIic_DynSendという関数を使っていますが、
このコードでも割り込みが正しく動作するのかなどは未確認です。

MicroBlazeで実行時間を測定する方法

概要

MicroBlazeで命令の実行時間を測定する方法について考えました。
MicroBlazeではxtime_l.hが存在せず、sys/time.hのgettimeofday()が使えなかったため、
外部にAXI_Timer IPを入れることで実行時間を測定することにしました。

Vivadoプロジェクトの作成

全体図

ブロックデザインにMicroBlazeとAXI Timer、AXI UARTを入れました。
図にはAXI Interrupt Controllerが入っていますが、実際には必要ありませんでした。

AXI Timerの設定

AXI Timerの設定は32bitでTimer1のみを使う設定にしました。
クロックは100MHzを入れているので、42.9秒までは測れそうです。

SDKプロジェクトの作成

ソースコードは次のようにしました。
AXI Timerをフリーランニングした状態で、実行前と実行後にタイマの値を取得して実行時間を測定しています。

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

XTmrCtr TimerCounterInst;

int init(){
	int Status;

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

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

int main()
{
    u32 tStart, tEnd;
    init_platform();

    init();

    tStart = XTmrCtr_GetValue(&TimerCounterInst, 0);
    print("Hello World\n\r");
    tEnd = XTmrCtr_GetValue(&TimerCounterInst, 0);

    printf("End - Start = %u\n\r", (unsigned int)(tEnd - tStart));
    printf("Time = %.2f us\n\r", 1.0 * (unsigned int)(tEnd - tStart) / 100000000 * 1000000);

    cleanup_platform();
    return 0;
}

実行結果

printfをした場合の結果

Hello World
End - Start = 1904
Time = 19.04 us

printfを外して、XTmrCtr_GetValueにかかる時間

End - Start = 201
Time = 2.01 us

ZYNQのPSで実行時間を測定する方法

概要

ZYNQである処理の実行時間を測定したいときにどうすればよいのかについて紹介します。
一般的なCのコードではtime.hを使ったりしますが、ZYNQではこの方法は非推奨なようです。
今回はXilinxが用意したxtime_l.hというファイルを用います。

Vivadoプロジェクトの作成

今回はZYNQのPS部分のみを使用します。

SDKプロジェクトの作成

HelloWorldのテンプレートでプロジェクトを作成し、HelloWorld.cを次のように書き換えました。

#include <stdio.h>
#include "platform.h"
#include "xparameters.h"
#include "xtime_l.h"


int main()
{
    XTime tStart, tEnd;
    init_platform();

    XTime_GetTime(&tStart);
    printf("Hello World\n\r");
    XTime_GetTime(&tEnd);

    printf("Output took %llu clock cycles.\n\r", 2 * (tEnd - tStart));
    printf("Output took %.2lf us.\n\r", 1.0 * (tEnd - tStart) / (COUNTS_PER_SECOND / 100000000));

    cleanup_platform();
    return 0;
}

実行結果

printfをした場合

Hello World
Output took 10388 clock cycles.
Output took 1731.33 us.

printfを外してGetTimeにかかる時間を測定

Output took 50 clock cycles.
Output took 8.33 us.

AXI GPIOについて

概要

AXI GPIOの使い方についてのメモ書きです。

AXI GPIOはFPGAのピンの入出力としてももちろん使えますが、
FPGA内部のIPの入出力をコントロールすることもできて非常に便利なIPです。

Vivadoプロジェクトの作成

Block DesignなどでAXI GPIOを追加するだけなので、省略します。

SDKプロジェクトの作成

LEDを点灯するサンプルコードを下記に示します。

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

// Gpio構造体
XGpio Gpio_led;

int main()
{
    int Status;

    init_platform();

    // AXI GPIOの初期化
    Status = XGpio_Initialize(&Gpio_led, XPAR_AXI_GPIO_0_DEVICE_ID);
    if (Status != XST_SUCCESS){
    	xil_printf("XPAR_AXI_GPIO_0_DEVICE_ID initialize failed\n\r");
    	return XST_FAILURE;
    }

    // AXI GPIOの入出力設定
    XGpio_SetDataDirection(&Gpio_led, 1, 0); // all output

    // AXI GPIOの出力値設定
    XGpio_DiscreteWrite(&Gpio_led, 1, 0x1); // led high

    cleanup_platform();
    return 0;
}

AXI GPIOの初期化

int XGpio_Initialize(XGpio *InstancePtr, u16 DeviceId);

でAXI GPIOが初期化されます。

DeviceIdはxparameters.hを見て、初期化したいAXI GPIOのDeviceIdを設定してください。

IPの初期化に成功するとXST_SUCCESSが返ってきます。
それ以外の場合はデバイスIDが間違っていたり、AXI GPIOがなかったりする可能性があります。
今回は初期化に失敗したらXST_FAILUREを返してプログラムを修了するようにしています。

AXI GPIOの入出力設定

void XGpio_SetDataDirection(XGpio *InstancePtr, unsigned Channel,
			    u32 DirectionMask);

で使用するピンの入出力方向を設定します。

Channelには「1」か「2」の値が入ります。
AXI GPIOはチャネルが2つまで設定できます。
どちらのチャネルを設定するのか、それぞれ決めなければいけません。
今回はチャネル1のみ使用するため1を設定しました。

DirectionMaskで入出力方向を決めます。
各bitは
0:出力
1:入力
となります。

AXI GPIOの出力値設定

void XGpio_DiscreteWrite(XGpio *InstancePtr, unsigned Channel, u32 Mask);

でピンの出力値が設定できます。
Maskで出力値のHigh/Lowを切り替えます。
各bitは
0:Low
1:High
となります。

AXI GPIOの入力値読み出し

u32 XGpio_DiscreteRead(XGpio *InstancePtr, unsigned Channel);

でピンの入力値が読み出せます。
予め各ピンは入力と設定されている必要があります。

AXI GPIOのビットセット/クリア

void XGpio_DiscreteSet(XGpio *InstancePtr, unsigned Channel, u32 Mask);
void XGpio_DiscreteClear(XGpio *InstancePtr, unsigned Channel, u32 Mask);

でビット単位でセット/クリアができます。

割り込み設定

AXI GPIOのIP設定画面から割り込みを有効にすることができます。

void XGpio_InterruptGlobalEnable(XGpio *InstancePtr);
void XGpio_InterruptGlobalDisable(XGpio *InstancePtr);
void XGpio_InterruptEnable(XGpio *InstancePtr, u32 Mask);
void XGpio_InterruptDisable(XGpio *InstancePtr, u32 Mask);
void XGpio_InterruptClear(XGpio *InstancePtr, u32 Mask);
u32 XGpio_InterruptGetEnabled(XGpio *InstancePtr);
u32 XGpio_InterruptGetStatus(XGpio *InstancePtr);

という関数があります。

これらの使い方については次の記事を参考にしてください。
taltalp.hatenablog.jp

Cmod A7のSPIFlashにSDKのプロジェクトを書き込む

概要

Digilent社のCmodA7というFPGAボードにMicroBlazeを実装したBitstreamとそのSDKプロジェクトをSPI Flashに書き込む方法についてです。

この方法はDigilent社の公式HPに、そのやり方が書いてあるのですが、かなり省略された説明でわかりにくかったため、
分かりにくかったところを追記しようと思います。

基本的にはDigilent社のページを見ながらやって見てください。
How To Store Your SDK Project in SPI Flash [Reference.Digilentinc]

vivadoプロジェクトの作成

公式HPの説明ですと、Vivadoプロジェクトについて
「Before you start; this guide assumes that you already have a Microblaze system built complete with Quad SPI, External Memory, and Uart cores, and that you have the appropriate QSpi mode jumper setting. This tutorial takes place in SDK.」
としか書かれていません。

この部分について実際に作ってみましょう。

プロジェクトの作成

プロジェクト名:任意
ボードファイル:CmodA7-XXT (使う予定のボードに合わせてボードファイルを定義してください。)
でプロジェクトを作成してください。

BlockDesignの作成

BlockDesignの名前は適当につけてください。

まず、MicroBlazeを追加して、Run Block Automationを実行します。
このときの設定は、

Preset None
Local Memory 32kB
Local Memory ECC None
Cache Configuration None
Debug Module Debug Only
Peripheral AXI Port Enabled
Interrupt Controller Checked
Clock Connection New Clocking Wizard

としました。
メモリのサイズは適当です。

私は最初、Interrupt Controllerにチェックマークを入れず、自分でAXI Interrupt Controllerを入れていたのですが、なぜかこれでは後にSDKでエラーになってしまい、
かなり苦労しました。

この後は、画像のようにBoardタブより
・System Clock
・Cell Ram
・QSPI Flash
・Reset
・USB UART
を追加して、Run Connection Automationを実行しました。

次に、AXI UARTとAXI QUAD SPIからInterrupt ControllerにInterrupt信号を接続しましたが、これが必要かはわかりません。。。

ここまで終わったら、Generate Bitstreamを実行します。

公式HPに書いてあるBitstreamの圧縮は任意で行ってください。

SDKプロジェクトの作成

ここは公式HPどおりです。
Cmod-A7では、4MBのn25q032a13ef440fというSPI Flashを使っているため、
FLASH_IMAGE_BASEADDRは0x00300000
Program Flash MemoryのFlash Typeはn25q32-3.3v-spi-x1_x2_x4
に設定するというところに気をつける必要があります。

このような設定にしたところ、SPI FlashからSDKのプロジェクトを読み出すことができるようになりました。

さいごに

結局、Interrput Controllerを今回の方法で設定したらうまくいくようになったんですが、理由はよくわかりません。

EZ-BLEでI2C to BLEを試す

今回はCypressのEZ-BLEモジュールでI2C→BLE変換を試してみたいと思います。

EZ-BLEモジュール用プロジェクトの作成

I2C→BLE変換プロジェクトはCypress公式のPSoC-4-BLEのサンプルプロジェクトとして公開されています。
PSoC-4-BLE/100_Projects_in_100_Days/Day035_I2C_BLE_Bridge at master · cypresssemiconductorco/PSoC-4-BLE · GitHub
今回はこれをCYBLE-022001-00用にビルドして用いました。

I2C_BLE_Bridgeサンプルロジェクトを開き、プロジェクトのデバイスを変更します。
プロジェクトを右クリック→Device Selector
からCYBLE-022001-00を選択しました。

次にこちらを参考に、コンポーネントをアップデートしたり、Bootloadableを追加したりします。
必要に応じてピン配置を変える必要もあります。
EZ-BLE PRoC を使ってみる - たるたるソース

ビルドがうまく行ったら書き込みEZ-BLEモジュールの準備は完了です。

PCとの接続

PCとの接続方法はI2C_BLE_BridgeプロジェクトのドキュメントP5~7に書いてあるため省略します。
PSoC-4-BLE/I2C-BLE Peripheral.pdf at master · cypresssemiconductorco/PSoC-4-BLE · GitHub

ドキュメントのFigure 8 Enabling notificationsの画面が表示されればそれ以降はやらなくてOKです。
この画面に受信したデータが表示されます。

EZ-BLEにI2C信号を入力する

今回はI2Cのテスト信号としてESP32を用いました。
I2Cの信号が出ればなんでもOKです。

I2C_BLE_BridgeプロジェクトのI2Cモジュールのプロパティを見ると、slave addressは0x08になっています。
なので、0x08を送ってから送りたいデータを送ればOKです。

ESP32で使用したソースコードはこちらです。

#include <Wire.h>

void setup() {
  int SDA = 25;
  int SCL = 26;

  Wire.begin(SDA, SCL);
}

byte val = 0;

void loop() {
  Wire.beginTransmission(0x08);
  Wire.write(val);
  Wire.endTransmission(true);

  val++;
  delay(20);
}

実際に通信してみる

受信したデータはCySmartの画面から見れます。青いところが受信したデータです。
f:id:taltalp:20170926232257p:plain
通信中のI2C信号も見てみました。 ちゃんとACKが返ってきています。
f:id:taltalp:20170926232335p:plain