周期定时器
使用技术:OSAL 周期定时器、GPIO 驱动
学习目标
- 掌握
osal_timer结构体的字段含义(handler、data、interval)和初始化顺序 - 掌握
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);
handler和data在init时被内核记录下来,之后修改结构体字段不会同步到内核。interval例外——每次start时重新读取。
涉及 API
| API | 谁调用 | 用途 | 头文件 |
|---|---|---|---|
osal_timer_init(osal_timer *timer) |
入口 | 初始化定时器(需先设 handler 和 data) |
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(任务初始阻塞) |
程序运行流程:
- 配置
osal_timer结构体(handler→data→init→interval) osal_timer_start()启动- 每 1000ms 触发一次回调 → 翻转 LED(简单模式)/ 通知任务(进阶模式)
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
案例操作指导
第一步:编译
第二步:烧录
第三步:验证
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 等复杂操作时用)。