Overview
The timer is one of the most commonly used peripherals in embedded development: LED blinking, button scanning, timeout checks, periodic data collection — all rely on it. This tutorial uses a hardware timer periodic interrupt to toggle a GPIO output every 100us, producing a square wave.
In plain words: the timer is like an "alarm clock" inside the board. You set the time (say 100 microseconds); when the time comes, the alarm automatically "rings" (triggers an interrupt), interrupting whatever is being done to run the small task you arranged (toggling the pin level), then continues timing. This tutorial uses this alarm to toggle the pin every 100 microseconds, outputting a square wave.
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/peripherals/demo_timer; the code can be found directly in your local SDK.
Wire per the official example (see SDK applications/peripherals/demo_timer/README.md):
| Ai-WB2 Pin | Connected To |
|---|---|
| IO3 | Logic analyzer probe |
💡 The example outputs a 100us-period square wave on IO3; a logic analyzer or oscilloscope shows the waveform directly. Without instruments, connect an LED to IO3 to observe the blinking rhythm.
Open a terminal and enter the official demo_timer example project directory:
cd ~/Ai-Thinker-WB2/applications/peripherals/demo_timer
Note:
cdis the “change directory” command; this enters the demo_timer example project. All subsequentmakebuild andmake flashcommands must run inside this directory first.
Open demo_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/peripherals/demo_timer/demo_timer/main.c).
Code highlights:
| Code | Purpose |
|---|---|
period = 100 |
Timer period 100us (unit is microseconds, not milliseconds); wrong unit means a 1000× difference in period |
reload_mode = TIMER_RELOAD_PERIODIC |
Auto-reload mode: after ringing once it automatically keeps timing and triggers continuously; wrong choice means it rings only once |
timer_cb |
Timer interrupt callback (the function that runs automatically when the time comes): toggles IO3 level on each trigger; without it there’s no waveform |
hosal_timer_init(&timer0) |
Initialize the timer; without it the later start does nothing |
hosal_timer_start(&timer0) |
Start the timer (the alarm begins ticking); without it it never triggers |
Build in the project directory:
make -j8
Note:
makeis the “build” command, turning code into firmware (a program file) the board can run;-j8builds with 8 parallel cores, faster.
On success a firmware build_out/demo_timer.bin is generated.
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 board’s chip. Afterp=comes the serial device (change it to your computer’s actual port — check withls /dev/ttyUSB*);b=is 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.
After flashing, the board automatically restarts; the logic analyzer observes a 100us-period square wave on IO3 (toggling every 50us, 50% duty cycle), matching the official example’s waveform.

💡 Change
periodto1000000(1 second) and connect an LED, and you’ll see the LED blink once per second — compared tovTaskDelayblocking delays, the timer doesn’t occupy the CPU and its timing precision is guaranteed by hardware.
Seeing a 100us-period square wave on IO3 from the logic analyzer means success; if there’s no waveform or the frequency is wrong, check the “FAQ & Troubleshooting” section at the end.
API Summary for This Tutorial
hosal_timer_init(tim)
Configures the hardware timer per the period, reload mode, callback, etc. in the dev struct (100us periodic trigger in this tutorial).
Parameters:
tim:hosal_timer_dev_tstruct pointer, required. Key fields:config.period(period, unit microseconds us,100here),config.reload_mode(values:TIMER_RELOAD_PERIODICauto-reload periodic trigger /TIMER_RELOAD_ONCEsingle trigger),config.cb(expiry callback function pointer, shapedvoid cb(void *arg)),config.arg(callback argument, passNULLif none)
Return: 0 on success; negative error code on failure
hosal_timer_start(tim)
Starts timing; triggers the callback on expiry (repeats automatically in periodic mode).
Parameters:
tim:hosal_timer_dev_tstruct pointer (needshosal_timer_initfirst)
Return: 0 on success; negative error code on failure
hosal_timer_stop(tim)
Stops timing; no more callbacks (start again to restart timing).
Parameters:
tim:hosal_timer_dev_tstruct pointer
Return: 0 on success; negative error code on failure
hosal_timer_finalize(tim)
Unregisters the timer and frees the occupied hardware resources (call before no longer using it).
Parameters:
tim:hosal_timer_dev_tstruct pointer
Return: 0 on success; negative error code on failure
bl_gpio_enable_output(pin, pullup, pulldown)
Configures the specified pin as push-pull output, capable of driving loads like LEDs and buzzers (used here to output the square wave).
Parameters:
pin: pin number, values:0~22(GPIO0~GPIO22); this tutorial uses3pullup: enable internal pull-up, values:1enable /0disable (push-pull output needs no pull-up, pass0)pulldown: enable internal pull-down, values:1enable /0disable
Return: 0 on success; negative error code on failure
bl_gpio_output_set(pin, value)
Outputs a high or low level on a pin already configured as output (toggling IO3 in the timer callback in this tutorial).
Parameters:
pin: pin number, values asbl_gpio_enable_output(0~22)value: output level, values:1high (3.3V) /0low (0V)
Return: 0 on success; negative error code on failure
Full Code
Below is the complete demo_timer/main.c source, identical to the official example (applications/peripherals/demo_timer/demo_timer/main.c):
📜 Click to expand the full demo_timer/main.c code
#include <stdio.h>
#include <FreeRTOS.h>
#include <task.h>
#include <hosal_timer.h>
#include <bl_gpio.h>
#include <blog.h>
static void timer_cb(void *arg)
{
static int i = 0;
if (i % 2) {
bl_gpio_output_set(3, 0);
} else {
bl_gpio_output_set(3, 1);
}
i++;
}
int main(void)
{
bl_gpio_enable_output(3, 1, 0);
static hosal_timer_dev_t timer0 = {
.config = {
.arg = NULL,
.cb = timer_cb,
.period = 100, /// 100us
.reload_mode = TIMER_RELOAD_PERIODIC,
},
.port = 0,
};
hosal_timer_init(&timer0);
hosal_timer_start(&timer0);
return 0;
}FAQ & Troubleshooting
⚠️ Waveform frequency doesn't match expectations
Cause: period unit is us; converting with ms wrongly gives a 1000× period difference
Fix: for 1ms timing write period = 1000; period = 2×period, so for a 200us square wave set period to 100
⚠️ The timer triggers only once then stops
Cause: reload_mode was wrongly set to TIMER_RELOAD_ONCE (single-shot mode)
Fix: use TIMER_RELOAD_PERIODIC when periodic triggering is needed
⚠️ Time-consuming operations in the callback distort the waveform
Cause: running printf or other time-consuming operations in the callback, occupying more time than the period
Fix: only do lightweight operations in the callback (set a flag, toggle IO); put heavy processing in the main loop; use blog_info (non-blocking LOG) if logging is needed
⚠️ Flashing keeps waiting, the progress bar doesn't move
Cause: download mode wasn't entered, or the cable only charges and can't transfer data
Fix: press and hold EN to enter download mode when prompted; try a Type-C cable that can transfer data and retry
⚠️ Serial device not found or permission denied
Cause: /dev/ttyUSB0 doesn't exist or permissions are insufficient on Linux; USB-to-serial driver not installed on Windows
Fix: on Linux confirm the device with ls /dev/ttyUSB*, for permissions run sudo usermod -aG dialout $USER then log in again; on Windows install the driver in Device Manager and confirm the COM number
Self-Check
The logic analyzer observes a 100us-period square wave on IO3 — the timer is verified.

