Embedded/STM32

STM32 MCU에서 비트 필드(Bit Field) 구조체 사용해 보기

변화의 물결1 2025. 11. 12. 01:06

 

 

 

안녕하세요.

 

 임베디드 시스템 하면서 비트 설정하는 것이 어렵고, 비트 연산도 헷갈릴 때도 있는데, 이때 비트 필드 구조체를 만들어서 사용하면 조금 쉽게 사용할 수 있습니다.

 


 

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

반응형