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


kdpv.svg


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


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



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


* Upd. — на самом деле, как подсказывают в комментариях, это не так. Есть варианты как на стороне софта так и железа. Поэтому, из перечисленных способов RTT будет, пожалуй, самым универсальным.


И сразу некоторое пояснение по тому софту, что будет использоваться далее. В качестве ОС сейчас у меня 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.




К сожалению, не доступен сервер mySQL