Showing Posts From

야근

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보다 더 까다롭다고 들었다. 미리 두통약 챙겨야겠다.

STM32 레지스터, 이 1비트 때문에 3시간이 걸렸다

STM32 레지스터, 이 1비트 때문에 3시간이 걸렸다

STM32 레지스터, 이 1비트 때문에 3시간이 걸렸다 오전 10시, 시작은 평범했다 출근했다. 커피 마시고 보드 켰다. 어제 작성한 UART 코드 확인할 차례. STM32F4. 익숙한 칩이다. 이번 프로젝트에서 벌써 세 번째 보드. UART2로 센서 데이터 받아서 USB로 PC에 전송하는 펌웨어. 간단해 보였다. 코드 작성은 30분. HAL 라이브러리 쓰면 금방이다. 빌드, 업로드. 정상. 그런데 데이터가 안 온다.터미널 열었다. 아무것도 없다. 센서는 분명 데이터 보내고 있다. 오실로스코ープ로 확인했으니까. '설정 문제겠지.' 보레이트 확인. 115200. 맞다. 패리티 없음. 스톱비트 1. 다 맞다. HAL_UART_Receive() 호출. 리턴값은 HAL_OK. 근데 버퍼는 비어있다. 이상하다. 오전 11시, 기본부터 다시 문제를 단순화했다. 센서 연결 끊고 UART 루프백 테스트. TX와 RX 핀 쇼트. 자기가 보낸 거 자기가 받는 거다. 이것도 안 되면 설정 문제 확실. 코드 수정했다. uint8_t tx_data = 0x55; uint8_t rx_data = 0x00;HAL_UART_Transmit(&huart2, &tx_data, 1, 100); HAL_UART_Receive(&huart2, &rx_data, 1, 100);실행. rx_data는 여전히 0x00. 'HAL 문제인가?' 레지스터 직접 봤다. USART2->SR 레지스터. Status Register. TXE(Transmit Data Register Empty) 비트는 1. 전송 완료됐다는 뜻. RXNE(Read Data Register Not Empty) 비트는 0. 데이터 안 받았다는 뜻. 분명 TX에서 나간 신호가 RX로 들어와야 하는데. 오실로스코프 다시 꺼냈다.파형 확인. TX 핀에서 정상적으로 신호 나온다. 0x55. 10101010. 깔끔하다. RX 핀도 똑같다. 물리적으로는 신호 들어온다. 그럼 문제는 소프트웨어다. 정오, 스펙 문서 지옥 Reference Manual 펼쳤다. RM0090. 1700페이지. USART 챕터. 30장. 60페이지 분량. 레지스터 맵 표 봤다. USART_CR1, CR2, CR3. Control Register. 하나씩 확인. CR1:UE(USART Enable): 1. 맞다. TE(Transmitter Enable): 1. 맞다. RE(Receiver Enable): 1. 맞다.CR2:STOP bits: 00. 1 스톱비트. 맞다.CR3:다 0. 기본값.'설정은 다 맞는데?' HAL 코드 뜯어봤다. HAL_UART_Init() 함수. 뭐 하는지 하나씩. 클럭 활성화. GPIO 설정. USART 레지스터 초기화. 순서대로. 이상 없어 보인다. 점심시간. 편의점 김밥 사 왔다. 먹으면서도 생각했다. '뭘 놓쳤지?' 오후 2시, 다른 접근 HAL 버리고 레지스터 직접 제어하기로 했다. 새 프로젝트 만들었다. HAL 없이. 레지스터만. // 클럭 활성화 RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; RCC->APB1ENR |= RCC_APB1ENR_USART2EN;// GPIO 설정 (PA2: TX, PA3: RX) GPIOA->MODER |= (2 << 4) | (2 << 6); // Alternate function GPIOA->AFR[0] |= (7 << 8) | (7 << 12); // AF7 (USART2)// USART 설정 USART2->BRR = 0x16D; // 115200 baud (16MHz / 115200) USART2->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;빌드. 업로드. 똑같다. 전송은 되는데 수신은 안 된다.'GPIO 문제인가?' PA3 핀 설정 다시 봤다. Alternate Function 맞다. AF7 맞다. '풀업? 풀다운?' GPIOA->PUPDR &= ~(3 << 6); // No pull원래 설정 안 했었다. 기본값이 No pull이니까. 혹시나 해서 풀업으로 바꿔봤다. GPIOA->PUPDR |= (1 << 6); // Pull-up실행. 여전히 안 된다. 오후 3시, 커뮤니티 검색 구글 켰다. "STM32 UART not receiving". Stack Overflow. 비슷한 질문 수십 개.보레이트 안 맞음 → 내 경우는 아님 클럭 설정 틀림 → 확인했음 GPIO AF 잘못 설정 → 맞음 인터럽트 충돌 → 폴링으로 하는데도움 안 됐다. STM32 공식 포럼. 2018년 글 하나. "UART RX not working on PA3, but works on PA10." '핀을 바꿔?' 우리 보드는 PA3만 라우팅되어 있다. PA10은 다른 용도. 댓글 읽었다. "Check JTAG pin conflict." JTAG? 데이터시트 봤다. PA3는... 그냥 GPIO다. JTAG 아니다. 막혔다. 오후 4시, 실험 시작 논리적으로 안 풀리면 실험이다. 모든 레지스터 값 프린트했다. printf("CR1: 0x%08X\n", USART2->CR1); printf("CR2: 0x%08X\n", USART2->CR2); printf("CR3: 0x%08X\n", USART2->CR3); printf("SR: 0x%08X\n", USART2->SR); printf("BRR: 0x%08X\n", USART2->BRR);출력: CR1: 0x0000200C CR2: 0x00000000 CR3: 0x00000000 SR: 0x000000C0 BRR: 0x0000016DReference Manual과 비교. CR1: 0x0000200CBit 13 (UE): 1 Bit 3 (TE): 1 Bit 2 (RE): 1맞다. SR: 0x000000C0Bit 7 (TXE): 1 Bit 6 (TC): 1전송은 완료됐다. 근데 RXNE는 0. '레지스터 값은 정상인데 왜 안 되는 거야?' 그때 생각났다. CR1 말고 CR3. CR3: 0x00000000. 스펙 읽었다. CR3 비트들.Bit 10 (CTSIE): CTS interrupt Bit 9 (CTSE): CTS enable Bit 8 (RTSE): RTS enable ...다 0. 우리는 하드웨어 플로우 컨트롤 안 쓴다. 근데 한 줄이 눈에 들어왔다. "Bit 0 (EIE): Error interrupt enable" '에러 인터럽트?' 관련 없어 보였다. 우리는 폴링 방식이니까. 그래도 혹시나 싶어서 켜봤다. USART2->CR3 |= USART_CR3_EIE;빌드. 업로드. 똑같다. 오후 5시, 1비트의 비밀 CR1 다시 봤다. 한 비트씩. Bit 15~14: OVER8. 오버샘플링. Reference Manual: "0: oversampling by 16 (default)" "1: oversampling by 8" 우리는 0. 기본값. Bit 13: UE. USART Enable. 1. Bit 12: M. Word length. "0: 1 Start bit, 8 Data bits"맞다.Bit 11: WAKE. 웨이크업 방식. 관련 없다. Bit 10: PCE. Parity Control Enable. 0. 패리티 없음. Bit 9: PS. Parity Selection. 관련 없다. Bit 8: PEIE. PE interrupt enable. '잠깐.' PEIE. Parity Error Interrupt Enable. "This bit is set and cleared by software." 설정은 소프트웨어가 한다. 맞다. "Default value: 0" 기본값 0. 근데 주석 하나 더 있었다. "Note: This bit should be cleared before USART is enabled (UE=1)." 'UE 켜기 전에 클리어?' 기본값이 0인데 왜 클리어? 그 순간 깨달았다. '기본값이 0이 아닐 수도 있다.' 리셋 후 레지스터 초기값. 스펙에는 0이라고 나와 있다. 근데 실제로는? 코드 수정했다. UE 켜기 전에 명시적으로 클리어. USART2->CR1 &= ~USART_CR1_PEIE; // 명시적 클리어 USART2->CR1 |= USART_CR1_UE; // 그 다음 Enable빌드. 업로드. 터미널 열었다. 데이터 들어온다. "..." 3시간. 오후 5시 30분, 원인 분석 왜 PEIE 비트가 문제였을까? 추측: 전에 다른 펌웨어 테스트하다가 비트가 1로 설정됐다. 리셋했지만 일부 레지스터는 유지됐다. 아니면: 보드 전원 문제. 완전히 방전되지 않으면 일부 값이 남아있다. 확인 방법: 보드 전원 완전히 끄고 30초 기다린 후 다시 켜기. 해봤다. 그 상태에서는 원래 코드도 작동했다. '역시.' 스펙 문서는 "리셋 후 기본값"을 명시한다. 근데 "전원 차단 후"는 보장 안 한다. 개발 중에는 전원 계속 켜놓고 리셋만 누른다. 그 상태에서 레지스터 일부가 유지될 수 있다. 교훈: 레지스터 설정할 때 "기본값이니까 안 써도 되겠지" 하지 말 것. 명시적으로 써라. 모든 비트를. // 나쁜 예 USART2->CR1 = USART_CR1_UE; // 다른 비트는 기본값이겠지?// 좋은 예 USART2->CR1 = 0; // 일단 초기화 USART2->CR1 |= USART_CR1_TE | USART_CR1_RE | USART_CR1_UE; // 필요한 것만오후 6시, 코드 정리 전체 코드 리팩토링했다. 모든 레지스터 초기화 코드에 명시적 클리어 추가. void UART_Init(void) { // 클럭 RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; RCC->APB1ENR |= RCC_APB1ENR_USART2EN; // GPIO 완전 초기화 GPIOA->MODER &= ~((3 << 4) | (3 << 6)); GPIOA->MODER |= (2 << 4) | (2 << 6); GPIOA->AFR[0] &= ~((0xF << 8) | (0xF << 12)); GPIOA->AFR[0] |= (7 << 8) | (7 << 12); // USART 완전 초기화 USART2->CR1 = 0; // 모든 비트 클리어 USART2->CR2 = 0; USART2->CR3 = 0; USART2->BRR = 0x16D; USART2->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE; }테스트. 전원 켰다 껐다 반복. 10번. 매번 정상 작동. 이제 됐다. 오후 7시, 회고 3시간이 걸렸다. 1비트 때문에. 스펙 문서에는 "기본값: 0"이라고 나와 있었다. 근데 실제로는 아니었다. 임베디드는 이런 거다. 가정하면 안 된다. 모든 걸 명시해야 한다. "당연히 0이겠지" → 3시간 날림 "명시적으로 0을 쓰자" → 10초 추가, 3시간 절약 다음부터는:레지스터 쓸 때 전체를 초기화 필요한 비트만 OR로 설정 기본값 믿지 말기HAL 라이브러리도 완벽하지 않다. 내부 구현 확인해야 한다. 저수준은 이런 맛이다. 웹 개발자들은 모를 거다. 변수 초기화 안 해도 undefined일 뿐. 프로그램은 돌아간다. 우리는 다르다. 1비트가 전체를 멈춘다. 그래도 찾아냈을 때의 쾌감. 이것 때문에 한다.내일은 SPI다. 또 무슨 일이 있을까.

1000페이지 영어 스펙 문서, 번역은 구글 번역기

1000페이지 영어 스펙 문서, 번역은 구글 번역기

1000페이지 영어 스펙 문서, 번역은 구글 번역기 아침 9시, 스펙 문서와의 싸움이 시작된다 출근한다. 피곤하다. 어제 테스트는 또 실패했다. 화면을 켠다. 메일함에 스펙 문서 링크가 떠 있다. Reference Manual. 1047페이지. PDF 파일명에는 'v2.3_EN'이라고 적혀 있다. 한국어 버전? 없다. 당연하지, 있을 리가. 팀장이 어제 말했다. "이 칩셋 쓰려고 하니까 이거 한 번 읽어봐. 핵심만." 핵심만. 1047페이지 중 핵심이 뭔데. 일단 열어본다. Chapter 1. Overview. Chapter 2. Pin Configuration. Chapter 3. Electrical Characteristics. 여기까진 괜찮다. 그림도 있고 테이블도 있고. 그런데 Chapter 5. Register Description부터 지옥이 시작된다. "Register 0x24: CTRL_REG_A (Control Register A)" 좋다. "Bit 7-5: Reserved. The bit must be set to 0." 확인했다. "Bit 4: FS1. Full-scale selection. Setting this bit will enable the full-scale mode operation as specified in the electrical characteristics section." 음. 여기부터 애매한데. 번역기를 켠다. "비트 4: FS1. 풀 스케일 선택. 이 비트를 설정하면 전기 특성 섹션에 지정된 대로 풀 스케일 모드 작동을 활성화합니다." 뭔 소린데. "Full-scale mode operation as specified in the electrical characteristics section." 여기서 'as specified'가 뭔지 알려면 electrical characteristics를 다시 봐야 한다. 그걸 번역기로 돌리면 또 다른 용어가 나온다. 그 용어를 다시 찾아봐야 한다. 악순환. 10시, 일단 구글 번역기를 믿어본다 신입 때는 달랐다. 신입 때는 영어를 열심히 공부했다. 회사에서 영어 교육도 해줬고. 비즈니스 영어. Technical English. 근데 배운 게 다였다. '설정하다' '활성화하다' 이런 건 번역기도 한다. 문제는 칩 제조사 엔지니어들의 영어다. 그들은 데이터시트를 쓸 때 최대한 간결하게 쓴다. 한 문장에 정보를 때려 넣는다. 중괄호도 많고, 약자도 많고, 암묵적인 가정도 많다. "The register must be read back after programming to ensure proper synchronization." 번역기: "프로그래밍 후 레지스터를 읽어 적절한 동기화를 보장해야 합니다." 그런데 이게 뭔 뜻인가. '레지스터를 읽어'는 뭐다. SPI로 읽어야 한다는 건가. 아니면 메모리 주소로 읽어야 한다는 건가. '동기화'는 뭔가. 칩 내부의 state machine과 host processor 사이의 동기화인가. 아니면 내부 clock과의 동기화인가. 영어 원문을 다시 읽어도 애매하다. 그럼 이제 뭘 한다. Slack에 올린다. "이거 뭔 뜻일까요?" 팀원 이준호가 답한다. 20분 뒤에. "아 이건 한 번 쓰고 나서 SPI로 읽어서 값이 제대로 들어갔는지 확인하라는 뜻 같은데요. 옆 회사 때 본 데이터시트도 이 칩이었는데 그렇게 했어요." '옆 회사 때 본 데이터시트'. 이게 임베디드 개발자의 성장이다. 데이터시트를 읽을 때 영어 능력이 아니라 경험치를 쓴다. 예전에 본 칩 중에 비슷한 게 있었나. 그렇다면 그건 뭐였나. 그리고 그건 왜 그렇게 동작했나. 구글 번역기의 한계다. 11시 30분, 번역기를 버린다 일단 정해진 시간이 있다. 점심 12시. 그 전에 끝낼 게 있는 데 아직 Register Description 20%밖에 안 봤다. 빠르게 스캔한다. Bit 3, Bit 2, Bit 1, Bit 0까지 모두 같은 방식이다. 각 비트마다 "Setting this bit will..."이 반복된다. 이제 번역기를 켜지 않는다. 그냥 원문을 읽는다. 한국어가 없다면, 영어를 바로 이해하는 수밖에 없다. 이건 새로운 기술이 아니다. 적응이다. 3년 차 때부터 시작했다. Reference Manual을 펼칠 때 한국어 번역을 찾지 않는다. 원문에서 바로 구조를 찾는다. Bit [7:5]라고 쓰여 있으면 7번 비트부터 5번 비트까지라는 건 번역이 필요 없다. R/W라고 쓰여 있으면 Read/Write다. 한국어 대사전보다는 기술 용어 사전이 필요해진다. 예: "LSB First"는 "Least Significant Bit First"다. 번역하면 '최하위 비트 우선'. 근데 이건 번역하는 순간 더 복잡해진다. 그냥 LSB라고 부른다. 같은 팀의 송미영 개발자는 어제 말했다. "한국어 번역본을 기다리기보다 영어 원문에 익숙해지는 게 빠르더라고요. 처음엔 힘들지만." 그 말이 맞다.12시 30분, 한국 자료를 찾아본 지 6개월이 지났다 점심을 먹으며 유튜브를 본다. 'STM32 Reference Manual 한국어' 검색 결과는 없다. 대신 '우리 회사 선배가 쓴 코드 예제'라는 수동적 학습 방법에 의존한다. 하드드라이브에 있는 폴더: /Legacy_Project/2019_Smart_Lock/firmware/src 여기엔 내가 입사하기 전 개발된 코드다. // Written by Park_JH (2019-08-14) // Reference: STM32L152 Reference Manual // Page 287: RTC_CR Register Description uint32_t rtc_init(void) { // Enable PWR clock RCC->APB1ENR |= RCC_APB1ENR_PWREN; PWR->CR |= PWR_CR_DBP; // This is critical: must read back after write // See page 312 of RM uint32_t dummy = PWR->CR; (void)dummy; return 0; }주석이 있다. "Page 312 of RM" 이 사람은 이 코드를 쓸 때 Reference Manual 312페이지를 봤다는 뜻이다. 6년 전에. 그리고 지금 그 페이지가 어디 있는지 몰라도, 이 코드를 복사+붙여넣기 하면 작동한다. 한국어 자료가 없는 대신 '선배 개발자의 코드'가 한국어 자료다. 이게 임베디드 회사의 생태계다. 문서화는 없다. 대신 '이미 작동하는 코드'가 있다. 14시, 구글 번역기의 두 번째 쓸모 그렇다고 구글 번역기가 완전히 쓸모없는 건 아니다. 쓸모 있게 사용하는 방법이 있다. 1단계: 원문을 3번 읽는다 "The FIFO buffer can be configured to generate an interrupt when the data count exceeds the programmable threshold level specified in the FIFO_THR register." 1차 읽음. 뭔 소린지 모름. 다시 읽음. FIFO. 버퍼. 임계값. 뭔가 관련이 있는 것 같다. 다시 읽음. 아, FIFO가 어느 정도 찼을 때 인터럽트를 날린다는 뜻인가? 2단계: 그 다음에 번역기를 킨다 번역기: "FIFO 버퍼는 데이터 개수가 FIFO_THR 레지스터에 지정된 프로그래밍 가능한 임계값을 초과할 때 인터럽트를 생성하도록 구성할 수 있습니다." 내 이해: FIFO가 FIFO_THR 이상으로 차면 인터럽트 발생. 이 값은 프로그래밍으로 설정 가능. 3단계: 코드로 검증한다 // FIFO threshold = 16 bytes CHIP->FIFO_THR = 16;// Enable FIFO interrupt CHIP->INTR_ENABLE |= INTR_FIFO_FULL;작동했다. 내 이해가 맞았다. 이게 올바른 방법이다. 번역기에만 의존하면 틀린다. 원문을 이해해야 번역기를 제대로 쓸 수 있다. 아이러니다. 15시, 중국산 칩의 악몽 어제 하드웨어 팀에서 메시지가 왔다. "이번에 BOM cost 깎으려고 중국산 센서 쓰기로 결정했습니다. 사용 가능한지 펌웨어로 확인해주세요." 첨부 파일: XC1234_Datasheet_ZH_V1.2.pdf 확장자는 PDF인데 전부 중국어다. 구글 번역기를 켜본다. 중국어 → 영어: "50% 확률로 맞음. 50% 확률로 뭔 소린지 몰라." 중국어 → 한국어: "70% 확률로 틀림. 문법이 산산조각." 그럼 이제 뭘 한다. 사진을 찍어서 온라인 이미지 번역기에 올린다. "寄存器 0x12: 控制寄存器" 이미지 번역기: "Register 0x12: Control Register" 그리고 영어 번역기로 다시 돌린다. "Register 0x12: Control Register" 번역기: "레지스터 0x12: 제어 레지스터" 원점이다. 팀장에게 메일을 보낸다. "중국산 센서 사용이 가능하지만, 데이터시트가 중국어만 있어서 약 2주 정도 더 필요할 것 같습니다." 회신: "알겠습니다. 그래도 빨리 부탁합니다. BOM cost가 30% 줄어듭니다." 30%. 2주 vs 30% cost reduction. 회사가 뭘 선택할지는 뻔하다.16시, 그래도 살아가는 방법 Slack에서 임베디드 커뮤니티 링크를 찾는다. Reddit의 r/embedded EEVblog의 Electronics Design Forum STM32 공식 포럼 이 곳들엔 같은 고민을 하는 사람들이 있다. "Has anyone used the XC1234 sensor? The datasheet is only in Chinese..." 21분 뒤에 답이 온다. "Yeah, that's the old model. Use the XC1234A instead. English datasheet available on their official site." 구글 번역기보다 빠르다. 이제 이게 내 방법이다.영어 원문 읽기 구글 번역기로 검증 해석이 안 되면 온라인 커뮤니티에 물어보기 코드로 테스트하기 안 되면 오실로스코프로 파형 확인이 과정은 길다. 어떨 때는 하나의 레지스터 설정이 3시간이 걸린다. 그런데 이제 익숙하다. 펌웨어 개발자는 본래 이렇게 산다. 혼자 영어 문서와 싸우면서. 아무도 도와주지 않는다. 마케팅 팀은 한국어로 된 기획안을 준다. 하드웨어 팀은 한국어로 설명한다. 펌웨어 팀은 영어로 된 데이터시트를 받는다. 그리고 혼자 이해한다. 17시, 결론 대신 내일 계획 오늘도 Register Description 40%까지만 봤다. 내일 또 이어서 본다. 내일도 구글 번역기를 키고 닫는다. 계속해서 원문을 읽는다. 그리고 6개월 뒤쯤이면 이 칩의 모든 레지스터를 외우지 않아도 직관적으로 이해할 것이다. 이게 경험이다. 한국 대학교 임베디드 강의에선 안 배운다. 교재는 모두 한국어고, 한국 교수도 영어 문서를 피한다. 그래서 졸업생은 회사 와서 깜짝 놀란다. "아, 우리 이 칩 쓰는데 이 영어 문서 한 번 보고 시작해." 그 순간부터가 진짜 펌웨어 공부다. 불행인가. 그냥 일이다.내일도 출근해서 1000페이지를 펼칠 것이다. 구글 번역기는 여전히 50%만 맞출 것이다. 근데 괜찮다. 나머지 50%는 경험과 코드와 오실로스코프로 채운다.