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-11 14:52:23