Перенаправляем printf() из STM32 в консоль Qt Creator

в 12:57, , рубрики: debug, itm, printf, qt creator, retarget, stm32, swo, uart, программирование микроконтроллеров

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.

Автор: esynr3z

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js