跳转至

周期定时器

使用技术:OSAL 周期定时器、GPIO 驱动

前置阅读:多任务并发信号量同步 — 定时器回调通知任务的标准模式需要信号量

学习目标

  • 掌握 osal_timer 结构体的字段含义(handlerdatainterval)和初始化顺序
  • 掌握 osal_timer_init()osal_timer_start()osal_timer_stop() 的标准调用链
  • 理解定时器回调的运行上下文限制——不能阻塞、不能调 SLE API
  • 掌握"定时器回调 → sem_up 通知任务"的经典模式,将复杂业务从回调下沉到任务
  • 能够在 WS63 上用周期定时器替代 osal_msleep() 循环驱动周期性任务

基本概念

定时器回调的上下文限制

定时器回调运行在定时器软中断上下文中,和 ISR 类似——不能阻塞。

操作 回调中能做? 说明
uapi_gpio_toggle() 非阻塞硬件操作
osal_sem_up() 非阻塞,通知任务的标准方式
osal_msg_queue_write_copy(NO_WAIT) 非阻塞写队列
osal_sem_down() 不能 可能阻塞,死机
osal_msleep() 不能 阻塞,死机
printf() 不能 可能阻塞且极慢
调 SLE/WiFi API 不能 协议栈 API 可能阻塞

核心原则:回调越短越好(< 100μs)。耗时的活(打印、协议处理、复杂计算)全部推到任务中做。

"回调通知任务"模式

这是定时器编程中最常用的模式——回调做最小的事(发信号),任务做复杂的事。

sequenceDiagram
    participant T as 定时器
    participant CB as 回调
    participant S as 信号量
    participant TK as 任务

    T->>CB: 定时器到期,触发回调
    Note over CB: 只做非阻塞操作: sem_up
    CB->>S: osal_sem_up — 通知任务
    Note over CB: 回调返回,< 10μs

    S->>TK: 唤醒任务
    Note over TK: 任务可以做任何事:<br/>printf、调 SLE API、<br/>osal_msleep...
    TK->>TK: 执行复杂业务
    TK->>S: osal_sem_down — 继续等下次通知

对比错误做法——把所有逻辑放在回调中:

flowchart LR
    A[回调中 printf] --> B[死机或卡死]
    C[回调中 osal_msleep] --> D[定时器任务阻塞<br/>后续定时器全部失效]

配置顺序的陷阱

osal_timer 的字段有严格的设置时序——搞错顺序是新手最常犯的错误:

/* 正确顺序 */
osal_timer timer = {0};
timer.handler = my_callback;   // 第1步: 必须在 init 之前
timer.data = 0;                // 第2步: 必须在 init 之前
osal_timer_init(&timer);       // 第3步: init 后 handler/data 不能再改

timer.interval = 1000;         // 第4步: interval 可以在每次 start 前改
osal_timer_start(&timer);      // 第5步: 启动
/* 错误顺序——handler 在 init 之后设置,无效! */
osal_timer timer = {0};
osal_timer_init(&timer);       // 先 init
timer.handler = my_callback;   // 后设 handler —— 无效!回调永远不会被调用
osal_timer_start(&timer);

handlerdatainit 时被内核记录下来,之后修改结构体字段不会同步到内核。interval 例外——每次 start 时重新读取。

涉及 API

API 谁调用 用途 头文件
osal_timer_init(osal_timer *timer) 入口 初始化定时器(需先设 handlerdata osal_timer.h
osal_timer_start(osal_timer *timer) 入口 启动周期定时器,按 timer.interval 周期触发 osal_timer.h
osal_timer_stop(osal_timer *timer) 入口/任务 停止定时器 osal_timer.h

osal_timer 结构体定义:{ void *timer; void (*handler)(unsigned long); unsigned long data; unsigned int interval; }

案例说明

做什么

最简单场景:周期定时器每 1 秒触发回调翻转 LED,无需任务参与。进阶模式:回调中 sem_up 通知任务,任务中打印日志。

规格与功能

规格项 简单模式 进阶模式
定时器周期 1000ms 1000ms
回调动作 uapi_gpio_toggle(LED_PIN) osal_sem_up(&g_sem)
任务动作 无任务 sem_down → printf
回调耗时 < 10μs < 10μs
信号量初值 0(任务初始阻塞)

程序运行流程:

  1. 配置 osal_timer 结构体(handlerdatainitinterval
  2. osal_timer_start() 启动
  3. 每 1000ms 触发一次回调 → 翻转 LED(简单模式)/ 通知任务(进阶模式)
  4. osal_timer_stop() 停止

案例流程

sequenceDiagram
    participant E as 入口
    participant TM as 周期定时器
    participant CB as 回调
    participant G as GPIO

    E->>TM: timer.handler = cb<br/>timer.interval = 1000
    E->>TM: osal_timer_init
    E->>TM: osal_timer_start

    loop 每 1000ms
        TM->>CB: 到期,调用回调
        CB->>G: uapi_gpio_toggle(LED_PIN)
        Note over CB: 回调返回,< 10μs
    end

案例操作指导

第一步:编译

fbb build ws63-liteos-app

第二步:烧录

第三步:验证

LED 以精确 1 秒间隔闪烁(与 osal_msleep 循环相比,定时器驱动的闪烁速率更稳定,不受任务负载影响)。

关键配置

参数 说明
interval 1000ms start 前设置,每次启动可改不同值
handler 设置时机 init 之前 init 之后改无效——回调永远不会触发
data 设置时机 init 之前 同 handler,传给回调的参数
回调耗时 < 100μs 只做 GPIO 翻转或 sem_up,绝不做阻塞操作
进阶模式信号量初值 0 任务初始阻塞,等第一个定时器触发

代码详解

最简周期回调——无任务参与

#include "osal_timer.h"
#include "driver/gpio.h"

static osal_timer g_led_timer;

/* 回调函数——运行在定时器上下文,不能阻塞 */
static void led_timer_callback(unsigned long data)
{
    (void)data;
    uapi_gpio_toggle(LED_PIN);
    /* 只有 GPIO 翻转,< 10μs,绝对安全 */
}

/* 入口中初始化 */
static void app_entry(void)
{
    uapi_gpio_init();
    uapi_gpio_set_dir(LED_PIN, GPIO_DIRECTION_OUTPUT);

    /* 第1步: 设 handler 和 data —— 必须在 init 之前 */
    g_led_timer.handler = led_timer_callback;
    g_led_timer.data = 0;

    /* 第2步: init */
    osal_timer_init(&g_led_timer);

    /* 第3步: 设 interval —— 可以在每次 start 前改 */
    g_led_timer.interval = 1000;  // 1000ms = 1s

    /* 第4步: 启动 */
    osal_timer_start(&g_led_timer);
    /* LED 开始每秒翻转一次 */
}
app_run(app_entry);

回调通知任务的进阶模式

当回调中需要做复杂操作(如 printf)时,把活交给任务:

#include "osal_semaphore.h"

static osal_timer g_timer;
static osal_semaphore g_sem;

/* 回调 —— 只做最小的事 */
static void timer_callback(unsigned long data)
{
    (void)data;
    osal_sem_up(&g_sem);   // 非阻塞,通知任务"时间到了"
    /* < 10μs,绝对安全 */
}

/* 任务 —— 做所有复杂的事 */
static int worker_task_handler(void *data)
{
    (void)data;
    int count = 0;
    while (1) {
        osal_sem_down(&g_sem);                 // 阻塞等通知
        count++;
        printf("timer fired: %d\n", count);    // 任务中安全 printf
        /* 这里可以做任何事:调 SLE API、复杂计算... */
    }
    return 0;
}

static void app_entry(void)
{
    osal_sem_init(&g_sem, 0);  // 初值 0,任务初始阻塞

    g_timer.handler = timer_callback;
    g_timer.data = 0;
    osal_timer_init(&g_timer);
    g_timer.interval = 1000;
    osal_timer_start(&g_timer);

    /* 创建工作任务 */
    osal_task *task = osal_kthread_create(
        (osal_kthread_handler)worker_task_handler,
        NULL, "TimerWorker", 4096);
    osal_kthread_set_priority(task, OSAL_TASK_PRIORITY_LOW);
}

对比两种模式:简单模式回调直接翻转 GPIO(不需要任务),进阶模式回调 sem_up + 任务处理(需要 printf 等复杂操作时用)。