在很久前还使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() 返回值判断到超时已发生,然后进行后续处理。