C언어 구조체 비트필드와 C#에서 처리

C언어 구조체 비트필드는 사용시 구조체를 메모리에 할당 했을때 실제 값들은 할당된 메모리 블럭의 앞부분 부터 비트를 채워나가는게 아니라 뒤에서 부터 채워나갑니다.
일반적으로 변수에 값을 할당해도 뒤에서 부터 채워나가죠 (0x1A는 0001 1010이지 1101 0000으로 할당하지 않음)

struct mybitfields  
{  
    unsigned short a : 4;  
    unsigned short b : 5;  
    unsigned short c : 7;  
}test;  
  
int main( void );  
{  
    test.a = 2;  
    test.b = 31;  
    test.c = 0;  
}

그래서 실제 비트는 다음과 같이 정렬됨.

00000001 11110010  
cccccccb bbbbaaaa

다만 8086계열은 리틀엔디안 방식이라 byte단위로 끊어서 시작주소에 하위바이트 부터 기록하기 때문에 이 경우 실제 메모리는 아래와 같은 방식으로 저장됩니다.

11110010 00000001
bbbbaaaa cccccccb

참고 : https://msdn.microsoft.com/ko-kr/library/yszfawxh.aspx

 

 

자 그럼 이제 이 글을 포스팅하게 된 원인를 적어봅니다.
DJI 문서에 따르면 onboard SDK는 프로토콜을 아래와 같이 정의 했습니다. [출처: Onboard SDK OPEN Protocol ]

|<---------------------Header-------------------------->|<--Data-->|<--Tail-->|
|SOF|LEN|VER|SESSION|ACK|RES0|PADDING|ENC|RES1|SEQ|CRC16|   DATA   |  CRC32   |

그리고 각 요소들은 아래와 같이 필드별로 비트로 나뉘어 있습니다.

저는 시작부터 8bit는 SOF 값을 그리고 그 뒤 10bit는 프레임 길이를 이런식으로 생각했습니다.

예를들어 len=19, ver=7, Session=2, ACK=cmd(0)인 프레임이라면 아래와 같이 비트가 되겠죠

|    SOF    |      LEN     |   VER   | SESSION |ACK|
| 1010 1010 | 0000 0100 11 | 0001 11 |  0001 0 | 0 |

하지만 실제 DJI 샘플 프로그램이 전송하는 데이터는 달랐습니다.

 

원인은 구조체 비트필드를 사용했기에 때문인데 구조체 비트필드로 비트를 정렬하면 아래와 같습니다.

| ACK | SESSION |   VER   |      LEN     |    SOF    |
|  0  |  0001 0 | 0001 11 | 0000 0100 11 | 1010 1010 |

그리고 리틀엔디안이기 때문에 1바이트(8bit)로 끊어서 역순 정렬하면

| 1010 1010 | 0001 0011 | 0001 1100 | 0000 0010 |
|   0xAA    |    0x13   |    0x1C   |    0x02   |

이런식으로 메모리에 저장됩니다.

typedef struct Header
{
	unsigned int SOF : 8;
	unsigned int LEN : 10;
	unsigned int VER : 6;
	unsigned int SESSION: 5;
	unsigned int ACK : 1;
	
} Header;

int main()
{
	Header *hh = new Header;
	memset(hh, 0, sizeof(Header));
	
	hh->SOF = 0xAA;
	hh->LEN = 19;
	hh->VER = 7;
	hh->SESSION = 2;
	hh->ACK = 0;
    return 0;
}

테스트 소스입니다.  한번 돌려보세요!

그리고 아래는 실제 DJI SDK 소스에 있는 header struct입니다.

typedef struct Header
{
  unsigned int sof : 8;
  unsigned int length : 10;
  unsigned int version : 6;
  unsigned int sessionID : 5;
  unsigned int isAck : 1;
  //! @warning this field will change reversed0 --> reserved0 in the next release 
  unsigned int reversed0 : 2; // always 0

  unsigned int padding : 5;
  unsigned int enc : 3;
  //! @warning this field will change reversed1 --> reserved1 in the next release 
  unsigned int reversed1 : 24;

  unsigned int sequenceNumber : 16;
  unsigned int crc : 16;
} Header;

 

 

이제 c#에서 처리할 방법을 찾아야 할때입니다.

열심히 검색해봤는데 일괄적으로 처리해야 하는 방법중 딱히 좋아보이는건 없었습니다.
하나 하나 for문 돌려서 비트를 체크한다던지 하는 식으로 처리하거나 long형(64비트) 변수에 넣고 비트 시프팅 하고 옮기고 하는 식이더군요.

열심히 머리를 굴려봤지만 딱히 마땅한건 없습니다.
인터넷에 저 보다 머리 좋은 수많은 애들이 결론지은게 저것뿐인데 뭐 방법이 있나요 저도 그렇게 해야죠.

저는 일괄적으로 처리하는 방식 보다는 그냥 해당 struct Bit Field마다 클래스나 함수 하나 만들어서  처리하는게 좋다고 생각하고 DJI onboard가 데이터 송신 할때 쓰는 헤더 전문적으로 만드는 클래스를 만들었습니다.

설명을 위해 클래스의 일부를 소개하자면 아래와 같은 내용으로 구성되어 있고 객체를 생성해서 멤버변수에 그냥 원하는 값들을 채워넣고 toByte() 함수를 호출해서 비트 시프팅된 배열을 받도록 만들었지요.

class DJIHeader{
	public uint _sof = 0xAA;            //8bit starting byte, fixed to be 0xAA
	public uint _length = 0;            //10bit len of frame
	public uint _version = 0;           //6bit version of the frame header, set to be 0
	
	public bool toByte(ref byte[] data){
		long tmpLong1 = 0, tmpLong2 = 0;
		.....
		//LEN(10bit), VER(6bit)
		tmpLong1 = _length & 0x3ff;
		tmpLong2 = _version & 0x3f;
		tmpLong2 <<= 10;
		tmpLong1 |= tmpLong2;
		Buffer.BlockCopy(BitConverter.GetBytes(tmpLong1), 0, data, dataOffset + 1, 2);
		....
	}
}

비트 시프팅에 대해 살짝 부연 설명을 하자면 long이라는 64비트 변수를 두개 깔아두고 byte 길이를 단위기준으로 잘라서 채워넣고 시프팅 하고 byte로 변환해 채워넣고 이런식입니다.

위 DJI 헤더를 기준으로 하면 sof는 그냥 8bit니 그대로 넣어주면 되고  LEN이랑 VER는 두개 합쳐 2byte니 그 두개를 다음과 같이 처리합니다.

LEN은 10bit니 10비트에 해당하는 0x3ff를 and 연산해서 10비트를 넘어서는것들은 잘라내고 long형 변수에 저장합니다 VER도 마찬가지로 6비트만큼만 남겨두고 long형 변수에 저장합니다.

구조체 비트필드  우측에서 좌측으로 비트를 차곡차곡 쌓아가는 방식이니 VER을 10bit 좌측으로 밀어주고 LEN이랑 or 연산을 통해 2바이트 값을 만듭니다 그리고 이걸 byte[]로 변환해서 출력할 버퍼를 채웁니다.

이런식으로 하나하나 채워가다 보면 C#에서도 C언어 보다는 좀 불편하긴 하지만 C언어에서  Struct Bit Field를 사용한것과 같은 패킷을 만들수 있습니다.


Comments

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다