本記事の概要
前回の記事から、レーザー測距センサVL53L0XをZynqで動かす方法を紹介しています。
VL53L0XはSTマイクロ社から販売されているレーザー測距センサです。このセンサは対象までの距離を測定する測距センサで、レーザーが対象に反射してから戻ってくるまでの時間から対象までの距離を計算しています。
前回の記事で、Zynqで制御するためのプラットフォームをVivadoで作成しました。
次に、作成したプラットフォーム上で動作するアプリケーションをVitisでビルドしたいと思います。アプリケーションの作成に当たり、STマイクロ社から公開されているVL53L0Xを扱うためのAPIを利用しました。
本記事では、実際にVitisで動作するアプリケーションを例に、APIの利用方法を紹介いたします。
目標
前回から引き続き、Zynqが1秒おきに割り込みでVL53L0Xから距離をI2C通信で読み出すようなアプリケーションを作成します。読み出した距離をPCにUARTで送信し、PCは受信した距離を表示するようにしています。
VL53L0Xライブラリのダウンロード
VL53L0Xを動かす正規のAPIはSTマイクロ社のホームページからダウンロードすることが可能です。ただ、低レイヤーにあたるI2C通信部分のコードが理解しづらかったので、今回はサードパーティから提供されるArduinoのコードを改良することにしました、
元にしたArduinoのコードは、Seeed studio社のライブラリを利用しました。GithubにArduinoのソースコードと用例が用意されています。このGithubからライブラリをダウンロードしました。
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ライブラリに実装しました。
他にも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というライブラリを作成しました。
ソースコードが長いのと、正直スレーブ部分を実装していない状態では未完成と言わざるを得ないので、本記事では公開を控えます。
最上位のmain関数のソースコード
ソースコード例
srcのフォルダ構成です。ダウンロードしたGroveフォルダと作成したXWireフォルダを、srcフォルダに入れます。
なお、Groveフォルダ内でWireを参照している関数はすべてXWireに変更します。
ソース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秒を測るようにしました。
[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から流用できるようにすると、既存のライブラリを活用できてよい方法かなと思いました。
最後までご覧いただきありがとうございました!
コメント