Stack 크기를 256에서 512로 늘렸더니 해결됐다

Stack 크기를 256에서 512로 늘렸더니 해결됐다

Stack 크기를 256에서 512로 늘렸더니 해결됐다 이상한 증상 출근했다. 어제 밤 11시까지 디버깅했는데 똑같다. 보드가 이상하다. 부팅은 된다. UART 로그도 찍힌다. 근데 5분 지나면 먹통이다. 규칙이 없다. 3분에 죽을 때도 있고 10분 버틸 때도 있다. 에러 문구? 없다. HardFault? 안 뜬다. 그냥 조용히 죽는다. "하드웨어 이슈 아닐까요?" HW팀에 물었다. 전원 확인했다. 괜찮다. 클럭도 정상이다. 온도도 문제없다. 결국 내 문제다.로그를 믿지 마라 printf 디버깅부터 시작했다. printf("Task A start\n"); // ... 작업 printf("Task A end\n");Task A는 정상이다. Task B도 정상이다. Task C에서 로그가 끊긴다. "Task C 문제네." 아니었다. Task C 코드를 주석 처리했다. 똑같이 죽는다. 그럼 Task D? 주석 처리. 여전히 죽는다. 3시간 썼다. 의미 없었다. FreeRTOS 쓰고 있었다. Task가 5개다. 우선순위도 다 다르다. 누가 죽이는지 모르겠다. printf는 거짓말한다. 버퍼에 쌓여 있다가 나중에 출력된다. 죽기 직전 로그는 안 나온다. JTAG도 힌트가 없다 오실로스코ープ는 소용없다. 신호는 정상이다. JTAG 붙였다. Breakpoint 걸었다. 한 줄씩 따라갔다. 멀쩡하다. Breakpoint 없이 돌리면? 죽는다. "타이밍 이슈인가?" Interrupt 의심했다. ISR 코드 확인했다. 간단하다. Flag만 세운다. void EXTI_IRQHandler(void) { flag = 1; HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_5); }문제없어 보인다.간헐적 버그의 지옥 이틀째다. 팀장이 물었다. "진행 상황이?" "디버깅 중입니다." "언제 끝나?" 모른다. 간헐적 버그는 예측 불가다. 재현이 안 된다. 5분에 죽을 때도 있고 30분 버틸 때도 있다. 규칙을 찾을 수가 없다. 코드 리뷰했다. 동적 할당? 안 쓴다. Malloc 금지다. 전역 변수? 있다. Mutex로 보호했다. 레이스 컨디션? 가능성 있다. 근데 어디서? 밤 10시까지 봤다. 못 찾겠다. 집에 와서도 생각났다. 샤워하다가도 생각했다. 어디가 문제일까. 메모리 주소를 봐라 셋째 날 아침이다. 다른 접근을 해야 한다. 생각을 바꿨다. "증상이 없는데 어떻게 추적하지?" 메모리를 봤다. RAM 사용량을 확인했다. .data: 2048 bytes .bss: 4096 bytes heap: 8192 bytes stack: ???? bytesStack은 런타임에 바뀐다. 컴파일러가 미리 계산 못 한다. Map 파일을 열었다. 각 Task의 Stack 크기가 나온다. TaskA: 256 bytes TaskB: 256 bytes TaskC: 512 bytes TaskD: 256 bytes TaskE: 256 bytesTaskA를 집중했다. 뭘 하는 Task인가? void TaskA(void *param) { char buffer[128]; while(1) { snprintf(buffer, 128, "Sensor: %d", read_sensor()); send_uart(buffer); vTaskDelay(1000); } }Buffer 128바이트다. Stack은 256바이트다. 괜찮아 보인다. 근데 snprintf는 내부적으로 Stack을 더 쓴다. 얼마나? 모른다. libc 구현마다 다르다. 혹시?256을 512로 Stack 크기를 늘렸다. #define TASK_A_STACK_SIZE 512 // was 256컴파일했다. 보드에 올렸다. 전원 켰다. 5분 지났다. 안 죽는다. 10분 지났다. 멀쩡하다. 30분 돌렸다. 정상이다. "이게 문제였어?" 믿기지 않았다. 다시 256으로 줄였다. 3분 만에 죽는다. 512로 늘렸다. 안 죽는다. 확정이다. Stack Overflow였다. 근데 이상하다. FreeRTOS는 Stack Overflow를 감지한다. configCHECK_FOR_STACK_OVERFLOW 옵션이 있다. 켜져 있었다. 왜 안 잡혔을까? 찾아봤다. FreeRTOS는 Context Switch 시점에만 체크한다. 그 사이에 오버플로우 나면 못 잡는다. TaskA는 1초마다 실행된다. 그 1초 사이에 Stack이 터진다. 감지 못 한다. 증상이 이상한 이유 Stack Overflow는 조용하다. 메모리를 덮어쓴다. 바로 크래시 안 난다. 다른 변수를 오염시킨다. 나중에 이상한 동작을 한다. 이번 케이스는 운이 나빴다. Stack 옆에 다른 Task의 제어 구조체가 있었다. 그걸 덮어썼다. FreeRTOS는 TCB(Task Control Block)를 RAM에 둔다. Stack Overflow가 TCB를 건드렸다. Task 스케줄링이 꼬였다. 시스템이 조용히 죽었다. 에러 메시지? 없다. HardFault? 안 뜬다. 그냥 멈춘다. 간헐적인 이유는 간단하다. 실행 경로가 매번 다르다. Sensor 값에 따라 snprintf 포맷팅이 다르다. "1"이랑 "1234"는 Stack 사용량이 다르다. 큰 값이 들어오면? Stack이 깊어진다. 터진다. 작은 값이면? 괜찮다. 규칙 없이 죽는 게 정상이었다. 교훈이랄 것도 없다 Stack 크기는 넉넉하게. 256은 작다. Printf 계열 함수 쓰면 최소 512는 줘야 한다. Floating point 쓰면 1024도 모자랄 수 있다. libc는 Stack을 많이 먹는다. 특히 가변 인자 함수들. snprintf, sscanf, sprintf... 다 조심해야 한다. 디버깅 교훈도 있다. 증상이 없으면 메모리를 봐라. 로그는 거짓말한다. 메모리는 거짓말 안 한다. FreeRTOS Stack Overflow 감지는 완벽하지 않다. 믿지 마라. 직접 계산하거나 넉넉하게 할당하거나. 해결했다. 팀장한테 보고했다. "Stack 크기 문제였습니다." "그게 3일 걸려?" 뭐라 하겠나. "네." 양산 전이라 다행이다 이거 양산 나갔으면 끝이었다. 현장에서 간헐적으로 죽는다? 재현 안 된다? 펌웨어 업데이트로 해결 안 된다? (RAM 크기는 고정이니까) 리콜이다. 보드 전부 회수다. 비용? 억 단위다. 다행히 개발 단계에서 잡았다. 양산 일정은 밀렸지만. 퇴근했다. 밤 11시다. 편의점에서 맥주 샀다. 집에서 혼자 마셨다. "Stack 512면 이제 괜찮겠지?" 모른다. 다른 Task도 체크해야 한다. 내일 할 일이다. 오늘은 여기까지다.Stack 256바이트. 펌웨어 개발자의 3일. 결국 숫자 하나였다.

토요일에도 회사, 양산 준비 열기 1주일 남음

토요일에도 회사, 양산 준비 열기 1주일 남음

토요일에도 회사, 양산 준비 열기 1주일 남음 오전 10시, 토요일 출근 알람 없이 일어났다. 7시 반. 토요일인데 몸이 알아서 깬다. 평소 같으면 "토요일은 무슨"이라고 했을 거다. 이번은 다르다. 양산 D-7. 팀장이 어제 말했다. "내일 나올 수 있는 사람?" 아무도 대답 안 했다. 5초쯤 지나서. "전 나오겠습니다." 내가 먼저 말했다. 그러니까 다들 나온다고 했다. 5명 전원. 씻고 나왔다. 회사 도착 9시 50분. 주차장이 텅 비었다. 우리 팀 차만 5대.주차장에서 코드 리뷰 사무실 들어가기 전에 차에 앉았다. 노트북 켰다. 어젯밤 커밋한 코드. DMA 버퍼 오버플로우 체크 로직. 주차장에서 다시 봤다. if(dma_buffer_idx >= DMA_BUFFER_SIZE) { // 여기서 뭘 해야 하지 }주석이 이렇게 되어 있다. 어젯밤 새벽 2시에 쓴 거다. 뭘 생각했는지 기억이 안 난다. 10분 동안 봤다. 아, 인터럽트 끄고 리셋해야지. 노트북에 메모했다. 사무실 가서 고치자. 차에서 내렸다. 팀장 차도 와 있다. 회의실에 모인 5명 다들 와 있었다. 회의실에 노트북 펼쳐놓고. "커피 타올게요." 막내가 말했다. "나도." "저도요." 결국 5잔. 팀장이 화이트보드에 썼다.UART 타임아웃 이슈 DMA 버퍼 관리 저전력 모드 전류 초과 OTA 검증 미완4개. 1주일에 4개. "하나씩 가자. UART부터." 시작했다.UART 타임아웃, 그놈의 9600bps UART 통신이 가끔 멈춘다. 재현이 안 된다. 100번 중 1번꼴. "타임아웃 값이 문제 아닐까요?" "이미 늘렸어요. 1초로." "그럼 보레이트?" "9600으로 고정이에요. 센서 스펙." 센서 스펙. 저가 센서라 9600bps 고정이다. 바꿀 수가 없다. 오실로스코ープ 가져왔다. 실제 파형 봤다. Start 비트는 정상. Data 비트도 정상. Stop 비트에서 가끔 글리치. "하드웨어 문제 아닐까요?" 내가 물었다. "PCB는 검증 끝났어요." HW 팀장 답변. 그럼 뭐지. 점심때까지 봤다. 결론: 일단 리트라이 로직 강화. 근본 원인은 모른다. 치킨과 디버깅 점심은 치킨. 토요일이니까 치킨. 회사 앞 치킨집. "양산 나가면 괜찮을까요?" 막내가 물었다. "괜찮아야지." 팀장이 웃었다. 웃긴데 안 웃겼다. 치킨 먹으면서도 코드 봤다. 노트북 옆에 치킨. 키보드에 기름 묻었다. 휴지로 닦았다. "DMA 버퍼는 제가 오늘 고칠게요." 내가 말했다. "저는 저전력 모드 볼게요." 선배가 말했다. 역할 분담 끝. 치킨 다 먹었다. 오후 2시.DMA 버퍼 수정, 인터럽트 지옥 사무실로 돌아왔다. 에어컨 빵빵하다. 토요일이라 전기 아깝지 않은가 봐. DMA 버퍼 코드 열었다. 아침에 본 그거. void DMA_IRQHandler(void) { if(dma_buffer_idx >= DMA_BUFFER_SIZE) { __disable_irq(); dma_buffer_idx = 0; memset(dma_buffer, 0, DMA_BUFFER_SIZE); __enable_irq(); error_count++; } }이렇게 고쳤다. 빌드. 보드에 올렸다. 테스트 시작. 1분. 정상. 5분. 정상. 10분. 정상. 좋아. 30분째. 에러 로그 떴다. error_count: 1 뭐지. 다시 코드 봤다. 인터럽트 안에서 memset? 이게 문제인가? 수정. volatile uint8_t buffer_reset_flag = 0;void DMA_IRQHandler(void) { if(dma_buffer_idx >= DMA_BUFFER_SIZE) { buffer_reset_flag = 1; error_count++; } }// 메인 루프에서 if(buffer_reset_flag) { __disable_irq(); dma_buffer_idx = 0; memset(dma_buffer, 0, DMA_BUFFER_SIZE); buffer_reset_flag = 0; __enable_irq(); }다시 빌드. 다시 올렸다. 오후 4시. 저전력 모드, 18mA의 비극 선배가 소리쳤다. "이거 봐요!" 멀티미터 들고 있다. "18mA요." 스펙은 10mA 이하. 거의 두 배. "어디서 새는 거예요?" "모르겠어요. LED 다 껐는데." 회로도 펼쳤다. A4 용지 10장. 하나씩 봤다.MCU 슬립 모드: OK 센서 파워다운: OK 통신 모듈 오프: OK그럼? "풀업 저항 아닐까요?" 내가 말했다. "다 확인했어요." 30분 더 봤다. 발견했다. ADC 클럭. 슬립 모드에서도 안 꺼져 있다. 레지스터 하나 놓쳤다. RCC->APB2ENR &= ~RCC_APB2ENR_ADC1EN;이 한 줄. 안 들어가 있었다. 추가하고 측정. 9.2mA. 성공. 오후 6시. 저녁 7시, 아직 2개 남음 저녁은 편의점. 컵라면이랑 삼각김밥. 회의실에서 먹었다. 화이트보드 봤다.UART 타임아웃 이슈 (임시 해결) DMA 버퍼 관리 (완료) 저전력 모드 전류 초과 (완료) OTA 검증 미완하나 남았다. 제일 큰 거. OTA. Over The Air 펌웨어 업데이트. 양산 나가면 필드 업데이트 필요하다. 근데 검증이 덜 됐다. "부트로더 점프 부분 확인해 봤어요?" 팀장이 물었다. "네, 그건 되는데요." 막내가 답했다. "뭐가 문제?" "CRC 체크가 가끔 실패해요." CRC. 펌웨어 무결성 검사. 이게 실패하면 벽돌. "가끔?" "네. 100번 중 3번 정도." 100번 중 3번. 3%면 적은 건가? 아니다. 양산 1만 개면 300개 벽돌. "오늘 못 고치면?" "월요일에 해야죠." 팀장 얼굴이 어둡다. 다들 어둡다. OTA, 끝나지 않는 밤 CRC 실패 원인 찾기. 로그 뽑았다. Flash Write: OK Flash Read: OK CRC Calculate: 0xA3B5C2D1 CRC Expected: 0xA3B5C2D1 Result: PASS이렇게 나올 때도 있고. Flash Write: OK Flash Read: OK CRC Calculate: 0xA3B5C2D1 CRC Expected: 0xA3B5C2D8 Result: FAIL이렇게 나올 때도 있다. Expected 값이 달라진다. "플래시에 쓸 때 문제인가?" "근데 Flash Read는 OK잖아요." 코드 다시 봤다. 플래시 쓰기 부분. for(int i = 0; i < fw_size; i += 4) { HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_APP_ADDR + i, *(uint32_t*)(fw_buffer + i)); }뭔가 이상하다. 플래시 쓰기 후 검증이 없다. 추가했다. uint32_t written_data = *(uint32_t*)(FLASH_APP_ADDR + i); uint32_t expected_data = *(uint32_t*)(fw_buffer + i); if(written_data != expected_data) { // 에러 처리 }다시 돌렸다. 100번. 실패 0번. 됐다. 오후 9시 반. 퇴근, 토요일 밤 "오늘 고생했습니다." 팀장이 말했다. "수고하셨어요." 다들 인사. 짐 챙겼다. 노트북, 충전기, 텀블러. 주차장 나왔다. 여전히 우리 차만 5대. 시동 걸었다. 라디오 켰다. 토요일 밤 음악 프로그램. 집 가는 길. 10분. 편하다. 양산까지 7일. 월요일부터 또 전쟁. 근데 오늘은 괜찮았다. 문제 3개 해결했다. 팀원들이랑 같이. 토요일에 회사 나온 게 아깝지 않다. 이상하게. 집 도착. 현관문 열고. 침대에 누웠다. 내일은 쉰다. 진짜 쉰다.월요일이 두렵지만, 오늘은 잘했다.

새로운 칩이 도입됐다, 레지스터맵은 완전히 다르다

새로운 칩이 도입됐다, 레지스터맵은 완전히 다르다

새로운 칩이 도입됐다 월요일 아침 출근했다. 팀장이 부른다. "이번 프로젝트, 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는 안 바뀐다. 우리는 하드웨어에 종속된다. 하드웨어가 바뀌면 다 바뀐다.레지스터맵이 바뀌면 코드도 바뀐다. 당연한데 매번 힘들다.

디버그 UART를 잘못 설정했는데 2시간이 걸렸다

디버그 UART를 잘못 설정했는데 2시간이 걸렸다

디버그 UART를 잘못 설정했는데 2시간이 걸렸다 출근 평소보다 30분 일찍 왔다. 어제 코드가 거의 완성됐거든. 오늘은 UART로 로그 찍어서 동작 확인만 하면 된다. 간단하다고 생각했다. 커피 마시고 보드 켰다. 터미널 실행했다. 아무것도 안 나온다. '응?'기본 체크 먼저 기본부터. USB-to-Serial 어댑터 인식되나? 확인. /dev/ttyUSB0 있다. 케이블 연결됐나? 확인. TX-RX 크로스 맞다. 보드 전원 들어왔나? 확인. LED 켜져 있다. 터미널 열었다. 여전히 아무것도 안 나온다. 검은 화면만. '코드 문제인가?' 코드 다시 확인했다. UART_Init(115200, UART_PARITY_NONE, UART_STOPBITS_1); printf("System Start\n");문제없어 보인다. printf도 UART로 리다이렉트 해놨다. '하드웨어 문제?' 오실로스코프 꺼냈다. TX 핀에 프로브 연결. 파형 나온다. 뭔가 보내고 있다. '그럼 왜 안 보이지?'보레이트부터 가장 기본. 보레이트 설정. 코드: 115200 터미널: 115200 맞다. 그런데 혹시? 9600으로 바꿔봤다. 안 된다. 19200으로 바꿨다. 안 된다. 230400으로 올렸다. 역시 안 된다. 다시 115200으로 돌렸다. '보레이트는 아니네.' 시계 봤다. 벌써 1시간 지났다. 데이터 비트 8비트가 표준이다. 근데 혹시나. 터미널 설정 들어갔다.Data bits: 8 Parity: None Stop bits: 1다 맞다. '뭐가 문제야.' 코드 다시 봤다. UART_Init(115200, UART_PARITY_NONE, UART_STOPBITS_1);UART_Init 함수 정의로 들어갔다. void UART_Init(uint32_t baudrate, uint8_t parity, uint8_t stopbits) { // ... USART->CR1 |= (parity << 10); USART->CR2 |= (stopbits << 12); // ... }레지스터 설정 보인다. 문제없어 보인다. '그럼 뭐지?'스펙 문서 할 수 없다. Reference Manual 펼쳤다. STM32 RM0090. 1700페이지짜리. USART 챕터 찾았다. 30장. 페이지 넘기다가 한 문장 보였다. "The USART_CR1 register must be configured before enabling the USART." '당연한 소리 아닌가?' 더 읽었다. "When M bit is set, data length is 9 bits..." 'M 비트?' 코드 다시 봤다. USART->CR1 |= (parity << 10);비트 10번이 패리티 인에이블이다. 맞다. 그런데 비트 12번. "Bit 12: M - Word length" 이게 뭐지. 스펙 읽었다.0: 8 data bits 1: 9 data bits'아.' 문제 발견 코드 다시 확인했다. #define UART_PARITY_NONE 0x00 #define UART_PARITY_EVEN 0x01 #define UART_PARITY_ODD 0x03PARITY_ODD가 0x03이다. 이걸 비트 10에 시프트하면? 0x03 << 10 = 0x0C00 비트 10번이랑 11번이 켜진다. 아니다. 비트 11번이랑 12번이다. 12번이 M 비트다. '세상에.' 패리티 설정하면서 데이터 길이를 9비트로 바꿔버린 거다. 터미널은 8비트로 받는데 보드는 9비트로 보내니까 데이터가 안 맞는다. 그래서 깨진 텍스트만 나왔던 거다. 아니다. 아예 안 보였다. 완전히 동기화가 안 됐다. 수정 코드 수정했다. #define UART_PARITY_NONE 0x00 #define UART_PARITY_EVEN 0x01 #define UART_PARITY_ODD 0x01 // PE 비트만아니다. 이것도 이상하다. 함수 자체를 다시 짰다. void UART_Init(uint32_t baudrate, uint8_t parity, uint8_t stopbits) { // M 비트 클리어 USART->CR1 &= ~USART_CR1_M; // 패리티 설정 if (parity == UART_PARITY_EVEN) { USART->CR1 |= USART_CR1_PCE; USART->CR1 &= ~USART_CR1_PS; } else if (parity == UART_PARITY_ODD) { USART->CR1 |= USART_CR1_PCE; USART->CR1 |= USART_CR1_PS; } // 나머지 설정... }컴파일. 업로드. 리셋. 터미널 봤다. System Start GPIO Init Done Timer Init Done Ready나온다. '드디어.' 시계 봤다. 2시간 10분 지났다. 교훈 UART는 간단하다고 생각했다. TX, RX 연결하고 보레이트 맞추면 된다고. 틀렸다. 보레이트, 패리티, 스톱비트, 데이터 비트. 하나만 달라도 안 된다. 더 정확히는 레지스터 하나만 잘못 건드려도 전부 꼬인다. 비트 연산 조심해야 한다. Define 쓸 때 값 확인해야 한다. 스펙 문서 읽어야 한다. 처음부터. '오늘 할 일은 뭐였지?' 로그 찍고 기능 테스트하는 거였다. 로그 찍는 데만 2시간 썼다. 기능 테스트는 내일. 점심시간 지났다. 배고프다.UART 설정 하나에 오전이 날아갔다. 내일은 진짜 기능 개발이나 해야지.

수요일 오후, 갑자기 '이건 사실 하드웨어 이슈 같은데요?'

수요일 오후, 갑자기 '이건 사실 하드웨어 이슈 같은데요?'

수요일 오후, 갑자기 '이건 사실 하드웨어 이슈 같은데요?' 오후 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 검증을 더 빡빡하게 해야겠다고. 근데 또 새로운 문제가 생길 거다. 그게 이 일이니까.