故障排除

引言与范围

本章强调了刚接触FreeRTOS的用户最常遇到的问题。首先,它集中讨论了三个问题,这三个问题已被证明是多年来最频繁的支持请求的来源;不正确的中断优先级分配,堆栈溢出,以及不适当地使用 printf()。然后,它以FAQ的形式简要介绍了其他常见的错误,它们可能的原因,以及它们的解决方案。

使用 configASSERT() 可以立即捕获和识别许多最常见的错误来源,从而提高生产力。强烈建议在开发或调试FreeRTOS应用程序时定义 configASSERT()configASSERT() 在之前描述过。

中断优先级

注意: 这是导致支持请求的头号原因,在大多数端口中,定义configASSERT()将立即捕获这个错误

如果使用的FreeRTOS端口支持中断嵌套,并且中断的服务例程使用了FreeRTOS的API,那么必须将中断的优先级设置为或低于configMAX_SYSCALL_INTERRUPT_PRIORITY,如6.8节所述,中断嵌套。如果不这样做,将导致无效的关键部分,这反过来又会导致间歇性的故障。

如果在一个处理器上运行FreeRTOS,请特别注意:

  • 中断的优先级默认为具有最高的优先级,在一些ARM Cortex处理器上是这样的,可能还有其他处理器。在这种处理器上,使用FreeRTOS API的中断的优先级不能不被初始化。

  • 数字上的高优先级数字代表逻辑上的低中断优先级,这似乎有悖于直觉,因此会引起混淆。这也是ARM Cortex处理器的情况,也可能是其他处理器的情况。

  • 例如,在这样的处理器上,一个正在执行的优先级为5的中断本身可以被一个优先级为4的中断打断。 因此,如果configMAX_SYSCALL_INTERRUPT_PRIORITY被设置为5,任何使用FreeRTOS API的中断只能被分配一个数字上高于或等于5的优先级。 在这种情况下,5或6的中断优先级将是有效的,但3的中断优先级肯定是无效的。

  • 不同的库实现希望以不同的方式来指定中断的优先级。同样,这与以ARM Cortex处理器为目标的库特别相关,在那里,中断的优先级在写入硬件寄存器之前被移位。有些库会自己执行位移,而其他库则希望在优先级被传入库函数之前执行位移。

  • 同一架构的不同实现方式实现了不同数量的中断优先位。例如,一个制造商的Cortex-M处理器可能实现3个优先位,而另一个制造商的Cortex-M处理器可能实现4个优先位。

  • 定义中断优先级的位可以分成定义抢占式优先级的位和定义子优先级的位。确保所有的位都被分配给指定一个抢占性优先级,所以子优先级不被使用。

  • 在某些 FreeRTOS 端口中, configMAX_SYSCALL_INTERRUPT_PRIORITY 有另一个名字 configMAX_API_CALL_INTERRUPT_PRIORITY

堆栈溢出

堆栈溢出是支持请求的第二大来源。FreeRTOS提供了一些功能来帮助捕获和调试堆栈相关的问题1。

uxTaskGetStackHighWaterMark() API函数

uxTaskGetStackHighWaterMark() 用于查询一个任务离溢出分配给它的堆栈空间还有多远。这个值被称为堆栈 "高水位线"。

UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );

清单173. uxTaskGetStackHighWaterMark() API函数原型

表 60. uxTaskGetStackHighWaterMark() 参数和返回值

这些功能在FreeRTOS的Windows端口中是不可用的。

运行时间堆栈检查——概述

FreeRTOS包括两个可选的运行时堆栈检查机制。这两种机制由 FreeRTOSConfig.h 中的configCHECK_FOR_STACK_OVERFLOW编译时配置常数控制。

堆栈溢出钩(或堆栈溢出回调)是一个函数,当内核检测到堆栈溢出时,它被调用。调用的函数。要使用一个堆栈溢出钩函数:

  1. FreeRTOSConfig.h中把configCHECK_FOR_STACK_OVERFLOW设置为1或2,如下面各小节所述。

  2. 提供钩函数的实现,使用清单174中所示的确切的函数名称和原型。

void vApplicationStackOverflowHook( TaskHandle_t *pxTask, signed char *pcTaskName );

清单174. 堆栈溢出钩函数原型

提供堆栈溢出钩是为了使捕获和调试堆栈错误更容易,但当堆栈溢出发生时,没有真正的方法来恢复。该函数的参数将已经溢出堆栈的任务的句柄和名称传递给钩函数。

堆栈溢出钩从中断的上下文中被调用。

一些微控制器在检测到不正确的内存访问时产生一个故障异常,有可能在内核有机会调用堆栈溢出钩函数之前就触发了一个故障。

运行时堆栈检查——方法一

configCHECK_FOR_STACK_OVERFLOW被设置为1时,方法一被选中。

一个任务的整个执行环境在每次被换出时都会被保存到它的堆栈中。这很可能是堆栈使用达到高峰的时候。当configCHECK_FOR_STACK_OVERFLOW被设置为1时,内核会检查堆栈指针在上下文被保存后是否仍在有效的堆栈空间内。如果发现堆栈指针超出其有效范围,则调用堆栈溢出钩。

方法一执行起来很快,但会错过上下文切换之间发生的堆栈溢出。

运行时堆栈检查——方法二

方法二在方法一已经描述的基础上执行额外的检查。当configCHECK_FOR_STACK_OVERFLOW被设置为2时,它被选中。

当一个任务被创建时,它的堆栈被填充了一个已知的模式。方法二测试任务堆栈空间的最后20个有效字节,以验证该模式没有被覆盖。如果这20个字节中的任何一个改变了它们的预期值,就会调用堆栈溢出钩函数。

方法二的执行速度没有方法一快,但仍然比较快,因为只测试20个字节。 最有可能的是,它将捕捉到所有的堆栈溢出;然而,有可能(但极不可能)会错过一些溢出。

不当使用printf()和sprintf()

不适当地使用printf()是一个常见的错误来源,而且,由于没有意识到这一点,应用程序开发人员通常会进一步调用printf()来帮助调试,这样做会使问题更加严重。

许多交叉编译器供应商将提供一个适合在小型嵌入式系统中使用的 printf() 实现。即使是这样,该实现也可能不是线程安全的,可能不适合在中断服务例程中使用,而且根据输出的方向,需要花费相对较长的时间来执行。

如果没有专门为小型嵌入式系统设计的printf()实现,而使用通用的 printf() 实现,则必须特别小心,如:

  • 仅仅包括对printf()sprintf() 的调用就会大量增加应用程序的可执行文件的大小。

  • printf() sprintf() 可能会调用 malloc() ,如果使用的是heap_3以外的内存分配方案,这可能是无效的。更多信息请参见内存分配方案示例。

  • printf() sprintf() 可能需要一个比原来大很多倍的栈。

Printf-stdarg.c

许多FreeRTOS演示项目使用了一个名为printf——stdarg.c的文件,它提供了一个最小的、堆栈有效的sprintf() 实现,可以用来代替标准库版本。在大多数情况下,这将允许为每个调用sprintf()和相关函数的任务分配一个小得多的堆栈。

printf-stdarg.c 还提供了一种机制,用于将printf()的输出逐个引向一个端口,这虽然很慢,但可以进一步减少栈的使用。

请注意,并非所有包含在FreeRTOS下载中的printf——stdarg.c的副本都实现了snprintf()。 没有实现snprintf() 的副本只是忽略了缓冲区大小参数,因为它们直接映射到sprintf()

Printf-stdarg.c是开源的,但由第三方拥有,因此与FreeRTOS分开许可。许可证条款包含在源文件的顶部。

其他常见的错误来源

症状:在演示中添加一个简单的任务会导致演示崩溃

创建一个任务需要从堆中获取内存。许多演示应用程序项目都要求堆的大小正好够创建演示任务——因此,在任务创建之后,将没有足够的堆来添加任何进一步的任务、队列、事件组或信号。

vTaskStartScheduler() 被调用时,空闲任务以及可能的RTOS守护任务会被自动创建。vTaskStartScheduler() 只会在没有足够的堆内存来创建这些任务时返回。在调用 vTaskStartScheduler() 后加入一个空循环[ for(;;);]可以使这个错误更容易被调试。

为了能够添加更多的任务,要么增加堆的大小,要么删除一些现有的演示任务。更多信息请参见内存分配方案示例。

症状:在一个中断中使用API函数会导致应用程序崩溃

不要在中断服务程序中使用API函数,除非API函数的名称以"...FromISR() "结尾。特别是,不要在一个中断中创建一个关键部分,除非使用中断安全宏。参见:从ISR中使用FreeRTOS API,以获得更多信息。

在支持中断嵌套的FreeRTOS端口中,不要在被分配的中断优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断中使用任何API函数。更多信息请参见 :中断嵌套。

症状:有时应用程序在一个中断服务例程中崩溃

首先要检查的是,中断没有导致堆栈溢出。有些端口只检查任务内的堆栈溢出,而不是中断内的堆栈溢出。

中断的定义和使用方式在不同的端口和不同的编译器之间是不同的。因此,第二件要检查的事情是,在中断服务例程中使用的语法、宏和调用约定,与所使用的端口的文档页上的描述完全一致,并且与端口提供的演示应用程序中的演示完全一致。

如果应用程序运行在一个使用数字上的低优先级数字来代表逻辑上的高优先级的处理器上,那么确保分配给每个中断的优先级考虑到这一点,因为它可能看起来是反直觉的。如果应用程序运行在一个将每个中断的优先级默认为最大可能的优先级的处理器上,那么要确保每个中断的优先级不停留在其默认值。更多信息请参见:中断嵌套和中断优先级。

症状:调度程序在试图启动第一个任务时崩溃

确保FreeRTOS的中断处理程序已经安装。请参考正在使用的FreeRTOS端口的文档页,以及为该端口提供的演示应用程序的例子。

一些处理器在启动调度程序之前必须处于特权模式。实现这一目标的最简单方法是在调用main()之前,C语言启动代码中把处理器放入特权模式。

症状:中断被意外地禁用,或关键部分不能正确嵌套

如果一个FreeRTOS API函数在调度器启动之前被调用,那么中断将被故意关闭,直到第一个任务开始执行时才重新启用。这样做是为了保护系统在系统初始化期间,在调度程序启动之前,以及在调度程序可能处于不一致的状态下,避免中断试图使用FreeRTOS API函数而导致的崩溃。

除了调用 taskENTER_CRITICAL() taskEXIT_CRITICAL() 以外,不要用任何方法改变微控制器的中断使能位或优先级标志。这些宏保持其调用嵌套深度的计数,以确保只有当调用嵌套完全解开为零时,中断才会再次启用。请注意,一些库函数本身可能会启用和禁用中断。

症状:应用程序甚至在调度程序启动之前就崩溃

在调度器启动之前,不允许执行可能导致上下文切换的中断服务例程。同样的情况也适用于任何试图向FreeRTOS对象(如队列或信号)发送或接收的中断服务例程。在调度器启动之后,才能发生上下文切换。

许多API函数在调度器启动后才能被调用。最好是将API的使用限制在创建对象上,比如任务、队列和信号,而不是使用这些对象,直到调用 vTaskStartScheduler() 之后。

症状:在调度器暂停时调用API函数,或从关键部分内部调用API函数,导致应用程序崩溃

调用 vTaskSuspendAll() 暂停调度程序,并通过调用 xTaskResumeAll() 恢复(未暂停)。通过调用taskENTER_CRITICAL() 进入关键部分,并通过调用 taskEXIT_CRITICAL() 退出。

不要在调度程序暂停时或从关键部分内部调用API函数。

Last updated