CallBackRef 是什么:为什么回调里既要函数指针,也要一个 void* 上下文

在 Xilinx 驱动(例如 XSpiPs)里,经常能看到这样的接口:

void XSpiPs_SetStatusHandler(
    XSpiPs *InstancePtr,
    void *CallBackRef,
    XSpiPs_StatusHandler FunctionPtr
);

很多人第一次看会困惑:既然已经传了回调函数 FunctionPtr,为什么还需要一个 CallBackRef

答案是:FunctionPtr 决定“做什么”(代码),而 CallBackRef 决定“对谁做”(上下文数据)。
这两者解决的是两个不同维度的问题。


1. FunctionPtr:回调函数指针(代码地址)

回调函数指针告诉驱动:发生事件时调用哪段逻辑。例如 SPI 传输完成、模式错误等事件:

  • 传输完成 → 通知上层继续下一步
  • 错误发生 → 记录日志、重试或上报

驱动不可能替所有应用场景写死策略,所以用函数指针把“策略”交给用户。


2. CallBackRef:回调上下文指针(数据地址)

C 语言的函数指针不携带上下文,回调函数执行时,如果要访问某个对象或状态,就需要一个入口指针。CallBackRef 就是这个入口。

它的特点:

  • 类型是 void *:可以指向任何用户自定义的数据结构
  • 驱动不会解析它:仅在触发回调时原样传回
  • 在回调函数中由用户将其强制转换回真实类型

这相当于在 C 语言里用“函数指针 + void*”手工模拟面向对象里的“成员函数回调”或“闭包”。


3. 为什么不能只用一个回调函数?

如果只用 FunctionPtr,回调函数怎么知道它在服务哪个对象?

例如一个工程里可能有多个 SPI 设备或多条总线:

  • SPI0 控制 AD9528
  • SPI1 控制 Flash 或 ADC
  • 或同一 SPI 上挂多个片选设备

如果只有函数指针,回调函数就必须依赖全局变量来区分“这是哪一个设备的完成事件”,工程规模一大就会:

  • 难以复用
  • 容易写错
  • 多实例难维护

有了 CallBackRef,同一个回调函数可以用于多实例,只要传不同的上下文指针即可。


4. 最典型的用法:传一个“上下文结构体”

这是最推荐的方式:用结构体保存你在回调里需要用到的所有信息。

typedef struct {
    volatile int done;
    u32 last_event;
    u32 bytes;
} spi_ctx_t;

static spi_ctx_t ctx;

static void spi_status_cb(void *ref, u32 event, u32 bytes)
{
    spi_ctx_t *c = (spi_ctx_t *)ref;   // CallBackRef -> 真实类型
    c->last_event = event;
    c->bytes      = bytes;
    c->done       = 1;                 // 通知上层“传输完成/出错”
}

注册回调:

XSpiPs_SetStatusHandler(&SpiInstance, &ctx, spi_status_cb);

这样,驱动内部发生事件时会调用:

InstancePtr->StatusHandler(InstancePtr->StatusRef, EventCode, Bytes);

其中 StatusRef 就是你注册的 CallBackRef


5. 在 FreeRTOS 中更常见:把“句柄”作为 CallBackRef

在 RTOS 场景里,回调通常发生在 ISR 中。最常见做法是 ISR 回调里唤醒任务,典型手段包括:

  • SemaphoreHandle_t 信号量
  • TaskHandle_t 任务通知
  • QueueHandle_t 队列事件

这些 handle 在 FreeRTOS 实现中往往就是“内核对象控制块的引用/指针”,正好可放进 void*,因此可以直接作为 CallBackRef 传入。

例如用信号量等待 SPI 完成:

static SemaphoreHandle_t spi_sem;

static void spi_status_cb(void *ref, u32 event, u32 bytes)
{
    SemaphoreHandle_t sem = (SemaphoreHandle_t)ref;

    BaseType_t hpw = pdFALSE;
    if (event == XST_SPI_TRANSFER_DONE) {
        xSemaphoreGiveFromISR(sem, &hpw);
        portYIELD_FROM_ISR(hpw);
    }
}

void init_spi_cb(void)
{
    spi_sem = xSemaphoreCreateBinary();
    XSpiPs_SetStatusHandler(&SpiInstance, (void*)spi_sem, spi_status_cb);
}

核心思想:
CallBackRef 指向“该通知谁”的目标,回调函数负责“怎么通知”。


6. CallBackRef 使用注意事项(工程必读)

6.1 生命周期必须足够长

回调是异步发生的(中断触发),所以 不能把栈上局部变量地址作为 CallBackRef

void bad()
{
    int local;
    XSpiPs_SetStatusHandler(&SpiInstance, &local, cb); // 错:local 很快失效
}

推荐使用:

  • 全局/static 变量
  • 长生命周期对象(设备结构体、RTOS 句柄)
  • 动态分配(需要你自己管理释放时机)

6.2 回调里要做的事情尽量短

尤其在 ISR 回调中:

  • 只做置位、发信号量/通知、推队列等
  • 不做耗时打印、不做阻塞等待、不做长循环

6.3 类型转换要一致

驱动不会检查类型,你传什么,回调就按你传的类型去转换。类型不一致会导致严重错误。


7. 一句话总结

  • FunctionPtr(回调函数)= 行为/策略(代码)
  • CallBackRef = 上下文/对象引用(数据)

二者结合,让驱动在不依赖全局变量的前提下支持:

  • 多实例
  • 可复用回调
  • ISR/RTOS 同步
  • 应用层自定义处理策略
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐