앞선 작업에서는 8KB 링버퍼를 구성하기에 앞서 SRAM을 확보하기 위해 .data와 .bss, FreeRTOS 태스크 Stack, 함수별 Call Stack까지 다시 살펴봤다. 그 과정에서 일부 태스크의 Stack 크기를 줄였지만, 정적 분석만으로 작업을 끝낼 수는 없었다. 계산상으로는 충분해 보여도 충전기가 동작하는 동안 예상하지 못한 호출 경로나 순간적인 Stack 사용량 증가가 생길 수 있기 때문이다.
결국 Stack을 줄였다면 다음에 필요한 것은 실제 운용 중 어느 정도까지 사용하고 있는지 확인하는 일이었다. 다행히 이 용도로 예전에 사수가 다른 모델에서 만들어둔 FreeRTOS System Monitor가 있었고, 이번에는 이 기능을 현 제품의 메모리 상태에 맞게 수정하여 Stack과 Heap 상태를 확인하는 데 활용했다.
디버깅 옵션으로 둔 System Monitor
System Monitor는 제품에서 항상 사용하는 기능이 아니다. 필요할 때만 활성화하는 디버깅 기능으로 두었고, FreeRTOSConfig.h에서도 USE_SYSTEM_MONITOR가 정의된 경우에만 Run Time Stats 기능을 활성화한다.
/* For Debugging */
/**************************************************************************/
#ifdef USE_SYSTEM_MONITOR
#define configGENERATE_RUN_TIME_STATS 1
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()
#define portGET_RUN_TIME_COUNTER_VALUE() xTaskGetTickCount()
#endif
/**************************************************************************/
현재 구현에서는 별도의 하드웨어 타이머를 사용하지 않고 xTaskGetTickCount()를 Run Time Counter로 사용한다. 따라서 CPU 사용률은 정밀한 성능 프로파일링 값이라기보다 태스크별 실행 비중을 확인하기 위한 디버깅 지표에 가깝다. System Monitor 소스 역시 USE_SYSTEM_MONITOR 조건 안에서만 빌드되도록 구성해 평상시에는 관련 코드와 메모리 사용을 제외한다.
uxTaskGetSystemState() 기반 태스크 상태 수집
모니터링의 핵심은 FreeRTOS의 uxTaskGetSystemState()다. 현재 구현에서는 먼저 태스크 수를 확인한 뒤 전체 태스크 상태를 가져온다.
uxCurrentNumberOfTasks = uxTaskGetNumberOfTasks();
uxArraySize = uxTaskGetSystemState(
pxTaskStatusArray,
uxCurrentNumberOfTasks,
&ulTotalTime
);
이를 통해 각 태스크의 이름과 상태, 우선순위, 누적 실행 시간, Stack High Water Mark 등을 얻을 수 있다. System Monitor는 이 정보를 1초마다 UART 터미널에 갱신하며 태스크별 CPU 비율과 상태, Stack 사용량, FreeRTOS Heap 상태를 한 화면에서 확인할 수 있도록 했다.
Stack High Water Mark
이번 작업에서 CPU 사용률보다 더 중요하게 본 값은 usStackHighWaterMark였다. 현재 구현에서는 프로젝트가 관리하는 태스크 정보에서 생성 당시 Stack 크기를 찾은 뒤, FreeRTOS가 제공하는 High Water Mark를 이용해 사용량을 계산한다.
used_stack =
stack_size -
task->usStackHighWaterMark * sizeof(StackType_t);
Stack High Water Mark는 현재 남아 있는 Stack 크기가 아니다. 태스크가 생성된 이후 지금까지 가장 적게 남았던 Stack 크기다. 따라서 전체 Stack 크기에서 이 값을 빼면 지금까지 관찰된 최대 Stack 사용량에 가까운 값을 확인할 수 있다. 예를 들어 1,024바이트 Stack을 가진 태스크에서 최소 잔여량이 80 words이고 StackType_t가 4바이트라면 최소 잔여량은 320바이트이고, 관찰된 최대 사용량은 약 704바이트가 된다.
앞선 작업에서 Call Stack을 정적으로 분석했다면, 이번에는 실제 동작 중 기록되는 High Water Mark를 함께 보면서 줄여놓은 Stack이 충분한 여유를 가지고 있는지 확인하는 방식이다.
태스크별 Stack 크기 변경
코드 레벨에서 주요 태스크에 할당하고 있는 Stack 크기를 정리하면 다음과 같다. 단위는 Bytes다.
| 태스크 | 기존 크기 | 변경된 크기 | 차이 |
| main | 1,024 | 1,024 | 0 |
| relay | 512 | 512 | 0 |
| buzzer | 512 | 512 | 0 |
| SY7M213H | 1,536 | 1,024 | -512 |
| ESP32 | 1,536 | 1,536 | 0 |
| PN7150 | 1,024 | 1,024 | 0 |
| EPD | 512 | 512 | 0 |
| GQSE | 2,048 | 1,536 | -512 |
| SECC | 1,024 | 1,024 | 0 |
| LED | 512 | 512 | 0 |
| Charge | 1,536 | 1,536 | 0 |
| UI | 2,048 | 2,048 | 0 |
| OCPP | 2,048 | 2,048 | 0 |
| 합계 | 15,872 | 14,848 | -1,024 |
SY7M213H와 GQSE에서 각각 512바이트씩 줄였고, 코드에서 직접 할당하는 태스크 Stack 기준으로 총 1KB를 줄였다. 중요한 것은 단순히 1KB를 줄였다는 결과보다 줄일 수 있는 범위를 정적으로 분석하고, 이후 충전기에서는 High Water Mark를 보면서 그 판단이 맞았는지 다시 확인할 수 있게 된 점이다.
System Monitor의 3KB 디버깅 비용
Stack과 Heap을 확인하기 위해 사용하는 System Monitor 자체도 적지 않은 메모리를 사용한다. 현재 구조에는 UART 출력용 1,024바이트 버퍼가 있고, System Monitor 태스크 자체에도 1,024바이트 Stack을 할당한다. 여기에 전체 태스크 상태를 저장하기 위한 TaskStatus_t 배열까지 FreeRTOS Heap에서 동적으로 확보한다.
- System Monitor Handle 및 출력 버퍼: 약 1KB
- System Monitor 태스크 Stack: 1KB
- TaskStatus_t 배열: 약 1KB
- TCB와 Heap 관리 오버헤드
결국 System Monitor를 실행하면 FreeRTOS Heap을 약 3KB 이상 추가로 사용하게 된다. SRAM을 줄이기 위해 사용하는 모니터가 상시 3KB를 차지하고 있다면 본말이 전도된다. 그래서 이 기능은 처음부터 제품에서 항상 켜두는 기능이 아니라 디버깅할 때만 활성화하는 옵션으로 두었다.
USE_SYSTEM_MONITOR ON
↓
System Monitor 실행
↓
Stack High Water Mark / CPU / Task State / Heap 확인
↓
측정 및 검증 완료
↓
USE_SYSTEM_MONITOR OFF
System Monitor는 메모리를 아끼는 기능이 아니다. 필요할 때만 약 3KB를 사용해 시스템 상태를 측정하고, 그 결과를 바탕으로 제품에서 항상 사용하는 메모리를 줄이기 위한 도구다.
누적 CPU 사용률
System Monitor 화면은 1초마다 갱신하지만 화면에 표시되는 CPU 비율이 최근 1초간의 사용률이라는 뜻은 아니다.
ulTotalTime /= 100UL;
ulStatsAsPercentage =
task->ulRunTimeCounter / ulTotalTime;
현재 구현은 FreeRTOS가 태스크별로 누적한 실행 시간을 기준으로 CPU 비율을 계산한다. 따라서 화면 자체는 매초 갱신되지만 CPU 비율은 최근 1초가 아니라 부팅 이후 누적 실행 시간을 기준으로 한 평균에 가깝다. 이번 System Monitor의 주 목적은 정밀한 CPU 프로파일링이 아니라 Stack과 Heap 상태 확인이기 때문에, CPU 값은 태스크별 실행 비중을 참고하는 정도로 사용한다.
정적 분석 이후의 런타임 검증
앞선 작업에서는 map 파일과 함수별 Call Stack을 보면서 SRAM을 어디까지 줄일 수 있는지 계산했다. 하지만 정적 분석으로 계산한 수치와 실제 운용 환경에서 나타나는 수치는 항상 같다고 볼 수 없다. 그래서 이번에는 예전에 직접 만들어두었던 FreeRTOS System Monitor를 다시 꺼내 Stack High Water Mark와 Heap 상태를 실제로 확인할 수 있도록 했다. 기존 코드는 이번 작업 과정에서 Codex로 다시 검토했지만, System Monitor 구현 자체는 이전에 직접 작성해둔 디버깅 도구다.
재미있는 점은 SRAM을 줄이기 위한 이 측정 도구 자체가 약 3KB의 메모리를 사용한다는 것이다. 그래서 항상 켜두는 대신 필요할 때만 USE_SYSTEM_MONITOR를 활성화하고, 측정이 끝나면 다시 빌드에서 제외한다. 앞선 작업에서 SRAM을 쥐어짰다면, 이번 작업은 그렇게 줄여놓은 상태가 충전기에서도 안전한지 확인하기 위한 과정이다.
System Monitor 실행 화면

위 화면은 태스크별 CPU 비율과 상태, Stack 사용량, FreeRTOS Heap 상태가 한 화면에 보이는 캡처 한 것이다. CPO 웹소켓에 연결하여 부팅 시퀀스를 완료한 상태이며, 충전등과 같은 동작 실행시 각 Task의 Stack 사용량을 확인할 수 있다.
