FreeRTOS记录(四、FreeRTOS任务堆栈溢出问题和临界区)

简介: 本来计划是消息队列、信号量、任务通知、事件集、邮件的文章但是因为自己调试的时候遇到了一个问题,还是把堆栈溢出问题放到前面来说
本来计划是消息队列、信号量、任务通知、事件集、邮件的文章
但是因为自己调试的时候遇到了一个问题,还是把堆栈溢出问题放到前面来说

说明:FreeRTOS 专栏与我的 RT-Thread 专栏不同,我的 RT-Thread 专栏是从理论学习一步一步循序渐进,从 0 起步的 完整教学,而 FreeRTOS 更偏向于 我直接拿来使用,需要用到什么,然后引出知识点,在使用中发现问题,解然后再解决问题,

本 FreeRTOS 专栏记录的开发环境:
FreeRTOS记录(一、熟悉开发环境以及CubeMX下FreeRTOS配置)
FreeRTOS记录(二、FreeRTOS任务API认识和源码简析)
FreeRTOS记录(三、RTOS任务调度原理解析_Systick、PendSV、SVC)

任务堆栈问题的出现

为了写记录,自己先建立好了几个任务,其中就有 I2C读取温湿度传感器数据的任务,最初每个任务是使用的系统默认128字(512字节)作为默认大小,但是我感觉有些任务512字节有些浪费,比如提示系统运行的跑马灯,于是我把一些任务改成了64字大小,当然设置成为64,需要先把系统的最小任务大小设置为64。

在这里插入图片描述
刚开始我把这些简单的任务都设置成为64了,然后发现运行的时候,就会死掉,死掉的原因当然是堆栈溢出了,当然,很容易想到是我的 I2C 读取温湿度的任务导致的溢出,其他任务实在没干什么事情,
I2C任务和其他任务的大小如下:

osThreadDef(Led_toggle, Start_Led_toggle, osPriorityLow, 0,64);
  Led_toggleHandle = osThreadCreate(osThread(Led_toggle), NULL);

  /* definition and creation of printfTask */
  osThreadDef(printfTask, StartprintfTask, osPriorityLow, 0, 128);
  printfTaskHandle = osThreadCreate(osThread(printfTask), NULL);

  /* definition and creation of KeyTask */
  osThreadDef(KeyTask, StartKeyTask, osPriorityIdle, 0, 64);
  KeyTaskHandle = osThreadCreate(osThread(KeyTask), NULL);

  /* definition and creation of THread */
  osThreadDef(THread, StartTHread, osPriorityNormal, 0, 64);
  THreadHandle = osThreadCreate(osThread(THread), NULL);

  /* definition and creation of spiflash */
  osThreadDef(spiflash, Startspiflash, osPriorityIdle, 0, 64);
  spiflashHandle = osThreadCreate(osThread(spiflash), NULL);

...
/* USER CODE END Header_StartTHread */
void StartTHread(void const * argument)
{
  /* USER CODE BEGIN StartTHread */
  float T=0,H=0;
  /*64会溢出字的内存空间不够SHT21 协议读取*/
  /* Infinite loop */
  for(;;)
  {
    SHT2X_THMeasure();
    T=(getTemperature()/100.0);
    H=(getHumidity()/100.0); 
    osThreadSuspendAll();
    printf("\r\n%4.2f C\r\n%4.2f%%\r\n",T,H);
    osThreadResumeAll();
    osDelay(3000);
  }
  /* USER CODE END StartTHread */
}

后面还是把THread任务大小改成128,看上去每隔一定时间采集一定的次数,一切正常
(实际上有个bug就存在了,只是其他任务比较简单,看不出任何问题)。

本来准备把任务通知、消息队列的使用说明一下,然后开启了一个硬件定时器准备周期采集温湿度数据,从中断时发送任务通知,和从任务中发送任务通知。

开启定时器以后,定时器中断优先级改为6,定时器时间为1S一次中断,受FreeRTOS管理的中断优先级我是按照默认的设置为5:
在这里插入图片描述

通过我们前面的知识就知道,FreeRTOS进入临界区以后,硬件定时器的中断是会被屏蔽的。所以在检查问题的时候这个问题不用考虑。
在定时器中断中做了个简单的计数:

/**
  * @brief This function handles TIM3 global interrupt.
  */
void TIM3_IRQHandler(void)
{
  /* USER CODE BEGIN TIM3_IRQn 0 */
  time3_count++;
  if(time3_count >= 10){
    time3_count = 0;
  }
  /* USER CODE END TIM3_IRQn 0 */
  HAL_TIM_IRQHandler(&htim3);
  /* USER CODE BEGIN TIM3_IRQn 1 */

  /* USER CODE END TIM3_IRQn 1 */
}

通过按键任务查看一下计数,这么做只是为了测试一切正常:

/* USER CODE END Header_StartKeyTask */
void StartKeyTask(void const * argument)
{
  /* USER CODE BEGIN StartKeyTask */
  /* Infinite loop */
  for(;;)
  {
    if(HAL_GPIO_ReadPin(K2_GPIO_Port,K2_Pin) == 0){
      osDelay(10);
      if(HAL_GPIO_ReadPin(K2_GPIO_Port,K2_Pin) == 0){
        osThreadSuspendAll();
        printf("K2 pushed!!,time3_count is :%d\r\n",time3_count);
        osThreadResumeAll();
        while(HAL_GPIO_ReadPin(K2_GPIO_Port,K2_Pin) == 0){
          osDelay(10);
        }
      }
    }
    osDelay(1);   
  }
  /* USER CODE END StartKeyTask */
}

void Start_Led_toggle(void const * argument)
{
  /* USER CODE BEGIN Start_Led_toggle */
  /* Infinite loop */
  for(;;)
  {  
    osDelay(500);
    HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);  
    osDelay(500);
    HAL_GPIO_TogglePin(LED2_GPIO_Port,LED2_Pin);
    osDelay(500);
  }
  /* USER CODE END Start_Led_toggle */
}

于是问题就出来了。

现象是:我只要按了一次按键,I2C读取任务 和 LED灯任务就永远不会运行了,但是按键任务,包括硬件定时器数据都是正常的,就是程序没有死机,只是有2个任务死掉了。

能够想到应该是堆栈的问题,因为确实没有什么复杂的程序运行,优先级的问题我也考虑过了,每个任务是否会释放CPU控制权也都检测过。

还将FreeRTOS能够用的总堆大小改成了10K:
在这里插入图片描述
依然不行,最后还是将所有的任务大小还是改回了128字,任务看上去解决了,按键按下,能够获取数值,而且所有任务能够周期运行。

FreeRTOS任务栈溢出检测

然后想着FreeRTOS是有检测任务堆栈溢出功能的,于是找到相关的内容,参考了B站一个视频:
堆栈溢出问题B站视频

在CubeMX中,选择使用堆栈溢出钩子函数,启动栈溢出检测方案为方案二,如下图:
在这里插入图片描述

vApplicationStackOverflowHook

选中以后生成的代码,多了一个vApplicationStackOverflowHook函数,直接在里面打印溢出的任务名,如下图:

/* Hook prototypes */
void vApplicationStackOverflowHook(xTaskHandle xTask, signed char *pcTaskName);

/* USER CODE BEGIN 4 */
__weak void vApplicationStackOverflowHook(xTaskHandle xTask, signed char *pcTaskName)
{
   /* Run time stack overflow checking is performed if
   configCHECK_FOR_STACK_OVERFLOW is defined to 1 or 2. This hook function is
   called if a stack overflow is detected. */
   printf("任务:%s 溢出\r\n",pcTaskName);
}

终于,问题的根本原因终于找到了:
在这里插入图片描述
顿时心中压抑一阵的疑问终于打开,温湿度读取使用128字的大小还是不够,那么解决办法当然还是增加这个任务的空间大小,改成192字,终于所有任务都正常了。

FreeRTOS任务运行情况查询

问题虽然已经解决,但是我们以后要怎样才能确定自己的任务大小呢,这时候,我们可以使用到可视化追踪功能,查看所有任务的运行情况和堆栈使用大小,在CubeMX中使能相应的功能,如下图:
在这里插入图片描述
然后还需要在Include definetions部分使能eTaskGetState:
在这里插入图片描述

vTaskList

然后在程序中,我们使用的是osThreadList 函数,其实也就是调用了vTaskList函数:

/**
* @brief   Lists all the current threads, along with their current state 
*          and stack usage high water mark.
* @param   buffer   A buffer into which the above mentioned details
*          will be written
* @retval  status code that indicates the execution status of the function.
*/
osStatus osThreadList (uint8_t *buffer)
{
#if ( ( configUSE_TRACE_FACILITY == 1 ) && ( configUSE_STATS_FORMATTING_FUNCTIONS == 1 ) )
  vTaskList((char *)buffer);
#endif
  return osOK;
}

打印任务查看任务状态:

我们为了说明问题,下面的代码中THread任务的大小还是128字,因为还没有讲任务通知,信号量之类的知识,我这里使用自己定义的一个标志位printfstate_on 来判断是否需要打印任务状态:

  /* definition and creation of Led_toggle */
  osThreadDef(Led_toggle, Start_Led_toggle, osPriorityLow, 0, 128);
  Led_toggleHandle = osThreadCreate(osThread(Led_toggle), NULL);

  /* definition and creation of printfTask */
  osThreadDef(printfTask, StartprintfTask, osPriorityLow, 0, 256);
  printfTaskHandle = osThreadCreate(osThread(printfTask), NULL);

  /* definition and creation of KeyTask */
  osThreadDef(KeyTask, StartKeyTask, osPriorityIdle, 0, 128);
  KeyTaskHandle = osThreadCreate(osThread(KeyTask), NULL);

  /* definition and creation of THread */
  osThreadDef(THread, StartTHread, osPriorityNormal, 0, 128);
  THreadHandle = osThreadCreate(osThread(THread), NULL);

  /* definition and creation of spiflash */
  osThreadDef(spiflash, Startspiflash, osPriorityIdle, 0, 128);
  spiflashHandle = osThreadCreate(osThread(spiflash), NULL);


/* USER CODE END Header_StartprintfTask */
void StartprintfTask(void const * argument)
{
  /* USER CODE BEGIN StartprintfTask */
  /* Infinite loop */
  for(;;)
  {
    if(printfstate_on){
      printfstate_on =0;
      uint8_t mytaskstatebuffer[500];
      printf("==================================\r\n");
      printf("任务名          任务状态    优先级     剩余栈   任务序号\r\n");
      osThreadList((uint8_t *)&mytaskstatebuffer);
      printf("%s\r\n",mytaskstatebuffer);
    }
    osDelay(10);//释放CPU占用权不要忘了延时
  }
  /* USER CODE END StartprintfTask */
}

/* USER CODE END Header_StartKeyTask */
void StartKeyTask(void const * argument)
{
  /* USER CODE BEGIN StartKeyTask */
  /* Infinite loop */
  for(;;)
  {
    if(HAL_GPIO_ReadPin(K2_GPIO_Port,K2_Pin) == 0){
      osDelay(10);
      if(HAL_GPIO_ReadPin(K2_GPIO_Port,K2_Pin) == 0){
        taskENTER_CRITICAL();
        printf("K2 pushed!!,time3_count is :%d\r\n",time3_count);
        taskEXIT_CRITICAL();
        printfstate_on = 1;
        while(HAL_GPIO_ReadPin(K2_GPIO_Port,K2_Pin) == 0){
          osDelay(10);
        }
      }
    }
    osDelay(1);
    
  }
  /* USER CODE END StartKeyTask */
}

结果如下图:
在这里插入图片描述
图中有2个地方有问题,第一个是温湿度中的剩余栈为0 ,溢出了,第二个是KeyTask的任务序号异常。
这个基本上可以确定是,THread任务和 KeyTask任务 在内存空间上是使用的连续的内存空间,一前一后,THread任务溢出导致改写了属于 KeyTask任务所在内存的数据,导致他的任务序号异常。这是内存空间相关的知识。

然后还需要说一下任务状态的意思:


/* Task states returned by eTaskGetState. */
typedef enum
{
    eRunning = 0,    /* X  A task is querying the state of itself, so must be running. */
    eReady,            /* R The task being queried is in a read or pending ready list. */
    eBlocked,        /* B The task being queried is in the Blocked state. */
    eSuspended,        /* S The task being queried is in the Suspended state, or is in the Blocked state with an infinite time out. */
    eDeleted,        /* D The task being queried has been deleted, but its TCB has not yet been freed. */
    eInvalid            /* Used as an 'invalid state' value. */
} eTaskState;

我们把THread任务大小改成 192 字,运行结果如下:
在这里插入图片描述
为了更加说明任务堆栈大小的问题,根据推荐视频里面的教程我也做了测试,我们可以看到 printfTask 的剩余栈只剩下12个字,所以我们在 printfTask 中把剩余的空间占用测试一下:
在这里插入图片描述
测试结果如下,printfTask 任务栈大小多用了2个字:
在这里插入图片描述
这个剩余栈的大小在视频中定义了一个为0的数组,直接会占用任务使用的栈,我测试并没有出现这种小现象,可能是因为gcc编译器的优化处理。
在这里插入图片描述
任务状态的查看是需要占用一定的的内存空间的,尤其是当任务多了以后,我们这里的使用只是方便调试阶段查找确定问题。

临界区的使用

在我们上面的例子中,在调试中使用到printf的时候都加了任务挂起和任务恢复osThreadSuspendAll()osThreadResumeAll(),除了这种操作,更加建议的操作是使用临界区。合理使用了临界区也会使得程序减少很多不必要的bug。

临界区在前面文章我们已经提到过很多FreeRTOS的临界区屏蔽中断使用的是basepri寄存器,那么什么情况下使用临界区呢:

  • 用户不想被打断的代码
  • 调用公共函数的代码(不可重入函数)
  • 读取或者修改变量(全局变量)
  • 对时序有精准要求的操作
  • 使用硬件资源(比如I2C通讯,但是得注意在通讯中不能使用利用了systick的延时函数)

临界区API介绍

临界区的相关API如下:

API名称 API说明
taskENTER_CRITICAL 进入临界段,内部调用taskDISABLE_INTERRUPTS
taskEXIT_CRITICAL 退出临界段,内部调用taskENABLE_INTERRUPTS
taskENTER_CRITICAL_FROM_ISR 进入临界段(在中断中使用)
taskEXIT_CRITICAL_FROM_ISR(x) 退出临界段(在中断中使用)
taskENABLE_INTERRUPTS 开启中断
taskDISABLE_INTERRUPTS 关闭受FreeRTOS管理的中断

x:上次中断屏蔽寄存器操作值

#define taskENTER_CRITICAL()        portENTER_CRITICAL()
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()

/**
 * task. h
 *
 * Macro to mark the end of a critical code region.  Preemptive context
 * switches cannot occur when in a critical region.
 *
 * NOTE: This may alter the stack (depending on the portable implementation)
 * so must be used with care!
 *
 * \defgroup taskEXIT_CRITICAL taskEXIT_CRITICAL
 * \ingroup SchedulerControl
 */
#define taskEXIT_CRITICAL()            portEXIT_CRITICAL()
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
#define portSET_INTERRUPT_MASK_FROM_ISR()        ulPortRaiseBASEPRI()
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x)    vPortSetBASEPRI(x)
#define portDISABLE_INTERRUPTS()                vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS()                    vPortSetBASEPRI(0)
#define portENTER_CRITICAL()                    vPortEnterCritical()
#define portEXIT_CRITICAL()                        vPortExitCritical()

taskENTER_CRITICAL

taskENTER_CRITICAL 最终还是调用了vPortRaiseBASEPRI函数实现屏蔽中断的操作:

/*----------------------------------------------*/
...
#define portENTER_CRITICAL()                    vPortEnterCritical()
/*----------------------------------------------*/
...
/*----------------------------------------------*/
void vPortEnterCritical( void )
{
    portDISABLE_INTERRUPTS();
    uxCriticalNesting++;

    /* This is not the interrupt safe version of the enter critical function so
    assert() if it is being called from an interrupt context.  Only API
    functions that end in "FromISR" can be used in an interrupt.  Only assert if
    the critical nesting count is 1 to protect against recursive calls if the
    assert function also uses a critical section. */
    if( uxCriticalNesting == 1 )
    {
        configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
    }
}
/*----------------------------------------------*/
...
/*----------------------------------------------*/
#define portDISABLE_INTERRUPTS()                vPortRaiseBASEPRI()
/*----------------------------------------------*/
...
/*----------------------------------------------*/

portFORCE_INLINE static void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI;

    __asm volatile
    (
        "    mov %0, %1                                                \n" \
        "    msr basepri, %0                                            \n" \
        "    isb                                                        \n" \
        "    dsb                                                        \n" \
        :"=r" (ulNewBASEPRI) : "i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY ) : "memory"
    );
}

taskEXIT_CRITICAL

taskEXIT_CRITICAL最终还是调用了vPortSetBASEPRI函数实现使能中断的操作:

/*----------------------------------------------*/
...
#define portEXIT_CRITICAL()                        vPortExitCritical()
/*----------------------------------------------*/
...
/*----------------------------------------------*/
void vPortExitCritical( void )
{
    configASSERT( uxCriticalNesting );
    uxCriticalNesting--;
    if( uxCriticalNesting == 0 )
    {
        portENABLE_INTERRUPTS();
    }
}
/*----------------------------------------------*/
...
/*----------------------------------------------*/
#define portENABLE_INTERRUPTS()                    vPortSetBASEPRI(0)
/*----------------------------------------------*/
...
/*----------------------------------------------*/

portFORCE_INLINE static void vPortSetBASEPRI( uint32_t ulNewMaskValue )
{
    __asm volatile
    (
        "    msr basepri, %0    " :: "r" ( ulNewMaskValue ) : "memory"
    );
}

taskENTER_CRITICAL_FROM_ISR

taskENTER_CRITICAL_FROM_ISR最终调用了ulPortRaiseBASEPRI函数实现屏蔽中断的操作:

/*----------------------------------------------*/
...
#define portSET_INTERRUPT_MASK_FROM_ISR()        ulPortRaiseBASEPRI()
/*----------------------------------------------*/
...
/*----------------------------------------------*/
portFORCE_INLINE static uint32_t ulPortRaiseBASEPRI( void )
{
uint32_t ulOriginalBASEPRI, ulNewBASEPRI;

    __asm volatile
    (
        "    mrs %0, basepri                                            \n" \
        "    mov %1, %2                                                \n" \
        "    msr basepri, %1                                            \n" \
        "    isb                                                        \n" \
        "    dsb                                                        \n" \
        :"=r" (ulOriginalBASEPRI), "=r" (ulNewBASEPRI) : "i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY ) : "memory"
    );

    /* This return will not be reached but is necessary to prevent compiler
    warnings. */
    return ulOriginalBASEPRI;
}
/*-----------------------------------------------------------*/

taskEXIT_CRITICAL_FROM_ISR( x )

taskENTER_CRITICAL_FROM_ISRtaskEXIT_CRITICAL一样,调用了vPortSetBASEPRI函数实现使能中断的操作,只不过他多了一个参数,参数为 上次中断屏蔽寄存器操作值:

/*----------------------------------------------*/
...
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x)    vPortSetBASEPRI(x)
/*----------------------------------------------*/
...
/*----------------------------------------------*/
portFORCE_INLINE static void vPortSetBASEPRI( uint32_t ulNewMaskValue )
{
    __asm volatile
    (
        "    msr basepri, %0    " :: "r" ( ulNewMaskValue ) : "memory"
    );
}
/*-----------------------------------------------------------*/

知道这些以后,所以在任务使用printf的前后改成(中断中是不建议使用printf的):

    taskENTER_CRITICAL();
    printf("\r\n%4.2f C\r\n%4.2f%%\r\n",T,H);
    taskEXIT_CRITICAL();

任务挂起与临界区区别

在我们前面使用过vTaskSuspendAll() xTaskResumeAll() 函数。

taskENTER_CRITICALvTaskSuspendAll()的区别在于:

taskENTER_CRITICALtaskENABLE_INTERRUPTS不仅不能切换任务,还不能响应中断(不能响应可屏蔽中断),而
vTaskSuspendAll() 他们只是不能进行任务切换,但是没有屏蔽中断。

taskENTER_CRITICALtaskENABLE_INTERRUPTS的区别在于:

使用taskDISABLE_INTERRUPTS()taskENABLE_INTERRUPTS()不支持嵌套(比如程序中前面调用了2个taskDISABLE_INTERRUPTS() ,后面使用一个taskENABLE也可以打开中断)。
使用taskENTER_CRITICAL() taskEXIT_CRITICAL()支持嵌套,调用了几个taskENTER_CRITICAL() 就得调用几个 taskEXIT_CRITICAL()才能使能中断。

在使用中发现问题,解决问题,思考问题!

相关文章
|
Arthas IDE Java
一种获取阻塞线程栈帧数据的思路
一种获取阻塞线程栈帧数据的思路
158 2
|
存储 缓存 内存技术
【CSAPP】虚拟内存 | 地址空间 | 页表内存保护 | 页错误引发异常逐出 (evicted)
【CSAPP】虚拟内存 | 地址空间 | 页表内存保护 | 页错误引发异常逐出 (evicted)
70 0
|
存储 Java C++
JVM - 结合代码示例彻底搞懂Java内存区域_线程栈 | 本地方法栈 | 程序计数器
JVM - 结合代码示例彻底搞懂Java内存区域_线程栈 | 本地方法栈 | 程序计数器
107 0
9-FreeRTOS之静态内存分配与动态内存分配
9-FreeRTOS之静态内存分配与动态内存分配
|
存储 算法 编译器
12-FreeRTOS内存管理
12-FreeRTOS内存管理
freertos任务堆栈的描述
freertos任务堆栈的描述
119 0
freertos任务堆栈的描述