函数指针的回调函数与函数跳转执行
在C语言中,函数指针是一项重要的知识点,到了嵌入式,这个知识点的重要性更是如此,例如回调函数,Bootloader的绝对地址跳转,让我们不得不重视这个知识点。本章就是讲解函数指针的两种主要用法。
1.函数指针定义
认识一个人时,外貌通常是第一印象的来源。函数指针同样具备独特的语法表现形式,其声明方式直接反映了它的本质特性。
void cal_sum(void);1.普通函数定义
/*(*func_ptr)括号不能丢,丢了就是返回一个指针的普通函数*/
void (*func_ptr)(void);/*指针函数指向一个函数的地址*/
func_ptr= &cal_sum;2.typedef 函数指针typedef void (*func_ptr)(void);/*声明了一个函数指针变量 pFun*/
func_ptr pFun=cal_sum;
第一个方法看着好像跟我们的指针变量其实也差不多,其实指针的本质都是如此,即存放的都是一个地址罢了。
第二个的typedef关键字的使用方法好像跟之前使用的不太一样,其实这个的意思是要定义的类型是void (*)(void)
,没有输入参数,返回值为void的函数指针,定义的别名是func_ptr
。
他们实现的效果都是一样的。并无区别。
2.回调函数:
2.1 回调函数的概念
回调函数本质上是通过函数指针来实现的。简单来说,当我们把一个函数的指针(即函数的入口地址)作为参数传递给另一个函数时,在被调用的函数内部,就可以通过这个指针来调用其所指向的函数,这个被调用的函数就被称为回调函数。
回调函数是一种 “逆向” 的函数调用方式,不是由该函数的实现方法直接调用,而是在特定的事件或条件发生时,由另外的代码通过函数指针来触发调用。
2.2 核心价值
- 非阻塞:主程序不用傻等(你可以继续玩手机等通知)。
- 异步处理:耗时操作完成后自动触发后续动作。
- 解耦:点餐系统和取餐动作分离(厨房无需知道你怎么取餐)。
2.3 回调函数的实现机制
举个栗子:
-
你点餐(发起请求)
告诉服务员:“我要一份牛排,做好后叫我取餐”(牛排:callback_func,
服务员:register_callback) -
厨房工作(主程序执行)
厨师开始烹饪,你不需要站在厨房门口等(厨师:void (*callback)(int)回调函数) -
叫你取餐(回调执行)
牛排做好后,服务员主动通知你:“您的餐好了!”
1.3.1 普通函数指针定义的代码例子实现
void callback_func(int value) {printf("Callback triggered with value: %d\n", value);
}void register_callback(void (*callback)(int)) {int event_value = 42;callback(event_value); // 通过函数指针触发回调
}int main() {register_callback(callback_func); // 传递函数指针return 0;
}
1.3.2 typedef定义的代码例子实现
#include <stdio.h>typedef void (*CallbackFunc)(int);void handleEvent(int value) {printf("Callback triggered with value: %d\n", value);
}void registerCallback(CallbackFunc func) {func(42);
}int main() {registerCallback(handleEvent);return 0;
}
既然我们讲完了回调函数的概念,那么他在嵌入式中有什么使用的地方呢?
2.4 一般使用场景
2.4.1 中断服务程序(ISR)中异步通知用户代码。
// 硬件层定义中断回调类型
typedef void (*isr_callback_t)(void);// 注册回调函数指针
volatile isr_callback_t user_callback = NULL;void register_isr_handler(isr_callback_t cb) {user_callback = cb;
}// 实际中断处理程序
void TIM2_IRQHandler() {if(TIM2->SR & TIM_SR_UIF) {TIM2->SR &= ~TIM_SR_UIF;if(user_callback) user_callback();}
}
2.4.2 硬件抽象层(HAL)中实现驱动与应用的解耦。
// HAL库定义UART接收回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {if(huart->Instance == USART1) {// 应用层处理接收完成事件process_received_data(huart->pRxBuffPtr);}
}// 应用层使用示例
uint8_t rx_buffer[32];
HAL_UART_Receive_IT(&huart1, rx_buffer, sizeof(rx_buffer));
2.4.3 协议栈(如TCP/IP)中处理数据到达事件。
// LwIP协议栈定义接收回调
err_t tcp_recv_callback(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{if(p != NULL) {// 应用层处理网络数据包process_network_packet(p->payload, p->len);pbuf_free(p);}return ERR_OK;
}// 注册回调函数
tcp_recv(tcp_conn, tcp_recv_callback);
2.4.4 定时器模块中定义超时后的自定义行为。
// 定时器模块接口
typedef void (*timer_callback)(void*);struct timer {uint32_t timeout;timer_callback cb;void* user_data;
};// 定时器到期处理
void timer_expire(struct timer* t) {if(t->cb) t->cb(t->user_data);
}// 应用层使用示例
void led_toggle(void* arg) {GPIO_TogglePin(LED_PORT, LED_PIN);
}struct timer led_timer = {.timeout = 1000,.cb = led_toggle,.user_data = NULL
};
3.跳转执行
3.1 跳转执行的概念
在单片机编程中,尤其是涉及到固件更新或者Bootloader设计时,绝对地址跳转是一项重要的技术。在C语言环境中,我们通常不直接操作内存地址,而是通过函数指针来实现这种跳转。
跳转执行他前面的流程其实和回调函数一样,但回调函数执行完还会返回到调用的地方/地址,函数跳转是在跳转到的那个地址执行下去,不会返回了(这是我们的期望结果)。
就比如你在main函数执行跳转执行的代码,那他跳转过后不返回,后面就不会就行执行main函数的代码了。(就是离家出走不回来了)
有的人可能很懵逼,主函数都不执行了,那我还能维持整个项目的运行?
当然可以的,兄弟,我们只要跳转到的地址里面是另外一个初始化程序+main函数,那么我们就可以执行新的程序的代码了。
3.2 核心价值
- 跳转:跳转到另外一个程序(APP)的入口点。
- 不返回:执行另外一个程序,那自然就让他执行下去嘛,旧程序的使命就完成了,那就不要回去了。
3.3 跳转函数的实现机制(嵌入式)
目前我基础到的函数跳转的使用就是嵌入式的 STM32 Bootloader 中实现跳转到用户程序(APP)。因此我举例的也是嵌入式的函数跳转例子供大家参考,阅读起来会有点难度。
typedef void (*pFunction)(void);void JumpToApplication(uint32_t appAddr)
{uint32_t stackPointer = *(volatile uint32_t*)appAddr;uint32_t resetHandler = *(volatile uint32_t*)(appAddr + 4);// 检查栈指针是否在 SRAM 地址范围内(0x20000000 ~ 0x2001FFFF)if ((stackPointer & 0x2FFE0000) != 0x20000000) {printf("Invalid stack pointer in application.\r\n");return;}// 关闭全局中断__disable_irq();// 设置主栈指针 MSP__set_MSP(stackPointer);// 跳转到用户程序入口pFunction jumpToApp = (pFunction)resetHandler;jumpToApp();
}
步骤 | 操作 | 说明 |
---|---|---|
1 | uint32_t stackPointer = *(volatile uint32_t*)appAddr; | 从 Flash 起始地址读取栈顶地址 |
2 | uint32_t resetHandler = *(volatile uint32_t*)(appAddr + 4); | 读取复位处理函数地址(即程序入口) |
3 | __set_MSP(stackPointer); | 设置主栈指针为用户程序的栈顶值 |
4 | jumpToApp(); | 实际跳转到用户程序运行 |
3.4 一般使用场景
3.4.1 STM32 Bootloader 中实现跳转到用户程序(APP)
代码如上
4.总结
特性 | callback() (回调函数) | Jump_To_Application() (跳转执行) |
---|---|---|
是否返回 | ✅ 是 | ❌ 否 |
是否跳转到其他程序 | ❌ 否 | ✅ 是 |
是否改变程序流 | ❌ 否 | ✅ 是 |
是否释放控制权 | ❌ 否 | ✅ 是 |
应用场景 | 初始化、检查、配置等 | 跳转到用户程序 |
虽然我列了两部分,但你理解的时候可以看成一个知识点来理解。下面给出我的理解方法。
1.当我们初始化了函数了之后,那肯定是有返回地址的,这个是我们设置了函数的最后会有LR寄存器来保护返回地址。因此能返回回来。
2.但我们的函数跳转我们只是传入了地址,这个地址是没有定义函数的,没有初始化地址那就没有LR寄存器来保护返回地址。因此无法返回地址。因此只能一直执行下去。