[왕초보 임베디드-C 공부 일기] 17. 열거형(enum), 그리고...다음 챕터로!
임베디드 시스템은 본질적으로 수많은 상태(State)를 가진다. 센서의 상태는 '정상, 오류, 대기'로 나뉠 수 있고, 로버의 주행 상태는 '직진, 정지, 회전'으로 나뉠 수 있다. 이러한 상태를 하드웨어 레지스터에 기록하기 위해 0, 1, 2와 같은 의미 없는 숫자를 직접 코드에 적용하는 것은 좋은 방법이 아니다. C언어의 열거형(Enumeration) 은 이러한 정수형 상수들의 집합에 명확한 '이름'과 '규격'을 부여하여, 시스템의 안전성과 가독성을 높일 수 있는 자료형이다. ---1. 정의
열거형은 논리적으로 연관된 정수형 상수들의 집합을 정의하는 사용자 정의 자료형이다.- 기계어 레벨의 취급 : 컴파일러는 열거형의 멤버들을 단순한 정수로 취급한다. 즉, 메모리 상에서는 4바이트 정수로 동작한다.
- 자동 할당 규칙 : 값을 명시하지 않으면 첫 번째 요소는 0이 되며, 이후 요소들은 이전 값보다 1씩 자동으로 증가한다.
- 명시적 할당 : 임베디드 통신 프로토콜이나 오류 코드처럼 특정 16진수 값이 강제되어야 할 때에는 명시적으로 값을 지정할 수도 있다.
// [방식 A] 자동 할당
typedef enum {
STATE_INIT, // 0으로 자동 할당
STATE_IDLE, // 1로 자동 할당
STATE_RUNNING, // 2로 자동 할당
STATE_ERROR // 3으로 자동 할당
} SystemState_e;
// [방식 B] 명시적 할당 (하드웨어 오류 코드 등)
typedef enum {
ERR_NONE = 0x00,
ERR_TIMEOUT = 0x01,
ERR_SENSOR_FAIL = 0x02,
ERR_CRITICAL = 0xFF // 임의의 값 지정 가능
} ErrorCode_e;
---
2. #define 매크로 대신 enum을 쓰는 경우
C언어에서 상수를 정의할 때 #define 을 사용할 수도 있지만, 실무 펌웨어 개발에서는 상태나 옵션을 정의할 때 반드시 enum을 사용한다. 그 이유는 디버깅(Debuging) 과 자료형의 독립성에 있다.
- 디버거(Debugger)의 가시성 :
#define STATE_IDLE 1로 정의하면, 컴파일러가 코드를 번역하기 전에 전처리기(Preprocessor)가 이를 모두 숫자 1로 치환해 버린다. 나중에 디버거로 메모리를 열어보면 그냥 1이라고만 보여서 무슨 상태인지 알 수 없다. 반면 enum은 심볼(Symbol)정보가 유지되어(메모리를 차지하지는 않는다.), 디버거 화면에 숫자 1대신 STATE_IDLE이라는 글자가 명확하게 표시된다. - 타입 안전성 : 서로 다른 종류의 상태(모터 상태, 통신 상태, 센서 상태...)를 각각의 enum타입으로 묶어두면, 함수 매개변수로 넘길 때 개발자가 엉뚱한 상태 값을 넣는 실수를 방지할 수 있다.
3. 구조체(Struct) 및 배열(Array)과의 결합
이전에 배운 개념들과 열거형이 결합하면 코드를 한층 더 체계적으로 작성할 수 있다.1) 구조체 내부의 상태 플래그
구조체 안에 변수들의 상태를 나타내는 멤버로 enum을 배치한다.typedef struct {
uint32_t current_speed;
float temperature;
SystemState_e move_state; // enum 타입으로 구조체 멤버 선언
ErrorCode_e last_error; // enum 타입으로 오류 기록
} RoverControl_t;
2) 열거형을 활용한 배열 인덱싱
열거형이 0, 1, 2..로 증가하는 정수라는 점을 이용하여, 베열의 크기나 인덱스로 활용하는 유용한 방법이다.typedef enum {
SENSOR_FRONT, // 0
SENSOR_LEFT, // 1
SENSOR_RIGHT, // 2
SENSOR_BEHIND // 3 (마지막에 위치시키면 자동으로 배열의 '크기'를 나타내게 됨.)
} Side_Sensor_e;
// SENSOR_BEHIND(3)을 이용해 배열의 크기를 동적으로 확보
uint32_t sensor_readings[SENSOR_COUNT];
// 매직 넘버(0, 1) 대신 enum을 인덱스로 사용하여 가독성 극대화
sensor_readings[SENSOR_FRONT] = 150;
4. 실무 사례 : 하드웨어 드라이버 설계(GPIO 제어)
임베디드 보드의 핀 하나를 제어하기 위해서는 '동작 모드(MODER)', '풀업/풀다운 저항 사용 여부(PUPDR)', '동작 속도(OSPEEDR)' 등 수많은 레지스터 설정이 필요하다. 과거에는 레지스터에 0x01, 0x02같은 설정 값을 직접 입력해야했지만, 현대의 펌웨어(HAL 드라이버 등)은 모든 설정 값을 enum으로 정의하고 이를 구조체로 묶어(초기화 패킷) 함수로 전달하는 정돈된 방식을 사용한다. 이전 장에서 예시로 다룬 레지스터 구조체 포인터(GPIO_TypeDef)와 결합해 살펴보자.
#include
// 1. 하드웨어 설정값들을 열거형(Enum)으로 엄밀하게 정의
// 데이터시트에 적힌 레지스터 셋팅 값을 명시적으로 할당
typedef enum {
GPIO_MODE_INPUT = 0x00,
GPIO_MODE_OUTPUT = 0x01,
GPIO_MODE_AF = 0x02, // Alternate Function (통신 등)
GPIO_MODE_ANALOG = 0x03
} GPIOMode_e;
typedef enum {
GPIO_NOPULL = 0x00,
GPIO_PULLUP = 0x01,
GPIO_PULLDOWN = 0x02
} GPIOPull_e;
// 2. 구조체와 열거형의 결합 : GPIO 초기화 패킷 (Configuration Struct)
// 설정하고자 하는 핀 번호와 셋팅할 enum 값들을 하나의 바구니로 묶는다.
typedef struct {
uint32_t Pin; // 제어할 핀 번호 (예 : 1 << 5)
GPIOMode_e Mode; // 동작 모드r
GPIOPull_e Pull; // 풀업/풀다운 설정
.
// 원래는 GPIO핀 초기화를 위해 총 5개의 설정이 추가로 필요하다.
.
.
.
} GPIO_InitTypeDef;
// 3. 드라이버 초기화 함수 (Call By Reference)
// 레지스터 맵(GPIOx)과 초기화 패킷(init)을 포인터로 전달받아 물리적으로 설정한다.
void GPIO_Init(GPIO_Typedef *GPIOx, GPIO_InitTypeDef *Init) {
// 개부적으로 Init->Mode 와 Init->Pull 값을 읽어서
// 앞서 배운 비트 마스킹(A & ~B | C) 기법으로 GPIOx->MODER 레지스터에 값을 작성한다.
// 디버거로 이 함수 내부를 보면, 매개변수 Init->Mode 안에 숫자 1이 아니라
// 'GPIO_MODE_OUTPUT'이라는 문자가 명확히 적혀 있어 디버깅이 매우 편리해진다.
// 요약 : 버그 관리에 용이해진다.
}
int main() {
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.Pin = (1 << 5); // 5번 핀 선택
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT; // 출력 모드로 설정
GPIO_InitStruct.Pull = GPIO_NOPULL; // 풀업/풀다운 저항 미사용
// 실제 하드웨어 포트 A에 직접 제작한 설정 구조체를 밀어 넣음
GPIO_Init(GPIOA, &GPIO_InitStruct);
return 0;
}
위 예시를 통해 정수형 설정값을 열거형으로 묶은 뒤에 구조체 내에 멤버로 사용할 수 있는 유용한 사례를 알 수 있었다. 이는 단순히 가독성을 좋게 할 뿐만 아니라 잘못된 설정값으로 인한 하드웨어 파손을 막는 안전성을 높인다. 예를 들어 GPIO_InitStruct.Mode = 99; 라고 엉뚱한 값을 넣으려 하면, 컴파일러가 "그 값은 GPIOMode_e타입에 정의되어 있지 않다" 라고 즉각적으로 경고를 띄워주는 것이다.
---
요약
> 열거형(Enum) 은 인간의 언어(단어)를 기계의 언어(정수)로 완벽하게 1:1 연결해주는 유일한 공식 문법이다. ---[왕초보 임베디드 공부 일기] 종료 기념 주저리주저리
다소 의식의 흐름대로 말하니 주의해주세요...
글을 작성하면서 든 생각들, 그리고 나의 진짜 목표
17챕터에 걸친 '왕초보 임베디드-C 공부 일기' 를 마무리하려 한다. 이 글을 시작할 때는 그저 내가 공부하는 것들을 복습 차원에서 기록하는 느낌으로 업로드하려 했으나, 적다보니 욕심이 생겨 추가적으로 학습을 진행하거나 AI의 도움을 빌려 조금이라도 더 완성도 있는 글을 작성하려고 노력했던 것 같다. 그 과정에서 AI의 도움을 지나치게 많이 빌린 글이 나오기도 했고, 개요를 정형화시켜야하는지, 글마다 다른 개요로 접근해도 되는지 고민이 들기도 했다. 그러한 고민들 끝에 불완전하지만 왕초보 임베디드-C 공부 일기를 마무리할 수 있었고, 추후에 콘텐츠는 어떻게 작성해야할지 밑그림이 잡히기도 했다.공대 휴학생으로써 감히 내 처지를 객관화하면, 'AI시대에 기계를 가까이하는 대학생'이라고 할 수 있을 것 같다. 어떻게 보면 시대를 역행하고 있는, 동떨어진 느낌이고, 또 어떻게 보면 정반대편에 있기 때문에 '나중에 AI를 사용할 때에 가장 역동적으로 사용할 수 있게 되지 않을까?' 하는 생각이 든다(예 : Physical AI?). 하지만 휴학하기 전에 기계공학과를 겨우 한 학기 다녔고, 그 한 학기마저도 성적을.....말아먹었다. 그래서 어쩌면 도망치듯이 휴학해서 공학에 대한 기본적인 학문적 지식도 없이, 임베디드 공부 하겠답시고 이것저것 찾아보고 물어보며(AI에게) 공부하는 상황인데, 결국에 휴학을 했으니 무언가 결과물을 내야 한다는 생각이 가장 강하게 든다. --- 결론부터 말하자면 올해 안에 Self-balancing-robot을 혼자서 만들어보는 것이 목표이다. 유튜브에 찾아보면 다양한 예시들이 나오는데, 아래 사진을 통해 바로 확인할 수 있다.
이것을 구현하려면 소프트웨어 단에서는 크게 세 가지 요소가 필요하다.
- 드라이버(30%)
- OS(30%)
- 알고리즘(40%)
또 다른 콘텐츠
kdevcon에 연재하는 콘텐츠와 별개로, 얼마 전에 유튜브 채널을 열어 기초적인 회로 실습을 진행하는 영상을 업로드했다. 아직 보잘것없지만, 글보다 내가 학습하고 있는 것들을 더 잘 보여줄 수 있는 것이 영상이라고 생각하기에 텀이 길더라도 긴 호흡으로, 하지만 확실히 진행해보려 한다. Phase 0.5에 해당하는 이론과 실습 동영상도 업로드될 예정이니 응원을...해줬으면 좋겠다. 유튜브 링크 : https://www.youtube.com/@BUZZLIGHTYEAR2578깃허브 링크 : https://github.com/justyoonjh ---