CAN 버스 OTA 부트로더
블록 단위 Stop-and-Wait, 3단계 검증, 프레임 단위 대비 ACK 트래픽 48배 감소를 구현한 STM32F446용 커스텀 CAN 부트로더
배경
- 시스템: Wally v2 아키텍처 (RPi5 + STM32F446 베이스보드)
- 요구사항: RPi5에서 CAN으로 STM32 펌웨어 현장 업데이트
- 제약: A/B 파티션용 외부 플래시 없음, CAN 프레임당 8바이트 제한, 부트로더 32KB 제한
핵심 문제
CAN으로 펌웨어를 업데이트하려면 여러 문제를 해결해야 합니다:
- CAN 8바이트 프레임 제한으로 480KB 펌웨어 = 60,000개 이상의 프레임
- A/B 파티션 없음—쓰기 실패하면 장치가 벽돌
- 가전제품 환경의 CAN 버스 노이즈
- 모든 에러 케이스를 처리하면서 32KB 부트로더에 맞춰야 함
어려운 점: 프레임마다 ACK하면 60,000개의 ACK가 필요합니다 (480KB의 제어 트래픽). 싱글 슬롯이라 커밋 전에 모든 바이트를 검증해야 합니다.
핵심 아이디어
블록 단위 Stop-and-Wait (64프레임 = 384바이트당 ACK 1개)로 프레임 단위 대비 제어 트래픽을 48배 감소시키면서 다단계 검증으로 신뢰성을 유지합니다.
왜 384바이트인가? LCM(6, 4) = 12. 블록 크기는 6의 배수 (프레임당 페이로드)이면서 4의 배수 (플래시 워드 크기)여야 합니다. 384 = 12 × 32로 두 조건을 만족하면서 RAM을 최소화합니다.
접근 방식
1) 이미지 헤더가 포함된 메모리 레이아웃
Flash: 512KB
+------------------+ 0x08000000
| Bootloader | 32KB (Sectors 0-1)
+------------------+ 0x08008000
| Image Header | 256 bytes
| Application | ~480KB (Sectors 2-7)
+------------------+ 0x08080000
이미지 헤더 (256 bytes):
typedef struct {
uint32_t magic; // 0x52565441 ("RVTA")
uint32_t header_ver; // 호환성 체크
uint32_t image_size; // 헤더 제외
uint32_t image_crc32; // 앱 코드만의 CRC
uint32_t vector_addr; // 엔트리 포인트 검증
uint32_t build_time; // 타임스탬프
// ... 향후 확장용 예약
} image_header_t;
헤더로 부트로더가 애플리케이션으로 점프하기 전에 이미지를 검증합니다—손상된 벡터로 인한 하드 폴트를 방지합니다.
2) 블록 단위 Stop-and-Wait 프로토콜
| CAN ID | 방향 | 페이로드 |
|---|---|---|
| 0x2F0 | RPi → STM | OTA_START [size(4), crc(4)] |
| 0x2F1 | RPi → STM | OTA_DATA [seq(2), data(6)] |
| 0x2F2 | RPi → STM | OTA_END [] |
| 0x1F0 | STM → RPi | OTA_ACK [status, seq, progress, error] |
| 0x1F1 | STM → RPi | OTA_HELLO [state, progress, error, ver] |
트래픽 비교:
| 프로토콜 | 프레임 | ACK 수 | ACK 트래픽 |
|---|---|---|---|
| 프레임 단위 | 60,000 | 60,000 | 480 KB |
| 블록 단위 (384B) | 60,000 | 1,250 | 10 KB |
제어 트래픽 48배 감소.
3) 3단계 검증
1단계: 블록별 읽기 검증 (즉시)
↓
2단계: 헤더 + 벡터 검증 (전송 완료 후)
↓
3단계: 전체 이미지 CRC (최종)
1단계 - 블록별 검증:
// 플래시에 쓰기
flash_write_buffer(addr, buffer, 384);
// 즉시 읽어서 비교
const uint8_t *flash = (const uint8_t *)addr;
for (int i = 0; i < 384; i++) {
if (flash[i] != buffer[i]) return false;
}
2단계 - 헤더 검증:
- 매직 값 확인 (0x52565441)
- 헤더 버전 호환성
- 벡터 테이블 주소가 유효 범위 내인지
3단계 - CRC 검증:
- 전체 애플리케이션에 대해 CRC 계산
- 헤더의 예상 CRC와 비교
4) 타이밍 분석이 포함된 인터럽트 안전성
CAN RX 중에 플래시 쓰기가 일어나면 버퍼가 손상될 수 있습니다:
HAL_NVIC_DisableIRQ(CAN1_RX0_IRQn); // ~2ms 윈도우
flash_write_buffer(addr, buffer, len);
// 읽기 검증
HAL_NVIC_EnableIRQ(CAN1_RX0_IRQn);
타이밍 분석:
- 플래시 쓰기 (384 bytes): ~1.5ms
- 읽기 검증: ~0.3ms
- 총 인터럽트 비활성화: 블록당 ~2ms
- 480KB / 384B = 1,250 블록 × 2ms = 총 2.5초 (10초 전송에 분산)
플래시 쓰기 중 호스트는 BUSY (50ms로 rate-limit)를 받습니다—타임아웃을 방지하면서 “살아있지만 작업 중”임을 알립니다.
5) 시퀀스 복구 메커니즘
프레임 손실/손상에 대한 3단계 처리:
if (seq == expected_seq) {
// 정상: 프레임 수락
} else if (seq < block_start_seq) {
// 이미 쓴 블록의 재전송
// 호스트가 ACK를 못 받음—다시 보냄
send_ack(OK);
} else {
// 시퀀스 이탈: 블록 시작으로 리셋
received -= buffer_len; // 부분 카운트 취소
buffer_len = 0;
expected_seq = block_start_seq;
send_ack(ERROR_SEQ);
}
핵심: 프레임이 아닌 블록 경계로 리셋해서 깨끗한 복구를 보장합니다. 부분 블록은 패치하지 않고 버립니다.
6) 동적 타임아웃
타임아웃은 ACK가 아닌 모든 프레임에서 리셋됩니다:
void handle_data(uint8_t *data) {
last_data_tick = HAL_GetTick(); // 모든 프레임에서 리셋
// ... 데이터 처리
}
느린 링크도 graceful하게 처리합니다—2초마다 1프레임이어도 프레임이 계속 오면 괜찮습니다. 타임아웃은 침묵에서만 발생합니다.
트레이드오프
| 결정 | 이유 | 대가 |
|---|---|---|
| 64프레임 블록 (384B) | LCM(6,4)=12, RAM과 플래시 쓰기 균형 | 384 bytes 정적 RAM |
| 싱글 슬롯 (A/B 없음) | 32KB 부트로더 제한, 앱 공간 최대화 | ACK 전 반드시 검증 |
| 블록 단위 Stop-and-Wait | 프레임 단위 대비 ACK 트래픽 48배 감소 | 파이프라인보다 약간 느림 |
| ACK 2회 전송 | CAN에 재전송 레이어 없음, ACK 손실 대응 | 0.004% 오버헤드 |
| 2ms 인터럽트 비활성화 | 플래시 무결성 > 지연시간 | 호스트가 BUSY 처리해야 함 |
| 256바이트 이미지 헤더 | 점프 전 검증, 잘못된 벡터 감지 | 앱 공간 256B 감소 |
| 동적 타임아웃 | 다양한 링크 속도 대응 | 약간 복잡한 로직 |
결과
성능:
- 처리량: 46.9 KB/s 실효 (500 kbps × 75% 효율)
- 업데이트 시간: 33KB 이미지에 ~10초
- 데이터 전송: 0.7초
- 플래시 작업: ~2.5초
- 오버헤드 (소거, 검증): ~6.8초
- 부트로더 크기: 8KB (32KB 제한 내)
신뢰성:
- 벽돌된 장치: 테스트에서 0대
- 검증 커버리지: 100% (블록별 + 헤더 + CRC)
- 복구: 시퀀스 불일치 시 자동 재시도
ACK 상태 코드:
| 코드 | 의미 |
|---|---|
| 0x00 | OK - 계속 |
| 0x01 | READY - 플래시 소거됨 |
| 0x02 | BUSY - 플래시 쓰기 중 |
| 0x03 | COMPLETE - 모든 검증 완료 |
| 0x10 | ERROR_CRC |
| 0x11 | ERROR_SIZE |
| 0x12 | ERROR_FLASH |
| 0x13 | ERROR_TIMEOUT |
| 0x14 | ERROR_SEQ |
핵심 교훈
블록 단위 Stop-and-Wait와 3단계 검증으로 복잡한 윈도잉 프로토콜 없이 안정적인 CAN OTA를 구현했습니다. LCM 제약을 활용한 블록 크기 설계로 프레임 단위 대비 ACK 트래픽을 48배 줄였습니다. 블록별 검증으로 에러를 즉시 감지하고, 헤더 검증으로 손상된 벡터로의 점프를 방지하며, 최종 CRC로 엔드투엔드 무결성을 확인합니다.