CAN 메시지 오는 대로 처리하면 코드가 스파게티가 된다.

상태 머신으로 정리하자.


상태 정의

typedef enum {
    IAP_IDLE,           // 대기 (연결 대기)
    IAP_CONNECTED,      // 연결됨 (인증 대기)
    IAP_AUTHENTICATED,  // 인증됨 (크기 대기)
    IAP_RECEIVING,      // 데이터 수신 중
    IAP_VERIFYING,      // 검증 중
    IAP_COMPLETE,       // 완료
    IAP_ERROR           // 에러
} IapState;

상태 전이

         CONNECT
IDLE ──────────────> CONNECTED
                         │
                    AUTH │
                         ▼
                   AUTHENTICATED
                         │
                    SIZE │
                         ▼
                    RECEIVING ◄──┐
                         │       │
                   PAGE_END     │ PAGE_REQ
                         │       │
                         ▼       │
                    VERIFYING ───┘
                         │
                   VERIFY_OK
                         ▼
                    COMPLETE

상태 구조체

typedef struct {
    IapState state;
    uint32_t seed;          // 인증용
    uint32_t fw_size;       // 펌웨어 크기
    uint32_t received;      // 받은 바이트
    uint16_t current_page;  // 현재 페이지
    uint16_t total_pages;   // 전체 페이지
    uint32_t page_offset;   // 페이지 내 오프셋
    uint32_t crc;           // CRC 계산값
    uint32_t last_tick;     // 타임아웃용
} IapContext;

IapContext iap;

메인 루프

void iap_process(void) {
    // 타임아웃 체크
    if (iap_check_timeout()) {
        iap_reset();
        return;
    }
    
    // CAN 메시지 있으면 처리
    if (can_rx_available()) {
        CAN_RxHeaderTypeDef header;
        uint8_t data[8];
        can_receive(&header, data);
        
        iap_handle_message(header.StdId, data, header.DLC);
    }
}

메시지 핸들러

void iap_handle_message(uint32_t id, uint8_t *data, uint8_t len) {
    if (id != CAN_ID_PC_TO_BMS) return;
    
    uint8_t cmd = data[0];
    
    switch (iap.state) {
        case IAP_IDLE:
            if (cmd == CMD_CONNECT) {
                iap_handle_connect();
            }
            break;
            
        case IAP_CONNECTED:
            if (cmd == CMD_AUTH) {
                iap_handle_auth(data);
            }
            break;
            
        case IAP_AUTHENTICATED:
            if (cmd == CMD_SIZE) {
                iap_handle_size(data);
            }
            break;
            
        case IAP_RECEIVING:
            if (cmd == CMD_PAGE_START) {
                iap_handle_page_start(data);
            } else if (cmd == CMD_PAGE_END) {
                iap_handle_page_end(data);
            } else {
                // raw data
                iap_handle_data(data, len);
            }
            break;
    }
    
    iap.last_tick = HAL_GetTick();
}

연결 처리

void iap_handle_connect(void) {
    // 랜덤 seed 생성
    iap.seed = generate_seed();
    
    // CHALLENGE 전송
    uint8_t resp[8] = {0};
    resp[0] = RESP_CHALLENGE;
    memcpy(&resp[1], &iap.seed, 4);
    can_send(CAN_ID_BMS_TO_PC, resp, 5);
    
    iap.state = IAP_CONNECTED;
}

인증 처리

void iap_handle_auth(uint8_t *data) {
    uint32_t key;
    memcpy(&key, &data[1], 4);
    
    uint32_t expected = calc_key(iap.seed);
    
    if (key == expected) {
        // 인증 성공
        uint8_t resp[1] = {RESP_AUTH_OK};
        can_send(CAN_ID_BMS_TO_PC, resp, 1);
        
        // 크기 요청
        uint8_t req[1] = {RESP_SIZE_REQ};
        can_send(CAN_ID_BMS_TO_PC, req, 1);
        
        iap.state = IAP_AUTHENTICATED;
    } else {
        // 인증 실패
        iap_send_error(ERR_AUTH);
        iap_reset();
    }
}

타임아웃 처리

#define IAP_TIMEOUT_MS  10000

bool iap_check_timeout(void) {
    if (iap.state == IAP_IDLE) {
        return false;  // IDLE은 타임아웃 없음
    }
    
    if ((HAL_GetTick() - iap.last_tick) > IAP_TIMEOUT_MS) {
        return true;
    }
    return false;
}

void iap_reset(void) {
    memset(&iap, 0, sizeof(iap));
    iap.state = IAP_IDLE;
}

다음 글에서 데이터 수신 및 쓰기.

#15 - 데이터 수신 및 쓰기