Showing Posts From
야근
- 27 Dec, 2025
새로운 칩이 도입됐다, 레지스터맵은 완전히 다르다
새로운 칩이 도입됐다 월요일 아침 출근했다. 팀장이 부른다. "이번 프로젝트, ESP32로 간다." STM32 쓰던 프로젝트였다. 5개월 작업했다. 이제 버린다. "비용 문제래. ESP32가 싸." 알겠다고 했다. 속으로는 욕했다.책상으로 돌아왔다. ESP32 데이터시트를 받았다. PDF 648페이지다. STM32 레퍼런스 매뉴얼은 1200페이지였다. 이게 짧아 보인다. 익숙해진 거다. 첫 페이지를 열었다. 익숙한 단어들이다. GPIO, SPI, I2C, UART. 같은 거 아닌가? 아니었다. 레지스터맵의 배신 STM32는 이랬다. GPIOA->MODER = 0x28000000; GPIOA->ODR |= (1 << 5);간단했다. 모드 설정하고 비트 켜면 끝. ESP32는 달랐다. gpio_config_t io_conf = {}; io_conf.pin_bit_mask = (1ULL << GPIO_NUM_5); io_conf.mode = GPIO_MODE_OUTPUT; gpio_config(&io_conf);구조체다. 함수다. HAL도 아니고 ESP-IDF다. "왜 이렇게 만들었지?" 물어봐도 답 없다. 그냥 이렇게 쓰라는 거다.레지스터 직접 접근도 가능하다. 하지만 권장하지 않는다. GPIO.out_w1ts = (1 << 5); // 이렇게 쓸 수 있긴 하다근데 ESP-IDF 업데이트되면 바뀔 수 있다고 한다. 그래서 다들 SDK 쓴다. STM32는 레지스터가 정답이었다. HAL은 너무 무거웠다. ESP32는 SDK가 정답이다. 레지스터는 문서화가 부족하다. 같은 펌웨어인데 철학이 다르다. 클록 트리의 지옥 STM32 클록 설정은 악명 높다. PLL 설정, AHB 프리스케일러, APB1, APB2... CubeMX 없으면 못 한다. 하지만 익숙했다. 5년 동안 수십 번 했다. ESP32는 더 간단하다. 대신 더 이상하다. esp_clk_cpu_freq(); // 현재 CPU 클록그냥 함수 부르면 나온다. 160MHz다. "240MHz로 올리려면?" menuconfig에서 설정한다. 코드가 아니다. 빌드 설정이다.STM32는 코드로 다 했다. RCC->PLLCFGR = ...; RCC->CR |= RCC_CR_PLLON; while(!(RCC->CR & RCC_CR_PLLRDY));이게 정상이었다. 부팅 시퀀스를 내가 제어한다. ESP32는 부트로더가 다 한다. 나는 main() 이후부터다. 제어권이 적다. 편한데 불안하다. 메모리 맵이 다르다 STM32 Flash는 0x08000000부터다. SRAM은 0x20000000이다. 외웠다. 디버거 없이도 메모리 덤프 보면 안다. ESP32는 다르다. Flash는 0x400xxxxx다. 근데 매핑된 거다. 실제 SPI Flash는 0x3F400000에 있다. SRAM은 여러 개다. DRAM, IRAM, RTC 메모리... uint8_t data[100];이게 어디에 할당될까? 모른다. 링커 스크립트가 정한다. STM32는 명확했다. uint8_t data[100] __attribute__((section(".ccmram")));이러면 CCM RAM에 간다. 내가 정한다. ESP32도 가능하다. 하지만 섹션 이름이 다르다. uint8_t data[100] DRAM_ATTR;DRAM_ATTR, IRAM_ATTR, RTC_DATA_ATTR... 매크로다. 의미는 같은데 방식이 다르다. 적응해야 한다. 인터럽트가 이상하다 STM32 인터럽트는 NVIC다. ARM Cortex-M 표준이다. NVIC_EnableIRQ(EXTI0_IRQn); NVIC_SetPriority(EXTI0_IRQn, 1);우선순위 숫자 낮을수록 높다. 헷갈리지만 익숙하다. ESP32는 Xtensa 코어다. ARM이 아니다. 인터럽트 컨트롤러가 다르다. esp_intr_alloc(ETS_GPIO_INTR_SOURCE, 0, gpio_isr_handler, NULL, &handle);동적 할당이다. 런타임에 인터럽트를 등록한다. STM32는 컴파일 타임이다. 벡터 테이블이 고정이다. "왜 이렇게?" 찾아봤다. Xtensa는 인터럽트 레벨이 7개다. ARM은 우선순위 기반이고. 아키텍처가 다르니 당연하다. 하지만 코드는 전부 다시 써야 한다. 타이머도 다르다 STM32 타이머는 복잡하다. TIMx 18개, 각각 모드가 다르다. PWM, 인코더, 원샷... 근데 강력하다. 하드웨어로 다 된다. ESP32 타이머는 4개다. timer_config_t config = { .divider = 80, .counter_dir = TIMER_COUNT_UP, .counter_en = TIMER_PAUSE, .alarm_en = TIMER_ALARM_EN, .auto_reload = TIMER_AUTORELOAD_EN, };또 구조체다. 하지만 단순하다. PWM은 LEDC로 한다. LED Control의 약자다. "LED 깜빡이려고 만든 건데 PWM도 된대." 문서에 그렇게 써있다. 솔직하다. STM32 고급 타이머 쓰다가 이거 쓰니까 허전하다. 하지만 동작은 한다. 그걸로 됐다. DMA의 차이 STM32 DMA는 채널이다. DMA1_Stream0->CR = ...; DMA1_Stream0->NDTR = size; DMA1_Stream0->PAR = (uint32_t)&peripheral; DMA1_Stream0->M0AR = (uint32_t)buffer;레지스터 몇 개 설정하면 끝이다. ESP32는 각 페리페럴마다 다르다. SPI는 SPI DMA가 있다. I2S는 I2S DMA가 있다. 범용 DMA가 없다. spi_bus_config_t buscfg = { .mosi_io_num = PIN_NUM_MOSI, // ... .max_transfer_sz = 4096, };설정하면 알아서 DMA 쓴다. 편하다. 하지만 제어권이 적다. DMA 채널 번호가 뭔지도 모른다. SDK가 관리한다. "나는 그냥 쓰기만 하면 돼." 맞는 말이다. 하지만 찝찝하다. 디버깅이 다르다 STM32는 SWD다. ST-Link 꽂으면 끝이다. Eclipse, Keil, IAR... 다 된다. 중단점 잡고 레지스터 보고 메모리 덤프 뜨고. ESP32는 JTAG이다. 근데 USB로 바로 된다. openocd -f board/esp32-wrover-kit-3.3v.cfgOpenOCD 돌리고 GDB 붙인다. 되긴 한다. 하지만 느리다. 그래서 다들 printf 디버깅한다. ESP_LOGI(TAG, "Value: %d", value);로그 레벨 있다. 색깔도 나온다. STM32에서 printf는 UART로 빼야 했다. 귀찮았다. ESP32는 USB-UART 칩이 보드에 있다. 그냥 된다. 편하긴 하다. 하지만 중단점 없이 디버깅하는 게 익숙하지 않다. 빌드 시스템도 다르다 STM32는 Makefile이었다. SOURCES = main.c system.c OBJECTS = $(SOURCES:.c=.o)간단했다. arm-none-eabi-gcc 부르면 끝. ESP32는 CMake다. ESP-IDF 프레임워크다. idf.py build idf.py flash idf.py monitor명령어 세 개면 된다. 편하다. 하지만 내부를 모르겠다. 빌드 옵션 바꾸려면 menuconfig 들어가야 한다. Makefile은 그냥 열어서 고치면 됐다. 제어권이 없다. 프레임워크에 맡긴다. 이게 현대적인 방식이라고 한다. 맞는 말이다. 하지만 답답하다. 코드를 다시 쓴다 결국 이렇게 됐다. 기존 STM32 코드 5000줄. 재사용 가능한 건 로직뿐이다. HAL 호출 부분은 전부 버린다. // Before (STM32) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);// After (ESP32) gpio_set_level(GPIO_NUM_5, 1);의미는 같다. 문법이 다르다. GPIO만 그런 게 아니다. SPI, I2C, UART... 전부 다시 쓴다. "비슷한 건데 왜 다시 써야 해?" 팀원이 물었다. 맞는 말이다. 하지만 어쩔 수 없다. 칩이 다르니까. 2주가 지났다 GPIO 포팅 끝났다. SPI도 거의 끝났다. I2C에서 막혔다. 타이밍이 안 맞는다. STM32는 400kHz에서 잘 됐다. ESP32는 100kHz로 낮춰도 에러다. 오실로스코프 꺼냈다. SCL 파형이 이상하다. Glitch가 보인다. "하드웨어 문제 아니에요?" HW팀에 물었다. "ESP32 레퍼런스 디자인 그대로인데요." 그럼 내 코드가 문제다. 밤을 샜다. 알고 보니 pull-up 저항 설정이 문제였다. io_conf.pull_up_en = GPIO_PULLUP_ENABLE;이거 하나 추가하니 됐다. STM32는 외부 저항 달았었다. ESP32는 내부 풀업이 있다. 알았으면 10분이다. 몰라서 8시간 걸렸다. 익숙함의 가치 STM32에서 5년 일했다. 레지스터 주소 외웠다. 에러 패턴 안다. 회로 문제인지 코드 문제인지 감으로 안다. ESP32는 처음이다. 같은 문제를 만나도 시간이 2배 걸린다. "왜 안 되지?" 할 때가 많다. STM32였으면 바로 알았을 텐데. 익숙함은 속도다. 속도는 돈이다. "그래서 ESP32 쓰면 얼마나 아끼는데?" 팀장한테 물어봤다. "BOM 비용 칩 하나에 2달러." 1000개 만들면 200만원이다. 내 야근 수당이 그것보다 많다. 하지만 회사는 칩 값을 본다. 인건비는 안 본다. 이미 주는 월급이니까. 한 달이 지났다 이제 좀 익숙하다. ESP-IDF 문서 찾는 속도가 빨라졌다. menuconfig 구조도 알겠다. 코드 포팅 90% 끝났다. 테스트 중이다. 버그 잡는 중이다. STM32 때보다 빠르다고 할 수는 없다. 하지만 느리지도 않다. 새로운 걸 배웠다. ESP32도 나쁘지 않다. WiFi, Bluetooth가 내장이다. STM32는 외부 칩이었다. 전력 소모도 적다. Deep sleep 모드가 좋다. 장단이 있다. 근데 또 바뀔 것 같다. 다음 프로젝트는 nRF52래. Nordic 칩이다. 또 새로운 SDK다. "또요?" 물었다. "BLE가 더 좋대." 그렇다고 한다. 펌웨어는 이렇다. 칩이 바뀐다. 레지스터가 바뀐다. 매번 다시 배운다. 웹 개발자가 부럽다. API는 안 바뀐다. 우리는 하드웨어에 종속된다. 하드웨어가 바뀌면 다 바뀐다.레지스터맵이 바뀌면 코드도 바뀐다. 당연한데 매번 힘들다.
- 21 Dec, 2025
라이브 제품 버그, 하지만 재현이 안 된다
금요일 오후 4시 고객사에서 전화가 왔다. "또 멈췄어요." 이번 주에 세 번째다. 제품 돌려봤다. 정상이다. 어제도 밤새 돌렸다. 문제없었다. 로그 파일 확인했다. 깨끗하다. "몇 시에 멈췄나요?" 물었다. "오전 11시쯤이요." 대답이 왔다. 우리 랩에선 11시에도 돌았다. 24시간 내내 돌았다.팀장이 "현장 가봐야 하나" 물었다. 부산이다. KTX 타고 3시간. 가서 뭘 본다는 건지 모르겠다. 제품 들고 가서 보면 또 정상일 거다. 항상 그랬다. 재현되지 않는 버그 펌웨어 개발자의 악몽이다. 버그가 있는데 재현이 안 된다. 고쳐야 하는데 뭘 고치나. 증상을 봐야 원인을 찾지. 증상이 안 나타나면 손을 못 댄다. 웹개발자들 부럽다. 로그 서버에 다 남잖아. 스택 트레이스 보면 어디서 터졌는지 나오잖아. 우리는? UART 로그가 전부다. 그것도 고객사에 케이블 연결해달라고 부탁해야 한다. "이거 연결하면 보증 안 돼요?" 묻는다. 할 말이 없다.문제는 환경이다. 확신한다. 우리 랩이랑 현장이랑 뭔가 다른 거다. 온도? 랩은 23도로 유지된다. 현장은? 모른다. 습도? 랩은 50%. 현장은? 역시 모른다. 전원? 랩은 SMPS로 깨끗하게 들어간다. 현장은 공장 전원이다. 다른 장비들이 같이 쓴다. 노이즈 있을 거다. 전자파? 랩은 조용하다. 현장은? WiFi 공유기, 블루투스 기기, 모터, 형광등... 온갖 게 다 있다. 로그를 달라고 했다 고객사에 요청했다. "UART 로그 좀 받아주세요. 멈출 때 어떤 메시지가 나오는지 봐야 해요." "어떻게 받나요?" 답이 왔다. 설명서 보냈다. USB-UART 케이블 연결하고, 시리얼 터미널 열고, 115200 baud로 설정하고... 사흘 뒤에 연락 왔다. "케이블이 없어요." 택배로 보냈다. 일주일 뒤에 또 연락 왔다. "연결했는데 아무것도 안 나와요." 사진 보내달라고 했다. RX랑 TX를 반대로 꽂았다. 다시 설명했다. "노란 선이 RX, 하얀 선이 TX..." 이틀 뒤에 스크린샷이 왔다. 로그가 찍혀 있었다. 감동이었다.그런데 로그를 봐도 모르겠다. 마지막 줄이 "WDT Reset" 이다. Watchdog Timer가 시스템을 리셋한 거다. 뭔가 무한루프를 돌았거나, 인터럽트가 계속 들어왔거나, 태스크가 블록됐거나. 셋 중 하나다. 문제는 왜 그랬느냐다. 원인을 모른다. 코드를 다시 봤다 밤 10시. 사무실에 나 혼자 남았다. 코드를 처음부터 다시 읽었다. WiFi 연결 루틴. 문제없어 보인다. MQTT 통신 부분. 타임아웃 처리 돼 있다. 센서 읽기. 에러 체크 있다. 그런데 한 군데가 신경 쓰인다. WiFi 재연결 로직이다. while(WiFi.status() != WL_CONNECTED) { WiFi.reconnect(); vTaskDelay(100); }타임아웃이 없다. 만약 WiFi가 계속 안 잡히면? 이 루프를 못 빠져나온다. Watchdog이 걸린다. "설마 WiFi가 안 잡힐 리가." 생각했었다. 고객사는 WiFi 환경이 좋다고 했다. 공유기 바로 옆이라고. 하지만 전자레인지를 돌리면? 2.4GHz 대역에 간섭이 생긴다. WiFi가 끊길 수 있다. 재연결 시도하는데 계속 실패하면? 루프를 못 빠져나온다. "이거다." 확신은 없지만 가능성은 있다. 패치를 만들었다 타임아웃을 추가했다. 10초 동안 연결 안 되면 루프를 빠져나온다. 에러 플래그를 세운다. 다음 사이클에서 다시 시도한다. uint32_t start = xTaskGetTickCount(); while(WiFi.status() != WL_CONNECTED) { if(xTaskGetTickCount() - start > pdMS_TO_TICKS(10000)) { connection_error = true; break; } WiFi.reconnect(); vTaskDelay(100); }로그도 추가했다. "WiFi reconnection timeout" 메시지가 뜨게. 빌드했다. 테스트했다. WiFi 공유기를 껐다. 10초 후에 타임아웃 로그가 떴다. 시스템은 리셋되지 않았다. 좋다. 펌웨어를 고객사에 보냈다. "이걸로 업데이트해보세요. 혹시 전자레인지나 다른 2.4GHz 기기가 근처에 있나요?" 답장이 왔다. "옆 테이블에 전자레인지 있어요. 점심시간에 씁니다." 빙고일 수도 있다. 11시쯤 멈춘다고 했다. 점심 준비 시작하는 시간이다. 일주일을 기다렸다 업데이트 후에 문제가 안 생겼다. 월요일, 화요일, 수요일... 일주일이 지났다. "아직 안 멈췄어요." 고객사에서 연락 왔다. 목소리가 밝았다. 확신은 여전히 없다. 정말 전자레인지 때문이었는지 모른다. 다른 이유였을 수도 있다. 타이밍 이슈였거나, 메모리 문제였거나. 하지만 결과적으로 해결됐다. 그걸로 됐다. 팀장이 "잘했어" 했다. 기분은 좋지 않았다. 운이 좋았을 뿐이다. 정확한 원인을 찾은 게 아니다. 가능성 있는 부분에 방어 코드를 넣은 거다. 현장 디버깅의 어려움 펌웨어는 환경에 민감하다. 같은 코드가 랩에선 되고 현장에선 안 된다. 온도. MCU 스펙에 -40도에서 85도까지 동작한다고 써 있다. 하지만 70도 넘어가면 이상한 일이 생긴다. 클럭이 불안정해진다. EEPROM 쓰기가 실패한다. 전원. 스펙에 3.3V ±10%라고 돼 있다. 2.97V도 괜찮다는 뜻이다. 하지만 2.97V에서 WiFi 켜면 전압이 순간적으로 더 떨어진다. Brown-out이 걸린다. 리셋된다. 타이밍. 인터럽트가 100us마다 온다. 처리 시간이 95us다. 여유가 5us다. 그런데 캐시 미스가 한 번 나면? 110us 걸린다. 다음 인터럽트를 놓친다. 전자파. CE 인증 받았다. EMC 테스트 통과했다. 그런데 현장에 가면 다르다. 대형 모터가 돈다. 용접기가 작동한다. I2C 통신이 가끔 깨진다. 이런 것들은 랩에서 재현이 안 된다. 환경을 똑같이 만들 수 없다. 방어적 코딩 결국 답은 방어적 코딩이다. 모든 경우의 수를 생각한다. 타임아웃을 건다. 무한루프를 만들지 않는다. 10초 안에 응답 안 오면? 포기하고 다음으로 넘어간다. 재시도 로직을 넣는다. 한 번 실패했다고 끝이 아니다. 3번, 5번 시도한다. 그래도 안 되면 에러 플래그를 세운다. Watchdog을 건다. 시스템이 멈추면 자동으로 리셋된다. 영구적으로 죽는 것보단 낫다. 전원을 체크한다. 전압이 떨어지면 WiFi를 끈다. 센서 읽기를 줄인다. 파워를 아낀다. 로그를 남긴다. 될 수 있는 한 많이. 나중에 분석한다. 완벽한 코드는 없다. 하지만 덜 깨지는 코드는 있다. 여전히 불안하다 패치가 나간 지 한 달이 지났다. 문제 보고가 없다. 하지만 확신은 없다. 진짜 원인을 찾은 건지 모른다. 그냥 증상을 숨긴 건 아닌지. 다른 조건에서 또 터질까 봐 걱정된다. 겨울에 온도가 내려가면? 습도가 올라가면? 양산 물량이 늘어나면 더 다양한 환경에 노출된다. 지금은 100대가 돌고 있다. 다음 달에 1000대가 나간다. 통계적으로 문제가 생길 확률이 높아진다. 밤에 전화 올까 봐 무섭다. "또 멈춰요." 그 말이. 교훈이랄 것도 없다 재현 안 되는 버그는 항상 있다. 피할 수 없다. 할 수 있는 건 최선을 다하는 거다. 로그를 많이 남긴다. 타임아웃을 건다. 에러 처리를 꼼꼼히 한다. 그리고 운을 믿는다. 엔지니어가 운을 믿는다는 게 웃기지만 사실이다. 현장은 랩이 아니다. 변수가 너무 많다. 모든 걸 컨트롤할 수 없다. 다음 주에 또 전화 올까. 아닐 수도 있다. 모른다. 주말이다. 쉬어야 하는데 머릿속에서 코드가 돈다. WiFi 루프, 인터럽트 타이밍, 전원 노이즈... 그만 생각하자. 맥주나 마셔야겠다.원인을 못 찾아도 일단 돌아가면 된다. 그게 현실이다.
- 16 Dec, 2025
nRF52 BLE 연결이 끊긴다? 연결 간격을 조정해봤나
nRF52 BLE 연결이 끊긴다? 연결 간격을 조정해봤나 아침부터 시작된 악몽 출근했다. 메일함에 빨간 글씨. "양산 일정 2주 남았습니다. BLE 안정성 이슈 해결 바랍니다."시제품 20대 중 3대가 랜덤하게 연결 끊김. 재현도 안 돼. 가장 싫은 타입의 버그다. nRF52832 쓰는 IoT 센서 프로젝트. 스마트폰 앱이랑 BLE로 통신한다. 센서 데이터 1초마다 보내는 간단한 구조인데, 왜 끊기는지 모르겠다. HW팀은 "펌웨어 문제 아니냐"고 한다. 앱팀은 "안드로이드는 문제없다"고 한다. 나만 혼자 샌드백이다. 스펙 문서는 거짓말을 안 한다 일단 nRF52 SDK 문서부터 다시 읽었다. BLE Connection Parameters. 읽어도 읽어도 어렵다.Connection Interval: 7.5ms ~ 4000ms Slave Latency: 0 ~ 499 Supervision Timeout: 100ms ~ 32000ms이게 뭔 소리인지 알아? 나도 처음엔 몰랐다. Connection Interval은 통신 주기다. 짧을수록 빠르게 데이터 주고받지만 전력 소모가 크다. Slave Latency는 센서가 몇 번까지 응답 안 해도 괜찮은지. 전력 아끼려고 쓴다. Supervision Timeout은 "이 시간 동안 응답 없으면 연결 끊는다"는 뜻. 우리 코드는 이랬다. #define MIN_CONN_INTERVAL MSEC_TO_UNITS(100, UNIT_1_25_MS) #define MAX_CONN_INTERVAL MSEC_TO_UNITS(200, UNIT_1_25_MS) #define SLAVE_LATENCY 0 #define CONN_SUP_TIMEOUT MSEC_TO_UNITS(4000, UNIT_10_MS)100~200ms 간격으로 통신. Latency 0. Timeout 4초. 보기엔 문제없어 보인다. 그런데 스펙 문서 깊숙이 읽다가 발견했다. "Supervision Timeout must be larger than (1 + Slave Latency) × Connection Interval × 2" 계산해보니 우리는 딱 맞춰놨다. 여유가 없다. 200ms × 2 = 400ms < 4000ms. 수식상 문제없지만, 실전은 다르다.오실로스코프는 진실을 말한다 이론만 보면 안 된다. 실제 신호를 봐야 한다. Logic Analyzer 연결했다. nRF52 DK에 프로브 꽂고, GPIO 토글로 타이밍 찍어뒀다. 연결 이벤트마다 핀 하이/로우. 원시적이지만 확실하다. 돌렸다. 30분 관찰. 정상일 때: 200ms 간격으로 깔끔하게 이벤트 발생. 끊기기 직전: 간격이 들쑥날쑥. 150ms, 180ms, 250ms... 왜 이러지? 로그 뒤져보니 원인 발견. 센서 읽는 I2C 통신이 가끔 지연된다. 온도센서 응답 느릴 때 있다. 그 사이 BLE 이벤트 놓친다. RTOS Priority도 문제였다. BLE 스택이랑 센서 태스크 우선순위가 같았다. 센서 읽다가 BLE 이벤트 늦게 처리하면, Central(스마트폰)은 "쟤 죽은 거 아니야?" 판단한다. Supervision Timeout 4초. 여유 있어 보이지만, 실전에선 불안하다. RF 간섭 있으면? 패킷 재전송 있으면? 누적되면 4초 금방이다. 삽질의 시작 첫 번째 시도: Timeout 늘리기. #define CONN_SUP_TIMEOUT MSEC_TO_UNITS(6000, UNIT_10_MS)4초 → 6초. 결과: 여전히 끊긴다. 근본 해결 아니다. 두 번째 시도: Connection Interval 늘리기. #define MIN_CONN_INTERVAL MSEC_TO_UNITS(200, UNIT_1_25_MS) #define MAX_CONN_INTERVAL MSEC_TO_UNITS(400, UNIT_1_25_MS)생각: 주기 늘리면 센서 읽을 시간 여유 생긴다. 결과: 연결은 안정됐는데, 데이터 전송 느려져서 앱팀이 불만. "왜 이렇게 느려졌냐." 세 번째 시도: Slave Latency 추가. #define SLAVE_LATENCY 3센서가 3번까지 응답 안 해도 괜찮다는 뜻. 바쁠 때 건너뛸 수 있다. 결과: 더 불안정해졌다. 왜? 로그 보니, Latency 쓰면 Central이 "언제 응답 올지 모른다"고 타이밍 예측 못 한다. 안드로이드 BLE 스택이 짜증 낸다. 이게 스펙엔 없는 현실이다.답은 의외로 간단했다 삽질 1주일. 동기한테 하소연했다. 걔는 웨어러블 기기 펌웨어 한다. nRF52 경험 많다. "야, Connection Interval 협상 제대로 했어?" 협상? 뭔 소리야. 알고 보니 BLE는 연결 후 Parameter를 재협상한다. Central이 제안 → Peripheral(우리 센서)이 수락/거절. 우리 코드는 초기값만 설정하고, 협상 핸들링을 안 했다. Central이 "30ms 간격으로 하자" 제안하면, 그냥 수락한다. 30ms? 우리 센서는 I2C 읽는 데만 50ms 걸리는데? 코드 수정했다. void on_ble_evt(ble_evt_t const * p_ble_evt) { switch (p_ble_evt->header.evt_id) { case BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST: { ble_gap_conn_params_t const * p_req; p_req = &p_ble_evt->evt.gap_evt.params.conn_param_update_request; // 최소 Interval 체크 if (p_req->max_conn_interval < MIN_ACCEPTABLE_INTERVAL) { // 거절하고 우리 값 제안 ble_gap_conn_params_t our_params = { .min_conn_interval = MIN_CONN_INTERVAL, .max_conn_interval = MAX_CONN_INTERVAL, .slave_latency = 0, .conn_sup_timeout = CONN_SUP_TIMEOUT }; sd_ble_gap_conn_param_update(p_ble_evt->evt.gap_evt.conn_handle, &our_params); } else { // 수락 sd_ble_gap_conn_params_reply(p_ble_evt->evt.gap_evt.conn_handle, p_req, NULL); } break; } } }Central이 너무 짧은 간격 요청하면 거절하고, 우리가 감당 가능한 값 제안한다. 추가로 BLE 이벤트 핸들러 우선순위 올렸다. // FreeRTOS Task Priority #define BLE_TASK_PRIORITY (configMAX_PRIORITIES - 1) // 최우선 #define SENSOR_TASK_PRIORITY (tskIDLE_PRIORITY + 2) // 낮춤센서 읽다가 BLE 이벤트 놓치는 일 없도록. 테스트는 잔인하다 수정 후 24시간 Stress Test. 시제품 20대 전부 켜두고, 앱 연결 → 끊기 → 재연결 반복. 사무실 한쪽에 휴대폰 20개 놓고 스크립트 돌렸다. 밤새 돌렸다. 아침에 출근해서 확인. 로그 확인: 연결 끊김 0건. 재연결 시간: 평균 1.2초. 이전엔 3~5초 걸렸다. 데이터 누락: 없음. HW팀 불러서 보여줬다. "이제 됩니다." 걔네는 그냥 "아 그래?" 하더라. 1주일 고생한 건 관심 없다. 앱팀한테도 보고. "속도는 괜찮냐?" "이전이랑 똑같네요." 그래, 그럼 됐다. 스펙엔 없는 현실 BLE 스펙 문서는 1000페이지 넘는다. 다 읽어도 실전은 다르다.Connection Interval은 협상된다. 초기값은 제안일 뿐. Central(폰)마다 행동이 다르다. 삼성은 이렇고, 애플은 저렇고. RF 환경에 따라 신뢰성이 천차만별. 사무실이랑 공장이 다르다. Supervision Timeout은 여유 있게. "이론상 충분"은 실전에서 불충분.코드로만 해결 안 된다. 파형 봐야 하고, 로그 쌓아야 하고, 24시간 돌려야 안다. 양산 일정은 2주 남았다. 아직 해야 할 테스트 많다. -30도, +85도 온도 챔버 테스트. EMI 테스트. 인증 준비. 끝나면 한잔해야겠다.오늘도 스펙 문서와 현실 사이 어딘가에서 싸운다.
- 07 Dec, 2025
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보다 더 까다롭다고 들었다. 미리 두통약 챙겨야겠다.
- 03 Dec, 2025
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다. 또 무슨 일이 있을까.
- 02 Dec, 2025
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%는 경험과 코드와 오실로스코프로 채운다.