エンディアンが異なるマイコンでの移植テクニック

マイコン移植で大きな問題のひとつにエンディアンの違いがあります。エンディアンが異なるCPUに移植をすると、これまで動いていたプログラムをそのままコンパイルし直しただけでは正しく動作しない場合があります。ここでは、ビックエンディアンのCPUを使っていたユーザが、ARM用IAR Embedded Workbenchを使ってリトルエンディアンのCPUに移植する時に使えるテクニックをご紹介します。ここでは、Cortex-Mをリトルエンディアンで動作させることを想定しています。

 

エンディアンについて

複数バイトのデータを扱うときにメモリにどう格納するか?ということで、リトルエンディアンとビックエンディアンがあります。たとえば、32ビットのデータとして0x01020304があったとします。これを、メモリ0x100~103までの領域に格納することを考えてみます。

endianness_figure1.png

単純にintなどのデータ型を使っている場合には、問題になることは少ないです。ところが、以下のような記述を使うと問題が発生します。このケースでは共用体で、unsigned intのdatとunsigned charの配列を定義しています。どちらも32ビットの領域です。このような共用体はデータの入力や出力は32ビットでまとめて実施、細かい操作は8ビット単位で実施する場合などで良く出てくる記述です(場合によってはビットフィールドが出てくると思います)。

union {
unsigned int dat;
unsigned char c[4];
}X;

void foo( ) {
int t0;
X.dat = 0x01020304;
t0 = X.c[0];
・・・
}

この記述をビッグエンディアンのCPUで実施するとt0の値は0x01ですし、リトルエンディアンのCortex-Mで実施するとt0の値は0x04となります。

ARM用IAR Embedded Workbenchで使えるテクニックの紹介

ARM用IAR Embedded Workbenchではエンディアンの違いを吸収するのに利用できるテクニックがあります。ここでは、以下の2つを紹介します。

  1. キーワード:__big_endian
  2. 組込み関数: __REV, __REV16, __REVSH, RBIT

キーワード:__big_endian

ARM用IAR Embedded Workbenchのマニュアルからキーワード__big_endianの説明を見てみます。 「__big_endian キーワードは、残りのアプリケーションで使用されるバイトオーダに関係なく、ビッグエンディアンバイトオーダに格納される変数へのアクセスに使用されます。__big_endian キーワードは、ARMv6 以上でコンパイルする場合に使用できます。 このキーワードはポインタ上では使用できません。また、配列上でも使用できません。」 先ほどの例をこのキーワード__big_endianを使って書き換えてみると変数宣言の部分にキーワードを加えただけの変更です。

____big_endian union {
unsigned int dat;
unsigned char c[4];
}X;

void foo( ) {
int t0;
X.dat = 0x01020304;
t0 = X.c[0];
・・・
}

ARM用IAR EMBEDDED WORKBENCHのマニュアルからキーワード__big_endianの説明を見てみます。 「__big_endian キーワードは、残りのアプリケーションで使用されるバイトオーダに関係なく、ビッグエンディアンバイトオーダに格納される変数へのアクセスに使用されます。__big_endian キーワードは、ARMv6 以上でコンパイルする場合に使用できます。 このキーワードはポインタ上では使用できません。また、配列上でも使用できません。」 先ほどの例をこのキーワード__big_endianを使って書き換えてみると変数宣言の部分にキーワードを加えただけの変更です。

それでは、キーワード__big_endianを使うとエンディアンにまつわるすべての問題が解決できるのか?を見ていきたいと思います。キーワード__big_endianが着けてコンパイルするとREV命令によりバイト順の入れ替えを実行しています。実はアクセスがあるたびにこのREV命令が挿入されますので、実行速度およびコード量で影響を与えます。

endianness_figure2.png

キーワード__big_endianの問題点の2つ目は、複雑なデータ型などで利用できないことです。たとえば、以下の記述はコンパイル時にエラーとされてしまいます。

__big_endian
union {
unsigned long dat;
unsigned char c[4];
struct {
unsigned long a0: 1;
unsigned long a1: 1;
unsigned long a2: 2;
unsigned long a3: 4;
unsigned long a4: 8;
unsigned long a5: 16;
}s;
} f1_dat2;

こうした場合には、次に紹介するバイト操作命令を利用する必要があります。

組込み関数: __REV, __REV16, __REVSH,

エンディアンの違いは、データのバイトでの並びが異なることが原因です。最初の0x01020304のデータで考えてみると、バイト単位でデータを並び替えればよいことになります。

endianness_figure3.png

これを実行するコード例は以下のようになります。

typedef unsigned long uint32_t;
uint32_t bswap_32(uint32_t x) {
  uint32_t t = x;
  uint32_t s;
  s = ( (((uint32_t)(t) & (uint32_t)0x000000ffUL) << 24) |
    (((uint32_t)(t) & (uint32_t)0x0000ff00UL) << 8) |
     (((uint32_t)(t) & (uint32_t)0x00ff0000UL) >> 8) |
(((uint32_t)(t) & (uint32_t)0xff000000UL) >> 24) );
return s;
}

見て分かる通り、かなり実行にオーバーヘッドが出ることが分かります。4つの&、4つのシフト、3つの|が実行に必要となります。

実はCortex-MなどのARM V6アーキテクチャ以降であれば、REV命令がありさきほどのbswap_32と同じ実行を1命令で実行することが可能です。

REV命令を使うには通常アセンブラでの記述が必要となりますが、ARM用IAR Embedded Workbenchでは組込み関数として用意されていますので,#includeをしていただくことで、以下の関数を利用することが出来ます。

  • __REV: ワード内のバイト順序を反転させます。
  • __REV16: 各ハーフワード内のバイト順序を独自に反転させます。
  • __REVSH:下位ハーフワード内のバイト順序を反転させ、それを 32 ビットに符号拡張します。
  • __RBIT: 32 ビットワード内のビット順序を反転させます。

RBITは参考までですが、データのビットを反転にしてくれます。その他については実際にデータがどのように変換されるかを2つの例を示します。REVSHは下位ハーフワードを入れ替え符号拡張を実施します。

endianness_figure4.png

実際の記述例は以下のようになります。関数として利用できるため簡単に利用できます。

#include <intrinsics.h>
void x1( void ) {
s2 = __REV(s1);
s3 = __REV16(s1);
s4 = __REVSH(s1);
}

まとめ

今回は、ビックエンディアンからリトルエンディアンへの移植ということで、キーワード__big_endianを紹介しました。 簡単な例ではこの__big_endianというキーワードで移植が完了します。複雑なケースでこのキーワードが使えませんので、REV命令などを利用してデータの並びを入れ替えてやる必要があります。 ここでは、触れませんでしたが、リトルエンディアンからビッグエンディアンの移植ではキーワード__little_endianが利用できます。

 

お申し込み受付後に送付される演習キットを使用して、テキストと動画を見ながら、Arm Cortex-Mマイコン評価ボードによる「IAR Embedded Workbench for Arm」の基礎から実践的な使い方までが学べます。

組込みソフトウェアエンジニアでArm Cortex-Mマイコンの基礎的なプログラミングを習得したい方にお勧めです。

 

セルフハンズオン セミナー申し込み