그라운드 임베디드 EN 유튜브 구독

노트 / 자료구조와 알고리즘

상태 머신: 펌웨어 설계의 기본 도구

복잡한 동작 순서를 상태와 전이로 정리합니다. 플래그가 늘어나 if문이 얽히기 시작하면 상태 머신으로 바꿀 때입니다.

GROUND EMBEDDED · 정리 노트자료구조 05상태 머신 1typedef enum { IDLE, RUN, FAULT } state… 2static state_t st = IDLE; 3void motor_task(void) { 4 switch (st) {

전기밥솥은 대기, 취사, 뜸 들이기, 보온 중 하나의 상태에 있습니다. 버튼이나 온도 같은 이벤트가 오면 다음 상태로 넘어갑니다. 펌웨어의 대부분은 이렇게 표현할 수 있습니다.

IDLE대기시작 버튼RUN동작 중고장 감지ERROR정지
상태 머신. 지금 상태와 들어온 이벤트가 다음 상태를 정합니다.

switch로 만드는 기본형

typedef enum { IDLE, RUN, FAULT } state_t;
static state_t st = IDLE;

void motor_task(void) {
    switch (st) {
    case IDLE:  if (start_pressed()) { pwm_on(); st = RUN; } break;
    case RUN:   if (overcurrent()) { pwm_off(); st = FAULT; }
                else if (stop_pressed()) { pwm_off(); st = IDLE; } break;
    case FAULT: if (reset_pressed()) st = IDLE; break;
    }
}

좋은 상태 머신의 조건

  • 상태 그림을 먼저 그립니다. 동그라미는 상태, 화살표는 이벤트와 동작입니다.
  • 모든 상태에서 모든 이벤트에 대해 "무엇을 하는가"가 정해져 있어야 합니다. 정의되지 않은 조합이 버그가 됩니다.
  • 상태에 들어갈 때 한 번 하는 일(entry)과 머무는 동안 하는 일을 구분합니다.
  • 고장 상태에서 빠져나오는 길을 반드시 만듭니다.

상태가 많아지면 상태별 처리 함수를 함수 포인터 표로 관리하거나, 상위 상태를 두는 계층형 상태 머신으로 확장합니다.

왜 플래그 대신 상태인가

is_running, is_error, is_paused 같은 플래그 세 개는 여덟 가지 조합을 만듭니다. 그중 "동작 중이면서 오류"처럼 있어서는 안 되는 조합까지 코드가 허용하게 됩니다. 상태를 하나의 변수로 두면 한 번에 하나만 될 수 있습니다.

표로 먼저 설계하기

자판기 모터 제어를 예로 들면, 코드를 쓰기 전에 표부터 채웁니다.

현재 상태 사건 다음 상태 할 일
대기 시작 버튼 가속 드라이버 켜기
가속 목표 속도 도달 정속 없음
가속 과전류 오류 드라이버 끄기
정속 정지 버튼 감속 목표 속도 0
정속 과전류 오류 드라이버 끄기
감속 속도 0 대기 드라이버 끄기
오류 리셋 버튼 대기 오류 지우기

빈칸이 곧 "그 상태에서 그 사건은 무시한다"는 결정입니다. 표를 채우다 보면 "감속 중에 시작 버튼을 누르면?" 같은 빠뜨린 경우가 드러납니다.

switch로 구현

typedef enum { ST_IDLE, ST_ACCEL, ST_RUN, ST_DECEL, ST_ERROR } state_t;
static state_t st = ST_IDLE;

void fsm_step(void) {                        // 1 ms마다 호출
    switch (st) {
    case ST_IDLE:
        if (btn_start()) { driver_on(); st = ST_ACCEL; }
        break;
    case ST_ACCEL:
        if (overcurrent())      { driver_off(); st = ST_ERROR; }
        else if (speed_ok())    { st = ST_RUN; }
        break;
    case ST_RUN:
        if (overcurrent())      { driver_off(); st = ST_ERROR; }
        else if (btn_stop())    { set_target(0); st = ST_DECEL; }
        break;
    case ST_DECEL:
        if (speed_zero())       { driver_off(); st = ST_IDLE; }
        break;
    case ST_ERROR:
        if (btn_reset())        { clear_fault(); st = ST_IDLE; }
        break;
    default:
        driver_off(); st = ST_ERROR;         // 값이 깨졌을 때 안전한 쪽으로
        break;
    }
}
무료 전자책이 주제를 한 권으로 이어 읽기 (PDF 3권)→

기다리는 동안 멈추지 않기

delay_ms(500)으로 기다리면 그동안 버튼도 통신도 처리하지 못합니다. 상태에 들어온 시각을 기록하고, 매번 지난 시간을 확인합니다.

case ST_WAIT:
    if ((uint32_t)(now_ms() - entered_ms) >= 500u) st = ST_NEXT;
    break;

부호 없는 뺄셈을 쓰면 틱 카운터가 한 바퀴 돌아도 차이가 올바르게 계산됩니다.

들어갈 때 한 번만 할 일

상태가 바뀌는 순간에만 해야 하는 일(타이머 시작, 메시지 전송)은 전이하는 곳에서 처리하거나, 이전 상태를 기억해 두고 달라졌을 때만 실행합니다.

if (st != prev) { on_enter(st); entered_ms = now_ms(); prev = st; }

현업에서 자주 터지는 문제

  • 상태가 너무 많아짐: 독립적인 일(통신 연결 상태와 모터 상태)은 상태 머신을 따로 둡니다. 하나로 합치면 조합만큼 불어납니다.
  • 인터럽트에서 상태를 직접 바꿈: 메인 루프가 판단하는 도중에 상태가 바뀝니다. 인터럽트는 사건만 기록하고 전이는 한곳에서 합니다.
  • 오류 상태에서 빠져나갈 길이 없음: 모든 상태에서 "어떻게 여기서 나가는가"를 표에서 확인합니다.
  • 디버깅이 어려움: 상태가 바뀔 때마다 이전과 다음 상태를 로그로 남기면 현장 문제의 대부분이 풀립니다.
영상으로 이어 보기공부법과 진로 시리즈 15편2025 임베디드 프로그래밍 공부 순서 | 공부 방법 | 로드맵PROJECT COURSE배운 자료구조를 실제 펌웨어에 넣어 보고 싶다면밸런싱 로봇 하나를 요구사항, 회로와 PCB, 펌웨어, 앱까지 실무 순서대로 만드는 강의입니다. 10파트 165강.강의 살펴보기 →FREE PDF무료 전자책 3권 받기이 주제를 한 권으로 이어서 읽고 싶다면. BLDC 모터 제어, 직렬통신, 센서를 책 분량으로 묶었습니다. 이메일만 남기면 바로 받습니다.무료로 받기 →