« 2010年1月 | トップページ | 2010年3月 »

2010年2月の記事

NMEA-0183メッセージの解析

GPSモジュールが出力するNMEA-0183メッセージと呼ばれる出力文字列の解析プログラムの実験を行いました。最終的にはSTM32 Primer2に組み込んで使用するのですが、今回はPC上(VC++ 2008)で動作確認を行いました。

NMEA-0183メッセージとは

GPSモジュールが衛星を補足すると、以下のような文字列をシリアルインターフェースから吐き出します(自宅で収集したデーターのため、緯度・経度を示す部分は、xxxx.xxxx, yyyy.yyyyに書き換えています)。

$GPGGA,230400.600,xxxx.xxxx,N,yyyy.yyyy,E,1,4,39.03,-170.6,M,39.4,M,,*4D
$GPGLL,xxxx.xxxx,N,yyyy.yyyy,E,230400.600,A,A*56
$GPGSA,A,3,06,03,31,19,,,,,,,,,46.41,39.03,25.10*35
$GPGSV,3,1,10,16,64,350,,06,56,223,29,31,56,129,26,03,42,229,30*74
$GPGSV,3,2,10,23,34,287,,21,31,086,,19,17,218,26,13,14,320,*7A
$GPGSV,3,3,10,29,08,043,,25,04,317,*7A
$GPRMC,230400.600,A,xxxx.xxxx,N,yyyy.yyyy,E,0.05,169.01,131109,,,A*60
$GPVTG,169.01,T,,M,0.05,N,0.08,K,A*3F

各パラメーターの意味は、このリンクを参照ください。概略を示すと;

  • GPGGAメッセージから、衛星の補足状態、経度・緯度や補足している衛星の数が分かります
  • GPGSVメッセージから、個々の衛星の位置(仰角・方位)、受信信号レベルが分かります
  • 経度・緯度情報は複数のメッセージに挿入されており、GPGLL, GPRMCメッセージでも取得できます

NMEA-0183メッセージの解析

経度・緯度を得るためには、GPGGAの「GPSのクオリティ(Position Fix Indicator)」が1 or 2になった後で、経度・緯度情報を取り出す必要があります。NMEA-0183メッセージ(以下NMEAメッセージ)は、カンマ区切り形式の単純な構造であるため、解析はそれほど難しくないと思うのですが、チェックサムの計算とか色々考慮すべき点もあります。また、経度・緯度情報だけ抜き取るのなら簡単ですが、汎用性を持たせて任意のデーターの取り出しまで求めるとプログラムは結構複雑そうです。

そこで、例によって人様の成果を活用させてもらうべく、フリーのnmeaパーサーを探しました。グーグル様で検索してみるとC言語で使用できるライブラリとしては、[dmh2000] – NMEAP, NMEA Libraryが見つかりました。NMEAPはGPGGA/GPRMCのみを解析しているのに対して、NMEA LibraryはすべてのNMEAメッセージを解析できます。GPGSVメッセージも解析して衛星からの信号強度も表示したいため、NMEA Libraryを使用することにします。

NMEA Libraryの使い方

パッケージに含まれるexampleをベースに以下のサンプルプログラムを書いてみました(ほとんどexampleそのままです、、)。サンプルプログラムはテキスト形式で保存したNMEAメッセージのログファイルを読み込んで、位置情報と信号強度を表示します。

#include "nmea/nmea.h"

#include <string.h>
#include <stdio.h>

#ifdef NMEA_WIN
#   include <io.h>
#endif

void trace(const char *str, int str_size)  // debug用のcallback
{
    printf("Trace: ");
    write(1, str, str_size);
}
void error(const char *str, int str_size)
{
    printf("Error: ");
    write(1, str, str_size);
}

int main(int argc, char *argv[])
{
    nmeaINFO info;             // 取得情報を保持するための構造体
    nmeaPARSER parser;         // パーサーの内部管理情報
    FILE *file;
    char buff[256];            // 受信データーのバッファ
    int size, it = 0, i;

    if (argc < 2)
    {
        printf("need log filename");
        return -1;
    }

    file = fopen(argv[1], "rb");

    if(!file)
    {
        printf("File open error");
        return -1;
    }

    nmea_property()->trace_func = &trace;  // trace callbackの登録
    nmea_property()->error_func = &error;

    nmea_zero_INFO(&info);      // nmeaINFO構造体の初期値設定
    nmea_parser_init(&parser);  // パーサーオブジェクトの初期化

    while(!feof(file))
    {
        size = (int)fread(&buff[0], 1, 128, file);   // NMEAメッセージの取得
        nmea_parse(&parser, buff, size, &info);  // パーサーの起動

        printf(                     // 経度・緯度情報の表示
            "%03d, Lat: %f, Lon: %f, Sig: %d, Fix: %d\n",
            it++, info.lat, info.lon, info.sig, info.fix );
        for (i = 0; i < NMEA_MAXSAT; i++)
        {                           // 受信信号強度の表示
            if (info.satinfo.sat[i].in_use)
                printf(
                    "  sat_id:%02d, sig:%02d\n", 
                    info.satinfo.sat[i].id , info.satinfo.sat[i].sig);
        }
    }

    fseek(file, 0, SEEK_SET);
    nmea_parser_destroy(&parser);  // パーサーオブジェクトの廃棄(メモリーの開放など)
    fclose(file);
    return 0;
}

10, 15行目は、デバック用のcallback関数で、43, 44行目で登録しています。ソースを眺めてみたのですが、どのタイミングで呼び出しているかは分からず。

51, 52行目で、128文字単位にファイルからテキストを読み出してパーサーに渡しています。すなわち、メッセージの切れ目(改行)を意識する必要がないということになります。実際にGPSモジュールをつないで動かした場合、割り込みで文字を受信して一定の文字数がたまったらパーサーを起動するようにすればよく、処理が簡略化できそうです。

解析した情報は、nmeaINFO構造体に格納されます。構造体の定義は以下となっています。

typedef struct _nmeaINFO
{
    int     smask;      /**< Mask specifying types of packages from which data have been obtained */

    nmeaTIME utc;       /**< UTC of position */

    int     sig;        /**< GPS quality indicator (0 = Invalid; 1 = Fix; 2 = Differential, 3 = Sensitive) */
    int     fix;        /**< Operating mode, used for navigation (1 = Fix not available; 2 = 2D; 3 = 3D) */

    double  PDOP;       /**< Position Dilution Of Precision */
    double  HDOP;       /**< Horizontal Dilution Of Precision */
    double  VDOP;       /**< Vertical Dilution Of Precision */

    double  lat;        /**< Latitude in NDEG - +/-[degree][min].[sec/60] */
    double  lon;        /**< Longitude in NDEG - +/-[degree][min].[sec/60] */
    double  elv;        /**< Antenna altitude above/below mean sea level (geoid) in meters */
    double  speed;      /**< Speed over the ground in kilometers/hour */
    double  direction;  /**< Track angle in degrees True */
    double  declination; /**< Magnetic variation degrees (Easterly var. subtracts from true course) */

    nmeaSATINFO satinfo; /**< Satellites information */

} nmeaINFO;


typedef struct _nmeaSATINFO
{
    int     inuse;      /**< Number of satellites in use (not those in view) */
    int     inview;     /**< Total number of satellites in view */
    nmeaSATELLITE sat[NMEA_MAXSAT]; /**< Satellites information */

} nmeaSATINFO;


typedef struct _nmeaSATELLITE
{
    int     id;         /**< Satellite PRN number */
    int     in_use;     /**< Used in position fix */
    int     elv;        /**< Elevation in degrees, 90 maximum */
    int     azimuth;    /**< Azimuth, degrees from true north, 000 to 359 */
    int     sig;        /**< Signal, 00-99 dB */

} nmeaSATELLITE; 

構造体が階層化されており、ちょっとややこしいですが;

  • 経度緯度は、info.lat, info.lonで取得できます(infoはnmeaINFOのインスタンス)
  • 信号強度は階層をたぐって、info.satinfo.sat[i].sigで取得できます

サンプルを動かした結果を以下に示します(経度・緯度はxxxx.xxxx, yyyy.yyyyで、、)。
パーサーには128文字単位でデーターを渡していますが、パーサー内部でメッセージ単位に処理を行う作りになっています。そのため、最初のGPGSAメッセージが読み込まれた時点で(最初のTrace表示)000番目の解析結果が表示され、続けてGPGSAとGPGSVが読み込まれた時点で001番目の解析結果が表示されています。

Trace: $GPGGA,112645.000,xxxx.xxxxx,N,yyyy.yyyy,E,2,05,1.7,465.4,M,39.4,M,3.8,0000*7C
000, Lat: xxxx.xxxxx, Lon: yyyy.yyyy, Sig: 2, Fix: 1
Trace: $GPGSA,A,3,12,24,21,09,18,,,,,,,,2.0,1.7,1.0*31
Trace: $GPGSV,3,1,12,09,80,345,22,27,71,022,17,32,68,311,,18,62,305,16*7E
001, Lat: xxxx.xxxxx, Lon: yyyy.yyyy, Sig: 2, Fix: 3
Trace: $GPGSV,3,2,12,15,42,071,,12,29,161,39,21,28,241,31,26,26,292,*7E
Trace: $GPGSV,3,3,12,22,26,314,17,24,15,198,37,30,09,186,21,05,06,142,*7A
002, Lat: xxxx.xxxx, Lon: yyyy.yyyy, Sig: 2, Fix: 3
Trace: $GPRMC,112645.000,A,xxxx.xxxx,N,yyyy.yyyy,E,0.00,,170110,,,D*7B
Trace: $GPVTG,,T,,M,0.00,N,0.0,K,D*16
Trace: $GPGGA,112646.000,xxxx.xxxx,N,yyyy.yyyy,E,2,05,1.7,465.4,M,39.4,M,4.8,0000*78
003, Lat: xxxx.xxxx, Lon: yyyy.yyyy, Sig: 2, Fix: 3
Trace: $GPGSA,A,3,12,24,21,09,18,,,,,,,,2.0,1.7,1.0*31
Trace: $GPGSV,3,1,12,09,80,347,22,27,70,022,16,32,69,311,,18,62,305,16*7D
004, Lat: xxxx.xxxx, Lon: yyyy.yyyy, Sig: 2, Fix: 3
  sat_id:09, sig:22
  sat_id:18, sig:16
  sat_id:12, sig:39
  sat_id:21, sig:31
  sat_id:24, sig:37
Trace: $GPGSV,3,2,12,15,42,072,,12,29,160,39,21,28,240,31,26,26,293,*7C
Trace: $GPGSV,3,3,12,22,26,314,17,24,14,197,37,30,09,186,21,05,06,142,*74
005, Lat: xxxxx.xxxxx, Lon: yyyyy.yyyyy, Sig: 2, Fix: 3
  sat_id:09, sig:22
  sat_id:18, sig:16
  sat_id:12, sig:39
  sat_id:21, sig:31
  sat_id:24, sig:37

余談

パーサーが動くようになったので、Primer2につないでCircleOS配下でNMEA Libraryを動かそうと作業を始めたのですが、ここで問題発生。Primer2のVBATとGを、GPSモジュールのGとVCCに接続していまい(電源をショートさせてしまい)、GPSモジュールがあえなく昇天してしまいました。SparkFunから直接買ったモジュールで、送料込みで$90以上かかったのに涙です・・・ 電源周りの接続ミスで部品を壊したのは2度目。まったく注意散漫です。

ここまできたので、同じモジュールを再購入して出直しを図ろうかと。
しかし、Primer2を触り始めてから、本体を壊して2個目を買ったりGPSモジュールを2個壊したりとどうも縁起が悪いので、mbedに寄り道してみようかしら、、と思っています。

STM32 Primer2 V1.2

初期のPrimer2は、電源On時に発生する電源電圧の瞬間的な変動でボルテージレギュレーターが(U9, U17)が破損する問題を抱えており私も憂き目に会いました。レギュレーターを交換してだましまだし使っていたのですが、対策を施したV1.2を買ってしまいました(秋月さんで購入)。

基板にV1.2のシルク印刷が入っています。

STM32_Primer2_V12

変更点

stm32circle.comに記載された対策が入っています。

対策1:レギュレーターU9(2.84V主電源用)の入力側に入っているC23を積層セラコンに置換し、その上に過電圧保護用のトランシル(SM2T3V3A)を乗っけています。

V12_C23

対策2:レギュレーターU17(3.18Vバックライト用)の入力側に入っているC56を積層セラコンに置換しています。

V12_C56

対策は十分か

stm32circle.comには、この対策で十分かはまだ判断できない(but it is too early to know if this will offer complete protection for the regulators.)と、弱気なことが書いてあったりします(外部電源の抜き差しをするような箇所は、本来もっと耐圧の高い部品を使うべきなんでしょうか)。

安全のためにUSBケーブルの挿抜は電源を落とした状態でやらねば。

STM32 ADCの設定

前回記事「MOS FETを使用したハイサイドスイッチ」で、GPSモジュールの電源制御をOn-Off制御信号(サスペンド・レジューム信号)にて行えないかと記載しましたが、実験したところ結果はOKでした。使用したGPSモジュールのOn-Off制御信号は1.2Vロジックのため電圧変換の抵抗が必要ですが(1.8V→1.2V変換の分圧抵抗がモジュール内に入っているため、2.8V→1.2Vとなるように直列の抵抗を1本追加)、MOSFETを使う回路に比べて部品点数が少ないため、On-Off制御信号を使うことにしました。

電源制御は、On-Off pinにパルスを入力することによってサスペンド・レジュームがトグル動作しますが、GPSモジュールへサスペンド要求を出した後、本当に電源が落ちているかを確認したいと思いました。サスペンド動作は、モジュール内部で生成しているVCC(1.8V)が0Vになることで確認ができます。そのため、GPSモジュールのVCCをSTM32のADCでモニターしてサスペンド状態への移行を確認してからGPSアプリを終了することにします。

上記の理由から、STM32のADCを始めて使ってみました。STM32のペリフェラルはやたらと機能が多いと先人の方がブログに書いていますが、確かに「これでもか!」と言わんばかりの機能が入っています。NXP LPC17xxのマニュアルを見たのですが、STM32のADCはLPC17xxより機能が多く複雑です。

これだけの機能を使いこなせれば最強なのでしょうが、先ずは、必要最小限のAD変換機能を動かすための設定方法を記載します。

STM32 ADCの動作モード

動作モードを表にすると以下となります。

STM32ADC_Functions

ちなみに、今回使用した機能は紫の網掛け部分で、以下の内容となります

  • 入力として1chを使用
  • Single shot変換(変換毎にソフト指定のトリガーを出す)
  • サンプル時間は55.5 cycleを指定

AD変換のコード

先ずは、ADC初期設定のコードです。入力ピンに使用するGPIOもあわせて設定します。

/*******************************************************************************
* Function Name  : InitADC2
* Description    : Initialize ADC2 as single shot mode
*                  Set PC.4 (Primer2 Extention Connector Pin 11) as analog input
*
*******************************************************************************/
void InitADC2( void )
{
    GPIO_InitTypeDef GPIO_InitStructure;
    ADC_InitTypeDef  ADC_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC2, ENABLE);

    GPIO_StructInit (&GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOC, &GPIO_InitStructure);

    /* ADC1 Configuration ------------------------------------------------------*/
    ADC_InitStructure.ADC_Mode                = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode        = DISABLE;
    ADC_InitStructure.ADC_ContinuousConvMode  = DISABLE;
    ADC_InitStructure.ADC_ExternalTrigConv    = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign           = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel        = 1; 
    ADC_Init( ADC2, &ADC_InitStructure );

    /* ADC2 regular channel14 configuration to sample time = 55.5 cycles */ 
    ADC_RegularChannelConfig( ADC2, ADC_Channel_14, 1, ADC_SampleTime_55Cycles5);

    /* Enable ADC2  */
    ADC_Cmd(ADC2, ENABLE);

    /* Enable ADC2 reset calibaration register */   
    ADC_ResetCalibration(ADC2);

    /* Check the end of ADC1 reset calibration register */
    while(ADC_GetResetCalibrationStatus(ADC2));

    /* Start ADC2 calibaration */
    ADC_StartCalibration(ADC2);
    /* Check the end of ADC2 calibration */
    while(ADC_GetCalibrationStatus(ADC2)); 
}

上記以外にADCクロックの分周比を設定する必要がありますが、CircleOSが初期設定しているため割愛。生で使う場合は設定が必要です。

データー取得のコードは以下です。

/*******************************************************************************
* Function Name  : getCxAdc1Value
* Description    : Get ADC converted value of ADC2 ch-14 (CX_ADC1)
*                  Use single conversion mode
* Input          : NONE
* Return         : Converted value
*******************************************************************************/
u16 getCxAdc1Value(void)
{
    // Start ADC2 Software Conversion
    ADC_SoftwareStartConvCmd( ADC2, ENABLE );
    // Wait until conversion completion
    while(ADC_GetFlagStatus(ADC2, ADC_FLAG_EOC) == RESET);
    // Get the conversion value
    return ADC_GetConversionValue(ADC2);
}

思ったこと

ST Libraryの命名規則がしっかりしており(CMSISというやつですね)、名前から設定内容が分かるためコードの見通しはそこそこよいのですが初期化に20行程度必要って面倒ですね、、

初めてWindows APIの本を読んだときに、窓を1つ開くだけで40行近くのコードが必要なのにびっくりして、DOSの方がよっぽどプログラムは楽だと思ったのに似ています。最近のWindowsプログラミングは、C#/VBなんかのRAD(Rapid Application Development)ツールを使えば、窓を開くだけならプログラムコードはまったく不要で(GUIデザイナーで画面イメージを作るだけ)、複雑怪奇なWindows APIを意識することなくコーディングができます(その代わりに膨大なクラスライブラリの機能を把握するのが一苦労ですが)。

上記から、Arduinoやmbedのように、C++のオブジェクトを使ってデバイス初期設定などの下回りを隠蔽するコンセプトは捨てたものではないと思いました。ちなみに、mbedならADCの初期化と値の取得(上記のAD変換コード相当)は、以下のようにたったの2行ですみます。

AnalogIn ain(p20);         // コンストラクターの呼び出し(ADCの初期化)
float vol = ain.read();     // %値(0.0~1.0)として読み取り。ain.read_u16()だと12bitの生データーを取得できる

STM32の複雑なモード設定をオブジェクト化すると、メンバー変数(モードを保持するプロパティー値)が増えそうではありますが、CMSISよりコードの見通しがよくなると思います。ただ、オブジェクト化するということは、ハードの設定や動作を一定の枠に押し込むことになり、ハードの能力を使い切る必要に迫られれる組み込みプログラミングには適さない部分もあると思います。あと、オブジェクト指向だとコードサイズが大きくなってしまうでしょうか。

結局、Cでゴリゴリ書くのが今は王道なのかしら。

« 2010年1月 | トップページ | 2010年3月 »

2018年10月
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      
無料ブログはココログ