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: по ней отладчик находит блок поиском в заданном диапазоне.
Дальше — число каналов и описатели. У каждого канала своё кольцо. Канал
«вверх» пишет прошивка, читает компьютер. Канал «вниз» — наоборот, это
ввод с клавиатуры в прошивку.
Главное правило: указатель записи двигает только тот, кто пишет, указатель чтения — только тот, кто читает. Каждое 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 Итоги
- RTT — кольца в ОЗУ и управляющий блок с признаком
SEGGER RTT; отладчик находит его поиском и читает память по SWD, не останавливая ядро. Выводов сверх SWD — 0, работает и на Cortex‑M0/M0+. - Блок: 16 байт признака, два счётчика и описатели по 24 байта; при четырёх каналах «вверх» и одном «вниз» — 144 байта.
wrдвигает только писатель,rd— только читатель: блокировки не нужны. Один байт кольца всегда пуст — в 64 байта помещается 63.- Переполнение 160 байт в кольцо на 64: пропуск оставил 48 байт (3 целых сообщения), обрезка — 63 байта с обрубком, блокировка довела все 160, но без читателя ждёт вечно.
- Запись строки: 53 + 8 команд на байт на M3/M4 (gcc), 84 + 11 на M0; armclang на M4 — 7 команд на байт.
- В QEMU читателем служит порт GDB: gdb командами
xи свой читатель на Python, который забирает вывод и отправляет строку в прошивку.
Полные исходники
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 «Внешняя обвязка и индикация» — сдвиговые регистры, семисегментные индикаторы и адресные светодиоды.