Embedded HandbookПриложения
Все разделы › IX. Язык C: от основ до продвинутого
STM32F103, STM32F407 · уровень средний · 46 мин чтения
Открыть в интерактивном справочнике

75а. Профилировщик на самом камне: выборка счётчика команд по таймеру и через DWT

Раздел 75.2 показал профилировщик на компьютере и оставил для камня два приёма: счётчик тактов по участкам кода (73.1, 35.3) и учёт времени задач ядром ОСРВ (26.9, 33б). У обоих одно условие — надо заранее знать, что мерить: расставить метки или разбить работу на задачи. Когда неизвестно, где искать, нужен третий приём: прерывание таймера раз за разом смотрит, какую команду программа выполняла в этот миг. Тысяча таких выборок — и доля выборок в каждой функции равна её доле времени. Меток в коде нет вовсе.

Здесь два способа. Первый — свой обработчик прерывания таймера: работает на любом Cortex-M, весь код — 114 байт без выдачи. Он собран, прогнан в QEMU и сверен с истиной — счётом каждой выполненной команды. На этом же стенде пойманы две ловушки, из-за которых профиль врёт, не подавая виду: выборка в такт программе и участки с запретом прерываний. Второй — выборку ведёт само ядро: блок DWT шлёт адреса в поток ITM по линии SWO (раздел 21), и программа не тратит на это ни одной команды. Для него здесь настройка регистров и свой разбор потока, проверенный на синтетических данных.

Опоры: кадр исключения и EXC_RETURN — раздел 7а; SWO, ITM и пакеты — раздел 21; счётчик тактов — 35.3 и 73; трассировка задач — 33б. Документы: core_cm3.h и core_cm4.h из CMSIS 5.9.0 (биты DWT->CTRL), RM0090 и RM0008 (блоки DWT и ITM у F4 и F1), RM0468 (описание полей DWT_CTRL). Формат пакетов DWT взят из разборщика pyOCD 0.45.1 (github.com/pyocd/pyOCD, файл pyocd/trace/swo.py, лицензия Apache-2.0). Опыт — в образе stm-arm:1: arm-none-eabi-gcc 12.2.1, QEMU 7.2.22, машины netduinoplus2 (Cortex-M4, та же, что в 33б) и netduino2 (Cortex-M3; RCC, USART1 и TIM2 у неё по тем же адресам, что у F4).

Как это называется в документации

Оригинал Перевод
sampling (statistical) profiler профилировщик выборкой: раз в период смотрит, где программа
PC sample выборка счётчика команд
histogram bucket корзина: один счётчик на диапазон адресов
lockstep sampling выборка в такт программе: период кратен её обороту
randomized sampling period период выборки со случайной добавкой
skid снос: выборка приписана не той команде, что работала
periodic PC sample packet пакет периодической выборки PC от блока DWT
hardware source packet пакет от железа ядра, а не от программы
POSTCNT, POSTPRESET счётчик-делитель периода выборки в DWT и число, с которого он считает

75а.1 Идея: выборка вместо меток

Счётчик тактов отвечает на вопрос «сколько длится этот участок». Выборка отвечает на другой: «на что уходит время всей программы». Таймер прерывает программу через равные промежутки; обработчик записывает адрес команды, на которой её застал. Функция, занимающая 70 % времени, попадётся в 70 % выборок — как прохожие на фотографиях улицы, снятых раз в минуту.

Путь выборки: полоса времени программы из отрезков hot_a, hot_b и hot_c, под ней стрелки прерываний таймера с периодом, не кратным обороту программы; кадр исключения из восьми слов R0, R1, R2, R3, R12, LR, PC, xPSR, слово PC со смещением 24 выделено; обработчик SysTick_Handler выбирает стек по биту 2 EXC_RETURN и передаёт PC в prof_tick, та кладёт его в кольцо prof_pc или в корзины prof_hist; prof_dump выдаёт адреса по UART, на ПК prof75a.py по таблице символов ELF сводит их в таблицу: 10 000 выборок дали hot_a 69,89 %, hot_b 19,89 %, hot_c 10,22 % при истине 69,77, 19,99 и 10,04 %

Что это даёт и чего не даёт:


75а.2 Обработчик: PC из кадра исключения

На входе в прерывание ядро само кладёт на стек восемь слов (7а.1). Слово с индексом 6, смещение 24 байта, — PC: адрес команды, которая выполнилась бы следующей. Его и надо забрать. Тонкость одна: кадр лежит на том стеке, на котором работал прерванный код, — на MSP или на PSP; какой из двух, говорит бит 2 числа EXC_RETURN в LR (7а.3). Пролог из шести команд находит кадр и передаёт PC обычной функции на C:

__attribute__((naked)) void PROF_ISR(void)
{
    __asm volatile(
        "tst   lr, #4        \n"         /* бит 2 EXC_RETURN: на каком стеке лежит кадр */
        "ite   eq            \n"
        "mrseq r0, msp       \n"
        "mrsne r0, psp       \n"
        "ldr   r0, [r0, #24] \n"         /* слово 6 кадра — PC прерванной программы */
        "b     prof_tick     \n");
}

PROF_ISR — имя обработчика того таймера, который ведёт выборку. Их два на выбор, и разница между ними спрятана в три макроса:

#if PROF_TIM3                            /* выборку ведёт TIM3 */
#define PROF_ISR          TIM3_IRQHandler
#define PROF_ACK()        (TIM_SR(TIM3_BASE) = ~1u)          /* снять флаг обновления UIF */
#define PROF_PERIOD(t)    (TIM_ARR(TIM3_BASE) = (t) - 1u)
#else                                    /* выборку ведёт SysTick: флаг снимать не нужно */
#define PROF_ISR          SysTick_Handler
#define PROF_ACK()        ((void)0)
#define PROF_PERIOD(t)    (SYST_RVR = (t) - 1u)
#endif

Сама выборка — запись адреса в кольцо или прибавление единицы к корзине:

__attribute__((used)) void prof_tick(uint32_t pc)
{
    PROF_ACK();                          /* первым делом, иначе прерывание TIM3 повторится */
#if PROF_HIST
    uint32_t i = (pc - (uint32_t)&_stext) >> PROF_SHIFT;
    if (i >= PROF_BINS)
        prof_out++;                      /* адрес вне корзин: ОЗУ, загрузчик, чужой код */
    else if (prof_hist[i] != 0xFFFFu)
        prof_hist[i]++;                  /* насыщение: счётчик не обернётся в ноль */
#else
    prof_pc[prof_n & (PROF_RAW_N - 1u)] = pc;   /* кольцо: новая выборка затирает самую старую */
#ifdef PROF_TAG
    prof_tag[prof_n & (PROF_RAW_N - 1u)] = (uint32_t)PROF_TAG;
#endif
#endif
#if PROF_JITTER
    prof_rnd ^= prof_rnd << 13;          /* xorshift32: следующий период — случайной длины */
    prof_rnd ^= prof_rnd >> 17;
    prof_rnd ^= prof_rnd << 5;
    PROF_PERIOD(prof_period + (prof_rnd & PROF_JITTER));
#endif
    if (++prof_n == prof_limit)
        prof_stop();                     /* набрали заказанное — таймер стоп */
}

prof_tick возвращается обычным bx lr: в LR всё ещё лежит EXC_RETURN, и ядро само снимает кадр. Зачем строки PROF_JITTER и PROF_TAG — в 75а.4 и 75а.5. Запуск:

void prof_start(uint32_t period, uint32_t limit)
{
    prof_n = 0u;
    prof_limit = limit;                  /* 0 — без остановки: в кольце последние PROF_RAW_N выборок */
#if PROF_JITTER
    prof_period = period;
#endif
#if PROF_TIM3
    RCC_APB1ENR |= RCC_TIM3EN;
    TIM_CR1(TIM3_BASE) = 0u;
    TIM_PSC(TIM3_BASE) = PROF_TIM3_PSC;  /* TIM3 16-битный: период длиннее 65 536 отсчётов — через делитель */
    PROF_PERIOD(period);
    TIM_EGR(TIM3_BASE) = 1u;             /* UG: делитель и период действуют сразу */
    TIM_SR(TIM3_BASE) = 0u;              /* флаг от UG не нужен */
    TIM_DIER(TIM3_BASE) = 1u;            /* UIE: прерывание по обновлению */
    NVIC_IPR(TIM3_IRQ) = 0u;             /* приоритет 0 — срочнее всех */
    NVIC_ISER0 = 1u << TIM3_IRQ;
    TIM_CR1(TIM3_BASE) = 1u;             /* CEN */
#else
    SCB_SHPR_SYSTICK = 0u;               /* приоритет 0 — срочнее всех */
    PROF_PERIOD(period);                 /* период — в тактах ядра, до 2^24 */
    SYST_CVR = 0u;
    SYST_CSR = 7u;                       /* ENABLE | TICKINT | CLKSOURCE: счёт от частоты ядра */
#endif
}

Какой таймер. SysTick есть у каждого STM32, но в живой прошивке он обычно занят: тик HAL или ядра ОСРВ. Тогда выборку отдают свободному таймеру — сборка с PROF_TIM3=1, обработчик тот же. Опыты ниже идут на SysTick; вариант с TIM3 собран для F103 и F407, но не запускался: в QEMU 7.2 событие обновления TIM3 приходит не с периодом ARR — при ARR = 9999 между прерываниями прошло больше 100 000 отсчётов.

Cortex-M0. Команд tst с константой и ite там нет; в файле есть второй пролог из десяти команд, он выбирается сам по __ARM_ARCH_6M__ и собран для -mcpu=cortex-m0.

После сборки -Os обработчик — 6 команд пролога и 15 команд prof_tick: 21 команда на выборку. Набранное выдаёт prof_dump() — адреса шестнадцатерично, по восемь в строке:

prof raw n=10000
p 080002b4 080002b8 080002ac 080002b2 080002ce 080002aa 080002b0 080002b0
p 080002b4 080002b8 080002ac 080002b2 080002ce 080002aa 080002ba 080002b0

75а.3 Разбор на компьютере и первый профиль

Адреса превращает в имена таблица символов ELF. Команда arm-none-eabi-nm -n -S печатает для каждой функции адрес и размер; остаётся найти, в чей диапазон попал каждый адрес, и посчитать. Скрипт prof75a.py — пять функций (строки import и запуск из командной строки опущены):

def load_symbols(elf, nm="arm-none-eabi-nm"):
    """Функции из ELF: список (адрес, размер, имя) по возрастанию адреса."""
    out = subprocess.run([nm, "-n", "-S", "--defined-only", elf],
                         capture_output=True, text=True, check=True).stdout
    syms, seen = [], set()
    for line in out.splitlines():
        f = line.split()
        if len(f) == 4 and f[2] in "TtW" and int(f[0], 16) not in seen:
            seen.add(int(f[0], 16))             # слабые синонимы по тому же адресу — один раз
            syms.append((int(f[0], 16), int(f[1], 16), f[3]))
    return syms


def lookup(syms, pc):
    """Имя функции, в которую попадает адрес, или «?»."""
    i = bisect.bisect_right(syms, (pc, 1 << 32)) - 1
    if i >= 0 and pc < syms[i][0] + syms[i][1]:
        return syms[i][2]
    return "?"


def read_dump(path):
    """Вернуть (сырые адреса, корзины): корзины — список (начало, размер, счётчик)."""
    raw, bins, base, shift, k = [], [], 0, 0, 0
    for line in open(path, encoding="ascii", errors="replace"):
        m = re.match(r"prof hist base=0x([0-9a-f]+) shift=(\d+)", line)
        if m:
            base, shift, k = int(m.group(1), 16), int(m.group(2)), 0
        elif line.startswith("p "):
            raw += [int(x, 16) for x in line.split()[1:]]
        elif line.startswith("h "):
            for x in line.split()[1:]:
                if int(x, 16):
                    bins.append((base + (k << shift), 1 << shift, int(x, 16)))
                k += 1
    return raw, bins


def by_function(syms, raw, bins):
    """Счёт выборок по функциям. Корзина на стыке функций делится между ними по числу байт."""
    cnt = collections.Counter()
    for pc in raw:
        cnt[lookup(syms, pc)] += 1
    for start, size, n in bins:
        owners = [lookup(syms, a) for a in range(start, start + size, 2)]   # команды Thumb — по чётным адресам
        owners = [o for o in owners if o != "?"] or ["?"]                   # заполнитель между функциями не в счёт
        for o in owners:
            cnt[o] += n / len(owners)
    return cnt


def report(cnt):
    total = sum(cnt.values())
    print("samples %d" % round(total))
    for name, n in sorted(cnt.items(), key=lambda kv: (-kv[1], kv[0])):
        print("  %6.2f %%  %8.1f  %s" % (100.0 * n / total, n, name))

Контрольная программа. Чтобы проверить профилировщик, нужна программа, про которую ответ известен заранее. Три одинаковых цикла, доля времени задаётся числом оборотов:

__attribute__((noinline)) void hot_a(uint32_t n)
{
    for (uint32_t i = 0; i < n; i++)
        __asm volatile("nop");
}
        while (prof_n < limit) {
            hot_a(700);
            hot_b(200);
            hot_c(100);
        }

Истина. QEMU запущен с ключом -icount shift=0: модельное время идёт по командам, одна команда — одна наносекунда, и прогон повторяется команда в команду. С ключами -singlestep -d exec эмулятор пишет в журнал каждую выполненную команду с именем функции. По журналу один оборот цикла — ровно 5022 команды: hot_a — 3504 (69,77 %), hot_b — 1004 (19,99 %), hot_c — 504 (10,04 %), сам цикл в run — 10 (0,20 %). Это и есть истинные доли: в QEMU команды стоят одинаково.

Профиль. В SysTick->LOAD — 2012: при частоте ядра 168 МГц у этой машины QEMU выходит одна выборка в 11 982 команды. Таблица prof75a.py по 10 000 выборок:

samples 10000
   69.89 %    6989.0  hot_a
   19.89 %    1989.0  hot_b
   10.22 %    1022.0  hot_c

То же по первым 100 и 1000 выборкам, рядом с истиной (последний столбец — наибольшее расхождение, в процентных пунктах):

N            hot_a     hot_b     hot_c       run   max error
truth        69.77     19.99     10.04      0.20
100          70.00     20.00     10.00      0.00   0.23
1000         69.80     19.90     10.30      0.00   0.26
10000        69.89     19.89     10.22      0.00   0.20

Профиль верен до четверти процента уже на ста выборках. Но две вещи должны насторожить. Ошибка не падает: десять тысяч выборок не точнее сотни. И run не получила ни одной выборки из десяти тысяч, хотя ей положено двадцать. Обе странности — одна ловушка.


75а.4 Две ловушки: выборка в такт программе и запрет прерываний

Выборка в такт программе

Между двумя выборками программа проходит 11 982 − 21 = 11 961 команду: 21 съедает сам обработчик. Оборот цикла — 5022 команды, то есть за одну выборку точка внутри оборота сдвигается на 11 961 − 2 × 5022 = 1917 команд. У чисел 1917 и 5022 общий делитель 27. Значит, выборка обходит всего 5022 / 27 = 186 точек оборота и дальше ходит по ним по кругу. Разбор дампа это подтвердил: последовательность адресов повторяется каждые 186 выборок, разных адресов среди десяти тысяч — 15. Команды run в эти 186 точек не попали — и не попадут никогда. После 186-й выборки профиль новых сведений не получает.

Хуже, когда период подобран «удачно»:

LOAD + 1 Период, команд Что вышло
1691 10 065 = 2 × 5022 + 21 все 1000 выборок — один адрес 0x080002b0: hot_a — 100 %
1681 10 005 сдвиг 60 команд за выборку; первые 100 выборок: 58 / 25 / 16 / 1 % — ошибка 11,77
2013 11 982 186 точек оборота; ошибка 0,2 и не падает

Свободный цикл попадает в такт случайно. Но есть программы, которые в такте всегда: главный цикл, работающий по таймеру.

__attribute__((noinline)) void wait_tick(void)
{
    while ((int32_t)(now() - next_at) < 0) {
    }
    next_at += TICK;
}
    while (prof_n < limit) {
        wait_tick();
        control();
        filter();
    }

Раз в 9999 команд — управление и фильтр, остальное время — ожидание. Истина по журналу: wait_tick — 59,82 %, control — 30,08 %, filter — 10,03 %. Выборка с тем же периодом 9999 команд — естественное желание «мерить раз в тик» — дала три прогона и три разных ответа, смотря когда главный цикл стартовал относительно выборки:

Сдвиг старта, команд Профиль по 1000 выборок
1000 wait_tick — 100 %
8000 control — 100 %
6500 filter — 100 %

Каждый ответ выглядит убедительно и каждый ложный. Период на шесть команд длиннее (10 005) — тоже плохо: за 1000 выборок точка проползла 6000 команд из 9999, и вышло 33,20 / 50,10 / 16,70 % при истине 59,82 / 30,08 / 10,03. Период 10 577, далёкий от кратного, дал 60,00 / 30,20 / 9,80 % — ошибка 0,23.

Лечение — случайная добавка к периоду. Сборка с PROF_JITTER=2047: после каждой выборки обработчик прибавляет к периоду случайное число от 0 до 2047 тактов таймера (строки xorshift32 в prof_tick). Основной период взят нарочно худший — 1691, тот самый, что давал один адрес. Со случайной добавкой 100 000 выборок контрольной программы дали 69,98 / 19,91 / 9,93 / 0,18 % — и run получила свои 0,18 % при истинных 0,20. Последовательность больше не повторяется.

Цена — честная статистика. Выборки стали независимыми, и ошибка теперь подчиняется закону √N: для функции с долей p среднеквадратичное отклонение равно √(p(1 − p) / N). Те же 100 000 выборок, разбитые на блоки по N; в каждой клетке — измеренный разброс и теория, в процентных пунктах:

N в блоке Блоков hot_a hot_b hot_c
100 1000 4,59 / 4,59 4,07 / 4,00 2,90 / 3,00
1000 100 1,54 / 1,45 1,25 / 1,26 0,97 / 0,95
10 000 10 0,47 / 0,46 0,37 / 0,40 0,36 / 0,30

Измеренное совпало с теорией. Отсюда правило: вдесятеро точнее — в сто раз больше выборок. Тысяча выборок различает функции с точностью около процента — для поиска узкого места этого хватает. Программа с главным циклом по таймеру с той же добавкой дала на 1000 выборок 58,80 / 32,40 / 8,70 % — ошибка 2,32 при теоретическом разбросе 1,45.

Запрет прерываний

Вторая ловушка — код, в который прерывание войти не может.

__attribute__((noinline)) void irq_off(void)
{
    __asm volatile("cpsid i");
}

__attribute__((noinline)) void irq_on(void)
{
    __asm volatile("cpsie i");
}

__attribute__((noinline)) void locked(uint32_t n)
{
    irq_off();
    for (uint32_t i = 0; i < n; i++)
        __asm volatile("nop");
    irq_on();
}

Программа по очереди зовёт locked(300) и hot_a(700). По журналу locked занимает 30,00 % времени, irq_on — 0,04 %: в ней две команды. Профиль по 10 000 выборок (с добавкой к периоду):

samples 10000
   69.89 %    6989.0  hot_a
   29.92 %    2992.0  irq_on
    0.15 %      15.0  main
    0.03 %       3.0  locked
    0.01 %       1.0  irq_off

Всё время locked досталось irq_on. Таймер сработал внутри закрытого участка, прерывание ждало — и вошло сразу после cpsie i. Все 2992 выборки лежат на одном адресе 0x080002fc: это bx lr, следующая команда за cpsie i; addr2line называет строку 169 — закрывающую скобку irq_on. Доля верная, место — нет: время приписано первой команде после участка.

Если участок длиннее периода выборки, теряется и доля. Прерывание умеет ждать только в единственном числе: сколько бы раз таймер ни сработал за время запрета, выборка будет одна. locked(7500) — это 37 507 команд, около двух периодов. Истина та же, 30,00 %; профиль показал 17,10 % у irq_on и 82,89 % у hot_a. На те же 10 000 выборок ушло 213 734 338 команд против 180 510 156 в прошлом прогоне: 15,5 % срабатываний таймера пропали.

Чужой обработчик — тот же запрет. Прерывание не вытесняет обработчик с тем же или более срочным приоритетом. В опыте обработчик EXTI0 крутит isr_work — 29,97 % времени по журналу. При равном приоритете с выборкой isr_work не получила ни одной выборки из 10 000, а 30,35 % легли на main: из них 30,26 — на одну команду по адресу 0x08000366, следующую за той, что подняла прерывание. Стоило сделать EXTI0 менее срочным (приоритет 0x80) — и isr_work получила свои 30,22 %. Поэтому prof_start ставит выборке приоритет 0.


75а.5 Цена, корзины и задачи ОСРВ

Команды на выборку. Одна и та же работа прогнана без профилировщика и с ним; разность счётчика команд, делённая на число выборок, — целое число, на Cortex-M3 и Cortex-M4 одинаковое:

Сборка Команд на выборку
кольцо сырых адресов 21
корзины 30
кольцо + случайная добавка 32
корзины + случайная добавка 41

Такты на плате не мерялись. Оценка сверху по правилу из 9.7 — по 12 тактов на вход и выход и по два такта на команду: 12 + 12 + 2 × 21 = 66 тактов.

1000 выборок в секунду 10 000 в секунду
F103, 72 МГц 0,09 % 0,92 %
F407, 168 МГц 0,04 % 0,39 %

Тысяча выборок в секунду почти бесплатна. Самая дорогая сборка, 41 команда, — это 106 тактов и 1,47 % на 72 МГц при 10 000 выборок в секунду.

Память (-Os, Cortex-M3, адреса F103). Пролог, prof_tick, prof_start и prof_stop — 20 + 44 + 40 + 10 = 114 байт; prof_dump — ещё 118; весь объектный файл — 256 байт кода и строк. ОЗУ — 4104 байта: кольцо на 1024 выборки по 4 байта и два счётчика. Сборка с корзинами — 348 байт кода и 524 байта ОЗУ: 256 корзин по 2 байта покрывают 4 КБ кода.

Кольцо или корзины. Кольцо хранит адрес каждой выборки: точность — до команды, но 10 000 выборок — это 40 КБ, вдвое больше всего ОЗУ F103C8. Корзины — счётчик на диапазон адресов: память не зависит от числа выборок, копить можно часами. Расплата — точность. Одни и те же 100 000 выборок, разложенные по корзинам разного размера (код программы — 760 байт):

Корзина, байт Корзин ОЗУ, байт hot_a hot_b hot_c run Ошибка
сырые адреса — 400 000 69,98 19,91 9,93 0,18 0
2 380 760 69,98 19,91 9,93 0,18 0
4 190 380 63,02 26,87 8,94 1,16 6,96
16 48 96 43,45 31,98 10,22 3,86 26,53

Корзина в 2 байта точна — команды Thumb лежат по чётным адресам, — но ОЗУ под неё столько же, сколько кода. Крупнее — и корзина накрывает стык функций: hot_a занимает 14 байт, и её горячие команды делят корзину с соседкой. Прошивка с корзинами по 16 байт показала hot_a — 46,06 %, а 13,95 % отдала prof_dump, которая в это время не работала: она просто лежит перед hot_a.

Лечение — поставить функции на границы корзин. Ключ -ffunction-sections и слово SUBALIGN(16) у секции .text в скрипте компоновщика выравнивают каждую функцию по 16 байт. Код вырос с 1064 до 1208 байт, зато те же корзины по 16 байт дали 69,20 / 20,23 / 10,34 / 0,23 %. В настоящей прошивке функции крупнее четырнадцати байт, и на стыках лежит малая часть выборок; но проверить это стоит на своей программе — одним прогоном с кольцом.

Под ОСРВ. Задачи работают на PSP, и кадр прерванной задачи лежит на её стеке (7а.3). Пролог из 75а.2 это уже учитывает: контрольная программа, переведённая на PSP, дала те же 686, 193 и 121 выборку из 1000, что и на MSP. Остаётся вопрос «чьё это время»: одну функцию зовут разные задачи. Ответ — класть рядом с адресом указатель на блок текущей задачи. Сборка с -DPROF_TAG=pxCurrentTCB добавляет в prof_tick одну запись (строки PROF_TAG в 75а.2) и 4 байта на выборку. Опыт — FreeRTOS 10.5.1 (github.com/FreeRTOS/FreeRTOS-Kernel, лицензия MIT), порт ARM_CM3; две задачи зовут одну и ту же hot_a:

static void task_motor(void *arg)
{
    (void)arg;
    prof_start(PERIOD, cfg_samples);     /* после старта планировщика: он опускает приоритет SysTick */
    for (;;) {
        hot_a(600);
        if (prof_n >= cfg_samples)
            finish();
        taskYIELD();
    }
}

static void task_link(void *arg)
{
    (void)arg;
    for (;;) {
        hot_a(300);
        hot_b(100);
        taskYIELD();
    }
}

Перед дампом прошивка печатает таблицу «адрес блока — имя» (pcTaskGetName), и разбор делит 5000 выборок по задачам:

samples 5000, tags 5000
   60.46 %    3023  task motor
   39.54 %    1977  task link
   59.06 %    2953  motor / hot_a
   29.02 %    1451  link / hot_a
    9.14 %     457  link / hot_b

По функциям hot_a — 88,08 % (по журналу 87,72 %), и эти проценты делятся между задачами как два к одному — как 600 оборотов к 300. Переключение задач (vTaskSwitchContext и PendSV_Handler) заняло 2,36 % выборок при 2,02 % по журналу. Три оговорки. Выборка идёт с приоритетом 0 — срочнее порога configMAX_SYSCALL_INTERRUPT_PRIORITY, поэтому видит и критические секции ядра, но звать функции ядра из неё нельзя (28.6); чтение одного указателя — можно. В опыте SysTick отдан профилировщику, тик ядра выключен, задачи уступают друг другу сами; на плате тик остаётся ядру, а выборке дают TIMx. И учёт времени задач ядром (33б.6, 35.3) даёт те же доли по задачам дешевле — выборка нужна, когда вопрос «какая функция внутри задачи».


75а.6 Выборка самим ядром: DWT и ITM

У Cortex-M3 и M4 выборку умеет делать железо. RM0090 и RM0008 называют среди ролей блока DWT «PC sampler», а среди пакетов ITM — программные, от записи в канал (раздел 21), и аппаратные: их порождает DWT, а ITM только отправляет. Обработчика нет, программа не тратит ни одной команды. Цена другая: нужен вывод SWO и отладчик, который его принимает (21.1).

Биты управления — в DWT->CTRL. Позиции взяты из core_cm3.h и core_cm4.h (они совпадают), смысл полей — из RM0468, где этот регистр расписан для Cortex-M7 той же архитектуры; RM0090 и RM0008 полей не описывают:

Поле Биты Что делает
CYCCNTENA 0 включает счётчик тактов CYCCNT (35.3)
POSTPRESET 4:1 число, с которого счётчик-делитель POSTCNT считает вниз
POSTINIT 8:5 его начальное значение; пишется, только пока POSTCNT выключен
CYCTAP 9 по какому биту CYCCNT шагает POSTCNT: 0 — бит 6, 1 — бит 10
PCSAMPLENA 12 POSTCNT работает таймером пакетов периодической выборки PC
CYCEVTENA 22 второй потребитель POSTCNT — пакеты счётчика событий

Ещё один бит — в ITM->TCR: DWTENA, бит 3, пропускает пакеты DWT в поток. Настройка целиком; линию SWO, ключ ITM и делитель скорости готовит swo_init() из 21.2:

/* Включить периодическую выборку PC: tap — PCS_TAP_64 или PCS_TAP_1024, reload — 0…15. */
void pcs_start(uint32_t tap, uint32_t reload)
{
    swo_init();
    ITM->TCR |= ITM_TCR_DWTENA_Msk;      /* пропускать пакеты DWT в поток ITM */

    /* POSTINIT пишется, только пока POSTCNT выключен: сначала снять оба разрешения */
    DWT->CTRL &= ~(DWT_CTRL_PCSAMPLENA_Msk | DWT_CTRL_CYCEVTENA_Msk);
    DWT->CYCCNT = 0u;
    DWT->CTRL = (DWT->CTRL & ~(DWT_CTRL_CYCTAP_Msk | DWT_CTRL_POSTPRESET_Msk | DWT_CTRL_POSTINIT_Msk))
              | (tap << DWT_CTRL_CYCTAP_Pos)
              | (reload << DWT_CTRL_POSTPRESET_Pos)      /* с какого числа POSTCNT считает вниз */
              | (reload << DWT_CTRL_POSTINIT_Pos)        /* и с какого начинает */
              | DWT_CTRL_CYCCNTENA_Msk;                  /* POSTCNT шагает по биту CYCCNT: счётчик тактов должен идти */
    DWT->CTRL |= DWT_CTRL_PCSAMPLENA_Msk;                /* выборка пошла */
}

Период. Бит 6 двоичного счётчика переключается раз в 64 такта, бит 10 — раз в 1024: это шаг POSTCNT. Счётчик четырёхбитный; если считать, что от 15 до перезагрузки он делает 16 шагов, самая редкая выборка — раз в 1024 × 16 = 16 384 такта: 4394 выборки в секунду на 72 МГц и 10 254 на 168 МГц. На плате период не мерялся.

Пакет. Формат — по разборщику pyOCD. Заголовок пакета источника устроен как в 21.6: биты 1:0 — размер данных (1, 2 или 4 байта), биты 7:3 — номер источника. Добавляется бит 2: ноль — пишет программа, единица — железо. У железа номер 0 — счётчики событий, 1 — трассировка исключений, 2 — периодическая выборка PC. Отсюда заголовок выборки 0x17 и четыре байта адреса младшим вперёд; адрес 0x080002b4 на линии — 17 b4 02 00 08. Нулевой адрес pyOCD трактует как выборку во сне: PC нет. В синтетическом потоке ниже такой пакет короткий — 15 00. Пять байт на выборку при 200 000 байт в секунду на линии 2 МГц (21.7) — это потолок в 40 000 выборок в секунду на весь поток.

Разборщик itm75a.py — одна функция:

def parse(data, wait_sync=False):
    """Вернуть список событий (тип, значение, …). Типы: pc, sleep, itm, cnt, exc, dwt, sync, ovf, ts, bad."""
    i, out = 0, []
    if wait_sync:                               # подключились посреди потока: ждать синхропакет
        i = data.find(SYNC)
        i = len(data) if i < 0 else i
    while i < len(data):
        h = data[i]
        i += 1
        if h == 0x00:                           # синхропакет: нули и завершающий 0x80
            j = i
            while j < len(data) and data[j] == 0x00:
                j += 1
            if j < len(data) and data[j] == 0x80 and j - i >= 4:
                out.append(("sync",))
            else:
                out.append(("bad", h))
            i = j + 1
        elif h == 0x70:                         # переполнение: часть пакетов потеряна
            out.append(("ovf",))
        elif h & 0x03 == 0:                     # служебный пакет: метка времени или расширение
            while h & 0x80 and i < len(data):   # бит 7 — «дальше ещё байт»
                h = data[i]
                i += 1
            out.append(("ts",))
        else:                                   # пакет источника: 1, 2 или 4 байта данных
            size = 1 << ((h & 0x03) - 1)
            if i + size > len(data):
                break                           # хвост потока оборван посреди пакета
            value = int.from_bytes(data[i:i + size], "little")
            i += size
            src = h >> 3
            if h & 0x04 == 0:
                out.append(("itm", src, value, size))       # запись программы в канал src
            elif src == 2:                                  # периодическая выборка PC
                out.append(("pc", value) if size == 4 else ("sleep", value))
            elif src == 0:
                out.append(("cnt", value))                  # переполнение счётчика событий DWT
            elif src == 1:
                out.append(("exc", value))                  # вход в исключение и выход
            else:
                out.append(("dwt", src, value))             # трассировка данных
    return out

Проверка. QEMU блоки DWT и ITM не моделирует: после записи DEMCR, ITM->TCR и DWT->CTRL читаются нулями, CYCCNT стоит. Поэтому поток синтетический: 10 000 адресов из опыта 75а.3 упакованы в пакеты выборки вперемешку со всем, что бывает на линии, — записями в каналы 0 и 1, метками времени, трассировкой исключений, счётчиками событий, пакетами сна, переполнениями и синхропакетами:

packet for pc 0x080002b4: 17 b4 02 00 08
stream 54887 bytes from 10000 samples: pc=10000 sleep=156 itm=1629 cnt=20 exc=100 dwt=0 sync=11 ovf=10 ts=101 bad=0
decoded = source: yes

Адреса вернулись все и в том же порядке; таблица prof75a.py по разобранному потоку совпала с таблицей по дампу UART. Тот же поток разобран разборщиком pyOCD — события совпали. Поток, обрезанный посреди пакета, начался с ложной записи в канал 0; с ожиданием синхропакета 00 00 00 00 00 80 (ключ --sync) разбор начинается с целого пакета.

Чем отличается от способа A. Выборка DWT — не прерывание, поэтому запрет прерываний и чужие обработчики мешать ей не должны: вторая ловушка из 75а.4 к ней не относится. Первая остаётся: период выбирается из немногих ступеней, случайной добавки нет. В Keil то же включается в настройках трассировки (PC Sampling), записи видны в окне Trace Records как «PC Sample». Есть и готовые открытые приёмники такого потока — например, Orbuculum (github.com/orbcode/orbuculum); здесь он не запускался. На железе способ B в разделе не проверялся.


75а.7 Грабли

Профиль не уточняется, сколько ни копи; короткая функция не получила ни одной выборки. Программа периодична, период выборки постоянный — выборка ходит по кругу по немногим точкам. В опыте их было 186 из 5022. Лечит случайная добавка к периоду.

Одна функция — 100 %. Период выборки кратен периоду программы: главный цикл по таймеру и выборка «раз в тик». Три прогона дали 100 % трём разным функциям. Период не кратный и не близкий к кратному, а лучше — со случайной добавкой.

В профиле «горячая» функция из двух команд. Это конец участка с запретом прерываний: время закрытого участка досталось команде после cpsie i. В опыте — 29,92 % у irq_on. Смотреть надо на то, что было перед этим адресом.

Долгий закрытый участок получил меньше, чем должен. За время запрета запоминается одно прерывание. Участок в два периода выборки показал 17,10 % вместо 30,00 %.

Обработчик прерывания не виден, а код после него «горячий». У выборки приоритет не срочнее, чем у него. В опыте 30,35 % ушли в main; с приоритетом выборки выше — 30,22 % у обработчика.

Все адреса — нули или мусор. Кадр взят не с того стека. Основной код на PSP, а пролог читает MSP: в опыте все 1000 выборок дали адрес 0x00000000. Стек выбирают по биту 2 EXC_RETURN.

Функция пропала из профиля. Компилятор её встроил. Та же программа при -O2: 100 % у main, hot_a нет вовсе. Сырые адреса это лечат — arm-none-eabi-addr2line -f -i разворачивает встроенные вызовы:

hot_a at app75a.c:27
 (inlined by) run at app75a.c:79
 (inlined by) main at app75a.c:97

Корзины показали функцию, которая не работала. Корзина накрыла стык функций: 13,95 % у prof_dump при корзине 16 байт и функциях по 14. Корзина мельче или функции, выровненные по её границе.

Имена не те. Адреса переводит в имена только ELF той самой сборки (35в.7): после любой пересборки функции переезжают.

Поток SWO начинается с мусора. Приёмник включился посреди пакета. Ждать синхропакет.

В QEMU профиль по TIM3 не сходится. Модель таймера в QEMU 7.2 не повторяет обновление с периодом ARR; для опытов в эмуляторе — SysTick.


75а.8 Итоги

Полные исходники

prof75a.c — профилировщик выборкой.

/* Раздел 75а: профилировщик выборкой. Прерывание таймера берёт из кадра исключения PC
 * прерванной программы и кладёт его в кольцо (PROF_HIST = 0) или в гистограмму (PROF_HIST = 1). */
#include "prof75a.h"
#include "io75a.h"

#if PROF_TIM3                            /* выборку ведёт TIM3 */
#define PROF_ISR          TIM3_IRQHandler
#define PROF_ACK()        (TIM_SR(TIM3_BASE) = ~1u)          /* снять флаг обновления UIF */
#define PROF_PERIOD(t)    (TIM_ARR(TIM3_BASE) = (t) - 1u)
#else                                    /* выборку ведёт SysTick: флаг снимать не нужно */
#define PROF_ISR          SysTick_Handler
#define PROF_ACK()        ((void)0)
#define PROF_PERIOD(t)    (SYST_RVR = (t) - 1u)
#endif

extern uint32_t _stext;                  /* начало кода — из скрипта компоновщика */

volatile uint32_t prof_n;
static uint32_t prof_limit;
#if PROF_HIST
uint16_t prof_hist[PROF_BINS];
uint32_t prof_out;
#else
uint32_t prof_pc[PROF_RAW_N];
#ifdef PROF_TAG                          /* метка к каждой выборке, например текущая задача ОСРВ */
extern void *volatile PROF_TAG;
uint32_t prof_tag[PROF_RAW_N];
#endif
#endif
#if PROF_JITTER
static uint32_t prof_period;
static uint32_t prof_rnd = 2463534242u;
#endif

#if defined(__ARM_ARCH_6M__)             /* Cortex-M0/M0+: команд tst с константой и ite там нет */
__attribute__((naked)) void PROF_ISR(void)
{
    __asm volatile(
        "movs  r0, #4        \n"
        "mov   r1, lr        \n"
        "tst   r1, r0        \n"         /* бит 2 EXC_RETURN: на каком стеке лежит кадр */
        "bne   1f            \n"
        "mrs   r0, msp       \n"
        "b     2f            \n"
        "1: mrs r0, psp      \n"
        "2: ldr r0, [r0, #24]\n"         /* слово 6 кадра — PC прерванной программы */
        "ldr   r1, =prof_tick\n"
        "bx    r1            \n");
}
#else
__attribute__((naked)) void PROF_ISR(void)
{
    __asm volatile(
        "tst   lr, #4        \n"         /* бит 2 EXC_RETURN: на каком стеке лежит кадр */
        "ite   eq            \n"
        "mrseq r0, msp       \n"
        "mrsne r0, psp       \n"
        "ldr   r0, [r0, #24] \n"         /* слово 6 кадра — PC прерванной программы */
        "b     prof_tick     \n");
}
#endif

__attribute__((used)) void prof_tick(uint32_t pc)
{
    PROF_ACK();                          /* первым делом, иначе прерывание TIM3 повторится */
#if PROF_HIST
    uint32_t i = (pc - (uint32_t)&_stext) >> PROF_SHIFT;
    if (i >= PROF_BINS)
        prof_out++;                      /* адрес вне корзин: ОЗУ, загрузчик, чужой код */
    else if (prof_hist[i] != 0xFFFFu)
        prof_hist[i]++;                  /* насыщение: счётчик не обернётся в ноль */
#else
    prof_pc[prof_n & (PROF_RAW_N - 1u)] = pc;   /* кольцо: новая выборка затирает самую старую */
#ifdef PROF_TAG
    prof_tag[prof_n & (PROF_RAW_N - 1u)] = (uint32_t)PROF_TAG;
#endif
#endif
#if PROF_JITTER
    prof_rnd ^= prof_rnd << 13;          /* xorshift32: следующий период — случайной длины */
    prof_rnd ^= prof_rnd >> 17;
    prof_rnd ^= prof_rnd << 5;
    PROF_PERIOD(prof_period + (prof_rnd & PROF_JITTER));
#endif
    if (++prof_n == prof_limit)
        prof_stop();                     /* набрали заказанное — таймер стоп */
}

void prof_start(uint32_t period, uint32_t limit)
{
    prof_n = 0u;
    prof_limit = limit;                  /* 0 — без остановки: в кольце последние PROF_RAW_N выборок */
#if PROF_JITTER
    prof_period = period;
#endif
#if PROF_TIM3
    RCC_APB1ENR |= RCC_TIM3EN;
    TIM_CR1(TIM3_BASE) = 0u;
    TIM_PSC(TIM3_BASE) = PROF_TIM3_PSC;  /* TIM3 16-битный: период длиннее 65 536 отсчётов — через делитель */
    PROF_PERIOD(period);
    TIM_EGR(TIM3_BASE) = 1u;             /* UG: делитель и период действуют сразу */
    TIM_SR(TIM3_BASE) = 0u;              /* флаг от UG не нужен */
    TIM_DIER(TIM3_BASE) = 1u;            /* UIE: прерывание по обновлению */
    NVIC_IPR(TIM3_IRQ) = 0u;             /* приоритет 0 — срочнее всех */
    NVIC_ISER0 = 1u << TIM3_IRQ;
    TIM_CR1(TIM3_BASE) = 1u;             /* CEN */
#else
    SCB_SHPR_SYSTICK = 0u;               /* приоритет 0 — срочнее всех */
    PROF_PERIOD(period);                 /* период — в тактах ядра, до 2^24 */
    SYST_CVR = 0u;
    SYST_CSR = 7u;                       /* ENABLE | TICKINT | CLKSOURCE: счёт от частоты ядра */
#endif
}

void prof_stop(void)
{
#if PROF_TIM3
    TIM_CR1(TIM3_BASE) = 0u;
#else
    SYST_CSR = 0u;
#endif
}

void prof_dump(void)
{
    uint32_t n = prof_n;
#if PROF_HIST
    puts1("prof hist base=0x");
    puthex((uint32_t)&_stext, 8);
    puts1(" shift=");
    putdec(PROF_SHIFT);
    puts1(" n=");
    putdec(n);
    puts1(" out=");
    putdec(prof_out);
    for (uint32_t i = 0; i < PROF_BINS; i++) {
        puts1((i % 16u) ? " " : "\nh ");
        puthex(prof_hist[i], 4);
    }
#else
    uint32_t first = (n > PROF_RAW_N) ? n - PROF_RAW_N : 0u;
    puts1("prof raw n=");
    putdec(n);
    for (uint32_t i = first; i < n; i++) {
        puts1(((i - first) % 8u) ? " " : "\np ");
        puthex(prof_pc[i & (PROF_RAW_N - 1u)], 8);
    }
#ifdef PROF_TAG
    for (uint32_t i = first; i < n; i++) {
        puts1(((i - first) % 8u) ? " " : "\ng ");
        puthex(prof_tag[i & (PROF_RAW_N - 1u)], 8);
    }
#endif
#endif
    puts1("\nend\n");
}

app75a.c — контрольные программы для профилировщика.

/* Раздел 75а: контрольные программы для профилировщика. QEMU, -icount shift=0: одна команда —
 * один отсчёт TIM2. SCEN 1 — три функции с долями около 70/20/10 %; 2 — главный цикл с жёстким
 * периодом (ловушка синхронной выборки); 3 — участок с запретом прерываний; 4 — работа в чужом
 * обработчике; 5 — цена одной выборки. */
#include "prof75a.h"
#include "io75a.h"

#ifndef SCEN
#define SCEN 1
#endif
#ifndef PERIOD
#define PERIOD 1681u                     /* период выборки в тактах SysTick */
#endif
#ifndef SAMPLES
#define SAMPLES 10000u
#endif
#ifndef CHUNKS
#define CHUNKS 1u
#endif

/* сколько выборок набрать — константой в данных: код прошивки от её значения не зависит */
const volatile uint32_t cfg_samples = SAMPLES;

/* три одинаковых цикла: доля времени задаётся числом оборотов */
__attribute__((noinline)) void hot_a(uint32_t n)
{
    for (uint32_t i = 0; i < n; i++)
        __asm volatile("nop");
}

__attribute__((noinline)) void hot_b(uint32_t n)
{
    for (uint32_t i = 0; i < n; i++)
        __asm volatile("nop");
}

__attribute__((noinline)) void hot_c(uint32_t n)
{
    for (uint32_t i = 0; i < n; i++)
        __asm volatile("nop");
}

#if SCEN == 1
#if USE_PSP
static uint32_t pstack[128];

__attribute__((naked, noinline)) static void thread_on_psp(__attribute__((unused)) uint32_t *top)
{
    __asm volatile(
        "msr   psp, r0     \n"
        "movs  r0, #2      \n"           /* CONTROL.SPSEL = 1: основной код — на PSP, как задача ОСРВ */
        "msr   control, r0 \n"
        "isb               \n"
        "bx    lr          \n");
}
#endif

#if BAD_MSP
/* Нарочно неверный пролог: кадр всегда берётся с MSP. В опыте подменяет обработчик из prof75a.c
 * (там символ ослаблен командой objcopy --weaken-symbol). */
void prof_tick(uint32_t pc);

__attribute__((naked)) void SysTick_Handler(void)
{
    __asm volatile(
        "mrs   r0, msp       \n"
        "ldr   r0, [r0, #24] \n"
        "b     prof_tick     \n");
}
#endif

__attribute__((noinline)) static void run(void)
{
    uint32_t limit = cfg_samples;
    for (uint32_t c = 0; c < CHUNKS; c++) {
        uint32_t t0 = now();
        prof_start(PERIOD, limit);
        while (prof_n < limit) {
            hot_a(700);
            hot_b(200);
            hot_c(100);
        }
        puts1("span=");
        putdec(now() - t0);
        puts1("\n");
        prof_dump();
    }
}

int main(void)
{
    uart_init();
    clock_start();
#if USE_PSP
    thread_on_psp(&pstack[128]);
#endif
    run();
    sh_exit();
    for (;;) {
    }
}
#endif

#if SCEN == 2
/* главный цикл с жёстким периодом: раз в TICK команд — управление и фильтр, остальное — ожидание */
#ifndef TICK
#define TICK 9999u
#endif
#ifndef PHASE
#define PHASE 1000u                      /* когда первый запуск цикла относительно старта выборки */
#endif
static uint32_t next_at;

__attribute__((noinline)) void wait_tick(void)
{
    while ((int32_t)(now() - next_at) < 0) {
    }
    next_at += TICK;
}

__attribute__((noinline)) void control(void)
{
    for (uint32_t i = 0; i < 1000u; i++)
        __asm volatile("nop");
}

__attribute__((noinline)) void filter(void)
{
    for (uint32_t i = 0; i < 333u; i++)
        __asm volatile("nop");
}

int main(void)
{
    uint32_t limit = cfg_samples;
    uart_init();
    clock_start();
    prof_start(PERIOD, limit);
    next_at = now() + PHASE;
    while (prof_n < limit) {
        wait_tick();
        control();
        filter();
    }
    prof_dump();
    sh_exit();
    for (;;) {
    }
}
#endif

#if SCEN == 3
/* участок с запретом прерываний: LOCK_N оборотов под cpsid, OPEN_N — с открытыми прерываниями */
#ifndef LOCK_N
#define LOCK_N 300u
#endif
#ifndef OPEN_N
#define OPEN_N 700u
#endif

__attribute__((noinline)) void irq_off(void)
{
    __asm volatile("cpsid i");
}

__attribute__((noinline)) void irq_on(void)
{
    __asm volatile("cpsie i");
}

__attribute__((noinline)) void locked(uint32_t n)
{
    irq_off();
    for (uint32_t i = 0; i < n; i++)
        __asm volatile("nop");
    irq_on();
}

int main(void)
{
    uint32_t limit = cfg_samples;
    uart_init();
    clock_start();
    uint32_t t0 = now();
    prof_start(PERIOD, limit);
    while (prof_n < limit) {
        locked(LOCK_N);
        hot_a(OPEN_N);
    }
    puts1("span=");
    putdec(now() - t0);
    puts1("\n");
    prof_dump();
    sh_exit();
    for (;;) {
    }
}
#endif

#if SCEN == 4
/* работа в чужом обработчике: EXTI0 поднимается программно и крутит 300 оборотов */
#ifndef ISR_PRIO
#define ISR_PRIO 0u                      /* 0 — как у выборки; 0x80 — менее срочный */
#endif
#define NVIC_ISPR0 REG(0xE000E200u)
#define EXTI0_IRQ  6

__attribute__((noinline)) void isr_work(uint32_t n)
{
    for (uint32_t i = 0; i < n; i++)
        __asm volatile("nop");
}

void EXTI0_IRQHandler(void)
{
    isr_work(300);
}

int main(void)
{
    uint32_t limit = cfg_samples;
    uart_init();
    clock_start();
    NVIC_IPR(EXTI0_IRQ) = ISR_PRIO;
    NVIC_ISER0 = 1u << EXTI0_IRQ;
    prof_start(PERIOD, limit);
    while (prof_n < limit) {
        NVIC_ISPR0 = 1u << EXTI0_IRQ;    /* поднять прерывание: обработчик отработает сразу */
        hot_a(700);
    }
    prof_dump();
    sh_exit();
    for (;;) {
    }
}
#endif

#if SCEN == 5
/* цена выборки: одна и та же работа без профилировщика и с ним, разность — команды обработчика */
int main(void)
{
    uart_init();
    clock_start();
    uint32_t t0 = now();
    hot_a(250000);
    uint32_t off = now() - t0;
    prof_start(PERIOD, 0u);
    t0 = now();
    hot_a(250000);
    uint32_t on = now() - t0;
    uint32_t n = prof_n;
    prof_stop();
    puts1("cost off=");
    putdec(off);
    puts1(" on=");
    putdec(on);
    puts1(" samples=");
    putdec(n);
    puts1(" per_sample=");
    putdec((on - off) / n);
    puts1(" rem=");
    putdec((on - off) % n);
    puts1("\n");
    sh_exit();
    for (;;) {
    }
}
#endif

rtos75a.c — выборка под FreeRTOS с меткой задачи.

/* Раздел 75а: выборка под ОСРВ. FreeRTOS 10.5.1, порт ARM_CM3, QEMU netduino2. Две задачи зовут
 * одну и ту же функцию hot_a; рядом с PC профилировщик кладёт pxCurrentTCB (сборка с
 * -DPROF_TAG=pxCurrentTCB), и выборки делятся не только по функциям, но и по задачам. */
#include "FreeRTOS.h"
#include "task.h"
#include "prof75a.h"
#include "io75a.h"

#ifndef PERIOD
#define PERIOD 1691u
#endif
#ifndef SAMPLES
#define SAMPLES 10000u
#endif

const volatile uint32_t cfg_samples = SAMPLES;
static TaskHandle_t task_handle[2];

__attribute__((noinline)) void hot_a(uint32_t n)
{
    for (uint32_t i = 0; i < n; i++)
        __asm volatile("nop");
}

__attribute__((noinline)) void hot_b(uint32_t n)
{
    for (uint32_t i = 0; i < n; i++)
        __asm volatile("nop");
}

void vPortSetupTimerInterrupt(void)      /* тик ядру не нужен: SysTick занят профилировщиком */
{
}

void rtos_assert(void)
{
    puts1("assert\n");
    sh_exit();
}

static void finish(void)
{
    for (uint32_t i = 0; i < 2u; i++) {  /* таблица «адрес блока задачи — имя» для разбора */
        puts1("task ");
        puthex((uint32_t)task_handle[i], 8);
        putc1(' ');
        puts1(pcTaskGetName(task_handle[i]));
        puts1("\n");
    }
    prof_dump();
    sh_exit();
}

static void task_motor(void *arg)
{
    (void)arg;
    prof_start(PERIOD, cfg_samples);     /* после старта планировщика: он опускает приоритет SysTick */
    for (;;) {
        hot_a(600);
        if (prof_n >= cfg_samples)
            finish();
        taskYIELD();
    }
}

static void task_link(void *arg)
{
    (void)arg;
    for (;;) {
        hot_a(300);
        hot_b(100);
        taskYIELD();
    }
}

int main(void)
{
    uart_init();
    clock_start();
    xTaskCreate(task_motor, "motor", 128, NULL, 1, &task_handle[0]);
    xTaskCreate(task_link, "link", 128, NULL, 1, &task_handle[1]);
    vTaskStartScheduler();
    for (;;) {
    }
}

prof75a.py — сводка выборок в таблицу «функция — доля времени».

#!/usr/bin/env python3
"""Раздел 75а: свести выборки профилировщика в таблицу «функция — доля времени».

    python3 prof75a.py uart.txt fw.elf [первые N выборок]

uart.txt — вывод prof_dump(): строки «p …» с сырыми адресами или «h …» со счётчиками корзин.
Функции и их размеры берутся из таблицы символов ELF командой arm-none-eabi-nm."""
import bisect
import collections
import re
import subprocess
import sys


def load_symbols(elf, nm="arm-none-eabi-nm"):
    """Функции из ELF: список (адрес, размер, имя) по возрастанию адреса."""
    out = subprocess.run([nm, "-n", "-S", "--defined-only", elf],
                         capture_output=True, text=True, check=True).stdout
    syms, seen = [], set()
    for line in out.splitlines():
        f = line.split()
        if len(f) == 4 and f[2] in "TtW" and int(f[0], 16) not in seen:
            seen.add(int(f[0], 16))             # слабые синонимы по тому же адресу — один раз
            syms.append((int(f[0], 16), int(f[1], 16), f[3]))
    return syms


def lookup(syms, pc):
    """Имя функции, в которую попадает адрес, или «?»."""
    i = bisect.bisect_right(syms, (pc, 1 << 32)) - 1
    if i >= 0 and pc < syms[i][0] + syms[i][1]:
        return syms[i][2]
    return "?"


def read_dump(path):
    """Вернуть (сырые адреса, корзины): корзины — список (начало, размер, счётчик)."""
    raw, bins, base, shift, k = [], [], 0, 0, 0
    for line in open(path, encoding="ascii", errors="replace"):
        m = re.match(r"prof hist base=0x([0-9a-f]+) shift=(\d+)", line)
        if m:
            base, shift, k = int(m.group(1), 16), int(m.group(2)), 0
        elif line.startswith("p "):
            raw += [int(x, 16) for x in line.split()[1:]]
        elif line.startswith("h "):
            for x in line.split()[1:]:
                if int(x, 16):
                    bins.append((base + (k << shift), 1 << shift, int(x, 16)))
                k += 1
    return raw, bins


def by_function(syms, raw, bins):
    """Счёт выборок по функциям. Корзина на стыке функций делится между ними по числу байт."""
    cnt = collections.Counter()
    for pc in raw:
        cnt[lookup(syms, pc)] += 1
    for start, size, n in bins:
        owners = [lookup(syms, a) for a in range(start, start + size, 2)]   # команды Thumb — по чётным адресам
        owners = [o for o in owners if o != "?"] or ["?"]                   # заполнитель между функциями не в счёт
        for o in owners:
            cnt[o] += n / len(owners)
    return cnt


def report(cnt):
    total = sum(cnt.values())
    print("samples %d" % round(total))
    for name, n in sorted(cnt.items(), key=lambda kv: (-kv[1], kv[0])):
        print("  %6.2f %%  %8.1f  %s" % (100.0 * n / total, n, name))


if __name__ == "__main__":
    symbols = load_symbols(sys.argv[2])
    samples, buckets = read_dump(sys.argv[1])
    if len(sys.argv) > 3:
        samples = samples[:int(sys.argv[3])]
    report(by_function(symbols, samples, buckets))

dwt75a.c — выборка PC самим ядром через DWT и ITM.

/* Раздел 75а, способ B: выборку PC ведёт само ядро. Блок DWT раз в заданное число тактов
 * выдаёт пакет с текущим PC, ITM ставит его в поток, поток уходит по линии SWO в отладчик.
 * Нужны Cortex-M3/M4/M7 и отладчик, принимающий SWO. Заголовки — CMSIS из пака устройства. */
#if defined(STM32F10X_MD)
#include "stm32f10x.h"
#else
#include "stm32f4xx.h"
#endif

#define PCS_TAP_64    0u                 /* шаг POSTCNT — бит 6 счётчика тактов: раз в 64 такта */
#define PCS_TAP_1024  1u                 /* бит 10: раз в 1024 такта */

void swo_init(void);                     /* раздел 21.2: TRACE_IOEN, TRCENA, ключ ITM, делитель SWO, ITMENA */

_Static_assert(DWT_CTRL_PCSAMPLENA_Msk == (1u << 12), "PCSAMPLENA - bit 12");
_Static_assert(DWT_CTRL_CYCTAP_Msk == (1u << 9), "CYCTAP - bit 9");
_Static_assert(DWT_CTRL_POSTINIT_Msk == (0xFu << 5), "POSTINIT - bits 8:5");
_Static_assert(DWT_CTRL_POSTPRESET_Msk == (0xFu << 1), "POSTPRESET - bits 4:1");
_Static_assert(DWT_CTRL_CYCEVTENA_Msk == (1u << 22), "CYCEVTENA - bit 22");
_Static_assert(DWT_CTRL_CYCCNTENA_Msk == 1u, "CYCCNTENA - bit 0");
_Static_assert(ITM_TCR_DWTENA_Msk == (1u << 3), "ITM TCR DWTENA - bit 3");

/* Включить периодическую выборку PC: tap — PCS_TAP_64 или PCS_TAP_1024, reload — 0…15. */
void pcs_start(uint32_t tap, uint32_t reload)
{
    swo_init();
    ITM->TCR |= ITM_TCR_DWTENA_Msk;      /* пропускать пакеты DWT в поток ITM */

    /* POSTINIT пишется, только пока POSTCNT выключен: сначала снять оба разрешения */
    DWT->CTRL &= ~(DWT_CTRL_PCSAMPLENA_Msk | DWT_CTRL_CYCEVTENA_Msk);
    DWT->CYCCNT = 0u;
    DWT->CTRL = (DWT->CTRL & ~(DWT_CTRL_CYCTAP_Msk | DWT_CTRL_POSTPRESET_Msk | DWT_CTRL_POSTINIT_Msk))
              | (tap << DWT_CTRL_CYCTAP_Pos)
              | (reload << DWT_CTRL_POSTPRESET_Pos)      /* с какого числа POSTCNT считает вниз */
              | (reload << DWT_CTRL_POSTINIT_Pos)        /* и с какого начинает */
              | DWT_CTRL_CYCCNTENA_Msk;                  /* POSTCNT шагает по биту CYCCNT: счётчик тактов должен идти */
    DWT->CTRL |= DWT_CTRL_PCSAMPLENA_Msk;                /* выборка пошла */
}

void pcs_stop(void)
{
    DWT->CTRL &= ~DWT_CTRL_PCSAMPLENA_Msk;
}

itm75a.py — разбор потока ITM/DWT с линии SWO.

#!/usr/bin/env python3
"""Раздел 75а, способ B: разбор потока ITM/DWT с линии SWO — достать пакеты выборки PC.

    python3 itm75a.py parse swo.bin [--sync] > dump.txt     адреса — строками «p …» для prof75a.py
    python3 itm75a.py gen   dump.txt swo.bin                синтетический поток из дампа способа A
    python3 itm75a.py test  dump.txt                        собрать поток, разобрать, сравнить

Формат пакетов — как его разбирает pyOCD (github.com/pyocd/pyOCD, pyocd/trace/swo.py)."""
import sys

SYNC = bytes([0, 0, 0, 0, 0, 0x80])


def parse(data, wait_sync=False):
    """Вернуть список событий (тип, значение, …). Типы: pc, sleep, itm, cnt, exc, dwt, sync, ovf, ts, bad."""
    i, out = 0, []
    if wait_sync:                               # подключились посреди потока: ждать синхропакет
        i = data.find(SYNC)
        i = len(data) if i < 0 else i
    while i < len(data):
        h = data[i]
        i += 1
        if h == 0x00:                           # синхропакет: нули и завершающий 0x80
            j = i
            while j < len(data) and data[j] == 0x00:
                j += 1
            if j < len(data) and data[j] == 0x80 and j - i >= 4:
                out.append(("sync",))
            else:
                out.append(("bad", h))
            i = j + 1
        elif h == 0x70:                         # переполнение: часть пакетов потеряна
            out.append(("ovf",))
        elif h & 0x03 == 0:                     # служебный пакет: метка времени или расширение
            while h & 0x80 and i < len(data):   # бит 7 — «дальше ещё байт»
                h = data[i]
                i += 1
            out.append(("ts",))
        else:                                   # пакет источника: 1, 2 или 4 байта данных
            size = 1 << ((h & 0x03) - 1)
            if i + size > len(data):
                break                           # хвост потока оборван посреди пакета
            value = int.from_bytes(data[i:i + size], "little")
            i += size
            src = h >> 3
            if h & 0x04 == 0:
                out.append(("itm", src, value, size))       # запись программы в канал src
            elif src == 2:                                  # периодическая выборка PC
                out.append(("pc", value) if size == 4 else ("sleep", value))
            elif src == 0:
                out.append(("cnt", value))                  # переполнение счётчика событий DWT
            elif src == 1:
                out.append(("exc", value))                  # вход в исключение и выход
            else:
                out.append(("dwt", src, value))             # трассировка данных
    return out


def pc_packet(pc):
    return bytes([0x17]) + pc.to_bytes(4, "little")         # 0x17 = источник 2, железный, 4 байта


def synth(pcs):
    """Поток, какой дала бы линия SWO: выборки PC вперемешку с остальными видами пакетов."""
    s = bytearray(SYNC)
    for k, pc in enumerate(pcs):
        s += pc_packet(pc)
        if k % 7 == 3:
            s += bytes([0x01, 0x48 + k % 20])               # канал 0, один байт (printf раздела 21)
        if k % 50 == 10:
            s += bytes([0x0B]) + (k * 977).to_bytes(4, "little")    # канал 1, слово (замер из 21.9)
        if k % 64 == 20:
            s += bytes([0x15, 0x00])                        # выборка во сне: PC нет
        if k % 100 == 30:
            s += bytes([0xC0, 0x85, 0x12])                  # метка времени с двумя байтами продолжения
        if k % 100 == 60:
            s += bytes([0x0E, 0x0F, 0x10])                  # трассировка исключений: вход в SysTick (15)
        if k % 500 == 250:
            s += bytes([0x05, 0x04])                        # переполнение счётчика событий
        if k % 1000 == 500:
            s += bytes([0x70])                              # переполнение
        if k % 1000 == 999:
            s += SYNC
    s += bytes([0x20])                                      # короткая метка времени — последним пакетом
    return bytes(s)


def read_pcs(path):
    pcs = []
    for line in open(path, encoding="ascii", errors="replace"):
        if line.startswith("p "):
            pcs += [int(x, 16) for x in line.split()[1:]]
    return pcs


def dump(events):
    pcs = [e[1] for e in events if e[0] == "pc"]
    print("prof raw n=%d" % len(pcs))
    for k in range(0, len(pcs), 8):
        print("p " + " ".join("%08x" % pc for pc in pcs[k:k + 8]))
    print("end")


def count(events):
    kinds = ("pc", "sleep", "itm", "cnt", "exc", "dwt", "sync", "ovf", "ts", "bad")
    return " ".join("%s=%d" % (k, sum(1 for e in events if e[0] == k)) for k in kinds)


if __name__ == "__main__":
    cmd = sys.argv[1]
    if cmd == "parse":
        dump(parse(open(sys.argv[2], "rb").read(), "--sync" in sys.argv))
    elif cmd == "gen":
        open(sys.argv[3], "wb").write(synth(read_pcs(sys.argv[2])))
    elif cmd == "test":
        src = read_pcs(sys.argv[2])
        stream = synth(src)
        ev = parse(stream)
        print("packet for pc 0x%08x: %s" % (src[0], pc_packet(src[0]).hex(" ")))
        print("stream %d bytes from %d samples: %s" % (len(stream), len(src), count(ev)))
        print("decoded = source: %s" % ("yes" if [e[1] for e in ev if e[0] == "pc"] == src else "NO"))
        cut = stream[5005:]                                 # приёмник включился посреди пакета
        blind, synced = parse(cut), parse(cut, wait_sync=True)
        tail = [e[1] for e in synced if e[0] == "pc"]
        print("cut at byte 5005: %s" % cut[:8].hex(" "))
        print("  no sync wait, first events: %s" % " ".join(str(e) for e in blind[:2]))
        print("  wait for sync: %s; tail of source: %s"
              % (count(synced), "yes" if tail and tail == src[len(src) - len(tail):] else "NO"))

Дальше: раздел 76 «Алгоритмы».

Открыть в интерактивном справочнике