ハードウェアを意識したデータ宣言とBDT

microchipが提供しているPICマイコン向けのサンプルコードを見ると、自分が普段使わない宣言がいくつかあったのでメモっておく。

該当するPICのBDTの宣言

PICのUSBホスト処理で、「BDT(Buffer Descriptor Table)の構造体の先頭アドレスが512バイト境界に合わせる」云々という記述がある。該当するデータのヘッダなど見ていると、以下のようになっている。(PIC24Fの場合)

typedef union _BD_STAT
{
	struct{
		unsigned            :2;     //Byte count
		unsigned    BSTALL  :1;     //Buffer Stall Enable
		unsigned    DTSEN   :1;     //Data Toggle Synch Enable
		unsigned            :2;     //Reserved - write as 00
		unsigned    DTS     :1;     //Data Toggle Synch Value
		unsigned    UOWN    :1;     //USB Ownership
	};
	struct{
		unsigned            :2;
		unsigned    PID0    :1;
		unsigned    PID1    :1;
		unsigned    PID2    :1;
		unsigned    PID3    :1;
	};
	struct{
		unsigned            :2;
		unsigned    PID     :4;     // Packet Identifier
	};
	uint8_t            Val;
} BD_STAT;//Buffer Descriptor Status Register

// BDT Entry Layout
typedef union __BDT{
    union
    {
        struct
        {
            uint8_t CNT;
            BD_STAT STAT __attribute__ ((packed));
        };
        struct
        {
			//test
            uint16_t count:10;
            uint8_t :6;
			//Buffer Address
            uint16_t ADR;
        };
    };

    uint32_t Val;
    uint16_t v[2];
} BDT_ENTRY;

static BDT_ENTRY __attribute__ ((aligned(512))) BDT[2];

全く分からない。。とりあえず一つずつ確認する。

attribute ((packed)), attribute ((aligned(x)))

構造体の詰め物を抑制する attribute ((packed))

使っているCPUプロセッサの種類によって変わってくるのか、構造体のメンバの型のサイズとsizeofしたサイズが合わないことがしばしばある。普通にドットやアロー使ってアクセスする分には気にしなくても良いが、キャストしたりすると問題になる。サイズが合わなくなるのは、パフォーマンスを考えて、アクセスしやすいように、構造体メンバの間に詰め物を自動的に差し込まれるのが原因である。

struct _pk{
  char a;
  short b;
  int c;
}pk;
pk.a = 1;
pk.b = 2;
pk.c = 3;

メンバのサイズを合計すると、1+2+4=7bytesになる(VisualStudioの場合)が、実際には8bytesになる。代入後のメモリを1バイト区切りで見ると、

01 cc 02 00 03 00 00 00

となり、aとbの間に詰め物が1バイト分入っているのが分かる。もちろん特にintのサイズがプラットフォームごとに変わるので、どうなるかは実際に試す必要がある。

この詰め物を抑制する宣言が、gccコンパイラだと__attribute__ ((packed))になる。(VisualStudioだと#pragma pack(1))

struct _pk{
  char a;
  short b;
  int c;
} __attribute__ ((packed)) pk;
pk.a = 1;
pk.b = 2;
pk.c = 3;

この宣言を追加して、もう一度実行すると、sizeof(pk) = 7となって

01 02 00 03 00 00 00

となって詰め物がなくなった。CPUの効率は悪くなるらしいので、むやみにやることは避けたい。

先頭アドレスを指定サイズごとにそろえる attribute ((align))

うまく言えないが、変数のアドレス(配列なら先頭アドレス)を指定したバイトの倍数になるように位置を合わせることもできる。(指定サイズは2の乗数だったはずだし、最大値もある)

PIC24FのBDT関連の変数宣言を確認すると、以下になっている(ピンポンバッファOFFの場合)

static BDT_ENTRY __attribute__ ((aligned(512))) BDT[2];

USBホストの場合、エンドポイント0に相当する部分しか利用しないらしいので、BDT_ENTRYが2個分の配列になっている。この先頭アドレスが512バイトごとに区切った位置に、揃うようなアロケーションになる。データシートの記述に準拠した実装になっている。

配列でビットフィールドを表現する

2つの構造体があり、unsigned short(2バイト、16ビット)のメンバが3つ定義されている。どちらもドットやアロー演算子でアクセスすると違いがない。

struct _st{
    unsigned short a;
    unsigned short b;
    unsigned short c;
}test;

test.a = 2;
test.b = 31;
test.c = 0;

struct _st2{
    unsigned short a : 4;
    unsigned short b : 5;
    unsigned short c : 7;
} test2;

test2.a = 2;
test2.b = 31;
test2.c = 0;

が、sizeofでサイズを確認すると違いが出る。

sizeof(test) = 6 // 2byte x 3
sizeof(test2) = 2 // ???

testは2byteデータが3つなので6bytes(詰め物がない場合)になるが、test2はコロンで区切って数字を指定すると、ビットサイズを指定できるようになる。つまり、aが4bit、bが5bit、cは7bitのデータが連続するようになる。合計16bitなので、2バイトになる。

ちなみに以下のように1bitだけのデータの場合は、その変数で定義されている最小サイズは確保されるので、2バイトになる。

struct _st3{
  unsigned short a : 1;
} test3;

test3.a = 1;

// sizeof(test3) == 2

また、指定したビットサイズを超える値を代入しても無視されるみたいだ。

test3.a = 4 // 1ビットなので、0か1しか代入しても無視される

あと、PICのレジスタによくあるが、未使用のビットが有効ビットの間に入るような並びを表現する場合は、メンバ名を省略できる。それと並び順は、宣言順に下位ビットに割り当てられる。

struct _st2{
    unsigned short a : 4;
    unsigned short b : 5;
    unsigned short c : 7;
} test2;

// 15---- 8 7------0
// cccccccb bbbbaaaa
// 00000001 11110010
test2.a = 2;  // 0x02 b00000010
test2.b = 31; // 0x1f b00011111
test2.c = 0;  // 0x00 b00000000

データ定義をもう一度確認する

BD_STAT

まず、unionを使ったBD_STATのデータ定義について、最初のメンバ(1つ目の構造体)をベースとして、3つのデータが重なったようなデータ構造になる。BDT_ENTRYで使われるときは、__attribute__((packed))でパディングされずに1バイトのサイズになる。

typedef union _BD_STAT
{
	struct{
		unsigned            :2;     //Byte count
		unsigned    BSTALL  :1;     //Buffer Stall Enable
		unsigned    DTSEN   :1;     //Data Toggle Synch Enable
		unsigned            :2;     //Reserved - write as 00
		unsigned    DTS     :1;     //Data Toggle Synch Value
		unsigned    UOWN    :1;     //USB Ownership
	};
	struct{
		unsigned            :2;
		unsigned    PID0    :1;
		unsigned    PID1    :1;
		unsigned    PID2    :1;
		unsigned    PID3    :1;
	};
	struct{
		unsigned            :2;
		unsigned    PID     :4;     // Packet Identifier
	};
	uint8_t            Val;
} BD_STAT;//Buffer Descriptor Status Register

Valに値を代入すると、各構造体のビットに反映されるような使い勝手だと思われる。実際に定義して値を代入すると、以下のような感じになる。

bdstat

BD_ENTRY

さらにバッファのアドレスやら、カウントなどが積み重なってBDT(Buffer Descriptor Table)が定義されている。

// BDT Entry Layout
typedef union __BDT{
    union
    {
        struct
        {
            uint8_t CNT;
            BD_STAT STAT __attribute__ ((packed));
        };
        struct
        {
            uint16_t count:10;
            uint8_t :6;
            uint16_t ADR; //Buffer Address
        };
    };
    uint32_t Val;
    uint16_t v[2];
} BDT_ENTRY;

bdt

BDTの定義

ピンポンバッファの設定に応じて、BDTの配列のサイズが変わる。ホストモードの場合、EP0しか使わないので、EP1~EP15用のBDエントリが存在しない。

// ピンポンバッファOFF
static BDT_ENTRY __attribute__ ((aligned(512))) BDT[2];
#define BDT_IN  (&BDT[0]) // EP0 IN Buffer Descriptor
#define BDT_OUT (&BDT[1]) // EP0 OUT Buffer Descriptor
    
// EP0のOUTだけバッファON
static BDT_ENTRY __attribute__ ((aligned(512))) BDT[3];
#define BDT_IN      (&BDT[0]) // EP0 IN Buffer Descriptor
#define BDT_OUT     (&BDT[1]) // EP0 OUT Even Buffer Descriptor
#define BDT_OUT_ODD (&BDT[2]) // EP0 OUT Odd Buffer Descriptor

// バッファON
static BDT_ENTRY __attribute__ ((aligned(512))) BDT[4];
#define BDT_IN      (&BDT[0]) // EP0 IN Even Buffer Descriptor
#define BDT_IN_ODD  (&BDT[1]) // EP0 IN Odd Buffer Descriptor
#define BDT_OUT     (&BDT[2]) // EP0 OUT Even Buffer Descriptor
#define BDT_OUT_ODD (&BDT[3]) // EP0 OUT Odd Buffer Descriptor