Нередко при отладке ПО микроконтроллера возникает необходимость вывода отладочных сообщений, логов, захваченных данных и прочего на экран ПК. При этом хочется, чтобы и вывод был побыстрее, и чтобы строки отображались не где-нибудь, а прямо в IDE — не отходя от кода, так сказать. Собственно, об этом и статья — как я пытался printf() выводить и отображать внутри любимой, но не очень микроконтроллерной, среды Qt Creator.
В целом, можно придумать огромное количество способов вывода текстовой информации из микроконтроллера. Однако, наиболее часто применяющихся техник не так уж и много:
- Semihosting
- Segger RTT
- USB-CDC
- UART
- ITM
Semihosting — довольно медленный, RTT — завязан на программно-аппаратные решения Segger*, USB — есть не в каждом микроконтроллере. Поэтому обычно, я отдаю предпочтение последним двум — использование UART и ITM. О них и пойдёт ниже речь.
* Upd. — на самом деле, как подсказывают в комментариях, это не так. Есть варианты как на стороне софта так и железа. Поэтому, из перечисленных способов RTT будет, пожалуй, самым универсальным.
И сразу некоторое пояснение по тому софту, что будет использоваться далее. В качестве ОС сейчас у меня Fedora 28, а текущей связкой ПО для работы с микроконтроллерами являются:
- Qt Creator 4.8.1 (прямая ссылка на релизы, довольно тщательно спрятанная на сайте)
- GNU Arm Embedded Toolchain 7
- OpenOCD 0.10.0+dev
Перенаправление printf() в GCC
Итак, чтобы в GCC перенаправить вывод printf() необходимо добавить в ключи линкера
-specs=nosys.specs -specs=nano.specs
Если будет необходим вывод чисел с плавающей запятой, то нужно не забыть ключ
-u_printf_float
И реализовать функцию _write(). Например, примерно так
int _write(int fd, char* ptr, int len)
{
(void)fd;
int i = 0;
while (ptr[i] && (i < len)) {
retarget_put_char((int)ptr[i]);
if (ptr[i] == '\n') {
retarget_put_char((int)'\r');
}
i++;
}
return len;
}
где retarget_put_char() — это функция, которая будет загружать символ непосредственно в нужный интерфейс.
printf() -> ITM -> Qt Creator
Instrumentation Trace Macrocell (ITM) — это блок внутри ядра Cortex-M3/M4/M7, используемый для неинвазивного вывода (трассировки) различного вида диагностической информации. Для реализации printf() об ITM необходимо знать следующее:
- Использует тактовый сигнал TRACECLKIN, частота которого обычно равна частоте работы ядра
- Имеет 32 штуки так называемых stimulus ports для вывода данных
- CMSIS имеет в своем составе функцию ITM_SendChar(), которая загружает символ в stimulus port 0
- Данные выводятся наружу либо через синхронную шину (TRACEDATA, TRACECLK), либо по асинхронной однопроводной линии SWO (TRACESWO)
- Линия SWO обычно мультиплексирована с JTDO, а значит работает только в режиме отладки по SWD
- Вывод по SWO осуществляется либо с использованием кода Манчестер, либо NRZ (UART 8N1)
- Данные передаются фреймами определенного формата — нужен парсер на приёмной стороне
- Настраивается ITM обычно из IDE или соответствующей утилиты (однако, никто не запрещает настроить в коде программы — тогда вывод в SWO будет работать без поднятой отладочной сессии)
Наиболее удобным способом использования ITM является вывод через SWO с иcпользованием NRZ кодирования — таким образом, нужна всего одна линия, и принимать данные можно будет не только с помощью отладчика со специальным входом, но и обычным USB-UART переходником, пусть и с меньшей скоростью.
Я пошел по пути с использованием отладчика, и был вынужден доработать свой китайский STLink-V2, чтобы он стал поддерживать SWO. Далее всё просто — подключаем JTDO/TRACESWO микроконтроллера к соответствующему пину отладчика, и идём настраивать софт.
В openocd есть команда "tpiu config" — с помощью неё можно настроить способ вывода трассировочной информации (более подробно в OpenOCD User’s Guide). Так например, использование аргументов
tpiu config internal /home/esynr3z/itm.fifo uart off 168000000
настроит вывод в файл /home/esynr3z/itm.fifo, использование NRZ кодирования, и рассчитает максимальную скорость передачи, исходя из частоты TRACECLKIN 168 МГц — для STLink это 2МГц. А ещё одна команда
itm port 0 1
включит нулевой порт для передачи данных.
В состав исходников OpenOCD входит утилита itmdump (contrib/itmdump.c) — с помощью неё можно осуществить парсинг строк из полученных данных.
Чтобы скомпилировать вводим
gcc itmdump.c -o itmdump
При запуске указываем необходимый файл/pipe/ttyUSB* и ключ -d1 для того, чтобы выводить полученные байты данных как строки
./itmdump -f /home/esynr3z/itm.fifo -d1
И последнее. Чтобы отправить символ по SWO, дополняем _write(), описанный выше, функцией
int retarget_put_char(int ch)
{
ITM_SendChar((uint32_t)ch);
return 0;
}
Итак, общий план такой: внутри Qt Creator конфигурируем openocd на сохранение всей получаемой информации по SWO в предварительно созданный named pipe, а чтение pipe, парсинг строк и вывод на экран выполняем с помощью itmdump, запущенной как External Tool. Безусловно, существует и более элегантный способ решения поставленной задачи — написать соответствующий плагин для Qt Creator. Однако, надеюсь, что и описанный ниже подход окажется кому-нибудь полезным.
Заходим в настройки плагина Bare Metal (Tools->Options->Devices->Bare Metal).
Выбираем используемый GDB-сервер и добавляем в конец списка команд инициализации строки
monitor tpiu config internal /home/esynr3z/itm.fifo uart off 168000000
monitor itm port 0 1
Теперь, непосредственно перед тем как отладчик поставит курсор в самое начало main() будет происходить настройка ITM.
Добавляем itmdump в качестве External Tool (Tools->External->Configure...).
Не забываем установить переменную
QT_LOGGING_TO_CONSOLE=1
для отображения вывода утилиты в консоль Qt Creator (панель 7 General Messages).
Теперь включаем itmdump, активируем режим дебага, запускаем исполнение кода и… ничего не происходит. Однако, если прервать отладку, исполнение itmdump завершится, и на вкладке General Messages появятся все выведенные через printf() строки.
Путём недолгих изысканий было установлено, что строки из itmdump необходимо буферизировать и выводить в stderr — тогда они появляются в консоли интерактивно, во время отладки программы. Модифицированную версию itmdump я залил на GitHub.
Есть есть еще один нюанс. Отладка при запуске будет зависать на выполнении команды "monitor tpiu config ...", если не будет предварительно запущен itmdump. Происходит это из-за того, что открытие pipe (/home/esynr3z/itm.fifo) внутри openocd на запись — блокирующее, и дебагер будет висеть до тех пор, пока pipe не откроется на чтение с другого конца.
Это несколько неприятно, особенно, если в какой-то момент ITM не будет нужен, но придется вхолостую запускать itmdump, либо постоянно переключать GDB-сервер или удалять/добавлять строки в его настройках. Поэтому пришлось немного поковырять исходники openocd и найти то место, куда нужно подставить небольшой костыль.
В файле src/target/armv7m_trace.c есть строка с искомой процедурой открытия
armv7m->trace_config.trace_file = fopen(CMD_ARGV[cmd_idx], "ab");
её нужно заменить на
int fd = open(CMD_ARGV[cmd_idx], O_CREAT | O_RDWR, 0664);
armv7m->trace_config.trace_file = fdopen(fd, "ab");
Теперь наш pipe будет открываться сразу и не отсвечивать. А значит можно оставить настройки Bare Metal в покое, а itmdump запускать только когда это нужно.
В итоге, вывод сообщений во время отладки выглядит так
printf() -> UART -> Qt Creator
В этом случае всё примерно так же:
- Добавляем в код функцию с инициализацией UART
- Реализуем retarget_put_char(), где символ будет отправляться в буфер приемопередатчика
- Подключаем USB-UART адаптер
- Добавляем в External Tools утилиту, которая будет читать строки из виртуального COM-порта и выводить их на экран
Я набросал такую утилиту на C — uartdump. Использование довольно простое — нужно указать лишь имя порта и баудрейт.
Однако, стоит отметить одну особенность. Работа этой утилиты не зависит от отладки, а Qt Creator не предлагает никаких опций для закрытия запущенных External Tools. Поэтому, для прекращения чтения COM-порта я добавил ещё один внешний инструмент.
Ну и на всякий случай приложу ссылку на шаблон CMake проекта, который фигурировал на скринах — GitHub.
Комментарии (16)
Sdima1357
12.02.2019 19:30+1Спасибо, пригодилось. До сих пор использовал вот такую вставку:
void vprint(const char *fmt, va_list argp)
{
char string[MAX_PRINT_LINE];
if(0 < vsprintf(string,fmt,argp)) // build string
{
HAL_UART_Transmit(&huart2, (uint8_t*)string, strlen(string), 0xffffff); // send message via UART
//CDC_Transmit_FS((uint8_t*)string, strlen(string));
}
}
void mprintf(const char *fmt, ...) // custom printf() function
{
va_list argp;
va_start(argp, fmt);
vprint(fmt, argp);
va_end(argp);
}
Ну а переопределение _write не видел, хотя и искалesynr3z Автор
12.02.2019 19:45+2Рад помочь!
Ну, использование sprintf() это можно сказать "естественная реакция организма" на задачу вывода строки в UART. Собственно, сам так и делал, пока не узнал о ключевом слове "retarget". А дальше все довольно быстро нагуглилось =)
xztau
12.02.2019 20:05+1Если это NRZ, можно ли SWO как то к UART подцепить и в консоль вывод делать?
esynr3z Автор
12.02.2019 20:39Можно.
Наиболее удобным способом использования ITM является вывод через SWO с иcпользованием NRZ кодирования — таким образом, нужна всего одна линия, и принимать данные можно будет не только с помощью отладчика со специальным входом, но и обычным USB-UART переходником, пусть и с меньшей скоростью.
В этом случае с помощью itmdump нужно будет уже подключаться к COM-порту, а не к файлу/пайпу. В мануале на OpenOCD, собственно, такой способ и описывают в подразделе где идет речь о «tpiu config».
В принципе, можно даже и itmdump не использовать, а включить обычный эмулятор терминала — выводимые символы будет видно, но они будут разбавлены мусором.xztau
12.02.2019 21:46Ой! Всей фразы не заметил…
Параметры SWO не известны? Она зависит от частоты контроллера?
нашёл
8N1,скорость задаётся SWCLKэто для манчестерского. Хрен знает, какая тут скорость… Переходником цеплять не лучший вариант. Не подобрать стандартную скорость…esynr3z Автор
12.02.2019 22:33Баудрейт для SWO настраивается путем деления TRACECLKIN (Asynchronous_Reference_Clock далее; частота обычно равна частоте ядра) c помощью делителя SWOSCALER, задаваемым в регистре TPIU->ACPR
SWO output clock = Asynchronous_Reference_Clock/(SWOSCALAR +1)
Приведенные в статье вызовы команды «tpiu config» происходят без последнего аргумента, который как раз и определит желаемую скорость SWO. Делал я это для того, чтобы openocd сам считал максимально возможную скорость для текущего отладчика.
Но если бы я хотел использовать USB-UART, скажем на 115200, я бы писал:
tpiu config external uart off 168000000 115200
Ну и в общем то, на 115200 я как раз и тестил такой режим.
besitzeruf
14.02.2019 07:53По поводу RTT — не правда, что завязано на железе от SEGGER. Поищите такую цепочку: OpenOCD repository -> RTT patch.
AlexPublic
14.02.2019 17:40Semihosting — довольно медленный, RTT — завязан на программно-аппаратные решения Segger, USB — есть не в каждом микроконтроллере. Поэтому обычно, я отдаю предпочтение последним двум — использование UART и ITM.
SWO к сожалению тоже есть далеко не в каждом STM32. Ну а использование UART — это уже нагрузка на МК и использование периферии, т.е. уже не похоже на нормальный режим отладки. Так что на мой взгляд единственным универсальным и при этом эффективным способом логирования является технология типа Segger (там же на самом деле нет ничего такого секретного и проприетарного — просто периодическое чтения блока памяти через отладчик, можно самому легко написать, если есть желание).
Ну и кстати говоря, если уж говорить про непосредственно Segger, то их ПО (j-link, которое на мой взгляд на голову лучше openocd) спокойно работает с обычными st-link. Так что никакой привязки к их недешёвому железу нет.esynr3z Автор
14.02.2019 19:41Из всего семейства Сortex-M вывода SWO нет лишь у M0/M0+ — поэтому решение прокатывает в большинстве случаев. Однако, соглашусь с тем, что решение Segger наиболее универсально. Раньше вот даже не знал, что RTT уже c openocd скрестили, а теперь руки чешутся всё это дело проверить.
AlexPublic
15.02.2019 02:36Из всего семейства Сortex-M вывода SWO нет лишь у M0/M0+ — поэтому решение прокатывает в большинстве случаев.
Ну вот например у нас из МК используются исключительно STM32F0, так что для нас это «лишь» является наоборот «всем». )))
Раньше вот даже не знал, что RTT уже c openocd скрестили, а теперь руки чешутся всё это дело проверить.
А зачем обязательно openocd использовать? Почему не попробовать более мощный j-link? Оно же даже без логирования и отладки намного удобнее, например хотя бы временем прошивки…esynr3z Автор
15.02.2019 06:39Насколько софт JLink быстрее? Полагаю, что главным фактором, определяющим время прошивки, является всё же скорость соединения дебагера.
Ну а в целом: Qt Creator поддерживает только openocd и st-util — это раз, работа под линуксом — это два (хотя мб софт jlink тут тоже работает), openocd это универсальный комбайн, поддерживающий хоть stm32, stm8, отечественные кортексы — это три, целый зоопарк отладчиков помимо jlink и stlink — это четыре, ну и возможность лазить в сходники и делать любые фиксы — это пять.
AlexPublic
15.02.2019 16:24Насколько софт JLink быстрее? Полагаю, что главным фактором, определяющим время прошивки, является всё же скорость соединения дебагера.
В десятки раз. На том же железе (но с другой прошивкой программатора).
Qt Creator поддерживает только openocd и st-util
Это не так. Qt Creator изначально поддерживает ровно один универсальный режим отладки — удалённый gdb. Плюс к этому там есть две дополнительные предустановки для st-link и openocd. Однако это всего лишь для удобства — можно без проблем настроить тот же openocd (который естественно реализуют gdb сервер) через общий универсальный режим (он в настройках Qt Creator называется «по умолчанию»).
Так что отладка через j-link (ПО, а в качестве железа служит обычный st-link, перепрошитый под j-link их официальной утилитой) у меня отлично работает из Qt Creator.
работа под линуксом — это два (хотя мб софт jlink тут тоже работает)
Есть и под винду и под линух и под мак.
openocd это универсальный комбайн, поддерживающий хоть stm32, stm8, отечественные кортексы — это три
Так j-link аналогично. И более того, там получается одно универсальное решение не только со стороны ПО, но и одно аппаратное решение для всех МК.
целый зоопарк отладчиков помимо jlink и stlink — это четыре
В данном сравнение это получается уже скорее минус, чем плюс. )))
ну и возможность лазить в сходники и делать любые фиксы — это пять.
А вот это да, я бы тоже предпочёл иметь открытые исходники ПО от Segger. Но боюсь в таком случае у них не вышло бы нормально зарабатывать… ))) Так что уж лучше качественное закрытoe ПО.
Gorthauer87
Я помню еще хитрее делал: у меня к stm32 цеплялся ethernet и я просто подключался telnet'ом к плате и уже отправлял весь лог туда.