Overview
A software timer (in plain words: an "alarm clock" inside the program — at the set time it automatically runs the task you arranged) is built on the system clock (FreeRTOS software timer; FreeRTOS is the embedded operating system the board uses, in plain words: the chip's built-in "heartbeat" that beats a fixed number of times per second, and programs count time with it), the most commonly used delay method in task scheduling — suited to periodic polling (in plain words: checking a state at fixed intervals), timeout judgment, heartbeat detection and more. Unlike hardware timers (HWT, real timing circuits), software timers don't occupy hardware peripherals — you can create many and start/stop them anytime. This tutorial demonstrates creating 3 software timers, periodic triggering, stopping, deleting and period changing.
In plain words: a software timer is like the alarm clock in your phone — you can set "ring every 5 minutes" (periodic), or "ring only once" (one-shot), and cancel or reschedule anytime. "Timing" in a program works the same: set the time, and when it's reached the system automatically runs the code you prepared (the callback, in plain words: a function you wrote in advance — when the alarm rings, the system calls it automatically, just like an alarm ringing on its own).
This tutorial is based on the official Ai-Thinker SDK (Ai-Thinker-Open/Ai-Thinker-WB2, version
release_bl_iot_sdk_1.6.40) exampleapplications/system/soft_timer; the code can be found directly in your local SDK.
Open a terminal and enter the official soft_timer example project directory:
cd ~/Ai-Thinker-WB2/applications/system/soft_timer
Note:
cdis the “change directory” command and~means your user home directory. This enters the soft_timer example project; all subsequentmakecommands must run in this directory. If it saysNo such file or directory, the path is wrong — see the FAQ at the end.
Open soft_timer/main.c. The full code for this step has been moved to the end of this page:
📜 Full Code — in the “Full Code” section below, collapsed by default — click to expand, identical to the official example (
applications/system/soft_timer/soft_timer/main.c).
Code highlights:
| Code | Purpose |
|---|---|
bl_os_timer_create(cb, NULL) |
Create a timer and bind the callback; without creating there’s no “alarm” available |
bl_os_timer_start_periodic(timer, sec, nsec) |
Periodic start: fires repeatedly at the interval; without it the timer is just a “decoration” that never rings |
bl_os_timer_start_once(timer, sec, nsec) |
One-shot start: rings once then stops; for reminding only once |
bl_os_timer_stop(timer, BL_OS_NO_WAITING) |
Stop the timer; without stopping it keeps ringing — the demo’s chained timing needs it stopped |
bl_os_timer_delete(timer, BL_OS_NO_WAITING) |
Delete the timer and free resources; not deleting a finished timer wastes memory |
The chained timing of the three timers:
| Timer | Period | Action |
|---|---|---|
| timer1 | 100ms | Prints timer1_cb:n at every trigger (n accumulates) |
| timer2 | 1000ms | After one trigger: delete timer2 + stop timer1 |
| timer3 | 3000ms | One-shot trigger: restart timer1 periodically at 1000ms |
💡 The timeout is split into
seconds + nanoseconds:delay_ms / 1000is the seconds,(delay_ms % 1000) * 1e6is the nanoseconds.BL_OS_NO_WAITINGmeans don’t block waiting for the internal lock.
Build in the project directory:
make -j8
Note:
makeis the “build” command, translating the source code into machine code the board can run;-j8builds with 8 parallel cores, faster.
On success a firmware build_out/soft_timer.bin is generated (firmware: the program burned into the board after compilation, like the board’s “operating system + your program”).
Keep the board connected via USB, confirm the serial device, and flash:
make flash p=/dev/ttyUSB0 b=921600
Note:
make flashis the “flash” command, writing the compiled firmware into the chip (flashing: the process of writing a program into the chip);p=/dev/ttyUSB0is the serial device — change it to your computer’s actual port (likeCOM3on Windows),b=921600is the flash baud rate (transfer speed).
⏳ During flashing, press and hold the EN button on the board when prompted to enter download mode; wait for the progress bar to complete — that means the flash succeeded. If it keeps waiting or reports the serial port can’t open, see the FAQ at the end.
After flashing, the board automatically restarts and runs. Open a serial assistant (baud rate 921600) and observe the logs:
timer1_cb:1
timer1_cb:2
... (prints every 100ms, lasting 1 second)
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
... (now prints every 1000ms)
Log pattern verification:
| Phase | Observation |
|---|---|
| 0~1s | timer1 prints every 100ms (~9 times) |
| 1s | timer2 fires: deletes itself, stops timer1 |
| 3s | timer3 one-shot fires: restarts timer1 at 1000ms |
| after 3s | timer1 prints every 1000ms, visibly slower |
💡 timer2 doesn’t fire after deletion and timer3 doesn’t fire after its one-shot — verifying the behavior difference of
bl_os_timer_stop/bl_os_timer_delete/start_once.
✅ Expected result: first
timer1_cb:1~timer1_cb:9(~every 100ms),timer2_cbat 1s,timer3_cbat 3s after whichtimer1_cbbecomes ~every 1s — verified. If only a fewtimer1_cblines print and the log never changes, the timer chaining didn’t work — see the FAQ at the end.
API Summary for This Tutorial
bl_os_timer_create(cb, arg)
Creates a timer, binds the callback function and argument, and returns a timer handle.
Parameters:
cb: callback function pointer, shapedvoid cb(void *arg), called on expiry, requiredarg: callback argument pointer (passed through to the callback), passNULLif none
Return: timer handle (bl_os_timer_t) on success; NULL on failure
bl_os_timer_start_periodic(timer, sec, nsec)
Starts the timer with repeating periodic triggers (period = seconds + nanoseconds).
Parameters:
timer: handle returned bybl_os_timer_createsec: seconds part of the period, values: any non-negative integer, e.g.1(1 second)nsec: nanoseconds part of the period, values:0~999999999(0is fine when whole seconds suffice)
Return: 0 on success; negative error code on failure
bl_os_timer_start_once(timer, sec, nsec)
Starts the timer; after expiry it fires once then stops automatically.
Parameters:
timer: timer handlesec: seconds part of the delaynsec: nanoseconds part of the delay
Return: 0 on success; negative error code on failure
bl_os_timer_stop(timer, wait)
Stops the timer, no more triggers (a later start_* can restart it).
Parameters:
timer: timer handlewait: whether to wait for the timer task to finish processing the stop command, values:BL_OS_NO_WAITING(no wait, returns immediately) /BL_OS_WAITING(wait)
Return: 0 on success; negative error code on failure
bl_os_timer_delete(timer, wait)
Deletes the timer and frees the occupied resources (call when no longer used).
Parameters:
timer: timer handlewait: whether to wait, values:BL_OS_NO_WAITING/BL_OS_WAITING
Return: 0 on success; negative error code on failure
blog_info(fmt, ...)
Outputs an INFO-level log (UART0, filtered by level).
Parameters:
fmt: format string, same usage asprintf, required...: variadic args matchingfmtplaceholders, optional
Return: none
Full Code
Below is the complete soft_timer/main.c source, identical to the official example (applications/system/soft_timer/soft_timer/main.c):
📜 Click to expand the full soft_timer/main.c code
#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
}FAQ & Troubleshooting
⚠️ Timer callback doesn't fire
Cause: software timers are scheduled by the system timer task; a high-priority task blocks it too long, or no blog_set_level_log_component level was set so logs get filtered out
Fix: confirm the component level for the callback's prints is set correctly (soft_timer in this example); avoid business tasks occupying the CPU too long
⚠️ Period doesn't match real time (visibly slow)
Cause: software timer precision depends on the system tick (default tick) and jitters when tasks are busy
Fix: for µs-level precise timing use the hardware timer (see Timer (Hardware)); software timers suit ms-level applications
⚠️ Crashes after calling bl_os_timer_delete inside a timer callback
Cause: the timer whose callback is currently running gets deleted directly, conflicting with the FreeRTOS timer task context
Fix: follow the official approach — delete other timers in the callback (e.g. timer2's callback deleting timer2 itself is officially verified to work); for cross-task deletion, defer via a queue
⚠️ Multiple timers sharing one callback argument corrupts data
Cause: the callback's second parameter (void *data) points to a shared variable
Fix: allocate a separate argument struct for each timer, avoiding shared global data
⚠️ Serial port won't open / /dev/ttyUSB0 not found
Cause: USB-to-serial driver not installed, port occupied, or (on Linux) no access permission
Fix: on Linux confirm the device is recognized with lsusb, run sudo chmod 666 /dev/ttyUSB0 or add your user to the dialout group and retry; on Windows check the COM port in Device Manager and install the CH340/CP210x driver
⚠️ Flashing stuck waiting / chip not found
Cause: download mode wasn't entered, the cable only charges and can't transfer data, or the baud rate is wrong
Fix: press and hold EN during flashing until the progress bar appears; try a data cable; confirm p= port and b=921600 are correct
⚠️ cd reports No such file or directory / no Makefile found
Cause: make ran outside the example project directory, or the SDK install path differs from the tutorial
Fix: cd ~/Ai-Thinker-WB2/applications/system/soft_timer first, then run make; if ~/Ai-Thinker-WB2 doesn't exist, find the SDK with find ~ -name "Ai-Thinker-WB2"
Self-Check
The serial prints per the pattern "100ms × 9 → stop → 1000ms after 3s" — the software timer is verified.

