使用SystemView分析FreeRTOS应用

1.前言

       在使用FreeRTOS进行应用开发时经常会遇到普通的方式难以调试的问题,如栈内存不足等等,同时也希望对运行的多个Task进行实时的性能及资源占用的分析,通常的调试手段在这里就变的心有余而力不足了。以FreeRTOS为例,如何在长时间的运行过程中收集调试数据进行分析,以及如何调试不同的组件(如Queue,Notification,Semaphore等等)?这个时候就需要Trace工具帮忙了。针对RTOS的Trace需求,有多种商业工具可供选择,但其中可供免费使用的产品则寥寥无几。本文将针对SEGGER开发的SystemView Trace工具进行介绍。

2. SystemView

       SystemView是SEGGER开发的针对嵌入式系统的trace工具,支持多种RTOS,也支持自定义OS的移植(需实现trace API,参见User Manual)。其核心基于SEGGER RTT,一个Host-Target间的通信框架,可通过多种方式连接,除J-LINK之外还可以使用串口及TCP-IP协议,对非商业用途免费且无功能限制。

3. 环境准备

       我们使用的测试环境是STM32H750VB开发板,CPU为Cortex-M7,最高主频480MHz,程序位于QSPI Flash中,通过XIP方式运行。FreeRTOS为STM32CubeMX配置的未修改版本。运行一个自定义Task,Toggle开发板上的一个LED灯。

//测试Task
TaskHandle_t xTaskHelloHandle = NULL;
void vTaskHello(void *pvParameters) {
    for(;;) {
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_1);
        SEGGER_SYSVIEW_PrintfTarget("Hello world @%lu\n", HAL_GetTick());
        //这里使用了SystemView系统库提供的输出函数,向主机输出Message
        vTaskDelay(1000);
    }
    vTaskDelete(NULL);
}

在启动时自动创建该Task:

xTaskCreate(vTaskHello, "hello_task", 1024, NULL, 7, NULL);

栈深度为1024字,优先级为7

4. SystemView trace库移植

首先下载目标板源码,将其中的 SEGGER 目录下代码以及 Sample 中的FreeRTOS代码添加到项目。

SystemView
├── Config
│   ├── Global.h
│   ├── SEGGER_RTT_Conf.h
│   ├── SEGGER_SYSVIEW_Conf.h
│   └── SEGGER_SYSVIEW_Config_FreeRTOS.c
└── SEGGER
    ├── SEGGER.h
    ├── SEGGER_RTT.c
    ├── SEGGER_RTT.h
    ├── SEGGER_RTT_ASM_ARMv7M.s
    ├── SEGGER_RTT_printf.c
    ├── SEGGER_SYSVIEW.c
    ├── SEGGER_SYSVIEW.h
    ├── SEGGER_SYSVIEW_ConfDefaults.h
    ├── SEGGER_SYSVIEW_FreeRTOS.c
    ├── SEGGER_SYSVIEW_FreeRTOS.h
    ├── SEGGER_SYSVIEW_Int.h
    └── Syscalls
        └── SEGGER_RTT_Syscalls_GCC.c

3 directories, 16 files

接下来我们需要实现通过串口的RTT数据收发,这里使用一个额外的Task实现这个操作。

#define SYSVIEW_COMM_APP_HELLO_SIZE     32
#define SYSVIEW_COMM_TARGET_HELLO_SIZE  32

#define SYSVIEW_SINGLE_TX 256

U8 hello_message[SYSVIEW_COMM_TARGET_HELLO_SIZE] = {
    'S', 'E', 'G', 'G', 'E', 'R', ' ',
    'S', 'y', 's', 't', 'e', 'm', 'V', 'i', 'e', 'w',
    ' ', 'V', '0' + SEGGER_SYSVIEW_MAJOR,
    '.', '0' + (SEGGER_SYSVIEW_MINOR / 10),
    '0' + (SEGGER_SYSVIEW_MINOR % 10),
    '.', '0' + (SEGGER_SYSVIEW_REV / 10),
    '0' + (SEGGER_SYSVIEW_REV % 10),
    '\0', 0, 0, 0, 0, 0 
};

TaskHandle_t xTaskTraceCommHandle = NULL;
void vTaskTraceComm(void *pvParameters) {
    //获取Channel ID
    int channel_id = SEGGER_SYSVIEW_GetChannelID();

    //发送HELLO包
    HAL_UART_Transmit(&huart1, hello_message, SYSVIEW_COMM_TARGET_HELLO_SIZE, 1000);

    uint8_t rx_buf;
    uint8_t tx_buf[SYSVIEW_SINGLE_TX];

    //启动记录
    SEGGER_SYSVIEW_Start();

    //等待通过串口中断接收数据
    HAL_UART_Receive_IT(&huart1, &rx_buf, 0x01);

    for(;;) {
        if(xTaskNotifyWait(0x00, 0x01, NULL, pdMS_TO_TICKS(400)) == pdTRUE) {
            //接收到数据,写入到RTT缓存
            SEGGER_RTT_WriteDownBufferNoLock(channel_id, &rx_buf, 0x01);
            HAL_UART_Receive_IT(&huart1, &rx_buf, 0x01);
        }
        //获取上行缓存数据长度
        unsigned int tx_length = SEGGER_RTT_GetBytesInBuffer(channel_id);
        if(tx_length >= SYSVIEW_SINGLE_TX) { 
            //STM32 HAL代码有最大长度限制,这里默认设置为256字节,可调整
            //从RTT缓存中读出数据
            uint32_t num = SEGGER_RTT_ReadUpBufferNoLock(channel_id, tx_buf, SYSVIEW_SINGLE_TX);
            //通过串口发送,此处可使用中断方式发送,尚未实现。
            HAL_UART_Transmit(&huart1, tx_buf, num, 100);
        } else if (tx_length != 0){
            // 同上
            uint32_t num = SEGGER_RTT_ReadUpBufferNoLock(channel_id, tx_buf, tx_length);
            HAL_UART_Transmit(&huart1, tx_buf, num, 100);
        }
    }
    vTaskDelete(NULL);
}

//串口ISR
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if(huart->Instance == USART1) {
        //向Task发送通知,收到数据
        BaseType_t higher_woken = pdFALSE;
        xTaskNotifyFromISR(xTaskTraceCommHandle, 0x01, eSetBits, &higher_woken);
        portYIELD_FROM_ISR(higher_woken);
    }
}

创建Task:

xTaskCreate(vTaskTraceComm, "trace_task", 1024, NULL, 6, &xTaskTraceCommHandle);

在RTOS调度器启动之前执行 SEGGER_SYSVIEW_Conf()

默认情况下SystemView会从DWT的Cycle counter获取系统当前周期数用于生成时间戳:

/*********************************************************************
*
*       SystemView timestamp configuration
*/
#if !defined(SEGGER_SYSVIEW_GET_TIMESTAMP) && !defined(SEGGER_SYSVIEW_TIMESTAMP_BITS)
  #if SEGGER_SYSVIEW_CORE == SEGGER_SYSVIEW_CORE_CM3
    #define SEGGER_SYSVIEW_GET_TIMESTAMP()      (*(U32 *)(0xE0001004))          // Retrieve a system timestamp. Cortex-M cycle counter.
    #define SEGGER_SYSVIEW_TIMESTAMP_BITS       32                              // Define number of valid bits low-order delivered by clock source
  #else
    #define SEGGER_SYSVIEW_GET_TIMESTAMP()      SEGGER_SYSVIEW_X_GetTimestamp() // Retrieve a system timestamp via user-defined function
    #define SEGGER_SYSVIEW_TIMESTAMP_BITS       32                              // Define number of valid bits low-order delivered by SEGGER_SYSVIEW_X_GetTimestamp()
  #endif
#endif

在STM32CubeMX的初始化代码中该计数器默认未启用,在这里手工启用。

CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

修改 FreeRTOSConfig.h 引入SystemView的trace宏:

/* USER CODE BEGIN Defines */   	      
/* Section where parameter definitions can be added (for instance, to override default ones in FreeRTOS.h) */

#define INCLUDE_xTaskGetIdleTaskHandle 1

#include "SEGGER_SYSVIEW_FreeRTOS.h"

/* USER CODE END Defines */ 

至此,我们的移植过程基本结束。

注:在SEGGER代码中, _write 部分的实现存在签名不兼容的问题,此函数是用于将stdout通过RTT输出到SystemView,可直接删除 SEGGER/Syscalls/SEGGER_RTT_Syscalls_GCC.c ,也可修改函数签名使其与系统库一致。

注2:在初始化时使用了 strcpy() 函数,在测试环境中出现Hardfault问题,可在 SEGGER/SEGGER_RTT.c 中将


#ifndef   STRCPY
  #define STRCPY(pDest, pSrc, NumBytes)                   strcpy((pDest), (pSrc))
#endif
SEGGER/SEGGER_RTT.c

替换为


#ifndef   STRCPY
  #define STRCPY(pDest, pSrc, NumBytes)                   strncpy((pDest), (pSrc), (NumBytes))
#endif
SEGGER/SEGGER_RTT.c

初步推测为Null-Terminated的问题,有头绪欢迎联系指正。

2020-05-07更新:使用DMA传输以减少CPU开销

更新部分代码如下


    for(;;) {
        if(xTaskNotifyWait(0x00, 0x03, &notify_flag, pdMS_TO_TICKS(400)) == pdTRUE) {
            if(notify_flag & 0x01) {
                SEGGER_RTT_WriteDownBufferNoLock(channel_id, &rx_buf, 0x01);
                HAL_UART_Receive_IT(&huart1, &rx_buf, 0x01);
            }
            if(notify_flag & 0x02) {
                if(dma_in_progress) {
                    dma_in_progress = 0;
                    prev_send = xTaskGetTickCount();
                }
            }
        }
        if(dma_in_progress == 0 && xTaskGetTickCount() - prev_send >= pdMS_TO_TICKS(400)) {
            unsigned int tx_length = SEGGER_RTT_GetBytesInBuffer(channel_id);
            if(tx_length >= SYSVIEW_SINGLE_TX) {
                uint32_t num = SEGGER_RTT_ReadUpBufferNoLock(channel_id, tx_buf, SYSVIEW_SINGLE_TX);
                HAL_UART_Transmit_DMA(&huart1, tx_buf, num);
            } else if (tx_length != 0){
                uint32_t num = SEGGER_RTT_ReadUpBufferNoLock(channel_id, tx_buf, tx_length);
                HAL_UART_Transmit_DMA(&huart1, tx_buf, num);
            }
            dma_in_progress = 1;
        }
    }
void vTaskTraceComm(void *pvParameters)

同时添加DMA传输完成中断

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
    if(huart->Instance == USART1) {
        BaseType_t higher_woken = pdFALSE;
        xTaskNotifyFromISR(xTaskTraceCommHandle, 0x02, eSetBits, &higher_woken);
        portYIELD_FROM_ISR(higher_woken);
    }
}

即可实现DMA传输

5. 软件使用

启动SystemView,在Target->Recorder Configuration中指定串口及波特率,连接后RESET目标板即可接收事件。如下图。

SystemView主界面

6. Patch

默认的FreeRTOS trace宏不足以收集足够的参数,因此需要Patch FreeRTOS。提供参考Patch如下,来自SEGGER的Sample目录

diff -ur Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h ../FreeRTOS/Source/include/FreeRTOS.h
--- Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h	2020-05-07 22:43:33.533067758 +0800
+++ ../FreeRTOS/Source/include/FreeRTOS.h	2020-05-07 22:41:30.520012215 +0800
@@ -156,6 +156,10 @@
 	#define INCLUDE_uxTaskGetStackHighWaterMark 0
 #endif
 
+#ifndef INCLUDE_pxTaskGetStackStart
+	#define INCLUDE_pxTaskGetStackStart 0
+#endif
+
 #ifndef INCLUDE_uxTaskGetStackHighWaterMark2
 	#define INCLUDE_uxTaskGetStackHighWaterMark2 0
 #endif
@@ -400,6 +404,25 @@
 	#define tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )
 #endif
 
+#ifndef traceREADDED_TASK_TO_READY_STATE
+		#define traceREADDED_TASK_TO_READY_STATE( pxTCB )       traceMOVED_TASK_TO_READY_STATE( pxTCB )
+#endif
+
+#ifndef traceMOVED_TASK_TO_DELAYED_LIST
+		#define traceMOVED_TASK_TO_DELAYED_LIST()
+#endif
+
+#ifndef traceMOVED_TASK_TO_OVERFLOW_DELAYED_LIST
+		#define traceMOVED_TASK_TO_OVERFLOW_DELAYED_LIST()
+#endif
+
+#ifndef traceMOVED_TASK_TO_SUSPENDED_LIST
+		#define traceMOVED_TASK_TO_SUSPENDED_LIST( pxTCB )
+#endif
+
+
+
+
 #ifndef traceQUEUE_CREATE
 	#define traceQUEUE_CREATE( pxNewQueue )
 #endif
@@ -644,6 +667,18 @@
 	#define traceTASK_NOTIFY_GIVE_FROM_ISR()
 #endif
 
+#ifndef traceISR_EXIT_TO_SCHEDULER
+	#define traceISR_EXIT_TO_SCHEDULER()
+#endif
+
+#ifndef traceISR_EXIT
+	#define traceISR_EXIT()
+#endif
+
+#ifndef traceISR_ENTER
+	#define traceISR_ENTER()
+#endif
+
 #ifndef traceSTREAM_BUFFER_CREATE_FAILED
 	#define traceSTREAM_BUFFER_CREATE_FAILED( xIsMessageBuffer )
 #endif
diff -ur Middlewares/Third_Party/FreeRTOS/Source/include/task.h ../FreeRTOS/Source/include/task.h
--- Middlewares/Third_Party/FreeRTOS/Source/include/task.h	2020-05-07 22:43:33.529734437 +0800
+++ ../FreeRTOS/Source/include/task.h	2020-05-07 22:41:30.516678886 +0800
@@ -1438,6 +1438,25 @@
 
 /**
  * task.h
+ * <PRE>uint8_t* pxTaskGetStackStart( TaskHandle_t xTask);</PRE>
+ *
+ * INCLUDE_pxTaskGetStackStart must be set to 1 in FreeRTOSConfig.h for
+ * this function to be available.
+ *
+ * Returns the start of the stack associated with xTask.  That is,
+ * the highest stack memory address on architectures where the stack grows down
+ * from high memory, and the lowest memory address on architectures where the
+ * stack grows up from low memory.
+ *
+ * @param xTask Handle of the task associated with the stack returned.
+ * Set xTask to NULL to return the stack of the calling task.
+ *
+ * @return A pointer to the start of the stack.
+ */
+uint8_t* pxTaskGetStackStart( TaskHandle_t xTask) PRIVILEGED_FUNCTION;
+
+/**
+ * task.h
  * <PRE>configSTACK_DEPTH_TYPE uxTaskGetStackHighWaterMark2( TaskHandle_t xTask );</PRE>
  *
  * INCLUDE_uxTaskGetStackHighWaterMark2 must be set to 1 in FreeRTOSConfig.h for
diff -ur Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F/port.c ../FreeRTOS/Source/portable/GCC/ARM_CM4F/port.c
--- Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F/port.c	2020-05-07 22:43:33.533067758 +0800
+++ ../FreeRTOS/Source/portable/GCC/ARM_CM4F/port.c	2020-05-07 22:41:30.536678867 +0800
@@ -492,14 +492,20 @@
 	save and then restore the interrupt mask value as its value is already
 	known. */
 	portDISABLE_INTERRUPTS();
+	traceISR_ENTER();
 	{
 		/* Increment the RTOS tick. */
 		if( xTaskIncrementTick() != pdFALSE )
 		{
+			traceISR_EXIT_TO_SCHEDULER();
 			/* A context switch is required.  Context switching is performed in
 			the PendSV interrupt.  Pend the PendSV interrupt. */
 			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
 		}
+		else
+		{
+			traceISR_EXIT();
+		}
 	}
 	portENABLE_INTERRUPTS();
 }
diff -ur Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F/portmacro.h ../FreeRTOS/Source/portable/GCC/ARM_CM4F/portmacro.h
--- Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F/portmacro.h	2020-05-07 22:43:33.533067758 +0800
+++ ../FreeRTOS/Source/portable/GCC/ARM_CM4F/portmacro.h	2020-05-07 22:41:30.536678867 +0800
@@ -89,7 +89,7 @@
 
 #define portNVIC_INT_CTRL_REG		( * ( ( volatile uint32_t * ) 0xe000ed04 ) )
 #define portNVIC_PENDSVSET_BIT		( 1UL << 28UL )
-#define portEND_SWITCHING_ISR( xSwitchRequired ) if( xSwitchRequired != pdFALSE ) portYIELD()
+#define portEND_SWITCHING_ISR( xSwitchRequired ) { if( xSwitchRequired ) { traceISR_EXIT_TO_SCHEDULER(); portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; } else { traceISR_EXIT(); } }
 #define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x )
 /*-----------------------------------------------------------*/
 
diff -ur Middlewares/Third_Party/FreeRTOS/Source/tasks.c ../FreeRTOS/Source/tasks.c
--- Middlewares/Third_Party/FreeRTOS/Source/tasks.c	2020-05-07 22:43:33.549734367 +0800
+++ ../FreeRTOS/Source/tasks.c	2020-05-07 22:41:30.546678856 +0800
@@ -220,6 +220,17 @@
 	taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority );												\
 	vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) ); \
 	tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )
+
+/*
+ * Place the task represented by pxTCB which has been in a ready list before
+ * into the appropriate ready list for the task.
+ * It is inserted at the end of the list.
+ */
+#define prvReaddTaskToReadyList( pxTCB )															\
+		traceREADDED_TASK_TO_READY_STATE( pxTCB );													\
+		taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority );											\
+		vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) ); \
+		tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )
 /*-----------------------------------------------------------*/
 
 /*
@@ -1126,7 +1137,7 @@
 		#endif /* configUSE_TRACE_FACILITY */
 		traceTASK_CREATE( pxNewTCB );
 
-		prvAddTaskToReadyList( pxNewTCB );
+		prvReaddTaskToReadyList( pxNewTCB );
 
 		portSETUP_TCB( pxNewTCB );
 	}
@@ -1726,6 +1737,8 @@
 				mtCOVERAGE_TEST_MARKER();
 			}
 
+			traceMOVED_TASK_TO_SUSPENDED_LIST(pxTCB);
+
 			vListInsertEnd( &xSuspendedTaskList, &( pxTCB->xStateListItem ) );
 
 			#if( configUSE_TASK_NOTIFICATIONS == 1 )
@@ -3821,6 +3834,20 @@
 #endif /* INCLUDE_uxTaskGetStackHighWaterMark */
 /*-----------------------------------------------------------*/
 
+#if (INCLUDE_pxTaskGetStackStart == 1)
+		uint8_t* pxTaskGetStackStart( TaskHandle_t xTask)
+		{
+			TCB_t *pxTCB;
+			UBaseType_t uxReturn;
+		(void)uxReturn;
+
+				pxTCB = prvGetTCBFromHandle( xTask );
+				return ( uint8_t * ) pxTCB->pxStack;
+		}
+
+#endif /* INCLUDE_pxTaskGetStackStart */
+/*-----------------------------------------------------------*/
+
 #if ( INCLUDE_vTaskDelete == 1 )
 
 	static void prvDeleteTCB( TCB_t *pxTCB )
@@ -3990,7 +4017,7 @@
 
 					/* Inherit the priority before being moved into the new list. */
 					pxMutexHolderTCB->uxPriority = pxCurrentTCB->uxPriority;
-					prvAddTaskToReadyList( pxMutexHolderTCB );
+					prvReaddTaskToReadyList( pxMutexHolderTCB );
 				}
 				else
 				{
@@ -4080,7 +4107,7 @@
 					any other purpose if this task is running, and it must be
 					running to give back the mutex. */
 					listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) pxTCB->uxPriority ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
-					prvAddTaskToReadyList( pxTCB );
+					prvReaddTaskToReadyList( pxTCB );
 
 					/* Return true to indicate that a context switch is required.
 					This is only actually required in the corner case whereby
@@ -5112,6 +5139,7 @@
 			/* Add the task to the suspended task list instead of a delayed task
 			list to ensure it is not woken by a timing event.  It will block
 			indefinitely. */
+			traceMOVED_TASK_TO_SUSPENDED_LIST(pxCurrentTCB);
 			vListInsertEnd( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
 		}
 		else
@@ -5128,12 +5156,14 @@
 			{
 				/* Wake time has overflowed.  Place this item in the overflow
 				list. */
+				traceMOVED_TASK_TO_OVERFLOW_DELAYED_LIST();
 				vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
 			}
 			else
 			{
 				/* The wake time has not overflowed, so the current block list
 				is used. */
+				traceMOVED_TASK_TO_OVERFLOW_DELAYED_LIST();
 				vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
 
 				/* If the task entering the blocked state was placed at the
@@ -5163,11 +5193,13 @@
 		if( xTimeToWake < xConstTickCount )
 		{
 			/* Wake time has overflowed.  Place this item in the overflow list. */
+			traceMOVED_TASK_TO_OVERFLOW_DELAYED_LIST();
 			vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
 		}
 		else
 		{
 			/* The wake time has not overflowed, so the current block list is used. */
+			traceMOVED_TASK_TO_DELAYED_LIST();
 			vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
 
 			/* If the task entering the blocked state was placed at the head of the

7. 参考

F**king User Manual