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 fromcomponents/platform/hosal/include/hosal_gpio.h; the official usage can be found inapplications/iot-solution/qcloud_demo(thekey_irqbutton interrupt callback).
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.
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:
cdis the “change directory” command; this borrows the official blink example skeleton as this tutorial’s project. All subsequentmakebuild andmake flashcommands must run inside this directory first.
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_irqusage 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’skey_irq).
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/blink.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; 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_tstruct pointer, required.portselects the GPIO pin (0~22);configis 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_tstruct pointer (portspecifies the pin)value: output level, values:0low (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_tstruct pointertrigger: 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, shapedvoid handler(void *arg), runs in interrupt context, must return quicklyarg: callback argument pointer; passNULLif 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 asprintf, required...: variadic args matchingfmtplaceholders, 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 viapdMS_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
#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.

