임베디드 스터디 - 함수 포인터와 콜백
임베디드 스터디 - 함수 포인터와 콜백
이번 글 참고자료:
EmbeddedInterviewlab
Function Pointer
- 함수의 주소를 포인팅하는 포인터 변수
- 일반적으로 포인터 변수 생성과 동일하게 선언할 수 있지만,
typedef를 써서 선언하는 것이 가독성이 좋다.
1
2
3
4
5
6
7
8
9
10
11
/* Declaring without typedef -- ugly but you must be able to read it */
void (*callback)(uint8_t event_id); /* ptr to void f(uint8_t) */
int (*compare)(const void *, const void *); /* ptr to int f(const void*...) */
/* Declaring WITH typedef -- strongly preferred in real code */
typedef void (*event_cb_t)(uint8_t event_id);
typedef int (*comparator_t)(const void *, const void *);
event_cb_t on_button = NULL; /* readable, self-documenting */
comparator_t cmp = NULL;
typedef로 선언하면 함수 포인터 어레이, struct 타입 선언 시 가독성이 좋아진다.
1
2
3
4
5
6
7
8
9
10
11
12
13
typedef void (*cmd_handler_t)(const uint8_t *payload, uint16_t len);
/* Array of function pointers -- dispatch table */
cmd_handler_t cmd_table[CMD_COUNT];
/* Struct member */
typedef struct {
cmd_handler_t handler;
const char *name;
} cmd_entry_t;
/* Function parameter -- registering a callback */
void register_handler(uint8_t cmd_id, cmd_handler_t handler);
콜백 패턴
- 콜백 기능은 임의의 모듈에서 다른 모듈의 함수를 종속성 없이 실행시킬 때 사용하는 방식이다.
- 콜백 기능을 구현하는 패턴으로는 총 세 가지가 있다
- 이벤트 핸들러
- 디스패치 테이블(Switch 구분 대체)
- 상태 기계(State Machine) 구현
Event Handler : 레지스터 동작, 동작 알림
- 드라이버에서 함수를 관리하고, 어플리케이션에서 핸들러를 생성한 뒤, 이벤트가 발생하면 드라이버의 함수가 호출되는 형태
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef void (*button_cb_t)(uint8_t pin, uint8_t state);
static button_cb_t user_cb = NULL;
void button_register_callback(button_cb_t cb) {
user_cb = cb;
}
/* Called from ISR or polled loop */
void button_isr(void) {
uint8_t pin = /* ... */;
uint8_t state = /* ... */;
if (user_cb) { /* null check -- critical */
user_cb(pin, state);
}
}
Dispatch Table : 이벤트 동작, 명령 동작
- 긴
switch문법을 대체하는 형태. Command ID를 인덱스로 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef void (*cmd_handler_t)(const uint8_t *payload, uint16_t len);
static void cmd_ping(const uint8_t *p, uint16_t len) { /* ... */ }
static void cmd_reset(const uint8_t *p, uint16_t len) { /* ... */ }
static void cmd_read(const uint8_t *p, uint16_t len) { /* ... */ }
/* const -> .rodata (flash), saves RAM */
static const cmd_handler_t cmd_table[] = {
[CMD_PING] = cmd_ping,
[CMD_RESET] = cmd_reset,
[CMD_READ] = cmd_read,
};
void dispatch(uint8_t cmd_id, const uint8_t *payload, uint16_t len) {
if (cmd_id < sizeof(cmd_table) / sizeof(cmd_table[0])
&& cmd_table[cmd_id] != NULL) {
cmd_table[cmd_id](payload, len);
}
}
State Machine
- 다음 상태를 Function Pointer로 구현하는 형태. State 전환이 Function 실행으로 대체된다.
- 상태 추가 시 기존 코드를 수정하지 않고 새 State 함수만 추가하면 된다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef void (*state_fn_t)(void);
static void state_idle(void);
static void state_sampling(void);
static void state_transmit(void);
static state_fn_t current_state = state_idle;
void state_machine_run(void) {
if (current_state) {
current_state(); /* each state sets current_state to the next */
}
}
static void state_idle(void) {
if (start_requested()) {
current_state = state_sampling;
}
}
static void state_sampling(void) {
read_sensor();
current_state = state_transmit;
}
static void state_transmit(void) {
send_data();
current_state = state_idle;
}
Function Pointer 활용 예제
qsort 콜백 함수
qsort에서 각 인자 정렬 시 정렬 함수를 넣어줘야한다. 이 때, 정렬 함수를 전달하는 방식이 call back의 정석적인 예시이다.qsort에서 주의할 점은 정렬 함수의 결과를 단순히return *(int*)a - *(int*)b;으로 처리하면 안된다.uint의 경우 오버플로우가 발생할 수 있기에,return (*(int*)a > *(int*)b) - (*(int*)a < *(int*)b)같이 비교연산자를 직접 활용하는게 좋다.
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdlib.h>
/* Comparator: negative if a < b, zero if equal, positive if a > b */
int compare_uint16(const void *a, const void *b) {
uint16_t va = *(const uint16_t *)a;
uint16_t vb = *(const uint16_t *)b;
/* Subtraction trick is UNSAFE for large values (overflow).
Use explicit comparison for production code. */
return (va > vb) - (va < vb);
}
uint16_t readings[64];
qsort(readings, 64, sizeof(uint16_t), compare_uint16);
Function Pointer를 활용한 HAL 추상화
- 어플리케이션 레벨과 HAL의 독립성을 유지시키기 위해 Function Pointer를 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
typedef struct {
int (*init)(uint32_t baud);
int (*write)(const uint8_t *buf, uint16_t len);
int (*read)(uint8_t *buf, uint16_t len);
void (*deinit)(void);
} uart_driver_t;
/* Platform-specific implementation */
static int stm32_uart_init(uint32_t baud) { /* ... */ return 0; }
static int stm32_uart_write(const uint8_t *buf, uint16_t len) { /* ... */ return len; }
static int stm32_uart_read(uint8_t *buf, uint16_t len) { /* ... */ return len; }
static void stm32_uart_deinit(void) { /* ... */ }
/* Driver instance -- const places it in flash */
const uart_driver_t uart0 = {
.init = stm32_uart_init,
.write = stm32_uart_write,
.read = stm32_uart_read,
.deinit = stm32_uart_deinit,
};
/* Application code -- platform-agnostic */
void app_send(const uart_driver_t *drv, const uint8_t *msg, uint16_t len) {
if (drv && drv->write) {
drv->write(msg, len);
}
}
Vtable (Virtual table) 패턴
- C++의
virtual같이 Function Pointer 테이블을 생성하여 공통적인 API의 형태로 드라이버를 구현한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Base "class" */
typedef struct sensor sensor_t;
typedef struct {
int (*init)(sensor_t *self);
int16_t (*read)(sensor_t *self);
void (*sleep)(sensor_t *self);
} sensor_vtable_t;
struct sensor {
const sensor_vtable_t *vtable; /* pointer to shared vtable in flash */
uint8_t i2c_addr;
/* ... other common fields ... */
};
/* Generic API -- works on any sensor */
int16_t sensor_read(sensor_t *s) {
if (s && s->vtable && s->vtable->read) {
return s->vtable->read(s);
}
return -1;
}
Function Pointer 주의사항
- Function Pointer의 사용 중에 오류가 생기면 디버깅하기 까다롭다. 이에 Function Pointer 개발 시 다음 사항을 주의해야한다.
| Risk | Mitigation |
|---|---|
| Null function pointer call | 호출 전에 반드시 if (fp) 로 NULL 여부를 확인한다 |
| Signature mismatch | typedef를 사용하여 할당 시 컴파일러가 타입을 검사하게 한다 |
| Stale pointer (module unloaded) | 모듈 해제 시 포인터를 NULL로 초기화하고, 호출 전에 확인한다 |
| Table in RAM can be corrupted | const로 선언하여 테이블을 .rodata(flash)에 배치한다 |
| ISR context issues | 콜백을 짧게 유지하고, 후속 처리는 플래그나 큐를 통해 메인 루프로 위임한다 |
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.