Showing Posts From
전원이
- 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는 한 번 제대로 만들면 평생 쓴다. 지금은 안심하고 업데이트 누른다. 전원 나가도 된다.