[왕초보 임베디드-C 공부 일기] 16. 공용체(Union)

이번에 다룰 공용체(Union)은 구조체와 문법이 거의 똑같지만, 메모리를 다루는 방식이 정반대이다. PC 프로그램에서는 메모리를 아끼기 위한 구시대적 문법으로 취급받기도 하지만, 메모리의 바닥을 제어하는 임베디드 시스템에서는 통신 패킷을 일고 하드웨어 레지스터를 제어하기 위한 열쇠로 기능한다. ---

1. 정의

공용체는 내부에 선언된 여러 멤버 변수들이 단 하나의 물리적 메모리 공간을 공유(Overlap)하는 사용자 정의 자료형 이다.

기본 예시

#include 
#include 

// 공용체 선언 (구조체와 문법이 동일하다.)
typedef union {
	uint32_t as_integer; // 4바이트
	float as_float;      // 4바이트
	uint8_t as_byte;     // 1바이트
}  DataCaster_u;

int main() {
	DataCaster_u data;
	
	// 4바이트 공간에 정수 0x12345678 을 기록한다.
	data.as_integer = 0x12345678;
	
	//같은 메모리 공간을 1바이트짜리 as_byte의 시선으로 읽어온다.
	// 결과 : 0x78 
	printf("1바이트만 읽기 : 0x%X\n", data.as_byte);
	
	return 0;
}


2. 구조체와 공용체의 차이

두 자료형의 차이는 덧셈최댓값으로 명확히 구분된다. uint32_t (4) + uint16_t (2) = 최소 6바이트 이상의 메모리를 길게 이어 붙여 차지한다. (패딩 제외) 각 변수는 자신만의 독립적인 공간을 갖는다. uint32_t (4)와 uint16_t (2) 중 큰 값인 4바이트 딱 하나만 메모리에 만들어진다. 두 변수는 같은 방을 쓰며, 각 변수의 이름과 해석 방식만 다른 것이다. ---

3. 개념의 융합 : 임베디드 통신 문제 해결

지금까지 배운 배열, 포인터, 기본 자료형의 지식이 공용체와 결합하면 임베디드 통신의 난제를 해결할 수 있다. 센서에서 float 온도 값(4바이트)을 UART 통신으로 보낼 때, 하드웨어 핀은 한 번에 1바이트(8비트)씩만 데이터를 전송할 수 있다. 수신 측에서는 조각난 4개의 바이트 배열을 다시 float로 조립해야 한다. 과거에는 포인터 강제 캐스팅(*(float*)buffer)을 썼지만, 이는 안전하지 않은 방식이다. 공용체를 사용하면 깔끔하게 해결된다.
// 배열과 실수가 같은 4바이트 공간을 공유하도록 묶는다. 
typedef union {
	float final_temp;
	uint8_t rx_buffer[4];
} TempPacket_u;

void receive_uart_data() {
	TempPacket_u packet;
	
	// 통신으로 1바이트씩 들어오는 데이터를 배열(rx_buffer) 시선으로 차곡차곡 채워 넣는다.
	packet.rx_buffer[0] = 0x00;
	packet.rx_buffer[1] = 0x00;
	packet.rx_buffer[2] = 0xC8;
	packet.rx_buffer[3] = 0x41; // 0x41C80000 은 IEEE-754 규격으로 25.0f 이다. 
	
	// 데이터를 모두 채운 뒤, final_temp 시선으로 읽어오기만 하면
	// 번거로운 비트 시프트나 포인터 캐스팅 없이 즉시 실수(float)로 변환된다. 
	printf("현재 온도: %.lf\n", packet.final_temp);
}


이렇게 1바이트 단위의 통신 버퍼(배열)를 수십 바이트짜리 복잡한 데이터 패킷(구조체)으로 단번에 덮어씌워 해석하는 기법을 타입 펀닝(Type Punning)이라고 한다. 이 기법을 사용하면 메모리가 부족하고 처리 속도가 생명인 로버의 임베디드 펌웨어에서 데이터를 가장 효율적으로 해석할 수 있다. ---

4. 실전 복합 사례 : 구조체와 공용체의 하이브리드 (레지스터 제어)

임베디드 하드웨어 레지스터 제어의 정석이자, 실무에서 가장 많이 쓰이는 패턴이다. 특정 32비트 하드웨어 레지스터를 조작할 때, 어떤 경우에는 "32비트 전체를 한 번에" 초기화하고 싶고, 어떤 경우에는 "특정 1바이트만" 조작하고 싶을 때가 있다. 이때 바깥은 공용체, 안쪽은 구조체로 설계하면 완벽한 객체지향적 하드웨어 제어가 가능해진다.
// 32비트(4바이트) 하드웨어 레지스터를 위한 하이브리드 자료형
typedef union {
	uint32_t ALL; // 4바이트 전체를 한 번에 제어하는 시선 
	
	//1바이트씩 4개로 쪼개서 개별 제어하는 구조체 시선
	struct {
		uint8_t byte0; // 최하위 바이트 (비트 0~7)
		uint8_t byte1; // (비트 8 ~ 15)
		uint8_t byte2; // (비트 16 ~ 23)
		uint8_t byte3; // 최상위 바이트 (비트 24~31)
	} BYTES_t;
} HardwareReg_u;

int main() {
	HardwareReg_u timer_reg;
	
	// 1. 공용체의 ALL 멤버를 사용해 32비트 전체를 한 번에 초기화 (빠름)
	timer_reg.ALL = 0x00000000;
	
	// 2. 구조체 멤버를 타고 들어가 특정 바이트만 비트 연산으로 조작 (정밀함)
	timer_reg.BYTES.byte1 = timer_reg.BYTES.byte1 | 0x0F;
	
	// 3. 다시 전체 값을 확인해보면, byte1의 조작이 ALL에도 완벽하게 반영되어 있다. 
	printf("최종 레지스터 값 : 0x%08X\n", timer_reg.ALL); // 출력: 0x00000F00
	
	return 0;
}
---

요약

구조체가 변수들의 물리적 공간를 넓히는 도구라면,
공용체는 동일한 공간을 어떤 안경을 쓰고 바라볼지 결정하는 도구이다.