Showing Posts From

이렇게

팀원: '이건 왜 이렇게 복잡하게 했어요?' 나: '...이유가 있었어'

팀원: '이건 왜 이렇게 복잡하게 했어요?' 나: '...이유가 있었어'

팀원: '이건 왜 이렇게 복잡하게 했어요?' 나: '...이유가 있었어' 코드 리뷰 시간 회의실에 모였다. 신입 팀원이 내 코드를 보고 있다. "선배님, 이 부분요." 화면을 가리킨다. 인터럽트 핸들러 안에 이상한 딜레이가 있다. 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년 전의 나. 다 코드 안에 있다. "이건 왜 이렇게 했어요?" "이유가 있었어." 항상 있었다. 지금도, 앞으로도.주석이 코드보다 긴 날이 온다. 그게 성장이다.

센서 캘리브레이션, 왜 이렇게 시간이 걸리나

센서 캘리브레이션, 왜 이렇게 시간이 걸리나

센서 캘리브레이션, 왜 이렇게 시간이 걸리나 시작은 단순했다 프로젝트 킥오프 때 PM이 말했다. "온도 센서 하나 달고, 가속도 센서 하나 달면 되죠?" 그때 난 고개를 끄덕였다. 센서 데이터시트 보고 I2C 연결하면 끝이라고 생각했다. 2주면 충분하다고 보고했다. 지금 6주째다. 회의실에서 시연할 때였다. 온도를 보여주는데 25.3도가 나왔다. 실제 온도계는 23.1도. "2도 차이는 오차 범위 아닌가요?" PM이 물었다. 나는 대답했다. "데이터시트상 오차는 ±0.5도입니다." 그날부터 캘리브레이션 지옥이 시작됐다.데이터시트는 거짓말을 하지 않는다 데이터시트를 다시 읽었다. "Typical accuracy ±0.5°C"라고 적혀 있다. Typical이란 단어가 눈에 들어왔다. 각주를 봤다. "at 25°C, in ideal conditions". 이상적 조건. 우리 제품은 영하 10도에서 영상 50도까지 써야 한다. 보드 위에는 MCU가 있고, 전원 IC가 있고, 다 발열한다. 센서 옆에 파워 LED가 있다. 켜지면 0.3도가 올라간다. 이상적이지 않다. HW팀에 물었다. "센서 위치 옮길 수 있나요?" 답은 "레이아웃 다시 그려야 하는데요"였다. 양산 2달 남았다. 레이아웃 변경은 불가능하다. 결국 소프트웨어로 해결해야 한다. 캘리브레이션이다. 첫 번째 시도: 오프셋 보정 간단하게 생각했다. 실제값 - 측정값 = 오프셋. 이걸 빼주면 되지 않나. 항온 챔버를 빌렸다. 25도로 맞췄다. 센서는 27.2도를 가리킨다. 오프셋 -2.2도. 코드에 넣었다. float get_calibrated_temp(void) { float raw = read_temp_sensor(); return raw - 2.2; }다시 측정했다. 25.0도. 완벽하다. PM에게 보여줬다. "이제 됩니다." 다음 날 HW팀이 왔다. "0도에서 측정해봤는데 1.5도 차이 납니다." 챔버를 0도로 맞췄다. 센서는 1.2도. 보정하면 -1.0도. 실제는 0도. 1도 차이다. 오프셋이 온도마다 다르다.두 번째 시도: 선형 보정 고등학교 수학이 떠올랐다. 일차 함수. y = ax + b. 두 점을 측정했다. (0도, 1.2도), (25도, 27.2도). 기울기를 구했다. a = (27.2 - 1.2) / (25 - 0) = 1.04. 센서가 1.04배씩 틀린다. float get_calibrated_temp(void) { float raw = read_temp_sensor(); return (raw - 1.2) / 1.04; }0도에서 테스트. 0.0도. 25도에서 테스트. 25.1도. 완벽하다. "이제 정말 됩니다." PM에게 보고했다. 다음 주 HW팀이 또 왔다. "50도에서 측정해봤는데 2도 차이 납니다." 챔버를 50도로 맞췄다. 센서는 52.8도. 보정하면 49.6도. 실제는 50도. 0.4도 차이. 선형이 아니다. 세 번째 시도: 다항식 보정 이차 함수를 써야 한다. y = ax² + bx + c. 세 점이 필요하다. 0도, 25도, 50도. 각각 측정했다. 연립방정식을 풀었다. 대학교 선형대수 교재를 꺼냈다. 행렬 계산기를 돌렸다. 계수가 나왔다. a = 0.00012, b = 1.038, c = -1.15. float get_calibrated_temp(void) { float raw = read_temp_sensor(); float corrected = 0.00012 * raw * raw + 1.038 * raw - 1.15; return corrected; }전 구간을 측정했다. 오차가 ±0.3도 이내다. 데이터시트보다 좋다. 성공했다고 생각했다.문제는 개체차 양산 시제품이 왔다. 10개. 모두 측정했다. 1번: 0.2도 오차 2번: 0.8도 오차 3번: 1.3도 오차 ... 10번: 0.5도 오차 센서마다 특성이 다르다. 당연하다. 반도체 공정은 완벽하지 않다. 같은 웨이퍼에서 나와도 다르다. PM이 물었다. "양산할 때마다 캘리브레이션 하면 되지 않나요?" 양산 계획은 월 1만 개다. 하나당 3개 온도에서 10분씩 측정해야 한다. 30분 × 10,000개 = 5,000시간. 불가능하다. "자동화하면요?" PM이 또 물었다. 챔버가 100만원이다. 10대 사면 1억. 공간도 필요하다. 인건비도 필요하다. 제품 원가가 3만원인데. 불가능하다. 네 번째 시도: 원포인트 캘리브레이션 다른 방법을 생각했다. 한 점만 측정하면 어떨까. 25도에서만 측정한다. 그 오차를 저장한다. 부팅할 때 불러온다. 전체 구간에 적용한다. "25도는 어떻게 만드나요?" HW팀이 물었다. 에어컨 튼 사무실. 온도계 옆에 보드를 놓는다. 30분 기다린다. 25도 ±1도 정도는 된다. 정밀하진 않지만 대량 생산에선 현실적이다. void factory_calibration(void) { float raw = read_temp_sensor(); float offset = 25.0 - raw; // 25도 기준 write_flash(OFFSET_ADDR, &offset, sizeof(float)); }float get_calibrated_temp(void) { float offset; read_flash(OFFSET_ADDR, &offset, sizeof(float)); float raw = read_temp_sensor(); return raw + offset; }10개 시제품에 적용했다. 전 구간 오차가 ±1도 이내다. 데이터시트보다 나쁘지만 스펙은 만족한다. PM이 승인했다. 생산팀에 전달했다. 2주 후 생산팀에서 연락 왔다. "25도를 어떻게 확인하나요?" 가속도계는 더 복잡하다 온도 센서는 그나마 나았다. 가속도계는 차원이 다르다. 3축이다. X, Y, Z. 각각 오프셋이 있다. 각각 감도가 다르다. 축 간 크로스토크도 있다. 데이터시트를 봤다. "Factory calibrated". 공장에서 보정했다고 한다. 믿었다. 보드를 평평하게 놓았다. Z축은 1g를 가리켜야 한다. 중력이니까. 측정값: 0.97g. 보드를 세웠다. X축이 1g를 가리켜야 한다. 측정값: 1.03g. Factory calibrated라더니. 6면 캘리브레이션 방법을 찾았다. 보드를 6방향으로 놓는다. +X, -X, +Y, -Y, +Z, -Z. 각각 ±1g가 나와야 한다. 실제로 측정했다.+X: 1.03g -X: -0.98g +Y: 1.01g -Y: -0.99g +Z: 0.97g -Z: -1.02g평균을 구했다. 오프셋을 구했다. 스케일을 구했다. typedef struct { float offset_x, offset_y, offset_z; float scale_x, scale_y, scale_z; } accel_calib_t;void six_point_calibration(accel_calib_t *calib) { // 각 면에서 측정 // 오프셋 = (max + min) / 2 // 스케일 = 2.0 / (max - min) // 복잡한 계산... }한 개 보정하는데 10분 걸렸다. 각 면에서 안정화를 기다려야 한다. 정확히 수평을 맞춰야 한다. 양산은 불가능하다. 결국 타협 PM과 회의했다. "가속도계 정확도를 낮추면 안 될까요?" 스펙 문서를 다시 봤다. "가속도 정확도 ±5%". 현재 오차는 ±3%. 스펙은 만족한다. "그럼 캘리브레이션 안 해도 되는 거 아닌가요?" PM이 물었다. "지금은 그렇습니다. 근데 센서 로트 바뀌면 또 모릅니다." "그럼요?" "그때 다시 보겠습니다." 엔지니어링의 현실이다. 완벽한 해답은 없다. 시간과 비용과 정확도의 균형이다. 배운 것들 6주간 배운 걸 정리했다.데이터시트의 'Typical'은 '최선의 경우'다 센서마다 특성이 다르다 온도는 비선형이다 개체차는 피할 수 없다 양산성을 고려해야 한다 완벽한 캘리브레이션은 불가능하다 스펙을 만족하면 충분하다책상 서랍에 노트를 넣었다. 온도 보정 공식, 측정 데이터, 시행착오가 빼곡하다. 다음 프로젝트에서 또 쓸 것이다. 센서는 항상 틀린다. 그걸 보정하는 게 내 일이다. 완벽할 순 없다. 하지만 쓸 만하게는 만들 수 있다. 그게 펌웨어 엔지니어가 하는 일이다. 다음 프로젝트 오늘 새 프로젝트 킥오프가 있었다. PM이 말했다. "이번엔 압력 센서도 들어갑니다." 나는 데이터시트를 펼쳤다. "Typical accuracy ±2%"라고 적혀 있다. 각주를 봤다. "at 25°C, 1atm, in ideal conditions". 한숨이 나왔다. "캘리브레이션 일정 좀 넉넉히 잡아주세요." 내가 말했다. "얼마나요?" PM이 물었다. "4주요." "2주면 안 될까요?" 또 시작이다.센서는 거짓말쟁이다. 근데 우린 그걸 믿어야 한다. 보정하고, 측정하고, 또 보정한다. 결국 경험만 쌓인다.