在驱动程序中处理中断

概览

本文档介绍了如何编写和测试能够以高效方式监听中断的 Fuchsia 驱动程序。中断是一种常用工具,用于让驱动程序了解何时发生了特定硬件(或虚拟)事件。在 C++ 中,中断由 zx::interrupt表示。您可能会看到“interrupt”和“irq”这两个字词交替使用。在此上下文中,两者都表示中断。

获取中断

驱动程序获取中断对象的方式取决于具体情况。一种常见的方法是从 FIDL 服务实例请求中断。例如,如果驱动程序需要一个表示与特定 GPIO 引脚相关的 GPIO 事件的中断对象,则该驱动程序可以通过向驱动程序传入命名空间中的 fuchsia.hardware.gpio.Service FIDL 服务实例发送 fuchsia.hardware.gpio.Gpio:GetInterrupt() FIDL 请求来请求一个。

聆听中断

监听中断是指在触发中断时执行代码。在驱动程序中,中断在其生命周期内被触发多次的情况很常见。驱动程序应能尽可能快速地处理中断,并且不会与其他驱动程序代码发生数据竞争。根据这些要求,建议使用 async::IrqMethod来监听中断。

IrqMethod 接受一个类实例方法(即回调),每当触发相应的中断时,该方法都会执行。它还接受用于执行回调的调度程序。建议使用驱动程序的调度器 DriverBase::dispatcher()。如果驱动程序的调度器是同步的(驱动程序调度器默认情况下是同步的),则回调的执行将等待调度器当前未执行其他代码。请注意,这意味着中断处理程序的回调执行会阻塞调度器,使其无法执行其他代码,直到回调执行完成。这与在发生中断触发时在单独的线程中执行代码相反。在这种情况下,驱动程序可能会因其他原因在第一个线程中执行其他代码,并且这两个线程之间可能会发生数据竞争。不建议采用这种方法,因为它需要使用会增加驱动程序复杂性、降低其可读性/可维护性并引入难以调试的同步 bug 的同步方法。

以下示例展示了驱动程序如何使用 async::IrqMethod 监听中断:

#include <lib/async/cpp/irq.h>

class MyDriver : public fdf::DriverBase {
 public:
  zx::result<> Start() override {
    // Get the interrupt for a GPIO FIDL service.
    zx::result<fidl::ClientEnd<fuchsia_hardware_gpio::Gpio>> gpio =
      incoming()->Connect<fuchsia_hardware_gpio::Service::Device>(kIrqGpioParentName);
    if (gpio.is_error()) {
      fdf::error("Failed to connect to irq gpio: {}", gpio);
      return gpio.take_error();
    }
    fidl::WireResult interrupt = fidl::WireCall(gpio.value())->GetInterrupt({});
    if (!interrupt.ok()) {
      fdf::error("Failed to send GetInterrupt request: {}", interrupt.status_string());
      return zx::error(interrupt.status());
    }
    if (interrupt->is_error()) {
      fdf::error("Failed to get interrupt: {}", zx_status_get_string(interrupt->error_value()));
      return interrupt->take_error();
    }
    interrupt_ = std::move(interrupt->value()->interrupt);

    // Start listening to `interrupt_`. `interrupt_handler_` will execute its
    // associated callback on dispatcher `dispatcher()` when `interrupt_` is
    // triggered.
    interrupt_handler_.set_object(interrupt_.get());
    zx_status_t status = interrupt_handler_.Begin(dispatcher());
    if (status != ZX_OK) {
      fdf::error("Failed to listen to interrupt: {}",
        zx_status_get_string(status));
      return zx::error(status);
    }

    return zx::ok();
  }

 private:
  // Called by `interrupt_handler_` when `interrupt_` is triggered.
  void HandleInterrupt(
    // Dispatcher that `HandleInterrupt()` was executed on.
    async_dispatcher_t* dispatcher,

    // Object that executed `HandleInterrupt()`.
    async::IrqBase* irq,

    // Status of handling the interrupt.
    zx_status_t status,

    // Information related to the interrupt.
    const zx_packet_interrupt_t* interrupt_packet) {

    if (status != ZX_OK) {
      if (status == ZX_ERR_CANCELED) {
        // Expected behavior as this occurs when `interrupt_handler_` is
        // destructed.
        fdf::debug("Interrupt handler cancelled");
      } else {
        fdf::error("Failed to handle interrupt: {}",
          zx_status_get_string(status));
      }

      // An error status means that the interrupt was not triggered so don't
      // handle it.
      return;
    }

    // Wrap the interrupt ack in a defer to ensure that the interrupt is
    // acknowledged even in the case that an error occurs while trying to
    // handle the interrupt.
    auto ack_interrupt = fit::defer([this] {
      // Acknowledge the interrupt. This "re-arms" the interrupt. If the
      // interrupt is not acknowledged then `interrupt_` cannot be triggered
      // again and `HandleInterrupt()` will not get called again.
      interrupt_.ack();
    });

    // Perform work in response to triggered interrupt.
  }

  // Interrupt to listen to.
  zx::interrupt interrupt_;

  // Calls `this->HandleInterrupt()` every time `interrupt_` is triggered.
  // Destructing `interrupt_handler_` means to no longer listen to `interrupt_`.
  async::IrqMethod<MyDriver, &MyDriver::HandleInterrupt> interrupt_handler_{this};
};

async::IrqMethod 属于 async-cpp 库,因此别忘了将其作为依赖项添加到驱动程序中:

GN

 source_set("my-driver") {
   deps = [
     "//sdk/lib/async:async-cpp",
   ]
 }

Bazel

 cc_library(
     name = "my-driver",
     deps = [
         "@fuchsia_sdk//pkg/async-cpp",
     ],
 )

测试中断

驱动程序的单元测试应测试驱动程序响应中断的能力。这要求测试能够触发中断,而无需等待真实的硬件事件,并且能够验证驱动程序是否确认中断。

提供中断

测试应创建要提供给驱动程序的虚拟中断。虚拟中断是可以“虚拟”触发的中断(即测试的代码可以显式触发中断,而无需等待实际的硬件事件)。可以按如下方式创建虚拟中断:

zx::interrupt interrupt;
ASSERT_EQ(
  zx::interrupt::create(zx::resource(), 0, ZX_INTERRUPT_VIRTUAL, &interrupt),
  ZX_OK);

测试应复制此中断,并将复制的中断发送到驱动程序。 测试如何将重复项发送到驱动程序取决于具体情境。建议模拟驱动程序实际获取中断的方式。例如,如果驱动程序从 fuchsia.hardware.gpio.Service FIDL 服务实例获取 GPIO 中断,则测试应伪造该 FIDL 服务实例。以下是复制中断的方法:

zx::interrupt duplicate;
ASSERT_EQ(interrupt.duplicate(ZX_RIGHT_SAME_RIGHTS, &duplicate), ZX_OK);

触发中断

测试可以触发虚拟中断,如下所示: ```cpp ASSERT_EQ( interrupt.trigger( // 选项。 0,

// Timestamp of when the interrupt was triggered.
zx::clock::get_boot()),

ZX_OK); ```

这会导致驱动程序的中断处理程序执行其针对中断触发的回调。

验证中断是否已确认

下一步是验证驱动程序是否确认了中断。当驱动程序确认中断触发时,中断会返回到“未触发”状态。中断还会发送有关此状态变化的信号。测试将监听此信号,以了解中断何时得到确认。首次创建虚拟中断时,也会发送此信号。

建议使用 async::WaitMethod来等待中断信号。与 async::IrqMethod 类似,当相应的中断发送特定信号时,它会调用其回调。一个重要的区别是,在调用 async::WaitMethod 的回调后,需要重新“启用”async::WaitMethod,否则,当它收到多个信号时,将不会调用其回调。

以下示例展示了如何监听中断确认:

#include <lib/async/cpp/wait.h>

class MyDriverEnvironment : public fdf_testing::Environment {
 public:
  zx::result<> Serve(fdf::OutgoingDirectory& to_driver_vfs) override {
    // Create a virtual interrupt to be listened to by the driver.
    EXPECT_EQ(
      zx::interrupt::create(zx::resource(), 0, ZX_INTERRUPT_VIRTUAL, &interrupt_),
      ZX_OK);


    zx::interrupt duplicate;
    EXPECT_EQ(interrupt_.duplicate(ZX_RIGHT_SAME_RIGHTS, &duplicate), ZX_OK);
    // Send duplicate interrupt to driver.

    // Dispatcher used to execute `HandleInterruptAck()`. In a driver unit test,
    // it is recommended to use the environment dispatcher so that
    // `HandleInterruptAck()` doesn't block the driver's code execution.
    async_dispatcher_t* dispatcher = fdf::Dispatcher::GetCurrent()->async_dispatcher();

    // Listen for when `interrupt_` is acknowledged.
    interrupt_ack_handler_.set_object(interrupt_.get());
    EXPECT_EQ(interrupt_ack_handler_.Begin(dispatcher), ZX_OK);

    return zx::ok();
  }

 private:
  // Called when `interrupt_` receives an acknowledgement.
  void HandleInterruptAck(
    // Dispatcher that `HandleInterruptAck()` was called on.
    async_dispatcher_t* dispatcher,

    // Object responsible for calling `HandleInterruptAck()`.
    async::WaitBase* wait,

    // Status of waiting for the acknowledgement.
    zx_status_t status,

    // Information related to the acknowledgement.
    const zx_packet_signal_t* signal) {

    if (status != ZX_OK) {
        FAIL() << "Failed to wait for interrupt ack" << zx_status_get_string(status);
    }

    // Do something in response to the acknowledgement.

    // "Re-arm" the listener. Wait for the next time `interrupt_` is
    // acknowledged.
    status = wait->Begin(dispatcher);
    if (status != ZX_OK) {
        fdf::error("Failed to re-arm interrupt ack handler: {}", zx_status_get_string(status));
    }
  }

  // Virtual interrupt that the driver is listening to.
  zx::interrupt interrupt_;

  // Calls `HandleInterruptAck()` whenever `interrupt_` receives an
  // acknowledgement.
  async::WaitMethod<InterruptController, &InterruptController::HandleInterruptAck>
    interrupt_ack_handler_{
      // Class instance to call `HandleInterruptAck()` on.
      this,

      // The object that the signals belong to. The test will provide the
      // interrupt object to `interrupt_ack_handler_` after the interrupt is
      // constructed.
      ZX_HANDLE_INVALID,

      // Call the callback when `interrupt_` is in the "untriggered" state.
      ZX_VIRTUAL_INTERRUPT_UNTRIGGERED,

      // Only call `HandleInterruptAck()` if the
      // ZX_VIRTUAL_INTERRUPT_UNTRIGGERED signal was received after
      // `interrupt_ack_handler_.Begin()` was called. If `interrupt_` is already
      // in the "untriggered" state before `interrupt_ack_handler_.Begin()` is
      // called then don't call `HandleInterruptAck()`.
      ZX_WAIT_ASYNC_EDGE
  };
};

class MyDriverTestConfiguration final {
 public:
  using DriverType = MyDriver;
  using EnvironmentType = MyDriverEnvironment;
};

class MyDriverTest : public testing::Test {
 public:
  void SetUp() override {
    ASSERT_EQ(driver_test_.StartDriver().status_value(), ZX_OK);
  }

 private:
  fdf_testing::BackgroundDriverTest<MyDriverTestConfiguration> driver_test_;
};

async::WaitMethod 属于 async-cpp 库,因此请务必将其添加为驱动程序测试的依赖项:

GN

 test("my-driver-test-bin") {
   deps = [
     "//sdk/lib/async:async-cpp",
   ]
 }

Bazel

 fuchsia_cc_test(
     name = "my-driver-test",
     deps = [
         "@fuchsia_sdk//pkg/async-cpp",
     ],
 )