임베디드 스터디 - 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 라이센스를 따릅니다.