嵌入式启航 嵌入式启航
首页 作品展示 个人主页
首页 作品展示 个人主页
binarybard
binarybard
嵌入式软件工程师,专注于MCU+RTOS技术栈,了解各种MCU底层。
binarybard
binarybard
嵌入式软件工程师,专注于MCU+RTOS技术栈,了解各种MCU底层。

分类

  • 默认分类 1
  • 开发环境 1
  • keil_C51 5

最新文章

  • 51单片机函数调用与可重入函数
    2026-01-10
  • 51单片机中断系统与响应优化
    2026-01-03
  • 51单片机bank功能与扩展关键字
    2025-12-27
  • 51单片机内存模型
    2025-12-20
  • 51单片机函数指针覆盖分析问题
    2025-12-13

51单片机中断系统与响应优化

binarybard 2026年01月03日 keil_C51 0 条评论

对于开发过51单片机的人来说红框中的内容一定不会觉得陌生,意思是这是一个中断函数,函数后面的 interrupt T5_VECTOR是在给编译器说明这个函数是定时器5的中断服务函数。可是编译器又对标记为中断服务函数的函数做了什么特殊处理,和普通的函数又有什么区别,为什么加上之后MCU就知道发生中断时执行这个中断服务函数?

中断过程:

进入中断:

MCU怎么通过一个 interrupt T5_VECTOR修饰把函数这中断联系起来的,这涉及到一个叫做中断向量表的东西。中断向量表这个东西一定是不需要自己来写的,因为每个中断有一个固定的地址,这和硬件息息相关也只有硬件厂商能够知道在哪里。

上面的信息已经很丰富了指明了定时器5中断发生时要执行0x6B处的指令,51单片机的中断向量表构造是这样的前三个字节是调用启动代码,之后每8个字节一组,如果启用了对应的中断,就在里面写上调用中断服务函数的指令。

在我这个工程中定时器5中断最终被分配到C:0x32A1的地址,0x6B处的02是指令 LJMP后面紧跟中断服务函数地址,执行 LJMP指令的时候会把PC寄存器压入栈中,也就中断函数结束时 RETI指令弹出的寄存器。

每8个字节一组并不代表这部分空间只能用来跳转中断了,链接器非常聪明如果发现代码里没有某个中断函数那原本应该存放跳转中断服务函数的地址以及有中断服务函数情况下8个字节使用3个字节之后剩余的空间都会被尽可能的放入其他代码,以提高ROM空间利用率。

现场保护恢复:

对于中断函数本身我觉得最好的方式就是把同样的函数加上 interrupt T5_VECTOR和去掉进行对比,下面是一个对比图,左下角是去掉 interrupt T5_VECTOR也就是普通函数的汇编代码,左上和右侧连在一起是一个完整的定时器中断服务函数汇编代码。

首先看蓝色框中函数体几乎是一致的(不可能完全一致因为这个函数从中断服务函数编程普通函数影响了链接器最终定址),主要还是看我红色框起来的地方,编译器为函数添加上了一系列保存寄存器和恢复寄存器的代码,另外就是橙色方框内函数返回指令的差异。

通常情况下MCU内部只有一组核心寄存器(处理器寄存器),当程序正在运行的时候这些寄存器有的一定在使用,有些可能在使用。中断的来临是随机的(你可以手动按键制造一个外部中断但对于MCU核心来说一切都是随机的),也就是说响应中断的时候任何一个寄存器都可能在使用中,里面的值都是有意义的。

要在中断处理结束之后继续正常的程序,就要对所有可能在使用中的寄存器进行保存并在返回之前恢复,也就是当一个函数被告知是中断函数时在原本的函数体前后添加现场保存恢复代码的原因,有的地方也叫序言尾声。

中断返回:

最后我们看到函数的最后返回指令有一点差别,这两个指令同样的效果是从硬件栈中弹出两个字节放入PC寄存器继续执行;RETI指令比 RET多的一点效果是会通知中断控制器本次中断完成,在有些8051里面可能就会有自动清除中断标志位或者其他硬件操作。

自动优化的中断函数:

上面是一般情况下51单片机中断的整个过程,但是当中断函数非常简单的时候也会有一些不一样的情况,就比如说下面这个函数编译器完全去掉了中断函数前后的现场保存与恢复过程。

但是到底什么情况下编译器会省去中断函数的现场保存和恢复过程,我其实也没有找到一个固定的触发优化的路径,优化等级在其中作用不明。但是根据我的经验,一是函数逻辑越简单越容易触发,二是如果函数与中断函数在一个文件内容易触发。比如说中断服务函数调用A函数,如果A函数定义与中断服务函数所在文件不同,即使函数很简单也不能省去现场保存恢复过程,如果定义在同一个文件内就比较容易触发优化。

手动优化中断函数:

如果编译器没有自动优化也并不意味着开发者对于中断服务函数优化就束手无策了,51单片机有一个叫寄存器Bank的特殊的机制,据我了解不管是经典51还是扩展51对于这个特性都是支持的。而且通常情况下这种机制都是被用来优化中断响应速度了,所以我觉得有必要在51单片机中断部分讲一讲。

寄存器R0-R7在data区域映射,默认情况下只映射到0x00-0x07区域,在编译之后生成的map文件中也可以看到前八个字节就是寄存器bank0(蓝色方框),但是实际上寄存器R0-R7一共可以在datd区域映射4组,只需要使用using关键字即可开启,using x(x :0-3),切换使用的寄存器bank需要操作PSW寄存器,其中PSW[4:3]表示当前使用的寄存器bank。

还是使用定时器5中断服务函数举例,加上using关键字修饰之后编译可以看到汇编代码里面现场保存恢复的部分已经不需要保存恢复寄存器R0-R7,不过多了红色框里切换寄存器bank的指令,生成的MAP文件中0x08-0x0F也显示用来作为寄存器bank1了。

using关键字可以优化中断响应速度,但是也被称为小白杀手,有两种情况非常容易导致程序跑飞崩溃。一种是两个中断指定使用同一个bank又发生嵌套,比如A,B中断同时使用寄存器bank1优化,A中断执行中B中断抢占,但是B中断没有为A中断保存完整现场漏了寄存器R0-R7返回之后A中断服务函数崩溃。

官方建议是主程序默认使用寄存器bank0,同一个优先级的中断使用同一组寄存器bank避免出现嵌套。但是我个人习惯是中断唯一bank,就是让系统内最频繁的中断独占一个Bank,不怎么频繁的中断我觉得偶尔发生一次压栈寄存器R0-R7也完全可以接受。

另外一种错误的情况是被调用的函数与调用者使用不同的寄存器bank,比如说定时器5中断使用了using 1切换到了bank1,并且调用了external_input_Callback函数。

先说明这个地方最后的计算结果应该是500(0x1F4),但是最后这是时候计算出来结果是0xF(0xF = 1000000/0x10100);?C?ULDIV计算的时候为了保证我们使用任何寄存器bank都可以,使用的是相对访问方法;使用时先将被除数放在R4-R7,除数放在R0-R3,计算之后的结果商在R4-R7,余数在R0-R3中。

图片红框中将第一次的商移动到R0-R3作为第二次除法的除数,但是编译器为了优化,读取时使用的绝对地址访问,并且读取了自己以为正在使用的bank0,后面的都不用想了肯定计算错误。

除了读取错误还有可能使用绝对地址对bank0进行修改,拿这个例子来说如果external_input_Callback函数因为使用绝对地址访问寄存器修改了bank0部分然后返回定时器5中断,定时器5中断进入的时候有没有对bank0部分保存结尾也不需要恢复,就破坏了主程序的上下文。另外如果是一个有返回值的函数可能会通过绝对地址把返回值写入bank0,而调用者却在bank1部分读取。

解决方法也有很多,比如说在配置中勾选不使用绝对寄存器访问,另外下面两种方式经过我实测都可以把函数内绝对访问地址换成bank1的地址。使用第一种会在函数开头生成切换寄存器bank的代码,但是在中断里已经设置PSW寄存器切换过了导致重复操作。所以我建议使用第二种方式,不过要注意#pragma REGISTERBANK(x)指令需要成对出现,函数结束后记得恢复成bank0,不然难以确定指令影响范围。


void external_input_Callback(void) using 1 {
 //......
}

#pragma REGISTERBANK(1)
void external_input_Callback(void) {
 //......
}
#pragma REGISTERBANK(0)
上一篇
51单片机bank功能与扩展关键字
下一篇
51单片机函数调用与可重入函数

评论已关闭

© 2025-2026 嵌入式启航.
豫ICP备2025159703号    备案图标 豫公网安备41172102000273号