实战二:外设中断与下半部处理——GPIO 仿真、按键中断与工作队列

在真实嵌入式系统中,外部按键或传感器 DRDY 引脚会触发 GPIO 硬件中断(ISR)。由于 ISR 上下文中严禁休眠或执行耗时操作,工业级固件通常在 ISR(上半部)中仅调用 k_work_reschedule(),将消抖和业务处理延后到系统工作队列线程(下半部)中执行。

本节我们在纯软件模拟的 qemu_cortex_a53 上,通过 app.overlay 挂载 zephyr,gpio-emul 仿真控制器,用标准 GPIO 驱动模型与中断接口 与 工作队列 接口完整实现这一链路。

Step 1:编写 app.overlay 挂载虚拟 LED 与按键

创建 smart_node/app.overlay,定义 报警灯(led0)与 用户按键(sw0):

#include <zephyr/dt-bindings/gpio/gpio.h>

/ {
    aliases {
        led0 = &alarm_led;
        sw0 = &user_button;
    };

    gpio_emul0: gpio_emul {
        compatible = "zephyr,gpio-emul";
        gpio-controller;
        #gpio-cells = <2>;
        ngpios = <8>;
        rising-edge;
        falling-edge;
        status = "okay";
    };

    leds {
        compatible = "gpio-leds";
        alarm_led: led_0 {
            gpios = <&gpio_emul0 0 GPIO_ACTIVE_HIGH>;
            label = "Alarm LED 0";
        };
    };

    buttons {
        compatible = "gpio-keys";
        user_button: button_0 {
            gpios = <&gpio_emul0 1 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
            label = "User Button 0";
        };
    };
};

备注

根据 zephyr,gpio-emul.yaml 绑定规则,必须在 gpio_emul0 节点中显式声明 rising-edge; 和 falling-edge; 布尔属性,仿真驱动才允许配置边沿触发中断。

在 smart_node/prj.conf 中开启 GPIO 驱动与仿真器:

CONFIG_LOG=y
CONFIG_SHELL=y
CONFIG_GPIO=y
CONFIG_GPIO_EMUL=y

Step 2:实现 GPIO ISR 上半部与延迟工作队列下半部

编写 smart_node/src/main.c:

  • 上半部(ISR):button_pressed_isr 被 GPIO 边沿中断触发时,翻转报警灯并通过 k_work_reschedule(&debounce_work, K_MSEC(50)) 安排 50ms 后的下半部工作。

  • 下半部(Workqueue 线程):debounce_work_handler 在系统工作队列线程中运行(k_is_in_isr() == 0),安全执行消抖后的状态上报。

  • 按键仿真命令:注册 node press 命令,调用 gpio_emul_input_set() 模拟物理按键电平翻转。

#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/gpio/gpio_emul.h>
#include <zephyr/logging/log.h>
#include <zephyr/shell/shell.h>

LOG_MODULE_REGISTER(smart_node_gpio, LOG_LEVEL_INF);

static const struct gpio_dt_spec alarm_led = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);
static const struct gpio_dt_spec user_btn  = GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios);
static struct gpio_callback btn_cb_data;
static atomic_t press_count = ATOMIC_INIT(0);

/* 下半部:运行在系统工作队列线程上下文 (in_isr=0) */
static void debounce_work_handler(struct k_work *work)
{
    ARG_UNUSED(work);
    int led_val = gpio_emul_output_get(alarm_led.port, alarm_led.pin);

    LOG_INF("[Workqueue] Debounced button event processed (in_isr=%d, count=%ld, led=%d)",
            k_is_in_isr(), atomic_get(&press_count), led_val);
}

K_WORK_DELAYABLE_DEFINE(debounce_work, debounce_work_handler);

/* 上半部:GPIO 硬件中断服务例程 (in_isr=1),严禁阻塞 */
static void button_pressed_isr(const struct device *dev, struct gpio_callback *cb,
                               uint32_t pins)
{
    ARG_UNUSED(dev);
    ARG_UNUSED(cb);
    ARG_UNUSED(pins);

    long cnt = atomic_inc(&press_count) + 1;
    gpio_pin_toggle_dt(&alarm_led);
    int led_val = gpio_emul_output_get(alarm_led.port, alarm_led.pin);

    LOG_INF("[ISR] Button interrupt! count=%ld, alarm_led=%d", cnt, led_val);
    /* 投递/重置 50ms 延迟工作项 */
    k_work_reschedule(&debounce_work, K_MSEC(50));
}

static int cmd_node_press(const struct shell *sh, size_t argc, char **argv)
{
    ARG_UNUSED(argc);
    ARG_UNUSED(argv);

    shell_print(sh, "Simulating physical button press on %s pin %d...",
                user_btn.port->name, user_btn.pin);
    /* GPIO_ACTIVE_LOW:物理低电平 (0) 为按下,高电平 (1) 为松开 */
    gpio_emul_input_set(user_btn.port, user_btn.pin, 0);
    k_msleep(20);
    gpio_emul_input_set(user_btn.port, user_btn.pin, 1);
    return 0;
}

SHELL_STATIC_SUBCMD_SET_CREATE(sub_node,
    SHELL_CMD_ARG(press, NULL, "Simulate button press interrupt", cmd_node_press, 1, 0),
    SHELL_SUBCMD_SET_END
);
SHELL_CMD_REGISTER(node, &sub_node, "Smart Node GPIO commands", NULL);

int main(void)
{
    if (!gpio_is_ready_dt(&alarm_led) || !gpio_is_ready_dt(&user_btn)) {
        LOG_ERR("GPIO devices not ready");
        return -ENODEV;
    }

    gpio_pin_configure_dt(&alarm_led, GPIO_OUTPUT_INACTIVE);
    gpio_pin_configure_dt(&user_btn, GPIO_INPUT);
    gpio_emul_input_set(user_btn.port, user_btn.pin, 1);

    gpio_pin_interrupt_configure_dt(&user_btn, GPIO_INT_EDGE_TO_ACTIVE);
    gpio_init_callback(&btn_cb_data, button_pressed_isr, BIT(user_btn.pin));
    gpio_add_callback_dt(&user_btn, &btn_cb_data);

    LOG_INF("GPIO initialized: LED on pin %d, Button on pin %d",
            alarm_led.pin, user_btn.pin);

    /* 启动后自动演示一次按键触发 */
    k_msleep(200);
    gpio_emul_input_set(user_btn.port, user_btn.pin, 0);
    k_msleep(20);
    gpio_emul_input_set(user_btn.port, user_btn.pin, 1);
    return 0;
}

Step 3:在 qemu_cortex_a53 上验证中断与延迟工作时序

编译并运行后,可以在终端中输入 node press 触发中断,观察 [ISR] 与 50ms 后的 [Workqueue] 协同执行:

*** Booting Zephyr OS build 321e39a45766 ***
[00:00:00.010,000] <inf> smart_node_gpio: GPIO initialized: LED on pin 0, Button on pin 1
[00:00:00.220,000] <inf> smart_node_gpio: [ISR] Button interrupt! count=1, alarm_led=1
[00:00:00.270,000] <inf> smart_node_gpio: [Workqueue] Debounced button event processed (in_isr=0, count=1, led=1)
uart:~$ node press
Simulating physical button press on gpio_emul pin 1...
[00:00:00.980,000] <inf> smart_node_gpio: [ISR] Button interrupt! count=2, alarm_led=0
[00:00:01.030,000] <inf> smart_node_gpio: [Workqueue] Debounced button event processed (in_isr=0, count=2, led=0)

相关参考