3346|1

1382

帖子

2

TA的资源

五彩晶圆(初级)

楼主
 

使用C语言库 setjmp/longjmp 函数进行异常恢复 [复制链接]

  在很久前还使Turbo C 2.0写程序的时候,我在帮助功能里面浏览库函数的时候见过有 setjmp() 和 longjmp() 函数,但是从未去了解这是用来做什么的。时隔二十多年了,我才从网上别人的博文中了解到了 setjmp() 和 longjmp() 函数的功能,觉得这可算是 C 语言提供的一个“神器”了。从名称上看,可以猜它们是实现某种 "Goto" 功能的,但必然和 C 语言关键字 goto 不是一回事。C 语言有 goto, 但是我写程序从来不用(带我入门C语言的老师叫我不要用goto, 受此影响),只有在退出多重循环的时候觉得……这里要用一下goto就省事了。

  虽然 goto 可以直接从嵌套的循环中跳出到最外面,但是跳转限制在一个函数内部(因为 C 语言一个函数是一个代码模块,编译时无法确定别的函数内部的地址)。想从函数调用嵌套中跳出来?常规做法只能一级一级函数返回。尽管这样保持了程序结构清晰,有时候为了更高效地实现是可以借助……

  C++ 语言有 try, catch 块功能,提供了高级的异常处理支持,可以实现入上这种“跨函数”的退出。而 C 语言的 setjmp(), longjmp() 函数提供了相似的异常处理支持,注意是在库函数一级实现,不是语言内置。longjmp() 函数可以跳转到 setjmp() 函数调用的位置去执行,好象是代码从 setjmp() 函数返回一般。

 

  那么,在单片机程序里用这两个奇特的函数有什么好处?

  我刚做的一个小东西里面,要在执行某个命令操作的时候进行超时检查。在这个操作过程中要使用很多个 SPI slave 模式收发,因为 SPI 时钟是外部给的,我在等待 SPI 状态寄存器更新的时候就用一个循环在反复读状态寄存器。如果要增加一个超时检查,那么就要在循环中测试定时器的状态,或者是测试一个由定时器中断修改的全局变量。这样一来,每个等待 SPI 状态的循环都要写得更多一些(对状态更新的响应时间也会增加);然后,在检测到超时故障后需要一个条件分支转去异常处理(若用goto写,可以简化)包括可能需要从嵌套函数返回。结果就是,得到了逻辑上正确但是我觉得冗长累赘的代码。

  于是我改用了 setjmp/longjmp 来实现这个超时异常处理。

 

  第一步,需要 #include <setjmp.h> 

  第二步,定义一个全局变量 jmp_buf timeout_jmp;

  第三步,在要执行的命令操作的代码当中,写上如下代码:

    SysTick_Config(TIMEOUT_VALUE);    /*  使用 SysTick 中断进行超时捕捉,须设定好定时器  */

    if(setjmp(timeout_jmp))    /*  发生超时异常,函数将返回真 */
    {

       /* 此处编写发生异常后需要的软件硬件重新初始化代码 */
        return ERR_CODE_TIMEOUT;  /* 因超时,命令未完成,结束 */
    }
    /* 下面的代码开始执行命令 */

   /*    过程省略   */

    SysTick->CTRL = 0;    /* 完成,定时器禁用 */

    return cmd_status;


  第四步,编写用来执行 longjmp() 的辅助函数

void timeout_catch(void)
{
    SysTick->CTRL = 0;
    longjmp(timeout_jmp,1);
}

  最后一步,编写定时器的中断处理程序,当超时发生时执行

void SysTick_Handler(void
{
    asm volatile (
    "str %[ret_addr], [sp,#0x18]\n"
    ::[ret_addr]"r"(timeout_catch) :
    );
}

 

  实现原理:setjmp() 函数用了一个 jmp_buf 结构类型的变量来保存现场,longjmp() 则从保存的现场恢复。也就是 longjmp() 的参数提供了保存的现场存放在哪里的地址,从这块存储中还获取到继续执行的地址。超时异常捕捉是用定时器中断引发,那么为什么不在中断处理程序中调用 longjmp() 呢?因为 longjmp() 只恢复软件层面上的现场,对中断状态是一无所知的,必须要回到非中断模式下再调用 longjmp(), 否则后续代码未退出中断。我上面写的 timeout_catch() 就作此用途——让中断返回到 timeout_catch() 函数中去。

  这里我又用了一个特殊技巧,这是用ARM Cortex-m0处理器汇编代码书写。因为中断要返回的地址保存在堆栈里面,所以把 timeout_catch() 函数入口地址写到堆栈中原来 PC 寄存器的位置——就是当中断发生时,要执行的下一条指令的地址(现在不需要执行那里了,因为发生异常,要取消执行)。这样中断返回到 timeout_catch() 函数,然后先关掉定时器,再执行 longjmp().  此时,程序就转移到 setjmp() 调用后的那段异常处理代码中了。

 

  可以反汇编看一下 setjmp() 和 longjmp() 都做了什么:

080017b8 <setjmp>:
 80017b8:	c0f0      	stmia	r0!, {r4, r5, r6, r7}
 80017ba:	4641      	mov	r1, r8
 80017bc:	464a      	mov	r2, r9
 80017be:	4653      	mov	r3, sl
 80017c0:	465c      	mov	r4, fp
 80017c2:	466d      	mov	r5, sp
 80017c4:	4676      	mov	r6, lr
 80017c6:	c07e      	stmia	r0!, {r1, r2, r3, r4, r5, r6}
 80017c8:	3828      	subs	r0, #40	; 0x28
 80017ca:	c8f0      	ldmia	r0!, {r4, r5, r6, r7}
 80017cc:	2000      	movs	r0, #0
 80017ce:	4770      	bx	lr

080017d0 <longjmp>:
 80017d0:	3010      	adds	r0, #16
 80017d2:	c87c      	ldmia	r0!, {r2, r3, r4, r5, r6}
 80017d4:	4690      	mov	r8, r2
 80017d6:	4699      	mov	r9, r3
 80017d8:	46a2      	mov	sl, r4
 80017da:	46ab      	mov	fp, r5
 80017dc:	46b5      	mov	sp, r6
 80017de:	c808      	ldmia	r0!, {r3}
 80017e0:	3828      	subs	r0, #40	; 0x28
 80017e2:	c8f0      	ldmia	r0!, {r4, r5, r6, r7}
 80017e4:	1c08      	adds	r0, r1, #0
 80017e6:	d100      	bne.n	80017ea <longjmp+0x1a>
 80017e8:	2001      	movs	r0, #1
 80017ea:	4718      	bx	r3

  它们保存和恢复了若干寄存器。包括PC, SP寄存器,这样程序堆栈也能够恢复到 setjmp() 调用时候的样子——后续子函数调用中堆栈是向下生长的,当SP寄存器恢复,那些子函数的局部变量也就无效了。longjmp() 做的现场恢复也仅限于此,硬件寄存器的改变,动态分配内存的情况等,就要编写软件的自己处理了。

 

  在我上面的实现中,SysTick 定时器专用来触发超时动作,若正常操作在 SysTick 定时器走到0之前结束,将关闭它,就不会再引发中断。这样,处理事情的代码中不需要去检测是否发生超时。当超时发生时不论代码执行到哪里,都由 SysTick 中断服务程序转移到 timeout_catch() 这个函数中,调用 longjmp() 恢复已保存的现场。这样,就在初始代码中从 setjmp() 返回值判断到超时已发生,然后进行后续处理。

此帖出自单片机论坛

最新回复

学习了   详情 回复 发表于 2021-10-8 20:25

赞赏

1

查看全部赞赏

点赞(1) 关注(1)
 

回复
举报

994

帖子

3

TA的资源

一粒金砂(高级)

沙发
 

学习了

此帖出自单片机论坛
 
 

回复
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/6 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表