[Из песочницы] Перенаправляем printf() из STM32 в консоль Qt Creator

kdpv.svg

Нередко при отладке ПО микроконтроллера возникает необходимость вывода отладочных сообщений, логов, захваченных данных и прочего на экран ПК. При этом хочется, чтобы и вывод был побыстрее, и чтобы строки отображались не где-нибудь, а прямо в IDE — не отходя от кода, так сказать. Собственно, об этом и статья — как я пытался printf () выводить и отображать внутри любимой, но не очень микроконтроллерной, среды Qt Creator.

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

Semihosting — довольно медленный, RTT — завязан на программно-аппаратные решения Segger, USB — есть не в каждом микроконтроллере. Поэтому обычно, я отдаю предпочтение последним двум — использование UART и ITM. О них и пойдёт ниже речь.

И сразу некоторое пояснение по тому софту, что будет использоваться далее. В качестве ОС сейчас у меня Fedora 28, а текущей связкой ПО для работы с микроконтроллерами являются:


Перенаправление 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).

config_baremetal.png

Выбираем используемый 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…).

external_itmdump.png

Не забываем установить переменную

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 запускать только когда это нужно.

В итоге, вывод сообщений во время отладки выглядит так

debug.png


printf () → UART → Qt Creator

В этом случае всё примерно так же:


  • Добавляем в код функцию с инициализацией UART
  • Реализуем retarget_put_char (), где символ будет отправляться в буфер приемопередатчика
  • Подключаем USB-UART адаптер
  • Добавляем в External Tools утилиту, которая будет читать строки из виртуального COM-порта и выводить их на экран

Я набросал такую утилиту на C — uartdump. Использование довольно простое — нужно указать лишь имя порта и баудрейт.

external_uartdump.png

Однако, стоит отметить одну особенность. Работа этой утилиты не зависит от отладки, а Qt Creator не предлагает никаких опций для закрытия запущенных External Tools. Поэтому, для прекращения чтения COM-порта я добавил ещё один внешний инструмент.

external_uartdump_close.png

Ну и на всякий случай приложу ссылку на шаблон CMake проекта, который фигурировал на скринах — GitHub.

© Habrahabr.ru