Skip to content

Overview ​

The previous section GPIO Input (Button Detection) used polling to detect the button, with the CPU constantly querying the level. This tutorial switches to the external interrupt approach: when the button triggers an interrupt, the callback automatically runs to toggle the LED — the CPU responds without polling. This is a common approach in embedded development.

In plain words: polling is like a doorman staring at the door to see if anyone rings the bell — tiring; an interrupt is like installing an automatic device on the doorbell — when nobody presses, the CPU does other things at ease (even sleeping); when someone rings (pin level changes), the device automatically interrupts the current work to answer the door (run the callback). This tutorial uses this "doorbell" to detect the button — each press toggles the LED once.

This tutorial is based on the official Ai-Thinker SDK (Ai-Thinker-Open/Ai-Thinker-WB2, version release_bl_iot_sdk_1.6.40) interrupt APIs. The GPIO interrupt capability comes from components/platform/hosal/include/hosal_gpio.h; the official usage can be found in applications/iot-solution/qcloud_demo (the key_irq button interrupt callback).

🎯Page GoalDetect the button with GPIO external interrupt — each press toggles the LED — and learn interrupt configuration and callback writing.
🧰Prerequisites① Ai-WB2 development board, button, LED, 330Ω current-limiting resistor, jumper wires ② Environment set up per [SDK Installation](../sdk/sdk_intro) and [GPIO Input (Button Detection)](./gpio_button) completed.
🔗RelatedPolling approach: [GPIO Input (Button Detection)](./gpio_button); GPIO basic output: [GPIO Output (Blink LED)](./gpio_led).

①
Hardware Wiring

Wiring is the same as the button detection tutorial (IO8 button, IO14 LED):

Ai-WB2 Pin Peripheral
IO14 LED anode (with a 330Ω current-limiting resistor in series); LED cathode to GND
IO8 One end of the button; the other end to 3V3
3V3 / GND Power supply

💡 This tutorial configures the button as internal pull-down input (INPUT_PULL_DOWN, reads low by default when floating), with the button’s other end to 3V3 — the moment of pressing, IO8 rises from low to high producing a rising edge (the instant the level jumps from low to high), triggering the external interrupt. This is the opposite wiring of the polling tutorial’s external pull-up — pay attention when wiring.

②
Enter the Example Project

The official SDK has no standalone external interrupt example, so this tutorial rewrites the official blink example skeleton (main/main.c + Makefile):

cd ~/Ai-Thinker-WB2/applications/get-started/blink

Note: cd is the “change directory” command; this borrows the official blink example skeleton as this tutorial’s project. All subsequent make build and make flash commands must run inside this directory first.

③
Write the Code

Replace main/main.c with the following. 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. This tutorial’s rewritten code is based on the official blink skeleton (applications/get-started/blink); the API style matches the official example’s (applications/iot-solution/qcloud_demo) key_irq usage exactly.

Code highlights:

Code Purpose
led.config = OUTPUT_PUSH_PULL Configure the LED pin as push-pull output (can output high/low); without it the LED can’t light
key.config = INPUT_PULL_DOWN Configure the button pin as internal pull-down input (low when floating); without it presses can’t be detected
hosal_gpio_irq_set(&key, HOSAL_IRQ_TRIG_POS_PULSE, key_irq, NULL) Register the interrupt: the button’s rising edge (the instant of low-to-high) triggers the key_irq callback; without it pressing does nothing
key_irq(void *arg) Interrupt callback (the function the system runs automatically when triggered); toggles the LED output level inside, giving one toggle per press
blog_info("[key irq]") Prints an interrupt log (INFO level), confirming every press triggers the interrupt

⚠️ Don’t perform blocking operations in the interrupt callback (delays, printing large logs, etc.); handle quickly and return. Use vTaskNotifyGiveFromISR() to wake a task if needed (see the official qcloud_demo’s key_irq).

④
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/blink.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; each button press toggles the LED on/off state once.

Open a serial assistant (baud rate 921600) and check the logs — each press prints one interrupt log line:

[key irq]
[key irq]
...

💡 Unlike polling, in interrupt mode the CPU stays idle/sleeping normally and only enters the callback when the button triggers — higher power efficiency and responsiveness.

Seeing “each press toggles the LED once” and the serial printing one [key irq] line per press means success; if pressing does nothing or one press prints multiple lines, check the “FAQ & Troubleshooting” section at the end.


API Summary for This Tutorial ​

hosal_gpio_init(gpio) ​

Configures the pin per the port (pin) and config (direction/mode) fields of the dev struct (used here for LED output and button input).

Parameters:

  • gpio: hosal_gpio_dev_t struct pointer, required. port selects the GPIO pin (0~22); config is the direction/mode enum, values: OUTPUT_PUSH_PULL (push-pull output) / INPUT_PULL_DOWN (internal pull-down input) / INPUT_HIGH_IMPEDANCE (high-impedance input)

Return: 0 on success; negative error code on failure

hosal_gpio_output_set(gpio, value) ​

Outputs a high or low level on a pin already configured as output.

Parameters:

  • gpio: hosal_gpio_dev_t struct pointer (port specifies the pin)
  • value: output level, values: 0 low (0V) / non-0 (e.g. 1) high (3.3V)

Return: 0 on success; negative error code on failure

hosal_gpio_irq_set(gpio, trigger, handler, arg) ​

Registers an interrupt handler for the pin; called automatically when the level change meets the trigger condition (rising edge of the button in this tutorial).

Parameters:

  • gpio: hosal_gpio_dev_t struct pointer
  • trigger: trigger type, values: HOSAL_IRQ_TRIG_POS_PULSE (rising edge) / HOSAL_IRQ_TRIG_NEG_PULSE (falling edge) / HOSAL_IRQ_TRIG_POS_LEVEL (high level) / HOSAL_IRQ_TRIG_NEG_LEVEL (low level)
  • handler: interrupt callback function pointer, shaped void handler(void *arg), runs in interrupt context, must return quickly
  • arg: callback argument pointer; pass NULL if none

Return: 0 on success; negative error code on failure

blog_info(fmt, ...) ​

Outputs an INFO-level log (UART0, filtered by the blog_set_level level).

Parameters:

  • fmt: format string, same usage as printf, required
  • ...: variadic args matching fmt placeholders, optional

Return: none

vTaskDelay(ms) ​

Suspends the current task for the specified number of milliseconds, yielding the CPU to other tasks.

Parameters:

  • ms: delay in milliseconds, values: any non-negative integer (internally converted to system ticks via pdMS_TO_TICKS)

Return: none


Full Code ​

Below is the complete rewritten main/main.c source. The official SDK has no standalone external interrupt example; this code is based on the official blink skeleton (applications/get-started/blink), and the API style matches the official example's (applications/iot-solution/qcloud_demo) key_irq usage exactly:

📜 Click to expand the full main/main.c code
c
#include <stdio.h>
#include <string.h>
#include <FreeRTOS.h>
#include <task.h>
#include <hosal_gpio.h>
#include <blog.h>

#define GPIO_BUTTON_PIN 8
#define GPIO_LED_PIN 14

static hosal_gpio_dev_t led;
static hosal_gpio_dev_t key;

/* 按键中断回调:翻转 LED 状态 */
static void key_irq(void *arg)
{
    static uint8_t value = 0;

    value = !value;
    hosal_gpio_output_set(&led, value);
    blog_info("[key irq]");
}

void main(void)
{
    /* LED:推挽输出 */
    led.port = GPIO_LED_PIN;
    led.config = OUTPUT_PUSH_PULL;
    hosal_gpio_init(&led);
    hosal_gpio_output_set(&led, 0);

    /* 按键:内部下拉输入,上升沿触发中断 */
    key.port = GPIO_BUTTON_PIN;
    key.config = INPUT_PULL_DOWN;
    hosal_gpio_init(&key);
    hosal_gpio_irq_set(&key, HOSAL_IRQ_TRIG_POS_PULSE, key_irq, NULL);

    for (;;) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

FAQ & Troubleshooting ​

⚠️ Pressing the button does nothing, the LED doesn't toggle
Cause: the button wiring doesn't match the interrupt trigger type (pull-down input must pair with rising edge HOSAL_IRQ_TRIG_POS_PULSE; pull-up input with falling edge HOSAL_IRQ_TRIG_NEG_PULSE)
Fix: confirm one end of the button connects to IO8 and the other to 3V3 (not GND); confirm INPUT_PULL_DOWN + HOSAL_IRQ_TRIG_POS_PULSE

⚠️ One press toggles multiple times
Cause: mechanical button contacts bounce on press/release, producing multiple edge interrupts
Fix: software debounce in the interrupt callback (record the press timestamp; ignore intervals under 20ms), or hardware debounce with a 100nF capacitor in parallel with the button

⚠️ Build error hosal_gpio.h: No such file or directory
Cause: the hosal component isn't enabled in the project's proj_config.mk
Fix: confirm the CONFIG_COMPONENT_* switches in proj_config.mk include HOSAL (enabled by default officially; check if you modified the project config)

⚠️ Calling printf in the interrupt callback freezes the system
Cause: printf can block in interrupt context
Fix: use blog_info in the callback (official example usage) or just set a flag and print in the main loop

⚠️ 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

⚠️ Running make reports no Makefile found
Cause: the build command ran in the wrong directory (it must run inside the example project)
Fix: first cd ~/Ai-Thinker-WB2/applications/get-started/blink into the project directory, then run make -j8

Self-Check

Each button press toggles the LED on/off once and the serial prints one [key irq] line per press — the external interrupt is verified.

Released under the MIT License. Build Time 2026-09-30 17:31:25