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

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.

Поделиться публикацией

Комментарии 19

    +1

    Я помню еще хитрее делал: у меня к stm32 цеплялся ethernet и я просто подключался telnet'ом к плате и уже отправлял весь лог туда.

      +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 не видел, хотя и искал
        +2

        Рад помочь!
        Ну, использование sprintf() это можно сказать "естественная реакция организма" на задачу вывода строки в UART. Собственно, сам так и делал, пока не узнал о ключевом слове "retarget". А дальше все довольно быстро нагуглилось =)

        +1
        Если это NRZ, можно ли SWO как то к UART подцепить и в консоль вывод делать?
          0
          Можно.
          Наиболее удобным способом использования ITM является вывод через SWO с иcпользованием NRZ кодирования — таким образом, нужна всего одна линия, и принимать данные можно будет не только с помощью отладчика со специальным входом, но и обычным USB-UART переходником, пусть и с меньшей скоростью.

          В этом случае с помощью itmdump нужно будет уже подключаться к COM-порту, а не к файлу/пайпу. В мануале на OpenOCD, собственно, такой способ и описывают в подразделе где идет речь о «tpiu config».

          В принципе, можно даже и itmdump не использовать, а включить обычный эмулятор терминала — выводимые символы будет видно, но они будут разбавлены мусором.
            0
            Ой! Всей фразы не заметил…
            Параметры SWO не известны? Она зависит от частоты контроллера?
            нашёл
            8N1, скорость задаётся SWCLKэто для манчестерского. Хрен знает, какая тут скорость… Переходником цеплять не лучший вариант. Не подобрать стандартную скорость…
              0
              Баудрейт для 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 я как раз и тестил такой режим.
                0
                Дурацкий вопрос. При настройке SWO, отладка не отвалится?
                Если, допустим разделить отладку через SWD и настроить скорость сообщения в SWO по ком-порту.
                  0

                  Не отвалится. Настроить скорость можно хоть в самой прошивке, хоть через отладчик.

          +1
          По поводу RTT — не правда, что завязано на железе от SEGGER. Поищите такую цепочку: OpenOCD repository -> RTT patch.
            0
            Спасибо за наводку, посмотрю.
            Там даже Jlink в результате не нужен?

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

            SWO к сожалению тоже есть далеко не в каждом STM32. Ну а использование UART — это уже нагрузка на МК и использование периферии, т.е. уже не похоже на нормальный режим отладки. Так что на мой взгляд единственным универсальным и при этом эффективным способом логирования является технология типа Segger (там же на самом деле нет ничего такого секретного и проприетарного — просто периодическое чтения блока памяти через отладчик, можно самому легко написать, если есть желание).

            Ну и кстати говоря, если уж говорить про непосредственно Segger, то их ПО (j-link, которое на мой взгляд на голову лучше openocd) спокойно работает с обычными st-link. Так что никакой привязки к их недешёвому железу нет.
              0
              Из всего семейства Сortex-M вывода SWO нет лишь у M0/M0+ — поэтому решение прокатывает в большинстве случаев. Однако, соглашусь с тем, что решение Segger наиболее универсально. Раньше вот даже не знал, что RTT уже c openocd скрестили, а теперь руки чешутся всё это дело проверить.
                0
                Из всего семейства Сortex-M вывода SWO нет лишь у M0/M0+ — поэтому решение прокатывает в большинстве случаев.

                Ну вот например у нас из МК используются исключительно STM32F0, так что для нас это «лишь» является наоборот «всем». )))

                Раньше вот даже не знал, что RTT уже c openocd скрестили, а теперь руки чешутся всё это дело проверить.

                А зачем обязательно openocd использовать? Почему не попробовать более мощный j-link? Оно же даже без логирования и отладки намного удобнее, например хотя бы временем прошивки…
                  0

                  Насколько софт JLink быстрее? Полагаю, что главным фактором, определяющим время прошивки, является всё же скорость соединения дебагера.


                  Ну а в целом: Qt Creator поддерживает только openocd и st-util — это раз, работа под линуксом — это два (хотя мб софт jlink тут тоже работает), openocd это универсальный комбайн, поддерживающий хоть stm32, stm8, отечественные кортексы — это три, целый зоопарк отладчиков помимо jlink и stlink — это четыре, ну и возможность лазить в сходники и делать любые фиксы — это пять.

                    0
                    Насколько софт 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 ПО.
                      0
                      Ну всё, почти все пункты биты =)

                      В десятки раз. На том же железе (но с другой прошивкой программатора).

                      Однако, довольно сильное преувеличение. Для интереса взял STM32F4-DISCO c набортным ST-LinkV2 — сначала пробовал заливать бинарники через openocd, затем перепрошил отладчик в Jlink OB официальной тулзой и повторял через софт SEGGER на той же скорости SWD. На бинарнике в 1МБ — разница почти в 2 раза получилась (22 против 38 секунд). На бинарнике 100кБ — 3 против 4 секунд. На меньших размерах — разница, полагаю, будет еще менее заметна.
                        0
                        Да, я вспомнил что в десятки раз — это касалось не openocd, а родной утилиты от STМ (называется ST-LINK Utility), которой я пользовался для прошивки до j-link.
                          0
                          если всё-же интересно только ускорение прошивки любой ценой, то я добивался 9 секунд для 400к прошивки через обычный USB-UART мост
                          habr.com/ru/post/305800/#Results

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое