指针函数是C语言实现应用层与驱动层解耦的关键工具,本质上是用"指针"封装"函数地址",也就是说让上层(应用层)不直接依赖下层(驱动层)的具体实现,而是依赖统一接口的规范,实现 "接口与实现分离"。
指针函数 :返回值是指针的函数(如 int* func(int a))
函数指针 :存储函数的入口地址,可像调用普通函数一样通过指针调用目标函数(函数指针就是指向函数的指针,如 int (*pFunc)(int a)),是解耦的关键。
驱动层 vs 应用层(嵌入式场景)
驱动层 :底层硬件操作(如 GPIO、UART、SPI、传感器读写),与硬件强相关,负责具体实现 。
应用层 :业务逻辑(如数据处理、状态机、用户交互),与硬件无关,负责调用接口 。
解耦目标:应用层不关心驱动层的硬件细节,驱动层修改(如换传感器、换 MCU)不影响应用层代码。
解耦的核心原理:接口抽象 + 函数指针注册:
// 1. 定义抽象接口(函数指针类型)
typedef int (*DriverReadFunc)(uint8_t* data, uint16_t len);
// 2. 驱动层实现接口(具体硬件操作)
int SensorA_Read(uint8_t* data, uint16_t len) { /* 传感器A的I2C读取 */ }
int SensorB_Read(uint8_t* data, uint16_t len) { /* 传感器B的SPI读取 */ }
// 3. 应用层注册+调用(仅依赖接口,不依赖具体实现)
DriverReadFunc pRead = SensorA_Read; // 绑定传感器A
pRead(data, len); // 调用,无需关心是SensorA还是SensorB
上面这套伪代码实际上就是先定义了一个抽象接口,然后给了两个驱动层的函数,然后决定使用哪个驱动函数,应用层只负责实现功能,到底是函数A还是函数B在工作,应用层是不需要关心的,这也就是解耦的关键,可以用一句话来理解一下就是"对内封装细节,对外暴露接口"
下面我们用一个历程再来详细的说明一下:
-
驱动层头文件:driver_led.h(只定规矩 + 对外接口,应用层看这个)
#ifndef __DRIVER_LED_H
#define __DRIVER_LED_H// 【核心1:定规矩!】
// 定义函数指针类型:LED控制函数的格式(返回void,参数是LED编号+亮灭)
// 所有驱动都必须按这个格式写,应用层也只认这个格式
typedef void (*LED_Control)(int led_id, int on_off);// 【核心2:注册函数】
// 作用:把具体驱动的函数地址,装进驱动层的"函数指针盒子"
void LED_Register(LED_Control driver);// 【核心3:对外接口】
// 应用层唯一能调用的LED控制函数,不直接碰具体驱动
void LED_Set(int led_id, int on_off);#endif
-
驱动层源文件:driver_led.c(写具体驱动,应用层看不到)
#include "driver_led.h"
#include <stdio.h>// 【核心4:函数指针盒子】
// 驱动层内部的变量,存具体驱动的函数地址(应用层看不到)
static LED_Control g_led_driver = NULL;// 【核心5:具体驱动实现(苦力)】
// 模拟STM32的LED控制(实际项目里是操作GPIO寄存器,这里用printf代替)
void STM32_LED_Control(int led_id, int on_off) {
if (on_off == 1) {
printf("【STM32驱动】LED%d 亮了\n", led_id);
} else {
printf("【STM32驱动】LED%d 灭了\n", led_id);
}
}// 【核心6:注册函数实现】
// 把具体驱动的函数地址,装进"函数指针盒子"g_led_driver
void LED_Register(LED_Control driver) {
if (driver != NULL) { // 防止传空地址
g_led_driver = driver;
}
}// 【核心7:对外接口实现】
// 应用层调用LED_Set,内部通过函数指针调用具体驱动
void LED_Set(int led_id, int on_off) {
if (g_led_driver != NULL) { // 先检查盒子里有没有驱动
g_led_driver(led_id, on_off); // 【解耦关键!】通过指针调用,不直接写STM32_LED_Control
}
}// 驱动初始化:默认绑定STM32驱动(简化,不用手动注册也能跑)
void LED_Init(void) {
LED_Register(STM32_LED_Control);
} -
应用层头文件:app_led.h(业务接口,主函数看这个)
#ifndef __APP_LED_H
#define __APP_LED_H// 应用层业务:LED闪烁(led_id:LED编号,times:闪烁次数)
void LED_Blink(int led_id, int times);#endif
-
应用层源文件:app_led.c(写业务逻辑,不碰任何硬件)
#include "app_led.h"
#include "driver_led.h" // 只包含驱动层接口,不包含driver_led.c// 【应用层:只写业务,不管硬件】
void LED_Blink(int led_id, int times) {
for (int i = 0; i < times; i++) {
LED_Set(led_id, 1); // 亮(调用驱动层对外接口)
LED_Set(led_id, 0); // 灭(调用驱动层对外接口)
}
} -
主函数:main.c(绑定驱动 + 跑业务)
#include "driver_led.h"
#include "app_led.h"int main(void) {
LED_Init(); // 驱动初始化(绑定STM32驱动)// 调用应用层业务:LED1闪烁3次 printf("===== 开始闪烁 =====\n"); LED_Blink(1, 3); printf("===== 闪烁结束 =====\n"); return 0;}
上面的这套代码大体分为以下几个步骤:
-
函数指针类型(规矩)
typedef void (*LED_Control)(int led_id, int on_off);
- 意思:定义一个 "类型" 叫 LED_Control,它代表 "返回 void、参数是 int+int 的函数"
- 作用:应用和驱动都按这个格式来,相当于 "签合同",谁都不能改格式
2. 函数指针盒子(存驱动地址)
static LED_Control g_led_driver = NULL;
- 意思:定义一个变量 g_led_driver,它的类型是 LED_Control(函数指针),初始为空
- 作用:这个变量是驱动层的 "盒子",专门存具体驱动函数的地址(比如 STM32_LED_Control 的地址),调用这个变量(加括号传参),就等于调用它存的那个函数。
3. 具体驱动实现(苦力干活)
void STM32_LED_Control(int led_id, int on_off) {
// 实际项目:操作STM32的GPIO寄存器(比如HAL_GPIO_WritePin)
// 这里用printf模拟,让你能看到效果
if (on_off == 1) {
printf("【STM32驱动】LED%d 亮了\n", led_id);
} else {
printf("【STM32驱动】LED%d 灭了\n", led_id);
}
}
- 作用:真正控制硬件的代码,和具体芯片(STM32)强相关,应用层看不到
4. 注册函数(把驱动装进盒子)
void LED_Register(LED_Control driver) {
if (driver != NULL) {
g_led_driver = driver; // 把具体驱动的地址,赋值给盒子变量
}
}
- 作用:绑定驱动,比如传 STM32_LED_Control,盒子里就存了这个函数的地址
- 关键:应用层通过这个函数,告诉驱动层 "用哪个苦力干活"
5. 对外接口(应用层唯一入口)
void LED_Set(int led_id, int on_off) {
if (g_led_driver != NULL) {
g_led_driver(led_id, on_off); // 【核心!】通过指针调用具体驱动
}
}
- 作用:应用层只调用这个函数,不用管盒子里是 STM32 还是 ESP32 的驱动
- 解耦体现:应用层代码里,没有任何 STM32_LED_Control 的字样,换驱动不用改应用层
下面我们再引入回调函数:
#include <stdio.h>
// ====================== 1. 公共头文件:定义函数指针(核心) ======================
// 1.1 普通计算函数指针(int (int,int))
typedef int (*CalcFunc)(int a, int b);
// 1.2 中断回调函数指针(嵌入式常用:无返回值,可传参数/全局数据)
// 注意:回调函数格式要和中断模块约定好!这里约定:无返回值,传2个int参数(a和b)
typedef void (*IntCallbackFunc)(int a, int b);
// ====================== 2. 中断模块:模拟嵌入式中断控制器(解耦核心) ======================
// 中断模块内部:存「注册的回调函数地址」(和之前g_led_driver一样,是函数指针变量)
static IntCallbackFunc s_int_callback = NULL;
// 2.1 中断回调注册函数(应用层调用,把回调函数地址传给中断模块)
void Int_RegisterCallback(IntCallbackFunc callback) {
if (callback != NULL) {
s_int_callback = callback; // 把回调地址存进中断模块的"盒子"
}
}
// 2.2 模拟中断服务函数(ISR):真实硬件中,这是CPU自动执行的中断处理逻辑
static void Int_ISR(int a, int b) {
printf("【中断服务】中断触发!开始执行中断逻辑...\n");
// 中断核心:调用注册的回调函数(被动调用,解耦关键!)
if (s_int_callback != NULL) {
s_int_callback(a, b); // 中断模块调用应用层的回调,不依赖具体回调实现
}
printf("【中断服务】中断执行完毕,返回主程序\n");
}
// 2.3 模拟中断触发函数(模拟硬件中断发生:比如定时器溢出、GPIO中断)
// 真实嵌入式中,这一步是硬件自动触发,不用手动调用;这里手动调用模拟
void TriggerInterrupt(int a, int b) {
printf("\n===== 模拟硬件中断触发 =====\n");
Int_ISR(a, b); // 调用中断服务函数(ISR)
printf("===== 模拟中断结束 =====\n\n");
}
// ====================== 3. 功能模块:加法函数(作为中断回调函数) ======================
// 加法函数:格式必须和IntCallbackFunc约定一致(无返回值,2个int参数)
// 注意:回调函数要遵守中断模块的"接口契约",否则会跑飞!
void Add_Callback(int a, int b) {
int res = a + b; // 核心计算:2+3=5
printf("【回调函数】执行加法:%d + %d = %d\n", a, b, res);
}
// ====================== 4. 主函数:应用层逻辑(注册回调+触发中断) ======================
int main() {
printf("===== 主程序开始 =====\n");
// 步骤1:应用层注册回调(把加法回调函数地址,传给中断模块)
// 相当于:告诉中断模块"中断发生时,帮我调用Add_Callback这个函数"
Int_RegisterCallback(Add_Callback);
printf("【主程序】已注册加法回调函数\n");
// 步骤2:模拟触发中断(传参数2和3,和之前例子一致)
// 真实嵌入式中,这一步是硬件自动触发(比如定时器到时间了)
TriggerInterrupt(2, 3);
// 步骤3:主程序继续执行(中断结束后返回)
printf("===== 主程序结束 =====\n");
return 0;
}
然后我们再来大体介绍一下这套代码:
-
回调函数指针的定义(和普通函数指针的区别)
// 普通计算函数指针(主动调用:你调它)
typedef int (*CalcFunc)(int a, int b);// 中断回调函数指针(被动调用:中断调它)
typedef void (*IntCallbackFunc)(int a, int b);
- 本质都是函数指针 ,只是用途和调用者不同 :
CalcFunc:你主动调用(比如CalcFunc p = Add; p(2,3);)IntCallbackFunc:中断模块主动调用(你只注册,不主动调)
- 关键约束 :回调函数的参数、返回值 必须和中断模块约定的
IntCallbackFunc完全一致(比如这里都是void返回,2 个int参数),否则会栈破坏、程序跑飞!
2. 中断模块的「回调盒子」(和之前 g_led_driver 完全一样)
// 中断模块内部:存回调函数地址的变量(函数指针,静态,仅中断模块可见)
static IntCallbackFunc s_int_callback = NULL;
- 这就是中断模块的 "函数指针盒子" ,和之前 LED 例子的
g_led_driver、加法例子的p是同一个东西:- 存的是函数地址 (这里存
Add_Callback的地址) - 初始
NULL(没注册回调时,中断不会乱调用)
- 存的是函数地址 (这里存
3. 注册回调:把加法函数地址装进 "中断盒子"
// 应用层调用:注册加法回调
Int_RegisterCallback(Add_Callback);
// 注册函数实现:把回调地址存进盒子
void Int_RegisterCallback(IntCallbackFunc callback) {
if (callback != NULL) {
s_int_callback = callback; // 核心:赋值函数地址
}
}
- 这一步和之前
LED_Register(STM32_LED_Control)、p = Add完全一样 :- 都是把函数的地址,赋值给函数指针变量
- 区别:之前是给 "驱动盒子" 赋值,现在是给 "中断盒子" 赋值
4. 模拟中断触发:硬件打断主程序,执行中断服务
// 主程序手动调用(模拟硬件自动触发)
TriggerInterrupt(2, 3);
// 中断服务函数(ISR):中断发生时CPU自动执行
static void Int_ISR(int a, int b) {
printf("【中断服务】中断触发!开始执行中断逻辑...\n");
// 中断核心:调用注册的回调(被动调用)
if (s_int_callback != NULL) {
s_int_callback(a, b); // 【关键!】中断模块调用应用层回调
}
printf("【中断服务】中断执行完毕,返回主程序\n");
}
- 真实嵌入式中,
TriggerInterrupt是硬件自动触发 (比如定时器溢出、GPIO 电平变高),CPU 会暂停主程序,跳去执行Int_ISR(中断服务函数) - 中断服务里的
s_int_callback(a, b):- 和之前
g_led_driver(led_id, on_off)、p(2,3)完全一样 :- 都是通过函数指针,调用它存的函数
- 区别:之前是应用层主动调用 ,现在是中断模块被动调用
- 和之前
5. 回调函数执行:加法计算 2+3=5
// 加法回调函数(遵守中断模块的接口契约)
void Add_Callback(int a, int b) {
int res = a + b; // 核心计算:2+3=5
printf("【回调函数】执行加法:%d + %d = %d\n", a, b, res);
}
- 这就是应用层的业务逻辑 (加法计算),和中断模块完全解耦 :
- 中断模块不知道回调里是加法、减法还是打印,只负责按约定格式调用
- 你想换业务(比如改成减法),只需要写
Sub_Callback,重新注册即可,中断模块代码一行不改