Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103, STM32F407, STM32G474 · уровень средний · 17 мин чтения
Открыть в интерактивном справочнике

21а. RTT: вывод и ввод через отладчик без SWO и без UART

Раздел 21 вывел текст через SWO: быстро и без UART, но нужен вывод PB3, приёмник SWO в отладчике и ядро не младше Cortex‑M3. У Cortex‑M0 и M0+ (F0, G0, L0, C0) нет ни ITM, ни вывода SWO. Semihosting (раздел 35а) работает на любом ядре, но останавливает его на каждом вызове, а без отладчика падает в HardFault. UART (раздел 16) стоит двух выводов и переходника.

RTT устроен проще всех. Прошивка только пишет байты в кольцевой буфер в своём ОЗУ. Отладчик по SWD читает эту память на ходу и забирает новое. Ни одного вывода сверх SWD, ни остановок ядра, и работает на M0. Здесь — своя минимальная реализация (не код SEGGER, но раскладка та же, что ищут отладчики), опыт в QEMU на трёх ядрах с читателем на ПК, три режима переполнения, цена записи в командах и сравнение с SWO, semihosting и UART.

Опоры: руководство OpenOCD 0.12.0 из zephyr-sdk (глава 15.6 «Real Time Transfer»), заголовок SEGGER_RTT.h из дерева Zephyr (только раскладка и имена режимов), заголовки устройств F103, F407 и G474 из паков Keil (начало ОЗУ). Опыт — QEMU 7.2 в образе stm-arm:1: прошивка собрана gcc 12.2 для Cortex‑M4, M3 и M0, память читает gdb 13.1 и свой читатель на Python. Листинг — ещё и armclang 6.24.

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

Оригинал Перевод
Real Time Transfer (RTT) обмен через память в реальном времени
control block управляющий блок
up-buffer, down-buffer канал «вверх» (к компьютеру) и «вниз» (к МК)
WrOff, RdOff указатель записи и указатель чтения
SEGGER_RTT_MODE_NO_BLOCK_SKIP пропуск: не влезло — не писать ничего
SEGGER_RTT_MODE_NO_BLOCK_TRIM обрезка: записать сколько влезет
SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL блокировка: ждать места
background memory access доступ к памяти без остановки ядра
GDB remote serial protocol протокол удалённой отладки GDB

21а.1 Идея: память вместо линии

SWO гонит байты по отдельной линии, UART — по своей паре выводов. У RTT линии нет: отладчик и так умеет читать и писать память по SWD. Руководство OpenOCD называет условие прямо — нужен доступ к памяти, пока ядро работает (background memory access), и отдельно упоминает, что это интересно для ядер без SWO, таких как Cortex‑M0.

Всё хозяйство — в ОЗУ. Управляющий блок начинается строкой‑признаком SEGGER RTT: по ней отладчик находит блок поиском в заданном диапазоне. Дальше — число каналов и описатели. У каждого канала своё кольцо. Канал «вверх» пишет прошивка, читает компьютер. Канал «вниз» — наоборот, это ввод с клавиатуры в прошивку.

Управляющий блок RTT в ОЗУ: строка-признак, число каналов и описатели по 24 байта; в канале вверх прошивка пишет байты и указатель записи, отладчик по SWD читает байты и пишет указатель чтения; в канале вниз наоборот

Главное правило: указатель записи двигает только тот, кто пишет, указатель чтения — только тот, кто читает. Каждое 32‑битное слово меняет одна сторона, поэтому ни блокировок, ни запрета прерываний между прошивкой и отладчиком не нужно.


21а.2 Управляющий блок: байт за байтом

Описатель канала — шесть слов по 4 байта, 24 байта:

typedef struct {
    const char *name;                    /* имя канала для ПК, может быть 0 */
    char *buf;                           /* начало кольца */
    uint32_t size;                       /* размер кольца; полезно size - 1 байт */
    volatile uint32_t wr;                /* куда писать следующий байт: пишет только писатель */
    volatile uint32_t rd;                /* откуда читать следующий байт: пишет только читатель */
    uint32_t flags;                      /* биты 1:0 — режим при переполнении */
} rtt_ring_t;

Блок — 16 байт признака, два счётчика каналов по 4 байта и описатели. Размер: 24 + 24 × (число каналов). В опыте четыре канала «вверх» и один «вниз» — 144 байта; один и один — 72 байта; три и три (умолчание по комментариям в SEGGER_RTT_Conf.h) — 168 байт. Сами кольца лежат отдельно, где угодно в ОЗУ.

Так блок выглядит в памяти F407 в QEMU (команда gdb x/36xw &rtt_cb, первые шесть строк):

0x20000018 <rtt_cb>: 0x47474553 0x52205245 0x00005454 0x00000000
0x20000028 <rtt_cb+16>: 0x00000004 0x00000001 0x0800044c 0x200000a8
0x20000038 <rtt_cb+32>: 0x00000080 0x0000002c 0x00000000 0x00000000
0x20000048 <rtt_cb+48>: 0x08000458 0x20000128 0x00000040 0x00000030
0x20000058 <rtt_cb+64>: 0x00000000 0x00000000 0x08000460 0x20000168
0x20000068 <rtt_cb+80>: 0x00000040 0x0000003f 0x00000000 0x00000001

Первые 12 байт — «SEGGER RTT» и нули; gdb печатает их словами, и байты в слове идут справа налево. Потом 4 и 1 — число каналов. Канал 0: имя во флеш (0x0800044c), кольцо с 0x200000a8, размер 0x80 = 128, wr = 0x2c = 44, rd = 0, режим 0. Имя канала лежит во флеш — отладчик читает её так же, как ОЗУ.

Порядок запуска важен: признак пишется последним, когда описатели уже в памяти. Иначе отладчик может найти блок раньше, чем в нём появятся адреса колец:

void rtt_start(void)
{
    static const char id[16] = "SEGGER RTT";   /* константа во флеш, в ОЗУ — только копия в блоке */
    volatile char *dst = rtt_cb.id;
    DMB();                               /* описатели каналов уже в памяти */
    for (unsigned i = 0; i < sizeof(id); i++)
        dst[i] = id[i];                  /* признак — последним: ПК не найдёт полупустой блок */
    DMB();
}

Функция rtt_init первым делом стирает весь блок, и признак вместе с ним: после сброса без выключения питания старый блок в ОЗУ не обманет отладчик.


21а.3 Кольцо: один байт всегда пуст

Если wr == rd, кольцо пусто. Если бы писатель занял все байты, wr снова догнал бы rd, и полное кольцо не отличалось бы от пустого. Поэтому один байт не занимают никогда: в кольцо на 64 байта помещается 63. Так же поступает и SEGGER — это сказано в комментарии к SizeOfBuffer.

static uint32_t room(uint32_t rd, uint32_t wr, uint32_t size)
{
    return rd > wr ? rd - wr - 1u : size - wr + rd - 1u;   /* один байт не занимаем */
}

Писатель копирует байты и только потом публикует новый wr. Между ними — барьер DMB: ядро не переставит запись указателя раньше записи данных. Команда есть и на ARMv6‑M, прошивка для Cortex‑M0 собралась с ней же:

        while (k--) {
            buf[wr] = (char)p[done++];
            if (++wr == size)
                wr = 0;
        }
    }
    DMB();                               /* сначала данные, потом указатель записи */
    r->wr = wr;

Читатель на компьютере делает зеркально: читает wr, забирает байты от rd до wr и записывает новый rd. Прошивка rd канала «вверх» только читает. В канале «вниз» роли меняются: компьютер кладёт байты и сдвигает wr, прошивка в rtt_read забирает их и пишет rd.


21а.4 Опыт в QEMU: прошивка пишет, gdb и читатель забирают

Прошивка _verify/rtt21a/app21a.c не трогает ни UART, ни SWO, ни semihosting. Канал 0 «Terminal» — журнал на 128 байт, каналы 1–3 — одно и то же переполнение в трёх режимах (кольца по 64 байта), канал «вниз» 0 — ввод строки на 16 байт. Одна и та же прошивка собрана для трёх машин QEMU: netduinoplus2 (Cortex‑M4, как F407 и G474), stm32vldiscovery (Cortex‑M3, как F103) и microbit (Cortex‑M0). Последняя — не STM32, а nRF51: другой машины на M0 в QEMU 7.2 нет, но ядро то же, что у F0, G0, L0 и C0. Во всех трёх ОЗУ начинается с 0x20000000, как у F103, F407 и G474 по их заголовкам.

OpenOCD к QEMU не подключить: ему нужен отладочный адаптер, а QEMU отдаёт только порт GDB. Поэтому в роли отладчика — gdb и свой читатель rtt_host21a.py, который говорит с тем же портом пакетами m (прочитать память) и M (записать). Сначала gdb читает блок командами x:

break rtt_block_wait
x/s rtt_cb.id
x/2dw &rtt_cb.n_up
x/6xw &rtt_cb.up[3]
0x20000018 <rtt_cb>: "SEGGER RTT"
0x20000028 <rtt_cb+16>: 4 1
0x20000078 <rtt_cb+96>: 0x08000468 0x200001a8 0x00000040 0x0000003f
0x20000088 <rtt_cb+112>: 0x00000000 0x00000002

Точка останова в rtt_block_wait сработала сама: gdb ничего не читал, канал 3 в режиме блокировки заполнился до 63 байт (0x3f) и встал ждать. Об этом — в 21а.5.

Затем читатель: ищет признак в первых 8 КБ ОЗУ, разбирает описатели и каждые 50 мс забирает новое из всех каналов «вверх». Увидев в журнале type:, кладёт в канал «вниз» строку ping. Вывод на F407:

search 0x20000000+0x2000: 1 match(es) 0x20000018
control block 0x20000018: up 4, down 1, 144 bytes
  up   0 Terminal buf=0x200000a8 size=128 wr= 44 rd=  0 mode=0
  up   3 Block    buf=0x200001a8 size= 64 wr= 63 rd=  0 mode=2
  down 0 Terminal buf=0x200001e8 size= 16 wr=  0 rd=  0 mode=0
== up 0 Terminal: 97 bytes
hello, RTT
skip 48 trim 63 block 160 of 160
got: ping
bye

Прошивка приняла строку по каналу «вниз» и ответила got: ping. На M3 и M0 вывод тот же, блок — тоже 144 байта по 0x20000018; меняются только адреса колец.

Одно отличие от платы. QEMU, получив от отладчика пакет, останавливает машину, поэтому читатель на каждом опросе останавливает её, читает и пускает дальше. Настоящий отладчик читает память по SWD, не останавливая ядро, — на это и рассчитан RTT.

На плате ту же работу делает OpenOCD. По его руководству: rtt setup <адрес> <размер> <признак> задаёт, где искать блок, rtt start — начать, rtt server start <порт> <канал> открывает канал как TCP‑порт, к которому подключается любой терминал, rtt polling_interval — как часто (в миллисекундах) опрашивать каналы. С ST‑Link на плате это не проверено.


21а.5 Переполнение: пропуск, обрезка, блокировка

Если читатель не успевает, кольцо заполняется. Что делать с сообщением, которое не влезает, решают два младших бита flags. В опыте в каждое кольцо на 64 байта пишутся 10 сообщений по 16 байт (msg0 abcdefghij и перевод строки, цифра — номер), всего 160, а читатель в это время не опрашивает:

Режим Осталось в кольце Что потеряно
пропуск (0) 48 байт: 3 целых сообщения 7 сообщений целиком
обрезка (1) 63 байта: 3 целых и 15 байт четвёртого его перевод строки и 6 сообщений
блокировка (2) 63 байта, прошивка ждёт на четвёртом ничего: все 160 байт дошли

Эти числа сняты с прошивки на всех трёх ядрах и совпали с моделью кольца на ПК (model21a.py). Модель же показывает, как растёт запас, если сто сообщений по 16 байт пишутся без читателя:

Кольцо Пропуск: цело Пропуск: потеряно Обрезка Блокировка
64 байта 3 97 3 целых + 15 байт ждёт на 4‑м
256 байт 15 85 15 целых + 15 байт ждёт на 16‑м
1024 байта 63 37 63 целых + 15 байт ждёт на 64‑м

У обрезки есть неприятность: хвост потерян, а начало следующего сообщения прилипает к обрубку. Модель: кольцо на 64 байта, читатель освободил его перед сообщением 6 — в потоке msg3 abcdefghijmsg6, сообщения 4 и 5 исчезли без следа. Пропуск теряет только целые сообщения, и потерю видно по возвращённому 0.

Блокировка ничего не теряет, но без читателя прошивка встаёт навсегда — ровно это поймал gdb в 21а.4. Отсюда обычная расстановка: журнал — пропуск, редкие важные данные на ПК — блокировка, только когда отладчик точно подключён.


21а.6 Цена записи и сравнение с SWO, semihosting и UART

Сколько стоит rtt_write, gdb посчитал пошагово (stepi от входа до возврата) на трёх первых вызовах в журнал — строки из 11, 1 и 32 байт:

Ядро (gcc 12.2, -O2) 1 байт 11 байт 32 байта Формула
Cortex‑M4 (F407, G474) 61 141 309 53 + 8 на байт
Cortex‑M3 (F103) 61 141 309 53 + 8 на байт
Cortex‑M0 95 205 436 84 + 11 на байт

Это команды, а не такты: QEMU такты не считает. На M0 нет условного выполнения IT, поэтому байт обходится в 11 команд вместо 8. armclang 6.24 для Cortex‑M4 разворачивает цикл копирования по 4 байта: 28 команд на 4 байта, 7 на байт. Строка из 60 символов на M3 и M4 по формуле — 533 команды, без ожидания какой‑либо линии, пока в кольце есть место. Код писателя — 188 байт на M4, 192 на M3, 232 на M0; читатель канала «вниз» — 92, 92 и 124 байта.

Как быстро байты доходят до компьютера, решает уже отладчик: частота SWD и то, как часто он опрашивает кольцо. Этого здесь не мерили.

UART (раздел 16) SWO (раздел 21) Semihosting (раздел 35а) RTT
выводов сверх SWD 2 (или 1) и переходник PB3 0 0
Cortex‑M0 и M0+ есть нет есть есть
ядро на время вывода работает работает, при потоке ждёт линию останавливается на каждом вызове работает
без отладчика работает не работает HardFault пишет в ОЗУ; в блокировке — ждёт вечно
ввод в прошивку есть нет есть есть, канал «вниз»
каналов один поток 32 вывод и файлы на ПК сколько описано в блоке
на стороне ПК переходник USB–UART приёмник SWO в отладчике отладчик любой отладчик, читающий память на ходу

21а.7 Грабли

Отладчик не находит блок. Диапазон поиска не покрывает адрес rtt_cb или прошивка ещё не дошла до rtt_start. Адрес — в nm или файле карты памяти (в опыте 0x20000018), диапазон — от начала ОЗУ с запасом.

Нашлось два блока. Строка SEGGER RTT лежит в ОЗУ ещё раз — например, массив с начальным значением попал в .data. В опыте признак — константа во флеш, и поиск по 8 КБ ОЗУ дал ровно одно совпадение.

Без отладчика прошивка встала. Канал в режиме блокировки заполнился, а читать некому. Для журнала — пропуск; блокировку включать, только когда отладчик подключён.

Строки склеились. Режим обрезки: хвост сообщения потерян, следующее прилипло (msg3 abcdefghijmsg6). Если важна целостность строк — пропуск.

Сообщения пропадают молча. Пропуск возвращает 0 и больше ничего не сообщает: на кольце в 64 байта из ста сообщений дошли три. Считайте неудачи или увеличьте кольцо.

Мусор или старые данные на ПК. Указатель записи опубликован раньше данных. Нужен барьер DMB между копированием и записью wr, а поля wr и rd — volatile.

Режим канала не действует на ввод. В руководстве OpenOCD отмечено, что он не смотрит на флаги каналов. Режим задаёт только, как пишет прошивка.

В QEMU ядро останавливается на каждом опросе. Это свойство порта GDB у QEMU, а не RTT. На плате отладчик читает память на ходу.


21а.8 Итоги

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

rtt21a.h — управляющий блок и кольца RTT.

/* Раздел 21а: своя минимальная реализация RTT — управляющий блок и кольца в ОЗУ.
 * Раскладка блока совместима с тем, что ищут отладчики: строка-признак 16 байт,
 * число каналов «вверх» и «вниз», затем описатели каналов по 24 байта. */
#ifndef RTT21A_H
#define RTT21A_H
#include <stdint.h>

#define RTT_N_UP    4u                   /* каналов «вверх»: прошивка -> ПК */
#define RTT_N_DOWN  1u                   /* каналов «вниз»: ПК -> прошивка */

#define RTT_MODE_SKIP   0u               /* не влезает — не писать ничего */
#define RTT_MODE_TRIM   1u               /* не влезает — записать сколько влезет */
#define RTT_MODE_BLOCK  2u               /* не влезает — ждать, пока ПК прочитает */
#define RTT_MODE_MASK   3u

typedef struct {
    const char *name;                    /* имя канала для ПК, может быть 0 */
    char *buf;                           /* начало кольца */
    uint32_t size;                       /* размер кольца; полезно size - 1 байт */
    volatile uint32_t wr;                /* куда писать следующий байт: пишет только писатель */
    volatile uint32_t rd;                /* откуда читать следующий байт: пишет только читатель */
    uint32_t flags;                      /* биты 1:0 — режим при переполнении */
} rtt_ring_t;

typedef struct {
    char id[16];                         /* «SEGGER RTT» и нули: по нему ПК находит блок */
    int32_t n_up;
    int32_t n_down;
    rtt_ring_t up[RTT_N_UP];
    rtt_ring_t down[RTT_N_DOWN];
} rtt_cb_t;

_Static_assert(sizeof(rtt_ring_t) == 24, "descriptor must be 24 bytes");
_Static_assert(sizeof(rtt_cb_t) == 16 + 4 + 4 + 24 * (RTT_N_UP + RTT_N_DOWN), "control block layout");

extern rtt_cb_t rtt_cb;

void rtt_init(void);
void rtt_setup_up(unsigned ch, const char *name, char *buf, uint32_t size, uint32_t mode);
void rtt_setup_down(unsigned ch, const char *name, char *buf, uint32_t size);
void rtt_start(void);
unsigned rtt_write(unsigned ch, const void *data, unsigned n);
unsigned rtt_read(unsigned ch, void *data, unsigned max);
void rtt_block_wait(void);

#endif

rtt21a.c — писатель RTT в прошивке.

/* Раздел 21а: писатель RTT в прошивке. Кольцо «вверх»: wr двигает только прошивка,
 * rd — только ПК; кольцо «вниз» — наоборот. Поэтому ни блокировок, ни запретов
 * прерываний между прошивкой и отладчиком не нужно. Код свой, не SEGGER. */
#include "rtt21a.h"

rtt_cb_t rtt_cb;                         /* в .bss; адрес ПК находит поиском по строке */

#define DMB() __asm volatile("dmb" ::: "memory")

static uint32_t room(uint32_t rd, uint32_t wr, uint32_t size)
{
    return rd > wr ? rd - wr - 1u : size - wr + rd - 1u;   /* один байт не занимаем */
}

void rtt_init(void)
{
    volatile uint8_t *b = (volatile uint8_t *)&rtt_cb;
    for (unsigned i = 0; i < sizeof(rtt_cb); i++)
        b[i] = 0;                        /* первым стирается и признак */
    rtt_cb.n_up = RTT_N_UP;
    rtt_cb.n_down = RTT_N_DOWN;
}

void rtt_setup_up(unsigned ch, const char *name, char *buf, uint32_t size, uint32_t mode)
{
    rtt_ring_t *r = &rtt_cb.up[ch];
    r->name = name;
    r->buf = buf;
    r->size = size;
    r->wr = 0;
    r->rd = 0;
    r->flags = mode & RTT_MODE_MASK;
}

void rtt_setup_down(unsigned ch, const char *name, char *buf, uint32_t size)
{
    rtt_ring_t *r = &rtt_cb.down[ch];
    r->name = name;
    r->buf = buf;
    r->size = size;
    r->wr = 0;
    r->rd = 0;
    r->flags = 0;
}

void rtt_start(void)
{
    static const char id[16] = "SEGGER RTT";   /* константа во флеш, в ОЗУ — только копия в блоке */
    volatile char *dst = rtt_cb.id;
    DMB();                               /* описатели каналов уже в памяти */
    for (unsigned i = 0; i < sizeof(id); i++)
        dst[i] = id[i];                  /* признак — последним: ПК не найдёт полупустой блок */
    DMB();
}

__attribute__((noinline)) void rtt_block_wait(void)
{
    __asm volatile("nop" ::: "memory");  /* ждать, пока ПК сдвинет rd; тут же удобно ставить точку останова */
}

unsigned rtt_write(unsigned ch, const void *data, unsigned n)
{
    rtt_ring_t *r = &rtt_cb.up[ch];
    const uint8_t *p = data;
    char *buf = r->buf;                  /* копии: запись через char * иначе заставит */
    uint32_t size = r->size;             /* перечитывать их на каждом байте */
    uint32_t mode = r->flags & RTT_MODE_MASK;
    uint32_t wr = r->wr;
    unsigned done = 0;

    if (mode != RTT_MODE_BLOCK) {
        uint32_t space = room(r->rd, wr, size);
        if (n > space) {
            if (mode == RTT_MODE_SKIP)
                return 0;                /* пропуск: сообщение не пишется совсем */
            n = space;                   /* обрезка: пишется начало, сколько влезло */
        }
    }
    while (done < n) {
        uint32_t k = room(r->rd, wr, size);
        if (k == 0) {                    /* сюда попадает только режим блокировки */
            DMB();
            r->wr = wr;                  /* отдать ПК то, что уже записано */
            rtt_block_wait();
            continue;
        }
        if (k > n - done)
            k = n - done;
        while (k--) {
            buf[wr] = (char)p[done++];
            if (++wr == size)
                wr = 0;
        }
    }
    DMB();                               /* сначала данные, потом указатель записи */
    r->wr = wr;
    return done;
}

unsigned rtt_read(unsigned ch, void *data, unsigned max)
{
    rtt_ring_t *r = &rtt_cb.down[ch];
    uint8_t *p = data;
    uint32_t rd = r->rd;
    unsigned got = 0;

    while (got < max && rd != r->wr) {
        p[got++] = (uint8_t)r->buf[rd];
        if (++rd == r->size)
            rd = 0;
    }
    DMB();                               /* байты забраны — теперь можно отдать место */
    r->rd = rd;
    return got;
}

Дальше: раздел 22 «Внешняя обвязка и индикация» — сдвиговые регистры, семисегментные индикаторы и адресные светодиоды.

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