Skip to content

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) example applications/peripherals/demo_timer; the code can be found directly in your local SDK.

🎯Page GoalTrigger an interrupt every 100us with the hardware timer, toggle IO3 in the callback to output a square wave, and learn timer init, start and callback writing.
🧰Prerequisites① Ai-WB2 development board, logic analyzer (or oscilloscope) ② Environment set up per [SDK Installation](../sdk/sdk_intro) and [GPIO Output (Blink LED)](./gpio_led) completed.
🔗RelatedDelay approach: [System Control - Software Timer](../system/soft_timer); GPIO output: [GPIO Output (Blink LED)](./gpio_led).

Hardware Wiring

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.

Enter the Example Project

Open a terminal and enter the official demo_timer example project directory:

cd ~/Ai-Thinker-WB2/applications/peripherals/demo_timer

Note: cd is the “change directory” command; this enters the demo_timer example project. All subsequent make build and make flash commands must run inside this directory first.

Write the Code

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 the Project

Build in the project directory:

make -j8

Note: make is the “build” command, turning code into firmware (a program file) the board can run; -j8 builds with 8 parallel cores, faster.

On success a firmware build_out/demo_timer.bin is generated.

Flash the Firmware

Keep the board connected via USB, confirm the serial device, and flash:

make flash p=/dev/ttyUSB0 b=921600

Note: make flash is the “flash” command, writing the compiled firmware into the board’s chip. After p= comes the serial device (change it to your computer’s actual port — check with ls /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.

Run and Verify

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.

img

💡 Change period to 1000000 (1 second) and connect an LED, and you’ll see the LED blink once per second — compared to vTaskDelay blocking 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_t struct pointer, required. Key fields: config.period (period, unit microseconds us, 100 here), config.reload_mode (values: TIMER_RELOAD_PERIODIC auto-reload periodic trigger / TIMER_RELOAD_ONCE single trigger), config.cb (expiry callback function pointer, shaped void cb(void *arg)), config.arg (callback argument, pass NULL if 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_t struct pointer (needs hosal_timer_init first)

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_t struct 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_t struct 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 uses 3
  • pullup: enable internal pull-up, values: 1 enable / 0 disable (push-pull output needs no pull-up, pass 0)
  • pulldown: enable internal pull-down, values: 1 enable / 0 disable

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 as bl_gpio_enable_output (0~22)
  • value: output level, values: 1 high (3.3V) / 0 low (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
c
#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.

Released under the MIT License. Build Time 2026-09-11 14:52:23