안녕하세요.
임베디드 시스템 하면서 비트 설정하는 것이 어렵고, 비트 연산도 헷갈릴 때도 있는데, 이때 비트 필드 구조체를 만들어서 사용하면 조금 쉽게 사용할 수 있습니다.
1. 비트 필드 구조체란? (이론 및 필요성)
1) 비트 필드 구조체
임베디드 시스템, 특히 MCU(마이크로컨트롤러) 프로그래밍에서는 레지스터(Register)를 제어하여 하드웨어 기능을 설정하고 상태를 확인합니다.
이 레지스터들은 보통 8비트(1바이트) 또는 16비트(2바이트) 단위로 구성되어 있으며, 각 비트(Bit) 하나하나가 독립적인 의미를 가집니다.
비트 필드 구조체는 이러한 레지스터의 비트 레이아웃(Bit Layout)을 C 언어 코드에 직관적으로 매핑할 수 있게 해주는 특별한 구조체 문법입니다.
2) 기존 비트 연산 방식의 문제점
비트 필드를 사용하지 않을 경우, 개발자는 항상 비트 마스크(1 << N)와 비트 연산자(|, &)를 사용해야 합니다.
// 예: 4번 비트를 설정 (Set)
reg_value |= (1 << 4);
// 예: 1번 비트가 설정되었는지 확인 (Check)
if (reg_value & (1 << 1)) { /* ... */ }
이 방식은 코드가 복잡해지고, 비트의 위치(N)를 기억해야 하므로 가독성과 유지보수성이 떨어집니다.
3) 비트 필드의 장점
비트 필드를 사용하면 레지스터의 특정 비트나 비트 그룹에 마치 일반 변수처럼 접근할 수 있습니다.
- 직관성 및 가독성 향상: status.Bit.TXE = 1;
- 하드웨어 매핑: 레지스터의 구조를 코드가 그대로 반영하여 오류를 줄일 수 있습니다.
- 메모리 최적화: 원하는 비트 수만큼만 정확히 메모리를 할당하므로 공간 낭비가 없습니다.
비트 필드는 일반적으로 union (유니온)과 함께 사용하여, 전체 레지스터(바이트/워드) 접근과 개별 비트(필드) 접근을 모두 가능하게 합니다.
2. 비트 순서 이해하기: LSB와 MSB
비트 필드를 포함하여 데이터를 비트로 다룰 때 MSB(Most Significant Bit)와 LSB(Least Significant Bit)의 개념을 아는 것이 중요합니다. 이는 데이터를 메모리나 레지스터에 저장하는 순서를 나타냅니다.

| 비트 위치 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 비트값 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 0 |
| 자리값 | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
| 실제 기여값 | 0 | 64 | 32 | 0 | 0 | 4 | 0 | 0 |
참고로, 바이트 단위에서 이야기하는 Endian도 간단하게 정리 확인해 보겠습니다.
16비트 = 2바이트 = [상위 바이트][하위 바이트]
(0x12가 상위 바이트, 0x34가 하위 바이트)
표현 바이트 저장 순서 (주소 증가 방향 →) 설명
Big Endian 0x12 0x34 상위 바이트(MSB)가 먼저 저장됨
Little Endian 0x34 0x12 하위 바이트(LSB)가 먼저 저장됨
즉,
Big Endian: 사람이 읽는 순서 그대로 (큰 값 → 작은 값)
Little Endian: 작은 값이 먼저 메모리에 들어감
3. 1바이트(8비트) 비트 필드 예제
1바이트 크기의 가상 통신 상태 레지스터를 정의하고 사용해 보겠습니다.
1) 1바이트 비트 필드 정의
1바이트(uint8_t) 크기의 CommStatusReg_t 유니온을 정의합니다.
typedef union {
// 1. 전체 바이트 접근 (레지스터의 모든 비트 동시 접근)
uint8_t Reg;
// 2. 비트 필드 구조체로 개별 비트에 접근
struct {
uint8_t TXE : 1; // Bit 0: 송신 버퍼 Empty 플래그 (1비트)
uint8_t RXNE : 1; // Bit 1: 수신 데이터 Not Empty 플래그 (1비트)
uint8_t Busy : 1; // Bit 2: 통신 중 상태 (1비트)
uint8_t Mode : 2; // Bit 3~4: 동작 모드 설정 (2비트)
uint8_t : 3; // Bit 5~7: 미사용 (패딩)
} Bit;
} CommStatusReg_t;
2) 비트 필드 사용 실습
// status_byte 변수 선언 및 초기화 (Reg = 0x00)
CommStatusReg_t status_byte = {.Reg = 0x00};
int main(void)
{
/* 초기화 코드 생략 */
while (1)
{
// 1. TXE 플래그 설정 (Bit 0을 1로)
status_byte.Bit.TXE = 1;
// status_byte.Reg는 0x01이 됨
// 2. Mode 필드 (Bit 3,4)에 값 2 (0b10) 설정
status_byte.Bit.Mode = 2;
// Reg 값은 0x01 | 0x08 = 0x09이 됨 (Bit 3이 1)
printf("상태 레지스터 값: 0x%02X\n\r", status_byte.Reg); // 출력: 0x09
// 3. Busy 상태 확인
if (status_byte.Bit.Busy == 0) {
printf("현재 통신 상태: Idle (비트 필드 확인)\n\r");
}
HAL_Delay(2000);
}
}
TXE 플래그를 1로, Mode를 2로 설정하는 과정이 비트 연산 없이 간결하게 처리됨을 알 수 있습니다.
4. 2바이트(16비트) 비트 필드 확장 예제
2바이트(uint16_t) 레지스터에서도 비트 필드를 사용하여 16비트를 효율적으로 분할할 수 있습니다.
1) 2바이트 비트 필드 정의
typedef union {
// 1. 전체 2바이트 접근
uint16_t Reg16;
// 2. 16비트 비트 필드 구조체
struct {
uint16_t ClockEnable : 1; // Bit 0: 클럭 활성화 (1비트)
uint16_t Speed : 3; // Bit 1~3: 속도 설정 (3비트)
uint16_t Reserved : 4; // Bit 4~7: 예약 (4비트 패딩)
uint16_t DataLength : 5; // Bit 8~12: 데이터 길이 (5비트)
uint16_t ErrorFlag : 3; // Bit 13~15: 오류 코드 (3비트)
} Bit16;
} ConfigReg_t;
2) 2바이트 비트 필드 사용 실습
int main(void)
{
ConfigReg_t config_reg = {.Reg16 = 0x0000};
// 1. 클럭 활성화 (Bit 0 = 1)
config_reg.Bit16.ClockEnable = 1;
// 2. Speed를 7로 설정 (3비트 최대 값)
config_reg.Bit16.Speed = 7; // 0b111 -> Bit 1~3이 모두 1
// 3. DataLength를 15로 설정
config_reg.Bit16.DataLength = 15; // 0b01111 -> Bit 8~12
printf("Config 레지스터 값: 0x%04X\n\r", config_reg.Reg16);
// 계산된 Reg16 값: (1) + (7 << 1) + (15 << 8) = 1 + 14 + 3840 = 3855 (0x0F0F)
// 출력: Config 레지스터 값: 0x0F0F
// 4. 오류 플래그 확인
uint8_t error = config_reg.Bit16.ErrorFlag;
printf("현재 오류 코드: %d\n\r", error); // 출력: 0 (초기화하지 않았으므로)
return 0;
}
비트 필드는 임베디드 프로그래밍에서 레지스터를 다루는 가장 효율적이고 직관적인 방법 중 하나입니다.
실제 개발 환경에서 이 기법을 활용하여 코딩 효율을 높여 볼 수 있을 거라고 생각됩니다.
5. 출력 결과 확인
printf구문을 시리얼 포트와 연결할 경우 결과를 확인할 수 있습니다.
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
// 4. (디버깅) 전체 레지스터 값 확인
printf("최종 상태 레지스터 값: 0x%02X\n\r", status_byte.Reg);
HAL_Delay(2000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
status_byte.Bit.Mode = 2;
printf("최종 상태 레지스터 값: 0x%02X\n\r", status_byte.Reg);
HAL_Delay(2000);
status_byte.Bit.Mode = 0;
}
/* USER CODE END 3 */

감사합니다.
<참고 사이트>
1. Embedded C: Struct and Union (Part 2) - Atadiat
https://atadiat.com/en/e-embedded-c-struct-union-part-2/
2. [MSB와 LSB 이해하기]
https://www.hackerschool.org/Sub_Html/HS_University/HardwareHacking/24.html
'Embedded > STM32' 카테고리의 다른 글
| [STM32] STM32G4 ADC 정밀도 높이는 캘리브레이션(Calibration) 함수 사용해 보기 (1) | 2026.03.03 |
|---|---|
| [STM32] UART DMA Circular 모드와 Ring Buffer 이용해 수신해 보기 (0) | 2026.02.22 |
| NUCLEO-C031C6 리뷰 - 저가 저전력 STM32C0 시리즈 보드 (0) | 2025.10.26 |
| STM32 버튼 입력에서 플래그를 활용한 소프트웨어 채터링 약간의 개선 기법 중 한 가지 (0) | 2025.10.04 |
| CORTEX-M4 기초 및 응용, 도서 공부하기 6 - F411RE 버튼 인터럽트 연결하기 (2) | 2025.09.07 |