ARM Cortex-M MCUs에서의 코드 실행 시간 계산
본 기사는 RTOS 전문가인 Jean J. Labrosse가 작성하였습니다.
코드 실행 시간을 측정하는 방법에는 여러 가지가 있습니다. 임베디드 엔지니어인 저의 경우, 하나 이상의 디지털 출력과 오실로스코프를 자주 사용했습니다. 이는 모니터링하려는 코드를 실행하기 전에 출력 중 하나를 높게 설정하고 나중에 출력을 낮게 설정하기만 하면 됩니다. 물론 이 작업을 수행하기 전에, 하나 이상의 막혀 있지 않은 출력을 찾고, 쉽게 액세스할 수 있는지 확인하고, 포트를 출력으로 구성하고, 코드를 작성하고, 컴파일하고, 범위를 설정하는 등의 작업을 수행하는 등, 상당한 양의 설정 작업이 필요합니다. 신호가 존재할 경우, 최소값과 최대값을 확인하기 위해 잠시 동안 신호를 모니터링해야 할 수 있습니다. 디지털 스토리지 스코프를 사용하면 이 프로세스를 더 쉽게 할 수 있으나, 더 쉽게 할 수 있는 다른 방법도 있습니다.
실행 시간을 측정하는 또 다른 방법은 추적 가능한 디버그 프로브를 사용하는 것입니다. 코드를 실행하고, 추적 사항을 확인하고, 델타 시간을 (대체로 수동으로) 계산하고, CPU 주기를 마이크로 초로 변환하기만 하면 됩니다. 그러나 추적작업 역시 하나의 실행 인스턴스를 제공하며, 최악의 경우의 실행 시간을 찾아내기 위해 추적 캡처 내용을 더욱 자세히 살펴봐야 할 수도 있습니다. 이러한 과정은 지루한 과정일 수 있습니다.
Cortex-M 싸이클 카운터
대부분의 Cortex-M 기반 프로세서에 있는 CoreSight 디버그 포트에는, CPU 클럭 주기를 계산하는 32비트 자유 실행 카운터(free running counter)가 포함되어 있습니다. 카운터는 디버그 감시 및 추적(DWT) 모듈의 일부이며 코드 실행 시간을 측정하기에 용이합니다. 다음의 코드를 이용하여, 이러한 유용한 기능을 활성화하고 초기화할 수 있습니다.
#define ARM_CM_DEMCR (*(uint32_t *)0xE000EDFC)
#define ARM_CM_DWT_CTRL (*(uint32_t *)0xE0001000)
#define ARM_CM_DWT_CYCCNT (*(uint32_t *)0xE0001004)
if (ARM_CM_DWT_CTRL != 0) { // See if DWT is available
ARM_CM_DEMCR |= 1 << 24; // Set bit 24
ARM_CM_DWT_CYCCNT = 0;
ARM_CM_DWT_CTRL |= 1 << 0; // Set bit 0
}
DWT 싸이클 카운터를 활용한 코드 실행 시간 계산
해당 세그먼트 전후의 사이클 카운터 값을 읽어 코드 세그먼트의 실행 시간을 측정하고 계산하는 과정은 다음과 같습니다. 이는 코드를 측정해야 하기는 하지만, 그만큼 매우 정확한 값을 얻는다는 것을 의미합니다.
uint32_t start;
uint32_t stop;
uint32_t delta;
start = ARM_CM_DWT_CYCCNT;
// Code to measure
stop = ARM_CM_DWT_CYCCNT;
delta = stop – start;
해당 코드의 수식에서는 부호를 사용하지 않기 때문에 델타는 측정된 코드의 실제 실행 시간(CPU 클럭 주기)을 나타냅니다. 즉, 중지 시간이 시작 시간보다 짧은 경우에도 델타는 측정된 코드의 실제 실행 시간입니다.
물론, 읽기 시작과 중지 사이에, 괄호로 묶인 코드의 실행 시간을 측정하는 동안 인터럽트가 발생할 수 있기 때문에 이 시퀀스를 실행할 때마다 다른 값을 가질 가능성 또한 큽니다. 이러한 경우, 인터럽트를 비활성화하면 측정 중 인터럽트 대기 시간을 크게 줄일 수 있다는 점을 고려하면, 측정 중 인터럽트를 비활성화하여 아래와 같이 코드에 영향을 끼치는 인위적인 요소를 제거하고 싶을 수도 있습니다. 그러나, 인터럽트로 인한 인위적인 영향은 코드 실행의 마감 시점까지 영향을 미치기 때문에, 이를 포함하는 것이 전체 코드 실행 시간 측정에 유용할 수 있습니다.
Disable Interrupts;
start = ARM_CM_DWT_CYCCNT;
// Code to measure
stop = ARM_CM_DWT_CYCCNT;
Enable Interrupts;
delta = stop – start;
측정 중인 코드에 조건문, 루프 또는 변형을 유발할 수 있는 것이 포함되어 있는 경우, 코드 실행 시간을 측정하기 위해 획득된 값이 최악의 경우의 실행 시간은 나타내지 못할 수도 있습니다. 이를 수정하려면 아래와 같이 피크 검출기를 추가하기만 하면 됩니다. 측정 수행 전에 max를 선언하고, 가장 작은 값(즉, 0)으로 초기화하는 작업은 필수입니다.
start = ARM_CM_DWT_CYCCNT;
// Code to measure
stop = ARM_CM_DWT_CYCCNT;
delta = stop – start;
if (max < delta) {
max = delta;
}
마찬가지로 최소 실행 시간을 구하는 것도 흥미롭고 유용할 수 있습니다. min은 측정 전에 선언하고, 가능한 가장 큰 값(즉, 0xFFFFFFFF)으로 초기화해야 합니다. 새로운 코드는 다음과 같습니다.
start = ARM_CM_DWT_CYCCNT;
// Code to measure
stop = ARM_CM_DWT_CYCCNT;
delta = stop – start;
if (max < delta) {
max = delta;
}
if (min > delta) {
min = delta;
}
또한, (RTOS 사용 가정 시의) 작업이 대기 중인 이벤트를 처리하기 위해 소요되는 시간도 측정할 수도 있습니다. 예를 들어 작업이 마감일을 준수하는지 여부를 알고 싶을 수 있습니다. 이러한 경우에, ‘Wait for …’ 코드 바로 뒤에 ‘stop’ 코드를 추가하거나, 또는 작업이 실제로 인터럽트에 대한 응답으로 필요한 작업을 수행한 후에 ‘stop’ 코드를 추가하면 됩니다. 이러한 과정으로 도출되는 두 값 모두 유의미한 값일 수 있습니다.
void MyISR (void)
{
Clear interrupt source;
start = ARM_CM_DWT_CYCCNT;
Notify task that interrupt occurred;
}
void MyTask (void)
{
Task initialization;
while (1) {
Wait for notification from ISR;
stop = ARM_CM_DWT_CYCCNT;
delta = stop – start;
if (max < delta) {
max = delta;
}
if (min > delta) {
min = delta;
}
Code to handle event;
}
}
// Code to measure
stop = ARM_CM_DWT_CYCCNT;
delta = stop – start;
if (max < delta) {
max = delta;
}
if (min > delta) {
min = delta;
}
또한, 일부 Cortex-M4 프로세서 및 Cortex-M7의 경우와 같이, CPU에 캐시가 장착 여부에 따라 실행 시간이 달라집니다. 시스템에서 명령 또는 데이터 캐시를 사용하는 경우, 코드의 동일한 섹션에 대한 여러 측정값이 일치하지 않을 수 있습니다. 최악의 조건에서의 코드 실행 시간을 측정하고자 한다면, 캐시 비활성화를 고려해 볼 수도 있습니다.
값을 표시하기 위해 대부분의 디버거에서는 해당 변수 값을 실시간으로 표시할 수 있습니다. 그렇다면, 표시된 변수는 변수의 값을 유지하고 실시간 모니터링을 허용하기 위해 광역 범위로 선언되어야 합니다. 그러나 표시된 변수의 값들은 CPU 클럭 주기를 나타내며, 대부분의 디버거는 변수를 표시하기 위해 광역 범위로 확장할 만큼 정교하지 않다는 점이 아쉬운 점입니다. 이 때, 16MHz CPU 클럭 속도를 가정하면 1123 사이클보다 70.19마이크로초를 표시하는 것이 훨씬 더 편리할 수 있습니다. 여러 상황이 원하는 바와 같이 갖추어져 있고, CPU가 100MHz에서 실행 중이면 변환이 쉽게 수행된다고 느낄 수 있습니다.
소요 시간 모듈
코드 스니펫을 애플리케이션에 추가하거나, 본 문서에 포함된 간단한 모듈을 사용하기만 하면 됩니다. 'elapsed_time.c' 모듈은 단 4개의 함수로 구성되어 있습니다.
사용 방법:
- 간단히 #include <elapsed_time.h>를 실행합니다.
- c에 정의된 다른 함수를 사용하기 전에 elapsed_time_init()를 호출합니다.
- ELAPSED_TIME_MAX_SECTIONS를 설정하여, 최대 경과 시간 측정 구조 수를 정의합니다. 이는 곧 중지/시작 코드로 래핑하려는 다양한 코드 스니펫의 개수입니다.
- elapsed_time_start()를 호출하고, 모니터링하려는 코드 스니펫의 인덱스(예: 0 .. ELAPSED_TIME_MAX_SECTIONS-1)에 전달합니다.
- elapsed_time_stop()을 호출하고 elapsed_time_start()에서 사용한 것과 동일한 인덱스에 전달합니다.
- 디버거에서 변수를 실시간으로 모니터링할 수 있는 경우(즉, 모니터링의 대상이 실행되는 동안) elapsed_time_tbl[]을 표시하고, 사용했던 인덱스에 대응되는 ELAPSED_TIME의 구조를 볼 수 있습니다.
- ELAPSED_TIME 구조의 .min 및 .max 필드가 측정 중인 코드 스니펫의 실행 시간을 보기 좋게 도시하도록, 4~6단계를 반복적으로 수행하며 최악의 경우와 최상의 경우 각각에 코드를 종속시킵니다.
ISR이 관련되어 있을 수 있으며, ISR과의 관련성이 인지된 바의 실행 시간에 어떤 영향을 미치는지 궁금하실 수 있기 때문에, 이를 알려드리기 위하여 제가 해당 측정 기간 동안 인터럽트를 비활성화하지 않았음을 아실 수 있을 것입니다(elapsed_time.c 참조).
void main (void)
{
// Some code
elapsed_time_init(); // Initialize the module
// Some code
}
void MyCode (void)
{
// Some code here
elapsed_time_start(0); // Start measurement of code snippet #0
// Code being measured
elapsed_time_stop(0); // Stop and
// Some other code
}
물론, 최소 및 최대 실행 시간은 코드가 각각 최상의 조건과 최악의 조건에 종속되었거나 영향을 받는지 여부와, 측정을 수행하는 빈도에 따라 결정됩니다.
다음의 IAR이 내장된 작업대(IAR Embedded Workbench)의 LiveWatch 창 스크린샷은 가공되지 않은 본래 형식으로 도출된 값입니다. elapsed_time_tbl[]은 다른 코드 스니펫에 대한 측정값을 저장하는 배열(Array)입니다. 참고로 시작 필드에는 (예를 들어 변경되는 것을 보면 측정이 수행되고 있음을 나타내는 것과 같은) 활동을 표시하는 것 외에 제한된 표시 값이 있습니다. 또한, 값은 CPU 클럭 주기로 표시되고, 안타깝게도 C-SPY는 이러한 값을 마이크로 초 단위로 변환하는 스케일링 기능을 제공하지 않습니다.
elapsed_time.* 모듈은 사용자가 확실히 개선할 수 있습니다. 다음은 추가할 수 있는 몇 가지 기능입니다. 작업을 좀 더 용이하도록 하기 위한 프로토타입, 변수 및 구조체를 구성하는 변수들의 이름을 추가해 두었으나, 사용자가 반드시 유용하다고 생각하게 될, 필요한 코드를 구현할 수 있도록 해드리겠습니다.
- 최대 실행 시간이 임계값(.threshold)을 초과할 때 호출되는 콜백 함수를 추가합니다. 이와 같은 콜백 함수 추가는 임계값 초과 상황을 발생 즉시 (LED 켜기, 경보 울리기 등의 수단으로) 인지하고자 할 경우에 유용할 수 있습니다. 실제로 임계값을 초과했던 조건을 사후에 확인하고자 할 경우, 중단점을 사전에 설정할 수도 있습니다. 또한, ELAPSED_TIME에 .callback_ptr member 함수를 추가할 수 있습니다.
- 채널별로 측정을 활성화/비활성화하려면 불리언(Boolean) 자료형을 추가합니다. 그럼으로써 코드를 계측한 후에 활성화 또는 비활성화할 측정을 분류하여 결정할 수 있습니다. 또한, ELAPSED_TIME 구조에 .enable과 같은 구조 멤버를 지정할 수 있습니다. 디버깅하는 동안 C-SPY를 사용하여 .enable을 1(활성화) 또는 0(비활성화)으로 설정하거나 API를 호출하여 동일한 작업을 수행할 수 있습니다.
- 작업 사이클에서 마이크로초로 변환하기 위해 사용 가능한, CPU 클럭 주파수(elapsed_CPU_clock_frequency)를 명시하는 모듈에 대한 광역 변수가 있습니다. 이러한 광역 변수를 이용하기 위해서는, 다른 필드(.current, .min 및 .max)를 elapsed_time_stop()에서 elapsed_CPU_clock_frequency로 나누기만 하면 됩니다. 0으로 나누는 상황을 사전에 방지하려면, 해당 변수를 NON-ZERO 값으로 초기화해야 합니다.
저자 소개
본 기사는 RTOS 개발 애플리케이션에 관한 시리즈의 일부입니다.
Jean Labrose는 높은 인지도를 가진 uC/OS-II와 uC/OS-III 커널의 저자이자 Micrium의 설립자로, 임베디드 소프트웨어의 uC/라인의 발전에 적극적으로 관여하고 있습니다.
Jean은 풍부한 경험과 임베디드 시스템 시장에 대한 깊은 이해를 바탕으로 Weston Embedded Solutions의 수석 조언자 및 컨설턴트로 재직하고 있으며, 현재 RTOS 제품의 향후 보다 발전된 제안을 마련하는데 기여하고 있습니다. Weston Embedded Solutions는 Micrium 코드베이스에서 파생된 매우 안정적인 Cesium RTOS 제품군의 지원 및 개발을 전문으로 합니다.
[email protected].