구조체, 공용체, 비트필드
레지스터 묶음과 통신 패킷을 구조체로 표현하고, 메모리 정렬과 패딩이 왜 생기는지 확인합니다.
구조체
관련된 값을 하나로 묶습니다. MCU 헤더 파일은 주변장치의 레지스터를 주소 순서대로 구조체 멤버로 선언하고, 그 구조체 포인터를 주변장치 시작 주소에 맞춥니다. TIM2->CNT가 바로 이 방식입니다.
정렬과 패딩
32비트 CPU는 4바이트 값을 4의 배수 주소에 두려 합니다. 그래서 컴파일러가 멤버 사이에 빈 바이트를 넣습니다.
struct { uint8_t a; uint32_t b; }; // sizeof는 5가 아니라 8
통신 패킷을 구조체로 그대로 보내면 이 빈 바이트까지 나가서 상대가 엉뚱하게 해석합니다. 패킷은 바이트 단위로 직렬화하거나 packed 속성을 쓰되 정렬되지 않은 접근의 비용을 알고 씁니다.
공용체
같은 메모리를 여러 자료형으로 봅니다. float를 4바이트 배열로 쪼개 보낼 때 자주 씁니다.
비트필드
uint32_t en : 1;처럼 비트 단위로 멤버를 나눕니다. 읽기는 편하지만 비트 순서가 컴파일러마다 달라서 레지스터나 통신 포맷을 정의할 때는 마스크와 시프트를 쓰는 편이 안전합니다.
구조체는 생각보다 크다
struct a { uint8_t flag; uint32_t value; uint8_t id; }; // 12바이트
struct b { uint32_t value; uint8_t flag; uint8_t id; }; // 8바이트
컴파일러는 각 멤버를 그 크기의 배수 주소에 놓으려고 사이에 빈칸(패딩)을 넣습니다.
큰 멤버부터 차례로 선언하면 패딩이 줄어듭니다. 배열로 수백 개를 만드는 구조체에서는 이 차이가 곧 RAM 사용량입니다.
통신 패킷에 구조체를 그대로 쓰면
typedef struct __attribute__((packed)) {
uint8_t header;
uint16_t length;
uint32_t timestamp;
} packet_t; // 패딩 없이 정확히 7바이트
packed를 붙이면 패딩이 사라져 수신 버퍼에 그대로 겹쳐 읽을 수 있습니다. 대신 두 가지를 조심합니다.
- 정렬되지 않은 접근: 홀수 주소의
uint32_t를 읽게 됩니다. Cortex-M0처럼 이를 지원하지 않는 코어에서는 폴트가 납니다. - 바이트 순서: 보내는 쪽과 받는 쪽의 엔디안이 다르면 값이 뒤집힙니다. 프로토콜 문서에 바이트 순서를 명시하고, 안전하게는 한 바이트씩 조립합니다.
uint16_t len = (uint16_t)buf[1] | ((uint16_t)buf[2] << 8); // 리틀 엔디안으로 조립
무료 전자책이 주제를 한 권으로 이어 읽기 (PDF 3권)→공용체: 같은 메모리를 두 가지로 보기
typedef union {
uint32_t word;
uint8_t byte[4];
} u32_t;
u32_t v; v.word = 0x12345678;
// 리틀 엔디안 MCU: byte[0] = 0x78, byte[3] = 0x12
32비트 값을 바이트 단위로 쪼개 보낼 때 편리합니다. 다만 결과가 엔디안에 따라 달라지므로 다른 종류의 MCU와 주고받는 데이터에는 시프트 연산으로 직접 쪼개는 편이 안전합니다.
비트필드: 편하지만 이식성은 없다
typedef struct {
uint32_t enable : 1;
uint32_t mode : 2;
uint32_t : 5; // 예약
uint32_t prescaler : 8;
} ctrl_t;
코드가 읽기 쉬워지지만, 비트를 어느 쪽부터 채우는지는 컴파일러가 정합니다. 같은 선언이 다른 컴파일러에서는 다른 비트 위치가 될 수 있습니다. 그래서 하드웨어 레지스터나 통신 패킷처럼 비트 위치가 정해져 있는 곳에는 마스크와 시프트를 쓰고, 비트필드는 프로그램 내부의 플래그 묶음에만 쓰는 것이 일반적인 기준입니다.
구조체를 함수에 넘길 때
값으로 넘기면 구조체 전체가 스택에 복사됩니다. 포인터로 넘기면 4바이트만 전달됩니다. 읽기만 한다면 const를 붙여 의도를 드러냅니다.
void motor_update(const motor_cfg_t *cfg, motor_state_t *st);
스스로 확인
struct { uint8_t a; uint16_t b; uint8_t c; }의 크기는 몇 바이트일까요? 순서를 바꾸면 얼마까지 줄일 수 있을까요?sizeof(packet_t)로 수신 길이를 검사하는 코드는packed를 빼면 어떻게 될까요?
영상으로 이어 보기공부법과 진로 시리즈 15편2025 임베디드 프로그래밍 공부 순서 | 공부 방법 | 로드맵PROJECT COURSEC 문법을 실제 펌웨어 코드로 써 보고 싶다면밸런싱 로봇 하나를 요구사항, 회로와 PCB, 펌웨어, 앱까지 실무 순서대로 만드는 강의입니다. 10파트 165강.강의 살펴보기 →FREE PDF무료 전자책 3권 받기이 주제를 한 권으로 이어서 읽고 싶다면. BLDC 모터 제어, 직렬통신, 센서를 책 분량으로 묶었습니다. 이메일만 남기면 바로 받습니다.무료로 받기 →