Showing Posts From
펌웨어
- 15 Dec, 2025
펌웨어 버전 v1.2.4 릴리스, 하지만 버전 번호만 올렸다
펌웨어 버전 v1.2.4 릴리스, 하지만 버전 번호만 올렸다 오전 10시, 릴리스 노트 작성 중 모니터 앞에 앉았다. 빈 텍스트 파일이 나를 보고 있다. Release Notes - Firmware v1.2.4타이핑을 멈췄다. 뭘 쓰지. 사실 바뀐 게 없다. 코드는 v1.2.3이랑 똑같다. 단 한 줄도 안 건드렸다. 그런데 버전은 올려야 한다. 이유는 간단하다. 하드웨어가 바뀌었으니까.HW팀에서 온 메일 어제 오후 3시. HW팀 김대리가 메일을 보냈다. "펌웨어팀님, PCB 리비전 올라갑니다. Rev.B → Rev.C. 레귤레이터 교체했어요. 출력 전압은 동일하고요. 펌웨어 수정 필요 없습니다." 좋은 소식이다. 펌웨어 수정 없다니. 그런데 메일 끝에 한 줄이 더 있었다. "버전은 올려주세요. 나중에 AS 들어오면 구분해야 해서요." 아. 그렇다. 똑같은 코드인데 다른 하드웨어에서 돌아가면, 버전을 구분해야 한다. 나중에 문제 생기면 '이 제품은 Rev.B 보드였나 Rev.C 보드였나' 추적해야 하니까. 레귤레이터 교체. TPS73733에서 RT9013로. 둘 다 3.3V 출력. 성능은 비슷하다. 가격이 100원 쌌다고 들었다. 대량 양산하면 몇천만원 차이. 우리 팀은 상관없다. 전압만 안정적이면 된다. 코드 수정 없다. 그런데 버전은 올려야 한다. 시맨틱 버저닝의 딜레마 우리 팀은 시맨틱 버저닝을 쓴다. v메이저.마이너.패치 형식이다.메이저: API 바뀌거나 큰 기능 변경 마이너: 기능 추가 패치: 버그 수정그럼 이번엔 뭘 올려야 하나. 기능 안 바뀌었으니 메이저 아니다. 기능 추가 없으니 마이너도 아니다. 버그도 안 고쳤으니 패치도 아니다. 그런데 올려야 한다. 결국 패치 번호를 올렸다. v1.2.3 → v1.2.4. 제일 낮은 단위니까. 시맨틱 버저닝 원칙에는 안 맞는다. 하지만 현실에서는 맞다. 하드웨어 세계에서는.릴리스 노트에 뭘 쓸까 다시 빈 텍스트 파일이다. Release Notes - Firmware v1.2.4정직하게 쓸까. "코드 변경 없음. 하드웨어 호환성 때문에 버전만 올림." 아니다. 그러면 나중에 문서 보는 사람이 헷갈린다. "왜 버전을 올렸지? 의미 없는 거 아니야?" 좀 더 전문적으로 쓸까. Changes: - Compatible with PCB Rev.C - Hardware regulator changed (TPS73733 → RT9013) - No firmware modification required이것도 이상하다. 'No firmware modification'이라고 쓰면, '그럼 왜 버전을 올렸어?' 또 물어본다. 결국 이렇게 썼다. Release Notes - Firmware v1.2.4Changes: - Support for PCB Rev.C (hardware revision update) - Regulator component change: TPS73733 → RT9013 - Voltage output remains 3.3V, firmware logic unchanged - For traceability in production and after-serviceNote: This version is functionally identical to v1.2.3 but must be distinguished for hardware compatibility tracking.길다. 설명이 너무 많다. 하지만 나중에 내가 다시 봤을 때 이해할 수 있어야 한다. 3개월 뒤에 AS 문의 들어오면, "v1.2.4가 뭐였더라?" 찾아볼 거다. 그때 이 문서를 본다. 그때 내가 이해할 수 있으면 된다. Git 커밋 메시지 릴리스 노트 썼으니 이제 Git에 태그를 달아야 한다. git tag -a v1.2.4 -m "Release v1.2.4" git push origin v1.2.4잠깐. 커밋 메시지는 뭐라고 쓰지. 마지막 커밋은 일주일 전이다. "Fix UART timeout bug in low power mode". v1.2.3 릴리스 후 버그 하나 고쳤던 거다. 그 이후로 커밋이 없다. 그럼 v1.2.4 태그는 그 커밋에 다는 건가. 그런데 그 커밋은 v1.2.3용 버그 픽스였는데. 고민했다. 그냥 빈 커밋을 하나 만들까. git commit --allow-empty -m "Bump version to v1.2.4 for PCB Rev.C compatibility"--allow-empty. 파일 변경 없이 커밋만 만드는 옵션이다. 이런 경우에 쓴다. 커밋 메시지도 고민이다. 짧게 쓰면 정보가 부족하고, 길게 쓰면 나중에 로그 보기 불편하다. 결국 이렇게 썼다. Bump version to v1.2.4 for PCB Rev.C compatibility- Hardware: PCB Rev.B → Rev.C - Regulator: TPS73733 → RT9013 - Firmware code: no changes from v1.2.3 - Purpose: version tracking for production/AS커밋 본문에 상세하게. 첫 줄은 짧게. Push 했다. 이제 v1.2.4 태그가 원격 저장소에 올라갔다.양산팀에 전달 버전 올렸으니 양산팀한테 알려야 한다. 메일을 썼다. "양산팀님, 펌웨어 v1.2.4 릴리스되었습니다. PCB Rev.C용입니다. 바이너리 파일 첨부합니다." 바이너리 파일은 v1.2.3이랑 똑같다. SHA256 해시값도 같다. 그냥 파일 이름만 firmware_v1.2.4.bin으로 바꿨다. 양산팀에서 답장 왔다. "확인했습니다. 그런데 v1.2.3이랑 뭐가 다른가요?" 예상한 질문이다. "코드는 동일합니다. 하드웨어 버전 구분용입니다. Rev.C 보드에는 v1.2.4를 넣어주세요. 나중에 추적 필요할 수 있어서요." "알겠습니다." 간단한 대화. 하지만 이게 중요하다. 지금은 별거 아닌 것 같지만, 6개월 뒤 고객사에서 문제 리포트 들어오면 다르다. "제품 시리얼 12345에서 전원 이슈가 있습니다." 그럼 우리는 추적한다. 시리얼 번호로 생산 로그 찾는다. "아, 이거 Rev.C 보드네요." 펌웨어 버전 확인한다. "v1.2.4 맞네요." 그다음 HW팀이랑 회의한다. "Rev.C 보드 RT9013 레귤레이터 쓴 거 맞죠? 혹시 그쪽 문제 아닐까요?" 이런 식으로 범위를 좁혀간다. 만약 버전 구분 안 했으면, "이게 어떤 보드에 들어간 건지" 추적이 어렵다. 생산 날짜로 추정해야 한다. "8월 1일 이후면 아마 Rev.C일 거예요." 이렇게 '아마'로 시작하면 디버깅이 오래 걸린다. 문서 업데이트 릴리스 노트 썼고, Git 태그 달았고, 양산팀한테 전달했다. 이제 문서를 업데이트해야 한다. 우리 팀 위키에 "Firmware Version History" 페이지가 있다. 표 형식이다.Version Date PCB Rev Changesv1.2.3 2024-11-15 Rev.B UART timeout fixv1.2.4 2024-11-22 Rev.C Hardware compatibility간단하다. 나중에 누가 봐도 한눈에 들어온다. "아, v1.2.4부터 Rev.C구나." 이게 전부다. 복잡한 설명 필요 없다. 표 한 줄이면 된다. 근데 이걸 안 해두면, 나중에 누가 v1.2.4를 보고 "이게 뭐지?" 헤맨다. 코드 diff 보면 변경 없으니 더 혼란스럽다. 문서가 중요한 이유다. 사실 찜찜하다 솔직히 찜찜하다. 코드는 똑같은데 버전만 올린다. 시맨틱 버저닝 원칙에도 안 맞다. 이게 맞는 방법인가. 다른 방법도 생각해봤다.빌드 번호를 쓴다. v1.2.3+build.001, v1.2.3+build.002 이런 식으로. 하지만 우리 빌드 시스템은 빌드 번호를 지원 안 한다.메타데이터를 쓴다. v1.2.3+revC 이런 식으로. 근데 이것도 시맨틱 버저닝 표준이고, 우리 툴체인이 '+' 기호를 제대로 처리 못 한다.하드웨어 버전을 별도로 관리한다. 펌웨어 버전은 v1.2.3 고정, 하드웨어 버전 정보를 펌웨어 안에 define으로 넣는다. #define HW_REV_C. 이게 제일 깔끔한데, 이미 양산 중인 제품이라 지금 구조 바꾸기 어렵다.결국 패치 버전을 올리는 게 제일 현실적이다. 찝찝하지만, 이게 펌웨어 세계다. 웹 개발자들은 이런 거 안 겪는다. 프론트엔드 코드는 하드웨어랑 상관없다. 어떤 폰에서든 똑같은 코드가 돌아간다. 우리는 다르다. 똑같은 코드라도 보드가 다르면 추적해야 한다. 팀장님 리뷰 오후 2시. 팀장님이 내 자리로 왔다. "v1.2.4 릴리스 봤어요. 코드 변경 없는데 버전 올린 거?" "네. Rev.C 보드 대응입니다. HW팀에서 요청했어요." "흠. 릴리스 노트에 설명 잘 써놨네요. 나중에 추적 가능하겠어요." "네. 표도 업데이트했습니다." "좋아요. 근데 이런 거 너무 자주 하면 버전 번호 인플레이션 생겨요. 가능하면 묶어서 올리는 게 나을 텐데." 팀장님 말이 맞다. 하드웨어 리비전 올라갈 때마다 펌웨어 버전 올리면, 버전 번호가 너무 빨리 증가한다. v1.2.10, v1.2.15... 이렇게 되면 버전만 봐서는 큰 변화인지 작은 변화인지 구분이 안 된다. "다음부턴 HW팀이랑 미리 조율해볼게요. 리비전 올라가는 거 미리 알려달라고 하고, 펌웨어 업데이트랑 같이 묶을 수 있으면 묶고." "그래요. 그게 나을 거예요." 팀장님 돌아갔다. 사실 이번에도 묶으려고 했다. 다음 주에 BLE 통신 개선 작업이 있어서, 그거랑 같이 v1.3.0으로 올리려고 했다. 근데 HW팀이 급했다. 부품 수급 문제로 레귤레이터를 당장 교체해야 했다. 양산 라인이 멈출 뻔했다. 어쩔 수 없이 버전만 올렸다. 현실은 계획대로 안 된다. 6개월 뒤를 생각한다 지금은 귀찮다. 코드 안 바뀌었는데 버전 올리고, 문서 쓰고, 메일 보내고. 근데 6개월 뒤엔 고마울 거다. 문제 생겼을 때, 정리된 문서가 있으면 30분이면 원인 찾는다. 없으면 3일 걸린다. "이 펌웨어가 어떤 보드용이었지? 코드 diff 봐야 하나? Git 로그 뒤져야 하나? 양산팀한테 물어봐야 하나?" 이런 거 안 하려고 지금 문서 쓰는 거다. 펌웨어 개발은 코딩만 하는 게 아니다. 추적 가능성을 만드는 것도 일이다. 특히 양산 제품은 더 그렇다. 코드는 잘 돌아가는데, 3년 뒤 AS 들어왔을 때 뭐가 뭔지 모르면 소용없다. 그래서 버전 관리가 중요하다. 귀찮지만 해야 한다. 다른 팀은 어떻게 하나 점심시간. 옆 부서 이재훈이랑 밥 먹으면서 물어봤다. "너네도 이런 거 있어? 코드 안 바뀌었는데 버전 올리는 거?" "당연하지. 우리도 하드웨어 바뀔 때마다 올려. 근데 우리는 빌드 메타데이터로 관리해." "어떻게?" "버전은 그대로 두고, 빌드 ID를 다르게 줘. 펌웨어 안에 HW_VERSION define 있어서 거기다 Rev.C 같은 거 박아." "그럼 버전 번호는 안 올라가?" "응. v2.1.0에서 계속 쓰고, 내부 빌드 정보로 구분하지." 부럽다. 그게 더 깔끔하다. "너네는 왜 그렇게 했어?" "처음 설계할 때부터 그렇게 했어. 하드웨어 버전이랑 펌웨어 버전을 분리하는 게 맞다고 판단했대. 선배들이." 우리 팀은 그런 거 없이 시작했다. 처음엔 하드웨어 버전이 하나뿐이었으니까. Rev.A만 있었다. 그러다 Rev.B 나오고, Rev.C 나오고, 이제 와서 구조 바꾸기 애매하다. 기술 부채다. "다음 제품에선 그렇게 해볼까 생각 중이야. 지금 제품은 이미 늦었고." "ㅇㅇ 다음에는 처음부터 구조 잡고 시작해." 맞는 말이다. 매뉴얼도 업데이트 오후 4시. 고객사 매뉴얼도 업데이트해야 한다는 걸 깨달았다. 우리 제품은 B2B다. 고객사에서 우리 모듈을 사서 자기네 제품에 넣는다. 그래서 기술 매뉴얼이 있다. 매뉴얼 48페이지. "Firmware Version Information" 섹션이 있다. Supported Firmware Versions: - v1.2.3: Compatible with PCB Rev.B - v1.2.4: Compatible with PCB Rev.C이것만 추가하면 된다. 근데 매뉴얼은 PDF다. 워드 파일 열고, 수정하고, PDF 다시 뽑고, 서버에 업로드하고. 30분 걸렸다. 고객사한테도 메일 보냈다. "기술 매뉴얼 업데이트되었습니다. v1.2.4 정보 추가되었습니다." 답장 없다. 읽기만 했다. 그러면 된 거다. 결국 누구를 위한 버전인가 저녁 7시. 퇴근 준비하면서 생각했다. v1.2.4. 코드는 안 바뀌었지만 버전은 올라갔다. 이게 누구를 위한 건가. 개발자를 위한 거다. 내년에 나를, 3년 뒤 이 코드를 인계받을 후배를 위한 거다. 사용자는 모른다. 제품 쓸 때 펌웨어 버전 같은 거 안 본다. v1.2.3이든 v1.2.4든 똑같이 쓴다. 고객사도 별로 신경 안 쓴다. 잘 돌아가면 된다. AS팀이 본다. 문제 생기면. 그때 버전 추적한다. 결국 내부 사람들 위한 거다. 엔지니어들 위한 버전 번호다. 그래서 더 신경 써야 한다. 코드만큼 중요한 게 버전 관리다. 나중에 내가 이 코드를 다시 볼 때, "아, v1.2.4였구나" 하고 바로 이해할 수 있어야 한다. 그게 좋은 버전 관리다. 내일은 내일 출근하면 v1.2.4 바이너리를 최종 테스트한다. Rev.C 보드 10개에 올려보고, 기능 테스트 한 번 더 돌린다. 코드는 똑같지만, 하드웨어가 다르니 혹시 모른다. 레귤레이터가 달라지면서 전압 라이즈 타임이 미세하게 달라질 수 있다. 그러면 MCU 부팅 타이밍이 바뀔 수 있다. 가능성 낮지만, 확인해야 한다. 펌웨어 엔지니어는 의심쟁이여야 한다. "설마 이것도 영향 있겠어?" 하는 것들이 실제로 영향 있다. 테스트 통과하면, 양산 승인. v1.2.4 정식 릴리스. 그리고 다음 작업으로 넘어간다. BLE 통신 개선. 그건 v1.3.0이 될 거다.버전 번호는 숫자가 아니라 역사다. 코드의 타임라인이다. 귀찮아도 제대로 써야 한다.
- 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는 한 번 제대로 만들면 평생 쓴다. 지금은 안심하고 업데이트 누른다. 전원 나가도 된다.