포스트

임베디드 스터디 - Struct, Union, Enum 타입과 Bitfields

임베디드 스터디 - Struct, Union, Enum 타입과 Bitfields

이번 글 참고자료:
EmbeddedInterviewlab

Struct 타입

  • 연관있는 변수들을 모아 하나의 데이터 형태로 구현하는 데이터 선언.

Struct 레이아웃과 패딩 (Padding)

  • Struct 안의 변수는 선언한 순서대로 메모리에 지정된다.
    • 이 때, 컴파일러는 칩셋의 워드 크기에 따라 Struct 변수를 최적화하며, 워드 단위로 데이터를 읽기 위해 빈 공간(Padding)을 넣는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Poorly ordered struct -- maximum padding */
struct badly_ordered {
    uint8_t  a;       // offset 0  (1 byte)
                      // 1 byte padding at offset 1
    uint16_t b;       // offset 2  (2 bytes)
    uint8_t  c;       // offset 4  (1 byte)
                      // 3 bytes padding at offsets 5-7
    uint32_t d;       // offset 8  (4 bytes)
};
// sizeof = 12, but only 8 bytes of actual data

/* Same fields, better order -- zero wasted bytes */
struct well_ordered {
    uint32_t d;       // offset 0  (4 bytes)
    uint16_t b;       // offset 4  (2 bytes)
    uint8_t  a;       // offset 6  (1 byte)
    uint8_t  c;       // offset 7  (1 byte)
};
// sizeof = 8 -- no padding at all
  • 만약 Struct 변수가 컴파일 최적화 없이 사용돼야 한다면(Peripheral 통신, 혹은 레지스터 뱅크 사용 시),
    Sturct 변수 내 패딩을 삭제할 수 있다.
    • 단, 패딩 삭제 시 Struct 내 종속 변수에 대한 접근 성능이 저하된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* GCC / Clang */
typedef struct __attribute__((packed)) {
    uint8_t  cmd;
    uint16_t addr;
    uint32_t data;
} spi_frame_t;  // sizeof = 7, guaranteed

/* MSVC / IAR / portable alternative */
#pragma pack(push, 1)
typedef struct {
    uint8_t  cmd;
    uint16_t addr;
    uint32_t data;
} spi_frame_t;
#pragma pack(pop)  // sizeof = 7

Designated Initializers and Compound Literals

  • C에서는 Struct 타입을 초기 선언할 때 부분 초기화 시 명시되지 않은 나머지 필드는 0으로 초기화된다.
    • 단, 전역/static 변수에서만 해당하며, 지역 변수는 초기화하지 않으면 쓰레기 값이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct {
    uint32_t id;
    uint8_t  priority;
    uint8_t  dlc;
    uint8_t  data[8];
} can_msg_t;

/* Designated initializer -- self-documenting, order-independent */
can_msg_t msg = {
    .id       = 0x123,
    .priority = 3,
    .dlc      = 4,
    .data     = {0xDE, 0xAD, 0xBE, 0xEF},
};
// msg.data[4] through msg.data[7] are guaranteed to be 0

/* Compound literal -- creates a temporary struct in-place */
send_can(&(can_msg_t){
    .id  = 0x200,
    .dlc = 2,
    .data = {0x01, 0x02},
});

FAM (Flexible Array Members) 활용

  • FAM은 Struct 타입 변수 마지막에 선언되는 어레이 변수이다. 해당 변수는 struct 변수에 변수 길이를 자유롭게 설정해서 데이터를 삽입하는 것이 가능하다.
  • malloc()없이 사용할 때, union trick를 활용해 버퍼를 삽입하거나, 단순히 어레이의 최대 크기를 설정해서 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct {
    uint16_t length;
    uint8_t  type;
    uint8_t  data[];   // flexible array member -- must be last
} packet_t;

/* Allocate a packet with 64 bytes of payload */
packet_t *pkt = malloc(sizeof(packet_t) + 64);
pkt->length = 64;
pkt->type   = MSG_SENSOR_DATA;
memcpy(pkt->data, sensor_buf, 64);

/* sizeof(packet_t) does NOT include data[] -- it returns
   the size of length + type + any padding (typically 4 bytes) */

Opaque 포인터를 활용한 캡슐화 디자인

  • 라이브러리의 헤더에 struct 타입 정의만 하고, 라이브러리 파일에 struct의 구조를 정의하여 캡슐화 디자인을 적용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* sensor.h -- public API */
typedef struct sensor sensor_t;        // forward declaration only
sensor_t *sensor_create(uint8_t addr);
int       sensor_read(sensor_t *s);
void      sensor_destroy(sensor_t *s);

/* sensor.c -- private implementation */
struct sensor {
    uint8_t  i2c_addr;
    uint16_t last_reading;
    uint8_t  calibration[16];
};

sensor_t *sensor_create(uint8_t addr) {
    sensor_t *s = malloc(sizeof(*s));
    s->i2c_addr = addr;
    return s;
}

Union 타입

  • 같은 메모리 영역을 공유하는 변수 집합을 선언할 수 있다.

Type punning

  • 임의의 데이터타입을 다른 형태로도 사용할 때 union을 사용한다.
  • C에서는 사용가능한 방법이지만 C++에서는 memcpy로 이러한 기능을 대체해야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef union {
    float    f;
    uint32_t u;
} float_bits_t;

/* Inspect the IEEE 754 representation of a float */
float_bits_t fb;
fb.f = -1.0f;
printf("0x%08X\n", fb.u);  // 0xBF800000

/* Extract sign, exponent, mantissa */
uint32_t sign     = (fb.u >> 31) & 1;
uint32_t exponent = (fb.u >> 23) & 0xFF;
uint32_t mantissa =  fb.u & 0x7FFFFF;

레지스터 오버레이

  • 레지스터 전체 영역에 대한 읽기/쓰기를 시행할 때 Struct 타입과 단순 변수를 Union으로 처리하여 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef union {
    struct {
        uint32_t enable     : 1;
        uint32_t tx_en      : 1;
        uint32_t rx_en      : 1;
        uint32_t loopback   : 1;
        uint32_t baud_div   : 16;
        uint32_t reserved   : 12;
    } bits;
    uint32_t raw;
} uart_ctrl_t;

volatile uart_ctrl_t *const UART0_CTRL =
    (volatile uart_ctrl_t *)0x40011000;

/* Read-modify-write using named fields */
uart_ctrl_t tmp = *UART0_CTRL;
tmp.bits.enable = 1;
tmp.bits.tx_en  = 1;
tmp.bits.baud_div = 104;   // 115200 baud
*UART0_CTRL = tmp;

/* Or blast the whole register at once */
UART0_CTRL->raw = 0x00680007;

Bitfield

  • HDL에서 비트 어레이를 정의하듯, C에서도 Struct 타입의 비트범위를 설정하여 선언할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct {
    uint32_t mode       : 2;   // bits [1:0]
    uint32_t speed      : 2;   // bits [3:2]
    uint32_t pull       : 2;   // bits [5:4]
    uint32_t alt_func   : 4;   // bits [9:6]
    uint32_t reserved   : 22;  // bits [31:10]
} gpio_config_t;

volatile gpio_config_t *const GPIOA_CFG =
    (volatile gpio_config_t *)0x40020000;

GPIOA_CFG->mode     = 0x01;  // Output mode
GPIOA_CFG->speed    = 0x03;  // High speed
GPIOA_CFG->alt_func = 0x07;  // AF7 (USART)
  • Bitfield 방식은 이식성(Portability)에 대해 세 가지 문제점을 고려해야한다
    • Bit Ordering : HDL과 달리 C는 LSB나 MSB를 정의할 수 없다. 따라서 컴파일러나 칩셋의 특징에 따라 변수값이 다르게 저장될 수 있다.
    • Storage-Unit Boundary : Bitfield가 데이터 저장 단위 경계를 넘나드는 경우, 컴파일러에 따라 패딩을 삽입하거나 필드를 분리할 수도 있다.
    • int의 Signedness 지정 : 컴파일러에 따라 int <변수명>: <Bitfield>의 데이터 범위가 Signed, Unsigned로 변경될 수 있다.
  • Bitfield의 이식성 문제로 인해 MISRA 나 AUTOSAR에서는 Bitfield 사용을 금지시키고, shift-and-mask 매크로를 사용하도록 권장한다.
1
2
3
4
5
6
/* MISRA-compliant alternative to bitfields */
#define GPIO_MODE_MASK  0x03u
#define GPIO_MODE_SHIFT 0u
#define GPIO_SET_MODE(reg, val) \
    ((reg) = ((reg) & ~(GPIO_MODE_MASK << GPIO_MODE_SHIFT)) \
                     | (((val) & GPIO_MODE_MASK) << GPIO_MODE_SHIFT))

Enum

  • Enum은 정수형 상수에 이름을 붙여 보기 쉽게 처리해주는 데이터 타입이다.
  • Enum의 첫 변수에 초기값을 설정하여 자동으로 상수를 지정하거나, 직접 각각의 변수에 상수를 할당할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
typedef enum {
    Sunday = 0,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
} days_of_week;

days_of_week week;
week = Tuesday;     //week = 2
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.