회사 '낡은' 컨벤션 vs 인터넷의 '신규' 트렌드

회사 '낡은' 컨벤션 vs 인터넷의 '신규' 트렌드

출근길에 본 글 지하철에서 Reddit 봤다. "Modern Embedded C Programming in 2024" 같은 제목. 클릭했다. 댓글에 누가 적어놨더라. "왜 아직도 헝가리안 표기법 쓰냐", "typedef 남발은 90년대나 하던 거". 웃겼다. 우리 회사 코드가 정확히 그거다. 회사 도착. 컴퓨터 켰다. 어제 푸시한 코드 리뷰가 떠 있다. "변수명 규칙 지켜주세요. g_u8DataBuffer 이런 식으로요." 헝가리안 표기법이다. 타입까지 변수명에 적는 거. C++이나 모던 C에서는 이미 안 쓴다는 게 정설인데.10년 전 코드베이스 우리 회사 메인 프로젝트는 2014년에 시작됐다. 벌써 11년째다. 그때 정한 코딩 컨벤션을 아직도 쓴다. 파일 하나 열면 이런 식이다. /****************************************************************************** * File Name : drv_uart.c * Description : UART Driver Module * Author : Kim XX * Date : 2014.03.15 * Version : v1.0 ******************************************************************************/typedef unsigned char UINT8; typedef unsigned short UINT16; typedef signed char INT8;static UINT8 g_u8RxBuffer[256]; static UINT16 g_u16RxIndex = 0;주석 박스. typedef로 타입 재정의. 전역변수에 g_ 접두사. 타입까지 변수명에. 2014년이면 이게 맞았다. 당시 임베디드 책들이 다 이렇게 가르쳤다. 문제는 2025년에도 이걸 쓴다는 거다. 신입이 작년에 들어왔다. 코드 보더니 물었다. "왜 stdint.h 안 쓰고 typedef로 다시 정의하나요?" 답은 간단했다. "원래 그렇게 해왔어요." 인터넷에서 본 트렌드 점심시간. Reddit r/embedded 들어갔다. 누가 올린 글. "Stop using global variables in embedded C". 내용 읽어봤다. 전역변수 대신 구조체로 컨텍스트 만들고, 함수에 포인터로 넘기라는 거다. 테스트하기도 쉽고 재사용성도 좋다고. 댓글도 다들 동의한다. "2024년에 전역변수는 레거시", "static 남발은 유지보수 지옥". 맞는 말이다. 우리 코드 전역변수 천지다. static uint8_t g_u8SensorData[10]; static bool g_bIsConnected = false; static uint32_t g_u32Timestamp = 0;파일 하나에 전역변수 30개 넘는 것도 있다. 어디서 바뀌는지 추적하려면 전체 검색 돌려야 한다. GitHub에서 최신 프로젝트들 봤다. Zephyr, FreeRTOS 예제 코드들. struct sensor_context { uint8_t data[10]; bool is_connected; uint32_t timestamp; };void sensor_read(struct sensor_context *ctx) { // ... }깔끔하다. 컨텍스트 구조체 하나로 상태 관리. 함수는 순수하게 로직만.개선 제안서 작성 저녁 먹고 돌아왔다. 생각했다. 제안서 써볼까. PPT 켰다. 제목 적었다. "코드 컨벤션 개선 제안". 슬라이드 3장 만들었다.현재 문제점: typedef 중복 정의, 전역변수 과다, 헝가리안 표기법 개선안: stdint.h 사용, 구조체 기반 컨텍스트, 모던 C 네이밍 기대효과: 가독성 향상, 유지보수성 개선, 신규 인력 적응 빠름레퍼런스도 달았다. NASA C Style Guide, Google C++ Style Guide, Linux Kernel Coding Style. 다음 날 팀장한테 보여줬다. 5분 봤다. 말했다. "취지는 좋은데요." 여기서 끝나면 좋겠는데. "지금은 여유가 없어요." 지금은 여유가 없다 팀장 말을 정리하면 이렇다. "지금 프로젝트 일정 빡빡합니다. 양산 2달 남았어요. 코드 스타일 바꾸다가 버그 생기면 어떻게 할 건데요. 제품 나간 다음에 생각해봅시다." 맞는 말이다. 양산 앞둔 시점에 대규모 리팩토링은 위험하다. 그런데. 2년 전에도 들었다. 그때도 "양산 끝나고". 1년 전에도 들었다. 그때는 "다음 프로젝트 시작할 때". 지금 또 들었다. "제품 나간 다음에". 결국 안 바뀐다. 영원히. 이유는 간단하다. 임베디드는 항상 바쁘다. 양산 끝나면 다음 제품. 다음 제품 끝나면 또 다음. 여유라는 게 없다. 선배한테 물었다. 7년차. "형, 우리 코드 스타일 언제부터 이랬어요?" "내가 입사했을 때도 이랬어. 그때도 바꾸자는 얘기 나왔는데." "안 바뀐 거네요." "응. 계속 바쁘다고."실무와 이상의 간극 퇴근길. 생각했다. 인터넷에서 보는 건 이상적이다. 깨끗한 코드, 모던한 기법, 최신 트렌드. 실제 회사는 다르다. 레거시 코드, 빡빡한 일정, 리스크 회피. 누구 잘못도 아니다. 구조적인 문제다. 웹 개발자들 부럽다. 걔네는 배포하고 A/B 테스트하고 롤백한다. 실험할 수 있다. 우리는 못한다. 한 번 양산 나가면 끝이다. 펌웨어 업데이트 구조 없으면 그냥 그대로 간다. 리스크가 크니까 보수적이 된다. 보수적이니까 안 바뀐다. 악순환이다. 신입이 물어봤다. 어제. "선배님, 학교에서 배운 거랑 너무 달라요. 전역변수 쓰지 말라고 배웠는데 여기는..." 할 말이 없었다. "그냥... 익숙해져." 작은 개선이라도 포기는 안 한다. 큰 개선은 안 돼도 작은 건 된다. 새로 작성하는 파일에는 stdint.h 썼다. uint8_t, uint32_t 이런 식으로. 리뷰에서 지적 안 들어왔다. 팀장도 그냥 넘어갔다. "기존 코드랑 다른데요." "새 모듈이라 따로 뺐습니다." "음... 뭐 돌아가면 되지." 승인 떨어졌다. 다음 파일도 그렇게 했다. 조금씩. 전역변수도 줄였다. 새 기능은 구조체로 만들었다. struct ble_context { uint8_t rx_buffer[256]; uint16_t rx_index; bool is_connected; };함수도 컨텍스트 받게 수정했다. 기존 코드는 못 고친다. 건드리면 테스트 다시 해야 한다. 시간 없다. 새로 짜는 것만이라도 깨끗하게. 이게 현실적이다. 10년 후엔 생각해봤다. 10년 후. 2035년. 나도 11년차가 돼 있다. 신입이 들어온다. 코드 보고 물어본다. "왜 이렇게 작성했어요?" 나는 뭐라고 답할까. "원래 그렇게 해왔어." 이러고 싶지는 않다. 그렇다고 지금 당장 전부 바꿀 수는 없다. 현실이 그렇다. 타협점을 찾아야 한다. 레거시는 유지. 신규는 개선. 천천히. 10년 걸려서 바뀔 수도 있다. 어쩌면 안 바뀔 수도 있다. 그래도 시도는 한다. 포기하면 영원히 안 바뀐다.회사 코드는 박물관이다. 신입은 적응한다. 나도 그랬다.

ESP32의 WiFi 연결이 불안정하다고? 지연시간이 길었나?

ESP32의 WiFi 연결이 불안정하다고? 지연시간이 길었나?

ESP32 WiFi 연결이 불안정하다고? 지연시간이 길었나? 오늘도 WiFi가 안 된다 출근했다. 어제 밤 11시까지 ESP32 WiFi 코드 손봤는데 오늘 아침에 테스트하니까 또 안 된다. 연결은 되는데 5분 지나면 끊긴다. 로그 보니까 WIFI_EVENT_STA_DISCONNECTED. 이유 코드는 WIFI_REASON_BEACON_TIMEOUT. 비콘 타임아웃. 즉 공유기한테 신호를 못 받았다는 거다. "코드 문제 아닌가요?" 팀장이 물었다. 코드는 어제도 돌아갔다. 장소만 바뀌었다. 개발실에서는 되는데 회의실에서는 안 된다.무선 통신은 이렇다. 같은 코드가 어디서는 완벽하고 어디서는 쓰레기가 된다. 하드웨어 환경에 따라 달라진다. 개발자가 제어할 수 없는 영역이다. 커피 마시고 오실로스코프 켰다. 안테나가 문제일까 ESP32 모듈 두 종류 있다. 하나는 PCB 안테나, 하나는 외부 안테나 커넥터. 우리 제품은 PCB 안테나다. 싸니까. PCB 안테나는 보드 배치가 중요하다. 안테나 밑에 GND 플레인 있으면 신호 약해진다. 주변에 금속 케이스 있어도 문제다. 우리 케이스는 플라스틱이지만 내부에 EMI 차폐용 금속 테이프 붙어 있다. HW팀한테 물었다. "안테나 주변 keep-out 영역 지켰어요?" "네, 5mm 확보했습니다." 데이터시트에는 10mm 권장이다. 5mm면 부족하다. 하지만 PCB 크기 제약이 있었다. 제품이 작아야 한다고 했다. 그래서 안테나 영역을 줄였다."그럼 외부 안테나 버전으로 테스트해보죠." 외부 안테나 달았다. 연결 안정적이다. 10분 지나도 안 끊긴다. 1시간 돌렸다. 문제없다. 문제는 양산이다. 외부 안테나는 부품 비용이 오르고 조립 공정이 추가된다. 원가 담당자가 싫어한다. "PCB 안테나로 안 되나요?" 된다. 조건만 맞으면. 전파 간섭이라는 늪 회의실에서 안 되는 이유를 찾아야 한다. WiFi 스캔 돌렸다. 주변 AP가 12개다. 다들 2.4GHz 채널 1, 6, 11 쓴다. 우리 공유기는 채널 6. 옆 회사 공유기도 채널 6. 채널 겹치면 간섭 생긴다. 특히 2.4GHz는 채널 폭이 넓어서 1, 6, 11만 겹치지 않는다. 나머지는 다 겹친다. 공유기 설정 들어가서 채널 11로 바꿨다. 다시 테스트. 조금 나아졌다. 그래도 10분 지나면 끊긴다. RSSI 로그 찍어봤다. 신호 강도다. -70dBm 정도 나온다. 약하다. -50dBm 이상은 되어야 안정적이다. -70dBm이면 패킷 로스 생긴다.공유기를 가까이 가져왔다. 1m 거리. RSSI -40dBm. 연결 안정적이다. 30분 테스트, 문제없다. 거리가 문제다. 그리고 벽이 문제다. 회의실과 개발실 사이에 콘크리트 벽 하나 있다. 2.4GHz는 벽 투과 잘 되지만 신호는 약해진다. 팀장한테 보고했다. "안테나 성능 부족입니다. 거리 3m 이내에서는 안정적인데 벽 넘어가면 불안정합니다." "고객 환경은 더 안 좋을 텐데요." 맞다. 고객은 공유기를 어디 둘지 모른다. 10m 떨어질 수도 있다. 벽 두 개 넘어갈 수도 있다. 코드로 버틸 수 있는 것들 하드웨어 못 바꾸면 펌웨어로 버텨야 한다. 할 수 있는 게 몇 가지 있다. 첫째, WiFi 파워 세이빙 끄기. ESP32는 기본적으로 DTIM 주기마다 슬립 들어간다. 전력 아끼려고. 하지만 슬립 들어가면 비콘 놓칠 수 있다. 신호 약할 때는 치명적이다. esp_wifi_set_ps(WIFI_PS_NONE);이거 하나로 연결 안정성 올라간다. 대신 전류 소모 20mA 증가. 배터리 제품이면 고민해야 한다. 우리는 DC 전원이라 상관없다. 둘째, 재연결 로직 강화. 연결 끊기면 자동으로 재연결 시도한다. ESP-IDF 기본 동작이다. 하지만 재연결 시도 간격이 짧으면 공유기가 부담스러워한다. 너무 길면 서비스 중단 시간이 길어진다. 나는 5초 간격으로 설정했다. 3번 실패하면 10초로 늘린다. 10번 실패하면 리부팅. 완벽하지 않지만 최선이다. 셋째, TCP Keepalive. TCP 연결 유지용 패킷이다. 일정 시간마다 상대한테 '살아있냐' 물어본다. 대답 없으면 연결 끊는다. int keepalive = 1; setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(int));int keepidle = 30; // 30초 idle 후 시작 int keepintvl = 5; // 5초 간격 int keepcnt = 3; // 3번 실패하면 끊기 setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(int)); setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(int)); setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(int));이것도 도움 된다. 연결 끊겼는지 빨리 알 수 있다. 지연시간은 또 다른 문제 연결 안정성 해결했다고 끝이 아니다. 지연시간 문제가 남았다. ping 찍어봤다. 평균 50ms. 나쁘지 않다. 그런데 가끔 500ms 튄다. 1초 넘을 때도 있다. 이게 문제다. 우리 제품은 실시간 제어 장치다. 명령 내리면 100ms 안에 반응해야 한다. 지연시간 500ms면 사용자는 '고장 났나' 생각한다. 원인을 찾아야 한다. Wireshark 켰다. 패킷 캡처 시작. 보니까 재전송이 많다. TCP 패킷이 안 가고 다시 보낸다. WiFi 환경이 불안정하니까 패킷 로스 생긴다. 로스 생기면 재전송하고, 재전송하면 지연 증가한다. 해결 방법? 근본적으로는 WiFi 신호 강도 올려야 한다. 안테나 개선이 답이다. 펌웨어로 할 수 있는 건 UDP 쓰기. TCP는 신뢰성 보장하려고 재전송한다. UDP는 안 한다. 대신 데이터 유실 가능하다. 실시간성이 중요하면 UDP가 낫다. 제어 명령은 최신 상태가 중요하지 과거 명령은 의미 없다. 100ms 전 명령어 1초 후에 도착하면 쓸모없다. "UDP로 바꿔볼까요?" 제안했다. "데이터 유실되면요?" 팀장이 물었다. "중요한 건 ACK 받고, 나머지는 재전송 안 하는 걸로요." Application 레벨에서 ACK 구현하기로 했다. 중요한 명령만 확인 응답 받는다. 나머지는 그냥 보낸다. 유실되면 다음 거 보낸다. 코드 수정 일주일 걸렸다. 테스트해보니 지연시간 평균 30ms. 튀어도 150ms. 개선됐다. 양산 전 마지막 테스트 양산 전에 환경 테스트 한다. 다양한 조건에서 돌려본다.거리 테스트: 1m, 3m, 5m, 10m 장애물 테스트: 벽 없음, 벽 1개, 벽 2개 간섭 테스트: AP 1개, AP 5개, AP 10개 장시간 테스트: 24시간, 72시간거리 테스트. 10m에서 RSSI -75dBm. 연결 유지되지만 패킷 로스 5%. 사용 가능하지만 권장하지 않는다. 매뉴얼에 '공유기와 5m 이내 설치 권장' 쓰기로 했다. 장애물 테스트. 벽 2개 넘어가면 연결 불안정. 이건 어쩔 수 없다. 안테나 성능 한계다. 매뉴얼에 '장애물 최소화' 추가. 간섭 테스트. AP 10개 환경에서 연결 유지된다. 하지만 지연시간 증가. 평균 80ms. 허용 범위다. 장시간 테스트. 72시간 돌렸다. 중간에 두 번 끊겼다. 자동 재연결 됐다. 괜찮다. 테스트 보고서 작성했다. 팀장한테 올렸다. "양산 가능합니다. 단, 안테나 개선 버전 2차 개발 건의합니다." "1차는 이대로 가고 2차에서 개선하죠." 그렇게 결정됐다. 결국 하드웨어다 펌웨어 개발자지만 인정한다. WiFi 안정성은 결국 하드웨어다. 안테나 배치, 케이스 설계, PCB 레이아웃. 이게 90%다. 펌웨어는 나머지 10% 보완하는 거다. 개발실에서 완벽해도 현장에서 안 되면 소용없다. 고객 환경은 통제 불가능하다. 공유기 위치, 주변 간섭, 벽 재질, 전부 다르다. 그래서 마진을 남겨야 한다. 신호 강도 -50dBm 이상 목표로 설계해야 현장에서 -70dBm 나와도 버틴다. 우리는 -70dBm 목표로 설계했다. 그래서 현장에서 불안정하다. 다음 버전에서는 외부 안테나 쓰기로 했다. 원가 오르지만 안정성이 중요하다. 고객 클레임 처리 비용이 안테나 비용보다 비싸다. 한 번 출장 가면 20만원이다. 클레임 10건이면 200만원. 안테나는 개당 500원 차이다. 1000개 팔아도 50만원 차이. 계산하면 답 나온다. 퇴근하면서 생각했다. 무선 통신 제품은 어렵다. 유선은 꽂으면 된다. 무선은 환경 따라 달라진다. 다음 프로젝트는 유선으로 하고 싶다. 하지만 IoT 시대에 유선 제품은 없다. 다들 무선 원한다. 편하니까. 편한 건 사용자고 고생은 개발자다. 뭐 어쩌겠나. 월급 받는 일이다.내일은 BLE 디버깅이다. WiFi보다 더 까다롭다고 들었다. 미리 두통약 챙겨야겠다.

팀원이 온 지 3개월, 아직 FreeRTOS 세마포어 이해를 못 했다

팀원이 온 지 3개월, 아직 FreeRTOS 세마포어 이해를 못 했다

3개월째인데 팀에 신입이 왔다. 3개월 전이다. 대학원 출신. 임베디드 전공. 논문도 썼다. 나보다 학벌 좋다. 근데 아직도 세마포어를 못 쓴다. 정확히는, 쓰긴 쓰는데 왜 쓰는지 모른다. 그냥 선배 코드 복붙한다. 오늘 코드 리뷰했다. "여기 왜 xSemaphoreTake 썼어요?" "...경쟁 상태 막으려고요." "무슨 경쟁 상태?" "..." 침묵이 길었다.이론은 안다 신입이 멍청한 건 아니다. Context switching 개념은 안다. Race condition도 설명할 수 있다. Deadlock은 교과서적으로 외우고 있다. 문제는 실제다. "이 변수는 왜 보호 안 해요?" "어... 여기는 괜찮지 않나요?" "ISR에서 접근하는데?" "아..." 매번 이렇다. 교과서랑 다르다. 실제 펌웨어는 더럽다. ISR 있고, Task 여러 개 있고, 공유 버퍼 있고, 타이머 있고. 어디서 뭐가 먼저 실행될지 모른다. 신입은 순서도를 그린다. 깔끔하게. "Task A가 먼저 실행되고..." "누가 먼저 실행된다고 했어?" "...아 스케줄러가..." "스케줄러는 Priority 보고, 같으면 Round-robin이고, ISR 들어오면 선점되고." "..." 또 침묵. 내가 가르치면서 헷갈린다 신입 가르치다 보니 나도 헷갈린다. "Mutex랑 Binary Semaphore 차이가 뭐죠?" "음... Mutex는 소유권이 있고..." "그게 정확히 뭔데요?" 대답하려다 막혔다. Priority inheritance 얘기해야 하나. 근데 우리 프로젝트에서 그거 쓰나. 확인 안 해봤다. 집에 가서 찾아봤다. FreeRTOS 문서. "A mutex is a binary semaphore that includes a priority inheritance mechanism." 아 맞다. 이거였다. 근데 Priority inheritance가 정확히 어떻게 동작하지. High priority task가 block되면 low priority task의 priority를 올려준다. 그게 언제까지지. Mutex release할 때까지인가. 다시 문서 읽었다. 30분 걸렸다. 다음 날 신입한테 설명했다. "아~ 그래서 Mutex 써야 하는구나." 나도 이제 안다.Deadlock 재현이 안 된다 신입이 물었다. "Deadlock은 어떻게 막아요?" "Lock 순서 정하면 돼. A 먼저, B 나중에. 모든 Task가 이 순서 지키면 Deadlock 안 생겨." "근데 만약 실수하면요?" "...Deadlock 나지." "그럼 어떻게 디버깅해요?" 좋은 질문이다. 대답 못 했다. Deadlock 본 적이 거의 없다. 이론으로는 안다. Task A가 Lock1 잡고 Lock2 기다리고, Task B가 Lock2 잡고 Lock1 기다리면 둘 다 멈춘다. 간단하다. 근데 실제로는 안 본다. 우리 코드에는 Mutex가 3개밖에 없다. Lock 순서 지키기 어렵지 않다. "한번 만들어볼까요? Deadlock." 신입이 재미있다는 표정이다. 테스트 코드 짰다. void taskA(void *param) { xSemaphoreTake(mutex1, portMAX_DELAY); vTaskDelay(10); xSemaphoreTake(mutex2, portMAX_DELAY); // ... }void taskB(void *param) { xSemaphoreTake(mutex2, portMAX_DELAY); vTaskDelay(10); xSemaphoreTake(mutex1, portMAX_DELAY); // ... }돌렸다. 안 멈춘다. "왜 안 되죠?" "...타이밍 문제 아닐까." vTaskDelay를 1로 줄였다. 여전히 안 멈춘다. 30분 동안 삽질했다. 알고 보니 Priority가 같아서 Round-robin으로 돌아간다. Delay가 있으면 서로 엇갈린다. Priority 다르게 줬다. 그래도 안 멈춘다. 왜지. 결국 찾았다. Higher priority task가 먼저 두 개 다 잡는다. Lower priority는 실행도 안 된다. "아... Preemptive라서." 신입이 이해했다. Deadlock 재현하려면 두 Task가 정확히 동시에 실행돼야 한다. 불가능하다. Core가 하나라서. 억지로 만들었다. ISR에서 한 Task 깨우고, 다른 Task는 이미 실행 중이고. 복잡했다. 겨우 Deadlock 봤다. 시스템 멈췄다. "와, 진짜 멈추네요." 당연하지. 근데 이거 실제로 겪으면 어떻게 찾지. 로그도 안 찍힌다. Task 멈춰서. "configUSE_TRACE_FACILITY 켜면 Task state 볼 수 있어." "그거 뭐예요?" 나도 안 써봤다.Race condition은 재현이 더 안 된다 "Race condition은요?" 신입이 계속 묻는다. 좋은 태도다. "공유 변수 여러 Task에서 쓰면 생겨." "그러면요?" "값이 이상해져." 예제 보여줬다. uint32_t counter = 0;void taskA(void *param) { counter++; }void taskB(void *param) { counter++; }"이거 1000번 돌리면 2000 안 나올 수 있어." "정말요? 해봐도 돼요?" 해봤다. 2000 나온다. 항상. "...왜 안 깨지죠?" "음..." 생각해봤다. STM32는 32bit CPU다. uint32_t 쓰기는 atomic이다. 한 instruction이다. "아, uint32_t라서 그래. uint64_t 써봐." 써봤다. 그래도 2000 나온다. 왜지. 한참 생각했다. counter++ 는 단순해 보이지만 실제로는 세 단계다. Load, Increment, Store. 근데 컴파일러 최적화 때문에 레지스터에 올라가면 끝이다. Task 전환돼도 레지스터는 저장된다. Context switching이 그거다. "이거 실제로 깨지게 하려면..." 어떻게 하지. Volatile 없애고, 최적화 켜고, 그래도 안 깨진다. Task priority 같게 하고, 루프 1000000번 돌려도 2000 나온다. "선배, 이거 안 깨지는 거 아니에요?" "...아니야. 깨져야 하는데." 검색했다. "ARM Cortex-M에서는 단일 레지스터 쓰기 atomic" 아. 그래서 안 깨진다. 억지로 깨뜨렸다. 구조체 만들고, 여러 필드 동시에 쓰게 하고. struct { uint32_t a; uint32_t b; } data;void taskA(void *param) { data.a++; data.b++; }이것도 안 깨진다. 왜냐면 둘 다 따로 atomic이라서. 결국 포기했다. "실제로는 SPI 버퍼 같은 데서 깨져. 거기는 여러 바이트니까." "아~ 네." 신입이 이해했는지 모르겠다. 나도 헷갈린다. 문서가 애매하다 FreeRTOS 문서 읽으면서 느낀다. 애매한 부분이 많다. "fromISR 함수는 언제 써요?" "ISR에서 쓸 때." "왜 따로 있어요?" "...스케줄러 동작이 달라서?" 정확히 모른다. 찾아봤다. 일반 함수는 block될 수 있다. ISR에서 block되면 안 된다. 그래서 fromISR은 block 안 된다. 대신 결과를 리턴한다. "그럼 xQueueSendFromISR에서 큐가 꽉 차면요?" "못 넣고 리턴해." "데이터 유실되는 거네요?" "...그렇지." 생각해보니 맞다. ISR에서는 기다릴 수 없다. 큐 꽉 차면 버린다. 근데 우리 코드는 어떻게 처리했지. 확인 안 해봤다. 다음 날 코드 봤다. 리턴 값 확인 안 한다. 그냥 xQueueSendFromISR 호출하고 끝. "이거 실패하면 어떡하죠?" "...모르지." 수정했다. 실패하면 에러 카운터 올리게. if (xQueueSendFromISR(queue, &data, &xHigherPriorityTaskWoken) != pdPASS) { error_count++; }근데 이 에러 카운터를 누가 보나. 로그에도 안 찍는다. ISR에서 printf 못 쓴다. "주기적으로 체크하는 Task 만들까요?" "...나중에." 결국 안 만들었다. 양산 나가면 에러 생겨도 모른다. 뭐 어쩌겠나. 신입이 물어본 것들 "Binary semaphore랑 Counting semaphore 차이요?" "Binary는 0, 1만. Counting은 여러 개." "언제 Counting 써요?" "...리소스가 여러 개 있을 때?" 실제로 써본 적 없다. 우리 프로젝트에는 다 Binary다. "Queue랑 Semaphore 차이요?" "Queue는 데이터 전달. Semaphore는 신호만." "Queue도 신호 아니에요?" "...음, 데이터 있는 신호?" 대답이 이상하다. 근데 맞는 것 같기도 하다. "Event group은요?" "여러 이벤트 기다릴 수 있어." "예를 들면요?" "...음..." 예를 들지 못했다. 써본 적 없다. "Stream buffer는요?" "바이트 스트림 전달할 때." "Queue랑 뭐가 달라요?" "...더 빨라." 정말 빠른지 모른다. 측정 안 해봤다. 신입이 질문 많다. 좋은 건데 대답이 힘들다. 내가 모르는 걸 깨닫는다. 결국 동작하면 된다 신입한테 말했다. "완벽하게 이해 안 해도 돼. 일단 돌아가게 만들어." 냉정하게 들릴 수 있다. 근데 사실이다. 우리 프로젝트는 복잡하지 않다. Task 5개. Mutex 3개. Queue 2개. ISR 4개. 패턴은 정해져 있다. 센서 데이터 읽는 Task, Queue에 넣는다. 처리하는 Task, Queue에서 꺼낸다. 통신하는 Task, Mutex로 보호한다. 이론 몰라도 복붙하면 된다. 선배 코드 따라 하면 된다. "세마포어를 왜 쓰는지 이해 안 가도, 일단 써. 나중에 이해돼." "...네." 신입이 실망한 표정이다. 미안하다. 근데 나도 그렇게 배웠다. 5년 전 나도 몰랐다. 선배 코드 복붙했다. 돌아갔다. 이해는 나중에 됐다. 지금도 완전히 이해했는지 모르겠다. 오늘 신입 가르치면서 또 헷갈렸다. 근데 코드는 돌아간다. 5년 동안 Deadlock 없었다. Race condition도 없었다. 적어도 발견 못 했다. 그러면 된 거 아닌가.신입이 언제쯤 세마포어 이해할까. 나는 이해한 건가.

오시로스코프 프로브, 이제 내 손의 연장이다

오시로스코프 프로브, 이제 내 손의 연장이다

아침 9시, 프로브부터 꺼낸다 출근했다. 가방에서 노트북보다 먼저 꺼내는 게 프로브다. 10배 프로브 2개, 접지 클립 여분까지. 없으면 일을 못 한다. 책상 위 오실로스코프 전원 켠다. Rigol 4채널, 100MHz. 팀 막내가 쓰던 50MHz보다 낫다. 예산 싸울 때 이긴 보람이 있다. 어제 퇴근 전에 STM32 보드 올려놨다. UART 통신이 안 돼서 밤 11시까지 붙잡았다. 데이터시트는 'works normally'라는데 뭐가 정상인지 모르겠다. 프로브 끝을 TX 핀에 댄다. 익숙한 무게감. GND 클립 보드에 물린다. 파형이 뜬다. 3.3V 기준으로 신호가 왔다 갔다. 그런데 타이밍이 이상하다.파형이 답을 알려준다 UART 설정은 115200 baud. 계산하면 비트당 8.68µs여야 한다. 화면에는 10µs로 나온다. 보레이트가 안 맞는 거다. 클럭 설정 코드 다시 본다. PLL 설정, 분주비, 버스 클럭... 한참 본다. APB1이 36MHz인 줄 알았는데 실제로는 32MHz였다. 계산 실수다. 코드 고친다. 빌드, 플래싱, 리셋. 프로브는 그대로 물려둔다. 다시 파형 본다. 8.7µs. 거의 맞다. 터미널 열어본다. "Hello World" 뜬다. 됐다. printf 디버깅으로는 못 찾는다. '왜 안 나오지?'만 반복한다. 파형 보면 명확하다. 신호가 나가는지, 타이밍이 맞는지, 노이즈는 없는지. 다 보인다. HW팀 민수 선배가 이렇게 말했다. "펌웨어는 눈으로 못 보잖아. 그래서 오실로스코프가 필요한 거야." 맞다. 코드는 추상적이다. 레지스터에 값 쓰면 '잘 됐겠지' 생각한다. 실제로는 클럭이 안 돌거나, 핀 설정이 안 돼있거나, 타이밍이 엇갈린다. 파형으로 확인해야 진짜다.인터럽트 타이밍은 µs 단위 싸움이다 오후 3시. 새 프로젝트 시작이다. IoT 센서 모듈. nRF52 BLE 칩 쓴다. 센서 데이터를 I2C로 읽어서 BLE로 쏴야 한다. 센서는 100Hz 샘플링. 10ms마다 인터럽트 걸어서 읽는다. 코드 짜고 올린다. 동작한다. 그런데 가끔 데이터가 누락된다. 로그 찍어봐도 모른다. "Read OK" 다 뜬다. 그런데 BLE 패킷 보면 중간에 빈다. 10개 중 1개씩 빠진다. 프로브 꺼낸다. CH1은 센서 INT 핀, CH2는 I2C SCL, CH3는 I2C SDA. 트리거는 INT 하강 엣지. 파형 본다. INT 떨어지고 3µs 후 SCL 올라간다. 정상이다. 그런데 한참 보니까 가끔 INT와 BLE 타이머 인터럽트가 겹친다. 우선순위 문제다. BLE 스택이 내부적으로 타이머 인터럽트 쓴다. 1ms마다. 센서 인터럽트랑 동시에 걸리면 BLE가 먼저 처리된다. 그 사이 센서 FIFO 오버플로우. 데이터 날아간다. 해결은 간단하다. 센서 읽는 걸 메인 루프로 빼고 플래그로 처리. 인터럽트는 플래그만 세운다. 타이밍 겹쳐도 데이터 안 날아간다. 다시 테스트. 프로브로 확인. INT 떨어지고 → 플래그 세우고 → 메인에서 읽는다. 타이밍 다 맞다. 3시간 돌렸는데 누락 없다. 이게 Low 레벨이다. µs 단위로 뭐가 먼저 실행되는지 봐야 한다. 코드로는 모른다. 파형으로 봐야 안다.하드웨어 이슈 찾을 때도 프로브다 지난주 목요일. 양산 보드 샘플 도착했다. 프로토타입에서 잘 되던 코드 올렸다. 부팅 안 된다. LED 안 깜빡인다. UART 출력 없다. 뭔가 심각하다. 전원부터 본다. 프로브로 3.3V 핀 측정. 3.28V. 정상이다. GND도 본다. 0V. 맞다. 리셋 핀 본다. 3.3V에서 계속 있다. 리셋 안 풀린다. 회로도 본다. 리셋 핀에 풀업 저항 10kΩ, 리셋 버튼은 풀다운. 문제없어 보인다. 그런데 프로브로 리셋 버튼 핀 보니까 1.5V다. 뭔가 이상하다. HW팀 불렀다. 민수 선배가 테스터 들고 온다. 저항값 재본다. 리셋 풀업이 100Ω이다. 회로도는 10kΩ인데 실제 부품은 100Ω. 발주 실수다. 100Ω이면 전류 33mA 흐른다. MCU 리셋 핀은 최대 5mA. 내부 로직 망가진 거다. 보드 버린다. 10개 샘플 중 8개 똑같았다. 발주 다시 넣었다. 이번엔 제대로 확인했다. 1주일 걸렸다. 일정 밀렸다. PM이 화났다. 그래도 어쩔 수 없다. 프로브 안 댔으면 '펌웨어 문제' 찾느라 3일 날렸다. 파형 보고 1시간 만에 하드웨어 이슈 확인했다. 진짜 문제가 뭔지 프로브가 알려준다. 프로브 없으면 일 못 한다 웹 개발자 친구들 부럽다. 걔네는 브라우저 개발자 도구 열면 된다. 콘솔 로그 찍으면 된다. 디버거 붙이면 된다. 나는 printf도 안 될 때가 있다. UART 핀 없으면 못 쓴다. 디버거 붙이면 타이밍 바뀐다. RTOS 멀티 태스킹 환경에서 브레이크포인트 걸면 다른 태스크가 영향 받는다. 그래서 프로브다. 비침습적이다. 신호 관찰만 한다. 시스템 동작 안 바뀐다. 타이밍 그대로 본다. 진짜 상황 본다. 프로브 선택도 중요하다. 10배 프로브 써야 한다. 1배는 입력 임피던스 낮아서 신호 왜곡된다. 고속 신호는 Active 프로브 써야 하는데 비싸다. 한 개에 200만원. 접지도 중요하다. 긴 접지 클립 쓰면 노이즈 잡힌다. 짧은 스프링 쓴다. 접지 루프 최소화. 파형 깨끗해진다. 트리거 설정도 익숙해져야 한다. Edge, Pulse Width, Setup/Hold. 프로토콜 디코딩도 쓴다. I2C, SPI, UART 자동 해석. 편하다. 요즘은 Logic Analyzer도 쓴다. Saleae. 8채널, 디지털 신호 동시에 본다. 프로토콜 여러 개 섞였을 때 좋다. 근데 아날로그는 못 본다. 그래서 오실로스코프도 필요하다. 새벽 1시, 파형 보다가 발견한다 양산 일정 2주 남았다. 펌웨어 베타 테스트 중이다. 간헐적 리셋 현상 있다. 재현이 안 된다. 30분에 한 번씩 랜덤하게 리셋된다. 로그 없다. 리셋되면 다 날아간다. Watchdog은 아니다. 꺼놨다. Hard Fault도 아니다. 핸들러 안 탄다. 전원 문제 의심했다. 오실로스코프 켜고 3.3V 라인 모니터링. 트리거를 하강 엣지로 설정. 3.0V 이하 떨어지면 캡처. 보드 돌린다. 한참 기다린다. 20분 지났다. 파형 떴다. 3.3V에서 2.8V까지 순간 떨어졌다가 복구. 100µs 정도. 리셋 전압이 2.9V. 그래서 리셋된 거다. 원인 찾아야 한다. 전원 노이즈다. BLE 송신할 때 순간 전류 증가. 20mA에서 80mA로 튄다. Decoupling Cap이 부족하다. 회로도 본다. 3.3V 라인에 10µF 1개. 부족하다. 100nF 세라믹 캐패시터 추가해야 한다. MCU 가까이. 민수 선배한테 말했다. "3.3V 라인에 100nF 추가 필요해요. BLE TX 때 노이즈 있어요." "파형 봤어요?" "네. 캡처했어요. 2.8V까지 떨어져요." "알았어요. 다음 리비전에 넣을게요." 리비전 기다릴 수 없다. 납땜했다. 100nF 캐패시터 MCU 바로 옆 GND-3.3V 사이. 플럭스 닦고 테스트. 2시간 돌렸다. 리셋 없다. 파형도 안정적이다. 프로브로 문제 찾고, 프로브로 해결 확인했다. 없었으면 못 찾았다. 퇴근 못 했다. 새벽 2시. 그래도 찾아서 후련하다. 이제 손의 연장이다 5년 하다 보니 프로브가 자연스럽다. 마우스처럼. 키보드처럼. 보드 보면 어디에 프로브 댈지 안다. 클럭 신호, 데이터 라인, 인터럽트 핀. 파형 보면 뭐가 문제인지 느낌 온다. 신입 때는 몰랐다. 파형 봐도 뭔지 모르겠고, 뭘 측정해야 할지 몰랐다. 선배들이 "여기 프로브 대봐" 하면 시키는 대로 했다. 지금은 안다. 타이밍 문제면 여러 채널 동시에 본다. 전원 문제면 AC 커플링 켜고 노이즈 본다. 프로토콜 문제면 디코더 쓴다. 후배한테도 가르쳐준다. "printf만 믿지 마. 파형 봐야 돼." "네, 선배님. 근데 어디를 봐야 하나요?" "일단 의심되는 신호부터. 클럭이면 클럭, 데이터면 데이터. 동시에 여러 개 보면 상관관계 보여." 후배가 프로브 들고 있는 모습 보면 신입 때 내 모습 같다. 어색하게 들고, 접지 클립 어디 물릴지 헤맨다. 시간 지나면 익숙해진다. 프로브는 도구다. 하지만 단순한 도구 아니다. 보이지 않는 세계를 보여주는 창이다. 전자 신호의 세계. µs 단위의 세계. Low 레벨의 진실. 책상 서랍에 프로브 5개 있다. 10배 2개, 1배 2개, 고주파용 1개. 다 쓴다. 프로젝트마다 필요한 게 다르다. 회사 장비실에 예전에 쓰던 아날로그 오실로스코프 있다. Tektronix 옛날 모델. 지금은 안 쓴다. 디지털이 편하다. 캡처하고, 저장하고, USB로 빼고. 그래도 가끔 아날로그 화면 보면 감성 있다. 장비는 계속 좋아진다. 예산 싸워서 이번에 200MHz 모델 신청했다. 승인 나면 4채널, 디지털 16채널 Logic Analyzer 포함. 기대된다.프로브 쥐면 일할 준비 됐다는 느낌. 이제 내 손의 일부다.

중국산 칩의 데이터시트가 중국어다

중국산 칩의 데이터시트가 중국어다

중국어 데이터시트와의 전쟁시작은 가격이었다 회의실. 구매팀 과장이 엑셀을 띄웠다. "STM32는 개당 8달러. 중국 칩은 2달러." 양산 수량 10만 개. 계산기 두드릴 필요도 없다. 6억 차이. "기능은 비슷합니까?" 하드웨어 팀장이 물었다. "거의 동일합니다. Cortex-M4 코어에 플래시 256KB..." 나는 듣고 있었다. 불안했다. "데이터시트 봤어요?" 내가 물었다. "있습니다. PDF로." "한글 버전요?" "중국어인데, 번역하면 되죠." 회의는 그렇게 끝났다. 결정됐다. 중국 칩. 이름도 처음 듣는 회사. 퇴근길에 검색했다. 자료가 없었다. 영문 리뷰도 없었다. 다음 날 샘플이 도착했다. 데이터시트 PDF도 함께. 820페이지. 전부 중국어. 번역기는 거짓말을 한다구글 번역에 넣었다. 1분 걸렸다. 결과물을 열었다. "GPIO configuration method: please configure pin function enable switch after power on sequence completes successfully." 무슨 소리지. 원문을 다시 봤다. 한자가 빽빽했다. 파파고도 돌렸다. 비슷했다. "핀 기능 활성화 스위치를 전원 순서 완료 후 설정하십시오." 핀 기능? 활성화 스위치? 전원 순서? 레지스터 맵을 봤다.地址 名称 说明0x4000 0000 GPIO_CFG 配置寄存器번역기: "Configuration register" 그래. 그건 알겠어. 비트 필드를 봤다.位 名称 说明[7:0] PIN_FUNC 引脚功能选择번역기: "Pin function selection" 도움이 안 됐다. 어떻게 선택하는데? 값은? 0x00이 뭐고 0x01이 뭔데? 표를 찾았다. 40페이지 뒤에 있었다.值 功能0x00 通用输入输出0x01 复用功能10x02 复用功能2번역기: "General input output", "Multiplexing function 1", "Multiplexing function 2" 복용 기능이 뭔데. 다시 검색했다. "alternate function"이었다. 하나하나 이랬다. 820페이지를. 예제 코드는 없다 보통 데이터시트 뒤에는 예제가 있다. STM32는 HAL 라이브러리에 수백 개 예제가 있다. 중국 칩은? SDK 다운로드 링크가 있었다. 중국 사이트. 회원가입 필요. 휴대폰 인증은 중국 번호만. 막혔다. 하드웨어 팀한테 물었다. "칩 회사 연락처 있어요?" "있긴 한데, 이메일 답장이 늦어요." 메일 보냈다. 영어로. "Could you provide SDK download link?" 3일 뒤 답장 왔다. 중국어. 번역기 돌렸다. "请访问我们的官方网站下载。" 공식 사이트 방문하래. 거기가 안 되는데. 다시 메일 보냈다. "Cannot access from Korea." 5일 뒤 답장. 중국어. "请使用VPN。" VPN 쓰래. 회사에서 VPN? 보안팀 허락받아야 한다. 보안팀: "업무용인가요?" "네." "중국 사이트요?" "칩 제조사 공식 사이트입니다." "검토 후 회신 드리겠습니다." 2주 걸렸다. 승인 안 났다. 결국 집에서 내 노트북으로 받았다. 규정 위반인지 모르겠다. SDK 압축 풀었다. README.txt 열었다. 중국어. 예제 폴더 열었다. 주석이 전부 중국어. // 初始化GPIO void gpio_init(void) { // 使能时钟 RCC->APB2ENR |= (1 << 3); // 配置为推挽输出 GPIOB->CRL &= ~(0x0F << 0); GPIOB->CRL |= (0x03 << 0); }"使能时钟". 번역기: "Enable clock". 이건 알겠다. "推挽输出". 번역기: "Push pull output". 이것도 알겠다. 하지만 레지스터 값이 맞는지는 모른다. 데이터시트랑 대조해야 한다. 중국어 데이터시트랑. 커뮤니티는 더 없다STM32 쓸 때는 몰랐다. 구글에 검색하면 답이 나온다는 걸. 스택오버플로우에 물어보면 30분 안에 답글 달린다는 걸. 중국 칩은? 검색해도 안 나온다. 영문 포럼 없다. 깃허브 이슈도 없다. 중국 포럼은 있다. CSDN이라는 사이트. 전부 중국어다. 회원가입했다. 질문 올렸다. 영어로. "How to configure UART interrupt priority?" 답글 없었다. 일주일 지나도. 중국어로 번역해서 다시 올렸다. "如何配置UART中断优先级?" 구글 번역 돌린 문장이라 자연스럽지 않았을 거다. 답글 하나 달렸다. 중국어. "请参考手册第245页。" 번역: "Please refer to page 245 of the manual." 245페이지 봤다. 이미 봤던 페이지. 도움 안 됐다. 다시 답글 달았다. "Still not clear." 답 없었다. 유튜브 찾아봤다. 튜토리얼 영상 있을까. 없었다. 중국 사이트 Bilibili에는 있었다. 중국어 음성. 자막 없음. 영상 보면서 따라했다. 레지스터 값 적었다. 주석 없이 코드만 따라 치는 느낌. 왜 이 값인지 모른다. 그냥 작동하니까. 실전은 더 지옥이다 타이머 인터럽트 설정하는데 3일 걸렸다. 데이터시트에는 이렇게 써 있었다. "定时器中断使能位于NVIC_ISER寄存器。" 번역: "Timer interrupt enable is located in NVIC_ISER register." NVIC_ISER는 ARM 표준이다. 이건 안다. 근데 인터럽트 번호가 뭔지 안 나와 있다. 표를 찾았다. 60페이지 뒤에.中断源 中断号TIM1 25TIM2 26TIM1이 25번. 코드 짰다. NVIC->ISER[0] |= (1 << 25);안 됐다. 인터럽트 안 들어온다. 하루 디버깅했다. 오실로스코프로 핀 확인. 파형 나온다. 인터럽트만 안 된다. 데이터시트 다시 봤다. 200페이지 더 읽었다. 작은 주석 발견. "注意:使用前需配置中断向量表偏移。" 번역: "Note: Need to configure interrupt vector table offset before use." 뭐? 벡터 테이블 오프셋? STM32는 자동인데. 코드 추가했다. SCB->VTOR = 0x08000000;됐다. 인터럽트 들어왔다. 3일 걸린 이유: 데이터시트 한 줄. 200페이지 뒤에 숨어 있음. 중국어로. 하드웨어 버그인가 펌웨어 버그인가 ADC 값이 이상했다. 3.3V 인가했는데 읽히는 값이 2.8V. 하드웨어팀: "회로는 문제없어요." 나: "그럼 캘리브레이션 문제?" 데이터시트 찾아봤다. ADC 챕터. 80페이지. 캘리브레이션 절차 있었다. 중국어. 번역기 돌렸다. "校准步骤:设置校准位 等待校准完成 读取校准值""Calibration steps:Set calibration bit Wait for calibration to complete Read calibration value"괜찮네. 따라했다. ADC->CR2 |= ADC_CR2_CAL; while(ADC->CR2 & ADC_CR2_CAL); uint16_t cal_val = ADC->DR;안 됐다. 여전히 2.8V. 에러타 찾았다. Errata sheet. 칩 버그 리스트. 중국어. 당연히. 번역 돌렸다. 30개 버그 중 하나 발견. "ADC校准值需乘以系数1.18。" "ADC calibration value needs to be multiplied by factor 1.18." 뭐? 곱하기 1.18? 왜? 이유 안 나와 있다. 일단 했다. float voltage = adc_value * 3.3 / 4096 * 1.18;됐다. 3.3V 나왔다. 이유는 모른다. 지금도. 양산이 다가온다 개발 3개월 걸렸다. STM32였으면 한 달. 디버깅 시간의 70%는 데이터시트 읽는 시간. 중국어 해석하고, 번역 의심하고, 예제 찾고, 안 되고. 팀장: "일정 괜찮아?" 나: "빡셉니다." 팀장: "다음엔 검증된 칩 쓰자." 나: "네." 다음에도 안 그럴 거다. 가격 때문에. 이제 양산 준비. 펌웨어 동작은 확인됐다. 근데 불안하다. 내가 놓친 중국어 주석이 있을까. 200페이지 뒤에 숨어있는 "注意" 또 있을까. 그냥 운에 맡긴다. 밤새 데이터시트 다시 읽는다. 중국어. 820페이지. 번역기는 여전히 거짓말한다. "请确保正确配置。" 번역: "Please ensure correct configuration." 뭘 어떻게 확인하라는 건데. 모른다. 내일 또 읽는다.6억 아끼려다 3개월 날렸다. 그래도 양산은 나간다.