指针函数的应用层与驱动层:解耦核心与实践

指针函数是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在工作,应用层是不需要关心的,这也就是解耦的关键,可以用一句话来理解一下就是"对内封装细节,对外暴露接口"

下面我们用一个历程再来详细的说明一下:

  1. 驱动层头文件: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

  2. 驱动层源文件: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);
    }

  3. 应用层头文件: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

  4. 应用层源文件: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); // 灭(调用驱动层对外接口)
    }
    }

  5. 主函数: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;

    }

上面的这套代码大体分为以下几个步骤:

  1. 函数指针类型(规矩)

    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;
}

然后我们再来大体介绍一下这套代码:

  1. 回调函数指针的定义(和普通函数指针的区别)

    // 普通计算函数指针(主动调用:你调它)
    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,重新注册即可,中断模块代码一行不改
相关推荐
码农水水2 小时前
大疆Java面试被问:使用Async-profiler进行CPU热点分析和火焰图解读
java·开发语言·jvm·数据结构·后端·面试·职场和发展
Elias不吃糖2 小时前
Java 常用数据结构:API + 实现类型 + 核心原理 + 例子 + 选型与性能(完整版)
java·数据结构·性能·实现类
Hx_Ma162 小时前
List 转二维 List
数据结构·windows·list
Full Stack Developme2 小时前
算法与数据结构,到底是怎么节省时间和空间的
数据结构·算法
BHXDML2 小时前
数据结构:(三)字符串——从暴力匹配到 KMP 的跨越
数据结构·算法
不穿格子的程序员3 小时前
数据结构篇1:不仅是存储:透视数据结构的底层逻辑与复杂度美学
数据结构·时间复杂度·空间复杂度
好奇龙猫3 小时前
【大学院-筆記試験練習:线性代数和数据结构(19)】
数据结构·线性代数
2401_841495643 小时前
【LeetCode刷题】LRU缓存
数据结构·python·算法·leetcode·缓存·lru缓存·查找
一路往蓝-Anbo3 小时前
第 2 篇:单例模式 (Singleton) 与 懒汉式硬件初始化
开发语言·数据结构·stm32·单片机·嵌入式硬件·链表·单例模式