C语言模拟 MCU 上电后程序的执行顺序 + 回调函数机制 + 程序计数器(PC)和堆栈的作用

引言

前不久听到别人讨论回调函数的原理,我就感觉有点好奇,然后刚好那会了解了下STC8单片机上电启动后做的一些事情,就突发奇想:要不自己尝试来用C语言区模拟写一个试试?这不,我就试了一下,写是写了,但是写到后面感觉变成中断的样子了哈哈哈,有点奇怪,但是还是也有回调函数的样子,可能是个二不像吧。不过,整个过程下来,感觉可以在回调和中断或者说单片机上电后执行的顺序应该多少能有一些收获。

一、大致思路

因为是在C语言中进行模拟,所以函数指针这个概念我们是逃不了了,因为我们需要拿到程序执行入口的地址,程序我们得靠函数去模拟,相应的入口地址就靠函数指针模拟了。

对于函数指针的理解,笔者是这样理解的:对应函数内程序指令所在的内存地址。

最开始笔者有个基础的思路:感觉只要可以把程序执行时记录下一个指令地址修改成我的回调函数指针,然后里面调用旁边的参数就能在指定状态执行回调函数了,只不过还需要执行完以后再修改下一条指令地址为原来的下一条指令。

然后笔者就开始试着一点点写代码了,然后后续的修改也是随机应变了。

二、程序代码

复制代码
#include <stdio.h>
#include <stdlib.h>

// 启动程序 -> 输入俩数触发事件 -> 记录原PC,回调覆盖PC -> (执行回调,计算 -> 打印输出值) -> 恢复原执行顺序


// 定义函数指针类型
typedef void (*FuncPtr)();

// PC,用于记录下一条程序指令地址,函数指针模拟
FuncPtr current_pc;

// 用于保存被中断的PC值(模拟堆栈)
FuncPtr saved_pc[2];
int stackPtr = 0;	// 模拟堆栈指针

int num1, num2, result;

// 启动程序之后的指令
void Post_Start_Func()
{
	printf("执行启动程序之后的指令...\n");
	printf("正在处理系统常规任务...\n");
	printf("常规任务处理完毕\n");
}

// 启动程序,进行初始化,普通函数调用来模拟
void START_Func()
{
	printf("开始执行启动程序...\n");

	printf("...初始化1...完成\n");
	printf("...初始化2...完成\n");
	printf("...初始化3...完成\n");

	// PC永远指向下一条执行指令地址
	current_pc = Post_Start_Func;
}

// 后续处理函数
void After_Calc_Func()
{
	printf("\n===== 5. 执行计算后函数 =====\n");
	printf("继续处理其他任务...\n");
	printf("所有任务处理完毕!\n\n");
}

// 获取用户输入
void Input_Func()
{
	printf("\n===== 2. 执行输入函数 =====\n");
	printf("请输入两个整数:");
	scanf_s("%d %d", &num1, &num2);
	printf("已输入: % d 和 % d\n\n", num1, num2);
}

// 定义回调函数指针类型
typedef int (*CalcCallback)(int, int);

// 普通计算函数:接收参数和回调函数
int calculate(int a, int b, CalcCallback callback)
{
	printf("\n===== 3. 执行普通计算函数 =====\n");
	printf("接收到参数: %d 和 %d\n", a, b);

	// 准备进入回调:将当前指令执行位置(模拟入栈)
	printf("将当前执行程序的下一条指令压入模拟堆栈(栈指针: %d)\n", stackPtr);
	saved_pc[stackPtr++] = current_pc;
	
	// 调用回调函数处理计算
	printf("调用回调函数处理计算...\n");
	int res = callback(a, b);

	printf("计算函数执行完毕 \n\n");
	return res;
}

int add_callback(int a, int b);

int main()
{
	// 1. 执行启动指令
	// 1.1 强制PC指向启动程序指令地址
	current_pc = START_Func;

	// 1.2 开始执行启动程序 
	current_pc();

	// ==================== 主程序开始 =====================

	// 2. 假设此时进入main中执行主程序,先将当前下一指令地址压入模拟堆栈
	printf("向模拟堆栈压入启动后下一条指令地址(栈指针: %d)\n", stackPtr);
	saved_pc[stackPtr++] = current_pc;
	
	// 2.1 修改PC,后面先输入两个数字,然后将PC改成另一指令地址
	current_pc = Input_Func;
	// 2.2 执行输入程序
	current_pc();
	// 2.3 手动修改PC,指向计算后的函数
	current_pc = After_Calc_Func;

	// 接着继续往后执行(PC被修改,所以执行后续处理函数)
	// 2.4 调用计算函数
	result = calculate(num1, num2, add_callback);
	
	// 打印计算结果
	printf("===== 计算结果 =====\n");
	printf("结果为 %d\n\n", result);

	// ============ 整个回调结束 ==============

	// 2.5 弹出之前程序地址(计算后处理)赋值给PC
	current_pc = saved_pc[--stackPtr];
	printf("模拟堆栈弹出计算后指令地址(栈指针: %d)\n", stackPtr);
	// 接着继续执行原来的程序执行后的下一条指令,即计算结束提示
	current_pc();

	// =================== 主程序结束 =====================

	// main程序中的所有内容全部结束
	printf("弹出启动程序后的下一条指令地址给PC,继续原顺序执行\n");
	// 从模拟堆栈中弹出原启动程序执行后的下一条指令,赋值给当前PC
	current_pc = saved_pc[--stackPtr];
	printf("模拟堆栈弹出计算后指令地址(栈指针: %d)\n", stackPtr);
	
	if (current_pc != NULL) 
	{
		current_pc();
	}

	return 0;
}


// 回调函数:实际执行加法
int add_callback(int a, int b)
{
	printf("\n===== 4. 执行回调函数 =====\n");
	printf("正在计算 %d + %d...\n", a, b);
	int res = a + b;
	printf("回调计算完成,结果: %d\n\n", res);
	return res;
}

运行后如下:

这其中主要涉及的几个关键点如下:

1、上电启动流程
1.1 START_Func() 就像 MCU 的 Reset_Handler,先执行初始化。
1.2 执行完之后 current_pc 改为 Post_Start_Func,就像 MCU 会跳到 main() 或其他初始化后的主程序入口。
2、模拟 PC(程序计数器)
2.1 current_pc 存储下一条要执行的"指令"(其实是函数地址)。
2.2 调用 current_pc() 就相当于"取指 -> 执行"。
PC 的修改就是通过直接改变 current_pc 来实现的。
3、模拟堆栈保存返回地址
3.1 saved_pc[stackPtr++] = current_pc;
这一步相当于中断发生时 MCU 自动把返回地址压栈(这里你是手动模拟)。
3.2 current_pc = saved_pc[--stackPtr];

这一步相当于执行 RET 或 POP PC 把返回地址弹出来。
4、回调函数模拟中断/事件处理
4.1 calculate() 里调用 callback(),就像中断服务程序调用特定任务处理逻辑。
4.2 add_callback() 就是回调本体,它只关心业务(加法),不关心外部 PC 流程。
5、执行顺序可视化
5.1 通过 printf 把每一步都打印出来,让人清楚看到"压栈 -> 跳转 -> 回调 -> 弹栈 -> 恢复"的全过程。


三、小结

本次叙述了笔者一个简单的想法,尝试模拟单片机上电后的流程和回调的原理,涉及到的代码是一很形象的 MCU + 回调函数工作原理的示例,融合了:

  • MCU 上电启动流程

  • 中断/事件触发机制

  • 程序计数器(PC)的作用

  • 堆栈保存/恢复执行位置

  • 回调函数的调用与返回


笔者小白,能力有限,以上内容难免存在不足和纰漏,仅供参考,各位阅读时请带着批判性思维学习,遇到问题多查查。同时欢迎各位评论区批评指正。谢谢。