포스트

임베디드 스터디 - 함수 포인터와 콜백

임베디드 스터디 - 함수 포인터와 콜백

이번 글 참고자료:
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);

콜백 패턴

  • 콜백 기능은 임의의 모듈에서 다른 모듈의 함수를 종속성 없이 실행시킬 때 사용하는 방식이다.
  • 콜백 기능을 구현하는 패턴으로는 총 세 가지가 있다
    1. 이벤트 핸들러
    2. 디스패치 테이블(Switch 구분 대체)
    3. 상태 기계(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 개발 시 다음 사항을 주의해야한다.
RiskMitigation
Null function pointer call호출 전에 반드시 if (fp) 로 NULL 여부를 확인한다
Signature mismatchtypedef를 사용하여 할당 시 컴파일러가 타입을 검사하게 한다
Stale pointer (module unloaded)모듈 해제 시 포인터를 NULL로 초기화하고, 호출 전에 확인한다
Table in RAM can be corruptedconst로 선언하여 테이블을 .rodata(flash)에 배치한다
ISR context issues콜백을 짧게 유지하고, 후속 처리는 플래그나 큐를 통해 메인 루프로 위임한다
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.