1、回调函数
1.1、回调函数的优缺点
- 核心思想是反向调用:提前注册好调用函数,当指定事件发生时,请自动调用我预先注册好的这个函数
- 优点:
- 解耦与模块化:
- 调用方(框架、库)和实现方(业务代码)不需要知道对方的具体实现细节。它们只通过一个预定义的函数接口(回调函数的原型)进行通信。
- 比如:写业务代码时有业务需要定时被执行,可以调用库函数里的定时器注册函数,传入回调函数和定时周期,这样就可以实现函数被周期调用,而调用方不用关系定时器的内部实现细节
- 便于团队协作开发:在嵌入式开发中,特别是使用linux系统的产品开发中,软件大致会分为两拨人:linux系统开发(内核态)和业务开发(用户态)。系统开发主要维护linux系统基本功能,开发底层框架;业务开发则使用框架来开发应用层业务。在项目初期,双方约定好回调接口形式,之后就可以并行开发,双方都不必关心对方的实现细节。
- 可以实现异步调用和事件驱动框架
- 当程序发起一个比较耗时的操作(比如DMA搬运),当操作完成后程序需要做某些处理。有两种处理方式:阻塞等待、回调函数。
- 阻塞等待:程序不停查询DMA搬运是否完成,在此期间不能及时响应其他事件
- 回调函数:注册好回调函数,当DMA搬运完成时调用回调函数通知DMA搬运完成,可进行相应处理
- 好处:允许程序在发起一个耗时的操作(如IO请求、网络下载、DMA搬运)后不阻塞等待,而是继续执行后续代码。当那个操作完成后,再通过回调函数来通知和处理结果。
- 缺点:
- 过度嵌套回调会导致代码难以阅读和维护
- 在代码中可以看到回调函数被注册,
1.2、以中断回调为例
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long trigger, const char *name);
- 假设DMA完成使用的中断号是1,处理DMA搬运的流程:
- 注册DMA搬运完成的中断处理函数,注册时选择中断号1
- 发起DMA搬运
- 处理其他业务
- 当DMA搬运完成时上报中断,注册的回调函数被调用,进行相应处理
2、代码逻辑优化
2.1、不使用函数指针
#include <stdio.h>
#include <string.h>typedef void (*CommandHandler)();void help() { printf("Showing help...\n"); }
void quit() { printf("Exiting...\n"); }
void run() { printf("Running program...\n"); }int main() {char userInput[20];printf("Enter a command (help, quit, run): ");scanf("%s", userInput);if(strcmp("help", userInput) == 0){help();}else if(strcmp("quit", userInput) == 0){quit();}else if(strcmp("run", userInput){run();}else{printf("Unknown command.\n");}return 0;
}
2.2、使用函数指针
#include <stdio.h>
#include <string.h>typedef void (*CommandHandler)();void help() { printf("Showing help...\n"); }
void quit() { printf("Exiting...\n"); }
void run() { printf("Running program...\n"); }struct Command {char name[20];CommandHandler handler;
};
struct Command commands[] = {{"help", help},{"quit", quit},{"run", run}
};int main() {char userInput[20];printf("Enter a command (help, quit, run): ");scanf("%s", userInput);int numCommands = sizeof(commands) / sizeof(commands[0]);for (int i = 0; i < numCommands; i++) {if (strcmp(commands[i].name, userInput) == 0) {commands[i].handler(); return 0;}}printf("Unknown command.\n");return 0;
}
2.3、两种实现对比
- 不使用函数指针:
- 代码结构会显示冗余,支持的命令越多,if判断分支就越复杂,不利于看代码和代码维护
- 使用函数指针:
- 不管支持多少个命令都是一个for循环进行判断
- 代码结构简单, 便于阅读、维护
3、代码架构分层
3.1、分层思想和抽象层隔离

- 代码架构分层:
- 就是将代码按照一定层次结构进行组织,每层都会对外提供交互的接口,各层之间通信只用关心暴露的对外接口,而不必关心层次内部的实现细节。
- 每层都管理向下一层,并向上一层提供服务,并且不能越级访问。比如应用软件要操作硬件必须通过操作系统,不能越过操作系统去操作硬件
- 每一层都有相对完整的功能,代码层级清晰,可以支持并行开发,没个层次的软件开发人员按照对外接口进行开发程序,然后再联调
- 层次又可以细分出一些抽象层,抽象层就是把同一类事物的共同点抽象出来,并不具体指向某个事物,但是抽象出来的特征是这一类事物都共有的。
- 抽象层的作用是可以起到隔离的作用:比如我们可以把操作系统看做是一个抽象层,底层硬件不管怎么变(arm架构、x86结构、riscv架构),操作系统对上层软件提供的接口是保持不变的,应用软件不必感知底层硬件的变动
3.2、led驱动子系统
struct led_classdev {const char *name; unsigned int brightness; unsigned int max_brightness; int flags; int (*brightness_set_blocking)(struct led_classdev *led_cdev,enum led_brightness brightness);enum led_brightness (*brightness_get)(struct led_classdev *led_cdev);...........}
- 在linux的led驱动子系统中,使用led_classdev结构体来描述led,里面描述了led的共性特征,其中使用函数指针来表示led的操作函数
- 不管是什么硬件平台的led灯,都会有设置亮度、获取亮度的操作。在硬件初始化时,不同的硬件平台对函数指针赋值成不同的操作函数
- 上层应用操作led时只需要调用这两个函数指针,而具体的操作细节不必关系,这样不管底层硬件如何变,只用系统开发人员适配好,上层业务代码是不需要变动的