Для встраиваемых систем критически важно контролировать время выполнения определенных участков кода, чтобы оно не превышало заданную величину. Время обработки может иметь решающее значение для приложения, и скорость вычислений может быть повышена за счет использования новых алгоритмов или применения специальных оптимизаций для особо критичных участков кода. На скорость выполнения также влияют опции компилятора. Однако перед тем, как попытаться улучшить производительность кода, может потребоваться измерить реальную скорость выполнения определенных участков кода.
Известна популярная техника измерения времени выполнения с помощью дергания ножкой GPIO и осциллографа, и это конечно достоверный и реалистичный метод. Однако можно измерить время выполнения кода с помощью других достаточно простых способов, основанных на функционале MCU и средств разработки. В этой статье описаны 3 базовые техники и некоторые более продвинутые методы измерения времени на платформах ARM Cortex-M3/M4 и IAR Embedded Workbench for ARM.
[Техника 1: использование Log Breakpoint]
Если Вам нужно только грубое измерение, то можно использовать точку останова вывода в лог (Log Breakpoint). Когда код достигнет установленной Log Breakpoint, в окне лога отладки (debug log) будет отображено текущее время компьютера. Свойства этого метода:
• Не требуется модификация исходного кода. • Точность измерения времени 1 секунда.
Как разрешить log breakpoint: сделайте правый клик на строке кода, в которой нужно узнать время выполнения, и выберите пункт "Toggle Breakpoint (Log)".
Установленная точка останова лога помечается в коде красным кружком с буквой "L":
При достижении установленной точки в лог будет выводиться информация о текущем времени, имени файла исходного кода и его номере строки, где сработала Log Breakpoint, и счетчик срабатываний:
...
Mon Mar 23, 2020 10:47:37: Hardware reset with strategy 0 was performed
Mon Mar 23, 2020 10:47:37: Target reset
Mon Mar 23, 2020 10:48:51: [USARTconsole.c:209.7] #0
Mon Mar 23, 2020 10:48:54: [USARTconsole.c:209.7] #1
Mon Mar 23, 2020 10:48:55: [USARTconsole.c:209.7] #2
Mon Mar 23, 2020 10:48:55: [USARTconsole.c:209.7] #3
Mon Mar 23, 2020 10:48:55: [USARTconsole.c:209.7] #4
Mon Mar 23, 2020 10:48:56: [USARTconsole.c:209.7] #5
Mon Mar 23, 2020 10:48:56: [USARTconsole.c:209.7] #6
Mon Mar 23, 2020 10:48:57: [USARTconsole.c:209.7] #7
Mon Mar 23, 2020 10:48:57: [USARTconsole.c:209.7] #8
Конечно, эта техника не подойдет для точного измерения времени, однако она очень простая. Вы также можете сделать повторение целевой функции 1000 раз, и зафиксировать время до начала повторений и после. Затем, чтобы узнать точное время выполнения функции, нужно измеренное время разделить на 1000.
[Техника 2: использование регистра CycleCount]
Современные MCU содержат в себе встроенный аппаратный регистр, который показывает количество прошедших тактов ядра. Ядра микроконтроллеров ARM Cortex-M3/4/7 предоставляют для этого регистр с именем CYCLECOUNTER (счетчик циклов).
Следует взять значение CYCLECOUNTER в двух местах кода, время выполнения которого нужно измерить. Затем, зная значение тактовой частоты CPU, Вы можете измерить реальное время выполнения кода. Например, если CPU работает на частоте 100 МГц, то можно полученное значение прошедших тактов поделить на 100000000, и в результате получится измеренное время в секундах. Особенности метода:
• Не требуется модификация кода. • Точность измерения времени составляет 1 такт CPU. • Поддержка метода зависит от применяемого MCU.
Для использования CYCLECOUNTER сделайте следующее:
1. Запустите сессию отладки и откройте окно просмотра регистров через меню View.
2. В открывшемся окне просмотра регистров сделайте правый клик мышью, и выберите View Group -> CPU Registers (в некоторых версиях IAR нужно выбрать Current CPU Registers). Откроется окно, где будет показано значение CYCLECOUNTER, которое равно количеству тактов, которое прошло с момента запуска программы.
3. Чтобы измерить время выполнения функции, сделайте на ней шаг отладчика без захода в тело функции (Step Over). Значение CCSTEP покажет разницу между началом выполнения шага и его завершением:
424(текущий CYCLECOUNTER) – 416(предыдущий CYCLECOUNTER) = 8(текущий CCSTEP)
Счетчик CYCLECOUNTER предоставляется ARM Cortex-M3 MCU, однако он запрещен по умолчанию в IAR Embedded Workbench for ARM. Если Вы используете отладчик I-jet, то CYCLECOUNTER будет разрешен. Если у Вас другой отладчик, и CYCLECOUNTER не обновляется, то попробуйте установить бит CYCCNTENA в регистре DWT Control (DWT_CTRL.CYCCNTEN), который можно найти в группе регистров Data Watchpoint and Trace unit.
CYCLECOUNTER прост в использовании, и дает точное измерение времени. Если нужно узнать время в секундах, то разделите количество прошедших тактов на тактовую частоту CPU. Имейте в виду, что CYCLECOUNTER может отличаться при разных измерениях из-за влияния конвейера команд (pipeline) или попаданий кэша. Метод не рекомендуется для измерения времени выполнения одной или двух инструкций, однако для на десятке или большем количестве инструкций можно получить более точные данные.
[Техника 3: использование таймера MCU]
Таймер это важная функциональная особенность MCU, предназначенная для периодического запуска процессов. Часто таймер используется для получения прерывания на считающем вверх или вниз счетчике таймера. Вы также можете измерить время в программе чтением значения счетчика таймера. Особенности этого метода:
• Необходима модификация исходного кода. • Нужно сконфигурировать таймер. • Код должен получить доступ к значению счетчика таймера. • Значение времени можно получать как в единицах счета таймера, так и в количествах его переполнений. • Конфигурация таймера может отличаться для каждого конкретного MCU, однако общий принцип измерения времени одинаковый.
Таймер MCU для измерения времени используют следующим образом. Сначала нужно определиться с источником тактирования таймера и его прескалером. Если прескалер 10, то через каждые 10 периодов тактовой частоты таймера он изменит свой счетчик на 1 вверх или вниз. Например, если частота тактирования таймера совпадает с частотой CPU 100 МГц, коэффициент прескалера 1000, то таймер изменяет свое значение с частотой 100000000/1000 = 100000 Гц, или каждые 0,00001 секунды (каждую 0.01 мс). Таким образом, если разница в значении счетчика в начале и конце измеряемого кода составляет 100, то время его выполнения составит 0.01 * 100 = 1 мс. Ниже показан пример кода, где измеряется время выполнения функции func():
unsigned int cnt1 = 0;
unsigned int cnt2 = 0;
cnt1 = TIM3->CNT;
func();
cnt2 = TIM3->CNT;
printf("cnt1:%u cnt2:%u diff:%u \n",cnt1,cnt2,cnt2-cnt1);
Результат может выглядеть примерно так:
cnt1:370 cnt2:740 diff:370
Если вы используете время в реальной функции, то имейте в виду, что счетчик времени может быть иногда сброшен обработчиком прерывания, поэтому рекомендуется использовать таймер только для измерения времени. Также при остановке отладчика счетчик может продолжать свой счет, поэтому необходимо сохранять значение счетчика в переменной.
Счетчик таймера SysTick процессора ARM Cortex-M останавливается, когда CPU не запущен. Это делает его полезным для измерения времени, особенно для ARM Cortex-M0/M0+ MCU, в котором не реализован регистр CYCLECOUNTER.
[Техника 4: использование Data Log breakpoint]
Data Log breakpoint - мощная функция отладки MCU ARM Cortex-M3 и Cortex-M4 devices. С отладчиками, которые поддерживают SWD/SWO, такими как I-jet, можно в реальном времени получать доступ к данным лога и меткам времени.
Перед запуском сессии отладки нужно настроить отладчик для SWD и SWO:
Пример кода:
static int data_main = 0;
static int data_systick = 0;
void Delay(Int32U Dly)
{
for(volatile Int32U j = Dly; j; j--) { }
}
void SysTick_Handler(void)
{
data_systick += 10;
}
void main(void)
{
SystemInit();
SysTick_Config(900000);
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
while(1)
{
Delay(100000);
data_main++;
}
}
В главном цикле функции main переменная data_main будет инкрементироваться с некоторыми интервалами времени (определяются функцией задержки Delay). Здесь также есть обработчик прерывания таймера SysTick, который инкрементирует переменную data_systick. Используя Data Log Breakpoint, можно просмотреть значения переменных и время доступа.
Data Log Breakpoint устанавливается следующим образом. Выполните правый клик на переменной data_main в редакторе кода, и выберите в контекстном меню Set Data Log Breakpoint for 'data_main'.
Также Вы можете установить Data Log Breakpoint для data_systick:
После этого запустите сессию отладки:
Информацию по существующим точкам останова можно просмотреть выбором View -> Breakpoints.
Вы увидите список установленных Data Log breakpoints.
По умолчанию Data Log запрещен. Чтобы разрешить его, откройте окно Data Log (I-jet/JTAGjet -> Data Log).
Сделайте правый клик в окне Data Log и выберите Enable.
Запустите код в отладчике на выполнение. Когда программа будет обращаться к этим двум переменным, их значение и время обращения будет показано в окне Data Log.
Время также может отображаться в тактах CPU.
Если мы видим, что для 'data_main' W:12 и 'data_main' W:13 разница составляет 866768 тактов, то можно заключить, что время выполнения функции Delay(100000) составляет около 866768 тактов.
Изначально Data Log предназначен для трассировки доступа к данным. Однако на практике также его можно использовать и для измерения времени.
Имейте в виду:
• Переменные для Data Log Breakpoints должны иметь в памяти фиксированные адреса, т. е. это должны быть статические или глобальные переменные. • Data Log не несет никаких дополнительных вычислительных затрат. • Согласно спецификации ARM Cortex-M3/M4, можно установить максимум 4 экземпляра Data Log Breakpoints.
[Техника 5: использование Event Log]
Лог событий (Event Log) это еще одна функция отладки микроконтроллеров ARM Cortex-M3/M4. Вы можете добавить в свое приложение код события (event code), и каждый раз при выполнении этого кода будет генерироваться запись в Event Log.
Предыдущий пример, в котором изменено несколько строк:
#include < arm_itm.h>
static int data_main = 0;
static int data_systick = 0;
void Delay(Int32U Dly)
{
for(volatile Int32U j = Dly; j; j--) { }
}
void SysTick_Handler(void)
{
data_systick += 10;
ITM_EVENT32_WITH_PC(2,data_systick);
}
void main(void)
{
SystemInit();
SysTick_Config(900000);
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
while(1)
{
Delay(100000);
data_main++;
ITM_EVENT32_WITH_PC(1,data_main);
}
}
Для генерации лога событий должен быть подключен заголовочный файл arm_itm.h. В нем определен макрос ITM_EVENT32_WITH_PC, у которого первый параметр это канал, а второй это данные. Данными могут быть константа или переменная, канал может быть от 1 до 4.
Чтобы увидеть Event Log в сессии отладки, откройте и разрешите окно Event Log.
Сделайте правый клик в окне Event Log и выберите Enable.
Теперь можно запустить приложение в отладчике, и увидеть данные лога событий.
Данные канала 1 отображены в столбце ITM1, и данные канала 2 - в столбце ITM2. Также Вы можете увидеть метку времени в микросекундах (time stamp, столбец Time) и значение счетчика программы PC (столбец Program Counter), которые выводит в лог код макроса.
Также можно поменять отображаемое время в тактах CPU (Cycles).
Время, прошедшее между ITM1 56 и ITM1 57 составляет 48541271 – 47674478 = 866793. Это значение почти такое же, как мы видели в окне Data Log.
Имейте в виду:
• Использование макроса событий (ITM event macro) вводит небольшую дополнительную нагрузку в виде кода и времени выполнения. Поэтому использование этого макроса уместно для не слишком малых интервалов времени (которые значительно больше времени выполнения макроса). • В заголовочном файле arm_itm.h file имеются и другие макросы, такие как ITM_EVENT16_WITH_PC, ITM_EVENT16. Если Вам не нужна информация об адресе кода (не нужен столбец Program Counter), то для снижения нагрузки используйте макросы ITM_EVENT8, ITM_EVENT16, ITM_EVENT32.
Собранные данные можно сохранить в текстовый файл для последующего анализа.
[Просмотр Data Log и Event Log в окне Timeline]
Лог данных и событий можно просмотреть в окне шкалы времени Timeline, а одном и том же масштабе времени. Можно визуализировать, когда произошел каждый доступ к данным и когда произошло событие ITM, и узнать между ними разницу во времени. Для этого откройте окно I-jet/JTAGjet -> Timeline.
Вы увидите несколько цветовых полос. Сделайте правый клик на область Data Log и область Events, и выберите Enable.
Можно поменять шкалу времени в пункте Zoom контекстного меню (правый клик -> Zoom).
Данные Data Log и данные ITM Event отобразятся графически, вместе с метками времени.
При подведении курсора мыши к данным будет отображаться подробная информация о времени.
Можно выбрать диапазон времени, и тогда будет отображена информация об этом диапазоне.
Перед тем, как попытаться улучшить производительность кода, Вам следует настроить соответствующие средства измерения времени выполнения. Для измерения прошедшего времени существуют как традиционные техники, так и специфические для используемого MCU и отладчика. Вы можете адаптировать и улучшить техники, описанные в этой статье, чтобы они соответствовали требованиям отладки и разработки. См. также дополнительную информацию по описанному здесь функционалу в руководстве по отладке "C-SPY Debugging Guide", доступном в меню Help среды разработки IAR Embedded Workbench.
[Ссылки]
1. Techniques for measuring the elapsed time site:iar.com. |