펌웨어 업데이트 중 전원이 나간다면?
- 12 Dec, 2025
펌웨어 업데이트 중 전원이 나간다면?
벽돌 한 번 만들어봤다
지난달이다. 제품 50개가 벽돌이 됐다.
OTA 업데이트 중에 전원이 나갔다. 정확히는 사용자가 플러그를 뽑았다. 부트로더가 깨졌다. 복구 불가능. A/S 센터로 회수.
손실 금액 계산했더니 2300만원. 내 연봉의 절반이다.
회의실에서 질책받았다. “왜 대비 안 했어요?” 대비했다고 생각했는데 아니었다. CRC 체크만으로는 부족했다.
그날부터 OTA 재설계 시작했다. 3개월 걸렸다. 이제는 좀 안다. 전원이 언제 나가도 된다.

부트로더가 죽으면 끝이다
부트로더는 MCU가 켜지면 제일 먼저 실행되는 코드다. 여기가 망가지면 아무것도 못 한다. USB도 안 잡힌다. UART도 응답 없다.
우리 제품은 nRF52840 쓴다. Flash 1MB. 부트로더는 앞쪽 16KB에 박혀있다. 여기를 건드리면 안 된다.
처음에는 단순했다. 펌웨어를 통째로 교체하는 방식. Flash 지우고 → 새 펌웨어 쓰고 → 리부트. 간단하다.
문제는 쓰는 도중 전원이 나가면? Flash 절반만 써진 상태. 부트로더는 이걸 읽으려고 시도한다. CRC 안 맞는다. 뭘 해야 할지 모른다. 멈춘다.
복구 방법이 없다. JTAG 연결해서 다시 써야 하는데, 제품이 이미 고객한테 갔다. 택배로 회수해야 한다.
그게 50개였다. 다신 안 그런다고 다짐했다.
Bank A/B로 나눈다
해결책은 Flash를 둘로 나누는 거다. Bank A와 Bank B.
[Bootloader 16KB]
[Bank A 480KB] ← 현재 실행 중인 펌웨어
[Bank B 480KB] ← 새 펌웨어 다운로드
[User Data 48KB]
동작 방식은 이렇다.
- 현재는 Bank A에서 실행 중
- OTA 시작하면 Bank B에 새 펌웨어 다운로드
- 다운로드 완료되면 CRC 체크
- 맞으면 Bank B로 전환, 리부트
- 부트로더가 Bank B 실행
여기서 핵심은 Bank A는 절대 안 지운다는 거다. 다운로드 중에 전원 나가도 Bank A는 멀쩡하다. 다시 켜면 Bank A로 부팅된다.
안전하다. 하지만 문제가 있다.

새 펌웨어가 버그면?
Bank B로 전환했다. 부팅됐다. 그런데 새 펌웨어에 치명적 버그가 있다. 3초 후 크래시. 리부트. 다시 크래시. 무한 루프.
사용자는 제품을 못 쓴다. 벽돌은 아니지만 벽돌이나 마찬가지다.
롤백이 필요하다. 자동으로.
우리는 이렇게 구현했다.
부트로더가 Boot Count를 센다. Flash의 특정 영역에 카운터를 둔다. 부팅할 때마다 +1. 펌웨어가 정상 동작하면 카운터를 0으로 리셋.
만약 카운터가 3 이상이면? 부트로더가 판단한다. “이 펌웨어는 망했다.” Bank A로 롤백한다.
코드는 대충 이렇다.
uint8_t boot_count = read_boot_count();
boot_count++;
write_boot_count(boot_count);
if (boot_count >= 3) {
// 롤백
switch_to_bank_a();
reset_boot_count();
system_reset();
}
// 정상 부팅 시도
start_firmware();
// 여기까지 오면 펌웨어가 정상
reset_boot_count();
펌웨어는 부팅 후 5초 이내에 reset_boot_count()를 호출해야 한다. 부트로더한테 “나 정상이야” 신호 보내는 거다.
만약 크래시 나면? reset_boot_count() 못 부른다. 리부트. 카운터 증가. 3번 반복. 롤백.
이제 좀 안전하다.
전원이 쓰는 도중 나간다면
그래도 문제가 남아있다. Flash 쓰는 도중 전원 나가면?
Flash는 페이지 단위로 쓴다. nRF52는 4KB 페이지. 페이지 지우고 → 데이터 쓰고 → 다음 페이지.
만약 3번째 페이지 쓰는 중에 전원 나가면? 3번째 페이지는 절반만 써진다. CRC 체크하면 안 맞는다. 다운로드 실패. 다시 시도해야 한다.
이건 괜찮다. Bank A는 멀쩡하니까.
하지만 효율이 나쁘다. 480KB 펌웨어면 120개 페이지다. 119개 다 쓰고 마지막에 전원 나가면? 처음부터 다시.
해결책은 Chunked Write다.
펌웨어를 4KB Chunk로 나눈다. 각 Chunk마다 CRC를 붙인다. 서버에서 Chunk 단위로 보낸다.
디바이스는 Chunk를 받을 때마다:
- CRC 체크
- Flash에 쓰기
- Flash에서 읽어서 다시 CRC 체크
- 맞으면 서버한테 ACK
- 다음 Chunk 요청
전원이 나가면? 마지막 ACK 보낸 Chunk까지만 써진 거다. 다시 켜면 거기서부터 이어 받는다.
코드는 이렇다.
typedef struct {
uint32_t chunk_id;
uint32_t offset;
uint16_t size;
uint16_t crc;
uint8_t data[4096];
} firmware_chunk_t;
bool write_chunk(firmware_chunk_t *chunk) {
// CRC 체크
uint16_t calc_crc = calculate_crc(chunk->data, chunk->size);
if (calc_crc != chunk->crc) {
return false;
}
// Flash 쓰기
flash_write(BANK_B_BASE + chunk->offset,
chunk->data, chunk->size);
// Verify
uint8_t verify_buf[4096];
flash_read(BANK_B_BASE + chunk->offset,
verify_buf, chunk->size);
if (memcmp(chunk->data, verify_buf, chunk->size) != 0) {
return false;
}
// 진행 상황 저장
save_ota_progress(chunk->chunk_id);
return true;
}
save_ota_progress()가 중요하다. 이게 있어야 재시작 시 이어받기가 된다.

부트로더도 업데이트해야 한다면
제일 무서운 케이스다. 부트로더 자체를 업데이트해야 할 때.
부트로더에 버그가 있거나, 새 기능이 필요하거나. 어쩔 수 없이 해야 한다.
이건 진짜 조심해야 한다. 부트로더 업데이트 중에 전원 나가면? 복구 불가능. 무조건 벽돌.
우리는 Two-Stage Bootloader를 쓴다.
[Stage 1 Bootloader 8KB] ← 절대 안 건드림
[Stage 2 Bootloader 8KB] ← 업데이트 가능
[Bank A 480KB]
[Bank B 480KB]
[User Data 48KB]
Stage 1은 최소한의 기능만. Flash에서 Stage 2 읽어서 실행. 그게 다다.
Stage 1은 한 번 박으면 끝. 절대 업데이트 안 한다. 버그 있으면 안 된다. 그래서 코드가 100줄도 안 된다.
Stage 2가 실제 부트로더. 여기에 모든 OTA 로직이 있다. 업데이트 필요하면 Stage 2만 교체.
Stage 2 업데이트 과정:
- 새 Stage 2를 임시 영역에 다운로드
- CRC 체크
- Stage 1한테 “업데이트 플래그” 설정
- 리부트
- Stage 1이 플래그 확인
- Stage 1이 임시 영역에서 Stage 2 교체
- CRC 다시 체크
- 맞으면 새 Stage 2 실행
- 안 맞으면 이전 Stage 2로 복구
Stage 1이 하는 거라 안전하다. Stage 1은 간단해서 버그가 없다.
서버 쪽도 중요하다
디바이스만 잘 만들어도 안 된다. 서버가 개판이면 소용없다.
우리는 AWS S3에 펌웨어 올린다. CloudFront로 배포. 전 세계 어디서든 빠르다.
펌웨어 버전 관리는 이렇게 한다.
{
"version": "2.3.1",
"build": 1234,
"min_bootloader": "1.2.0",
"file_size": 491520,
"sha256": "a3b5c...",
"chunks": 120,
"url": "https://cdn.example.com/fw/2.3.1.bin",
"release_notes": "Bug fixes",
"mandatory": false,
"rollout_percentage": 10
}
rollout_percentage가 핵심이다. 처음엔 10%만 배포. 문제 없으면 50%, 그 다음 100%.
만약 문제 생기면? 서버에서 즉시 중단. 이미 다운받은 디바이스는? 롤백된다. Boot Count 메커니즘 덕분에.
디바이스는 1시간마다 서버에 버전 체크한다. 새 버전 있으면 다운로드. 없으면 다음에.
배터리 10% 이하면 다운로드 안 한다. 충전 중일 때만. 전원 나갈 확률 줄이는 거다.
테스트는 어떻게 하나
실험실에서는 다 된다. 문제는 현장이다.
우리는 파워 서플라이에 릴레이 달았다. 랜덤 타이밍에 전원 끊는다. 하루 종일 OTA 돌리면서 전원 100번 끊는다.
한 번도 벽돌 안 되면 통과. 한 번이라도 되면 코드 수정.
이거 하느라 2주 걸렸다. 처음엔 10번 중 1번 벽돌됐다. CRC 체크 타이밍 문제였다. Flash 쓰고 바로 읽으면 안 됐다. 10ms 딜레이 넣으니까 해결됐다.
그 다음은 필드 테스트. 베타 사용자 50명한테 배포. 이번엔 롤백 테스트. 일부러 버그 있는 펌웨어 보냈다. 크래시 나게.
50명 전부 롤백됐다. 한 명도 벽돌 안 됐다. 성공.
실제로 써보니
양산 나간 지 6개월. 디바이스 5000개. OTA 업데이트 8번 했다.
전원 나간 케이스 237건. 전부 복구됐다. 벽돌 0건.
롤백된 케이스 3건. 펌웨어 버그 때문. 자동으로 롤백됐다. 사용자는 몰랐다.
A/S 비용 95% 감소. 예전엔 한 달에 200만원 나갔다. 지금은 10만원.
상사가 물었다. “이거 다른 제품에도 적용 가능해?” 가능하다. 근데 시간 걸린다. 제품마다 MCU가 다르다. 부트로더 다시 짜야 한다.
지금 다른 팀 도와주는 중이다. STM32 제품에 적용하고 있다. Flash 레이아웃이 달라서 코드 수정 많이 했다.
배운 것들
OTA는 쉬운 게 아니다. 겉으로 보면 “펌웨어 다운받아서 업데이트”인데, 속은 복잡하다.
핵심은 “언제 전원 나가도 복구 가능”이다. 이게 안 되면 OTA 하면 안 된다.
Bank A/B는 필수다. 플래시 용량이 부족해도 어떻게든 만들어야 한다. 벽돌 만드는 것보다 낫다.
롤백 메커니즘도 필수. 버그 있는 펌웨어 배포하면 끝이다. 자동 롤백 없으면 A/S 지옥.
서버 쪽 rollout percentage 꼭 쓴다. 처음부터 100% 배포하면 위험하다.
테스트는 파워 서플라이 끊으면서 한다. 이거 안 하면 필드에서 터진다.
시간 많이 걸린다. 처음 만들 때 3개월 걸렸다. 다른 제품 포팅하는 데도 한 달씩 걸린다. 하지만 안 하면 나중에 더 고생한다.
OTA는 한 번 제대로 만들면 평생 쓴다. 지금은 안심하고 업데이트 누른다. 전원 나가도 된다.
