Android OS向けGPSドライバ開発:要求仕様の解説と実装ガイド
こんにちは。Turing株式会社でインターンをしている、東京大学学部4年の三輪です。
TuringのUXチームでは、Android OSを採用して独自の車載UI開発を進めています。Android OSはセンターディスプレイにあたる部分で主に利用される予定で、エアコン、ドア、ライトなどの操作をディスプレイ上で行えるようにするほか、ナビアプリや音声アシスタントなどの実装をAndroidプラットフォーム上で進めていく予定です。
自動車に搭載するOSを開発していくうえで、さまざまなハードウェアをOS上で扱えることは必須の要件になります。しかし、Android OSでのハードウェアの取り扱いはベースであるLinuxとは異なる独自の部分が多く、慣れが必要です。
この記事では、GPSデバイスのドライバを実装し、AndroidのネイティブフレームワークからHALを介してGPSデバイスを透過的に扱えるようにする方法について説明します。
HALとドライバの考え方
デバイスドライバ
そもそも、デバイスドライバとは何でしょうか。
デバイスドライバは、OSがコンピュータと接続した機器を扱うために利用されるプログラムです。こう説明すると抽象的ですが、以下のように考えるとわかりやすいかもしれません。
一般に、OS上で動作するプログラムはOSが提供するAPIを通じてハードウェアを制御します。しかし、OSがすべてのハードウェアの仕様を把握するのは現実的に不可能です。
そこで、代わりにOSはドライバがどのようなAPIを実装しなければならないかを事前に決定します。ドライバの側はOSの要求に合わせて適切にAPIを実装し、その中でハードウェアを操作します。このようにすると、ハードウェアに合わせたドライバがあれば、OSはそのドライバが提供するAPIを介してハードウェアを制御できるようになります。
従って、ドライバを実装する際は、OSが要求するフォーマットに合わせて、ハードウェアを操作するAPIを提供する必要があります。
今回扱うのは、GPSデバイスです。より複雑なハードウェアの場合には組み込みの深い知識が必要になりますが、今回はキャラクタデバイスを介してデータを読み出すだけで済むため、難易度はそこまで高くありません。さらに、実はオープンソースの実装(後述)もあるため、それを参照することができます。
つまり、ドライバの実装と言いつつも、やることはキャラクタデバイスを読み取り、OSのAPIに合わせた形に加工して提供する層を実装するだけです。
Hardware Abstraction Layer(ハードウェア抽象化層, HAL)
上の図は非常に単純化した関係図ですが、Androidにおいてはもう少し複雑な構造になっています。
Androidプラットフォームでアプリケーションを開発する場合、開発者に直接見えているのはLocationManagerをはじめとするandroid.location
のAPIです[1]。これは内部的にcom.android.server.location.gnss
というAPIを利用します。
ここで登場するのがHAL(Hardware Abstraction Layer)です。HALはBluetoothやGnss、Cameraなど、ハードウェアごとに事前に定義されています。このような中間層を挟むことによって、OSの更新時のメリットや、コピーレフトライセンスとの兼ね合いなど、さまざまな点でメリットがあります。
先ほどのcom.android.server.location.gnss
は、GnssのHALへのエントリであるGnssNative
というAPIを経由し、JNI(Java Native Interface, JavaとC++のinteroperationに利用される)のレイヤであるandroid::gnss
を挟んでandroid::hardware::gnss
を利用します。これはHALのデフォルト実装にあたります。この後説明しますが、この中でhardware/gps.h
を経由して、GPSのドライバが利用されます。
ドライバの要求仕様を特定するには、この図の下半分辺りを見ていくのが良いでしょう。
ドライバの要求仕様の特定
まずはhardware/interfaces
を見てみます。ここがハードウェアに関係するHALが集まったディレクトリです。この中に「gnss」という項目があります。
$ ls hardware/interfaces
... gnss ...
この「gnss」の中身は以下のようになっています。
$ ls hardware/interfaces/gnss
1.0 1.1 2.0 2.1 aidl common measurement_corrections visibility_control
フォルダはHALのバージョンで分けられています。今回は1.0を見ていきます。
$ ls hardware/interfaces/gnss/1.0
Android.bp IGnssBatching.hal IGnssGeofencing.hal IGnssNiCallback.hal
IAGnss.hal IGnssBatchingCallback.hal IGnssMeasurement.hal IGnssXtra.hal
IAGnssCallback.hal IGnssCallback.hal IGnssMeasurementCallback.hal IGnssXtraCallback.hal
IAGnssRil.hal IGnssConfiguration.hal IGnssNavigationMessage.hal default
IAGnssRilCallback.hal IGnssDebug.hal IGnssNavigationMessageCallback.hal types.hal
IGnss.hal IGnssGeofenceCallback.hal IGnssNi.hal vts
.hal
というファイルがたくさんあるのがわかります。これがHALのインターフェース定義を記述したファイルで、HIDLと呼ばれる言語で書かれています[2]。
同一階層に、default
というディレクトリがあります。この中に、HALのデフォルト実装が存在します。先ほどの図でandroid::hardware::gnss
として示したレイヤです。
$ ls hardware/interfaces/gnss/1.0/default/
AGnss.cpp GnssBatching.h GnssMeasurement.h GnssXtra.h
AGnss.h GnssConfiguration.cpp GnssNavigationMessage.cpp OWNERS
AGnssRil.cpp GnssConfiguration.h GnssNavigationMessage.h ThreadCreationWrapper.cpp
AGnssRil.h GnssDebug.cpp GnssNi.cpp ThreadCreationWrapper.h
Android.bp GnssDebug.h GnssNi.h android.hardware.gnss@1.0-service.rc
Gnss.cpp GnssGeofencing.cpp GnssUtils.cpp android.hardware.gnss@1.0-service.xml
Gnss.h GnssGeofencing.h GnssUtils.h service.cpp
GnssBatching.cpp GnssMeasurement.cpp GnssXtra.cpp
名前が対応するのが対応するHALの実装です。
HALの実装についてはこちらも参考になります。
Gnss.h
を見てみると、#include <hardware/gps.h>
という行があります。実はこの/hardware/libhardware/include/hardware/gps.h
がドライバのヘッダファイルです。従って、ドライバの要求仕様はこのファイルを見ていけばわかります。
まとめると、要求仕様を特定するには、以下の二点が重要です。
-
hardware/interfaces
の中にあるHALと、その実装を参照する -
hardware/libhardware/include/hardware
の中にあるヘッダファイルを参照する
では、これらの情報に基づいてドライバを実装します。
ドライバの実装
ドライバの実装は次のようになります。
まず、入口となるのがhw_module_t HAL_MODULE_INFO_SYM
の実装です。HAL_MODULE_INFO_SYM
はマクロで、実際にはHMI
という名前になっていますが、この宣言ではHAL_MODULE_INFO_SYM
を利用するのが慣習です。ドライバはhw_module_t
型のHMI
を必ず実装する必要があります。
struct hw_module_t HAL_MODULE_INFO_SYM = {
.tag = HARDWARE_MODULE_TAG,
.version_major = 1,
.version_minor = 0,
.id = GPS_HARDWARE_MODULE_ID,
.name = "Dummy GPS Module",
.author = "The Android Open Source Project",
.methods = &gps_module_methods,
};
この中で.methods
フィールドにgps_module_methods
のポインタを指定しています。gps_module_methods
はhw_module_methods_t
という型の値で、open
という関数を実装する必要があります。hw_module_methods_t
は様々なドライバで共通の型です。
static struct hw_module_methods_t gps_module_methods = {
.open = open_gps
};
ここで、open
はhardware/libhardware/include/hardware/hardware.h
において以下のように宣言されています。open
はドライバのイニシャライザやコンストラクタのようなものだと考えるとわかりやすいと思います。
typedef struct hw_module_methods_t {
/** Open a specific device */
int (*open)(const struct hw_module_t* module, const char* id,
struct hw_device_t** device);
} hw_module_methods_t;
ここでhw_device_t
のポインタのポインタが渡ってきていることに注目します。open_gps
ではこのポインタを変更します。
static int open_gps(const struct hw_module_t* module, char const* name, struct hw_device_t** device)
{
struct gps_device_t *dev = (struct gps_device_t *)malloc(sizeof(struct gps_device_t));
memset(dev, 0, sizeof(*dev));
dev->common.tag = HARDWARE_DEVICE_TAG;
dev->common.version = 0;
dev->common.module = (struct hw_module_t*)module;
dev->get_gps_interface = gps_get_hardware_interface;
*device = (struct hw_device_t*)dev;
return 0;
}
gps_get_hardware_interface
がGPS関係のインターフェースの実装です。
common
はgps_device_t
の最初のメンバであるため、*device = &dev->common
の行は、gps_device_t
へのポインタを渡したことになります。
struct gps_device_t {
struct hw_device_t common;
/**
* Set the provided lights to the provided values.
*
* Returns: 0 on success, error code on failure.
*/
const GpsInterface* (*get_gps_interface)(struct gps_device_t* dev);
};
ここまでまとめると、ドライバは、ドライバに関する情報をhw_module_t
型の値をHMI
という名前で公開し、その中でhw_module_methods_t
へのアクセスを提供します。hw_module_methods_t
はopen
という関数を提供し、open
が実行されるとGPSデバイス固有のインターフェースを取得する関数であるgps_get_hardware_interface
へのアクセスが提供されます。
さらに読んでいきましょう。gps_get_hardware_interface
の実装はこのようになっています。
const GpsInterface serialGpsInterface = {
sizeof(GpsInterface),
serial_gps_init,
serial_gps_start,
serial_gps_stop,
serial_gps_cleanup,
serial_gps_inject_time,
serial_gps_inject_location,
serial_gps_delete_aiding_data,
serial_gps_set_position_mode,
serial_gps_get_extension,
};
const GpsInterface* gps_get_hardware_interface()
{
D("GPS dev get_hardware_interface");
return &serialGpsInterface;
}
GpsInterface
はGPS関係のAPIを集約した型です。つまり、これらのAPIを一つ一つ実装するのが、ドライバの実装ということになります。
GpsInterface
に要求される実装
GpsInterface
はgps.h
で定義されているので、これを読みます。
GPSドライバは、次の関数を実装する必要があります。それぞれの関数について簡単に見ておきましょう。
-
int (*init)( GpsCallbacks* callbacks );
初期化時に呼び出される。コールバックが渡ってくるので、これを保持しておいて以降適宜呼び出す。
-
int (*start)( void );
GPSデータの読み出しを開始したタイミングで呼び出される。
-
int (*stop)( void );
GPSデータの読み出しを停止したタイミングで呼び出される。
-
void (*cleanup)( void );
インターフェースが閉じられる際に呼び出される。リソースなどを開放する。
-
int (*inject_time)(GpsUtcTime time, int64_t timeReference, int uncertainty);
現在時刻を注入する。最低限の動作においては不要。
-
int (*inject_location)(double latitude, double longitude, float accuracy);
ほかのLocation Provider経由で位置情報を注入する。latとlonはdegrees、accuracyはmeter. 最低限の動作においては不要。
-
void (*delete_aiding_data)(GpsAidingData flags);
次にスタートするとき、フラッグで示された情報を利用しないという情報を受け取る。最低限の動作においては不要。
-
int (*set_position_mode)(GpsPositionMode mode, GpsPositionRecurrence recurrence, uint32_t min_interval, uint32_t preferred_accuracy, uint32_t preferred_time);
GPSのポジションモードを指定する。最低限の動作においては不要。
-
get_extension
GPS以外の拡張機能がある場合、その機能のためのAPIを関数ポインタとして返す。
init
前後で呼び出される。最低限の動作においては不要。今回は実装していない。実装例
static const void * dummy_gps_get_extension(const char *__unused name) { const void *ret_val = NULL; if (strcmp(name, GPS_MEASUREMENT_INTERFACE) == 0) { D("gps_get_extension() special support for gps_measurement"); ret_val = &dummyGpsMeasurementInterface; } return ret_val; }
実装する場合、インターフェースなどはGPSドライバ自体の実装と同じ方式でやっていくことになります。
以上で、実装すべきAPIがわかりました!
実装
今回扱うGPSデバイスは、シリアル通信によってNMEA 0183フォーマットでGPSデータを出力します。デバイスとの接続はUSBです。これは市販のGPSデバイスとして一般的な構成だと思います。
前提としてAndroidがGPSデバイスを認識できる必要があります。これを確認する方法はいくつかありますが、Androidのデバッグツールであるadb(Android Debug Bridge)を用いて端末内に入って確認するのがスムーズです。
$ adb shell ls /dev
... ttyUSB0 ...
$ adb shell cat /dev/ttyUSB0
... (NMEA 0183フォーマットのGPSデータ)
GPSデバイスが認識されない場合
/dev/ttyUSB0
やその他の名前でGPSデバイスが認識されていない場合、いくつかの可能性が考えられます。
ここでは解決のための一般的なステップを紹介します。
-
lsusb
などのコマンドで認識されているか試す- 認識されていない場合、デバイス自体に問題がある可能性もあります
-
dmesg
を確認し、USB Serialドライバが正しく動作しているか確認する- USB自体は認識されているがUSB Serialドライバが動作していない場合、GPSデバイスに対応するUSB SerialドライバがOS側に存在していない可能性があります。
- Android本体に手を加えられるのであれば、カーネルのビルドオプションを変更することで当該USBデバイス向けのシリアルドライバを追加する必要があるかもしれません。
このように、デバイスファイルを通してGPSのテキストデータにアクセスできる状態から、ドライバの開発を始めます。
実装方針
必要な実装はわかっているので、実装方針を立てます。やることはシンプルです。
まず、GPSデバイスの読み出しを行うスレッド(無限ループ)を実装します。次に、その中でNMEA 0183フォーマットのGPSデータをパースします。最後に、init
時に渡されたコールバックを呼び出し、位置情報の更新をトリガーします。
実際の実装には、start
/stop
の管理やエラー処理、configなどの読み取りが必要ですが、大部分は普通のソフトウェアの実装と変わりません。
ビルドシステムへの追加
以上の方法でドライバを実装していくことができますが、これをコンパイルするにはOS全体のビルドにドライバのビルドを組み込む必要があります。今回は特定のAndroidデバイス専用のドライバとして実装したので、device/vendor/gps/
の位置にソースコードを配置します。
単にソースコードを置いただけではビルドされず、device/vendor/gps/Android.mk
というビルドスクリプトを追加します。これは親ディレクトリのdevice/vendor/Android.mk
によって自動的に呼び出されます。
何点か気を付けなければならないことがあります。
-
LOCAL_MODULE_PATH
かLOCAL_MODULE_RELATIVE_PATH
を適切に指定する必要があります。lib/lib64
の両方向けにビルドするためには、LOCAL_MODULE_RELATIVE_PATH
を利用します。 -
LOCAL_PROPRIETARY_MODULE
をtrueにする必要があります。これをしないとビルドスクリプトが実行されてもビルド成果物が出力されません。
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# Multitarget (lib/lib64) 向けビルドではLOCAL_MODULE_RELATIVE_PATHを指定する
LOCAL_MODULE_RELATIVE_PATH := hw
# Shared Libraryを指定する
LOCAL_SHARED_LIBRARIES := liblog libcutils libhardware
# Vendor Moduleのビルドの場合は、必ずLOCAL_PROPRIETARY_MODULE := trueを指定する必要がある
LOCAL_PROPRIETARY_MODULE := true
# Source Fileを指定する
LOCAL_SRC_FILES := misc.c interface.c state.c minmea.c gps_status.c reader.c notifier.c device.c gps.c load_config.c
# Module名を指定する(.soは不要)
LOCAL_MODULE := gps.default
# Shared Libraryをビルドする
include $(BUILD_SHARED_LIBRARY)
詳しくは以下を参照してください。
このようにすることで、ビルド後に適切な位置にドライバのShared Libraryが配置されます。
$ ls /vendor/lib64/hw
... gps.default.so ...
モック実装を参照する
以上は一般的な議論ですが、AOSPのソースコードには参考にできる実装が含まれていることがあります。GPSドライバの場合は、device/google/gs101/gnss/
がモック実装になっており、上記のAndroid.mk
の実装や、gps.c
のモック実装など、ドライバのおおよその構成を把握することができます。
このように、上で説明したドライバの構成の知識とともに、適宜あたりをつけてモック実装などを探すことで、より円滑に要求仕様を特定したり、実装の方針を検討することができます
オープンソースの実装を利用する
GPSドライバの要件と、実際に実装する方法がわかったので、本格的に実装していくことができます。
ただし、GPLv2ライセンスのオープンソース実装があるのでこれをベースとすることもできます。
このドライバ実装では今回扱ったようなNMEA 0183フォーマットでGPSデータをしゃべるGPSモジュールを扱っており、最低限の動作であれば、ほとんど変更なく使えます。
社内で試験的にこの実装を利用した際には、二か所のみ変更がありました。調達したデバイスによってこの程度の変更は必要になるかもしれませんが、基本的にはこのまま活用できると思います。
- GPGSAの出力仕様が異なるモジュールだったので、それに合わせてパーサを部分的に修正しました
- この実装ではSerialデバイスのデバイスファイルとボーレートをカーネルプロパティ(
ro.kernel.android.gps
)から読んでいましたが、既存のGPSデバイス向けの設定ファイルに合わせました
あとは要件に合わせてドライバの実装を改善していくことができます。
まとめ
この記事ではAndroidでのGPSデバイスのドライバ実装について解説しました。HAL周りにはAndroid固有の考え方が多く難しい部分がありますが、GPSドライバを追加する程度であれば今回のようにあまり労力なく行えます。
今回は物理デバイスを利用するためにGPSのHALを参照してドライバの実装を行いましたが、車載OSとしてTuringが採用するAndroid Automotive OSではVehicle HAL(VHAL)が多用されており、OSレイヤの業務ではHALに関連した実装を実際にバリバリやっていくことになります。Android Automotive OSに興味のある方はこちらもご覧ください。
TuringのUXチームではさらに、Androidアプリやサーバサイドのタスク、音声認識まで、多様な範囲のエンジニアリングを扱っています。こうした開発に興味がある方はぜひ、弊社求人一覧およびWantedlyをご覧ください。
Discussion
AAOSのHALについて、このような詳細かつ実用的な情報がどこにも見当たらなかったので大変勉強になりました。ありがとうございます!
「ドライバの実装」の章から理解が追い付かなくなったのですが、以下2つのファイルをもとに、既存の実装内容を整理しているという内容であっていますでしょうか?
HAL_MODULE_INFO_SYMやopen_gps()、GpsInterfaceなどの実装が見当たらなかったのですが、どこで定義されているのでしょうか?