詳しくはこちら
FreeRTOS trace macros
何ができるようになるの?
どのタスクが、いつ、どれくらいの時間、動いているか、わかるようになります!!
例えば、以下のような感じになります。
一番上から順番にother、task1、task2、tasuk3のタイミングを示しており、task1が4マイクロ秒くらいの小さな処理を行い、続いてtask3が63usくらいの、task2が122usくらいの処理を行い、残りの時間をotherが消費し、1ミリ秒周期で繰り返される、というような流れが見て取れます。
この例では単純に1ミリ秒周期の処理を見ているだけですが、もっと複雑な処理でも見えますし、特定のタスクにトリガしてやれば、動く頻度が低いタスクが処理するタイミングも知ることができます。
もちろん、ロジックアナライザで見ているので、そのタスクが起動したときに入力されていた信号や、そのタスクで出力された信号も、同時に見ることができます。オシロのトリガとして使えば、アナログ信号も同時に見ることができます。
ドキュメントによると、タスクの挙動のみならず、キューやヒープや様々な挙動をトレースできるようです。あるキューが操作されたのがいつか、そのときにどのタスクが動いていたか、といったことを監視できるようになります。
どうやって使うの?
タスクをトレースする場合、#define traceTASK_SWITCHED_OUT()
および#define traceTASK_SWITCHED_IN()
のふたつのマクロを定義します(実装によってはOUTは不要です)。
RTOSの実装の中で参照されるので、かなり高い位置で定義しておく必要があります。今回はSTM32CubeMXが生成するFreeRTOSConfig.hの中で定義しました。
以下のような感じです。
/* USER CODE BEGIN Defines */
/* Section where parameter definitions can be added (for instance, to override default ones in FreeRTOS.h) */
# define configUSE_APPLICATION_TASK_TAG 1
# include <stm32f4xx.h>
# define traceTASK_SWITCHED_OUT() \
{ \
GPIOB->BSRR = (uint32_t)(GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11) << 16; \
}
# define traceTASK_SWITCHED_IN() \
{ \
switch ((uint32_t)pxCurrentTCB->pxTaskTag) \
{ \
default: \
GPIOB->BSRR = (uint32_t)(GPIO_PIN_8); \
break; \
case 1: \
GPIOB->BSRR = (uint32_t)(GPIO_PIN_9); \
break; \
case 2: \
GPIOB->BSRR = (uint32_t)(GPIO_PIN_10); \
break; \
case 3: \
GPIOB->BSRR = (uint32_t)(GPIO_PIN_11); \
break; \
} \
}
/* USER CODE END Defines */
GPIOの出力先の選択に使うので、TASK_TAGを有効化しておきます。
その後で、STM32のライブラリを読み込んだあとで、OUT/INからGPIOに対して操作を行っています。処理を終了するときに呼ばれるOUTでGPIOのビットをクリアし、操作を始めるときに呼ばれるINでGPIOのビットをセットしています。また、INでは、pxTaskTagを使用してセットするビットを選んでいます。
マクロは定義すれば勝手に呼ばれますが、GPIOの初期化とタグの設定が必要です。
GPIOの初期化はMX_GPIO_Init();
の後とか、適当な場所で行っておきます。
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {
.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11,
.Mode = GPIO_MODE_OUTPUT_PP,
.Pull = GPIO_NOPULL,
.Speed = GPIO_SPEED_FREQ_VERY_HIGH,
};
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
監視したいタスクの中でvTaskSetApplicationTaskTag(NULL, (void *)1);
のようにタグを設定します。
例えば以下のような感じになります。
void StartDefaultTask(void const *argument)
{
/* USER CODE BEGIN 5 */
vTaskSetApplicationTaskTag(NULL, (void *)1);
/* Infinite loop */
for (;;)
{
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, (HAL_GetTick() % 1000) < 100 ? GPIO_PIN_SET : GPIO_PIN_RESET);
osDelay(1);
}
/* USER CODE END 5 */
}
この場合、1ミリ秒ごとにHAL_GetTickを読んでPD2に幅10%周期1secのパルスを出す処理になり、実行中はtask1のピン(今回の例ではPB9)がセットされます。
注意点
未確認ですが、おそらく割り込みで重い処理をやると、正常に監視できません(割り込みはRTOSの管轄外なので、OUT/INの処理が行われないため)。
割り込みも含めてタイミングを確認したい場合は、割り込みに入ったり出たりするタイミングで責任を持ってGPIOをトグルする必要があります。
今回、稀に起動時にハングアップすることがありました。pxCurrentTCB
はNULLで初期化されていて、RTOSが走る前にSysTickがコンテキストスイッチを呼んで、traceTASK_SWITCHED_IN
の中でNULLを参照して、落ちるようです。
対策としては、traceTASK_SWITCHED_IN
の中でpxCurrentTCB
が非NULLであることを確認する、あるいはRTOSを起動する直前までSysTickを停止しておく、といった方法が考えられます。
応用
今回はタスクごとにGPIOを割り当てましたが、他の方法を使うこともできます。例えば公式のドキュメントではDACに出力し、オシロで電圧を見てタスクを識別しているようです。他にも、十分に早い(数十Mbaudクラスの)シリアル(SPI等)で出力するという方法も使えます。この方法であれば少ないGPIOで多くの情報(8bitや16bit)を出力できます。