Хорошие инструменты для отладки встраиваемого ПО микроконтроллеров давно стали делом привычным. Возможности таких инструментов определяются как архитектурой ядра, так и выбором отладчика. Рассмотрим три понятия: DAP (Debug access port), ITM (Instrumentation Trace Macrocell) и RTT (Real-Time Transfer). Всё это «механизмы» позволяющие выводить отладочную информацию в том или ином виде. DAP – это аппаратный блок, который дает доступ к шинам и ядру микроконтроллера. ITM – это специальный блок внутри Cortex-M (начиная с M3 и выше), предназначенный для сообщений с минимальными потерями времени. RTT – технология компании SEGGER, построенная на использовании кольцевого буфера внутри RAM. Именно о ней и пойдет речь в публикации.

RTT Viewer

RTT (Real-Time Transfer) от SEGGER J-Link — это механизм быстрого обмена данными между микроконтроллером и компьютером без остановки CPU. Работает это дело через кольцевые буферы, размещаемые в RAM целевого МК. Технология RTT поддерживает двустороннюю передачу данных, но обычно используются для вывода отладочной информации, попросту говоря нужна для функции printf.

Для настройки необходимо скачать файлы с репозитория и добавить их к проекту. В качестве примера я выбрал устройство, работающее с OpenTherm. Во время отладки потребовался вывод лога сообщений о текущем состоянии газового котла. Проект собран в IDE Keil uVision, но нужно помнить, что RTT не привязан к выбору среды разработки.

Рисунок 1 - Подключение исходников RTT к проекту
Рисунок 1 - Подключение исходников RTT к проекту

Собственно, на этом все, в файле SEGGER_RTT определяются механизмы работы с кольцевым буфером, название файла SEGGER_RTT_printf говорит само за себя. Скомпилируем и запустим проект. В отладке это выглядит примерно так:

Рисунок 2 - Структура Control Block
Рисунок 2 - Структура Control Block

Блок управления RTT (CB, Control Block) содержит идентификатор, количество каналов и описание кольцевых буферов на чтение и запись. В памяти МК это хранится в виде структуры SEGGER_RTT, которая служит «точкой входа» для отладчика J-Link при обмене данными с целевым устройством, для запуска RTT J-Link должен найти в оперативной памяти эту структуру. В большинстве случаев поиск происходит автоматически, но, при необходимости, есть механизмы указания определённого адреса.

Перейдем к практической части. Подключаем заголовочные файлы:

#include <stdio.h>
#include "SEGGER_RTT.h"

Добавляем «обертки» под разные типы сообщений (опционально):

#define LOG_INFO(msg, ...)  { SEGGER_RTT_TerminalOut(0, "INFO: "); SEGGER_RTT_printf(0, msg, ##__VA_ARGS__); }

#define LOG_ERR(msg, ...)   { SEGGER_RTT_TerminalOut(0, "ERROR: "); SEGGER_RTT_printf(0, msg, ##__VA_ARGS__); }

void LOG_FLOAT(const char* prefix, float value, const char* suffix) {
  char buf[64];
  snprintf(buf, sizeof(buf), "%s%.2f%s", prefix, value, suffix);
  SEGGER_RTT_TerminalOut(0, buf);
}

Вызываем функцию инициализации CB

SEGGER_RTT_Init();

Выводим лог:

LOG_INFO("Central Heating: on\r\n", Boiler.isCentralHeatingActive(response) ? "on" : "off");
LOG_ERR("Error: OpenTherm is not initialized");

Утилита RTT Viewer устанавливается вместе с пакетом драйверов для программатора J-Link. При запуске утилиты необходимо выбрать целевой МК, интерфейс подключения программатора (USB, TCP/IP) и интерфейс отладки (SWD, JTAG).

Рисунок 3 - Вывод отладочной информации через RTT Viewer
Рисунок 3 - Вывод отладочной информации через RTT Viewer

Обратите внимание, что макросы LOG_INFO, LOG_ERR используют функции SEGGER_RTT_TerminalOut(…) и SEGGER_RTT_printf(…) которые позволяют выбрать канал и терминал. Номер терминала и канала это не одно и то же. Первое служит лишь визуальной составляющей при работе с утилитой RTT Viewer, т.е. позволяет выводить информацию на разные вкладки. Второе относится к числу структур CB (SEGGER_RTT) в памяти МК и позволяет организовать более продвинутую передачу пользовательских данных.

Технология SEGGER RTT имеет ряд существенных преимуществ:

  • позволяет выводить лог printf через SWD, не задействуя дополнительных выводов МК, т.е. не требуется настройка ножки SWO;

  • может использоваться параллельно работе отладчика IDE, поскольку использует аппаратный блок DAP (работает на Cortex-M0/M0+ где нет блока ITM);

  • проста в настройке, базовая структура занимает около 24 байт (ID) + по 24 байта на каждый объявленный канал в RAM + размеры буферов;

  • имеет очень высокую скорость работы:

    Рисунок 4 - Сравнение скорости работы RTT с другими технологиями [1]
    Рисунок 4 - Сравнение скорости работы RTT с другими технологиями [1]

System Viewer

На механизме кольцевых буферов RTT работает ещё одна крайне полезная утилита, к��торая использует стандартную библиотеку SEGGER RTT как транспортный уровень. SystemView – это инструмент для анализа и визуализации работы встраиваемых систем в режиме реального времени. Настройка проекта для работы с SystemView чуть сложнее, чем в случае с RTT. Разобьём последовательность действий на несколько шагов.

Шаг 1. Скачиваем необходимые файлы из репозитория и подключаем к проекту. Для примера я взял контроллер адресных светодиодов, работающий на плате We Act STM32F411.

Рисунок 5 - Подключение исходников SEGGER_SYSVIEW к проекту
Рисунок 5 - Подключение исходников SEGGER_SYSVIEW к проекту

SystemView работает, перехватывая макросы трассировки FreeRTOS (trace-макросы), которые находятся в ключевых точках ядра FreeRTOS (например, при создании задачи или переключении контекста). По умолчанию эти макросы пусты, поэтому не влияют на работу программы. SystmView определяет этим макросы и ядро автоматически вызывает код в нужный момент. Определения макросов можно найти в файле SEGGER_SYSVIEW_FreeRTOS.h, выглядят они примерно так:

#define traceTASK_SWITCHED_IN()  SEGGER_SYSVIEW_OnTaskStartExec((U32)pxCurrentTCB)
#define traceTASK_SWITCHED_OUT() SEGGER_SYSVIEW_OnTaskStopExec()
#define traceISR_ENTER()         SEGGER_SYSVIEW_RecordEnterISR()
// и т.д.

Шаг 2. В самом конце файла FreeRTOSConfig.h подключаем заголовочный файл SEGGER_SYSVIEW_FreeRTOS.h. Убеждаемся, что:

#define configUSE_TRACE_FACILITY 1, 

для FreeRTOS V10+ добавляем макрос

#define INCLUDE_xTaskGetIdleTaskHandle 1 

Шаг 3. Cortex-M3/M4 имеют встроенный модуль DWT, который содержит 32-битный счетчик циклов (CYCCNT). SystemView использует его как эталон времени. Для того, чтобы предоставить SystemView доступ к счётчику вносим свой код в функцию SEGGER_SYSVIEW_Config():

void SEGGER_SYSVIEW_Conf(void) {

  // ---- Setup DWT (for Cortex-M3/M4) ----
  #define DEMCR           (*(volatile U32*)0xE000EDFCu)
  #define DWT_CTRL        (*(volatile U32*)0xE0001000u)
  #define DWT_CYCCNT      (*(volatile U32*)0xE0001004u)
  #define DEMCR_TRCENA    (1u << 24)
  #define DWT_CTRL_CYCCNTENA (1u << 0)

  DEMCR |= DEMCR_TRCENA;                    // Enable access to TRCENA
  DWT_CYCCNT = 0;                           // Clear  counter
  DWT_CTRL |= DWT_CTRL_CYCCNTENA;           // Start DWT count

  SEGGER_SYSVIEW_Init(SYSVIEW_TIMESTAMP_FREQ, SYSVIEW_CPU_FREQ, 
                      &SYSVIEW_X_OS_TraceAPI, _cbSendSystemDesc);
  SEGGER_SYSVIEW_SetRAMBase(SYSVIEW_RAM_BASE);
}

Поле установки регистра DWT_CTRL |= DWT_CTRL_CYCCNTENA счётчик DWT_CYCCNT будет инкрементироваться каждый так процессора.

На этом шаге также проверьте макрос #define SYSVIEW_RAM_BASE в соответствии с адресом RAM на выбранном МК.

Шаг 4. В отличие от переключения задач, которое SystemView перехватывает через макросы ядра FreeRTOS, прерывания нужно «подсвечивать». Для этого в обработчики прерываний добавляем вызовы функций SEGGER_SYSVIEW_RecordEnterISR() и SEGGER_SYSVIEW_RecordExitISR(). Ориентировочно это выглядит так:

void EINT15_10_IRQHandler()
{
  SEGGER_SYSVIEW_RecordEnterISR();
  if(EINT->IPEND & EINT_IPEND_11)  {  
    // Ваш код
  }
  SEGGER_SYSVIEW_RecordExitISR();  
}

Шаг 5. Для того, чтобы SystemView знала имена прерываний, ей необходимо об этом сообщить, дополнив функцию _cbSendSystemDesc(). Она находится в файле SEGGER_SYSVIEW_Config_FreeRTOS.c.

static void _cbSendSystemDesc(void) {
  SEGGER_SYSVIEW_SendSysDesc("N="SYSVIEW_APP_NAME",D="SYSVIEW_DEVICE_NAME",O=FreeRTOS");
  SEGGER_SYSVIEW_SendSysDesc("I#15=SysTickIRQ");
  SEGGER_SYSVIEW_SendSysDesc("I#46=ModbusIRQ");
  //...
}

Шаг 6. Информацию, подготовленную на предыдущих шагах необходимо передать SystemView. Для этого добавим две в main (или другое удобное место) дополнительные функции:

// A function to get the number of the current active interrupt (ISR)
U32 SEGGER_SYSVIEW_X_GetInterruptId(void) {
  // In the Cortex-M core, the interrupt number is stored in the lower 9 bits of the IPSR register.
  return ((*(volatile U32*)(0xE000ED04u)) & 0x1FFu);
}

// In Cortex-M3/M4, we use the DWT_CYCCNT core clock counter register
U32 SEGGER_SYSVIEW_X_GetTimestamp(void) {
   return (*(volatile U32*)(0xE0001004u));
}

Шаг 7. Теперь мы готовы к запуску. После инициализации аппаратной части (тактирование и прочей периферии) в функции main размещаем SEGGER_SYSVIEW_Conf(); И только после этого запускаем диспетчер vTaskStartScheduler();

InitPhy();
SEGGER_SYSVIEW_Conf();            // Segger SystemView Initialization
CreateDeviceTasks();
vTaskStartScheduler();            // Start the real time scheduler

Остается лишь запустить утилиту SystemView (можно скачать на сайте SEGGER), в случае успешной настройки получаем достойную награду за приложенные усилия.

Рисунок 6 - Окно утилиты Segger SystemView
Рисунок 6 - Окно утилиты Segger SystemView

Изучение интерфейса утилиты и доступных опций остаются заинтересованному читателю. Вероятно настройка под ваш микроконтроллер не пойдет гладко, но к счастью даже бесплатные нейросети дают правильные ответы по выбранной теме. Уверен, вы без труда сможете подключить SystemView к своему проекту. Преимущества от использования SystemView очевидны, но хочу упомянуть следующую фишку: утилита работает даже в том случае, если целевой МК не находится в режиме отладки. Т.е. вы можете разместить своё устройство в реальных условиях эксплуатации, а когда что-то пойдет не так, просто подключить JTAG и заглянуть внутрь прошивки. Как по мне, это просто фантастика.

Целью этой небольшой публикации было знакомство читателя с технологиями RTT и SystmView и общим паттерном их настройки. Тем кому тема показалась интересной, предлагаю ответить на следующие вопросы:

  1. Почему SystemView использует DWT_CYCCNT  вместо того, чтобы считывать значения SysTick?

  2. Через какой канал (буфер) RTT работает SystemView? Можно ли его менять?

  3. Какую из двух функций SEGGER_RTT_printf или SEGGER_SYSVIEW_Print лучше использовать в проектах с RTOS?

  4. Можно ли использовать SystemView без RTOS?

  5. Какие существуют инструменты, аналогичные SystemView?

Ресурсы

  1. RTT. База знаний Segger;

  2. Исходные файлы RTT;

  3. J-Link Software and Documentation Pack;

  4. Исходные файлы SystmView;

  5. Утилита SystemView для ПК.