概述
软件定时器(大白话:程序里的"闹钟",到点自动执行你安排的事)基于系统时钟(FreeRTOS 软件定时器,FreeRTOS 是开发板用的嵌入式操作系统,大白话:芯片内置的"心跳",每秒跳动固定次数,程序靠它计时)实现,是任务调度中最常用的延时手段,适合周期轮询(大白话:每隔固定时间检查一次状态)、超时判断、心跳检测等场景。与硬件定时器(HWT,真实存在的计时电路)不同,软件定时器不占用硬件外设,可创建多个、随时启停。本教程演示 3 个软件定时器的创建、周期触发、停止、删除与周期修改。
用大白话讲:软件定时器就像你手机里的闹钟——可以定"每 5 分钟响一次"(周期定时),也可以定"只响一次"(一次性定时),还能随时取消闹钟或改时间。程序里"定时"也一样:设置好时间,到点系统自动执行你安排好的代码(回调函数,大白话:你提前写好的函数,闹钟一响系统自动帮你调用它,就像闹钟到点自动响铃)。
本教程基于安信可官方 SDK(Ai-Thinker-Open/Ai-Thinker-WB2,版本
release_bl_iot_sdk_1.6.40)的官方示例applications/system/soft_timer编写,代码可在本地 SDK 中直接找到。
打开终端,进入官方 soft_timer 示例工程目录:
cd ~/Ai-Thinker-WB2/applications/system/soft_timer
说明:
cd是"进入目录"的命令,~表示你的用户主目录。这条命令进入 soft_timer 示例工程,后面所有make命令都要在这个目录里执行;如果提示No such file or directory(没有这个目录),说明路径不对,见文末 FAQ。
打开 soft_timer/main.c,本步完整代码已移至文末,见:
📜 完整代码 — 位于本页「完整代码」章节,默认折叠,点击展开,与官方示例(
applications/system/soft_timer/soft_timer/main.c)完全一致。
代码要点:
| 代码 | 作用 |
|---|---|
bl_os_timer_create(cb, NULL) |
创建定时器并绑定回调;不创建就没有"闹钟"可用 |
bl_os_timer_start_periodic(timer, sec, nsec) |
周期启动:到点反复触发;不做这个定时器只是"摆设"不会响 |
bl_os_timer_start_once(timer, sec, nsec) |
一次性启动:响一次就停;适合只提醒一次的场景 |
bl_os_timer_stop(timer, BL_OS_NO_WAITING) |
停止定时器;不停它就会一直响,演示联动时序需要停 |
bl_os_timer_delete(timer, BL_OS_NO_WAITING) |
删除定时器释放资源;不用了不删会白白占用内存 |
三个定时器的联动时序:
| 定时器 | 周期 | 动作 |
|---|---|---|
| timer1 | 100ms | 每次触发打印 timer1_cb:n(n 累加) |
| timer2 | 1000ms | 触发一次后:删除 timer2 + 停止 timer1 |
| timer3 | 3000ms | 一次性触发后:以 1000ms 重新周期启动 timer1 |
💡 超时时间拆分为
秒 + 纳秒两部分:delay_ms / 1000为秒,(delay_ms % 1000) * 1e6为纳秒。BL_OS_NO_WAITING表示不阻塞等待内部锁。
在工程目录执行编译:
make -j8
说明:
make是"编译工程"的命令,把源代码翻译成开发板能运行的机器码;-j8表示用 8 个核心并行编译,更快。
编译成功后生成固件 build_out/soft_timer.bin(固件:编译后烧进开发板的程序,相当于开发板的"操作系统+你的程序")。
开发板保持 USB 连接,确认串口设备号后执行烧录:
make flash p=/dev/ttyUSB0 b=921600
说明:
make flash是"烧录"命令,把编译好的固件下载进芯片(烧录:把程序写进芯片的过程);p=/dev/ttyUSB0是串口设备号,要换成你电脑上实际的串口(Windows 下形如COM3),b=921600是烧录波特率(传输速度)。
⏳ 烧录过程中按提示长按开发板 EN 键进入下载模式,等待进度条完成即烧录成功。若一直卡在等待或报串口打不开,见文末 FAQ。
烧录完成后开发板自动重启运行,打开串口助手(波特率 921600)观察日志:
timer1_cb:1
timer1_cb:2
... (每 100ms 打印一次,持续 1 秒)
timer1_cb:9
timer2_cb:delete timer2 and stop timer1
timer3_cb:start timer1 again and change timer cycle
timer1_cb:10
timer1_cb:11
... (改为每 1000ms 打印一次)
日志规律验证:
| 阶段 | 观察点 |
|---|---|
| 0~1s | timer1 每 100ms 打印一次(约 9 次) |
| 1s | timer2 触发:删除自己、停止 timer1 |
| 3s | timer3 一次性触发:以 1000ms 周期重启 timer1 |
| 3s 后 | timer1 每 1000ms 打印一次,频率明显变慢 |
💡 timer2 删除后不再触发、timer3 一次性触发后不再触发,验证了
bl_os_timer_stop/bl_os_timer_delete/start_once的行为差异。
✅ 预期结果:先看到
timer1_cb:1~timer1_cb:9(约 100ms 一次),1 秒处出现timer2_cb,3 秒处出现timer3_cb后timer1_cb变为约 1 秒一次,即验证通过。若只打印几行timer1_cb后日志不再变化,说明定时器联动没生效,见文末 FAQ。
代码执行流程
例程从启动到运行的完整流程如下(图中的循环箭头表示反复执行):
本文 API 汇总
bl_os_timer_create(cb, arg)
创建定时器并绑定回调函数与参数,返回定时器句柄。
参数:
cb:回调函数指针,形如void cb(void *arg),到期时调用,必填arg:回调参数指针(透传给回调),无参传NULL
返回值:成功返回定时器句柄(bl_os_timer_t);失败返回 NULL
bl_os_timer_start_periodic(timer, sec, nsec)
启动定时器并周期重复触发(周期 = 秒 + 纳秒)。
参数:
timer:bl_os_timer_create返回的句柄sec:周期秒部分,可选值:任意非负整数,如1(1 秒)nsec:周期纳秒部分,可选值:0~999999999(0即可,秒够用时)
返回值:成功返回 0;失败返回负值错误码
bl_os_timer_start_once(timer, sec, nsec)
启动定时器,到期触发一次后自动停止。
参数:
timer:定时器句柄sec:延时秒部分nsec:延时纳秒部分
返回值:成功返回 0;失败返回负值错误码
bl_os_timer_stop(timer, wait)
停止定时器,不再触发(再次 start_* 可重新开始)。
参数:
timer:定时器句柄wait:是否等待定时器任务处理完停止命令,可选值:BL_OS_NO_WAITING(不等待,立即返回)/BL_OS_WAITING(等待)
返回值:成功返回 0;失败返回负值错误码
bl_os_timer_delete(timer, wait)
删除定时器并释放占用的资源(不再使用时调用)。
参数:
timer:定时器句柄wait:是否等待,可选值:BL_OS_NO_WAITING/BL_OS_WAITING
返回值:成功返回 0;失败返回负值错误码
完整代码
以下为 soft_timer/main.c 完整源码,与官方示例(applications/system/soft_timer/soft_timer/main.c)完全一致:
📜 点击展开 soft_timer/main.c 完整代码
#include <stdio.h>
#include <string.h>
#include <FreeRTOS.h>
#include <task.h>
#include <blog.h>
#include "bl_sys.h"
#include <bl_os_hal.h>
#include "timers.h"
#include <blog.h>
struct timer_adpt *timer1;
struct timer_adpt *timer2;
struct timer_adpt *timer3;
uint32_t delay_ms;
static void timer1_cb(void *data)
{
static int cnt;
cnt++;
blog_info("timer1_cb:%d\r\n",cnt);
}
static void timer2_cb(void *data)
{
blog_info("timer2_cb:delete timer2 and stop timer1\r\n");
bl_os_timer_delete(timer2, BL_OS_NO_WAITING); //delete timer2
bl_os_timer_stop(timer1,BL_OS_NO_WAITING); //stop timer1
}
static void timer3_cb(void *data)
{
blog_info("timer3_cb:start timer1 again and change timer cycle\r\n");
delay_ms=1000; //cycle =1000ms
bl_os_timer_start_periodic(timer1,(delay_ms / 1000),((delay_ms % 1000) * 1e6));
}
void main(void)
{
blog_set_level_log_component(BLOG_LEVEL_INFO, "soft_timer");
delay_ms=100;
timer1=bl_os_timer_create(timer1_cb,NULL);
bl_os_timer_start_periodic(timer1,(delay_ms / 1000),((delay_ms % 1000) * 1e6)); // cycle =100ms,start periodic
timer2=bl_os_timer_create(timer2_cb,NULL);
delay_ms=1000;
bl_os_timer_start_periodic(timer2,(delay_ms / 1000),((delay_ms % 1000) * 1e6)); //cycle =1000ms,start periodic
timer3=bl_os_timer_create(timer3_cb,NULL);
delay_ms=3000;
bl_os_timer_start_once(timer3,(delay_ms / 1000),((delay_ms % 1000) * 1e6)); //cycle =3000ms,start once
}常见问题与踩坑提示
⚠️ 定时器回调不执行
原因:软件定时器由系统定时器任务调度,任务被高优先级任务长时间阻塞,或未 blog_set_level_log_component 设置级别导致日志被过滤
解决:确认回调打印的组件级别设置正确(本示例为 soft_timer);避免业务任务长时间占用 CPU
⚠️ 周期与实际时间不符(明显偏慢)
原因:软件定时器精度依赖系统节拍(默认 tick),且在任务繁忙时会有抖动
解决:需要 µs 级精确计时请改用硬件定时器(见 定时器(硬件));软件定时器适合 ms 级应用
⚠️ 定时器回调里调用 bl_os_timer_delete 后死机
原因:回调正在执行的定时器被直接删除,FreeRTOS 定时器任务上下文冲突
解决:参照官方做法:回调里删除其他定时器(如 timer2 回调里删 timer2 自身是被官方验证可行的);跨任务删除用队列延后处理
⚠️ 多个定时器共用一个回调参数导致数据错乱
原因:回调第二个参数(void *data)指向共享变量
解决:为每个定时器单独分配参数结构体,避免共享全局数据
⚠️ 串口打不开 / 找不到 /dev/ttyUSB0
原因:USB 转串口驱动未装、串口被占用,Linux 下还可能是没有访问权限
解决:Linux 用 lsusb 确认设备被识别,sudo chmod 666 /dev/ttyUSB0 或把用户加入 dialout 组后重试;Windows 在设备管理器查看 COM 口并安装 CH340/CP210x 驱动
⚠️ 烧录一直卡在等待 / 提示找不到芯片
原因:未进入下载模式、数据线只能充电不能传数据,或波特率不对
解决:烧录时长按 EN 键直到进度条出现;换一根能传数据的数据线;确认 p= 串口号与 b=921600 无误
⚠️ cd 报错 No such file or directory / 找不到 Makefile
原因:没进入示例工程目录就执行了 make,或 SDK 安装路径与教程不同
解决:先 cd ~/Ai-Thinker-WB2/applications/system/soft_timer 再执行 make;若 ~/Ai-Thinker-WB2 不存在,用 find ~ -name "Ai-Thinker-WB2" 找到 SDK 实际位置
运行自检
串口按「100ms×9 次 → 停止 → 3s 后变 1000ms」规律打印,即软件定时器验证通过。
遇到问题?
如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

