構造体を使ったテクニック①
組み込みのプログラミングでは外部デバイスとアドレスの受け渡しを行うことが良くあります。デバイスメーカが決めたプロトコルに従ったり、独自のプロトコルを取り決めたりとさまざまなケースがあると思います。ここではbyte単位でコマンドを送受信する場合を考えてみたいと思います。アドレスは最近では32bit環境がほとんどかと思いますので4byteと考えます。送受信用のBufferはunsigned char型の配列として準備し、アドレス情報はunsigned int型で保持すると想定します。なおデバイスとのやりとりの取り決めでアドレスはビッグエンディアンでやりとりするとします。さて内部で保持しているint型のアドレスを配列にどうやって格納しますでしょうか?もしくはchar型で受けたアドレスをどうやってint型に格納しますでしょうか?
unsigned char iSndBuf[4]; unsigned char iRcvBuf[] = {0x01, 0xAF, 0xff, 0xff}; unsigned int iaddr = 0x01AFFFFF; /* 送信するaddressをBufferに格納する 1*/ iSndBuf[3] = (unsigned char)iaddr ; iSndBuf[2] = (unsigned char)(iaddr >>= 8 ); iSndBuf[1] = (unsigned char)(iaddr >>= 8 ); iSndBuf[0] = (unsigned char)(iaddr >>= 8 ); printf("iSndBuf[0]= %x \n", iSndBuf[0]); printf("iSndBuf[1]= %x \n", iSndBuf[1]); printf("iSndBuf[2]= %x \n", iSndBuf[2]); printf("iSndBuf[3]= %x \n", iSndBuf[3]); /* 受信したaddressをBufferに格納する 1*/ iaddr = 0; iaddr |= iRcvBuf[0]; iaddr <<= 8; iaddr |= iRcvBuf[1]; iaddr <<= 8; iaddr |= iRcvBuf[2]; iaddr <<= 8; iaddr |= iRcvBuf[3]; printf("iaddr= %x \n", iaddr);
上のプログラムはビット操作を使ってint -> char, char -> intを展開しています。正直、慣れていますのですぐにそれだと判りますが、ビッグエンディアンでデータを格納しているのがすぐにわかりますでしょうか?やはりビット操作はプログラムの意図を読み手に伝えるのにはあまり良い方法ではありません。そこで一工夫してポインタのバッファを使う方法にすると、以下のようになります。
/* 送信するaddressをBufferに格納する 2*/ unsigned char *pBuf = (unsigned char *)&iaddr ; iSndBuf[0] = pBuf[3]; iSndBuf[1] = pBuf[2]; iSndBuf[2] = pBuf[1]; iSndBuf[3] = pBuf[0]; printf("iSndBuf[0]= %x \n", iSndBuf[0]); printf("iSndBuf[1]= %x \n", iSndBuf[1]); printf("iSndBuf[2]= %x \n", iSndBuf[2]); printf("iSndBuf[3]= %x \n", iSndBuf[3]); /* 受信したaddressをBufferに格納する 2*/ iaddr = 0; pBuf[3] = iRcvBuf[0]; pBuf[2] = iRcvBuf[1]; pBuf[1] = iRcvBuf[2]; pBuf[0] = iRcvBuf[3]; printf("iaddr= %x \n", iaddr);
すっきりしましたね。ただやっぱり少しだけビッグエンディアンで頭を悩ませます。そこでstructへの型変換を使ってみましょう。
/* 送信するaddressをBufferに格納する 3*/ typedef struct StEndian_{ unsigned char LSB; unsigned char SLSB; unsigned char SMSB; unsigned char MSB; }StEndian; #define DfEndian(X) (*( (StEndian*)(&X) )) StEndian *psBuf = (StEndian *)&iaddr ; iSndBuf[0] = psBuf->MSB; iSndBuf[1] = psBuf->SMSB; iSndBuf[2] = psBuf->SLSB; iSndBuf[3] = psBuf->LSB; printf("iSndBuf[0]= %x \n", iSndBuf[0]); printf("iSndBuf[1]= %x \n", iSndBuf[1]); printf("iSndBuf[2]= %x \n", iSndBuf[2]); printf("iSndBuf[3]= %x \n", iSndBuf[3]); /* 受信したaddressをBufferに格納する 3*/ iaddr = 0; psBuf->MSB = iRcvBuf[0]; psBuf->SMSB = iRcvBuf[1]; psBuf->SLSB = iRcvBuf[2]; psBuf->LSB = iRcvBuf[3]; printf("iaddr= %x \n", iaddr);
sturctについては前回のエントリ構造体変数の考察で書きましたが。つまるところ構造体変数にキャストするというのは「変数をstructのメンバの型の大きさに切断する」という意味合いになります。その特性を利用したのが上のプログラムです。メンバに名前を付けられるのでこの方法ならビッグエンディアンでDataを格納するのに頭を悩ます必要はありません。頭を悩ますのは最初に構造体を定義する時だけです。(システムがリトルエンディアンの場合は一番上がLeast Significant Byteです。)、なおビットフィールドも同様にリトルエンディアンとビッグエンディアンの問題があります。一番上の定義をLSBにするかMSBにするかはマイコンのオプションで設定できたりしますので注意してみてください。バイト列はリトルエンディアンで保持、ビット並びはビッグエンディアンで保持、ビットフィールドは一番上をLeast Significant Bitにしているシステムが多いと思います。
まとめ
「構造体は大きな変数をメンバの型で切断する」