測距センサとZynqをI2C通信できるようにWireクラスを変更

Xilinx SoC
スポンサーリンク

本記事の概要

概要

レーザー測距センサVL53L0XをZynqとをI2C通信できるようにしました。
ArduinoのWireクラスをZynqで使えるように変更し、VL53L0XとI2C通信するためのZynqのアプリケーションプロジェクトを作成しました。

前回の記事から、レーザー測距センサVL53L0XをZynqで動かす方法を紹介しています。

VL53L0XはSTマイクロ社から販売されているレーザー測距センサです。このセンサは対象までの距離を測定する測距センサで、レーザーが対象に反射してから戻ってくるまでの時間から対象までの距離を計算しています。

前回の記事で、Zynqで制御するためのプラットフォームをVivadoで作成しました。

次に、作成したプラットフォーム上で動作するアプリケーションをVitisでビルドしたいと思います。アプリケーションの作成に当たり、STマイクロ社から公開されているVL53L0Xを扱うためのAPIを利用しました。

本記事では、実際にVitisで動作するアプリケーションを例に、APIの利用方法を紹介いたします。

本記事の対象読者:以下の状況に直面している方
  • VL53L0XをZynqで使いたい
  • ZynqをマスターとしたI2C通信の用例を確認したい
  • VL53L0XのAPIの利用方法を知りたい

目標

前回から引き続き、Zynqが1秒おきに割り込みでVL53L0Xから距離をI2C通信で読み出すようなアプリケーションを作成します。読み出した距離をPCにUARTで送信し、PCは受信した距離を表示するようにしています。

VL53L0Xライブラリのダウンロード

VL53L0Xを動かす正規のAPIはSTマイクロ社のホームページからダウンロードすることが可能です。ただ、低レイヤーにあたるI2C通信部分のコードが理解しづらかったので、今回はサードパーティから提供されるArduinoのコードを改良することにしました、

元にしたArduinoのコードは、Seeed studio社のライブラリを利用しました。GithubにArduinoのソースコードと用例が用意されています。このGithubからライブラリをダウンロードしました。

GitHub - Seeed-Studio/Grove-Ranging-sensor-VL53L0X: Grove Ranging sensor VL53L0X-World’s smallest Time-of-Flight ranging and gesture detection sensor
Grove Ranging sensor VL53L0X-World’s smallest Time-of-Flight ranging and gesture detection sensor - Seeed-Studio/Grove-Ranging-sensor-VL53L0X

ArduinoプログラムをZynqで使えるようにする

GithubからダウンロードしたArduinoのソースコードは、そのままZynqで利用することはできません。なぜなら、Arduinoのソースコードの建付けは図のようになっており、ZynqではなくArduino基板に実装されるavrマイコンを動かすためのコードだからです。

Arduinoではmain関数は見えないように隠されていますが、実際はsetup関数で初期設定をして、無限ループ内でloop()関数を呼び出すような処理がなされています。

Seeed studioの用例(例えば、continuous_ranging.inoなど)では、setup関数やloop関数の中でVL53L0Xのライブラリを参照しています。Seeed_vl53l0x.hというヘッダファイルを参照し、この中でVL53L0Xを動かすための関数が定義されています。

VL53L0Xのライブラリは、さらに低レイヤのArduinoのWireライブラリを参照しています。Wireライブラリを使って、Arduinoの基板上に実装されているAVRマイコンを制御し、I2C通信を行うペリフェラルを動かすことができます。

この建付けを参考に、私はWireライブラリを改変し、AVRマイコンではなく、ZynqのI2C通信を行うペリフェラルを制御するライブラリを作成しました。

WireライブラリのZynq版XWire

Wireライブラリを改変したZynq版のライブラリをXWireという名前にしました。XWireを使ったプログラムの建付けは図のようにしています。VL53L0XライブラリとZynqのドライバAPIとの間で仲介役の位置づけです。

このようにして、公開されているVL53L0Xのライブラリを、ほぼ変更なしに使えるようにしています。名称をWireからXWireに変えたため、一部の参照するヘッダファイル名や関数名は変える必要はありますが、比較的少ない変更で済むのではないかと思います。

XWireライブラリは、Wireライブラリの構成に倣い、XTwoWireというクラスを定義しています。このクラス内でWireと同じメソッドを用意しておきます。基本的にはWire.hやWire.cppをそのまま流用しています。

Wire.hやWire.cppはさらに低レイヤーのライブラリtwi(twi.c, twi.h)を参照しています。このtwiライブラリは完全にArduino.hやらavr/io.hやらArduinoに依存したコードになっています。したがって、このtwiライブラリを完全に書き換えて、ZynqのドライバAPIを参照するようなxtwiライブラリを新しく作りました。

twiライブラリでは多くの関数が定義されていますが、このうちZynqをマスターとして動作させるのに必要になった以下の関数をxtwiライブラリに実装しました。

xtwiライブラリに実装した関数
  • xtwi_init: I2C通信の初期化
  • xtwi_setFrequency: I2C通信周期を設定
  • xtwi_readFrom: マスターとして扱うときのスレーブからの読み出し
  • xtwi_writeTo: マスターとして扱うときのスレーブへの書き込み

他にもZynqがスレーブになるときの関数を定義してもよかったのですが、直近で必要になった上記の関数のみを定義しておきました。

xtwiライブラリに実装した関数では、ZynqのドライバAPIを参照するようにしています。以前の記事でも紹介しましたが、Vitisでは各種ペリフェラルを制御するためのドライバが用意されています。

例えば、I2C通信を行うためのライブラリとしてxiicpsが用意されています。そこで、xtwiライブラリがZynqのI2C通信を行うペリフェラルを制御するライブラリxiicpsを参照するようにしました。

具体例を挙げると…

uint8_t twi_readFrom(uint8_t address, uint8_t* data, uint8_t length, uint8_t sendStop)

という関数がtwi.cには定義されています。新しく作ったxtwi.cにも、同様の関数xtwi_readFromを用意しました。このとき、twi_readFromが特定のアドレスのデバイスからI2C通信でデータを読み出すという処理をしているので、Zynqがマスターになってデータを受信する処理

XIicPs_MasterRecvPolled(&Iic, xtwi_masterBuffer, length, address);

を利用して、

uint8_t xtwi_readFrom(uint8_t address, uint8_t* data, uint8_t length, uint8_t sendStop)

という関数を作成しています。同様に、writeToやsetFrequencyもxiicpsの関数を利用して、できるだけ楽にライブラリを作成しています。

また、Arduinoでmillisやmicrosというミリ秒やマイクロ秒を測るカウンターが用意されているのですが、Zynqではそういった関数を定義していないのでエラーがでました。そこで、同じようにミリ秒とマイクロ秒を測る関数を、xtime_l.hのXTime_GetTimeというZynq内の処理経過時間を計測する関数を利用して作成しています。これをavr2xf_typesというライブラリに用意し、XWireフォルダ内に作成しました。これを先ほどの図に入れ込むと、このようになります。

以上の構成のもと、XWireというライブラリを作成しました。

ソースコードが長いのと、正直スレーブ部分を実装していない状態では未完成と言わざるを得ないので、本記事では公開を控えます。

XWireライブラリの構成
  • XWireフォルダ
    • XWire.cpp, XWire.h
    • utilityフォルダ
      • xtwi.c, xtwi.h
      • avr2xf_type.cpp, avr2xf_type.h

最上位のmain関数のソースコード

ソースコード例

srcのフォルダ構成です。ダウンロードしたGroveフォルダと作成したXWireフォルダを、srcフォルダに入れます。

なお、Groveフォルダ内でWireを参照している関数はすべてXWireに変更します。

フォルダ構成
  • srcフォルダ
    • Groveフォルダ:GithubのSeeed studio社のページからダウンロードしたVL53L0Xライブラリ
    • XWireフォルダ:前節で説明したフォルダ
    • vl53l0x.cppファイル:main関数、setup関数、loop関数を定義

ソースvl53l0x.cppではArduinoと同様にsetup関数とloop関数を宣言し、それぞれ初期化とループ中の動作を記述しました。タイマーからの割り込みの発生したタイミングでVL53L0Xからの読み出しを行いたいので、ループは単なる無限ループとし、setup関数のなかで割り込みハンドラTMR_Intr_Handlerを登録しました。このあたりの記述はタイマーを使ったLED点滅回路と同様です。

図でいうところの水色の部分に対応します。main関数と、下位のsetup関数を別ファイルに分けてもよいのですが、正直main関数が短すぎるので1ファイルにまとめました。このvl53l0x.cppのコードを紹介します。

#include "xtmrctr.h"
#include "xscugic.h"
#include "Grove/Seeed_vl53l0x.h"

Seeed_vl53l0x VL53L0X;
XScuGic GICInst;
XTmrCtr TMRInst;

#define INTC_DEVICE_ID          XPAR_PS7_SCUGIC_0_DEVICE_ID
#define TMR_DEVICE_ID           XPAR_TMRCTR_0_DEVICE_ID
#define INTC_TMR_INTERRUPT_ID   XPAR_FABRIC_AXI_TIMER_0_INTERRUPT_INTR
#define TMR_LOAD                100000000 - 2 //clock frequency 100MHz, Load period 1 sec

// -------- function prototypes
static void TMR_Intr_Handler(void *baseaddr_p);

//----------------------------------------------------------------------------
void setup()
{
    int status;

    //-----------------------------------------------------
    // 0. 例外処理
    //-----------------------------------------------------
    // 0.1 ARMプロセッサの割り込み発生時に呼び出されるインスタンス(今回はGIC)の割り込みハンドラを登録
    Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
            (Xil_ExceptionHandler) XScuGic_InterruptHandler,
            &GICInst);
    // ARMの割り込みをイネーブル
    Xil_ExceptionEnable();

    //-----------------------------------------------------
    // 1. GIC(Generic Interrupt Controller)
    //-----------------------------------------------------
    // 1.1 GICの初期化
    XScuGic_Config *IntcConfig;
    IntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);
    status = XScuGic_CfgInitialize(&GICInst, IntcConfig,
            IntcConfig->CpuBaseAddress);
    if (status != XST_SUCCESS) {
        xil_printf("GIC initialization failed!\n");
    }
    
    // 1.2 GIC割り込み発生時に呼び出されるインスタンス(今回はタイマー)の割り込みハンドラを登録
    status = XScuGic_Connect(&GICInst, INTC_TMR_INTERRUPT_ID,
            (Xil_ExceptionHandler)XTmrCtr_InterruptHandler, (void *) &TMRInst);
    if (status != XST_SUCCESS) {
        xil_printf("GIC initialization failed!\n");
    }
    // GICの割り込みをイネーブル
    XScuGic_Enable(&GICInst, INTC_TMR_INTERRUPT_ID);

    //-----------------------------------------------------
    // 2. AXI Timer
    //-----------------------------------------------------
    // 2.1 AXI Timerの初期化
    status = XTmrCtr_Initialize(&TMRInst, TMR_DEVICE_ID);
    if (status != XST_SUCCESS) {
        xil_printf("timer initialization failed!\n");
    }

    // 2.2 AXI Timer割り込み発生時に呼び出されるインスタンス(今回はタイマー)の割り込みハンドラを登録
    XTmrCtr_SetHandler(&TMRInst, (XTmrCtr_Handler) TMR_Intr_Handler, &TMRInst);
    
    // 2.3 AXI Timerのリセット値の設定
    XTmrCtr_SetResetValue(&TMRInst, 0, TMR_LOAD);

    // 2.4 AXI Timer optionの設定 (Interrupt Mode And Auto Reload )
    XTmrCtr_SetOptions(&TMRInst, 0,
            XTC_INT_MODE_OPTION | XTC_AUTO_RELOAD_OPTION | XTC_DOWN_COUNT_OPTION);
  
    //-----------------------------------------------------
    // 3. VL53L0X 初期化
    //-----------------------------------------------------
    VL53L0X_Error Status = VL53L0X_ERROR_NONE;
    Status = VL53L0X.VL53L0X_common_init();
    if (VL53L0X_ERROR_NONE != Status) {
        xil_printf("start vl53l0x mesurement failed!\n");
        VL53L0X.print_pal_error(Status);
        while (1);
    }
    VL53L0X.VL53L0X_continuous_ranging_init();
    if (VL53L0X_ERROR_NONE != Status) {
        xil_printf("start vl53l0x mesurement failed!\n");
        VL53L0X.print_pal_error(Status);
        while (1);
    }
    
    //-----------------------------------------------------
    // 4. タイマーのカウント開始
    //-----------------------------------------------------
    XTmrCtr_Start(&TMRInst, 0);
}

void loop() {}

//-------------------------------------------------
// A. ユーザー割り込み処理関数の定義
//-------------------------------------------------
void TMR_Intr_Handler(void *data)
{
    VL53L0X_RangingMeasurementData_t RangingMeasurementData;
    VL53L0X.PerformContinuousRangingMeasurement(&RangingMeasurementData);
    if (RangingMeasurementData.RangeMilliMeter >= 2000) {
        xil_printf("out of ranger\n");
    } else {
        xil_printf("distance::");
        xil_printf("%u\n", RangingMeasurementData.RangeMilliMeter);
    }
}


//----------------------------------------------------------------------------
int main(int argc, char* argv[])
{
    setup();

    while(1){
        loop();

    }

    return 0;
}

ソースコードの解説

ソースコードでは、主にsetup関数と割り込みハンドラTMR_Intr_Handlerを定義し、そのほかは特筆すべきことは行っていません。

setup関数

setup関数のフローチャートを図に示します。

最初に、割り込みの処理を指定しています。割り込みの処理の詳細は以下の記事をご覧ください。この記事を応用しています。

AXI Timerでタイマーのリセット値をTMR_LOADで指定します。TMR_LOADを100,000,000とし、そこからカウントダウンして、1秒を測るようにしました。

AXI Timerの設定

AXI Timerではカウントダウンモードとカウントアップモードがあります。デフォルトではカウントアップモードに設定されていて、

TIMING_INTERVAL = (MAX_COUNT – TMR_LOAD + 2) * AXI_CLOCK_PERIOD

という関係式で1周期が決定されています。MAX_COUNTは32bitなら、2^32-1という値になります。

カウントダウンモードに設定すると、

TIMING_INTERVAL = (TMR_LOAD + 2) * AXI_CLOCK_PERIOD

という関係式になるので、1周期がより分かりやすくなります。ただ、

XTmrCtr_SetOptions(&TMRInst, 0, XTC_INT_MODE_OPTION | XTC_AUTO_RELOAD_OPTION | XTC_DOWN_COUNT_OPTION);

のようにして、XTC_DOWN_COUNT_OPTIONを設定するのを忘れないようにしましょう。

[3. VL53L0X 初期化]でXWireの初期化とVL53L0Xのライブラリを参照して、VL53L0Xの初期化を行いました。これは、Seeed studioからダウンロードしたexampleのうち、スケッチcontinuous_ranging.inoのコードのsetup関数の一部をそのまま使用しています。

割り込みハンドラTMR_Intr_Handler関数

割り込み発生時の動作をTMR_Intr_Handler関数で指定しています。ここもスケッチcontinuous_ranging.inoのコードのloop関数の中身をそっくりそのまま使用しています。

動作検証

では、このソースコードをビルドし、Zyboに実装してみましょう。

写真のような配置にして、TeraTermを起動して正しく距離が表示されるかを確認しました。

測距信号は受信できているようです。定規で確認したところ、常に20mm程度大きな値が出力されるので、キャリブレーションをしないといけないようです。ばらつきは標準偏差で2-3%くらいなのでデータシート通りかなという印象です。

ひがし
ひがし

測距センサの信号を受信できるようになると楽しいですね。Arduino向けのライブラリは巷に溢れていますが、Zynq向けにはほとんどありません。そこで、今回のように一部のライブラリを書き換えて、Arduinoから流用できるようにすると、既存のライブラリを活用できてよい方法かなと思いました。

最後までご覧いただきありがとうございました!

スポンサーリンク


参考:コードの動作を確認したシステムの構成

開発環境

環境
  • 開発用PC: Windows 10, 64bit
    • Vivado Design Suite – HLx Edition – 2020.2
    • Vitis コア開発キット – 2021.2
  • 開発用基板: Zybo Zynq-7010評価ボード(Board Rev.4)
    • Zynq XC7Z010-1CLG400C

開発ボード Zybo Zynq-7010評価ボード

コメント