Freertos多个xTimerPendFunctionCall,设置了不同等待时间,是如何排序插入的,原理?

FreeRTOS 的 xTimerPendFunctionCall() 并不直接支持按“等待时间”排序。实际上,它的 xTicksToWait 参数控制的是调用者(发送命令的任务)的最大阻塞时间,而命令本身的执行顺序严格遵循先入先出(FIFO) 原则。

其核心原理主要分为三个层面:

🧠 1. 核心原理:排队等待

  • 通信机制:调用该函数时,系统会将“待执行函数”封装成一个命令,放入 定时器命令队列 的末尾。
  • 处理机制:专门的 定时器服务任务 (Daemon Task) 会从队列头部依次取出命令并执行。
  • 结论:这意味着先发出的请求会被先处理,xTicksToWait 并不改变这个顺序。

⏳ 2. 参数作用:控制“发送失败”时的等待

xTicksToWait 仅在队列已满、无法立刻发送命令时生效:

  • ·调用任务会进入阻塞态,等待队列空出位置。
  • ·若在指定时间内发送成功,函数返回 pdPASS;若超时仍未发送,则返回 errQUEUE_FULL。
  • 需注意,这只是保证了发送成功,不改变命令在队列中的处理顺序

⚙️ 3. 执行时间:受“优先级”影响

虽然处理顺序是FIFO,但实际执行时间受守护任务优先级 (configTIMER_TASK_PRIORITY) 影响:

· 优先级高:能抢占低优先级任务,命令响应更及时。
· 优先级低:需等待高优先级任务让出CPU。

如果需要“延时执行”特定函数,可以在待执行的回调函数内部,结合普通软件定时器 API(如 xTimerStart)来实现,而不是依赖 xTicksToWait 参数。

 

他是与timer一起放到timer queue中,如果没有创建timer,是不是pendcall执行的更快?

 

简短的回答是:是的,会更快。

如果没有创建任何软件定时器,xTimerPendFunctionCall 的执行效率确实更高。原因在于定时器服务任务(Daemon Task) 的工作负载发生了根本变化。

具体原理如下:

1. 有软件定时器时:队列中混合了两种命令

定时器服务任务维护一个命令队列,里面有两种命令:

  • 定时器命令:由 xTimerStart() 等API产生,用于创建、启动、复位定时器。
  • 函数调用命令:由 xTimerPendFunctionCall() 产生。

服务任务的处理流程是:

  • 每当系统节拍(Tick)中断到来,它都会检查所有已激活的定时器是否超时。
  • 如果没有超时,它才会去队列里取下一个命令。

因此,如果有大量定时器,服务任务每次都要花时间检查它们,处理 PendFunctionCall 的实时性就会受影响。

2. 没有软件定时器时:服务任务变为纯线程

如果代码中从未创建和启动任何软件定时器(即 configUSE_TIMERS 仍为1,但 NULL 传入 xTimerCreate):

  • 没有定时器链表:服务任务内部用于管理定时器的数据结构是空的。
  • 无需检查超时:每次系统节拍到来,服务任务会瞬间判断出“无定时器待处理”,然后立即去处理队列中的命令。

在这种情况下,xTimerPendFunctionCall 的命令一旦入队,几乎立刻就会被Daemon Task(本质上是系统的prvTimerTask)取出出并执行。它的行为非常接近一个高优先级的回调线程。

总结:对性能的具体影响

场景 定时器服务任务行为 PendCall 执行时机
有大量活跃定时器 每个 Tick 都需遍历检查定时器超时 受定时器检查耗时影响,有延迟
有定时器但未启动 链表非空但无活跃定时器,检查开销较小 几乎无延迟
完全无定时器 链表为空,检查开销极小 最快,接近立即执行

一个重要的注意事项

这个效率提升的前提是:configUSE_TIMERS 宏必须为 1。如果设为 0,定时器服务任务根本不会创建,此时调用 xTimerPendFunctionCall 会直接失败(返回 pdFALSE)。

所以,如果你只需要“在中断服务函数中执行原本属于任务的工作”这个功能(即 Deferred Interrupt Handling),不创建任何软件定时器确实能让 PendCall 得到最优性能。

Deferred interrupt handling是中断下半部吗?

是的,可以这么理解。 FreeRTOS 的 Deferred Interrupt Handling 与 Linux 的“中断下半部”在核心思想上完全一致,都是为了缩短中断关闭时间,将非紧急的处理工作推迟到任务级完成

但由于 FreeRTOS 是实时操作系统且没有内核态/用户态之分,它的具体实现更直接。

核心原理:前半部 vs 后半部

FreeRTOS 的两种实现方式

1. 经典方法(更常见):中断中仅发送信号量,唤醒一个高优先级的处理任务。该任务等待信号量,被唤醒后执行耗时操作。这是典型的“中断告知,任务处理”。

2. 你提到的 xTimerPendFunctionCall:把整个耗时的回调函数打包,通过队列发送给定时器服务任务 (Daemon Task) 执行。好处是无需额外创建任务,但缺点是回调函数执行时间不能太长,否则会影响该任务处理其他定时器。

 

关键区别 vs Linux 下半部

· 触发频率:上半部/下半部非常适合极高频率、可能错过事件的中断。而 FreeRTOS 任务若响应不及,通常只是数据丢失,机制本身不会崩溃。

· 优先级:Linux 下半部优先级高于普通用户进程。FreeRTOS 中,处理任务只需设置高优先级即可,本质仍是普通任务。

 

实际建议

· 一般情况:用信号量/通知唤醒任务即可,逻辑最清晰。

· 希望省掉一个任务:用 xTimerPendFunctionCall 也可。

· 注意:无论哪种方式,回调或任务中执行的代码必须短小,且不能阻塞重要操作,否则可能拉长任务响应时间。

 

回调函数中代码不能执行耗时操作,那如果有需求,应该怎么处理?

这触及了实时系统设计的一个核心矛盾:“中断下半部”要求快,但业务逻辑可能很慢

如果在 xTimerPendFunctionCall 的回调(或任何中断下半部任务)中直接执行耗时操作,最直接的后果就是阻塞定时器服务任务——导致所有软件定时器失效、其他 PendCall 无法执行。

遇到这种需求,标准解法是 “工作流水线化”,把大任务拆解成多个小块。

 

方案一:任务委托(最推荐)

在回调中不做事,只发通知,唤醒一个专门的工作任务。

```c

`

```c

// 1. 创建专门的工作任务(优先级低于定时器服务任务)

void vWorkTask(void *pvParameters) {

    for(;;) {

        ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 等待通知

        vDoHeavyWork(); // 耗时操作在这里安全执行

    }

}

 

// 2. PendCall 回调中仅发送通知

void vPendCallCallback(void *params) {

    xTaskNotifyGive(xWorkTaskHandle); // 瞬间完成

}

```

优点:定时器服务任务无阻塞,系统实时性不受影响。

适用:耗时操作不频繁,或有独立优先级需求。

 

方案二:将任务拆解为状态机

如果不能创建新任务,可以把大操作拆成多个小步骤,在每次回调中执行一小步。

```c

typedef enum { STEP_ONE, STEP_TWO, STEP_THREE } eStep;

eStep xCurrentStep = STEP_ONE;

 

void vPendCallCallback(void *params) {

    switch(xCurrentStep) {

        case STEP_ONE: doStep1(); xCurrentStep = STEP_TWO; 

                       xTimerPendCall(..., 0); // 立即调度下一步

                       break;

        case STEP_TWO: doStep2(); xCurrentStep = STEP_THREE; 

                       xTimerPendCall(..., 0);

                       break;

        case STEP_THREE: doStep3(); xCurrentStep = STEP_ONE; break;

    }

}

```

 

优点:完全不增加任务,资源极省。

缺点:操作必须有明确的“可分割点”;多步之间状态需手动维护。

 

方案三:借助任务通知的“链式触发”

类似方案一,但利用 xTimerPendCall 自身链式触发,模拟简单任务流水线。

 

```c

void vFirstHalf(void *p) {

    doFirstHalfOfWork(); // 快速部分

    // 让定时器服务任务稍后再调用 SecondHalf

    xTimerPendCall(vSecondHalf, 0, 0, NULL);

}

```

 

核心原则总结

 

场景 推荐方案

能加任务 方案一:任务委托

无法加任务,操作可拆分 方案二:状态机

需要延迟重试或分批 方案三:链式触发

绝对不能拖住服务任务 最差情况下,在回调里创建新任务(动态)

 

记住一个铁律:xTimerPendFunctionCall 的回调要像“快递员”一样快——只负责把“包裹”(工作任务)转交给真正干活的人(任务或状态机),而不是自己把包裹拆开慢慢处理。

 

这种设计来源,把timet和pendcall放一起管理?

这个设计来源于 《实时系统设计中“统一事件处理”的思想》,具体到 FreeRTOS,主要基于以下三个来源和考量:

 

1. 直接来源:协作式调度器模式

   最早见于 MiniRIX 等小型 RTOS。它发现定时器事件函数调用请求在本质上是一样的:“需要延迟到非中断上下文执行的动作”。两者都无需立刻执行,可以排队等待一个专门的后台任务来处理。

2. 核心设计逻辑:复用基础设施

   · 复用任务:不必为 PendCall 单独创建一个任务,直接利用现有的定时器服务任务。

   · 复用队列:定时器命令队列本身就是一个现成的、线程安全的通信通道。将 PendCall 命令也放进这个队列,可以完美实现“从中断到任务”的数据传递。

3. 关键抽象:统一的命令结构

   无论是定时器操作还是函数回调,都被封装成一个统一的 DaemonTaskMessage 结构体。服务任务只需要从队列中取出消息,查看消息类型:

   · 是定时器命令 → 操作定时器列表

   · 是 PendCall → 直接执行回调函数

这样设计的最大好处是节省系统资源: 不用额外创建任务和队列,只复用已有的定时器管理框架,就优雅地解决了中断延迟处理的问题。

 

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐