포인터 심화와 함수 포인터
포인터 연산, 이중 포인터, 그리고 콜백과 상태 머신에 쓰는 함수 포인터를 정리했습니다.
포인터 연산
p + 1은 주소에 1을 더하는 것이 아니라 가리키는 자료형 크기만큼 이동합니다. uint32_t *p라면 4바이트를 건너뜁니다. 배열 이름이 첫 요소의 주소처럼 동작하기 때문에 buf[i]와 *(buf + i)는 같습니다.
이중 포인터
포인터의 주소를 담는 포인터입니다. 함수 안에서 포인터 자체를 바꾸고 싶을 때(예: 버퍼를 할당해 돌려줄 때) 씁니다.
함수 포인터
함수도 주소를 가집니다. 그 주소를 변수에 담아 두면 실행할 함수를 나중에 고를 수 있습니다.
typedef void (*handler_t)(uint8_t);
handler_t on_rx = parse_command;
on_rx(byte); // parse_command(byte) 실행
펌웨어에서 쓰는 곳은 이렇습니다.
- 콜백: 드라이버가 "수신 완료" 같은 일을 위쪽 코드에 알릴 때
- 상태 머신: 상태마다 처리 함수를 표에 넣어 두고 현재 상태의 함수를 호출
- 벡터 테이블: 인터럽트 처리 함수 주소를 모아 둔 배열 자체가 함수 포인터 표입니다
함수도 주소가 있다
함수는 Flash의 어느 주소에 놓인 코드입니다. 함수 포인터는 그 주소를 담는 변수이고, 담긴 주소로 뛰어 실행할 수 있습니다.
void led_on(void) { /* ... */ }
void led_off(void) { /* ... */ }
void (*action)(void) = led_on; // 주소를 담고
action(); // 그 주소의 함수를 실행
action = led_off; // 실행 중에 바꿀 수 있음
선언이 낯설면 typedef로 이름을 붙입니다.
typedef void (*handler_t)(uint8_t arg);
쓰임 1: 명령 테이블
switch가 길어지는 대신, 명령 번호와 처리 함수를 표로 묶습니다.
typedef struct { uint8_t id; void (*fn)(const uint8_t *data, uint8_t len); } cmd_t;
static const cmd_t table[] = {
{ 0x01, cmd_read_status },
{ 0x02, cmd_set_speed },
{ 0x10, cmd_reset },
};
void dispatch(uint8_t id, const uint8_t *data, uint8_t len) {
for (size_t i = 0; i < sizeof(table) / sizeof(table[0]); i++)
if (table[i].id == id) { table[i].fn(data, len); return; }
send_nack(id); // 표에 없는 명령
}
명령을 추가할 때 표에 한 줄만 넣으면 됩니다. 표가 const이므로 Flash에 놓여 RAM도 쓰지 않습니다.
쓰임 2: 콜백으로 계층 나누기
드라이버가 응용 코드를 직접 부르면 서로 얽힙니다. 대신 "일이 끝나면 이 함수를 불러 달라"고 등록합니다.
static void (*rx_cb)(uint8_t byte);
void uart_set_rx_callback(void (*cb)(uint8_t)) { rx_cb = cb; }
void USART1_IRQHandler(void) {
uint8_t b = (uint8_t)USART1->RDR;
if (rx_cb) rx_cb(b); // NULL 확인 후 호출
}
UART 드라이버는 누가 데이터를 쓰는지 모릅니다. 같은 드라이버를 다른 프로젝트에 그대로 가져갈 수 있습니다.
쓰임 3: 상태 머신
typedef void (*state_fn)(void);
static state_fn state = state_idle;
void state_idle(void) { if (start_pressed()) state = state_running; }
void state_running(void) { if (fault()) state = state_error; }
while (1) { state(); }
지금 상태가 곧 실행할 함수입니다. 상태가 많아져도 메인 루프는 한 줄입니다.
벡터 테이블도 함수 포인터 배열이다
MCU의 인터럽트 벡터 테이블은 핸들러 주소를 순서대로 늘어놓은 함수 포인터 배열입니다. 인터럽트가 걸리면 CPU가 번호에 해당하는 칸의 주소를 읽어 그곳으로 뜁니다.
현업에서 자주 터지는 문제
- NULL 호출: 등록하지 않은 콜백을 부르면 주소 0으로 뛰어 하드폴트가 납니다. 부르기 전에 확인합니다.
- 형이 다른 함수 대입: 인자 개수나 종류가 다른 함수를 캐스팅해 넣으면 컴파일은 되지만 스택이 어긋납니다.
- ISR에서 부르는 콜백이 길다: 콜백은 인터럽트 문맥에서 실행됩니다. 짧게 끝내도록 약속합니다.
- 표의 순서와 번호가 어긋남: 인덱스로 바로 찾는 표는 번호와 위치가 맞아야 합니다. 범위 검사를 먼저 합니다.
영상으로 이어 보기공부법과 진로 시리즈 15편2025 임베디드 프로그래밍 공부 순서 | 공부 방법 | 로드맵PROJECT COURSEC 문법을 실제 펌웨어 코드로 써 보고 싶다면밸런싱 로봇 하나를 요구사항, 회로와 PCB, 펌웨어, 앱까지 실무 순서대로 만드는 강의입니다. 10파트 165강.강의 살펴보기 →FREE PDF무료 전자책 3권 받기이 주제를 한 권으로 이어서 읽고 싶다면. BLDC 모터 제어, 직렬통신, 센서를 책 분량으로 묶었습니다. 이메일만 남기면 바로 받습니다.무료로 받기 →