함수 포인터와 콜백 완벽 정리: 임베디드 시스템 기초
함수 포인터란? 콜백이란?

들어가며
"함수는 어디에 위차하고 있을까요?"
함수도 다른 것처럼 메모리에 저장되어 있습니다.
그리고 그들도 모두 주소를 가지고 있죠.
프로그램 메모리 구조:
┌────────────────┐
│ Code (Text) │
├────────────────┤
│ main()의 주소 │ 예: 0x08001000
│ MySensor()의 │ 0x08001234
│ 주소 │ 0x08001567
│ ... │ 0x08001890
└────────────────┘
지금까지:
int x = 10; // 변수 주소로 접근 (&x)
int *ptr = &x;
이제:
void MyFunction() { ... } // 함수 주소로 접근 (&MyFunction)
void (*func_ptr)() = &MyFunction;
함수 포인터는 변수 포인터와 마찬가지로 함수의 주소를 저장합니다.
그리고 이를 통해 "나중에 특정 함수를 실행하겠다"는 개념이 탄생하게 되죠.
이것이 바로 콜백(Callback) 이라는 개념입니다.
자동차 ECU에서는 콜백이 무조건 필수입니다.
왜냐하면 안전이라는 개념에 콜백이 꼭 필요하기 떄문이죠
무슨 말이냐구요?
인터럽트:
버튼이 눌렸다 → ISR 콜백 함수 호출
CAN 메시지 도착 → CAN RX 콜백 함수 호출
타이머 만료 → 타이머 콜백 함수 호출
센서 데이터 준비 → 센서 콜백 함수 호출
이 글에서는 함수 포인터의 원리, 콜백의 개념, 그리고 자동차 개발에서의 실무 활용법에 대해 알아보는 시간을 가져보겠습니다.

1. 함수 포인터란?
1.1 정의
함수 포인터 = 함수의 주소를 저장하는 포인터
// 일반 함수
void PrintHello() {
printf("Hello\n");
}
// 함수 포인터 변수
void (*func_ptr)(); // 함수 포인터 선언
// 함수 포인터에 함수 주소 저장
func_ptr = PrintHello; // 또는 &PrintHello
// 함수 포인터를 통해 함수 호출
func_ptr(); // PrintHello()를 간접 호출
// 출력: Hello
1.2 함수 포인터 문법
void (*func_ptr)();
│ │ │
│ │ └─ 함수의 매개변수 (없음)
│ └──────────────── 함수 포인터의 이름
└────────────────────── 함수의 반환 타입
읽는 방법:
"func_ptr는 void를 반환하고, 매개변수가 없는 함수를 가리키는 포인터"
1.3 함수 포인터 선언 (다양한 형태)
// [형태 1] 매개변수 없음, 반환값 없음
void (*func1)();
// [형태 2] int 매개변수 1개, 반환값 없음
void (*func2)(int);
// [형태 3] int, char 매개변수, int 반환
int (*func3)(int, char);
// [형태 4] 배열 포인터 매개변수
void (*func4)(int *arr, int size);
// [형태 5] 구조체 포인터 매개변수
void (*func5)(struct Data *data);
1.4 메모리 레이아웃
메모리:
코드 영역:
┌──────────────────┐
│ main() 코드 │ 주소: 0x08001000
├──────────────────┤
│ PrintHello()코드 │ 주소: 0x08001100
├──────────────────┤
│ Calculate()코드 │ 주소: 0x08001200
└──────────────────┘
데이터 영역 (Stack):
┌──────────────────┐
│ func_ptr │ 주소: 0x20000100
│ [0x08001100] │ ← PrintHello()의 주소 저장!
└──────────────────┘
func_ptr();을 실행하면:
1. func_ptr의 값 읽기 → 0x08001100
2. 그 주소로 점프 → PrintHello() 실행
2. 함수 포인터 사용법
2.1 함수 포인터 초기화
// [방법 1] 선언 후 할당
void PrintRPM(int rpm) {
printf("RPM: %d\n", rpm);
}
int main() {
void (*callback)(int); // 함수 포인터 선언
callback = PrintRPM; // 함수 할당
callback(5000); // PrintRPM(5000) 실행
}
// [방법 2] 선언과 초기화
void (*callback)(int) = PrintRPM;
// [방법 3] & 연산자 명시
void (*callback)(int) = &PrintRPM;
// 결과는 모두 같음
callback(5000); // PrintRPM(5000)
2.2 함수 포인터 호출
// 두 가지 방법 (동일)
// 방법 1: 직접 호출
callback(5000);
// 방법 2: 역참조
(*callback)(5000);
// 결과는 같음 (매개변수 5000을 전달)
2.3 NULL 체크
void (*callback)(int) = NULL;
// 호출 전 체크 필수!
if (callback != NULL) {
callback(5000); // 안전
}
// NULL 체크 없으면 크래시!
// callback(5000); // 위험! (프로그램 충돌)
3. 콜백(Callback)이란?
3.1 정의
콜백 = 나중에 호출할 함수를 미리 등록해두는 메커니즘
일반적인 함수 호출 (Caller → Callee):
┌─────────────┐
│ Caller │
│ (main) │
└──────┬──────┘
│ 함수 호출
▼
┌──────────────┐
│ Callee │
│(MyFunction) │
│ 작업 수행 │
│ 결과 반환 │
└──────────────┘
콜백 호출 (Control Inversion):
┌──────────────┐
│ 프레임워크 │ (ECU, OS, 라이브러리)
│ (Library) │
│ │
│ 1. "콜백을 │
│ 등록해주세요"
└──────┬───────┘
│ 함수 포인터 등록
▼
┌──────────────┐
│ 사용자 코드 │
│ 콜백함수 │ ← 함수 포인터 등록
│ 정의 │
└──────────────┘
이벤트 발생 시:
┌──────────────┐
│ 프레임워크 │
│ 이벤트 감지 │
│ (버튼 클릭) │
└──────┬───────┘
│ 콜백 호출
▼
┌──────────────┐
│ 사용자 코드 │ ← 자동 실행!
│ 콜백함수 │
│ 실행 │
└──────────────┘
3.2 콜백의 흐름
Step 1: 콜백 함수 정의
───────────────────
void OnButtonPressed(int button_id) {
printf("Button %d pressed!\n", button_id);
}
Step 2: 콜백 함수 등록
──────────────────
typedef struct {
int button_id;
void (*on_press)(int); // 콜백 포인터
} Button;
Button btn;
btn.button_id = 1;
btn.on_press = OnButtonPressed; // 콜백 등록
Step 3: 이벤트 대기 (프레임워크가 함)
────────────────────────────────
while (1) {
if (IsButtonPressed(1)) {
// 버튼이 눌렸다!
// Step 4로 이동
}
}
Step 4: 콜백 자동 호출
──────────────────
if (btn.on_press != NULL) {
btn.on_press(1); // OnButtonPressed(1) 호출!
}
4. 자동차 ECU의 인터럽트와 콜백
4.1 인터럽트 = 콜백
자동차의 인터럽트:
버튼 클릭
↓
│GPIO 인터럽트 신호 발생
↓
│ISR (Interrupt Service Routine) 호출
↓
│콜백 함수 실행!
코드로 본다면:
void OnGPIOInterrupt() { // 콜백 함수
printf("Button pressed!\n");
// 이벤트 처리
}
void InitGPIO() {
// GPIO 콜백 등록
RegisterGPIOCallback(OnGPIOInterrupt);
}
// ISR에서:
void GPIO_ISR() {
if (gpio_callback != NULL) {
gpio_callback(); // 콜백 호출
}
}
4.2 CAN 메시지 수신 콜백
// CAN 메시지 수신 콜백 정의
void OnCANMessageReceived(uint32_t can_id, uint8_t *data, uint8_t dlc) {
if (can_id == 0x123) {
// Engine RPM 메시지
int rpm = (data[0] << 8) | data[1];
printf("Engine RPM: %d\n", rpm);
}
}
// CAN 드라이버 구조체
typedef struct {
uint32_t can_id;
void (*on_receive)(uint32_t, uint8_t *, uint8_t); // 콜백
} CANFilter;
CANFilter can_filter;
can_filter.can_id = 0x123;
can_filter.on_receive = OnCANMessageReceived;
// CAN ISR에서:
void CAN_RX_ISR(uint32_t can_id, uint8_t *data, uint8_t dlc) {
if (can_filter.can_id == can_id && can_filter.on_receive != NULL) {
can_filter.on_receive(can_id, data, dlc); // 콜백 호출
}
}
4.3 타이머 인터럽트 콜백
// 타이머 콜백 정의
void OnTimer100ms() {
// 100ms마다 실행
ReadSensors();
UpdateControlLogic();
SendCANMessage();
}
// 타이머 설정
void InitTimer() {
RegisterTimerCallback(100, OnTimer100ms); // 100ms 주기
}
// 타이머 ISR:
void Timer_ISR() {
if (timer_callback != NULL) {
timer_callback(); // 100ms마다 호출
}
}
5. 배열로 여러 콜백 관리
5.1 콜백 배열
#define MAX_SENSORS 8
typedef void (*SensorCallback)(int); // 센서 콜백 타입
SensorCallback sensor_callbacks[MAX_SENSORS];
// 콜백 등록
void RegisterSensorCallback(int sensor_id, SensorCallback callback) {
if (sensor_id < MAX_SENSORS) {
sensor_callbacks[sensor_id] = callback;
}
}
// 모든 센서에서 데이터 수신
void OnAllSensorData() {
for (int i = 0; i < MAX_SENSORS; i++) {
if (sensor_callbacks[i] != NULL) {
sensor_callbacks[i](i); // 각 센서의 콜백 호출
}
}
}
// 사용:
void OnSensor0Data(int id) { printf("Sensor 0: %d\n", id); }
void OnSensor1Data(int id) { printf("Sensor 1: %d\n", id); }
RegisterSensorCallback(0, OnSensor0Data);
RegisterSensorCallback(1, OnSensor1Data);
6. 구조체와 콜백
6.1 센서 구조체에 콜백 포함
typedef struct {
uint16_t id;
int16_t value;
char *name;
void (*on_data_ready)(struct Sensor *); // 콜백 포인터
} Sensor;
// 온도 센서 콜백
void OnTemperatureSensor(Sensor *sensor) {
printf("%s: %d°C\n", sensor->name, sensor->value);
if (sensor->value > 100) {
printf("Temperature too high!\n");
}
}
// 압력 센서 콜백
void OnPressureSensor(Sensor *sensor) {
printf("%s: %d kPa\n", sensor->name, sensor->value);
if (sensor->value < 30) {
printf("Pressure too low!\n");
}
}
// 센서 초기화
Sensor temp_sensor = {
.id = 1,
.name = "Temperature",
.on_data_ready = OnTemperatureSensor
};
Sensor pressure_sensor = {
.id = 2,
.name = "Pressure",
.on_data_ready = OnPressureSensor
};
// 데이터 수신 시
void ProcessSensorData(Sensor *sensor) {
sensor->value = ReadADC(sensor->id);
if (sensor->on_data_ready != NULL) {
sensor->on_data_ready(sensor); // 각 센서의 콜백 호출
}
}
7. 상태 머신과 콜백
7.1 상태별 콜백
typedef enum {
STATE_IDLE,
STATE_RUNNING,
STATE_ERROR,
STATE_MAX
} SystemState;
typedef void (*StateCallback)();
StateCallback state_callbacks[STATE_MAX];
// 상태별 콜백 정의
void OnEnterIdle() {
printf("Entering IDLE state\n");
DisableAllMotors();
SetLED(LED_OFF);
}
void OnEnterRunning() {
printf("Entering RUNNING state\n");
EnableMotor1();
SetLED(LED_GREEN);
}
void OnEnterError() {
printf("Entering ERROR state\n");
DisableAllMotors();
SetLED(LED_RED);
SendWarning();
}
// 콜백 등록
void InitStateMachine() {
state_callbacks[STATE_IDLE] = OnEnterIdle;
state_callbacks[STATE_RUNNING] = OnEnterRunning;
state_callbacks[STATE_ERROR] = OnEnterError;
}
// 상태 전환
void ChangeState(SystemState new_state) {
printf("State: %d → %d\n", current_state, new_state);
if (state_callbacks[new_state] != NULL) {
state_callbacks[new_state](); // 상태 진입 콜백
}
current_state = new_state;
}
8. 고급: 콜백 컨텍스트 (userData)
8.1 문제: 함수 포인터만으로 데이터 전달이 어렵다
!! 문제:
void (*callback)(); // 함수 포인터
// 콜백 호출 시 어떤 데이터를 전달할지?
callback(); // 데이터 전달 어려움
8.2 해결: 콜백 컨텍스트
typedef struct {
int sensor_id;
int threshold;
char *name;
} SensorContext;
typedef struct {
void (*on_event)(void *context); // void * 컨텍스트
void *context; // 데이터 포인터
} Callback;
// 온도 센서 콜백
void OnTemperatureEvent(void *context) {
SensorContext *ctx = (SensorContext *)context;
printf("Sensor %s (ID: %d)\n", ctx->name, ctx->sensor_id);
printf("Threshold: %d\n", ctx->threshold);
}
// 콜백 등록
Callback temp_callback;
SensorContext temp_context = {
.sensor_id = 1,
.threshold = 100,
.name = "Temperature"
};
temp_callback.on_event = OnTemperatureEvent;
temp_callback.context = &temp_context;
// 콜백 호출
if (temp_callback.on_event != NULL) {
temp_callback.on_event(temp_callback.context);
}
9. 실무 시나리오
9.1 Scenario 1: CAN 메시지 수신 처리
#define MAX_CAN_HANDLERS 10
typedef struct {
uint32_t can_id;
void (*on_receive)(uint8_t *data, uint8_t dlc);
} CANHandler;
CANHandler can_handlers[MAX_CAN_HANDLERS];
int can_handler_count = 0;
// CAN 핸들러 등록
void RegisterCANHandler(uint32_t can_id,
void (*callback)(uint8_t *, uint8_t)) {
if (can_handler_count < MAX_CAN_HANDLERS) {
can_handlers[can_handler_count].can_id = can_id;
can_handlers[can_handler_count].on_receive = callback;
can_handler_count++;
}
}
// Engine RPM 메시지 콜백
void OnEngineRPMMessage(uint8_t *data, uint8_t dlc) {
uint16_t rpm = (data[0] << 8) | data[1];
UpdateEngineControl(rpm);
}
// Brake 메시지 콜백
void OnBrakeMessage(uint8_t *data, uint8_t dlc) {
uint8_t brake_pressure = data[0];
ApplyBrake(brake_pressure);
}
// 초기화
void InitCANHandlers() {
RegisterCANHandler(0x100, OnEngineRPMMessage);
RegisterCANHandler(0x200, OnBrakeMessage);
}
// CAN ISR (인터럽트)
void CAN_RX_ISR(uint32_t can_id, uint8_t *data, uint8_t dlc) {
for (int i = 0; i < can_handler_count; i++) {
if (can_handlers[i].can_id == can_id) {
if (can_handlers[i].on_receive != NULL) {
can_handlers[i].on_receive(data, dlc);
}
break;
}
}
}
10. 자주 하는 실수
10.1 NULL 콜백 호출
!! 잘못된 예:
void (*callback)(int); // 초기화하지 않음 (랜덤 주소)
callback(100); // 크래시
!! 올바른 예:
void (*callback)(int) = NULL;
if (callback != NULL) {
callback(100); // 안전
}
10.2 콜백 중 긴 처리
!! 잘못된 예:
void OnTimer100ms() {
// 현재 이 콜백은 ISR에서 호출됨
// ISR 중에 모든 다른 인터럽트 비활성화!
Sleep(500); // !! 500ms 동안 인터럽트 정지!
// 다른 인터럽트 (CAN, GPIO) 놓칠 수 있음
}
-> 올바른 예:
void OnTimer100ms() {
// ISR에서 실행되므로 최소한의 작업만
flag_process_sensors = 1; // 플래그만 설정
}
void MainLoop() {
if (flag_process_sensors) {
ProcessSensors(); // 메인 루프에서 처리
flag_process_sensors = 0;
}
}
10.3 콜백 등록 중복
!! 잘못된 예:
void RegisterCallback(int sensor_id, SensorCallback callback) {
sensor_callbacks[sensor_id] = callback;
// 매번 덮어씀 (이전 콜백 손실)
}
// 여러 번 호출하면 마지막 콜백만 등록됨
-> 올바른 예:
void RegisterCallback(int sensor_id, SensorCallback callback) {
if (sensor_callbacks[sensor_id] != NULL) {
printf("Warning: Callback already registered\n");
return; // 중복 등록 방지
}
sensor_callbacks[sensor_id] = callback;
}
11. 핵심 정리
함수 포인터:
- 함수의 주소를 저장하는 변수
- 문법: void (*func_ptr)(int, char)
- 타입 안정성 필수
콜백:
- "나중에 호출할 함수"
- 이벤트 기반 프로그래밍
- 제어의 역전 (Inversion of Control)
ECU에서의 콜백:
- 인터럽트 핸들러 = 콜백
- CAN 수신 콜백
- 타이머 콜백
- GPIO 콜백
- 센서 콜백
배열과 구조체:
- 여러 콜백을 배열로 관리
- 구조체에 콜백 포인터 포함
- 상태별 콜백 (상태 머신)
컨텍스트:
- void * 포인터로 데이터 전달
- 같은 콜백으로 다양한 데이터 처리
주의:
- NULL 체크 필수
- ISR에서 긴 처리 피하기
- 콜백 중복 등록 주의
'임베디드 기초' 카테고리의 다른 글
| C 포인터와 메모리 관리 완벽 정리: 임베디드 시스템 기초 (0) | 2026.01.22 |
|---|---|
| 임베디드 시스템 완벽 정리: 일상 속 숨겨진 컴퓨터들의 세계 (1) | 2026.01.21 |
| 히스테리시스(Hysteresis) 완벽 정리: 불안정한 경계값을 안정화시키는 기법 (0) | 2026.01.08 |
| 채터링(Chattering) 완벽 정리: 원인, 문제점, 디바운싱 실무 가이드 (0) | 2026.01.07 |
| 포인터와 배열 - 입문자가 반드시 알아야 할 관계 (0) | 2025.12.29 |