写点什么

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

作者:矜辰所致
  • 2022 年 9 月 08 日
    江苏
  • 本文字数:8286 字

    阅读完需:约 27 分钟

FreeRTOS记录(四、FreeRTOS任务堆栈溢出问题和临界区)
本来计划是消息队列、信号量、任务通知、事件集、邮件的文章但是因为自己调试的时候遇到了一个问题,还是把堆栈溢出问题放到前面来说
复制代码


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


本 FreeRTOS 专栏记录的开发环境:

FreeRTOS 记录(一、熟悉开发环境以及 CubeMX 下 FreeRTOS 配置)

https://xie.infoq.cn/article/e82c7e10af242105b509bee2e

FreeRTOS 记录(二、FreeRTOS 任务 API 认识和源码简析)

https://xie.infoq.cn/article/250c67b1cc68b91241347f8e6

FreeRTOS 记录(三、RTOS 任务调度原理解析 _Systick、PendSV、SVC)

https://xie.infoq.cn/article/e0c671e5ca2d472ce40d272d4

一、任务堆栈问题的出现


为了写记录,自己先建立好了几个任务,其中就有 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 是有检测任务堆栈溢出功能的,于是网上查找了一下。


在 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 如下:



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()才能使能中断。


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

发布于: 刚刚阅读数: 6
用户头像

矜辰所致

关注

CSDN、知乎、微信公众号: 矜辰所致 2022.08.02 加入

不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开! 为了活下去的嵌入式工程师,画画板子,敲敲代码,玩玩RTOS,搞搞Linux ...

评论

发布
暂无评论
FreeRTOS记录(四、FreeRTOS任务堆栈溢出问题和临界区)_堆栈溢出_矜辰所致_InfoQ写作社区