[TOC]

手动实现51单片机函数切换

一、前言

为什么要研究单片机函数切换的过程?实际上是我在20年暑假时给51单片机写了一个简单的实时操作系统,具有简单的抢占式内核调度功能,虽然很简单,但我还是想把实现的过程分享出来,这篇文章是其中的内容之一,有兴趣的同学可以先了解一下,点个关注收藏,后面持续更新!

二、函数切换原理

在使用C语言编写51单片机的程序时,如果我们在函数一中调用另外一个函数,只需要添加一行 函数名+括号及参数 就可以执行另外一个函数,就就像下面的例子:

1
2
3
4
5
6
7
int main(void)
{
int a=0;
Fun1(a);
Fun2(a);
return 0;
}

在main函数中直接调用Fun1,Fun2函数,然后程序就会跳转。但是问题来了,函数是怎么跳转的呢?在函数跳转的过程中51单片机的寄存器是如何变换的呢?

实际上,函数的切换过程其实就是将当前函数的运行状态和数据以及返回地址等保存到堆栈,然后读取新函数的运行状态和数据,PC(程序计数器)再跳转到调用函数的地址执行对应的函数,这些操作其实都是在对51单片机的寄存器进行操作,具体用到的几个寄存器如下:

寄存器 功能
R0-R7 工作寄存器R0~R7:存储当前程序的 “环境“
DPH 数据地址指针(高8位):DPH和DPL组合在一起使用,用它来访问外部数据存储器中的任一单元,也可以作为通用寄存器来用
DPL 数据地址指针(低8位):DPH和DPL组合在一起使用,用它来访问外部数据存储器中的任一单元,也可以作为通用寄存器来用
PSW 程序状态字:里面放了CPU工作时的很多状态,可以了解CPU的当前状态
B B寄存器:在做乘、除法时放乘数或除数
ACC 累加器:运算寄存器
SP 堆栈指针:指向堆栈操作的栈顶地址,是8位计数器
PC 程序计数器:指向下一条待执行的指令

下面我们来用汇编手动编写一个函数切换函数,然后在定时器中断中调用,不停的切换两个函数,编写前先了解一下切换框架和使用到的汇编代码

  • POP出栈指令

    弹出堆栈数据到data,然后SP指针减一

    1
    POP data
  • PUSH压栈指令

    先把SP指针加一,然后将data数据压入堆栈

    1
    PUSH data
  • RET返回指令

    把弹出堆栈两个字节的数据到PC,指向下一个程序的执行地址

三、函数切换代码实现

函数代码我们使用51单片机作为运行平台,在主函数中通过切换函数1切换到函数1,函数1是一个死循环,之后我们在函数1里面调用函数切换2切换到函数2运行,函数2延时一段时间后再切换回1,一直循环下去;代码如下:

定义用到的函数:

1
2
3
void task1(void); //函数1
void task2(void); //函数2
void delay(unsigned short time);//延时函数

定义用到的变量和类型

1
2
3
4
5
6
7
8
9
10
11
12
unsigned char a;	//函数一运行的标志
unsigned char b; //函数二运行的标志
unsigned char task1_stack[20]; //函数堆栈
unsigned char task2_stack[20]; //函数堆栈
//声明函数控制块结构体
typedef struct
{
unsigned char Task_SP; //函数堆栈指针
}TASK_TCB;
//定义TCB
TASK_TCB task1_tcb;
TASK_TCB task2_tcb;

编写main函数主体初始化,此处定义两个函数控制块tcb,用来存放函数的堆栈指针(函数的堆栈其实就是一个数组,用来保存函数的运行数据),然后我们在将函数的入口地址保存在堆栈的最低两位,接着将SP指针向上偏移14位,因为我们要保存的寄存器加起来有13位,同时在一开始要把函数入口保存在堆栈所以是14位

而切换到函数的时候是要先从函数堆栈出栈,所以预先偏移14位地址,main函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void main(void)
{
//保存堆栈指针和函数入口
task1_tcb.Task_SP = task1_stack;
task1_stack[0]= (unsigned char)task1;
task1_stack[0]= (unsigned char)task1>>8;
//偏移堆栈
task1_tcb.Task_SP += 14;
//保存堆栈指针和函数入口
task2_tcb.Task_SP = task2_stack;
task2_stack[0]= (unsigned char)task2;
task2_stack[0]= (unsigned char)task2>>8;
//偏移堆栈
task2_tcb.Task_SP += 14;
//切换到函数1
Task_Sched_1();
while(1);
}

编写函数1和函数2实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void task1(void) 
{
while(1)
{
a=1;
b=0;
delay(100); //延时
Task_Sched_2();//切换到函数2
}
}

void task2(void)
{
while(1)
{
a=0;
b=1;
delay(100);//延时
Task_Sched_1();//切换到函数1
}
}

编写函数切换函数

切换到函数1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void Task_Sched_1(void)
{
__asm PUSH ACC //保护当前寄存器,压栈
__asm PUSH B
__asm PUSH PSW
__asm PUSH DPL
__asm PUSH DPH
__asm PUSH 0 //0-7为工作寄存器
__asm PUSH 1
__asm PUSH 2
__asm PUSH 3
__asm PUSH 4
__asm PUSH 5
__asm PUSH 6
__asm PUSH 7
SP = (task1_tcb.Task_SP);
__asm POP 7 //恢复目标函数寄存器
__asm POP 6
__asm POP 5
__asm POP 4
__asm POP 3
__asm POP 2
__asm POP 1
__asm POP 0
__asm POP DPH
__asm POP DPL
__asm POP PSW
__asm POP B
__asm POP ACC
}

切换到函数2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void Task_Sched_2(void)
{
__asm PUSH ACC //保护当前寄存器,压栈
__asm PUSH B
__asm PUSH PSW
__asm PUSH DPL
__asm PUSH DPH
__asm PUSH 0 //0-7为工作寄存器
__asm PUSH 1
__asm PUSH 2
__asm PUSH 3
__asm PUSH 4
__asm PUSH 5
__asm PUSH 6
__asm PUSH 7
SP = (task2_tcb.Task_SP);
__asm POP 7 //恢复目标函数寄存器
__asm POP 6
__asm POP 5
__asm POP 4
__asm POP 3
__asm POP 2
__asm POP 1
__asm POP 0
__asm POP DPH
__asm POP DPL
__asm POP PSW
__asm POP B
__asm POP ACC
}

注意此处的切换函数使用汇编编译,主要内容就是保存当前函数的运行环境到函数堆栈,然后从下一个函数的堆栈读取其运行环境,切换代码我写在一个os.c文件里面,编译前需要汇编编译,步骤如下:

右击文件->options

20210808163102

开启嵌入汇编程序,使C语言中可以编译汇编代码,加__asm声明一下是汇编就行

20210808163124

四、实验现象

函数1中把a取1,b取0,而函数2相反,当这两个函数交叉运行时a和b的波形应该相反,所以仿真后结果如下,手动切换函数完成

20210808161548

wechat