写在前面

主要是为刚接触 FreeRTOS 的用户指出那些新手通常容易遇到的问题。这里把最主要的篇幅放在栈溢出以及栈溢出j检测上,因为栈相关的问题是初学者遇到最多的问题。

printf-stdarg.c

当调用 C 标准库 的函数时,栈空间使用量可能会急剧上升,特别是 IO 与字符串处理函数,比如 sprintf()、printf()等。在 FreeRTOS 源码包中有一个名为 printf-stdarg.c 的文件。这个文件实现了一个栈效率优化版的小型 sprintf()、printf(),可以用来代替标准 C 库函数版本。在大多数情况下,这样做可以使得调用 sprintf()及相关函数的任务对栈空间的需求量小很多。
可能很多人都不知道freertos中有这样子的一个文件,它放在第三方资料中,路径为“FreeRTOSv9.0.0\FreeRTOS-Plus\Demo\FreeRTOS_Plus_UDP_and_CLI_LPC1830_GCC”,我们发布工程的时候就无需依赖 C 标准库,这样子就能减少栈的使用,能优化不少空间。
该文件源码(部分):

static int print( char **out, const char *format, va_list args )
{
    register int width, pad;
    register int pc = 0;
    char scr[2];

    for (; *format != 0; ++format) {
        if (*format == '%') {
            ++format;
            width = pad = 0;
            if (*format == '\0') break;
            if (*format == '%') goto out;
            if (*format == '-') {
                ++format;
                pad = PAD_RIGHT;
            }
            while (*format == '0') {
                ++format;
                pad |= PAD_ZERO;
            }
            for ( ; *format >= '0' && *format <= '9'; ++format) {
                width *= 10;
                width += *format - '0';
            }
            if( *format == 's' ) {
                register char *s = (char *)va_arg( args, int );
                pc += prints (out, s?s:"(null)", width, pad);
                continue;
            }
            if( *format == 'd' || *format == 'i' ) {
                pc += printi (out, va_arg( args, int ), 10, 1, width, pad, 'a');
                continue;
            }
            if( *format == 'x' ) {
                pc += printi (out, va_arg( args, int ), 16, 0, width, pad, 'a');
                continue;
            }
            if( *format == 'X' ) {
                pc += printi (out, va_arg( args, int ), 16, 0, width, pad, 'A');
                continue;
            }
            if( *format == 'u' ) {
                pc += printi (out, va_arg( args, int ), 10, 0, width, pad, 'a');
                continue;
            }
            if( *format == 'c' ) {
                /* char are converted to int then pushed on the stack */
                scr[0] = (char)va_arg( args, int );
                scr[1] = '\0';
                pc += prints (out, scr, width, pad);
                continue;
            }
        }
        else {
        out:
            printchar (out, *format);
            ++pc;
        }
    }
    if (out) **out = '\0';
    va_end( args );
    return pc;
}

int printf(const char *format, ...)
{
    va_list args;

    va_start( args, format );
    return print( 0, format, args );
}

int sprintf(char *out, const char *format, ...)
{
    va_list args;

    va_start( args, format );
    return print( &out, format, args );
}

int snprintf( char *buf, unsigned int count, const char *format, ... )
{
    va_list args;

    ( void ) count;

    va_start( args, format );
    return print( &buf, format, args );
}

使用的例子与 C 标准库基本一样:

int main(void)
{
    char *ptr = "Hello world!";
    char *np = 0;
    int i = 5;
    unsigned int bs = sizeof(int)*8;
    int mi;
    char buf[80];

    mi = (1 << (bs-1)) + 1;
    printf("%s\n", ptr);
    printf("printf test\n");
    printf("%s is null pointer\n", np);
    printf("%d = 5\n", i);
    printf("%d = - max int\n", mi);
    printf("char %c = 'a'\n", 'a');
    printf("hex %x = ff\n", 0xff);
    printf("hex %02x = 00\n", 0);
    printf("signed %d = unsigned %u = hex %x\n", -3, -3, -3);
    printf("%d %s(s)%", 0, "message");
    printf("\n");
    printf("%d %s(s) with %%\n", 0, "message");
    sprintf(buf, "justif: \"%-10s\"\n", "left"); printf("%s", buf);
    sprintf(buf, "justif: \"%10s\"\n", "right"); printf("%s", buf);
    sprintf(buf, " 3: %04d zero padded\n", 3); printf("%s", buf);
    sprintf(buf, " 3: %-4d left justif.\n", 3); printf("%s", buf);
    sprintf(buf, " 3: %4d right justif.\n", 3); printf("%s", buf);
    sprintf(buf, "-3: %04d zero padded\n", -3); printf("%s", buf);
    sprintf(buf, "-3: %-4d left justif.\n", -3); printf("%s", buf);
    sprintf(buf, "-3: %4d right justif.\n", -3); printf("%s", buf);

    return 0;
}

栈计算

每个任务都独立维护自己的栈空间, 任务栈空间总量在任务创建时进行设定。uxTaskGetStackHighWaterMark()主要用来查询指定任务的运行历史中, 其栈空间还差多少就要溢出。这个值被称为栈空间的High Water Mark
函数原型:

UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask )

想要使用它,需要将对应的宏定义打开:INCLUDE_uxTaskGetStackHighWaterMark

函数描述:
|参数|说明|
|--|--|
| xTask | 被查询任务的句柄如果传入 NULL 句柄,则任务查询的是自身栈空间的高水线 |
| 返回值| 任务栈空间的实际使用量会随着任务执行和中断处理过程上下浮动。uxTaskGetStackHighWaterMark()返回从任务启动执行开始的运行历史中,栈空间具有的最小剩余量。这个值即是栈空间使用达到最深时的剩下的未使用的栈空间。这个值越是接近 0,则这个任务就越是离栈溢出不远。|

如果不知道怎么计算任务栈大小,就使用这个函数进行统计一下,然后将任务运行时最大的栈空间作为任务栈空间的80%大小即可。即假设统计得到的任务栈大小为常量 A ,那么在创建线程的时候需要 X 大小的空间,那么 X * 80% = A,算到的 X 作为任务栈大小就差不多了。

运行时栈检测

FreeRTOS 包含两种运行时栈j检测机制,由 FreeRTOSConfig.h 中的配置常量configCHECK_FOR_STACK_OVERFLOW 进行控制。这两种方式都会增加上下切换开销。

栈溢出钩子函数(或称回调函数)由内核在j检测到栈溢出时调用。要使用栈溢出钩子函数,需要进行以下配置:

  • 在 FreeRTOSConfig.h 中把 configCHECK_FOR_STACK_OVERFLOW 设为 1 或者 2
  • 提供钩子函数的具体实现,采用下面所示的函数名和函数原型。
void vApplicationStackOverflowHook( xTaskHandle *pxTask, signed portCHAR *pcTaskName );

补充说明:

  • 栈溢出钩子函数只是为了使跟踪调试栈空间错误更容易,而无法在栈溢出时对其进行恢复。函数的入口参数传入了任务句柄和任务名,但任务名很可能在溢出时已经遭到破坏。
  • 栈溢出钩子函数还可以在中断的上下文中进行调用
  • 某些微控制器在检测到内存访问错误时会产生错误异常,很可能在内核调用栈溢出钩子函数之前就触发了错误异常中断。

方法1

configCHECK_FOR_STACK_OVERFLOW 设置为 1 时选用方法 1
任务被交换出去的时候,该任务的整个上下文被保存到它自己的栈空间中。这时任务栈的使用应当达到了一个峰值。当 configCHECK_FOR_STACK_OVERFLOW 设为1 时,内核会在任务上下文保存后检查栈指针是否还指向有效栈空间。一旦检测到栈指针的指向已经超出任务栈的有效范围,栈溢出钩子函数就会被调用。
方法 1 具有较快的执行速度,但栈溢出有可能发生在两次上下文保存之间,这种情况不会被检测到,因为这种检测方式仅在任务切换中检测。

方法2

configCHECK_FOR_STACK_OVERFLOW 设为 2 就可以选用方法 2。方法 2在方法 1 的基础上进行了一些补充。
当创建任务时,任务栈空间中就预置了一个标记。方法 2 会检查任务栈的最后 20个字节的数据,查看预置在这里的标记数据是否被覆盖。如果最后 20 个字节的标记数据与预设值不同,则栈溢出钩子函数就会被调用。
方法 2 没有方法 1 的执行速度快,但测试仅仅 20 个字节相对来说也是很快的。这种方法应该可以j检测到任何时候发生的栈溢出,虽然理论上还是有可能漏掉一些情况,但这些情况几乎是不可能发生的。

其它常见错误

在一个 Demo 应用程序中增加了一个简单的任务,导致应用程序崩溃

可能的情况:

  1. 任务创建时需要在内存堆中分配空间。许多 Demo 应用程序定义的堆空间大小只够用于创建 Demo 任务——所以当任务创建完成后,就没有足够的剩余空间来增加其它的任务,队列或信号量
  2. 空闲任务是在 vTaskStartScheduler()调用中自动创建的。如果由于内存不足而无法创建空闲任务,vTaskStartScheduler()会直接返回。所以一般在调用 vTaskStartScheduler()后加上一条空循环for(;;) / while(1)可以使这种错误更加容易调试。
    如果要添加更多的任务,可以增加内存堆空间大小(修改配置文件),或是删掉一些已存在的 Demo任务。

在中断中调用一个 API 函数,导致应用程序崩溃

需要做的第一件事是检查中断是否导致了栈溢出。

然后检查API接口是否正确,除了具有后缀为FromISR函数名的 API 函数,千万不要在中断服务程序中调用其它 API 函数。

除此之外,还需要注意中断的优先级:
FreeRTOSConfig.h文件中可以配置系统可管理的最高中断优先级数值,宏定义configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY是用于配置basepri寄存器的,当basepri设置为某个值的时候,会让系统不响应比该优先级低的中断,而优先级比之更高的中断则不受影响。就是说当这个宏定义配置为5的时候,中断优先级数值在0、1、2、3、4的这些中断是不受FreeRTOS管理的,不可被屏蔽,同时也不能调用FreeRTOS中的API函数接口,而中断优先级在5到15的这些中断是受到系统管理,可以被屏蔽的,也可以调用FreeRTOS中的API函数接口。

临界区无法正确嵌套

除了 taskENTER_CRITICA()和 taskEXIT_CRITICAL(),千万不要在其它地方修改控制器的中断使能位或优先级标志。这两个宏维护了一个嵌套深度计数,所以只有当所有的嵌套调用都退出后计数值才会为 0,也才会使能中断。

在调度器启动前应用程序就崩溃了

这个问题我也会遇到,如果一个中断会产生上下文切换,则这个中断不能在调度器启动之前使能。这同样适用于那些需要读写队列或信号量的中断。在调度器启动之前,不能进行上下文切换。
还有一些 API 函数不能在调度器启动之前调用。在调用 vTaskStartScheduler()之前,最好是限定只使用创建任务,队列和信号量的 API 函数。
比如有一些初始化需要中断的,或者在初始化完成的时候回产生一个中断,这些驱动的初始化最好放在一个任务中进行,我是这样子处理的,在main函数中创建一个任务,在任务中进行bsp初始化,然后再创建消息队列、信号量、互斥量、事件以及任务等操作。

在调度器挂起时调用 API 函数,导致应用程序崩溃

调用 vTaskSuspendAll()使得调度器挂起,而唤醒调度器调用 xTaskResumeAll()。千万不要在调度器挂起时调用其它 API 函数。

关注我

更多资料欢迎关注“物联网IoT开发”公众号!

FreeRTOS优化与错误排查方法的更多相关文章

  1. java web程序 jdbc连接数据库错误排查方法

    学习jsp.我遇到了麻烦,我总是看不懂500错误,因为每次都显示整个页面的错误,都是英文 我看不懂,后来,把他弄烦了,我也烦了,比起学习java.那个异常可以很简单的就知道.现在解决 了第一个问题,5 ...

  2. 运行shell脚本报/bin/bash^M: bad interpreter错误排查方法

    今天遇到一个奇怪的问题,从一个服务器上down下来的脚本,在本地电脑做了点修改之后,上传到另外一台服务器上来执行,就报这个错误,问度娘,是编码格式的问题,windows把sh格式的编码改成dos格式的 ...

  3. 【Crash】C++程序崩溃排查方法

    windows下C++程序release版本崩溃错误排查方法. 一个你精心设计的24小时不间断运行,多线程的程序,突然运行了几个月后崩了,此问题是非常难以排查的,也是很头疼的问题. 现利用Google ...

  4. Spark程序运行常见错误解决方法以及优化

    转载自:http://bigdata.51cto.com/art/201704/536499.htm Spark程序运行常见错误解决方法以及优化 task倾斜原因比较多,网络io,cpu,mem都有可 ...

  5. zabbix告警邮件、短信发送错误快速排查方法

    zabbix告警邮件.短信发送错误快速排查方法 背景 zabbix告警邮件.短信经常有同事反馈发送错误的情况,这个问题排查的角度很多,那么最快捷的角度是什么呢? 在我看来,最快的角度就是判断这个告警邮 ...

  6. JAVA常见错误处理方法 和 JVM内存结构

    OutOfMemoryError在开发过程中是司空见惯的,遇到这个错误,新手程序员都知道从两个方面入手来解决:一是排查程序是否有BUG导致内存泄漏:二是调整JVM启动参数增大内存.OutOfMemor ...

  7. 转:JAVA常见错误处理方法 和 JVM内存结构

    OutOfMemoryError在开发过程中是司空见惯的,遇到这个错误,新手程序员都知道从两个方面入手来解决:一是排查程序是否有BUG导致内存泄漏:二是调整JVM启动参数增大内存.OutOfMemor ...

  8. 【C#代码实战】群蚁算法理论与实践全攻略——旅行商等路径优化问题的新方法

    若干年前读研的时候,学院有一个教授,专门做群蚁算法的,很厉害,偶尔了解了一点点.感觉也是生物智能的一个体现,和遗传算法.神经网络有异曲同工之妙.只不过当时没有实际需求学习,所以没去研究.最近有一个这样 ...

  9. hadoop本地库与系统版本不一致引起的错误解决方法

    hadoop本地库与系统版本不一致引起的错误解决方法 部署hadoop的集群环境为 操作系统 centos 5.8 hadoop版本为cloudera   hadoop-0.20.2-cdh3u3 集 ...

随机推荐

  1. 基于LinkedhashMap实现的LRU算法

    LRU全称是Least Recently Used,即最近最久未使用的意思.LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小.也就是说,当限定的空间已存 ...

  2. jQuery - 02. 样式表属性操作/类操作、动画、显示隐藏、滑入、淡入、停止动画、节点操作、添加对象、清空节点

    样式表属性操作.css $("div").css({'width':100,'height':100,'background':'red'}); $("div" ...

  3. (转)java程序调用内存变化过程分析(详细)

    原博地址: https://blog.csdn.net/Myuhua/article/details/81385609 (一)不含静态变量的java程序运行时内存变化过程分析 代码: package ...

  4. List<Object> 多条件去重

    上一篇将到根据某一条件去重List<Object> 对象链表.本文章根据多条件去重List<Object>去重 private List<StaingMD0010> ...

  5. Elastic Stack 笔记(七)Elasticsearch5.6 聚合分析

    博客地址:http://www.moonxy.com 一.前言 Elasticsearch 是一个分布式的全文搜索引擎,索引和搜索是 Elasticsarch 的基本功能.同时,Elasticsear ...

  6. hadoop生态系列

    1.hadoop高可用安装和原理详解 2.hadoop2.7+spark2.2+zookeeper3.4.简单安装 3.windows下通过idea连接hadoop和spark集群 4.hadoop2 ...

  7. 使用git在github远程仓库中操作

    在github上创建一个仓库,这一步参考廖雪峰老师的git教程,以及其他的一些准备工作略,我只记录几个重要的命令. 从其他github地址克隆项目 $ git clone git@github.com ...

  8. Windows认证 | 域认证

    在Windows中的身份认证方式有很多,也在不断的升级,但是在域中,依旧使用的是Kerberos认证. Kerberos 是一种网络认证协议,它的实现不依赖于主机操作系统的认证,无需基于主机地址的信任 ...

  9. Sentinel Core流程分析

     上次介绍了Sentinel的基本概念,并在文章的最后介绍了基本的用法.这次将对用法中的主要流程和实现做说明,该部分主要涉及到源码中的sentinel-core模块. 1.token获取   如上为t ...

  10. springboot全局异常拦截源码解读

    在springboot中我们可以通过注解@ControllerAdvice来声明一个异常拦截类,通过@ExceptionHandler获取拦截类抛出来的具体异常类,我们可以通过阅读源码并debug去解 ...