21. Отладочный вывод SWO и ITM
Отладочный printf через UART (раздел 16) стоит двух выводов, переходника
и заметного времени на передачу. SWO решает ту же задачу иначе: вывод идёт
по линии, которая и так подключена к отладчику, на скорости в десятки раз
выше, и почти не тормозит программу. Раздел показывает настройку на F103
и F407, что именно бежит по линии — пакеты ITM байт за байтом, — сколько
символов в секунду она на самом деле пропускает и как с её помощью мерить
время участков кода.
Как это называется в документации
Оригинал Перевод serial wire output (SWO) последовательный отладочный вывод instrumentation trace macrocell (ITM) модуль трассировки trace port interface unit (TPIU) формирователь выходного потока stimulus port канал трассировки trace enable разрешение трассировки prescaler делитель скорости lock access register регистр защиты доступа software source packet пакет канала: заголовок и данные synchronization packet синхропакет data watchpoint and trace (DWT) блок наблюдения и счётчик тактов cycle counter ( CYCCNT)счётчик тактов ядра
21.1 Что это такое
В ядре Cortex‑M3, M4, M7 и M33 есть отдельный блок трассировки.
Программа пишет байты в его регистр, блок упаковывает их в поток и выдаёт
наружу через один вывод — SWO. Отладчик принимает поток и показывает
в окне вывода. У Cortex‑M0 и M0+ (F0, G0, L0, C0) этого блока нет —
ни ITM, ни SWO, ни счётчика тактов DWT->CYCCNT: отладочный вывод там —
UART или semihosting через отладчик (раздел 35а).
Ключевые отличия от UART:
| UART | SWO | |
|---|---|---|
| выводов | 2 (или 1 на передачу) | 1 — PB3, он же JTDO |
| скорость | до 115 200 бод обычно | 2 МГц и выше |
| переходник | нужен USB–UART | не нужен, работает через отладчик |
| без отладчика | работает | не работает |
| каналов | один поток | 32 независимых |
Вывод SWO у F1 и F4 — это PB3, тот же, что служит линией JTDO. Поэтому
работает он только при подключении по SWD, а не по JTAG: в режиме SWD
линия JTDO свободна и отдаётся под трассировку.
21.2 Настройка
#define ITM_LAR_KEY 0xC5ACCE55u
void swo_init(void)
{
/* 1. Разрешить вывод трассировки наружу.
Заодно это переводит PB3 из режима JTDO в режим SWO. */
DBGMCU->CR |= DBGMCU_CR_TRACE_IOEN;
/* 2. Разрешить ядру отладочные функции. */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
/* 3. Снять защиту записи регистров ITM. */
ITM->LAR = ITM_LAR_KEY;
/* 4. Делитель и протокол. Делитель кодируется как «значение минус один». */
TPI->ACPR = (CORE_HZ / SWO_HZ) - 1u;
TPI->SPPR = 2u; /* 2 -> асинхронный NRZ, как у UART */
/* 5. Включить модуль трассировки и каналы 0 и 1. */
ITM->TCR = ITM_TCR_ITMENA_Msk
| ITM_TCR_SYNCENA_Msk
| (1u << ITM_TCR_TraceBusID_Pos);
ITM->TER = 3u; /* разрешены каналы 0 и 1 */
}
Пять шагов, и пропуск любого даёт молчание без диагностики. Особенно
неочевиден третий: регистры ITM защищены от случайной записи, и снимается
защита записью фиксированного значения 0xC5ACCE55.
Код одинаков для F103 и F407: DBGMCU, CoreDebug, ITM и TPI — части
ядра Cortex‑M и отладочного блока ST с теми же битами. Различается только
частота ядра, от которой считается делитель:
| Делитель для 2 МГц | В ACPR |
|
|---|---|---|
| F103, ядро 72 МГц | 36 | 35 |
| F407, ядро 168 МГц | 84 | 83 |
21.3 Вывод символа
int swo_putchar(int ch)
{
/* Модуль выключен (его не включили ни прошивка, ни отладчик) — выходим,
иначе программа зависнет в ожидании готовности. */
if (((ITM->TCR & ITM_TCR_ITMENA_Msk) == 0u) || ((ITM->TER & 1u) == 0u)) {
return ch;
}
while (ITM->PORT[0].u32 == 0u) { } /* канал занят предыдущим байтом */
ITM->PORT[0].u8 = (uint8_t)ch;
return ch;
}
⚠️ Проверка на включённый модуль обязательна. Без неё прошивка, в которой ITM никто не включил, зависнет на первом же выводе: программа будет вечно ждать готовности канала, которую некому дать. Это ровно тот случай, когда «на столе работает, в поле не запускается».
Но проверка смотрит, включён ли модуль, а не подключён ли отладчик. Если модуль включает сама прошивка (
swo_initиз 21.2), проверка проходит всегда, и без отладчика байты просто уходят на вывод со скоростью SWO — время тратится впустую. Чтобы в поле его не тратить,swo_initвызывают, только когда отладчик подключён:if (CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk) swo_init();. БитC_DEBUGENставит отладчик; после его отключения бит может остаться до сброса по питанию.
Дальше вывод перенаправляют на printf тем же способом, что и для UART, —
переопределением fputc в Keil или _write в GCC.
21.4 Приём на стороне компьютера
В Keil окно View → Serial Windows → Debug (printf) Viewer, а трассировку
надо включить в настройках отладчика на вкладке Trace. В CubeIDE: в
настройках отладки на вкладке отладчика включить Serial Wire Viewer и указать
частоту ядра, затем открыть окно SWV ITM Data Console, в его настройках
отметить порт 0 (и 1 для замеров из 21.9) и нажать кнопку начала трассировки —
без неё окно пусто. У других сред и отладчиков есть свои утилиты.
⚠️ Частота ядра в настройках отладчика обязана совпадать с реальной. Отладчик восстанавливает поток, зная скорость, и вычисляет её из указанной частоты ядра. Разогнали камень до 72 МГц, а в настройках осталось 8 — увидите мусор или ничего. Симптом узнаваемый: вывод есть, но нечитаемый.
Второй нюанс — скорость SWO должна делить частоту ядра нацело. При 72 МГц хорошие значения: 2 МГц (делитель 36), 4 МГц (18), 1 МГц (72). Дробный делитель даст сыпучий поток. У отладчика есть и свой предел скорости приёма — его смотрят в описании отладчика.
21.5 Тридцать два канала
Помимо простого вывода текста у ITM есть возможность, ради которой его и стоит осваивать: 32 независимых канала.
Можно развести потоки по темам — состояние конечного автомата в один канал, данные с датчика в другой, ошибки в третий — и фильтровать их на стороне компьютера, не смешивая в общую ленту. Номер канала едет в каждом пакете (21.6), так что разделение ничего не стоит.
Помимо ручного вывода блок умеет отдавать и автоматическую трассировку: события входа и выхода из прерываний, счётчики тактов. Это уже инструменты профилирования — тема раздела 35.
21.6 Что бежит по линии: пакеты ITM
Каждая запись в канал превращается в пакет: байт заголовка и данные. В заголовке номер канала сдвинут на три бита влево, а младшие два бита — размер записи: 01 — байт, 10 — полуслово, 11 — слово. Данные идут младшим байтом вперёд. Модель пакетов и независимый декодер дают для строки «Hi!\n»:
- по байту, как пишет
swo_putchar: 01 48 01 69 01 21 01 0A — восемь байт, половина из них заголовки; - одним словом: 03 48 69 21 0A — пять байт.
Иногда в поток вставляется синхропакет 00 00 00 00 00 80 — по нему приёмник
находит начало пакетов, если подключился посреди потока. Его включает бит
SYNCENA в настройке.
21.7 Пропускная способность
Линия SWO в режиме NRZ передаёт байт как UART: старт, 8 бит, стоп — 10 битовых интервалов. На 2 МГц это 200 000 байт в секунду по линии. Отсюда:
| Символов в секунду | Строка из 60 символов | |
|---|---|---|
| UART 115 200 | 11 520 | 5,21 мс |
| SWO 2 МГц, по байту | 100 000 | 0,60 мс |
| SWO 2 МГц, словом | 160 000 | 0,375 мс |
SWO по байту в 8,7 раза быстрее UART, словом — в 13,9 раза. Запись в канал стоит единицы тактов, пока в блоке есть место; если выводить непрерывно, программа всё равно ждёт линию — отсюда совет писать словами:
void swo_write(const char *s, uint32_t n)
{
if ((ITM->TCR & ITM_TCR_ITMENA_Msk) == 0u || (ITM->TER & 1u) == 0u)
return;
while (n >= 4u) {
uint32_t w = (uint32_t)(uint8_t)s[0] | (uint32_t)(uint8_t)s[1] << 8
| (uint32_t)(uint8_t)s[2] << 16 | (uint32_t)(uint8_t)s[3] << 24;
while (ITM->PORT[0].u32 == 0u) { }
ITM->PORT[0].u32 = w; /* младший байт уходит первым */
s += 4;
n -= 4u;
}
while (n--)
swo_putchar(*s++);
}
21.8 F407: быстрое ядро, те же делители
На 168 МГц делитель для 2 МГц — 84, и выбор целых скоростей другой: 2 МГц, 4 МГц (42), 6 МГц (28), 8 МГц (21). Частота в настройках отладчика — 168 МГц, а не частота кварца: частая причина мусора на F407, где ядро разгоняют PLL до 168 МГц из кварца 8 МГц.
На обеих сериях есть ловушка с PB3: после сброса он в режиме JTDO,
и если программа отдаёт его под GPIO (часто — чтобы освободить вывод
на плате), SWO пропадает: вывод один на двоих.
21.9 🔧 Замер времени: счётчик тактов и канал 1
Счётчик тактов DWT->CYCCNT (раздел 35) вместе с каналом ITM даёт замер
времени участка кода без осциллографа: обнулить, запустить, в конце
отправить значение словом в отдельный канал, чтобы оно не смешивалось
с текстом.
void swo_cycles_begin(void)
{
DWT->CYCCNT = 0u;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
uint32_t t = DWT->CYCCNT;
while (ITM->PORT[1].u32 == 0u) { }
ITM->PORT[1].u32 = t; /* на линии: 0x0B и четыре байта */
123 456 тактов в канале 1 — это 0B 40 E2 01 00 на линии: заголовок
канала 1 со словом, затем число младшим байтом вперёд. Приёмник на
компьютере показывает канал 1 отдельным окном или пишет в файл —
удобно собирать статистику по тысячам замеров. Счётчику нужен тот же
бит TRCENA, что и ITM, — он уже включён в swo_init.
21.10 Грабли
Ничего не выводится. Проверяйте по порядку: подключение по SWD (не JTAG),
DBGMCU_CR_TRACE_IOEN, TRCENA, ключ разблокировки, включённый канал,
настройки Trace в отладчике.
Мусор вместо текста. Частота ядра в настройках отладчика не совпадает с реальной: на F407 — 168 МГц, а не 8.
Программа зависает без отладчика. Нет проверки готовности модуля в функции вывода.
PB3 занят под GPIO. Вывод один и тот же — либо трассировка, либо
ваша периферия.
Работает, но теряет символы или тормозит программу. Выводится больше, чем пропускает линия: по байту — 100 000 символов в секунду на 2 МГц. Писать словами, поднять скорость, выводить меньше.
Не работает на дешёвом отладчике. Клоны ST‑Link часто не поддерживают приём SWO вовсе, хотя программируют и отлаживают нормально. Проверьте, что ваш экземпляр это умеет, прежде чем искать ошибку в коде.
Замеры тактов в канале 1 не видны. Канал не разрешён в TER
(нужно 3, а не 1) или приёмник показывает только канал 0.
21.11 Итоги
- SWO даёт отладочный вывод без переходника, по одной линии —
PB3у F1 и F4, — и на порядок быстрее UART, но работает только при подключённом отладчике. PB3— та же линия, чтоJTDO, поэтому нужен режим SWD, а не JTAG, а под GPIO её не отдают: SWO пропадёт.- Настройка из пяти шагов, одинакова для F103 и F407; в
ACPR— 35 и 83 для 2 МГц. - В функции вывода обязательна проверка, что модуль включён, — иначе
автономный запуск зависнет. А
swo_initвызывают, только если отладчик подключён (битC_DEBUGEN): иначе вывод без приёмника тратит время. - По линии идут пакеты: заголовок с каналом и размером, данные младшим байтом вперёд; «Hi!\n» — 8 байт по байту и 5 словом.
- 2 МГц — 100 000 символов в секунду по байту, 160 000 словом; UART 115 200 — 11 520.
- Канал 1 и счётчик тактов — замер времени без осциллографа.
Полные исходники
swo21.c — SWO и ITM на F103 и F407: настройка, вывод байта и слова, замер счётчиком тактов.
/* Раздел 21 (обогащение): SWO и ITM на F103 и F407 — настройка, вывод байта и слова,
* замер времени счётчиком тактов DWT с отправкой в канал 1.
* Собирается armclang с -include stm32f10x.h (STM32F10X_MD) или stm32f4xx.h (STM32F407xx). */
#include <stdint.h>
#if defined(STM32F10X_MD)
#define CORE_HZ 72000000u
#else
#define CORE_HZ 168000000u
#endif
#define SWO_HZ 2000000u
#define ITM_LAR_KEY 0xC5ACCE55u
void swo_init(void)
{
/* 1. Разрешить вывод трассировки наружу.
Заодно это переводит PB3 из режима JTDO в режим SWO. */
DBGMCU->CR |= DBGMCU_CR_TRACE_IOEN;
/* 2. Разрешить ядру отладочные функции. */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
/* 3. Снять защиту записи регистров ITM. */
ITM->LAR = ITM_LAR_KEY;
/* 4. Делитель и протокол. Делитель кодируется как «значение минус один». */
TPI->ACPR = (CORE_HZ / SWO_HZ) - 1u;
TPI->SPPR = 2u; /* 2 -> асинхронный NRZ, как у UART */
/* 5. Включить модуль трассировки и каналы 0 и 1. */
ITM->TCR = ITM_TCR_ITMENA_Msk
| ITM_TCR_SYNCENA_Msk
| (1u << ITM_TCR_TraceBusID_Pos);
ITM->TER = 3u; /* разрешены каналы 0 и 1 */
}
int swo_putchar(int ch)
{
/* Модуль выключен (его не включили ни прошивка, ни отладчик) — выходим,
иначе программа зависнет в ожидании готовности. */
if (((ITM->TCR & ITM_TCR_ITMENA_Msk) == 0u) || ((ITM->TER & 1u) == 0u)) {
return ch;
}
while (ITM->PORT[0].u32 == 0u) { } /* канал занят предыдущим байтом */
ITM->PORT[0].u8 = (uint8_t)ch;
return ch;
}
/* Четыре символа одной записью: пакет из пяти байт вместо восьми. */
void swo_write(const char *s, uint32_t n)
{
if ((ITM->TCR & ITM_TCR_ITMENA_Msk) == 0u || (ITM->TER & 1u) == 0u)
return;
while (n >= 4u) {
uint32_t w = (uint32_t)(uint8_t)s[0] | (uint32_t)(uint8_t)s[1] << 8
| (uint32_t)(uint8_t)s[2] << 16 | (uint32_t)(uint8_t)s[3] << 24;
while (ITM->PORT[0].u32 == 0u) { }
ITM->PORT[0].u32 = w; /* младший байт уходит первым */
s += 4;
n -= 4u;
}
while (n--)
swo_putchar(*s++);
}
/* Замер участка кода: счётчик тактов DWT (раздел 35), результат — словом в канал 1. */
void swo_cycles_begin(void)
{
DWT->CYCCNT = 0u;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
void swo_cycles_end(void)
{
uint32_t t = DWT->CYCCNT;
if ((ITM->TCR & ITM_TCR_ITMENA_Msk) && (ITM->TER & 2u)) {
while (ITM->PORT[1].u32 == 0u) { }
ITM->PORT[1].u32 = t; /* на линии: 0x0B и четыре байта */
}
}
/* ── проверка расчётов ────────────────────────────────────────────────── */
_Static_assert(CORE_HZ % SWO_HZ == 0u, "скорость SWO делит частоту ядра нацело");
_Static_assert((CORE_HZ / SWO_HZ) - 1u == (CORE_HZ == 168000000u ? 83u : 35u), "ACPR: 83 на F407, 35 на F103");
_Static_assert(DBGMCU_CR_TRACE_IOEN == (1u << 5), "TRACE_IOEN — бит 5 у обеих серий");
_Static_assert(CoreDebug_DEMCR_TRCENA_Msk == (1u << 24), "TRCENA — бит 24");
_Static_assert(DWT_CTRL_CYCCNTENA_Msk == 1u, "CYCCNTENA — бит 0");
_Static_assert(sizeof(ITM->PORT) / sizeof(ITM->PORT[0]) == 32u, "32 канала");
int main(void)
{
swo_init();
swo_cycles_begin();
swo_write("Hello, SWO!\n", 12u);
swo_cycles_end();
for (;;) { }
}
Дальше: раздел 21а «RTT: вывод и ввод через отладчик без SWO и без UART» — вывод через память, которую отладчик читает на ходу, в том числе на Cortex‑M0.