4. 使用内核定时器实现精确延时

任务目标

在Zephyr RTOS中,k_sleep()和k_usleep()会让当前线程进入挂起状态,从而让出CPU给其他就绪线程。本案例实现一个基于内核定时器的非阻塞延时工具:调用方传入延时毫秒数,函数立即返回,延时完成后通过回调通知。该工具适用于需要周期性采样或超时控制、但又不希望阻塞当前线程的场景。

实现原理

使用Zephyr的struct k_timer API。初始化定时器时绑定到期回调函数;每次调用timer_delay_start()时,先检查上一次延时是否已完成(防止重入),然后启动单次定时器。定时器到期后,内核在系统时钟中断上下文中调用回调,置位完成标志。调用方可通过timer_delay_is_done()轮询标志位,或在回调中直接处理后续逻辑。

完整代码

#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>

/* 定时器回调函数:在系统时钟中断上下文中执行 */
static void timer_expiry_fn(struct k_timer *timer_id)
{
    printk("Timer expired at tick: %lld\n", k_uptime_get());
}

/* 定义并初始化内核定时器对象 */
static struct k_timer my_timer;

/* 启动一次非阻塞延时 */
static void start_nonblocking_delay(int32_t delay_ms)
{
    /* 若上一次延时尚未完成,先停止并重置 */
    if (k_timer_remaining_get(&my_timer) > 0) {
        k_timer_stop(&my_timer);
        printk("Previous timer still active, restarted.\n");
    }
    /* 启动单次定时器,延时单位为毫秒 */
    k_timer_start(&my_timer, K_MSEC(delay_ms), K_NO_WAIT);
}

/* 查询延时是否已完成(返回1表示完成/未启动,0表示仍在计时) */
static int is_delay_complete(void)
{
    return (k_timer_status_get(&my_timer) == 0) ? 1 : 0;
}

/* 演示线程:发起非阻塞延时后,线程可继续执行其他工作 */
void timer_demo_thread(void *arg1, void *arg2, void *arg3)
{
    ARG_UNUSED(arg1);
    ARG_UNUSED(arg2);
    ARG_UNUSED(arg3);

    k_timer_init(&my_timer, timer_expiry_fn, NULL);

    printk("Demo thread started at tick: %lld\n", k_uptime_get());

    /* 发起500ms非阻塞延时 */
    start_nonblocking_delay(500);

    /* 延时未阻塞线程,此处模拟其他工作 */
    printk("Delay started, thread continues. Doing other work...\n");
    k_sleep(K_MSEC(100)); /* 模拟其他耗时操作 */

    /* 检查延时是否已完成 */
    if (is_delay_complete()) {
        printk("Delay already completed.\n");
    } else {
        printk("Delay still pending, waiting for completion...\n");
        /* 阻塞等待定时器完成(仅用于演示,实际应用中可做其他事) */
        k_timer_status_sync(&my_timer);
        printk("Delay completed after sync wait.\n");
    }

    /* 再次启动一个200ms延时,验证可重复使用 */
    start_nonblocking_delay(200);
    k_sleep(K_MSEC(250)); /* 等待超过延时时间 */
    printk("Second delay completed.\n");
}

/* 线程定义 */
K_THREAD_DEFINE(timer_demo_tid, 1024,
                timer_demo_thread, NULL, NULL, NULL,
                5, 0, 0);

构建与运行

  1. 将上述代码保存为main.c,放入Zephyr应用目录(如app/src/)。
  2. 在prj.conf中添加(若使用printk需启用):
    CONFIG_PRINTK=y
  3. 构建并运行(以qemu_cortex_m3为例):
    west build -b qemu_cortex_m3 app
    west build -t run

验证方法

运行后观察串口输出,应看到以下时序特征:

  • “Demo thread started”后立即打印“Delay started, thread continues”,证明start_nonblocking_delay()未阻塞线程;
  • 约100ms后(线程模拟工作结束),若定时器尚未到期(500ms未到),会打印“Delay still pending”;随后调用k_timer_status_sync()阻塞等待,约400ms后定时器到期,回调打印“Timer expired”,然后同步等待返回并打印“Delay completed after sync wait”;
  • 第二次启动200ms延时后,线程睡眠250ms,期间定时器到期打印“Timer expired”,随后打印“Second delay completed”。

若输出顺序与上述一致,且两次“Timer expired”的时间戳分别比对应启动时间晚约500ms和200ms,则说明非阻塞延时功能正确实现。

关键说明

  • 定时器回调运行在系统时钟中断上下文,不能调用阻塞API(如k_sleep、k_timer_status_sync),本例回调仅打印时间戳,符合约束。
  • k_timer_status_get()返回定时器剩余到期前的状态:返回0表示定时器已到期或未启动,返回非0表示仍在计时。本例利用该返回值判断延时是否完成。
  • 若需在延时完成后执行更复杂的处理(如唤醒线程),可在回调中调用k_sem_give或k_msgq_put等中断安全API,而非直接处理业务逻辑。