センサをマイコンにつなげて、決められたフォーマットで送られてくるバイナリデータをセンサーの数値として、取得したいってことがままあります。
その時にセンサのデータ型がuint8とか単純なものなら、特に悩まずにデータを取得できますが、float型だったり、しかも信号がひとつではなくて、複数あったりとかしだすと、ちょっと悩みます。
こういう場合は共用体(union)を使うと、簡単にバイナリのデータから値を取得できます。今回はそんな便利な共用体の使い方と注意点なんぞをメモしてみました。
ちなみに使用したマイコンはFRDM-K64F(NXPマイコン ARM-CortexM4)
例題
例えば、下の6つのfloat型のデータがマイコンに送られてきたとします。
受信側のマイコンはリトルエンディアンなので、それに合わせて、下のようなフォーマットできたとします。
通信手段はUARTだとかSPIだとかはおいといて、運よくマイコン側でg_dataの配列に値が格納できたとします。こんなぐあいに。
typedef unsigned char byte;
byte g_data[] = { 0xD8,0x0F,0x49,0x40,
0x4D,0xF8,0x2D,0x40,
0xBD,0x1B,0xCF,0x3F,
0x0F,0x62,0x92,0x48,
0x31,0xE6,0x1c,0x41,
0x62,0xDE,0x54,0x3E
};
問題は、ここからいかに6つの数値データを簡単に抽出するかってことです。
UNIONの使い方
まず、何も考えず、フォーマットどおりに変数を定義して、構造体にぶちこみます。
typedef struct
{
float pi; //円周率
float e; //ネイピア
float ratio; //黄金比
float vLight; //光の速度
float g; //重力
float ii; //iのi乗
}Msg_data_t;
次に共用体を使って、全データ分の24バイト配列 buf [] と上の構造体を定義します。これでbuf[]と構造体は同じアドレス上で定義されることになります。
typedef union
{
byte buf[24];
Msg_data_t msg;
}Msg_union_t;
使い方としては、シリアル通信で入ってきたデータを逐次bufに格納します。今回は通信部分はプログラムにしていないので、単純に値とれたものとして memcpyを使います。
Msg_union_t data;
memcpy(&data,&g_data,sizeof(g_data));
これだけです。あとは
PRINTF("pi:%f\n", data.msg.pi);
とかして、値をみるだけです。結果を下のとおりです。
良い感じに値を取得できています。
共用体のわな
わなというほどもないんですが、気を付けないといけないことがあるので、メモしときます。さっきのフォーマットを下のようにしたとします。floatではなく最初にuint8のデータをいれました。クリックで拡大してください。先頭に1バイトのデータがたされてます。
byte g_data_2[] = {
0xFF,
0xD8,0x0F,0x49,0x40,
0x4D,0xF8,0x2D,0x40,
0xBD,0x1B,0xCF,0x3F,
0x0F,0x62,0x92,0x48,
0x31,0xE6,0x1c,0x41
};
構造体と共用体の定義は下のように変更します。先頭にvalが入りました
typedef struct
{
uint8_t val;
float pi_2;
float e_2;
float ratio_2;
float vLight_2;
float g_2;
}Msg_data_2_t;
typedef union
{
byte buf_2[20];
Msg_data_2_t msg;
}Msg_union_2_t;
値を確認すると、先頭のvalはいいんですが、後の値がでたらめになってしまいました。
バイト境界(データアライメント)
上が、うまくいったときのデータフォーマット、下が、失敗におわったデータフォーマットになります。
下のフォーマットでは、先頭の値は正しくよめていますが、その次の円周率の値はbyte1-4ではなく、3バイトジャンプして、byte4-7を読みに行っていることがわかります。縦の点線をデータ4バイト毎に引いてみましたが、これみるとわかる通り、実はフォーマットを変えても、読みにく先頭アドレスは何も変わっていないことがわかります。つまり、送り側のフォーマットは変えたが、マイコンの読み方はなにも変わってないから、変になったってことになります。
点線の部分をバイト境界といいます。ARMマイコンの場合、一般的にnバイトのデータを保存するための先頭アドレスはnで割り切れることが必要です。
例えば、今回のケースではn=4なので、データの各要素のうち赤帯のところからでしか、4バイトデータを取得できません。
並べなおすか、詰め物をする
解決方法で一番シンプルなのが、他の1バイトデータがあれば、信号の順番を並びかえたり、下のようにダミーの1バイトデータを3つ追加したりして、次の4バイトデータのために頭出しをしとくことです。
アライメントを強制的に1バイトにする
他にも問題の共用体の定義のときだけ#pragma packをつかって、強制的に1バイト境界とするほ方法もあります。
#pragma pack(push) /* 現在のアライメントをスタックにプッシュ */
#pragma pack(1) /* 1バイト境界にアライメントを設定 */
typedef union
{
byte buf_2[20];
Msg_data_2_t msg;
}Msg_union_2_t;
#pragma pack(pop) /* スタックから元のアライメントを復元 */
以上です。UNIONは便利ですが、センサー側のデータフォーマットはマイコン側の都合など無関係に送られてくることも多いので、データの配置によっては、トラブルが発生する場合もあるということですね。
UNIONばんざい。