在驱动程序中处理中断

概览

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

获取中断

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

监听中断

监听中断是指在触发中断时执行代码。 在驱动程序中,中断在其生命周期内被触发多次的情况很常见。驱动程序应能够尽可能快速地处理中断,并且不会与其他驱动程序代码发生数据争用。

C++

根据这些要求,建议使用 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",
     ],
 )

Rust

在 Rust 中,中断通常通过以下方式处理:从中断句柄创建 fuchsia_async::OnInterrupt 结构体 ,并在生成的任务中对其进行处理。OnInterrupt 实现 futures::Stream ,可以异步监听。

以下示例展示了 Rust 驱动程序如何监听中断:

use fdf_component::{Driver, DriverContext};
use fuchsia_async::{OnInterrupt, Task};
use futures::StreamExt;
use zx::{Interrupt, RealInterruptKind};

pub struct MyDriver {
  /// Task that handles interrupts. Hold onto it so that when the driver is
  /// dropped the task is cancelled.
  interrupt_handler: Task<()>,
}

impl Driver for MyDriver {
    async fn start(context: DriverContext) -> Result<Self, Status> {
        let interrupt: Interrupt<RealInterruptKind> = todo!();
        let interrupt_stream = OnInterrupt::new(interrupt);
        let interrupt_handler = Task::spawn(async move {
          while let Some(Ok(_time)) = interrupt_stream.next().await {
            // Perform work in response to triggered interrupt.
            if let Err(e) = Self::handle_interrupt() {
                log::error!("Failed to handle interrupt: {e:?}");
                // Don't return early as we still want to ack the interrupt.
            }

            // Acknowledge the interrupt. This "re-arms" the interrupt. If the
            // interrupt is not acknowledged then it cannot be triggered again.
            if let Err(e) = interrupt_stream.ack() {
                log::error!("Failed to ack interrupt: {e:?}");
            }
          }
        });

        Ok(Self { interrupt_handler })
    }

    async fn handle_interrupt() -> Result<(), Status> {
        todo!();
    }
}

OnInterrupt 属于 fuchsia_async 库,因此请务必将其作为依赖项添加到驱动程序:

GN

 fuchsia_component("my-driver") {
   deps = [
     "//src/lib/fuchsia-async",
     "//third_party/rust_crates:futures",
   ]
 }

测试中断

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

提供中断

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

C++

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

Rust

let interrupt = zx::VirtualInterrupt::create_virtual()?;

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

C++

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

Rust

// Duplicate the virtual interrupt.
let interrupt: zx::VirtualInterrupt =
  self.interrupt.duplicate_handle(Rights::SAME_RIGHTS)?;

// Convert the duplicated virtual interrupt into a real interrupt as the
// driver expects a real interrupt.
let interrupt = Interrupt::from(interrupt.into_handle());

触发中断

测试可以按如下方式触发虚拟中断:

C++

ASSERT_EQ(
  interrupt.trigger(
    // Options.
    0,

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

Rust

interrupt.trigger(
  // Timestamp of when the interrupt was triggered.
  zx::Instant::from_nanos(0)
)?;

这将向驱动程序发出中断已触发的信号。

验证中断是否已确认

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

C++

建议使用 async::WaitMethod 来 等待中断的信号。与 async::IrqMethod 类似,当相应中断发送特定信号时,它会调用其回调。一个重要的区别是,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",
     ],
 )

Rust

在 Rust 中,您可以使用 fuchsia_async::OnSignals 异步等待 VIRTUAL_INTERRUPT_UNTRIGGERED 信号 :OnSignals 实现 Future ,因此可以等待。

use fuchsia_async::OnSignals;
use zx::Signals;

// Wait for the interrupt to be acknowledged.
OnSignals::new(&interrupt, Signals::VIRTUAL_INTERRUPT_UNTRIGGERED).await?;

如需在 Rust 驱动程序测试中使用此功能,您需要将 fuchsia-async 依赖项添加到 BUILD.gnBUILD.bazel 文件中:

GN

 fuchsia_unittest("my-driver-test") {
   deps = [
     "//src/lib/fuchsia-async",
   ]
 }

Bazel

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