🌐

Android OS向けGPSドライバ開発:要求仕様の解説と実装ガイド

2023/06/26に公開

こんにちは。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がすべてのハードウェアの仕様を把握するのは現実的に不可能です。

Native APIがハードウェアにリクエストを送ろうとしても、OSにはハードウェアの仕様がわからない。

そこで、代わりにOSはドライバがどのようなAPIを実装しなければならないかを事前に決定します。ドライバの側はOSの要求に合わせて適切にAPIを実装し、その中でハードウェアを操作します。このようにすると、ハードウェアに合わせたドライバがあれば、OSはそのドライバが提供するAPIを介してハードウェアを制御できるようになります。

OSとハードウェアの間にドライバが挟まることで、リクエストが適切に届くようになる。

従って、ドライバを実装する際は、OSが要求するフォーマットに合わせて、ハードウェアを操作するAPIを提供する必要があります。

今回扱うのは、GPSデバイスです。より複雑なハードウェアの場合には組み込みの深い知識が必要になりますが、今回はキャラクタデバイスを介してデータを読み出すだけで済むため、難易度はそこまで高くありません。さらに、実はオープンソースの実装(後述)もあるため、それを参照することができます。

つまり、ドライバの実装と言いつつも、やることはキャラクタデバイスを読み取り、OSのAPIに合わせた形に加工して提供する層を実装するだけです。

Hardware Abstraction Layer(ハードウェア抽象化層, HAL)

上の図は非常に単純化した関係図ですが、Androidにおいてはもう少し複雑な構造になっています。

AndroidにおけるNative APIからハードウェアまでのレイヤ。

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の実装についてはこちらも参考になります。

https://source.android.com/docs/core/architecture/hidl/memoryblock?hl=ja

Gnss.hを見てみると、#include <hardware/gps.h>という行があります。実はこの/hardware/libhardware/include/hardware/gps.hがドライバのヘッダファイルです。従って、ドライバの要求仕様はこのファイルを見ていけばわかります。

まとめると、要求仕様を特定するには、以下の二点が重要です。

  1. hardware/interfacesの中にあるHALと、その実装を参照する
  2. 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_methodshw_module_methods_tという型の値で、openという関数を実装する必要があります。hw_module_methods_tは様々なドライバで共通の型です。

static struct hw_module_methods_t gps_module_methods = {
    .open = open_gps
};

ここで、openhardware/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関係のインターフェースの実装です。

commongps_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_topenという関数を提供し、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に要求される実装

GpsInterfacegps.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_PATHLOCAL_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)

詳しくは以下を参照してください。

https://source.android.com/docs/core/audio/implement-shared-library?hl=ja

https://developer.android.com/ndk/guides/android_mk?hl=ja

このようにすることで、ビルド後に適切な位置にドライバのShared Libraryが配置されます。

$ ls /vendor/lib64/hw
... gps.default.so ...

モック実装を参照する

以上は一般的な議論ですが、AOSPのソースコードには参考にできる実装が含まれていることがあります。GPSドライバの場合は、device/google/gs101/gnss/がモック実装になっており、上記のAndroid.mkの実装や、gps.cのモック実装など、ドライバのおおよその構成を把握することができます。

このように、上で説明したドライバの構成の知識とともに、適宜あたりをつけてモック実装などを探すことで、より円滑に要求仕様を特定したり、実装の方針を検討することができます

オープンソースの実装を利用する

GPSドライバの要件と、実際に実装する方法がわかったので、本格的に実装していくことができます。

ただし、GPLv2ライセンスのオープンソース実装があるのでこれをベースとすることもできます。

https://github.com/dipcore/gps-glonass-android-driver

このドライバ実装では今回扱ったような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に興味のある方はこちらもご覧ください。

https://zenn.dev/turing_motors/articles/9c18325ffdfb57

TuringのUXチームではさらに、Androidアプリやサーバサイドのタスク、音声認識まで、多様な範囲のエンジニアリングを扱っています。こうした開発に興味がある方はぜひ、弊社求人一覧およびWantedlyをご覧ください。

脚注
  1. ただし、一般的な開発環境ではGoogle Location Servicesが推奨されます。 ↩︎

  2. .halのファイルはHIDLで書かれていますが、Android 13以降はHALもAIDLで書くことが推奨されています(参照1, 参照2)。 ↩︎

Tech Blog - Turing

Discussion