花了3个晚上,把这个章节看完,受益匪浅。
- 最有用的应该是与中断相关的错误,优先排查中断优先级设置。
- 堆栈溢出检查,可能用到,一般先把堆栈设置的足够大,只要没有溢出就是好事,溢出了,掌握了栈溢出钩子函数排错很方便。
printf()
相关的问题应该尽量不会出现,毕竟只要需要打印调试信息的情况下才使用,而且嵌入式系统一般都是用串口重定向的。
讲真,嵌入式中printf()
真的挺烦的,严重影响性能,我的开发案例中发现,串口打印会影响板子的 power save性能,这是实测到的。
此章节涉及新手最常遇见的3种问题:
- 错误的中断优先级设置
- 栈溢出
- 不恰当的使用
printf()
使用configASSERT()
能够显著地提高生产效率,它能够捕获、识别多种类型的错误。强烈建议在开发或者调试中开启宏configASSERT()
。
中断优先级
注意:这是头号需要技术支持的问题,在大多数的移植版本中通过定义configASSERT()
就能够立刻捕获这个错误。
如果FreeRTOS移植版本支持中断嵌套,并且中断服务程序使用了FreeRTOS API,那么必须把中断优先级设置为configMAX_SYSCALL_INTERRUPT_PRIORITY
或者低一点。没有这么设置将会导致临界区失效,反过来就会导致间歇性的错误。
当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个优先级比特位,但是另一个厂商实现了4个优先级比特位。 - 定义一个中断优先级的比特位被分成两个部分,一部分定义抢占的级别,另外的比特位定义子优先级。确保所有的比特位都是指定抢占的优先级,而子优先级不使用。
在某些移植版本中configMAX_SYSCALL_INTERRUPT_PRIORITY
有一个别名configMAX_API_CALL_INTERRUPT_PRIORITY
。
栈溢出
栈溢出是第二个经常寻求技术支持的问题。FreeRTOS提供了几个特性来辅助捕获和调试和栈相关的问题。
API函数uxTaskGetStackHighWaterMark()
每个任务都在维护自己的栈,栈的总大小在创建任务的时候就指定了。函数uxTaskGetStackHighWaterMark()
就是用来查询分配给这个任务的栈接近栈溢出的程度。返回值称为栈的高水位线。
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );
任务使用栈的多少,随着任务的运行和中断的处理时而增加时而减少。uxTaskGetStackHighWaterMark()
返回自任务开始运行以来剩余可用的栈空间的最小值。它返回的是栈未使用的空间占最大值的比值。高水位越接近于0,那么这个任务的栈就越快要溢出。
运行时栈检查
FreeRTOS提供了两种在运行时检查栈的机制。都是由文件FreeRTOSConfig.h
中的configCHECK_FOR_STACK_OVERFLOW
来在编译时进行控制的。两种方法都会增加上下文切换的时间。
栈溢出钩子函数(又称为栈溢出回调函数)是一个由内核检查到栈溢出时调用的函数。要使用栈溢出钩子函数要满足以下条件:
- 在文件
FreeRTOSConfig.h
中把configCHECK_FOR_STACK_OVERFLOW
设置为1或者2。 - 实现以下钩子函数,使用完全一样的函数名字和原型:
void vApplicationStackOverflowHook( TaskHandle_t *pxTask, signed char *pcTaskName);
栈溢出钩子函数会让捕获和调试栈错误更加的简单,但是发生栈溢出错误时是没有办法恢复的。此函数把发生栈溢出的任务的句柄和名字传递进去。
栈溢出钩子函数会在一个中断的上下文中进行调用。
某些微控制器在检测到一个错误的内存访问时会产生一个错误异常,这个错误异常的触发会使得内核根本就没有机会调用栈溢出钩子函数。
运行时栈检查--方法1
当进行如下设置是会选择方法1.
#define configCHECK_FOR_STACK_OVERFLOW 1
每当一个任务被切换出去时它的整个的执行上下文都会被保存到它自己的栈中。很有可能这就是栈使用率达到最大值的时候。当使用方法1是,当任务的上下文被保存之后内核回去检查栈指针是否在栈可用空间内。如果发现栈指针已经超出了可用的范围那么就会调用栈溢出钩子函数。
方法1执行速度快,但是在发生上下文切换时有可能会错过栈溢出。
运行时栈检查--方法2
进行如下设置后才会选择方法2.
#define configCHECK_FOR_STACK_OVERFLOW 2
除了方法1中的行为,方法2还会执行其他的检查。
创建任务时它的栈会被一个已知的样本填充。任务2检查栈空间的最后20个字节,验证这个已知的样本是否已经被覆盖。如果这20个字节的值与预期值不一样那么就会调用栈溢出钩子函数。
方法2不如方法1快,当时相对来讲还是快,毕竟只是测试20个字节。很有可能方法2会捕获到所有的栈溢出,但是有可能(几乎不可能)某些栈溢出还是漏掉了。
不恰当地使用printf()
和sprintf()
不恰当地只用printf()
是一种常见的错误源,并且没有意识到这种错误,通常应用开发者会增加更多的printf()
来辅助调试,结果就是加重了问题。
许多交叉编译器厂商会提供一种适合在小型嵌入式系统中使用的printf()
的实现。即便在这种情形下,printf()
的实现可能并不是线程安全的,几乎可以肯定不适合在中断服务程序中使用,并且取决于输出被重定向到哪里,会占用相当长的一段执行时间。。
如果小型嵌入式系统的printf()
的实现不可用,并且使用了通用的printf()
的实现,那么就需要特别注意了:
- 仅仅增加了一个对
printf()
或者sprintf()
的调用就会急剧的增加应用执行文件的体积; - 如果使用了
heap_3
以外的存储空间方案,printf()
和sprintf()
调用了malloc()
,这个是无效的。 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是分开的。它的授权条款在文件的首部。
其他的常见错误
症状:添加一个简单任务到例程中却导致了例程挂掉
创建任务需要从堆中分配内存。许多示例工程的栈空间仅仅能够容纳例程任务,因此在创建了例程任务后,没有足够的堆空间留给其他更多的任务,队列,事件组,信号量。
空闲任务,又或许是FreeRTOS的守护进程,在调用vTaskStartScheduler()
时是自动创建的。只有当堆空间不足以创建这些任务时vTaskStartScheduler()
才会返回。在调用vTaskStartScheduler()
之后添加一个for(;;);
会让这个问题更容易排错。
要想添加更多的任务,要么扩大堆空间,要么减少已经存在的例子任务。
症状:中断中使用API导致应用挂掉
在中断服务程序中不要使用API,除非API名字是以FromISR()
结尾。特别地,在中断中不要创建一个临界区,除非使用中断安全的宏。
在支持中断嵌套的FreeRTOS移植版本中,如果中断优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY
就不要在其中使用 API 函数。
症状:有时应用程序在中断服务函数中挂掉
首先要检查中断是否产生了栈溢出。有的移植版本中只检查任务的栈溢出,并没有检查中断是否栈溢出。
中断的定义和使用方式随着移植版本和编译器的不同而不同。因此,其次要检查语法,宏定义,调用规则在中断服务程序中的使用是否与移植文档中的完全一致,是否与例程中的完全一致。
如果应用运行在数值越低的优先级表示逻辑上越高的优先级的处理器上,那么需要确保分配给中断的优先级要考虑到这种情况,因为它看起来是违反直觉的。
如果应用运行在把中断优先级默认设置为最大可能的优先级的处理器上,需要确保每个中断的优先级没有留置为默认值。
症状:调度器尝试启动第一个任务时挂掉
确保设置了FreeRTOS的中断句柄。参考FreeRTOS移植文档,还有示例程序。
某些处理器必须在调度器启动之前处于特权模式。最简单的实现方法是在C语言main()
之前的启动代码中就把处理器置于特权模式。
症状: 中断被异常地禁止了,又或者临界区没有正确地嵌套
如果在调度器启动之前就调用了FreeRTOS API函数那么中断就会被蓄意地禁止,直到第一个任务启动之后才会重新使能。这样做是为了保护系统不挂掉,原因在于初始化过程中中断尝试调用FreeRTOS API函数,然而调度器还没有启动,它可能处于一个不一致的状态。
除了调用taskENTER_CRITICAL()
和taskEXIT_CRITICAL()
函数之外不要使用任何其它的方法来更改微处理器的中断使能位和优先级标志。这两个宏会统计中断嵌套深度确保当中断嵌套深度为0时中断又使能。需要知悉某些库函数可能在内部使能和禁止中断。
症状:远在调度器启动之前应用就挂掉了
有可能发生上下文切换的中断是禁止在调度器启动之前就开始执行的。同样的规则适用于尝试发送或者接收FreeRTOS对象(例如队列和信号量)的任何中断服务程序。上下文切换必须在调度器启动之后才能发生。
很多API函数必须在调度器启动之后才能调用。最好是在调用vTaskStartScheduler()
之后将API的使用限制在创建诸如任务,队列和信号量上,而不是使用这些对象。
症状: 在调度器挂起时又或者是临界区内部调用API函数会导致应用程序挂掉
调用函数vTaskSuspendAll()
会挂起调度器,调用函数xTaskResumeAll()
会恢复调度器。
调用函数taskENTER_CRITICAL()
会进入临界区,调用函数taskEXIT_CRITICAL()
会退出临界区。
在调度器挂起时或者临界区内永远不要调用API函数。
声明
欢迎转载,请注明出处和作者,同时保留声明。
作者:LinTeX9527
出处:http://www.cnblogs.com/LinTeX9527/p/8031565.html
本博客的文章如无特殊说明,均为原创,转载请注明出处。如未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。