Нередко при отладке ПО микроконтроллера возникает необходимость вывода отладочных сообщений, логов, захваченных данных и прочего на экран ПК. При этом хочется, чтобы и вывод был побыстрее, и чтобы строки отображались не где-нибудь, а прямо в IDE — не отходя от кода, так сказать. Собственно, об этом и статья — как я пытался printf() выводить и отображать внутри любимой, но не очень микроконтроллерной, среды Qt Creator.
В целом, можно придумать огромное количество способов вывода текстовой информации из микроконтроллера. Однако, наиболее часто применяющихся техник не так уж и много:
- Semihosting
- Segger RTT
- USB-CDC
- UART
- ITM
Semihosting — довольно медленный, RTT — завязан на программно-аппаратные решения Segger, USB — есть не в каждом микроконтроллере. Поэтому обычно, я отдаю предпочтение последним двум — использование UART и ITM. О них и пойдёт ниже речь.
И сразу некоторое пояснение по тому софту, что будет использоваться далее. В качестве ОС сейчас у меня 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.
Автор: esynr3z