지난 9월, STM32의 제한된 SRAM에서 PnC(Plug and Charge) 기능을 구현하기 위해 메모리 확보 작업을 진행했다. 당시 SECC 통신을 담당하는 GQSE 모듈의 UART 수신 구조를 슬롯 방식에서 링버퍼로 변경하고, 송신은 512바이트 스트리밍 방식으로 재설계했다. RX 링버퍼는 기양산 모델에서 8KB, 신규 모델에서는 32KB를 사용하도록 구성했다. 관련 내용은 이전 STM32 메모리 확보 작업에서 정리했다.
현재는 당시 검증했던 테스트 코드를 실제 개발(양산) 코드에 하나씩 반영하고 있다. 변경 범위가 크기 때문에 부분적으로 적용한 뒤 Codex 코드 리뷰와 테스트, GitHub Connector Review를 반복하면서 병합하는 방식으로 진행 중이다. 그 과정에서 흥미로운 일이 있었다.
UART Timeout 경쟁 조건
GQSE UART RX 처리 구조를 변경한 뒤 Codex에게 코드 리뷰를 요청했다. 여러 검토 항목 중 UART 수신 Timeout에서 경쟁 조건(Race Condition)이 발견됐다. 현재 코드는 UART ISR에서 수신 데이터를 처리하고, 별도의 Task에서 Timeout을 검사한다. 수신 중인 프레임의 제한 시간을 넘기면 ISR이 폐기하도록 요청 플래그를 설정하는 구조다.
if (os_timer_expired(rx->frame_start_tick, 1000)) {
rx->frame_discard_request = 1;
}
Codex의 지적은 이랬다. Task가 A 프레임의 Timeout을 확인하는 사이 UART ISR이 A를 완료하고 B 프레임 수신을 시작할 수 있다. 그 직후 Task가 폐기 플래그를 설정하면 원래 대상이던 A가 아니라 정상적인 B 프레임이 폐기될 수 있다는 것이다. 기술적으로 맞는 지적이다. Timeout 검사와 플래그 설정은 원자적 동작이 아니며, Codex도 임시 테스트에서 이 순서를 재현했다고 보고했다. 그리고 해결책 중 하나로 다음을 제안했다.
수신 상태 확인 → timeout 검사 → 폐기 요청 설정을 짧은 임계 구역으로 보호하거나, 요청에 프레임 식별자를 넣어 ISR이 해당 프레임에만 적용하도록 해야 합니다.
그런데 임계구역이라는 말에서 과거의 논의가 떠올랐다.
임계구역과 10µs ADC 샘플링
메모리 확보를 위해 테스트 코드를 작성할 당시에도 Codex가 비슷한 제안을 했던 것으로 기억한다. 하지만 나는 CP(Control Pilot) PWM 전압 측정을 위해 10µs 단위의 ADC 샘플링을 수행하고 있어서 인터럽트에 영향을 주는 임계구역을 적용하는 데 반대했다. 당시 Codex 역시 그 위험성을 인정했던 것으로 기억한다.
다만 회사 노트북을 포맷하면서 로컬에 저장된 Codex 대화 기록이 사라져 과거 대화의 정확한 표현을 다시 확인할 수는 없었다. 그래서 이번에는 CP 관련 코드를 직접 확인해 보라고 요청했다. Codex가 기양산 모델의 코드를 확인한 결과, 인터럽트 구성은 다음과 같았다.
| 구분역할 | IRQ | 우선순위 |
| TIM3 | CP PWM 1ms 주기 | 8 |
| TIM2 → ADC5 → DMA | 10µs ADC 샘플링 | 7 |
| USART3 | GQSE UART RX | 5 |
CP 측정은 10µs 간격의 ADC 변환과 DMA 완료 처리에 의존한다. 그런데 현재 FreeRTOS 설정에서 taskENTER_CRITICAL()을 사용하면 우선순위 7과 8인 CP 관련 인터럽트도 지연될 수 있다. 하드웨어 PWM 출력은 계속되더라도 ADC 샘플 처리나 샘플링 정지 시점이 달라질 가능성이 있다는 의미다. 특히 DMA 수신 버퍼가 한 샘플 크기인 현재 구조에서는 처리 지연을 가볍게 볼 수 없다. CP 코드를 확인한 뒤 Codex도 다음과 같이 답변했다.
따라서 GQSE timeout 보호를 위해 전체 IRQ 차단이나 일반 FreeRTOS 임계 구역을 제안한 것은 이 CP 경로를 충분히 고려하지 못했습니다.
과거에 임계구역을 피하려고 했던 기술적 이유를 실제 코드에서 다시 확인한 셈이다. 물론 이번 검토에서 인터럽트 지연에 따른 실제 측정 오차를 계측한 것은 아니다.
1초 Timeout의 설계 목적
기본적으로 SECC UART 통신은 Request / Response 방식이다. EVSE에서 Request를 보내면 정상적인 상황에서는 1초 안에 완성된 Response 프레임이 들어와야 한다. 일부 차량에서는 충전 중 약 25ms 간격으로 메시지가 발생하기도 하지만, CAN처럼 메시지가 끊임없이 쏟아지는 통신 구조는 아니다. 수신 또한 All or Nothing 방식으로 처리한다. 프레임을 끝까지 수신하고 검증을 통과해야 정상 메시지로 인정한다.
1초 Timeout의 목적은 모든 UART 통신 오류를 완벽하게 해결하는 것이 아니라, 비정상적인 상황에서 무한 대기를 방지하는 것이다. 응답을 받지 못하면 필요한 경우 Retry하고, 반복해서 실패하면 UART 통신 경로나 관련 하드웨어 이상을 의심한다. 이 통신 특성과 CP ADC의 실시간성을 고려했을 때, 드물게 발생할 수 있는 경쟁 조건을 해결하겠다고 일반적인 임계구역을 추가하는 것은 적절하지 않다고 판단했다.
경쟁 조건이 없다는 뜻은 아니다. 프레임 식별자를 사용하는 등 인터럽트 차단 없이 보완할 방법도 있지만, 그 변경을 지금 적용해야 하는지는 별도의 판단이다.
Codex의 지적을 모두 반려한 것은 아니다. 실제로 GQSE Disable 이후 메시지 큐에 남은 RX descriptor가 링버퍼 인덱스를 잘못 변경하는 문제를 발견했고, 검증 후 큐 reset 처리를 추가했다. 배터리 데이터의 Base64 변환 결과가 출력 버퍼를 넘는 문제도 재현 테스트를 거쳐 수정했다. 반면 일부 지적은 예정된 통신 프로토콜 변경이거나 향후 PnC 구현 과정에서 다룰 사항이었다. 무엇을 지금 수정하고 무엇을 유지할지는 실제 동작 조건을 기준으로 구분했다.
이번 Timeout 경쟁 조건도 마찬가지다. Codex가 잠재적인 문제를 발견한 것은 맞지만, 제안된 해결책이 시스템 전체에 적합한지는 다시 확인해야 했다. 관련 CP 코드를 검토하게 하자 그제야 인터럽트 실시간성 제약이 드러났다. 코드 리뷰는 앞으로도 Codex와 Connector Review를 활용해 반복할 생각이다. 다만 임베디드에서는 코드 한 부분의 위험을 줄이려는 수정이 다른 기능의 실시간 처리에 영향을 줄 수도 있다. 이번에는 기존 방식을 유지하기로 했다.
그리고 과거에 AI와 함께 검토했던 설계라도, 판단 근거가 남아 있지 않으면 같은 논의를 다시 반복하게 된다.
이번에는 내가 그 이유를 기억하고 있었을 뿐이다.
