Debugging multithreaded programs based on FreeRTOS

Debugging multithreaded programs based on FreeRTOS

 
Debugging multitasking programs is not easy, especially if you are faced with this for the first time. After the joy of launching the first task or the first demo of the program, from the infinitely disturbing observation of the LEDs, each of which blinks in its own task, the time comes when you realize that you understand very little ( , You do not get at all ) about what is actually happening. Classics of the genre: "I allocated 3KB to the operating system and launched only 3 tasks with a 128B stack, and for the fourth time, for some reason, there is not enough memory" or "How many stacks should I allocate to the task? Is that enough? And how much? ". Many solve these problems by trial and error, so in this article I decided to combine most of the moments that, at the moment, greatly simplify my life and allow more consciously debug multithreaded programs based on FreeRTOS.
 
 
This article is designed, first of all, for those who have just started to learn FreeRTOS, but it is likely that readers familiar with this operating system will find something interesting for themselves here. In addition, despite the fact that the article is aimed at developers of embedded software, it will be interesting to applied programmers, too. many words will be said about FreeRTOS as such, regardless of microcontroller romance.
 
 

In this article I will talk about the following points:


 
 
Configuring OpenOCD to work with FreeRTOS.
 
Do not forget to include hooks.
 
Static or dynamic allocation of memory?
 
Tale, about the configMINIMAL_STACK_SIZE parameter.
 
Monitoring the use of resources.
 
to the project. FreeRTOS-openocd.c
 
Add flags to the linker (Properties> C /C ++ Build> Settings> Cross ARM C ++ Linker> Miscellaneous> Other linker flags):
 
 
-Wl, --undefined = uxTopUsedPriority  
Add flags to the debugger (Run> Debugs configurations> Debugger> Config options):
 
 
    -c "$ _TARGETNAME configure -rtos auto"    
 
Uncheck Run> Debugs configurations> Startup> Set breakpoint at main.  

 
After these settings in the Debug window, all existing threads with detailed information will be displayed. we will always have access to information about the state of this or that process and what it is currently doing:
 
 

 
 

in this article [/i] ). We begin to check the function body, whether there is an error or a typo. An hour passes, the eye starts to twitch, and we are sure that the function is written correctly as firmly as we are sure that the chair on which we are sitting exists. So what's the deal? And the fact is that the error may have nothing to do with your function, and the problem lies precisely in the operation of the OS. And then we get help khuki.
 
 
In FreeRTOS, the following hooks exist:
 
 

    /* Hook function related definitions. * /
#define configUSE_IDLE_HOOK 0
#define configUSE_TICK_HOOK 0
#define configCHECK_FOR_STACK_OVERFLOW 2
#define configUSE_MALLOC_FAILED_HOOK 1
#define configUSE_DAEMON_TASK_STARTUP_HOOK 0

 
The most important, within the debugging program, are configCHECK_FOR_STACK_OVERFLOW and configUSE_MALLOC_FAILED_HOOK.
 
 
Parameter configCHECK_FOR_STACK_OVERFLOW can be enabled with a value of 1 or ? depending on which method of stack overflow detection you want to use. More about this you can read here . If you enable this hook, you will need to define the function
 
void vApplicationStackOverflowHook (TaskHandle_t xTask, signed char * pcTaskName), which will be executed every time the stack allocated for the task is not enough for its work, and most importantly you will see it in the call stack of a particular task. Thus, to solve the problem, it will only be necessary to increase the stack size allocated for the task.
 
 
vApplicationStackOverflowHook [/b]
    void vApplicationStackOverflowHook (TaskHandle_t xTask, char * pcTaskName)
{
rtos :: CriticalSection :: Enter ();
{
while (true)
{
portNOP ();
}
}
rtos :: CriticalSection :: Exit ();
}

 
Paramert configUSE_MALLOC_FAILED_HOOK ? like most FreeRTOS configurable parameters, is enabled. If you enable this hook, you will need to define the void vApplicationMallocFailedHook () function. This function will be called when free space in the heap allocated to FreeRTOS is not enough to accommodate the next entity. And, again, the main thing is that we will see all this in the stack of calls. Therefore, all we need to do to solve this problem is to increase the size of the heap allocated for FreeRTOS.
 
 
vApplicationallocFailedHook [/b]
    void vApplicationMallocFailedHook ()
{
rtos :: CriticalSection :: Enter ();
{
while (true)
{
portNOP ();
}
}
rtos :: CriticalSection :: Exit ();
}

 
Now, if we run our program again, when it drops into hard_fault_handler (), we see the reason for this fall in the Debug window:
 
 

 
 
By the way, if you've ever found an interesting application configUSE_IDLE_HOOK, configUSE_TICK_HOOK or configUSE_DAEMON_TASK_STARTUP_HOOK, then it would be very interesting to read about it in the comments)
 
 

here , and the detailed comparative characteristics of various memory managers are represented by here .
 
 
As for my opinion, at this stage of development I do not see any sense in using static allocation of memory, so in the above configuration the static memory allocation is turned off. And here's why:
 
 

  1.  
  2. Why separate the buffer for the stack and the StaticTask_t structure, if the operating system supports as many as 5 different memory managers for every taste and color, who will figure out where, what and how to create, and even let them know if they have failed? In particular, for most programs for microcontrollers more than fully suitable heap_1.c  
  3. You may need some third-party library written very optimally and efficiently, but using malloc (), calloc () or new[]inside of you. (). And what should I do? Refuse it in favor of less than optimal (is it even if there is a choice)? And you can just use dynamic memory allocation with heap_2.c or heap_4.c . The only thing you need to do is to redefine the corresponding functions so that the memory allocation takes place using FreeRTOS in the heap provided to it:
     
     
    code snippet [/b]
        void * malloc (size_t size) {
    return pvPortMalloc (size);
    }
    void * calloc (size_t num, size_t size) {
    return pvPortMalloc (num * size);
    }
    void free (void * ptr) {
    return vPortFree (ptr);
    }
    void * operator new (size_t sz) {
    return pvPortMalloc (sz);
    }
    void * operator new[](size_t sz) {
    return pvPortMalloc (sz);
    }
    void operator delete (void * p) {
    vPortFree (p);
    }
    void operator delete[](void * p) {
    vPortFree (p);
    }
     

 
In my projects, I only use dynamic allocation of memory with heap_4.c, giving the maximum amount of memory under the OS heap, and I always override the functions malloc (), calloc (), new (), etc., regardless of whether they are used at the moment or not.
 
 
In advocating the dynamic allocation of memory, I, of course, do not deny that there are tasks for which the ideal solution is the static allocation of memory (this, also, can be discussed in the comments).
 
 

Here ).  

We see the state of each task, where B = Blocked, R = Ready, S = Suspended, D = Deleted.  
We see the priority of each task.  
We see the minimum size of free space on the stack, since the creation of the task. And here for us it becomes obvious that for the majority of tasks, we have allocated too much stack. For example, for the LoggerTask task, a stack of 256 words was selected, and in reality it uses only 40. Thus, a stack of 64 words is enough for the task to function. Here to you and the beginning of optimization.  
We see the current and the minimum (with the start of the scheduler) the amount of free space in the heap. In our simple example, these values ​​are equal, but in more complex programs these two variables are, of course, different. Thus, we understand that out of the 100KB given to FreeRTOS, it uses less10KB, therefore in our hands more than 90KB of free memory.  
And, finally, we see the amount of time that has elapsed since the start of the scheduler in milliseconds.  

 
Applying this knowledge to the TasksConfig.h file and lowering the value of configMINIMAL_STACK_SIZE from 128 to 6? we get the following picture:
 
 

 
 
Super! Now each task has an optimal stock of free space on the stack: not too large, and not too small. In addition, we released almost 3K of memory.
 
 
And now it's time to talk about what we have not seen in the log. We do not see how much CPU time each task uses, i.e. how long, the task was in the Running state. To find out, we need to set the parameter configGENERATE_RUN_TIME_STATS in 1 and add the file FreeRTOSConfig.h with the following definitions:
 
 
    #if configGENERATE_RUN_TIME_STATS == 1
void vConfigureTimerForRunTimeStats (void);
unsigned long vGetTimerForRunTimeStats (void);
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS () vConfigureTimerForRunTimeStats ()
#define portGET_RUN_TIME_COUNTER_VALUE () vGetTimerForRunTimeStats ()
#endif

 
Now we need to create an external timer that counts the time (preferably in microseconds, because some tasks may take less than a millisecond to complete, but we still want to know about everything). Let's add the file MonitorTask.h by declaring two static functions:
 
 
    static void config_timer ();
static unsigned long get_counter_value ();

 
In the MonitorTask.cpp file, write their implementation:
 
 
    void MonitorTask :: config_timer ()
{
_timer-> disable_counter ();
_timer-> set_counter_direction (cm3cpp :: tim :: Timer :: CounterDirection :: UP);
_timer-> set_alignment (cm3cpp :: tim :: Timer :: Alignment :: EDGE);
_timer-> set_clock_division (cm3cpp :: tim :: Timer :: ClockDivision :: TIMER_CLOCK_MUL_1);
_timer-> set_prescaler_value (hw :: config :: MONITOR_TIMER_PRESQ);
_timer-> set_autoreload_value (hw :: config :: MONITOR_AUTORELOAD);
_timer-> enable_counter ();
_timer-> set_counter_value (0);
}
unsigned long MonitorTask :: get_counter_value ()
{
static unsigned long _counter = 0;
_counter + = _timer-> get_counter_value ();
_timer-> set_counter_value (0);
return (_counter);
}

 
And in the main.cpp file, write the implementation of the functions vConfigureTimerForRunTimeStats () and vGetTimerForRunTimeStats (), which we declared in FreeRTOSConfig.h:
 
 
    #if configGENERATE_RUN_TIME_STATS == 1
void vConfigureTimerForRunTimeStats (void)
{
tasks :: MonitorTask :: config_timer ();
}
unsigned long vGetTimerForRunTimeStats (void)
{
return (tasks :: MonitorTask :: get_counter_value ());
}
#endif

 
Now, after starting the program, our log becomes like this:
 
 

 
 
Comparing the values ​​of Total RunTime and System Uptime, we can conclude that only a third of the time our program is busy executing tasks, with 98% of the time spent on IDLE, and 2% on all other tasks. What does our program do the remaining two-thirds of the time? This time is spent on the work of the scheduler and switching between all tasks. Sad, but true. Of course, there are ways to optimize this time, but this is already a topic for the next article.
 
 
As for the parameter configUSE_STATS_FORMATTING_FUNCTIONS , then it is very secondary, most often it is used in various demos provided by FreeRTOS developers. Its essence lies in the fact that it includes two functions:
 
 
    void vTaskList (char * pcWriteBuffer);
void vTaskGetRunTimeStats (char * pcWriteBuffer);

 
Both of these functions are NOT part of FreeRTOS. Within themselves, they call the same function uxTaskGetSystemState, which we used above, and add the already formatted data to pcWriteBuffer. The developers themselves do not recommend using these functions (but, of course, they do not prohibit it), pointing out that their task is rather demonstrative, and they are directly using the uxTaskGetSystemState function directly, as we did.
 
 
That's all. As always, I hope that this article was useful and informative)
 
 
For assembly and debugging demo of the project , described in the article, used the Eclipse + GNU MCU Eclipse bundle (formerly GNU ARM Eclipse) + OpenOCD.
 
 
Blog of the company Third Pin
+ 0 -

Add comment