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

收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。
img
img

如果你需要这些资料,可以戳这里获取

需要这些体系化资料的朋友,可以加我V获取:vip1024c (备注嵌入式)

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人

都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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:


![在这里插入图片描述](https://img-blog.csdnimg.cn/d5bb125814f043999f158db6eb90dda9.png)


通过我们前面的知识就知道,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:


![在这里插入图片描述](https://img-blog.csdnimg.cn/0f4e38e718094c39ac5514d5aaaca64b.png)


依然不行,最后还是将所有的任务大小还是改回了128字,任务看上去解决了,按键按下,能够获取数值,而且所有任务能够周期运行。


## FreeRTOS任务栈溢出检测


然后想着FreeRTOS是有检测任务堆栈溢出功能的,于是找到相关的内容,参考了一些网上的资料。


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/30e21d5a920f44c3bcbb1940e7d687d6.png)


### 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);
}


终于,问题的根本原因终于找到了:


![在这里插入图片描述](https://img-blog.csdnimg.cn/027af8c2e3de40a1a25d2ee33e22c581.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA55-c6L6w5omA6Ie0,size_18,color_FFFFFF,t_70,g_se,x_16)  
 顿时心中压抑一阵的疑问终于打开,温湿度读取使用128字的大小还是不够,那么解决办法当然还是增加这个任务的空间大小,改成192字,终于所有任务都正常了。


## FreeRTOS任务运行情况查询


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/8acc6a6a20ec49909229b5fe0f7564c6.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA55-c6L6w5omA6Ie0,size_20,color_FFFFFF,t_70,g_se,x_16)


然后还需要在`Include definetions`部分使能`eTaskGetState`:


![在这里插入图片描述](https://img-blog.csdnimg.cn/1cede20e0a7b472fbdf5df058a2ed4d6.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA55-c6L6w5omA6Ie0,size_18,color_FFFFFF,t_70,g_se,x_16)


### 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 */
}


结果如下图:


![在这里插入图片描述](https://img-blog.csdnimg.cn/92153ce397d2432bbf70867bde1076f1.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA55-c6L6w5omA6Ie0,size_20,color_FFFFFF,t_70,g_se,x_16)  
 图中有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 字,运行结果如下:


![在这里插入图片描述](https://img-blog.csdnimg.cn/38461940eb2f4d678746d92922f12430.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA55-c6L6w5omA6Ie0,size_17,color_FFFFFF,t_70,g_se,x_16)


为了更加说明任务堆栈大小的问题,根据推荐视频里面的教程我也做了测试,我们可以看到 printfTask 的剩余栈只剩下12个字,所以我们在 printfTask 中把剩余的空间占用测试一下:


![在这里插入图片描述](https://img-blog.csdnimg.cn/253a7909ab8e47dab90b1d4618c54665.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA55-c6L6w5omA6Ie0,size_20,color_FFFFFF,t_70,g_se,x_16)


测试结果如下,`printfTask` 任务栈大小多用了2个字:


![在这里插入图片描述](https://img-blog.csdnimg.cn/83f06777860a4809982d32513f0b5b82.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA55-c6L6w5omA6Ie0,size_20,color_FFFFFF,t_70,g_se,x_16)


这个剩余栈的大小在视频中定义了一个为0的数组,直接会占用任务使用的栈,我测试并没有出现这种小现象,可能是因为gcc编译器的优化处理。


![在这里插入图片描述](https://img-blog.csdnimg.cn/e108a8f2d02b4d4fb2b0bd80f6a8ed45.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA55-c6L6w5omA6Ie0,size_20,color_FFFFFF,t_70,g_se,x_16)


任务状态的查看是需要占用一定的的内存空间的,尤其是当任务多了以后,我们这里的使用只是方便调试阶段查找确定问题。


## 临界区的使用


在我们上面的例子中,在调试中使用到`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`函数实现使能中断的操作:


![img](https://img-blog.csdnimg.cn/img_convert/8c3c89bd2805461fe2c6d12fc3531e71.png)
![img](https://img-blog.csdnimg.cn/img_convert/96da76b0b392918bbb453b87b3d0f6d5.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新**

**需要这些体系化资料的朋友,可以加我V获取:vip1024c (备注嵌入式)**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618679757)**

( configMAX_SYSCALL_INTERRUPT_PRIORITY ) : "memory"
	);
}


taskEXIT_CRITICAL

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

[外链图片转存中…(img-HmV7bDZF-1715786486808)]
[外链图片转存中…(img-CneW7Hb9-1715786486808)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新

需要这些体系化资料的朋友,可以加我V获取:vip1024c (备注嵌入式)

如果你需要这些资料,可以戳这里获取

  • 28
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值