Showing Posts From
하드웨어
- 25 Dec, 2025
수요일 오후, 갑자기 '이건 사실 하드웨어 이슈 같은데요?'
수요일 오후, 갑자기 '이건 사실 하드웨어 이슈 같은데요?' 오후 3시, 그 순간 테스트를 돌렸다. 세 번째다. 같은 증상이다. UART 통신이 중간에 끊긴다. 정확히 2.3초 후. 매번 같은 타이밍이다. 소프트웨어 타이밍 이슈는 이렇게 정확하지 않다. 변수가 있다. 인터럽트 우선순위, 태스크 스케줄링, DMA 버퍼 크기... 뭔가 하나라도 달라지면 타이밍이 흔들린다. 근데 이건 딱 2.3초다. 오차 ±10ms. 오실로스코ープ를 켰다. TX 핀에 프로브를 댔다. 파형을 봤다. 2.3초 지점에서 전압이 이상하다. 3.3V로 올라가야 하는데 2.8V에서 멈춘다. 그리고 다시 0V로 떨어진다. "아..." 이건 내 문제가 아니다.HW팀에 연락 슬랙을 열었다. HW팀 채널에 메시지를 썼다. "UART TX 핀 파형 이상한 것 같습니다. 2.3초 후 전압이 2.8V에서 멈춥니다. 회로 확인 부탁드려요." 오실로스코ープ 캡처 이미지를 첨부했다. 빨간 마커로 이상한 부분을 표시했다. 답장이 왔다. 10분 후. "회로도 확인해볼게요. 근데 레이아웃은 이미 나간 상태라..." 레이아웃이 나갔다는 건 PCB 제작이 들어갔다는 뜻이다. 만약 회로 문제면 다시 만들어야 한다. 비용이 수백만원이다. 일정은 2주 늦어진다. "일단 확인 부탁드립니다." 답장을 보내고 자리에서 일어났다. 커피를 마시러 갔다. 내가 할 수 있는 건 기다리는 것뿐이다.목요일, 답이 없다 다음 날 아침. HW팀 김대리가 왔다. "어제 확인해봤는데요." 그는 노트북을 들고 있었다. 회로도를 보여줬다. "UART TX 핀이랑 LED 핀이 같은 풀업 저항을 쓰고 있어요. LED가 켜지면서 전류가 많이 흐르면 전압이 떨어질 수 있겠네요." "아..." LED였다. 우리가 디버깅용으로 달아놓은 LED다. 2.3초마다 깜빡이게 해놨다. 통신 상태를 보려고. "근데 이건 설계 단계에서 검증했어야 하는 거 아닌가요?" 김대리가 어색하게 웃었다. "시뮬레이션에서는 안 나왔어요. 실제로 만들어보니까..." "그럼 어떻게 해야 하나요?" "일단 임시 방편으로 LED 빼고 테스트해보세요. 근본적으로는 회로 수정해야 하는데..." 그는 말을 흐렸다. 레이아웃 다시 나가면 일정이 늦어진다는 걸 알고 있었다. 금요일, 회의 회의가 잡혔다. 오후 2시. 회의실에 HW팀, SW팀, PM이 모였다. PM이 물었다. "정확한 원인이 뭡니까?" HW팀 김대리가 설명했다. 풀업 저항, LED, 전압 강하... 기술적인 이야기를 5분 정도 했다. PM이 고개를 끄덕였다. "그럼 해결 방법은요?" "회로 수정입니다. PCB 다시 제작해야 합니다." "비용이랑 일정은요?" "비용은 300만원 정도. 일정은 2주 추가입니다." PM의 표정이 굳었다. 그는 노트북을 봤다. 간트 차트가 보였다. 일정이 빨간색으로 표시되어 있었다. "2주면... 양산 일정을 못 맞추는데요." 침묵이 흘렀다. 그때 우리 팀장이 말했다. "임시 방편으로 LED를 빼면 되지 않나요? 일단 양산은 LED 없이 나가고, 다음 버전에서 회로 수정하는 걸로." PM이 고개를 끄덕였다. "그게 낫겠네요. 일단 LED 빼고 테스트 진행하세요." 회의가 끝났다. 30분이 걸렸다. 결론은 'LED를 빼자'였다.월요일, 다시 내 일 주말이 지났다. 월요일 아침. 출근했다. HW팀에서 LED를 뺀 보드를 가져왔다. 납땜 자국이 있었다. 수작업으로 제거한 거다. "테스트 부탁드립니다." 보드를 받았다. 내 자리로 돌아왔다. 코드를 올렸다. 테스트를 돌렸다. UART 통신이 정상적으로 동작했다. 2.3초가 지나도 끊기지 않았다. 10분을 돌려봤다. 문제없었다. 슬랙에 메시지를 썼다. "테스트 정상 완료했습니다." 답장이 왔다. "수고하셨습니다." 그게 끝이었다. 일주일이 걸렸다. 실제 작업 시간은 1시간도 안 됐다. 나머지는 기다림이었다. 화요일, 또 다른 문제 다음 날. 새로운 문제가 생겼다. SPI 통신이 안 된다. 센서에서 데이터가 안 온다. 레지스터 설정을 확인했다. 문제없었다. 클럭 주파수를 확인했다. 문제없었다. 오실로스코ープ를 켰다. CLK, MOSI, MISO 핀에 프로브를 댔다. 파형을 봤다. CLK는 정상이다. MOSI도 정상이다. 근데 MISO가 이상하다. 신호가 너무 약하다. 1.2V밖에 안 나온다. 3.3V가 나와야 한다. "또...?" 이것도 하드웨어 문제 같다. 풀업 저항? 아니면 센서 불량? 슬랙을 열었다. HW팀 채널에 메시지를 썼다. "SPI MISO 핀 전압이 1.2V밖에 안 나옵니다. 회로 확인 부탁드려요." 오실로스코프 캡처를 첨부했다. 답장이 왔다. "확인해볼게요." 또 기다림이다. 수요일, 1주일 후 일주일이 지났다. HW팀에서 확인 결과가 왔다. 센서 불량이었다. 다른 센서로 교체했다. 테스트를 돌렸다. 정상 동작했다. "감사합니다." 답장을 보냈다. 결국 2주가 걸렸다. UART 문제 1주일, SPI 문제 1주일. 내가 실제로 작업한 시간은 2시간도 안 됐다. 코드를 올리고, 테스트를 돌리고, 결과를 확인하는 시간. 나머지는 기다림이었다. 이게 펌웨어 개발이다 펌웨어 개발은 소프트웨어만 다루는 게 아니다. 하드웨어도 같이 다룬다. 아니, 정확히는 하드웨어 문제를 찾아내야 한다. 코드가 완벽해도 회로가 잘못되면 안 된다. 레지스터 설정이 맞아도 전압이 이상하면 안 된다. 타이밍이 정확해도 부품이 불량이면 안 된다. 그리고 그걸 판단하는 게 내 일이다. "이건 소프트웨어 문제인가, 하드웨어 문제인가?" 오실로스코ープ를 보고 판단한다. 파형을 보고 판단한다. 전압을 측정하고 판단한다. 그리고 하드웨어 문제면 HW팀에 넘긴다. 기다린다. 확인 결과가 온다. 다시 테스트한다. 이 과정이 며칠씩 걸린다. 때로는 일주일씩 걸린다. 내가 할 수 있는 건 기다리는 것뿐이다. 다음 프로젝트는 다음 프로젝트도 똑같을 거다. 코드를 짠다. 테스트를 돌린다. 문제가 생긴다. 디버깅한다. 하드웨어 문제를 발견한다. HW팀에 넘긴다. 기다린다. 이게 반복된다. 웹 개발자들이 부럽다. 걔네는 하드웨어 걱정이 없다. 서버가 다운되면 재시작하면 된다. 버그가 생기면 패치하면 된다. 나는 보드가 고장 나면 새로 만들어야 한다. 버그가 양산에 들어가면 리콜이다. 비용이 억 단위다. 그래서 더 신중해진다. 더 많이 테스트한다. 더 많이 확인한다. 그리고 하드웨어 문제를 찾아낸다. 오실로스코프를 들고. 금요일 저녁 이번 주도 야근이다. 새로운 보드가 왔다. 이번엔 회로가 수정된 버전이다. LED 문제가 해결됐다. 코드를 올렸다. 테스트를 돌렸다. 정상 동작했다. "드디어..." 2주 만이다. UART 문제가 완전히 해결됐다. 슬랙에 메시지를 썼다. "최종 테스트 완료했습니다. 양산 가능합니다." PM에게서 답장이 왔다. "고생하셨습니다." 그게 끝이다. 퇴근했다. 시계를 봤다. 밤 10시다. 집에 도착했다. 맥주를 꺼냈다. 마셨다. 내일은 토요일이다. 쉴 수 있다. 그리고 월요일이 되면 또 출근한다. 새로운 문제가 기다리고 있을 것이다. "이건 사실 하드웨어 이슈 같은데요?" 또 이 말을 하게 될 것이다.오실로스코프 프로브를 정리하면서 생각했다. 다음엔 HW 검증을 더 빡빡하게 해야겠다고. 근데 또 새로운 문제가 생길 거다. 그게 이 일이니까.
- 22 Dec, 2025
팀원: '이건 왜 이렇게 복잡하게 했어요?' 나: '...이유가 있었어'
팀원: '이건 왜 이렇게 복잡하게 했어요?' 나: '...이유가 있었어' 코드 리뷰 시간 회의실에 모였다. 신입 팀원이 내 코드를 보고 있다. "선배님, 이 부분요." 화면을 가리킨다. 인터럽트 핸들러 안에 이상한 딜레이가 있다. void EXTI_IRQHandler(void) { // Clear interrupt flag EXTI->PR |= EXTI_PR_PR0; // WTF delay? for(volatile int i=0; i<100; i++); // Read GPIO status = GPIOA->IDR; }"이건 왜 이렇게 했어요? 인터럽트 안에서 딜레이는..." 맞는 말이다. 인터럽트는 빨리 끝내야 한다. 교과서에도 나온다. "이유가 있었어." 대답하면서 기억을 더듬는다. 뭔가... 있었는데.6개월 전 그날 2월이었다. 양산 직전. 보드에서 간헐적으로 GPIO 읽기가 실패했다. 10번에 1번꼴. "왜지?" 오실로스코프를 꺼냈다. 프로브를 GPIO 핀에 댔다. 파형을 봤다. 인터럽트 발생 → GPIO 읽기까지 시간이 너무 짧다. 약 50ns. 하드웨어 팀에 물어봤다. "GPIO 셋팅 후에 읽을 수 있을 때까지 시간이 얼마나 걸리나요?" "음... 데이터시트에는 없는데. 실측해보니까 200ns 정도?" "..." 인터럽트가 발생하는 순간, GPIO는 아직 안정화 안 됐다. 그래서 딜레이를 넣었다. 100 루프면 약 250ns. 문제가 해결됐다. 양산 나갔다. 커밋 메시지에는 이렇게 썼다. "Add delay for GPIO stabilization (HW issue workaround)"그런데 지금 "HW 이슈 워크어라운드요?" 신입이 묻는다. "Rev2 보드부터는 그 문제 없지 않나요?" 맞다. 하드웨어가 개선됐다. 풀업 저항 값을 바꿨던가. "그러면 이 딜레이는 이제 필요 없는 거 아닌가요?" 맞는 말이다. "근데 Rev1 보드도 아직 쓰는 곳 있지 않아?" "그건... 재고 소진용으로 몇 개 있긴 한데..." 애매하다. 지워도 될까? 아니면 조건부 컴파일? #ifdef HW_REV1 for(volatile int i=0; i<100; i++); #endif이렇게? 근데 누가 매번 매크로 체크할까. "일단 놔두자." "네?" "건드려서 문제 생기는 것보다는..." 변명처럼 들린다. 나도 안다.코드 고고학 6개월은 짧은 시간이다. 그런데 기억이 흐릿하다. 커밋 로그를 뒤졌다. commit a3f5c82 Author: 김펌웨어 Date: Fri Feb 16 23:47:32 2024Add delay for GPIO stabilization (HW issue workaround)- Rev1 board GPIO settling time issue - Measured 200ns required - Added 250ns delay loop - See issue #247Issue #247을 찾았다. 닫혀 있다. 관련 댓글들:HW팀 김과장: "Rev2에서 수정 완료" 나: "확인했습니다" PM: "양산은 Rev2로 진행"그럼 이제 Rev1 코드는? 재고 확인했다. Rev1 보드 37개 남음. 렌탈 제품 AS용. 언젠가 다 쓸 거다. 근데 그때까지 이 코드를 유지? 워크어라운드의 무덤 내 코드에는 이런 게 많다. // TODO: Remove after HW fix // FIXME: Temporary solution // HACK: Don't ask why // NOTE: See email from 2023.08.14다 이유가 있었다.전원 시퀀스 타이밍 I2C 버스 플로팅 ADC 노이즈 타이머 클럭 지터하드웨어 문제를 소프트웨어로 땜질. 그게 펌웨어 개발자의 숙명이다. 시간이 지나면 하드웨어는 개선된다. 근데 코드는 남는다. "혹시 몰라서" 못 지운다. 쌓인다. 스파게티가 된다. 정리할 시간은 없고 신입이 물었다. "그럼 이번에 리팩토링하면서 정리할까요?" 하고 싶다. 정말. 근데 일정표를 봤다.다음 주: 신규 센서 드라이버 추가 2주 후: OTA 업데이트 기능 3주 후: 전력 최적화 4주 후: 필드 테스트리팩토링 일정은 없다. "나중에 하자." "언제요?" "...여유 생기면." 여유는 안 생긴다. 안다. PM이 회의실에 들어왔다. "다들 있네. 좋아. 급한 거 하나 있는데." 또 시작이다. 레거시의 탄생 6개월 전의 나는 급했다.양산 일정 D-7 GPIO 문제 해결 필요 하드웨어 수정 불가 (보드 이미 제작됨) 소프트웨어로 해결해야 함딜레이 100루프를 넣었다. 테스트했다. 됐다. 커밋했다. 푸시했다. 양산 나갔다. "나중에 정리하지 뭐." 그때는 생각했다. 근데 나중은 안 왔다. 지금의 나는 6개월 전의 나를 이해한다. 그리고 6개월 후의 나도 지금의 나를 이해할 거다. "왜 이걸 안 지웠지?" "...이유가 있었어." 주석이라도 결국 타협했다. 코드는 남기되, 주석을 자세히 달았다. void EXTI_IRQHandler(void) { EXTI->PR |= EXTI_PR_PR0; /** * [WORKAROUND] GPIO stabilization delay * * Context: Rev1 board HW issue (resolved in Rev2) * - GPIO settling time: ~200ns required * - Interrupt to GPIO read: ~50ns (too fast) * - This delay: ~250ns (100 loops at 72MHz) * * Status: Required for Rev1 boards (37 units remaining) * Reference: Issue #247, Email thread 2024.02.16 * * TODO: Remove when Rev1 inventory depleted * Estimated: Q2 2025 */ for(volatile int i=0; i<100; i++); status = GPIOA->IDR; }신입이 봤다. "오, 이러면 나중에 봐도 알겠네요." "응. 그게 최선이야." 주석이 코드보다 길다. 근데 이게 맞다. 미래의 나를 위해. 6개월 뒤에 또 누가 물어볼 거다. "이건 왜 이렇게 했어요?" 그때 나는 이 주석을 보여주면 된다. "이유가 여기 있어." 펌웨어 개발자의 기억 웹 개발은 다르다. 친구가 말했다. 프론트엔드 개발자. "우리는 레거시 코드 그냥 지워. 빌드 깨지면 고치면 되지." 부럽다. "배포 잘못해도 롤백하면 되잖아." 우리는 못 한다. 펌웨어는 하드웨어에 묶여 있다.보드 리비전 양산 시기 재고 현황 AS 정책다 고려해야 한다. 코드 한 줄을 지우려면:언제 들어간 코드인가 왜 들어갔는가 어느 하드웨어 버전에 해당하는가 그 하드웨어가 아직 쓰이는가 지워도 안전한가5단계 검증. 그래서 안 지운다. "혹시 몰라서." 다음 프로젝트에서는 회의가 끝났다. 신입이 물었다. "다음 프로젝트에서는 이런 거 어떻게 관리할까요?" 생각해봤다. "처음부터 조건부 컴파일로 가야겠지." #if (HW_REVISION == 1) workaround_rev1_gpio_delay(); #endif"문서화도 확실히 하고." 워크어라운드 전용 문서. 스프레드시트로 관리.날짜 HW 리비전 문제 설명 해결 방법 제거 예정일"그리고 정기적으로 리뷰하는 시간을 잡자." "진짜요?" "...잡을 수 있으면." 현실은 안다. 또 급하게 될 거다. 근데 시도는 해야지. 그래도 퇴근길. 오늘 딜레이 코드는 안 지웠다. 주석만 달았다. 찝찝하다. 근데 어쩌겠어. 이게 펌웨어 개발이다. 하드웨어의 역사를 코드로 짊어지는 일. 6개월 전의 나, 1년 전의 나, 2년 전의 나. 다 코드 안에 있다. "이건 왜 이렇게 했어요?" "이유가 있었어." 항상 있었다. 지금도, 앞으로도.주석이 코드보다 긴 날이 온다. 그게 성장이다.
- 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이 될 거다.버전 번호는 숫자가 아니라 역사다. 코드의 타임라인이다. 귀찮아도 제대로 써야 한다.
- 13 Dec, 2025
목요일 밤 11시, 명일 릴리스를 위해 코드 리뷰 중
목요일 밤 11시, 명일 릴리스를 위해 코드 리뷰 중 11시 3분. 팀원 코드 열었다. 내일 오전 10시 릴리스. 지금 발견하면 안 되는데.처음엔 괜찮아 보였다 커밋 메시지: "타이머 인터럽트 우선순위 조정 완료" 좋아. 이 부분 문제였는데. 코드 열어봤다. 한 200줄 정도 수정됐다. 처음 50줄은 깔끔했다. 우선순위 설정 부분, 문제없다. 다음 100줄도 괜찮았다. 그런데. void TIMER_IRQHandler(void) { GPIO_ToggleBit(LED_PORT, LED_PIN); // 다른 처리들... delay_us(100); // <- 이거 }인터럽트 핸들러 안에서 delay를 쓴다. 11시 15분. 슬랙 열었다. "민수씨, 혹시 아직 안 주무셨나요?" 읽음 표시 떴다. 3초 후 답장. "네 형 아직이요" 다행이다. 전화했다.Low 레벨에서 보면 "민수씨, 67번 줄 delay_us 있는데요" "아 그거요? LED 깜빡임 보려고 넣었어요" "네... 근데 여기 인터럽트 핸들러잖아요" 침묵. "인터럽트 타이밍이 문제가 될 수 있을 것 같은데요" "아..." 설명했다. 100us 딜레이 중에 다른 인터럽트 못 받는다. UART 인터럽트 우선순위가 같으면 데이터 놓친다. 시리얼 통신 깨진다. "죄송합니다 형. 제가 생각을 못 했네요" "괜찮아요. 지금 발견해서 다행이에요" 다행이긴 뭐가 다행이야. 내일 아침인데. 11시 28분. 수정 방법 논의했다.딜레이 빼기 → 가장 간단 플래그로 메인에서 처리 → 더 안전 타이머 우선순위 더 낮추기 → 근본 해결 아님2번으로 결론. 민수가 지금 수정한대. "30분이면 될 것 같아요" 좋아. 12시까지 커밋해달라고 했다.테스트는 내가 해야 한다 민수 코드 수정하는 동안. 나는 테스트 시나리오 짰다. 확인해야 할 것:LED 정상 동작 UART 통신 안정성 인터럽트 중첩 상황 엣지 케이스보드 3개 꺼냈다. 오실로스코프 채널 4개 다 쓸 거다. 11시 50분. 커밋 알림 왔다. "수정 완료했습니다" 코드 확인했다. 깔끔하다. volatile uint8_t led_toggle_flag = 0;void TIMER_IRQHandler(void) { led_toggle_flag = 1; // 다른 처리들... }// 메인 루프에서 if (led_toggle_flag) { GPIO_ToggleBit(LED_PORT, LED_PIN); led_toggle_flag = 0; }좋아. 이제 올려보자. 12시 5분. 빌드 시작. 경고 2개. 무시 가능한 거다. 플래시 굽기 시작. "Programming... OK" 전원 켰다. LED 깜빡인다. 정상. 시리얼 터미널 열었다. 데이터 들어온다. 정상. 이제 스트레스 테스트. 새벽까지 테스트 12시 30분. 본격 시작. 테스트 1: UART 연속 수신 → 10만 바이트 전송했다. → 에러 0개. 통과. 테스트 2: 타이머 + UART 동시 → 양쪽 다 인터럽트 발생시켰다. → 파형 깨끗하다. 통과. 테스트 3: 최악의 상황 → 모든 인터럽트 동시 발생. → SPI, I2C, UART, 타이머 전부. → 오실로스코프가 크리스마스트리 같다. → 근데 데이터는 정상. 통과. 1시 15분. 엣지 케이스 몇 개 더 돌렸다. 전부 통과. 슬랙에 썼다. "테스트 완료했습니다. 문제없어요" 민수: "감사합니다 형ㅠㅠ" "고생하셨어요. 내일 봐요" 1시 30분. 문서 작업 시작했다. 수정 사항 정리.변경된 파일 목록 테스트 결과 알려진 이슈 (없음)릴리스 노트 업데이트. 버전 넘버 확인. v2.4.3 2시. 팀장님한테 보고 메일 보냈다. "내일 오전 릴리스 준비 완료" 답장 바로 왔다. "고생 많으셨습니다" 팀장님도 안 주무신 거다. 다들 긴장한다. 양산 첫 펌웨어라. 릴리스 전날은 다 이렇다 2시 20분. 정리했다. 보드 3개 전원 끄고. 프로브 정리하고. 테스트 로그 폴더에 저장하고. 가방 쌌다. 내일은... 아니 오늘은 9시 출근. 7시간 후다. 불 끄기 전에. 코드 한 번 더 봤다. 괜찮다. 이번엔 문제없을 거다. 사무실 나왔다. 새벽 공기가 차갑다. 걸어가면서 생각했다. 만약 내일... 아니 오늘 아침에 발견했으면? 릴리스 못 했다. 일정 미뤄졌다. 민수 혼났을 거다. 코드 리뷰를 밤에 한 게. 귀찮았지만. 잘한 거다. 잠들기 전 2시 40분. 집 도착. 씻지도 않고 침대에 누웠다. 내일... 오늘 할 일.9시 출근 10시 릴리스 오후 모니터링 이슈 없으면 퇴근이슈 없기를. 눈 감았다. 근데 잠이 안 온다. 혹시 놓친 게 있나? 타이머 설정 다시 확인했나? 인터럽트 우선순위 표 맞나? 일어나서 노트북 켰다. 코드 다시 봤다. 괜찮다. 진짜 괜찮다. 3시 10분. 다시 누웠다.내일 릴리스 잘 되면, 민수한테 점심 사줘야지.
- 11 Dec, 2025
인터럽트 우선순위 때문에 3시간을 고민했다
인터럽트 우선순위 때문에 3시간을 고민했다 아침엔 몰랐다 출근했다. 어제 올린 코드 테스트 결과 확인했다. 타이머 값이 이상했다. 100ms마다 찍히는 로그가 98ms, 102ms, 97ms... 들쭉날쭉이었다. '뭐지?' 처음엔 타이머 설정 문제인 줄 알았다. 클럭 소스 확인했다. LSE 32.768kHz. 프리스케일러 확인했다. 맞다. ARR 값도 맞다. 그럼 왜? 커피 마시고 다시 봤다. 여전히 이상했다.인터럽트가 두 개였다 우리 제품은 인터럽트가 두 개 돌아간다.UART 인터럽트 - 센서 데이터 받기 (1ms마다) 타이머 인터럽트 - 메인 로직 (100ms마다)UART는 우선순위 1로 설정했다. 타이머는 2. 숫자 낮을수록 높은 우선순위다. 센서 데이터 놓치면 안 되니까 UART를 높게 뒀다. 문제는 여기서 시작이었다. UART 인터럽트가 타이머 인터럽트를 블락한다. 우선순위가 높으니까. 타이머 인터럽트가 발생해도 UART 처리 끝날 때까지 기다린다. 그래서 타이밍이 밀린 거였다. 오실로스코프 꺼냈다 확인이 필요했다. 프로브 두 개 연결했다.CH1: UART RX 핀 CH2: 디버그용 GPIO (타이머 인터럽트 들어갈 때 HIGH)트리거 걸고 돌렸다. 화면에 보였다. UART 신호 들어올 때 GPIO가 안 올라간다. UART 끝나고 나서야 올라간다. 지연이 0.5ms에서 2ms까지 다양했다. '이거구나.' 센서에서 1ms마다 데이터가 온다. UART 인터럽트 처리에 평균 0.3ms 걸린다. 그런데 타이머 인터럽트 시점이랑 겹치면? 타이머는 기다린다. UART가 끝날 때까지. 100ms 주기인데 매번 랜덤하게 밀리니까 로그가 들쭉날쭉한 거였다.문제는 ADC였다 우리 메인 로직은 타이머 인터럽트에서 ADC 읽는다. 배터리 전압 체크한다. 100ms마다 읽어서 평균내서 상태 판단한다. 그런데 ADC 읽는 타이밍이 밀리면? ADC는 타이밍 크리티컬하다. 샘플링 시점이 중요하다. 특히 우리 회로는 RC 필터 달려있어서 settling time이 필요하다. 정확히 100ms 간격으로 읽어야 한다. 근데 98ms, 102ms 이러니까 값이 튄다. 배터리 90%인데 85%로 보였다가 92%로 보였다가. 양산 들어가면 고객 불만 들어온다. '배터리 표시가 왔다갔다해요.' 리콜이다. 3시간 동안 고민했다 점심 먹고 돌아와서 계속 생각했다. 해결 방법은 세 가지였다. 1. 우선순위를 바꾼다 타이머를 1로, UART를 2로. 그러면 타이머가 먼저 처리된다. ADC 타이밍 문제 해결. 근데 UART 데이터 놓친다. 센서 통신 프로토콜이 타임아웃 100ms다. 데이터 놓치면 센서가 에러 뱉는다. 이것도 문제. 2. 타이머 인터럽트 처리 시간을 줄인다 ADC 읽는 걸 인터럽트에서 빼고 메인 루프로. 인터럽트에선 플래그만 세운다. 근데 메인 루프는 다른 작업도 많다. RTOS 안 쓰는 베어메탈이라 루프 한 바퀴 도는 시간이 일정하지 않다. 이것도 타이밍 보장 안 된다. 3. 설계를 다시 한다 ADC를 DMA로 읽는다. 인터럽트 안 쓰고. 하드웨어 타이머로 트리거 걸어서 자동으로 100ms마다 읽히게. 이게 답이었다. 근데 DMA 설정 다시 해야 한다. 타이머 트리거 설정 해야 한다. 버퍼 관리 다시 짜야 한다. 오후 3시였다. 팀장한테 보고했다. "설계 수정 필요합니다." "얼마나 걸려?" "하루면 됩니다." "내일까지."DMA 설정했다 레퍼런스 매뉴얼 펼쳤다. STM32 RM0090. 1700페이지 중에 DMA 챕터는 10장. 80페이지. 읽었다. 다 읽었다. // ADC를 타이머 트리거로 DMA 읽기 void adc_dma_init(void) { // DMA 클럭 활성화 RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN; // ADC 클럭 활성화 RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; // 타이머 클럭 활성화 RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // DMA 스트림 설정 DMA2_Stream0->CR = 0; while(DMA2_Stream0->CR & DMA_SxCR_EN); // 스트림 비활성화 대기 DMA2_Stream0->PAR = (uint32_t)&ADC1->DR; // 소스: ADC 데이터 레지스터 DMA2_Stream0->M0AR = (uint32_t)adc_buffer; // 목적지: 버퍼 DMA2_Stream0->NDTR = ADC_BUFFER_SIZE; // 전송 개수 // 설정: peripheral to memory, circular mode, 16bit DMA2_Stream0->CR = DMA_SxCR_CHSEL_0 | // 채널 0 DMA_SxCR_MSIZE_0 | // 메모리 16bit DMA_SxCR_PSIZE_0 | // peripheral 16bit DMA_SxCR_MINC | // 메모리 주소 증가 DMA_SxCR_CIRC | // circular mode DMA_SxCR_EN; // 활성화 // ADC 설정 ADC1->CR1 = 0; ADC1->CR2 = ADC_CR2_ADON | // ADC 켜기 ADC_CR2_DMA | // DMA 활성화 ADC_CR2_DDS | // DMA 연속 요청 ADC_CR2_EXTEN_0 | // 외부 트리거 rising edge ADC_CR2_EXTSEL; // TIM2 TRGO // 타이머 설정 (100ms 주기) TIM2->PSC = 8399; // 84MHz / 8400 = 10kHz TIM2->ARR = 999; // 10kHz / 1000 = 10Hz = 100ms TIM2->CR2 = TIM_CR2_MMS_1; // TRGO on update TIM2->CR1 = TIM_CR1_CEN; // 타이머 시작 }컴파일했다. 에러 없다. 올렸다. 안 됐다. 레지스터 순서가 중요했다 문제는 초기화 순서였다. ADC_CR2에 ADON 비트 쓰고 나서 설정을 더 써야 한다. 근데 나는 한 번에 다 썼다. 데이터시트 읽어보니 ADON을 1로 만들고 나서 다른 설정 해야 한다고 나와 있었다. 수정했다. // 순서 중요 ADC1->CR2 = ADC_CR2_ADON; // 먼저 켜고 for(volatile int i=0; i<100; i++); // 안정화 대기 ADC1->CR2 |= ADC_CR2_DMA | // 그 다음 설정 ADC_CR2_DDS | ADC_CR2_EXTEN_0 | ADC_CR2_EXTSEL;다시 올렸다. 됐다. 테스트했다 오실로스코프로 확인했다.CH1: ADC 입력 (배터리 전압) CH2: DMA Transfer Complete 인터럽트 GPIO100ms마다 정확히 찍힌다. 오차 없다. UART 인터럽트 들어와도 상관없다. DMA가 하드웨어로 처리하니까. 로그 찍어봤다. 100.0ms, 100.0ms, 100.0ms... 완벽하다. 배터리 전압도 안정적이다. 90%면 90%, 89%면 89%. 튀지 않는다. 오후 6시였다. 팀장한테 보고했다 "됐습니다." "테스트는?" "오실로스코프로 확인했습니다. 100ms 정확합니다." "좋아. 내일 통합 테스트 넣어." "네." 커피 한 잔 더 마셨다. 네 번째였다. 배운 것 인터럽트 우선순위는 신중하게 정해야 한다. 높은 우선순위가 낮은 우선순위를 블락한다. 당연한 건데 실제로 부딪히니까 다르다. 타이밍 크리티컬한 작업은 인터럽트에 의존하면 안 된다. 특히 다른 인터럯트가 많으면. DMA를 쓰면 CPU 부하도 줄고 타이밍도 보장된다. 하드웨어 트리거 쓰면 더 좋다. 레퍼런스 매뉴얼은 끝까지 읽어야 한다. "Note:" 부분이 중요하다. 거기에 초기화 순서 같은 게 나온다. 3시간 걸렸다. 길다. 하지만 양산 나가서 리콜되는 것보단 낫다. 퇴근 저녁 8시에 나왔다. 일찍 나온 거다. 편의점에서 맥주 샀다. 집 와서 샤워하고 마셨다. 내일은 통합 테스트다. HW팀이랑 같이 실제 배터리 연결해서 돌려본다. 또 문제 나올 수도 있다. 근데 오늘은 해결했다. 그걸로 됐다. 맥주 한 모금 더 마셨다. 시원했다.인터럽트 우선순위, 생각보다 복잡하다.
- 03 Dec, 2025
금요일 오후 3시, 라이브 제품에서 버그 리포트가 들어왔다
금요일 오후 3시, 라이브 제품에서 버그 리포트가 들어왔다 금요일 오후 3시 17분 슬랙에 빨간 점이 떴다. CS팀 채널. "펌웨어팀 @김펌웨어 님, 고객사에서 제품 이상 동작 리포트 들어왔습니다. 확인 부탁드립니다." 첨부된 영상을 봤다. 우리 제품이 멈춰있다. 화면에 아무것도 안 뜬다. LED만 깜빡인다. 양산 나간 지 3개월 된 제품이다. 지금까지 문제없었다. 갑자기 왜. "재현되나요?" 물었다. "고객사에서는 자주 발생한다고 합니다. 저희 테스트에서는 재현 안 됩니다." 제일 싫은 유형이다.일단 로그부터 고객사에 요청했다. "디버그 로그 남아있나요?" 30분 후 답장. "로그 기능 꺼져있었습니다." 당연하지. 양산 버전은 로그 비활성화했다. 플래시 용량 아끼려고. "언제부터 발생했나요?" "정확히는 모르겠습니다. 이번 주에 여러 번 발생했다고 합니다." 증상만 봐서는 모른다. 리셋인지, 하드폴트인지, 워치독인지. 우리 테스트 환경에서는 3시간째 돌려도 멀쩡하다. 고객 환경이 뭐가 다른 거지. 팀장한테 보고했다. "양산 제품 이슈 들어왔습니다. 재현은 안 됩니다." "심각한가?" "고객사에서 자주 발생한다고 합니다." "주말에 대응 가능한가?" 알았다는 뜻이다. 환경 차이 고객사 환경을 조사했다. CS팀이 정리해준 내용.24시간 연속 동작 온도: 실내, 에어컨 있음 전원: 5V 어댑터 네트워크: WiFi, 공유기는 TP-Link 펌웨어 버전: v1.2.3 (최신)우리 테스트 환경이랑 똑같다. 뭐가 다르지. HW팀 민수한테 물었다. "하드웨어 이슈 가능성?" "양산 전에 다 검증했는데요. EMC 테스트도 통과했고." "혹시 로트 문제?" "같은 로트 제품 다른 데서는 문제없어요." 소프트웨어 문제다. 그것도 특정 조건에서만.금요일 오후 5시 팀원들 다 퇴근했다. 팀장만 남아있다. "일단 집에 가. 주말에 보자." 나도 가고 싶다. 근데 못 간다. 고객사는 월요일까지 답변 원한다. 일요일 밤까지 원인 찾아야 한다. 회사 냉장고에서 레드불 두 개 꺼냈다. 오늘 밤샐 것 같다. 일단 코드 리뷰부터 시작했다. v1.2.3 릴리즈 이후 변경점. 아무것도 없다. v1.2.3이 최신이고, 그 이후 수정사항 없다. 그럼 v1.2.3 자체에 버그가 있다는 건데. 3개월 동안 왜 안 나왔지. 테스트 시나리오를 다시 봤다. 우리가 놓친 케이스가 있다. 24시간 고객사는 24시간 연속 동작이라고 했다. 우리 테스트는 최대 8시간. 보통 8시간이면 충분하다. 메모리 릭도 잡히고, 타이밍 이슈도 나온다. 근데 24시간 이상 돌려야 나오는 버그도 있다. 타이머 오버플로우 같은 거. 코드에서 타이머 쓰는 부분을 찾았다. 여러 개다. WiFi 재연결 타이머, 센서 폴링 타이머, 워치독 타이머, RTC... 한 개씩 체크했다. 변수 타입, 오버플로우 조건, 래핑 처리. 있다. WiFi 재연결 로직에서 uint32_t 타이머를 밀리초로 쓴다. 49일마다 오버플로우. 근데 비교 로직이 단순 대소비교다. 오버플로우 케이스를 안 본다. if (current_time > reconnect_time + timeout) { wifi_reconnect(); }49일 넘어가면 current_time이 0으로 돌아간다. reconnect_time은 큰 값. 이 조건이 영원히 참이 된다. WiFi 재연결을 무한 반복한다. 찾았다.금요일 밤 11시 재현 시나리오를 짰다. 타이머를 강제로 49일 근처 값으로 세팅. 보드에 올렸다. 돌렸다. 10분 후 멈췄다. 똑같은 증상. LED만 깜빡이고, 응답 없다. 로그 출력도 멈췄다. 원인 확정이다. 수정은 간단하다. 타이머 비교 로직을 래핑 세이프하게. if ((int32_t)(current_time - reconnect_time) > timeout) { wifi_reconnect(); }부호 있는 정수로 캐스팅하면 오버플로우 케이스도 올바르게 동작한다. 수정했다. 다시 테스트. 이번엔 안 멈춘다. 1시간 돌렸다. 문제없다. 패치 버전을 만들었다. v1.2.4. 빌드하고, 테스트 보드에 올리고, 검증했다. 새벽 2시다. 문서 작업 버그 리포트를 썼다. 원인: WiFi 재연결 타이머 오버플로우 처리 누락발생 조건: 연속 동작 49일 경과 시영향: 제품 응답 없음, LED만 동작수정: 타이머 비교 로직 오버플로우 세이프하게 수정패치 버전: v1.2.4 CS팀에 전달할 자료도 만들었다. "고객사에 전달 부탁드립니다. 월요일 오전까지 v1.2.4 펌웨어로 업데이트하면 해결됩니다." 양산 제품 패치 계획도 세웠다. 이미 나간 제품들은 OTA로 업데이트. 다행히 우리 제품은 OTA 지원한다. 안 했으면 리콜이다. 새벽 3시. 집에 갔다. 토요일 오전 11시에 일어났다. 슬랙 확인. 팀장: "고생했다. 월요일은 오후 출근해라." 고맙다. CS팀: "고객사에 전달했습니다. 월요일 업데이트 예정입니다." 됐다. 침대에 누웠다. 천장을 봤다. 49일. 1180시간. 누가 그렇게 오래 테스트하나. 우리 테스트 시나리오는 8시간이 최대다. 비용 때문에. 시간 때문에. 양산 전에 못 잡은 버그다. 고객이 먼저 발견했다. Low 레벨 개발은 이런 거다. 한 줄 실수가 49일 뒤에 터진다. 웹이면 고쳐서 배포하면 된다. 5분이면 된다. 펌웨어는 OTA 있어도 조마조마하다. 업데이트 중에 전원 나가면 브릭이다. 그래도 찾아서 다행이다. 교훈 같은 건 없다 타이머 오버플로우는 기본 중의 기본이다. 알고 있었다. 그런데 놓쳤다. 코드 리뷰 때 못 봤다. 테스트로도 못 잡았다. 완벽한 코드는 없다. 완벽한 테스트도 없다. 고객이 발견하기 전에 찾으면 좋은 거고, 못 찾으면 이렇게 된다. 금요일 오후 3시에 리포트 들어오고, 주말 날리고, 새벽에 고치고. 이게 펌웨어 개발이다. 월요일에 팀 회의 때 공유할 것이다. "타이머 비교 로직 체크리스트에 추가하자." 다들 "아 그거" 할 거다. 알면서 놓친 거니까. 그래도 체크리스트에 넣는다. 다음에 또 놓칠 테니까. Low 레벨은 이렇다. 같은 실수를 반복하지 않으려고 체크리스트 만들고, 그래도 놓친다. 그럼 또 추가한다. 체크리스트가 100개 넘어간다. 읽는 사람은 없다. 너무 길어서. 일요일 푹 잤다. 아무것도 안 했다. 월요일 오후 출근 예정이다. 고객사 업데이트 결과 확인해야 한다. 문제없이 되면 좋겠다. OTA 실패하면 또 야근이다. 침대에 누워서 생각했다. 웹 개발자들은 지금 뭐 하고 있을까. 카페에서 맥북 켜고 코딩하고 있을까. 배포 버튼 누르면 끝일까. 부럽다. 나는 보드 없으면 일 못 한다. 회사 와야 한다. 오실로스코프 봐야 한다. 디버거 연결하고, 플래시 지우고, 펌웨어 올리고, 리셋하고, 로그 보고. 49일 버그는 집에서 못 잡는다. 장비가 없어서. 그래도 찾았다. 그걸로 됐다.금요일 오후의 빨간 알림은 늘 시작이다. 주말이 끝나고 월요일이 온다.
- 02 Dec, 2025
웹개발자는 롤백하고, 나는?
웹개발자는 롤백하고, 나는? 지난주 팀 술자리에서 웹개발 하는 대학 후배가 했던 말이 자꾸만 떠올라. "어제 배포 망쳤는데 1분 만에 롤백했어. 문제 없음." 그러곤 웃었다. 맥주 한 잔 마시더니 말이다. 나는 그 말을 듣고 어떤 표정을 했는지 모르겠다. 웃었을 수도 있고, 그냥 마셨을 수도 있고. 기억이 안 난다. 그냥 '아, 그렇구나' 했던 것 같다. 펌웨어는 그렇지 않다. 한 번 나가면 끝 양산이 나가기 전, 문제가 발견되는 게 가장 좋다. 그럼 다시 테스트하고, 버그 고치고, 다시 나간다. 시간은 걸리지만 그래도 시간이면 된다. 양산이 나간 후, 문제가 발견되는 건 다르다. 시리즈 50만 개가 판매되고 있다고 치자. 그 중 200개 정도에서 특정 상황에서만 먹통이 되는 버그가 있다고 하자. 그럼 어떻게 되나? 회사는 리콜을 결정한다. 50만 개를 다 회수해야 할까? 아니다. 200개만이라도 빼서 수리하는 데 생기는 비용. 택배비. 검수 비용. 인건비. 시간. 신뢰도 하락. 그 숫자가 억 단위가 된다. 수십억일 수도 있다. 그게 내 잘못일 수도 있다. 출근하면 테스트 결과부터 어제 밤 10시 50분에 보드에 올린 펌웨어의 테스트 결과가 10시간 돌려졌다. 실패다. 또다시 실패다. "UART로 로그 떴어?" 아니. 부팅도 안 돼. 하드웨어 이슈가 아닐까. HW팀에 연락했다. "그쪽 회로 정상 맞나요?" 이건 내가 이미 3번째 하는 질문이다. "펌웨어가 Initialize 못 하는 거 아니야?" 둘 다 맞을 수도 있다. 하드웨어 설계 스펙이 펌웨어 예상과 다를 수 있다. Reference Manual은 2년 전 버전이고, 실제 칩은 새로 나온 패키지다. 스펙이 바뀌었을 수도 있다. 누군가는 문서를 수정해야 했는데, 그 누군가가 일을 안 했을 수도 있다. 한국 회사지만, 칩은 중국 제조사다. 중국 매뉴얼을 영어로 번역한 거다. 한국어 번역은 없다. 그 스펙 매뉴얼이 1000페이지다. 찾는 게 뭔지를 정확히 모르면서 1000페이지를 찾는다. 마치 영어 사전에서 스펠링 모르는 단어를 찾는 것처럼. 웹개발자의 세계 회의실에서 웹팀 리드가 웃으면서 말했다. "저희는 버그 발견되면 바로 핫픽스하고 배포하면 돼요. 2시간이면 사용자가 문제 없다고 느껴요." 부러운 게 아니다. 그게 상식이기 때문에 더 부럽다. 생각해보니, 웹은 내가 배포한 버전이 뭐가 됐든 상관없다. 사용자는 항상 최신 버전을 본다. 내가 만든 거 못 봐. 버그도 못 봐. 내가 고쳤으면 끝이다. 앱도 마찬가지다. 물론 앱 스토어 심사 시간이 있지만, 긴급 업데이트는 몇 시간 안에 나간다. 사용자가 다운받으면 끝. 과거 버전은 사라진다. 우리는 다르다. 양산이 나가는 그날 양산이 나가는 날은 회의가 많다. "완전히 테스트했나?" "네, 온도 테스트, 습도 테스트, ESD 테스트, 수명 테스트 다 했습니다." "예상 이슈는?" "진동 테스트에서 한 번 부팅이 느렸는데, 재현이 안 돼서 일단 기록만 해뒀습니다." 그 말을 하는 순간부터 신경 쓴다. "한 번 부팅이 느렸다"는 게 뭔가. 왜 그랬나. 다시 재현되면? 그때는 뭐 할 건가. ECN 절차가 있다. 양산 후 발견된 버그를 고쳐서 나머지 로트에 반영하는 절차. 시간도 걸리고 비용도 든다. 1-2개 로트면 괜찮은데, 5-6개 로트까지 나갔을 땐 점점 비용이 커진다. 가장 최악의 경우는 리콜이다. 밤새는 일상 지난 화요일, 새벽 1시에 보드를 보고 있었다. 오실로스코프에서 SPI 신호가 이상했다. 클록이 들쑥날쑥했다. 펌웨어 버그일까, 하드웨어일까. HW팀 설계자에게 슬랙 메시지를 보냈다. "아직 안 주무셨나요?" 새벽 2시였다. 설계자도 깨어있었다. "어? 너도 봤어?" 그럼 다 같이 본다는 뜻이다. 보드를 들었다 놨다를 반복했다. 회로도를 봤다. 매뉴얼을 봤다. 코드를 봤다. 결론은 저항값 하나였다. Pull-up 저항이 약한 거였다. 나는 펌웨어가 잘못된 줄 알고 2시간을 낭비했다. "이거 저 고치고 다시 보내면 될 거 같은데?" HW팀이 말했다. "알겠습니다." 나는 왜 펌웨어만 의심했을까. 아마도 하드웨어는 기판이 나가면 고칠 수 없다는 생각 때문이었을 것 같다. 펌웨어는 다시 올릴 수 있으니까. 하지만 기판도 다시 나갈 수 있다. 다만 비용이 비싸고, 시간이 오래 걸릴 뿐이다. 돌이킬 수 없는 결정 웹개발자들이 부러운 이유는 뭘까. 아마도 피드백 루프가 빠르다는 것 때문일 것 같다. 내가 코드를 짰다. 1시간 뒤에 사용자가 버그를 발견했다. 1시간 뒤에 나는 버그를 알았다. 1시간 뒤에 내가 고쳤다. 1시간 뒤에 사용자가 고쳐진 버전을 봤다. 4시간 사이클. 그 안에 내가 배웠고, 고쳤고, 다시 배웠다. 우리는 다르다. 내가 코드를 짰다. 2주일을 테스트했다. 배포했다. 3개월 뒤에 버그가 발견됐다. 그 버그는 극단적인 상황에서만 나타난다. 사용자는 그 상황을 만났고, 우리는 그 상황을 못 만들었다. 이제 어떻게 해야 할까. 하드웨어를 회수하고, 펌웨어를 업데이트하고, 다시 배포한다. 3개월이 지난 버전을 수정해야 한다. 그 사이에 나는 다른 프로젝트를 했을 것이다. 코드를 까먹었을 것이다. 그 로직이 왜 그렇게 짜여있었는지 모를 것이다. 그래도 고쳐야 한다. 롤백은 불가능한 말 "혹시 이전 버전으로 롤백할 수 있나?" 그 질문은 펌웨어에서 가능할 수도, 불가능할 수도 있다. OTA(Over The Air) 업데이트를 지원하는 기기라면 가능할 수도 있다. 이전 버전의 바이너리를 기기에 내장시켜놓으면 된다. 하지만 그러려면 플래시 용량이 필요하다. 용량이 크면 가격이 올라간다. 가격이 올라가면 경쟁력이 떨어진다. OTA를 안 지원하는 기기라면 불가능하다. 그 기기를 들고 회사에 와서, 기사가 프로그래머에 연결해서, 플래시를 지우고, 다시 굽는다. 손으로. 유선으로. 사용자가 5000명이라면? 5000번을 해야 한다. 아니면 회수해서 한다. 회수 비용이 더 싸다. 5천만 원 정도. "저희는 롤백 정책이 없습니다. 대신 처음부터 완벽하게 만듭니다." 내 팀장이 지난 입사 면접에서 한 말이다. 완벽하다는 게 뭘까. 테스트? 배포 전에 할 수 있는 모든 테스트를 했다고 해도, 실제 환경에서는 뭐가 나타날지 모른다. 온도가 55도를 넘어가는 환경에서만 문제가 생기는 버그가 있다고 하자. 우리 테스트 챔버는 50도까지만 간다. 누가 이 문제를 찾을까. 사용자다. 밤 10시의 선택 어제도 밤 10시까지 있었다. 내일도 있을 것 같다. 마감이 일주일밖에 안 남았고, 발견된 버그가 3개다. 그 중 하나는 HW 이슈고, 하나는 내가 고쳐야 할 이슈고, 하나는 뭔지 모르는 이슈다. 회의실에서 팀장이 말했다. "정해진 마감까지 모든 버그를 고칠 수 없으면, 가장 중요한 버그만 고쳐서 나가는 것도 옵션입니다." 그 말은 뭔가. 남은 버그는 그냥 양산에 넣는다는 뜻이다. 나중에 ECN으로 고친다는 뜻이다. 아무도 반대하지 않았다. 모두가 알고 있다. 일정이 진짜 타이트하다는 걸. 그리고 완벽함은 환상이라는 걸. 그 순간, 웹개발자 후배가 떠올랐다. "어제 배포 망쳤는데 1분 만에 롤백했어." 1분. 1분이면 내 팀은 한 줄의 코드 리뷰도 못 한다. 모두가 아는 그 불안감 펌웨어 팀의 공통된 불안감이 있다. 그건 배포 바로 다음 날이다. 첫 번째 보고가 오기를 기다린다. 첫 번째 불량이 나오는 건 피할 수 없다. 전체 50만 개 중에 1개가 이상할 확률이 0이 될 수 없다. 하지만 그 1개가 우리 펌웨어 때문인지, 하드웨어 때문인지, 조립 공정 때문인지는 다르다. 만약 우리 펌웨어 때문이라면? 그 말은 50만 개가 모두 같은 문제를 가지고 있을 수도 있다는 뜻이다. 첫 번째 보고가 나오지 않으면, 다음 날을 기다린다. 1주일을 기다린다. 1개월을 기다린다. 기다리는 동안 내 머리는 자동으로 최악의 시나리오를 그린다. "혹시 특정 환경에서만 터지는 버그 있나?" "혹시 메모리 리크 있나?" "혹시 초기화 루틴을 빠뜨린 게 있나?" 밤에는 더 심하다. 근데 왜 계속하는가 질문 자체가 이상하다. 왜냐하면 이미 답을 알고 있으니까. 첫째, 다른 회사 갈 돈이 없다. 전직 시 정착금이 별로 없고, 새 회사에서 연봉 올려줄 가능성도 낮다. 경력 5년이면 중간이다. 주니어도 아니고 시니어도 아니다. 둘째, 이 일에 익숙해졌다. 다른 일을 하려면 또 배워야 한다. SI를 간 후배들 보니 나은 게 별로 없는 것 같았다. 그냥 다른 고통일 뿐이다. 셋째, 그리고 이게 가장 솔직한 이유인데, 나는 이 일이 싫지 않다. 디버깅할 때의 그 쾌감이 있다. 오실로스코프에서 파형을 보고, "아, 이거 타이밍 문제다"라고 깨닫는 그 순간. 아무도 안 돌던 코드가 돌기 시작했을 때의 그 희열. 웹개발자들이 이걸 경험하나? 모르겠다. 아마 비슷한 뭔가가 있을 것 같긴 한데. 다만 내가 만드는 건 서버에서 사라지지 않는다. 하드웨어에 박혀있다. 10년 뒤에도 누군가 쓸 것이다. 내 코드가. 내 버그도. 그게 또 다른 종류의 두려움이면서, 또 다른 종류의 자부심이다. 그래도 롤백은 아니다 웹개발자들을 부러워하는 건 여전하다. 하지만 달라진 게 하나 있다. 이제는 그들의 롤백 가능성이 부럽기보다, 내 돌이킬 수 없음이 당연해 보인다. 펌웨어는 그런 거다. 한 번 나가면 끝이다. 그 대신 뭔가를 얻는다. 책임감. 긴장감. 그리고 가끔씩 오는 성취감. 회사 물건이 고장 안 나고 계속 잘 돌아가는 그걸 보며 느끼는, 말로 설명하기 어려운 안정감. 내일도 9시에 출근한다. 어제 돌린 테스트가 실패했을 것 같다. 또 디버깅을 해야 할 것 같다. 또 밤 10시까지 있을 것 같다. 또 밤에 몰래 코딩을 할 것 같다. 그리고 이 모든 게 반복될 것 같다. 웹개발자는 롤백한다. 나는 그냥 계속 나간다.한 번 나가면 끝. 웹은 롤백. 펌웨어는 앞으로만 간다.
- 02 Dec, 2025
월요일 아침, 어제 밤 테스트가 또 실패했다
월요일 아침, 또 실패했다 지난 금요일 오후 5시. 나는 한 가지 결정을 했다. 이번 주말은 펌웨어 테스트에 바치겠다고. 양산 일정이 2주 남았고, 메모리 누수 문제가 자꾸만 나타나고 있었다. 재현율도 일정하지 않아서 더 답답했다. 혹시 특정 시나리오에서만 발생하는 건 아닐까 싶어서, 토요일 밤 10시부터 테스트 케이스를 짜기 시작했다. 테스트 자동화 스크립트를 짜고, 보드에 펌웨어를 올렸다. 그리고 돌렸다. 50번. 100번. 200번. 중간에 서버를 켜서 로그를 쌓기도 했다. 무언가 패턴이 있을 거야. 뭔가 타이밍 이슈가 분명히 있어. 그렇게 스스로를 다독였다.결국 새벽 2시. 내 눈은 이미 초점을 잃은 지 오래였지만, 모니터는 계속 켜져 있었다. 테스트 로그가 한 줄씩 쌓여간다. 잠에서 깨어나다를 반복하면서, 나는 휴대폰 알람을 6시로 맞춰놨다. 지금이라도 테스트 결과를 다시 확인할 수 있으려고. 일요일 아침. 침대에서 일어나자마자 가장 먼저 한 일은 회사 랩탑을 켜는 것이었다. 아내가 있었다면 지금쯤 싸웠을 것 같다. 하지만 나는 혼자였고, 혼자라는 건 아무도 나를 막을 수 없다는 뜻이었다. 테스트 결과 폴더를 열었다. test_result_20250120.txt 손가락이 떨렸다. 마우스를 클릭했다. Test Run 1: PASS Test Run 2: PASS Test Run 3: PASS ... Test Run 127: FAIL - Stack Overflow detected Test Run 128: PASS ... Test Run 200: FAIL - Memory corruption at 0x20019A4C한숨이 나왔다. 스택 오버플로우라니. 내가 이미 다 확인했는데? 재귀 함수도 없는데? malloc도 거의 안 쓰는데? 스택 사이즈는 충분하게 잡아놨는데?일요일 하루 종일 로그를 뒤졌다. 스택 오버플로우 전에 어떤 함수가 호출되었는가. 뭔가 재귀 호출이 숨어 있는 건 아닐까. 콜스택을 추적했다. 라이브러리 함수도 다시 읽어봤다. 혹시 RTOS 태스크 스위칭 중에 문제가 생기는 건 아닐까. 밤 11시. 나는 결론을 내렸다. 이건 펌웨어 이슈가 맞는 것 같은데, 타이밍이 맞아야 재현된다. 아마도 특정 조건—아마도 센서 데이터 처리와 블루투스 통신이 동시에 일어날 때—에서 벌어지는 문제인 것 같다. 그렇다면 월요일 아침, 하드웨어팀에 질문해야 한다.월요일 아침 8시 47분. 회사 자리에 앉자마자 모니터를 켰다. 본메일을 열었다. 토요일 밤 11시에 내가 보낸 이메일이 보였다.제목: 메모리 이슈 관련 질문 - 혹시 하드웨어 문제 아닐까요? 안녕하세요. 펌웨어 테스트 중 다음과 같은 증상을 발견했습니다.약 200번 중 2-3번 스택 오버플로우 발생 재현율이 일정하지 않음 센서 값 읽기와 BLE 전송 중에 주로 발생?혹시 센서 전원 공급 라인이 불안정하거나, 아니면 크리스탈 타이밍이 미세하게 어긋나는 건 아닐까요?제목을 다시 읽으니 한숨이 나왔다. 또 '펌웨어 문제 아니겠죠?' 하는 톤이다. 매번 이런 식이다. 내가 물어보는 것처럼 보이지만, 사실 나는 이미 내 것은 다 확인했다. 이제 남은 건 하드웨어 팀의 회신뿐이다. 카톡이 울렸다. 하드웨어팀 선임: "펌웨어 맞나? 우리 회로는 다 문제없는데..." 당연하다. 매번 이 반복이다. 나 혼자만 자기 것을 의심하고, 상대팀은 절대 자기 것을 의심하지 않는다.나는 다시 펌웨어 코드를 열었다. 이번엔 다르게 접근해보겠다. 스택 오버플로우가 맞다면, 스택 사용량을 동적으로 모니터링하는 코드를 삽입해야겠다. 높이수 마크를 남겨놓고, 런타임에 계속 확인한다면 어디서 스택이 터지는지 알 수 있을 것 같다. 코드를 작성했다. extern unsigned int _estack; extern unsigned int _sstack;uint32_t stack_high_water_mark = 0; uint32_t* stack_top = (uint32_t*)&_sstack;void update_stack_monitor() { uint32_t current_sp; asm volatile ("mov %0, sp" : "=r" (current_sp)); uint32_t used = (uint32_t)stack_top - current_sp; if (used > stack_high_water_mark) { stack_high_water_mark = used; // Log this } }이런 식으로 계속 모니터링하면, 진짜 스택이 얼마나 사용되는지 알 수 있다. 그리고 혹시 내가 놓친 부분이 있다면—혹은 라이브러리에서 몰래 큰 배열을 선언한다면—그걸 잡을 수 있을 것이다. 오전 10시. 회의실에서 양산 미팅이 있었다. 영업팀, 하드웨어팀, 펌웨어팀이 모였다. 일정을 확인했다. 14일 뒤 중국 공장으로 샘플을 보낸다고 했다. 샘플. 그 단어만 들어도 가슴이 철렁했다. 샘플이 괜찮으면 양산이 결정된다. 양산이 결정되면 내 코드는 이제 수정 불가다. 딱 이 상태 그대로 나간다. 나는 손을 들었다. "죄송한데, 아직 메모리 스태빌리티 이슈가 남아있어서요. 최소 1주일이 더 필요할 것 같습니다." 회의실이 조용해졌다. 영업팀 과장이 말했다. "메모리 문제라고? 그게 뭔 문제야?" 하드웨어팀 선임이 말했다. "펌웨어 쪽 문제 아닐까요?" 부장이 나를 봤다. "정확한 원인이 뭐야?" 나는 깊게 숨을 쉬었다. "저... 아직 정확히는 모릅니다. 타이밍 이슈인 것 같은데, 센서 입력과 BLE 통신 사이의 레이스 컨디션일 수도 있고, 아니면 진짜 스택 오버플로우일 수도 있습니다. 자동화 테스트로 재현을 기다리고 있어요. 로그에서 패턴을 찾으면 원인을 특정할 수 있을 것 같습니다." 부장은 고개를 끄덕였다. "좋아, 원인을 빨리 찾아." 회의실을 나왔을 때, 내 머리는 이미 다음 시도를 생각하고 있었다. 혹시 센서 드라이버에서 문제가 있는 건 아닐까. 아니면 타이머 인터럽트의 우선순위 설정이 잘못된 건 아닐까. 혹은 그냥 운이 없었을 거고, 계속 테스트하면 한 번은 성공할 거야. 오후 3시. 다시 보드 위의 LED를 본다. 초록 불이 깜박인다. 실행 중이다. 또 테스트를 돌리고 있다. 200번. 500번. 아마 밤새도록 돌릴 것 같다. 그리고 내일 아침, 월요일과 같은 절망감으로 깨어날 것 같다.결국 오늘도 또 다른 월요일의 시작일 뿐이다.